pax_global_header00006660000000000000000000000064144603367760014532gustar00rootroot0000000000000052 comment=28345a55f9b21dae89472111635fd6e41809d958 WALinuxAgent-2.9.1.1/000077500000000000000000000000001446033677600142105ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/.gitattributes000066400000000000000000000047261446033677600171140ustar00rootroot00000000000000############################################################################### # Set default behavior to automatically normalize line endings. ############################################################################### * text=auto ############################################################################### # Set default behavior for command prompt diff. # # This is need for earlier builds of msysgit that does not have it on by # default for csharp files. # Note: This is only used by command line ############################################################################### #*.cs diff=csharp ############################################################################### # Set the merge driver for project and solution files # # Merging from the command prompt will add diff markers to the files if there # are conflicts (Merging from VS is not affected by the settings below, in VS # the diff markers are never inserted). Diff markers may cause the following # file extensions to fail to load in VS. An alternative would be to treat # these files as binary and thus will always conflict and require user # intervention with every merge. To do so, just uncomment the entries below ############################################################################### #*.sln merge=binary #*.csproj merge=binary #*.vbproj merge=binary #*.vcxproj merge=binary #*.vcproj merge=binary #*.dbproj merge=binary #*.fsproj merge=binary #*.lsproj merge=binary #*.wixproj merge=binary #*.modelproj merge=binary #*.sqlproj merge=binary #*.wwaproj merge=binary ############################################################################### # behavior for image files # # image files are treated as binary by default. ############################################################################### #*.jpg binary #*.png binary #*.gif binary ############################################################################### # diff behavior for common document formats # # Convert binary document formats to text before diffing them. This feature # is only available from the command line. Turn it on by uncommenting the # entries below. ############################################################################### #*.doc diff=astextplain #*.DOC diff=astextplain #*.docx diff=astextplain #*.DOCX diff=astextplain #*.dot diff=astextplain #*.DOT diff=astextplain #*.pdf diff=astextplain #*.PDF diff=astextplain #*.rtf diff=astextplain #*.RTF diff=astextplain WALinuxAgent-2.9.1.1/.github/000077500000000000000000000000001446033677600155505ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/.github/CONTRIBUTING.md000066400000000000000000000105271446033677600200060ustar00rootroot00000000000000# Contributing to Linux Guest Agent First, thank you for contributing to WALinuxAgent repository! ## Basics If you would like to become an active contributor to this project, please follow the instructions provided in [Microsoft Azure Projects Contribution Guidelines](http://azure.github.io/guidelines/). ## Table of Contents [Before starting](#before-starting) - [Github basics](#github-basics) - [Code of Conduct](#code-of-conduct) [Making Changes](#making-changes) - [Pull Requests](#pull-requests) - [Pull Request Guidelines](#pull-request-guidelines) - [Cleaning up commits](#cleaning-up-commits) - [General guidelines](#general-guidelines) - [Testing guidelines](#testing-guidelines) ## Before starting ### Github basics #### GitHub workflow If you don't have experience with Git and Github, some of the terminology and process can be confusing. [Here's a guide to understanding Github](https://guides.github.com/introduction/flow/). #### Forking the Azure/Guest-Configuration-Extension repository Unless you are working with multiple contributors on the same file, we ask that you fork the repository and submit your Pull Request from there. [Here's a guide to forks in Github](https://guides.github.com/activities/forking/). ### Code of Conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. ## Making Changes ### Pull Requests You can find all of the pull requests that have been opened in the [Pull Request](https://github.com/Azure/Guest-Configuration-Extension/pulls) section of the repository. To open your own pull request, click [here](https://github.com/Azure/WALinuxAgent/compare). When creating a pull request, keep the following in mind: - Make sure you are pointing to the fork and branch that your changes were made in - Choose the correct branch you want your pull request to be merged into - The pull request template that is provided **should be filled out**; this is not something that should just be deleted or ignored when the pull request is created - Deleting or ignoring this template will elongate the time it takes for your pull request to be reviewed ### Pull Request Guidelines A pull request template will automatically be included as a part of your PR. Please fill out the checklist as specified. Pull requests **will not be reviewed** unless they include a properly completed checklist. #### Cleaning up Commits If you are thinking about making a large change, **break up the change into small, logical, testable chunks, and organize your pull requests accordingly**. Often when a pull request is created with a large number of files changed and/or a large number of lines of code added and/or removed, GitHub will have a difficult time opening up the changes on their site. This forces the Azure Guest-Configuration-Extension team to use separate software to do a code review on the pull request. If you find yourself creating a pull request and are unable to see all the changes on GitHub, we recommend **splitting the pull request into multiple pull requests that are able to be reviewed on GitHub**. If splitting up the pull request is not an option, we recommend **creating individual commits for different parts of the pull request, which can be reviewed individually on GitHub**. For more information on cleaning up the commits in a pull request, such as how to rebase, squash, and cherry-pick, click [here](https://github.com/Azure/azure-powershell/blob/dev/documentation/cleaning-up-commits.md). #### General guidelines The following guidelines must be followed in **EVERY** pull request that is opened. - Title of the pull request is clear and informative - There are a small number of commits that each have an informative message - A description of the changes the pull request makes is included, and a reference to the issue being resolved, if the change address any - All files have the Microsoft copyright header #### Testing Guidelines The following guidelines must be followed in **EVERY** pull request that is opened. - Pull request includes test coverage for the included changesWALinuxAgent-2.9.1.1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001446033677600177335ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000016351446033677600224320ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: "[BUG] Bug Title" --- **Describe the bug: A clear and concise description of what the bug is.** Note: Please add some context which would help us understand the problem better 1. Section of the log where the error occurs. 2. Serial console output 3. Steps to reproduce the behavior. **Distro and WALinuxAgent details (please complete the following information):** - Distro and Version: [e.g. Ubuntu 16.04] - WALinuxAgent version [e.g. 2.2.40, you can copy the output of `waagent --version`, more info [here](https://github.com/Azure/WALinuxAgent/wiki/FAQ#what-does-goal-state-agent-mean-in-waagent---version-output) ] **Additional context** Add any other context about the problem here. **Log file attached** If possible, please provide the full /var/log/waagent.log file to help us understand the problem better and get the context of the issue. WALinuxAgent-2.9.1.1/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000022151446033677600213510ustar00rootroot00000000000000 ## Description Issue # --- ### PR information - [ ] The title of the PR is clear and informative. - [ ] There are a small number of commits, each of which has an informative message. This means that previously merged commits do not appear in the history of the PR. For information on cleaning up the commits in your pull request, [see this page](https://github.com/Azure/azure-powershell/blob/master/documentation/development-docs/cleaning-up-commits.md). - [ ] If applicable, the PR references the bug/issue that it fixes in the description. - [ ] New Unit tests were added for the changes made ### Quality of Code and Contribution Guidelines - [ ] I have read the [contribution guidelines](https://github.com/Azure/WALinuxAgent/blob/master/.github/CONTRIBUTING.md).WALinuxAgent-2.9.1.1/.github/codecov.yml000066400000000000000000000000461446033677600177150ustar00rootroot00000000000000github_checks: annotations: false WALinuxAgent-2.9.1.1/.github/workflows/000077500000000000000000000000001446033677600176055ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/.github/workflows/ci_pr.yml000066400000000000000000000071601446033677600214300ustar00rootroot00000000000000name: CI Unit tests on: push: branches: [ "*" ] pull_request: branches: [ "*" ] workflow_dispatch: jobs: test-legacy-python-versions: strategy: fail-fast: false matrix: include: - python-version: 2.6 - python-version: 3.4 name: "Python ${{ matrix.python-version }} Unit Tests" runs-on: ubuntu-20.04 container: image: ubuntu:16.04 volumes: - /home/waagent:/home/waagent defaults: run: shell: bash -l {0} env: NOSEOPTS: "--verbose" steps: - uses: actions/checkout@v3 - name: Install Python ${{ matrix.python-version }} run: | apt-get update apt-get install -y curl bzip2 sudo python3 curl https://dcrdata.blob.core.windows.net/python/python-${{ matrix.python-version }}.tar.bz2 -o python-${{ matrix.python-version }}.tar.bz2 sudo tar xjvf python-${{ matrix.python-version }}.tar.bz2 --directory / - name: Test with nosetests run: | if [[ ${{ matrix.python-version }} == 2.6 ]]; then source /home/waagent/virtualenv/python2.6.9/bin/activate else source /home/waagent/virtualenv/python3.4.8/bin/activate fi ./ci/nosetests.sh exit $? test-current-python-versions: strategy: fail-fast: false matrix: include: - python-version: 2.7 PYLINTOPTS: "--rcfile=ci/2.7.pylintrc --ignore=tests_e2e,makepkg.py" - python-version: 3.5 PYLINTOPTS: "--rcfile=ci/3.6.pylintrc --ignore=tests_e2e,makepkg.py" - python-version: 3.6 PYLINTOPTS: "--rcfile=ci/3.6.pylintrc --ignore=tests_e2e" - python-version: 3.7 PYLINTOPTS: "--rcfile=ci/3.6.pylintrc --ignore=tests_e2e" - python-version: 3.8 PYLINTOPTS: "--rcfile=ci/3.6.pylintrc --ignore=tests_e2e" - python-version: 3.9 PYLINTOPTS: "--rcfile=ci/3.6.pylintrc" additional-nose-opts: "--with-coverage --cover-erase --cover-inclusive --cover-branches --cover-package=azurelinuxagent" name: "Python ${{ matrix.python-version }} Unit Tests" runs-on: ubuntu-20.04 env: PYLINTOPTS: ${{ matrix.PYLINTOPTS }} PYLINTFILES: "azurelinuxagent setup.py makepkg.py tests tests_e2e" NOSEOPTS: "--with-timer ${{ matrix.additional-nose-opts }}" PYTHON_VERSION: ${{ matrix.python-version }} steps: - name: Checkout WALinuxAgent repo uses: actions/checkout@v3 - name: Setup Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies id: install-dependencies run: | sudo env "PATH=$PATH" python -m pip install --upgrade pip sudo env "PATH=$PATH" pip install -r requirements.txt sudo env "PATH=$PATH" pip install -r test-requirements.txt - name: Run pylint run: | pylint $PYLINTOPTS --jobs=0 $PYLINTFILES - name: Test with nosetests if: success() || (failure() && steps.install-dependencies.outcome == 'success') run: | ./ci/nosetests.sh exit $? - name: Compile Coverage if: matrix.python-version == 3.9 run: | echo looking for coverage files : ls -alh | grep -i coverage sudo env "PATH=$PATH" coverage combine coverage.*.data sudo env "PATH=$PATH" coverage xml sudo env "PATH=$PATH" coverage report - name: Upload Coverage if: matrix.python-version == 3.9 uses: codecov/codecov-action@v2 with: file: ./coverage.xmlWALinuxAgent-2.9.1.1/.gitignore000066400000000000000000000020471446033677600162030ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # Virtualenv py3env/ # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyCharm .idea/ .idea_modules/ # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .cache nosetests.xml coverage.xml # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ waagentc *.pyproj *.sln *.suo waagentc bin/waagent2.0c # rope project .ropeproject/ # mac osx specific files .DS_Store ### VirtualEnv template # Virtualenv # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ .Python pyvenv.cfg .venv pip-selfcheck.json # virtualenv venv/ ENV/ # dotenv .env # pyenv .python-version .vscode/ WALinuxAgent-2.9.1.1/CODEOWNERS000066400000000000000000000012521446033677600156030ustar00rootroot000000000000001 # See https://help.github.com/articles/about-codeowners/ # for more info about CODEOWNERS file # It uses the same pattern rule for gitignore file # https://git-scm.com/docs/gitignore#_pattern_format # Provisioning Agent # The Azure Linux Provisioning team is interested in getting notifications # when there are requests for changes in the provisioning agent. For any # questions, please feel free to reach out to thstring@microsoft.com. /azurelinuxagent/pa/ @trstringer @anhvoms /tests/pa/ @trstringer @anhvoms # # RDMA # /azurelinuxagent/common/rdma.py @longlimsft /azurelinuxagent/pa/rdma/ @longlimsft # # Linux Agent team # * @narrieta @ZhidongPeng @nagworld9 @maddieford WALinuxAgent-2.9.1.1/LICENSE.txt000066400000000000000000000261301446033677600160350ustar00rootroot00000000000000 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 2016 Microsoft Corporation 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. WALinuxAgent-2.9.1.1/MAINTENANCE.md000066400000000000000000000013131446033677600162520ustar00rootroot00000000000000## Microsoft Azure Linux Agent Maintenance Guide ### Version rules * Production releases are public * Test releases are for internal use * Production versions use only [major].[minor].[revision] * Test versions use [major].[minor].[revision].[build] * Test a.b.c.0 is equivalent to Prod a.b.c * Publishing to Production requires incrementing the revision and dropping the build number * We do not use pre-release labels on any builds ### Version updates * The version of the agent can be found at https://github.com/Azure/WALinuxAgent/blob/master/azurelinuxagent/common/version.py#L53 assigned to AGENT_VERSION * Update the version here and send for PR before declaring a release via GitHub WALinuxAgent-2.9.1.1/MANIFEST000066400000000000000000000005701446033677600153430ustar00rootroot00000000000000# file GENERATED by distutils, do NOT edit README setup.py bin/waagent config/waagent.conf config/waagent.logrotate test/test_logger.py walinuxagent/__init__.py walinuxagent/agent.py walinuxagent/conf.py walinuxagent/envmonitor.py walinuxagent/extension.py walinuxagent/install.py walinuxagent/logger.py walinuxagent/protocol.py walinuxagent/provision.py walinuxagent/util.py WALinuxAgent-2.9.1.1/MANIFEST.in000066400000000000000000000001141446033677600157420ustar00rootroot00000000000000recursive-include bin * recursive-include init * recursive-include config * WALinuxAgent-2.9.1.1/NOTICE000066400000000000000000000002411446033677600151110ustar00rootroot00000000000000Microsoft Azure Linux Agent Copyright 2012 Microsoft Corporation This product includes software developed at Microsoft Corporation (http://www.microsoft.com/). WALinuxAgent-2.9.1.1/README.md000066400000000000000000000504111446033677600154700ustar00rootroot00000000000000 # Microsoft Azure Linux Agent ## Linux distributions support Our daily automation tests most of the [Linux distributions supported by Azure](https://docs.microsoft.com/en-us/azure/virtual-machines/linux/endorsed-distros); the Agent can be used on other distributions as well, but development, testing and support for those are done by the open source community. Testing is done using the develop branch, which can be unstable. For a stable build please use the master branch instead. [![CodeCov](https://codecov.io/gh/Azure/WALinuxAgent/branch/develop/graph/badge.svg)](https://codecov.io/gh/Azure/WALinuxAgent/branch/develop) ## Introduction The Microsoft Azure Linux Agent (waagent) manages Linux provisioning and VM interaction with the Azure Fabric Controller. It provides the following functionality for Linux IaaS deployments: * Image Provisioning * Creation of a user account * Configuring SSH authentication types * Deployment of SSH public keys and key pairs * Setting the host name * Publishing the host name to the platform DNS * Reporting SSH host key fingerprint to the platform * Resource Disk Management * Formatting and mounting the resource disk * Configuring swap space * Networking * Manages routes to improve compatibility with platform DHCP servers * Ensures the stability of the network interface name * Kernel * Configure virtual NUMA (disable for kernel <2.6.37) * Configure SCSI timeouts for the root device (which could be remote) * Diagnostics * Console redirection to the serial port * SCVMM Deployments * Detect and bootstrap the VMM agent for Linux when running in a System Center Virtual Machine Manager 2012R2 environment * VM Extension * Inject component authored by Microsoft and Partners into Linux VM (IaaS) to enable software and configuration automation * VM Extension reference implementation on [GitHub](https://github.com/Azure/azure-linux-extensions) ## Communication The information flow from the platform to the agent occurs via two channels: * A boot-time attached DVD for IaaS deployments. This DVD includes an OVF-compliant configuration file that includes all provisioning information other than the actual SSH keypairs. * A TCP endpoint exposing a REST API used to obtain deployment and topology configuration. The agent will use an HTTP proxy if provided via the `http_proxy` (for `http` requests) or `https_proxy` (for `https` requests) environment variables. The `HttpProxy.Host` and `HttpProxy.Port` configuration variables (see below), if used, will override the environment settings. Due to limitations of Python, the agent *does not* support HTTP proxies requiring authentication. Note that when the agent service is managed by systemd, environment variables such as `http_proxy` and `https_proxy` should be defined using one the mechanisms provided by systemd (e.g. by using Environment or EnvironmentFile in the service file). ## Requirements The following systems have been tested and are known to work with the Azure Linux Agent. Please note that this list may differ from the official list of supported systems on the Microsoft Azure Platform as described [here](https://docs.microsoft.com/en-us/azure/virtual-machines/linux/endorsed-distros). Waagent depends on some system packages in order to function properly: * Python 2.6+ * OpenSSL 1.0+ * OpenSSH 5.3+ * Filesystem utilities: sfdisk, fdisk, mkfs, parted * Password tools: chpasswd, sudo * Text processing tools: sed, grep * Network tools: ip-route ## Installation Installation via your distribution's package repository is preferred. You can also customize your own RPM or DEB packages using the configuration samples provided (see deb and rpm sections below). For more advanced installation options, such as installing to custom locations or prefixes, you can use **setuptools** to install from source by running: ```bash sudo python setup.py install --register-service ``` For Python 3, use: ```bash sudo python3 setup.py install --register-service ``` You can view more installation options by running: ```bash sudo python setup.py install --help ``` The agent's log file is kept at `/var/log/waagent.log`. ## Upgrade Upgrading via your distribution's package repository is strongly preferred. If upgrading manually, same with installation above by running: ```bash sudo python setup.py install --force ``` Restart waagent service,for most of linux distributions: ```bash sudo service waagent restart ``` For Ubuntu, use: ```bash sudo service walinuxagent restart ``` For CoreOS, use: ```bash sudo systemctl restart waagent ``` ## Command line options ### Flags `-verbose`: Increase verbosity of specified command `-force`: Skip interactive confirmation for some commands ### Commands `-help`: Lists the supported commands and flags. `-deprovision`: Attempt to clean the system and make it suitable for re-provisioning, by deleting the following: * All SSH host keys (if Provisioning.RegenerateSshHostKeyPair is 'y' in the configuration file) * Nameserver configuration in /etc/resolv.conf * Root password from /etc/shadow (if Provisioning.DeleteRootPassword is 'y' in the configuration file) * Cached DHCP client leases * Resets host name to localhost.localdomain **WARNING!** Deprovision does not guarantee that the image is cleared of all sensitive information and suitable for redistribution. `-deprovision+user`: Performs everything under deprovision (above) and also deletes the last provisioned user account and associated data. `-version`: Displays the version of waagent `-serialconsole`: Configures GRUB to mark ttyS0 (the first serial port) as the boot console. This ensures that kernel bootup logs are sent to the serial port and made available for debugging. `-daemon`: Run waagent as a daemon to manage interaction with the platform. This argument is specified to waagent in the waagent init script. `-start`: Run waagent as a background process `-collect-logs [-full]`: Runs the log collector utility that collects relevant agent logs for debugging and stores them in the agent folder on disk. Exact location will be shown when run. Use flag `-full` for more exhaustive log collection. ## Configuration A configuration file (/etc/waagent.conf) controls the actions of waagent. Blank lines and lines whose first character is a `#` are ignored (end-of-line comments are *not* supported). A sample configuration file is shown below: ```yml Extensions.Enabled=y Extensions.GoalStatePeriod=6 Provisioning.Agent=auto Provisioning.DeleteRootPassword=n Provisioning.RegenerateSshHostKeyPair=y Provisioning.SshHostKeyPairType=rsa Provisioning.MonitorHostName=y Provisioning.DecodeCustomData=n Provisioning.ExecuteCustomData=n Provisioning.PasswordCryptId=6 Provisioning.PasswordCryptSaltLength=10 ResourceDisk.Format=y ResourceDisk.Filesystem=ext4 ResourceDisk.MountPoint=/mnt/resource ResourceDisk.MountOptions=None ResourceDisk.EnableSwap=n ResourceDisk.EnableSwapEncryption=n ResourceDisk.SwapSizeMB=0 Logs.Verbose=n Logs.Collect=y Logs.CollectPeriod=3600 OS.AllowHTTP=n OS.RootDeviceScsiTimeout=300 OS.EnableFIPS=n OS.OpensslPath=None OS.SshClientAliveInterval=180 OS.SshDir=/etc/ssh HttpProxy.Host=None HttpProxy.Port=None ``` The various configuration options are described in detail below. Configuration options are of three types : Boolean, String or Integer. The Boolean configuration options can be specified as "y" or "n". The special keyword "None" may be used for some string type configuration entries as detailed below. ### Configuration File Options #### __Extensions.Enabled__ _Type: Boolean_ _Default: y_ This allows the user to enable or disable the extension handling functionality in the agent. Valid values are "y" or "n". If extension handling is disabled, the goal state will still be processed and VM status is still reported, but only every 5 minutes. Extension config within the goal state will be ignored. Note that functionality such as password reset, ssh key updates and backups depend on extensions. Only disable this if you do not need extensions at all. _Note_: disabling extensions in this manner is not the same as running completely without the agent. In order to do that, the `provisionVMAgent` flag must be set at provisioning time, via whichever API is being used. We will provide more details on this on our wiki when it is generally available. #### __Extensions.GoalStatePeriod__ _Type: Integer_ _Default: 6_ How often to poll for new goal states (in seconds) and report the status of the VM and extensions. Goal states describe the desired state of the extensions on the VM. _Note_: setting up this parameter to more than a few minutes can make the state of the VM be reported as unresponsive/unavailable on the Azure portal. Also, this setting affects how fast the agent starts executing extensions. #### __AutoUpdate.Enabled__ _Type: Boolean_ _Default: y_ Enables auto-update of the Extension Handler. The Extension Handler is responsible for managing extensions and reporting VM status. The core functionality of the agent is contained in the Extension Handler, and we encourage users to enable this option in order to maintain an up to date version. On most distros the default value is 'y'. For more information on the agent version, see our [FAQ](https://github.com/Azure/WALinuxAgent/wiki/FAQ#what-does-goal-state-agent-mean-in-waagent---version-output). #### __Provisioning.Agent__ _Type: String_ _Default: auto_ Choose which provisioning agent to use (or allow waagent to figure it out by specifying "auto"). Possible options are "auto" (default), "waagent", "cloud-init", or "disabled". #### __Provisioning.Enabled__ (*removed in 2.2.45*) _Type: Boolean_ _Default: y_ This allows the user to enable or disable the provisioning functionality in the agent. Valid values are "y" or "n". If provisioning is disabled, SSH host and user keys in the image are preserved and any configuration specified in the Azure provisioning API is ignored. _Note_: This configuration option has been removed and has no effect. waagent now auto-detects cloud-init as a provisioning agent (with an option to override with `Provisioning.Agent`). #### __Provisioning.MonitorHostName__ _Type: Boolean_ _Default: n_ Monitor host name changes and publish changes via DHCP requests. #### __Provisioning.MonitorHostNamePeriod__ _Type: Integer_ _Default: 30_ How often to monitor host name changes (in seconds). This setting is ignored if MonitorHostName is not set. #### __Provisioning.UseCloudInit__ _Type: Boolean_ _Default: n_ This options enables / disables support for provisioning by means of cloud-init. When true ("y"), the agent will wait for cloud-init to complete before installing extensions and processing the latest goal state. _Provisioning.Enabled_ must be disabled ("n") for this option to have an effect. Setting _Provisioning.Enabled_ to true ("y") overrides this option and runs the built-in agent provisioning code. _Note_: This configuration option has been removed and has no effect. waagent now auto-detects cloud-init as a provisioning agent (with an option to override with `Provisioning.Agent`). #### __Provisioning.DeleteRootPassword__ _Type: Boolean_ _Default: n_ If set, the root password in the /etc/shadow file is erased during the provisioning process. #### __Provisioning.RegenerateSshHostKeyPair__ _Type: Boolean_ _Default: y_ If set, all SSH host key pairs (ecdsa, dsa and rsa) are deleted during the provisioning process from /etc/ssh/. And a single fresh key pair is generated. The encryption type for the fresh key pair is configurable by the Provisioning.SshHostKeyPairType entry. Please note that some distributions will re-create SSH key pairs for any missing encryption types when the SSH daemon is restarted (for example, upon a reboot). #### __Provisioning.SshHostKeyPairType__ _Type: String_ _Default: rsa_ This can be set to an encryption algorithm type that is supported by the SSH daemon on the VM. The typically supported values are "rsa", "dsa" and "ecdsa". Note that "putty.exe" on Windows does not support "ecdsa". So, if you intend to use putty.exe on Windows to connect to a Linux deployment, please use "rsa" or "dsa". #### __Provisioning.MonitorHostName__ _Type: Boolean_ _Default: y_ If set, waagent will monitor the Linux VM for hostname changes (as returned by the "hostname" command) and automatically update the networking configuration in the image to reflect the change. In order to push the name change to the DNS servers, networking will be restarted in the VM. This will result in brief loss of Internet connectivity. #### __Provisioning.DecodeCustomData__ _Type: Boolean_ _Default: n_ If set, waagent will decode CustomData from Base64. #### __Provisioning.ExecuteCustomData__ _Type: Boolean_ _Default: n_ If set, waagent will execute CustomData after provisioning. #### __Provisioning.PasswordCryptId__ _Type: String_ _Default: 6_ Algorithm used by crypt when generating password hash. * 1 - MD5 * 2a - Blowfish * 5 - SHA-256 * 6 - SHA-512 #### __Provisioning.PasswordCryptSaltLength__ _Type: String_ _Default: 10_ Length of random salt used when generating password hash. #### __ResourceDisk.Format__ _Type: Boolean_ _Default: y_ If set, the resource disk provided by the platform will be formatted and mounted by waagent if the filesystem type requested by the user in "ResourceDisk.Filesystem" is anything other than "ntfs". A single partition of type Linux (83) will be made available on the disk. Note that this partition will not be formatted if it can be successfully mounted. #### __ResourceDisk.Filesystem__ _Type: String_ _Default: ext4_ This specifies the filesystem type for the resource disk. Supported values vary by Linux distribution. If the string is X, then mkfs.X should be present on the Linux image. SLES 11 images should typically use 'ext3'. BSD images should use 'ufs2' here. #### __ResourceDisk.MountPoint__ _Type: String_ _Default: /mnt/resource_ This specifies the path at which the resource disk is mounted. #### __ResourceDisk.MountOptions__ _Type: String_ _Default: None_ Specifies disk mount options to be passed to the mount -o command. This is a comma separated list of values, ex. 'nodev,nosuid'. See mount(8) for details. #### __ResourceDisk.EnableSwap__ _Type: Boolean_ _Default: n_ If set, a swap file (/swapfile) is created on the resource disk and added to the system swap space. #### __ResourceDisk.EnableSwapEncryption__ _Type: Boolean_ _Default: n_ If set, the swap file (/swapfile) is mounted as an encrypted filesystem (flag supported only on FreeBSD.) #### __ResourceDisk.SwapSizeMB__ _Type: Integer_ _Default: 0_ The size of the swap file in megabytes. #### __Logs.Verbose__ _Type: Boolean_ _Default: n_ If set, log verbosity is boosted. Waagent logs to /var/log/waagent.log and leverages the system logrotate functionality to rotate logs. #### __Logs.Collect__ _Type: Boolean_ _Default: y_ If set, agent logs will be periodically collected and uploaded to a secure location for improved supportability. NOTE: This feature relies on the agent's resource usage features (cgroups); this flag will not take effect on any distro not supported. #### __Logs.CollectPeriod__ _Type: Integer_ _Default: 3600_ This configures how frequently to collect and upload logs. Default is each hour. NOTE: This only takes effect if the Logs.Collect option is enabled. #### __OS.AllowHTTP__ _Type: Boolean_ _Default: n_ If SSL support is not compiled into Python, the agent will fail all HTTPS requests. You can set this option to 'y' to make the agent fall-back to HTTP, instead of failing the requests. NOTE: Allowing HTTP may unintentionally expose secure data. #### __OS.EnableRDMA__ _Type: Boolean_ _Default: n_ If set, the agent will attempt to install and then load an RDMA kernel driver that matches the version of the firmware on the underlying hardware. #### __OS.EnableFIPS__ _Type: Boolean_ _Default: n_ If set, the agent will emit into the environment "OPENSSL_FIPS=1" when executing OpenSSL commands. This signals OpenSSL to use any installed FIPS-compliant libraries. Note that the agent itself has no FIPS-specific code. _If no FIPS-compliant certificates are installed, then enabling this option will cause all OpenSSL commands to fail._ #### __OS.MonitorDhcpClientRestartPeriod__ _Type: Integer_ _Default: 30_ The agent monitor restarts of the DHCP client and restores network rules when it happens. This setting determines how often (in seconds) to monitor for restarts. #### __OS.RootDeviceScsiTimeout__ _Type: Integer_ _Default: 300_ This configures the SCSI timeout in seconds on the root device. If not set, the system defaults are used. #### __OS.RootDeviceScsiTimeoutPeriod__ _Type: Integer_ _Default: 30_ How often to set the SCSI timeout on the root device (in seconds). This setting is ignored if RootDeviceScsiTimeout is not set. #### __OS.OpensslPath__ _Type: String_ _Default: None_ This can be used to specify an alternate path for the openssl binary to use for cryptographic operations. #### __OS.RemovePersistentNetRulesPeriod__ _Type: Integer_ _Default: 30_ How often to remove the udev rules for persistent network interface names (75-persistent-net-generator.rules and /etc/udev/rules.d/70-persistent-net.rules) (in seconds) #### __OS.SshClientAliveInterval__ _Type: Integer_ _Default: 180_ This values sets the number of seconds the agent uses for the SSH ClientAliveInterval configuration option. #### __OS.SshDir__ _Type: String_ _Default: `/etc/ssh`_ This option can be used to override the normal location of the SSH configuration directory. #### __HttpProxy.Host, HttpProxy.Port__ _Type: String_ _Default: None_ If set, the agent will use this proxy server to access the internet. These values *will* override the `http_proxy` or `https_proxy` environment variables. Lastly, `HttpProxy.Host` is required (if to be used) and `HttpProxy.Port` is optional. #### __CGroups.EnforceLimits__ _Type: Boolean_ _Default: y_ If set, the agent will attempt to set cgroups limits for cpu and memory for the agent process itself as well as extension processes. See the wiki for further details on this. #### __CGroups.Excluded__ _Type: String_ _Default: customscript,runcommand_ The list of extensions which will be excluded from cgroups limits. This should be comma separated. ### Telemetry WALinuxAgent collects usage data and sends it to Microsoft to help improve our products and services. The data collected is used to track service health and assist with Azure support requests. Data collected does not include any personally identifiable information. Read our [privacy statement](http://go.microsoft.com/fwlink/?LinkId=521839) to learn more. WALinuxAgent does not support disabling telemetry at this time. WALinuxAgent must be removed to disable telemetry collection. If you need this feature, please open an issue in GitHub and explain your requirement. ### Appendix We do not maintain packaging information in this repo but some samples are shown below as a reference. See the downstream distribution repositories for officially maintained packaging. #### deb packages The official Ubuntu WALinuxAgent package can be found [here](https://launchpad.net/ubuntu/+source/walinuxagent). Run once: 1. Install required packages ```bash sudo apt-get -y install ubuntu-dev-tools pbuilder python-all debhelper ``` 2. Create the pbuilder environment ```bash sudo pbuilder create --debootstrapopts --variant=buildd ``` 3. Obtain `waagent.dsc` from a downstream package repo To compile the package, from the top-most directory: 1. Build the source package ```bash dpkg-buildpackage -S ``` 2. Build the package ```bash sudo pbuilder build waagent.dsc ``` 3. Fetch the built package, usually from `/var/cache/pbuilder/result` #### rpm packages The instructions below describe how to build an rpm package. 1. Install setuptools ```bash curl https://bootstrap.pypa.io/ez_setup.py -o - | python ``` 2. The following command will build the binary and source RPMs: ```bash python setup.py bdist_rpm ``` ----- This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. WALinuxAgent-2.9.1.1/SECURITY.md000066400000000000000000000053051446033677600160040ustar00rootroot00000000000000 ## Security Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. ## Reporting Security Issues **Please do not report security vulnerabilities through public GitHub issues.** Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) * Full paths of source file(s) related to the manifestation of the issue * The location of the affected source code (tag/branch/commit or direct URL) * Any special configuration required to reproduce the issue * Step-by-step instructions to reproduce the issue * Proof-of-concept or exploit code (if possible) * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. ## Preferred Languages We prefer all communications to be in English. ## Policy Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). WALinuxAgent-2.9.1.1/__main__.py000066400000000000000000000012521446033677600163020ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import azurelinuxagent.agent as agent agent.main() WALinuxAgent-2.9.1.1/azurelinuxagent/000077500000000000000000000000001446033677600174355ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/azurelinuxagent/__init__.py000066400000000000000000000011651446033677600215510ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # WALinuxAgent-2.9.1.1/azurelinuxagent/agent.py000066400000000000000000000407061446033677600211140ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # """ Module agent """ from __future__ import print_function import os import re import subprocess import sys import threading from azurelinuxagent.common import cgroupconfigurator, logcollector from azurelinuxagent.common.cgroupapi import SystemdCgroupsApi import azurelinuxagent.common.conf as conf import azurelinuxagent.common.event as event import azurelinuxagent.common.logger as logger from azurelinuxagent.common.future import ustr from azurelinuxagent.common.logcollector import LogCollector, OUTPUT_RESULTS_FILE_PATH from azurelinuxagent.common.osutil import get_osutil from azurelinuxagent.common.utils import fileutil, textutil from azurelinuxagent.common.utils.flexible_version import FlexibleVersion from azurelinuxagent.common.utils.networkutil import AddFirewallRules from azurelinuxagent.common.version import AGENT_NAME, AGENT_LONG_VERSION, AGENT_VERSION, \ DISTRO_NAME, DISTRO_VERSION, \ PY_VERSION_MAJOR, PY_VERSION_MINOR, \ PY_VERSION_MICRO, GOAL_STATE_AGENT_VERSION, \ get_daemon_version, set_daemon_version from azurelinuxagent.ga.collect_logs import CollectLogsHandler, get_log_collector_monitor_handler from azurelinuxagent.pa.provision.default import ProvisionHandler class AgentCommands(object): """ This is the list of all commands that the Linux Guest Agent supports """ DeprovisionUser = "deprovision+user" Deprovision = "deprovision" Daemon = "daemon" Start = "start" RegisterService = "register-service" RunExthandlers = "run-exthandlers" Version = "version" ShowConfig = "show-configuration" Help = "help" CollectLogs = "collect-logs" SetupFirewall = "setup-firewall" Provision = "provision" class Agent(object): def __init__(self, verbose, conf_file_path=None): """ Initialize agent running environment. """ self.conf_file_path = conf_file_path self.osutil = get_osutil() # Init stdout log level = logger.LogLevel.VERBOSE if verbose else logger.LogLevel.INFO logger.add_logger_appender(logger.AppenderType.STDOUT, level) # Init config conf_file_path = self.conf_file_path \ if self.conf_file_path is not None \ else self.osutil.get_agent_conf_file_path() conf.load_conf_from_file(conf_file_path) # Init log verbose = verbose or conf.get_logs_verbose() level = logger.LogLevel.VERBOSE if verbose else logger.LogLevel.INFO logger.add_logger_appender(logger.AppenderType.FILE, level, path=conf.get_agent_log_file()) # echo the log to /dev/console if the machine will be provisioned if conf.get_logs_console() and not ProvisionHandler.is_provisioned(): self.__add_console_appender(level) if event.send_logs_to_telemetry(): logger.add_logger_appender(logger.AppenderType.TELEMETRY, logger.LogLevel.WARNING, path=event.add_log_event) ext_log_dir = conf.get_ext_log_dir() try: if os.path.isfile(ext_log_dir): raise Exception("{0} is a file".format(ext_log_dir)) if not os.path.isdir(ext_log_dir): fileutil.mkdir(ext_log_dir, mode=0o755, owner="root") except Exception as e: logger.error( "Exception occurred while creating extension " "log directory {0}: {1}".format(ext_log_dir, e)) # Init event reporter # Note that the reporter is not fully initialized here yet. Some telemetry fields are filled with data # originating from the goal state or IMDS, which requires a WireProtocol instance. Once a protocol # has been established, those fields must be explicitly initialized using # initialize_event_logger_vminfo_common_parameters(). Any events created before that initialization # will contain dummy values on those fields. event.init_event_status(conf.get_lib_dir()) event_dir = os.path.join(conf.get_lib_dir(), event.EVENTS_DIRECTORY) event.init_event_logger(event_dir) event.enable_unhandled_err_dump("WALA") def __add_console_appender(self, level): logger.add_logger_appender(logger.AppenderType.CONSOLE, level, path="/dev/console") def daemon(self): """ Run agent daemon """ set_daemon_version(AGENT_VERSION) logger.set_prefix("Daemon") threading.current_thread().setName("Daemon") child_args = None \ if self.conf_file_path is None \ else "-configuration-path:{0}".format(self.conf_file_path) from azurelinuxagent.daemon import get_daemon_handler daemon_handler = get_daemon_handler() daemon_handler.run(child_args=child_args) def provision(self): """ Run provision command """ from azurelinuxagent.pa.provision import get_provision_handler provision_handler = get_provision_handler() provision_handler.run() def deprovision(self, force=False, deluser=False): """ Run deprovision command """ from azurelinuxagent.pa.deprovision import get_deprovision_handler deprovision_handler = get_deprovision_handler() deprovision_handler.run(force=force, deluser=deluser) def register_service(self): """ Register agent as a service """ print("Register {0} service".format(AGENT_NAME)) self.osutil.register_agent_service() print("Stop {0} service".format(AGENT_NAME)) self.osutil.stop_agent_service() print("Start {0} service".format(AGENT_NAME)) self.osutil.start_agent_service() def run_exthandlers(self, debug=False): """ Run the update and extension handler """ logger.set_prefix("ExtHandler") threading.current_thread().setName("ExtHandler") # # Agents < 2.2.53 used to echo the log to the console. Since the extension handler could have been started by # one of those daemons, output a message indicating that output to the console will stop, otherwise users # may think that the agent died if they noticed that output to the console stops abruptly. # # Feel free to remove this code if telemetry shows there are no more agents <= 2.2.53 in the field. # if conf.get_logs_console() and get_daemon_version() < FlexibleVersion("2.2.53"): self.__add_console_appender(logger.LogLevel.INFO) try: logger.info(u"The agent will now check for updates and then will process extensions. Output to /dev/console will be suspended during those operations.") finally: logger.disable_console_output() from azurelinuxagent.ga.update import get_update_handler update_handler = get_update_handler() update_handler.run(debug) def show_configuration(self): configuration = conf.get_configuration() for k in sorted(configuration.keys()): print("{0} = {1}".format(k, configuration[k])) def collect_logs(self, is_full_mode): logger.set_prefix("LogCollector") if is_full_mode: logger.info("Running log collector mode full") else: logger.info("Running log collector mode normal") # Check the cgroups unit cpu_cgroup_path, memory_cgroup_path, log_collector_monitor = None, None, None if CollectLogsHandler.should_validate_cgroups(): cgroups_api = SystemdCgroupsApi() cpu_cgroup_path, memory_cgroup_path = cgroups_api.get_process_cgroup_paths("self") cpu_slice_matches = (cgroupconfigurator.LOGCOLLECTOR_SLICE in cpu_cgroup_path) memory_slice_matches = (cgroupconfigurator.LOGCOLLECTOR_SLICE in memory_cgroup_path) if not cpu_slice_matches or not memory_slice_matches: logger.info("The Log Collector process is not in the proper cgroups:") if not cpu_slice_matches: logger.info("\tunexpected cpu slice") if not memory_slice_matches: logger.info("\tunexpected memory slice") sys.exit(logcollector.INVALID_CGROUPS_ERRCODE) try: log_collector = LogCollector(is_full_mode, cpu_cgroup_path, memory_cgroup_path) log_collector_monitor = get_log_collector_monitor_handler(log_collector.cgroups) log_collector_monitor.run() archive = log_collector.collect_logs_and_get_archive() logger.info("Log collection successfully completed. Archive can be found at {0} " "and detailed log output can be found at {1}".format(archive, OUTPUT_RESULTS_FILE_PATH)) except Exception as e: logger.error("Log collection completed unsuccessfully. Error: {0}".format(ustr(e))) logger.info("Detailed log output can be found at {0}".format(OUTPUT_RESULTS_FILE_PATH)) sys.exit(1) finally: if log_collector_monitor is not None: log_collector_monitor.stop() @staticmethod def setup_firewall(firewall_metadata): print("Setting up firewall for the WALinux Agent with args: {0}".format(firewall_metadata)) try: AddFirewallRules.add_iptables_rules(firewall_metadata['wait'], firewall_metadata['dst_ip'], firewall_metadata['uid']) print("Successfully set the firewall rules") except Exception as error: print("Unable to add firewall rules. Error: {0}".format(ustr(error))) sys.exit(1) def main(args=None): """ Parse command line arguments, exit with usage() on error. Invoke different methods according to different command """ if args is None: args = [] if len(args) <= 0: args = sys.argv[1:] command, force, verbose, debug, conf_file_path, log_collector_full_mode, firewall_metadata = parse_args(args) if command == AgentCommands.Version: version() elif command == AgentCommands.Help: print(usage()) elif command == AgentCommands.Start: start(conf_file_path=conf_file_path) else: try: agent = Agent(verbose, conf_file_path=conf_file_path) if command == AgentCommands.DeprovisionUser: agent.deprovision(force, deluser=True) elif command == AgentCommands.Deprovision: agent.deprovision(force, deluser=False) elif command == AgentCommands.Provision: agent.provision() elif command == AgentCommands.RegisterService: agent.register_service() elif command == AgentCommands.Daemon: agent.daemon() elif command == AgentCommands.RunExthandlers: agent.run_exthandlers(debug) elif command == AgentCommands.ShowConfig: agent.show_configuration() elif command == AgentCommands.CollectLogs: agent.collect_logs(log_collector_full_mode) elif command == AgentCommands.SetupFirewall: agent.setup_firewall(firewall_metadata) except Exception as e: logger.error(u"Failed to run '{0}': {1}", command, textutil.format_exception(e)) def parse_args(sys_args): """ Parse command line arguments """ cmd = AgentCommands.Help force = False verbose = False debug = False conf_file_path = None log_collector_full_mode = False firewall_metadata = { "dst_ip": None, "uid": None, "wait": "" } regex_cmd_format = "^([-/]*){0}" for arg in sys_args: if arg == "": # Don't parse an empty parameter continue m = re.match("^(?:[-/]*)configuration-path:([\w/\.\-_]+)", arg) # pylint: disable=W1401 if not m is None: conf_file_path = m.group(1) if not os.path.exists(conf_file_path): print("Error: Configuration file {0} does not exist".format( conf_file_path), file=sys.stderr) print(usage()) sys.exit(1) elif re.match("^([-/]*)deprovision\\+user", arg): cmd = AgentCommands.DeprovisionUser elif re.match(regex_cmd_format.format(AgentCommands.Deprovision), arg): cmd = AgentCommands.Deprovision elif re.match(regex_cmd_format.format(AgentCommands.Daemon), arg): cmd = AgentCommands.Daemon elif re.match(regex_cmd_format.format(AgentCommands.Start), arg): cmd = AgentCommands.Start elif re.match(regex_cmd_format.format(AgentCommands.RegisterService), arg): cmd = AgentCommands.RegisterService elif re.match(regex_cmd_format.format(AgentCommands.RunExthandlers), arg): cmd = AgentCommands.RunExthandlers elif re.match(regex_cmd_format.format(AgentCommands.Version), arg): cmd = AgentCommands.Version elif re.match(regex_cmd_format.format("verbose"), arg): verbose = True elif re.match(regex_cmd_format.format("debug"), arg): debug = True elif re.match(regex_cmd_format.format("force"), arg): force = True elif re.match(regex_cmd_format.format(AgentCommands.ShowConfig), arg): cmd = AgentCommands.ShowConfig elif re.match("^([-/]*)(help|usage|\\?)", arg): cmd = AgentCommands.Help elif re.match(regex_cmd_format.format(AgentCommands.CollectLogs), arg): cmd = AgentCommands.CollectLogs elif re.match(regex_cmd_format.format("full"), arg): log_collector_full_mode = True elif re.match(regex_cmd_format.format(AgentCommands.SetupFirewall), arg): cmd = AgentCommands.SetupFirewall elif re.match(regex_cmd_format.format("dst_ip=(?P[\\d.]{7,})"), arg): firewall_metadata['dst_ip'] = re.match(regex_cmd_format.format("dst_ip=(?P[\\d.]{7,})"), arg).group( 'dst_ip') elif re.match(regex_cmd_format.format("uid=(?P[\\d]+)"), arg): firewall_metadata['uid'] = re.match(regex_cmd_format.format("uid=(?P[\\d]+)"), arg).group('uid') elif re.match(regex_cmd_format.format("(w|wait)$"), arg): firewall_metadata['wait'] = "-w" else: cmd = AgentCommands.Help break return cmd, force, verbose, debug, conf_file_path, log_collector_full_mode, firewall_metadata def version(): """ Show agent version """ print(("{0} running on {1} {2}".format(AGENT_LONG_VERSION, DISTRO_NAME, DISTRO_VERSION))) print("Python: {0}.{1}.{2}".format(PY_VERSION_MAJOR, PY_VERSION_MINOR, PY_VERSION_MICRO)) print("Goal state agent: {0}".format(GOAL_STATE_AGENT_VERSION)) def usage(): """ Return agent usage message """ s = "\n" s += ("usage: {0} [-verbose] [-force] [-help] " "-configuration-path:" "-deprovision[+user]|-register-service|-version|-daemon|-start|" "-run-exthandlers|-show-configuration|-collect-logs [-full]|-setup-firewall [-dst_ip= -uid= [-w/--wait]]" "").format(sys.argv[0]) s += "\n" return s def start(conf_file_path=None): """ Start agent daemon in a background process and set stdout/stderr to /dev/null """ args = [sys.argv[0], '-daemon'] if conf_file_path is not None: args.append('-configuration-path:{0}'.format(conf_file_path)) with open(os.devnull, 'w') as devnull: subprocess.Popen(args, stdout=devnull, stderr=devnull) if __name__ == '__main__' : main() WALinuxAgent-2.9.1.1/azurelinuxagent/common/000077500000000000000000000000001446033677600207255ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/azurelinuxagent/common/AgentGlobals.py000066400000000000000000000023221446033677600236400ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2020 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ class AgentGlobals(object): """ This class is used for setting AgentGlobals which can be used all throughout the Agent. """ GUID_ZERO = "00000000-0000-0000-0000-000000000000" # # Some modules (e.g. telemetry) require an up-to-date container ID. We update this variable each time we # fetch the goal state. # _container_id = GUID_ZERO @staticmethod def get_container_id(): return AgentGlobals._container_id @staticmethod def update_container_id(container_id): AgentGlobals._container_id = container_id WALinuxAgent-2.9.1.1/azurelinuxagent/common/__init__.py000066400000000000000000000011661446033677600230420ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # WALinuxAgent-2.9.1.1/azurelinuxagent/common/agent_supported_feature.py000066400000000000000000000104551446033677600262220ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # class SupportedFeatureNames(object): """ Enum for defining the Feature Names for all features that we the agent supports """ MultiConfig = "MultipleExtensionsPerHandler" ExtensionTelemetryPipeline = "ExtensionTelemetryPipeline" FastTrack = "FastTrack" class AgentSupportedFeature(object): """ Interface for defining all features that the Linux Guest Agent supports and reports their if supported back to CRP """ def __init__(self, name, version="1.0", supported=False): self.__name = name self.__version = version self.__supported = supported @property def name(self): return self.__name @property def version(self): return self.__version @property def is_supported(self): return self.__supported class _MultiConfigFeature(AgentSupportedFeature): __NAME = SupportedFeatureNames.MultiConfig __VERSION = "1.0" __SUPPORTED = True def __init__(self): super(_MultiConfigFeature, self).__init__(name=_MultiConfigFeature.__NAME, version=_MultiConfigFeature.__VERSION, supported=_MultiConfigFeature.__SUPPORTED) class _ETPFeature(AgentSupportedFeature): __NAME = SupportedFeatureNames.ExtensionTelemetryPipeline __VERSION = "1.0" __SUPPORTED = True def __init__(self): super(_ETPFeature, self).__init__(name=self.__NAME, version=self.__VERSION, supported=self.__SUPPORTED) # This is the list of features that Agent supports and we advertise to CRP __CRP_ADVERTISED_FEATURES = { SupportedFeatureNames.MultiConfig: _MultiConfigFeature() } # This is the list of features that Agent supports and we advertise to Extensions __EXTENSION_ADVERTISED_FEATURES = { SupportedFeatureNames.ExtensionTelemetryPipeline: _ETPFeature() } def get_supported_feature_by_name(feature_name): if feature_name in __CRP_ADVERTISED_FEATURES: return __CRP_ADVERTISED_FEATURES[feature_name] if feature_name in __EXTENSION_ADVERTISED_FEATURES: return __EXTENSION_ADVERTISED_FEATURES[feature_name] raise NotImplementedError("Feature with Name: {0} not found".format(feature_name)) def get_agent_supported_features_list_for_crp(): """ List of features that the GuestAgent currently supports (like FastTrack, MultiConfig, etc). We need to send this list as part of Status reporting to inform CRP of all the features the agent supports. :return: Dict containing all CRP supported features with the key as their names and the AgentFeature object as the value if they are supported by the Agent Eg: { MultipleExtensionsPerHandler: _MultiConfigFeature() } """ return dict((name, feature) for name, feature in __CRP_ADVERTISED_FEATURES.items() if feature.is_supported) def get_agent_supported_features_list_for_extensions(): """ List of features that the GuestAgent currently supports (like Extension Telemetry Pipeline, etc) needed by Extensions. We need to send this list as environment variables when calling extension commands to inform Extensions of all the features the agent supports. :return: Dict containing all Extension supported features with the key as their names and the AgentFeature object as the value if the feature is supported by the Agent. Eg: { CRPSupportedFeatureNames.ExtensionTelemetryPipeline: _ETPFeature() } """ return dict((name, feature) for name, feature in __EXTENSION_ADVERTISED_FEATURES.items() if feature.is_supported) WALinuxAgent-2.9.1.1/azurelinuxagent/common/cgroup.py000066400000000000000000000361241446033677600226040ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ import errno import os import re from datetime import timedelta from azurelinuxagent.common import logger, conf from azurelinuxagent.common.exception import CGroupsException from azurelinuxagent.common.future import ustr from azurelinuxagent.common.osutil import get_osutil from azurelinuxagent.common.utils import fileutil _REPORT_EVERY_HOUR = timedelta(hours=1) _DEFAULT_REPORT_PERIOD = timedelta(seconds=conf.get_cgroup_check_period()) AGENT_NAME_TELEMETRY = "walinuxagent.service" # Name used for telemetry; it needs to be consistent even if the name of the service changes AGENT_LOG_COLLECTOR = "azure-walinuxagent-logcollector" class CounterNotFound(Exception): pass class MetricValue(object): """ Class for defining all the required metric fields to send telemetry. """ def __init__(self, category, counter, instance, value, report_period=_DEFAULT_REPORT_PERIOD): self._category = category self._counter = counter self._instance = instance self._value = value self._report_period = report_period @property def category(self): return self._category @property def counter(self): return self._counter @property def instance(self): return self._instance @property def value(self): return self._value @property def report_period(self): return self._report_period class MetricsCategory(object): MEMORY_CATEGORY = "Memory" CPU_CATEGORY = "CPU" class MetricsCounter(object): PROCESSOR_PERCENT_TIME = "% Processor Time" TOTAL_MEM_USAGE = "Total Memory Usage" MAX_MEM_USAGE = "Max Memory Usage" THROTTLED_TIME = "Throttled Time" SWAP_MEM_USAGE = "Swap Memory Usage" AVAILABLE_MEM = "Available MBytes" USED_MEM = "Used MBytes" re_user_system_times = re.compile(r'user (\d+)\nsystem (\d+)\n') class CGroup(object): def __init__(self, name, cgroup_path): """ Initialize _data collection for the Memory controller :param: name: Name of the CGroup :param: cgroup_path: Path of the controller :return: """ self.name = name self.path = cgroup_path def __str__(self): return "{0} [{1}]".format(self.name, self.path) def _get_cgroup_file(self, file_name): return os.path.join(self.path, file_name) def _get_file_contents(self, file_name): """ Retrieve the contents to file. :param str file_name: Name of file within that metric controller :return: Entire contents of the file :rtype: str """ parameter_file = self._get_cgroup_file(file_name) return fileutil.read_file(parameter_file) def _get_parameters(self, parameter_name, first_line_only=False): """ Retrieve the values of a parameter from a controller. Returns a list of values in the file. :param first_line_only: return only the first line. :param str parameter_name: Name of file within that metric controller :return: The first line of the file, without line terminator :rtype: [str] """ result = [] try: values = self._get_file_contents(parameter_name).splitlines() result = values[0] if first_line_only else values except IndexError: parameter_filename = self._get_cgroup_file(parameter_name) logger.error("File {0} is empty but should not be".format(parameter_filename)) raise CGroupsException("File {0} is empty but should not be".format(parameter_filename)) except Exception as e: if isinstance(e, (IOError, OSError)) and e.errno == errno.ENOENT: # pylint: disable=E1101 raise e parameter_filename = self._get_cgroup_file(parameter_name) raise CGroupsException("Exception while attempting to read {0}".format(parameter_filename), e) return result def is_active(self): try: tasks = self._get_parameters("tasks") if tasks: return len(tasks) != 0 except (IOError, OSError) as e: if e.errno == errno.ENOENT: # only suppressing file not found exceptions. pass else: logger.periodic_warn(logger.EVERY_HALF_HOUR, 'Could not get list of tasks from "tasks" file in the cgroup: {0}.' ' Internal error: {1}'.format(self.path, ustr(e))) except CGroupsException as e: logger.periodic_warn(logger.EVERY_HALF_HOUR, 'Could not get list of tasks from "tasks" file in the cgroup: {0}.' ' Internal error: {1}'.format(self.path, ustr(e))) return False def get_tracked_metrics(self, **_): """ Retrieves the current value of the metrics tracked for this cgroup and returns them as an array. Note: Agent won't track the metrics if the current cpu ticks less than previous value and returns empty array. """ raise NotImplementedError() class CpuCgroup(CGroup): def __init__(self, name, cgroup_path): super(CpuCgroup, self).__init__(name, cgroup_path) self._osutil = get_osutil() self._previous_cgroup_cpu = None self._previous_system_cpu = None self._current_cgroup_cpu = None self._current_system_cpu = None self._previous_throttled_time = None self._current_throttled_time = None def _get_cpu_ticks(self, allow_no_such_file_or_directory_error=False): """ Returns the number of USER_HZ of CPU time (user and system) consumed by this cgroup. If allow_no_such_file_or_directory_error is set to True and cpuacct.stat does not exist the function returns 0; this is useful when the function can be called before the cgroup has been created. """ try: cpuacct_stat = self._get_file_contents('cpuacct.stat') except Exception as e: if not isinstance(e, (IOError, OSError)) or e.errno != errno.ENOENT: # pylint: disable=E1101 raise CGroupsException("Failed to read cpuacct.stat: {0}".format(ustr(e))) if not allow_no_such_file_or_directory_error: raise e cpuacct_stat = None cpu_ticks = 0 if cpuacct_stat is not None: # # Sample file: # # cat /sys/fs/cgroup/cpuacct/azure.slice/walinuxagent.service/cpuacct.stat # user 10190 # system 3160 # match = re_user_system_times.match(cpuacct_stat) if not match: raise CGroupsException( "The contents of {0} are invalid: {1}".format(self._get_cgroup_file('cpuacct.stat'), cpuacct_stat)) cpu_ticks = int(match.groups()[0]) + int(match.groups()[1]) return cpu_ticks def get_throttled_time(self): try: with open(os.path.join(self.path, 'cpu.stat')) as cpu_stat: # # Sample file: # # # cat /sys/fs/cgroup/cpuacct/azure.slice/walinuxagent.service/cpu.stat # nr_periods 51660 # nr_throttled 19461 # throttled_time 1529590856339 # for line in cpu_stat: match = re.match(r'throttled_time\s+(\d+)', line) if match is not None: return int(match.groups()[0]) raise Exception("Cannot find throttled_time") except (IOError, OSError) as e: if e.errno == errno.ENOENT: return 0 raise CGroupsException("Failed to read cpu.stat: {0}".format(ustr(e))) except Exception as e: raise CGroupsException("Failed to read cpu.stat: {0}".format(ustr(e))) def _cpu_usage_initialized(self): return self._current_cgroup_cpu is not None and self._current_system_cpu is not None def initialize_cpu_usage(self): """ Sets the initial values of CPU usage. This function must be invoked before calling get_cpu_usage(). """ if self._cpu_usage_initialized(): raise CGroupsException("initialize_cpu_usage() should be invoked only once") self._current_cgroup_cpu = self._get_cpu_ticks(allow_no_such_file_or_directory_error=True) self._current_system_cpu = self._osutil.get_total_cpu_ticks_since_boot() self._current_throttled_time = self.get_throttled_time() def get_cpu_usage(self): """ Computes the CPU used by the cgroup since the last call to this function. The usage is measured as a percentage of utilization of 1 core in the system. For example, using 1 core all of the time on a 4-core system would be reported as 100%. NOTE: initialize_cpu_usage() must be invoked before calling get_cpu_usage() """ if not self._cpu_usage_initialized(): raise CGroupsException("initialize_cpu_usage() must be invoked before the first call to get_cpu_usage()") self._previous_cgroup_cpu = self._current_cgroup_cpu self._previous_system_cpu = self._current_system_cpu self._current_cgroup_cpu = self._get_cpu_ticks() self._current_system_cpu = self._osutil.get_total_cpu_ticks_since_boot() cgroup_delta = self._current_cgroup_cpu - self._previous_cgroup_cpu system_delta = max(1, self._current_system_cpu - self._previous_system_cpu) return round(100.0 * self._osutil.get_processor_cores() * float(cgroup_delta) / float(system_delta), 3) def get_cpu_throttled_time(self, read_previous_throttled_time=True): """ Computes the throttled time (in seconds) since the last call to this function. NOTE: initialize_cpu_usage() must be invoked before calling this function Compute only current throttled time if read_previous_throttled_time set to False """ if not read_previous_throttled_time: return float(self.get_throttled_time() / 1E9) if not self._cpu_usage_initialized(): raise CGroupsException( "initialize_cpu_usage() must be invoked before the first call to get_throttled_time()") self._previous_throttled_time = self._current_throttled_time self._current_throttled_time = self.get_throttled_time() return float(self._current_throttled_time - self._previous_throttled_time) / 1E9 def get_tracked_metrics(self, **kwargs): tracked = [] cpu_usage = self.get_cpu_usage() if cpu_usage >= float(0): tracked.append( MetricValue(MetricsCategory.CPU_CATEGORY, MetricsCounter.PROCESSOR_PERCENT_TIME, self.name, cpu_usage)) if 'track_throttled_time' in kwargs and kwargs['track_throttled_time']: throttled_time = self.get_cpu_throttled_time() if cpu_usage >= float(0) and throttled_time >= float(0): tracked.append( MetricValue(MetricsCategory.CPU_CATEGORY, MetricsCounter.THROTTLED_TIME, self.name, throttled_time)) return tracked class MemoryCgroup(CGroup): def __init__(self, name, cgroup_path): super(MemoryCgroup, self).__init__(name, cgroup_path) self._counter_not_found_error_count = 0 def _get_memory_stat_counter(self, counter_name): try: with open(os.path.join(self.path, 'memory.stat')) as memory_stat: # cat /sys/fs/cgroup/memory/azure.slice/memory.stat # cache 67178496 # rss 42340352 # rss_huge 6291456 # swap 0 for line in memory_stat: re_memory_counter = r'{0}\s+(\d+)'.format(counter_name) match = re.match(re_memory_counter, line) if match is not None: return int(match.groups()[0]) except (IOError, OSError) as e: if e.errno == errno.ENOENT: raise raise CGroupsException("Failed to read memory.stat: {0}".format(ustr(e))) except Exception as e: raise CGroupsException("Failed to read memory.stat: {0}".format(ustr(e))) raise CounterNotFound("Cannot find counter: {0}".format(counter_name)) def get_memory_usage(self): """ Collect RSS+CACHE from memory.stat cgroup. :return: Memory usage in bytes :rtype: int """ cache = self._get_memory_stat_counter("cache") rss = self._get_memory_stat_counter("rss") return cache + rss def try_swap_memory_usage(self): """ Collect SWAP from memory.stat cgroup. :return: Memory usage in bytes :rtype: int Note: stat file is the only place to get the SWAP since other swap related file memory.memsw.usage_in_bytes is for total Memory+SWAP. """ try: return self._get_memory_stat_counter("swap") except CounterNotFound as e: if self._counter_not_found_error_count < 1: logger.periodic_info(logger.EVERY_HALF_HOUR, '{0} from "memory.stat" file in the cgroup: {1}---[Note: This log for informational purpose only and can be ignored]'.format(ustr(e), self.path)) self._counter_not_found_error_count += 1 return 0 def get_max_memory_usage(self): """ Collect memory.max_usage_in_bytes from the cgroup. :return: Memory usage in bytes :rtype: int """ usage = 0 try: usage = int(self._get_parameters('memory.max_usage_in_bytes', first_line_only=True)) except Exception as e: if isinstance(e, (IOError, OSError)) and e.errno == errno.ENOENT: # pylint: disable=E1101 raise raise CGroupsException("Exception while attempting to read {0}".format("memory.max_usage_in_bytes"), e) return usage def get_tracked_metrics(self, **_): return [ MetricValue(MetricsCategory.MEMORY_CATEGORY, MetricsCounter.TOTAL_MEM_USAGE, self.name, self.get_memory_usage()), MetricValue(MetricsCategory.MEMORY_CATEGORY, MetricsCounter.MAX_MEM_USAGE, self.name, self.get_max_memory_usage(), _REPORT_EVERY_HOUR), MetricValue(MetricsCategory.MEMORY_CATEGORY, MetricsCounter.SWAP_MEM_USAGE, self.name, self.try_swap_memory_usage(), _REPORT_EVERY_HOUR) ] WALinuxAgent-2.9.1.1/azurelinuxagent/common/cgroupapi.py000066400000000000000000000410661446033677600232770ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ import os import re import shutil import subprocess import threading import uuid from azurelinuxagent.common import logger from azurelinuxagent.common.cgroup import CpuCgroup, MemoryCgroup from azurelinuxagent.common.cgroupstelemetry import CGroupsTelemetry from azurelinuxagent.common.conf import get_agent_pid_file_path from azurelinuxagent.common.exception import CGroupsException, ExtensionErrorCodes, ExtensionError, \ ExtensionOperationError from azurelinuxagent.common.future import ustr from azurelinuxagent.common.osutil import systemd from azurelinuxagent.common.utils import fileutil, shellutil from azurelinuxagent.common.utils.extensionprocessutil import handle_process_completion, read_output, \ TELEMETRY_MESSAGE_MAX_LEN from azurelinuxagent.common.utils.flexible_version import FlexibleVersion from azurelinuxagent.common.version import get_distro CGROUPS_FILE_SYSTEM_ROOT = '/sys/fs/cgroup' CGROUP_CONTROLLERS = ["cpu", "memory"] EXTENSION_SLICE_PREFIX = "azure-vmextensions" class SystemdRunError(CGroupsException): """ Raised when systemd-run fails """ def __init__(self, msg=None): super(SystemdRunError, self).__init__(msg) class CGroupsApi(object): @staticmethod def cgroups_supported(): distro_info = get_distro() distro_name = distro_info[0] try: distro_version = FlexibleVersion(distro_info[1]) except ValueError: return False return distro_name.lower() == 'ubuntu' and distro_version.major >= 16 @staticmethod def track_cgroups(extension_cgroups): try: for cgroup in extension_cgroups: CGroupsTelemetry.track_cgroup(cgroup) except Exception as exception: logger.warn("Cannot add cgroup '{0}' to tracking list; resource usage will not be tracked. " "Error: {1}".format(cgroup.path, ustr(exception))) @staticmethod def get_processes_in_cgroup(cgroup_path): with open(os.path.join(cgroup_path, "cgroup.procs"), "r") as cgroup_procs: return [int(pid) for pid in cgroup_procs.read().split()] @staticmethod def _foreach_legacy_cgroup(operation): """ Previous versions of the daemon (2.2.31-2.2.40) wrote their PID to /sys/fs/cgroup/{cpu,memory}/WALinuxAgent/WALinuxAgent; starting from version 2.2.41 we track the agent service in walinuxagent.service instead of WALinuxAgent/WALinuxAgent. Also, when running under systemd, the PIDs should not be explicitly moved to the cgroup filesystem. The older daemons would incorrectly do that under certain conditions. This method checks for the existence of the legacy cgroups and, if the daemon's PID has been added to them, executes the given operation on the cgroups. After this check, the method attempts to remove the legacy cgroups. :param operation: The function to execute on each legacy cgroup. It must take 2 arguments: the controller and the daemon's PID """ legacy_cgroups = [] for controller in ['cpu', 'memory']: cgroup = os.path.join(CGROUPS_FILE_SYSTEM_ROOT, controller, "WALinuxAgent", "WALinuxAgent") if os.path.exists(cgroup): logger.info('Found legacy cgroup {0}', cgroup) legacy_cgroups.append((controller, cgroup)) try: for controller, cgroup in legacy_cgroups: procs_file = os.path.join(cgroup, "cgroup.procs") if os.path.exists(procs_file): procs_file_contents = fileutil.read_file(procs_file).strip() daemon_pid = CGroupsApi.get_daemon_pid() if ustr(daemon_pid) in procs_file_contents: operation(controller, daemon_pid) finally: for _, cgroup in legacy_cgroups: logger.info('Removing {0}', cgroup) shutil.rmtree(cgroup, ignore_errors=True) return len(legacy_cgroups) @staticmethod def get_daemon_pid(): return int(fileutil.read_file(get_agent_pid_file_path()).strip()) class SystemdCgroupsApi(CGroupsApi): """ Cgroups interface via systemd """ def __init__(self): self._cgroup_mountpoints = None self._agent_unit_name = None self._systemd_run_commands = [] self._systemd_run_commands_lock = threading.RLock() def get_systemd_run_commands(self): """ Returns a list of the systemd-run commands currently running (given as PIDs) """ with self._systemd_run_commands_lock: return self._systemd_run_commands[:] def get_cgroup_mount_points(self): """ Returns a tuple with the mount points for the cpu and memory controllers; the values can be None if the corresponding controller is not mounted """ # the output of mount is similar to # $ mount -t cgroup # cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd) # cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct) # cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory) # etc # if self._cgroup_mountpoints is None: cpu = None memory = None for line in shellutil.run_command(['mount', '-t', 'cgroup']).splitlines(): match = re.search(r'on\s+(?P/\S+(memory|cpuacct))\s', line) if match is not None: path = match.group('path') if 'cpuacct' in path: cpu = path else: memory = path self._cgroup_mountpoints = {'cpu': cpu, 'memory': memory} return self._cgroup_mountpoints['cpu'], self._cgroup_mountpoints['memory'] @staticmethod def get_process_cgroup_relative_paths(process_id): """ Returns a tuple with the path of the cpu and memory cgroups for the given process (relative to the mount point of the corresponding controller). The 'process_id' can be a numeric PID or the string "self" for the current process. The values returned can be None if the process is not in a cgroup for that controller (e.g. the controller is not mounted). """ # The contents of the file are similar to # # cat /proc/1218/cgroup # 10:memory:/system.slice/walinuxagent.service # 3:cpu,cpuacct:/system.slice/walinuxagent.service # etc cpu_path = None memory_path = None for line in fileutil.read_file("/proc/{0}/cgroup".format(process_id)).splitlines(): match = re.match(r'\d+:(?P(memory|.*cpuacct.*)):(?P.+)', line) if match is not None: controller = match.group('controller') path = match.group('path').lstrip('/') if match.group('path') != '/' else None if controller == 'memory': memory_path = path else: cpu_path = path return cpu_path, memory_path def get_process_cgroup_paths(self, process_id): """ Returns a tuple with the path of the cpu and memory cgroups for the given process. The 'process_id' can be a numeric PID or the string "self" for the current process. The values returned can be None if the process is not in a cgroup for that controller (e.g. the controller is not mounted). """ cpu_cgroup_relative_path, memory_cgroup_relative_path = self.get_process_cgroup_relative_paths(process_id) cpu_mount_point, memory_mount_point = self.get_cgroup_mount_points() cpu_cgroup_path = os.path.join(cpu_mount_point, cpu_cgroup_relative_path) \ if cpu_mount_point is not None and cpu_cgroup_relative_path is not None else None memory_cgroup_path = os.path.join(memory_mount_point, memory_cgroup_relative_path) \ if memory_mount_point is not None and memory_cgroup_relative_path is not None else None return cpu_cgroup_path, memory_cgroup_path def get_unit_cgroup_paths(self, unit_name): """ Returns a tuple with the path of the cpu and memory cgroups for the given unit. The values returned can be None if the controller is not mounted. Ex: ControlGroup=/azure.slice/walinuxagent.service controlgroup_path[1:] = azure.slice/walinuxagent.service """ controlgroup_path = systemd.get_unit_property(unit_name, "ControlGroup") cpu_mount_point, memory_mount_point = self.get_cgroup_mount_points() cpu_cgroup_path = os.path.join(cpu_mount_point, controlgroup_path[1:]) \ if cpu_mount_point is not None else None memory_cgroup_path = os.path.join(memory_mount_point, controlgroup_path[1:]) \ if memory_mount_point is not None else None return cpu_cgroup_path, memory_cgroup_path @staticmethod def get_cgroup2_controllers(): """ Returns a tuple with the mount point for the cgroups v2 controllers, and the currently mounted controllers; either value can be None if cgroups v2 or its controllers are not mounted """ # the output of mount is similar to # $ mount -t cgroup2 # cgroup2 on /sys/fs/cgroup/unified type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate) # for line in shellutil.run_command(['mount', '-t', 'cgroup2']).splitlines(): match = re.search(r'on\s+(?P/\S+)\s', line) if match is not None: mount_point = match.group('path') controllers = None controllers_file = os.path.join(mount_point, 'cgroup.controllers') if os.path.exists(controllers_file): controllers = fileutil.read_file(controllers_file) return mount_point, controllers return None, None @staticmethod def _is_systemd_failure(scope_name, stderr): stderr.seek(0) stderr = ustr(stderr.read(TELEMETRY_MESSAGE_MAX_LEN), encoding='utf-8', errors='backslashreplace') unit_not_found = "Unit {0} not found.".format(scope_name) return unit_not_found in stderr or scope_name not in stderr @staticmethod def get_extension_slice_name(extension_name, old_slice=False): # The old slice makes it difficult for user to override the limits because they need to place drop-in files on every upgrade if extension slice is different for each version. # old slice includes .- # new slice without version . if not old_slice: extension_name = extension_name.rsplit("-", 1)[0] # Since '-' is used as a separator in systemd unit names, we replace it with '_' to prevent side-effects. return EXTENSION_SLICE_PREFIX + "-" + extension_name.replace('-', '_') + ".slice" def start_extension_command(self, extension_name, command, cmd_name, timeout, shell, cwd, env, stdout, stderr, error_code=ExtensionErrorCodes.PluginUnknownFailure): scope = "{0}_{1}".format(cmd_name, uuid.uuid4()) extension_slice_name = self.get_extension_slice_name(extension_name) with self._systemd_run_commands_lock: process = subprocess.Popen( # pylint: disable=W1509 # Some distros like ubuntu20 by default cpu and memory accounting enabled. Thus create nested cgroups under the extension slice # So disabling CPU and Memory accounting prevents from creating nested cgroups, so that all the counters will be present in extension Cgroup # since slice unit file configured with accounting enabled. "systemd-run --property=CPUAccounting=no --property=MemoryAccounting=no --unit={0} --scope --slice={1} {2}".format(scope, extension_slice_name, command), shell=shell, cwd=cwd, stdout=stdout, stderr=stderr, env=env, preexec_fn=os.setsid) # We start systemd-run with shell == True so process.pid is the shell's pid, not the pid for systemd-run self._systemd_run_commands.append(process.pid) scope_name = scope + '.scope' logger.info("Started extension in unit '{0}'", scope_name) cpu_cgroup = None try: cgroup_relative_path = os.path.join('azure.slice/azure-vmextensions.slice', extension_slice_name) cpu_cgroup_mountpoint, memory_cgroup_mountpoint = self.get_cgroup_mount_points() if cpu_cgroup_mountpoint is None: logger.info("The CPU controller is not mounted; will not track resource usage") else: cpu_cgroup_path = os.path.join(cpu_cgroup_mountpoint, cgroup_relative_path) cpu_cgroup = CpuCgroup(extension_name, cpu_cgroup_path) CGroupsTelemetry.track_cgroup(cpu_cgroup) if memory_cgroup_mountpoint is None: logger.info("The Memory controller is not mounted; will not track resource usage") else: memory_cgroup_path = os.path.join(memory_cgroup_mountpoint, cgroup_relative_path) memory_cgroup = MemoryCgroup(extension_name, memory_cgroup_path) CGroupsTelemetry.track_cgroup(memory_cgroup) except IOError as e: if e.errno == 2: # 'No such file or directory' logger.info("The extension command already completed; will not track resource usage") logger.info("Failed to start tracking resource usage for the extension: {0}", ustr(e)) except Exception as e: logger.info("Failed to start tracking resource usage for the extension: {0}", ustr(e)) # Wait for process completion or timeout try: return handle_process_completion(process=process, command=command, timeout=timeout, stdout=stdout, stderr=stderr, error_code=error_code, cpu_cgroup=cpu_cgroup) except ExtensionError as e: # The extension didn't terminate successfully. Determine whether it was due to systemd errors or # extension errors. if not self._is_systemd_failure(scope, stderr): # There was an extension error; it either timed out or returned a non-zero exit code. Re-raise the error raise # There was an issue with systemd-run. We need to log it and retry the extension without systemd. process_output = read_output(stdout, stderr) # Reset the stdout and stderr stdout.truncate(0) stderr.truncate(0) if isinstance(e, ExtensionOperationError): # no-member: Instance of 'ExtensionError' has no 'exit_code' member (no-member) - Disabled: e is actually an ExtensionOperationError err_msg = 'Systemd process exited with code %s and output %s' % ( e.exit_code, process_output) # pylint: disable=no-member else: err_msg = "Systemd timed-out, output: %s" % process_output raise SystemdRunError(err_msg) finally: with self._systemd_run_commands_lock: self._systemd_run_commands.remove(process.pid) def cleanup_legacy_cgroups(self): """ Previous versions of the daemon (2.2.31-2.2.40) wrote their PID to /sys/fs/cgroup/{cpu,memory}/WALinuxAgent/WALinuxAgent; starting from version 2.2.41 we track the agent service in walinuxagent.service instead of WALinuxAgent/WALinuxAgent. If we find that any of the legacy groups include the PID of the daemon then we need to disable data collection for this instance (under systemd, moving PIDs across the cgroup file system can produce unpredictable results) """ return CGroupsApi._foreach_legacy_cgroup(lambda *_: None) WALinuxAgent-2.9.1.1/azurelinuxagent/common/cgroupconfigurator.py000066400000000000000000001601471446033677600252320ustar00rootroot00000000000000# -*- encoding: utf-8 -*- # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ import glob import json import os import re import subprocess import threading from azurelinuxagent.common import conf from azurelinuxagent.common import logger from azurelinuxagent.common.cgroup import CpuCgroup, AGENT_NAME_TELEMETRY, MetricsCounter, MemoryCgroup from azurelinuxagent.common.cgroupapi import CGroupsApi, SystemdCgroupsApi, SystemdRunError, EXTENSION_SLICE_PREFIX from azurelinuxagent.common.cgroupstelemetry import CGroupsTelemetry from azurelinuxagent.common.exception import ExtensionErrorCodes, CGroupsException, AgentMemoryExceededException from azurelinuxagent.common.future import ustr from azurelinuxagent.common.osutil import get_osutil, systemd from azurelinuxagent.common.version import get_distro from azurelinuxagent.common.utils import shellutil, fileutil from azurelinuxagent.common.utils.extensionprocessutil import handle_process_completion from azurelinuxagent.common.event import add_event, WALAEventOperation AZURE_SLICE = "azure.slice" _AZURE_SLICE_CONTENTS = """ [Unit] Description=Slice for Azure VM Agent and Extensions DefaultDependencies=no Before=slices.target """ _VMEXTENSIONS_SLICE = EXTENSION_SLICE_PREFIX + ".slice" _AZURE_VMEXTENSIONS_SLICE = AZURE_SLICE + "/" + _VMEXTENSIONS_SLICE _VMEXTENSIONS_SLICE_CONTENTS = """ [Unit] Description=Slice for Azure VM Extensions DefaultDependencies=no Before=slices.target [Slice] CPUAccounting=yes MemoryAccounting=yes """ _EXTENSION_SLICE_CONTENTS = """ [Unit] Description=Slice for Azure VM extension {extension_name} DefaultDependencies=no Before=slices.target [Slice] CPUAccounting=yes CPUQuota={cpu_quota} MemoryAccounting=yes """ LOGCOLLECTOR_SLICE = "azure-walinuxagent-logcollector.slice" # More info on resource limits properties in systemd here: # https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/resource_management_guide/sec-modifying_control_groups _LOGCOLLECTOR_SLICE_CONTENTS_FMT = """ [Unit] Description=Slice for Azure VM Agent Periodic Log Collector DefaultDependencies=no Before=slices.target [Slice] CPUAccounting=yes CPUQuota={cpu_quota} MemoryAccounting=yes """ _LOGCOLLECTOR_CPU_QUOTA = "5%" LOGCOLLECTOR_MEMORY_LIMIT = 30 * 1024 ** 2 # 30Mb _AGENT_DROP_IN_FILE_SLICE = "10-Slice.conf" _AGENT_DROP_IN_FILE_SLICE_CONTENTS = """ # This drop-in unit file was created by the Azure VM Agent. # Do not edit. [Service] Slice=azure.slice """ _DROP_IN_FILE_CPU_ACCOUNTING = "11-CPUAccounting.conf" _DROP_IN_FILE_CPU_ACCOUNTING_CONTENTS = """ # This drop-in unit file was created by the Azure VM Agent. # Do not edit. [Service] CPUAccounting=yes """ _DROP_IN_FILE_CPU_QUOTA = "12-CPUQuota.conf" _DROP_IN_FILE_CPU_QUOTA_CONTENTS_FORMAT = """ # This drop-in unit file was created by the Azure VM Agent. # Do not edit. [Service] CPUQuota={0} """ _DROP_IN_FILE_MEMORY_ACCOUNTING = "13-MemoryAccounting.conf" _DROP_IN_FILE_MEMORY_ACCOUNTING_CONTENTS = """ # This drop-in unit file was created by the Azure VM Agent. # Do not edit. [Service] MemoryAccounting=yes """ class DisableCgroups(object): ALL = "all" AGENT = "agent" EXTENSIONS = "extensions" def _log_cgroup_info(format_string, *args): message = format_string.format(*args) logger.info("[CGI] " + message) add_event(op=WALAEventOperation.CGroupsInfo, message=message) def _log_cgroup_warning(format_string, *args): message = format_string.format(*args) logger.info("[CGW] " + message) # log as INFO for now, in the future it should be logged as WARNING add_event(op=WALAEventOperation.CGroupsInfo, message=message, is_success=False, log_event=False) class CGroupConfigurator(object): """ This class implements the high-level operations on CGroups (e.g. initialization, creation, etc) NOTE: with the exception of start_extension_command, none of the methods in this class raise exceptions (cgroup operations should not block extensions) """ class _Impl(object): def __init__(self): self._initialized = False self._cgroups_supported = False self._agent_cgroups_enabled = False self._extensions_cgroups_enabled = False self._cgroups_api = None self._agent_cpu_cgroup_path = None self._agent_memory_cgroup_path = None self._agent_memory_cgroup = None self._check_cgroups_lock = threading.RLock() # Protect the check_cgroups which is called from Monitor thread and main loop. def initialize(self): try: if self._initialized: return # This check is to reset the quotas if agent goes from cgroup supported to unsupported distros later in time. if not CGroupsApi.cgroups_supported(): agent_drop_in_path = systemd.get_agent_drop_in_path() try: if os.path.exists(agent_drop_in_path) and os.path.isdir(agent_drop_in_path): files_to_cleanup = [] agent_drop_in_file_slice = os.path.join(agent_drop_in_path, _AGENT_DROP_IN_FILE_SLICE) agent_drop_in_file_cpu_accounting = os.path.join(agent_drop_in_path, _DROP_IN_FILE_CPU_ACCOUNTING) agent_drop_in_file_memory_accounting = os.path.join(agent_drop_in_path, _DROP_IN_FILE_MEMORY_ACCOUNTING) agent_drop_in_file_cpu_quota = os.path.join(agent_drop_in_path, _DROP_IN_FILE_CPU_QUOTA) files_to_cleanup.extend([agent_drop_in_file_slice, agent_drop_in_file_cpu_accounting, agent_drop_in_file_memory_accounting, agent_drop_in_file_cpu_quota]) self.__cleanup_all_files(files_to_cleanup) self.__reload_systemd_config() logger.info("Agent reset the quotas if distro: {0} goes from supported to unsupported list", get_distro()) except Exception as err: logger.warn("Unable to delete Agent drop-in files while resetting the quotas: {0}".format(err)) # check whether cgroup monitoring is supported on the current distro self._cgroups_supported = CGroupsApi.cgroups_supported() if not self._cgroups_supported: logger.info("Cgroup monitoring is not supported on {0}", get_distro()) return # check that systemd is detected correctly self._cgroups_api = SystemdCgroupsApi() if not systemd.is_systemd(): _log_cgroup_warning("systemd was not detected on {0}", get_distro()) return _log_cgroup_info("systemd version: {0}", systemd.get_version()) # This is temporarily disabled while we analyze telemetry. Likely it will be removed. # self.__collect_azure_unit_telemetry() # self.__collect_agent_unit_files_telemetry() if not self.__check_no_legacy_cgroups(): return agent_unit_name = systemd.get_agent_unit_name() agent_slice = systemd.get_unit_property(agent_unit_name, "Slice") if agent_slice not in (AZURE_SLICE, "system.slice"): _log_cgroup_warning("The agent is within an unexpected slice: {0}", agent_slice) return self.__setup_azure_slice() cpu_controller_root, memory_controller_root = self.__get_cgroup_controllers() self._agent_cpu_cgroup_path, self._agent_memory_cgroup_path = self.__get_agent_cgroups(agent_slice, cpu_controller_root, memory_controller_root) if self._agent_cpu_cgroup_path is not None or self._agent_memory_cgroup_path is not None: self.enable() if self._agent_cpu_cgroup_path is not None: _log_cgroup_info("Agent CPU cgroup: {0}", self._agent_cpu_cgroup_path) self.__set_cpu_quota(conf.get_agent_cpu_quota()) CGroupsTelemetry.track_cgroup(CpuCgroup(AGENT_NAME_TELEMETRY, self._agent_cpu_cgroup_path)) if self._agent_memory_cgroup_path is not None: _log_cgroup_info("Agent Memory cgroup: {0}", self._agent_memory_cgroup_path) self._agent_memory_cgroup = MemoryCgroup(AGENT_NAME_TELEMETRY, self._agent_memory_cgroup_path) CGroupsTelemetry.track_cgroup(self._agent_memory_cgroup) _log_cgroup_info('Agent cgroups enabled: {0}', self._agent_cgroups_enabled) except Exception as exception: _log_cgroup_warning("Error initializing cgroups: {0}", ustr(exception)) finally: self._initialized = True @staticmethod def __collect_azure_unit_telemetry(): azure_units = [] try: units = shellutil.run_command(['systemctl', 'list-units', 'azure*', '-all']) for line in units.split('\n'): match = re.match(r'\s?(azure[^\s]*)\s?', line, re.IGNORECASE) if match is not None: azure_units.append((match.group(1), line)) except shellutil.CommandError as command_error: _log_cgroup_warning("Failed to list systemd units: {0}", ustr(command_error)) for unit_name, unit_description in azure_units: unit_slice = "Unknown" try: unit_slice = systemd.get_unit_property(unit_name, "Slice") except Exception as exception: _log_cgroup_warning("Failed to query Slice for {0}: {1}", unit_name, ustr(exception)) _log_cgroup_info("Found an Azure unit under slice {0}: {1}", unit_slice, unit_description) if len(azure_units) == 0: try: cgroups = shellutil.run_command('systemd-cgls') for line in cgroups.split('\n'): if re.match(r'[^\x00-\xff]+azure\.slice\s*', line, re.UNICODE): logger.info(ustr("Found a cgroup for azure.slice\n{0}").format(cgroups)) # Don't add the output of systemd-cgls to the telemetry, since currently it does not support Unicode add_event(op=WALAEventOperation.CGroupsInfo, message="Found a cgroup for azure.slice") except shellutil.CommandError as command_error: _log_cgroup_warning("Failed to list systemd units: {0}", ustr(command_error)) @staticmethod def __collect_agent_unit_files_telemetry(): agent_unit_files = [] agent_service_name = get_osutil().get_service_name() try: fragment_path = systemd.get_unit_property(agent_service_name, "FragmentPath") if fragment_path != systemd.get_agent_unit_file(): agent_unit_files.append(fragment_path) except Exception as exception: _log_cgroup_warning("Failed to query the agent's FragmentPath: {0}", ustr(exception)) try: drop_in_paths = systemd.get_unit_property(agent_service_name, "DropInPaths") for path in drop_in_paths.split(): agent_unit_files.append(path) except Exception as exception: _log_cgroup_warning("Failed to query the agent's DropInPaths: {0}", ustr(exception)) for unit_file in agent_unit_files: try: with open(unit_file, "r") as file_object: _log_cgroup_info("Found a custom unit file for the agent: {0}\n{1}", unit_file, file_object.read()) except Exception as exception: _log_cgroup_warning("Can't read {0}: {1}", unit_file, ustr(exception)) def __check_no_legacy_cgroups(self): """ Older versions of the daemon (2.2.31-2.2.40) wrote their PID to /sys/fs/cgroup/{cpu,memory}/WALinuxAgent/WALinuxAgent. When running under systemd this could produce invalid resource usage data. Cgroups should not be enabled under this condition. """ legacy_cgroups = self._cgroups_api.cleanup_legacy_cgroups() if legacy_cgroups > 0: _log_cgroup_warning("The daemon's PID was added to a legacy cgroup; will not monitor resource usage.") return False return True def __get_cgroup_controllers(self): # # check v1 controllers # cpu_controller_root, memory_controller_root = self._cgroups_api.get_cgroup_mount_points() if cpu_controller_root is not None: logger.info("The CPU cgroup controller is mounted at {0}", cpu_controller_root) else: _log_cgroup_warning("The CPU cgroup controller is not mounted") if memory_controller_root is not None: logger.info("The memory cgroup controller is mounted at {0}", memory_controller_root) else: _log_cgroup_warning("The memory cgroup controller is not mounted") # # check v2 controllers # cgroup2_mount_point, cgroup2_controllers = self._cgroups_api.get_cgroup2_controllers() if cgroup2_mount_point is not None: _log_cgroup_info("cgroups v2 mounted at {0}. Controllers: [{1}]", cgroup2_mount_point, cgroup2_controllers) return cpu_controller_root, memory_controller_root @staticmethod def __setup_azure_slice(): """ The agent creates "azure.slice" for use by extensions and the agent. The agent runs under "azure.slice" directly and each extension runs under its own slice ("Microsoft.CPlat.Extension.slice" in the example below). All the slices for extensions are grouped under "vmextensions.slice". Example: -.slice ├─user.slice ├─system.slice └─azure.slice ├─walinuxagent.service │ ├─5759 /usr/bin/python3 -u /usr/sbin/waagent -daemon │ └─5764 python3 -u bin/WALinuxAgent-2.2.53-py2.7.egg -run-exthandlers └─azure-vmextensions.slice └─Microsoft.CPlat.Extension.slice └─5894 /usr/bin/python3 /var/lib/waagent/Microsoft.CPlat.Extension-1.0.0.0/enable.py This method ensures that the "azure" and "vmextensions" slices are created. Setup should create those slices under /lib/systemd/system; but if they do not exist, __ensure_azure_slices_exist will create them. It also creates drop-in files to set the agent's Slice and CPUAccounting if they have not been set up in the agent's unit file. Lastly, the method also cleans up unit files left over from previous versions of the agent. """ # Older agents used to create this slice, but it was never used. Cleanup the file. CGroupConfigurator._Impl.__cleanup_unit_file("/etc/systemd/system/system-walinuxagent.extensions.slice") unit_file_install_path = systemd.get_unit_file_install_path() azure_slice = os.path.join(unit_file_install_path, AZURE_SLICE) vmextensions_slice = os.path.join(unit_file_install_path, _VMEXTENSIONS_SLICE) logcollector_slice = os.path.join(unit_file_install_path, LOGCOLLECTOR_SLICE) agent_unit_file = systemd.get_agent_unit_file() agent_drop_in_path = systemd.get_agent_drop_in_path() agent_drop_in_file_slice = os.path.join(agent_drop_in_path, _AGENT_DROP_IN_FILE_SLICE) agent_drop_in_file_cpu_accounting = os.path.join(agent_drop_in_path, _DROP_IN_FILE_CPU_ACCOUNTING) agent_drop_in_file_memory_accounting = os.path.join(agent_drop_in_path, _DROP_IN_FILE_MEMORY_ACCOUNTING) files_to_create = [] if not os.path.exists(azure_slice): files_to_create.append((azure_slice, _AZURE_SLICE_CONTENTS)) if not os.path.exists(vmextensions_slice): files_to_create.append((vmextensions_slice, _VMEXTENSIONS_SLICE_CONTENTS)) # Update log collector slice contents slice_contents = _LOGCOLLECTOR_SLICE_CONTENTS_FMT.format(cpu_quota=_LOGCOLLECTOR_CPU_QUOTA) files_to_create.append((logcollector_slice, slice_contents)) if fileutil.findre_in_file(agent_unit_file, r"Slice=") is not None: CGroupConfigurator._Impl.__cleanup_unit_file(agent_drop_in_file_slice) else: if not os.path.exists(agent_drop_in_file_slice): files_to_create.append((agent_drop_in_file_slice, _AGENT_DROP_IN_FILE_SLICE_CONTENTS)) if fileutil.findre_in_file(agent_unit_file, r"CPUAccounting=") is not None: CGroupConfigurator._Impl.__cleanup_unit_file(agent_drop_in_file_cpu_accounting) else: if not os.path.exists(agent_drop_in_file_cpu_accounting): files_to_create.append((agent_drop_in_file_cpu_accounting, _DROP_IN_FILE_CPU_ACCOUNTING_CONTENTS)) if fileutil.findre_in_file(agent_unit_file, r"MemoryAccounting=") is not None: CGroupConfigurator._Impl.__cleanup_unit_file(agent_drop_in_file_memory_accounting) else: if not os.path.exists(agent_drop_in_file_memory_accounting): files_to_create.append( (agent_drop_in_file_memory_accounting, _DROP_IN_FILE_MEMORY_ACCOUNTING_CONTENTS)) if len(files_to_create) > 0: # create the unit files, but if 1 fails remove all and return try: for path, contents in files_to_create: CGroupConfigurator._Impl.__create_unit_file(path, contents) except Exception as exception: _log_cgroup_warning("Failed to create unit files for the azure slice: {0}", ustr(exception)) for unit_file in files_to_create: CGroupConfigurator._Impl.__cleanup_unit_file(unit_file) return CGroupConfigurator._Impl.__reload_systemd_config() @staticmethod def __reload_systemd_config(): # reload the systemd configuration; the new slices will be used once the agent's service restarts try: logger.info("Executing systemctl daemon-reload...") shellutil.run_command(["systemctl", "daemon-reload"]) except Exception as exception: _log_cgroup_warning("daemon-reload failed (create azure slice): {0}", ustr(exception)) @staticmethod def __create_unit_file(path, contents): parent, _ = os.path.split(path) if not os.path.exists(parent): fileutil.mkdir(parent, mode=0o755) exists = os.path.exists(path) fileutil.write_file(path, contents) _log_cgroup_info("{0} {1}", "Updated" if exists else "Created", path) @staticmethod def __cleanup_unit_file(path): if os.path.exists(path): try: os.remove(path) _log_cgroup_info("Removed {0}", path) except Exception as exception: _log_cgroup_warning("Failed to remove {0}: {1}", path, ustr(exception)) @staticmethod def __cleanup_all_files(files_to_cleanup): for path in files_to_cleanup: if os.path.exists(path): try: os.remove(path) _log_cgroup_info("Removed {0}", path) except Exception as exception: _log_cgroup_warning("Failed to remove {0}: {1}", path, ustr(exception)) @staticmethod def __create_all_files(files_to_create): # create the unit files, but if 1 fails remove all and return try: for path, contents in files_to_create: CGroupConfigurator._Impl.__create_unit_file(path, contents) except Exception as exception: _log_cgroup_warning("Failed to create unit files : {0}", ustr(exception)) for unit_file in files_to_create: CGroupConfigurator._Impl.__cleanup_unit_file(unit_file) return def is_extension_resource_limits_setup_completed(self, extension_name, cpu_quota=None): unit_file_install_path = systemd.get_unit_file_install_path() old_extension_slice_path = os.path.join(unit_file_install_path, SystemdCgroupsApi.get_extension_slice_name(extension_name, old_slice=True)) # clean up the old slice from the disk if os.path.exists(old_extension_slice_path): CGroupConfigurator._Impl.__cleanup_unit_file(old_extension_slice_path) extension_slice_path = os.path.join(unit_file_install_path, SystemdCgroupsApi.get_extension_slice_name(extension_name)) cpu_quota = str( cpu_quota) + "%" if cpu_quota is not None else "" # setting an empty value resets to the default (infinity) slice_contents = _EXTENSION_SLICE_CONTENTS.format(extension_name=extension_name, cpu_quota=cpu_quota) if os.path.exists(extension_slice_path): with open(extension_slice_path, "r") as file_: if file_.read() == slice_contents: return True return False def __get_agent_cgroups(self, agent_slice, cpu_controller_root, memory_controller_root): agent_unit_name = systemd.get_agent_unit_name() expected_relative_path = os.path.join(agent_slice, agent_unit_name) cpu_cgroup_relative_path, memory_cgroup_relative_path = self._cgroups_api.get_process_cgroup_relative_paths( "self") if cpu_cgroup_relative_path is None: _log_cgroup_warning("The agent's process is not within a CPU cgroup") else: if cpu_cgroup_relative_path == expected_relative_path: _log_cgroup_info('CPUAccounting: {0}', systemd.get_unit_property(agent_unit_name, "CPUAccounting")) _log_cgroup_info('CPUQuota: {0}', systemd.get_unit_property(agent_unit_name, "CPUQuotaPerSecUSec")) else: _log_cgroup_warning( "The Agent is not in the expected CPU cgroup; will not enable monitoring. Cgroup:[{0}] Expected:[{1}]", cpu_cgroup_relative_path, expected_relative_path) cpu_cgroup_relative_path = None # Set the path to None to prevent monitoring if memory_cgroup_relative_path is None: _log_cgroup_warning("The agent's process is not within a memory cgroup") else: if memory_cgroup_relative_path == expected_relative_path: memory_accounting = systemd.get_unit_property(agent_unit_name, "MemoryAccounting") _log_cgroup_info('MemoryAccounting: {0}', memory_accounting) else: _log_cgroup_info( "The Agent is not in the expected memory cgroup; will not enable monitoring. CGroup:[{0}] Expected:[{1}]", memory_cgroup_relative_path, expected_relative_path) memory_cgroup_relative_path = None # Set the path to None to prevent monitoring if cpu_controller_root is not None and cpu_cgroup_relative_path is not None: agent_cpu_cgroup_path = os.path.join(cpu_controller_root, cpu_cgroup_relative_path) else: agent_cpu_cgroup_path = None if memory_controller_root is not None and memory_cgroup_relative_path is not None: agent_memory_cgroup_path = os.path.join(memory_controller_root, memory_cgroup_relative_path) else: agent_memory_cgroup_path = None return agent_cpu_cgroup_path, agent_memory_cgroup_path def supported(self): return self._cgroups_supported def enabled(self): return self._agent_cgroups_enabled or self._extensions_cgroups_enabled def agent_enabled(self): return self._agent_cgroups_enabled def extensions_enabled(self): return self._extensions_cgroups_enabled def enable(self): if not self.supported(): raise CGroupsException( "Attempted to enable cgroups, but they are not supported on the current platform") self._agent_cgroups_enabled = True self._extensions_cgroups_enabled = True def disable(self, reason, disable_cgroups): if disable_cgroups == DisableCgroups.ALL: # disable all # Reset quotas self.__reset_agent_cpu_quota() extension_services = self.get_extension_services_list() for extension in extension_services: logger.info("Resetting extension : {0} and it's services: {1} CPUQuota".format(extension, extension_services[extension])) self.__reset_extension_cpu_quota(extension_name=extension) self.__reset_extension_services_cpu_quota(extension_services[extension]) self.__reload_systemd_config() CGroupsTelemetry.reset() self._agent_cgroups_enabled = False self._extensions_cgroups_enabled = False elif disable_cgroups == DisableCgroups.AGENT: # disable agent self._agent_cgroups_enabled = False self.__reset_agent_cpu_quota() CGroupsTelemetry.stop_tracking(CpuCgroup(AGENT_NAME_TELEMETRY, self._agent_cpu_cgroup_path)) message = "[CGW] Disabling resource usage monitoring. Reason: {0}".format(reason) logger.info(message) # log as INFO for now, in the future it should be logged as WARNING add_event(op=WALAEventOperation.CGroupsDisabled, message=message, is_success=False, log_event=False) @staticmethod def __set_cpu_quota(quota): """ Sets the agent's CPU quota to the given percentage (100% == 1 CPU) NOTE: This is done using a dropin file in the default dropin directory; any local overrides on the VM will take precedence over this setting. """ quota_percentage = "{0}%".format(quota) _log_cgroup_info("Ensuring the agent's CPUQuota is {0}", quota_percentage) if CGroupConfigurator._Impl.__try_set_cpu_quota(quota_percentage): CGroupsTelemetry.set_track_throttled_time(True) @staticmethod def __reset_agent_cpu_quota(): """ Removes any CPUQuota on the agent NOTE: This resets the quota on the agent's default dropin file; any local overrides on the VM will take precedence over this setting. """ logger.info("Resetting agent's CPUQuota") if CGroupConfigurator._Impl.__try_set_cpu_quota(''): # setting an empty value resets to the default (infinity) _log_cgroup_info('CPUQuota: {0}', systemd.get_unit_property(systemd.get_agent_unit_name(), "CPUQuotaPerSecUSec")) @staticmethod def __try_set_cpu_quota(quota): try: drop_in_file = os.path.join(systemd.get_agent_drop_in_path(), _DROP_IN_FILE_CPU_QUOTA) contents = _DROP_IN_FILE_CPU_QUOTA_CONTENTS_FORMAT.format(quota) if os.path.exists(drop_in_file): with open(drop_in_file, "r") as file_: if file_.read() == contents: return True # no need to update the file; return here to avoid doing a daemon-reload CGroupConfigurator._Impl.__create_unit_file(drop_in_file, contents) except Exception as exception: _log_cgroup_warning('Failed to set CPUQuota: {0}', ustr(exception)) return False try: logger.info("Executing systemctl daemon-reload...") shellutil.run_command(["systemctl", "daemon-reload"]) except Exception as exception: _log_cgroup_warning("daemon-reload failed (set quota): {0}", ustr(exception)) return False return True def check_cgroups(self, cgroup_metrics): self._check_cgroups_lock.acquire() try: if not self.enabled(): return errors = [] process_check_success = False try: self._check_processes_in_agent_cgroup() process_check_success = True except CGroupsException as exception: errors.append(exception) quota_check_success = False try: if cgroup_metrics: self._check_agent_throttled_time(cgroup_metrics) quota_check_success = True except CGroupsException as exception: errors.append(exception) reason = "Check on cgroups failed:\n{0}".format("\n".join([ustr(e) for e in errors])) if not process_check_success and conf.get_cgroup_disable_on_process_check_failure(): self.disable(reason, DisableCgroups.ALL) if not quota_check_success and conf.get_cgroup_disable_on_quota_check_failure(): self.disable(reason, DisableCgroups.AGENT) finally: self._check_cgroups_lock.release() def _check_processes_in_agent_cgroup(self): """ Verifies that the agent's cgroup includes only the current process, its parent, commands started using shellutil and instances of systemd-run (those processes correspond, respectively, to the extension handler, the daemon, commands started by the extension handler, and the systemd-run commands used to start extensions on their own cgroup). Other processes started by the agent (e.g. extensions) and processes not started by the agent (e.g. services installed by extensions) are reported as unexpected, since they should belong to their own cgroup. Raises a CGroupsException if the check fails """ unexpected = [] agent_cgroup_proc_names = [] try: daemon = os.getppid() extension_handler = os.getpid() agent_commands = set() agent_commands.update(shellutil.get_running_commands()) systemd_run_commands = set() systemd_run_commands.update(self._cgroups_api.get_systemd_run_commands()) agent_cgroup = CGroupsApi.get_processes_in_cgroup(self._agent_cpu_cgroup_path) # get the running commands again in case new commands started or completed while we were fetching the processes in the cgroup; agent_commands.update(shellutil.get_running_commands()) systemd_run_commands.update(self._cgroups_api.get_systemd_run_commands()) for process in agent_cgroup: agent_cgroup_proc_names.append(self.__format_process(process)) # Note that the agent uses systemd-run to start extensions; systemd-run belongs to the agent cgroup, though the extensions don't. if process in (daemon, extension_handler) or process in systemd_run_commands: continue # check shell systemd_run process if above process check didn't catch it if self._check_systemd_run_process(process): continue # systemd_run_commands contains the shell that started systemd-run, so we also need to check for the parent if self._get_parent(process) in systemd_run_commands and self._get_command( process) == 'systemd-run': continue # check if the process is a command started by the agent or a descendant of one of those commands current = process while current != 0 and current not in agent_commands: current = self._get_parent(current) # Verify if Process started by agent based on the marker found in process environment or process is in Zombie state. # If so, consider it as valid process in agent cgroup. if current == 0 and not (self.__is_process_descendant_of_the_agent(process) or self.__is_zombie_process(process)): unexpected.append(self.__format_process(process)) if len(unexpected) >= 5: # collect just a small sample break except Exception as exception: _log_cgroup_warning("Error checking the processes in the agent's cgroup: {0}".format(ustr(exception))) if len(unexpected) > 0: self._report_agent_cgroups_procs(agent_cgroup_proc_names, unexpected) raise CGroupsException("The agent's cgroup includes unexpected processes: {0}".format(unexpected)) @staticmethod def _get_command(pid): try: with open('/proc/{0}/comm'.format(pid), "r") as file_: comm = file_.read() if comm and comm[-1] == '\x00': # if null-terminated, remove the null comm = comm[:-1] return comm.rstrip() except Exception: return "UNKNOWN" @staticmethod def __format_process(pid): """ Formats the given PID as a string containing the PID and the corresponding command line truncated to 64 chars """ try: cmdline = '/proc/{0}/cmdline'.format(pid) if os.path.exists(cmdline): with open(cmdline, "r") as cmdline_file: return "[PID: {0}] {1:64.64}".format(pid, cmdline_file.read()) except Exception: pass return "[PID: {0}] UNKNOWN".format(pid) @staticmethod def __is_process_descendant_of_the_agent(pid): """ Returns True if the process is descendant of the agent by looking at the env flag(AZURE_GUEST_AGENT_PARENT_PROCESS_NAME) that we set when the process starts otherwise False. """ try: env = '/proc/{0}/environ'.format(pid) if os.path.exists(env): with open(env, "r") as env_file: environ = env_file.read() if environ and environ[-1] == '\x00': environ = environ[:-1] return "{0}={1}".format(shellutil.PARENT_PROCESS_NAME, shellutil.AZURE_GUEST_AGENT) in environ except Exception: pass return False @staticmethod def __is_zombie_process(pid): """ Returns True if process is in Zombie state otherwise False. Ex: cat /proc/18171/stat 18171 (python3) S 18103 18103 18103 0 -1 4194624 57736 64902 0 3 """ try: stat = '/proc/{0}/stat'.format(pid) if os.path.exists(stat): with open(stat, "r") as stat_file: return stat_file.read().split()[2] == 'Z' except Exception: pass return False @staticmethod def _check_systemd_run_process(process): """ Returns True if process is shell systemd-run process started by agent otherwise False. Ex: sh,7345 -c systemd-run --unit=enable_7c5cab19-eb79-4661-95d9-9e5091bd5ae0 --scope --slice=azure-vmextensions-Microsoft.OSTCExtensions.VMAccessForLinux_1.5.11.slice /var/lib/waagent/Microsoft.OSTCExtensions.VMAccessForLinux-1.5.11/processes.sh """ try: process_name = "UNKNOWN" cmdline = '/proc/{0}/cmdline'.format(process) if os.path.exists(cmdline): with open(cmdline, "r") as cmdline_file: process_name = "{0}".format(cmdline_file.read()) match = re.search(r'systemd-run.*--unit=.*--scope.*--slice=azure-vmextensions.*', process_name) if match is not None: return True except Exception: pass return False @staticmethod def _report_agent_cgroups_procs(agent_cgroup_proc_names, unexpected): for proc_name in unexpected: if 'UNKNOWN' in proc_name: msg = "Agent includes following processes when UNKNOWN process found: {0}".format("\n".join([ustr(proc) for proc in agent_cgroup_proc_names])) add_event(op=WALAEventOperation.CGroupsInfo, message=msg) @staticmethod def _check_agent_throttled_time(cgroup_metrics): for metric in cgroup_metrics: if metric.instance == AGENT_NAME_TELEMETRY and metric.counter == MetricsCounter.THROTTLED_TIME: if metric.value > conf.get_agent_cpu_throttled_time_threshold(): raise CGroupsException("The agent has been throttled for {0} seconds".format(metric.value)) def check_agent_memory_usage(self): if self.enabled() and self._agent_memory_cgroup: metrics = self._agent_memory_cgroup.get_tracked_metrics() current_usage = 0 for metric in metrics: if metric.counter == MetricsCounter.TOTAL_MEM_USAGE: current_usage += metric.value elif metric.counter == MetricsCounter.SWAP_MEM_USAGE: current_usage += metric.value if current_usage > conf.get_agent_memory_quota(): raise AgentMemoryExceededException("The agent memory limit {0} bytes exceeded. The current reported usage is {1} bytes.".format(conf.get_agent_memory_quota(), current_usage)) @staticmethod def _get_parent(pid): """ Returns the parent of the given process. If the parent cannot be determined returns 0 (which is the PID for the scheduler) """ try: stat = '/proc/{0}/stat'.format(pid) if os.path.exists(stat): with open(stat, "r") as stat_file: return int(stat_file.read().split()[3]) except Exception: pass return 0 def start_tracking_unit_cgroups(self, unit_name): """ TODO: Start tracking Memory Cgroups """ try: cpu_cgroup_path, memory_cgroup_path = self._cgroups_api.get_unit_cgroup_paths(unit_name) if cpu_cgroup_path is None: logger.info("The CPU controller is not mounted; will not track resource usage") else: CGroupsTelemetry.track_cgroup(CpuCgroup(unit_name, cpu_cgroup_path)) if memory_cgroup_path is None: logger.info("The Memory controller is not mounted; will not track resource usage") else: CGroupsTelemetry.track_cgroup(MemoryCgroup(unit_name, memory_cgroup_path)) except Exception as exception: logger.info("Failed to start tracking resource usage for the extension: {0}", ustr(exception)) def stop_tracking_unit_cgroups(self, unit_name): """ TODO: remove Memory cgroups from tracked list. """ try: cpu_cgroup_path, memory_cgroup_path = self._cgroups_api.get_unit_cgroup_paths(unit_name) if cpu_cgroup_path is not None: CGroupsTelemetry.stop_tracking(CpuCgroup(unit_name, cpu_cgroup_path)) if memory_cgroup_path is not None: CGroupsTelemetry.stop_tracking(MemoryCgroup(unit_name, memory_cgroup_path)) except Exception as exception: logger.info("Failed to stop tracking resource usage for the extension service: {0}", ustr(exception)) def stop_tracking_extension_cgroups(self, extension_name): """ TODO: remove extension Memory cgroups from tracked list """ try: extension_slice_name = SystemdCgroupsApi.get_extension_slice_name(extension_name) cgroup_relative_path = os.path.join(_AZURE_VMEXTENSIONS_SLICE, extension_slice_name) cpu_cgroup_mountpoint, memory_cgroup_mountpoint = self._cgroups_api.get_cgroup_mount_points() cpu_cgroup_path = os.path.join(cpu_cgroup_mountpoint, cgroup_relative_path) memory_cgroup_path = os.path.join(memory_cgroup_mountpoint, cgroup_relative_path) if cpu_cgroup_path is not None: CGroupsTelemetry.stop_tracking(CpuCgroup(extension_name, cpu_cgroup_path)) if memory_cgroup_path is not None: CGroupsTelemetry.stop_tracking(MemoryCgroup(extension_name, memory_cgroup_path)) except Exception as exception: logger.info("Failed to stop tracking resource usage for the extension service: {0}", ustr(exception)) def start_extension_command(self, extension_name, command, cmd_name, timeout, shell, cwd, env, stdout, stderr, error_code=ExtensionErrorCodes.PluginUnknownFailure): """ Starts a command (install/enable/etc) for an extension and adds the command's PID to the extension's cgroup :param extension_name: The extension executing the command :param command: The command to invoke :param cmd_name: The type of the command(enable, install, etc.) :param timeout: Number of seconds to wait for command completion :param cwd: The working directory for the command :param env: The environment to pass to the command's process :param stdout: File object to redirect stdout to :param stderr: File object to redirect stderr to :param stderr: File object to redirect stderr to :param error_code: Extension error code to raise in case of error """ if self.enabled(): try: return self._cgroups_api.start_extension_command(extension_name, command, cmd_name, timeout, shell=shell, cwd=cwd, env=env, stdout=stdout, stderr=stderr, error_code=error_code) except SystemdRunError as exception: reason = 'Failed to start {0} using systemd-run, will try invoking the extension directly. Error: {1}'.format( extension_name, ustr(exception)) self.disable(reason, DisableCgroups.ALL) # fall-through and re-invoke the extension # subprocess-popen-preexec-fn Disabled: code is not multi-threaded process = subprocess.Popen(command, shell=shell, cwd=cwd, env=env, stdout=stdout, stderr=stderr, preexec_fn=os.setsid) # pylint: disable=W1509 return handle_process_completion(process=process, command=command, timeout=timeout, stdout=stdout, stderr=stderr, error_code=error_code) def __reset_extension_cpu_quota(self, extension_name): """ Removes any CPUQuota on the extension NOTE: This resets the quota on the extension's slice; any local overrides on the VM will take precedence over this setting. """ if self.enabled(): self.setup_extension_slice(extension_name, cpu_quota=None) def setup_extension_slice(self, extension_name, cpu_quota): """ Each extension runs under its own slice (Ex "Microsoft.CPlat.Extension.slice"). All the slices for extensions are grouped under "azure-vmextensions.slice. This method ensures that the extension slice is created. Setup should create under /lib/systemd/system if it is not exist. TODO: set memory quotas """ if self.enabled(): unit_file_install_path = systemd.get_unit_file_install_path() extension_slice_path = os.path.join(unit_file_install_path, SystemdCgroupsApi.get_extension_slice_name(extension_name)) try: cpu_quota = str(cpu_quota) + "%" if cpu_quota is not None else "" # setting an empty value resets to the default (infinity) if cpu_quota == "": _log_cgroup_info("CPUQuota not set for {0}", extension_name) else: _log_cgroup_info("Ensuring the {0}'s CPUQuota is {1}", extension_name, cpu_quota) slice_contents = _EXTENSION_SLICE_CONTENTS.format(extension_name=extension_name, cpu_quota=cpu_quota) CGroupConfigurator._Impl.__create_unit_file(extension_slice_path, slice_contents) except Exception as exception: _log_cgroup_warning("Failed to set the extension {0} slice and quotas: {1}", extension_name, ustr(exception)) CGroupConfigurator._Impl.__cleanup_unit_file(extension_slice_path) def remove_extension_slice(self, extension_name): """ This method ensures that the extension slice gets removed from /lib/systemd/system if it exist Lastly stop the unit. This would ensure the cleanup the /sys/fs/cgroup controller paths """ if self.enabled(): unit_file_install_path = systemd.get_unit_file_install_path() extension_slice_name = SystemdCgroupsApi.get_extension_slice_name(extension_name) extension_slice_path = os.path.join(unit_file_install_path, extension_slice_name) if os.path.exists(extension_slice_path): self.stop_tracking_extension_cgroups(extension_name) CGroupConfigurator._Impl.__cleanup_unit_file(extension_slice_path) def set_extension_services_cpu_memory_quota(self, services_list): """ Each extension service will have name, systemd path and it's quotas. This method ensures that drop-in files are created under service.d folder if quotas given. ex: /lib/systemd/system/extension.service.d/11-CPUAccounting.conf TODO: set memory quotas """ if self.enabled() and services_list is not None: for service in services_list: service_name = service.get('name', None) unit_file_path = systemd.get_unit_file_install_path() if service_name is not None and unit_file_path is not None: files_to_create = [] drop_in_path = os.path.join(unit_file_path, "{0}.d".format(service_name)) drop_in_file_cpu_accounting = os.path.join(drop_in_path, _DROP_IN_FILE_CPU_ACCOUNTING) files_to_create.append((drop_in_file_cpu_accounting, _DROP_IN_FILE_CPU_ACCOUNTING_CONTENTS)) drop_in_file_memory_accounting = os.path.join(drop_in_path, _DROP_IN_FILE_MEMORY_ACCOUNTING) files_to_create.append( (drop_in_file_memory_accounting, _DROP_IN_FILE_MEMORY_ACCOUNTING_CONTENTS)) cpu_quota = service.get('cpuQuotaPercentage', None) if cpu_quota is not None: cpu_quota = str(cpu_quota) + "%" _log_cgroup_info("Ensuring the {0}'s CPUQuota is {1}", service_name, cpu_quota) drop_in_file_cpu_quota = os.path.join(drop_in_path, _DROP_IN_FILE_CPU_QUOTA) cpu_quota_contents = _DROP_IN_FILE_CPU_QUOTA_CONTENTS_FORMAT.format(cpu_quota) files_to_create.append((drop_in_file_cpu_quota, cpu_quota_contents)) self.__create_all_files(files_to_create) self.__reload_systemd_config() def __reset_extension_services_cpu_quota(self, services_list): """ Removes any CPUQuota on the extension service NOTE: This resets the quota on the extension service's default dropin file; any local overrides on the VM will take precedence over this setting. """ if self.enabled() and services_list is not None: service_name = None try: for service in services_list: service_name = service.get('name', None) unit_file_path = systemd.get_unit_file_install_path() if service_name is not None and unit_file_path is not None: files_to_create = [] drop_in_path = os.path.join(unit_file_path, "{0}.d".format(service_name)) cpu_quota = "" # setting an empty value resets to the default (infinity) drop_in_file_cpu_quota = os.path.join(drop_in_path, _DROP_IN_FILE_CPU_QUOTA) cpu_quota_contents = _DROP_IN_FILE_CPU_QUOTA_CONTENTS_FORMAT.format(cpu_quota) if os.path.exists(drop_in_file_cpu_quota): with open(drop_in_file_cpu_quota, "r") as file_: if file_.read() == cpu_quota_contents: return files_to_create.append((drop_in_file_cpu_quota, cpu_quota_contents)) self.__create_all_files(files_to_create) except Exception as exception: _log_cgroup_warning('Failed to reset CPUQuota for {0} : {1}', service_name, ustr(exception)) def remove_extension_services_drop_in_files(self, services_list): """ Remove the dropin files from service .d folder for the given service """ if services_list is not None: for service in services_list: service_name = service.get('name', None) unit_file_path = systemd.get_unit_file_install_path() if service_name is not None and unit_file_path is not None: files_to_cleanup = [] drop_in_path = os.path.join(unit_file_path, "{0}.d".format(service_name)) drop_in_file_cpu_accounting = os.path.join(drop_in_path, _DROP_IN_FILE_CPU_ACCOUNTING) files_to_cleanup.append(drop_in_file_cpu_accounting) drop_in_file_memory_accounting = os.path.join(drop_in_path, _DROP_IN_FILE_MEMORY_ACCOUNTING) files_to_cleanup.append(drop_in_file_memory_accounting) cpu_quota = service.get('cpuQuotaPercentage', None) if cpu_quota is not None: drop_in_file_cpu_quota = os.path.join(drop_in_path, _DROP_IN_FILE_CPU_QUOTA) files_to_cleanup.append(drop_in_file_cpu_quota) CGroupConfigurator._Impl.__cleanup_all_files(files_to_cleanup) _log_cgroup_info("Drop in files removed for {0}".format(service_name)) def stop_tracking_extension_services_cgroups(self, services_list): """ Remove the cgroup entry from the tracked groups to stop tracking. """ if self.enabled() and services_list is not None: for service in services_list: service_name = service.get('name', None) if service_name is not None: self.stop_tracking_unit_cgroups(service_name) def start_tracking_extension_services_cgroups(self, services_list): """ Add the cgroup entry to start tracking the services cgroups. """ if self.enabled() and services_list is not None: for service in services_list: service_name = service.get('name', None) if service_name is not None: self.start_tracking_unit_cgroups(service_name) @staticmethod def get_extension_services_list(): """ ResourceLimits for extensions are coming from /HandlerManifest.json file. Use this pattern to determine all the installed extension HandlerManifest files and read the extension services if ResourceLimits are present. """ extensions_services = {} for manifest_path in glob.iglob(os.path.join(conf.get_lib_dir(), "*/HandlerManifest.json")): match = re.search("(?P[\\w+\\.-]+).HandlerManifest\\.json", manifest_path) if match is not None: extensions_name = match.group('extname') if not extensions_name.startswith('WALinuxAgent'): try: data = json.loads(fileutil.read_file(manifest_path)) resource_limits = data[0].get('resourceLimits', None) services = resource_limits.get('services') if resource_limits else None extensions_services[extensions_name] = services except (IOError, OSError) as e: _log_cgroup_warning( 'Failed to load manifest file ({0}): {1}'.format(manifest_path, e.strerror)) except ValueError: _log_cgroup_warning('Malformed manifest file ({0}).'.format(manifest_path)) return extensions_services # unique instance for the singleton _instance = None @staticmethod def get_instance(): if CGroupConfigurator._instance is None: CGroupConfigurator._instance = CGroupConfigurator._Impl() return CGroupConfigurator._instance WALinuxAgent-2.9.1.1/azurelinuxagent/common/cgroupstelemetry.py000066400000000000000000000075241446033677600247240ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ import errno import threading from azurelinuxagent.common import logger from azurelinuxagent.common.cgroup import CpuCgroup from azurelinuxagent.common.future import ustr class CGroupsTelemetry(object): """ """ _tracked = {} _track_throttled_time = False _rlock = threading.RLock() @staticmethod def set_track_throttled_time(value): CGroupsTelemetry._track_throttled_time = value @staticmethod def get_track_throttled_time(): return CGroupsTelemetry._track_throttled_time @staticmethod def track_cgroup(cgroup): """ Adds the given item to the dictionary of tracked cgroups """ if isinstance(cgroup, CpuCgroup): # set the current cpu usage cgroup.initialize_cpu_usage() with CGroupsTelemetry._rlock: if not CGroupsTelemetry.is_tracked(cgroup.path): CGroupsTelemetry._tracked[cgroup.path] = cgroup logger.info("Started tracking cgroup {0}", cgroup) @staticmethod def is_tracked(path): """ Returns true if the given item is in the list of tracked items O(1) operation. """ with CGroupsTelemetry._rlock: if path in CGroupsTelemetry._tracked: return True return False @staticmethod def stop_tracking(cgroup): """ Stop tracking the cgroups for the given path """ with CGroupsTelemetry._rlock: if cgroup.path in CGroupsTelemetry._tracked: CGroupsTelemetry._tracked.pop(cgroup.path) logger.info("Stopped tracking cgroup {0}", cgroup) @staticmethod def poll_all_tracked(): metrics = [] inactive_cgroups = [] with CGroupsTelemetry._rlock: for cgroup in CGroupsTelemetry._tracked.values(): try: metrics.extend(cgroup.get_tracked_metrics(track_throttled_time=CGroupsTelemetry._track_throttled_time)) except Exception as e: # There can be scenarios when the CGroup has been deleted by the time we are fetching the values # from it. This would raise IOError with file entry not found (ERRNO: 2). We do not want to log # every occurrences of such case as it would be very verbose. We do want to log all the other # exceptions which could occur, which is why we do a periodic log for all the other errors. if not isinstance(e, (IOError, OSError)) or e.errno != errno.ENOENT: # pylint: disable=E1101 logger.periodic_warn(logger.EVERY_HOUR, '[PERIODIC] Could not collect metrics for cgroup ' '{0}. Error : {1}'.format(cgroup.name, ustr(e))) if not cgroup.is_active(): inactive_cgroups.append(cgroup) for inactive_cgroup in inactive_cgroups: CGroupsTelemetry.stop_tracking(inactive_cgroup) return metrics @staticmethod def reset(): with CGroupsTelemetry._rlock: CGroupsTelemetry._tracked.clear() # emptying the dictionary CGroupsTelemetry._track_throttled_time = False WALinuxAgent-2.9.1.1/azurelinuxagent/common/conf.py000066400000000000000000000470761446033677600222420ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # """ Module conf loads and parses configuration file """ # pylint: disable=W0105 import os import os.path from azurelinuxagent.common.utils.fileutil import read_file #pylint: disable=R0401 from azurelinuxagent.common.exception import AgentConfigError DISABLE_AGENT_FILE = 'disable_agent' class ConfigurationProvider(object): """ Parse and store key:values in /etc/waagent.conf. """ def __init__(self): self.values = dict() def load(self, content): if not content: raise AgentConfigError("Can't not parse empty configuration") for line in content.split('\n'): if not line.startswith("#") and "=" in line: parts = line.split('=', 1) if len(parts) < 2: continue key = parts[0].strip() value = parts[1].split('#')[0].strip("\" ").strip() self.values[key] = value if value != "None" else None @staticmethod def _get_default(default): if hasattr(default, '__call__'): return default() return default def get(self, key, default_value): """ Retrieves a string parameter by key and returns its value. If not found returns the default value, or if the default value is a callable returns the result of invoking the callable. """ val = self.values.get(key) return val if val is not None else self._get_default(default_value) def get_switch(self, key, default_value): """ Retrieves a switch parameter by key and returns its value as a boolean. If not found returns the default value, or if the default value is a callable returns the result of invoking the callable. """ val = self.values.get(key) if val is not None and val.lower() == 'y': return True elif val is not None and val.lower() == 'n': return False return self._get_default(default_value) def get_int(self, key, default_value): """ Retrieves an int parameter by key and returns its value. If not found returns the default value, or if the default value is a callable returns the result of invoking the callable. """ try: return int(self.values.get(key)) except TypeError: return self._get_default(default_value) except ValueError: return self._get_default(default_value) __conf__ = ConfigurationProvider() def load_conf_from_file(conf_file_path, conf=__conf__): """ Load conf file from: conf_file_path """ if os.path.isfile(conf_file_path) == False: raise AgentConfigError(("Missing configuration in {0}" "").format(conf_file_path)) try: content = read_file(conf_file_path) conf.load(content) except IOError as err: raise AgentConfigError(("Failed to load conf file:{0}, {1}" "").format(conf_file_path, err)) __SWITCH_OPTIONS__ = { "OS.AllowHTTP": False, "OS.EnableFirewall": False, "OS.EnableFIPS": False, "OS.EnableRDMA": False, "OS.UpdateRdmaDriver": False, "OS.CheckRdmaDriver": False, "Logs.Verbose": False, "Logs.Console": True, "Logs.Collect": True, "Extensions.Enabled": True, "Provisioning.AllowResetSysUser": False, "Provisioning.RegenerateSshHostKeyPair": False, "Provisioning.DeleteRootPassword": False, "Provisioning.DecodeCustomData": False, "Provisioning.ExecuteCustomData": False, "Provisioning.MonitorHostName": False, "DetectScvmmEnv": False, "ResourceDisk.Format": False, "ResourceDisk.EnableSwap": False, "ResourceDisk.EnableSwapEncryption": False, "AutoUpdate.Enabled": True, "EnableOverProvisioning": True, # # "Debug" options are experimental and may be removed in later # versions of the Agent. # "Debug.CgroupLogMetrics": False, "Debug.CgroupDisableOnProcessCheckFailure": True, "Debug.CgroupDisableOnQuotaCheckFailure": True, "Debug.EnableAgentMemoryUsageCheck": False, "Debug.EnableFastTrack": True, "Debug.EnableGAVersioning": False } __STRING_OPTIONS__ = { "Lib.Dir": "/var/lib/waagent", "DVD.MountPoint": "/mnt/cdrom/secure", "Pid.File": "/var/run/waagent.pid", "Extension.LogDir": "/var/log/azure", "OS.OpensslPath": "/usr/bin/openssl", "OS.SshDir": "/etc/ssh", "OS.HomeDir": "/home", "OS.PasswordPath": "/etc/shadow", "OS.SudoersDir": "/etc/sudoers.d", "OS.RootDeviceScsiTimeout": None, "Provisioning.Agent": "auto", "Provisioning.SshHostKeyPairType": "rsa", "Provisioning.PasswordCryptId": "6", "HttpProxy.Host": None, "ResourceDisk.MountPoint": "/mnt/resource", "ResourceDisk.MountOptions": None, "ResourceDisk.Filesystem": "ext3", "AutoUpdate.GAFamily": "Prod", "Debug.CgroupMonitorExpiryTime": "2022-03-31", "Debug.CgroupMonitorExtensionName": "Microsoft.Azure.Monitor.AzureMonitorLinuxAgent", } __INTEGER_OPTIONS__ = { "Extensions.GoalStatePeriod": 6, "Extensions.InitialGoalStatePeriod": 6, "OS.EnableFirewallPeriod": 300, "OS.RemovePersistentNetRulesPeriod": 30, "OS.RootDeviceScsiTimeoutPeriod": 30, "OS.MonitorDhcpClientRestartPeriod": 30, "OS.SshClientAliveInterval": 180, "Provisioning.MonitorHostNamePeriod": 30, "Provisioning.PasswordCryptSaltLength": 10, "HttpProxy.Port": None, "ResourceDisk.SwapSizeMB": 0, "Autoupdate.Frequency": 3600, "Logs.CollectPeriod": 3600, # # "Debug" options are experimental and may be removed in later # versions of the Agent. # "Debug.CgroupCheckPeriod": 300, "Debug.AgentCpuQuota": 50, "Debug.AgentCpuThrottledTimeThreshold": 120, "Debug.AgentMemoryQuota": 30 * 1024 ** 2, "Debug.EtpCollectionPeriod": 300, "Debug.AutoUpdateHotfixFrequency": 14400, "Debug.AutoUpdateNormalFrequency": 86400, "Debug.FirewallRulesLogPeriod": 86400 } def get_configuration(conf=__conf__): options = {} for option in __SWITCH_OPTIONS__: options[option] = conf.get_switch(option, __SWITCH_OPTIONS__[option]) for option in __STRING_OPTIONS__: options[option] = conf.get(option, __STRING_OPTIONS__[option]) for option in __INTEGER_OPTIONS__: options[option] = conf.get_int(option, __INTEGER_OPTIONS__[option]) return options def get_default_value(option): if option in __STRING_OPTIONS__: return __STRING_OPTIONS__[option] raise ValueError("{0} is not a valid configuration parameter.".format(option)) def get_int_default_value(option): if option in __INTEGER_OPTIONS__: return int(__INTEGER_OPTIONS__[option]) raise ValueError("{0} is not a valid configuration parameter.".format(option)) def get_switch_default_value(option): if option in __SWITCH_OPTIONS__: return __SWITCH_OPTIONS__[option] raise ValueError("{0} is not a valid configuration parameter.".format(option)) def enable_firewall(conf=__conf__): return conf.get_switch("OS.EnableFirewall", False) def get_enable_firewall_period(conf=__conf__): return conf.get_int("OS.EnableFirewallPeriod", 300) def get_remove_persistent_net_rules_period(conf=__conf__): return conf.get_int("OS.RemovePersistentNetRulesPeriod", 30) def get_monitor_dhcp_client_restart_period(conf=__conf__): return conf.get_int("OS.MonitorDhcpClientRestartPeriod", 30) def enable_rdma(conf=__conf__): return conf.get_switch("OS.EnableRDMA", False) or \ conf.get_switch("OS.UpdateRdmaDriver", False) or \ conf.get_switch("OS.CheckRdmaDriver", False) def enable_rdma_update(conf=__conf__): return conf.get_switch("OS.UpdateRdmaDriver", False) def enable_check_rdma_driver(conf=__conf__): return conf.get_switch("OS.CheckRdmaDriver", True) def get_logs_verbose(conf=__conf__): return conf.get_switch("Logs.Verbose", False) def get_logs_console(conf=__conf__): return conf.get_switch("Logs.Console", True) def get_collect_logs(conf=__conf__): return conf.get_switch("Logs.Collect", True) def get_collect_logs_period(conf=__conf__): return conf.get_int("Logs.CollectPeriod", 3600) def get_lib_dir(conf=__conf__): return conf.get("Lib.Dir", "/var/lib/waagent") def get_published_hostname(conf=__conf__): # Some applications rely on this file; do not remove this setting return os.path.join(get_lib_dir(conf), 'published_hostname') def get_dvd_mount_point(conf=__conf__): return conf.get("DVD.MountPoint", "/mnt/cdrom/secure") def get_agent_pid_file_path(conf=__conf__): return conf.get("Pid.File", "/var/run/waagent.pid") def get_ext_log_dir(conf=__conf__): return conf.get("Extension.LogDir", "/var/log/azure") def get_agent_log_file(): return "/var/log/waagent.log" def get_fips_enabled(conf=__conf__): return conf.get_switch("OS.EnableFIPS", False) def get_openssl_cmd(conf=__conf__): return conf.get("OS.OpensslPath", "/usr/bin/openssl") def get_ssh_client_alive_interval(conf=__conf__): return conf.get("OS.SshClientAliveInterval", 180) def get_ssh_dir(conf=__conf__): return conf.get("OS.SshDir", "/etc/ssh") def get_home_dir(conf=__conf__): return conf.get("OS.HomeDir", "/home") def get_passwd_file_path(conf=__conf__): return conf.get("OS.PasswordPath", "/etc/shadow") def get_sudoers_dir(conf=__conf__): return conf.get("OS.SudoersDir", "/etc/sudoers.d") def get_sshd_conf_file_path(conf=__conf__): return os.path.join(get_ssh_dir(conf), "sshd_config") def get_ssh_key_glob(conf=__conf__): return os.path.join(get_ssh_dir(conf), 'ssh_host_*key*') def get_ssh_key_private_path(conf=__conf__): return os.path.join(get_ssh_dir(conf), 'ssh_host_{0}_key'.format(get_ssh_host_keypair_type(conf))) def get_ssh_key_public_path(conf=__conf__): return os.path.join(get_ssh_dir(conf), 'ssh_host_{0}_key.pub'.format(get_ssh_host_keypair_type(conf))) def get_root_device_scsi_timeout(conf=__conf__): return conf.get("OS.RootDeviceScsiTimeout", None) def get_root_device_scsi_timeout_period(conf=__conf__): return conf.get_int("OS.RootDeviceScsiTimeoutPeriod", 30) def get_ssh_host_keypair_type(conf=__conf__): keypair_type = conf.get("Provisioning.SshHostKeyPairType", "rsa") if keypair_type == "auto": ''' auto generates all supported key types and returns the rsa thumbprint as the default. ''' return "rsa" return keypair_type def get_ssh_host_keypair_mode(conf=__conf__): return conf.get("Provisioning.SshHostKeyPairType", "rsa") def get_extensions_enabled(conf=__conf__): return conf.get_switch("Extensions.Enabled", True) def get_goal_state_period(conf=__conf__): return conf.get_int("Extensions.GoalStatePeriod", 6) def get_initial_goal_state_period(conf=__conf__): return conf.get_int("Extensions.InitialGoalStatePeriod", default_value=lambda: get_goal_state_period(conf=conf)) def get_allow_reset_sys_user(conf=__conf__): return conf.get_switch("Provisioning.AllowResetSysUser", False) def get_regenerate_ssh_host_key(conf=__conf__): return conf.get_switch("Provisioning.RegenerateSshHostKeyPair", False) def get_delete_root_password(conf=__conf__): return conf.get_switch("Provisioning.DeleteRootPassword", False) def get_decode_customdata(conf=__conf__): return conf.get_switch("Provisioning.DecodeCustomData", False) def get_execute_customdata(conf=__conf__): return conf.get_switch("Provisioning.ExecuteCustomData", False) def get_password_cryptid(conf=__conf__): return conf.get("Provisioning.PasswordCryptId", "6") def get_provisioning_agent(conf=__conf__): return conf.get("Provisioning.Agent", "auto") def get_provision_enabled(conf=__conf__): """ Provisioning (as far as waagent is concerned) is enabled if either the agent is set to 'auto' or 'waagent'. This wraps logic that was introduced for flexible provisioning agent configuration and detection. The replaces the older bool setting to turn provisioning on or off. """ return get_provisioning_agent(conf) in ("auto", "waagent") def get_password_crypt_salt_len(conf=__conf__): return conf.get_int("Provisioning.PasswordCryptSaltLength", 10) def get_monitor_hostname(conf=__conf__): return conf.get_switch("Provisioning.MonitorHostName", False) def get_monitor_hostname_period(conf=__conf__): return conf.get_int("Provisioning.MonitorHostNamePeriod", 30) def get_httpproxy_host(conf=__conf__): return conf.get("HttpProxy.Host", None) def get_httpproxy_port(conf=__conf__): return conf.get_int("HttpProxy.Port", None) def get_detect_scvmm_env(conf=__conf__): return conf.get_switch("DetectScvmmEnv", False) def get_resourcedisk_format(conf=__conf__): return conf.get_switch("ResourceDisk.Format", False) def get_resourcedisk_enable_swap(conf=__conf__): return conf.get_switch("ResourceDisk.EnableSwap", False) def get_resourcedisk_enable_swap_encryption(conf=__conf__): return conf.get_switch("ResourceDisk.EnableSwapEncryption", False) def get_resourcedisk_mountpoint(conf=__conf__): return conf.get("ResourceDisk.MountPoint", "/mnt/resource") def get_resourcedisk_mountoptions(conf=__conf__): return conf.get("ResourceDisk.MountOptions", None) def get_resourcedisk_filesystem(conf=__conf__): return conf.get("ResourceDisk.Filesystem", "ext3") def get_resourcedisk_swap_size_mb(conf=__conf__): return conf.get_int("ResourceDisk.SwapSizeMB", 0) def get_autoupdate_gafamily(conf=__conf__): return conf.get("AutoUpdate.GAFamily", "Prod") def get_autoupdate_enabled(conf=__conf__): return conf.get_switch("AutoUpdate.Enabled", True) def get_autoupdate_frequency(conf=__conf__): return conf.get_int("Autoupdate.Frequency", 3600) def get_enable_overprovisioning(conf=__conf__): return conf.get_switch("EnableOverProvisioning", True) def get_allow_http(conf=__conf__): return conf.get_switch("OS.AllowHTTP", False) def get_disable_agent_file_path(conf=__conf__): return os.path.join(get_lib_dir(conf), DISABLE_AGENT_FILE) def get_cgroups_enabled(conf=__conf__): return conf.get_switch("CGroups.Enabled", True) def get_monitor_network_configuration_changes(conf=__conf__): return conf.get_switch("Monitor.NetworkConfigurationChanges", False) def get_cgroup_check_period(conf=__conf__): """ How often to perform checks on cgroups (are the processes in the cgroups as expected, has the agent exceeded its quota, etc) NOTE: This option is experimental and may be removed in later versions of the Agent. """ return conf.get_int("Debug.CgroupCheckPeriod", 300) def get_cgroup_log_metrics(conf=__conf__): """ If True, resource usage metrics are written to the local log NOTE: This option is experimental and may be removed in later versions of the Agent. """ return conf.get_switch("Debug.CgroupLogMetrics", False) def get_cgroup_disable_on_process_check_failure(conf=__conf__): """ If True, cgroups will be disabled if the process check fails NOTE: This option is experimental and may be removed in later versions of the Agent. """ return conf.get_switch("Debug.CgroupDisableOnProcessCheckFailure", True) def get_cgroup_disable_on_quota_check_failure(conf=__conf__): """ If True, cgroups will be disabled if the CPU quota check fails NOTE: This option is experimental and may be removed in later versions of the Agent. """ return conf.get_switch("Debug.CgroupDisableOnQuotaCheckFailure", True) def get_agent_cpu_quota(conf=__conf__): """ CPU quota for the agent as a percentage of 1 CPU (100% == 1 CPU) NOTE: This option is experimental and may be removed in later versions of the Agent. """ return conf.get_int("Debug.AgentCpuQuota", 50) def get_agent_cpu_throttled_time_threshold(conf=__conf__): """ Throttled time threshold for agent cpu in seconds. NOTE: This option is experimental and may be removed in later versions of the Agent. """ return conf.get_int("Debug.AgentCpuThrottledTimeThreshold", 120) def get_agent_memory_quota(conf=__conf__): """ Memory quota for the agent in bytes. NOTE: This option is experimental and may be removed in later versions of the Agent. """ return conf.get_int("Debug.AgentMemoryQuota", 30 * 1024 ** 2) def get_enable_agent_memory_usage_check(conf=__conf__): """ If True, Agent checks it's Memory usage. NOTE: This option is experimental and may be removed in later versions of the Agent. """ return conf.get_switch("Debug.EnableAgentMemoryUsageCheck", False) def get_cgroup_monitor_expiry_time(conf=__conf__): """ cgroups monitoring for pilot extensions disabled after expiry time NOTE: This option is experimental and may be removed in later versions of the Agent. """ return conf.get("Debug.CgroupMonitorExpiryTime", "2022-03-31") def get_cgroup_monitor_extension_name (conf=__conf__): """ cgroups monitoring extension name NOTE: This option is experimental and may be removed in later versions of the Agent. """ return conf.get("Debug.CgroupMonitorExtensionName", "Microsoft.Azure.Monitor.AzureMonitorLinuxAgent") def get_enable_fast_track(conf=__conf__): """ If True, the agent use FastTrack when retrieving goal states NOTE: This option is experimental and may be removed in later versions of the Agent. """ return conf.get_switch("Debug.EnableFastTrack", True) def get_etp_collection_period(conf=__conf__): """ Determines the frequency to perform ETP collection on extensions telemetry events. NOTE: This option is experimental and may be removed in later versions of the Agent. """ return conf.get_int("Debug.EtpCollectionPeriod", 300) def get_hotfix_upgrade_frequency(conf=__conf__): """ Determines the frequency to check for Hotfix upgrades (. version changed in new upgrades). NOTE: This option is experimental and may be removed in later versions of the Agent. """ return conf.get_int("Debug.AutoUpdateHotfixFrequency", 4 * 60 * 60) def get_normal_upgrade_frequency(conf=__conf__): """ Determines the frequency to check for Normal upgrades (. version changed in new upgrades). NOTE: This option is experimental and may be removed in later versions of the Agent. """ return conf.get_int("Debug.AutoUpdateNormalFrequency", 24 * 60 * 60) def get_enable_ga_versioning(conf=__conf__): """ If True, the agent uses GA Versioning for auto-updating the agent vs automatically auto-updating to the highest version. NOTE: This option is experimental and may be removed in later versions of the Agent. """ return conf.get_switch("Debug.EnableGAVersioning", False) def get_firewall_rules_log_period(conf=__conf__): """ Determine the frequency to perform the periodic operation of logging firewall rules. NOTE: This option is experimental and may be removed in later versions of the Agent. """ return conf.get_int("Debug.FirewallRulesLogPeriod", 86400) WALinuxAgent-2.9.1.1/azurelinuxagent/common/datacontract.py000066400000000000000000000052561446033677600237560ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2019 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # from azurelinuxagent.common.exception import ProtocolError import azurelinuxagent.common.logger as logger # pylint: disable=W0105 """ Base class for data contracts between guest and host and utilities to manipulate the properties in those contracts """ # pylint: enable=W0105 class DataContract(object): pass class DataContractList(list): def __init__(self, item_cls): # pylint: disable=W0231 self.item_cls = item_cls def validate_param(name, val, expected_type): if val is None: raise ProtocolError("{0} is None".format(name)) if not isinstance(val, expected_type): raise ProtocolError(("{0} type should be {1} not {2}" "").format(name, expected_type, type(val))) def set_properties(name, obj, data): if isinstance(obj, DataContract): validate_param("Property '{0}'".format(name), data, dict) for prob_name, prob_val in data.items(): prob_full_name = "{0}.{1}".format(name, prob_name) try: prob = getattr(obj, prob_name) except AttributeError: logger.warn("Unknown property: {0}", prob_full_name) continue prob = set_properties(prob_full_name, prob, prob_val) setattr(obj, prob_name, prob) return obj elif isinstance(obj, DataContractList): validate_param("List '{0}'".format(name), data, list) for item_data in data: item = obj.item_cls() item = set_properties(name, item, item_data) obj.append(item) return obj else: return data def get_properties(obj): if isinstance(obj, DataContract): data = {} props = vars(obj) for prob_name, prob in list(props.items()): data[prob_name] = get_properties(prob) return data elif isinstance(obj, DataContractList): data = [] for item in obj: item_data = get_properties(item) data.append(item_data) return data else: return obj WALinuxAgent-2.9.1.1/azurelinuxagent/common/dhcp.py000066400000000000000000000351701446033677600222230ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ import array import os import socket import time import azurelinuxagent.common.logger as logger from azurelinuxagent.common.exception import DhcpError from azurelinuxagent.common.osutil import get_osutil from azurelinuxagent.common.utils.restutil import KNOWN_WIRESERVER_IP from azurelinuxagent.common.utils.textutil import hex_dump, hex_dump2, \ hex_dump3, \ compare_bytes, str_to_ord, \ unpack_big_endian, \ int_to_ip4_addr # the kernel routing table representation of 168.63.129.16 KNOWN_WIRESERVER_IP_ENTRY = '10813FA8' def get_dhcp_handler(): return DhcpHandler() class DhcpHandler(object): """ Azure use DHCP option 245 to pass endpoint ip to VMs. """ def __init__(self): self.osutil = get_osutil() self.endpoint = None self.gateway = None self.routes = None self._request_broadcast = False self.skip_cache = False def run(self): """ Send dhcp request Configure default gateway and routes Save wire server endpoint if found """ if self.wireserver_route_exists or self.dhcp_cache_exists: return self.send_dhcp_req() self.conf_routes() def wait_for_network(self): """ Wait for network stack to be initialized. """ ipv4 = self.osutil.get_ip4_addr() while ipv4 == '' or ipv4 == '0.0.0.0': logger.info("Waiting for network.") time.sleep(10) logger.info("Try to start network interface.") self.osutil.start_network() ipv4 = self.osutil.get_ip4_addr() @property def wireserver_route_exists(self): """ Determine whether a route to the known wireserver ip already exists, and if so use that as the endpoint. This is true when running in a virtual network. :return: True if a route to KNOWN_WIRESERVER_IP exists. """ route_exists = False logger.info("Test for route to {0}".format(KNOWN_WIRESERVER_IP)) try: route_table = self.osutil.read_route_table() if any((KNOWN_WIRESERVER_IP_ENTRY in route) for route in route_table): # reset self.gateway and self.routes # we do not need to alter the routing table self.endpoint = KNOWN_WIRESERVER_IP self.gateway = None self.routes = None route_exists = True logger.info("Route to {0} exists".format(KNOWN_WIRESERVER_IP)) else: logger.warn("No route exists to {0}".format(KNOWN_WIRESERVER_IP)) except Exception as e: logger.error( "Could not determine whether route exists to {0}: {1}".format( KNOWN_WIRESERVER_IP, e)) return route_exists @property def dhcp_cache_exists(self): """ Check whether the dhcp options cache exists and contains the wireserver endpoint, unless skip_cache is True. :return: True if the cached endpoint was found in the dhcp lease """ if self.skip_cache: return False exists = False logger.info("Checking for dhcp lease cache") cached_endpoint = self.osutil.get_dhcp_lease_endpoint() # pylint: disable=E1128 if cached_endpoint is not None: self.endpoint = cached_endpoint exists = True logger.info("Cache exists [{0}]".format(exists)) return exists def conf_routes(self): logger.info("Configure routes") logger.info("Gateway:{0}", self.gateway) logger.info("Routes:{0}", self.routes) # Add default gateway if self.gateway is not None and self.osutil.is_missing_default_route(): self.osutil.route_add(0, 0, self.gateway) if self.routes is not None: for route in self.routes: self.osutil.route_add(route[0], route[1], route[2]) def _send_dhcp_req(self, request): __waiting_duration__ = [0, 10, 30, 60, 60] for duration in __waiting_duration__: try: self.osutil.allow_dhcp_broadcast() response = socket_send(request) validate_dhcp_resp(request, response) return response except DhcpError as e: logger.warn("Failed to send DHCP request: {0}", e) time.sleep(duration) return None def send_dhcp_req(self): """ Check if DHCP is available """ dhcp_available = self.osutil.is_dhcp_available() if not dhcp_available: logger.info("send_dhcp_req: DHCP not available") self.endpoint = KNOWN_WIRESERVER_IP return # pylint: disable=W0105 """ Build dhcp request with mac addr Configure route to allow dhcp traffic Stop dhcp service if necessary """ # pylint: enable=W0105 logger.info("Send dhcp request") mac_addr = self.osutil.get_mac_addr() # Do unicast first, then fallback to broadcast if fails. req = build_dhcp_request(mac_addr, self._request_broadcast) if not self._request_broadcast: self._request_broadcast = True # Temporary allow broadcast for dhcp. Remove the route when done. missing_default_route = self.osutil.is_missing_default_route() ifname = self.osutil.get_if_name() if missing_default_route: self.osutil.set_route_for_dhcp_broadcast(ifname) # In some distros, dhcp service needs to be shutdown before agent probe # endpoint through dhcp. if self.osutil.is_dhcp_enabled(): self.osutil.stop_dhcp_service() resp = self._send_dhcp_req(req) if self.osutil.is_dhcp_enabled(): self.osutil.start_dhcp_service() if missing_default_route: self.osutil.remove_route_for_dhcp_broadcast(ifname) if resp is None: raise DhcpError("Failed to receive dhcp response.") self.endpoint, self.gateway, self.routes = parse_dhcp_resp(resp) def validate_dhcp_resp(request, response): # pylint: disable=R1710 bytes_recv = len(response) if bytes_recv < 0xF6: logger.error("HandleDhcpResponse: Too few bytes received:{0}", bytes_recv) return False logger.verbose("BytesReceived:{0}", hex(bytes_recv)) logger.verbose("DHCP response:{0}", hex_dump(response, bytes_recv)) # check transactionId, cookie, MAC address cookie should never mismatch # transactionId and MAC address may mismatch if we see a response # meant from another machine if not compare_bytes(request, response, 0xEC, 4): logger.verbose("Cookie not match:\nsend={0},\nreceive={1}", hex_dump3(request, 0xEC, 4), hex_dump3(response, 0xEC, 4)) raise DhcpError("Cookie in dhcp respones doesn't match the request") if not compare_bytes(request, response, 4, 4): logger.verbose("TransactionID not match:\nsend={0},\nreceive={1}", hex_dump3(request, 4, 4), hex_dump3(response, 4, 4)) raise DhcpError("TransactionID in dhcp respones " "doesn't match the request") if not compare_bytes(request, response, 0x1C, 6): logger.verbose("Mac Address not match:\nsend={0},\nreceive={1}", hex_dump3(request, 0x1C, 6), hex_dump3(response, 0x1C, 6)) raise DhcpError("Mac Addr in dhcp respones " "doesn't match the request") def parse_route(response, option, i, length, bytes_recv): # pylint: disable=W0613 # http://msdn.microsoft.com/en-us/library/cc227282%28PROT.10%29.aspx logger.verbose("Routes at offset: {0} with length:{1}", hex(i), hex(length)) routes = [] if length < 5: logger.error("Data too small for option:{0}", option) j = i + 2 while j < (i + length + 2): mask_len_bits = str_to_ord(response[j]) mask_len_bytes = (((mask_len_bits + 7) & ~7) >> 3) mask = 0xFFFFFFFF & (0xFFFFFFFF << (32 - mask_len_bits)) j += 1 net = unpack_big_endian(response, j, mask_len_bytes) net <<= (32 - mask_len_bytes * 8) net &= mask j += mask_len_bytes gateway = unpack_big_endian(response, j, 4) j += 4 routes.append((net, mask, gateway)) if j != (i + length + 2): logger.error("Unable to parse routes") return routes def parse_ip_addr(response, option, i, length, bytes_recv): if i + 5 < bytes_recv: if length != 4: logger.error("Endpoint or Default Gateway not 4 bytes") return None addr = unpack_big_endian(response, i + 2, 4) ip_addr = int_to_ip4_addr(addr) return ip_addr else: logger.error("Data too small for option:{0}", option) return None def parse_dhcp_resp(response): """ Parse DHCP response: Returns endpoint server or None on error. """ logger.verbose("parse Dhcp Response") bytes_recv = len(response) endpoint = None gateway = None routes = None # Walk all the returned options, parsing out what we need, ignoring the # others. We need the custom option 245 to find the the endpoint we talk to # as well as to handle some Linux DHCP client incompatibilities; # options 3 for default gateway and 249 for routes; 255 is end. i = 0xF0 # offset to first option while i < bytes_recv: option = str_to_ord(response[i]) length = 0 if (i + 1) < bytes_recv: length = str_to_ord(response[i + 1]) logger.verbose("DHCP option {0} at offset:{1} with length:{2}", hex(option), hex(i), hex(length)) if option == 255: logger.verbose("DHCP packet ended at offset:{0}", hex(i)) break elif option == 249: routes = parse_route(response, option, i, length, bytes_recv) elif option == 3: gateway = parse_ip_addr(response, option, i, length, bytes_recv) logger.verbose("Default gateway:{0}, at {1}", gateway, hex(i)) elif option == 245: endpoint = parse_ip_addr(response, option, i, length, bytes_recv) logger.verbose("Azure wire protocol endpoint:{0}, at {1}", endpoint, hex(i)) else: logger.verbose("Skipping DHCP option:{0} at {1} with length {2}", hex(option), hex(i), hex(length)) i += length + 2 return endpoint, gateway, routes def socket_send(request): sock = None try: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(("0.0.0.0", 68)) sock.sendto(request, ("", 67)) sock.settimeout(10) logger.verbose("Send DHCP request: Setting socket.timeout=10, " "entering recv") response = sock.recv(1024) return response except IOError as e: raise DhcpError("{0}".format(e)) finally: if sock is not None: sock.close() def build_dhcp_request(mac_addr, request_broadcast): """ Build DHCP request string. """ # # typedef struct _DHCP { # UINT8 Opcode; /* op: BOOTREQUEST or BOOTREPLY */ # UINT8 HardwareAddressType; /* htype: ethernet */ # UINT8 HardwareAddressLength; /* hlen: 6 (48 bit mac address) */ # UINT8 Hops; /* hops: 0 */ # UINT8 TransactionID[4]; /* xid: random */ # UINT8 Seconds[2]; /* secs: 0 */ # UINT8 Flags[2]; /* flags: 0 or 0x8000 for broadcast*/ # UINT8 ClientIpAddress[4]; /* ciaddr: 0 */ # UINT8 YourIpAddress[4]; /* yiaddr: 0 */ # UINT8 ServerIpAddress[4]; /* siaddr: 0 */ # UINT8 RelayAgentIpAddress[4]; /* giaddr: 0 */ # UINT8 ClientHardwareAddress[16]; /* chaddr: 6 byte eth MAC address */ # UINT8 ServerName[64]; /* sname: 0 */ # UINT8 BootFileName[128]; /* file: 0 */ # UINT8 MagicCookie[4]; /* 99 130 83 99 */ # /* 0x63 0x82 0x53 0x63 */ # /* options -- hard code ours */ # # UINT8 MessageTypeCode; /* 53 */ # UINT8 MessageTypeLength; /* 1 */ # UINT8 MessageType; /* 1 for DISCOVER */ # UINT8 End; /* 255 */ # } DHCP; # # tuple of 244 zeros # (struct.pack_into would be good here, but requires Python 2.5) request = [0] * 244 trans_id = gen_trans_id() # Opcode = 1 # HardwareAddressType = 1 (ethernet/MAC) # HardwareAddressLength = 6 (ethernet/MAC/48 bits) for a in range(0, 3): request[a] = [1, 1, 6][a] # fill in transaction id (random number to ensure response matches request) for a in range(0, 4): request[4 + a] = str_to_ord(trans_id[a]) logger.verbose("BuildDhcpRequest: transactionId:%s,%04X" % ( hex_dump2(trans_id), unpack_big_endian(request, 4, 4))) if request_broadcast: # set broadcast flag to true to request the dhcp server # to respond to a boradcast address, # this is useful when user dhclient fails. request[0x0A] = 0x80 # fill in ClientHardwareAddress for a in range(0, 6): request[0x1C + a] = str_to_ord(mac_addr[a]) # DHCP Magic Cookie: 99, 130, 83, 99 # MessageTypeCode = 53 DHCP Message Type # MessageTypeLength = 1 # MessageType = DHCPDISCOVER # End = 255 DHCP_END for a in range(0, 8): request[0xEC + a] = [99, 130, 83, 99, 53, 1, 1, 255][a] return array.array("B", request) def gen_trans_id(): return os.urandom(4) WALinuxAgent-2.9.1.1/azurelinuxagent/common/errorstate.py000066400000000000000000000021661446033677600234760ustar00rootroot00000000000000from datetime import datetime, timedelta ERROR_STATE_DELTA_DEFAULT = timedelta(minutes=15) ERROR_STATE_DELTA_INSTALL = timedelta(minutes=5) ERROR_STATE_HOST_PLUGIN_FAILURE = timedelta(minutes=5) class ErrorState(object): def __init__(self, min_timedelta=ERROR_STATE_DELTA_DEFAULT): self.min_timedelta = min_timedelta self.count = 0 self.timestamp = None def incr(self): if self.count == 0: self.timestamp = datetime.utcnow() self.count += 1 def reset(self): self.count = 0 self.timestamp = None def is_triggered(self): if self.timestamp is None: return False delta = datetime.utcnow() - self.timestamp if delta >= self.min_timedelta: return True return False @property def fail_time(self): if self.timestamp is None: return 'unknown' delta = round((datetime.utcnow() - self.timestamp).seconds / 60.0, 2) if delta < 60: return '{0} min'.format(delta) delta_hr = round(delta / 60.0, 2) return '{0} hr'.format(delta_hr) WALinuxAgent-2.9.1.1/azurelinuxagent/common/event.py000066400000000000000000001000231446033677600224140ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import atexit import json import os import platform import re import sys import threading import time import traceback from datetime import datetime import azurelinuxagent.common.conf as conf import azurelinuxagent.common.logger as logger from azurelinuxagent.common.AgentGlobals import AgentGlobals from azurelinuxagent.common.exception import EventError, OSUtilError from azurelinuxagent.common.future import ustr from azurelinuxagent.common.datacontract import get_properties, set_properties from azurelinuxagent.common.osutil import get_osutil from azurelinuxagent.common.telemetryevent import TelemetryEventParam, TelemetryEvent, CommonTelemetryEventSchema, \ GuestAgentGenericLogsSchema, GuestAgentExtensionEventsSchema, GuestAgentPerfCounterEventsSchema from azurelinuxagent.common.utils import fileutil, textutil from azurelinuxagent.common.utils.textutil import parse_doc, findall, find, getattrib, str_to_encoded_ustr from azurelinuxagent.common.version import CURRENT_VERSION, CURRENT_AGENT, AGENT_NAME, DISTRO_NAME, DISTRO_VERSION, DISTRO_CODE_NAME, AGENT_EXECUTION_MODE from azurelinuxagent.common.protocol.imds import get_imds_client EVENTS_DIRECTORY = "events" _EVENT_MSG = "Event: name={0}, op={1}, message={2}, duration={3}" TELEMETRY_EVENT_PROVIDER_ID = "69B669B9-4AF8-4C50-BDC4-6006FA76E975" TELEMETRY_EVENT_EVENT_ID = 1 TELEMETRY_METRICS_EVENT_ID = 4 TELEMETRY_LOG_PROVIDER_ID = "FFF0196F-EE4C-4EAF-9AA5-776F622DEB4F" TELEMETRY_LOG_EVENT_ID = 7 # # When this flag is enabled the TODO comment in Logger.log() needs to be addressed; also the tests # marked with "Enable this test when SEND_LOGS_TO_TELEMETRY is enabled" should be enabled. # SEND_LOGS_TO_TELEMETRY = False MAX_NUMBER_OF_EVENTS = 1000 AGENT_EVENT_FILE_EXTENSION = '.waagent.tld' EVENT_FILE_REGEX = re.compile(r'(?P\.waagent)?\.tld$') def send_logs_to_telemetry(): return SEND_LOGS_TO_TELEMETRY class WALAEventOperation: ActivateResourceDisk = "ActivateResourceDisk" AgentBlacklisted = "AgentBlacklisted" AgentEnabled = "AgentEnabled" AgentMemory = "AgentMemory" AgentUpgrade = "AgentUpgrade" ArtifactsProfileBlob = "ArtifactsProfileBlob" CGroupsCleanUp = "CGroupsCleanUp" CGroupsDisabled = "CGroupsDisabled" CGroupsInfo = "CGroupsInfo" CollectEventErrors = "CollectEventErrors" CollectEventUnicodeErrors = "CollectEventUnicodeErrors" ConfigurationChange = "ConfigurationChange" CustomData = "CustomData" DefaultChannelChange = "DefaultChannelChange" Deploy = "Deploy" Disable = "Disable" Downgrade = "Downgrade" Download = "Download" Enable = "Enable" ExtensionProcessing = "ExtensionProcessing" ExtensionTelemetryEventProcessing = "ExtensionTelemetryEventProcessing" FetchGoalState = "FetchGoalState" Firewall = "Firewall" GoalState = "GoalState" GoalStateUnsupportedFeatures = "GoalStateUnsupportedFeatures" HealthCheck = "HealthCheck" HealthObservation = "HealthObservation" HeartBeat = "HeartBeat" HostPlugin = "HostPlugin" HostPluginHeartbeat = "HostPluginHeartbeat" HostPluginHeartbeatExtended = "HostPluginHeartbeatExtended" HttpErrors = "HttpErrors" HttpGet = "HttpGet" ImdsHeartbeat = "ImdsHeartbeat" Install = "Install" InitializeHostPlugin = "InitializeHostPlugin" Log = "Log" LogCollection = "LogCollection" OSInfo = "OSInfo" Partition = "Partition" PersistFirewallRules = "PersistFirewallRules" PluginSettingsVersionMismatch = "PluginSettingsVersionMismatch" InvalidExtensionConfig = "InvalidExtensionConfig" Provision = "Provision" ProvisionGuestAgent = "ProvisionGuestAgent" RemoteAccessHandling = "RemoteAccessHandling" ReportEventErrors = "ReportEventErrors" ReportEventUnicodeErrors = "ReportEventUnicodeErrors" ReportStatus = "ReportStatus" ReportStatusExtended = "ReportStatusExtended" Restart = "Restart" SequenceNumberMismatch = "SequenceNumberMismatch" SetCGroupsLimits = "SetCGroupsLimits" SkipUpdate = "SkipUpdate" StatusProcessing = "StatusProcessing" UnhandledError = "UnhandledError" UnInstall = "UnInstall" Unknown = "Unknown" Update = "Update" VmSettings = "VmSettings" VmSettingsSummary = "VmSettingsSummary" SHOULD_ENCODE_MESSAGE_LEN = 80 SHOULD_ENCODE_MESSAGE_OP = [ WALAEventOperation.Disable, WALAEventOperation.Enable, WALAEventOperation.Install, WALAEventOperation.UnInstall, ] class EventStatus(object): EVENT_STATUS_FILE = "event_status.json" def __init__(self): self._path = None self._status = {} def clear(self): self._status = {} self._save() def event_marked(self, name, version, op): return self._event_name(name, version, op) in self._status def event_succeeded(self, name, version, op): event = self._event_name(name, version, op) if event not in self._status: return True return self._status[event] is True def initialize(self, status_dir=conf.get_lib_dir()): self._path = os.path.join(status_dir, EventStatus.EVENT_STATUS_FILE) self._load() def mark_event_status(self, name, version, op, status): event = self._event_name(name, version, op) self._status[event] = (status is True) self._save() def _event_name(self, name, version, op): return "{0}-{1}-{2}".format(name, version, op) def _load(self): try: self._status = {} if os.path.isfile(self._path): with open(self._path, 'r') as f: self._status = json.load(f) except Exception as e: logger.warn("Exception occurred loading event status: {0}".format(e)) self._status = {} def _save(self): try: with open(self._path, 'w') as f: json.dump(self._status, f) except Exception as e: logger.warn("Exception occurred saving event status: {0}".format(e)) __event_status__ = EventStatus() __event_status_operations__ = [ WALAEventOperation.ReportStatus ] def parse_json_event(data_str): data = json.loads(data_str) event = TelemetryEvent() set_properties("TelemetryEvent", event, data) event.file_type = "json" return event def parse_event(data_str): try: try: return parse_json_event(data_str) except ValueError: return parse_xml_event(data_str) except Exception as e: raise EventError("Error parsing event: {0}".format(ustr(e))) def parse_xml_param(param_node): name = getattrib(param_node, "Name") value_str = getattrib(param_node, "Value") attr_type = getattrib(param_node, "T") value = value_str if attr_type == 'mt:uint64': value = int(value_str) elif attr_type == 'mt:bool': value = bool(value_str) elif attr_type == 'mt:float64': value = float(value_str) return TelemetryEventParam(name, value) def parse_xml_event(data_str): try: xml_doc = parse_doc(data_str) event_id = getattrib(find(xml_doc, "Event"), 'id') provider_id = getattrib(find(xml_doc, "Provider"), 'id') event = TelemetryEvent(event_id, provider_id) param_nodes = findall(xml_doc, 'Param') for param_node in param_nodes: event.parameters.append(parse_xml_param(param_node)) event.file_type = "xml" return event except Exception as e: raise ValueError(ustr(e)) def _encode_message(op, message): """ Gzip and base64 encode a message based on the operation. The intent of this message is to make the logs human readable and include the stdout/stderr from extension operations. Extension operations tend to generate a lot of noise, which makes it difficult to parse the line-oriented waagent.log. The compromise is to encode the stdout/stderr so we preserve the data and do not destroy the line oriented nature. The data can be recovered using the following command: $ echo '' | base64 -d | pigz -zd You may need to install the pigz command. :param op: Operation, e.g. Enable or Install :param message: Message to encode :return: gzip'ed and base64 encoded message, or the original message """ if len(message) == 0: return message if op not in SHOULD_ENCODE_MESSAGE_OP: return message try: return textutil.compress(message) except Exception: # If the message could not be encoded a dummy message ('<>') is returned. # The original message was still sent via telemetry, so all is not lost. return "<>" def _log_event(name, op, message, duration, is_success=True): global _EVENT_MSG # pylint: disable=W0603 if not is_success: logger.error(_EVENT_MSG, name, op, message, duration) else: logger.info(_EVENT_MSG, name, op, message, duration) class CollectOrReportEventDebugInfo(object): """ This class is used for capturing and reporting debug info that is captured during event collection and reporting to wireserver. It captures the count of unicode errors and any unexpected errors and also a subset of errors with stacks to help with debugging any potential issues. """ __MAX_ERRORS_TO_REPORT = 5 OP_REPORT = "Report" OP_COLLECT = "Collect" def __init__(self, operation=OP_REPORT): self.__unicode_error_count = 0 self.__unicode_errors = set() self.__op_error_count = 0 self.__op_errors = set() if operation == self.OP_REPORT: self.__unicode_error_event = WALAEventOperation.ReportEventUnicodeErrors self.__op_errors_event = WALAEventOperation.ReportEventErrors elif operation == self.OP_COLLECT: self.__unicode_error_event = WALAEventOperation.CollectEventUnicodeErrors self.__op_errors_event = WALAEventOperation.CollectEventErrors def report_debug_info(self): def report_dropped_events_error(count, errors, operation_name): err_msg_format = "DroppedEventsCount: {0}\nReasons (first {1} errors): {2}" if count > 0: add_event(op=operation_name, message=err_msg_format.format(count, CollectOrReportEventDebugInfo.__MAX_ERRORS_TO_REPORT, ', '.join(errors)), is_success=False) report_dropped_events_error(self.__op_error_count, self.__op_errors, self.__op_errors_event) report_dropped_events_error(self.__unicode_error_count, self.__unicode_errors, self.__unicode_error_event) @staticmethod def _update_errors_and_get_count(error_count, errors, error): error_count += 1 if len(errors) < CollectOrReportEventDebugInfo.__MAX_ERRORS_TO_REPORT: errors.add("{0}: {1}".format(ustr(error), traceback.format_exc())) return error_count def update_unicode_error(self, unicode_err): self.__unicode_error_count = self._update_errors_and_get_count(self.__unicode_error_count, self.__unicode_errors, unicode_err) def update_op_error(self, op_err): self.__op_error_count = self._update_errors_and_get_count(self.__op_error_count, self.__op_errors, op_err) class EventLogger(object): def __init__(self): self.event_dir = None self.periodic_events = {} # # All events should have these parameters. # # The first set comes from the current OS and is initialized here. These values don't change during # the agent's lifetime. # # The next two sets come from the goal state and IMDS and must be explicitly initialized using # initialize_vminfo_common_parameters() once a protocol for communication with the host has been # created. Their values don't change during the agent's lifetime. Note that we initialize these # parameters here using dummy values (*_UNINITIALIZED) since events sent to the host should always # match the schema defined for them in the telemetry pipeline. # # There is another set of common parameters that must be computed at the time the event is created # (e.g. the timestamp and the container ID); those are added to events (along with the parameters # below) in _add_common_event_parameters() # # Note that different kinds of events may also include other parameters; those are added by the # corresponding add_* method (e.g. add_metric for performance metrics). # self._common_parameters = [] # Parameters from OS osutil = get_osutil() self._common_parameters.append(TelemetryEventParam(CommonTelemetryEventSchema.OSVersion, EventLogger._get_os_version())) self._common_parameters.append(TelemetryEventParam(CommonTelemetryEventSchema.ExecutionMode, AGENT_EXECUTION_MODE)) self._common_parameters.append(TelemetryEventParam(CommonTelemetryEventSchema.RAM, int(EventLogger._get_ram(osutil)))) self._common_parameters.append(TelemetryEventParam(CommonTelemetryEventSchema.Processors, int(EventLogger._get_processors(osutil)))) # Parameters from goal state self._common_parameters.append(TelemetryEventParam(CommonTelemetryEventSchema.TenantName, "TenantName_UNINITIALIZED")) self._common_parameters.append(TelemetryEventParam(CommonTelemetryEventSchema.RoleName, "RoleName_UNINITIALIZED")) self._common_parameters.append(TelemetryEventParam(CommonTelemetryEventSchema.RoleInstanceName, "RoleInstanceName_UNINITIALIZED")) # # # Parameters from IMDS self._common_parameters.append(TelemetryEventParam(CommonTelemetryEventSchema.Location, "Location_UNINITIALIZED")) self._common_parameters.append(TelemetryEventParam(CommonTelemetryEventSchema.SubscriptionId, "SubscriptionId_UNINITIALIZED")) self._common_parameters.append(TelemetryEventParam(CommonTelemetryEventSchema.ResourceGroupName, "ResourceGroupName_UNINITIALIZED")) self._common_parameters.append(TelemetryEventParam(CommonTelemetryEventSchema.VMId, "VMId_UNINITIALIZED")) self._common_parameters.append(TelemetryEventParam(CommonTelemetryEventSchema.ImageOrigin, 0)) @staticmethod def _get_os_version(): return "{0}:{1}-{2}-{3}:{4}".format(platform.system(), DISTRO_NAME, DISTRO_VERSION, DISTRO_CODE_NAME, platform.release()) @staticmethod def _get_ram(osutil): try: return osutil.get_total_mem() except OSUtilError as e: logger.warn("Failed to get RAM info; will be missing from telemetry: {0}", ustr(e)) return 0 @staticmethod def _get_processors(osutil): try: return osutil.get_processor_cores() except OSUtilError as e: logger.warn("Failed to get Processors info; will be missing from telemetry: {0}", ustr(e)) return 0 def initialize_vminfo_common_parameters(self, protocol): """ Initializes the common parameters that come from the goal state and IMDS """ # create an index of the event parameters for faster updates parameters = {} for p in self._common_parameters: parameters[p.name] = p try: vminfo = protocol.get_vminfo() parameters[CommonTelemetryEventSchema.TenantName].value = vminfo.tenantName parameters[CommonTelemetryEventSchema.RoleName].value = vminfo.roleName parameters[CommonTelemetryEventSchema.RoleInstanceName].value = vminfo.roleInstanceName except Exception as e: logger.warn("Failed to get VM info from goal state; will be missing from telemetry: {0}", ustr(e)) try: imds_client = get_imds_client(protocol.get_endpoint()) imds_info = imds_client.get_compute() parameters[CommonTelemetryEventSchema.Location].value = imds_info.location parameters[CommonTelemetryEventSchema.SubscriptionId].value = imds_info.subscriptionId parameters[CommonTelemetryEventSchema.ResourceGroupName].value = imds_info.resourceGroupName parameters[CommonTelemetryEventSchema.VMId].value = imds_info.vmId parameters[CommonTelemetryEventSchema.ImageOrigin].value = int(imds_info.image_origin) except Exception as e: logger.warn("Failed to get IMDS info; will be missing from telemetry: {0}", ustr(e)) def save_event(self, data): if self.event_dir is None: logger.warn("Cannot save event -- Event reporter is not initialized.") return try: fileutil.mkdir(self.event_dir, mode=0o700) except (IOError, OSError) as e: msg = "Failed to create events folder {0}. Error: {1}".format(self.event_dir, ustr(e)) raise EventError(msg) try: existing_events = os.listdir(self.event_dir) if len(existing_events) >= MAX_NUMBER_OF_EVENTS: logger.periodic_warn(logger.EVERY_MINUTE, "[PERIODIC] Too many files under: {0}, current count: {1}, " "removing oldest event files".format(self.event_dir, len(existing_events))) existing_events.sort() oldest_files = existing_events[:-999] for event_file in oldest_files: os.remove(os.path.join(self.event_dir, event_file)) except (IOError, OSError) as e: msg = "Failed to remove old events from events folder {0}. Error: {1}".format(self.event_dir, ustr(e)) raise EventError(msg) filename = os.path.join(self.event_dir, ustr(int(time.time() * 1000000))) try: with open(filename + ".tmp", 'wb+') as hfile: hfile.write(data.encode("utf-8")) os.rename(filename + ".tmp", filename + AGENT_EVENT_FILE_EXTENSION) except (IOError, OSError) as e: msg = "Failed to write events to file: {0}".format(e) raise EventError(msg) def reset_periodic(self): self.periodic_events = {} def is_period_elapsed(self, delta, h): return h not in self.periodic_events or \ (self.periodic_events[h] + delta) <= datetime.now() def add_periodic(self, delta, name, op=WALAEventOperation.Unknown, is_success=True, duration=0, version=str(CURRENT_VERSION), message="", log_event=True, force=False): h = hash(name + op + ustr(is_success) + message) if force or self.is_period_elapsed(delta, h): self.add_event(name, op=op, is_success=is_success, duration=duration, version=version, message=message, log_event=log_event) self.periodic_events[h] = datetime.now() def add_event(self, name, op=WALAEventOperation.Unknown, is_success=True, duration=0, version=str(CURRENT_VERSION), message="", log_event=True): if (not is_success) and log_event: _log_event(name, op, message, duration, is_success=is_success) event = TelemetryEvent(TELEMETRY_EVENT_EVENT_ID, TELEMETRY_EVENT_PROVIDER_ID) event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.Name, str_to_encoded_ustr(name))) event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.Version, str_to_encoded_ustr(version))) event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.Operation, str_to_encoded_ustr(op))) event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.OperationSuccess, bool(is_success))) event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.Message, str_to_encoded_ustr(message))) event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.Duration, int(duration))) self.add_common_event_parameters(event, datetime.utcnow()) data = get_properties(event) try: self.save_event(json.dumps(data)) except EventError as e: logger.periodic_error(logger.EVERY_FIFTEEN_MINUTES, "[PERIODIC] {0}".format(ustr(e))) def add_log_event(self, level, message): event = TelemetryEvent(TELEMETRY_LOG_EVENT_ID, TELEMETRY_LOG_PROVIDER_ID) event.parameters.append(TelemetryEventParam(GuestAgentGenericLogsSchema.EventName, WALAEventOperation.Log)) event.parameters.append(TelemetryEventParam(GuestAgentGenericLogsSchema.CapabilityUsed, logger.LogLevel.STRINGS[level])) event.parameters.append(TelemetryEventParam(GuestAgentGenericLogsSchema.Context1, str_to_encoded_ustr(self._clean_up_message(message)))) event.parameters.append(TelemetryEventParam(GuestAgentGenericLogsSchema.Context2, datetime.utcnow().strftime(logger.Logger.LogTimeFormatInUTC))) event.parameters.append(TelemetryEventParam(GuestAgentGenericLogsSchema.Context3, '')) self.add_common_event_parameters(event, datetime.utcnow()) data = get_properties(event) try: self.save_event(json.dumps(data)) except EventError: pass def add_metric(self, category, counter, instance, value, log_event=False): """ Create and save an event which contains a telemetry event. :param str category: The category of metric (e.g. "cpu", "memory") :param str counter: The specific metric within the category (e.g. "%idle") :param str instance: For instanced metrics, the instance identifier (filesystem name, cpu core#, etc.) :param value: Value of the metric :param bool log_event: If true, log the collected metric in the agent log """ if log_event: message = "Metric {0}/{1} [{2}] = {3}".format(category, counter, instance, value) _log_event(AGENT_NAME, "METRIC", message, 0) event = TelemetryEvent(TELEMETRY_METRICS_EVENT_ID, TELEMETRY_EVENT_PROVIDER_ID) event.parameters.append(TelemetryEventParam(GuestAgentPerfCounterEventsSchema.Category, str_to_encoded_ustr(category))) event.parameters.append(TelemetryEventParam(GuestAgentPerfCounterEventsSchema.Counter, str_to_encoded_ustr(counter))) event.parameters.append(TelemetryEventParam(GuestAgentPerfCounterEventsSchema.Instance, str_to_encoded_ustr(instance))) event.parameters.append(TelemetryEventParam(GuestAgentPerfCounterEventsSchema.Value, float(value))) self.add_common_event_parameters(event, datetime.utcnow()) data = get_properties(event) try: self.save_event(json.dumps(data)) except EventError as e: logger.periodic_error(logger.EVERY_FIFTEEN_MINUTES, "[PERIODIC] {0}".format(ustr(e))) @staticmethod def _clean_up_message(message): # By the time the message has gotten to this point it is formatted as # # Old Time format # YYYY/MM/DD HH:mm:ss.fffffff LEVEL . # YYYY/MM/DD HH:mm:ss.fffffff . # YYYY/MM/DD HH:mm:ss LEVEL . # YYYY/MM/DD HH:mm:ss . # # UTC ISO Time format added in #1716 # YYYY-MM-DDTHH:mm:ss.fffffffZ LEVEL . # YYYY-MM-DDTHH:mm:ss.fffffffZ . # YYYY-MM-DDTHH:mm:ssZ LEVEL . # YYYY-MM-DDTHH:mm:ssZ . # # The timestamp and the level are redundant, and should be stripped. The logging library does not schematize # this data, so I am forced to parse the message using a regex. The format is regular, so the burden is low, # and usability on the telemetry side is high. if not message: return message # Adding two regexs to simplify the handling of logs and to keep it maintainable. Most of the logs would have # level includent in the log itself, but if it doesn't have, the second regex is a catch all case and will work # for all the cases. log_level_format_parser = re.compile(r"^.*(INFO|WARNING|ERROR|VERBOSE)\s*(.*)$") log_format_parser = re.compile(r"^[0-9:/\-TZ\s.]*\s(.*)$") # Parsing the log messages containing levels in it extract_level_message = log_level_format_parser.search(message) if extract_level_message: return extract_level_message.group(2) # The message bit else: # Parsing the log messages without levels in it. extract_message = log_format_parser.search(message) if extract_message: return extract_message.group(1) # The message bit else: return message def add_common_event_parameters(self, event, event_timestamp): """ This method is called for all events and ensures all telemetry fields are added before the event is sent out. Note that the event timestamp is saved in the OpcodeName field. """ common_params = [TelemetryEventParam(CommonTelemetryEventSchema.GAVersion, CURRENT_AGENT), TelemetryEventParam(CommonTelemetryEventSchema.ContainerId, AgentGlobals.get_container_id()), TelemetryEventParam(CommonTelemetryEventSchema.OpcodeName, event_timestamp.strftime(logger.Logger.LogTimeFormatInUTC)), TelemetryEventParam(CommonTelemetryEventSchema.EventTid, threading.current_thread().ident), TelemetryEventParam(CommonTelemetryEventSchema.EventPid, os.getpid()), TelemetryEventParam(CommonTelemetryEventSchema.TaskName, threading.current_thread().getName()), TelemetryEventParam(CommonTelemetryEventSchema.KeywordName, '')] if event.eventId == TELEMETRY_EVENT_EVENT_ID and event.providerId == TELEMETRY_EVENT_PROVIDER_ID: # Currently only the GuestAgentExtensionEvents has these columns, the other tables dont have them so skipping # this data in those tables. common_params.extend([TelemetryEventParam(GuestAgentExtensionEventsSchema.ExtensionType, event.file_type), TelemetryEventParam(GuestAgentExtensionEventsSchema.IsInternal, False)]) event.parameters.extend(common_params) event.parameters.extend(self._common_parameters) __event_logger__ = EventLogger() def get_event_logger(): return __event_logger__ def elapsed_milliseconds(utc_start): now = datetime.utcnow() if now < utc_start: return 0 d = now - utc_start return int(((d.days * 24 * 60 * 60 + d.seconds) * 1000) + \ (d.microseconds / 1000.0)) def report_event(op, is_success=True, message='', log_event=True): add_event(AGENT_NAME, version=str(CURRENT_VERSION), is_success=is_success, message=message, op=op, log_event=log_event) def report_periodic(delta, op, is_success=True, message=''): add_periodic(delta, AGENT_NAME, version=str(CURRENT_VERSION), is_success=is_success, message=message, op=op) def report_metric(category, counter, instance, value, log_event=False, reporter=__event_logger__): """ Send a telemetry event reporting a single instance of a performance counter. :param str category: The category of the metric (cpu, memory, etc) :param str counter: The name of the metric ("%idle", etc) :param str instance: For instanced metrics, the identifier of the instance. E.g. a disk drive name, a cpu core# :param value: The value of the metric :param bool log_event: If True, log the metric in the agent log as well :param EventLogger reporter: The EventLogger instance to which metric events should be sent """ if reporter.event_dir is None: logger.warn("Cannot report metric event -- Event reporter is not initialized.") message = "Metric {0}/{1} [{2}] = {3}".format(category, counter, instance, value) _log_event(AGENT_NAME, "METRIC", message, 0) return try: reporter.add_metric(category, counter, instance, float(value), log_event) except ValueError: logger.periodic_warn(logger.EVERY_HALF_HOUR, "[PERIODIC] Cannot cast the metric value. Details of the Metric - " "{0}/{1} [{2}] = {3}".format(category, counter, instance, value)) def initialize_event_logger_vminfo_common_parameters(protocol, reporter=__event_logger__): reporter.initialize_vminfo_common_parameters(protocol) def add_event(name=AGENT_NAME, op=WALAEventOperation.Unknown, is_success=True, duration=0, version=str(CURRENT_VERSION), message="", log_event=True, reporter=__event_logger__): if reporter.event_dir is None: logger.warn("Cannot add event -- Event reporter is not initialized.") _log_event(name, op, message, duration, is_success=is_success) return if should_emit_event(name, version, op, is_success): mark_event_status(name, version, op, is_success) reporter.add_event(name, op=op, is_success=is_success, duration=duration, version=str(version), message=message, log_event=log_event) def add_log_event(level, message, forced=False, reporter=__event_logger__): """ :param level: LoggerLevel of the log event :param message: Message :param forced: Force write the event even if send_logs_to_telemetry() is disabled (NOTE: Remove this flag once send_logs_to_telemetry() is enabled for all events) :param reporter: :return: """ if reporter.event_dir is None: return if not (forced or send_logs_to_telemetry()): return if level >= logger.LogLevel.WARNING: reporter.add_log_event(level, message) def add_periodic(delta, name, op=WALAEventOperation.Unknown, is_success=True, duration=0, version=str(CURRENT_VERSION), message="", log_event=True, force=False, reporter=__event_logger__): if reporter.event_dir is None: logger.warn("Cannot add periodic event -- Event reporter is not initialized.") _log_event(name, op, message, duration, is_success=is_success) return reporter.add_periodic(delta, name, op=op, is_success=is_success, duration=duration, version=str(version), message=message, log_event=log_event, force=force) def mark_event_status(name, version, op, status): if op in __event_status_operations__: __event_status__.mark_event_status(name, version, op, status) def should_emit_event(name, version, op, status): return \ op not in __event_status_operations__ or \ __event_status__ is None or \ not __event_status__.event_marked(name, version, op) or \ __event_status__.event_succeeded(name, version, op) != status def init_event_logger(event_dir): __event_logger__.event_dir = event_dir def init_event_status(status_dir): __event_status__.initialize(status_dir) def dump_unhandled_err(name): if hasattr(sys, 'last_type') and hasattr(sys, 'last_value') and \ hasattr(sys, 'last_traceback'): last_type = getattr(sys, 'last_type') last_value = getattr(sys, 'last_value') last_traceback = getattr(sys, 'last_traceback') error = traceback.format_exception(last_type, last_value, last_traceback) message = "".join(error) add_event(name, is_success=False, message=message, op=WALAEventOperation.UnhandledError) def enable_unhandled_err_dump(name): atexit.register(dump_unhandled_err, name) WALinuxAgent-2.9.1.1/azurelinuxagent/common/exception.py000066400000000000000000000170741446033677600233060ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # """ Defines all exceptions """ class ExitException(BaseException): """ Used to exit the agent's process """ def __init__(self, reason): super(ExitException, self).__init__() self.reason = reason class AgentUpgradeExitException(ExitException): """ Used to exit the agent's process due to Agent Upgrade """ class AgentError(Exception): """ Base class of agent error. """ def __init__(self, msg, inner=None): msg = u"[{0}] {1}".format(type(self).__name__, msg) if inner is not None: msg = u"{0}\nInner error: {1}".format(msg, inner) super(AgentError, self).__init__(msg) class AgentConfigError(AgentError): """ When configure file is not found or malformed. """ def __init__(self, msg=None, inner=None): super(AgentConfigError, self).__init__(msg, inner) class AgentMemoryExceededException(AgentError): """ When Agent memory limit reached. """ def __init__(self, msg=None, inner=None): super(AgentMemoryExceededException, self).__init__(msg, inner) class AgentNetworkError(AgentError): """ When network is not available. """ def __init__(self, msg=None, inner=None): super(AgentNetworkError, self).__init__(msg, inner) class CGroupsException(AgentError): """ Exception to classify any cgroups related issue. """ def __init__(self, msg=None, inner=None): super(CGroupsException, self).__init__(msg, inner) class ExtensionError(AgentError): """ When failed to execute an extension """ def __init__(self, msg=None, inner=None, code=-1): super(ExtensionError, self).__init__(msg, inner) self.code = code class ExtensionOperationError(ExtensionError): """ When the command times out or returns with a non-zero exit_code """ def __init__(self, msg=None, inner=None, code=-1, exit_code=-1): super(ExtensionOperationError, self).__init__(msg, inner) self.code = code self.exit_code = exit_code class ExtensionUpdateError(ExtensionError): """ Error raised when failed to update an extension """ class ExtensionDownloadError(ExtensionError): """ Error raised when failed to download and setup an extension """ class ExtensionsGoalStateError(ExtensionError): """ Error raised when the ExtensionsGoalState is malformed """ class ExtensionsConfigError(ExtensionsGoalStateError): """ Error raised when the ExtensionsConfig is malformed """ class MultiConfigExtensionEnableError(ExtensionError): """ Error raised when enable for a Multi-Config extension is failing. """ class ProvisionError(AgentError): """ When provision failed """ def __init__(self, msg=None, inner=None): super(ProvisionError, self).__init__(msg, inner) class ResourceDiskError(AgentError): """ Mount resource disk failed """ def __init__(self, msg=None, inner=None): super(ResourceDiskError, self).__init__(msg, inner) class DhcpError(AgentError): """ Failed to handle dhcp response """ def __init__(self, msg=None, inner=None): super(DhcpError, self).__init__(msg, inner) class OSUtilError(AgentError): """ Failed to perform operation to OS configuration """ def __init__(self, msg=None, inner=None): super(OSUtilError, self).__init__(msg, inner) class ProtocolError(AgentError): """ Azure protocol error """ def __init__(self, msg=None, inner=None): super(ProtocolError, self).__init__(msg, inner) class ProtocolNotFoundError(ProtocolError): """ Error raised when Azure protocol endpoint not found """ class HttpError(AgentError): """ Http request failure """ def __init__(self, msg=None, inner=None): super(HttpError, self).__init__(msg, inner) class InvalidContainerError(HttpError): """ Error raised when Container id sent in the header is invalid """ class EventError(AgentError): """ Event reporting error """ def __init__(self, msg=None, inner=None): super(EventError, self).__init__(msg, inner) class CryptError(AgentError): """ Encrypt/Decrypt error """ def __init__(self, msg=None, inner=None): super(CryptError, self).__init__(msg, inner) class UpdateError(AgentError): """ Update Guest Agent error """ def __init__(self, msg=None, inner=None): super(UpdateError, self).__init__(msg, inner) class ResourceGoneError(HttpError): """ The requested resource no longer exists (i.e., status code 410) """ def __init__(self, msg=None, inner=None): if msg is None: msg = "Resource is gone" super(ResourceGoneError, self).__init__(msg, inner) class InvalidExtensionEventError(AgentError): """ Error thrown when the extension telemetry event is invalid as defined per the contract with extensions. """ # Types of InvalidExtensionEventError MissingKeyError = "MissingKeyError" EmptyMessageError = "EmptyMessageError" OversizeEventError = "OversizeEventError" def __init__(self, msg=None, inner=None): super(InvalidExtensionEventError, self).__init__(msg, inner) class ServiceStoppedError(AgentError): """ Error thrown when trying to access a Service which is stopped """ def __init__(self, msg=None, inner=None): super(ServiceStoppedError, self).__init__(msg, inner) class ExtensionErrorCodes(object): """ Common Error codes used across by Compute RP for better understanding the cause and clarify common occurring errors """ # Unknown Failures PluginUnknownFailure = -1 # Success PluginSuccess = 0 # Catch all error code. PluginProcessingError = 1000 # Plugin failed to download PluginManifestDownloadError = 1001 # Cannot find or load successfully the HandlerManifest.json PluginHandlerManifestNotFound = 1002 # Cannot successfully serialize the HandlerManifest.json PluginHandlerManifestDeserializationError = 1003 # Cannot download the plugin package PluginPackageDownloadFailed = 1004 # Cannot extract the plugin form package PluginPackageExtractionFailed = 1005 # Install failed PluginInstallProcessingFailed = 1007 # Update failed PluginUpdateProcessingFailed = 1008 # Enable failed PluginEnableProcessingFailed = 1009 # Disable failed PluginDisableProcessingFailed = 1010 # Extension script timed out PluginHandlerScriptTimedout = 1011 # Invalid status file of the extension. PluginSettingsStatusInvalid = 1012 def __init__(self): pass class GoalStateAggregateStatusCodes(object): # Success Success = 0 # Unknown failure GoalStateUnknownFailure = -1 # The goal state requires features that are not supported by this version of the VM agent GoalStateUnsupportedRequiredFeatures = 2001 WALinuxAgent-2.9.1.1/azurelinuxagent/common/future.py000066400000000000000000000144671446033677600226250ustar00rootroot00000000000000import contextlib import platform import sys import os import re # Note broken dependency handling to avoid potential backward # compatibility issues on different distributions try: import distro # pylint: disable=E0401 except Exception: pass # pylint: disable=W0105 """ Add alias for python2 and python3 libs and functions. """ # pylint: enable=W0105 if sys.version_info[0] == 3: import http.client as httpclient # pylint: disable=W0611,import-error from urllib.parse import urlparse # pylint: disable=W0611,import-error,no-name-in-module """Rename Python3 str to ustr""" # pylint: disable=W0105 ustr = str bytebuffer = memoryview # We aren't using these imports in this file, but we want them to be available # to import from this module in others. # Additionally, python2 doesn't have this, so we need to disable import-error # as well. # unused-import, import-error Disabled: Due to backward compatibility between py2 and py3 from builtins import int, range # pylint: disable=unused-import,import-error from collections import OrderedDict # pylint: disable=W0611 from queue import Queue, Empty # pylint: disable=W0611,import-error # unused-import Disabled: python2.7 doesn't have subprocess.DEVNULL # so this import is only used by python3. import subprocess # pylint: disable=unused-import elif sys.version_info[0] == 2: import httplib as httpclient # pylint: disable=E0401,W0611 from urlparse import urlparse # pylint: disable=E0401 from Queue import Queue, Empty # pylint: disable=W0611,import-error # We want to suppress the following: # - undefined-variable: # These builtins are not defined in python3 # - redefined-builtin: # This is intentional, so that code that wants to use builtins we're # assigning new names to doesn't need to check python versions before # doing so. # pylint: disable=undefined-variable,redefined-builtin ustr = unicode # Rename Python2 unicode to ustr bytebuffer = buffer range = xrange int = long if sys.version_info[1] >= 7: from collections import OrderedDict # For Py 2.7+ else: from ordereddict import OrderedDict # Works only on 2.6 # pylint: disable=E0401 else: raise ImportError("Unknown python version: {0}".format(sys.version_info)) def get_linux_distribution(get_full_name, supported_dists): """Abstract platform.linux_distribution() call which is deprecated as of Python 3.5 and removed in Python 3.7""" try: supported = platform._supported_dists + (supported_dists,) osinfo = list( platform.linux_distribution( # pylint: disable=W1505 full_distribution_name=get_full_name, supported_dists=supported ) ) # The platform.linux_distribution() lib has issue with detecting OpenWRT linux distribution. # Merge the following patch provided by OpenWRT as a temporary fix. if os.path.exists("/etc/openwrt_release"): osinfo = get_openwrt_platform() if not osinfo or osinfo == ['', '', '']: return get_linux_distribution_from_distro(get_full_name) full_name = platform.linux_distribution()[0].strip() # pylint: disable=W1505 osinfo.append(full_name) except AttributeError: return get_linux_distribution_from_distro(get_full_name) return osinfo def get_linux_distribution_from_distro(get_full_name): """Get the distribution information from the distro Python module.""" # If we get here we have to have the distro module, thus we do # not wrap the call in a try-except block as it would mask the problem # and result in a broken agent installation osinfo = list( distro.linux_distribution( full_distribution_name=get_full_name ) ) full_name = distro.linux_distribution()[0].strip() osinfo.append(full_name) # Fixing is the problem https://github.com/Azure/WALinuxAgent/issues/2715. Distro.linux_distribution method not retuning full version # If best is true, the most precise version number out of all examined sources is returned. if "mariner" in osinfo[0].lower(): osinfo[1] = distro.version(best=True) return osinfo def get_openwrt_platform(): """ Add this workaround for detecting OpenWRT products because the version and product information is contained in the /etc/openwrt_release file. """ result = [None, None, None] openwrt_version = re.compile(r"^DISTRIB_RELEASE=['\"](\d+\.\d+.\d+)['\"]") openwrt_product = re.compile(r"^DISTRIB_ID=['\"]([\w-]+)['\"]") with open('/etc/openwrt_release', 'r') as fh: content = fh.readlines() for line in content: version_matches = openwrt_version.match(line) product_matches = openwrt_product.match(line) if version_matches: result[1] = version_matches.group(1) elif product_matches: if product_matches.group(1) == "OpenWrt": result[0] = "openwrt" return result def is_file_not_found_error(exception): # pylint for python2 complains, but FileNotFoundError is # defined for python3. # pylint: disable=undefined-variable if sys.version_info[0] == 2: # Python 2 uses OSError(errno=2) return isinstance(exception, OSError) and exception.errno == 2 elif sys.version_info[0] == 3: return isinstance(exception, FileNotFoundError) return isinstance(exception, FileNotFoundError) @contextlib.contextmanager def subprocess_dev_null(): if sys.version_info[0] == 3: # Suppress no-member errors on python2.7 yield subprocess.DEVNULL # pylint: disable=no-member else: try: devnull = open(os.devnull, "a+") yield devnull except Exception: yield None finally: if devnull is not None: devnull.close() def array_to_bytes(buff): # Python 3.9 removed the tostring() method on arrays, the new alias is tobytes() if sys.version_info[0] == 2: return buff.tostring() if sys.version_info[0] == 3 and sys.version_info[1] <= 8: return buff.tostring() return buff.tobytes() WALinuxAgent-2.9.1.1/azurelinuxagent/common/interfaces.py000066400000000000000000000027561446033677600234340ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2020 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # class ThreadHandlerInterface(object): """ Interface for all thread handlers created and maintained by the GuestAgent. """ @staticmethod def get_thread_name(): raise NotImplementedError("get_thread_name() not implemented") def run(self): raise NotImplementedError("run() not implemented") def keep_alive(self): """ Returns true if the thread handler should be restarted when the thread dies and false when it should remain dead. Defaults to True and can be overridden by sub-classes. """ return True def is_alive(self): raise NotImplementedError("is_alive() not implemented") def start(self): raise NotImplementedError("start() not implemented") def stop(self): raise NotImplementedError("stop() not implemented")WALinuxAgent-2.9.1.1/azurelinuxagent/common/logcollector.py000066400000000000000000000433031446033677600237720ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2020 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import glob import logging import os import subprocess import time import zipfile from datetime import datetime from heapq import heappush, heappop from azurelinuxagent.common.cgroup import CpuCgroup, AGENT_LOG_COLLECTOR, MemoryCgroup from azurelinuxagent.common.conf import get_lib_dir, get_ext_log_dir, get_agent_log_file from azurelinuxagent.common.event import initialize_event_logger_vminfo_common_parameters from azurelinuxagent.common.future import ustr from azurelinuxagent.common.logcollector_manifests import MANIFEST_NORMAL, MANIFEST_FULL # Please note: be careful when adding agent dependencies in this module. # This module uses its own logger and logs to its own file, not to the agent log. from azurelinuxagent.common.protocol.goal_state import GoalStateProperties from azurelinuxagent.common.protocol.util import get_protocol_util _EXTENSION_LOG_DIR = get_ext_log_dir() _AGENT_LIB_DIR = get_lib_dir() _AGENT_LOG = get_agent_log_file() _LOG_COLLECTOR_DIR = os.path.join(_AGENT_LIB_DIR, "logcollector") _TRUNCATED_FILES_DIR = os.path.join(_LOG_COLLECTOR_DIR, "truncated") OUTPUT_RESULTS_FILE_PATH = os.path.join(_LOG_COLLECTOR_DIR, "results.txt") COMPRESSED_ARCHIVE_PATH = os.path.join(_LOG_COLLECTOR_DIR, "logs.zip") CGROUPS_UNIT = "collect-logs.scope" GRACEFUL_KILL_ERRCODE = 3 INVALID_CGROUPS_ERRCODE = 2 _MUST_COLLECT_FILES = [ _AGENT_LOG, os.path.join(_AGENT_LIB_DIR, "waagent_status.json"), os.path.join(_AGENT_LIB_DIR, "history", "*.zip"), os.path.join(_EXTENSION_LOG_DIR, "*", "*"), os.path.join(_EXTENSION_LOG_DIR, "*", "*", "*"), "{0}.*".format(_AGENT_LOG) # any additional waagent.log files (e.g., waagent.log.1.gz) ] _FILE_SIZE_LIMIT = 30 * 1024 * 1024 # 30 MB _UNCOMPRESSED_ARCHIVE_SIZE_LIMIT = 150 * 1024 * 1024 # 150 MB _LOGGER = logging.getLogger(__name__) class LogCollector(object): _TRUNCATED_FILE_PREFIX = "truncated_" def __init__(self, is_full_mode=False, cpu_cgroup_path=None, memory_cgroup_path=None): self._is_full_mode = is_full_mode self._manifest = MANIFEST_FULL if is_full_mode else MANIFEST_NORMAL self._must_collect_files = self._expand_must_collect_files() self._create_base_dirs() self._set_logger() self._initialize_telemetry() self.cgroups = self._set_resource_usage_cgroups(cpu_cgroup_path, memory_cgroup_path) @staticmethod def _mkdir(dirname): if not os.path.isdir(dirname): os.makedirs(dirname) @staticmethod def _reset_file(filepath): with open(filepath, "wb") as out_file: out_file.write("".encode("utf-8")) @staticmethod def _create_base_dirs(): LogCollector._mkdir(_LOG_COLLECTOR_DIR) LogCollector._mkdir(_TRUNCATED_FILES_DIR) @staticmethod def _set_logger(): _f_handler = logging.FileHandler(OUTPUT_RESULTS_FILE_PATH, encoding="utf-8") _f_format = logging.Formatter(fmt='%(asctime)s %(levelname)s %(message)s', datefmt=u'%Y-%m-%dT%H:%M:%SZ') _f_format.converter = time.gmtime _f_handler.setFormatter(_f_format) _LOGGER.addHandler(_f_handler) _LOGGER.setLevel(logging.INFO) @staticmethod def _set_resource_usage_cgroups(cpu_cgroup_path, memory_cgroup_path): cpu_cgroup = CpuCgroup(AGENT_LOG_COLLECTOR, cpu_cgroup_path) msg = "Started tracking cpu cgroup {0}".format(cpu_cgroup) _LOGGER.info(msg) cpu_cgroup.initialize_cpu_usage() memory_cgroup = MemoryCgroup(AGENT_LOG_COLLECTOR, memory_cgroup_path) msg = "Started tracking memory cgroup {0}".format(memory_cgroup) _LOGGER.info(msg) return [cpu_cgroup, memory_cgroup] @staticmethod def _initialize_telemetry(): protocol = get_protocol_util().get_protocol(init_goal_state=False) protocol.client.reset_goal_state(goal_state_properties=GoalStateProperties.RoleConfig | GoalStateProperties.HostingEnv) # Initialize the common parameters for telemetry events initialize_event_logger_vminfo_common_parameters(protocol) @staticmethod def _run_shell_command(command, stdout=subprocess.PIPE, log_output=False): """ Runs a shell command in a subprocess, logs any errors to the log file, enables changing the stdout stream, and logs the output of the command to the log file if indicated by the `log_output` parameter. :param command: Shell command to run :param stdout: Where to write the output of the command :param log_output: If true, log the command output to the log file """ def format_command(cmd): return " ".join(cmd) if isinstance(cmd, list) else command def _encode_command_output(output): return ustr(output, encoding="utf-8", errors="backslashreplace") try: process = subprocess.Popen(command, stdout=stdout, stderr=subprocess.PIPE, shell=False) stdout, stderr = process.communicate() return_code = process.returncode except Exception as e: error_msg = u"Command [{0}] raised unexpected exception: [{1}]".format(format_command(command), ustr(e)) _LOGGER.error(error_msg) return if return_code != 0: encoded_stdout = _encode_command_output(stdout) encoded_stderr = _encode_command_output(stderr) error_msg = "Command: [{0}], return code: [{1}], stdout: [{2}] stderr: [{3}]".format(format_command(command), return_code, encoded_stdout, encoded_stderr) _LOGGER.error(error_msg) return if log_output: msg = "Output of command [{0}]:\n{1}".format(format_command(command), _encode_command_output(stdout)) _LOGGER.info(msg) @staticmethod def _expand_must_collect_files(): # Match the regexes from the MUST_COLLECT_FILES list to existing file paths on disk. manifest = [] for path in _MUST_COLLECT_FILES: manifest.extend(sorted(glob.glob(path))) return manifest def _read_manifest(self): return self._manifest.splitlines() @staticmethod def _process_ll_command(folder): LogCollector._run_shell_command(["ls", "-alF", folder], log_output=True) @staticmethod def _process_echo_command(message): _LOGGER.info(message) @staticmethod def _process_copy_command(path): file_paths = glob.glob(path) for file_path in file_paths: _LOGGER.info(file_path) return file_paths @staticmethod def _convert_file_name_to_archive_name(file_name): # File name is the name of the file on disk, whereas archive name is the name of that same file in the archive. # For non-truncated files: /var/log/waagent.log on disk becomes var/log/waagent.log in archive # (leading separator is removed by the archive). # For truncated files: /var/lib/waagent/logcollector/truncated/var/log/syslog.1 on disk becomes # truncated_var_log_syslog.1 in the archive. if file_name.startswith(_TRUNCATED_FILES_DIR): original_file_path = file_name[len(_TRUNCATED_FILES_DIR):].lstrip(os.path.sep) archive_file_name = LogCollector._TRUNCATED_FILE_PREFIX + original_file_path.replace(os.path.sep, "_") return archive_file_name else: return file_name.lstrip(os.path.sep) @staticmethod def _remove_uncollected_truncated_files(files_to_collect): # After log collection is completed, see if there are any old truncated files which were not collected # and remove them since they probably won't be collected in the future. This is possible when the # original file got deleted, so there is no need to keep its truncated version anymore. truncated_files = os.listdir(_TRUNCATED_FILES_DIR) for file_path in truncated_files: full_path = os.path.join(_TRUNCATED_FILES_DIR, file_path) if full_path not in files_to_collect: if os.path.isfile(full_path): os.remove(full_path) @staticmethod def _expand_parameters(manifest_data): _LOGGER.info("Using %s as $LIB_DIR", _AGENT_LIB_DIR) _LOGGER.info("Using %s as $LOG_DIR", _EXTENSION_LOG_DIR) _LOGGER.info("Using %s as $AGENT_LOG", _AGENT_LOG) new_manifest = [] for line in manifest_data: new_line = line.replace("$LIB_DIR", _AGENT_LIB_DIR) new_line = new_line.replace("$LOG_DIR", _EXTENSION_LOG_DIR) new_line = new_line.replace("$AGENT_LOG", _AGENT_LOG) new_manifest.append(new_line) return new_manifest def _process_manifest_file(self): files_to_collect = set() data = self._read_manifest() manifest_entries = LogCollector._expand_parameters(data) for entry in manifest_entries: # The entry can be one of the four flavours: # 1) ll,/etc/udev/rules.d -- list out contents of the folder and store to results file # 2) echo,### Gathering Configuration Files ### -- print message to results file # 3) copy,/var/lib/waagent/provisioned -- add file to list of files to be collected # 4) diskinfo, -- ignore commands from manifest other than ll, echo, and copy for now contents = entry.split(",") if len(contents) != 2: # If it's not a comment or an empty line, it's a malformed entry if not entry.startswith("#") and len(entry.strip()) > 0: _LOGGER.error("Couldn't parse \"%s\"", entry) continue command, value = contents if command == "ll": self._process_ll_command(value) elif command == "echo": self._process_echo_command(value) elif command == "copy": files_to_collect.update(self._process_copy_command(value)) return files_to_collect @staticmethod def _truncate_large_file(file_path): # Truncate large file to size limit (keep freshest entries of the file), copy file to a temporary location # and update file path in list of files to collect try: # Binary files cannot be truncated, don't include large binary files ext = os.path.splitext(file_path)[1] if ext in [".gz", ".zip", ".xz"]: _LOGGER.warning("Discarding large binary file %s", file_path) return None truncated_file_path = os.path.join(_TRUNCATED_FILES_DIR, file_path.replace(os.path.sep, "_")) if os.path.exists(truncated_file_path): original_file_mtime = os.path.getmtime(file_path) truncated_file_mtime = os.path.getmtime(truncated_file_path) # If the original file hasn't been updated since the truncated file, it means there were no changes # and we don't need to truncate it again. if original_file_mtime < truncated_file_mtime: return truncated_file_path # Get the last N bytes of the file with open(truncated_file_path, "w+") as fh: LogCollector._run_shell_command(["tail", "-c", str(_FILE_SIZE_LIMIT), file_path], stdout=fh) return truncated_file_path except OSError as e: _LOGGER.error("Failed to truncate large file: %s", ustr(e)) return None def _get_file_priority(self, file_entry): # The sooner the file appears in the must collect list, the bigger its priority. # Priority is higher the lower the number (0 is highest priority). try: return self._must_collect_files.index(file_entry) except ValueError: # Doesn't matter, file is not in the must collect list, assign a low priority return 999999999 def _get_priority_files_list(self, file_list): # Given a list of files to collect, determine if they show up in the must collect list and build a priority # queue. The queue will determine the order in which the files are collected, highest priority files first. priority_file_queue = [] for file_entry in file_list: priority = self._get_file_priority(file_entry) heappush(priority_file_queue, (priority, file_entry)) return priority_file_queue def _get_final_list_for_archive(self, priority_file_queue): # Given a priority queue of files to collect, add one by one while the archive size is under the size limit. # If a single file is over the file size limit, truncate it before adding it to the archive. _LOGGER.info("### Preparing list of files to add to archive ###") total_uncompressed_size = 0 final_files_to_collect = [] while priority_file_queue: file_path = heappop(priority_file_queue)[1] # (priority, file_path) file_size = min(os.path.getsize(file_path), _FILE_SIZE_LIMIT) if total_uncompressed_size + file_size > _UNCOMPRESSED_ARCHIVE_SIZE_LIMIT: _LOGGER.warning("Archive too big, done with adding files.") break if os.path.getsize(file_path) <= _FILE_SIZE_LIMIT: final_files_to_collect.append(file_path) _LOGGER.info("Adding file %s, size %s b", file_path, file_size) else: truncated_file_path = self._truncate_large_file(file_path) if truncated_file_path: _LOGGER.info("Adding truncated file %s, size %s b", truncated_file_path, file_size) final_files_to_collect.append(truncated_file_path) total_uncompressed_size += file_size _LOGGER.info("Uncompressed archive size is %s b", total_uncompressed_size) return final_files_to_collect def _create_list_of_files_to_collect(self): # The final list of files to be collected by zip is created in three steps: # 1) Parse given manifest file, expanding wildcards and keeping a list of files that exist on disk # 2) Assign those files a priority depending on whether they are in the must collect file list. # 3) In priority order, add files to the final list to be collected, until the size of the archive is under # the size limit. parsed_file_paths = self._process_manifest_file() prioritized_file_paths = self._get_priority_files_list(parsed_file_paths) files_to_collect = self._get_final_list_for_archive(prioritized_file_paths) return files_to_collect def collect_logs_and_get_archive(self): """ Public method that collects necessary log files in a compressed zip archive. :return: Returns the path of the collected compressed archive """ files_to_collect = [] try: # Clear previous run's output and create base directories if they don't exist already. self._create_base_dirs() LogCollector._reset_file(OUTPUT_RESULTS_FILE_PATH) start_time = datetime.utcnow() _LOGGER.info("Starting log collection at %s", start_time.strftime("%Y-%m-%dT%H:%M:%SZ")) _LOGGER.info("Using log collection mode %s", "full" if self._is_full_mode else "normal") files_to_collect = self._create_list_of_files_to_collect() _LOGGER.info("### Creating compressed archive ###") compressed_archive = None try: compressed_archive = zipfile.ZipFile(COMPRESSED_ARCHIVE_PATH, "w", compression=zipfile.ZIP_DEFLATED) for file_to_collect in files_to_collect: archive_file_name = LogCollector._convert_file_name_to_archive_name(file_to_collect) compressed_archive.write(file_to_collect.encode("utf-8"), arcname=archive_file_name) compressed_archive_size = os.path.getsize(COMPRESSED_ARCHIVE_PATH) _LOGGER.info("Successfully compressed files. Compressed archive size is %s b", compressed_archive_size) end_time = datetime.utcnow() duration = end_time - start_time elapsed_ms = int(((duration.days * 24 * 60 * 60 + duration.seconds) * 1000) + (duration.microseconds / 1000.0)) _LOGGER.info("Finishing log collection at %s", end_time.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")) _LOGGER.info("Elapsed time: %s ms", elapsed_ms) compressed_archive.write(OUTPUT_RESULTS_FILE_PATH.encode("utf-8"), arcname="results.txt") finally: if compressed_archive is not None: compressed_archive.close() return COMPRESSED_ARCHIVE_PATH except Exception as e: msg = "Failed to collect logs: {0}".format(ustr(e)) _LOGGER.error(msg) raise finally: self._remove_uncollected_truncated_files(files_to_collect) WALinuxAgent-2.9.1.1/azurelinuxagent/common/logcollector_manifests.py000066400000000000000000000061561446033677600260500ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2020 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # MANIFEST_NORMAL = """echo,### Probing Directories ### ll,/var/log ll,$LIB_DIR echo,### Gathering Configuration Files ### copy,/etc/*-release copy,/etc/HOSTNAME copy,/etc/hostname copy,/etc/waagent.conf echo, echo,### Gathering Log Files ### copy,$AGENT_LOG* copy,/var/log/dmesg* copy,/var/log/syslog* copy,/var/log/auth* copy,$LOG_DIR/*/* copy,$LOG_DIR/*/*/* copy,$LOG_DIR/custom-script/handler.log echo, echo,### Gathering Extension Files ### copy,$LIB_DIR/ovf-env.xml copy,$LIB_DIR/waagent_status.json copy,$LIB_DIR/*/status/*.status copy,$LIB_DIR/*/config/*.settings copy,$LIB_DIR/*/config/HandlerState copy,$LIB_DIR/*/config/HandlerStatus copy,$LIB_DIR/error.json copy,$LIB_DIR/history/*.zip echo, """ MANIFEST_FULL = """echo,### Probing Directories ### ll,/var/log ll,$LIB_DIR ll,/etc/udev/rules.d echo,### Gathering Configuration Files ### copy,$LIB_DIR/provisioned copy,/etc/fstab copy,/etc/ssh/sshd_config copy,/boot/grub*/grub.c* copy,/boot/grub*/menu.lst copy,/etc/*-release copy,/etc/HOSTNAME copy,/etc/hostname copy,/etc/network/interfaces copy,/etc/network/interfaces.d/*.cfg copy,/etc/netplan/50-cloud-init.yaml copy,/etc/nsswitch.conf copy,/etc/resolv.conf copy,/run/systemd/resolve/stub-resolv.conf copy,/run/resolvconf/resolv.conf copy,/etc/sysconfig/iptables copy,/etc/sysconfig/network copy,/etc/sysconfig/network/ifcfg-eth* copy,/etc/sysconfig/network/routes copy,/etc/sysconfig/network-scripts/ifcfg-eth* copy,/etc/sysconfig/network-scripts/route-eth* copy,/etc/sysconfig/SuSEfirewall2 copy,/etc/ufw/ufw.conf copy,/etc/waagent.conf copy,/var/lib/dhcp/dhclient.eth0.leases copy,/var/lib/dhclient/dhclient-eth0.leases copy,/var/lib/wicked/lease-eth0-dhcp-ipv4.xml echo, echo,### Gathering Log Files ### copy,$AGENT_LOG* copy,/var/log/syslog* copy,/var/log/rsyslog* copy,/var/log/messages* copy,/var/log/kern* copy,/var/log/dmesg* copy,/var/log/dpkg* copy,/var/log/yum* copy,/var/log/cloud-init* copy,/var/log/boot* copy,/var/log/auth* copy,/var/log/secure* copy,$LOG_DIR/*/* copy,$LOG_DIR/*/*/* copy,$LOG_DIR/custom-script/handler.log copy,$LOG_DIR/run-command/handler.log echo, echo,### Gathering Extension Files ### copy,$LIB_DIR/ovf-env.xml copy,$LIB_DIR/*/status/*.status copy,$LIB_DIR/*/config/*.settings copy,$LIB_DIR/*/config/HandlerState copy,$LIB_DIR/*/config/HandlerStatus copy,$LIB_DIR/SharedConfig.xml copy,$LIB_DIR/ManagedIdentity-*.json copy,$LIB_DIR/*/error.json copy,$LIB_DIR/waagent_status.json copy,$LIB_DIR/history/*.zip echo, echo,### Gathering Disk Info ### diskinfo, """ WALinuxAgent-2.9.1.1/azurelinuxagent/common/logger.py000066400000000000000000000266561446033677600225750ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and openssl_bin 1.0+ # """ Log utils """ import sys from datetime import datetime, timedelta from threading import currentThread from azurelinuxagent.common.future import ustr EVERY_DAY = timedelta(days=1) EVERY_HALF_DAY = timedelta(hours=12) EVERY_SIX_HOURS = timedelta(hours=6) EVERY_HOUR = timedelta(hours=1) EVERY_HALF_HOUR = timedelta(minutes=30) EVERY_FIFTEEN_MINUTES = timedelta(minutes=15) EVERY_MINUTE = timedelta(minutes=1) class Logger(object): """ Logger class """ # This format is based on ISO-8601, Z represents UTC (Zero offset) LogTimeFormatInUTC = u'%Y-%m-%dT%H:%M:%S.%fZ' def __init__(self, logger=None, prefix=None): self.appenders = [] self.logger = self if logger is None else logger self.periodic_messages = {} self.prefix = prefix self.silent = False def reset_periodic(self): self.logger.periodic_messages = {} def set_prefix(self, prefix): self.prefix = prefix def _is_period_elapsed(self, delta, h): return h not in self.logger.periodic_messages or \ (self.logger.periodic_messages[h] + delta) <= datetime.now() def _periodic(self, delta, log_level_op, msg_format, *args): h = hash(msg_format) if self._is_period_elapsed(delta, h): log_level_op(msg_format, *args) self.logger.periodic_messages[h] = datetime.now() def periodic_info(self, delta, msg_format, *args): self._periodic(delta, self.info, msg_format, *args) def periodic_verbose(self, delta, msg_format, *args): self._periodic(delta, self.verbose, msg_format, *args) def periodic_warn(self, delta, msg_format, *args): self._periodic(delta, self.warn, msg_format, *args) def periodic_error(self, delta, msg_format, *args): self._periodic(delta, self.error, msg_format, *args) def verbose(self, msg_format, *args): self.log(LogLevel.VERBOSE, msg_format, *args) def info(self, msg_format, *args): self.log(LogLevel.INFO, msg_format, *args) def warn(self, msg_format, *args): self.log(LogLevel.WARNING, msg_format, *args) def error(self, msg_format, *args): self.log(LogLevel.ERROR, msg_format, *args) def log(self, level, msg_format, *args): def write_log(log_appender): # pylint: disable=W0612 """ The appender_lock flag is used to signal if the logger is currently in use. This prevents a subsequent log coming in due to writing of a log statement to be not written. Eg: Assuming a logger with two appenders - FileAppender and TelemetryAppender. Here is an example of how using appender_lock flag can help. logger.warn("foo") |- log.warn() (azurelinuxagent.common.logger.Logger.warn) |- log() (azurelinuxagent.common.logger.Logger.log) |- FileAppender.appender_lock is currently False not log_appender.appender_lock is True |- We sets it to True. |- FileAppender.write completes. |- FileAppender.appender_lock sets to False. |- TelemetryAppender.appender_lock is currently False not log_appender.appender_lock is True |- We sets it to True. [A] |- TelemetryAppender.write gets called but has an error and writes a log.warn("bar") |- log() (azurelinuxagent.common.logger.Logger.log) |- FileAppender.appender_lock is set to True (log_appender.appender_lock was false when entering). |- FileAppender.write completes. |- FileAppender.appender_lock sets to False. |- TelemetryAppender.appender_lock is already True, not log_appender.appender_lock is False Thus [A] cannot happen again if TelemetryAppender.write is not getting called. It prevents faulty appenders to not get called again and again. :param log_appender: Appender :return: None """ if not log_appender.appender_lock: try: log_appender.appender_lock = True log_appender.write(level, log_item) finally: log_appender.appender_lock = False if self.silent: return # if msg_format is not unicode convert it to unicode if type(msg_format) is not ustr: msg_format = ustr(msg_format, errors="backslashreplace") if len(args) > 0: msg = msg_format.format(*args) else: msg = msg_format time = datetime.utcnow().strftime(Logger.LogTimeFormatInUTC) level_str = LogLevel.STRINGS[level] thread_name = currentThread().getName() if self.prefix is not None: log_item = u"{0} {1} {2} {3} {4}\n".format(time, level_str, thread_name, self.prefix, msg) else: log_item = u"{0} {1} {2} {3}\n".format(time, level_str, thread_name, msg) log_item = ustr(log_item.encode('ascii', "backslashreplace"), encoding="ascii") for appender in self.appenders: appender.write(level, log_item) # # TODO: we should actually call # # write_log(appender) # # (see PR #1659). Before doing that, write_log needs to be thread-safe. # # This needs to be done when SEND_LOGS_TO_TELEMETRY is enabled. # if self.logger != self: for appender in self.logger.appenders: appender.write(level, log_item) # # TODO: call write_log instead (see comment above) # def add_appender(self, appender_type, level, path): appender = _create_logger_appender(appender_type, level, path) self.appenders.append(appender) def console_output_enabled(self): """ Returns True if the current list of appenders includes at least one ConsoleAppender """ return any(isinstance(appender, ConsoleAppender) for appender in self.appenders) def disable_console_output(self): """ Removes all ConsoleAppenders from the current list of appenders """ self.appenders = [appender for appender in self.appenders if not isinstance(appender, ConsoleAppender)] class Appender(object): def __init__(self, level): self.appender_lock = False self.level = level def write(self, level, msg): pass class ConsoleAppender(Appender): def __init__(self, level, path): super(ConsoleAppender, self).__init__(level) self.path = path def write(self, level, msg): if self.level <= level: try: with open(self.path, "w") as console: console.write(msg) except IOError: pass class FileAppender(Appender): def __init__(self, level, path): super(FileAppender, self).__init__(level) self.path = path def write(self, level, msg): if self.level <= level: try: with open(self.path, "a+") as log_file: log_file.write(msg) except IOError: pass class StdoutAppender(Appender): def __init__(self, level): # pylint: disable=W0235 super(StdoutAppender, self).__init__(level) def write(self, level, msg): if self.level <= level: try: sys.stdout.write(msg) except IOError: pass class TelemetryAppender(Appender): def __init__(self, level, event_func): super(TelemetryAppender, self).__init__(level) self.event_func = event_func def write(self, level, msg): if self.level <= level: try: self.event_func(level, msg) except IOError: pass # Initialize logger instance DEFAULT_LOGGER = Logger() class LogLevel(object): VERBOSE = 0 INFO = 1 WARNING = 2 ERROR = 3 STRINGS = [ "VERBOSE", "INFO", "WARNING", "ERROR" ] class AppenderType(object): FILE = 0 CONSOLE = 1 STDOUT = 2 TELEMETRY = 3 def add_logger_appender(appender_type, level=LogLevel.INFO, path=None): DEFAULT_LOGGER.add_appender(appender_type, level, path) def console_output_enabled(): return DEFAULT_LOGGER.console_output_enabled() def disable_console_output(): DEFAULT_LOGGER.disable_console_output() def reset_periodic(): DEFAULT_LOGGER.reset_periodic() def set_prefix(prefix): DEFAULT_LOGGER.set_prefix(prefix) def periodic_info(delta, msg_format, *args): """ The hash-map maintaining the state of the logs gets reset here - azurelinuxagent.ga.monitor.MonitorHandler.reset_loggers. The current time period is defined by RESET_LOGGERS_PERIOD. """ DEFAULT_LOGGER.periodic_info(delta, msg_format, *args) def periodic_verbose(delta, msg_format, *args): """ The hash-map maintaining the state of the logs gets reset here - azurelinuxagent.ga.monitor.MonitorHandler.reset_loggers. The current time period is defined by RESET_LOGGERS_PERIOD. """ DEFAULT_LOGGER.periodic_verbose(delta, msg_format, *args) def periodic_error(delta, msg_format, *args): """ The hash-map maintaining the state of the logs gets reset here - azurelinuxagent.ga.monitor.MonitorHandler.reset_loggers. The current time period is defined by RESET_LOGGERS_PERIOD. """ DEFAULT_LOGGER.periodic_error(delta, msg_format, *args) def periodic_warn(delta, msg_format, *args): """ The hash-map maintaining the state of the logs gets reset here - azurelinuxagent.ga.monitor.MonitorHandler.reset_loggers. The current time period is defined by RESET_LOGGERS_PERIOD. """ DEFAULT_LOGGER.periodic_warn(delta, msg_format, *args) def verbose(msg_format, *args): DEFAULT_LOGGER.verbose(msg_format, *args) def info(msg_format, *args): DEFAULT_LOGGER.info(msg_format, *args) def warn(msg_format, *args): DEFAULT_LOGGER.warn(msg_format, *args) def error(msg_format, *args): DEFAULT_LOGGER.error(msg_format, *args) def log(level, msg_format, *args): DEFAULT_LOGGER.log(level, msg_format, args) def _create_logger_appender(appender_type, level=LogLevel.INFO, path=None): if appender_type == AppenderType.CONSOLE: return ConsoleAppender(level, path) elif appender_type == AppenderType.FILE: return FileAppender(level, path) elif appender_type == AppenderType.STDOUT: return StdoutAppender(level) elif appender_type == AppenderType.TELEMETRY: return TelemetryAppender(level, path) else: raise ValueError("Unknown appender type") WALinuxAgent-2.9.1.1/azurelinuxagent/common/osutil/000077500000000000000000000000001446033677600222445ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/azurelinuxagent/common/osutil/__init__.py000066400000000000000000000012631446033677600243570ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # from azurelinuxagent.common.osutil.factory import get_osutil WALinuxAgent-2.9.1.1/azurelinuxagent/common/osutil/alpine.py000066400000000000000000000031631446033677600240710ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.shellutil as shellutil from azurelinuxagent.common.osutil.default import DefaultOSUtil class AlpineOSUtil(DefaultOSUtil): def __init__(self): super(AlpineOSUtil, self).__init__() self.agent_conf_file_path = '/etc/waagent.conf' self.jit_enabled = True def is_dhcp_enabled(self): return True def get_dhcp_pid(self): return self._get_dhcp_pid(["pidof", "dhcpcd"]) def restart_if(self, ifname, retries=None, wait=None): logger.info('restarting {} (sort of, actually SIGHUPing dhcpcd)'.format(ifname)) pid = self.get_dhcp_pid() if pid != None: ret = shellutil.run_get_output('kill -HUP {}'.format(pid)) # pylint: disable=W0612 def set_ssh_client_alive_interval(self): # Alpine will handle this. pass def conf_sshd(self, disable_password): # Alpine will handle this. pass WALinuxAgent-2.9.1.1/azurelinuxagent/common/osutil/arch.py000066400000000000000000000041611446033677600235350ustar00rootroot00000000000000# # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import azurelinuxagent.common.utils.shellutil as shellutil from azurelinuxagent.common.osutil.default import DefaultOSUtil class ArchUtil(DefaultOSUtil): def __init__(self): super(ArchUtil, self).__init__() self.jit_enabled = True @staticmethod def get_systemd_unit_file_install_path(): return "/usr/lib/systemd/system" @staticmethod def get_agent_bin_path(): return "/usr/bin" def is_dhcp_enabled(self): return True def start_network(self): return shellutil.run("systemctl start systemd-networkd", chk_err=False) def restart_if(self, ifname=None, retries=None, wait=None): shellutil.run("systemctl restart systemd-networkd") def restart_ssh_service(self): # SSH is socket activated on CoreOS. No need to restart it. pass def stop_dhcp_service(self): return shellutil.run("systemctl stop systemd-networkd", chk_err=False) def start_dhcp_service(self): return shellutil.run("systemctl start systemd-networkd", chk_err=False) def start_agent_service(self): return shellutil.run("systemctl start {0}".format(self.service_name), chk_err=False) def stop_agent_service(self): return shellutil.run("systemctl stop {0}".format(self.service_name), chk_err=False) def get_dhcp_pid(self): return self._get_dhcp_pid(["pidof", "systemd-networkd"]) def conf_sshd(self, disable_password): # Don't whack the system default sshd conf pass WALinuxAgent-2.9.1.1/azurelinuxagent/common/osutil/bigip.py000066400000000000000000000331731446033677600237170ustar00rootroot00000000000000# Copyright 2016 F5 Networks Inc. # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import array import fcntl import os import platform import re import socket import struct import time from azurelinuxagent.common.future import array_to_bytes try: # WAAgent > 2.1.3 import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.shellutil as shellutil from azurelinuxagent.common.exception import OSUtilError from azurelinuxagent.common.osutil.default import DefaultOSUtil except ImportError: # WAAgent <= 2.1.3 import azurelinuxagent.logger as logger import azurelinuxagent.utils.shellutil as shellutil from azurelinuxagent.exception import OSUtilError from azurelinuxagent.distro.default.osutil import DefaultOSUtil class BigIpOSUtil(DefaultOSUtil): def __init__(self): # pylint: disable=W0235 super(BigIpOSUtil, self).__init__() def _wait_until_mcpd_is_initialized(self): """Wait for mcpd to become available All configuration happens in mcpd so we need to wait that this is available before we go provisioning the system. I call this method at the first opportunity I have (during the DVD mounting call). This ensures that the rest of the provisioning does not need to wait for mcpd to be available unless it absolutely wants to. :return bool: Returns True upon success :raises OSUtilError: Raises exception if mcpd does not come up within roughly 50 minutes (100 * 30 seconds) """ for retries in range(1, 100): # pylint: disable=W0612 # Retry until mcpd completes startup: logger.info("Checking to see if mcpd is up") rc = shellutil.run("/usr/bin/tmsh -a show sys mcp-state field-fmt 2>/dev/null | grep phase | grep running", chk_err=False) if rc == 0: logger.info("mcpd is up!") break time.sleep(30) if rc == 0: return True raise OSUtilError( "mcpd hasn't completed initialization! Cannot proceed!" ) def _save_sys_config(self): cmd = "/usr/bin/tmsh save sys config" rc = shellutil.run(cmd) if rc != 0: logger.error("WARNING: Cannot save sys config on 1st boot.") return rc def restart_ssh_service(self): return shellutil.run("/usr/bin/bigstart restart sshd", chk_err=False) def stop_agent_service(self): return shellutil.run("/sbin/service {0} stop".format(self.service_name), chk_err=False) def start_agent_service(self): return shellutil.run("/sbin/service {0} start".format(self.service_name), chk_err=False) def register_agent_service(self): return shellutil.run("/sbin/chkconfig --add {0}".format(self.service_name), chk_err=False) def unregister_agent_service(self): return shellutil.run("/sbin/chkconfig --del {0}".format(self.service_name), chk_err=False) def get_dhcp_pid(self): return self._get_dhcp_pid(["/sbin/pidof", "dhclient"]) def set_hostname(self, hostname): """Set the static hostname of the device Normally, tmsh is used to set the hostname for the system. For our purposes at this time though, I would hesitate to trust this function. Azure(Stack) uses the name that you provide in the Web UI or ARM (for example) as the value of the hostname argument to this method. The problem is that there is nowhere in the UI that specifies the restrictions and checks that tmsh has for the hostname. For example, if you set the name "bigip1" in the Web UI, Azure(Stack) considers that a perfectly valid name. When WAAgent gets around to running though, tmsh will reject that value because it is not a fully qualified domain name. The proper value should have been bigip.xxx.yyy WAAgent will not fail if this command fails, but the hostname will not be what the user set either. Currently we do not set the hostname when WAAgent starts up, so I am passing on setting it here too. :param hostname: The hostname to set on the device """ return None def set_dhcp_hostname(self, hostname): """Sets the DHCP hostname See `set_hostname` for an explanation of why I pass here :param hostname: The hostname to set on the device """ return None def useradd(self, username, expiration=None, comment=None): """Create user account using tmsh Our policy is to create two accounts when booting a BIG-IP instance. The first account is the one that the user specified when they did the instance creation. The second one is the admin account that is, or should be, built in to the system. :param username: The username that you want to add to the system :param expiration: The expiration date to use. We do not use this value. :param comment: description of the account. We do not use this value. """ if self.get_userentry(username): logger.info("User {0} already exists, skip useradd", username) return None cmd = ['/usr/bin/tmsh', 'create', 'auth', 'user', username, 'partition-access', 'add', '{', 'all-partitions', '{', 'role', 'admin', '}', '}', 'shell', 'bash'] self._run_command_raising_OSUtilError(cmd, err_msg="Failed to create user account:{0}".format(username)) self._save_sys_config() return 0 def chpasswd(self, username, password, crypt_id=6, salt_len=10): """Change a user's password with tmsh Since we are creating the user specified account and additionally changing the password of the built-in 'admin' account, both must be modified in this method. Note that the default method also checks for a "system level" of the user; based on the value of UID_MIN in /etc/login.defs. In our env, all user accounts have the UID 0. So we can't rely on this value. :param username: The username whose password to change :param password: The unencrypted password to set for the user :param crypt_id: If encrypting the password, the crypt_id that was used :param salt_len: If encrypting the password, the length of the salt value used to do it. """ # Start by setting the password of the user provided account self._run_command_raising_OSUtilError( ['/usr/bin/tmsh', 'modify', 'auth', 'user', username, 'password', password], err_msg="Failed to set password for {0}".format(username)) # Next, set the password of the built-in 'admin' account to be have # the same password as the user provided account userentry = self.get_userentry('admin') if userentry is None: raise OSUtilError("The 'admin' user account was not found!") self._run_command_raising_OSUtilError( ['/usr/bin/tmsh', 'modify', 'auth', 'user', 'admin', 'password', password], err_msg="Failed to set password for admin") self._save_sys_config() return 0 def del_account(self, username): """Deletes a user account. Note that the default method also checks for a "system level" of the user; based on the value of UID_MIN in /etc/login.defs. In our env, all user accounts have the UID 0. So we can't rely on this value. We also don't use sudo, so we remove that method call as well. :param username: :return: """ self._run_command_without_raising(["touch", "/var/run/utmp"]) self._run_command_without_raising(['/usr/bin/tmsh', 'delete', 'auth', 'user', username]) def get_dvd_device(self, dev_dir='/dev'): """Find BIG-IP's CD/DVD device This device is almost certainly /dev/cdrom so I added the ? to this pattern. Note that this method will return upon the first device found, but in my tests with 12.1.1 it will also find /dev/sr0 on occasion. This is NOT the correct CD/DVD device though. :todo: Consider just always returning "/dev/cdrom" here if that device device exists on all platforms that are supported on Azure(Stack) :param dev_dir: The root directory from which to look for devices """ patten = r'(sr[0-9]|hd[c-z]|cdrom[0-9]?)' for dvd in [re.match(patten, dev) for dev in os.listdir(dev_dir)]: if dvd is not None: return "/dev/{0}".format(dvd.group(0)) raise OSUtilError("Failed to get dvd device") # The linter reports that this function's arguments differ from those # of the function this overrides. This doesn't seem to be a problem, however, # because this function accepts any option that could'be been specified for # the original (and, by forwarding the kwargs to the original, will reject any # option _not_ accepted by the original). Additionally, this method allows us # to keep the defaults for mount_dvd in one place (the original function) instead # of having to duplicate it here as well. def mount_dvd(self, **kwargs): # pylint: disable=W0221 """Mount the DVD containing the provisioningiso.iso file This is the _first_ hook that WAAgent provides for us, so this is the point where we should wait for mcpd to load. I am just overloading this method to add the mcpd wait. Then I proceed with the stock code. :param max_retry: Maximum number of retries waagent will make when mounting the provisioningiso.iso DVD :param chk_err: Whether to check for errors or not in the mounting commands """ self._wait_until_mcpd_is_initialized() return super(BigIpOSUtil, self).mount_dvd(**kwargs) def eject_dvd(self, chk_err=True): """Runs the eject command to eject the provisioning DVD BIG-IP does not include an eject command. It is sufficient to just umount the DVD disk. But I will log that we do not support this for future reference. :param chk_err: Whether or not to check for errors raised by the eject command """ logger.warn("Eject is not supported on this platform") def get_first_if(self): """Return the interface name, and ip addr of the management interface. We need to add a struct_size check here because, curiously, our 64bit platform is identified by python in Azure(Stack) as 32 bit and without adjusting the struct_size, we can't get the information we need. I believe this may be caused by only python i686 being shipped with BIG-IP instead of python x86_64?? """ iface = '' expected = 16 # how many devices should I expect... python_arc = platform.architecture()[0] if python_arc == '64bit': struct_size = 40 # for 64bit the size is 40 bytes else: struct_size = 32 # for 32bit the size is 32 bytes sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) buff = array.array('B', b'\0' * (expected * struct_size)) param = struct.pack('iL', expected*struct_size, buff.buffer_info()[0]) ret = fcntl.ioctl(sock.fileno(), 0x8912, param) retsize = (struct.unpack('iL', ret)[0]) if retsize == (expected * struct_size): logger.warn(('SIOCGIFCONF returned more than {0} up ' 'network interfaces.'), expected) sock = array_to_bytes(buff) for i in range(0, struct_size * expected, struct_size): iface = self._format_single_interface_name(sock, i) # Azure public was returning "lo:1" when deploying WAF if b'lo' in iface: continue else: break return iface.decode('latin-1'), socket.inet_ntoa(sock[i+20:i+24]) # pylint: disable=undefined-loop-variable def _format_single_interface_name(self, sock, offset): return sock[offset:offset+16].split(b'\0', 1)[0] def route_add(self, net, mask, gateway): """Add specified route using tmsh. :param net: :param mask: :param gateway: :return: """ cmd = ("/usr/bin/tmsh create net route " "{0}/{1} gw {2}").format(net, mask, gateway) return shellutil.run(cmd, chk_err=False) def device_for_ide_port(self, port_id): """Return device name attached to ide port 'n'. Include a wait in here because BIG-IP may not have yet initialized this list of devices. :param port_id: :return: """ for retries in range(1, 100): # pylint: disable=W0612 # Retry until devices are ready if os.path.exists("/sys/bus/vmbus/devices/"): break else: time.sleep(10) return super(BigIpOSUtil, self).device_for_ide_port(port_id) WALinuxAgent-2.9.1.1/azurelinuxagent/common/osutil/clearlinux.py000066400000000000000000000075351446033677600247760ustar00rootroot00000000000000# # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os # pylint: disable=W0611 import re # pylint: disable=W0611 import pwd # pylint: disable=W0611 import shutil # pylint: disable=W0611 import socket # pylint: disable=W0611 import array # pylint: disable=W0611 import struct # pylint: disable=W0611 import fcntl # pylint: disable=W0611 import time # pylint: disable=W0611 import base64 # pylint: disable=W0611 import errno import azurelinuxagent.common.conf as conf import azurelinuxagent.common.logger as logger # pylint: disable=W0611 import azurelinuxagent.common.utils.fileutil as fileutil import azurelinuxagent.common.utils.shellutil as shellutil import azurelinuxagent.common.utils.textutil as textutil # pylint: disable=W0611 from azurelinuxagent.common.osutil.default import DefaultOSUtil from azurelinuxagent.common.exception import OSUtilError class ClearLinuxUtil(DefaultOSUtil): def __init__(self): super(ClearLinuxUtil, self).__init__() self.agent_conf_file_path = '/usr/share/defaults/waagent/waagent.conf' self.jit_enabled = True @staticmethod def get_systemd_unit_file_install_path(): return "/usr/lib/systemd/system" @staticmethod def get_agent_bin_path(): return "/usr/bin" def is_dhcp_enabled(self): return True def start_network(self) : return shellutil.run("systemctl start systemd-networkd", chk_err=False) def restart_if(self, ifname=None, retries=None, wait=None): shellutil.run("systemctl restart systemd-networkd") def restart_ssh_service(self): # SSH is socket activated. No need to restart it. pass def stop_dhcp_service(self): return shellutil.run("systemctl stop systemd-networkd", chk_err=False) def start_dhcp_service(self): return shellutil.run("systemctl start systemd-networkd", chk_err=False) def start_agent_service(self): return shellutil.run("systemctl start {0}".format(self.service_name), chk_err=False) def stop_agent_service(self): return shellutil.run("systemctl stop {0}".format(self.service_name), chk_err=False) def get_dhcp_pid(self): return self._get_dhcp_pid(["pidof", "systemd-networkd"]) def conf_sshd(self, disable_password): # Don't whack the system default sshd conf pass def del_root_password(self): try: passwd_file_path = conf.get_passwd_file_path() try: passwd_content = fileutil.read_file(passwd_file_path) if not passwd_content: # Empty file is no better than no file raise IOError(errno.ENOENT, "Empty File", passwd_file_path) except (IOError, OSError) as file_read_err: if file_read_err.errno != errno.ENOENT: raise new_passwd = ["root:*LOCK*:14600::::::"] else: passwd = passwd_content.split('\n') new_passwd = [x for x in passwd if not x.startswith("root:")] new_passwd.insert(0, "root:*LOCK*:14600::::::") fileutil.write_file(passwd_file_path, "\n".join(new_passwd)) except IOError as e: raise OSUtilError("Failed to delete root password:{0}".format(e)) pass # pylint: disable=W0107 WALinuxAgent-2.9.1.1/azurelinuxagent/common/osutil/coreos.py000066400000000000000000000057201446033677600241140ustar00rootroot00000000000000# # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os from azurelinuxagent.common.utils import shellutil from azurelinuxagent.common.osutil.default import DefaultOSUtil class CoreOSUtil(DefaultOSUtil): def __init__(self): super(CoreOSUtil, self).__init__() self.agent_conf_file_path = '/usr/share/oem/waagent.conf' self.waagent_path = '/usr/share/oem/bin/waagent' self.python_path = '/usr/share/oem/python/bin' self.jit_enabled = True if 'PATH' in os.environ: path = "{0}:{1}".format(os.environ['PATH'], self.python_path) else: path = self.python_path os.environ['PATH'] = path if 'PYTHONPATH' in os.environ: py_path = os.environ['PYTHONPATH'] py_path = "{0}:{1}".format(py_path, self.waagent_path) else: py_path = self.waagent_path os.environ['PYTHONPATH'] = py_path @staticmethod def get_agent_bin_path(): return "/usr/share/oem/bin" def is_sys_user(self, username): # User 'core' is not a sysuser. if username == 'core': return False return super(CoreOSUtil, self).is_sys_user(username) def is_dhcp_enabled(self): return True def start_network(self): return shellutil.run("systemctl start systemd-networkd", chk_err=False) def restart_if(self, ifname=None, retries=None, wait=None): shellutil.run("systemctl restart systemd-networkd") def restart_ssh_service(self): # SSH is socket activated on CoreOS. No need to restart it. pass def stop_dhcp_service(self): return shellutil.run("systemctl stop systemd-networkd", chk_err=False) def start_dhcp_service(self): return shellutil.run("systemctl start systemd-networkd", chk_err=False) def start_agent_service(self): return shellutil.run("systemctl start {0}".format(self.service_name), chk_err=False) def stop_agent_service(self): return shellutil.run("systemctl stop {0}".format(self.service_name), chk_err=False) def get_dhcp_pid(self): return self._get_dhcp_pid( ["systemctl", "show", "-p", "MainPID", "systemd-networkd"], transform_command_output=lambda o: o.replace("MainPID=", "")) def conf_sshd(self, disable_password): # In CoreOS, /etc/sshd_config is mount readonly. Skip the setting. pass WALinuxAgent-2.9.1.1/azurelinuxagent/common/osutil/debian.py000066400000000000000000000052431446033677600240440ustar00rootroot00000000000000# # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os # pylint: disable=W0611 import re # pylint: disable=W0611 import pwd # pylint: disable=W0611 import shutil # pylint: disable=W0611 import socket # pylint: disable=W0611 import array # pylint: disable=W0611 import struct # pylint: disable=W0611 import fcntl # pylint: disable=W0611 import time # pylint: disable=W0611 import base64 # pylint: disable=W0611 import azurelinuxagent.common.logger as logger # pylint: disable=W0611 import azurelinuxagent.common.utils.fileutil as fileutil # pylint: disable=W0611 import azurelinuxagent.common.utils.shellutil as shellutil import azurelinuxagent.common.utils.textutil as textutil # pylint: disable=W0611 from azurelinuxagent.common.osutil.default import DefaultOSUtil class DebianOSBaseUtil(DefaultOSUtil): def __init__(self): super(DebianOSBaseUtil, self).__init__() self.jit_enabled = True def restart_ssh_service(self): return shellutil.run("systemctl --job-mode=ignore-dependencies try-reload-or-restart ssh", chk_err=False) def stop_agent_service(self): return shellutil.run("service azurelinuxagent stop", chk_err=False) def start_agent_service(self): return shellutil.run("service azurelinuxagent start", chk_err=False) def start_network(self): pass def remove_rules_files(self, rules_files=""): pass def restore_rules_files(self, rules_files=""): pass def get_dhcp_lease_endpoint(self): return self.get_endpoint_from_leases_path('/var/lib/dhcp/dhclient.*.leases') class DebianOSModernUtil(DebianOSBaseUtil): def __init__(self): super(DebianOSModernUtil, self).__init__() self.jit_enabled = True self.service_name = self.get_service_name() @staticmethod def get_service_name(): return "walinuxagent" def stop_agent_service(self): return shellutil.run("systemctl stop {0}".format(self.service_name), chk_err=False) def start_agent_service(self): return shellutil.run("systemctl start {0}".format(self.service_name), chk_err=False) WALinuxAgent-2.9.1.1/azurelinuxagent/common/osutil/default.py000066400000000000000000001712671446033677600242600ustar00rootroot00000000000000# # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import base64 import datetime import errno import fcntl import glob import json import multiprocessing import os import platform import pwd import re import shutil import socket import struct import sys import time from pwd import getpwall import array from azurelinuxagent.common import conf from azurelinuxagent.common import logger from azurelinuxagent.common.utils import fileutil from azurelinuxagent.common.utils import shellutil from azurelinuxagent.common.utils import textutil from azurelinuxagent.common.exception import OSUtilError from azurelinuxagent.common.future import ustr, array_to_bytes from azurelinuxagent.common.utils.cryptutil import CryptUtil from azurelinuxagent.common.utils.flexible_version import FlexibleVersion from azurelinuxagent.common.utils.networkutil import RouteEntry, NetworkInterfaceCard, AddFirewallRules from azurelinuxagent.common.utils.shellutil import CommandError __RULES_FILES__ = ["/lib/udev/rules.d/75-persistent-net-generator.rules", "/etc/udev/rules.d/70-persistent-net.rules"] """ Define distro specific behavior. OSUtil class defines default behavior for all distros. Each concrete distro classes could overwrite default behavior if needed. """ _IPTABLES_VERSION_PATTERN = re.compile("^[^\d\.]*([\d\.]+).*$") # pylint: disable=W1401 _IPTABLES_LOCKING_VERSION = FlexibleVersion('1.4.21') def _add_wait(wait, command): """ If 'wait' is True, adds the wait option (-w) to the given iptables command line """ if wait: command.insert(1, "-w") return command def get_iptables_version_command(): return ["iptables", "--version"] def get_firewall_list_command(wait): return _add_wait(wait, ["iptables", "-t", "security", "-L", "-nxv"]) def get_firewall_packets_command(wait): return _add_wait(wait, ["iptables", "-t", "security", "-L", "OUTPUT", "--zero", "OUTPUT", "-nxv"]) # Precisely delete the rules created by the agent. # this rule was used <= 2.2.25. This rule helped to validate our change, and determine impact. def get_firewall_delete_conntrack_accept_command(wait, destination): return _add_wait(wait, ["iptables", "-t", "security", AddFirewallRules.DELETE_COMMAND, "OUTPUT", "-d", destination, "-p", "tcp", "-m", "conntrack", "--ctstate", "INVALID,NEW", "-j", "ACCEPT"]) def get_delete_accept_tcp_rule(wait, destination): return AddFirewallRules.get_accept_tcp_rule(AddFirewallRules.DELETE_COMMAND, destination, wait=wait) def get_firewall_delete_owner_accept_command(wait, destination, owner_uid): return _add_wait(wait, ["iptables", "-t", "security", AddFirewallRules.DELETE_COMMAND, "OUTPUT", "-d", destination, "-p", "tcp", "-m", "owner", "--uid-owner", str(owner_uid), "-j", "ACCEPT"]) def get_firewall_delete_conntrack_drop_command(wait, destination): return _add_wait(wait, ["iptables", "-t", "security", AddFirewallRules.DELETE_COMMAND, "OUTPUT", "-d", destination, "-p", "tcp", "-m", "conntrack", "--ctstate", "INVALID,NEW", "-j", "DROP"]) PACKET_PATTERN = "^\s*(\d+)\s+(\d+)\s+DROP\s+.*{0}[^\d]*$" # pylint: disable=W1401 ALL_CPUS_REGEX = re.compile('^cpu .*') ALL_MEMS_REGEX = re.compile('^Mem.*') _enable_firewall = True DMIDECODE_CMD = 'dmidecode --string system-uuid' PRODUCT_ID_FILE = '/sys/class/dmi/id/product_uuid' UUID_PATTERN = re.compile( r'^\s*[A-F0-9]{8}(?:\-[A-F0-9]{4}){3}\-[A-F0-9]{12}\s*$', re.IGNORECASE) IOCTL_SIOCGIFCONF = 0x8912 IOCTL_SIOCGIFFLAGS = 0x8913 IOCTL_SIOCGIFHWADDR = 0x8927 IFNAMSIZ = 16 IP_COMMAND_OUTPUT = re.compile('^\d+:\s+(\w+):\s+(.*)$') # pylint: disable=W1401 STORAGE_DEVICE_PATH = '/sys/bus/vmbus/devices/' GEN2_DEVICE_ID = 'f8b3781a-1e82-4818-a1c3-63d806ec15bb' class DefaultOSUtil(object): def __init__(self): self.agent_conf_file_path = '/etc/waagent.conf' self.selinux = None self.disable_route_warning = False self.jit_enabled = False self.service_name = self.get_service_name() @staticmethod def get_service_name(): return "waagent" @staticmethod def get_systemd_unit_file_install_path(): return "/lib/systemd/system" @staticmethod def get_agent_bin_path(): return "/usr/sbin" def get_firewall_dropped_packets(self, dst_ip=None): # If a previous attempt failed, do not retry global _enable_firewall # pylint: disable=W0603 if not _enable_firewall: return 0 try: wait = self.get_firewall_will_wait() try: output = shellutil.run_command(get_firewall_packets_command(wait)) pattern = re.compile(PACKET_PATTERN.format(dst_ip)) for line in output.split('\n'): m = pattern.match(line) if m is not None: return int(m.group(1)) except Exception as e: if isinstance(e, CommandError) and (e.returncode == 3 or e.returncode == 4): # pylint: disable=E1101 # Transient error that we ignore returncode 3. This code fires every loop # of the daemon (60m), so we will get the value eventually. # ignore returncode 4 as temporary fix (RULE_REPLACE failed (Invalid argument)) return 0 logger.warn("Failed to get firewall packets: {0}", ustr(e)) return -1 return 0 except Exception as e: _enable_firewall = False logger.warn("Unable to retrieve firewall packets dropped" "{0}".format(ustr(e))) return -1 def get_firewall_will_wait(self): # Determine if iptables will serialize access try: output = shellutil.run_command(get_iptables_version_command()) except Exception as e: msg = "Unable to determine version of iptables: {0}".format(ustr(e)) logger.warn(msg) raise Exception(msg) m = _IPTABLES_VERSION_PATTERN.match(output) if m is None: msg = "iptables did not return version information: {0}".format(output) logger.warn(msg) raise Exception(msg) wait = "-w" \ if FlexibleVersion(m.group(1)) >= _IPTABLES_LOCKING_VERSION \ else "" return wait def _delete_rule(self, rule): """ Continually execute the delete operation until the return code is non-zero or the limit has been reached. """ for i in range(1, 100): # pylint: disable=W0612 try: rc = shellutil.run_command(rule) # pylint: disable=W0612 except CommandError as e: if e.returncode == 1: return if e.returncode == 2: raise Exception("invalid firewall deletion rule '{0}'".format(rule)) def remove_firewall(self, dst_ip, uid, wait): # If a previous attempt failed, do not retry global _enable_firewall # pylint: disable=W0603 if not _enable_firewall: return False try: # This rule was <= 2.2.25 only, and may still exist on some VMs. Until 2.2.25 # has aged out, keep this cleanup in place. self._delete_rule(get_firewall_delete_conntrack_accept_command(wait, dst_ip)) self._delete_rule(get_delete_accept_tcp_rule(wait, dst_ip)) self._delete_rule(get_firewall_delete_owner_accept_command(wait, dst_ip, uid)) self._delete_rule(get_firewall_delete_conntrack_drop_command(wait, dst_ip)) return True except Exception as e: _enable_firewall = False logger.info("Unable to remove firewall -- " "no further attempts will be made: " "{0}".format(ustr(e))) return False def remove_legacy_firewall_rule(self, dst_ip): # This function removes the legacy firewall rule that was added <= 2.2.25. # Not adding the global _enable_firewall check here as this will only be called once per service start and # we dont want the state of this call to affect other iptable calls. try: wait = self.get_firewall_will_wait() # This rule was <= 2.2.25 only, and may still exist on some VMs. Until 2.2.25 # has aged out, keep this cleanup in place. self._delete_rule(get_firewall_delete_conntrack_accept_command(wait, dst_ip)) except Exception as error: logger.info( "Unable to remove legacy firewall rule, won't try removing it again. Error: {0}".format(ustr(error))) def enable_firewall(self, dst_ip, uid): """ It checks if every iptable rule exists and add them if not present. It returns a tuple(enable firewall success status, update rules flag) enable firewall success status: Returns True if every firewall rule exists otherwise False update rules flag: Returns True if rules are updated otherwise False """ # This is to send telemetry when iptable rules updated is_firewall_rules_updated = False # If a previous attempt failed, do not retry global _enable_firewall # pylint: disable=W0603 if not _enable_firewall: return False, is_firewall_rules_updated try: wait = self.get_firewall_will_wait() # check every iptable rule and delete others if any rule is missing # and append every iptable rule to the end of the chain. try: if not AddFirewallRules.verify_iptables_rules_exist(wait, dst_ip, uid): self.remove_firewall(dst_ip, uid, wait) AddFirewallRules.add_iptables_rules(wait, dst_ip, uid) is_firewall_rules_updated = True except CommandError as e: if e.returncode == 2: self.remove_firewall(dst_ip, uid, wait) msg = "please upgrade iptables to a version that supports the -C option" logger.warn(msg) raise except Exception as error: logger.warn(ustr(error)) raise return True, is_firewall_rules_updated except Exception as e: _enable_firewall = False logger.info("Unable to establish firewall -- " "no further attempts will be made: " "{0}".format(ustr(e))) return False, is_firewall_rules_updated def get_firewall_list(self, wait=None): try: if wait is None: wait = self.get_firewall_will_wait() output = shellutil.run_command(get_firewall_list_command(wait)) return output except Exception as e: logger.warn("Listing firewall rules failed: {0}".format(ustr(e))) return "" @staticmethod def _correct_instance_id(instance_id): """ Azure stores the instance ID with an incorrect byte ordering for the first parts. For example, the ID returned by the metadata service: D0DF4C54-4ECB-4A4B-9954-5BDF3ED5C3B8 will be found as: 544CDFD0-CB4E-4B4A-9954-5BDF3ED5C3B8 This code corrects the byte order such that it is consistent with that returned by the metadata service. """ if not UUID_PATTERN.match(instance_id): return instance_id parts = instance_id.split('-') return '-'.join([ textutil.swap_hexstring(parts[0], width=2), textutil.swap_hexstring(parts[1], width=2), textutil.swap_hexstring(parts[2], width=2), parts[3], parts[4] ]) def is_current_instance_id(self, id_that): """ Compare two instance IDs for equality, but allow that some IDs may have been persisted using the incorrect byte ordering. """ id_this = self.get_instance_id() logger.verbose("current instance id: {0}".format(id_this)) logger.verbose(" former instance id: {0}".format(id_that)) return id_this.lower() == id_that.lower() or \ id_this.lower() == self._correct_instance_id(id_that).lower() def get_agent_conf_file_path(self): return self.agent_conf_file_path def get_instance_id(self): """ Azure records a UUID as the instance ID First check /sys/class/dmi/id/product_uuid. If that is missing, then extracts from dmidecode If nothing works (for old VMs), return the empty string """ if os.path.isfile(PRODUCT_ID_FILE): s = fileutil.read_file(PRODUCT_ID_FILE).strip() else: rc, s = shellutil.run_get_output(DMIDECODE_CMD) if rc != 0 or UUID_PATTERN.match(s) is None: return "" return self._correct_instance_id(s.strip()) @staticmethod def get_userentry(username): try: return pwd.getpwnam(username) except KeyError: return None def is_sys_user(self, username): """ Check whether use is a system user. If reset sys user is allowed in conf, return False Otherwise, check whether UID is less than UID_MIN """ if conf.get_allow_reset_sys_user(): return False userentry = self.get_userentry(username) uidmin = None try: uidmin_def = fileutil.get_line_startingwith("UID_MIN", "/etc/login.defs") if uidmin_def is not None: uidmin = int(uidmin_def.split()[1]) except IOError as e: # pylint: disable=W0612 pass if uidmin == None: uidmin = 100 if userentry != None and userentry[2] < uidmin: return True else: return False def useradd(self, username, expiration=None, comment=None): """ Create user account with 'username' """ userentry = self.get_userentry(username) if userentry is not None: logger.info("User {0} already exists, skip useradd", username) return if expiration is not None: cmd = ["useradd", "-m", username, "-e", expiration] else: cmd = ["useradd", "-m", username] if comment is not None: cmd.extend(["-c", comment]) self._run_command_raising_OSUtilError(cmd, err_msg="Failed to create user account:{0}".format(username)) def chpasswd(self, username, password, crypt_id=6, salt_len=10): if self.is_sys_user(username): raise OSUtilError(("User {0} is a system user, " "will not set password.").format(username)) passwd_hash = textutil.gen_password_hash(password, crypt_id, salt_len) self._run_command_raising_OSUtilError(["usermod", "-p", passwd_hash, username], err_msg="Failed to set password for {0}".format(username)) def get_users(self): return getpwall() def conf_sudoer(self, username, nopasswd=False, remove=False): sudoers_dir = conf.get_sudoers_dir() sudoers_wagent = os.path.join(sudoers_dir, 'waagent') if not remove: # for older distros create sudoers.d if not os.path.isdir(sudoers_dir): # create the sudoers.d directory fileutil.mkdir(sudoers_dir) # add the include of sudoers.d to the /etc/sudoers sudoers_file = os.path.join(sudoers_dir, os.pardir, 'sudoers') include_sudoers_dir = "\n#includedir {0}\n".format(sudoers_dir) fileutil.append_file(sudoers_file, include_sudoers_dir) sudoer = None if nopasswd: sudoer = "{0} ALL=(ALL) NOPASSWD: ALL".format(username) else: sudoer = "{0} ALL=(ALL) ALL".format(username) if not os.path.isfile(sudoers_wagent) or \ fileutil.findstr_in_file(sudoers_wagent, sudoer) is False: fileutil.append_file(sudoers_wagent, "{0}\n".format(sudoer)) fileutil.chmod(sudoers_wagent, 0o440) else: # remove user from sudoers if os.path.isfile(sudoers_wagent): try: content = fileutil.read_file(sudoers_wagent) sudoers = content.split("\n") sudoers = [x for x in sudoers if username not in x] fileutil.write_file(sudoers_wagent, "\n".join(sudoers)) except IOError as e: raise OSUtilError("Failed to remove sudoer: {0}".format(e)) def del_root_password(self): try: passwd_file_path = conf.get_passwd_file_path() passwd_content = fileutil.read_file(passwd_file_path) passwd = passwd_content.split('\n') new_passwd = [x for x in passwd if not x.startswith("root:")] new_passwd.insert(0, "root:*LOCK*:14600::::::") fileutil.write_file(passwd_file_path, "\n".join(new_passwd)) except IOError as e: raise OSUtilError("Failed to delete root password:{0}".format(e)) @staticmethod def _norm_path(filepath): home = conf.get_home_dir() # Expand HOME variable if present in path path = os.path.normpath(filepath.replace("$HOME", home)) return path def deploy_ssh_keypair(self, username, keypair): """ Deploy id_rsa and id_rsa.pub """ path, thumbprint = keypair path = self._norm_path(path) dir_path = os.path.dirname(path) fileutil.mkdir(dir_path, mode=0o700, owner=username) lib_dir = conf.get_lib_dir() prv_path = os.path.join(lib_dir, thumbprint + '.prv') if not os.path.isfile(prv_path): raise OSUtilError("Can't find {0}.prv".format(thumbprint)) shutil.copyfile(prv_path, path) pub_path = path + '.pub' crytputil = CryptUtil(conf.get_openssl_cmd()) pub = crytputil.get_pubkey_from_prv(prv_path) fileutil.write_file(pub_path, pub) self.set_selinux_context(pub_path, 'unconfined_u:object_r:ssh_home_t:s0') self.set_selinux_context(path, 'unconfined_u:object_r:ssh_home_t:s0') os.chmod(path, 0o644) os.chmod(pub_path, 0o600) def openssl_to_openssh(self, input_file, output_file): cryptutil = CryptUtil(conf.get_openssl_cmd()) cryptutil.crt_to_ssh(input_file, output_file) def deploy_ssh_pubkey(self, username, pubkey): """ Deploy authorized_key """ path, thumbprint, value = pubkey if path is None: raise OSUtilError("Public key path is None") crytputil = CryptUtil(conf.get_openssl_cmd()) path = self._norm_path(path) dir_path = os.path.dirname(path) fileutil.mkdir(dir_path, mode=0o700, owner=username) if value is not None: if not value.startswith("ssh-"): raise OSUtilError("Bad public key: {0}".format(value)) if not value.endswith("\n"): value += "\n" fileutil.write_file(path, value) elif thumbprint is not None: lib_dir = conf.get_lib_dir() crt_path = os.path.join(lib_dir, thumbprint + '.crt') if not os.path.isfile(crt_path): raise OSUtilError("Can't find {0}.crt".format(thumbprint)) pub_path = os.path.join(lib_dir, thumbprint + '.pub') pub = crytputil.get_pubkey_from_crt(crt_path) fileutil.write_file(pub_path, pub) self.set_selinux_context(pub_path, 'unconfined_u:object_r:ssh_home_t:s0') self.openssl_to_openssh(pub_path, path) fileutil.chmod(pub_path, 0o600) else: raise OSUtilError("SSH public key Fingerprint and Value are None") self.set_selinux_context(path, 'unconfined_u:object_r:ssh_home_t:s0') fileutil.chowner(path, username) fileutil.chmod(path, 0o644) def is_selinux_system(self): """ Checks and sets self.selinux = True if SELinux is available on system. """ if self.selinux == None: if shellutil.run("which getenforce", chk_err=False) == 0: self.selinux = True else: self.selinux = False return self.selinux def is_selinux_enforcing(self): """ Calls shell command 'getenforce' and returns True if 'Enforcing'. """ if self.is_selinux_system(): output = shellutil.run_get_output("getenforce")[1] return output.startswith("Enforcing") else: return False def set_selinux_context(self, path, con): # pylint: disable=R1710 """ Calls shell 'chcon' with 'path' and 'con' context. Returns exit result. """ if self.is_selinux_system(): if not os.path.exists(path): logger.error("Path does not exist: {0}".format(path)) return 1 try: shellutil.run_command(['chcon', con, path], log_error=True) except shellutil.CommandError as cmd_err: return cmd_err.returncode return 0 def conf_sshd(self, disable_password): option = "no" if disable_password else "yes" conf_file_path = conf.get_sshd_conf_file_path() conf_file = fileutil.read_file(conf_file_path).split("\n") textutil.set_ssh_config(conf_file, "PasswordAuthentication", option) textutil.set_ssh_config(conf_file, "ChallengeResponseAuthentication", option) textutil.set_ssh_config(conf_file, "ClientAliveInterval", str(conf.get_ssh_client_alive_interval())) fileutil.write_file(conf_file_path, "\n".join(conf_file)) logger.info("{0} SSH password-based authentication methods." .format("Disabled" if disable_password else "Enabled")) logger.info("Configured SSH client probing to keep connections alive.") def get_dvd_device(self, dev_dir='/dev'): pattern = r'(sr[0-9]|hd[c-z]|cdrom[0-9]|cd[0-9]|vd[b-z])' device_list = os.listdir(dev_dir) for dvd in [re.match(pattern, dev) for dev in device_list]: if dvd is not None: return "/dev/{0}".format(dvd.group(0)) inner_detail = "The following devices were found, but none matched " \ "the pattern [{0}]: {1}\n".format(pattern, device_list) raise OSUtilError(msg="Failed to get dvd device from {0}".format(dev_dir), inner=inner_detail) def mount_dvd(self, max_retry=6, chk_err=True, dvd_device=None, mount_point=None, sleep_time=5): if dvd_device is None: dvd_device = self.get_dvd_device() if mount_point is None: mount_point = conf.get_dvd_mount_point() mount_list = shellutil.run_get_output("mount")[1] existing = self.get_mount_point(mount_list, dvd_device) if existing is not None: # already mounted logger.info("{0} is already mounted at {1}", dvd_device, existing) return if not os.path.isdir(mount_point): os.makedirs(mount_point) err = '' for retry in range(1, max_retry): return_code, err = self.mount(dvd_device, mount_point, option=["-o", "ro", "-t", "udf,iso9660,vfat"], chk_err=False) if return_code == 0: logger.info("Successfully mounted dvd") return else: logger.warn( "Mounting dvd failed [retry {0}/{1}, sleeping {2} sec]", retry, max_retry - 1, sleep_time) if retry < max_retry: time.sleep(sleep_time) if chk_err: raise OSUtilError("Failed to mount dvd device", inner=err) def umount_dvd(self, chk_err=True, mount_point=None): if mount_point is None: mount_point = conf.get_dvd_mount_point() return_code = self.umount(mount_point, chk_err=chk_err) if chk_err and return_code != 0: raise OSUtilError("Failed to unmount dvd device at {0}".format(mount_point)) def eject_dvd(self, chk_err=True): dvd = self.get_dvd_device() dev = dvd.rsplit('/', 1)[1] pattern = r'(vd[b-z])' # We should not eject if the disk is not a cdrom if re.search(pattern, dev): return try: shellutil.run_command(["eject", dvd]) except shellutil.CommandError as cmd_err: if chk_err: msg = "Failed to eject dvd: ret={0}\n[stdout]\n{1}\n\n[stderr]\n{2}"\ .format(cmd_err.returncode, cmd_err.stdout, cmd_err.stderr) raise OSUtilError(msg) def try_load_atapiix_mod(self): try: self.load_atapiix_mod() except Exception as e: logger.warn("Could not load ATAPI driver: {0}".format(e)) def load_atapiix_mod(self): if self.is_atapiix_mod_loaded(): return ret, kern_version = shellutil.run_get_output("uname -r") if ret != 0: raise Exception("Failed to call uname -r") mod_path = os.path.join('/lib/modules', kern_version.strip('\n'), 'kernel/drivers/ata/ata_piix.ko') if not os.path.isfile(mod_path): raise Exception("Can't find module file:{0}".format(mod_path)) ret, output = shellutil.run_get_output("insmod " + mod_path) # pylint: disable=W0612 if ret != 0: raise Exception("Error calling insmod for ATAPI CD-ROM driver") if not self.is_atapiix_mod_loaded(max_retry=3): raise Exception("Failed to load ATAPI CD-ROM driver") def is_atapiix_mod_loaded(self, max_retry=1): for retry in range(0, max_retry): ret = shellutil.run("lsmod | grep ata_piix", chk_err=False) if ret == 0: logger.info("Module driver for ATAPI CD-ROM is already present.") return True if retry < max_retry - 1: time.sleep(1) return False def mount(self, device, mount_point, option=None, chk_err=True): if not option: option = [] cmd = ["mount"] cmd.extend(option + [device, mount_point]) try: output = shellutil.run_command(cmd, log_error=chk_err) except shellutil.CommandError as cmd_err: detail = "[{0}] returned {1}:\n stdout: {2}\n\nstderr: {3}".format(cmd, cmd_err.returncode, cmd_err.stdout, cmd_err.stderr) return cmd_err.returncode, detail return 0, output def umount(self, mount_point, chk_err=True): try: shellutil.run_command(["umount", mount_point], log_error=chk_err) except shellutil.CommandError as cmd_err: return cmd_err.returncode return 0 def allow_dhcp_broadcast(self): # Open DHCP port if iptables is enabled. # We supress error logging on error. shellutil.run("iptables -D INPUT -p udp --dport 68 -j ACCEPT", chk_err=False) shellutil.run("iptables -I INPUT -p udp --dport 68 -j ACCEPT", chk_err=False) def remove_rules_files(self, rules_files=None): if rules_files is None: rules_files = __RULES_FILES__ lib_dir = conf.get_lib_dir() for src in rules_files: file_name = fileutil.base_name(src) dest = os.path.join(lib_dir, file_name) if os.path.isfile(dest): os.remove(dest) if os.path.isfile(src): logger.warn("Move rules file {0} to {1}", file_name, dest) shutil.move(src, dest) def restore_rules_files(self, rules_files=None): if rules_files is None: rules_files = __RULES_FILES__ lib_dir = conf.get_lib_dir() for dest in rules_files: filename = fileutil.base_name(dest) src = os.path.join(lib_dir, filename) if os.path.isfile(dest): continue if os.path.isfile(src): logger.warn("Move rules file {0} to {1}", filename, dest) shutil.move(src, dest) def get_mac_addr(self): """ Convenience function, returns mac addr bound to first non-loopback interface. """ ifname = self.get_if_name() addr = self.get_if_mac(ifname) return textutil.hexstr_to_bytearray(addr) def get_if_mac(self, ifname): """ Return the mac-address bound to the socket. """ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) param = struct.pack('256s', (ifname[:15] + ('\0' * 241)).encode('latin-1')) info = fcntl.ioctl(sock.fileno(), IOCTL_SIOCGIFHWADDR, param) sock.close() return ''.join(['%02X' % textutil.str_to_ord(char) for char in info[18:24]]) @staticmethod def _get_struct_ifconf_size(): """ Return the sizeof struct ifinfo. On 64-bit platforms the size is 40 bytes; on 32-bit platforms the size is 32 bytes. """ python_arc = platform.architecture()[0] struct_size = 32 if python_arc == '32bit' else 40 return struct_size def _get_all_interfaces(self): """ Return a dictionary mapping from interface name to IPv4 address. Interfaces without a name are ignored. """ expected = 16 # how many devices should I expect... struct_size = DefaultOSUtil._get_struct_ifconf_size() array_size = expected * struct_size buff = array.array('B', b'\0' * array_size) param = struct.pack('iL', array_size, buff.buffer_info()[0]) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) ret = fcntl.ioctl(sock.fileno(), IOCTL_SIOCGIFCONF, param) retsize = (struct.unpack('iL', ret)[0]) sock.close() if retsize == array_size: logger.warn(('SIOCGIFCONF returned more than {0} up ' 'network interfaces.'), expected) ifconf_buff = array_to_bytes(buff) ifaces = {} for i in range(0, array_size, struct_size): iface = ifconf_buff[i:i + IFNAMSIZ].split(b'\0', 1)[0] if len(iface) > 0: iface_name = iface.decode('latin-1') if iface_name not in ifaces: ifaces[iface_name] = socket.inet_ntoa(ifconf_buff[i + 20:i + 24]) return ifaces def get_first_if(self): """ Return the interface name, and IPv4 addr of the "primary" interface or, failing that, any active non-loopback interface. """ primary = self.get_primary_interface() ifaces = self._get_all_interfaces() if primary in ifaces: return primary, ifaces[primary] for iface_name in ifaces.keys(): if not self.is_loopback(iface_name): logger.info("Choosing non-primary [{0}]".format(iface_name)) return iface_name, ifaces[iface_name] return '', '' @staticmethod def _build_route_list(proc_net_route): """ Construct a list of network route entries :param list(str) proc_net_route: Route table lines, including headers, containing at least one route :return: List of network route objects :rtype: list(RouteEntry) """ idx = 0 column_index = {} header_line = proc_net_route[0] for header in filter(lambda h: len(h) > 0, header_line.split("\t")): column_index[header.strip()] = idx idx += 1 try: idx_iface = column_index["Iface"] idx_dest = column_index["Destination"] idx_gw = column_index["Gateway"] idx_flags = column_index["Flags"] idx_metric = column_index["Metric"] idx_mask = column_index["Mask"] except KeyError: msg = "/proc/net/route is missing key information; headers are [{0}]".format(header_line) logger.error(msg) return [] route_list = [] for entry in proc_net_route[1:]: route = entry.split("\t") if len(route) > 0: route_obj = RouteEntry(route[idx_iface], route[idx_dest], route[idx_gw], route[idx_mask], route[idx_flags], route[idx_metric]) route_list.append(route_obj) return route_list @staticmethod def read_route_table(): """ Return a list of strings comprising the route table, including column headers. Each line is stripped of leading or trailing whitespace but is otherwise unmolested. :return: Entries in the text route table :rtype: list(str) """ try: with open('/proc/net/route') as routing_table: return list(map(str.strip, routing_table.readlines())) except Exception as e: logger.error("Cannot read route table [{0}]", ustr(e)) return [] @staticmethod def get_list_of_routes(route_table): """ Construct a list of all network routes known to this system. :param list(str) route_table: List of text entries from route table, including headers :return: a list of network routes :rtype: list(RouteEntry) """ route_list = [] count = len(route_table) if count < 1: logger.error("/proc/net/route is missing headers") elif count == 1: logger.error("/proc/net/route contains no routes") else: route_list = DefaultOSUtil._build_route_list(route_table) return route_list def get_primary_interface(self): """ Get the name of the primary interface, which is the one with the default route attached to it; if there are multiple default routes, the primary has the lowest Metric. :return: the interface which has the default route """ # from linux/route.h RTF_GATEWAY = 0x02 DEFAULT_DEST = "00000000" primary_interface = None if not self.disable_route_warning: logger.info("Examine /proc/net/route for primary interface") route_table = DefaultOSUtil.read_route_table() def is_default(route): return route.destination == DEFAULT_DEST and int(route.flags) & RTF_GATEWAY == RTF_GATEWAY candidates = list(filter(is_default, DefaultOSUtil.get_list_of_routes(route_table))) if len(candidates) > 0: def get_metric(route): return int(route.metric) primary_route = min(candidates, key=get_metric) primary_interface = primary_route.interface if primary_interface is None: primary_interface = '' if not self.disable_route_warning: with open('/proc/net/route') as routing_table_fh: routing_table_text = routing_table_fh.read() logger.warn('Could not determine primary interface, ' 'please ensure /proc/net/route is correct') logger.warn('Contents of /proc/net/route:\n{0}'.format(routing_table_text)) logger.warn('Primary interface examination will retry silently') self.disable_route_warning = True else: logger.info('Primary interface is [{0}]'.format(primary_interface)) self.disable_route_warning = False return primary_interface def is_primary_interface(self, ifname): """ Indicate whether the specified interface is the primary. :param ifname: the name of the interface - eth0, lo, etc. :return: True if this interface binds the default route """ return self.get_primary_interface() == ifname def is_loopback(self, ifname): """ Determine if a named interface is loopback. """ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) ifname_buff = ifname + ('\0' * 256) result = fcntl.ioctl(s.fileno(), IOCTL_SIOCGIFFLAGS, ifname_buff) flags, = struct.unpack('H', result[16:18]) isloopback = flags & 8 == 8 if not self.disable_route_warning: logger.info('interface [{0}] has flags [{1}], ' 'is loopback [{2}]'.format(ifname, flags, isloopback)) s.close() return isloopback def get_dhcp_lease_endpoint(self): """ OS specific, this should return the decoded endpoint of the wireserver from option 245 in the dhcp leases file if it exists on disk. :return: The endpoint if available, or None """ return None @staticmethod def get_endpoint_from_leases_path(pathglob): """ Try to discover and decode the wireserver endpoint in the specified dhcp leases path. :param pathglob: The path containing dhcp lease files :return: The endpoint if available, otherwise None """ endpoint = None HEADER_LEASE = "lease" HEADER_OPTION_245 = "option unknown-245" HEADER_EXPIRE = "expire" FOOTER_LEASE = "}" FORMAT_DATETIME = "%Y/%m/%d %H:%M:%S" option_245_re = re.compile( r'\s*option\s+unknown-245\s+([0-9a-fA-F]+):([0-9a-fA-F]+):([0-9a-fA-F]+):([0-9a-fA-F]+);') logger.info("looking for leases in path [{0}]".format(pathglob)) for lease_file in glob.glob(pathglob): leases = open(lease_file).read() if HEADER_OPTION_245 in leases: cached_endpoint = None option_245_match = None expired = True # assume expired for line in leases.splitlines(): if line.startswith(HEADER_LEASE): cached_endpoint = None expired = True elif HEADER_EXPIRE in line: if "never" in line: expired = False else: try: expire_string = line.split(" ", 4)[-1].strip(";") expire_date = datetime.datetime.strptime(expire_string, FORMAT_DATETIME) if expire_date > datetime.datetime.utcnow(): expired = False except: # pylint: disable=W0702 logger.error("could not parse expiry token '{0}'".format(line)) elif FOOTER_LEASE in line: logger.info("dhcp entry:{0}, 245:{1}, expired:{2}".format( cached_endpoint, option_245_match is not None, expired)) if not expired and cached_endpoint is not None: endpoint = cached_endpoint logger.info("found endpoint [{0}]".format(endpoint)) # we want to return the last valid entry, so # keep searching else: option_245_match = option_245_re.match(line) if option_245_match is not None: cached_endpoint = '{0}.{1}.{2}.{3}'.format( int(option_245_match.group(1), 16), int(option_245_match.group(2), 16), int(option_245_match.group(3), 16), int(option_245_match.group(4), 16)) if endpoint is not None: logger.info("cached endpoint found [{0}]".format(endpoint)) else: logger.info("cached endpoint not found") return endpoint def is_missing_default_route(self): try: route_cmd = ["ip", "route", "show"] routes = shellutil.run_command(route_cmd) for route in routes.split("\n"): if route.startswith("0.0.0.0 ") or route.startswith("default "): return False return True except CommandError as e: logger.warn("Cannot get the routing table. {0} failed: {1}", ustr(route_cmd), ustr(e)) return False def get_if_name(self): if_name = '' if_found = False while not if_found: if_name = self.get_first_if()[0] if_found = len(if_name) >= 2 if not if_found: time.sleep(2) return if_name def get_ip4_addr(self): return self.get_first_if()[1] def set_route_for_dhcp_broadcast(self, ifname): try: route_cmd = ["ip", "route", "add", "255.255.255.255", "dev", ifname] return shellutil.run_command(route_cmd) except CommandError: return "" def remove_route_for_dhcp_broadcast(self, ifname): try: route_cmd = ["ip", "route", "del", "255.255.255.255", "dev", ifname] shellutil.run_command(route_cmd) except CommandError: pass def is_dhcp_available(self): return True def is_dhcp_enabled(self): return False def stop_dhcp_service(self): pass def start_dhcp_service(self): pass def start_network(self): pass def start_agent_service(self): pass def stop_agent_service(self): pass def register_agent_service(self): pass def unregister_agent_service(self): pass def restart_ssh_service(self): pass def route_add(self, net, mask, gateway): # pylint: disable=W0613 """ Add specified route """ try: cmd = ["ip", "route", "add", net, "via", gateway] return shellutil.run_command(cmd) except CommandError: return "" @staticmethod def _text_to_pid_list(text): return [int(n) for n in text.split()] @staticmethod def _get_dhcp_pid(command, transform_command_output=None): try: output = shellutil.run_command(command) if transform_command_output is not None: output = transform_command_output(output) return DefaultOSUtil._text_to_pid_list(output) except CommandError as exception: # pylint: disable=W0612 return [] def get_dhcp_pid(self): return self._get_dhcp_pid(["pidof", "dhclient"]) def set_hostname(self, hostname): fileutil.write_file('/etc/hostname', hostname) self._run_command_without_raising(["hostname", hostname], log_error=False) def set_dhcp_hostname(self, hostname): autosend = r'^[^#]*?send\s*host-name.*?(|gethostname[(,)])' dhclient_files = ['/etc/dhcp/dhclient.conf', '/etc/dhcp3/dhclient.conf', '/etc/dhclient.conf'] for conf_file in dhclient_files: if not os.path.isfile(conf_file): continue if fileutil.findre_in_file(conf_file, autosend): # Return if auto send host-name is configured return fileutil.update_conf_file(conf_file, 'send host-name', 'send host-name "{0}";'.format(hostname)) def restart_if(self, ifname, retries=3, wait=5): retry_limit = retries + 1 for attempt in range(1, retry_limit): return_code = shellutil.run("ifdown {0} && ifup {0}".format(ifname), expected_errors=[1] if attempt < retries else []) if return_code == 0: return logger.warn("failed to restart {0}: return code {1}".format(ifname, return_code)) if attempt < retry_limit: logger.info("retrying in {0} seconds".format(wait)) time.sleep(wait) else: logger.warn("exceeded restart retries") def publish_hostname(self, hostname): self.set_dhcp_hostname(hostname) self.set_hostname_record(hostname) ifname = self.get_if_name() self.restart_if(ifname) def set_scsi_disks_timeout(self, timeout): for dev in os.listdir("/sys/block"): if dev.startswith('sd'): self.set_block_device_timeout(dev, timeout) def set_block_device_timeout(self, dev, timeout): if dev is not None and timeout is not None: file_path = "/sys/block/{0}/device/timeout".format(dev) content = fileutil.read_file(file_path) original = content.splitlines()[0].rstrip() if original != timeout: fileutil.write_file(file_path, timeout) logger.info("Set block dev timeout: {0} with timeout: {1}", dev, timeout) def get_mount_point(self, mountlist, device): """ Example of mountlist: /dev/sda1 on / type ext4 (rw) proc on /proc type proc (rw) sysfs on /sys type sysfs (rw) devpts on /dev/pts type devpts (rw,gid=5,mode=620) tmpfs on /dev/shm type tmpfs (rw,rootcontext="system_u:object_r:tmpfs_t:s0") none on /proc/sys/fs/binfmt_misc type binfmt_misc (rw) /dev/sdb1 on /mnt/resource type ext4 (rw) """ if (mountlist and device): for entry in mountlist.split('\n'): if (re.search(device, entry)): tokens = entry.split() # Return the 3rd column of this line return tokens[2] if len(tokens) > 2 else None return None @staticmethod def _enumerate_device_id(): """ Enumerate all storage device IDs. Args: None Returns: Iterator[Tuple[str, str]]: VmBus and storage devices. """ if os.path.exists(STORAGE_DEVICE_PATH): for vmbus in os.listdir(STORAGE_DEVICE_PATH): deviceid = fileutil.read_file(os.path.join(STORAGE_DEVICE_PATH, vmbus, "device_id")) guid = deviceid.strip('{}\n') yield vmbus, guid @staticmethod def search_for_resource_disk(gen1_device_prefix, gen2_device_id): """ Search the filesystem for a device by ID or prefix. Args: gen1_device_prefix (str): Gen1 resource disk prefix. gen2_device_id (str): Gen2 resource device ID. Returns: str: The found device. """ device = None # We have to try device IDs for both Gen1 and Gen2 VMs. logger.info('Searching gen1 prefix {0} or gen2 {1}'.format(gen1_device_prefix, gen2_device_id)) try: for vmbus, guid in DefaultOSUtil._enumerate_device_id(): if guid.startswith(gen1_device_prefix) or guid == gen2_device_id: for root, dirs, files in os.walk(STORAGE_DEVICE_PATH + vmbus): # pylint: disable=W0612 root_path_parts = root.split('/') # For Gen1 VMs we only have to check for the block dir in the # current device. But for Gen2 VMs all of the disks (sda, sdb, # sr0) are presented in this device on the same SCSI controller. # Because of that we need to also read the LUN. It will be: # 0 - OS disk # 1 - Resource disk # 2 - CDROM if root_path_parts[-1] == 'block' and ( guid != gen2_device_id or root_path_parts[-2].split(':')[-1] == '1'): device = dirs[0] return device else: # older distros for d in dirs: if ':' in d and "block" == d.split(':')[0]: device = d.split(':')[1] return device except (OSError, IOError) as exc: logger.warn('Error getting device for {0} or {1}: {2}', gen1_device_prefix, gen2_device_id, ustr(exc)) return None def device_for_ide_port(self, port_id): """ Return device name attached to ide port 'n'. """ if port_id > 3: return None g0 = "00000000" if port_id > 1: g0 = "00000001" port_id = port_id - 2 gen1_device_prefix = '{0}-000{1}'.format(g0, port_id) device = DefaultOSUtil.search_for_resource_disk( gen1_device_prefix=gen1_device_prefix, gen2_device_id=GEN2_DEVICE_ID ) logger.info('Found device: {0}'.format(device)) return device def set_hostname_record(self, hostname): fileutil.write_file(conf.get_published_hostname(), contents=hostname) def get_hostname_record(self): hostname_record = conf.get_published_hostname() if not os.path.exists(hostname_record): # older agents (but newer or equal to 2.2.3) create published_hostname during provisioning; when provisioning is done # by cloud-init the hostname is written to set-hostname hostname = self._get_cloud_init_hostname() if hostname is None: logger.info("Retrieving hostname using socket.gethostname()") hostname = socket.gethostname() logger.info('Published hostname record does not exist, creating [{0}] with hostname [{1}]', hostname_record, hostname) self.set_hostname_record(hostname) record = fileutil.read_file(hostname_record) return record @staticmethod def _get_cloud_init_hostname(): """ Retrieves the hostname set by cloud-init; returns None if cloud-init did not set the hostname or if there is an error retrieving it. """ hostname_file = '/var/lib/cloud/data/set-hostname' try: if os.path.exists(hostname_file): # # The format is similar to # # $ cat /var/lib/cloud/data/set-hostname # { # "fqdn": "nam-u18", # "hostname": "nam-u18" # } # logger.info("Retrieving hostname from {0}", hostname_file) with open(hostname_file, 'r') as file_: hostname_info = json.load(file_) if "hostname" in hostname_info: return hostname_info["hostname"] except Exception as exception: logger.warn("Error retrieving hostname: {0}", ustr(exception)) return None def del_account(self, username): if self.is_sys_user(username): logger.error("{0} is a system user. Will not delete it.", username) self._run_command_without_raising(["touch", "/var/run/utmp"]) self._run_command_without_raising(['userdel', '-f', '-r', username]) self.conf_sudoer(username, remove=True) def decode_customdata(self, data): return base64.b64decode(data).decode('utf-8') def get_total_mem(self): # Get total memory in bytes and divide by 1024**2 to get the value in MB. return os.sysconf('SC_PAGE_SIZE') * os.sysconf('SC_PHYS_PAGES') / (1024 ** 2) def get_processor_cores(self): return multiprocessing.cpu_count() def check_pid_alive(self, pid): try: pid = int(pid) os.kill(pid, 0) except (ValueError, TypeError): return False except OSError as os_error: if os_error.errno == errno.EPERM: return True return False return True @property def is_64bit(self): return sys.maxsize > 2 ** 32 @staticmethod def _get_proc_stat(): """ Get the contents of /proc/stat. # cpu 813599 3940 909253 154538746 874851 0 6589 0 0 0 # cpu0 401094 1516 453006 77276738 452939 0 3312 0 0 0 # cpu1 412505 2423 456246 77262007 421912 0 3276 0 0 0 :return: A single string with the contents of /proc/stat :rtype: str """ results = None try: results = fileutil.read_file('/proc/stat') except (OSError, IOError) as ex: logger.warn("Couldn't read /proc/stat: {0}".format(ex.strerror)) raise return results @staticmethod def get_total_cpu_ticks_since_boot(): """ Compute the number of USER_HZ units of time that have elapsed in all categories, across all cores, since boot. :return: int """ system_cpu = 0 proc_stat = DefaultOSUtil._get_proc_stat() if proc_stat is not None: for line in proc_stat.splitlines(): if ALL_CPUS_REGEX.match(line): system_cpu = sum( int(i) for i in line.split()[1:8]) # see "man proc" for a description of these fields break return system_cpu @staticmethod def get_used_and_available_system_memory(): """ Get the contents of free -b in bytes. # free -b # total used free shared buff/cache available # Mem: 8340144128 619352064 5236809728 1499136 2483982336 7426314240 # Swap: 0 0 0 :return: used and available memory in megabytes """ used_mem = available_mem = 0 free_cmd = ["free", "-b"] memory = shellutil.run_command(free_cmd) for line in memory.split("\n"): if ALL_MEMS_REGEX.match(line): mems = line.split() used_mem = int(mems[2]) available_mem = int(mems[6]) # see "man free" for a description of these fields return used_mem/(1024 ** 2), available_mem/(1024 ** 2) def get_nic_state(self, as_string=False): """ Capture NIC state (IPv4 and IPv6 addresses plus link state). :return: By default returns a dictionary of NIC state objects, with the NIC name as key. If as_string is True returns the state as a string :rtype: dict(str,NetworkInformationCard) """ state = {} all_command = ["ip", "-a", "-o", "link"] inet_command = ["ip", "-4", "-a", "-o", "address"] inet6_command = ["ip", "-6", "-a", "-o", "address"] try: all_output = shellutil.run_command(all_command) except shellutil.CommandError as command_error: logger.verbose("Could not fetch NIC link info: {0}", ustr(command_error)) return "" if as_string else {} if as_string: def run_command(command): try: return shellutil.run_command(command) except shellutil.CommandError as command_error: return str(command_error) inet_output = run_command(inet_command) inet6_output = run_command(inet6_command) return "Executing {0}:\n{1}\nExecuting {2}:\n{3}\nExecuting {4}:\n{5}\n".format(all_command, all_output, inet_command, inet_output, inet6_command, inet6_output) else: self._update_nic_state_all(state, all_output) self._update_nic_state(state, inet_command, NetworkInterfaceCard.add_ipv4, "an IPv4 address") self._update_nic_state(state, inet6_command, NetworkInterfaceCard.add_ipv6, "an IPv6 address") return state @staticmethod def _update_nic_state_all(state, command_output): for entry in command_output.splitlines(): # Sample output: # 1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000\ link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 promiscuity 0 addrgenmode eui64 # 2: eth0: mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000\ link/ether 00:0d:3a:30:c3:5a brd ff:ff:ff:ff:ff:ff promiscuity 0 addrgenmode eui64 # 3: docker0: mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default \ link/ether 02:42:b5:d5:00:1d brd ff:ff:ff:ff:ff:ff promiscuity 0 \ bridge forward_delay 1500 hello_time 200 max_age 2000 ageing_time 30000 stp_state 0 priority 32768 vlan_filtering 0 vlan_protocol 802.1Q addrgenmode eui64 result = IP_COMMAND_OUTPUT.match(entry) if result: name = result.group(1) state[name] = NetworkInterfaceCard(name, result.group(2)) @staticmethod def _update_nic_state(state, ip_command, handler, description): """ Update the state of NICs based on the output of a specified ip subcommand. :param dict(str, NetworkInterfaceCard) state: Dictionary of NIC state objects :param str ip_command: The ip command to run :param handler: A method on the NetworkInterfaceCard class :param str description: Description of the particular information being added to the state """ try: output = shellutil.run_command(ip_command) for entry in output.splitlines(): # family inet sample output: # 1: lo inet 127.0.0.1/8 scope host lo\ valid_lft forever preferred_lft forever # 2: eth0 inet 10.145.187.220/26 brd 10.145.187.255 scope global eth0\ valid_lft forever preferred_lft forever # 3: docker0 inet 192.168.43.1/24 brd 192.168.43.255 scope global docker0\ valid_lft forever preferred_lft forever # # family inet6 sample output: # 1: lo inet6 ::1/128 scope host \ valid_lft forever preferred_lft forever # 2: eth0 inet6 fe80::20d:3aff:fe30:c35a/64 scope link \ valid_lft forever preferred_lft forever result = IP_COMMAND_OUTPUT.match(entry) if result: interface_name = result.group(1) if interface_name in state: handler(state[interface_name], result.group(2)) else: logger.error("Interface {0} has {1} but no link state".format(interface_name, description)) except shellutil.CommandError as command_error: logger.error("[{0}] failed: {1}", ' '.join(ip_command), str(command_error)) @staticmethod def _run_command_without_raising(cmd, log_error=True): try: shellutil.run_command(cmd, log_error=log_error) # Original implementation of run() does a blanket catch, so mimicking the behaviour here except Exception: pass @staticmethod def _run_multiple_commands_without_raising(commands, log_error=True, continue_on_error=False): for cmd in commands: try: shellutil.run_command(cmd, log_error=log_error) # Original implementation of run() does a blanket catch, so mimicking the behaviour here except Exception: if continue_on_error: continue break @staticmethod def _run_command_raising_OSUtilError(cmd, err_msg, cmd_input=None): # This method runs shell command using the new secure shellutil.run_command and raises OSUtilErrors on failures. try: return shellutil.run_command(cmd, log_error=True, input=cmd_input) except shellutil.CommandError as e: raise OSUtilError( "{0}, Retcode: {1}, Output: {2}, Error: {3}".format(err_msg, e.returncode, e.stdout, e.stderr)) except Exception as e: raise OSUtilError("{0}, Retcode: {1}, Error: {2}".format(err_msg, -1, ustr(e))) WALinuxAgent-2.9.1.1/azurelinuxagent/common/osutil/devuan.py000066400000000000000000000034531446033677600241050ustar00rootroot00000000000000# # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.shellutil as shellutil from azurelinuxagent.common.osutil.default import DefaultOSUtil class DevuanOSUtil(DefaultOSUtil): def __init__(self): super(DevuanOSUtil, self).__init__() self.jit_enabled = True def restart_ssh_service(self): logger.info("DevuanOSUtil::restart_ssh_service - trying to restart sshd") return shellutil.run("/usr/sbin/service restart ssh", chk_err=False) def stop_agent_service(self): logger.info("DevuanOSUtil::stop_agent_service - trying to stop waagent") return shellutil.run("/usr/sbin/service walinuxagent stop", chk_err=False) def start_agent_service(self): logger.info("DevuanOSUtil::start_agent_service - trying to start waagent") return shellutil.run("/usr/sbin/service walinuxagent start", chk_err=False) def start_network(self): pass def remove_rules_files(self, rules_files=""): pass def restore_rules_files(self, rules_files=""): pass def get_dhcp_lease_endpoint(self): return self.get_endpoint_from_leases_path('/var/lib/dhcp/dhclient.*.leases') WALinuxAgent-2.9.1.1/azurelinuxagent/common/osutil/factory.py000066400000000000000000000131751446033677600242740ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # from distutils.version import LooseVersion as Version # pylint: disable=no-name-in-module, import-error import azurelinuxagent.common.logger as logger from azurelinuxagent.common.version import DISTRO_NAME, DISTRO_CODE_NAME, DISTRO_VERSION, DISTRO_FULL_NAME from .alpine import AlpineOSUtil from .arch import ArchUtil from .bigip import BigIpOSUtil from .clearlinux import ClearLinuxUtil from .coreos import CoreOSUtil from .debian import DebianOSBaseUtil, DebianOSModernUtil from .default import DefaultOSUtil from .devuan import DevuanOSUtil from .freebsd import FreeBSDOSUtil from .gaia import GaiaOSUtil from .iosxe import IosxeOSUtil from .mariner import MarinerOSUtil from .nsbsd import NSBSDOSUtil from .openbsd import OpenBSDOSUtil from .openwrt import OpenWRTOSUtil from .redhat import RedhatOSUtil, Redhat6xOSUtil, RedhatOSModernUtil from .suse import SUSEOSUtil, SUSE11OSUtil from .photonos import PhotonOSUtil from .ubuntu import UbuntuOSUtil, Ubuntu12OSUtil, Ubuntu14OSUtil, \ UbuntuSnappyOSUtil, Ubuntu16OSUtil, Ubuntu18OSUtil from .fedora import FedoraOSUtil def get_osutil(distro_name=DISTRO_NAME, distro_code_name=DISTRO_CODE_NAME, distro_version=DISTRO_VERSION, distro_full_name=DISTRO_FULL_NAME): # We are adding another layer of abstraction here since we want to be able to mock the final result of the # function call. Since the get_osutil function is imported in various places in our tests, we can't mock # it globally. Instead, we add _get_osutil function and mock it in the test base class, AgentTestCase. return _get_osutil(distro_name, distro_code_name, distro_version, distro_full_name) def _get_osutil(distro_name, distro_code_name, distro_version, distro_full_name): if distro_name == "photonos": return PhotonOSUtil() if distro_name == "arch": return ArchUtil() if "Clear Linux" in distro_full_name: return ClearLinuxUtil() if distro_name == "ubuntu": if Version(distro_version) in [Version("12.04"), Version("12.10")]: return Ubuntu12OSUtil() if Version(distro_version) in [Version("14.04"), Version("14.10")]: return Ubuntu14OSUtil() if Version(distro_version) in [Version('16.04'), Version('16.10'), Version('17.04')]: return Ubuntu16OSUtil() if Version(distro_version) in [Version('18.04'), Version('18.10'), Version('19.04'), Version('19.10'), Version('20.04')]: return Ubuntu18OSUtil() if distro_full_name == "Snappy Ubuntu Core": return UbuntuSnappyOSUtil() return UbuntuOSUtil() if distro_name == "alpine": return AlpineOSUtil() if distro_name == "kali": return DebianOSBaseUtil() if distro_name in ("flatcar", "coreos") or distro_code_name in ("flatcar", "coreos"): return CoreOSUtil() if distro_name in ("suse", "sle_hpc", "sles", "opensuse"): if distro_full_name == 'SUSE Linux Enterprise Server' \ and Version(distro_version) < Version('12') \ or distro_full_name == 'openSUSE' and Version(distro_version) < Version('13.2'): return SUSE11OSUtil() return SUSEOSUtil() if distro_name == "debian": if "sid" in distro_version or Version(distro_version) > Version("7"): return DebianOSModernUtil() return DebianOSBaseUtil() # Devuan support only works with v4+ # Reason is that Devuan v4 (Chimaera) uses python v3.9, in which the # platform.linux_distribution module has been removed. This was unable # to distinguish between debian and devuan. The new distro.linux_distribution module # is able to distinguish between the two. if distro_name == "devuan" and Version(distro_version) >= Version("4"): return DevuanOSUtil() if distro_name in ("redhat", "rhel", "centos", "oracle", "almalinux", "cloudlinux", "rocky"): if Version(distro_version) < Version("7"): return Redhat6xOSUtil() if Version(distro_version) >= Version("8.6"): return RedhatOSModernUtil() return RedhatOSUtil() if distro_name == "euleros": return RedhatOSUtil() if distro_name == "uos": return RedhatOSUtil() if distro_name == "freebsd": return FreeBSDOSUtil() if distro_name == "openbsd": return OpenBSDOSUtil() if distro_name == "bigip": return BigIpOSUtil() if distro_name == "gaia": return GaiaOSUtil() if distro_name == "iosxe": return IosxeOSUtil() if distro_name == "mariner": return MarinerOSUtil() if distro_name == "nsbsd": return NSBSDOSUtil() if distro_name == "openwrt": return OpenWRTOSUtil() if distro_name == "fedora": return FedoraOSUtil() logger.warn("Unable to load distro implementation for {0}. Using default distro implementation instead.", distro_name) return DefaultOSUtil() WALinuxAgent-2.9.1.1/azurelinuxagent/common/osutil/fedora.py000066400000000000000000000045221446033677600240610ustar00rootroot00000000000000# # Copyright 2022 Red Hat Inc. # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import time import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.shellutil as shellutil from azurelinuxagent.common.osutil.default import DefaultOSUtil class FedoraOSUtil(DefaultOSUtil): def __init__(self): super(FedoraOSUtil, self).__init__() self.agent_conf_file_path = '/etc/waagent.conf' @staticmethod def get_systemd_unit_file_install_path(): return '/usr/lib/systemd/system' @staticmethod def get_agent_bin_path(): return '/usr/sbin' def is_dhcp_enabled(self): return True def start_network(self): pass def restart_if(self, ifname=None, retries=None, wait=None): retry_limit = retries+1 for attempt in range(1, retry_limit): return_code = shellutil.run("ip link set {0} down && ip link set {0} up".format(ifname)) if return_code == 0: return logger.warn("failed to restart {0}: return code {1}".format(ifname, return_code)) if attempt < retry_limit: logger.info("retrying in {0} seconds".format(wait)) time.sleep(wait) else: logger.warn("exceeded restart retries") def restart_ssh_service(self): shellutil.run('systemctl restart sshd') def stop_dhcp_service(self): pass def start_dhcp_service(self): pass def start_agent_service(self): return shellutil.run('systemctl start waagent', chk_err=False) def stop_agent_service(self): return shellutil.run('systemctl stop waagent', chk_err=False) def get_dhcp_pid(self): return self._get_dhcp_pid(["pidof", "dhclient"]) def conf_sshd(self, disable_password): pass WALinuxAgent-2.9.1.1/azurelinuxagent/common/osutil/freebsd.py000066400000000000000000000606551446033677600242440ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ import socket import struct import binascii import azurelinuxagent.common.utils.fileutil as fileutil import azurelinuxagent.common.utils.shellutil as shellutil import azurelinuxagent.common.utils.textutil as textutil import azurelinuxagent.common.logger as logger from azurelinuxagent.common.exception import OSUtilError from azurelinuxagent.common.osutil.default import DefaultOSUtil from azurelinuxagent.common.future import ustr class FreeBSDOSUtil(DefaultOSUtil): def __init__(self): super(FreeBSDOSUtil, self).__init__() self._scsi_disks_timeout_set = False self.jit_enabled = True @staticmethod def get_agent_bin_path(): return "/usr/local/sbin" def set_hostname(self, hostname): rc_file_path = '/etc/rc.conf' conf_file = fileutil.read_file(rc_file_path).split("\n") textutil.set_ini_config(conf_file, "hostname", hostname) fileutil.write_file(rc_file_path, "\n".join(conf_file)) self._run_command_without_raising(["hostname", hostname], log_error=False) def restart_ssh_service(self): return shellutil.run('service sshd restart', chk_err=False) def useradd(self, username, expiration=None, comment=None): """ Create user account with 'username' """ userentry = self.get_userentry(username) if userentry is not None: logger.warn("User {0} already exists, skip useradd", username) return if expiration is not None: cmd = ["pw", "useradd", username, "-e", expiration, "-m"] else: cmd = ["pw", "useradd", username, "-m"] if comment is not None: cmd.extend(["-c", comment]) self._run_command_raising_OSUtilError(cmd, err_msg="Failed to create user account:{0}".format(username)) def del_account(self, username): if self.is_sys_user(username): logger.error("{0} is a system user. Will not delete it.", username) self._run_command_without_raising(['touch', '/var/run/utx.active']) self._run_command_without_raising(['rmuser', '-y', username]) self.conf_sudoer(username, remove=True) def chpasswd(self, username, password, crypt_id=6, salt_len=10): if self.is_sys_user(username): raise OSUtilError(("User {0} is a system user, " "will not set password.").format(username)) passwd_hash = textutil.gen_password_hash(password, crypt_id, salt_len) self._run_command_raising_OSUtilError(['pw', 'usermod', username, '-H', '0'], cmd_input=passwd_hash, err_msg="Failed to set password for {0}".format(username)) def del_root_password(self): err = shellutil.run('pw usermod root -h -') if err: raise OSUtilError("Failed to delete root password: Failed to update password database.") def get_if_mac(self, ifname): data = self._get_net_info() if data[0] == ifname: return data[2].replace(':', '').upper() return None def get_first_if(self): return self._get_net_info()[:2] @staticmethod def read_route_table(): """ Return a list of strings comprising the route table as in the Linux /proc/net/route format. The input taken is from FreeBSDs `netstat -rn -f inet` command. Here is what the function does in detail: 1. Runs `netstat -rn -f inet` which outputs a column formatted list of ipv4 routes in priority order like so: > Routing tables > > Internet: > Destination Gateway Flags Refs Use Netif Expire > default 61.221.xx.yy UGS 0 247 em1 > 10 10.10.110.5 UGS 0 50 em0 > 10.10.110/26 link#1 UC 0 0 em0 > 10.10.110.5 00:1b:0d:e6:58:40 UHLW 2 0 em0 1145 > 61.221.xx.yy/29 link#2 UC 0 0 em1 > 61.221.xx.yy 00:1b:0d:e6:57:c0 UHLW 2 0 em1 1055 > 61.221.xx/24 link#2 UC 0 0 em1 > 127.0.0.1 127.0.0.1 UH 0 0 lo0 2. Convert it to an array of lines that resemble an equivalent /proc/net/route content on a Linux system like so: > Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT > gre828 00000000 00000000 0001 0 0 0 000000F8 0 0 0 > ens160 00000000 FE04700A 0003 0 0 100 00000000 0 0 0 > gre828 00000008 00000000 0001 0 0 0 000000FE 0 0 0 > ens160 0004700A 00000000 0001 0 0 100 00FFFFFF 0 0 0 > gre828 2504700A 00000000 0005 0 0 0 FFFFFFFF 0 0 0 > gre828 3704700A 00000000 0005 0 0 0 FFFFFFFF 0 0 0 > gre828 4104700A 00000000 0005 0 0 0 FFFFFFFF 0 0 0 :return: Entries in the ipv4 route priority list from `netstat -rn -f inet` in the linux `/proc/net/route` style :rtype: list(str) """ def _get_netstat_rn_ipv4_routes(): """ Runs `netstat -rn -f inet` and parses its output and returns a list of routes where the key is the column name and the value is the value in the column, stripped of leading and trailing whitespace. :return: List of dictionaries representing routes in the ipv4 route priority list from `netstat -rn -f inet` :rtype: list(dict) """ cmd = ["netstat", "-rn", "-f", "inet"] output = shellutil.run_command(cmd, log_error=True) output_lines = output.split("\n") if len(output_lines) < 3: raise OSUtilError("`netstat -rn -f inet` output seems to be empty") output_lines = [line.strip() for line in output_lines if line] if "Internet:" not in output_lines: raise OSUtilError("`netstat -rn -f inet` output seems to contain no ipv4 routes") route_header_line = output_lines.index("Internet:") + 1 # Parse the file structure and left justify the routes route_start_line = route_header_line + 1 route_line_length = max([len(line) for line in output_lines[route_header_line:]]) netstat_route_list = [line.ljust(route_line_length) for line in output_lines[route_start_line:]] # Parse the headers _route_headers = output_lines[route_header_line].split() n_route_headers = len(_route_headers) route_columns = {} for i in range(0, n_route_headers - 1): route_columns[_route_headers[i]] = ( output_lines[route_header_line].index(_route_headers[i]), (output_lines[route_header_line].index(_route_headers[i + 1]) - 1) ) route_columns[_route_headers[n_route_headers - 1]] = ( output_lines[route_header_line].index(_route_headers[n_route_headers - 1]), None ) # Parse the routes netstat_routes = [] n_netstat_routes = len(netstat_route_list) for i in range(0, n_netstat_routes): netstat_route = {} for column in route_columns: netstat_route[column] = netstat_route_list[i][ route_columns[column][0]:route_columns[column][1]].strip() netstat_route["Metric"] = n_netstat_routes - i netstat_routes.append(netstat_route) # Return the Sections return netstat_routes def _ipv4_ascii_address_to_hex(ipv4_ascii_address): """ Converts an IPv4 32bit address from its ASCII notation (ie. 127.0.0.1) to an 8 digit padded hex notation (ie. "0100007F") string. :return: 8 character long hex string representation of the IP :rtype: string """ # Raises socket.error if the IP is not a valid IPv4 return "%08X" % int(binascii.hexlify( struct.pack("!I", struct.unpack("=I", socket.inet_pton(socket.AF_INET, ipv4_ascii_address))[0])), 16) def _ipv4_cidr_mask_to_hex(ipv4_cidr_mask): """ Converts an subnet mask from its CIDR integer notation (ie. 32) to an 8 digit padded hex notation (ie. "FFFFFFFF") string representing its bitmask form. :return: 8 character long hex string representation of the IP :rtype: string """ return "{0:08x}".format( struct.unpack("=I", struct.pack("!I", (0xffffffff << (32 - ipv4_cidr_mask)) & 0xffffffff))[0]).upper() def _ipv4_cidr_destination_to_hex(destination): """ Converts an destination address from its CIDR notation (ie. 127.0.0.1/32 or default or localhost) to an 8 digit padded hex notation (ie. "0100007F" or "00000000" or "0100007F") string and its subnet bitmask also in hex (FFFFFFFF). :return: tuple of 8 character long hex string representation of the IP and 8 character long hex string representation of the subnet mask :rtype: tuple(string, int) """ destination_ip = "0.0.0.0" destination_subnetmask = 32 if destination != "default": if destination == "localhost": destination_ip = "127.0.0.1" else: destination_ip = destination.split("/") if len(destination_ip) > 1: destination_subnetmask = int(destination_ip[1]) destination_ip = destination_ip[0] hex_destination_ip = _ipv4_ascii_address_to_hex(destination_ip) hex_destination_subnetmask = _ipv4_cidr_mask_to_hex(destination_subnetmask) return hex_destination_ip, hex_destination_subnetmask def _try_ipv4_gateway_to_hex(gateway): """ If the gateway is an IPv4 address, return its IP in hex, else, return "00000000" :return: 8 character long hex string representation of the IP of the gateway :rtype: string """ try: return _ipv4_ascii_address_to_hex(gateway) except socket.error: return "00000000" def _ascii_route_flags_to_bitmask(ascii_route_flags): """ Converts route flags to a bitmask of their equivalent linux/route.h values. :return: integer representation of a 16 bit mask :rtype: int """ bitmask_flags = 0 RTF_UP = 0x0001 RTF_GATEWAY = 0x0002 RTF_HOST = 0x0004 RTF_DYNAMIC = 0x0010 if "U" in ascii_route_flags: bitmask_flags |= RTF_UP if "G" in ascii_route_flags: bitmask_flags |= RTF_GATEWAY if "H" in ascii_route_flags: bitmask_flags |= RTF_HOST if "S" not in ascii_route_flags: bitmask_flags |= RTF_DYNAMIC return bitmask_flags def _freebsd_netstat_rn_route_to_linux_proc_net_route(netstat_route): """ Converts a single FreeBSD `netstat -rn -f inet` route to its equivalent /proc/net/route line. ie: > default 0.0.0.0 UGS 0 247 em1 to > em1 00000000 00000000 0003 0 0 0 FFFFFFFF 0 0 0 :return: string representation of the equivalent /proc/net/route line :rtype: string """ network_interface = netstat_route["Netif"] hex_destination_ip, hex_destination_subnetmask = _ipv4_cidr_destination_to_hex(netstat_route["Destination"]) hex_gateway = _try_ipv4_gateway_to_hex(netstat_route["Gateway"]) bitmask_flags = _ascii_route_flags_to_bitmask(netstat_route["Flags"]) dummy_refcount = 0 dummy_use = 0 route_metric = netstat_route["Metric"] dummy_mtu = 0 dummy_window = 0 dummy_irtt = 0 return "{0}\t{1}\t{2}\t{3}\t{4}\t{5}\t{6}\t{7}\t{8}\t{9}\t{10}".format( network_interface, hex_destination_ip, hex_gateway, bitmask_flags, dummy_refcount, dummy_use, route_metric, hex_destination_subnetmask, dummy_mtu, dummy_window, dummy_irtt ) linux_style_route_file = ["Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\tMTU\tWindow\tIRTT"] try: netstat_routes = _get_netstat_rn_ipv4_routes() # Make sure the `netstat -rn -f inet` contains columns for Netif, Destination, Gateway and Flags which are needed to convert # to the Linux Format if len(netstat_routes) > 0: missing_headers = [] if "Netif" not in netstat_routes[0]: missing_headers.append("Netif") if "Destination" not in netstat_routes[0]: missing_headers.append("Destination") if "Gateway" not in netstat_routes[0]: missing_headers.append("Gateway") if "Flags" not in netstat_routes[0]: missing_headers.append("Flags") if missing_headers: raise KeyError( "`netstat -rn -f inet` output is missing columns required to convert to the Linux /proc/net/route format; columns are [{0}]".format( missing_headers)) # Parse the Netstat IPv4 Routes for netstat_route in netstat_routes: try: linux_style_route = _freebsd_netstat_rn_route_to_linux_proc_net_route(netstat_route) linux_style_route_file.append(linux_style_route) except Exception: # Skip the route continue except Exception as e: logger.error("Cannot read route table [{0}]", ustr(e)) return linux_style_route_file @staticmethod def get_list_of_routes(route_table): """ Construct a list of all network routes known to this system. :param list(str) route_table: List of text entries from route table, including headers :return: a list of network routes :rtype: list(RouteEntry) """ route_list = [] count = len(route_table) if count < 1: logger.error("netstat -rn -f inet is missing headers") elif count == 1: logger.error("netstat -rn -f inet contains no routes") else: route_list = DefaultOSUtil._build_route_list(route_table) return route_list def get_primary_interface(self): """ Get the name of the primary interface, which is the one with the default route attached to it; if there are multiple default routes, the primary has the lowest Metric. :return: the interface which has the default route """ RTF_GATEWAY = 0x0002 DEFAULT_DEST = "00000000" primary_interface = None if not self.disable_route_warning: logger.info("Examine `netstat -rn -f inet` for primary interface") route_table = self.read_route_table() def is_default(route): return (route.destination == DEFAULT_DEST) and (RTF_GATEWAY & route.flags) candidates = list(filter(is_default, self.get_list_of_routes(route_table))) if len(candidates) > 0: def get_metric(route): return int(route.metric) primary_route = min(candidates, key=get_metric) primary_interface = primary_route.interface if primary_interface is None: primary_interface = '' if not self.disable_route_warning: logger.warn('Could not determine primary interface, ' 'please ensure routes are correct') logger.warn('Primary interface examination will retry silently') self.disable_route_warning = True else: logger.info('Primary interface is [{0}]'.format(primary_interface)) self.disable_route_warning = False return primary_interface def is_primary_interface(self, ifname): """ Indicate whether the specified interface is the primary. :param ifname: the name of the interface - eth0, lo, etc. :return: True if this interface binds the default route """ return self.get_primary_interface() == ifname def is_loopback(self, ifname): """ Determine if a named interface is loopback. """ return ifname.startswith("lo") def route_add(self, net, mask, gateway): cmd = 'route add {0} {1} {2}'.format(net, gateway, mask) return shellutil.run(cmd, chk_err=False) def is_missing_default_route(self): """ For FreeBSD, the default broadcast goes to current default gw, not a all-ones broadcast address, need to specify the route manually to get it work in a VNET environment. SEE ALSO: man ip(4) IP_ONESBCAST, """ RTF_GATEWAY = 0x0002 DEFAULT_DEST = "00000000" route_table = self.read_route_table() routes = self.get_list_of_routes(route_table) for route in routes: if (route.destination == DEFAULT_DEST) and (RTF_GATEWAY & route.flags): return False return True def is_dhcp_enabled(self): return True def start_dhcp_service(self): shellutil.run("/etc/rc.d/dhclient start {0}".format(self.get_if_name()), chk_err=False) def allow_dhcp_broadcast(self): pass def set_route_for_dhcp_broadcast(self, ifname): return shellutil.run("route add 255.255.255.255 -iface {0}".format(ifname), chk_err=False) def remove_route_for_dhcp_broadcast(self, ifname): shellutil.run("route delete 255.255.255.255 -iface {0}".format(ifname), chk_err=False) def get_dhcp_pid(self): return self._get_dhcp_pid(["pgrep", "-n", "dhclient"]) def eject_dvd(self, chk_err=True): dvd = self.get_dvd_device() retcode = shellutil.run("cdcontrol -f {0} eject".format(dvd)) if chk_err and retcode != 0: raise OSUtilError("Failed to eject dvd: ret={0}".format(retcode)) def restart_if(self, ifname, retries=None, wait=None): # Restart dhclient only to publish hostname shellutil.run("/etc/rc.d/dhclient restart {0}".format(ifname), chk_err=False) def get_total_mem(self): cmd = "sysctl hw.physmem |awk '{print $2}'" ret, output = shellutil.run_get_output(cmd) if ret: raise OSUtilError("Failed to get total memory: {0}".format(output)) try: return int(output) / 1024 / 1024 except ValueError: raise OSUtilError("Failed to get total memory: {0}".format(output)) def get_processor_cores(self): ret, output = shellutil.run_get_output("sysctl hw.ncpu |awk '{print $2}'") if ret: raise OSUtilError("Failed to get processor cores.") try: return int(output) except ValueError: raise OSUtilError("Failed to get total memory: {0}".format(output)) def set_scsi_disks_timeout(self, timeout): if self._scsi_disks_timeout_set: return ret, output = shellutil.run_get_output('sysctl kern.cam.da.default_timeout={0}'.format(timeout)) if ret: raise OSUtilError("Failed set SCSI disks timeout: {0}".format(output)) self._scsi_disks_timeout_set = True def check_pid_alive(self, pid): return shellutil.run('ps -p {0}'.format(pid), chk_err=False) == 0 @staticmethod def _get_net_info(): """ There is no SIOCGIFCONF on freeBSD - just parse ifconfig. Returns strings: iface, inet4_addr, and mac or 'None,None,None' if unable to parse. We will sleep and retry as the network must be up. """ iface = '' inet = '' mac = '' err, output = shellutil.run_get_output('ifconfig -l ether', chk_err=False) if err: raise OSUtilError("Can't find ether interface:{0}".format(output)) ifaces = output.split() if not ifaces: raise OSUtilError("Can't find ether interface.") iface = ifaces[0] err, output = shellutil.run_get_output('ifconfig ' + iface, chk_err=False) if err: raise OSUtilError("Can't get info for interface:{0}".format(iface)) for line in output.split('\n'): if line.find('inet ') != -1: inet = line.split()[1] elif line.find('ether ') != -1: mac = line.split()[1] logger.verbose("Interface info: ({0},{1},{2})", iface, inet, mac) return iface, inet, mac def device_for_ide_port(self, port_id): """ Return device name attached to ide port 'n'. """ if port_id > 3: return None g0 = "00000000" if port_id > 1: g0 = "00000001" port_id = port_id - 2 err, output = shellutil.run_get_output('sysctl dev.storvsc | grep pnpinfo | grep deviceid=') if err: return None g1 = "000" + ustr(port_id) g0g1 = "{0}-{1}".format(g0, g1) # pylint: disable=W0105 """ search 'X' from 'dev.storvsc.X.%pnpinfo: classid=32412632-86cb-44a2-9b5c-50d1417354f5 deviceid=00000000-0001-8899-0000-000000000000' """ # pylint: enable=W0105 cmd_search_ide = "sysctl dev.storvsc | grep pnpinfo | grep deviceid={0}".format(g0g1) err, output = shellutil.run_get_output(cmd_search_ide) if err: return None cmd_extract_id = cmd_search_ide + "|awk -F . '{print $3}'" err, output = shellutil.run_get_output(cmd_extract_id) # pylint: disable=W0105 """ try to search 'blkvscX' and 'storvscX' to find device name """ # pylint: enable=W0105 output = output.rstrip() cmd_search_blkvsc = "camcontrol devlist -b | grep blkvsc{0} | awk '{{print $1}}'".format(output) err, output = shellutil.run_get_output(cmd_search_blkvsc) if err == 0: output = output.rstrip() cmd_search_dev = "camcontrol devlist | grep {0} | awk -F \( '{{print $2}}'|sed -e 's/.*(//'| sed -e 's/).*//'".format(output) # pylint: disable=W1401 err, output = shellutil.run_get_output(cmd_search_dev) if err == 0: for possible in output.rstrip().split(','): if not possible.startswith('pass'): return possible cmd_search_storvsc = "camcontrol devlist -b | grep storvsc{0} | awk '{{print $1}}'".format(output) err, output = shellutil.run_get_output(cmd_search_storvsc) if err == 0: output = output.rstrip() cmd_search_dev = "camcontrol devlist | grep {0} | awk -F \( '{{print $2}}'|sed -e 's/.*(//'| sed -e 's/).*//'".format(output) # pylint: disable=W1401 err, output = shellutil.run_get_output(cmd_search_dev) if err == 0: for possible in output.rstrip().split(','): if not possible.startswith('pass'): return possible return None @staticmethod def get_total_cpu_ticks_since_boot(): return 0 WALinuxAgent-2.9.1.1/azurelinuxagent/common/osutil/gaia.py000066400000000000000000000167361446033677600235340ustar00rootroot00000000000000# # Copyright 2017 Check Point Software Technologies # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import base64 import socket import struct import time import azurelinuxagent.common.conf as conf from azurelinuxagent.common.exception import OSUtilError from azurelinuxagent.common.future import ustr, bytebuffer, range, int # pylint: disable=redefined-builtin import azurelinuxagent.common.logger as logger from azurelinuxagent.common.osutil.default import DefaultOSUtil from azurelinuxagent.common.utils.cryptutil import CryptUtil import azurelinuxagent.common.utils.fileutil as fileutil import azurelinuxagent.common.utils.shellutil as shellutil import azurelinuxagent.common.utils.textutil as textutil class GaiaOSUtil(DefaultOSUtil): def __init__(self): # pylint: disable=W0235 super(GaiaOSUtil, self).__init__() def _run_clish(self, cmd): ret = 0 out = "" for i in range(10): # pylint: disable=W0612 try: final_command = ["/bin/clish", "-s", "-c", "'{0}'".format(cmd)] out = shellutil.run_command(final_command, log_error=True) ret = 0 break except shellutil.CommandError as e: ret = e.returncode out = e.stdout except Exception as e: ret = -1 out = ustr(e) if 'NMSHST0025' in out: # Entry for [hostname] already present ret = 0 break time.sleep(2) return ret, out def useradd(self, username, expiration=None, comment=None): logger.warn('useradd is not supported on GAiA') def chpasswd(self, username, password, crypt_id=6, salt_len=10): logger.info('chpasswd') passwd_hash = textutil.gen_password_hash(password, crypt_id, salt_len) ret, out = self._run_clish( 'set user admin password-hash ' + passwd_hash) if ret != 0: raise OSUtilError(("Failed to set password for {0}: {1}" "").format('admin', out)) def conf_sudoer(self, username, nopasswd=False, remove=False): logger.info('conf_sudoer is not supported on GAiA') def del_root_password(self): logger.info('del_root_password') ret, out = self._run_clish('set user admin password-hash *LOCK*') # pylint: disable=W0612 if ret != 0: raise OSUtilError("Failed to delete root password") def _replace_user(self, path, username): if path.startswith('$HOME'): path = '/home' + path[5:] parts = path.split('/') parts[2] = username return '/'.join(parts) def deploy_ssh_keypair(self, username, keypair): logger.info('deploy_ssh_keypair') username = 'admin' path, thumbprint = keypair path = self._replace_user(path, username) super(GaiaOSUtil, self).deploy_ssh_keypair( username, (path, thumbprint)) def openssl_to_openssh(self, input_file, output_file): cryptutil = CryptUtil(conf.get_openssl_cmd()) ret, out = shellutil.run_get_output( conf.get_openssl_cmd() + " rsa -pubin -noout -text -in '" + input_file + "'") if ret != 0: raise OSUtilError('openssl failed with {0}'.format(ret)) modulus = [] exponent = [] buf = None for line in out.split('\n'): if line.startswith('Modulus:'): buf = modulus buf.append(line) continue if line.startswith('Exponent:'): buf = exponent buf.append(line) continue if buf and line: buf.append(line.strip().replace(':', '')) def text_to_num(buf): if len(buf) == 1: return int(buf[0].split()[1]) return int(''.join(buf[1:]), 16) n = text_to_num(modulus) e = text_to_num(exponent) keydata = bytearray() keydata.extend(struct.pack('>I', len('ssh-rsa'))) keydata.extend(b'ssh-rsa') keydata.extend(struct.pack('>I', len(cryptutil.num_to_bytes(e)))) keydata.extend(cryptutil.num_to_bytes(e)) keydata.extend(struct.pack('>I', len(cryptutil.num_to_bytes(n)) + 1)) keydata.extend(b'\0') keydata.extend(cryptutil.num_to_bytes(n)) keydata_base64 = base64.b64encode(bytebuffer(keydata)) fileutil.write_file(output_file, ustr(b'ssh-rsa ' + keydata_base64 + b'\n', encoding='utf-8')) def deploy_ssh_pubkey(self, username, pubkey): logger.info('deploy_ssh_pubkey') username = 'admin' path, thumbprint, value = pubkey path = self._replace_user(path, username) super(GaiaOSUtil, self).deploy_ssh_pubkey( username, (path, thumbprint, value)) def eject_dvd(self, chk_err=True): logger.warn('eject is not supported on GAiA') def mount(self, device, mount_point, option=None, chk_err=True): if not option: option = [] if any('udf,iso9660' in opt for opt in option): ret, out = super(GaiaOSUtil, self).mount(device, mount_point, option=[opt.replace('udf,iso9660', 'udf') for opt in option], chk_err=chk_err) if not ret: return ret, out return super(GaiaOSUtil, self).mount( device, mount_point, option=option, chk_err=chk_err) def allow_dhcp_broadcast(self): logger.info('allow_dhcp_broadcast is ignored on GAiA') def remove_rules_files(self, rules_files=''): pass def restore_rules_files(self, rules_files=''): logger.info('restore_rules_files is ignored on GAiA') def restart_ssh_service(self): return shellutil.run('/sbin/service sshd condrestart', chk_err=False) def _address_to_string(self, addr): return socket.inet_ntoa(struct.pack("!I", addr)) def _get_prefix(self, mask): return str(sum([bin(int(x)).count('1') for x in mask.split('.')])) def route_add(self, net, mask, gateway): logger.info('route_add {0} {1} {2}', net, mask, gateway) if net == 0 and mask == 0: cidr = 'default' else: cidr = self._address_to_string(net) + '/' + self._get_prefix( self._address_to_string(mask)) ret, out = self._run_clish( # pylint: disable=W0612 'set static-route ' + cidr + ' nexthop gateway address ' + self._address_to_string(gateway) + ' on') return ret def set_hostname(self, hostname): logger.warn('set_hostname is ignored on GAiA') def set_dhcp_hostname(self, hostname): logger.warn('set_dhcp_hostname is ignored on GAiA') def publish_hostname(self, hostname): logger.warn('publish_hostname is ignored on GAiA') def del_account(self, username): logger.warn('del_account is ignored on GAiA') WALinuxAgent-2.9.1.1/azurelinuxagent/common/osutil/iosxe.py000066400000000000000000000067061446033677600237560ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.shellutil as shellutil from azurelinuxagent.common.future import ustr from azurelinuxagent.common.osutil.default import DefaultOSUtil, PRODUCT_ID_FILE, DMIDECODE_CMD, UUID_PATTERN from azurelinuxagent.common.utils import textutil, fileutil # pylint: disable=W0611 # pylint: disable=W0105 ''' The IOSXE distribution is a variant of the Centos distribution, version 7.1. The primary difference is that IOSXE makes some assumptions about the waagent environment: - only the waagent daemon is executed - no provisioning is performed - no DHCP-based services are available ''' # pylint: enable=W0105 class IosxeOSUtil(DefaultOSUtil): def __init__(self): # pylint: disable=W0235 super(IosxeOSUtil, self).__init__() @staticmethod def get_systemd_unit_file_install_path(): return "/usr/lib/systemd/system" def set_hostname(self, hostname): """ Unlike redhat 6.x, redhat 7.x will set hostname via hostnamectl Due to a bug in systemd in Centos-7.0, if this call fails, fallback to hostname. """ hostnamectl_cmd = ["hostnamectl", "set-hostname", hostname, "--static"] try: shellutil.run_command(hostnamectl_cmd) except Exception as e: logger.warn("[{0}] failed with error: {1}, attempting fallback".format(' '.join(hostnamectl_cmd), ustr(e))) DefaultOSUtil.set_hostname(self, hostname) def publish_hostname(self, hostname): """ Restart NetworkManager first before publishing hostname """ shellutil.run("service NetworkManager restart") super(IosxeOSUtil, self).publish_hostname(hostname) def register_agent_service(self): return shellutil.run("systemctl enable waagent", chk_err=False) def unregister_agent_service(self): return shellutil.run("systemctl disable waagent", chk_err=False) def openssl_to_openssh(self, input_file, output_file): DefaultOSUtil.openssl_to_openssh(self, input_file, output_file) def is_dhcp_available(self): return False def get_instance_id(self): ''' Azure records a UUID as the instance ID First check /sys/class/dmi/id/product_uuid. If that is missing, then extracts from dmidecode If nothing works (for old VMs), return the empty string ''' if os.path.isfile(PRODUCT_ID_FILE): try: s = fileutil.read_file(PRODUCT_ID_FILE).strip() return self._correct_instance_id(s.strip()) except IOError: pass rc, s = shellutil.run_get_output(DMIDECODE_CMD) if rc != 0 or UUID_PATTERN.match(s) is None: return "" return self._correct_instance_id(s.strip()) WALinuxAgent-2.9.1.1/azurelinuxagent/common/osutil/mariner.py000066400000000000000000000047151446033677600242620ustar00rootroot00000000000000# # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # from azurelinuxagent.common.osutil.default import DefaultOSUtil class MarinerOSUtil(DefaultOSUtil): def __init__(self): super(MarinerOSUtil, self).__init__() self.jit_enabled = True @staticmethod def get_systemd_unit_file_install_path(): return "/usr/lib/systemd/system" @staticmethod def get_agent_bin_path(): return "/usr/bin" def is_dhcp_enabled(self): return True def start_network(self): self._run_command_without_raising(["systemctl", "start", "systemd-networkd"], log_error=False) def restart_if(self, ifname=None, retries=None, wait=None): self._run_command_without_raising(["systemctl", "restart", "systemd-networkd"]) def restart_ssh_service(self): self._run_command_without_raising(["systemctl", "restart", "sshd"]) def stop_dhcp_service(self): self._run_command_without_raising(["systemctl", "stop", "systemd-networkd"], log_error=False) def start_dhcp_service(self): self._run_command_without_raising(["systemctl", "start", "systemd-networkd"], log_error=False) def start_agent_service(self): self._run_command_without_raising(["systemctl", "start", "{0}".format(self.service_name)], log_error=False) def stop_agent_service(self): self._run_command_without_raising(["systemctl", "stop", "{0}".format(self.service_name)], log_error=False) def register_agent_service(self): self._run_command_without_raising(["systemctl", "enable", "{0}".format(self.service_name)], log_error=False) def unregister_agent_service(self): self._run_command_without_raising(["systemctl", "disable", "{0}".format(self.service_name)], log_error=False) def get_dhcp_pid(self): return self._get_dhcp_pid(["pidof", "systemd-networkd"]) def conf_sshd(self, disable_password): pass WALinuxAgent-2.9.1.1/azurelinuxagent/common/osutil/nsbsd.py000066400000000000000000000133311446033677600237300ustar00rootroot00000000000000# # Copyright 2018 Stormshield # # 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. # import os import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.fileutil as fileutil import azurelinuxagent.common.utils.shellutil as shellutil from azurelinuxagent.common.exception import OSUtilError from azurelinuxagent.common.osutil.freebsd import FreeBSDOSUtil class NSBSDOSUtil(FreeBSDOSUtil): resolver = None def __init__(self): super(NSBSDOSUtil, self).__init__() if self.resolver is None: # NSBSD doesn't have a system resolver, configure a python one try: import dns.resolver except ImportError: raise OSUtilError("Python DNS resolver not available. Cannot proceed!") self.resolver = dns.resolver.Resolver() servers = [] cmd = "getconf /usr/Firewall/ConfigFiles/dns Servers | tail -n +2" ret, output = shellutil.run_get_output(cmd) # pylint: disable=W0612 for server in output.split("\n"): if server == '': break server = server[:-1] # remove last '=' cmd = "grep '{}' /etc/hosts".format(server) + " | awk '{print $1}'" ret, ip = shellutil.run_get_output(cmd) servers.append(ip) self.resolver.nameservers = servers dns.resolver.override_system_resolver(self.resolver) def set_hostname(self, hostname): self._run_command_without_raising( ['/usr/Firewall/sbin/setconf', '/usr/Firewall/System/global', 'SystemName', hostname]) self._run_command_without_raising(["/usr/Firewall/sbin/enlog"]) self._run_command_without_raising(["/usr/Firewall/sbin/enproxy", "-u"]) self._run_command_without_raising(["/usr/Firewall/sbin/ensl", "-u"]) self._run_command_without_raising(["/usr/Firewall/sbin/ennetwork", "-f"]) def restart_ssh_service(self): return shellutil.run('/usr/Firewall/sbin/enservice', chk_err=False) def conf_sshd(self, disable_password): option = "0" if disable_password else "1" shellutil.run('setconf /usr/Firewall/ConfigFiles/system SSH State 1', chk_err=False) shellutil.run('setconf /usr/Firewall/ConfigFiles/system SSH Password {}'.format(option), chk_err=False) shellutil.run('enservice', chk_err=False) logger.info("{0} SSH password-based authentication methods." .format("Disabled" if disable_password else "Enabled")) def useradd(self, username, expiration=None, comment=None): """ Create user account with 'username' """ logger.warn("User creation disabled") def del_account(self, username): logger.warn("User deletion disabled") def conf_sudoer(self, username, nopasswd=False, remove=False): logger.warn("Sudo is not enabled") def chpasswd(self, username, password, crypt_id=6, salt_len=10): self._run_command_raising_OSUtilError(["/usr/Firewall/sbin/fwpasswd", "-p", password], err_msg="Failed to set password for admin") # password set, activate webadmin and ssh access commands = [['setconf', '/usr/Firewall/ConfigFiles/webadmin', 'ACL', 'any'], ['ensl']] self._run_multiple_commands_without_raising(commands, log_error=False, continue_on_error=False) def deploy_ssh_pubkey(self, username, pubkey): """ Deploy authorized_key """ path, thumbprint, value = pubkey # pylint: disable=W0612 # overide parameters super(NSBSDOSUtil, self).deploy_ssh_pubkey('admin', ["/usr/Firewall/.ssh/authorized_keys", thumbprint, value]) def del_root_password(self): logger.warn("Root password deletion disabled") def start_dhcp_service(self): shellutil.run("/usr/Firewall/sbin/nstart dhclient", chk_err=False) def stop_dhcp_service(self): shellutil.run("/usr/Firewall/sbin/nstop dhclient", chk_err=False) def get_dhcp_pid(self): ret = "" pidfile = "/var/run/dhclient.pid" if os.path.isfile(pidfile): ret = fileutil.read_file(pidfile, encoding='ascii') return self._text_to_pid_list(ret) def eject_dvd(self, chk_err=True): pass def restart_if(self, ifname=None, retries=None, wait=None): # Restart dhclient only to publish hostname shellutil.run("ennetwork", chk_err=False) def set_dhcp_hostname(self, hostname): # already done by the dhcp client pass def get_firewall_dropped_packets(self, dst_ip=None): # disable iptables methods return 0 def get_firewall_will_wait(self): # disable iptables methods return "" def _delete_rule(self, rule): # disable iptables methods return def remove_firewall(self, dst_ip=None, uid=None, wait=""): # disable iptables methods return True def enable_firewall(self, dst_ip=None, uid=None): # disable iptables methods return True, True def get_firewall_list(self, wait=""): # disable iptables methods return "" WALinuxAgent-2.9.1.1/azurelinuxagent/common/osutil/openbsd.py000066400000000000000000000325041446033677600242540ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # Copyright 2017 Reyk Floeter # # 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. # # Requires Python 2.6+ and OpenSSL 1.0+ import os import re import time import glob import datetime import azurelinuxagent.common.utils.fileutil as fileutil import azurelinuxagent.common.utils.shellutil as shellutil import azurelinuxagent.common.logger as logger import azurelinuxagent.common.conf as conf from azurelinuxagent.common.exception import OSUtilError from azurelinuxagent.common.osutil.default import DefaultOSUtil UUID_PATTERN = re.compile( r'^\s*[A-F0-9]{8}(?:\-[A-F0-9]{4}){3}\-[A-F0-9]{12}\s*$', re.IGNORECASE) class OpenBSDOSUtil(DefaultOSUtil): def __init__(self): super(OpenBSDOSUtil, self).__init__() self.jit_enabled = True self._scsi_disks_timeout_set = False @staticmethod def get_agent_bin_path(): return "/usr/local/sbin" def get_instance_id(self): ret, output = shellutil.run_get_output("sysctl -n hw.uuid") if ret != 0 or UUID_PATTERN.match(output) is None: return "" return output.strip() def set_hostname(self, hostname): fileutil.write_file("/etc/myname", "{}\n".format(hostname)) self._run_command_without_raising(["hostname", hostname], log_error=False) def restart_ssh_service(self): return shellutil.run('rcctl restart sshd', chk_err=False) def start_agent_service(self): return shellutil.run('rcctl start {0}'.format(self.service_name), chk_err=False) def stop_agent_service(self): return shellutil.run('rcctl stop {0}'.format(self.service_name), chk_err=False) def register_agent_service(self): shellutil.run('chmod 0555 /etc/rc.d/{0}'.format(self.service_name), chk_err=False) return shellutil.run('rcctl enable {0}'.format(self.service_name), chk_err=False) def unregister_agent_service(self): return shellutil.run('rcctl disable {0}'.format(self.service_name), chk_err=False) def del_account(self, username): if self.is_sys_user(username): logger.error("{0} is a system user. Will not delete it.", username) self._run_command_without_raising(["touch", "/var/run/utmp"]) self._run_command_without_raising(["userdel", "-r", username]) self.conf_sudoer(username, remove=True) def conf_sudoer(self, username, nopasswd=False, remove=False): doas_conf = "/etc/doas.conf" doas = None if not remove: if not os.path.isfile(doas_conf): # always allow root to become root doas = "permit keepenv nopass root\n" fileutil.append_file(doas_conf, doas) if nopasswd: doas = "permit keepenv nopass {0}\n".format(username) else: doas = "permit keepenv persist {0}\n".format(username) fileutil.append_file(doas_conf, doas) fileutil.chmod(doas_conf, 0o644) else: # Remove user from doas.conf if os.path.isfile(doas_conf): try: content = fileutil.read_file(doas_conf) doas = content.split("\n") doas = [x for x in doas if username not in x] fileutil.write_file(doas_conf, "\n".join(doas)) except IOError as err: raise OSUtilError("Failed to remove sudoer: " "{0}".format(err)) def chpasswd(self, username, password, crypt_id=6, salt_len=10): if self.is_sys_user(username): raise OSUtilError(("User {0} is a system user. " "Will not set passwd.").format(username)) output = self._run_command_raising_OSUtilError(['encrypt'], cmd_input=password, err_msg="Failed to encrypt password for {0}".format(username)) passwd_hash = output.strip() self._run_command_raising_OSUtilError(['usermod', '-p', passwd_hash, username], err_msg="Failed to set password for {0}".format(username)) def del_root_password(self): ret, output = shellutil.run_get_output('usermod -p "*" root') if ret: raise OSUtilError("Failed to delete root password: " "{0}".format(output)) def get_if_mac(self, ifname): data = self._get_net_info() if data[0] == ifname: return data[2].replace(':', '').upper() return None def get_first_if(self): return self._get_net_info()[:2] def route_add(self, net, mask, gateway): cmd = 'route add {0} {1} {2}'.format(net, gateway, mask) return shellutil.run(cmd, chk_err=False) def is_missing_default_route(self): ret = shellutil.run("route -n get default", chk_err=False) if ret == 0: return False return True def is_dhcp_enabled(self): pass def start_dhcp_service(self): pass def stop_dhcp_service(self): pass def get_dhcp_lease_endpoint(self): """ OpenBSD has a sligthly different lease file format. """ endpoint = None pathglob = '/var/db/dhclient.leases.{}'.format(self.get_first_if()[0]) HEADER_LEASE = "lease" HEADER_OPTION = "option option-245" HEADER_EXPIRE = "expire" FOOTER_LEASE = "}" FORMAT_DATETIME = "%Y/%m/%d %H:%M:%S %Z" logger.info("looking for leases in path [{0}]".format(pathglob)) for lease_file in glob.glob(pathglob): leases = open(lease_file).read() if HEADER_OPTION in leases: cached_endpoint = None has_option_245 = False expired = True # assume expired for line in leases.splitlines(): if line.startswith(HEADER_LEASE): cached_endpoint = None has_option_245 = False expired = True elif HEADER_OPTION in line: try: ipaddr = line.split(" ")[-1].strip(";").split(":") cached_endpoint = \ ".".join(str(int(d, 16)) for d in ipaddr) has_option_245 = True except ValueError: logger.error("could not parse '{0}'".format(line)) elif HEADER_EXPIRE in line: if "never" in line: expired = False else: try: expire_string = line.split( " ", 4)[-1].strip(";") expire_date = datetime.datetime.strptime( expire_string, FORMAT_DATETIME) if expire_date > datetime.datetime.utcnow(): expired = False except ValueError: logger.error("could not parse expiry token " "'{0}'".format(line)) elif FOOTER_LEASE in line: logger.info("dhcp entry:{0}, 245:{1}, expired: {2}" .format(cached_endpoint, has_option_245, expired)) if not expired and cached_endpoint is not None and has_option_245: endpoint = cached_endpoint logger.info("found endpoint [{0}]".format(endpoint)) # we want to return the last valid entry, so # keep searching if endpoint is not None: logger.info("cached endpoint found [{0}]".format(endpoint)) else: logger.info("cached endpoint not found") return endpoint def allow_dhcp_broadcast(self): pass def set_route_for_dhcp_broadcast(self, ifname): return shellutil.run("route add 255.255.255.255 -iface " "{0}".format(ifname), chk_err=False) def remove_route_for_dhcp_broadcast(self, ifname): shellutil.run("route delete 255.255.255.255 -iface " "{0}".format(ifname), chk_err=False) def get_dhcp_pid(self): return self._get_dhcp_pid(["pgrep", "-n", "dhclient"]) def get_dvd_device(self, dev_dir='/dev'): pattern = r'cd[0-9]c' for dvd in [re.match(pattern, dev) for dev in os.listdir(dev_dir)]: if dvd is not None: return "/dev/{0}".format(dvd.group(0)) raise OSUtilError("Failed to get DVD device") def mount_dvd(self, max_retry=6, chk_err=True, dvd_device=None, mount_point=None, sleep_time=5): if dvd_device is None: dvd_device = self.get_dvd_device() if mount_point is None: mount_point = conf.get_dvd_mount_point() if not os.path.isdir(mount_point): os.makedirs(mount_point) for retry in range(0, max_retry): retcode = self.mount(dvd_device, mount_point, option=["-o", "ro", "-t", "udf"], chk_err=False) if retcode == 0: logger.info("Successfully mounted DVD") return if retry < max_retry - 1: mountlist = shellutil.run_get_output("/sbin/mount")[1] existing = self.get_mount_point(mountlist, dvd_device) if existing is not None: logger.info("{0} is mounted at {1}", dvd_device, existing) return logger.warn("Mount DVD failed: retry={0}, ret={1}", retry, retcode) time.sleep(sleep_time) if chk_err: raise OSUtilError("Failed to mount DVD.") def eject_dvd(self, chk_err=True): dvd = self.get_dvd_device() retcode = shellutil.run("cdio eject {0}".format(dvd)) if chk_err and retcode != 0: raise OSUtilError("Failed to eject DVD: ret={0}".format(retcode)) def restart_if(self, ifname, retries=3, wait=5): # Restart dhclient only to publish hostname shellutil.run("/sbin/dhclient {0}".format(ifname), chk_err=False) def get_total_mem(self): ret, output = shellutil.run_get_output("sysctl -n hw.physmem") if ret: raise OSUtilError("Failed to get total memory: {0}".format(output)) try: return int(output)/1024/1024 except ValueError: raise OSUtilError("Failed to get total memory: {0}".format(output)) def get_processor_cores(self): ret, output = shellutil.run_get_output("sysctl -n hw.ncpu") if ret: raise OSUtilError("Failed to get processor cores.") try: return int(output) except ValueError: raise OSUtilError("Failed to get total memory: {0}".format(output)) def set_scsi_disks_timeout(self, timeout): pass def check_pid_alive(self, pid): # pylint: disable=R1710 if not pid: return return shellutil.run('ps -p {0}'.format(pid), chk_err=False) == 0 @staticmethod def _get_net_info(): """ There is no SIOCGIFCONF on OpenBSD - just parse ifconfig. Returns strings: iface, inet4_addr, and mac or 'None,None,None' if unable to parse. We will sleep and retry as the network must be up. """ iface = '' inet = '' mac = '' ret, output = shellutil.run_get_output( 'ifconfig hvn | grep -E "^hvn.:" | sed "s/:.*//g"', chk_err=False) if ret: raise OSUtilError("Can't find ether interface:{0}".format(output)) ifaces = output.split() if not ifaces: raise OSUtilError("Can't find ether interface.") iface = ifaces[0] ret, output = shellutil.run_get_output( 'ifconfig ' + iface, chk_err=False) if ret: raise OSUtilError("Can't get info for interface:{0}".format(iface)) for line in output.split('\n'): if line.find('inet ') != -1: inet = line.split()[1] elif line.find('lladdr ') != -1: mac = line.split()[1] logger.verbose("Interface info: ({0},{1},{2})", iface, inet, mac) return iface, inet, mac def device_for_ide_port(self, port_id): """ Return device name attached to ide port 'n'. """ return "wd{0}".format(port_id) @staticmethod def get_total_cpu_ticks_since_boot(): return 0 WALinuxAgent-2.9.1.1/azurelinuxagent/common/osutil/openwrt.py000066400000000000000000000136201446033677600243160ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # Copyright 2018 Sonus Networks, Inc. (d.b.a. Ribbon Communications Operating Company) # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import re import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.shellutil as shellutil import azurelinuxagent.common.utils.fileutil as fileutil from azurelinuxagent.common.osutil.default import DefaultOSUtil from azurelinuxagent.common.utils.networkutil import NetworkInterfaceCard class OpenWRTOSUtil(DefaultOSUtil): def __init__(self): super(OpenWRTOSUtil, self).__init__() self.agent_conf_file_path = '/etc/waagent.conf' self.dhclient_name = 'udhcpc' self.ip_command_output = re.compile('^\d+:\s+(\w+):\s+(.*)$') # pylint: disable=W1401 self.jit_enabled = True def eject_dvd(self, chk_err=True): logger.warn('eject is not supported on OpenWRT') def useradd(self, username, expiration=None, comment=None): """ Create user account with 'username' """ userentry = self.get_userentry(username) if userentry is not None: logger.info("User {0} already exists, skip useradd", username) return if expiration is not None: cmd = ["useradd", "-m", username, "-s", "/bin/ash", "-e", expiration] else: cmd = ["useradd", "-m", username, "-s", "/bin/ash"] if not os.path.exists("/home"): os.mkdir("/home") if comment is not None: cmd.extend(["-c", comment]) self._run_command_raising_OSUtilError(cmd, err_msg="Failed to create user account:{0}".format(username)) def get_dhcp_pid(self): return self._get_dhcp_pid(["pidof", self.dhclient_name]) def get_nic_state(self, as_string=False): """ Capture NIC state (IPv4 and IPv6 addresses plus link state). :return: Dictionary of NIC state objects, with the NIC name as key :rtype: dict(str,NetworkInformationCard) """ if as_string: # as_string not supported on open wrt return '' state = {} status, output = shellutil.run_get_output("ip -o link", chk_err=False, log_cmd=False) if status != 0: logger.verbose("Could not fetch NIC link info; status {0}, {1}".format(status, output)) return {} for entry in output.splitlines(): result = self.ip_command_output.match(entry) if result: name = result.group(1) state[name] = NetworkInterfaceCard(name, result.group(2)) self._update_nic_state(state, "ip -o -f inet address", NetworkInterfaceCard.add_ipv4, "an IPv4 address") self._update_nic_state(state, "ip -o -f inet6 address", NetworkInterfaceCard.add_ipv6, "an IPv6 address") return state def _update_nic_state(self, state, ip_command, handler, description): """ Update the state of NICs based on the output of a specified ip subcommand. :param dict(str, NetworkInterfaceCard) state: Dictionary of NIC state objects :param str ip_command: The ip command to run :param handler: A method on the NetworkInterfaceCard class :param str description: Description of the particular information being added to the state """ status, output = shellutil.run_get_output(ip_command, chk_err=True) if status != 0: return for entry in output.splitlines(): result = self.ip_command_output.match(entry) if result: interface_name = result.group(1) if interface_name in state: handler(state[interface_name], result.group(2)) else: logger.error("Interface {0} has {1} but no link state".format(interface_name, description)) def is_dhcp_enabled(self): pass def start_dhcp_service(self): pass def stop_dhcp_service(self): pass def start_network(self) : return shellutil.run("/etc/init.d/network start", chk_err=True) def restart_ssh_service(self): # pylint: disable=R1710 # Since Dropbear is the default ssh server on OpenWRt, lets do a sanity check if os.path.exists("/etc/init.d/sshd"): return shellutil.run("/etc/init.d/sshd restart", chk_err=True) else: logger.warn("sshd service does not exists") def stop_agent_service(self): return shellutil.run("/etc/init.d/{0} stop".format(self.service_name), chk_err=True) def start_agent_service(self): return shellutil.run("/etc/init.d/{0} start".format(self.service_name), chk_err=True) def register_agent_service(self): return shellutil.run("/etc/init.d/{0} enable".format(self.service_name), chk_err=True) def unregister_agent_service(self): return shellutil.run("/etc/init.d/{0} disable".format(self.service_name), chk_err=True) def set_hostname(self, hostname): fileutil.write_file('/etc/hostname', hostname) commands = [['uci', 'set', 'system.@system[0].hostname={0}'.format(hostname)], ['uci', 'commit', 'system'], ['/etc/init.d/system', 'reload']] self._run_multiple_commands_without_raising(commands, log_error=False, continue_on_error=False) def remove_rules_files(self, rules_files=""): pass WALinuxAgent-2.9.1.1/azurelinuxagent/common/osutil/photonos.py000066400000000000000000000040161446033677600244700ustar00rootroot00000000000000# # Copyright 2021 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import azurelinuxagent.common.utils.shellutil as shellutil from azurelinuxagent.common.osutil.default import DefaultOSUtil class PhotonOSUtil(DefaultOSUtil): def __init__(self): super(PhotonOSUtil, self).__init__() self.agent_conf_file_path = '/etc/waagent.conf' @staticmethod def get_systemd_unit_file_install_path(): return '/usr/lib/systemd/system' @staticmethod def get_agent_bin_path(): return '/usr/bin' def is_dhcp_enabled(self): return True def start_network(self) : return shellutil.run('systemctl start systemd-networkd', chk_err=False) def restart_if(self, ifname=None, retries=None, wait=None): shellutil.run('systemctl restart systemd-networkd') def restart_ssh_service(self): shellutil.run('systemctl restart sshd') def stop_dhcp_service(self): return shellutil.run('systemctl stop systemd-networkd', chk_err=False) def start_dhcp_service(self): return shellutil.run('systemctl start systemd-networkd', chk_err=False) def start_agent_service(self): return shellutil.run('systemctl start waagent', chk_err=False) def stop_agent_service(self): return shellutil.run('systemctl stop waagent', chk_err=False) def get_dhcp_pid(self): return self._get_dhcp_pid(['pidof', 'systemd-networkd']) def conf_sshd(self, disable_password): pass WALinuxAgent-2.9.1.1/azurelinuxagent/common/osutil/redhat.py000066400000000000000000000145021446033677600240670ustar00rootroot00000000000000# # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os # pylint: disable=W0611 import re # pylint: disable=W0611 import pwd # pylint: disable=W0611 import shutil # pylint: disable=W0611 import socket # pylint: disable=W0611 import array # pylint: disable=W0611 import struct # pylint: disable=W0611 import fcntl # pylint: disable=W0611 import time # pylint: disable=W0611 import base64 # pylint: disable=W0611 import azurelinuxagent.common.conf as conf import azurelinuxagent.common.logger as logger from azurelinuxagent.common.future import ustr, bytebuffer # pylint: disable=W0611 from azurelinuxagent.common.exception import OSUtilError, CryptError import azurelinuxagent.common.utils.fileutil as fileutil import azurelinuxagent.common.utils.shellutil as shellutil import azurelinuxagent.common.utils.textutil as textutil # pylint: disable=W0611 from azurelinuxagent.common.utils.cryptutil import CryptUtil from azurelinuxagent.common.osutil.default import DefaultOSUtil class Redhat6xOSUtil(DefaultOSUtil): def __init__(self): super(Redhat6xOSUtil, self).__init__() self.jit_enabled = True def start_network(self): return shellutil.run("/sbin/service networking start", chk_err=False) def restart_ssh_service(self): return shellutil.run("/sbin/service sshd condrestart", chk_err=False) def stop_agent_service(self): return shellutil.run("/sbin/service {0} stop".format(self.service_name), chk_err=False) def start_agent_service(self): return shellutil.run("/sbin/service {0} start".format(self.service_name), chk_err=False) def register_agent_service(self): return shellutil.run("chkconfig --add {0}".format(self.service_name), chk_err=False) def unregister_agent_service(self): return shellutil.run("chkconfig --del {0}".format(self.service_name), chk_err=False) def openssl_to_openssh(self, input_file, output_file): pubkey = fileutil.read_file(input_file) try: cryptutil = CryptUtil(conf.get_openssl_cmd()) ssh_rsa_pubkey = cryptutil.asn1_to_ssh(pubkey) except CryptError as e: raise OSUtilError(ustr(e)) fileutil.append_file(output_file, ssh_rsa_pubkey) # Override def get_dhcp_pid(self): return self._get_dhcp_pid(["pidof", "dhclient"]) def set_hostname(self, hostname): """ Set /etc/sysconfig/network """ fileutil.update_conf_file('/etc/sysconfig/network', 'HOSTNAME', 'HOSTNAME={0}'.format(hostname)) self._run_command_without_raising(["hostname", hostname], log_error=False) def set_dhcp_hostname(self, hostname): ifname = self.get_if_name() filepath = "/etc/sysconfig/network-scripts/ifcfg-{0}".format(ifname) fileutil.update_conf_file(filepath, 'DHCP_HOSTNAME', 'DHCP_HOSTNAME={0}'.format(hostname)) def get_dhcp_lease_endpoint(self): return self.get_endpoint_from_leases_path('/var/lib/dhclient/dhclient-*.leases') class RedhatOSUtil(Redhat6xOSUtil): def __init__(self): super(RedhatOSUtil, self).__init__() self.service_name = self.get_service_name() @staticmethod def get_systemd_unit_file_install_path(): return "/usr/lib/systemd/system" def set_hostname(self, hostname): """ Unlike redhat 6.x, redhat 7.x will set hostname via hostnamectl Due to a bug in systemd in Centos-7.0, if this call fails, fallback to hostname. """ hostnamectl_cmd = ['hostnamectl', 'set-hostname', hostname, '--static'] try: shellutil.run_command(hostnamectl_cmd, log_error=False) except shellutil.CommandError: logger.warn("[{0}] failed, attempting fallback".format(' '.join(hostnamectl_cmd))) DefaultOSUtil.set_hostname(self, hostname) def publish_hostname(self, hostname): """ Restart NetworkManager first before publishing hostname """ shellutil.run("service NetworkManager restart") super(RedhatOSUtil, self).publish_hostname(hostname) def register_agent_service(self): return shellutil.run("systemctl enable {0}".format(self.service_name), chk_err=False) def unregister_agent_service(self): return shellutil.run("systemctl disable {0}".format(self.service_name), chk_err=False) def openssl_to_openssh(self, input_file, output_file): DefaultOSUtil.openssl_to_openssh(self, input_file, output_file) def get_dhcp_lease_endpoint(self): # dhclient endpoint = self.get_endpoint_from_leases_path('/var/lib/dhclient/dhclient-*.lease') if endpoint is None: # NetworkManager endpoint = self.get_endpoint_from_leases_path('/var/lib/NetworkManager/dhclient-*.lease') return endpoint class RedhatOSModernUtil(RedhatOSUtil): def __init__(self): # pylint: disable=W0235 super(RedhatOSModernUtil, self).__init__() def restart_if(self, ifname, retries=3, wait=5): """ Restart an interface by bouncing the link. systemd-networkd observes this event, and forces a renew of DHCP. """ retry_limit = retries + 1 for attempt in range(1, retry_limit): return_code = shellutil.run("ip link set {0} down && ip link set {0} up".format(ifname)) if return_code == 0: return logger.warn("failed to restart {0}: return code {1}".format(ifname, return_code)) if attempt < retry_limit: logger.info("retrying in {0} seconds".format(wait)) time.sleep(wait) else: logger.warn("exceeded restart retries") WALinuxAgent-2.9.1.1/azurelinuxagent/common/osutil/suse.py000066400000000000000000000155451446033677600236070ustar00rootroot00000000000000# # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import time import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.fileutil as fileutil import azurelinuxagent.common.utils.shellutil as shellutil # pylint: disable=W0611 from azurelinuxagent.common.exception import OSUtilError # pylint: disable=W0611 from azurelinuxagent.common.future import ustr # pylint: disable=W0611 from azurelinuxagent.common.osutil.default import DefaultOSUtil class SUSE11OSUtil(DefaultOSUtil): def __init__(self): super(SUSE11OSUtil, self).__init__() self.jit_enabled = True self.dhclient_name = 'dhcpcd' def set_hostname(self, hostname): fileutil.write_file('/etc/HOSTNAME', hostname) self._run_command_without_raising(["hostname", hostname], log_error=False) def get_dhcp_pid(self): return self._get_dhcp_pid(["pidof", self.dhclient_name]) def is_dhcp_enabled(self): return True def stop_dhcp_service(self): self._run_command_without_raising(["/sbin/service", self.dhclient_name, "stop"], log_error=False) def start_dhcp_service(self): self._run_command_without_raising(["/sbin/service", self.dhclient_name, "start"], log_error=False) def start_network(self): self._run_command_without_raising(["/sbin/service", "network", "start"], log_error=False) def restart_ssh_service(self): self._run_command_without_raising(["/sbin/service", "sshd", "restart"], log_error=False) def stop_agent_service(self): self._run_command_without_raising(["/sbin/service", self.service_name, "stop"], log_error=False) def start_agent_service(self): self._run_command_without_raising(["/sbin/service", self.service_name, "start"], log_error=False) def register_agent_service(self): self._run_command_without_raising(["/sbin/insserv", self.service_name], log_error=False) def unregister_agent_service(self): self._run_command_without_raising(["/sbin/insserv", "-r", self.service_name], log_error=False) class SUSEOSUtil(SUSE11OSUtil): def __init__(self): super(SUSEOSUtil, self).__init__() self.dhclient_name = 'wickedd-dhcp4' def publish_hostname(self, hostname): self.set_dhcp_hostname(hostname) self.set_hostname_record(hostname) ifname = self.get_if_name() # To push the hostname to the dhcp server we do not need to # bring down the interface, just make the make ifup do whatever is # necessary self.ifup(ifname) def ifup(self, ifname, retries=3, wait=5): logger.info('Interface {0} bounce with ifup'.format(ifname)) retry_limit=retries+1 for attempt in range(1, retry_limit): try: shellutil.run_command(['ifup', ifname], log_error=True) except Exception: if attempt < retry_limit: logger.info("retrying in {0} seconds".format(wait)) time.sleep(wait) else: logger.warn("exceeded restart retries") @staticmethod def get_systemd_unit_file_install_path(): return "/usr/lib/systemd/system" def set_hostname(self, hostname): self._run_command_without_raising( ["hostnamectl", "set-hostname", hostname], log_error=False ) def set_dhcp_hostname(self, hostname): dhcp_config_file_path = '/etc/sysconfig/network/dhcp' hostname_send_setting = fileutil.get_line_startingwith( 'DHCLIENT_HOSTNAME_OPTION', dhcp_config_file_path ) if hostname_send_setting: value = hostname_send_setting.split('=')[-1] # wicked's source accepts values with double quotes, single quotes, and no quotes at all. if value in ('"AUTO"', "'AUTO'", 'AUTO') or value == '"{0}"'.format(hostname): # Return if auto send host-name is configured or the current # hostname is already set up to be sent return else: # Do not use update_conf_file as it moves the setting to the # end of the file separating it from the contextual comment new_conf = [] dhcp_conf = fileutil.read_file( dhcp_config_file_path).split('\n') for entry in dhcp_conf: if entry.startswith('DHCLIENT_HOSTNAME_OPTION'): new_conf.append( 'DHCLIENT_HOSTNAME_OPTION="{0}"'. format(hostname) ) continue new_conf.append(entry) fileutil.write_file(dhcp_config_file_path, '\n'.join(new_conf)) else: fileutil.append_file( dhcp_config_file_path, 'DHCLIENT_HOSTNAME_OPTION="{0}"'. format(hostname) ) def stop_dhcp_service(self): self._run_command_without_raising(["systemctl", "stop", "{}.service".format(self.dhclient_name)], log_error=False) def start_dhcp_service(self): self._run_command_without_raising(["systemctl", "start", "{}.service".format(self.dhclient_name)], log_error=False) def start_network(self): self._run_command_without_raising(["systemctl", "start", "network.service"], log_error=False) def restart_ssh_service(self): self._run_command_without_raising(["systemctl", "restart", "sshd.service"], log_error=False) def stop_agent_service(self): self._run_command_without_raising(["systemctl", "stop", "{}.service".format(self.service_name)], log_error=False) def start_agent_service(self): self._run_command_without_raising(["systemctl", "start", "{}.service".format(self.service_name)], log_error=False) def register_agent_service(self): self._run_command_without_raising(["systemctl", "enable", "{}.service".format(self.service_name)], log_error=False) def unregister_agent_service(self): self._run_command_without_raising(["systemctl", "disable", "{}.service".format(self.service_name)], log_error=False) WALinuxAgent-2.9.1.1/azurelinuxagent/common/osutil/systemd.py000066400000000000000000000047721446033677600243200ustar00rootroot00000000000000# # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import re from azurelinuxagent.common.osutil import get_osutil from azurelinuxagent.common.utils import shellutil def _get_os_util(): if _get_os_util.value is None: _get_os_util.value = get_osutil() return _get_os_util.value _get_os_util.value = None def is_systemd(): """ Determine if systemd is managing system services; the implementation follows the same strategy as, for example, sd_booted() in libsystemd, or /usr/sbin/service """ return os.path.exists("/run/systemd/system/") def get_version(): # the output is similar to # $ systemctl --version # systemd 245 (245.4-4ubuntu3) # +PAM +AUDIT +SELINUX +IMA +APPARMOR +SMACK +SYSVINIT +UTMP etc # return shellutil.run_command(['systemctl', '--version']) def get_unit_file_install_path(): """ e.g. /lib/systemd/system """ return _get_os_util().get_systemd_unit_file_install_path() def get_agent_unit_name(): """ e.g. walinuxagent.service """ return _get_os_util().get_service_name() + ".service" def get_agent_unit_file(): """ e.g. /lib/systemd/system/walinuxagent.service """ return os.path.join(get_unit_file_install_path(), get_agent_unit_name()) def get_agent_drop_in_path(): """ e.g. /lib/systemd/system/walinuxagent.service.d """ return os.path.join(get_unit_file_install_path(), "{0}.d".format(get_agent_unit_name())) def get_unit_property(unit_name, property_name): output = shellutil.run_command(["systemctl", "show", unit_name, "--property", property_name]) # Output is similar to # # systemctl show walinuxagent.service --property CPUQuotaPerSecUSec # CPUQuotaPerSecUSec=50ms match = re.match("[^=]+=(?P.+)", output) if match is None: raise ValueError("Can't find property {0} of {1}".format(property_name, unit_name)) return match.group('value') WALinuxAgent-2.9.1.1/azurelinuxagent/common/osutil/ubuntu.py000066400000000000000000000122711446033677600241430ustar00rootroot00000000000000# # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import time import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.shellutil as shellutil from azurelinuxagent.common.osutil.default import DefaultOSUtil class Ubuntu14OSUtil(DefaultOSUtil): def __init__(self): super(Ubuntu14OSUtil, self).__init__() self.jit_enabled = True self.service_name = self.get_service_name() @staticmethod def get_service_name(): return "walinuxagent" def start_network(self): return shellutil.run("service networking start", chk_err=False) def stop_agent_service(self): try: shellutil.run_command(["service", self.service_name, "stop"]) except shellutil.CommandError as cmd_err: return cmd_err.returncode return 0 def start_agent_service(self): try: shellutil.run_command(["service", self.service_name, "start"]) except shellutil.CommandError as cmd_err: return cmd_err.returncode return 0 def remove_rules_files(self, rules_files=""): pass def restore_rules_files(self, rules_files=""): pass def get_dhcp_lease_endpoint(self): return self.get_endpoint_from_leases_path('/var/lib/dhcp/dhclient.*.leases') class Ubuntu12OSUtil(Ubuntu14OSUtil): def __init__(self): # pylint: disable=W0235 super(Ubuntu12OSUtil, self).__init__() # Override def get_dhcp_pid(self): return self._get_dhcp_pid(["pidof", "dhclient3"]) class Ubuntu16OSUtil(Ubuntu14OSUtil): """ Ubuntu 16.04, 16.10, and 17.04. """ def __init__(self): super(Ubuntu16OSUtil, self).__init__() self.service_name = self.get_service_name() def register_agent_service(self): return shellutil.run("systemctl unmask {0}".format(self.service_name), chk_err=False) def unregister_agent_service(self): return shellutil.run("systemctl mask {0}".format(self.service_name), chk_err=False) class Ubuntu18OSUtil(Ubuntu16OSUtil): """ Ubuntu 18.04, 18.10, 19.04, 19.10, 20.04 """ def __init__(self): super(Ubuntu18OSUtil, self).__init__() self.service_name = self.get_service_name() def restart_if(self, ifname, retries=3, wait=5): """ Restart systemd-networkd """ retry_limit=retries+1 for attempt in range(1, retry_limit): try: shellutil.run_command(["systemctl", "restart", "systemd-networkd"]) except shellutil.CommandError as cmd_err: logger.warn("failed to restart systemd-networkd: return code {1}".format(cmd_err.returncode)) if attempt < retry_limit: logger.info("retrying in {0} seconds".format(wait)) time.sleep(wait) else: logger.warn("exceeded restart retries") def get_dhcp_pid(self): return self._get_dhcp_pid(["pidof", "systemd-networkd"]) def start_network(self): return shellutil.run("systemctl start systemd-networkd", chk_err=False) def stop_network(self): return shellutil.run("systemctl stop systemd-networkd", chk_err=False) def start_dhcp_service(self): return self.start_network() def stop_dhcp_service(self): return self.stop_network() def start_agent_service(self): return shellutil.run("systemctl start {0}".format(self.service_name), chk_err=False) def stop_agent_service(self): return shellutil.run("systemctl stop {0}".format(self.service_name), chk_err=False) class UbuntuOSUtil(Ubuntu16OSUtil): def __init__(self): # pylint: disable=W0235 super(UbuntuOSUtil, self).__init__() def restart_if(self, ifname, retries=3, wait=5): """ Restart an interface by bouncing the link. systemd-networkd observes this event, and forces a renew of DHCP. """ retry_limit = retries+1 for attempt in range(1, retry_limit): return_code = shellutil.run("ip link set {0} down && ip link set {0} up".format(ifname)) if return_code == 0: return logger.warn("failed to restart {0}: return code {1}".format(ifname, return_code)) if attempt < retry_limit: logger.info("retrying in {0} seconds".format(wait)) time.sleep(wait) else: logger.warn("exceeded restart retries") class UbuntuSnappyOSUtil(Ubuntu14OSUtil): def __init__(self): super(UbuntuSnappyOSUtil, self).__init__() self.conf_file_path = '/apps/walinuxagent/current/waagent.conf' WALinuxAgent-2.9.1.1/azurelinuxagent/common/persist_firewall_rules.py000066400000000000000000000377421446033677600261040ustar00rootroot00000000000000# # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import sys import azurelinuxagent.common.conf as conf from azurelinuxagent.common import logger from azurelinuxagent.common.event import add_event, WALAEventOperation from azurelinuxagent.common.future import ustr from azurelinuxagent.common.osutil import get_osutil, systemd from azurelinuxagent.common.utils import shellutil, fileutil, textutil from azurelinuxagent.common.utils.networkutil import AddFirewallRules from azurelinuxagent.common.utils.shellutil import CommandError class PersistFirewallRulesHandler(object): __SERVICE_FILE_CONTENT = """ # This unit file (Version={version}) was created by the Azure VM Agent. # Do not edit. [Unit] Description=Setup network rules for WALinuxAgent Before=network-pre.target Wants=network-pre.target DefaultDependencies=no ConditionPathExists={binary_path} [Service] Type=oneshot ExecStart={py_path} {binary_path} RemainAfterExit=yes [Install] WantedBy=network.target """ __BINARY_CONTENTS = """ # This python file was created by the Azure VM Agent. Please do not edit. import os if __name__ == '__main__': if os.path.exists("{egg_path}"): os.system("{py_path} {egg_path} --setup-firewall --dst_ip={wire_ip} --uid={user_id} {wait}") else: print("{egg_path} file not found, skipping execution of firewall execution setup for this boot") """ _AGENT_NETWORK_SETUP_NAME_FORMAT = "{0}-network-setup.service" BINARY_FILE_NAME = "waagent-network-setup.py" _FIREWALLD_RUNNING_CMD = ["firewall-cmd", "--state"] # The current version of the unit file; Update it whenever the unit file is modified to ensure Agent can dynamically # modify the unit file on VM too _UNIT_VERSION = "1.3" @staticmethod def get_service_file_path(): osutil = get_osutil() service_name = PersistFirewallRulesHandler._AGENT_NETWORK_SETUP_NAME_FORMAT.format(osutil.get_service_name()) return os.path.join(osutil.get_systemd_unit_file_install_path(), service_name) def __init__(self, dst_ip, uid): """ This class deals with ensuring that Firewall rules are persisted over system reboots. It tries to employ using Firewalld.service if present first as it already has provisions for persistent rules. If not, it then creates a new agent-network-setup.service file and copy it over to the osutil.get_systemd_unit_file_install_path() dynamically On top of it, on every service restart it ensures that the WireIP is overwritten and the new IP is blocked as well. """ osutil = get_osutil() self._network_setup_service_name = self._AGENT_NETWORK_SETUP_NAME_FORMAT.format(osutil.get_service_name()) self._is_systemd = systemd.is_systemd() self._systemd_file_path = osutil.get_systemd_unit_file_install_path() self._dst_ip = dst_ip self._uid = uid self._wait = osutil.get_firewall_will_wait() # The custom service will try to call the current agent executable to setup the firewall rules self._current_agent_executable_path = os.path.join(os.getcwd(), sys.argv[0]) @staticmethod def _is_firewall_service_running(): # Check if firewall-cmd can connect to the daemon # https://docs.fedoraproject.org/en-US/Fedora/19/html/Security_Guide/sec-Check_if_firewalld_is_running.html # Eg: firewall-cmd --state # > running firewalld_state = PersistFirewallRulesHandler._FIREWALLD_RUNNING_CMD try: return shellutil.run_command(firewalld_state).rstrip() == "running" except Exception as error: logger.verbose("{0} command failed: {1}".format(' '.join(firewalld_state), ustr(error))) return False def setup(self): if not systemd.is_systemd(): logger.warn("Did not detect Systemd, unable to set {0}".format(self._network_setup_service_name)) return if self._is_firewall_service_running(): logger.info("Firewalld.service present on the VM, setting up permanent rules on the VM") # In case of a failure, this would throw. In such a case, we don't need to try to setup our custom service # because on system reboot, all iptable rules are reset by firewalld.service so it would be a no-op. self._setup_permanent_firewalld_rules() # Remove custom service if exists to avoid problems with firewalld try: fileutil.rm_files(*[self.get_service_file_path(), os.path.join(conf.get_lib_dir(), self.BINARY_FILE_NAME)]) except Exception as error: logger.info( "Unable to delete existing service {0}: {1}".format(self._network_setup_service_name, ustr(error))) return logger.info( "Firewalld service not running/unavailable, trying to set up {0}".format(self._network_setup_service_name)) self._setup_network_setup_service() def __verify_firewall_rules_enabled(self): # Check if firewall-rules have already been enabled # This function would also return False if the dest-ip is changed. So no need to check separately for that try: AddFirewallRules.check_firewalld_rule_applied(self._dst_ip, self._uid) except Exception as error: logger.verbose( "Check if Firewall rules already applied using firewalld.service failed: {0}".format(ustr(error))) return False return True def __remove_firewalld_rules(self): try: AddFirewallRules.remove_firewalld_rules(self._dst_ip, self._uid) except Exception as error: logger.warn( "failed to remove rule using firewalld.service: {0}".format(ustr(error))) def _setup_permanent_firewalld_rules(self): if self.__verify_firewall_rules_enabled(): logger.info("Firewall rules already set. No change needed.") return logger.info("Firewall rules not added yet, adding them now using firewalld.service") # Remove first if partial list present self.__remove_firewalld_rules() # Add rules if not already set AddFirewallRules.add_firewalld_rules(self._dst_ip, self._uid) logger.info("Successfully added the firewall commands using firewalld.service") def __verify_network_setup_service_enabled(self): # Check if the custom service has already been enabled cmd = ["systemctl", "is-enabled", self._network_setup_service_name] try: return shellutil.run_command(cmd).rstrip() == "enabled" except CommandError as error: msg = "{0} not enabled. Command: {1}, ExitCode: {2}\nStdout: {3}\nStderr: {4}".format( self._network_setup_service_name, ' '.join(cmd), error.returncode, error.stdout, error.stderr) except Exception as error: msg = "Ran into error, {0} not enabled. Error: {1}".format(self._network_setup_service_name, ustr(error)) logger.verbose(msg) return False def _setup_network_setup_service(self): # Even if service is enabled, we need to overwrite the binary file with the current IP in case it changed. # This is to handle the case where WireIP can change midway on service restarts. # Additionally, incase of auto-update this would also update the location of the new EGG file ensuring that # the service is always run from the most latest agent. self.__setup_binary_file() network_service_enabled = self.__verify_network_setup_service_enabled() if network_service_enabled and not self.__unit_file_version_modified(): logger.info("Service: {0} already enabled. No change needed.".format(self._network_setup_service_name)) self.__log_network_setup_service_logs() else: if not network_service_enabled: logger.info("Service: {0} not enabled. Adding it now".format(self._network_setup_service_name)) else: logger.info( "Unit file {0} version modified to {1}, setting it up again".format(self.get_service_file_path(), self._UNIT_VERSION)) # Create unit file with default values self.__set_service_unit_file() # Reload systemd configurations when we setup the service for the first time to avoid systemctl warnings self.__reload_systemd_conf() logger.info("Successfully added and enabled the {0}".format(self._network_setup_service_name)) def __setup_binary_file(self): binary_file_path = os.path.join(conf.get_lib_dir(), self.BINARY_FILE_NAME) try: fileutil.write_file(binary_file_path, self.__BINARY_CONTENTS.format(egg_path=self._current_agent_executable_path, wire_ip=self._dst_ip, user_id=self._uid, wait=self._wait, py_path=sys.executable)) logger.info("Successfully updated the Binary file {0} for firewall setup".format(binary_file_path)) except Exception: logger.warn( "Unable to setup binary file, removing the service unit file {0} to ensure its not run on system reboot".format( self.get_service_file_path())) self.__remove_file_without_raising(binary_file_path) self.__remove_file_without_raising(self.get_service_file_path()) raise def __set_service_unit_file(self): service_unit_file = self.get_service_file_path() binary_path = os.path.join(conf.get_lib_dir(), self.BINARY_FILE_NAME) try: fileutil.write_file(service_unit_file, self.__SERVICE_FILE_CONTENT.format(binary_path=binary_path, py_path=sys.executable, version=self._UNIT_VERSION)) fileutil.chmod(service_unit_file, 0o644) # Finally enable the service. This is needed to ensure the service is started on system boot cmd = ["systemctl", "enable", self._network_setup_service_name] try: shellutil.run_command(cmd) except CommandError as error: msg = ustr( "Unable to enable service: {0}; deleting service file: {1}. Command: {2}, Exit-code: {3}.\nstdout: {4}\nstderr: {5}").format( self._network_setup_service_name, service_unit_file, ' '.join(cmd), error.returncode, error.stdout, error.stderr) raise Exception(msg) except Exception: self.__remove_file_without_raising(service_unit_file) raise @staticmethod def __remove_file_without_raising(file_path): if os.path.exists(file_path): try: os.remove(file_path) except Exception as error: logger.warn("Unable to delete file: {0}; Error: {1}".format(file_path, ustr(error))) def __verify_network_setup_service_failed(self): # Check if the agent-network-setup.service failed in its last run # Note: # The `systemctl is-failed ` command would return "failed" and ExitCode: 0 if the service was actually # in a failed state. # For the rest of the cases (eg: active, in-active, dead, etc) it would return the state and a non-0 ExitCode. cmd = ["systemctl", "is-failed", self._network_setup_service_name] try: return shellutil.run_command(cmd).rstrip() == "failed" except CommandError as error: msg = "{0} not in a failed state. Command: {1}, ExitCode: {2}\nStdout: {3}\nStderr: {4}".format( self._network_setup_service_name, ' '.join(cmd), error.returncode, error.stdout, error.stderr) except Exception as error: msg = "Ran into error, {0} not failed. Error: {1}".format(self._network_setup_service_name, ustr(error)) logger.verbose(msg) return False def __log_network_setup_service_logs(self): # Get logs from journalctl - https://www.freedesktop.org/software/systemd/man/journalctl.html cmd = ["journalctl", "-u", self._network_setup_service_name, "-b", "--utc"] service_failed = self.__verify_network_setup_service_failed() try: stdout = shellutil.run_command(cmd) msg = ustr("Logs from the {0} since system boot:\n {1}").format(self._network_setup_service_name, stdout) logger.info(msg) except CommandError as error: msg = "Unable to fetch service logs, Command: {0} failed with ExitCode: {1}\nStdout: {2}\nStderr: {3}".format( ' '.join(cmd), error.returncode, error.stdout, error.stderr) logger.warn(msg) except Exception as e: msg = "Ran into unexpected error when getting logs for {0} service. Error: {1}".format( self._network_setup_service_name, textutil.format_exception(e)) logger.warn(msg) # Log service status and logs if we can fetch them from journalctl and send it to Kusto, # else just log the error of the failure of fetching logs add_event( op=WALAEventOperation.PersistFirewallRules, is_success=(not service_failed), message=msg, log_event=False) def __reload_systemd_conf(self): try: logger.info("Executing systemctl daemon-reload for setting up {0}".format(self._network_setup_service_name)) shellutil.run_command(["systemctl", "daemon-reload"]) except Exception as exception: logger.warn("Unable to reload systemctl configurations: {0}".format(ustr(exception))) def __get_unit_file_version(self): if not os.path.exists(self.get_service_file_path()): raise OSError("{0} not found".format(self.get_service_file_path())) match = fileutil.findre_in_file(self.get_service_file_path(), line_re="This unit file \\(Version=([\\d.]+)\\) was created by the Azure VM Agent.") if match is None: raise ValueError("Version tag not found in the unit file") return match.group(1).strip() def __unit_file_version_modified(self): """ Check if the unit file version changed from the expected version :return: True if unit file version changed else False """ try: unit_file_version = self.__get_unit_file_version() except Exception as error: logger.info("Unable to determine version of unit file: {0}, overwriting unit file".format(ustr(error))) # Since we can't determine the version, marking the file as modified to overwrite the unit file return True if unit_file_version != self._UNIT_VERSION: logger.info( "Unit file version: {0} does not match with expected version: {1}, overwriting unit file".format( unit_file_version, self._UNIT_VERSION)) return True logger.info( "Unit file version matches with expected version: {0}, not overwriting unit file".format(unit_file_version)) return False WALinuxAgent-2.9.1.1/azurelinuxagent/common/protocol/000077500000000000000000000000001446033677600225665ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/azurelinuxagent/common/protocol/__init__.py000066400000000000000000000011651446033677600247020ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # WALinuxAgent-2.9.1.1/azurelinuxagent/common/protocol/extensions_goal_state.py000066400000000000000000000157571446033677600275600ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2020 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ import datetime from azurelinuxagent.common import logger from azurelinuxagent.common.AgentGlobals import AgentGlobals from azurelinuxagent.common.exception import AgentError from azurelinuxagent.common.utils import textutil class GoalStateChannel(object): WireServer = "WireServer" HostGAPlugin = "HostGAPlugin" Empty = "Empty" class GoalStateSource(object): Fabric = "Fabric" FastTrack = "FastTrack" Empty = "Empty" class VmSettingsParseError(AgentError): """ Error raised when the VmSettings are malformed """ def __init__(self, message, etag, vm_settings_text, inner=None): super(VmSettingsParseError, self).__init__(message, inner) self.etag = etag self.vm_settings_text = vm_settings_text class ExtensionsGoalState(object): """ ExtensionsGoalState represents the extensions information in the goal state; that information can originate from ExtensionsConfig when the goal state is retrieved from the WireServe or from vmSettings when it is retrieved from the HostGAPlugin. NOTE: This is an abstract class. The corresponding concrete classes can be instantiated using the ExtensionsGoalStateFactory. """ def __init__(self): self._is_outdated = False @property def id(self): """ Returns a string that includes the incarnation number if the ExtensionsGoalState was created from ExtensionsConfig, or the etag if it was created from vmSettings. """ raise NotImplementedError() @property def is_outdated(self): """ A goal state can be outdated if, for example, the VM Agent is using Fast Track and support for it stops (e.g. the VM is migrated to a node with an older version of the HostGAPlugin) and now the Agent is fetching goal states via the WireServer. """ return self._is_outdated @is_outdated.setter def is_outdated(self, value): self._is_outdated = value @property def svd_sequence_number(self): raise NotImplementedError() @property def activity_id(self): raise NotImplementedError() @property def correlation_id(self): raise NotImplementedError() @property def created_on_timestamp(self): raise NotImplementedError() @property def channel(self): """ Whether the goal state was retrieved from the WireServer or the HostGAPlugin """ raise NotImplementedError() @property def source(self): """ Whether the goal state originated from Fabric or Fast Track """ raise NotImplementedError() @property def status_upload_blob(self): raise NotImplementedError() @property def status_upload_blob_type(self): raise NotImplementedError() def _set_status_upload_blob_type(self, value): raise NotImplementedError() @property def required_features(self): raise NotImplementedError() @property def on_hold(self): raise NotImplementedError() @property def agent_families(self): raise NotImplementedError() @property def extensions(self): raise NotImplementedError() def get_redacted_text(self): """ Returns the raw text (either the ExtensionsConfig or the vmSettings) with any confidential data removed, or an empty string for empty goal states. """ raise NotImplementedError() def _do_common_validations(self): """ Does validations common to vmSettings and ExtensionsConfig """ if self.status_upload_blob_type not in ["BlockBlob", "PageBlob"]: logger.info("Status Blob type '{0}' is not valid, assuming BlockBlob", self.status_upload_blob) self._set_status_upload_blob_type("BlockBlob") @staticmethod def _ticks_to_utc_timestamp(ticks_string): """ Takes 'ticks', a string indicating the number of ticks since midnight 0001-01-01 00:00:00, and returns a UTC timestamp (every tick is 1/10000000 of a second). """ minimum = datetime.datetime(1900, 1, 1, 0, 0) # min value accepted by datetime.strftime() as_date_time = minimum if ticks_string not in (None, ""): try: as_date_time = datetime.datetime.min + datetime.timedelta(seconds=float(ticks_string) / 10 ** 7) except Exception as exception: logger.verbose("Can't parse ticks: {0}", textutil.format_exception(exception)) as_date_time = max(as_date_time, minimum) return as_date_time.strftime(logger.Logger.LogTimeFormatInUTC) @staticmethod def _string_to_id(id_string): """ Takes 'id', a string indicating an ID, and returns a null GUID if the string is None or empty; otherwise return 'id' unchanged """ if id_string in (None, ""): return AgentGlobals.GUID_ZERO return id_string class EmptyExtensionsGoalState(ExtensionsGoalState): def __init__(self, incarnation): super(EmptyExtensionsGoalState, self).__init__() self._id = "incarnation_{0}".format(incarnation) self._incarnation = incarnation @property def id(self): return self._id @property def incarnation(self): return self._incarnation @property def svd_sequence_number(self): return self._incarnation @property def activity_id(self): return AgentGlobals.GUID_ZERO @property def correlation_id(self): return AgentGlobals.GUID_ZERO @property def created_on_timestamp(self): return datetime.datetime.min @property def channel(self): return GoalStateChannel.Empty @property def source(self): return GoalStateSource.Empty @property def status_upload_blob(self): return None @property def status_upload_blob_type(self): return None def _set_status_upload_blob_type(self, value): raise TypeError("EmptyExtensionsGoalState is immutable; cannot change the value of the status upload blob") @property def required_features(self): return [] @property def on_hold(self): return False @property def agent_families(self): return [] @property def extensions(self): return [] def get_redacted_text(self): return '' WALinuxAgent-2.9.1.1/azurelinuxagent/common/protocol/extensions_goal_state_factory.py000066400000000000000000000027341446033677600312760ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2020 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ from azurelinuxagent.common.protocol.extensions_goal_state import EmptyExtensionsGoalState from azurelinuxagent.common.protocol.extensions_goal_state_from_extensions_config import ExtensionsGoalStateFromExtensionsConfig from azurelinuxagent.common.protocol.extensions_goal_state_from_vm_settings import ExtensionsGoalStateFromVmSettings class ExtensionsGoalStateFactory(object): @staticmethod def create_empty(incarnation): return EmptyExtensionsGoalState(incarnation) @staticmethod def create_from_extensions_config(incarnation, xml_text, wire_client): return ExtensionsGoalStateFromExtensionsConfig(incarnation, xml_text, wire_client) @staticmethod def create_from_vm_settings(etag, json_text, correlation_id): return ExtensionsGoalStateFromVmSettings(etag, json_text, correlation_id) WALinuxAgent-2.9.1.1/azurelinuxagent/common/protocol/extensions_goal_state_from_extensions_config.py000066400000000000000000000675511446033677600344060ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2020 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ import json from collections import defaultdict from azurelinuxagent.common import logger from azurelinuxagent.common.event import add_event, WALAEventOperation from azurelinuxagent.common.exception import ExtensionsConfigError from azurelinuxagent.common.future import ustr from azurelinuxagent.common.protocol.extensions_goal_state import ExtensionsGoalState, GoalStateChannel, GoalStateSource from azurelinuxagent.common.protocol.restapi import ExtensionSettings, Extension, VMAgentFamily, ExtensionState, InVMGoalStateMetaData from azurelinuxagent.common.utils.textutil import parse_doc, parse_json, findall, find, findtext, getattrib, gettext, format_exception, \ is_str_none_or_whitespace, is_str_empty class ExtensionsGoalStateFromExtensionsConfig(ExtensionsGoalState): def __init__(self, incarnation, xml_text, wire_client): super(ExtensionsGoalStateFromExtensionsConfig, self).__init__() self._id = "incarnation_{0}".format(incarnation) self._is_outdated = False self._incarnation = incarnation self._text = xml_text self._status_upload_blob = None self._status_upload_blob_type = None self._required_features = [] self._on_hold = False self._activity_id = None self._correlation_id = None self._created_on_timestamp = None self._agent_families = [] self._extensions = [] try: self._parse_extensions_config(xml_text, wire_client) self._do_common_validations() except Exception as e: raise ExtensionsConfigError("Error parsing ExtensionsConfig (incarnation: {0}): {1}\n{2}".format(incarnation, format_exception(e), self.get_redacted_text())) def _parse_extensions_config(self, xml_text, wire_client): xml_doc = parse_doc(xml_text) ga_families_list = find(xml_doc, "GAFamilies") ga_families = findall(ga_families_list, "GAFamily") for ga_family in ga_families: name = findtext(ga_family, "Name") version = findtext(ga_family, "Version") uris_list = find(ga_family, "Uris") uris = findall(uris_list, "Uri") family = VMAgentFamily(name, version) for uri in uris: family.uris.append(gettext(uri)) self._agent_families.append(family) self.__parse_plugins_and_settings_and_populate_ext_handlers(xml_doc) required_features_list = find(xml_doc, "RequiredFeatures") if required_features_list is not None: self._parse_required_features(required_features_list) self._status_upload_blob = findtext(xml_doc, "StatusUploadBlob") status_upload_node = find(xml_doc, "StatusUploadBlob") self._status_upload_blob_type = getattrib(status_upload_node, "statusBlobType") logger.verbose("Extension config shows status blob type as [{0}]", self._status_upload_blob_type) self._on_hold = ExtensionsGoalStateFromExtensionsConfig._fetch_extensions_on_hold(xml_doc, wire_client) in_vm_gs_metadata = InVMGoalStateMetaData(find(xml_doc, "InVMGoalStateMetaData")) self._activity_id = self._string_to_id(in_vm_gs_metadata.activity_id) self._correlation_id = self._string_to_id(in_vm_gs_metadata.correlation_id) self._created_on_timestamp = self._ticks_to_utc_timestamp(in_vm_gs_metadata.created_on_ticks) @staticmethod def _fetch_extensions_on_hold(xml_doc, wire_client): def log_info(message): logger.info(message) add_event(op=WALAEventOperation.ArtifactsProfileBlob, message=message, is_success=True, log_event=False) def log_warning(message): logger.warn(message) add_event(op=WALAEventOperation.ArtifactsProfileBlob, message=message, is_success=False, log_event=False) artifacts_profile_blob = findtext(xml_doc, "InVMArtifactsProfileBlob") if is_str_none_or_whitespace(artifacts_profile_blob): log_info("ExtensionsConfig does not include a InVMArtifactsProfileBlob; will assume the VM is not on hold") return False try: profile = wire_client.fetch_artifacts_profile_blob(artifacts_profile_blob) except Exception as error: log_warning("Can't download the artifacts profile blob; will assume the VM is not on hold. {0}".format(ustr(error))) return False if is_str_empty(profile): log_info("The artifacts profile blob is empty; will assume the VM is not on hold.") return False try: artifacts_profile = _InVMArtifactsProfile(profile) except Exception as exception: log_warning("Can't parse the artifacts profile blob; will assume the VM is not on hold. Error: {0}".format(ustr(exception))) return False return artifacts_profile.get_on_hold() @property def id(self): return self._id @property def incarnation(self): return self._incarnation @property def svd_sequence_number(self): return self._incarnation @property def activity_id(self): return self._activity_id @property def correlation_id(self): return self._correlation_id @property def created_on_timestamp(self): return self._created_on_timestamp @property def channel(self): return GoalStateChannel.WireServer @property def source(self): return GoalStateSource.Fabric @property def status_upload_blob(self): return self._status_upload_blob @property def status_upload_blob_type(self): return self._status_upload_blob_type def _set_status_upload_blob_type(self, value): self._status_upload_blob_type = value @property def required_features(self): return self._required_features @property def on_hold(self): return self._on_hold @property def agent_families(self): return self._agent_families @property def extensions(self): return self._extensions def get_redacted_text(self): text = self._text for ext_handler in self._extensions: for extension in ext_handler.settings: if extension.protectedSettings is not None: text = text.replace(extension.protectedSettings, "*** REDACTED ***") return text def _parse_required_features(self, required_features_list): for required_feature in findall(required_features_list, "RequiredFeature"): feature_name = findtext(required_feature, "Name") # per the documentation, RequiredFeatures also have a "Value" attribute but currently it is not being populated self._required_features.append(feature_name) def __parse_plugins_and_settings_and_populate_ext_handlers(self, xml_doc): """ Sample ExtensionConfig Plugin and PluginSettings: { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"01_add_extensions_with_dependency":"ff2a3da6-8e12-4ab6-a4ca-4e3a473ab385"} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"01_add_extensions_with_dependency":"2e837740-cf7e-4528-b3a4-241002618f05"} } } ] } """ plugins_list = find(xml_doc, "Plugins") plugins = findall(plugins_list, "Plugin") plugin_settings_list = find(xml_doc, "PluginSettings") plugin_settings = findall(plugin_settings_list, "Plugin") for plugin in plugins: extension = Extension() try: ExtensionsGoalStateFromExtensionsConfig._parse_plugin(extension, plugin) ExtensionsGoalStateFromExtensionsConfig._parse_plugin_settings(extension, plugin_settings) except ExtensionsConfigError as error: extension.invalid_setting_reason = ustr(error) self._extensions.append(extension) @staticmethod def _parse_plugin(extension, plugin): """ Sample config: https://rdfecurrentuswestcache3.blob.core.test-cint.azure-test.net/0e53c53ef0be4178bacb0a1fecf12a74/Microsoft.Azure.Extensions_CustomScript_usstagesc_manifest.xml https://rdfecurrentuswestcache4.blob.core.test-cint.azure-test.net/0e53c53ef0be4178bacb0a1fecf12a74/Microsoft.Azure.Extensions_CustomScript_usstagesc_manifest.xml Note that the `additionalLocations` subnode is populated with links generated by PIR for resiliency. In regions with this feature enabled, CRP will provide any extra links in the format above. If no extra links are provided, the subnode will not exist. """ def _log_error_if_none(attr_name, value): # Plugin Name and Version are very essential fields, without them we wont be able to even report back to CRP # about that handler. For those cases we need to fail the GoalState completely but currently we dont support # reporting status at a GoalState level (we only report at a handler level). # Once that functionality is added to the GA, we would raise here rather than just report error in our logs. if value in (None, ""): add_event(op=WALAEventOperation.InvalidExtensionConfig, message="{0} is None for ExtensionConfig, logging error".format(attr_name), log_event=True, is_success=False) return value extension.name = _log_error_if_none("Extensions.Plugins.Plugin.name", getattrib(plugin, "name")) extension.version = _log_error_if_none("Extensions.Plugins.Plugin.version", getattrib(plugin, "version")) extension.state = getattrib(plugin, "state") if extension.state in (None, ""): raise ExtensionsConfigError("Received empty Extensions.Plugins.Plugin.state, failing Handler") def getattrib_wrapped_in_list(node, attr_name): attr = getattrib(node, attr_name) return [attr] if attr not in (None, "") else [] location = getattrib_wrapped_in_list(plugin, "location") failover_location = getattrib_wrapped_in_list(plugin, "failoverlocation") locations = location + failover_location additional_location_node = find(plugin, "additionalLocations") if additional_location_node is not None: nodes_list = findall(additional_location_node, "additionalLocation") locations += [gettext(node) for node in nodes_list] for uri in locations: extension.manifest_uris.append(uri) @staticmethod def _parse_plugin_settings(extension, plugin_settings): """ Sample config: { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"01_add_extensions_with_dependency":"ff2a3da6-8e12-4ab6-a4ca-4e3a473ab385"} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"source":{"script":"Write-Host First: Hello World TestTry2!"},"parameters":[{"name":"extensionName","value":"firstRunCommand"}],"timeoutInSeconds":120} } } ] } """ if plugin_settings is None: return extension_name = extension.name version = extension.version def to_lower(str_to_change): return str_to_change.lower() if str_to_change is not None else None extension_plugin_settings = [x for x in plugin_settings if to_lower(getattrib(x, "name")) == to_lower(extension_name)] if not extension_plugin_settings: return settings = [x for x in extension_plugin_settings if getattrib(x, "version") == version] if len(settings) != len(extension_plugin_settings): msg = "Extension PluginSettings Version Mismatch! Expected PluginSettings version: {0} for Extension: {1} but found versions: ({2})".format( version, extension_name, ', '.join(set([getattrib(x, "version") for x in extension_plugin_settings]))) add_event(op=WALAEventOperation.PluginSettingsVersionMismatch, message=msg, log_event=True, is_success=False) raise ExtensionsConfigError(msg) if len(settings) > 1: msg = "Multiple plugin settings found for the same extension: {0} and version: {1} (Expected: 1; Available: {2})".format( extension_name, version, len(settings)) raise ExtensionsConfigError(msg) plugin_settings_node = settings[0] runtime_settings_nodes = findall(plugin_settings_node, "RuntimeSettings") extension_runtime_settings_nodes = findall(plugin_settings_node, "ExtensionRuntimeSettings") if any(runtime_settings_nodes) and any(extension_runtime_settings_nodes): # There can only be a single RuntimeSettings node or multiple ExtensionRuntimeSettings nodes per Plugin msg = "Both RuntimeSettings and ExtensionRuntimeSettings found for the same extension: {0} and version: {1}".format( extension_name, version) raise ExtensionsConfigError(msg) if runtime_settings_nodes: if len(runtime_settings_nodes) > 1: msg = "Multiple RuntimeSettings found for the same extension: {0} and version: {1} (Expected: 1; Available: {2})".format( extension_name, version, len(runtime_settings_nodes)) raise ExtensionsConfigError(msg) # Only Runtime settings available, parse that ExtensionsGoalStateFromExtensionsConfig.__parse_runtime_settings(plugin_settings_node, runtime_settings_nodes[0], extension_name, extension) elif extension_runtime_settings_nodes: # Parse the ExtensionRuntime settings for the given extension ExtensionsGoalStateFromExtensionsConfig.__parse_extension_runtime_settings(plugin_settings_node, extension_runtime_settings_nodes, extension) @staticmethod def __get_dependency_level_from_node(depends_on_node, name): depends_on_level = 0 if depends_on_node is not None: try: depends_on_level = int(getattrib(depends_on_node, "dependencyLevel")) except (ValueError, TypeError): logger.warn("Could not parse dependencyLevel for handler {0}. Setting it to 0".format(name)) depends_on_level = 0 return depends_on_level @staticmethod def __parse_runtime_settings(plugin_settings_node, runtime_settings_node, extension_name, extension): """ Sample Plugin in PluginSettings containing DependsOn and RuntimeSettings (single settings per extension) - { "runtimeSettings": [ { "handlerSettings": { "protectedSettingsCertThumbprint": "", "protectedSettings": "", "publicSettings": {"UserName":"test1234"} } } ] } """ depends_on_nodes = findall(plugin_settings_node, "DependsOn") if len(depends_on_nodes) > 1: msg = "Extension Handler can only have a single dependsOn node for Single config extensions. Found: {0}".format( len(depends_on_nodes)) raise ExtensionsConfigError(msg) depends_on_node = depends_on_nodes[0] if depends_on_nodes else None depends_on_level = ExtensionsGoalStateFromExtensionsConfig.__get_dependency_level_from_node(depends_on_node, extension_name) ExtensionsGoalStateFromExtensionsConfig.__parse_and_add_extension_settings(runtime_settings_node, extension_name, extension, depends_on_level) @staticmethod def __parse_extension_runtime_settings(plugin_settings_node, extension_runtime_settings_nodes, extension): """ Sample PluginSettings containing DependsOn and ExtensionRuntimeSettings - { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"source":{"script":"Write-Host First: Hello World 1234!"}} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"source":{"script":"Write-Host First: Hello World 1234!"}} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"source":{"script":"Write-Host Third: Hello World 3!"}} } } ] } """ # Parse and cache the Dependencies for each extension first dependency_levels = defaultdict(int) for depends_on_node in findall(plugin_settings_node, "DependsOn"): extension_name = getattrib(depends_on_node, "name") if extension_name in (None, ""): raise ExtensionsConfigError("No Name not specified for DependsOn object in ExtensionRuntimeSettings for MultiConfig!") dependency_level = ExtensionsGoalStateFromExtensionsConfig.__get_dependency_level_from_node(depends_on_node, extension_name) dependency_levels[extension_name] = dependency_level extension.supports_multi_config = True for extension_runtime_setting_node in extension_runtime_settings_nodes: # Name and State will only be set for ExtensionRuntimeSettings for Multi-Config extension_name = getattrib(extension_runtime_setting_node, "name") if extension_name in (None, ""): raise ExtensionsConfigError("Extension Name not specified for ExtensionRuntimeSettings for MultiConfig!") # State can either be `ExtensionState.Enabled` (default) or `ExtensionState.Disabled` state = getattrib(extension_runtime_setting_node, "state") state = ustr(state.lower()) if state not in (None, "") else ExtensionState.Enabled ExtensionsGoalStateFromExtensionsConfig.__parse_and_add_extension_settings(extension_runtime_setting_node, extension_name, extension, dependency_levels[extension_name], state=state) @staticmethod def __parse_and_add_extension_settings(settings_node, name, extension, depends_on_level, state=ExtensionState.Enabled): seq_no = getattrib(settings_node, "seqNo") if seq_no in (None, ""): raise ExtensionsConfigError("SeqNo not specified for the Extension: {0}".format(name)) try: runtime_settings = json.loads(gettext(settings_node)) except ValueError as error: logger.error("Invalid extension settings: {0}", ustr(error)) # Incase of invalid/no settings, add the name and seqNo of the Extension and treat it as an extension with # no settings since we were able to successfully parse those data properly. Without this, we wont report # anything for that sequence number and CRP would eventually have to timeout rather than fail fast. extension.settings.append( ExtensionSettings(name=name, sequenceNumber=seq_no, state=state, dependencyLevel=depends_on_level)) return for plugin_settings_list in runtime_settings["runtimeSettings"]: handler_settings = plugin_settings_list["handlerSettings"] extension_settings = ExtensionSettings() # There is no "extension name" for single Handler Settings. Use HandlerName for those extension_settings.name = name extension_settings.state = state extension_settings.sequenceNumber = int(seq_no) extension_settings.publicSettings = handler_settings.get("publicSettings") extension_settings.protectedSettings = handler_settings.get("protectedSettings") extension_settings.dependencyLevel = depends_on_level thumbprint = handler_settings.get("protectedSettingsCertThumbprint") extension_settings.certificateThumbprint = thumbprint extension.settings.append(extension_settings) # Do not extend this class class _InVMArtifactsProfile(object): """ deserialized json string of InVMArtifactsProfile. It is expected to contain the following fields: * inVMArtifactsProfileBlobSeqNo * profileId (optional) * onHold (optional) * certificateThumbprint (optional) * encryptedHealthChecks (optional) * encryptedApplicationProfile (optional) """ def __init__(self, artifacts_profile_json): self._on_hold = False artifacts_profile = parse_json(artifacts_profile_json) on_hold = artifacts_profile.get('onHold') if on_hold is not None: # accept both bool and str values on_hold_normalized = str(on_hold).lower() if on_hold_normalized == "true": self._on_hold = True elif on_hold_normalized == "false": self._on_hold = False else: raise Exception("Invalid value for onHold: {0}".format(on_hold)) def get_on_hold(self): return self._on_hold WALinuxAgent-2.9.1.1/azurelinuxagent/common/protocol/extensions_goal_state_from_vm_settings.py000066400000000000000000000616731446033677600332230ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2020 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ import datetime import json import re import sys from azurelinuxagent.common import logger from azurelinuxagent.common.AgentGlobals import AgentGlobals from azurelinuxagent.common.future import ustr from azurelinuxagent.common.protocol.extensions_goal_state import ExtensionsGoalState, GoalStateChannel, VmSettingsParseError from azurelinuxagent.common.protocol.restapi import VMAgentFamily, Extension, ExtensionRequestedState, ExtensionSettings from azurelinuxagent.common.utils.flexible_version import FlexibleVersion class ExtensionsGoalStateFromVmSettings(ExtensionsGoalState): _MINIMUM_TIMESTAMP = datetime.datetime(1900, 1, 1, 0, 0) # min value accepted by datetime.strftime() def __init__(self, etag, json_text, correlation_id): super(ExtensionsGoalStateFromVmSettings, self).__init__() self._id = "etag_{0}".format(etag) self._etag = etag self._svd_sequence_number = 0 self._hostga_plugin_correlation_id = correlation_id self._text = json_text self._host_ga_plugin_version = FlexibleVersion('0.0.0.0') self._schema_version = FlexibleVersion('0.0.0.0') self._activity_id = AgentGlobals.GUID_ZERO self._correlation_id = AgentGlobals.GUID_ZERO self._created_on_timestamp = self._MINIMUM_TIMESTAMP self._source = None self._status_upload_blob = None self._status_upload_blob_type = None self._required_features = [] self._on_hold = False self._agent_families = [] self._extensions = [] try: self._parse_vm_settings(json_text) self._do_common_validations() except Exception as e: message = "Error parsing vmSettings [HGAP: {0} Etag:{1}]: {2}".format(self._host_ga_plugin_version, etag, ustr(e)) raise VmSettingsParseError(message, etag, self.get_redacted_text()) @property def id(self): return self._id @property def etag(self): return self._etag @property def svd_sequence_number(self): return self._svd_sequence_number @property def host_ga_plugin_version(self): return self._host_ga_plugin_version @property def schema_version(self): return self._schema_version @property def activity_id(self): """ The CRP activity id """ return self._activity_id @property def correlation_id(self): """ The correlation id for the CRP operation """ return self._correlation_id @property def hostga_plugin_correlation_id(self): """ The correlation id for the call to the HostGAPlugin vmSettings API """ return self._hostga_plugin_correlation_id @property def created_on_timestamp(self): """ Timestamp assigned by the CRP (time at which the goal state was created) """ return self._created_on_timestamp @property def channel(self): return GoalStateChannel.HostGAPlugin @property def source(self): return self._source @property def status_upload_blob(self): return self._status_upload_blob @property def status_upload_blob_type(self): return self._status_upload_blob_type def _set_status_upload_blob_type(self, value): self._status_upload_blob_type = value @property def required_features(self): return self._required_features @property def on_hold(self): return self._on_hold @property def agent_families(self): return self._agent_families @property def extensions(self): return self._extensions def get_redacted_text(self): return re.sub(r'("protectedSettings"\s*:\s*)"[^"]+"', r'\1"*** REDACTED ***"', self._text) def _parse_vm_settings(self, json_text): vm_settings = _CaseFoldedDict.from_dict(json.loads(json_text)) self._parse_simple_attributes(vm_settings) self._parse_status_upload_blob(vm_settings) self._parse_required_features(vm_settings) self._parse_agent_manifests(vm_settings) self._parse_extensions(vm_settings) def _parse_simple_attributes(self, vm_settings): # Sample: # { # "hostGAPluginVersion": "1.0.8.115", # "vmSettingsSchemaVersion": "0.0", # "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", # "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", # "inSvdSeqNo": 1, # "extensionsLastModifiedTickCount": 637726657706205217, # "extensionGoalStatesSource": "FastTrack", # ... # } # The HGAP version is included in some messages, so parse it first host_ga_plugin_version = vm_settings.get("hostGAPluginVersion") if host_ga_plugin_version is not None: self._host_ga_plugin_version = FlexibleVersion(host_ga_plugin_version) self._activity_id = self._string_to_id(vm_settings.get("activityId")) self._correlation_id = self._string_to_id(vm_settings.get("correlationId")) self._svd_sequence_number = self._string_to_id(vm_settings.get("inSvdSeqNo")) self._created_on_timestamp = self._ticks_to_utc_timestamp(vm_settings.get("extensionsLastModifiedTickCount")) schema_version = vm_settings.get("vmSettingsSchemaVersion") if schema_version is not None: self._schema_version = FlexibleVersion(schema_version) on_hold = vm_settings.get("onHold") if on_hold is not None: self._on_hold = on_hold self._source = vm_settings.get("extensionGoalStatesSource") if self._source is None: self._source = "UNKNOWN" def _parse_status_upload_blob(self, vm_settings): # Sample: # { # ... # "statusUploadBlob": { # "statusBlobType": "BlockBlob", # "value": "https://dcrcl3a0xs.blob.core.windows.net/$system/edp0plkw2b.86f4ae0a-61f8-48ae-9199-40f402d56864.status?sv=2018-03-28&sr=b&sk=system-1&sig=KNWgC2%3d&se=9999-01-01T00%3a00%3a00Z&sp=w" # }, # ... # } status_upload_blob = vm_settings.get("statusUploadBlob") if status_upload_blob is None: self._status_upload_blob = None self._status_upload_blob_type = "BlockBlob" else: self._status_upload_blob = status_upload_blob.get("value") if self._status_upload_blob is None: raise Exception("Missing statusUploadBlob.value") self._status_upload_blob_type = status_upload_blob.get("statusBlobType") if self._status_upload_blob_type is None: self._status_upload_blob_type = "BlockBlob" def _parse_required_features(self, vm_settings): # Sample: # { # ... # "requiredFeatures": [ # { # "name": "MultipleExtensionsPerHandler" # } # ], # ... # } required_features = vm_settings.get("requiredFeatures") if required_features is not None: if not isinstance(required_features, list): raise Exception("requiredFeatures should be an array (got {0})".format(required_features)) def get_required_features_names(): for feature in required_features: name = feature.get("name") if name is None: raise Exception("A required feature is missing the 'name' property (got {0})".format(feature)) yield name self._required_features.extend(get_required_features_names()) def _parse_agent_manifests(self, vm_settings): # Sample: # { # ... # "gaFamilies": [ # { # "name": "Prod", # "version": "9.9.9.9", # "uris": [ # "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml", # "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml" # ] # }, # { # "name": "Test", # "uris": [ # "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml", # "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml" # ] # } # ], # ... # } families = vm_settings.get("gaFamilies") if families is None: return if not isinstance(families, list): raise Exception("gaFamilies should be an array (got {0})".format(families)) for family in families: name = family["name"] version = family.get("version") uris = family.get("uris") if uris is None: uris = [] agent_family = VMAgentFamily(name, version) for u in uris: agent_family.uris.append(u) self._agent_families.append(agent_family) def _parse_extensions(self, vm_settings): # Sample (NOTE: The first sample is single-config, the second multi-config): # { # ... # "extensionGoalStates": [ # { # "name": "Microsoft.Azure.Monitor.AzureMonitorLinuxAgent", # "version": "1.9.1", # "location": "https://zrdfepirv2cbn04prdstr01a.blob.core.windows.net/a47f0806d764480a8d989d009c75007d/Microsoft.Azure.Monitor_AzureMonitorLinuxAgent_useast2euap_manifest.xml", # "state": "enabled", # "autoUpgrade": true, # "runAsStartupTask": false, # "isJson": true, # "useExactVersion": true, # "settingsSeqNo": 0, # "settings": [ # { # "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", # "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", # "publicSettings": "{\"GCS_AUTO_CONFIG\":true}" # } # ], # "dependsOn": [ # ... # ] # }, # { # "name": "Microsoft.CPlat.Core.RunCommandHandlerLinux", # "version": "1.2.0", # "location": "https://umsavbvncrpzbnxmxzmr.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml", # "failoverlocation": "https://umsajbjtqrb3zqjvgb2z.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml", # "additionalLocations": [ # "https://umsawqtlsshtn5v2nfgh.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml" # ], # "state": "enabled", # "autoUpgrade": true, # "runAsStartupTask": false, # "isJson": true, # "useExactVersion": true, # "settingsSeqNo": 0, # "isMultiConfig": true, # "settings": [ # { # "publicSettings": "{\"source\":{\"script\":\"echo '4abb1e88-f349-41f8-8442-247d9fdfcac5'\"}}", # "seqNo": 0, # "extensionName": "MCExt1", # "extensionState": "enabled" # }, # { # "publicSettings": "{\"source\":{\"script\":\"echo 'e865c9bc-a7b3-42c6-9a79-cfa98a1ee8b3'\"}}", # "seqNo": 0, # "extensionName": "MCExt2", # "extensionState": "enabled" # }, # { # "publicSettings": "{\"source\":{\"script\":\"echo 'f923e416-0340-485c-9243-8b84fb9930c6'\"}}", # "seqNo": 0, # "extensionName": "MCExt3", # "extensionState": "enabled" # } # ], # "dependsOn": [ # ... # ] # } # ... # ] # ... # } extension_goal_states = vm_settings.get("extensionGoalStates") if extension_goal_states is not None: if not isinstance(extension_goal_states, list): raise Exception("extension_goal_states should be an array (got {0})".format(type(extension_goal_states))) # report only the type, since the value may contain secrets for extension_gs in extension_goal_states: extension = Extension() extension.name = extension_gs['name'] extension.version = extension_gs['version'] extension.state = extension_gs['state'] if extension.state not in ExtensionRequestedState.All: raise Exception('Invalid extension state: {0} ({1})'.format(extension.state, extension.name)) is_multi_config = extension_gs.get('isMultiConfig') if is_multi_config is not None: extension.supports_multi_config = is_multi_config location = extension_gs.get('location') if location is not None: extension.manifest_uris.append(location) fail_over_location = extension_gs.get('failoverLocation') if fail_over_location is not None: extension.manifest_uris.append(fail_over_location) additional_locations = extension_gs.get('additionalLocations') if additional_locations is not None: if not isinstance(additional_locations, list): raise Exception('additionalLocations should be an array (got {0})'.format(additional_locations)) extension.manifest_uris.extend(additional_locations) # # Settings # settings_list = extension_gs.get('settings') if settings_list is not None: if not isinstance(settings_list, list): raise Exception("'settings' should be an array (extension: {0})".format(extension.name)) if not extension.supports_multi_config and len(settings_list) > 1: raise Exception("Single-config extension includes multiple settings (extension: {0})".format(extension.name)) for s in settings_list: settings = ExtensionSettings() public_settings = s.get('publicSettings') # Note that publicSettings, protectedSettings and protectedSettingsCertThumbprint can be None; do not change this to, for example, # empty, since those values are serialized to the extension's status file and extensions may depend on the current implementation # (for example, no public settings would currently be serialized as '"publicSettings": null') settings.publicSettings = None if public_settings is None else json.loads(public_settings) settings.protectedSettings = s.get('protectedSettings') thumbprint = s.get('protectedSettingsCertThumbprint') if thumbprint is None and settings.protectedSettings is not None: raise Exception("The certificate thumbprint for protected settings is missing (extension: {0})".format(extension.name)) settings.certificateThumbprint = thumbprint # in multi-config each settings have their own name, sequence number and state if extension.supports_multi_config: settings.name = s['extensionName'] settings.sequenceNumber = s['seqNo'] settings.state = s['extensionState'] else: settings.name = extension.name settings.sequenceNumber = extension_gs['settingsSeqNo'] settings.state = extension.state extension.settings.append(settings) # # Dependency level # depends_on = extension_gs.get("dependsOn") if depends_on is not None: self._parse_dependency_level(depends_on, extension) self._extensions.append(extension) @staticmethod def _parse_dependency_level(depends_on, extension): # Sample (NOTE: The first sample is single-config, the second multi-config): # { # ... # "extensionGoalStates": [ # { # "name": "Microsoft.Azure.Monitor.AzureMonitorLinuxAgent", # ... # "settings": [ # ... # ], # "dependsOn": [ # { # "DependsOnExtension": [ # { # "handler": "Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent" # } # ], # "dependencyLevel": 1 # } # ] # }, # { # "name": "Microsoft.CPlat.Core.RunCommandHandlerLinux", # ... # "isMultiConfig": true, # "settings": [ # { # ... # "extensionName": "MCExt1", # }, # { # ... # "extensionName": "MCExt2", # }, # { # ... # "extensionName": "MCExt3", # } # ], # "dependsOn": [ # { # "dependsOnExtension": [ # { # "extension": "...", # "handler": "..." # }, # { # "extension": "...", # "handler": "..." # } # ], # "dependencyLevel": 2, # "name": "MCExt1" # }, # { # "dependsOnExtension": [ # { # "extension": "...", # "handler": "..." # } # ], # "dependencyLevel": 1, # "name": "MCExt2" # } # ... # ] # ... # } if not isinstance(depends_on, list): raise Exception('dependsOn should be an array ({0}) (got {1})'.format(extension.name, depends_on)) if not extension.supports_multi_config: # single-config length = len(depends_on) if length > 1: raise Exception('dependsOn should be an array with exactly one item for single-config extensions ({0}) (got {1})'.format(extension.name, depends_on)) elif length == 0: logger.warn('dependsOn is an empty array for extension {0}; setting the dependency level to 0'.format(extension.name)) extension.settings[0].dependencyLevel = 0 else: extension.settings[0].dependencyLevel = depends_on[0]['dependencyLevel'] else: # multi-config settings_by_name = {} for settings in extension.settings: settings_by_name[settings.name] = settings for dependency in depends_on: settings = settings_by_name.get(dependency["name"]) if settings is None: raise Exception("Dependency '{0}' does not correspond to any of the settings in the extension (settings: {1})".format(dependency["name"], settings_by_name.keys())) settings.dependencyLevel = dependency["dependencyLevel"] # # TODO: The current implementation of the vmSettings API uses inconsistent cases on the names of the json items it returns. # To work around that, we use _CaseFoldedDict to query those json items in a case-insensitive matter, Do not use # _CaseFoldedDict for other purposes. Remove it once the vmSettings API is updated. # class _CaseFoldedDict(dict): @staticmethod def from_dict(dictionary): case_folded = _CaseFoldedDict() for key, value in dictionary.items(): case_folded[key] = _CaseFoldedDict._to_case_folded_dict_item(value) return case_folded def get(self, key): return super(_CaseFoldedDict, self).get(_casefold(key)) def has_key(self, key): return super(_CaseFoldedDict, self).get(_casefold(key)) def __getitem__(self, key): return super(_CaseFoldedDict, self).__getitem__(_casefold(key)) def __setitem__(self, key, value): return super(_CaseFoldedDict, self).__setitem__(_casefold(key), value) def __contains__(self, key): return super(_CaseFoldedDict, self).__contains__(_casefold(key)) @staticmethod def _to_case_folded_dict_item(item): if isinstance(item, dict): case_folded_dict = _CaseFoldedDict() for key, value in item.items(): case_folded_dict[_casefold(key)] = _CaseFoldedDict._to_case_folded_dict_item(value) return case_folded_dict if isinstance(item, list): return [_CaseFoldedDict._to_case_folded_dict_item(list_item) for list_item in item] return item def copy(self): raise NotImplementedError() @staticmethod def fromkeys(*args, **kwargs): raise NotImplementedError() def pop(self, key, default=None): raise NotImplementedError() def setdefault(self, key, default=None): raise NotImplementedError() def update(self, E=None, **F): # known special case of dict.update raise NotImplementedError() def __delitem__(self, *args, **kwargs): raise NotImplementedError() # casefold() does not exist on Python 2 so we use lower() there def _casefold(string): if sys.version_info[0] == 2: return type(string).lower(string) # the type of "string" can be unicode or str # Class 'str' has no 'casefold' member (no-member) -- Disabled: This warning shows up on Python 2.7 pylint runs # but this code is actually not executed on Python 2. return str.casefold(string) # pylint: disable=no-member WALinuxAgent-2.9.1.1/azurelinuxagent/common/protocol/goal_state.py000066400000000000000000000776011446033677600252750ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2020 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ import datetime import os import re import time import json from azurelinuxagent.common import conf from azurelinuxagent.common import logger from azurelinuxagent.common.AgentGlobals import AgentGlobals from azurelinuxagent.common.datacontract import set_properties from azurelinuxagent.common.event import add_event, WALAEventOperation from azurelinuxagent.common.exception import ProtocolError, ResourceGoneError from azurelinuxagent.common.future import ustr from azurelinuxagent.common.protocol.extensions_goal_state_factory import ExtensionsGoalStateFactory from azurelinuxagent.common.protocol.extensions_goal_state import VmSettingsParseError, GoalStateSource from azurelinuxagent.common.protocol.hostplugin import VmSettingsNotSupported, VmSettingsSupportStopped from azurelinuxagent.common.protocol.restapi import Cert, CertList, RemoteAccessUser, RemoteAccessUsersList, ExtHandlerPackage, ExtHandlerPackageList from azurelinuxagent.common.utils import fileutil from azurelinuxagent.common.utils.archive import GoalStateHistory, SHARED_CONF_FILE_NAME from azurelinuxagent.common.utils.cryptutil import CryptUtil from azurelinuxagent.common.utils.textutil import parse_doc, findall, find, findtext, getattrib, gettext GOAL_STATE_URI = "http://{0}/machine/?comp=goalstate" CERTS_FILE_NAME = "Certificates.xml" P7M_FILE_NAME = "Certificates.p7m" PEM_FILE_NAME = "Certificates.pem" TRANSPORT_CERT_FILE_NAME = "TransportCert.pem" TRANSPORT_PRV_FILE_NAME = "TransportPrivate.pem" _GET_GOAL_STATE_MAX_ATTEMPTS = 6 class GoalStateProperties(object): """ Enum for defining the properties that we fetch in the goal state """ RoleConfig = 0x1 HostingEnv = 0x2 SharedConfig = 0x4 ExtensionsGoalState = 0x8 Certificates = 0x10 RemoteAccessInfo = 0x20 All = RoleConfig | HostingEnv | SharedConfig | ExtensionsGoalState | Certificates | RemoteAccessInfo class GoalStateInconsistentError(ProtocolError): """ Indicates an inconsistency in the goal state (e.g. missing tenant certificate) """ def __init__(self, msg, inner=None): super(GoalStateInconsistentError, self).__init__(msg, inner) class GoalState(object): def __init__(self, wire_client, goal_state_properties=GoalStateProperties.All, silent=False): """ Fetches the goal state using the given wire client. Fetching the goal state involves several HTTP requests to the WireServer and the HostGAPlugin. There is an initial request to WireServer's goalstate API, which response includes the incarnation, role instance, container ID, role config, and URIs to the rest of the goal state (ExtensionsConfig, Certificates, Remote Access users, etc.). Additional requests are done using those URIs (all of them point to APIs in the WireServer). Additionally, there is a request to the HostGAPlugin for the vmSettings, which determines the goal state for extensions when using the Fast Track pipeline. To reduce the number of requests, when possible, create a single instance of GoalState and use the update() method to keep it up to date. """ try: self._wire_client = wire_client self._history = None self._extensions_goal_state = None # populated from vmSettings or extensionsConfig self._goal_state_properties = goal_state_properties self.logger = logger.Logger(logger.DEFAULT_LOGGER) self.logger.silent = silent # These properties hold the goal state from the WireServer and are initialized by self._fetch_full_wire_server_goal_state() self._incarnation = None self._role_instance_id = None self._role_config_name = None self._container_id = None self._hosting_env = None self._shared_conf = None self._certs = EmptyCertificates() self._certs_uri = None self._remote_access = None self.update(silent=silent) except ProtocolError: raise except Exception as exception: # We don't log the error here since fetching the goal state is done every few seconds raise ProtocolError(msg="Error fetching goal state", inner=exception) @property def incarnation(self): return self._incarnation @property def container_id(self): if not self._goal_state_properties & GoalStateProperties.RoleConfig: raise ProtocolError("ContainerId is not in goal state properties") else: return self._container_id @property def role_instance_id(self): if not self._goal_state_properties & GoalStateProperties.RoleConfig: raise ProtocolError("RoleInstanceId is not in goal state properties") else: return self._role_instance_id @property def role_config_name(self): if not self._goal_state_properties & GoalStateProperties.RoleConfig: raise ProtocolError("RoleConfig is not in goal state properties") else: return self._role_config_name @property def extensions_goal_state(self): if not self._goal_state_properties & GoalStateProperties.ExtensionsGoalState: raise ProtocolError("ExtensionsGoalState is not in goal state properties") else: return self._extensions_goal_state @property def certs(self): if not self._goal_state_properties & GoalStateProperties.Certificates: raise ProtocolError("Certificates is not in goal state properties") else: return self._certs @property def hosting_env(self): if not self._goal_state_properties & GoalStateProperties.HostingEnv: raise ProtocolError("HostingEnvironment is not in goal state properties") else: return self._hosting_env @property def shared_conf(self): if not self._goal_state_properties & GoalStateProperties.SharedConfig: raise ProtocolError("SharedConfig is not in goal state properties") else: return self._shared_conf @property def remote_access(self): if not self._goal_state_properties & GoalStateProperties.RemoteAccessInfo: raise ProtocolError("RemoteAccessInfo is not in goal state properties") else: return self._remote_access def fetch_agent_manifest(self, family_name, uris): """ This is a convenience method that wraps WireClient.fetch_manifest(), but adds the required 'use_verify_header' parameter and saves the manifest to the history folder. """ return self._fetch_manifest("agent", "waagent.{0}".format(family_name), uris) def fetch_extension_manifest(self, extension_name, uris): """ This is a convenience method that wraps WireClient.fetch_manifest(), but adds the required 'use_verify_header' parameter and saves the manifest to the history folder. """ return self._fetch_manifest("extension", extension_name, uris) def _fetch_manifest(self, manifest_type, name, uris): try: is_fast_track = self.extensions_goal_state.source == GoalStateSource.FastTrack xml_text = self._wire_client.fetch_manifest(uris, use_verify_header=is_fast_track) self._history.save_manifest(name, xml_text) return ExtensionManifest(xml_text) except Exception as e: raise ProtocolError("Failed to retrieve {0} manifest. Error: {1}".format(manifest_type, ustr(e))) @staticmethod def update_host_plugin_headers(wire_client): """ Updates the container ID and role config name that are send in the headers of HTTP requests to the HostGAPlugin """ # Fetching the goal state updates the HostGAPlugin so simply trigger the request GoalState._fetch_goal_state(wire_client) def update(self, silent=False): """ Updates the current GoalState instance fetching values from the WireServer/HostGAPlugin as needed """ self.logger.silent = silent try: self._update(force_update=False) except GoalStateInconsistentError as e: self.logger.warn("Detected an inconsistency in the goal state: {0}", ustr(e)) self._update(force_update=True) self.logger.info("The goal state is consistent") def _update(self, force_update): # # Fetch the goal state from both the HGAP and the WireServer # timestamp = datetime.datetime.utcnow() if force_update: self.logger.info("Refreshing goal state and vmSettings") incarnation, xml_text, xml_doc = GoalState._fetch_goal_state(self._wire_client) goal_state_updated = force_update or incarnation != self._incarnation if goal_state_updated: message = 'Fetched a new incarnation for the WireServer goal state [incarnation {0}]'.format(incarnation) self.logger.info(message) add_event(op=WALAEventOperation.GoalState, message=message) vm_settings, vm_settings_updated = None, False if self._goal_state_properties & GoalStateProperties.ExtensionsGoalState: try: vm_settings, vm_settings_updated = GoalState._fetch_vm_settings(self._wire_client, force_update=force_update) except VmSettingsSupportStopped as exception: # If the HGAP stopped supporting vmSettings, we need to use the goal state from the WireServer self._restore_wire_server_goal_state(incarnation, xml_text, xml_doc, exception) return if vm_settings_updated: self.logger.info('') message = "Fetched new vmSettings [HostGAPlugin correlation ID: {0} eTag: {1} source: {2}]".format(vm_settings.hostga_plugin_correlation_id, vm_settings.etag, vm_settings.source) self.logger.info(message) add_event(op=WALAEventOperation.GoalState, message=message) # Ignore the vmSettings if their source is Fabric (processing a Fabric goal state may require the tenant certificate and the vmSettings don't include it.) if vm_settings is not None and vm_settings.source == GoalStateSource.Fabric: if vm_settings_updated: message = "The vmSettings originated via Fabric; will ignore them." self.logger.info(message) add_event(op=WALAEventOperation.GoalState, message=message) vm_settings, vm_settings_updated = None, False # If neither goal state has changed we are done with the update if not goal_state_updated and not vm_settings_updated: return # Start a new history subdirectory and capture the updated goal state tag = "{0}".format(incarnation) if vm_settings is None else "{0}-{1}".format(incarnation, vm_settings.etag) self._history = GoalStateHistory(timestamp, tag) if goal_state_updated: self._history.save_goal_state(xml_text) if vm_settings_updated: self._history.save_vm_settings(vm_settings.get_redacted_text()) # # Continue fetching the rest of the goal state # extensions_config = None if goal_state_updated: extensions_config = self._fetch_full_wire_server_goal_state(incarnation, xml_doc) # # Lastly, decide whether to use the vmSettings or extensionsConfig for the extensions goal state # if goal_state_updated and vm_settings_updated: most_recent = vm_settings if vm_settings.created_on_timestamp > extensions_config.created_on_timestamp else extensions_config elif goal_state_updated: most_recent = extensions_config else: # vm_settings_updated most_recent = vm_settings if self._extensions_goal_state is None or most_recent.created_on_timestamp >= self._extensions_goal_state.created_on_timestamp: self._extensions_goal_state = most_recent # # For Fast Track goal states, verify that the required certificates are in the goal state. # # Some scenarios can produce inconsistent goal states. For example, during hibernation/resume, the Fabric goal state changes (the # tenant certificate is re-generated when the VM is restarted) *without* the incarnation necessarily changing (e.g. if the incarnation # is 1 before the hibernation; on resume the incarnation is set to 1 even though the goal state has a new certificate). If a Fast # Track goal state comes after that, the extensions will need the new certificate. The Agent needs to refresh the goal state in that # case, to ensure it fetches the new certificate. # if self._extensions_goal_state.source == GoalStateSource.FastTrack: self._check_certificates() def _check_certificates(self): # Re-download certificates in case they have been removed from disk since last download if self._goal_state_properties & GoalStateProperties.Certificates and self._certs_uri is not None: self._download_certificates(self._certs_uri) # Check that certificates needed by extensions are in goal state certs.summary for extension in self.extensions_goal_state.extensions: for settings in extension.settings: if settings.protectedSettings is None: continue certificates = self.certs.summary if not any(settings.certificateThumbprint == c['thumbprint'] for c in certificates): message = "Certificate {0} needed by {1} is missing from the goal state".format(settings.certificateThumbprint, extension.name) raise GoalStateInconsistentError(message) def _download_certificates(self, certs_uri): xml_text = self._wire_client.fetch_config(certs_uri, self._wire_client.get_header_for_cert()) certs = Certificates(xml_text, self.logger) # Log and save the certificates summary (i.e. the thumbprint but not the certificate itself) to the goal state history for c in certs.summary: message = "Downloaded certificate {0}".format(c) self.logger.info(message) add_event(op=WALAEventOperation.GoalState, message=message) if len(certs.warnings) > 0: self.logger.warn(certs.warnings) add_event(op=WALAEventOperation.GoalState, message=certs.warnings) self._history.save_certificates(json.dumps(certs.summary)) return certs def _restore_wire_server_goal_state(self, incarnation, xml_text, xml_doc, vm_settings_support_stopped_error): msg = 'The HGAP stopped supporting vmSettings; will fetched the goal state from the WireServer.' self.logger.info(msg) add_event(op=WALAEventOperation.VmSettings, message=msg) self._history = GoalStateHistory(datetime.datetime.utcnow(), incarnation) self._history.save_goal_state(xml_text) self._extensions_goal_state = self._fetch_full_wire_server_goal_state(incarnation, xml_doc) if self._extensions_goal_state.created_on_timestamp < vm_settings_support_stopped_error.timestamp: self._extensions_goal_state.is_outdated = True msg = "Fetched a Fabric goal state older than the most recent FastTrack goal state; will skip it.\nFabric: {0}\nFastTrack: {1}".format( self._extensions_goal_state.created_on_timestamp, vm_settings_support_stopped_error.timestamp) self.logger.info(msg) add_event(op=WALAEventOperation.VmSettings, message=msg) def save_to_history(self, data, file_name): self._history.save(data, file_name) @staticmethod def _fetch_goal_state(wire_client): """ Issues an HTTP request for the goal state (WireServer) and returns a tuple containing the response as text and as an XML Document """ uri = GOAL_STATE_URI.format(wire_client.get_endpoint()) # In some environments a few goal state requests return a missing RoleInstance; these retries are used to work around that issue # TODO: Consider retrying on 410 (ResourceGone) as well incarnation = "unknown" for _ in range(0, _GET_GOAL_STATE_MAX_ATTEMPTS): xml_text = wire_client.fetch_config(uri, wire_client.get_header()) xml_doc = parse_doc(xml_text) incarnation = findtext(xml_doc, "Incarnation") role_instance = find(xml_doc, "RoleInstance") if role_instance: break time.sleep(0.5) else: raise ProtocolError("Fetched goal state without a RoleInstance [incarnation {inc}]".format(inc=incarnation)) # Telemetry and the HostGAPlugin depend on the container id/role config; keep them up-to-date each time we fetch the goal state # (note that these elements can change even if the incarnation of the goal state does not change) container = find(xml_doc, "Container") container_id = findtext(container, "ContainerId") role_config = find(role_instance, "Configuration") role_config_name = findtext(role_config, "ConfigName") AgentGlobals.update_container_id(container_id) # Telemetry uses this global to pick up the container id wire_client.update_host_plugin(container_id, role_config_name) return incarnation, xml_text, xml_doc @staticmethod def _fetch_vm_settings(wire_client, force_update=False): """ Issues an HTTP request (HostGAPlugin) for the vm settings and returns the response as an ExtensionsGoalState. """ vm_settings, vm_settings_updated = (None, False) if conf.get_enable_fast_track(): try: try: vm_settings, vm_settings_updated = wire_client.get_host_plugin().fetch_vm_settings(force_update=force_update) except ResourceGoneError: # retry after refreshing the HostGAPlugin GoalState.update_host_plugin_headers(wire_client) vm_settings, vm_settings_updated = wire_client.get_host_plugin().fetch_vm_settings(force_update=force_update) except VmSettingsSupportStopped: raise except VmSettingsNotSupported: pass except VmSettingsParseError as exception: # ensure we save the vmSettings if there were parsing errors, but save them only once per ETag if not GoalStateHistory.tag_exists(exception.etag): GoalStateHistory(datetime.datetime.utcnow(), exception.etag).save_vm_settings(exception.vm_settings_text) raise return vm_settings, vm_settings_updated def _fetch_full_wire_server_goal_state(self, incarnation, xml_doc): """ Issues HTTP requests (to the WireServer) for each of the URIs in the goal state (ExtensionsConfig, Certificate, Remote Access users, etc) and populates the corresponding properties. Returns the value of ExtensionsConfig. """ try: self.logger.info('') message = 'Fetching full goal state from the WireServer [incarnation {0}]'.format(incarnation) self.logger.info(message) add_event(op=WALAEventOperation.GoalState, message=message) role_instance_id = None role_config_name = None container_id = None if GoalStateProperties.RoleConfig & self._goal_state_properties: role_instance = find(xml_doc, "RoleInstance") role_instance_id = findtext(role_instance, "InstanceId") role_config = find(role_instance, "Configuration") role_config_name = findtext(role_config, "ConfigName") container = find(xml_doc, "Container") container_id = findtext(container, "ContainerId") extensions_config_uri = findtext(xml_doc, "ExtensionsConfig") if not (GoalStateProperties.ExtensionsGoalState & self._goal_state_properties) or extensions_config_uri is None: extensions_config = ExtensionsGoalStateFactory.create_empty(incarnation) else: xml_text = self._wire_client.fetch_config(extensions_config_uri, self._wire_client.get_header()) extensions_config = ExtensionsGoalStateFactory.create_from_extensions_config(incarnation, xml_text, self._wire_client) self._history.save_extensions_config(extensions_config.get_redacted_text()) hosting_env = None if GoalStateProperties.HostingEnv & self._goal_state_properties: hosting_env_uri = findtext(xml_doc, "HostingEnvironmentConfig") xml_text = self._wire_client.fetch_config(hosting_env_uri, self._wire_client.get_header()) hosting_env = HostingEnv(xml_text) self._history.save_hosting_env(xml_text) shared_config = None if GoalStateProperties.SharedConfig & self._goal_state_properties: shared_conf_uri = findtext(xml_doc, "SharedConfig") xml_text = self._wire_client.fetch_config(shared_conf_uri, self._wire_client.get_header()) shared_config = SharedConfig(xml_text) self._history.save_shared_conf(xml_text) # SharedConfig.xml is used by other components (Azsec and Singularity/HPC Infiniband), so save it to the agent's root directory as well shared_config_file = os.path.join(conf.get_lib_dir(), SHARED_CONF_FILE_NAME) try: fileutil.write_file(shared_config_file, xml_text) except Exception as e: logger.warn("Failed to save {0}: {1}".format(shared_config, e)) certs = EmptyCertificates() certs_uri = findtext(xml_doc, "Certificates") if (GoalStateProperties.Certificates & self._goal_state_properties) and certs_uri is not None: certs = self._download_certificates(certs_uri) remote_access = None if GoalStateProperties.RemoteAccessInfo & self._goal_state_properties: remote_access_uri = findtext(container, "RemoteAccessInfo") if remote_access_uri is not None: xml_text = self._wire_client.fetch_config(remote_access_uri, self._wire_client.get_header_for_cert()) remote_access = RemoteAccess(xml_text) self._history.save_remote_access(xml_text) self._incarnation = incarnation self._role_instance_id = role_instance_id self._role_config_name = role_config_name self._container_id = container_id self._hosting_env = hosting_env self._shared_conf = shared_config self._certs = certs self._certs_uri = certs_uri self._remote_access = remote_access return extensions_config except Exception as exception: self.logger.warn("Fetching the goal state failed: {0}", ustr(exception)) raise ProtocolError(msg="Error fetching goal state", inner=exception) finally: message = 'Fetch goal state completed' self.logger.info(message) add_event(op=WALAEventOperation.GoalState, message=message) class HostingEnv(object): def __init__(self, xml_text): self.xml_text = xml_text xml_doc = parse_doc(xml_text) incarnation = find(xml_doc, "Incarnation") self.vm_name = getattrib(incarnation, "instance") role = find(xml_doc, "Role") self.role_name = getattrib(role, "name") deployment = find(xml_doc, "Deployment") self.deployment_name = getattrib(deployment, "name") class SharedConfig(object): def __init__(self, xml_text): self.xml_text = xml_text class Certificates(object): def __init__(self, xml_text, my_logger): self.cert_list = CertList() self.summary = [] # debugging info self.warnings = [] # Save the certificates local_file = os.path.join(conf.get_lib_dir(), CERTS_FILE_NAME) fileutil.write_file(local_file, xml_text) # Separate the certificates into individual files. xml_doc = parse_doc(xml_text) data = findtext(xml_doc, "Data") if data is None: return # if the certificates format is not Pkcs7BlobWithPfxContents do not parse it certificate_format = findtext(xml_doc, "Format") if certificate_format and certificate_format != "Pkcs7BlobWithPfxContents": message = "The Format is not Pkcs7BlobWithPfxContents. Format is {0}".format(certificate_format) my_logger.warn(message) add_event(op=WALAEventOperation.GoalState, message=message) return cryptutil = CryptUtil(conf.get_openssl_cmd()) p7m_file = os.path.join(conf.get_lib_dir(), P7M_FILE_NAME) p7m = ("MIME-Version:1.0\n" # pylint: disable=W1308 "Content-Disposition: attachment; filename=\"{0}\"\n" "Content-Type: application/x-pkcs7-mime; name=\"{1}\"\n" "Content-Transfer-Encoding: base64\n" "\n" "{2}").format(p7m_file, p7m_file, data) fileutil.write_file(p7m_file, p7m) trans_prv_file = os.path.join(conf.get_lib_dir(), TRANSPORT_PRV_FILE_NAME) trans_cert_file = os.path.join(conf.get_lib_dir(), TRANSPORT_CERT_FILE_NAME) pem_file = os.path.join(conf.get_lib_dir(), PEM_FILE_NAME) # decrypt certificates cryptutil.decrypt_p7m(p7m_file, trans_prv_file, trans_cert_file, pem_file) # The parsing process use public key to match prv and crt. buf = [] begin_crt = False # pylint: disable=W0612 begin_prv = False # pylint: disable=W0612 prvs = {} thumbprints = {} index = 0 v1_cert_list = [] with open(pem_file) as pem: for line in pem.readlines(): buf.append(line) if re.match(r'[-]+BEGIN.*KEY[-]+', line): begin_prv = True elif re.match(r'[-]+BEGIN.*CERTIFICATE[-]+', line): begin_crt = True elif re.match(r'[-]+END.*KEY[-]+', line): tmp_file = Certificates._write_to_tmp_file(index, 'prv', buf) pub = cryptutil.get_pubkey_from_prv(tmp_file) prvs[pub] = tmp_file buf = [] index += 1 begin_prv = False elif re.match(r'[-]+END.*CERTIFICATE[-]+', line): tmp_file = Certificates._write_to_tmp_file(index, 'crt', buf) pub = cryptutil.get_pubkey_from_crt(tmp_file) thumbprint = cryptutil.get_thumbprint_from_crt(tmp_file) thumbprints[pub] = thumbprint # Rename crt with thumbprint as the file name crt = "{0}.crt".format(thumbprint) v1_cert_list.append({ "name": None, "thumbprint": thumbprint }) os.rename(tmp_file, os.path.join(conf.get_lib_dir(), crt)) buf = [] index += 1 begin_crt = False # Rename prv key with thumbprint as the file name for pubkey in prvs: thumbprint = thumbprints[pubkey] if thumbprint: tmp_file = prvs[pubkey] prv = "{0}.prv".format(thumbprint) os.rename(tmp_file, os.path.join(conf.get_lib_dir(), prv)) else: # Since private key has *no* matching certificate, # it will not be named correctly self.warnings.append("Found NO matching cert/thumbprint for private key!") for pubkey, thumbprint in thumbprints.items(): has_private_key = pubkey in prvs self.summary.append({"thumbprint": thumbprint, "hasPrivateKey": has_private_key}) for v1_cert in v1_cert_list: cert = Cert() set_properties("certs", cert, v1_cert) self.cert_list.certificates.append(cert) @staticmethod def _write_to_tmp_file(index, suffix, buf): file_name = os.path.join(conf.get_lib_dir(), "{0}.{1}".format(index, suffix)) fileutil.write_file(file_name, "".join(buf)) return file_name class EmptyCertificates: def __init__(self): self.cert_list = CertList() self.summary = [] # debugging info self.warnings = [] class RemoteAccess(object): """ Object containing information about user accounts """ # # # # # # # # # # # # # def __init__(self, xml_text): self.xml_text = xml_text self.version = None self.incarnation = None self.user_list = RemoteAccessUsersList() if self.xml_text is None or len(self.xml_text) == 0: return xml_doc = parse_doc(self.xml_text) self.version = findtext(xml_doc, "Version") self.incarnation = findtext(xml_doc, "Incarnation") user_collection = find(xml_doc, "Users") users = findall(user_collection, "User") for user in users: remote_access_user = RemoteAccess._parse_user(user) self.user_list.users.append(remote_access_user) @staticmethod def _parse_user(user): name = findtext(user, "Name") encrypted_password = findtext(user, "Password") expiration = findtext(user, "Expiration") remote_access_user = RemoteAccessUser(name, encrypted_password, expiration) return remote_access_user class ExtensionManifest(object): def __init__(self, xml_text): if xml_text is None: raise ValueError("ExtensionManifest is None") logger.verbose("Load ExtensionManifest.xml") self.pkg_list = ExtHandlerPackageList() self._parse(xml_text) def _parse(self, xml_text): xml_doc = parse_doc(xml_text) self._handle_packages(findall(find(xml_doc, "Plugins"), "Plugin"), False) self._handle_packages(findall(find(xml_doc, "InternalPlugins"), "Plugin"), True) def _handle_packages(self, packages, isinternal): for package in packages: version = findtext(package, "Version") disallow_major_upgrade = findtext(package, "DisallowMajorVersionUpgrade") if disallow_major_upgrade is None: disallow_major_upgrade = '' disallow_major_upgrade = disallow_major_upgrade.lower() == "true" uris = find(package, "Uris") uri_list = findall(uris, "Uri") uri_list = [gettext(x) for x in uri_list] pkg = ExtHandlerPackage() pkg.version = version pkg.disallow_major_upgrade = disallow_major_upgrade for uri in uri_list: pkg.uris.append(uri) pkg.isinternal = isinternal self.pkg_list.versions.append(pkg) WALinuxAgent-2.9.1.1/azurelinuxagent/common/protocol/healthservice.py000066400000000000000000000152331446033677600257720ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import json from azurelinuxagent.common import logger from azurelinuxagent.common.exception import HttpError from azurelinuxagent.common.future import ustr from azurelinuxagent.common.utils import restutil from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION class Observation(object): def __init__(self, name, is_healthy, description='', value=''): if name is None: raise ValueError("Observation name must be provided") if is_healthy is None: raise ValueError("Observation health must be provided") if value is None: value = '' if description is None: description = '' self.name = name self.is_healthy = is_healthy self.description = description self.value = value @property def as_obj(self): return { "ObservationName": self.name[:64], "IsHealthy": self.is_healthy, "Description": self.description[:128], "Value": self.value[:128] } class HealthService(object): ENDPOINT = 'http://{0}:80/HealthService' API = 'reporttargethealth' VERSION = "1.0" OBSERVER_NAME = 'WALinuxAgent' HOST_PLUGIN_HEARTBEAT_OBSERVATION_NAME = 'GuestAgentPluginHeartbeat' HOST_PLUGIN_STATUS_OBSERVATION_NAME = 'GuestAgentPluginStatus' HOST_PLUGIN_VERSIONS_OBSERVATION_NAME = 'GuestAgentPluginVersions' HOST_PLUGIN_ARTIFACT_OBSERVATION_NAME = 'GuestAgentPluginArtifact' IMDS_OBSERVATION_NAME = 'InstanceMetadataHeartbeat' MAX_OBSERVATIONS = 10 def __init__(self, endpoint): self.endpoint = HealthService.ENDPOINT.format(endpoint) self.api = HealthService.API self.version = HealthService.VERSION self.source = HealthService.OBSERVER_NAME self.observations = list() @property def as_json(self): data = { "Api": self.api, "Version": self.version, "Source": self.source, "Observations": [o.as_obj for o in self.observations] } return json.dumps(data) def report_host_plugin_heartbeat(self, is_healthy): """ Reports a signal for /health :param is_healthy: whether the call succeeded """ self._observe(name=HealthService.HOST_PLUGIN_HEARTBEAT_OBSERVATION_NAME, is_healthy=is_healthy) self._report() def report_host_plugin_versions(self, is_healthy, response): """ Reports a signal for /versions :param is_healthy: whether the api call succeeded :param response: debugging information for failures """ self._observe(name=HealthService.HOST_PLUGIN_VERSIONS_OBSERVATION_NAME, is_healthy=is_healthy, value=response) self._report() def report_host_plugin_extension_artifact(self, is_healthy, source, response): """ Reports a signal for /extensionArtifact :param is_healthy: whether the api call succeeded :param source: specifies the api caller for debugging failures :param response: debugging information for failures """ self._observe(name=HealthService.HOST_PLUGIN_ARTIFACT_OBSERVATION_NAME, is_healthy=is_healthy, description=source, value=response) self._report() def report_host_plugin_status(self, is_healthy, response): """ Reports a signal for /status :param is_healthy: whether the api call succeeded :param response: debugging information for failures """ self._observe(name=HealthService.HOST_PLUGIN_STATUS_OBSERVATION_NAME, is_healthy=is_healthy, value=response) self._report() def report_imds_status(self, is_healthy, response): """ Reports a signal for /metadata/instance :param is_healthy: whether the api call succeeded and returned valid data :param response: debugging information for failures """ self._observe(name=HealthService.IMDS_OBSERVATION_NAME, is_healthy=is_healthy, value=response) self._report() def _observe(self, name, is_healthy, value='', description=''): # ensure we keep the list size within bounds if len(self.observations) >= HealthService.MAX_OBSERVATIONS: del self.observations[:HealthService.MAX_OBSERVATIONS-1] self.observations.append(Observation(name=name, is_healthy=is_healthy, value=value, description=description)) def _report(self): logger.verbose('HealthService: report observations') try: restutil.http_post(self.endpoint, self.as_json, headers={'Content-Type': 'application/json'}) logger.verbose('HealthService: Reported observations to {0}: {1}', self.endpoint, self.as_json) except HttpError as e: logger.warn("HealthService: could not report observations: {0}", ustr(e)) finally: # report any failures via telemetry self._report_failures() # these signals are not timestamped, so there is no value in persisting data del self.observations[:] def _report_failures(self): try: logger.verbose("HealthService: report failures as telemetry") from azurelinuxagent.common.event import add_event, WALAEventOperation for o in self.observations: if not o.is_healthy: add_event(AGENT_NAME, version=CURRENT_VERSION, op=WALAEventOperation.HealthObservation, is_success=False, message=json.dumps(o.as_obj)) except Exception as e: logger.verbose("HealthService: could not report failures: {0}".format(ustr(e))) WALinuxAgent-2.9.1.1/azurelinuxagent/common/protocol/hostplugin.py000066400000000000000000000754111446033677600253440ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import base64 import datetime import json import os.path import uuid from azurelinuxagent.common import logger, conf from azurelinuxagent.common.errorstate import ErrorState, ERROR_STATE_HOST_PLUGIN_FAILURE from azurelinuxagent.common.event import WALAEventOperation, add_event from azurelinuxagent.common.exception import HttpError, ProtocolError, ResourceGoneError from azurelinuxagent.common.utils.flexible_version import FlexibleVersion from azurelinuxagent.common.future import ustr, httpclient from azurelinuxagent.common.protocol.healthservice import HealthService from azurelinuxagent.common.protocol.extensions_goal_state import VmSettingsParseError, GoalStateSource from azurelinuxagent.common.protocol.extensions_goal_state_factory import ExtensionsGoalStateFactory from azurelinuxagent.common.utils import restutil, textutil, timeutil from azurelinuxagent.common.utils.textutil import remove_bom from azurelinuxagent.common.version import AGENT_NAME, AGENT_VERSION, PY_VERSION_MAJOR HOST_PLUGIN_PORT = 32526 URI_FORMAT_GET_API_VERSIONS = "http://{0}:{1}/versions" URI_FORMAT_VM_SETTINGS = "http://{0}:{1}/vmSettings" URI_FORMAT_GET_EXTENSION_ARTIFACT = "http://{0}:{1}/extensionArtifact" URI_FORMAT_PUT_VM_STATUS = "http://{0}:{1}/status" URI_FORMAT_PUT_LOG = "http://{0}:{1}/vmAgentLog" URI_FORMAT_HEALTH = "http://{0}:{1}/health" API_VERSION = "2015-09-01" _HEADER_CLIENT_NAME = "x-ms-client-name" _HEADER_CLIENT_VERSION = "x-ms-client-version" _HEADER_CORRELATION_ID = "x-ms-client-correlationid" _HEADER_CONTAINER_ID = "x-ms-containerid" _HEADER_DEPLOYMENT_ID = "x-ms-vmagentlog-deploymentid" _HEADER_VERSION = "x-ms-version" _HEADER_HOST_CONFIG_NAME = "x-ms-host-config-name" _HEADER_ARTIFACT_LOCATION = "x-ms-artifact-location" _HEADER_ARTIFACT_MANIFEST_LOCATION = "x-ms-artifact-manifest-location" _HEADER_VERIFY_FROM_ARTIFACTS_BLOB = "x-ms-verify-from-artifacts-blob" MAXIMUM_PAGEBLOB_PAGE_SIZE = 4 * 1024 * 1024 # Max page size: 4MB class HostPluginProtocol(object): is_default_channel = False FETCH_REPORTING_PERIOD = datetime.timedelta(minutes=1) STATUS_REPORTING_PERIOD = datetime.timedelta(minutes=1) def __init__(self, endpoint): """ NOTE: Before using the HostGAPlugin be sure to invoke GoalState.update_host_plugin_headers() to initialize the container id and role config name """ if endpoint is None: raise ProtocolError("HostGAPlugin: Endpoint not provided") self.is_initialized = False self.is_available = False self.api_versions = None self.endpoint = endpoint self.container_id = None self.deployment_id = None self.role_config_name = None self.manifest_uri = None self.health_service = HealthService(endpoint) self.fetch_error_state = ErrorState(min_timedelta=ERROR_STATE_HOST_PLUGIN_FAILURE) self.status_error_state = ErrorState(min_timedelta=ERROR_STATE_HOST_PLUGIN_FAILURE) self.fetch_last_timestamp = None self.status_last_timestamp = None self._version = FlexibleVersion("0.0.0.0") # Version 0 means "unknown" self._supports_vm_settings = None # Tri-state variable: None == Not Initialized, True == Supports, False == Does Not Support self._supports_vm_settings_next_check = datetime.datetime.now() self._vm_settings_error_reporter = _VmSettingsErrorReporter() self._cached_vm_settings = None # Cached value of the most recent vmSettings # restore the state of Fast Track if not os.path.exists(self._get_fast_track_state_file()): self._supports_vm_settings = False self._supports_vm_settings_next_check = datetime.datetime.now() self._fast_track_timestamp = timeutil.create_timestamp(datetime.datetime.min) else: self._supports_vm_settings = True self._supports_vm_settings_next_check = datetime.datetime.now() self._fast_track_timestamp = HostPluginProtocol.get_fast_track_timestamp() @staticmethod def _extract_deployment_id(role_config_name): # Role config name consists of: .(...) return role_config_name.split(".")[0] if role_config_name is not None else None def check_vm_settings_support(self): """ Returns True if the HostGAPlugin supports the vmSettings API. """ # _host_plugin_supports_vm_settings is set by fetch_vm_settings() if self._supports_vm_settings is None: _, _ = self.fetch_vm_settings() return self._supports_vm_settings def update_container_id(self, new_container_id): self.container_id = new_container_id def update_role_config_name(self, new_role_config_name): self.role_config_name = new_role_config_name self.deployment_id = self._extract_deployment_id(new_role_config_name) def update_manifest_uri(self, new_manifest_uri): self.manifest_uri = new_manifest_uri def ensure_initialized(self): if not self.is_initialized: self.api_versions = self.get_api_versions() self.is_available = API_VERSION in self.api_versions self.is_initialized = self.is_available add_event(op=WALAEventOperation.InitializeHostPlugin, is_success=self.is_available) return self.is_available def get_health(self): """ Call the /health endpoint :return: True if 200 received, False otherwise """ url = URI_FORMAT_HEALTH.format(self.endpoint, HOST_PLUGIN_PORT) logger.verbose("HostGAPlugin: Getting health from [{0}]", url) response = restutil.http_get(url, max_retry=1) return restutil.request_succeeded(response) def get_api_versions(self): url = URI_FORMAT_GET_API_VERSIONS.format(self.endpoint, HOST_PLUGIN_PORT) logger.verbose("HostGAPlugin: Getting API versions at [{0}]" .format(url)) return_val = [] error_response = '' is_healthy = False try: headers = {_HEADER_CONTAINER_ID: self.container_id} response = restutil.http_get(url, headers) if restutil.request_failed(response): error_response = restutil.read_response_error(response) logger.error("HostGAPlugin: Failed Get API versions: {0}".format(error_response)) is_healthy = not restutil.request_failed_at_hostplugin(response) else: return_val = ustr(remove_bom(response.read()), encoding='utf-8') is_healthy = True except HttpError as e: logger.error("HostGAPlugin: Exception Get API versions: {0}".format(e)) self.health_service.report_host_plugin_versions(is_healthy=is_healthy, response=error_response) return return_val def get_vm_settings_request(self, correlation_id): url = URI_FORMAT_VM_SETTINGS.format(self.endpoint, HOST_PLUGIN_PORT) headers = { _HEADER_VERSION: API_VERSION, _HEADER_CONTAINER_ID: self.container_id, _HEADER_HOST_CONFIG_NAME: self.role_config_name, _HEADER_CORRELATION_ID: correlation_id } return url, headers def get_artifact_request(self, artifact_url, use_verify_header, artifact_manifest_url=None): if not self.ensure_initialized(): raise ProtocolError("HostGAPlugin: Host plugin channel is not available") if textutil.is_str_none_or_whitespace(artifact_url): raise ProtocolError("HostGAPlugin: No extension artifact url was provided") url = URI_FORMAT_GET_EXTENSION_ARTIFACT.format(self.endpoint, HOST_PLUGIN_PORT) headers = { _HEADER_VERSION: API_VERSION, _HEADER_CONTAINER_ID: self.container_id, _HEADER_HOST_CONFIG_NAME: self.role_config_name, _HEADER_ARTIFACT_LOCATION: artifact_url} if use_verify_header: headers[_HEADER_VERIFY_FROM_ARTIFACTS_BLOB] = "true" if artifact_manifest_url is not None: headers[_HEADER_ARTIFACT_MANIFEST_LOCATION] = artifact_manifest_url return url, headers def report_fetch_health(self, uri, is_healthy=True, source='', response=''): if uri != URI_FORMAT_GET_EXTENSION_ARTIFACT.format(self.endpoint, HOST_PLUGIN_PORT): return if self.should_report(is_healthy, self.fetch_error_state, self.fetch_last_timestamp, HostPluginProtocol.FETCH_REPORTING_PERIOD): self.fetch_last_timestamp = datetime.datetime.utcnow() health_signal = self.fetch_error_state.is_triggered() is False self.health_service.report_host_plugin_extension_artifact(is_healthy=health_signal, source=source, response=response) def report_status_health(self, is_healthy, response=''): if self.should_report(is_healthy, self.status_error_state, self.status_last_timestamp, HostPluginProtocol.STATUS_REPORTING_PERIOD): self.status_last_timestamp = datetime.datetime.utcnow() health_signal = self.status_error_state.is_triggered() is False self.health_service.report_host_plugin_status(is_healthy=health_signal, response=response) @staticmethod def should_report(is_healthy, error_state, last_timestamp, period): """ Determine whether a health signal should be reported :param is_healthy: whether the current measurement is healthy :param error_state: the error state which is tracking time since failure :param last_timestamp: the last measurement time stamp :param period: the reporting period :return: True if the signal should be reported, False otherwise """ if is_healthy: # we only reset the error state upon success, since we want to keep # reporting the failure; this is different to other uses of error states # which do not have a separate periodicity error_state.reset() else: error_state.incr() if last_timestamp is None: last_timestamp = datetime.datetime.utcnow() - period return datetime.datetime.utcnow() >= (last_timestamp + period) def put_vm_log(self, content): """ Try to upload VM logs, a compressed zip file, via the host plugin /vmAgentLog channel. :param content: the binary content of the zip file to upload """ if not self.ensure_initialized(): raise ProtocolError("HostGAPlugin: HostGAPlugin is not available") if content is None: raise ProtocolError("HostGAPlugin: Invalid argument passed to upload VM logs. Content was not provided.") url = URI_FORMAT_PUT_LOG.format(self.endpoint, HOST_PLUGIN_PORT) response = restutil.http_put(url, data=content, headers=self._build_log_headers(), redact_data=True, timeout=30) if restutil.request_failed(response): error_response = restutil.read_response_error(response) raise HttpError("HostGAPlugin: Upload VM logs failed: {0}".format(error_response)) return response def put_vm_status(self, status_blob, sas_url, config_blob_type=None): """ Try to upload the VM status via the host plugin /status channel :param sas_url: the blob SAS url to pass to the host plugin :param config_blob_type: the blob type from the extension config :type status_blob: StatusBlob """ if not self.ensure_initialized(): raise ProtocolError("HostGAPlugin: HostGAPlugin is not available") if status_blob is None or status_blob.vm_status is None: raise ProtocolError("HostGAPlugin: Status blob was not provided") logger.verbose("HostGAPlugin: Posting VM status") blob_type = status_blob.type if status_blob.type else config_blob_type if blob_type == "BlockBlob": self._put_block_blob_status(sas_url, status_blob) else: self._put_page_blob_status(sas_url, status_blob) def _put_block_blob_status(self, sas_url, status_blob): url = URI_FORMAT_PUT_VM_STATUS.format(self.endpoint, HOST_PLUGIN_PORT) response = restutil.http_put(url, data=self._build_status_data( sas_url, status_blob.get_block_blob_headers(len(status_blob.data)), bytearray(status_blob.data, encoding='utf-8')), headers=self._build_status_headers()) if restutil.request_failed(response): error_response = restutil.read_response_error(response) is_healthy = not restutil.request_failed_at_hostplugin(response) self.report_status_health(is_healthy=is_healthy, response=error_response) raise HttpError("HostGAPlugin: Put BlockBlob failed: {0}" .format(error_response)) else: self.report_status_health(is_healthy=True) logger.verbose("HostGAPlugin: Put BlockBlob status succeeded") def _put_page_blob_status(self, sas_url, status_blob): url = URI_FORMAT_PUT_VM_STATUS.format(self.endpoint, HOST_PLUGIN_PORT) # Convert the status into a blank-padded string whose length is modulo 512 status = bytearray(status_blob.data, encoding='utf-8') status_size = int((len(status) + 511) / 512) * 512 status = bytearray(status_blob.data.ljust(status_size), encoding='utf-8') # First, initialize an empty blob response = restutil.http_put(url, data=self._build_status_data( sas_url, status_blob.get_page_blob_create_headers(status_size)), headers=self._build_status_headers()) if restutil.request_failed(response): error_response = restutil.read_response_error(response) is_healthy = not restutil.request_failed_at_hostplugin(response) self.report_status_health(is_healthy=is_healthy, response=error_response) raise HttpError("HostGAPlugin: Failed PageBlob clean-up: {0}" .format(error_response)) else: self.report_status_health(is_healthy=True) logger.verbose("HostGAPlugin: PageBlob clean-up succeeded") # Then, upload the blob in pages if sas_url.count("?") <= 0: sas_url = "{0}?comp=page".format(sas_url) else: sas_url = "{0}&comp=page".format(sas_url) start = 0 end = 0 while start < len(status): # Create the next page end = start + min(len(status) - start, MAXIMUM_PAGEBLOB_PAGE_SIZE) page_size = int((end - start + 511) / 512) * 512 buf = bytearray(page_size) buf[0: end - start] = status[start: end] # Send the page response = restutil.http_put(url, data=self._build_status_data( sas_url, status_blob.get_page_blob_page_headers(start, end), buf), headers=self._build_status_headers()) if restutil.request_failed(response): error_response = restutil.read_response_error(response) is_healthy = not restutil.request_failed_at_hostplugin(response) self.report_status_health(is_healthy=is_healthy, response=error_response) raise HttpError( "HostGAPlugin Error: Put PageBlob bytes " "[{0},{1}]: {2}".format(start, end, error_response)) # Advance to the next page (if any) start = end def _build_status_data(self, sas_url, blob_headers, content=None): headers = [] for name in iter(blob_headers.keys()): headers.append({ 'headerName': name, 'headerValue': blob_headers[name] }) data = { 'requestUri': sas_url, 'headers': headers } if not content is None: data['content'] = self._base64_encode(content) return json.dumps(data, sort_keys=True) def _build_status_headers(self): return { _HEADER_VERSION: API_VERSION, "Content-type": "application/json", _HEADER_CONTAINER_ID: self.container_id, _HEADER_HOST_CONFIG_NAME: self.role_config_name } def _build_log_headers(self): return { _HEADER_VERSION: API_VERSION, _HEADER_CONTAINER_ID: self.container_id, _HEADER_DEPLOYMENT_ID: self.deployment_id, _HEADER_CLIENT_NAME: AGENT_NAME, _HEADER_CLIENT_VERSION: AGENT_VERSION, _HEADER_CORRELATION_ID: str(uuid.uuid4()) } def _base64_encode(self, data): s = base64.b64encode(bytes(data)) if PY_VERSION_MAJOR > 2: return s.decode('utf-8') return s @staticmethod def _get_fast_track_state_file(): # This file keeps the timestamp of the most recent goal state if it was retrieved via Fast Track return os.path.join(conf.get_lib_dir(), "fast_track.json") @staticmethod def _save_fast_track_state(timestamp): try: with open(HostPluginProtocol._get_fast_track_state_file(), "w") as file_: json.dump({"timestamp": timestamp}, file_) except Exception as e: logger.warn("Error updating the Fast Track state ({0}): {1}", HostPluginProtocol._get_fast_track_state_file(), ustr(e)) @staticmethod def clear_fast_track_state(): try: if os.path.exists(HostPluginProtocol._get_fast_track_state_file()): os.remove(HostPluginProtocol._get_fast_track_state_file()) except Exception as e: logger.warn("Error clearing the current state for Fast Track ({0}): {1}", HostPluginProtocol._get_fast_track_state_file(), ustr(e)) @staticmethod def get_fast_track_timestamp(): """ Returns the timestamp of the most recent FastTrack goal state retrieved by fetch_vm_settings(), or None if the most recent goal state was Fabric or fetch_vm_settings() has not been invoked. """ if not os.path.exists(HostPluginProtocol._get_fast_track_state_file()): return timeutil.create_timestamp(datetime.datetime.min) try: with open(HostPluginProtocol._get_fast_track_state_file(), "r") as file_: return json.load(file_)["timestamp"] except Exception as e: logger.warn("Can't retrieve the timestamp for the most recent Fast Track goal state ({0}), will assume the current time. Error: {1}", HostPluginProtocol._get_fast_track_state_file(), ustr(e)) return timeutil.create_timestamp(datetime.datetime.utcnow()) def fetch_vm_settings(self, force_update=False): """ Queries the vmSettings from the HostGAPlugin and returns an (ExtensionsGoalState, bool) tuple with the vmSettings and a boolean indicating if they are an updated (True) or a cached value (False). Raises * VmSettingsNotSupported if the HostGAPlugin does not support the vmSettings API * VmSettingsSupportStopped if the HostGAPlugin stopped supporting the vmSettings API * VmSettingsParseError if the HostGAPlugin returned invalid vmSettings (e.g. syntax error) * ResourceGoneError if the container ID and roleconfig name need to be refreshed * ProtocolError if the request fails for any other reason (e.g. not supported, time out, server error) """ def raise_not_supported(): try: if self._supports_vm_settings: # The most recent goal state was delivered using FastTrack, and suddenly the HostGAPlugin does not support the vmSettings API anymore. # This can happen if, for example, the VM is migrated across host nodes that are running different versions of the HostGAPlugin. logger.warn("The HostGAPlugin stopped supporting the vmSettings API. If there is a pending FastTrack goal state, it will not be executed.") add_event(op=WALAEventOperation.VmSettings, message="[VmSettingsSupportStopped] HostGAPlugin: {0}".format(self._version), is_success=False, log_event=False) raise VmSettingsSupportStopped(self._fast_track_timestamp) else: logger.info("HostGAPlugin {0} does not support the vmSettings API. Will not use FastTrack.", self._version) add_event(op=WALAEventOperation.VmSettings, message="[VmSettingsNotSupported] HostGAPlugin: {0}".format(self._version), is_success=True) raise VmSettingsNotSupported() finally: self._supports_vm_settings = False self._supports_vm_settings_next_check = datetime.datetime.now() + datetime.timedelta(hours=6) # check again in 6 hours def format_message(msg): return "GET vmSettings [correlation ID: {0} eTag: {1}]: {2}".format(correlation_id, etag, msg) try: # Raise if VmSettings are not supported, but check again periodically since the HostGAPlugin could have been updated since the last check # Note that self._host_plugin_supports_vm_settings can be None, so we need to compare against False if not self._supports_vm_settings and self._supports_vm_settings_next_check > datetime.datetime.now(): # Raise VmSettingsNotSupported directly instead of using raise_not_supported() to avoid resetting the timestamp for the next check raise VmSettingsNotSupported() etag = None if force_update or self._cached_vm_settings is None else self._cached_vm_settings.etag correlation_id = str(uuid.uuid4()) self._vm_settings_error_reporter.report_request() url, headers = self.get_vm_settings_request(correlation_id) if etag is not None: headers['if-none-match'] = etag response = restutil.http_get(url, headers=headers, use_proxy=False, max_retry=1, return_raw_response=True) if response.status == httpclient.GONE: raise ResourceGoneError() if response.status == httpclient.NOT_FOUND: # the HostGAPlugin does not support FastTrack raise_not_supported() if response.status == httpclient.NOT_MODIFIED: # The goal state hasn't changed, return the current instance return self._cached_vm_settings, False if response.status != httpclient.OK: error_description = restutil.read_response_error(response) # For historical reasons the HostGAPlugin returns 502 (BAD_GATEWAY) for internal errors instead of using # 500 (INTERNAL_SERVER_ERROR). We add a short prefix to the error message in the hope that it will help # clear any confusion produced by the poor choice of status code. if response.status == httpclient.BAD_GATEWAY: error_description = "[Internal error in HostGAPlugin] {0}".format(error_description) error_description = format_message(error_description) if 400 <= response.status <= 499: self._vm_settings_error_reporter.report_error(error_description, _VmSettingsError.ClientError) elif 500 <= response.status <= 599: self._vm_settings_error_reporter.report_error(error_description, _VmSettingsError.ServerError) else: self._vm_settings_error_reporter.report_error(error_description, _VmSettingsError.HttpError) raise ProtocolError(error_description) for h in response.getheaders(): if h[0].lower() == 'etag': response_etag = h[1] break else: # since the vmSettings were updated, the response must include an etag message = format_message("The vmSettings response does not include an Etag header") raise ProtocolError(message) response_content = ustr(response.read(), encoding='utf-8') vm_settings = ExtensionsGoalStateFactory.create_from_vm_settings(response_etag, response_content, correlation_id) # log the HostGAPlugin version if vm_settings.host_ga_plugin_version != self._version: self._version = vm_settings.host_ga_plugin_version message = "HostGAPlugin version: {0}".format(vm_settings.host_ga_plugin_version) logger.info(message) add_event(op=WALAEventOperation.HostPlugin, message=message, is_success=True) # Don't support HostGAPlugin versions older than 133 if vm_settings.host_ga_plugin_version < FlexibleVersion("1.0.8.133"): raise_not_supported() self._supports_vm_settings = True self._cached_vm_settings = vm_settings if vm_settings.source == GoalStateSource.FastTrack: self._fast_track_timestamp = vm_settings.created_on_timestamp self._save_fast_track_state(vm_settings.created_on_timestamp) else: self.clear_fast_track_state() return vm_settings, True except (ProtocolError, ResourceGoneError, VmSettingsNotSupported, VmSettingsParseError): raise except Exception as exception: if isinstance(exception, IOError) and "timed out" in ustr(exception): message = format_message("Timeout") self._vm_settings_error_reporter.report_error(message, _VmSettingsError.Timeout) else: message = format_message("Request failed: {0}".format(textutil.format_exception(exception))) self._vm_settings_error_reporter.report_error(message, _VmSettingsError.RequestFailed) raise ProtocolError(message) finally: self._vm_settings_error_reporter.report_summary() class VmSettingsNotSupported(TypeError): """ Indicates that the HostGAPlugin does not support the vmSettings API """ class VmSettingsSupportStopped(VmSettingsNotSupported): """ Indicates that the HostGAPlugin supported the vmSettings API in previous calls, but now it does not support it for current call. This can happen, for example, if the VM is migrated across nodes with different HostGAPlugin versions. """ def __init__(self, timestamp): super(VmSettingsSupportStopped, self).__init__() self.timestamp = timestamp class _VmSettingsError(object): ClientError = 'ClientError' HttpError = 'HttpError' RequestFailed = 'RequestFailed' ServerError = 'ServerError' Timeout = 'Timeout' class _VmSettingsErrorReporter(object): _MaxErrors = 3 # Max number of errors reported to telemetry (by period) _Period = datetime.timedelta(hours=1) # How often to report the summary def __init__(self): self._reset() def _reset(self): self._request_count = 0 # Total number of vmSettings HTTP requests self._error_count = 0 # Total number of errors issuing vmSettings requests (includes all kinds of errors) self._client_error_count = 0 # Count of client side errors (HTTP status in the 400s) self._http_error_count = 0 # Count of HTTP errors other than 400s and 500s self._request_failure_count = 0 # Total count of requests that could not be issued (does not include timeouts or requests that were actually issued and failed, for example, with 500 or 400 statuses) self._server_error_count = 0 # Count of server side errors (HTTP status in the 500s) self._timeout_count = 0 # Count of timeouts on vmSettings requests self._next_period = datetime.datetime.now() + _VmSettingsErrorReporter._Period def report_request(self): self._request_count += 1 def report_error(self, error, category): self._error_count += 1 if self._error_count <= _VmSettingsErrorReporter._MaxErrors: add_event(op=WALAEventOperation.VmSettings, message="[{0}] {1}".format(category, error), is_success=True, log_event=False) if category == _VmSettingsError.ClientError: self._client_error_count += 1 elif category == _VmSettingsError.HttpError: self._http_error_count += 1 elif category == _VmSettingsError.RequestFailed: self._request_failure_count += 1 elif category == _VmSettingsError.ServerError: self._server_error_count += 1 elif category == _VmSettingsError.Timeout: self._timeout_count += 1 def report_summary(self): if datetime.datetime.now() >= self._next_period: summary = { "requests": self._request_count, "errors": self._error_count, "serverErrors": self._server_error_count, "clientErrors": self._client_error_count, "timeouts": self._timeout_count, "failedRequests": self._request_failure_count } message = json.dumps(summary) add_event(op=WALAEventOperation.VmSettingsSummary, message=message, is_success=True, log_event=False) if self._error_count > 0: logger.info("[VmSettingsSummary] {0}", message) self._reset() WALinuxAgent-2.9.1.1/azurelinuxagent/common/protocol/imds.py000066400000000000000000000336701446033677600241050ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the Apache License, Version 2.0 (the "License"); import json import re from collections import namedtuple import azurelinuxagent.common.utils.restutil as restutil from azurelinuxagent.common.exception import HttpError, ResourceGoneError from azurelinuxagent.common.future import ustr import azurelinuxagent.common.logger as logger from azurelinuxagent.common.datacontract import DataContract, set_properties from azurelinuxagent.common.utils.flexible_version import FlexibleVersion IMDS_ENDPOINT = '169.254.169.254' APIVERSION = '2018-02-01' BASE_METADATA_URI = "http://{0}/metadata/{1}?api-version={2}" IMDS_IMAGE_ORIGIN_UNKNOWN = 0 IMDS_IMAGE_ORIGIN_CUSTOM = 1 IMDS_IMAGE_ORIGIN_ENDORSED = 2 IMDS_IMAGE_ORIGIN_PLATFORM = 3 MetadataResult = namedtuple('MetadataResult', ['success', 'service_error', 'response']) IMDS_RESPONSE_SUCCESS = 0 IMDS_RESPONSE_ERROR = 1 IMDS_CONNECTION_ERROR = 2 IMDS_INTERNAL_SERVER_ERROR = 3 def get_imds_client(wireserver_endpoint): return ImdsClient(wireserver_endpoint) # A *slightly* future proof list of endorsed distros. # -> e.g. I have predicted the future and said that 20.04-LTS will exist # and is endored. # # See https://docs.microsoft.com/en-us/azure/virtual-machines/linux/endorsed-distros for # more details. # # This is not an exhaustive list. This is a best attempt to mark images as # endorsed or not. Image publishers do not encode all of the requisite information # in their publisher, offer, sku, and version to definitively mark something as # endorsed or not. This is not perfect, but it is approximately 98% perfect. ENDORSED_IMAGE_INFO_MATCHER_JSON = """{ "CANONICAL": { "UBUNTUSERVER": { "List": [ "14.04.0-LTS", "14.04.1-LTS", "14.04.2-LTS", "14.04.3-LTS", "14.04.4-LTS", "14.04.5-LTS", "14.04.6-LTS", "14.04.7-LTS", "14.04.8-LTS", "16.04-LTS", "16.04.0-LTS", "18.04-LTS", "20.04-LTS", "22.04-LTS" ] } }, "COREOS": { "COREOS": { "STABLE": { "Minimum": "494.4.0" } } }, "CREDATIV": { "DEBIAN": { "Minimum": "7" } }, "OPENLOGIC": { "CENTOS": { "Minimum": "6.3", "List": [ "7-LVM", "7-RAW" ] }, "CENTOS-HPC": { "Minimum": "6.3" } }, "REDHAT": { "RHEL": { "Minimum": "6.7", "List": [ "7-LVM", "7-RAW" ] }, "RHEL-HANA": { "Minimum": "6.7" }, "RHEL-SAP": { "Minimum": "6.7" }, "RHEL-SAP-APPS": { "Minimum": "6.7" }, "RHEL-SAP-HANA": { "Minimum": "6.7" } }, "SUSE": { "SLES": { "List": [ "11-SP4", "11-SP5", "11-SP6", "12-SP1", "12-SP2", "12-SP3", "12-SP4", "12-SP5", "12-SP6" ] }, "SLES-BYOS": { "List": [ "11-SP4", "12", "12-SP1", "12-SP2", "12-SP3", "12-SP4", "12-SP5", "15", "15-SP1", "15-SP2", "15-SP3", "15-SP4", "15-SP5" ] }, "SLES-SAP": { "List": [ "11-SP4", "12", "12-SP1", "12-SP2", "12-SP3", "12-SP4", "12-SP5", "15", "15-SP1", "15-SP2", "15-SP3", "15-SP4", "15-SP5" ] }, "SLE-HPC": { "List": [ "15-SP1", "15-SP2", "15-SP3", "15-SP4", "15-SP5" ] } } }""" class ImageInfoMatcher(object): def __init__(self, doc): self.doc = json.loads(doc) def is_match(self, publisher, offer, sku, version): def _is_match_walk(doci, keys): key = keys.pop(0).upper() if key is None: return False if key not in doci: return False if 'List' in doci[key] and keys[0] in doci[key]['List']: return True if 'Match' in doci[key] and re.match(doci[key]['Match'], keys[0]): return True if 'Minimum' in doci[key]: try: return FlexibleVersion(keys[0]) >= FlexibleVersion(doci[key]['Minimum']) except ValueError: pass return _is_match_walk(doci[key], keys) return _is_match_walk(self.doc, [ publisher, offer, sku, version ]) class ComputeInfo(DataContract): __matcher = ImageInfoMatcher(ENDORSED_IMAGE_INFO_MATCHER_JSON) def __init__(self, location=None, name=None, offer=None, osType=None, placementGroupId=None, platformFaultDomain=None, placementUpdateDomain=None, publisher=None, resourceGroupName=None, sku=None, subscriptionId=None, tags=None, version=None, vmId=None, vmSize=None, vmScaleSetName=None, zone=None): self.location = location self.name = name self.offer = offer self.osType = osType self.placementGroupId = placementGroupId self.platformFaultDomain = platformFaultDomain self.platformUpdateDomain = placementUpdateDomain self.publisher = publisher self.resourceGroupName = resourceGroupName self.sku = sku self.subscriptionId = subscriptionId self.tags = tags self.version = version self.vmId = vmId self.vmSize = vmSize self.vmScaleSetName = vmScaleSetName self.zone = zone @property def image_info(self): return "{0}:{1}:{2}:{3}".format(self.publisher, self.offer, self.sku, self.version) @property def image_origin(self): """ An integer value describing the origin of the image. 0 -> unknown 1 -> custom - user created image 2 -> endorsed - See https://docs.microsoft.com/en-us/azure/virtual-machines/linux/endorsed-distros 3 -> platform - non-endorsed image that is available in the Azure Marketplace. """ try: if self.publisher == "": return IMDS_IMAGE_ORIGIN_CUSTOM if ComputeInfo.__matcher.is_match(self.publisher, self.offer, self.sku, self.version): return IMDS_IMAGE_ORIGIN_ENDORSED else: return IMDS_IMAGE_ORIGIN_PLATFORM except Exception as e: logger.periodic_warn(logger.EVERY_FIFTEEN_MINUTES, "[PERIODIC] Could not determine the image origin from IMDS: {0}".format(ustr(e))) return IMDS_IMAGE_ORIGIN_UNKNOWN class ImdsClient(object): def __init__(self, wireserver_endpoint, version=APIVERSION): self._api_version = version self._headers = { 'User-Agent': restutil.HTTP_USER_AGENT, 'Metadata': True, } self._health_headers = { 'User-Agent': restutil.HTTP_USER_AGENT_HEALTH, 'Metadata': True, } self._regex_ioerror = re.compile(r".*HTTP Failed. GET http://[^ ]+ -- IOError .*") self._regex_throttled = re.compile(r".*HTTP Retry. GET http://[^ ]+ -- Status Code 429 .*") self._wireserver_endpoint = wireserver_endpoint def _get_metadata_url(self, endpoint, resource_path): return BASE_METADATA_URI.format(endpoint, resource_path, self._api_version) def _http_get(self, endpoint, resource_path, headers): url = self._get_metadata_url(endpoint, resource_path) return restutil.http_get(url, headers=headers, use_proxy=False) def _get_metadata_from_endpoint(self, endpoint, resource_path, headers): """ Get metadata from one of the IMDS endpoints. :param str endpoint: IMDS endpoint to call :param str resource_path: path of IMDS resource :param bool headers: headers to send in the request :return: Tuple status: one of the following response status codes: IMDS_RESPONSE_SUCCESS, IMDS_RESPONSE_ERROR, IMDS_CONNECTION_ERROR, IMDS_INTERNAL_SERVER_ERROR response: IMDS response on IMDS_RESPONSE_SUCCESS, failure message otherwise """ try: resp = self._http_get(endpoint=endpoint, resource_path=resource_path, headers=headers) except ResourceGoneError: return IMDS_INTERNAL_SERVER_ERROR, "IMDS error in /metadata/{0}: HTTP Failed with Status Code 410: Gone".format(resource_path) except HttpError as e: msg = str(e) if self._regex_throttled.match(msg): return IMDS_RESPONSE_ERROR, "IMDS error in /metadata/{0}: Throttled".format(resource_path) if self._regex_ioerror.match(msg): logger.periodic_warn(logger.EVERY_FIFTEEN_MINUTES, "[PERIODIC] [IMDS_CONNECTION_ERROR] Unable to connect to IMDS endpoint {0}".format(endpoint)) return IMDS_CONNECTION_ERROR, "IMDS error in /metadata/{0}: Unable to connect to endpoint".format(resource_path) return IMDS_INTERNAL_SERVER_ERROR, "IMDS error in /metadata/{0}: {1}".format(resource_path, msg) if resp.status >= 500: return IMDS_INTERNAL_SERVER_ERROR, "IMDS error in /metadata/{0}: {1}".format( resource_path, restutil.read_response_error(resp)) if restutil.request_failed(resp): return IMDS_RESPONSE_ERROR, "IMDS error in /metadata/{0}: {1}".format( resource_path, restutil.read_response_error(resp)) return IMDS_RESPONSE_SUCCESS, resp.read() def get_metadata(self, resource_path, is_health): """ Get metadata from IMDS, falling back to Wireserver endpoint if necessary. :param str resource_path: path of IMDS resource :param bool is_health: True if for health/heartbeat, False otherwise :return: instance of MetadataResult :rtype: MetadataResult """ headers = self._health_headers if is_health else self._headers endpoint = IMDS_ENDPOINT status, resp = self._get_metadata_from_endpoint(endpoint, resource_path, headers) if status == IMDS_CONNECTION_ERROR: endpoint = self._wireserver_endpoint status, resp = self._get_metadata_from_endpoint(endpoint, resource_path, headers) if status == IMDS_RESPONSE_SUCCESS: return MetadataResult(True, False, resp) elif status == IMDS_INTERNAL_SERVER_ERROR: return MetadataResult(False, True, resp) return MetadataResult(False, False, resp) def get_compute(self): """ Fetch compute information. :return: instance of a ComputeInfo :rtype: ComputeInfo """ # ensure we get a 200 result = self.get_metadata('instance/compute', is_health=False) if not result.success: raise HttpError(result.response) data = json.loads(ustr(result.response, encoding="utf-8")) compute_info = ComputeInfo() set_properties('compute', compute_info, data) return compute_info def validate(self): """ Determines whether the metadata instance api returns 200, and the response is valid: compute should contain location, name, subscription id, and vm size and network should contain mac address and private ip address. :return: Tuple is_healthy: False when service returns an error, True on successful response and connection failures. error_response: validation failure details to assist with debugging """ # ensure we get a 200 result = self.get_metadata('instance', is_health=True) if not result.success: # we should only return False when the service is unhealthy return (not result.service_error), result.response # ensure the response is valid json try: json_data = json.loads(ustr(result.response, encoding="utf-8")) except Exception as e: return False, "JSON parsing failed: {0}".format(ustr(e)) # ensure all expected fields are present and have a value try: # TODO: compute fields cannot be verified yet since we need to exclude rdfe vms (#1249) self.check_field(json_data, 'network') self.check_field(json_data['network'], 'interface') self.check_field(json_data['network']['interface'][0], 'macAddress') self.check_field(json_data['network']['interface'][0], 'ipv4') self.check_field(json_data['network']['interface'][0]['ipv4'], 'ipAddress') self.check_field(json_data['network']['interface'][0]['ipv4']['ipAddress'][0], 'privateIpAddress') except ValueError as v: return False, ustr(v) return True, '' @staticmethod def check_field(dict_obj, field): if field not in dict_obj or dict_obj[field] is None: raise ValueError('Missing field: [{0}]'.format(field)) if len(dict_obj[field]) == 0: raise ValueError('Empty field: [{0}]'.format(field)) WALinuxAgent-2.9.1.1/azurelinuxagent/common/protocol/metadata_server_migration_util.py000066400000000000000000000060131446033677600314140ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2020 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import azurelinuxagent.common.conf as conf import azurelinuxagent.common.logger as logger from azurelinuxagent.common.event import add_event, WALAEventOperation from azurelinuxagent.common.utils.restutil import KNOWN_WIRESERVER_IP from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION # Name for Metadata Server Protocol _METADATA_PROTOCOL_NAME = "MetadataProtocol" # MetadataServer Certificates for Cleanup _LEGACY_METADATA_SERVER_TRANSPORT_PRV_FILE_NAME = "V2TransportPrivate.pem" _LEGACY_METADATA_SERVER_TRANSPORT_CERT_FILE_NAME = "V2TransportCert.pem" _LEGACY_METADATA_SERVER_P7B_FILE_NAME = "Certificates.p7b" # MetadataServer Endpoint _KNOWN_METADATASERVER_IP = "169.254.169.254" def is_metadata_server_artifact_present(): metadata_artifact_path = os.path.join(conf.get_lib_dir(), _LEGACY_METADATA_SERVER_TRANSPORT_CERT_FILE_NAME) return os.path.isfile(metadata_artifact_path) def cleanup_metadata_server_artifacts(osutil): logger.info("Clean up for MetadataServer to WireServer protocol migration: removing MetadataServer certificates and resetting firewall rules.") _cleanup_metadata_protocol_certificates() _reset_firewall_rules(osutil) def _cleanup_metadata_protocol_certificates(): """ Removes MetadataServer Certificates. """ lib_directory = conf.get_lib_dir() _ensure_file_removed(lib_directory, _LEGACY_METADATA_SERVER_TRANSPORT_PRV_FILE_NAME) _ensure_file_removed(lib_directory, _LEGACY_METADATA_SERVER_TRANSPORT_CERT_FILE_NAME) _ensure_file_removed(lib_directory, _LEGACY_METADATA_SERVER_P7B_FILE_NAME) def _reset_firewall_rules(osutil): """ Removes MetadataServer firewall rule so IMDS can be used. Enables WireServer firewall rule based on if firewall is configured to be on. """ osutil.remove_firewall(dst_ip=_KNOWN_METADATASERVER_IP, uid=os.getuid(), wait=osutil.get_firewall_will_wait()) if conf.enable_firewall(): success, _ = osutil.enable_firewall(dst_ip=KNOWN_WIRESERVER_IP, uid=os.getuid()) add_event( AGENT_NAME, version=CURRENT_VERSION, op=WALAEventOperation.Firewall, is_success=success, log_event=False) def _ensure_file_removed(directory, file_name): """ Removes files if they are present. """ path = os.path.join(directory, file_name) if os.path.isfile(path): os.remove(path) WALinuxAgent-2.9.1.1/azurelinuxagent/common/protocol/ovfenv.py000066400000000000000000000116601446033677600244470ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # """ Copy and parse ovf-env.xml from provisioning ISO and local cache """ import os # pylint: disable=W0611 import re # pylint: disable=W0611 import shutil # pylint: disable=W0611 import xml.dom.minidom as minidom # pylint: disable=W0611 import azurelinuxagent.common.logger as logger from azurelinuxagent.common.exception import ProtocolError from azurelinuxagent.common.future import ustr # pylint: disable=W0611 import azurelinuxagent.common.utils.fileutil as fileutil # pylint: disable=W0611 from azurelinuxagent.common.utils.textutil import parse_doc, findall, find, findtext OVF_VERSION = "1.0" OVF_NAME_SPACE = "http://schemas.dmtf.org/ovf/environment/1" WA_NAME_SPACE = "http://schemas.microsoft.com/windowsazure" def _validate_ovf(val, msg): if val is None: raise ProtocolError("Failed to validate OVF: {0}".format(msg)) class OvfEnv(object): """ Read, and process provisioning info from provisioning file OvfEnv.xml """ def __init__(self, xml_text): if xml_text is None: raise ValueError("ovf-env is None") logger.verbose("Load ovf-env.xml") self.hostname = None self.username = None self.user_password = None self.customdata = None self.disable_ssh_password_auth = True self.ssh_pubkeys = [] self.ssh_keypairs = [] self.provision_guest_agent = None self.parse(xml_text) def parse(self, xml_text): """ Parse xml tree, retreiving user and ssh key information. Return self. """ wans = WA_NAME_SPACE ovfns = OVF_NAME_SPACE xml_doc = parse_doc(xml_text) environment = find(xml_doc, "Environment", namespace=ovfns) _validate_ovf(environment, "Environment not found") section = find(environment, "ProvisioningSection", namespace=wans) _validate_ovf(section, "ProvisioningSection not found") version = findtext(environment, "Version", namespace=wans) _validate_ovf(version, "Version not found") if version > OVF_VERSION: logger.warn("Newer provisioning configuration detected. " "Please consider updating waagent") conf_set = find(section, "LinuxProvisioningConfigurationSet", namespace=wans) _validate_ovf(conf_set, "LinuxProvisioningConfigurationSet not found") self.hostname = findtext(conf_set, "HostName", namespace=wans) _validate_ovf(self.hostname, "HostName not found") self.username = findtext(conf_set, "UserName", namespace=wans) _validate_ovf(self.username, "UserName not found") self.user_password = findtext(conf_set, "UserPassword", namespace=wans) self.customdata = findtext(conf_set, "CustomData", namespace=wans) auth_option = findtext(conf_set, "DisableSshPasswordAuthentication", namespace=wans) if auth_option is not None and auth_option.lower() == "true": self.disable_ssh_password_auth = True else: self.disable_ssh_password_auth = False public_keys = findall(conf_set, "PublicKey", namespace=wans) for public_key in public_keys: path = findtext(public_key, "Path", namespace=wans) fingerprint = findtext(public_key, "Fingerprint", namespace=wans) value = findtext(public_key, "Value", namespace=wans) self.ssh_pubkeys.append((path, fingerprint, value)) keypairs = findall(conf_set, "KeyPair", namespace=wans) for keypair in keypairs: path = findtext(keypair, "Path", namespace=wans) fingerprint = findtext(keypair, "Fingerprint", namespace=wans) self.ssh_keypairs.append((path, fingerprint)) platform_settings_section = find(environment, "PlatformSettingsSection", namespace=wans) _validate_ovf(platform_settings_section, "PlatformSettingsSection not found") platform_settings = find(platform_settings_section, "PlatformSettings", namespace=wans) _validate_ovf(platform_settings, "PlatformSettings not found") self.provision_guest_agent = findtext(platform_settings, "ProvisionGuestAgent", namespace=wans) _validate_ovf(self.provision_guest_agent, "ProvisionGuestAgent not found") WALinuxAgent-2.9.1.1/azurelinuxagent/common/protocol/restapi.py000066400000000000000000000267641446033677600246260ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import socket import time from azurelinuxagent.common.datacontract import DataContract, DataContractList from azurelinuxagent.common.future import ustr from azurelinuxagent.common.utils.flexible_version import FlexibleVersion from azurelinuxagent.common.utils.textutil import getattrib from azurelinuxagent.common.version import DISTRO_VERSION, DISTRO_NAME, CURRENT_VERSION VERSION_0 = "0.0.0.0" class VMInfo(DataContract): def __init__(self, subscriptionId=None, vmName=None, roleName=None, roleInstanceName=None, tenantName=None): self.subscriptionId = subscriptionId self.vmName = vmName self.roleName = roleName self.roleInstanceName = roleInstanceName self.tenantName = tenantName class CertificateData(DataContract): def __init__(self, certificateData=None): self.certificateData = certificateData class Cert(DataContract): def __init__(self, name=None, thumbprint=None, certificateDataUri=None, storeName=None, storeLocation=None): self.name = name self.thumbprint = thumbprint self.certificateDataUri = certificateDataUri self.storeLocation = storeLocation self.storeName = storeName class CertList(DataContract): def __init__(self): self.certificates = DataContractList(Cert) class VMAgentFamily(object): def __init__(self, name, version=None): self.name = name # This is the Requested version as specified by the Goal State, it defaults to 0.0.0.0 if not specified in GS self.requested_version_string = VERSION_0 if version is None else version self.uris = [] @property def requested_version(self): return FlexibleVersion(self.requested_version_string) @property def is_requested_version_specified(self): """ If we don't get any requested_version from the GS, we default it to 0.0.0.0. This property identifies if a requested Version was passed in the GS or not. """ return self.requested_version > FlexibleVersion(VERSION_0) def __repr__(self): return self.__str__() def __str__(self): return "[name: '{0}' uris: {1}]".format(self.name, self.uris) class ExtensionState(object): Enabled = ustr("enabled") Disabled = ustr("disabled") class ExtensionRequestedState(object): """ This is the state of the Handler as requested by the Goal State. CRP only supports 2 states as of now - Enabled and Uninstall Disabled was used for older XML extensions and we keep it to support backward compatibility. """ Enabled = ustr("enabled") Disabled = ustr("disabled") Uninstall = ustr("uninstall") All = [Enabled, Disabled, Uninstall] class ExtensionSettings(object): """ The runtime settings associated with a Handler - Maps to Extension.PluginSettings.Plugin.RuntimeSettings for single config extensions in the ExtensionConfig.xml Eg: 1.settings, 2.settings - Maps to Extension.PluginSettings.Plugin.ExtensionRuntimeSettings for multi-config extensions in the ExtensionConfig.xml Eg: .1.settings, .2.settings """ def __init__(self, name=None, sequenceNumber=None, publicSettings=None, protectedSettings=None, certificateThumbprint=None, dependencyLevel=0, state=ExtensionState.Enabled): self.name = name self.sequenceNumber = sequenceNumber self.publicSettings = publicSettings self.protectedSettings = protectedSettings self.certificateThumbprint = certificateThumbprint self.dependencyLevel = dependencyLevel self.state = state def dependency_level_sort_key(self, handler_state): level = self.dependencyLevel # Process uninstall or disabled before enabled, in reverse order # Prioritize Handler state and Extension state both when sorting extensions # remap 0 to -1, 1 to -2, 2 to -3, etc if handler_state != ExtensionRequestedState.Enabled or self.state != ExtensionState.Enabled: level = (0 - level) - 1 return level def __repr__(self): return self.__str__() def __str__(self): return "{0}".format(self.name) class Extension(object): """ The main Plugin/handler specified by the publishers. Maps to Extension.PluginSettings.Plugins.Plugin in the ExtensionConfig.xml file Eg: Microsoft.OSTC.CustomScript """ def __init__(self, name=None): self.name = name self.version = None self.state = None self.settings = [] self.manifest_uris = [] self.supports_multi_config = False self.__invalid_handler_setting_reason = None @property def is_invalid_setting(self): return self.__invalid_handler_setting_reason is not None @property def invalid_setting_reason(self): return self.__invalid_handler_setting_reason @invalid_setting_reason.setter def invalid_setting_reason(self, value): self.__invalid_handler_setting_reason = value def dependency_level_sort_key(self): levels = [e.dependencyLevel for e in self.settings] if len(levels) == 0: level = 0 else: level = min(levels) # Process uninstall or disabled before enabled, in reverse order # remap 0 to -1, 1 to -2, 2 to -3, etc if self.state != u"enabled": level = (0 - level) - 1 return level def __repr__(self): return self.__str__() def __str__(self): return "{0}-{1}".format(self.name, self.version) class InVMGoalStateMetaData(DataContract): """ Object for parsing the GoalState MetaData received from CRP Eg: """ def __init__(self, in_vm_metadata_node): self.correlation_id = getattrib(in_vm_metadata_node, "correlationId") self.activity_id = getattrib(in_vm_metadata_node, "activityId") self.created_on_ticks = getattrib(in_vm_metadata_node, "createdOnTicks") self.in_svd_seq_no = getattrib(in_vm_metadata_node, "inSvdSeqNo") class ExtHandlerPackage(DataContract): def __init__(self, version=None): self.version = version self.uris = [] # TODO update the naming to align with metadata protocol self.isinternal = False self.disallow_major_upgrade = False class ExtHandlerPackageList(DataContract): def __init__(self): self.versions = DataContractList(ExtHandlerPackage) class VMProperties(DataContract): def __init__(self, certificateThumbprint=None): # TODO need to confirm the property name self.certificateThumbprint = certificateThumbprint class ProvisionStatus(DataContract): def __init__(self, status=None, subStatus=None, description=None): self.status = status self.subStatus = subStatus self.description = description self.properties = VMProperties() class ExtensionSubStatus(DataContract): def __init__(self, name=None, status=None, code=None, message=None): self.name = name self.status = status self.code = code self.message = message class ExtensionStatus(DataContract): def __init__(self, name=None, configurationAppliedTime=None, operation=None, status=None, seq_no=None, code=None, message=None): self.name = name self.configurationAppliedTime = configurationAppliedTime self.operation = operation self.status = status self.sequenceNumber = seq_no self.code = code self.message = message self.substatusList = DataContractList(ExtensionSubStatus) class ExtHandlerStatus(DataContract): def __init__(self, name=None, version=None, status=None, code=0, message=None): self.name = name self.version = version self.status = status self.code = code self.message = message self.supports_multi_config = False self.extension_status = None class VMAgentStatus(DataContract): def __init__(self, status=None, message=None, gs_aggregate_status=None, update_status=None): self.status = status self.message = message self.hostname = socket.gethostname() self.version = str(CURRENT_VERSION) self.osname = DISTRO_NAME self.osversion = DISTRO_VERSION self.extensionHandlers = DataContractList(ExtHandlerStatus) self.vm_artifacts_aggregate_status = VMArtifactsAggregateStatus(gs_aggregate_status) self.update_status = update_status self._supports_fast_track = False @property def supports_fast_track(self): return self._supports_fast_track def set_supports_fast_track(self, value): self._supports_fast_track = value class VMStatus(DataContract): def __init__(self, status, message, gs_aggregate_status=None, vm_agent_update_status=None): self.vmAgent = VMAgentStatus(status=status, message=message, gs_aggregate_status=gs_aggregate_status, update_status=vm_agent_update_status) class GoalStateAggregateStatus(DataContract): def __init__(self, seq_no, status=None, message="", code=None): self.message = message self.in_svd_seq_no = seq_no self.status = status self.code = code self.__utc_timestamp = time.gmtime() @property def processed_time(self): return self.__utc_timestamp class VMArtifactsAggregateStatus(DataContract): def __init__(self, gs_aggregate_status=None): self.goal_state_aggregate_status = gs_aggregate_status class RemoteAccessUser(DataContract): def __init__(self, name, encrypted_password, expiration): self.name = name self.encrypted_password = encrypted_password self.expiration = expiration class RemoteAccessUsersList(DataContract): def __init__(self): self.users = DataContractList(RemoteAccessUser) class VMAgentUpdateStatuses(object): Success = ustr("Success") Transitioning = ustr("Transitioning") Error = ustr("Error") Unknown = ustr("Unknown") class VMAgentUpdateStatus(object): def __init__(self, expected_version, status=VMAgentUpdateStatuses.Success, message="", code=0): self.expected_version = expected_version self.status = status self.message = message self.code = code WALinuxAgent-2.9.1.1/azurelinuxagent/common/protocol/util.py000066400000000000000000000275371446033677600241330ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import errno import os import re import time import threading import azurelinuxagent.common.conf as conf import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.fileutil as fileutil from azurelinuxagent.common.singletonperthread import SingletonPerThread from azurelinuxagent.common.exception import ProtocolError, OSUtilError, \ ProtocolNotFoundError, DhcpError from azurelinuxagent.common.future import ustr from azurelinuxagent.common.osutil import get_osutil from azurelinuxagent.common.dhcp import get_dhcp_handler from azurelinuxagent.common.protocol.metadata_server_migration_util import cleanup_metadata_server_artifacts, \ is_metadata_server_artifact_present from azurelinuxagent.common.protocol.ovfenv import OvfEnv from azurelinuxagent.common.protocol.wire import WireProtocol from azurelinuxagent.common.utils.restutil import KNOWN_WIRESERVER_IP, \ IOErrorCounter OVF_FILE_NAME = "ovf-env.xml" PROTOCOL_FILE_NAME = "Protocol" MAX_RETRY = 360 PROBE_INTERVAL = 10 ENDPOINT_FILE_NAME = "WireServerEndpoint" PASSWORD_PATTERN = ".*?<" PASSWORD_REPLACEMENT = "*<" WIRE_PROTOCOL_NAME = "WireProtocol" def get_protocol_util(): return ProtocolUtil() class ProtocolUtil(SingletonPerThread): """ ProtocolUtil handles initialization for protocol instance. 2 protocol types are invoked, wire protocol and metadata protocols. Note: ProtocolUtil is a sub class of SingletonPerThread, this basically means that there would only be 1 single instance of ProtocolUtil object per thread. """ def __init__(self): self._lock = threading.RLock() # protects the files on disk created during protocol detection self._protocol = None self.endpoint = None self.osutil = get_osutil() self.dhcp_handler = get_dhcp_handler() def copy_ovf_env(self): """ Copy ovf env file from dvd to hard disk. Remove password before save it to the disk """ dvd_mount_point = conf.get_dvd_mount_point() ovf_file_path_on_dvd = os.path.join(dvd_mount_point, OVF_FILE_NAME) ovf_file_path = os.path.join(conf.get_lib_dir(), OVF_FILE_NAME) try: self.osutil.mount_dvd() except OSUtilError as e: raise ProtocolError("[CopyOvfEnv] Error mounting dvd: " "{0}".format(ustr(e))) try: ovfxml = fileutil.read_file(ovf_file_path_on_dvd, remove_bom=True) ovfenv = OvfEnv(ovfxml) except (IOError, OSError) as e: raise ProtocolError("[CopyOvfEnv] Error reading file " "{0}: {1}".format(ovf_file_path_on_dvd, ustr(e))) try: ovfxml = re.sub(PASSWORD_PATTERN, PASSWORD_REPLACEMENT, ovfxml) fileutil.write_file(ovf_file_path, ovfxml) except (IOError, OSError) as e: raise ProtocolError("[CopyOvfEnv] Error writing file " "{0}: {1}".format(ovf_file_path, ustr(e))) self._cleanup_ovf_dvd() return ovfenv def _cleanup_ovf_dvd(self): try: self.osutil.umount_dvd() self.osutil.eject_dvd() except OSUtilError as e: logger.warn(ustr(e)) def get_ovf_env(self): """ Load saved ovf-env.xml """ ovf_file_path = os.path.join(conf.get_lib_dir(), OVF_FILE_NAME) if os.path.isfile(ovf_file_path): xml_text = fileutil.read_file(ovf_file_path) return OvfEnv(xml_text) else: raise ProtocolError( "ovf-env.xml is missing from {0}".format(ovf_file_path)) def _get_protocol_file_path(self): return os.path.join( conf.get_lib_dir(), PROTOCOL_FILE_NAME) def _get_wireserver_endpoint_file_path(self): return os.path.join( conf.get_lib_dir(), ENDPOINT_FILE_NAME) def get_wireserver_endpoint(self): self._lock.acquire() try: if self.endpoint: return self.endpoint file_path = self._get_wireserver_endpoint_file_path() if os.path.isfile(file_path): try: self.endpoint = fileutil.read_file(file_path) if self.endpoint: logger.info("WireServer endpoint {0} read from file", self.endpoint) return self.endpoint logger.error("[GetWireserverEndpoint] Unexpected empty file {0}", file_path) except (IOError, OSError) as e: logger.error("[GetWireserverEndpoint] Error reading file {0}: {1}", file_path, str(e)) else: logger.error("[GetWireserverEndpoint] Missing file {0}", file_path) self.endpoint = KNOWN_WIRESERVER_IP logger.info("Using hardcoded Wireserver endpoint {0}", self.endpoint) return self.endpoint finally: self._lock.release() def _set_wireserver_endpoint(self, endpoint): try: self.endpoint = endpoint file_path = self._get_wireserver_endpoint_file_path() fileutil.write_file(file_path, endpoint) except (IOError, OSError) as e: raise OSUtilError(ustr(e)) def _clear_wireserver_endpoint(self): """ Cleanup previous saved wireserver endpoint. """ self.endpoint = None endpoint_file_path = self._get_wireserver_endpoint_file_path() if not os.path.isfile(endpoint_file_path): return try: os.remove(endpoint_file_path) except (IOError, OSError) as e: # Ignore file-not-found errors (since the file is being removed) if e.errno == errno.ENOENT: return logger.error("Failed to clear wiresever endpoint: {0}", e) def _detect_protocol(self, init_goal_state=True): """ Probe protocol endpoints in turn. """ self.clear_protocol() for retry in range(0, MAX_RETRY): try: endpoint = self.dhcp_handler.endpoint if endpoint is None: # pylint: disable=W0105 ''' Check if DHCP can be used to get the wire protocol endpoint ''' # pylint: enable=W0105 dhcp_available = self.osutil.is_dhcp_available() if dhcp_available: logger.info("WireServer endpoint is not found. Rerun dhcp handler") try: self.dhcp_handler.run() except DhcpError as e: raise ProtocolError(ustr(e)) endpoint = self.dhcp_handler.endpoint else: logger.info("_detect_protocol: DHCP not available") endpoint = self.get_wireserver_endpoint() try: protocol = WireProtocol(endpoint) protocol.detect(init_goal_state=init_goal_state) self._set_wireserver_endpoint(endpoint) return protocol except ProtocolError as e: logger.info("WireServer is not responding. Reset dhcp endpoint") self.dhcp_handler.endpoint = None self.dhcp_handler.skip_cache = True raise e except ProtocolError as e: logger.info("Protocol endpoint not found: {0}", e) if retry < MAX_RETRY - 1: logger.info("Retry detect protocol: retry={0}", retry) time.sleep(PROBE_INTERVAL) raise ProtocolNotFoundError("No protocol found.") def _save_protocol(self, protocol_name): """ Save protocol endpoint """ protocol_file_path = self._get_protocol_file_path() try: fileutil.write_file(protocol_file_path, protocol_name) except (IOError, OSError) as e: logger.error("Failed to save protocol endpoint: {0}", e) def clear_protocol(self): """ Cleanup previous saved protocol endpoint. """ self._lock.acquire() try: logger.info("Clean protocol and wireserver endpoint") self._clear_wireserver_endpoint() self._protocol = None protocol_file_path = self._get_protocol_file_path() if not os.path.isfile(protocol_file_path): return try: os.remove(protocol_file_path) except (IOError, OSError) as e: # Ignore file-not-found errors (since the file is being removed) if e.errno == errno.ENOENT: return logger.error("Failed to clear protocol endpoint: {0}", e) finally: self._lock.release() def get_protocol(self, init_goal_state=True): """ Detect protocol by endpoint. :returns: protocol instance """ self._lock.acquire() try: if self._protocol is not None: return self._protocol # If the protocol file contains MetadataProtocol we need to fall through to # _detect_protocol so that we can generate the WireServer transport certificates. protocol_file_path = self._get_protocol_file_path() if os.path.isfile(protocol_file_path) and fileutil.read_file(protocol_file_path) == WIRE_PROTOCOL_NAME: endpoint = self.get_wireserver_endpoint() self._protocol = WireProtocol(endpoint) # If metadataserver certificates are present we clean certificates # and remove MetadataServer firewall rule. It is possible # there was a previous intermediate upgrade before 2.2.48 but metadata artifacts # were not cleaned up (intermediate updated agent does not have cleanup # logic but we transitioned from Metadata to Wire protocol) if is_metadata_server_artifact_present(): cleanup_metadata_server_artifacts(self.osutil) return self._protocol logger.info("Detect protocol endpoint") protocol = self._detect_protocol(init_goal_state=init_goal_state) IOErrorCounter.set_protocol_endpoint(endpoint=protocol.get_endpoint()) self._save_protocol(WIRE_PROTOCOL_NAME) self._protocol = protocol # Need to clean up MDS artifacts only after _detect_protocol so that we don't # delete MDS certificates if we can't reach WireServer and have to roll back # the update if is_metadata_server_artifact_present(): cleanup_metadata_server_artifacts(self.osutil) return self._protocol finally: self._lock.release() WALinuxAgent-2.9.1.1/azurelinuxagent/common/protocol/wire.py000066400000000000000000001451471446033677600241220ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ import json import os import random import shutil import time import zipfile from collections import defaultdict from datetime import datetime, timedelta from xml.sax import saxutils from azurelinuxagent.common import conf from azurelinuxagent.common import logger from azurelinuxagent.common.utils import textutil from azurelinuxagent.common.agent_supported_feature import get_agent_supported_features_list_for_crp, SupportedFeatureNames from azurelinuxagent.common.datacontract import validate_param from azurelinuxagent.common.event import add_event, WALAEventOperation, report_event, \ CollectOrReportEventDebugInfo, add_periodic from azurelinuxagent.common.exception import ProtocolNotFoundError, \ ResourceGoneError, ExtensionDownloadError, InvalidContainerError, ProtocolError, HttpError, ExtensionErrorCodes from azurelinuxagent.common.future import httpclient, bytebuffer, ustr from azurelinuxagent.common.protocol.goal_state import GoalState, TRANSPORT_CERT_FILE_NAME, TRANSPORT_PRV_FILE_NAME, \ GoalStateProperties from azurelinuxagent.common.protocol.hostplugin import HostPluginProtocol from azurelinuxagent.common.protocol.restapi import DataContract, ProvisionStatus, VMInfo, VMStatus from azurelinuxagent.common.telemetryevent import GuestAgentExtensionEventsSchema from azurelinuxagent.common.utils import fileutil, restutil from azurelinuxagent.common.utils.cryptutil import CryptUtil from azurelinuxagent.common.utils.textutil import parse_doc, findall, find, \ findtext, gettext, remove_bom, get_bytes_from_pem, parse_json from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION VERSION_INFO_URI = "http://{0}/?comp=versions" HEALTH_REPORT_URI = "http://{0}/machine?comp=health" ROLE_PROP_URI = "http://{0}/machine?comp=roleProperties" TELEMETRY_URI = "http://{0}/machine?comp=telemetrydata" PROTOCOL_VERSION = "2012-11-30" ENDPOINT_FINE_NAME = "WireServer" SHORT_WAITING_INTERVAL = 1 # 1 second MAX_EVENT_BUFFER_SIZE = 2 ** 16 - 2 ** 10 _DOWNLOAD_TIMEOUT = timedelta(minutes=5) class UploadError(HttpError): pass class WireProtocol(DataContract): def __init__(self, endpoint): if endpoint is None: raise ProtocolError("WireProtocol endpoint is None") self.client = WireClient(endpoint) def detect(self, init_goal_state=True): self.client.check_wire_protocol_version() trans_prv_file = os.path.join(conf.get_lib_dir(), TRANSPORT_PRV_FILE_NAME) trans_cert_file = os.path.join(conf.get_lib_dir(), TRANSPORT_CERT_FILE_NAME) cryptutil = CryptUtil(conf.get_openssl_cmd()) cryptutil.gen_transport_cert(trans_prv_file, trans_cert_file) # Initialize the goal state, including all the inner properties if init_goal_state: logger.info('Initializing goal state during protocol detection') self.client.reset_goal_state() def update_host_plugin_from_goal_state(self): self.client.update_host_plugin_from_goal_state() def get_endpoint(self): return self.client.get_endpoint() def get_vminfo(self): goal_state = self.client.get_goal_state() hosting_env = self.client.get_hosting_env() vminfo = VMInfo() vminfo.subscriptionId = None vminfo.vmName = hosting_env.vm_name vminfo.tenantName = hosting_env.deployment_name vminfo.roleName = hosting_env.role_name vminfo.roleInstanceName = goal_state.role_instance_id return vminfo def get_certs(self): certificates = self.client.get_certs() return certificates.cert_list def get_goal_state(self): return self.client.get_goal_state() def report_provision_status(self, provision_status): validate_param("provision_status", provision_status, ProvisionStatus) if provision_status.status is not None: self.client.report_health(provision_status.status, provision_status.subStatus, provision_status.description) if provision_status.properties.certificateThumbprint is not None: thumbprint = provision_status.properties.certificateThumbprint self.client.report_role_prop(thumbprint) def report_vm_status(self, vm_status): validate_param("vm_status", vm_status, VMStatus) self.client.status_blob.set_vm_status(vm_status) self.client.upload_status_blob() def report_event(self, events_iterator): self.client.report_event(events_iterator) def upload_logs(self, logs): self.client.upload_logs(logs) def get_status_blob_data(self): return self.client.status_blob.data def _build_role_properties(container_id, role_instance_id, thumbprint): xml = (u"" u"" u"" u"{0}" u"" u"" u"{1}" u"" u"" u"" u"" u"" u"" u"" u"").format(container_id, role_instance_id, thumbprint) return xml def _build_health_report(incarnation, container_id, role_instance_id, status, substatus, description): # The max description that can be sent to WireServer is 4096 bytes. # Exceeding this max can result in a failure to report health. # To keep this simple, we will keep a 10% buffer and trim before # encoding the description. if description: max_chars_before_encoding = 3686 len_before_trim = len(description) description = description[:max_chars_before_encoding] trimmed_char_count = len_before_trim - len(description) if trimmed_char_count > 0: logger.info( 'Trimmed health report description by {0} characters'.format( trimmed_char_count ) ) # Escape '&', '<' and '>' description = saxutils.escape(ustr(description)) detail = u'' if substatus is not None: substatus = saxutils.escape(ustr(substatus)) detail = (u"
" u"{0}" u"{1}" u"
").format(substatus, description) xml = (u"" u"" u"{0}" u"" u"{1}" u"" u"" u"{2}" u"" u"{3}" u"{4}" u"" u"" u"" u"" u"" u"").format(incarnation, container_id, role_instance_id, status, detail) return xml def ga_status_to_guest_info(ga_status): """ Convert VMStatus object to status blob format """ v1_ga_guest_info = { "computerName": ga_status.hostname, "osName": ga_status.osname, "osVersion": ga_status.osversion, "version": ga_status.version, } return v1_ga_guest_info def __get_formatted_msg_for_status_reporting(msg, lang="en-US"): return { 'lang': lang, 'message': msg } def _get_utc_timestamp_for_status_reporting(time_format="%Y-%m-%dT%H:%M:%SZ", timestamp=None): timestamp = time.gmtime() if timestamp is None else timestamp return time.strftime(time_format, timestamp) def ga_status_to_v1(ga_status): v1_ga_status = { "version": ga_status.version, "status": ga_status.status, "formattedMessage": __get_formatted_msg_for_status_reporting(ga_status.message) } if ga_status.update_status is not None: v1_ga_status["updateStatus"] = get_ga_update_status_to_v1(ga_status.update_status) return v1_ga_status def get_ga_update_status_to_v1(update_status): v1_ga_update_status = { "expectedVersion": update_status.expected_version, "status": update_status.status, "code": update_status.code, "formattedMessage": __get_formatted_msg_for_status_reporting(update_status.message) } return v1_ga_update_status def ext_substatus_to_v1(sub_status_list): status_list = [] for substatus in sub_status_list: status = { "name": substatus.name, "status": substatus.status, "code": substatus.code, "formattedMessage": __get_formatted_msg_for_status_reporting(substatus.message) } status_list.append(status) return status_list def ext_status_to_v1(ext_status): if ext_status is None: return None timestamp = _get_utc_timestamp_for_status_reporting() v1_sub_status = ext_substatus_to_v1(ext_status.substatusList) v1_ext_status = { "status": { "name": ext_status.name, "configurationAppliedTime": ext_status.configurationAppliedTime, "operation": ext_status.operation, "status": ext_status.status, "code": ext_status.code, "formattedMessage": __get_formatted_msg_for_status_reporting(ext_status.message) }, "version": 1.0, "timestampUTC": timestamp } if len(v1_sub_status) != 0: v1_ext_status['status']['substatus'] = v1_sub_status return v1_ext_status def ext_handler_status_to_v1(ext_handler_status): v1_handler_status = { 'handlerVersion': ext_handler_status.version, 'handlerName': ext_handler_status.name, 'status': ext_handler_status.status, 'code': ext_handler_status.code, 'useExactVersion': True } if ext_handler_status.message is not None: v1_handler_status["formattedMessage"] = __get_formatted_msg_for_status_reporting(ext_handler_status.message) v1_ext_status = ext_status_to_v1(ext_handler_status.extension_status) if ext_handler_status.extension_status is not None and v1_ext_status is not None: v1_handler_status["runtimeSettingsStatus"] = { 'settingsStatus': v1_ext_status, 'sequenceNumber': ext_handler_status.extension_status.sequenceNumber } # Add extension name if Handler supports MultiConfig if ext_handler_status.supports_multi_config: v1_handler_status["runtimeSettingsStatus"]["extensionName"] = ext_handler_status.extension_status.name return v1_handler_status def vm_artifacts_aggregate_status_to_v1(vm_artifacts_aggregate_status): gs_aggregate_status = vm_artifacts_aggregate_status.goal_state_aggregate_status if gs_aggregate_status is None: return None v1_goal_state_aggregate_status = { "formattedMessage": __get_formatted_msg_for_status_reporting(gs_aggregate_status.message), "timestampUTC": _get_utc_timestamp_for_status_reporting(timestamp=gs_aggregate_status.processed_time), "inSvdSeqNo": gs_aggregate_status.in_svd_seq_no, "status": gs_aggregate_status.status, "code": gs_aggregate_status.code } v1_artifact_aggregate_status = { "goalStateAggregateStatus": v1_goal_state_aggregate_status } return v1_artifact_aggregate_status def vm_status_to_v1(vm_status): timestamp = _get_utc_timestamp_for_status_reporting() v1_ga_guest_info = ga_status_to_guest_info(vm_status.vmAgent) v1_ga_status = ga_status_to_v1(vm_status.vmAgent) v1_vm_artifact_aggregate_status = vm_artifacts_aggregate_status_to_v1( vm_status.vmAgent.vm_artifacts_aggregate_status) v1_handler_status_list = [] for handler_status in vm_status.vmAgent.extensionHandlers: v1_handler_status_list.append(ext_handler_status_to_v1(handler_status)) v1_agg_status = { 'guestAgentStatus': v1_ga_status, 'handlerAggregateStatus': v1_handler_status_list } if v1_vm_artifact_aggregate_status is not None: v1_agg_status['vmArtifactsAggregateStatus'] = v1_vm_artifact_aggregate_status v1_vm_status = { 'version': '1.1', 'timestampUTC': timestamp, 'aggregateStatus': v1_agg_status, 'guestOSInfo': v1_ga_guest_info } supported_features = [] for _, feature in get_agent_supported_features_list_for_crp().items(): supported_features.append( { "Key": feature.name, "Value": feature.version } ) if vm_status.vmAgent.supports_fast_track: supported_features.append( { "Key": SupportedFeatureNames.FastTrack, "Value": "1.0" # This is a dummy version; CRP ignores it } ) if supported_features: v1_vm_status["supportedFeatures"] = supported_features return v1_vm_status class StatusBlob(object): def __init__(self, client): self.vm_status = None self.client = client self.type = None self.data = None def set_vm_status(self, vm_status): validate_param("vmAgent", vm_status, VMStatus) self.vm_status = vm_status def to_json(self): report = vm_status_to_v1(self.vm_status) return json.dumps(report) __storage_version__ = "2014-02-14" def prepare(self, blob_type): logger.verbose("Prepare status blob") self.data = self.to_json() self.type = blob_type def upload(self, url): try: if not self.type in ["BlockBlob", "PageBlob"]: raise ProtocolError("Illegal blob type: {0}".format(self.type)) if self.type == "BlockBlob": self.put_block_blob(url, self.data) else: self.put_page_blob(url, self.data) return True except Exception as e: logger.verbose("Initial status upload failed: {0}", e) return False def get_block_blob_headers(self, blob_size): return { "Content-Length": ustr(blob_size), "x-ms-blob-type": "BlockBlob", "x-ms-date": _get_utc_timestamp_for_status_reporting(), "x-ms-version": self.__class__.__storage_version__ } def put_block_blob(self, url, data): logger.verbose("Put block blob") headers = self.get_block_blob_headers(len(data)) resp = self.client.call_storage_service(restutil.http_put, url, data, headers) if resp.status != httpclient.CREATED: raise UploadError( "Failed to upload block blob: {0}".format(resp.status)) def get_page_blob_create_headers(self, blob_size): return { "Content-Length": "0", "x-ms-blob-content-length": ustr(blob_size), "x-ms-blob-type": "PageBlob", "x-ms-date": _get_utc_timestamp_for_status_reporting(), "x-ms-version": self.__class__.__storage_version__ } def get_page_blob_page_headers(self, start, end): return { "Content-Length": ustr(end - start), "x-ms-date": _get_utc_timestamp_for_status_reporting(), "x-ms-range": "bytes={0}-{1}".format(start, end - 1), "x-ms-page-write": "update", "x-ms-version": self.__class__.__storage_version__ } def put_page_blob(self, url, data): logger.verbose("Put page blob") # Convert string into bytes and align to 512 bytes data = bytearray(data, encoding='utf-8') page_blob_size = int((len(data) + 511) / 512) * 512 headers = self.get_page_blob_create_headers(page_blob_size) resp = self.client.call_storage_service(restutil.http_put, url, "", headers) if resp.status != httpclient.CREATED: raise UploadError( "Failed to clean up page blob: {0}".format(resp.status)) if url.count("?") <= 0: url = "{0}?comp=page".format(url) else: url = "{0}&comp=page".format(url) logger.verbose("Upload page blob") page_max = 4 * 1024 * 1024 # Max page size: 4MB start = 0 end = 0 while end < len(data): end = min(len(data), start + page_max) content_size = end - start # Align to 512 bytes page_end = int((end + 511) / 512) * 512 buf_size = page_end - start buf = bytearray(buf_size) buf[0: content_size] = data[start: end] headers = self.get_page_blob_page_headers(start, page_end) resp = self.client.call_storage_service( restutil.http_put, url, bytebuffer(buf), headers) if resp is None or resp.status != httpclient.CREATED: raise UploadError( "Failed to upload page blob: {0}".format(resp.status)) start = end def event_param_to_v1(param): param_format = ustr('') param_type = type(param.value) attr_type = "" if param_type is int: attr_type = 'mt:uint64' elif param_type is str: attr_type = 'mt:wstr' elif ustr(param_type).count("'unicode'") > 0: attr_type = 'mt:wstr' elif param_type is bool: attr_type = 'mt:bool' elif param_type is float: attr_type = 'mt:float64' return param_format.format(param.name, saxutils.quoteattr(ustr(param.value)), attr_type) def event_to_v1_encoded(event, encoding='utf-8'): params = "" for param in event.parameters: params += event_param_to_v1(param) event_str = ustr('').format(event.eventId, params) return event_str.encode(encoding) class WireClient(object): def __init__(self, endpoint): logger.info("Wire server endpoint:{0}", endpoint) self._endpoint = endpoint self._goal_state = None self._host_plugin = None self.status_blob = StatusBlob(self) def get_endpoint(self): return self._endpoint def call_wireserver(self, http_req, *args, **kwargs): try: # Never use the HTTP proxy for wireserver kwargs['use_proxy'] = False resp = http_req(*args, **kwargs) if restutil.request_failed(resp): msg = "[Wireserver Failed] URI {0} ".format(args[0]) if resp is not None: msg += " [HTTP Failed] Status Code {0}".format(resp.status) raise ProtocolError(msg) # If the GoalState is stale, pass along the exception to the caller except ResourceGoneError: raise except Exception as e: raise ProtocolError("[Wireserver Exception] {0}".format(ustr(e))) return resp def decode_config(self, data): if data is None: return None data = remove_bom(data) xml_text = ustr(data, encoding='utf-8') return xml_text def fetch_config(self, uri, headers): resp = self.call_wireserver(restutil.http_get, uri, headers=headers) return self.decode_config(resp.read()) @staticmethod def call_storage_service(http_req, *args, **kwargs): # Default to use the configured HTTP proxy if not 'use_proxy' in kwargs or kwargs['use_proxy'] is None: kwargs['use_proxy'] = True return http_req(*args, **kwargs) def fetch_artifacts_profile_blob(self, uri): return self._fetch_content("artifacts profile blob", [uri], use_verify_header=False)[1] # _fetch_content returns a (uri, content) tuple def fetch_manifest(self, uris, use_verify_header): uri, content = self._fetch_content("manifest", uris, use_verify_header=use_verify_header) self.get_host_plugin().update_manifest_uri(uri) return content def _fetch_content(self, download_type, uris, use_verify_header): """ Walks the given list of 'uris' issuing HTTP GET requests; returns a tuple with the URI and the content of the first successful request. The 'download_type' is added to any log messages produced by this method; it should describe the type of content of the given URIs (e.g. "manifest", "extension package", etc). """ host_ga_plugin = self.get_host_plugin() direct_download = lambda uri: self.fetch(uri)[0] def hgap_download(uri): request_uri, request_headers = host_ga_plugin.get_artifact_request(uri, use_verify_header=use_verify_header) response, _ = self.fetch(request_uri, request_headers, use_proxy=False, retry_codes=restutil.HGAP_GET_EXTENSION_ARTIFACT_RETRY_CODES) return response return self._download_with_fallback_channel(download_type, uris, direct_download=direct_download, hgap_download=hgap_download) def download_zip_package(self, package_type, uris, target_file, target_directory, use_verify_header): """ Downloads the ZIP package specified in 'uris' (which is a list of alternate locations for the ZIP), saving it to 'target_file' and then expanding its contents to 'target_directory'. Deletes the target file after it has been expanded. The 'package_type' is only used in log messages and has no other semantics. It should specify the contents of the ZIP, e.g. "extension package" or "agent package" The 'use_verify_header' parameter indicates whether the verify header should be added when using the extensionArtifact API of the HostGAPlugin. """ host_ga_plugin = self.get_host_plugin() direct_download = lambda uri: self.stream(uri, target_file, headers=None, use_proxy=True) def hgap_download(uri): request_uri, request_headers = host_ga_plugin.get_artifact_request(uri, use_verify_header=use_verify_header, artifact_manifest_url=host_ga_plugin.manifest_uri) return self.stream(request_uri, target_file, headers=request_headers, use_proxy=False) on_downloaded = lambda: WireClient._try_expand_zip_package(package_type, target_file, target_directory) self._download_with_fallback_channel(package_type, uris, direct_download=direct_download, hgap_download=hgap_download, on_downloaded=on_downloaded) def _download_with_fallback_channel(self, download_type, uris, direct_download, hgap_download, on_downloaded=None): """ Walks the given list of 'uris' issuing HTTP GET requests, attempting to download the content of each URI. The download is done using both the default and the fallback channels, until one of them succeeds. The 'direct_download' and 'hgap_download' functions define the logic to do direct calls to the URI or to use the HostGAPlugin as a proxy for the download. Initially the default channel is the direct download and the fallback channel is the HostGAPlugin, but the default can be depending on the success/failure of each channel (see _download_using_appropriate_channel() for the logic to do this). The 'download_type' is added to any log messages produced by this method; it should describe the type of content of the given URIs (e.g. "manifest", "extension package, "agent package", etc). When the download is successful, _download_with_fallback_channel invokes the 'on_downloaded' function, which can be used to process the results of the download. This function should return True on success, and False on failure (it should not raise any exceptions). If the return value is False, the download is considered a failure and the next URI is tried. When the download succeeds, this method returns a (uri, response) tuple where the first item is the URI of the successful download and the second item is the response returned by the successful channel (i.e. one of direct_download and hgap_download). This method enforces a timeout (_DOWNLOAD_TIMEOUT) on the download and raises an exception if the limit is exceeded. """ logger.info("Downloading {0}", download_type) start_time = datetime.now() uris_shuffled = uris random.shuffle(uris_shuffled) most_recent_error = "None" for index, uri in enumerate(uris_shuffled): elapsed = datetime.now() - start_time if elapsed > _DOWNLOAD_TIMEOUT: message = "Timeout downloading {0}. Elapsed: {1} URIs tried: {2}/{3}. Last error: {4}".format(download_type, elapsed, index, len(uris), ustr(most_recent_error)) raise ExtensionDownloadError(message, code=ExtensionErrorCodes.PluginManifestDownloadError) try: # Disable W0640: OK to use uri in a lambda within the loop's body response = self._download_using_appropriate_channel(lambda: direct_download(uri), lambda: hgap_download(uri)) # pylint: disable=W0640 if on_downloaded is not None: on_downloaded() return uri, response except Exception as exception: most_recent_error = exception raise ExtensionDownloadError("Failed to download {0} from all URIs. Last error: {1}".format(download_type, ustr(most_recent_error)), code=ExtensionErrorCodes.PluginManifestDownloadError) @staticmethod def _try_expand_zip_package(package_type, target_file, target_directory): logger.info("Unzipping {0}: {1}", package_type, target_file) try: zipfile.ZipFile(target_file).extractall(target_directory) except Exception as exception: logger.error("Error while unzipping {0}: {1}", package_type, ustr(exception)) if os.path.exists(target_directory): try: shutil.rmtree(target_directory) except Exception as exception: logger.warn("Cannot delete {0}: {1}", target_directory, ustr(exception)) raise finally: try: os.remove(target_file) except Exception as exception: logger.warn("Cannot delete {0}: {1}", target_file, ustr(exception)) def stream(self, uri, destination, headers=None, use_proxy=None): """ Downloads the content of the given 'uri' and saves it to the 'destination' file. """ try: logger.verbose("Fetch [{0}] with headers [{1}] to file [{2}]", uri, headers, destination) response = self._fetch_response(uri, headers, use_proxy) if response is not None and not restutil.request_failed(response): chunk_size = 1024 * 1024 # 1MB buffer with open(destination, 'wb', chunk_size) as destination_fh: complete = False while not complete: chunk = response.read(chunk_size) destination_fh.write(chunk) complete = len(chunk) < chunk_size return "" except: if os.path.exists(destination): # delete the destination file, in case we did a partial download try: os.remove(destination) except Exception as exception: logger.warn("Can't delete {0}: {1}", destination, ustr(exception)) raise def fetch(self, uri, headers=None, use_proxy=None, decode=True, retry_codes=None, ok_codes=None): """ Returns a tuple with the content and headers of the response. The headers are a list of (name, value) tuples. """ logger.verbose("Fetch [{0}] with headers [{1}]", uri, headers) content = None response_headers = None response = self._fetch_response(uri, headers, use_proxy, retry_codes=retry_codes, ok_codes=ok_codes) if response is not None and not restutil.request_failed(response, ok_codes=ok_codes): response_content = response.read() content = self.decode_config(response_content) if decode else response_content response_headers = response.getheaders() return content, response_headers def _fetch_response(self, uri, headers=None, use_proxy=None, retry_codes=None, ok_codes=None): resp = None try: resp = self.call_storage_service( restutil.http_get, uri, headers=headers, use_proxy=use_proxy, retry_codes=retry_codes) host_plugin = self.get_host_plugin() if restutil.request_failed(resp, ok_codes=ok_codes): error_response = restutil.read_response_error(resp) msg = "Fetch failed from [{0}]: {1}".format(uri, error_response) logger.warn(msg) if host_plugin is not None: host_plugin.report_fetch_health(uri, is_healthy=not restutil.request_failed_at_hostplugin(resp), source='WireClient', response=error_response) raise ProtocolError(msg) else: if host_plugin is not None: host_plugin.report_fetch_health(uri, source='WireClient') except (HttpError, ProtocolError, IOError) as error: msg = "Fetch failed: {0}".format(error) logger.warn(msg) report_event(op=WALAEventOperation.HttpGet, is_success=False, message=msg, log_event=False) raise return resp def update_host_plugin_from_goal_state(self): """ Fetches a new goal state and updates the Container ID and Role Config Name of the host plugin client """ if self._host_plugin is not None: GoalState.update_host_plugin_headers(self) def update_host_plugin(self, container_id, role_config_name): if self._host_plugin is not None: self._host_plugin.update_container_id(container_id) self._host_plugin.update_role_config_name(role_config_name) def update_goal_state(self, silent=False): """ Updates the goal state if the incarnation or etag changed """ try: if self._goal_state is None: self._goal_state = GoalState(self, silent=silent) else: self._goal_state.update(silent=silent) except ProtocolError: raise except Exception as exception: raise ProtocolError("Error fetching goal state: {0}".format(ustr(exception))) def reset_goal_state(self, goal_state_properties=GoalStateProperties.All, silent=False): """ Resets the goal state """ try: if not silent: logger.info("Forcing an update of the goal state.") self._goal_state = GoalState(self, goal_state_properties=goal_state_properties, silent=silent) except ProtocolError: raise except Exception as exception: raise ProtocolError("Error fetching goal state: {0}".format(ustr(exception))) def get_goal_state(self): if self._goal_state is None: raise ProtocolError("Trying to fetch goal state before initialization!") return self._goal_state def get_hosting_env(self): if self._goal_state is None: raise ProtocolError("Trying to fetch Hosting Environment before initialization!") return self._goal_state.hosting_env def get_shared_conf(self): if self._goal_state is None: raise ProtocolError("Trying to fetch Shared Conf before initialization!") return self._goal_state.shared_conf def get_certs(self): if self._goal_state is None: raise ProtocolError("Trying to fetch Certificates before initialization!") return self._goal_state.certs def get_remote_access(self): if self._goal_state is None: raise ProtocolError("Trying to fetch Remote Access before initialization!") return self._goal_state.remote_access def check_wire_protocol_version(self): uri = VERSION_INFO_URI.format(self.get_endpoint()) version_info_xml = self.fetch_config(uri, None) version_info = VersionInfo(version_info_xml) preferred = version_info.get_preferred() if PROTOCOL_VERSION == preferred: logger.info("Wire protocol version:{0}", PROTOCOL_VERSION) elif PROTOCOL_VERSION in version_info.get_supported(): logger.info("Wire protocol version:{0}", PROTOCOL_VERSION) logger.info("Server preferred version:{0}", preferred) else: error = ("Agent supported wire protocol version: {0} was not " "advised by Fabric.").format(PROTOCOL_VERSION) raise ProtocolNotFoundError(error) def _call_hostplugin_with_container_check(self, host_func): """ Calls host_func on host channel and accounts for stale resource (ResourceGoneError or InvalidContainerError). If stale, it refreshes the goal state and retries host_func. """ try: return host_func() except (ResourceGoneError, InvalidContainerError) as error: host_plugin = self.get_host_plugin() old_container_id, old_role_config_name = host_plugin.container_id, host_plugin.role_config_name msg = "[PERIODIC] Request failed with the current host plugin configuration. " \ "ContainerId: {0}, role config file: {1}. Fetching new goal state and retrying the call." \ "Error: {2}".format(old_container_id, old_role_config_name, ustr(error)) logger.periodic_info(logger.EVERY_SIX_HOURS, msg) self.update_host_plugin_from_goal_state() new_container_id, new_role_config_name = host_plugin.container_id, host_plugin.role_config_name msg = "[PERIODIC] Host plugin reconfigured with new parameters. " \ "ContainerId: {0}, role config file: {1}.".format(new_container_id, new_role_config_name) logger.periodic_info(logger.EVERY_SIX_HOURS, msg) try: ret = host_func() msg = "[PERIODIC] Request succeeded using the host plugin channel after goal state refresh. " \ "ContainerId changed from {0} to {1}, " \ "role config file changed from {2} to {3}.".format(old_container_id, new_container_id, old_role_config_name, new_role_config_name) add_periodic(delta=logger.EVERY_SIX_HOURS, name=AGENT_NAME, version=CURRENT_VERSION, op=WALAEventOperation.HostPlugin, is_success=True, message=msg, log_event=True) return ret except (ResourceGoneError, InvalidContainerError) as error: msg = "[PERIODIC] Request failed using the host plugin channel after goal state refresh. " \ "ContainerId changed from {0} to {1}, role config file changed from {2} to {3}. " \ "Exception type: {4}.".format(old_container_id, new_container_id, old_role_config_name, new_role_config_name, type(error).__name__) add_periodic(delta=logger.EVERY_SIX_HOURS, name=AGENT_NAME, version=CURRENT_VERSION, op=WALAEventOperation.HostPlugin, is_success=False, message=msg, log_event=True) raise def _download_using_appropriate_channel(self, direct_download, hgap_download): """ Does a download using both the default and fallback channels. By default, the primary channel is direct, host channel is the fallback. We call the primary channel first and return on success. If primary fails, we try the fallback. If fallback fails, we return and *don't* switch the default channel. If fallback succeeds, we change the default channel. """ hgap_download_function_with_retry = lambda: self._call_hostplugin_with_container_check(hgap_download) if HostPluginProtocol.is_default_channel: primary_channel, secondary_channel = hgap_download_function_with_retry, direct_download else: primary_channel, secondary_channel = direct_download, hgap_download_function_with_retry try: return primary_channel() except Exception as exception: primary_channel_error = exception try: return_value = secondary_channel() # Since the secondary channel succeeded, flip the default channel HostPluginProtocol.is_default_channel = not HostPluginProtocol.is_default_channel message = "Default channel changed to {0} channel.".format("HostGAPlugin" if HostPluginProtocol.is_default_channel else "Direct") logger.info(message) add_event(AGENT_NAME, op=WALAEventOperation.DefaultChannelChange, version=CURRENT_VERSION, is_success=True, message=message, log_event=False) return return_value except Exception as exception: raise HttpError("Download failed both on the primary and fallback channels. Primary: [{0}] Fallback: [{1}]".format(ustr(primary_channel_error), ustr(exception))) def upload_status_blob(self): extensions_goal_state = self.get_goal_state().extensions_goal_state if extensions_goal_state.status_upload_blob is None: # the status upload blob is in ExtensionsConfig so force a full goal state refresh self.reset_goal_state(silent=True) extensions_goal_state = self.get_goal_state().extensions_goal_state if extensions_goal_state.status_upload_blob is None: raise ProtocolNotFoundError("Status upload uri is missing") logger.info("Refreshed the goal state to get the status upload blob. New Goal State ID: {0}", extensions_goal_state.id) blob_type = extensions_goal_state.status_upload_blob_type try: self.status_blob.prepare(blob_type) except Exception as e: raise ProtocolError("Exception creating status blob: {0}".format(ustr(e))) # Swap the order of use for the HostPlugin vs. the "direct" route. # Prefer the use of HostPlugin. If HostPlugin fails fall back to the # direct route. # # The code previously preferred the "direct" route always, and only fell back # to the HostPlugin *if* there was an error. We would like to move to # the HostPlugin for all traffic, but this is a big change. We would like # to see how this behaves at scale, and have a fallback should things go # wrong. This is why we try HostPlugin then direct. try: host = self.get_host_plugin() host.put_vm_status(self.status_blob, extensions_goal_state.status_upload_blob, extensions_goal_state.status_upload_blob_type) return except ResourceGoneError: # refresh the host plugin client and try again on the next iteration of the main loop self.update_host_plugin_from_goal_state() return except Exception as e: # for all other errors, fall back to direct msg = "Falling back to direct upload: {0}".format(ustr(e)) self.report_status_event(msg, is_success=True) try: if self.status_blob.upload(extensions_goal_state.status_upload_blob): return except Exception as e: msg = "Exception uploading status blob: {0}".format(ustr(e)) self.report_status_event(msg, is_success=False) raise ProtocolError("Failed to upload status blob via either channel") def report_role_prop(self, thumbprint): goal_state = self.get_goal_state() role_prop = _build_role_properties(goal_state.container_id, goal_state.role_instance_id, thumbprint) role_prop = role_prop.encode("utf-8") role_prop_uri = ROLE_PROP_URI.format(self.get_endpoint()) headers = self.get_header_for_xml_content() try: resp = self.call_wireserver(restutil.http_post, role_prop_uri, role_prop, headers=headers) except HttpError as e: raise ProtocolError((u"Failed to send role properties: " u"{0}").format(e)) if resp.status != httpclient.ACCEPTED: raise ProtocolError((u"Failed to send role properties: " u",{0}: {1}").format(resp.status, resp.read())) def report_health(self, status, substatus, description): goal_state = self.get_goal_state() health_report = _build_health_report(goal_state.incarnation, goal_state.container_id, goal_state.role_instance_id, status, substatus, description) health_report = health_report.encode("utf-8") health_report_uri = HEALTH_REPORT_URI.format(self.get_endpoint()) headers = self.get_header_for_xml_content() try: # 30 retries with 10s sleep gives ~5min for wireserver updates; # this is retried 3 times with 15s sleep before throwing a # ProtocolError, for a total of ~15min. resp = self.call_wireserver(restutil.http_post, health_report_uri, health_report, headers=headers, max_retry=30, retry_delay=15) except HttpError as e: raise ProtocolError((u"Failed to send provision status: " u"{0}").format(e)) if restutil.request_failed(resp): raise ProtocolError((u"Failed to send provision status: " u",{0}: {1}").format(resp.status, resp.read())) def send_encoded_event(self, provider_id, event_str, encoding='utf8'): uri = TELEMETRY_URI.format(self.get_endpoint()) data_format_header = ustr('').format( provider_id).encode(encoding) data_format_footer = ustr('').encode(encoding) # Event string should already be encoded by the time it gets here, to avoid double encoding, # dividing it into parts. data = data_format_header + event_str + data_format_footer try: header = self.get_header_for_xml_content() # NOTE: The call to wireserver requests utf-8 encoding in the headers, but the body should not # be encoded: some nodes in the telemetry pipeline do not support utf-8 encoding. resp = self.call_wireserver(restutil.http_post, uri, data, header) except HttpError as e: raise ProtocolError("Failed to send events:{0}".format(e)) if restutil.request_failed(resp): logger.verbose(resp.read()) raise ProtocolError( "Failed to send events:{0}".format(resp.status)) def report_event(self, events_iterator): buf = {} debug_info = CollectOrReportEventDebugInfo(operation=CollectOrReportEventDebugInfo.OP_REPORT) events_per_provider = defaultdict(int) def _send_event(provider_id, debug_info): try: self.send_encoded_event(provider_id, buf[provider_id]) except UnicodeError as uni_error: debug_info.update_unicode_error(uni_error) except Exception as error: debug_info.update_op_error(error) # Group events by providerId for event in events_iterator: try: if event.providerId not in buf: buf[event.providerId] = b"" event_str = event_to_v1_encoded(event) if len(event_str) >= MAX_EVENT_BUFFER_SIZE: # Ignore single events that are too large to send out details_of_event = [ustr(x.name) + ":" + ustr(x.value) for x in event.parameters if x.name in [GuestAgentExtensionEventsSchema.Name, GuestAgentExtensionEventsSchema.Version, GuestAgentExtensionEventsSchema.Operation, GuestAgentExtensionEventsSchema.OperationSuccess]] logger.periodic_warn(logger.EVERY_HALF_HOUR, "Single event too large: {0}, with the length: {1} more than the limit({2})" .format(str(details_of_event), len(event_str), MAX_EVENT_BUFFER_SIZE)) continue # If buffer is full, send out the events in buffer and reset buffer if len(buf[event.providerId] + event_str) >= MAX_EVENT_BUFFER_SIZE: logger.verbose("No of events this request = {0}".format(events_per_provider[event.providerId])) _send_event(event.providerId, debug_info) buf[event.providerId] = b"" events_per_provider[event.providerId] = 0 # Add encoded events to the buffer buf[event.providerId] = buf[event.providerId] + event_str events_per_provider[event.providerId] += 1 except Exception as error: logger.warn("Unexpected error when generating Events:{0}", textutil.format_exception(error)) # Send out all events left in buffer. for provider_id in list(buf.keys()): if buf[provider_id]: logger.verbose("No of events this request = {0}".format(events_per_provider[provider_id])) _send_event(provider_id, debug_info) debug_info.report_debug_info() def report_status_event(self, message, is_success): report_event(op=WALAEventOperation.ReportStatus, is_success=is_success, message=message, log_event=not is_success) def get_header(self): return { "x-ms-agent-name": "WALinuxAgent", "x-ms-version": PROTOCOL_VERSION } def get_header_for_xml_content(self): return { "x-ms-agent-name": "WALinuxAgent", "x-ms-version": PROTOCOL_VERSION, "Content-Type": "text/xml;charset=utf-8" } def get_header_for_cert(self): trans_cert_file = os.path.join(conf.get_lib_dir(), TRANSPORT_CERT_FILE_NAME) try: content = fileutil.read_file(trans_cert_file) except IOError as e: raise ProtocolError("Failed to read {0}: {1}".format(trans_cert_file, e)) cert = get_bytes_from_pem(content) return { "x-ms-agent-name": "WALinuxAgent", "x-ms-version": PROTOCOL_VERSION, "x-ms-cipher-name": "DES_EDE3_CBC", "x-ms-guest-agent-public-x509-cert": cert } def get_host_plugin(self): if self._host_plugin is None: self._host_plugin = HostPluginProtocol(self.get_endpoint()) GoalState.update_host_plugin_headers(self) return self._host_plugin def get_on_hold(self): return self.get_goal_state().extensions_goal_state.on_hold def upload_logs(self, content): host = self.get_host_plugin() return host.put_vm_log(content) class VersionInfo(object): def __init__(self, xml_text): """ Query endpoint server for wire protocol version. Fail if our desired protocol version is not seen. """ logger.verbose("Load Version.xml") self.parse(xml_text) def parse(self, xml_text): xml_doc = parse_doc(xml_text) preferred = find(xml_doc, "Preferred") self.preferred = findtext(preferred, "Version") logger.info("Fabric preferred wire protocol version:{0}", self.preferred) self.supported = [] supported = find(xml_doc, "Supported") supported_version = findall(supported, "Version") for node in supported_version: version = gettext(node) logger.verbose("Fabric supported wire protocol version:{0}", version) self.supported.append(version) def get_preferred(self): return self.preferred def get_supported(self): return self.supported # Do not extend this class class InVMArtifactsProfile(object): """ deserialized json string of InVMArtifactsProfile. It is expected to contain the following fields: * inVMArtifactsProfileBlobSeqNo * profileId (optional) * onHold (optional) * certificateThumbprint (optional) * encryptedHealthChecks (optional) * encryptedApplicationProfile (optional) """ def __init__(self, artifacts_profile): if not textutil.is_str_empty(artifacts_profile): self.__dict__.update(parse_json(artifacts_profile)) def is_on_hold(self): # hasattr() is not available in Python 2.6 if 'onHold' in self.__dict__: return str(self.onHold).lower() == 'true' # pylint: disable=E1101 return False WALinuxAgent-2.9.1.1/azurelinuxagent/common/rdma.py000066400000000000000000000531151446033677600222270ustar00rootroot00000000000000# Windows Azure Linux Agent # # Copyright 2016 Microsoft Corporation # # 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. # """ Handle packages and modules to enable RDMA for IB networking """ import os import re import time import azurelinuxagent.common.conf as conf import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.fileutil as fileutil import azurelinuxagent.common.utils.shellutil as shellutil from azurelinuxagent.common.utils.textutil import parse_doc, find, getattrib dapl_config_paths = [ '/etc/dat.conf', '/etc/rdma/dat.conf', '/usr/local/etc/dat.conf' ] def setup_rdma_device(nd_version, shared_conf): logger.verbose("Parsing SharedConfig XML contents for RDMA details") xml_doc = parse_doc(shared_conf.xml_text) if xml_doc is None: logger.error("Could not parse SharedConfig XML document") return instance_elem = find(xml_doc, "Instance") if not instance_elem: logger.error("Could not find in SharedConfig document") return rdma_ipv4_addr = getattrib(instance_elem, "rdmaIPv4Address") if not rdma_ipv4_addr: logger.error( "Could not find rdmaIPv4Address attribute on Instance element of SharedConfig.xml document") return rdma_mac_addr = getattrib(instance_elem, "rdmaMacAddress") if not rdma_mac_addr: logger.error( "Could not find rdmaMacAddress attribute on Instance element of SharedConfig.xml document") return # add colons to the MAC address (e.g. 00155D33FF1D -> # 00:15:5D:33:FF:1D) rdma_mac_addr = ':'.join([rdma_mac_addr[i:i + 2] for i in range(0, len(rdma_mac_addr), 2)]) logger.info("Found RDMA details. IPv4={0} MAC={1}".format( rdma_ipv4_addr, rdma_mac_addr)) # Set up the RDMA device with collected informatino RDMADeviceHandler(rdma_ipv4_addr, rdma_mac_addr, nd_version).start() logger.info("RDMA: device is set up") return class RDMAHandler(object): driver_module_name = 'hv_network_direct' nd_version = None def get_rdma_version(self): # pylint: disable=R1710 """Retrieve the firmware version information from the system. This depends on information provided by the Linux kernel.""" if self.nd_version: return self.nd_version kvp_key_size = 512 kvp_value_size = 2048 driver_info_source = '/var/lib/hyperv/.kvp_pool_0' base_kernel_err_msg = 'Kernel does not provide the necessary ' base_kernel_err_msg += 'information or the kvp daemon is not running.' if not os.path.isfile(driver_info_source): error_msg = 'RDMA: Source file "%s" does not exist. ' error_msg += base_kernel_err_msg logger.error(error_msg % driver_info_source) return with open(driver_info_source, "rb") as pool_file: while True: key = pool_file.read(kvp_key_size) value = pool_file.read(kvp_value_size) if key and value: key_0 = key.partition(b"\x00")[0] if key_0: key_0 = key_0.decode() value_0 = value.partition(b"\x00")[0] if value_0: value_0 = value_0.decode() if key_0 == "NdDriverVersion": self.nd_version = value_0 return self.nd_version else: break error_msg = 'RDMA: NdDriverVersion not found in "%s"' logger.error(error_msg % driver_info_source) return @staticmethod def is_kvp_daemon_running(): """Look for kvp daemon names in ps -ef output and return True/False """ # for centos, the hypervkvpd and the hv_kvp_daemon both are ok. # for suse, it uses hv_kvp_daemon kvp_daemon_names = ['hypervkvpd', 'hv_kvp_daemon'] exitcode, ps_out = shellutil.run_get_output("ps -ef") if exitcode != 0: raise Exception('RDMA: ps -ef failed: %s' % ps_out) for n in kvp_daemon_names: if n in ps_out: logger.info('RDMA: kvp daemon (%s) is running' % n) return True else: logger.verbose('RDMA: kvp daemon (%s) is not running' % n) return False def load_driver_module(self): """Load the kernel driver, this depends on the proper driver to be installed with the install_driver() method""" logger.info("RDMA: probing module '%s'" % self.driver_module_name) result = shellutil.run('modprobe --first-time %s' % self.driver_module_name) if result != 0: error_msg = 'Could not load "%s" kernel module. ' error_msg += 'Run "modprobe --first-time %s" as root for more details' logger.error( error_msg % (self.driver_module_name, self.driver_module_name) ) return False logger.info('RDMA: Loaded the kernel driver successfully.') return True def install_driver_if_needed(self): if self.nd_version: if conf.enable_check_rdma_driver(): self.install_driver() else: logger.info('RDMA: check RDMA driver is disabled, skip installing driver') else: logger.info('RDMA: skip installing driver when ndversion not present\n') def install_driver(self): """Install the driver. This is distribution specific and must be overwritten in the child implementation.""" logger.error('RDMAHandler.install_driver not implemented') def is_driver_loaded(self): """Check if the network module is loaded in kernel space""" cmd = 'lsmod | grep ^%s' % self.driver_module_name status, loaded_modules = shellutil.run_get_output(cmd) # pylint: disable=W0612 logger.info('RDMA: Checking if the module loaded.') if loaded_modules: logger.info('RDMA: module loaded.') return True logger.info('RDMA: module not loaded.') return False def reboot_system(self): """Reboot the system. This is required as the kernel module for the rdma driver cannot be unloaded with rmmod""" logger.info('RDMA: Rebooting system.') ret = shellutil.run('shutdown -r now') if ret != 0: logger.error('RDMA: Failed to reboot the system') dapl_config_paths = [ '/etc/dat.conf', '/etc/rdma/dat.conf', '/usr/local/etc/dat.conf'] class RDMADeviceHandler(object): """ Responsible for writing RDMA IP and MAC address to the /dev/hvnd_rdma interface. """ rdma_dev = '/dev/hvnd_rdma' sriov_dir = '/sys/class/infiniband' device_check_timeout_sec = 120 device_check_interval_sec = 1 ipoib_check_timeout_sec = 60 ipoib_check_interval_sec = 1 ipv4_addr = None mac_addr = None nd_version = None def __init__(self, ipv4_addr, mac_addr, nd_version): self.ipv4_addr = ipv4_addr self.mac_addr = mac_addr self.nd_version = nd_version def start(self): logger.info("RDMA: starting device processing.") self.process() logger.info("RDMA: completed device processing.") def process(self): try: if not self.nd_version: logger.info("RDMA: provisioning SRIOV RDMA device.") self.provision_sriov_rdma() else: logger.info("RDMA: provisioning Network Direct RDMA device.") self.provision_network_direct_rdma() except Exception as e: logger.error("RDMA: device processing failed: {0}".format(e)) def provision_network_direct_rdma(self): RDMADeviceHandler.update_dat_conf(dapl_config_paths, self.ipv4_addr) if not conf.enable_check_rdma_driver(): logger.info("RDMA: skip checking RDMA driver version") RDMADeviceHandler.update_network_interface(self.mac_addr, self.ipv4_addr) return skip_rdma_device = False module_name = "hv_network_direct" retcode, out = shellutil.run_get_output("modprobe -R %s" % module_name, chk_err=False) if retcode == 0: module_name = out.strip() else: logger.info("RDMA: failed to resolve module name. Use original name") retcode, out = shellutil.run_get_output("modprobe %s" % module_name) if retcode != 0: logger.error("RDMA: failed to load module %s" % module_name) return retcode, out = shellutil.run_get_output("modinfo %s" % module_name) if retcode == 0: version = re.search("version:\s+(\d+)\.(\d+)\.(\d+)\D", out, re.IGNORECASE) # pylint: disable=W1401 if version: v1 = int(version.groups(0)[0]) v2 = int(version.groups(0)[1]) if v1 > 4 or v1 == 4 and v2 > 0: logger.info("Skip setting /dev/hvnd_rdma on 4.1 or later") skip_rdma_device = True else: logger.info("RDMA: hv_network_direct driver version not present, assuming 4.0.x or older.") else: logger.warn("RDMA: failed to get module info on hv_network_direct.") if not skip_rdma_device: RDMADeviceHandler.wait_rdma_device( self.rdma_dev, self.device_check_timeout_sec, self.device_check_interval_sec) RDMADeviceHandler.write_rdma_config_to_device( self.rdma_dev, self.ipv4_addr, self.mac_addr) RDMADeviceHandler.update_network_interface(self.mac_addr, self.ipv4_addr) def provision_sriov_rdma(self): (key, value) = self.read_ipoib_data() if key: # provision multiple IP over IB addresses logger.info("RDMA: provisioning multiple IP over IB addresses") self.provision_sriov_multiple_ib(value) elif self.ipv4_addr: logger.info("RDMA: provisioning single IP over IB address") # provision a single IP over IB address RDMADeviceHandler.wait_any_rdma_device(self.sriov_dir, self.device_check_timeout_sec, self.device_check_interval_sec) RDMADeviceHandler.update_iboip_interface(self.ipv4_addr, self.ipoib_check_timeout_sec, self.ipoib_check_interval_sec) else: logger.info("RDMA: missing IP address") def read_ipoib_data(self) : # read from KVP pool 0 to figure out the IP over IB addresses kvp_key_size = 512 kvp_value_size = 2048 driver_info_source = '/var/lib/hyperv/.kvp_pool_0' if not os.path.isfile(driver_info_source): logger.error("RDMA: can't read KVP pool 0") return (None, None) key_0 = None value_0 = None with open(driver_info_source, "rb") as pool_file: while True: key = pool_file.read(kvp_key_size) value = pool_file.read(kvp_value_size) if key and value: key_0 = key.partition(b"\x00")[0] if key_0 : key_0 = key_0.decode() if key_0 == "IPoIB_Data": value_0 = value.partition(b"\x00")[0] if value_0 : value_0 = value_0.decode() break else: break if key_0 == "IPoIB_Data": return (key_0, value_0) return (None, None) def provision_sriov_multiple_ib(self, value) : mac_ip_array = [] values = value.split("|") num_ips = len(values) - 1 # values[0] tells how many IPs. Format - NUMPAIRS: match = re.match(r"NUMPAIRS:(\d+)", values[0]) if match: num = int(match.groups(0)[0]) if num != num_ips: logger.error("RDMA: multiple IPs reported num={0} actual number of IPs={1}".format(num, num_ips)) return else: logger.error("RDMA: failed to find number of IP addresses in {0}".format(values[0])) return for i in range(1, num_ips+1): # each MAC/IP entry is of format : match = re.match(r"([^:]+):(\d+\.\d+\.\d+\.\d+)", values[i]) if match: mac_addr = match.groups(0)[0] ipv4_addr = match.groups(0)[1] mac_ip_array.append((mac_addr, ipv4_addr)) else: logger.error("RDMA: failed to find MAC/IP address in {0}".format(values[i])) return # try to assign all MAC/IP addresses to IB interfaces # retry for up to 60 times, with 1 seconds delay between each retry = 60 while retry > 0: count = self.update_iboip_interfaces(mac_ip_array) if count == len(mac_ip_array): return time.sleep(1) retry -= 1 logger.error("RDMA: failed to set all IP over IB addresses") # Assign addresses to all IP over IB interfaces specified in mac_ip_array # Return the number of IP addresses successfully assigned def update_iboip_interfaces(self, mac_ip_array): net_dir = "/sys/class/net" nics = os.listdir(net_dir) count = 0 for nic in nics: # look for IBoIP interface of format ibXXX if not re.match(r"ib\w+", nic): continue mac_addr = None with open(os.path.join(net_dir, nic, "address")) as address_file: mac_addr = address_file.read() if not mac_addr: logger.error("RDMA: can't read address for device {0}".format(nic)) continue mac_addr = mac_addr.upper() match = re.match(r".+(\w\w):(\w\w):(\w\w):\w\w:\w\w:(\w\w):(\w\w):(\w\w)\n", mac_addr) if not match: logger.error("RDMA: failed to parse address for device {0} address {1}".format(nic, mac_addr)) continue # format an MAC address without : mac_addr = "" mac_addr = mac_addr.join(match.groups(0)) for mac_ip in mac_ip_array: if mac_ip[0] == mac_addr: ret = 0 try: # bring up the interface and set its IP address ip_command = ["ip", "link", "set", nic, "up"] shellutil.run_command(ip_command) ip_command = ["ip", "addr", "add", "{0}/16".format(mac_ip[1]), "dev", nic] shellutil.run_command(ip_command) except shellutil.CommandError as error: ret = error.returncode if ret == 0: logger.info("RDMA: set address {0} to device {1}".format(mac_ip[1], nic)) if ret and ret != 2: # return value 2 means the address is already set logger.error("RDMA: failed to set IP address {0} on device {1}".format(mac_ip[1], nic)) else: count += 1 break return count @staticmethod def update_iboip_interface(ipv4_addr, timeout_sec, check_interval_sec): logger.info("Wait for ib0 become available") total_retries = timeout_sec / check_interval_sec n = 0 found_ib0 = None while not found_ib0 and n < total_retries: ret, output = shellutil.run_get_output("ifconfig -a") if ret != 0: raise Exception("Failed to list network interfaces") found_ib0 = re.search("ib0", output, re.IGNORECASE) if found_ib0: break time.sleep(check_interval_sec) n += 1 if not found_ib0: raise Exception("ib0 is not available") netmask = 16 logger.info("RDMA: configuring IPv4 addr and netmask on ipoib interface") addr = '{0}/{1}'.format(ipv4_addr, netmask) if shellutil.run("ifconfig ib0 {0}".format(addr)) != 0: raise Exception("Could set addr to {0} on ib0".format(addr)) logger.info("RDMA: ipoib address and netmask configured on interface") @staticmethod def update_dat_conf(paths, ipv4_addr): """ Looks at paths for dat.conf file and updates the ip address for the infiniband interface. """ logger.info("Updating DAPL configuration file") for f in paths: logger.info("RDMA: trying {0}".format(f)) if not os.path.isfile(f): logger.info( "RDMA: DAPL config not found at {0}".format(f)) continue logger.info("RDMA: DAPL config is at: {0}".format(f)) cfg = fileutil.read_file(f) new_cfg = RDMADeviceHandler.replace_dat_conf_contents( cfg, ipv4_addr) fileutil.write_file(f, new_cfg) logger.info("RDMA: DAPL configuration is updated") return raise Exception("RDMA: DAPL configuration file not found at predefined paths") @staticmethod def replace_dat_conf_contents(cfg, ipv4_addr): old = "ofa-v2-ib0 u2.0 nonthreadsafe default libdaplofa.so.2 dapl.2.0 \"\S+ 0\"" # pylint: disable=W1401 new = "ofa-v2-ib0 u2.0 nonthreadsafe default libdaplofa.so.2 dapl.2.0 \"{0} 0\"".format( ipv4_addr) return re.sub(old, new, cfg) @staticmethod def write_rdma_config_to_device(path, ipv4_addr, mac_addr): data = RDMADeviceHandler.generate_rdma_config(ipv4_addr, mac_addr) logger.info( "RDMA: Updating device with configuration: {0}".format(data)) with open(path, "w") as f: logger.info("RDMA: Device opened for writing") f.write(data) logger.info("RDMA: Updated device with IPv4/MAC addr successfully") @staticmethod def generate_rdma_config(ipv4_addr, mac_addr): return 'rdmaMacAddress="{0}" rdmaIPv4Address="{1}"'.format(mac_addr, ipv4_addr) @staticmethod def wait_rdma_device(path, timeout_sec, check_interval_sec): logger.info("RDMA: waiting for device={0} timeout={1}s".format(path, timeout_sec)) total_retries = timeout_sec / check_interval_sec n = 0 while n < total_retries: if os.path.exists(path): logger.info("RDMA: device ready") return logger.verbose( "RDMA: device not ready, sleep {0}s".format(check_interval_sec)) time.sleep(check_interval_sec) n += 1 logger.error("RDMA device wait timed out") raise Exception("The device did not show up in {0} seconds ({1} retries)".format( timeout_sec, total_retries)) @staticmethod def wait_any_rdma_device(directory, timeout_sec, check_interval_sec): logger.info( "RDMA: waiting for any Infiniband device at directory={0} timeout={1}s".format( directory, timeout_sec)) total_retries = timeout_sec / check_interval_sec n = 0 while n < total_retries: r = os.listdir(directory) if r: logger.info("RDMA: device found in {0}".format(directory)) return logger.verbose( "RDMA: device not ready, sleep {0}s".format(check_interval_sec)) time.sleep(check_interval_sec) n += 1 logger.error("RDMA device wait timed out") raise Exception("The device did not show up in {0} seconds ({1} retries)".format( timeout_sec, total_retries)) @staticmethod def update_network_interface(mac_addr, ipv4_addr): netmask = 16 logger.info("RDMA: will update the network interface with IPv4/MAC") if_name = RDMADeviceHandler.get_interface_by_mac(mac_addr) logger.info("RDMA: network interface found: {0}", if_name) logger.info("RDMA: bringing network interface up") if shellutil.run("ifconfig {0} up".format(if_name)) != 0: raise Exception("Could not bring up RMDA interface: {0}".format(if_name)) logger.info("RDMA: configuring IPv4 addr and netmask on interface") addr = '{0}/{1}'.format(ipv4_addr, netmask) if shellutil.run("ifconfig {0} {1}".format(if_name, addr)) != 0: raise Exception("Could set addr to {1} on {0}".format(if_name, addr)) logger.info("RDMA: network address and netmask configured on interface") @staticmethod def get_interface_by_mac(mac): ret, output = shellutil.run_get_output("ifconfig -a") if ret != 0: raise Exception("Failed to list network interfaces") output = output.replace('\n', '') match = re.search(r"(eth\d).*(HWaddr|ether) {0}".format(mac), output, re.IGNORECASE) if match is None: raise Exception("Failed to get ifname with mac: {0}".format(mac)) output = match.group(0) eths = re.findall(r"eth\d", output) if eths is None or len(eths) == 0: raise Exception("ifname with mac: {0} not found".format(mac)) return eths[-1] WALinuxAgent-2.9.1.1/azurelinuxagent/common/singletonperthread.py000066400000000000000000000030561446033677600252040ustar00rootroot00000000000000from threading import Lock, currentThread class _SingletonPerThreadMetaClass(type): """ A metaclass that creates a SingletonPerThread base class when called. """ _instances = {} _lock = Lock() def __call__(cls, *args, **kwargs): with cls._lock: obj_name = "%s__%s" % (cls.__name__, currentThread().getName()) # Object Name = className__threadName if obj_name not in cls._instances: cls._instances[obj_name] = super(_SingletonPerThreadMetaClass, cls).__call__(*args, **kwargs) return cls._instances[obj_name] class SingletonPerThread(_SingletonPerThreadMetaClass('SingleObjectPerThreadMetaClass', (object,), {})): # This base class calls the metaclass above to create the singleton per thread object. This class provides an # abstraction over how to invoke the Metaclass so just inheriting this class makes the # child class a singleton per thread (As opposed to invoking the Metaclass separately for each derived classes) # More info here - https://stackoverflow.com/questions/6760685/creating-a-singleton-in-python # # Usage: # Inheriting this class will create a Singleton per thread for that class # To delete the cached object of a class, call DerivedClassName.clear() to delete the object per thread # Note: If the thread dies and is recreated with the same thread name, the existing object would be reused # and no new object for the derived class would be created unless DerivedClassName.clear() is called explicitly to # delete the cache pass WALinuxAgent-2.9.1.1/azurelinuxagent/common/telemetryevent.py000066400000000000000000000073301446033677600243560ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2019 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # from azurelinuxagent.common.datacontract import DataContract, DataContractList from azurelinuxagent.common.version import AGENT_NAME class CommonTelemetryEventSchema(object): # Common schema keys for GuestAgentExtensionEvents, GuestAgentGenericLogs # and GuestAgentPerformanceCounterEvents tables in Kusto. EventPid = "EventPid" EventTid = "EventTid" GAVersion = "GAVersion" ContainerId = "ContainerId" TaskName = "TaskName" OpcodeName = "OpcodeName" KeywordName = "KeywordName" OSVersion = "OSVersion" ExecutionMode = "ExecutionMode" RAM = "RAM" Processors = "Processors" TenantName = "TenantName" RoleName = "RoleName" RoleInstanceName = "RoleInstanceName" Location = "Location" SubscriptionId = "SubscriptionId" ResourceGroupName = "ResourceGroupName" VMId = "VMId" ImageOrigin = "ImageOrigin" class GuestAgentGenericLogsSchema(CommonTelemetryEventSchema): # GuestAgentGenericLogs table specific schema keys EventName = "EventName" CapabilityUsed = "CapabilityUsed" Context1 = "Context1" Context2 = "Context2" Context3 = "Context3" class GuestAgentExtensionEventsSchema(CommonTelemetryEventSchema): # GuestAgentExtensionEvents table specific schema keys ExtensionType = "ExtensionType" IsInternal = "IsInternal" Name = "Name" Version = "Version" Operation = "Operation" OperationSuccess = "OperationSuccess" Message = "Message" Duration = "Duration" class GuestAgentPerfCounterEventsSchema(CommonTelemetryEventSchema): # GuestAgentPerformanceCounterEvents table specific schema keys Category = "Category" Counter = "Counter" Instance = "Instance" Value = "Value" class TelemetryEventParam(DataContract): def __init__(self, name=None, value=None): self.name = name self.value = value def __eq__(self, other): return isinstance(other, TelemetryEventParam) and other.name == self.name and other.value == self.value class TelemetryEvent(DataContract): def __init__(self, eventId=None, providerId=None): self.eventId = eventId self.providerId = providerId self.parameters = DataContractList(TelemetryEventParam) self.file_type = "" # Checking if the particular param name is in the TelemetryEvent. def __contains__(self, param_name): return param_name in [param.name for param in self.parameters] def is_extension_event(self): # Events originating from the agent have "WALinuxAgent" as the Name parameter, or they don't have a Name # parameter, in the case of log and metric events. So, in case the Name parameter exists and it is not # "WALinuxAgent", it is an extension event. for param in self.parameters: if param.name == GuestAgentExtensionEventsSchema.Name: return param.value != AGENT_NAME return False def get_version(self): for param in self.parameters: if param.name == GuestAgentExtensionEventsSchema.Version: return param.value return None WALinuxAgent-2.9.1.1/azurelinuxagent/common/utils/000077500000000000000000000000001446033677600220655ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/azurelinuxagent/common/utils/__init__.py000066400000000000000000000011661446033677600242020ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # WALinuxAgent-2.9.1.1/azurelinuxagent/common/utils/archive.py000066400000000000000000000255071446033677600240710ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the Apache License. import errno import glob import os import re import shutil import zipfile from azurelinuxagent.common import conf from azurelinuxagent.common import logger from azurelinuxagent.common.utils import fileutil, timeutil # pylint: disable=W0105 """ archive.py The module supports the archiving of guest agent state. Guest agent state is flushed whenever there is a incarnation change. The flush is archived periodically (once a day). The process works as follows whenever a new incarnation arrives. 1. Flush - move all state files to a new directory under .../history/timestamp/. 2. Archive - enumerate all directories under .../history/timestamp and create a .zip file named timestamp.zip. Delete the archive directory 3. Purge - glob the list .zip files, sort by timestamp in descending order, keep the first 50 results, and delete the rest. ... is the directory where the agent's state resides, by default this is /var/lib/waagent. The timestamp is an ISO8601 formatted value. """ # pylint: enable=W0105 ARCHIVE_DIRECTORY_NAME = 'history' # TODO: See comment in GoalStateHistory._save_placeholder and remove this code when no longer needed _PLACEHOLDER_FILE_NAME = 'GoalState.1.xml' # END TODO _MAX_ARCHIVED_STATES = 50 _CACHE_PATTERNS = [ # # Note that SharedConfig.xml is not included here; this file is used by other components (Azsec and Singularity/HPC Infiniband) # re.compile(r"^VmSettings\.\d+\.json$"), re.compile(r"^(.*)\.(\d+)\.(agentsManifest)$", re.IGNORECASE), re.compile(r"^(.*)\.(\d+)\.(manifest\.xml)$", re.IGNORECASE), re.compile(r"^(.*)\.(\d+)\.(xml)$", re.IGNORECASE), re.compile(r"^HostingEnvironmentConfig\.xml$", re.IGNORECASE), re.compile(r"^RemoteAccess\.xml$", re.IGNORECASE), re.compile(r"^waagent_status\.\d+\.json$"), ] # # Legacy names # 2018-04-06T08:21:37.142697 # 2018-04-06T08:21:37.142697.zip # 2018-04-06T08:21:37.142697_incarnation_N # 2018-04-06T08:21:37.142697_incarnation_N.zip # 2018-04-06T08:21:37.142697_N-M # 2018-04-06T08:21:37.142697_N-M.zip # # Current names # # 2018-04-06T08-21-37__N-M # 2018-04-06T08-21-37__N-M.zip # _ARCHIVE_BASE_PATTERN = r"\d{4}\-\d{2}\-\d{2}T\d{2}[:-]\d{2}[:-]\d{2}(\.\d+)?((_incarnation)?_+(\d+|status)(-\d+)?)?" _ARCHIVE_PATTERNS_DIRECTORY = re.compile(r'^{0}$'.format(_ARCHIVE_BASE_PATTERN)) _ARCHIVE_PATTERNS_ZIP = re.compile(r'^{0}\.zip$'.format(_ARCHIVE_BASE_PATTERN)) _GOAL_STATE_FILE_NAME = "GoalState.xml" _VM_SETTINGS_FILE_NAME = "VmSettings.json" _CERTIFICATES_FILE_NAME = "Certificates.json" _HOSTING_ENV_FILE_NAME = "HostingEnvironmentConfig.xml" _REMOTE_ACCESS_FILE_NAME = "RemoteAccess.xml" _EXT_CONF_FILE_NAME = "ExtensionsConfig.xml" _MANIFEST_FILE_NAME = "{0}.manifest.xml" AGENT_STATUS_FILE = "waagent_status.json" SHARED_CONF_FILE_NAME = "SharedConfig.xml" # TODO: use @total_ordering once RHEL/CentOS and SLES 11 are EOL. # @total_ordering first appeared in Python 2.7 and 3.2 # If there are more use cases for @total_ordering, I will # consider re-implementing it. class State(object): def __init__(self, path, timestamp): self._path = path self._timestamp = timestamp @property def timestamp(self): return self._timestamp def delete(self): pass def archive(self): pass def __eq__(self, other): return self._timestamp == other.timestamp def __ne__(self, other): return self._timestamp != other.timestamp def __lt__(self, other): return self._timestamp < other.timestamp def __gt__(self, other): return self._timestamp > other.timestamp def __le__(self, other): return self._timestamp <= other.timestamp def __ge__(self, other): return self._timestamp >= other.timestamp class StateZip(State): def delete(self): os.remove(self._path) class StateDirectory(State): def delete(self): shutil.rmtree(self._path) def archive(self): fn_tmp = "{0}.zip.tmp".format(self._path) filename = "{0}.zip".format(self._path) ziph = None try: # contextmanager for zipfile.ZipFile doesn't exist for py2.6, manually closing it ziph = zipfile.ZipFile(fn_tmp, 'w') for current_file in os.listdir(self._path): full_path = os.path.join(self._path, current_file) ziph.write(full_path, current_file, zipfile.ZIP_DEFLATED) finally: if ziph is not None: ziph.close() os.rename(fn_tmp, filename) shutil.rmtree(self._path) class StateArchiver(object): def __init__(self, lib_dir): self._source = os.path.join(lib_dir, ARCHIVE_DIRECTORY_NAME) if not os.path.isdir(self._source): try: fileutil.mkdir(self._source, mode=0o700) except IOError as exception: if exception.errno != errno.EEXIST: logger.warn("{0} : {1}", self._source, exception.strerror) @staticmethod def purge_legacy_goal_state_history(): lib_dir = conf.get_lib_dir() for current_file in os.listdir(lib_dir): # Don't remove the placeholder goal state file. # TODO: See comment in GoalStateHistory._save_placeholder and remove this code when no longer needed if current_file == _PLACEHOLDER_FILE_NAME: continue # END TODO full_path = os.path.join(lib_dir, current_file) for pattern in _CACHE_PATTERNS: match = pattern.match(current_file) if match is not None: try: os.remove(full_path) except Exception as e: logger.warn("Cannot delete legacy history file '{0}': {1}".format(full_path, e)) break def archive(self): states = self._get_archive_states() if len(states) > 0: # Skip the most recent goal state, since it may still be in use for state in states[1:]: state.archive() def _get_archive_states(self): states = [] for current_file in os.listdir(self._source): full_path = os.path.join(self._source, current_file) match = _ARCHIVE_PATTERNS_DIRECTORY.match(current_file) if match is not None: states.append(StateDirectory(full_path, match.group(0))) match = _ARCHIVE_PATTERNS_ZIP.match(current_file) if match is not None: states.append(StateZip(full_path, match.group(0))) states.sort(key=lambda state: os.path.getctime(state._path), reverse=True) return states class GoalStateHistory(object): def __init__(self, time, tag): self._errors = False timestamp = timeutil.create_history_timestamp(time) self._root = os.path.join(conf.get_lib_dir(), ARCHIVE_DIRECTORY_NAME, "{0}__{1}".format(timestamp, tag) if tag is not None else timestamp) GoalStateHistory._purge() @staticmethod def tag_exists(tag): """ Returns True when an item with the given 'tag' already exists in the history directory """ return len(glob.glob(os.path.join(conf.get_lib_dir(), ARCHIVE_DIRECTORY_NAME, "*_{0}".format(tag)))) > 0 def save(self, data, file_name): try: if not os.path.exists(self._root): fileutil.mkdir(self._root, mode=0o700) with open(os.path.join(self._root, file_name), "w") as handle: handle.write(data) except Exception as e: if not self._errors: # report only 1 error per directory self._errors = True logger.warn("Failed to save {0} to the goal state history: {1} [no additional errors saving the goal state will be reported]".format(file_name, e)) _purge_error_count = 0 @staticmethod def _purge(): """ Delete "old" history directories and .zip archives. Old is defined as any directories or files older than the X newest ones. """ try: history_root = os.path.join(conf.get_lib_dir(), ARCHIVE_DIRECTORY_NAME) if not os.path.exists(history_root): return items = [] for current_item in os.listdir(history_root): full_path = os.path.join(history_root, current_item) items.append(full_path) items.sort(key=os.path.getctime, reverse=True) for current_item in items[_MAX_ARCHIVED_STATES:]: if os.path.isfile(current_item): os.remove(current_item) else: shutil.rmtree(current_item) if GoalStateHistory._purge_error_count > 0: GoalStateHistory._purge_error_count = 0 # Log a success message when we are recovering from errors. logger.info("Successfully cleaned up the goal state history directory") except Exception as e: GoalStateHistory._purge_error_count += 1 if GoalStateHistory._purge_error_count < 5: logger.warn("Failed to clean up the goal state history directory: {0}".format(e)) elif GoalStateHistory._purge_error_count == 5: logger.warn("Failed to clean up the goal state history directory [will stop reporting these errors]: {0}".format(e)) @staticmethod def _save_placeholder(): """ Some internal components took a dependency in the legacy GoalState.*.xml file. We create it here while those components are updated to remove the dependency. When removing this code, also remove the check in StateArchiver.purge_legacy_goal_state_history, and the definition of _PLACEHOLDER_FILE_NAME """ try: placeholder = os.path.join(conf.get_lib_dir(), _PLACEHOLDER_FILE_NAME) with open(placeholder, "w") as handle: handle.write("empty placeholder file") except Exception as e: logger.warn("Failed to save placeholder file ({0}): {1}".format(_PLACEHOLDER_FILE_NAME, e)) def save_goal_state(self, text): self.save(text, _GOAL_STATE_FILE_NAME) self._save_placeholder() def save_extensions_config(self, text): self.save(text, _EXT_CONF_FILE_NAME) def save_vm_settings(self, text): self.save(text, _VM_SETTINGS_FILE_NAME) def save_remote_access(self, text): self.save(text, _REMOTE_ACCESS_FILE_NAME) def save_certificates(self, text): self.save(text, _CERTIFICATES_FILE_NAME) def save_hosting_env(self, text): self.save(text, _HOSTING_ENV_FILE_NAME) def save_shared_conf(self, text): self.save(text, SHARED_CONF_FILE_NAME) def save_manifest(self, name, text): self.save(text, _MANIFEST_FILE_NAME.format(name)) WALinuxAgent-2.9.1.1/azurelinuxagent/common/utils/cryptutil.py000066400000000000000000000153021446033677600244770ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import base64 import errno import struct import os.path import subprocess from azurelinuxagent.common.future import ustr, bytebuffer from azurelinuxagent.common.exception import CryptError import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.shellutil as shellutil DECRYPT_SECRET_CMD = "{0} cms -decrypt -inform DER -inkey {1} -in /dev/stdin" class CryptUtil(object): def __init__(self, openssl_cmd): self.openssl_cmd = openssl_cmd def gen_transport_cert(self, prv_file, crt_file): """ Create ssl certificate for https communication with endpoint server. """ cmd = [self.openssl_cmd, "req", "-x509", "-nodes", "-subj", "/CN=LinuxTransport", "-days", "730", "-newkey", "rsa:2048", "-keyout", prv_file, "-out", crt_file] try: shellutil.run_command(cmd) except shellutil.CommandError as cmd_err: msg = "Failed to create {0} and {1} certificates.\n[stdout]\n{2}\n\n[stderr]\n{3}\n"\ .format(prv_file, crt_file, cmd_err.stdout, cmd_err.stderr) logger.error(msg) def get_pubkey_from_prv(self, file_name): if not os.path.exists(file_name): raise IOError(errno.ENOENT, "File not found", file_name) else: cmd = [self.openssl_cmd, "rsa", "-in", file_name, "-pubout"] pub = shellutil.run_command(cmd, log_error=True) return pub def get_pubkey_from_crt(self, file_name): if not os.path.exists(file_name): raise IOError(errno.ENOENT, "File not found", file_name) else: cmd = [self.openssl_cmd, "x509", "-in", file_name, "-pubkey", "-noout"] pub = shellutil.run_command(cmd, log_error=True) return pub def get_thumbprint_from_crt(self, file_name): if not os.path.exists(file_name): raise IOError(errno.ENOENT, "File not found", file_name) else: cmd = [self.openssl_cmd, "x509", "-in", file_name, "-fingerprint", "-noout"] thumbprint = shellutil.run_command(cmd) thumbprint = thumbprint.rstrip().split('=')[1].replace(':', '').upper() return thumbprint def decrypt_p7m(self, p7m_file, trans_prv_file, trans_cert_file, pem_file): if not os.path.exists(p7m_file): raise IOError(errno.ENOENT, "File not found", p7m_file) elif not os.path.exists(trans_prv_file): raise IOError(errno.ENOENT, "File not found", trans_prv_file) else: try: shellutil.run_pipe([ [self.openssl_cmd, "cms", "-decrypt", "-in", p7m_file, "-inkey", trans_prv_file, "-recip", trans_cert_file], [self.openssl_cmd, "pkcs12", "-nodes", "-password", "pass:", "-out", pem_file]]) except shellutil.CommandError as command_error: logger.error("Failed to decrypt {0} (return code: {1})\n[stdout]\n{2}\n[stderr]\n{3}", p7m_file, command_error.returncode, command_error.stdout, command_error.stderr) def crt_to_ssh(self, input_file, output_file): with open(output_file, "ab") as file_out: cmd = ["ssh-keygen", "-i", "-m", "PKCS8", "-f", input_file] try: shellutil.run_command(cmd, stdout=file_out, log_error=True) except shellutil.CommandError: pass # nothing to do; the error is already logged def asn1_to_ssh(self, pubkey): lines = pubkey.split("\n") lines = [x for x in lines if not x.startswith("----")] base64_encoded = "".join(lines) try: #TODO remove pyasn1 dependency from pyasn1.codec.der import decoder as der_decoder der_encoded = base64.b64decode(base64_encoded) der_encoded = der_decoder.decode(der_encoded)[0][1] # pylint: disable=unsubscriptable-object key = der_decoder.decode(self.bits_to_bytes(der_encoded))[0] n=key[0] # pylint: disable=unsubscriptable-object e=key[1] # pylint: disable=unsubscriptable-object keydata = bytearray() keydata.extend(struct.pack('>I', len("ssh-rsa"))) keydata.extend(b"ssh-rsa") keydata.extend(struct.pack('>I', len(self.num_to_bytes(e)))) keydata.extend(self.num_to_bytes(e)) keydata.extend(struct.pack('>I', len(self.num_to_bytes(n)) + 1)) keydata.extend(b"\0") keydata.extend(self.num_to_bytes(n)) keydata_base64 = base64.b64encode(bytebuffer(keydata)) return ustr(b"ssh-rsa " + keydata_base64 + b"\n", encoding='utf-8') except ImportError as e: raise CryptError("Failed to load pyasn1.codec.der") def num_to_bytes(self, num): """ Pack number into bytes. Retun as string. """ result = bytearray() while num: result.append(num & 0xFF) num >>= 8 result.reverse() return result def bits_to_bytes(self, bits): """ Convert an array contains bits, [0,1] to a byte array """ index = 7 byte_array = bytearray() curr = 0 for bit in bits: curr = curr | (bit << index) index = index - 1 if index == -1: byte_array.append(curr) curr = 0 index = 7 return bytes(byte_array) def decrypt_secret(self, encrypted_password, private_key): try: decoded = base64.b64decode(encrypted_password) args = DECRYPT_SECRET_CMD.format(self.openssl_cmd, private_key).split(' ') output = shellutil.run_command(args, input=decoded, stderr=subprocess.STDOUT, encode_input=False, encode_output=False) return output.decode('utf-16') except shellutil.CommandError as command_error: raise subprocess.CalledProcessError(command_error.returncode, "openssl cms -decrypt", output=command_error.stdout) except Exception as e: raise CryptError("Error decoding secret", e) WALinuxAgent-2.9.1.1/azurelinuxagent/common/utils/extensionprocessutil.py000066400000000000000000000151121446033677600267500ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright (c) Microsoft Corporation. All rights reserved. # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import signal import time from azurelinuxagent.common import logger from azurelinuxagent.common.exception import ExtensionErrorCodes, ExtensionOperationError, ExtensionError from azurelinuxagent.common.future import ustr TELEMETRY_MESSAGE_MAX_LEN = 3200 def wait_for_process_completion_or_timeout(process, timeout, cpu_cgroup): """ Utility function that waits for the process to complete within the given time frame. This function will terminate the process if when the given time frame elapses. :param process: Reference to a running process :param timeout: Number of seconds to wait for the process to complete before killing it :return: Two parameters: boolean for if the process timed out and the return code of the process (None if timed out) """ while timeout > 0 and process.poll() is None: time.sleep(1) timeout -= 1 return_code = None throttled_time = 0 if timeout == 0: throttled_time = get_cpu_throttled_time(cpu_cgroup) os.killpg(os.getpgid(process.pid), signal.SIGKILL) else: # process completed or forked; sleep 1 sec to give the child process (if any) a chance to start time.sleep(1) return_code = process.wait() return timeout == 0, return_code, throttled_time def handle_process_completion(process, command, timeout, stdout, stderr, error_code, cpu_cgroup=None): """ Utility function that waits for process completion and retrieves its output (stdout and stderr) if it completed before the timeout period. Otherwise, the process will get killed and an ExtensionError will be raised. In case the return code is non-zero, ExtensionError will be raised. :param process: Reference to a running process :param command: The extension command to run :param timeout: Number of seconds to wait before killing the process :param stdout: Must be a file since we seek on it when parsing the subprocess output :param stderr: Must be a file since we seek on it when parsing the subprocess outputs :param error_code: The error code to set if we raise an ExtensionError :param cpu_cgroup: Reference the cpu cgroup name and path :return: """ # Wait for process completion or timeout timed_out, return_code, throttled_time = wait_for_process_completion_or_timeout(process, timeout, cpu_cgroup) process_output = read_output(stdout, stderr) if timed_out: if cpu_cgroup is not None:# Report CPUThrottledTime when timeout happens raise ExtensionError("Timeout({0});CPUThrottledTime({1}secs): {2}\n{3}".format(timeout, throttled_time, command, process_output), code=ExtensionErrorCodes.PluginHandlerScriptTimedout) raise ExtensionError("Timeout({0}): {1}\n{2}".format(timeout, command, process_output), code=ExtensionErrorCodes.PluginHandlerScriptTimedout) if return_code != 0: raise ExtensionOperationError("Non-zero exit code: {0}, {1}\n{2}".format(return_code, command, process_output), code=error_code, exit_code=return_code) return process_output def read_output(stdout, stderr): """ Read the output of the process sent to stdout and stderr and trim them to the max appropriate length. :param stdout: File containing the stdout of the process :param stderr: File containing the stderr of the process :return: Returns the formatted concatenated stdout and stderr of the process """ try: stdout.seek(0) stderr.seek(0) stdout = ustr(stdout.read(TELEMETRY_MESSAGE_MAX_LEN), encoding='utf-8', errors='backslashreplace') stderr = ustr(stderr.read(TELEMETRY_MESSAGE_MAX_LEN), encoding='utf-8', errors='backslashreplace') return format_stdout_stderr(stdout, stderr) except Exception as e: return format_stdout_stderr("", "Cannot read stdout/stderr: {0}".format(ustr(e))) def format_stdout_stderr(stdout, stderr): """ Format stdout and stderr's output to make it suitable in telemetry. The goal is to maximize the amount of output given the constraints of telemetry. For example, if there is more stderr output than stdout output give more buffer space to stderr. :param str stdout: characters captured from stdout :param str stderr: characters captured from stderr :param int max_len: maximum length of the string to return :return: a string formatted with stdout and stderr that is less than or equal to max_len. :rtype: str """ template = "[stdout]\n{0}\n\n[stderr]\n{1}" # +6 == len("{0}") + len("{1}") max_len = TELEMETRY_MESSAGE_MAX_LEN max_len_each = int((max_len - len(template) + 6) / 2) if max_len_each <= 0: return '' def to_s(captured_stdout, stdout_offset, captured_stderr, stderr_offset): s = template.format(captured_stdout[stdout_offset:], captured_stderr[stderr_offset:]) return s if len(stdout) + len(stderr) < max_len: return to_s(stdout, 0, stderr, 0) elif len(stdout) < max_len_each: bonus = max_len_each - len(stdout) stderr_len = min(max_len_each + bonus, len(stderr)) return to_s(stdout, 0, stderr, -1*stderr_len) elif len(stderr) < max_len_each: bonus = max_len_each - len(stderr) stdout_len = min(max_len_each + bonus, len(stdout)) return to_s(stdout, -1*stdout_len, stderr, 0) else: return to_s(stdout, -1*max_len_each, stderr, -1*max_len_each) def get_cpu_throttled_time(cpu_cgroup): """ return the throttled time for the given cgroup. """ throttled_time = 0 if cpu_cgroup is not None: try: throttled_time = cpu_cgroup.get_cpu_throttled_time(read_previous_throttled_time=False) except Exception as e: logger.warn("Failed to get cpu throttled time for the extension: {0}", ustr(e)) return throttled_time WALinuxAgent-2.9.1.1/azurelinuxagent/common/utils/fileutil.py000066400000000000000000000151601446033677600242570ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # """ File operation util functions """ import errno as errno import glob import os import pwd import re import shutil import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.textutil as textutil from azurelinuxagent.common.future import ustr KNOWN_IOERRORS = [ errno.EIO, # I/O error errno.ENOMEM, # Out of memory errno.ENFILE, # File table overflow errno.EMFILE, # Too many open files errno.ENOSPC, # Out of space errno.ENAMETOOLONG, # Name too long errno.ELOOP, # Too many symbolic links encountered 121 # Remote I/O error (errno.EREMOTEIO -- not present in all Python 2.7+) ] def read_file(filepath, asbin=False, remove_bom=False, encoding='utf-8'): """ Read and return contents of 'filepath'. """ mode = 'rb' with open(filepath, mode) as in_file: data = in_file.read() if data is None: return None if asbin: return data if remove_bom: # remove bom on bytes data before it is converted into string. data = textutil.remove_bom(data) data = ustr(data, encoding=encoding) return data def write_file(filepath, contents, asbin=False, encoding='utf-8', append=False): """ Write 'contents' to 'filepath'. """ mode = "ab" if append else "wb" data = contents if not asbin: data = contents.encode(encoding) with open(filepath, mode) as out_file: out_file.write(data) def append_file(filepath, contents, asbin=False, encoding='utf-8'): """ Append 'contents' to 'filepath'. """ write_file(filepath, contents, asbin=asbin, encoding=encoding, append=True) def base_name(path): head, tail = os.path.split(path) # pylint: disable=W0612 return tail def get_line_startingwith(prefix, filepath): """ Return line from 'filepath' if the line startswith 'prefix' """ for line in read_file(filepath).split('\n'): if line.startswith(prefix): return line return None def mkdir(dirpath, mode=None, owner=None): if not os.path.isdir(dirpath): os.makedirs(dirpath) if mode is not None: chmod(dirpath, mode) if owner is not None: chowner(dirpath, owner) def chowner(path, owner): if not os.path.exists(path): logger.error("Path does not exist: {0}".format(path)) else: owner_info = pwd.getpwnam(owner) os.chown(path, owner_info[2], owner_info[3]) def chmod(path, mode): if not os.path.exists(path): logger.error("Path does not exist: {0}".format(path)) else: os.chmod(path, mode) def rm_files(*args): for paths in args: # find all possible file paths for path in glob.glob(paths): if os.path.isfile(path): os.remove(path) def rm_dirs(*args): """ Remove the contents of each directory """ for p in args: if not os.path.isdir(p): continue for pp in os.listdir(p): path = os.path.join(p, pp) if os.path.isfile(path): os.remove(path) elif os.path.islink(path): os.unlink(path) elif os.path.isdir(path): shutil.rmtree(path) def trim_ext(path, ext): if not ext.startswith("."): ext = "." + ext return path.split(ext)[0] if path.endswith(ext) else path def update_conf_file(path, line_start, val, chk_err=False): conf = [] if not os.path.isfile(path) and chk_err: raise IOError("Can't find config file:{0}".format(path)) conf = read_file(path).split('\n') conf = [x for x in conf if x is not None and len(x) > 0 and not x.startswith(line_start)] conf.append(val) write_file(path, '\n'.join(conf) + '\n') def search_file(target_dir_name, target_file_name): for root, dirs, files in os.walk(target_dir_name): # pylint: disable=W0612 for file_name in files: if file_name == target_file_name: return os.path.join(root, file_name) return None def chmod_tree(path, mode): for root, dirs, files in os.walk(path): # pylint: disable=W0612 for file_name in files: os.chmod(os.path.join(root, file_name), mode) def findstr_in_file(file_path, line_str): """ Return True if the line is in the file; False otherwise. (Trailing whitespace is ignored.) """ try: with open(file_path, 'r') as fh: for line in fh.readlines(): if line_str == line.rstrip(): return True except Exception: # swallow exception pass return False def findre_in_file(file_path, line_re): """ Return match object if found in file. """ try: with open(file_path, 'r') as fh: pattern = re.compile(line_re) for line in fh.readlines(): match = re.search(pattern, line) if match: return match except: # pylint: disable=W0702 pass return None def get_all_files(root_path): """ Find all files under the given root path """ result = [] for root, dirs, files in os.walk(root_path): # pylint: disable=W0612 result.extend([os.path.join(root, file) for file in files]) # pylint: disable=redefined-builtin return result def clean_ioerror(e, paths=None): """ Clean-up possibly bad files and directories after an IO error. The code ignores *all* errors since disk state may be unhealthy. """ if paths is None: paths = [] if isinstance(e, IOError) and e.errno in KNOWN_IOERRORS: for path in paths: if path is None: continue try: if os.path.isdir(path): shutil.rmtree(path, ignore_errors=True) else: os.remove(path) except Exception: # swallow exception pass WALinuxAgent-2.9.1.1/azurelinuxagent/common/utils/flexible_version.py000066400000000000000000000175361446033677600260120ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2020 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # from distutils import version # pylint: disable=no-name-in-module import re class FlexibleVersion(version.Version): """ A more flexible implementation of distutils.version.StrictVersion The implementation allows to specify: - an arbitrary number of version numbers: not only '1.2.3' , but also '1.2.3.4.5' - the separator between version numbers: '1-2-3' is allowed when '-' is specified as separator - a flexible pre-release separator: '1.2.3.alpha1', '1.2.3-alpha1', and '1.2.3alpha1' are considered equivalent - an arbitrary ordering of pre-release tags: 1.1alpha3 < 1.1beta2 < 1.1rc1 < 1.1 when ["alpha", "beta", "rc"] is specified as pre-release tag list Inspiration from this discussion at StackOverflow: http://stackoverflow.com/questions/12255554/sort-versions-in-python """ def __init__(self, vstring=None, sep='.', prerel_tags=('alpha', 'beta', 'rc')): version.Version.__init__(self) if sep is None: sep = '.' if prerel_tags is None: prerel_tags = () self.sep = sep self.prerel_sep = '' self.prerel_tags = tuple(prerel_tags) if prerel_tags is not None else () self._compile_pattern() self.prerelease = None self.version = () if vstring: self._parse(str(vstring)) return _nn_version = 'version' _nn_prerel_sep = 'prerel_sep' _nn_prerel_tag = 'tag' _nn_prerel_num = 'tag_num' _re_prerel_sep = r'(?P<{pn}>{sep})?'.format( pn=_nn_prerel_sep, sep='|'.join(map(re.escape, ('.', '-')))) @property def major(self): return self.version[0] if len(self.version) > 0 else 0 @property def minor(self): return self.version[1] if len(self.version) > 1 else 0 @property def patch(self): return self.version[2] if len(self.version) > 2 else 0 def _parse(self, vstring): m = self.version_re.match(vstring) if not m: raise ValueError("Invalid version number '{0}'".format(vstring)) self.prerelease = None self.version = () self.prerel_sep = m.group(self._nn_prerel_sep) tag = m.group(self._nn_prerel_tag) tag_num = m.group(self._nn_prerel_num) if tag is not None and tag_num is not None: self.prerelease = (tag, int(tag_num) if len(tag_num) else None) self.version = tuple(map(int, self.sep_re.split(m.group(self._nn_version)))) return def __add__(self, increment): version = list(self.version) # pylint: disable=W0621 version[-1] += increment vstring = self._assemble(version, self.sep, self.prerel_sep, self.prerelease) return FlexibleVersion(vstring=vstring, sep=self.sep, prerel_tags=self.prerel_tags) def __sub__(self, decrement): version = list(self.version) # pylint: disable=W0621 if version[-1] <= 0: raise ArithmeticError("Cannot decrement final numeric component of {0} below zero" \ .format(self)) version[-1] -= decrement vstring = self._assemble(version, self.sep, self.prerel_sep, self.prerelease) return FlexibleVersion(vstring=vstring, sep=self.sep, prerel_tags=self.prerel_tags) def __repr__(self): return "{cls} ('{vstring}', '{sep}', {prerel_tags})"\ .format( cls=self.__class__.__name__, vstring=str(self), sep=self.sep, prerel_tags=self.prerel_tags) def __str__(self): return self._assemble(self.version, self.sep, self.prerel_sep, self.prerelease) def __ge__(self, that): return not self.__lt__(that) def __gt__(self, that): return (not self.__lt__(that)) and (not self.__eq__(that)) def __le__(self, that): return (self.__lt__(that)) or (self.__eq__(that)) def __lt__(self, that): this_version, that_version = self._ensure_compatible(that) if this_version != that_version \ or self.prerelease is None and that.prerelease is None: return this_version < that_version if self.prerelease is not None and that.prerelease is None: return True if self.prerelease is None and that.prerelease is not None: return False this_index = self.prerel_tags_set[self.prerelease[0]] that_index = self.prerel_tags_set[that.prerelease[0]] if this_index == that_index: return self.prerelease[1] < that.prerelease[1] return this_index < that_index def __ne__(self, that): return not self.__eq__(that) def __eq__(self, that): this_version, that_version = self._ensure_compatible(that) if this_version != that_version: return False if self.prerelease != that.prerelease: return False return True def matches(self, that): if self.sep != that.sep or len(self.version) > len(that.version): return False for i in range(len(self.version)): if self.version[i] != that.version[i]: return False if self.prerel_tags: return self.prerel_tags == that.prerel_tags return True def _assemble(self, version, sep, prerel_sep, prerelease): # pylint: disable=W0621 s = sep.join(map(str, version)) if prerelease is not None: if prerel_sep is not None: s += prerel_sep s += prerelease[0] if prerelease[1] is not None: s += str(prerelease[1]) return s def _compile_pattern(self): sep, self.sep_re = self._compile_separator(self.sep) if self.prerel_tags: tags = '|'.join(re.escape(tag) for tag in self.prerel_tags) self.prerel_tags_set = dict(zip(self.prerel_tags, range(len(self.prerel_tags)))) release_re = '(?:{prerel_sep}(?P<{tn}>{tags})(?P<{nn}>\d*))?'.format( # pylint: disable=W1401 prerel_sep=self._re_prerel_sep, tags=tags, tn=self._nn_prerel_tag, nn=self._nn_prerel_num) else: release_re = '' version_re = r'^(?P<{vn}>\d+(?:(?:{sep}\d+)*)?){rel}$'.format( vn=self._nn_version, sep=sep, rel=release_re) self.version_re = re.compile(version_re) return def _compile_separator(self, sep): if sep is None: return '', re.compile('') return re.escape(sep), re.compile(re.escape(sep)) def _ensure_compatible(self, that): """ Ensures the instances have the same structure and, if so, returns length compatible version lists (so that x.y.0.0 is equivalent to x.y). """ if self.prerel_tags != that.prerel_tags or self.sep != that.sep: raise ValueError("Unable to compare: versions have different structures") this_version = list(self.version[:]) that_version = list(that.version[:]) while len(this_version) < len(that_version): this_version.append(0) while len(that_version) < len(this_version): that_version.append(0) return this_version, that_version WALinuxAgent-2.9.1.1/azurelinuxagent/common/utils/networkutil.py000066400000000000000000000271061446033677600250340ustar00rootroot00000000000000# # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # from azurelinuxagent.common.future import ustr from azurelinuxagent.common.utils import shellutil from azurelinuxagent.common.utils.shellutil import CommandError class RouteEntry(object): """ Represents a single route. The destination, gateway, and mask members are hex representations of the IPv4 address in network byte order. """ def __init__(self, interface, destination, gateway, mask, flags, metric): self.interface = interface self.destination = destination self.gateway = gateway self.mask = mask self.flags = int(flags, 16) self.metric = int(metric) @staticmethod def _net_hex_to_dotted_quad(value): if len(value) != 8: raise Exception("String to dotted quad conversion must be 8 characters") octets = [] for idx in range(6, -2, -2): octets.append(str(int(value[idx:idx + 2], 16))) return ".".join(octets) def destination_quad(self): return self._net_hex_to_dotted_quad(self.destination) def gateway_quad(self): return self._net_hex_to_dotted_quad(self.gateway) def mask_quad(self): return self._net_hex_to_dotted_quad(self.mask) def to_json(self): f = '{{"Iface": "{0}", "Destination": "{1}", "Gateway": "{2}", "Mask": "{3}", "Flags": "{4:#06x}", "Metric": "{5}"}}' return f.format(self.interface, self.destination_quad(), self.gateway_quad(), self.mask_quad(), self.flags, self.metric) def __str__(self): f = "Iface: {0}\tDestination: {1}\tGateway: {2}\tMask: {3}\tFlags: {4:#06x}\tMetric: {5}" return f.format(self.interface, self.destination_quad(), self.gateway_quad(), self.mask_quad(), self.flags, self.metric) def __repr__(self): return 'RouteEntry("{0}", "{1}", "{2}", "{3}", "{4:#04x}", "{5}")' \ .format(self.interface, self.destination, self.gateway, self.mask, self.flags, self.metric) class NetworkInterfaceCard: def __init__(self, name, link_info): self.name = name self.ipv4 = set() self.ipv6 = set() self.link = link_info def add_ipv4(self, info): self.ipv4.add(info) def add_ipv6(self, info): self.ipv6.add(info) def __eq__(self, other): return self.link == other.link and \ self.ipv4 == other.ipv4 and \ self.ipv6 == other.ipv6 @staticmethod def _json_array(items): return "[{0}]".format(",".join(['"{0}"'.format(x) for x in sorted(items)])) def __str__(self): entries = ['"name": "{0}"'.format(self.name), '"link": "{0}"'.format(self.link)] if len(self.ipv4) > 0: entries.append('"ipv4": {0}'.format(self._json_array(self.ipv4))) if len(self.ipv6) > 0: entries.append('"ipv6": {0}'.format(self._json_array(self.ipv6))) return "{{ {0} }}".format(", ".join(entries)) class FirewallCmdDirectCommands(object): # firewall-cmd --direct --permanent --passthrough ipv4 -t security -A OUTPUT -d 1.2.3.5 -p tcp -m owner --uid-owner 999 -j ACCEPT # success # adds the firewalld rule and returns the status PassThrough = "--passthrough" # firewall-cmd --direct --query-passthrough ipv4 -t security -A OUTPUT -d 1.2.3.5 -p tcp -m owner --uid-owner 9999 -j ACCEPT # yes # firewall-cmd --direct --permanent --query-passthrough ipv4 -t security -A OUTPUT -d 1.2.3.5 -p tcp -m owner --uid-owner 999 -j ACCEPT # no # checks if the firewalld rule is present or not QueryPassThrough = "--query-passthrough" # firewall-cmd --permanent --direct --remove-passthrough ipv4 -t security -A OUTPUT -d 168.63.129.16 -p tcp -m owner --uid-owner 0 -j ACCEPT # success # remove the firewalld rule RemovePassThrough = "--remove-passthrough" class AddFirewallRules(object): """ This class is a utility class which is only meant to orchestrate adding Firewall rules (both iptables and firewalld). This would also be called from a separate utility binary which would be very early up in the boot order of the VM, due to which it would not have access to basic mounts like file-system. Please make sure to not log anything in any function this class. """ # -A adds the rule to the end of the iptable chain APPEND_COMMAND = "-A" # -I inserts the rule at the index specified. If no number specified the rules get added to the top of the chain # iptables -t security -I OUTPUT 1 -d 168.63.129.16 -p tcp --destination-port 53 -j ACCEPT -w and # iptables -t security -I OUTPUT -d 168.63.129.16 -p tcp --destination-port 53 -j ACCEPT -w both adds the rule as the first rule of the chain INSERT_COMMAND = "-I" # -D deletes the specific rule in the iptable chain DELETE_COMMAND = "-D" # -C checks if a specific rule exists CHECK_COMMAND = "-C" @staticmethod def __get_iptables_base_command(wait=""): """ If 'wait' is True, adds the wait option (-w) to the given iptables command line """ if wait != "": return ["iptables", "-w"] return ["iptables"] @staticmethod def __get_firewalld_base_command(command): # For more documentation - https://firewalld.org/documentation/man-pages/firewall-cmd.html return ["firewall-cmd", "--permanent", "--direct", command, "ipv4"] @staticmethod def __get_common_command_params(command, destination): return ["-t", "security", command, "OUTPUT", "-d", destination, "-p", "tcp"] @staticmethod def __get_firewall_base_command(command, destination, firewalld_command="", wait=""): # Firewalld.service fails if we set `-w` in the iptables command, so not adding it at all for firewalld commands if firewalld_command != "": cmd = AddFirewallRules.__get_firewalld_base_command(firewalld_command) else: cmd = AddFirewallRules.__get_iptables_base_command(wait) cmd.extend(AddFirewallRules.__get_common_command_params(command, destination)) return cmd @staticmethod def get_accept_tcp_rule(command, destination, firewalld_command="", wait=""): # This rule allows DNS TCP request to wireserver ip for non root users cmd = AddFirewallRules.__get_firewall_base_command(command, destination, firewalld_command, wait) cmd.extend(['--destination-port', '53', '-j', 'ACCEPT']) return cmd @staticmethod def get_wire_root_accept_rule(command, destination, owner_uid, firewalld_command="", wait=""): cmd = AddFirewallRules.__get_firewall_base_command(command, destination, firewalld_command, wait) cmd.extend(["-m", "owner", "--uid-owner", str(owner_uid), "-j", "ACCEPT"]) return cmd @staticmethod def get_wire_non_root_drop_rule(command, destination, firewalld_command="", wait=""): cmd = AddFirewallRules.__get_firewall_base_command(command, destination, firewalld_command, wait) cmd.extend(["-m", "conntrack", "--ctstate", "INVALID,NEW", "-j", "DROP"]) return cmd @staticmethod def __raise_if_empty(val, name): if val == "": raise Exception("{0} should not be empty".format(name)) @staticmethod def __execute_cmd(cmd): try: shellutil.run_command(cmd) except CommandError as error: msg = "Command {0} failed with exit-code: {1}\nStdout: {2}\nStderr: {3}".format(' '.join(cmd), error.returncode, ustr(error.stdout), ustr(error.stderr)) raise Exception(msg) @staticmethod def __execute_check_command(cmd): # Here we primarily check if an iptable rule exist. True if it exits , false if not try: shellutil.run_command(cmd) return True except CommandError as err: # return code 1 is expected while using the check command. Raise if encounter any other return code if err.returncode != 1: raise return False @staticmethod def verify_iptables_rules_exist(wait, dst_ip, uid): check_cmd_tcp_rule = AddFirewallRules.get_accept_tcp_rule(AddFirewallRules.CHECK_COMMAND, dst_ip, wait=wait) check_cmd_accept_rule = AddFirewallRules.get_wire_root_accept_rule(AddFirewallRules.CHECK_COMMAND, dst_ip, uid, wait=wait) check_cmd_drop_rule = AddFirewallRules.get_wire_non_root_drop_rule(AddFirewallRules.CHECK_COMMAND, dst_ip, wait=wait) return AddFirewallRules.__execute_check_command(check_cmd_tcp_rule) and AddFirewallRules.__execute_check_command(check_cmd_accept_rule) \ and AddFirewallRules.__execute_check_command(check_cmd_drop_rule) @staticmethod def __execute_firewall_commands(dst_ip, uid, command=APPEND_COMMAND, firewalld_command="", wait=""): # The order in which the below rules are added matters for the ip table rules to work as expected AddFirewallRules.__raise_if_empty(dst_ip, "Destination IP") AddFirewallRules.__raise_if_empty(uid, "User ID") accept_tcp_rule = AddFirewallRules.get_accept_tcp_rule(command, dst_ip, firewalld_command=firewalld_command, wait=wait) AddFirewallRules.__execute_cmd(accept_tcp_rule) accept_cmd = AddFirewallRules.get_wire_root_accept_rule(command, dst_ip, uid, firewalld_command=firewalld_command, wait=wait) AddFirewallRules.__execute_cmd(accept_cmd) drop_cmd = AddFirewallRules.get_wire_non_root_drop_rule(command, dst_ip, firewalld_command=firewalld_command, wait=wait) AddFirewallRules.__execute_cmd(drop_cmd) @staticmethod def add_iptables_rules(wait, dst_ip, uid): AddFirewallRules.__execute_firewall_commands(dst_ip, uid, command=AddFirewallRules.APPEND_COMMAND, wait=wait) @staticmethod def add_firewalld_rules(dst_ip, uid): # Firewalld.service fails if we set `-w` in the iptables command, so not adding it at all for firewalld commands # Firewalld.service with the "--permanent --passthrough" parameter ensures that a firewall rule is set only once even if command is executed multiple times AddFirewallRules.__execute_firewall_commands(dst_ip, uid, firewalld_command=FirewallCmdDirectCommands.PassThrough) @staticmethod def check_firewalld_rule_applied(dst_ip, uid): AddFirewallRules.__execute_firewall_commands(dst_ip, uid, firewalld_command=FirewallCmdDirectCommands.QueryPassThrough) @staticmethod def remove_firewalld_rules(dst_ip, uid): AddFirewallRules.__execute_firewall_commands(dst_ip, uid, firewalld_command=FirewallCmdDirectCommands.RemovePassThrough) WALinuxAgent-2.9.1.1/azurelinuxagent/common/utils/restutil.py000066400000000000000000000537151446033677600243250ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import re import threading import time import socket import struct import azurelinuxagent.common.conf as conf import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.textutil as textutil from azurelinuxagent.common.exception import HttpError, ResourceGoneError, InvalidContainerError from azurelinuxagent.common.future import httpclient, urlparse, ustr from azurelinuxagent.common.version import PY_VERSION_MAJOR, AGENT_NAME, GOAL_STATE_AGENT_VERSION SECURE_WARNING_EMITTED = False DEFAULT_RETRIES = 6 DELAY_IN_SECONDS = 1 THROTTLE_RETRIES = 25 THROTTLE_DELAY_IN_SECONDS = 1 REDACTED_TEXT = "" SAS_TOKEN_RETRIEVAL_REGEX = re.compile(r'^(https?://[a-zA-Z0-9.].*sig=)([a-zA-Z0-9%-]*)(.*)$') RETRY_CODES = [ httpclient.RESET_CONTENT, httpclient.PARTIAL_CONTENT, httpclient.FORBIDDEN, httpclient.INTERNAL_SERVER_ERROR, httpclient.NOT_IMPLEMENTED, httpclient.BAD_GATEWAY, httpclient.SERVICE_UNAVAILABLE, httpclient.GATEWAY_TIMEOUT, httpclient.INSUFFICIENT_STORAGE, 429, # Request Rate Limit Exceeded ] # # Currently the HostGAPlugin has an issue its cache that may produce a BAD_REQUEST failure for valid URIs when using the extensionArtifact API. # Add this status to the retryable codes, but use it only when requesting downloads via the HostGAPlugin. The retry logic in the download code # would give enough time to the HGAP to refresh its cache. Once the fix to address that issue is deployed, consider removing the use of # HGAP_GET_EXTENSION_ARTIFACT_RETRY_CODES. # HGAP_GET_EXTENSION_ARTIFACT_RETRY_CODES = RETRY_CODES[:] # make a copy of RETRY_CODES HGAP_GET_EXTENSION_ARTIFACT_RETRY_CODES.append(httpclient.BAD_REQUEST) RESOURCE_GONE_CODES = [ httpclient.GONE ] OK_CODES = [ httpclient.OK, httpclient.CREATED, httpclient.ACCEPTED ] NOT_MODIFIED_CODES = [ httpclient.NOT_MODIFIED ] HOSTPLUGIN_UPSTREAM_FAILURE_CODES = [ 502 ] THROTTLE_CODES = [ httpclient.FORBIDDEN, httpclient.SERVICE_UNAVAILABLE, 429, # Request Rate Limit Exceeded ] RETRY_EXCEPTIONS = [ httpclient.NotConnected, httpclient.IncompleteRead, httpclient.ImproperConnectionState, httpclient.BadStatusLine ] # http://www.gnu.org/software/wget/manual/html_node/Proxies.html HTTP_PROXY_ENV = "http_proxy" HTTPS_PROXY_ENV = "https_proxy" NO_PROXY_ENV = "no_proxy" HTTP_USER_AGENT = "{0}/{1}".format(AGENT_NAME, GOAL_STATE_AGENT_VERSION) HTTP_USER_AGENT_HEALTH = "{0}+health".format(HTTP_USER_AGENT) INVALID_CONTAINER_CONFIGURATION = "InvalidContainerConfiguration" REQUEST_ROLE_CONFIG_FILE_NOT_FOUND = "RequestRoleConfigFileNotFound" KNOWN_WIRESERVER_IP = '168.63.129.16' HOST_PLUGIN_PORT = 32526 class IOErrorCounter(object): _lock = threading.RLock() _protocol_endpoint = KNOWN_WIRESERVER_IP _counts = {"hostplugin":0, "protocol":0, "other":0} @staticmethod def increment(host=None, port=None): with IOErrorCounter._lock: if host == IOErrorCounter._protocol_endpoint: if port == HOST_PLUGIN_PORT: IOErrorCounter._counts["hostplugin"] += 1 else: IOErrorCounter._counts["protocol"] += 1 else: IOErrorCounter._counts["other"] += 1 @staticmethod def get_and_reset(): with IOErrorCounter._lock: counts = IOErrorCounter._counts.copy() IOErrorCounter.reset() return counts @staticmethod def reset(): with IOErrorCounter._lock: IOErrorCounter._counts = {"hostplugin":0, "protocol":0, "other":0} @staticmethod def set_protocol_endpoint(endpoint=KNOWN_WIRESERVER_IP): IOErrorCounter._protocol_endpoint = endpoint def _compute_delay(retry_attempt=1, delay=DELAY_IN_SECONDS): fib = (1, 1) for _ in range(retry_attempt): fib = (fib[1], fib[0]+fib[1]) return delay*fib[1] def _is_retry_status(status, retry_codes=None): if retry_codes is None: retry_codes = RETRY_CODES return status in retry_codes def _is_retry_exception(e): return len([x for x in RETRY_EXCEPTIONS if isinstance(e, x)]) > 0 def _is_throttle_status(status): return status in THROTTLE_CODES def _parse_url(url): """ Parse URL to get the components of the URL broken down to host, port :rtype: string, int, bool, string """ o = urlparse(url) rel_uri = o.path if o.fragment: rel_uri = "{0}#{1}".format(rel_uri, o.fragment) if o.query: rel_uri = "{0}?{1}".format(rel_uri, o.query) secure = False if o.scheme.lower() == "https": secure = True return o.hostname, o.port, secure, rel_uri def _trim_url_parameters(url): """ Parse URL and return scheme://hostname:port/path """ o = urlparse(url) if o.hostname: if o.port: return "{0}://{1}:{2}{3}".format(o.scheme, o.hostname, o.port, o.path) else: return "{0}://{1}{2}".format(o.scheme, o.hostname, o.path) return url def is_valid_cidr(string_network): """ Very simple check of the cidr format in no_proxy variable. :rtype: bool """ if string_network.count('/') == 1: try: mask = int(string_network.split('/')[1]) except ValueError: return False if mask < 1 or mask > 32: return False try: socket.inet_aton(string_network.split('/')[0]) except socket.error: return False else: return False return True def dotted_netmask(mask): """Converts mask from /xx format to xxx.xxx.xxx.xxx Example: if mask is 24 function returns 255.255.255.0 :rtype: str """ bits = 0xffffffff ^ (1 << 32 - mask) - 1 return socket.inet_ntoa(struct.pack('>I', bits)) def address_in_network(ip, net): """This function allows you to check if an IP belongs to a network subnet Example: returns True if ip = 192.168.1.1 and net = 192.168.1.0/24 returns False if ip = 192.168.1.1 and net = 192.168.100.0/24 :rtype: bool """ ipaddr = struct.unpack('=L', socket.inet_aton(ip))[0] netaddr, bits = net.split('/') netmask = struct.unpack('=L', socket.inet_aton(dotted_netmask(int(bits))))[0] network = struct.unpack('=L', socket.inet_aton(netaddr))[0] & netmask return (ipaddr & netmask) == (network & netmask) def is_ipv4_address(string_ip): """ :rtype: bool """ try: socket.inet_aton(string_ip) except socket.error: return False return True def get_no_proxy(): no_proxy = os.environ.get(NO_PROXY_ENV) or os.environ.get(NO_PROXY_ENV.upper()) if no_proxy: no_proxy = [host for host in no_proxy.replace(' ', '').split(',') if host] # no_proxy in the proxies argument takes precedence return no_proxy def bypass_proxy(host): no_proxy = get_no_proxy() if no_proxy: if is_ipv4_address(host): for proxy_ip in no_proxy: if is_valid_cidr(proxy_ip): if address_in_network(host, proxy_ip): return True elif host == proxy_ip: # If no_proxy ip was defined in plain IP notation instead of cidr notation & # matches the IP of the index return True else: for proxy_domain in no_proxy: if host.lower().endswith(proxy_domain.lower()): # The URL does match something in no_proxy, so we don't want # to apply the proxies on this URL. return True return False def _get_http_proxy(secure=False): # Prefer the configuration settings over environment variables host = conf.get_httpproxy_host() port = None if not host is None: port = conf.get_httpproxy_port() else: http_proxy_env = HTTPS_PROXY_ENV if secure else HTTP_PROXY_ENV http_proxy_url = None for v in [http_proxy_env, http_proxy_env.upper()]: if v in os.environ: http_proxy_url = os.environ[v] break if not http_proxy_url is None: host, port, _, _ = _parse_url(http_proxy_url) return host, port def redact_sas_tokens_in_urls(url): return SAS_TOKEN_RETRIEVAL_REGEX.sub(r"\1" + REDACTED_TEXT + r"\3", url) def _http_request(method, host, rel_uri, timeout, port=None, data=None, secure=False, headers=None, proxy_host=None, proxy_port=None, redact_data=False): headers = {} if headers is None else headers headers['Connection'] = 'close' use_proxy = proxy_host is not None and proxy_port is not None if port is None: port = 443 if secure else 80 if 'User-Agent' not in headers: headers['User-Agent'] = HTTP_USER_AGENT if use_proxy: conn_host, conn_port = proxy_host, proxy_port scheme = "https" if secure else "http" url = "{0}://{1}:{2}{3}".format(scheme, host, port, rel_uri) else: conn_host, conn_port = host, port url = rel_uri if secure: conn = httpclient.HTTPSConnection(conn_host, conn_port, timeout=timeout) if use_proxy: conn.set_tunnel(host, port) else: conn = httpclient.HTTPConnection(conn_host, conn_port, timeout=timeout) payload = data if redact_data: payload = "[REDACTED]" # Logger requires the msg to be a ustr to log properly, ensuring that the data string that we log is always ustr logger.verbose("HTTP connection [{0}] [{1}] [{2}] [{3}]", method, redact_sas_tokens_in_urls(url), textutil.str_to_encoded_ustr(payload), headers) conn.request(method=method, url=url, body=data, headers=headers) return conn.getresponse() def http_request(method, url, data, timeout, headers=None, use_proxy=False, max_retry=None, retry_codes=None, retry_delay=DELAY_IN_SECONDS, redact_data=False, return_raw_response=False): """ NOTE: This method provides some logic to handle errors in the HTTP request, including checking the HTTP status of the response and handling some exceptions. If return_raw_response is set to True all the error handling will be skipped and the method will return the actual HTTP response and bubble up any exceptions while issuing the request. Also note that if return_raw_response is True no retries will be done. """ if max_retry is None: max_retry = DEFAULT_RETRIES if retry_codes is None: retry_codes = RETRY_CODES global SECURE_WARNING_EMITTED # pylint: disable=W0603 host, port, secure, rel_uri = _parse_url(url) # Use the HTTP(S) proxy proxy_host, proxy_port = (None, None) if use_proxy and not bypass_proxy(host): proxy_host, proxy_port = _get_http_proxy(secure=secure) if proxy_host or proxy_port: logger.verbose("HTTP proxy: [{0}:{1}]", proxy_host, proxy_port) # If httplib module is not built with ssl support, # fallback to HTTP if allowed if secure and not hasattr(httpclient, "HTTPSConnection"): if not conf.get_allow_http(): raise HttpError("HTTPS is unavailable and required") secure = False if not SECURE_WARNING_EMITTED: logger.warn("Python does not include SSL support") SECURE_WARNING_EMITTED = True # If httplib module doesn't support HTTPS tunnelling, # fallback to HTTP if allowed if secure and \ proxy_host is not None and \ proxy_port is not None \ and not hasattr(httpclient.HTTPSConnection, "set_tunnel"): if not conf.get_allow_http(): raise HttpError("HTTPS tunnelling is unavailable and required") secure = False if not SECURE_WARNING_EMITTED: logger.warn("Python does not support HTTPS tunnelling") SECURE_WARNING_EMITTED = True msg = '' attempt = 0 delay = 0 was_throttled = False while attempt < max_retry: if attempt > 0: # Compute the request delay # -- Use a fixed delay if the server ever rate-throttles the request # (with a safe, minimum number of retry attempts) # -- Otherwise, compute a delay that is the product of the next # item in the Fibonacci series and the initial delay value delay = THROTTLE_DELAY_IN_SECONDS \ if was_throttled \ else _compute_delay(retry_attempt=attempt, delay=retry_delay) logger.verbose("[HTTP Retry] " "Attempt {0} of {1} will delay {2} seconds: {3}", attempt+1, max_retry, delay, msg) time.sleep(delay) attempt += 1 try: resp = _http_request(method, host, rel_uri, timeout, port=port, data=data, secure=secure, headers=headers, proxy_host=proxy_host, proxy_port=proxy_port, redact_data=redact_data) logger.verbose("[HTTP Response] Status Code {0}", resp.status) if return_raw_response: # skip all error handling return resp if request_failed(resp): if _is_retry_status(resp.status, retry_codes=retry_codes): msg = '[HTTP Retry] {0} {1} -- Status Code {2}'.format(method, url, resp.status) # Note if throttled and ensure a safe, minimum number of # retry attempts if _is_throttle_status(resp.status): was_throttled = True max_retry = max(max_retry, THROTTLE_RETRIES) continue # If we got a 410 (resource gone) for any reason, raise an exception. The caller will handle it by # forcing a goal state refresh and retrying the call. if resp.status in RESOURCE_GONE_CODES: response_error = read_response_error(resp) raise ResourceGoneError(response_error) # If we got a 400 (bad request) because the container id is invalid, it could indicate a stale goal # state. The caller will handle this exception by forcing a goal state refresh and retrying the call. if resp.status == httpclient.BAD_REQUEST: response_error = read_response_error(resp) if INVALID_CONTAINER_CONFIGURATION in response_error: raise InvalidContainerError(response_error) return resp except httpclient.HTTPException as e: if return_raw_response: # skip all error handling raise clean_url = _trim_url_parameters(url) msg = '[HTTP Failed] {0} {1} -- HttpException {2}'.format(method, clean_url, e) if _is_retry_exception(e): continue break except IOError as e: if return_raw_response: # skip all error handling raise IOErrorCounter.increment(host=host, port=port) clean_url = _trim_url_parameters(url) msg = '[HTTP Failed] {0} {1} -- IOError {2}'.format(method, clean_url, e) continue raise HttpError("{0} -- {1} attempts made".format(msg, attempt)) def http_get(url, headers=None, use_proxy=False, max_retry=None, retry_codes=None, retry_delay=DELAY_IN_SECONDS, return_raw_response=False, timeout=10): """ NOTE: This method provides some logic to handle errors in the HTTP request, including checking the HTTP status of the response and handling some exceptions. If return_raw_response is set to True all the error handling will be skipped and the method will return the actual HTTP response and bubble up any exceptions while issuing the request. Also note that if return_raw_response is True no retries will be done. """ if max_retry is None: max_retry = DEFAULT_RETRIES if retry_codes is None: retry_codes = RETRY_CODES return http_request("GET", url, None, timeout, headers=headers, use_proxy=use_proxy, max_retry=max_retry, retry_codes=retry_codes, retry_delay=retry_delay, return_raw_response=return_raw_response) def http_head(url, headers=None, use_proxy=False, max_retry=None, retry_codes=None, retry_delay=DELAY_IN_SECONDS, timeout=10): if max_retry is None: max_retry = DEFAULT_RETRIES if retry_codes is None: retry_codes = RETRY_CODES return http_request("HEAD", url, None, timeout, headers=headers, use_proxy=use_proxy, max_retry=max_retry, retry_codes=retry_codes, retry_delay=retry_delay) def http_post(url, data, headers=None, use_proxy=False, max_retry=None, retry_codes=None, retry_delay=DELAY_IN_SECONDS, timeout=10): if max_retry is None: max_retry = DEFAULT_RETRIES if retry_codes is None: retry_codes = RETRY_CODES return http_request("POST", url, data, timeout, headers=headers, use_proxy=use_proxy, max_retry=max_retry, retry_codes=retry_codes, retry_delay=retry_delay) def http_put(url, data, headers=None, use_proxy=False, max_retry=None, retry_codes=None, retry_delay=DELAY_IN_SECONDS, redact_data=False, timeout=10): if max_retry is None: max_retry = DEFAULT_RETRIES if retry_codes is None: retry_codes = RETRY_CODES return http_request("PUT", url, data, timeout, headers=headers, use_proxy=use_proxy, max_retry=max_retry, retry_codes=retry_codes, retry_delay=retry_delay, redact_data=redact_data) def http_delete(url, headers=None, use_proxy=False, max_retry=None, retry_codes=None, retry_delay=DELAY_IN_SECONDS, timeout=10): if max_retry is None: max_retry = DEFAULT_RETRIES if retry_codes is None: retry_codes = RETRY_CODES return http_request("DELETE", url, None, timeout, headers=headers, use_proxy=use_proxy, max_retry=max_retry, retry_codes=retry_codes, retry_delay=retry_delay) def request_failed(resp, ok_codes=None): if ok_codes is None: ok_codes = OK_CODES return not request_succeeded(resp, ok_codes=ok_codes) def request_succeeded(resp, ok_codes=None): if ok_codes is None: ok_codes = OK_CODES return resp is not None and resp.status in ok_codes def request_not_modified(resp): return resp is not None and resp.status in NOT_MODIFIED_CODES def request_failed_at_hostplugin(resp, upstream_failure_codes=None): """ Host plugin will return 502 for any upstream issue, so a failure is any 5xx except 502 """ if upstream_failure_codes is None: upstream_failure_codes = HOSTPLUGIN_UPSTREAM_FAILURE_CODES return resp is not None and resp.status >= 500 and resp.status not in upstream_failure_codes def read_response_error(resp): result = '' if resp is not None: try: result = "[HTTP Failed] [{0}: {1}] {2}".format( resp.status, resp.reason, resp.read()) # this result string is passed upstream to several methods # which do a raise HttpError() or a format() of some kind; # as a result it cannot have any unicode characters if PY_VERSION_MAJOR < 3: result = ustr(result, encoding='ascii', errors='ignore') else: result = result\ .encode(encoding='ascii', errors='ignore')\ .decode(encoding='ascii', errors='ignore') result = textutil.replace_non_ascii(result) except Exception as e: logger.warn(textutil.format_exception(e)) return result WALinuxAgent-2.9.1.1/azurelinuxagent/common/utils/shellutil.py000066400000000000000000000352471446033677600244570ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import subprocess import tempfile import threading import azurelinuxagent.common.logger as logger from azurelinuxagent.common.future import ustr if not hasattr(subprocess, 'check_output'): def check_output(*popenargs, **kwargs): r"""Backport from subprocess module from python 2.7""" if 'stdout' in kwargs: raise ValueError('stdout argument not allowed, ' 'it will be overridden.') process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs) output, unused_err = process.communicate() retcode = process.poll() if retcode: cmd = kwargs.get("args") if cmd is None: cmd = popenargs[0] raise subprocess.CalledProcessError(retcode, cmd, output=output) return output # Exception classes used by this module. class CalledProcessError(Exception): def __init__(self, returncode, cmd, output=None): # pylint: disable=W0231 self.returncode = returncode self.cmd = cmd self.output = output def __str__(self): return ("Command '{0}' returned non-zero exit status {1}" "").format(self.cmd, self.returncode) subprocess.check_output = check_output subprocess.CalledProcessError = CalledProcessError # pylint: disable=W0105 """ Shell command util functions """ # pylint: enable=W0105 def has_command(cmd): """ Return True if the given command is on the path """ return not run(cmd, False) def run(cmd, chk_err=True, expected_errors=None): """ Note: Deprecating in favour of `azurelinuxagent.common.utils.shellutil.run_command` function. Calls run_get_output on 'cmd', returning only the return code. If chk_err=True then errors will be reported in the log. If chk_err=False then errors will be suppressed from the log. """ if expected_errors is None: expected_errors = [] retcode, out = run_get_output(cmd, chk_err=chk_err, expected_errors=expected_errors) # pylint: disable=W0612 return retcode def run_get_output(cmd, chk_err=True, log_cmd=True, expected_errors=None): """ Wrapper for subprocess.check_output. Execute 'cmd'. Returns return code and STDOUT, trapping expected exceptions. Reports exceptions to Error if chk_err parameter is True For new callers, consider using run_command instead as it separates stdout from stderr, returns only stdout on success, logs both outputs and return code on error and raises an exception. """ if expected_errors is None: expected_errors = [] if log_cmd: logger.verbose(u"Command: [{0}]", cmd) try: process = _popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True) output, _ = process.communicate() _on_command_completed(process.pid) output = __encode_command_output(output) if process.returncode != 0: if chk_err: msg = u"Command: [{0}], " \ u"return code: [{1}], " \ u"result: [{2}]".format(cmd, process.returncode, output) if process.returncode in expected_errors: logger.info(msg) else: logger.error(msg) return process.returncode, output except Exception as exception: if chk_err: logger.error(u"Command [{0}] raised unexpected exception: [{1}]" .format(cmd, ustr(exception))) return -1, ustr(exception) return 0, output def __format_command(command): """ Formats the command taken by run_command/run_pipe. Examples: > __format_command("sort") 'sort' > __format_command(["sort", "-u"]) 'sort -u' > __format_command([["sort"], ["unique", "-n"]]) 'sort | unique -n' """ if isinstance(command, list): if command and isinstance(command[0], list): return " | ".join([" ".join(cmd) for cmd in command]) return " ".join(command) return command def __encode_command_output(output): """ Encodes the stdout/stderr returned by subprocess.communicate() """ return ustr(output if output is not None else b'', encoding='utf-8', errors="backslashreplace") class CommandError(Exception): """ Exception raised by run_command/run_pipe when the command returns an error """ @staticmethod def _get_message(command, return_code, stderr): command_name = command[0] if isinstance(command, list) and len(command) > 0 else command return "'{0}' failed: {1} ({2})".format(command_name, return_code, stderr.rstrip()) def __init__(self, command, return_code, stdout, stderr): super(Exception, self).__init__(CommandError._get_message(command, return_code, stderr)) # pylint: disable=E1003 self.command = command self.returncode = return_code self.stdout = stdout self.stderr = stderr def __run_command(command_action, command, log_error, encode_output): """ Executes the given command_action and returns its stdout. The command_action is a function that executes a command/pipe and returns its exit code, stdout, and stderr. If there are any errors executing the command it raises a RunCommandException; if 'log_error' is True, it also logs details about the error. If encode_output is True the stdout is returned as a string, otherwise it is returned as a bytes object. """ try: return_code, stdout, stderr = command_action() if encode_output: stdout = __encode_command_output(stdout) stderr = __encode_command_output(stderr) if return_code != 0: if log_error: logger.error( "Command: [{0}], return code: [{1}], stdout: [{2}] stderr: [{3}]", __format_command(command), return_code, stdout, stderr) raise CommandError(command=__format_command(command), return_code=return_code, stdout=stdout, stderr=stderr) return stdout except CommandError: raise except Exception as exception: if log_error: logger.error(u"Command [{0}] raised unexpected exception: [{1}]", __format_command(command), ustr(exception)) raise # W0622: Redefining built-in 'input' -- disabled: the parameter name mimics subprocess.communicate() def run_command(command, input=None, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, log_error=False, encode_input=True, encode_output=True, track_process=True): # pylint:disable=W0622 """ Executes the given command and returns its stdout. If there are any errors executing the command it raises a RunCommandException; if 'log_error' is True, it also logs details about the error. If encode_output is True the stdout is returned as a string, otherwise it is returned as a bytes object. If track_process is False the command is not added to list of running commands This function is a thin wrapper around Popen/communicate in the subprocess module: * The 'input' parameter corresponds to the same parameter in communicate * The 'stdin' parameter corresponds to the same parameters in Popen * Only one of 'input' and 'stdin' can be specified * The 'stdout' and 'stderr' parameters correspond to the same parameters in Popen, except that they default to subprocess.PIPE instead of None * If the output of the command is redirected using the 'stdout' or 'stderr' parameters (i.e. if the value for these parameters is anything other than the default (subprocess.PIPE)), then the corresponding values returned by this function or the CommandError exception will be empty strings. Note: This is the preferred method to execute shell commands over `azurelinuxagent.common.utils.shellutil.run` function. """ if input is not None and stdin is not None: raise ValueError("The input and stdin arguments are mutually exclusive") def command_action(): popen_stdin = communicate_input = None if input is not None: popen_stdin = subprocess.PIPE communicate_input = input.encode() if encode_input and isinstance(input, str) else input # communicate() needs an array of bytes if stdin is not None: popen_stdin = stdin communicate_input = None if track_process: process = _popen(command, stdin=popen_stdin, stdout=stdout, stderr=stderr, shell=False) else: process = subprocess.Popen(command, stdin=popen_stdin, stdout=stdout, stderr=stderr, shell=False) command_stdout, command_stderr = process.communicate(input=communicate_input) if track_process: _on_command_completed(process.pid) return process.returncode, command_stdout, command_stderr return __run_command(command_action=command_action, command=command, log_error=log_error, encode_output=encode_output) def run_pipe(pipe, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, log_error=False, encode_output=True): """ Executes the given commands as a pipe and returns its stdout as a string. The pipe is a list of commands, which in turn are a list of strings, e.g. [["sort"], ["uniq", "-n"]] represents 'sort | unique -n' If there are any errors executing the command it raises a RunCommandException; if 'log_error' is True, it also logs details about the error. If encode_output is True the stdout is returned as a string, otherwise it is returned as a bytes object. This function is a thin wrapper around Popen/communicate in the subprocess module: * The 'stdin' parameter is used as input for the first command in the pipe * The 'stdout', and 'stderr' can be used to redirect the output of the pipe * If the output of the pipe is redirected using the 'stdout' or 'stderr' parameters (i.e. if the value for these parameters is anything other than the default (subprocess.PIPE)), then the corresponding values returned by this function or the CommandError exception will be empty strings. """ if len(pipe) < 2: raise ValueError("The pipe must consist of at least 2 commands") def command_action(): stderr_file = None try: popen_stdin = stdin # If stderr is subprocess.PIPE each call to Popen would create a new pipe. We want to collect the stderr of all the # commands in the pipe so we replace stderr with a temporary file that we read once the pipe completes. if stderr == subprocess.PIPE: stderr_file = tempfile.TemporaryFile() popen_stderr = stderr_file else: popen_stderr = stderr processes = [] i = 0 while i < len(pipe) - 1: processes.append(_popen(pipe[i], stdin=popen_stdin, stdout=subprocess.PIPE, stderr=popen_stderr)) popen_stdin = processes[i].stdout i += 1 processes.append(_popen(pipe[i], stdin=popen_stdin, stdout=stdout, stderr=popen_stderr)) i = 0 while i < len(processes) - 1: processes[i].stdout.close() # see https://docs.python.org/2/library/subprocess.html#replacing-shell-pipeline i += 1 pipe_stdout, pipe_stderr = processes[i].communicate() for proc in processes: _on_command_completed(proc.pid) if stderr_file is not None: stderr_file.seek(0) pipe_stderr = stderr_file.read() return processes[i].returncode, pipe_stdout, pipe_stderr finally: if stderr_file is not None: stderr_file.close() return __run_command(command_action=command_action, command=pipe, log_error=log_error, encode_output=encode_output) def quote(word_list): """ Quote a list or tuple of strings for Unix Shell as words, using the byte-literal single quote. The resulting string is safe for use with ``shell=True`` in ``subprocess``, and in ``os.system``. ``assert shlex.split(ShellQuote(wordList)) == wordList``. See POSIX.1:2013 Vol 3, Chap 2, Sec 2.2.2: http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_02_02 """ if not isinstance(word_list, (tuple, list)): word_list = (word_list,) return " ".join(list("'{0}'".format(s.replace("'", "'\\''")) for s in word_list)) # # The run_command/run_pipe/run/run_get_output functions maintain a list of the commands that they are currently executing. # # _running_commands = [] _running_commands_lock = threading.RLock() PARENT_PROCESS_NAME = "AZURE_GUEST_AGENT_PARENT_PROCESS_NAME" AZURE_GUEST_AGENT = "AZURE_GUEST_AGENT" def _popen(*args, **kwargs): with _running_commands_lock: # Add the environment variables env = {} if 'env' in kwargs: env.update(kwargs['env']) else: env.update(os.environ) # Set the marker before process start env[PARENT_PROCESS_NAME] = AZURE_GUEST_AGENT kwargs['env'] = env process = subprocess.Popen(*args, **kwargs) _running_commands.append(process.pid) return process def _on_command_completed(pid): with _running_commands_lock: _running_commands.remove(pid) def get_running_commands(): """ Returns the commands started by run/run_get_output/run_command/run_pipe that are currently running. NOTE: This function is not synchronized with process completion, so the returned array may include processes that have already completed. Also, keep in mind that by the time this function returns additional processes may have started or completed. """ with _running_commands_lock: return _running_commands[:] # return a copy, since the call may originate on another thread WALinuxAgent-2.9.1.1/azurelinuxagent/common/utils/textutil.py000066400000000000000000000303501446033677600243220ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ import base64 import crypt import hashlib import random import re import string import struct import sys import traceback import xml.dom.minidom as minidom import zlib from azurelinuxagent.common.future import ustr def parse_doc(xml_text): """ Parse xml document from string """ # The minidom lib has some issue with unicode in python2. # Encode the string into utf-8 first xml_text = xml_text.encode('utf-8') return minidom.parseString(xml_text) def findall(root, tag, namespace=None): """ Get all nodes by tag and namespace under Node root. """ if root is None: return [] if namespace is None: return root.getElementsByTagName(tag) else: return root.getElementsByTagNameNS(namespace, tag) def find(root, tag, namespace=None): """ Get first node by tag and namespace under Node root. """ nodes = findall(root, tag, namespace=namespace) if nodes is not None and len(nodes) >= 1: return nodes[0] else: return None def gettext(node): """ Get node text """ if node is None: return None for child in node.childNodes: if child.nodeType == child.TEXT_NODE: return child.data return None def findtext(root, tag, namespace=None): """ Get text of node by tag and namespace under Node root. """ node = find(root, tag, namespace=namespace) return gettext(node) def getattrib(node, attr_name): """ Get attribute of xml node """ if node is not None: return node.getAttribute(attr_name) else: return None def unpack(buf, offset, value_range): """ Unpack bytes into python values. """ result = 0 for i in value_range: result = (result << 8) | str_to_ord(buf[offset + i]) return result def unpack_little_endian(buf, offset, length): """ Unpack little endian bytes into python values. """ return unpack(buf, offset, list(range(length - 1, -1, -1))) def unpack_big_endian(buf, offset, length): """ Unpack big endian bytes into python values. """ return unpack(buf, offset, list(range(0, length))) def hex_dump3(buf, offset, length): """ Dump range of buf in formatted hex. """ return ''.join(['%02X' % str_to_ord(char) for char in buf[offset:offset + length]]) def hex_dump2(buf): """ Dump buf in formatted hex. """ return hex_dump3(buf, 0, len(buf)) def is_in_range(a, low, high): """ Return True if 'a' in 'low' <= a <= 'high' """ return low <= a <= high def is_printable(ch): """ Return True if character is displayable. """ return (is_in_range(ch, str_to_ord('A'), str_to_ord('Z')) or is_in_range(ch, str_to_ord('a'), str_to_ord('z')) or is_in_range(ch, str_to_ord('0'), str_to_ord('9'))) def hex_dump(buffer, size): # pylint: disable=redefined-builtin """ Return Hex formated dump of a 'buffer' of 'size'. """ if size < 0: size = len(buffer) result = "" for i in range(0, size): if (i % 16) == 0: result += "%06X: " % i byte = buffer[i] if type(byte) == str: byte = ord(byte.decode('latin1')) result += "%02X " % byte if (i & 15) == 7: result += " " if ((i + 1) % 16) == 0 or (i + 1) == size: j = i while ((j + 1) % 16) != 0: result += " " if (j & 7) == 7: result += " " j += 1 result += " " for j in range(i - (i % 16), i + 1): byte = buffer[j] if type(byte) == str: byte = str_to_ord(byte.decode('latin1')) k = '.' if is_printable(byte): k = chr(byte) result += k if (i + 1) != size: result += "\n" return result def str_to_ord(a): """ Allows indexing into a string or an array of integers transparently. Generic utility function. """ if type(a) == type(b'') or type(a) == type(u''): a = ord(a) return a def compare_bytes(a, b, start, length): for offset in range(start, start + length): if str_to_ord(a[offset]) != str_to_ord(b[offset]): return False return True def int_to_ip4_addr(a): """ Build DHCP request string. """ return "%u.%u.%u.%u" % ((a >> 24) & 0xFF, (a >> 16) & 0xFF, (a >> 8) & 0xFF, (a) & 0xFF) def hexstr_to_bytearray(a): """ Return hex string packed into a binary struct. """ b = b"" for c in range(0, len(a) // 2): b += struct.pack("B", int(a[c * 2:c * 2 + 2], 16)) return b def set_ssh_config(config, name, val): found = False no_match = -1 match_start = no_match for i in range(0, len(config)): if config[i].startswith(name) and match_start == no_match: config[i] = "{0} {1}".format(name, val) found = True elif config[i].lower().startswith("match"): if config[i].lower().startswith("match all"): # outside match block match_start = no_match elif match_start == no_match: # inside match block match_start = i if not found: if match_start != no_match: i = match_start config.insert(i, "{0} {1}".format(name, val)) return config def set_ini_config(config, name, val): notfound = True nameEqual = name + '=' length = len(config) text = "{0}=\"{1}\"".format(name, val) for i in reversed(range(0, length)): if config[i].startswith(nameEqual): config[i] = text notfound = False break if notfound: config.insert(length - 1, text) def replace_non_ascii(incoming, replace_char=''): outgoing = '' if incoming is not None: for c in incoming: if str_to_ord(c) > 128: outgoing += replace_char else: outgoing += c return outgoing def remove_bom(c): """ bom is comprised of a sequence of three chars,0xef, 0xbb, 0xbf, in case of utf-8. """ if not is_str_none_or_whitespace(c) and \ len(c) > 2 and \ str_to_ord(c[0]) > 128 and \ str_to_ord(c[1]) > 128 and \ str_to_ord(c[2]) > 128: c = c[3:] return c def gen_password_hash(password, crypt_id, salt_len): collection = string.ascii_letters + string.digits salt = ''.join(random.choice(collection) for _ in range(salt_len)) salt = "${0}${1}".format(crypt_id, salt) if sys.version_info[0] == 2: # if python 2.*, encode to type 'str' to prevent Unicode Encode Error from crypt.crypt password = password.encode('utf-8') return crypt.crypt(password, salt) def get_bytes_from_pem(pem_str): base64_bytes = "" for line in pem_str.split('\n'): if "----" not in line: base64_bytes += line return base64_bytes def compress(s): """ Compress a string, and return the base64 encoded result of the compression. This method returns a string instead of a byte array. It is expected that this method is called to compress smallish strings, not to compress the contents of a file. The output of this method is suitable for embedding in log statements. """ from azurelinuxagent.common.version import PY_VERSION_MAJOR if PY_VERSION_MAJOR > 2: return base64.b64encode(zlib.compress(bytes(s, 'utf-8'))).decode('utf-8') return base64.b64encode(zlib.compress(s)) def b64encode(s): from azurelinuxagent.common.version import PY_VERSION_MAJOR if PY_VERSION_MAJOR > 2: return base64.b64encode(bytes(s, 'utf-8')).decode('utf-8') return base64.b64encode(s) def b64decode(s): from azurelinuxagent.common.version import PY_VERSION_MAJOR if PY_VERSION_MAJOR > 2: return base64.b64decode(s).decode('utf-8') return base64.b64decode(s) def safe_shlex_split(s): import shlex from azurelinuxagent.common.version import PY_VERSION if PY_VERSION[:2] == (2, 6): return shlex.split(s.encode('utf-8')) return shlex.split(s) def swap_hexstring(s, width=2): r = len(s) % width if r != 0: s = ('0' * (width - (len(s) % width))) + s return ''.join(reversed( re.findall( r'[a-f0-9]{{{0}}}'.format(width), s, re.IGNORECASE))) def parse_json(json_str): """ Parse json string and return a resulting dictionary """ # trim null and whitespaces result = None if not is_str_empty(json_str): import json result = json.loads(json_str.rstrip(' \t\r\n\0')) return result def is_str_none_or_whitespace(s): return s is None or len(s) == 0 or s.isspace() def is_str_empty(s): return is_str_none_or_whitespace(s) or is_str_none_or_whitespace(s.rstrip(' \t\r\n\0')) def hash_strings(string_list): """ Compute a cryptographic hash of a list of strings :param string_list: The strings to be hashed :return: The cryptographic hash (digest) of the strings in the order provided """ sha1_hash = hashlib.sha1() for item in string_list: sha1_hash.update(item.encode()) return sha1_hash.digest() def format_memory_value(unit, value): units = {'bytes': 1, 'kilobytes': 1024, 'megabytes': 1024*1024, 'gigabytes': 1024*1024*1024} if unit not in units: raise ValueError("Unit must be one of {0}".format(units.keys())) try: value = float(value) except TypeError: raise TypeError('Value must be convertible to a float') return int(value * units[unit]) def str_to_encoded_ustr(s, encoding='utf-8'): """ This function takes the string and converts it into the corresponding encoded ustr if its not already a ustr. The encoding is utf-8 by default if not specified. Note: ustr() is a unicode object for Py2 and a str object for Py3. :param s: The string to convert to ustr :param encoding: Encoding to use. Utf-8 by default :return: Returns the corresponding ustr string. Returns None if input is None. """ # TODO: Import at the top of the file instead of a local import (using local import here to avoid cyclic dependency) from azurelinuxagent.common.version import PY_VERSION_MAJOR if s is None or type(s) is ustr: # If its already a ustr/None then return as is return s if PY_VERSION_MAJOR > 2: try: # For py3+, str() is unicode by default if isinstance(s, bytes): # str.encode() returns bytes which should be decoded to get the str. return s.decode(encoding) else: # If its not encoded, just return the string return ustr(s) except Exception: # If some issues in decoding, just return the string return ustr(s) # For Py2, explicitly convert the string to unicode with the specified encoding return ustr(s, encoding=encoding) def format_exception(exception): # Function to format exception message e = None if sys.version_info[0] == 2: _, e, tb = sys.exc_info() else: tb = exception.__traceback__ msg = ustr(exception) + "\n" if tb is None or (sys.version_info[0] == 2 and e != exception): msg += "[Traceback not available]" else: msg += ''.join(traceback.format_exception(type(exception), value=exception, tb=tb)) return msg WALinuxAgent-2.9.1.1/azurelinuxagent/common/utils/timeutil.py000066400000000000000000000022741446033677600243000ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the Apache License. import datetime def create_timestamp(dt=None): """ Returns a string with the given datetime in iso format. If no datetime is given as parameter, it uses datetime.utcnow(). """ if dt is None: dt = datetime.datetime.utcnow() return dt.isoformat() def create_history_timestamp(dt=None): """ Returns a string with the given datetime formatted as a timestamp for the agent's history folder """ if dt is None: dt = datetime.datetime.utcnow() return dt.strftime('%Y-%m-%dT%H-%M-%S') def datetime_to_ticks(dt): """ Converts 'dt', a datetime, to the number of ticks (1 tick == 1/10000000 sec) since datetime.min (0001-01-01 00:00:00). Note that the resolution of a datetime goes only to microseconds. """ return int(10 ** 7 * total_seconds(dt - datetime.datetime.min)) def total_seconds(dt): """ Compute the total_seconds for timedelta 'td'. Used instead timedelta.total_seconds() because 2.6 does not implement total_seconds. """ return ((24.0 * 60 * 60 * dt.days + dt.seconds) * 10 ** 6 + dt.microseconds) / 10 ** 6 WALinuxAgent-2.9.1.1/azurelinuxagent/common/version.py000066400000000000000000000252701446033677600227720ustar00rootroot00000000000000# Copyright 2019 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import re import platform import sys import azurelinuxagent.common.conf as conf import azurelinuxagent.common.utils.shellutil as shellutil from azurelinuxagent.common.utils.flexible_version import FlexibleVersion from azurelinuxagent.common.future import ustr, get_linux_distribution __DAEMON_VERSION_ENV_VARIABLE = '_AZURE_GUEST_AGENT_DAEMON_VERSION_' """ The daemon process sets this variable's value to the daemon's version number. The variable is set only on versions >= 2.2.53 """ def set_daemon_version(version): """ Sets the value of the _AZURE_GUEST_AGENT_DAEMON_VERSION_ environment variable. The given 'version' can be a FlexibleVersion or a string that can be parsed into a FlexibleVersion """ flexible_version = version if isinstance(version, FlexibleVersion) else FlexibleVersion(version) os.environ[__DAEMON_VERSION_ENV_VARIABLE] = ustr(flexible_version) def get_daemon_version(): """ Retrieves the value of the _AZURE_GUEST_AGENT_DAEMON_VERSION_ environment variable. The value indicates the version of the daemon that started the current agent process or, if the current process is the daemon, the version of the current process. If the variable is not set (because the agent is < 2.2.53, or the process was not started by the daemon and the process is not the daemon itself) the function returns "0.0.0.0" """ if __DAEMON_VERSION_ENV_VARIABLE in os.environ: return FlexibleVersion(os.environ[__DAEMON_VERSION_ENV_VARIABLE]) return FlexibleVersion("0.0.0.0") def get_f5_platform(): """ Add this workaround for detecting F5 products because BIG-IP/IQ/etc do not show their version info in the /etc/product-version location. Instead, the version and product information is contained in the /VERSION file. """ result = [None, None, None, None] f5_version = re.compile("^Version: (\d+\.\d+\.\d+)") # pylint: disable=W1401 f5_product = re.compile("^Product: ([\w-]+)") # pylint: disable=W1401 with open('/VERSION', 'r') as fh: content = fh.readlines() for line in content: version_matches = f5_version.match(line) product_matches = f5_product.match(line) if version_matches: result[1] = version_matches.group(1) elif product_matches: result[3] = product_matches.group(1) if result[3] == "BIG-IP": result[0] = "bigip" result[2] = "bigip" elif result[3] == "BIG-IQ": result[0] = "bigiq" result[2] = "bigiq" elif result[3] == "iWorkflow": result[0] = "iworkflow" result[2] = "iworkflow" return result def get_checkpoint_platform(): take = build = release = "" full_name = open("/etc/cp-release").read().strip() with open("/etc/cloud-version") as f: for line in f: k, _, v = line.partition(": ") v = v.strip() if k == "release": release = v elif k == "take": take = v elif k == "build": build = v return ["gaia", take + "." + build, release, full_name] def get_distro(): if 'FreeBSD' in platform.system(): release = re.sub('\-.*\Z', '', ustr(platform.release())) # pylint: disable=W1401 osinfo = ['freebsd', release, '', 'freebsd'] elif 'OpenBSD' in platform.system(): release = re.sub('\-.*\Z', '', ustr(platform.release())) # pylint: disable=W1401 osinfo = ['openbsd', release, '', 'openbsd'] elif 'Linux' in platform.system(): osinfo = get_linux_distribution(0, 'alpine') elif 'NS-BSD' in platform.system(): release = re.sub('\-.*\Z', '', ustr(platform.release())) # pylint: disable=W1401 osinfo = ['nsbsd', release, '', 'nsbsd'] else: try: # dist() removed in Python 3.8 osinfo = list(platform.dist()) + [''] # pylint: disable=W1505,E1101 except Exception: osinfo = ['UNKNOWN', 'FFFF', '', ''] # The platform.py lib has issue with detecting oracle linux distribution. # Merge the following patch provided by oracle as a temporary fix. if os.path.exists("/etc/oracle-release"): osinfo[2] = "oracle" osinfo[3] = "Oracle Linux" if os.path.exists("/etc/euleros-release"): osinfo[0] = "euleros" if os.path.exists("/etc/UnionTech-release"): osinfo[0] = "uos" if os.path.exists("/etc/mariner-release"): osinfo[0] = "mariner" # The platform.py lib has issue with detecting BIG-IP linux distribution. # Merge the following patch provided by F5. if os.path.exists("/shared/vadc"): osinfo = get_f5_platform() if os.path.exists("/etc/cp-release"): osinfo = get_checkpoint_platform() if os.path.exists("/home/guestshell/azure"): osinfo = ['iosxe', 'csr1000v', '', 'Cisco IOSXE Linux'] if os.path.exists("/etc/photon-release"): osinfo[0] = "photonos" # Remove trailing whitespace and quote in distro name osinfo[0] = osinfo[0].strip('"').strip(' ').lower() return osinfo COMMAND_ABSENT = ustr("Absent") COMMAND_FAILED = ustr("Failed") def get_lis_version(): """ This uses the Linux kernel's 'modinfo' command to retrieve the "version" field for the "hv_vmbus" kernel module (the LIS drivers). This is the documented method to retrieve the LIS module version. Every Linux guest on Hyper-V will have this driver, but it may not be installed as a module (it could instead be built into the kernel). In that case, this will return "Absent" instead of the version, indicating the driver version can be deduced from the kernel version. It will only return "Failed" in the presence of an exception. This function is used to generate telemetry for the version of the LIS drivers installed on the VM. The function and associated telemetry can be removed after a few releases. """ try: modinfo_output = shellutil.run_command(["modinfo", "-F", "version", "hv_vmbus"]) if modinfo_output: return modinfo_output # If the system doesn't have LIS drivers, 'modinfo' will # return nothing on stdout, which will cause 'run_command' # to return an empty string. return COMMAND_ABSENT except Exception: # Ignore almost every possible exception because this is in a # critical code path. Unfortunately the logger isn't already # imported in this module or we'd log this too. return COMMAND_FAILED def has_logrotate(): try: logrotate_version = shellutil.run_command(["logrotate", "--version"]).split("\n")[0] return logrotate_version except shellutil.CommandError: # A non-zero return code means that logrotate isn't present on # the system; --version shouldn't fail otherwise. return COMMAND_ABSENT except Exception: return COMMAND_FAILED AGENT_NAME = "WALinuxAgent" AGENT_LONG_NAME = "Azure Linux Agent" # # IMPORTANT: Please be sure that the version is always 9.9.9.9 on the develop branch. Automation requires this, otherwise # DCR may test the wrong agent version. # # When doing a release, be sure to use the actual agent version. Current agent version: 2.4.0.0 # AGENT_VERSION = '2.9.1.1' AGENT_LONG_VERSION = "{0}-{1}".format(AGENT_NAME, AGENT_VERSION) AGENT_DESCRIPTION = """ The Azure Linux Agent supports the provisioning and running of Linux VMs in the Azure cloud. This package should be installed on Linux disk images that are built to run in the Azure environment. """ AGENT_DIR_GLOB = "{0}-*".format(AGENT_NAME) AGENT_PKG_GLOB = "{0}-*.zip".format(AGENT_NAME) AGENT_PATTERN = "{0}-(.*)".format(AGENT_NAME) AGENT_NAME_PATTERN = re.compile(AGENT_PATTERN) AGENT_PKG_PATTERN = re.compile(AGENT_PATTERN+"\.zip") # pylint: disable=W1401 AGENT_DIR_PATTERN = re.compile(".*/{0}".format(AGENT_PATTERN)) # The execution mode of the VM - IAAS or PAAS. Linux VMs are only executed in IAAS mode. AGENT_EXECUTION_MODE = "IAAS" EXT_HANDLER_PATTERN = b".*/WALinuxAgent-(\d+.\d+.\d+[.\d+]*).*-run-exthandlers" # pylint: disable=W1401 EXT_HANDLER_REGEX = re.compile(EXT_HANDLER_PATTERN) __distro__ = get_distro() DISTRO_NAME = __distro__[0] DISTRO_VERSION = __distro__[1] DISTRO_CODE_NAME = __distro__[2] DISTRO_FULL_NAME = __distro__[3] PY_VERSION = sys.version_info PY_VERSION_MAJOR = sys.version_info[0] PY_VERSION_MINOR = sys.version_info[1] PY_VERSION_MICRO = sys.version_info[2] # Set the CURRENT_AGENT and CURRENT_VERSION to match the agent directory name # - This ensures the agent will "see itself" using the same name and version # as the code that downloads agents. def set_current_agent(): path = os.getcwd() lib_dir = conf.get_lib_dir() if lib_dir[-1] != os.path.sep: lib_dir += os.path.sep agent = path[len(lib_dir):].split(os.path.sep)[0] match = AGENT_NAME_PATTERN.match(agent) if match: version = match.group(1) else: agent = AGENT_LONG_VERSION version = AGENT_VERSION return agent, FlexibleVersion(version) def is_agent_package(path): path = os.path.basename(path) return not re.match(AGENT_PKG_PATTERN, path) is None def is_agent_path(path): path = os.path.basename(path) return not re.match(AGENT_NAME_PATTERN, path) is None CURRENT_AGENT, CURRENT_VERSION = set_current_agent() def set_goal_state_agent(): agent = None if os.path.isdir("/proc"): pids = [pid for pid in os.listdir('/proc') if pid.isdigit()] else: pids = [] for pid in pids: try: pname = open(os.path.join('/proc', pid, 'cmdline'), 'rb').read() match = EXT_HANDLER_REGEX.match(pname) if match: agent = match.group(1) if PY_VERSION_MAJOR > 2: agent = agent.decode('UTF-8') break except IOError: continue if agent is None: agent = CURRENT_VERSION return agent GOAL_STATE_AGENT_VERSION = set_goal_state_agent() WALinuxAgent-2.9.1.1/azurelinuxagent/daemon/000077500000000000000000000000001446033677600207005ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/azurelinuxagent/daemon/__init__.py000066400000000000000000000012611446033677600230110ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # from azurelinuxagent.daemon.main import get_daemon_handler WALinuxAgent-2.9.1.1/azurelinuxagent/daemon/main.py000066400000000000000000000161151446033677600222020ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import sys import time import azurelinuxagent.common.conf as conf import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.fileutil as fileutil from azurelinuxagent.common.event import add_event, WALAEventOperation, initialize_event_logger_vminfo_common_parameters from azurelinuxagent.common.future import ustr from azurelinuxagent.common.osutil import get_osutil from azurelinuxagent.common.protocol.goal_state import GoalState, GoalStateProperties from azurelinuxagent.common.protocol.util import get_protocol_util from azurelinuxagent.common.rdma import setup_rdma_device from azurelinuxagent.common.utils import textutil from azurelinuxagent.common.version import AGENT_NAME, AGENT_LONG_NAME, \ AGENT_VERSION, \ DISTRO_NAME, DISTRO_VERSION, PY_VERSION_MAJOR, PY_VERSION_MINOR, \ PY_VERSION_MICRO from azurelinuxagent.daemon.resourcedisk import get_resourcedisk_handler from azurelinuxagent.daemon.scvmm import get_scvmm_handler from azurelinuxagent.ga.update import get_update_handler from azurelinuxagent.pa.provision import get_provision_handler from azurelinuxagent.pa.rdma import get_rdma_handler OPENSSL_FIPS_ENVIRONMENT = "OPENSSL_FIPS" def get_daemon_handler(): return DaemonHandler() class DaemonHandler(object): """ Main thread of daemon. It will invoke other threads to do actual work """ def __init__(self): self.running = True self.osutil = get_osutil() def run(self, child_args=None): # # The Container ID in telemetry events is retrieved from the goal state. We can fetch the goal state # only after protocol detection, which is done during provisioning. # # Be aware that telemetry events emitted before that will not include the Container ID. # logger.info("{0} Version: {1}", AGENT_LONG_NAME, AGENT_VERSION) logger.info("OS: {0} {1}", DISTRO_NAME, DISTRO_VERSION) logger.info("Python: {0}.{1}.{2}", PY_VERSION_MAJOR, PY_VERSION_MINOR, PY_VERSION_MICRO) self.check_pid() self.initialize_environment() # If FIPS is enabled, set the OpenSSL environment variable # Note: # -- Subprocesses inherit the current environment if conf.get_fips_enabled(): os.environ[OPENSSL_FIPS_ENVIRONMENT] = '1' while self.running: try: self.daemon(child_args) except Exception as e: # pylint: disable=W0612 err_msg = textutil.format_exception(e) add_event(name=AGENT_NAME, is_success=False, message=ustr(err_msg), op=WALAEventOperation.UnhandledError) logger.warn("Daemon ended with exception -- Sleep 15 seconds and restart daemon") time.sleep(15) def check_pid(self): """Check whether daemon is already running""" pid = None pid_file = conf.get_agent_pid_file_path() if os.path.isfile(pid_file): pid = fileutil.read_file(pid_file) if self.osutil.check_pid_alive(pid): logger.info("Daemon is already running: {0}", pid) sys.exit(0) fileutil.write_file(pid_file, ustr(os.getpid())) def sleep_if_disabled(self): agent_disabled_file_path = conf.get_disable_agent_file_path() if os.path.exists(agent_disabled_file_path): import threading logger.warn("Disabling the guest agent by sleeping forever; " "to re-enable, remove {0} and restart" .format(agent_disabled_file_path)) self.running = False disable_event = threading.Event() disable_event.wait() def initialize_environment(self): # Create lib dir if not os.path.isdir(conf.get_lib_dir()): fileutil.mkdir(conf.get_lib_dir(), mode=0o700) os.chdir(conf.get_lib_dir()) def _initialize_telemetry(self): protocol = self.protocol_util.get_protocol() initialize_event_logger_vminfo_common_parameters(protocol) def daemon(self, child_args=None): logger.info("Run daemon") self.protocol_util = get_protocol_util() # pylint: disable=W0201 self.scvmm_handler = get_scvmm_handler() # pylint: disable=W0201 self.resourcedisk_handler = get_resourcedisk_handler() # pylint: disable=W0201 self.rdma_handler = get_rdma_handler() # pylint: disable=W0201 self.provision_handler = get_provision_handler() # pylint: disable=W0201 self.update_handler = get_update_handler() # pylint: disable=W0201 if conf.get_detect_scvmm_env(): self.scvmm_handler.run() if conf.get_resourcedisk_format(): self.resourcedisk_handler.run() # Always redetermine the protocol start (e.g., wireserver vs. # on-premise) since a VHD can move between environments self.protocol_util.clear_protocol() self.provision_handler.run() # Once we have the protocol, complete initialization of the telemetry fields # that require the goal state and IMDS self._initialize_telemetry() # Enable RDMA, continue in errors if conf.enable_rdma(): nd_version = self.rdma_handler.get_rdma_version() self.rdma_handler.install_driver_if_needed() logger.info("RDMA capabilities are enabled in configuration") try: # Ensure the most recent SharedConfig is available # - Changes to RDMA state may not increment the goal state # incarnation number. A forced update ensures the most # current values. protocol = self.protocol_util.get_protocol() goal_state = GoalState(protocol, goal_state_properties=GoalStateProperties.SharedConfig) setup_rdma_device(nd_version, goal_state.shared_conf) except Exception as e: logger.error("Error setting up rdma device: %s" % e) else: logger.info("RDMA capabilities are not enabled, skipping") self.sleep_if_disabled() # Disable output to /dev/console once provisioning has completed if logger.console_output_enabled(): logger.info("End of log to /dev/console. The agent will now check for updates and then will process extensions.") logger.disable_console_output() while self.running: self.update_handler.run_latest(child_args=child_args) WALinuxAgent-2.9.1.1/azurelinuxagent/daemon/resourcedisk/000077500000000000000000000000001446033677600234025ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/azurelinuxagent/daemon/resourcedisk/__init__.py000066400000000000000000000013471446033677600255200ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # from azurelinuxagent.daemon.resourcedisk.factory import get_resourcedisk_handler WALinuxAgent-2.9.1.1/azurelinuxagent/daemon/resourcedisk/default.py000066400000000000000000000354131446033677600254060ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import re import stat import sys import threading from time import sleep import azurelinuxagent.common.logger as logger from azurelinuxagent.common.future import ustr import azurelinuxagent.common.conf as conf from azurelinuxagent.common.event import add_event, WALAEventOperation import azurelinuxagent.common.utils.fileutil as fileutil import azurelinuxagent.common.utils.shellutil as shellutil from azurelinuxagent.common.exception import ResourceDiskError from azurelinuxagent.common.osutil import get_osutil from azurelinuxagent.common.version import AGENT_NAME DATALOSS_WARNING_FILE_NAME = "DATALOSS_WARNING_README.txt" DATA_LOSS_WARNING = """\ WARNING: THIS IS A TEMPORARY DISK. Any data stored on this drive is SUBJECT TO LOSS and THERE IS NO WAY TO RECOVER IT. Please do not use this disk for storing any personal or application data. For additional details to please refer to the MSDN documentation at : http://msdn.microsoft.com/en-us/library/windowsazure/jj672979.aspx """ class ResourceDiskHandler(object): def __init__(self): self.osutil = get_osutil() self.fs = conf.get_resourcedisk_filesystem() def start_activate_resource_disk(self): disk_thread = threading.Thread(target=self.run) disk_thread.start() def run(self): mount_point = None if conf.get_resourcedisk_format(): mount_point = self.activate_resource_disk() if mount_point is not None and \ conf.get_resourcedisk_enable_swap(): self.enable_swap(mount_point) def activate_resource_disk(self): logger.info("Activate resource disk") try: mount_point = conf.get_resourcedisk_mountpoint() mount_point = self.mount_resource_disk(mount_point) warning_file = os.path.join(mount_point, DATALOSS_WARNING_FILE_NAME) try: fileutil.write_file(warning_file, DATA_LOSS_WARNING) except IOError as e: logger.warn("Failed to write data loss warning:{0}", e) return mount_point except ResourceDiskError as e: logger.error("Failed to mount resource disk {0}", e) add_event(name=AGENT_NAME, is_success=False, message=ustr(e), op=WALAEventOperation.ActivateResourceDisk) return None def enable_swap(self, mount_point): logger.info("Enable swap") try: size_mb = conf.get_resourcedisk_swap_size_mb() self.create_swap_space(mount_point, size_mb) except ResourceDiskError as e: logger.error("Failed to enable swap {0}", e) def reread_partition_table(self, device): if shellutil.run("sfdisk -R {0}".format(device), chk_err=False): shellutil.run("blockdev --rereadpt {0}".format(device), chk_err=False) def mount_resource_disk(self, mount_point): device = self.osutil.device_for_ide_port(1) if device is None: raise ResourceDiskError("unable to detect disk topology") device = "/dev/{0}".format(device) partition = device + "1" mount_list = shellutil.run_get_output("mount")[1] existing = self.osutil.get_mount_point(mount_list, device) if existing: logger.info("Resource disk [{0}] is already mounted [{1}]", partition, existing) return existing try: fileutil.mkdir(mount_point, mode=0o755) except OSError as ose: msg = "Failed to create mount point " \ "directory [{0}]: {1}".format(mount_point, ose) logger.error(msg) raise ResourceDiskError(msg=msg, inner=ose) logger.info("Examining partition table") ret = shellutil.run_get_output("parted {0} print".format(device)) if ret[0]: raise ResourceDiskError("Could not determine partition info for " "{0}: {1}".format(device, ret[1])) force_option = 'F' if self.fs == 'xfs': force_option = 'f' mkfs_string = "mkfs.{0} -{2} {1}".format( self.fs, partition, force_option) if "gpt" in ret[1]: logger.info("GPT detected, finding partitions") parts = [x for x in ret[1].split("\n") if re.match(r"^\s*[0-9]+", x)] logger.info("Found {0} GPT partition(s).", len(parts)) if len(parts) > 1: logger.info("Removing old GPT partitions") for i in range(1, len(parts) + 1): logger.info("Remove partition {0}", i) shellutil.run("parted {0} rm {1}".format(device, i)) logger.info("Creating new GPT partition") shellutil.run( "parted {0} mkpart primary 0% 100%".format(device)) logger.info("Format partition [{0}]", mkfs_string) shellutil.run(mkfs_string) else: logger.info("GPT not detected, determining filesystem") ret = self.change_partition_type( suppress_message=True, option_str="{0} 1 -n".format(device)) ptype = ret[1].strip() if ptype == "7" and self.fs != "ntfs": logger.info("The partition is formatted with ntfs, updating " "partition type to 83") self.change_partition_type( suppress_message=False, option_str="{0} 1 83".format(device)) self.reread_partition_table(device) logger.info("Format partition [{0}]", mkfs_string) shellutil.run(mkfs_string) else: logger.info("The partition type is {0}", ptype) mount_options = conf.get_resourcedisk_mountoptions() mount_string = self.get_mount_string(mount_options, partition, mount_point) attempts = 5 while not os.path.exists(partition) and attempts > 0: logger.info("Waiting for partition [{0}], {1} attempts remaining", partition, attempts) sleep(5) attempts -= 1 if not os.path.exists(partition): raise ResourceDiskError( "Partition was not created [{0}]".format(partition)) logger.info("Mount resource disk [{0}]", mount_string) ret, output = shellutil.run_get_output(mount_string, chk_err=False) # if the exit code is 32, then the resource disk can be already mounted if ret == 32 and output.find("is already mounted") != -1: logger.warn("Could not mount resource disk: {0}", output) elif ret != 0: # Some kernels seem to issue an async partition re-read after a # 'parted' command invocation. This causes mount to fail if the # partition re-read is not complete by the time mount is # attempted. Seen in CentOS 7.2. Force a sequential re-read of # the partition and try mounting. logger.warn("Failed to mount resource disk. " "Retry mounting after re-reading partition info.") self.reread_partition_table(device) ret, output = shellutil.run_get_output(mount_string, chk_err=False) if ret: logger.warn("Failed to mount resource disk. " "Attempting to format and retry mount. [{0}]", output) shellutil.run(mkfs_string) ret, output = shellutil.run_get_output(mount_string) if ret: raise ResourceDiskError("Could not mount {0} " "after syncing partition table: " "[{1}] {2}".format(partition, ret, output)) logger.info("Resource disk {0} is mounted at {1} with {2}", device, mount_point, self.fs) return mount_point def change_partition_type(self, suppress_message, option_str): """ use sfdisk to change partition type. First try with --part-type; if fails, fall back to -c """ option_to_use = '--part-type' command = "sfdisk {0} {1} {2}".format( option_to_use, '-f' if suppress_message else '', option_str) err_code, output = shellutil.run_get_output( command, chk_err=False, log_cmd=True) # fall back to -c if err_code != 0: logger.info( "sfdisk with --part-type failed [{0}], retrying with -c", err_code) option_to_use = '-c' command = "sfdisk {0} {1} {2}".format( option_to_use, '-f' if suppress_message else '', option_str) err_code, output = shellutil.run_get_output(command, log_cmd=True) if err_code == 0: logger.info('{0} succeeded', command) else: logger.error('{0} failed [{1}: {2}]', command, err_code, output) return err_code, output def get_mount_string(self, mount_options, partition, mount_point): if mount_options is not None: return 'mount -t {0} -o {1} {2} {3}'.format( self.fs, mount_options, partition, mount_point ) else: return 'mount -t {0} {1} {2}'.format( self.fs, partition, mount_point ) @staticmethod def check_existing_swap_file(swapfile, swaplist, size): if swapfile in swaplist and os.path.isfile( swapfile) and os.path.getsize(swapfile) == size: logger.info("Swap already enabled") # restrict access to owner (remove all access from group, others) swapfile_mode = os.stat(swapfile).st_mode if swapfile_mode & (stat.S_IRWXG | stat.S_IRWXO): swapfile_mode = swapfile_mode & ~(stat.S_IRWXG | stat.S_IRWXO) logger.info( "Changing mode of {0} to {1:o}".format( swapfile, swapfile_mode)) os.chmod(swapfile, swapfile_mode) return True return False def create_swap_space(self, mount_point, size_mb): size_kb = size_mb * 1024 size = size_kb * 1024 swapfile = os.path.join(mount_point, 'swapfile') swaplist = shellutil.run_get_output("swapon -s")[1] if self.check_existing_swap_file(swapfile, swaplist, size): return if os.path.isfile(swapfile) and os.path.getsize(swapfile) != size: logger.info("Remove old swap file") shellutil.run("swapoff {0}".format(swapfile), chk_err=False) os.remove(swapfile) if not os.path.isfile(swapfile): logger.info("Create swap file") self.mkfile(swapfile, size_kb * 1024) shellutil.run("mkswap {0}".format(swapfile)) if shellutil.run("swapon {0}".format(swapfile)): raise ResourceDiskError("{0}".format(swapfile)) logger.info("Enabled {0}KB of swap at {1}".format(size_kb, swapfile)) def mkfile(self, filename, nbytes): """ Create a non-sparse file of that size. Deletes and replaces existing file. To allow efficient execution, fallocate will be tried first. This includes ``os.posix_fallocate`` on Python 3.3+ (unix) and the ``fallocate`` command in the popular ``util-linux{,-ng}`` package. A dd fallback will be tried too. When size < 64M, perform single-pass dd. Otherwise do two-pass dd. """ if not isinstance(nbytes, int): nbytes = int(nbytes) if nbytes <= 0: raise ResourceDiskError("Invalid swap size [{0}]".format(nbytes)) if os.path.isfile(filename): os.remove(filename) # If file system is xfs, use dd right away as we have been reported that # swap enabling fails in xfs fs when disk space is allocated with # fallocate ret = 0 fn_sh = shellutil.quote((filename,)) if self.fs not in ['xfs', 'ext4']: # os.posix_fallocate if sys.version_info >= (3, 3): # Probable errors: # - OSError: Seen on Cygwin, libc notimpl? # - AttributeError: What if someone runs this under... fd = None try: fd = os.open( filename, os.O_CREAT | os.O_WRONLY | os.O_EXCL, stat.S_IRUSR | stat.S_IWUSR) os.posix_fallocate(fd, 0, nbytes) # pylint: disable=no-member return 0 except BaseException: # Not confident with this thing, just keep trying... pass finally: if fd is not None: os.close(fd) # fallocate command ret = shellutil.run( u"umask 0077 && fallocate -l {0} {1}".format(nbytes, fn_sh)) if ret == 0: return ret logger.info("fallocate unsuccessful, falling back to dd") # dd fallback dd_maxbs = 64 * 1024 ** 2 dd_cmd = "umask 0077 && dd if=/dev/zero bs={0} count={1} " \ "conv=notrunc of={2}" blocks = int(nbytes / dd_maxbs) if blocks > 0: ret = shellutil.run(dd_cmd.format(dd_maxbs, blocks, fn_sh)) << 8 remains = int(nbytes % dd_maxbs) if remains > 0: ret += shellutil.run(dd_cmd.format(remains, 1, fn_sh)) if ret == 0: logger.info("dd successful") else: logger.error("dd unsuccessful") return ret WALinuxAgent-2.9.1.1/azurelinuxagent/daemon/resourcedisk/factory.py000066400000000000000000000025751446033677600254340ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # from azurelinuxagent.common.version import DISTRO_NAME, DISTRO_VERSION, DISTRO_FULL_NAME from .default import ResourceDiskHandler from .freebsd import FreeBSDResourceDiskHandler from .openbsd import OpenBSDResourceDiskHandler from .openwrt import OpenWRTResourceDiskHandler def get_resourcedisk_handler(distro_name=DISTRO_NAME, distro_version=DISTRO_VERSION, # pylint: disable=W0613 distro_full_name=DISTRO_FULL_NAME): # pylint: disable=W0613 if distro_name == "freebsd": return FreeBSDResourceDiskHandler() if distro_name == "openbsd": return OpenBSDResourceDiskHandler() if distro_name == "openwrt": return OpenWRTResourceDiskHandler() return ResourceDiskHandler() WALinuxAgent-2.9.1.1/azurelinuxagent/daemon/resourcedisk/freebsd.py000066400000000000000000000162501446033677600253720ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.fileutil as fileutil import azurelinuxagent.common.utils.shellutil as shellutil import azurelinuxagent.common.conf as conf from azurelinuxagent.common.exception import ResourceDiskError from azurelinuxagent.daemon.resourcedisk.default import ResourceDiskHandler class FreeBSDResourceDiskHandler(ResourceDiskHandler): """ This class handles resource disk mounting for FreeBSD. The resource disk locates at following slot: scbus2 on blkvsc1 bus 0: at scbus2 target 1 lun 0 (da1,pass2) There are 2 variations based on partition table type: 1. MBR: The resource disk partition is /dev/da1s1 2. GPT: The resource disk partition is /dev/da1p2, /dev/da1p1 is for reserved usage. """ def __init__(self): # pylint: disable=W0235 super(FreeBSDResourceDiskHandler, self).__init__() @staticmethod def parse_gpart_list(data): dic = {} for line in data.split('\n'): if line.find("Geom name: ") != -1: geom_name = line[11:] elif line.find("scheme: ") != -1: dic[geom_name] = line[8:] return dic def mount_resource_disk(self, mount_point): fs = self.fs if fs != 'ufs': raise ResourceDiskError( "Unsupported filesystem type:{0}, only ufs is supported.".format(fs)) # 1. Detect device err, output = shellutil.run_get_output('gpart list') if err: raise ResourceDiskError( "Unable to detect resource disk device:{0}".format(output)) disks = self.parse_gpart_list(output) device = self.osutil.device_for_ide_port(1) if device is None or device not in disks: # fallback logic to find device err, output = shellutil.run_get_output( 'camcontrol periphlist 2:1:0') if err: # try again on "3:1:0" err, output = shellutil.run_get_output( 'camcontrol periphlist 3:1:0') if err: raise ResourceDiskError( "Unable to detect resource disk device:{0}".format(output)) # 'da1: generation: 4 index: 1 status: MORE\npass2: generation: 4 index: 2 status: LAST\n' for line in output.split('\n'): index = line.find(':') if index > 0: geom_name = line[:index] if geom_name in disks: device = geom_name break if not device: raise ResourceDiskError("Unable to detect resource disk device.") logger.info('Resource disk device {0} found.', device) # 2. Detect partition partition_table_type = disks[device] if partition_table_type == 'MBR': provider_name = device + 's1' elif partition_table_type == 'GPT': provider_name = device + 'p2' else: raise ResourceDiskError( "Unsupported partition table type:{0}".format(output)) err, output = shellutil.run_get_output( 'gpart show -p {0}'.format(device)) if err or output.find(provider_name) == -1: raise ResourceDiskError("Resource disk partition not found.") partition = '/dev/' + provider_name logger.info('Resource disk partition {0} found.', partition) # 3. Mount partition mount_list = shellutil.run_get_output("mount")[1] existing = self.osutil.get_mount_point(mount_list, partition) if existing: logger.info("Resource disk {0} is already mounted", partition) return existing fileutil.mkdir(mount_point, mode=0o755) mount_cmd = 'mount -t {0} {1} {2}'.format(fs, partition, mount_point) err = shellutil.run(mount_cmd, chk_err=False) if err: logger.info( 'Creating {0} filesystem on partition {1}'.format( fs, partition)) err, output = shellutil.run_get_output( 'newfs -U {0}'.format(partition)) if err: raise ResourceDiskError( "Failed to create new filesystem on partition {0}, error:{1}" .format( partition, output)) err, output = shellutil.run_get_output(mount_cmd, chk_err=False) if err: raise ResourceDiskError( "Failed to mount partition {0}, error {1}".format( partition, output)) logger.info( "Resource disk partition {0} is mounted at {1} with fstype {2}", partition, mount_point, fs) return mount_point def create_swap_space(self, mount_point, size_mb): size_kb = size_mb * 1024 size = size_kb * 1024 swapfile = os.path.join(mount_point, 'swapfile') swaplist = shellutil.run_get_output("swapctl -l")[1] if self.check_existing_swap_file(swapfile, swaplist, size): return if os.path.isfile(swapfile) and os.path.getsize(swapfile) != size: logger.info("Remove old swap file") shellutil.run("swapoff {0}".format(swapfile), chk_err=False) os.remove(swapfile) if not os.path.isfile(swapfile): logger.info("Create swap file") self.mkfile(swapfile, size_kb * 1024) mddevice = shellutil.run_get_output( "mdconfig -a -t vnode -f {0}".format(swapfile))[1].rstrip() shellutil.run("chmod 0600 /dev/{0}".format(mddevice)) if conf.get_resourcedisk_enable_swap_encryption(): shellutil.run("kldload aesni") shellutil.run("kldload cryptodev") shellutil.run("kldload geom_eli") shellutil.run( "geli onetime -e AES-XTS -l 256 -d /dev/{0}".format(mddevice)) shellutil.run("chmod 0600 /dev/{0}.eli".format(mddevice)) if shellutil.run("swapon /dev/{0}.eli".format(mddevice)): raise ResourceDiskError("/dev/{0}.eli".format(mddevice)) logger.info( "Enabled {0}KB of swap at /dev/{1}.eli ({2})".format(size_kb, mddevice, swapfile)) else: if shellutil.run("swapon /dev/{0}".format(mddevice)): raise ResourceDiskError("/dev/{0}".format(mddevice)) logger.info( "Enabled {0}KB of swap at /dev/{1} ({2})".format(size_kb, mddevice, swapfile)) WALinuxAgent-2.9.1.1/azurelinuxagent/daemon/resourcedisk/openbsd.py000066400000000000000000000114431446033677600254110ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # Copyright 2017 Reyk Floeter # # 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. # # Requires Python 2.6+ and OpenSSL 1.0+ # import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.fileutil as fileutil import azurelinuxagent.common.utils.shellutil as shellutil import azurelinuxagent.common.conf as conf from azurelinuxagent.common.exception import ResourceDiskError from azurelinuxagent.daemon.resourcedisk.default import ResourceDiskHandler class OpenBSDResourceDiskHandler(ResourceDiskHandler): def __init__(self): super(OpenBSDResourceDiskHandler, self).__init__() # Fase File System (FFS) is UFS if self.fs == 'ufs' or self.fs == 'ufs2': self.fs = 'ffs' def create_swap_space(self, mount_point, size_mb): pass def enable_swap(self, mount_point): size_mb = conf.get_resourcedisk_swap_size_mb() if size_mb: logger.info("Enable swap") device = self.osutil.device_for_ide_port(1) err, output = shellutil.run_get_output("swapctl -a /dev/" "{0}b".format(device), chk_err=False) if err: logger.error("Failed to enable swap, error {0}", output) def mount_resource_disk(self, mount_point): fs = self.fs if fs != 'ffs': raise ResourceDiskError("Unsupported filesystem type: {0}, only " "ufs/ffs is supported.".format(fs)) # 1. Get device device = self.osutil.device_for_ide_port(1) if not device: raise ResourceDiskError("Unable to detect resource disk device.") logger.info('Resource disk device {0} found.', device) # 2. Get partition partition = "/dev/{0}a".format(device) # 3. Mount partition mount_list = shellutil.run_get_output("mount")[1] existing = self.osutil.get_mount_point(mount_list, partition) if existing: logger.info("Resource disk {0} is already mounted", partition) return existing fileutil.mkdir(mount_point, mode=0o755) mount_cmd = 'mount -t {0} {1} {2}'.format(self.fs, partition, mount_point) err = shellutil.run(mount_cmd, chk_err=False) if err: logger.info('Creating {0} filesystem on {1}'.format(fs, device)) fdisk_cmd = "/sbin/fdisk -yi {0}".format(device) err, output = shellutil.run_get_output(fdisk_cmd, chk_err=False) if err: raise ResourceDiskError("Failed to create new MBR on {0}, " "error: {1}".format(device, output)) size_mb = conf.get_resourcedisk_swap_size_mb() if size_mb: if size_mb > 512 * 1024: size_mb = 512 * 1024 disklabel_cmd = ("echo -e '{0} 1G-* 50%\nswap 1-{1}M 50%' " "| disklabel -w -A -T /dev/stdin " "{2}").format(mount_point, size_mb, device) ret, output = shellutil.run_get_output( disklabel_cmd, chk_err=False) if ret: raise ResourceDiskError("Failed to create new disklabel " "on {0}, error " "{1}".format(device, output)) err, output = shellutil.run_get_output("newfs -O2 {0}a" "".format(device)) if err: raise ResourceDiskError("Failed to create new filesystem on " "partition {0}, error " "{1}".format(partition, output)) err, output = shellutil.run_get_output(mount_cmd, chk_err=False) if err: raise ResourceDiskError("Failed to mount partition {0}, " "error {1}".format(partition, output)) logger.info("Resource disk partition {0} is mounted at {1} with fstype " "{2}", partition, mount_point, fs) return mount_point WALinuxAgent-2.9.1.1/azurelinuxagent/daemon/resourcedisk/openwrt.py000066400000000000000000000133371446033677600254610ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # Copyright 2018 Sonus Networks, Inc. (d.b.a. Ribbon Communications Operating Company) # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os from time import sleep import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.fileutil as fileutil import azurelinuxagent.common.utils.shellutil as shellutil import azurelinuxagent.common.conf as conf from azurelinuxagent.common.exception import ResourceDiskError from azurelinuxagent.daemon.resourcedisk.default import ResourceDiskHandler class OpenWRTResourceDiskHandler(ResourceDiskHandler): def __init__(self): super(OpenWRTResourceDiskHandler, self).__init__() # Fase File System (FFS) is UFS if self.fs == 'ufs' or self.fs == 'ufs2': self.fs = 'ffs' def reread_partition_table(self, device): ret, output = shellutil.run_get_output("hdparm -z {0}".format(device), chk_err=False) # pylint: disable=W0612 if ret != 0: logger.warn("Failed refresh the partition table.") def mount_resource_disk(self, mount_point): device = self.osutil.device_for_ide_port(1) if device is None: raise ResourceDiskError("unable to detect disk topology") logger.info('Resource disk device {0} found.', device) # 2. Get partition device = "/dev/{0}".format(device) partition = device + "1" logger.info('Resource disk partition {0} found.', partition) # 3. Mount partition mount_list = shellutil.run_get_output("mount")[1] existing = self.osutil.get_mount_point(mount_list, device) if existing: logger.info("Resource disk [{0}] is already mounted [{1}]", partition, existing) return existing try: fileutil.mkdir(mount_point, mode=0o755) except OSError as ose: msg = "Failed to create mount point " \ "directory [{0}]: {1}".format(mount_point, ose) logger.error(msg) raise ResourceDiskError(msg=msg, inner=ose) force_option = 'F' if self.fs == 'xfs': force_option = 'f' mkfs_string = "mkfs.{0} -{2} {1}".format(self.fs, partition, force_option) # Compare to the Default mount_resource_disk, we don't check for GPT that is not supported on OpenWRT ret = self.change_partition_type(suppress_message=True, option_str="{0} 1 -n".format(device)) ptype = ret[1].strip() if ptype == "7" and self.fs != "ntfs": logger.info("The partition is formatted with ntfs, updating " "partition type to 83") self.change_partition_type(suppress_message=False, option_str="{0} 1 83".format(device)) self.reread_partition_table(device) logger.info("Format partition [{0}]", mkfs_string) shellutil.run(mkfs_string) else: logger.info("The partition type is {0}", ptype) mount_options = conf.get_resourcedisk_mountoptions() mount_string = self.get_mount_string(mount_options, partition, mount_point) attempts = 5 while not os.path.exists(partition) and attempts > 0: logger.info("Waiting for partition [{0}], {1} attempts remaining", partition, attempts) sleep(5) attempts -= 1 if not os.path.exists(partition): raise ResourceDiskError("Partition was not created [{0}]".format(partition)) if os.path.ismount(mount_point): logger.warn("Disk is already mounted on {0}", mount_point) else: # Some kernels seem to issue an async partition re-read after a # command invocation. This causes mount to fail if the # partition re-read is not complete by the time mount is # attempted. Seen in CentOS 7.2. Force a sequential re-read of # the partition and try mounting. logger.info("Mounting after re-reading partition info.") self.reread_partition_table(device) logger.info("Mount resource disk [{0}]", mount_string) ret, output = shellutil.run_get_output(mount_string) if ret: logger.warn("Failed to mount resource disk. " "Attempting to format and retry mount. [{0}]", output) shellutil.run(mkfs_string) ret, output = shellutil.run_get_output(mount_string) if ret: raise ResourceDiskError("Could not mount {0} " "after syncing partition table: " "[{1}] {2}".format(partition, ret, output)) logger.info("Resource disk {0} is mounted at {1} with {2}", device, mount_point, self.fs) return mount_point WALinuxAgent-2.9.1.1/azurelinuxagent/daemon/scvmm.py000066400000000000000000000053271446033677600224060ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import re import os import sys import subprocess import time import azurelinuxagent.common.logger as logger import azurelinuxagent.common.conf as conf from azurelinuxagent.common.osutil import get_osutil VMM_CONF_FILE_NAME = "linuxosconfiguration.xml" VMM_STARTUP_SCRIPT_NAME= "install" def get_scvmm_handler(): return ScvmmHandler() class ScvmmHandler(object): def __init__(self): self.osutil = get_osutil() def detect_scvmm_env(self, dev_dir='/dev'): logger.info("Detecting Microsoft System Center VMM Environment") found=False # try to load the ATAPI driver, continue on failure self.osutil.try_load_atapiix_mod() # cycle through all available /dev/sr*|hd*|cdrom*|cd* looking for the scvmm configuration file mount_point = conf.get_dvd_mount_point() for devices in filter(lambda x: x is not None, [re.match(r'(sr[0-9]|hd[c-z]|cdrom[0-9]?|cd[0-9]+)', dev) for dev in os.listdir(dev_dir)]): dvd_device = os.path.join(dev_dir, devices.group(0)) self.osutil.mount_dvd(max_retry=1, chk_err=False, dvd_device=dvd_device, mount_point=mount_point) found = os.path.isfile(os.path.join(mount_point, VMM_CONF_FILE_NAME)) if found: self.start_scvmm_agent(mount_point=mount_point) break else: self.osutil.umount_dvd(chk_err=False, mount_point=mount_point) return found def start_scvmm_agent(self, mount_point=None): logger.info("Starting Microsoft System Center VMM Initialization " "Process") if mount_point is None: mount_point = conf.get_dvd_mount_point() startup_script = os.path.join(mount_point, VMM_STARTUP_SCRIPT_NAME) with open(os.devnull, 'w') as devnull: subprocess.Popen(["/bin/bash", startup_script, "-p " + mount_point], stdout=devnull, stderr=devnull) def run(self): if self.detect_scvmm_env(): logger.info("Exiting") time.sleep(300) sys.exit(0) WALinuxAgent-2.9.1.1/azurelinuxagent/distro/000077500000000000000000000000001446033677600207415ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/azurelinuxagent/distro/__init__.py000066400000000000000000000011661446033677600230560ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # WALinuxAgent-2.9.1.1/azurelinuxagent/distro/suse/000077500000000000000000000000001446033677600217205ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/azurelinuxagent/distro/suse/__init__.py000066400000000000000000000011661446033677600240350ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # WALinuxAgent-2.9.1.1/azurelinuxagent/ga/000077500000000000000000000000001446033677600200245ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/azurelinuxagent/ga/__init__.py000066400000000000000000000011661446033677600221410ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # WALinuxAgent-2.9.1.1/azurelinuxagent/ga/collect_logs.py000066400000000000000000000337331446033677600230600ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2020 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import datetime import os import sys import threading import time from azurelinuxagent.common import cgroupconfigurator, logcollector import azurelinuxagent.common.conf as conf from azurelinuxagent.common import logger from azurelinuxagent.common.cgroup import MetricsCounter from azurelinuxagent.common.event import elapsed_milliseconds, add_event, WALAEventOperation, report_metric from azurelinuxagent.common.future import ustr from azurelinuxagent.common.interfaces import ThreadHandlerInterface from azurelinuxagent.common.logcollector import COMPRESSED_ARCHIVE_PATH, GRACEFUL_KILL_ERRCODE from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator, LOGCOLLECTOR_MEMORY_LIMIT from azurelinuxagent.common.protocol.util import get_protocol_util from azurelinuxagent.common.utils import shellutil from azurelinuxagent.common.utils.shellutil import CommandError from azurelinuxagent.common.version import PY_VERSION_MAJOR, PY_VERSION_MINOR, AGENT_NAME, CURRENT_VERSION _INITIAL_LOG_COLLECTION_DELAY = 5 * 60 # Five minutes of delay def get_collect_logs_handler(): return CollectLogsHandler() def is_log_collection_allowed(): # There are three conditions that need to be met in order to allow periodic log collection: # 1) It should be enabled in the configuration. # 2) The system must be using cgroups to manage services. Needed for resource limiting of the log collection. # 3) The python version must be greater than 2.6 in order to support the ZipFile library used when collecting. conf_enabled = conf.get_collect_logs() cgroups_enabled = CGroupConfigurator.get_instance().enabled() supported_python = PY_VERSION_MINOR >= 6 if PY_VERSION_MAJOR == 2 else PY_VERSION_MAJOR == 3 is_allowed = conf_enabled and cgroups_enabled and supported_python msg = "Checking if log collection is allowed at this time [{0}]. All three conditions must be met: " \ "configuration enabled [{1}], cgroups enabled [{2}], python supported: [{3}]".format(is_allowed, conf_enabled, cgroups_enabled, supported_python) logger.info(msg) add_event( name=AGENT_NAME, version=CURRENT_VERSION, op=WALAEventOperation.LogCollection, is_success=is_allowed, message=msg, log_event=False) return is_allowed class CollectLogsHandler(ThreadHandlerInterface): """ Periodically collects and uploads logs from the VM to the host. """ _THREAD_NAME = "CollectLogsHandler" __CGROUPS_FLAG_ENV_VARIABLE = "_AZURE_GUEST_AGENT_LOG_COLLECTOR_MONITOR_CGROUPS_" @staticmethod def get_thread_name(): return CollectLogsHandler._THREAD_NAME @staticmethod def enable_cgroups_validation(): os.environ[CollectLogsHandler.__CGROUPS_FLAG_ENV_VARIABLE] = "1" @staticmethod def disable_cgroups_validation(): if CollectLogsHandler.__CGROUPS_FLAG_ENV_VARIABLE in os.environ: del os.environ[CollectLogsHandler.__CGROUPS_FLAG_ENV_VARIABLE] @staticmethod def should_validate_cgroups(): if CollectLogsHandler.__CGROUPS_FLAG_ENV_VARIABLE in os.environ: return os.environ[CollectLogsHandler.__CGROUPS_FLAG_ENV_VARIABLE] == "1" return False def __init__(self): self.protocol = None self.protocol_util = None self.event_thread = None self.should_run = True self.last_state = None self.period = conf.get_collect_logs_period() def run(self): self.start() def keep_alive(self): return self.should_run def is_alive(self): return self.event_thread.is_alive() def start(self): self.event_thread = threading.Thread(target=self.daemon) self.event_thread.setDaemon(True) self.event_thread.setName(self.get_thread_name()) self.event_thread.start() def join(self): self.event_thread.join() def stopped(self): return not self.should_run def stop(self): self.should_run = False if self.is_alive(): try: self.join() except RuntimeError: pass def init_protocols(self): # The initialization of ProtocolUtil for the log collection thread should be done within the thread itself # rather than initializing it in the ExtHandler thread. This is done to avoid any concurrency issues as each # thread would now have its own ProtocolUtil object as per the SingletonPerThread model. self.protocol_util = get_protocol_util() self.protocol = self.protocol_util.get_protocol() def daemon(self): # Delay the first collector on start up to give short lived VMs (that might be dead before the second # collection has a chance to run) an opportunity to do produce meaningful logs to collect. time.sleep(_INITIAL_LOG_COLLECTION_DELAY) try: CollectLogsHandler.enable_cgroups_validation() if self.protocol_util is None or self.protocol is None: self.init_protocols() while not self.stopped(): try: self.collect_and_send_logs() except Exception as e: logger.error("An error occurred in the log collection thread main loop; " "will skip the current iteration.\n{0}", ustr(e)) finally: time.sleep(self.period) except Exception as e: logger.error("An error occurred in the log collection thread; will exit the thread.\n{0}", ustr(e)) finally: CollectLogsHandler.disable_cgroups_validation() def collect_and_send_logs(self): if self._collect_logs(): self._send_logs() def _collect_logs(self): logger.info("Starting log collection...") # Invoke the command line tool in the agent to collect logs, with resource limits on CPU. # Some distros like ubuntu20 by default cpu and memory accounting enabled. Thus create nested cgroups under the logcollector slice # So disabling CPU and Memory accounting prevents from creating nested cgroups, so that all the counters will be present in logcollector Cgroup systemd_cmd = [ "systemd-run", "--property=CPUAccounting=no", "--property=MemoryAccounting=no", "--unit={0}".format(logcollector.CGROUPS_UNIT), "--slice={0}".format(cgroupconfigurator.LOGCOLLECTOR_SLICE), "--scope" ] # The log tool is invoked from the current agent's egg with the command line option collect_logs_cmd = [sys.executable, "-u", sys.argv[0], "-collect-logs"] final_command = systemd_cmd + collect_logs_cmd def exec_command(): start_time = datetime.datetime.utcnow() success = False msg = None try: shellutil.run_command(final_command, log_error=False) duration = elapsed_milliseconds(start_time) archive_size = os.path.getsize(COMPRESSED_ARCHIVE_PATH) msg = "Successfully collected logs. Archive size: {0} b, elapsed time: {1} ms.".format(archive_size, duration) logger.info(msg) success = True return True except Exception as e: duration = elapsed_milliseconds(start_time) err_msg = ustr(e) if isinstance(e, CommandError): # pylint has limited (i.e. no) awareness of control flow w.r.t. typing. we disable=no-member # here because we know e must be a CommandError but pylint still considers the case where # e is a different type of exception. err_msg = ustr("Log Collector exited with code {0}").format( e.returncode) # pylint: disable=no-member if e.returncode == logcollector.INVALID_CGROUPS_ERRCODE: # pylint: disable=no-member logger.info("Disabling periodic log collection until service restart due to process error.") self.stop() # When the log collector memory limit is exceeded, Agent gracefully exit the process with this error code. # Stop the periodic operation because it seems to be persistent. elif e.returncode == logcollector.GRACEFUL_KILL_ERRCODE: # pylint: disable=no-member logger.info("Disabling periodic log collection until service restart due to exceeded process memory limit.") self.stop() else: logger.info(err_msg) msg = "Failed to collect logs. Elapsed time: {0} ms. Error: {1}".format(duration, err_msg) # No need to log to the local log since we logged stdout, stderr from the process. return False finally: add_event( name=AGENT_NAME, version=CURRENT_VERSION, op=WALAEventOperation.LogCollection, is_success=success, message=msg, log_event=False) return exec_command() def _send_logs(self): msg = None success = False try: with open(COMPRESSED_ARCHIVE_PATH, "rb") as fh: archive_content = fh.read() self.protocol.upload_logs(archive_content) msg = "Successfully uploaded logs." logger.info(msg) success = True except Exception as e: msg = "Failed to upload logs. Error: {0}".format(ustr(e)) logger.warn(msg) finally: add_event( name=AGENT_NAME, version=CURRENT_VERSION, op=WALAEventOperation.LogCollection, is_success=success, message=msg, log_event=False) def get_log_collector_monitor_handler(cgroups): return LogCollectorMonitorHandler(cgroups) class LogCollectorMonitorHandler(ThreadHandlerInterface): """ Periodically monitor and checks the Log collector Cgroups and sends telemetry to Kusto. """ _THREAD_NAME = "LogCollectorMonitorHandler" @staticmethod def get_thread_name(): return LogCollectorMonitorHandler._THREAD_NAME def __init__(self, cgroups): self.event_thread = None self.should_run = True self.period = 2 # Log collector monitor runs every 2 secs. self.cgroups = cgroups self.__log_metrics = conf.get_cgroup_log_metrics() def run(self): self.start() def stop(self): self.should_run = False if self.is_alive(): self.join() def join(self): self.event_thread.join() def stopped(self): return not self.should_run def is_alive(self): return self.event_thread is not None and self.event_thread.is_alive() def start(self): self.event_thread = threading.Thread(target=self.daemon) self.event_thread.setDaemon(True) self.event_thread.setName(self.get_thread_name()) self.event_thread.start() def daemon(self): try: while not self.stopped(): try: metrics = self._poll_resource_usage() self._send_telemetry(metrics) self._verify_memory_limit(metrics) except Exception as e: logger.error("An error occurred in the log collection monitor thread loop; " "will skip the current iteration.\n{0}", ustr(e)) finally: time.sleep(self.period) except Exception as e: logger.error( "An error occurred in the MonitorLogCollectorCgroupsHandler thread; will exit the thread.\n{0}", ustr(e)) def _poll_resource_usage(self): metrics = [] for cgroup in self.cgroups: metrics.extend(cgroup.get_tracked_metrics(track_throttled_time=True)) return metrics def _send_telemetry(self, metrics): for metric in metrics: report_metric(metric.category, metric.counter, metric.instance, metric.value, log_event=self.__log_metrics) def _verify_memory_limit(self, metrics): current_usage = 0 for metric in metrics: if metric.counter == MetricsCounter.TOTAL_MEM_USAGE: current_usage += metric.value elif metric.counter == MetricsCounter.SWAP_MEM_USAGE: current_usage += metric.value if current_usage > LOGCOLLECTOR_MEMORY_LIMIT: msg = "Log collector memory limit {0} bytes exceeded. The max reported usage is {1} bytes.".format(LOGCOLLECTOR_MEMORY_LIMIT, current_usage) logger.info(msg) add_event( name=AGENT_NAME, version=CURRENT_VERSION, op=WALAEventOperation.LogCollection, message=msg) os._exit(GRACEFUL_KILL_ERRCODE) WALinuxAgent-2.9.1.1/azurelinuxagent/ga/collect_telemetry_events.py000066400000000000000000000700701446033677600255050ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2020 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import datetime import json import os import re import threading from collections import defaultdict import azurelinuxagent.common.logger as logger from azurelinuxagent.common import conf from azurelinuxagent.common.agent_supported_feature import get_supported_feature_by_name, SupportedFeatureNames from azurelinuxagent.common.event import EVENTS_DIRECTORY, TELEMETRY_LOG_EVENT_ID, \ TELEMETRY_LOG_PROVIDER_ID, add_event, WALAEventOperation, add_log_event, get_event_logger, \ CollectOrReportEventDebugInfo, EVENT_FILE_REGEX, parse_event from azurelinuxagent.common.exception import InvalidExtensionEventError, ServiceStoppedError from azurelinuxagent.common.future import ustr from azurelinuxagent.common.interfaces import ThreadHandlerInterface from azurelinuxagent.common.telemetryevent import TelemetryEvent, TelemetryEventParam, \ GuestAgentGenericLogsSchema, GuestAgentExtensionEventsSchema from azurelinuxagent.common.utils import textutil from azurelinuxagent.ga.exthandlers import HANDLER_NAME_PATTERN from azurelinuxagent.ga.periodic_operation import PeriodicOperation def get_collect_telemetry_events_handler(send_telemetry_events_handler): return CollectTelemetryEventsHandler(send_telemetry_events_handler) class ExtensionEventSchema(object): """ Class for defining the schema for Extension Events. Sample Extension Event Example: { "Version":"1.0.0.23", "Timestamp":"2018-01-02T22:08:12.510696Z" //(time in UTC (ISO-8601 standard), "TaskName":"TestRun" //Open for publishers, "EventLevel":"Critical/Error/Warning/Verbose/Informational/LogAlways", "Message": "Successful test" //(max 3K, 3072 characters), "EventPid":"1", "EventTid":"2", "OperationId":"Guid (str)" } From next version(2.10+) we accept integer values for EventPid and EventTid fields. But we still support string type for backward compatability """ Version = "Version" Timestamp = "Timestamp" TaskName = "TaskName" EventLevel = "EventLevel" Message = "Message" EventPid = "EventPid" EventTid = "EventTid" OperationId = "OperationId" class _ProcessExtensionEvents(PeriodicOperation): """ Periodic operation for collecting extension telemetry events and enqueueing them for the SendTelemetryHandler thread. """ _EXTENSION_EVENT_COLLECTION_PERIOD = datetime.timedelta(seconds=conf.get_etp_collection_period()) _EXTENSION_EVENT_FILE_NAME_REGEX = re.compile(r"^(\d+)\.json$", re.IGNORECASE) # Limits _MAX_NUMBER_OF_EVENTS_PER_EXTENSION_PER_PERIOD = 360 _EXTENSION_EVENT_FILE_MAX_SIZE = 4 * 1024 * 1024 # 4 MB = 4 * 1,048,576 Bytes _EXTENSION_EVENT_MAX_SIZE = 1024 * 6 # 6Kb or 6144 characters. Limit for the whole event. Prevent oversized events. _EXTENSION_EVENT_MAX_MSG_LEN = 1024 * 3 # 3Kb or 3072 chars. _EXTENSION_EVENT_REQUIRED_FIELDS = [attr.lower() for attr in dir(ExtensionEventSchema) if not callable(getattr(ExtensionEventSchema, attr)) and not attr.startswith("__")] def __init__(self, send_telemetry_events_handler): super(_ProcessExtensionEvents, self).__init__(_ProcessExtensionEvents._EXTENSION_EVENT_COLLECTION_PERIOD) self._send_telemetry_events_handler = send_telemetry_events_handler def _operation(self): if self._send_telemetry_events_handler.stopped(): logger.warn("{0} service is not running, skipping current iteration".format( self._send_telemetry_events_handler.get_thread_name())) return delete_all_event_files = True extension_handler_with_event_dirs = [] try: extension_handler_with_event_dirs = self._get_extension_events_dir_with_handler_name(conf.get_ext_log_dir()) if not extension_handler_with_event_dirs: logger.verbose("No Extension events directory exist") return for extension_handler_with_event_dir in extension_handler_with_event_dirs: handler_name = extension_handler_with_event_dir[0] handler_event_dir_path = extension_handler_with_event_dir[1] self._capture_extension_events(handler_name, handler_event_dir_path) except ServiceStoppedError: # Since the service stopped, we should not delete the extension files and retry sending them whenever # the telemetry service comes back up delete_all_event_files = False except Exception as error: msg = "Unknown error occurred when trying to collect extension events:{0}".format( textutil.format_exception(error)) add_event(op=WALAEventOperation.ExtensionTelemetryEventProcessing, message=msg, is_success=False) finally: # Always ensure that the events directory are being deleted each run except when Telemetry Service is stopped, # even if we run into an error and dont process them this run. if delete_all_event_files: self._ensure_all_events_directories_empty(extension_handler_with_event_dirs) @staticmethod def _get_extension_events_dir_with_handler_name(extension_log_dir): """ Get the full path to events directory for all extension handlers that have one :param extension_log_dir: Base log directory for all extensions :return: A list of full paths of existing events directory for all handlers """ extension_handler_with_event_dirs = [] for ext_handler_name in os.listdir(extension_log_dir): # Check if its an Extension directory if not os.path.isdir(os.path.join(extension_log_dir, ext_handler_name)) \ or re.match(HANDLER_NAME_PATTERN, ext_handler_name) is None: continue # Check if EVENTS_DIRECTORY directory exists extension_event_dir = os.path.join(extension_log_dir, ext_handler_name, EVENTS_DIRECTORY) if os.path.exists(extension_event_dir): extension_handler_with_event_dirs.append((ext_handler_name, extension_event_dir)) return extension_handler_with_event_dirs def _event_file_size_allowed(self, event_file_path): event_file_size = os.stat(event_file_path).st_size if event_file_size > self._EXTENSION_EVENT_FILE_MAX_SIZE: convert_to_mb = lambda x: (1.0 * x) / (1000 * 1000) msg = "Skipping file: {0} as its size is {1:.2f} Mb > Max size allowed {2:.1f} Mb".format( event_file_path, convert_to_mb(event_file_size), convert_to_mb(self._EXTENSION_EVENT_FILE_MAX_SIZE)) logger.warn(msg) add_log_event(level=logger.LogLevel.WARNING, message=msg, forced=True) return False return True def _capture_extension_events(self, handler_name, handler_event_dir_path): """ Capture Extension events and add them to the events_list :param handler_name: Complete Handler Name. Eg: Microsoft.CPlat.Core.RunCommandLinux :param handler_event_dir_path: Full path. Eg: '/var/log/azure/Microsoft.CPlat.Core.RunCommandLinux/events' """ # Filter out the files that do not follow the pre-defined EXTENSION_EVENT_FILE_NAME_REGEX event_files = [event_file for event_file in os.listdir(handler_event_dir_path) if re.match(self._EXTENSION_EVENT_FILE_NAME_REGEX, event_file) is not None] # Pick the latest files first, we'll discard older events if len(events) > MAX_EVENT_COUNT event_files.sort(reverse=True) captured_extension_events_count = 0 dropped_events_with_error_count = defaultdict(int) try: for event_file in event_files: event_file_path = os.path.join(handler_event_dir_path, event_file) try: logger.verbose("Processing event file: {0}", event_file_path) if not self._event_file_size_allowed(event_file_path): continue # We support multiple events in a file, read the file and parse events. captured_extension_events_count = self._enqueue_events_and_get_count(handler_name, event_file_path, captured_extension_events_count, dropped_events_with_error_count) # We only allow MAX_NUMBER_OF_EVENTS_PER_EXTENSION_PER_PERIOD=300 maximum events per period per handler if captured_extension_events_count >= self._MAX_NUMBER_OF_EVENTS_PER_EXTENSION_PER_PERIOD: msg = "Reached max count for the extension: {0}; Max Limit: {1}. Skipping the rest.".format( handler_name, self._MAX_NUMBER_OF_EVENTS_PER_EXTENSION_PER_PERIOD) logger.warn(msg) add_log_event(level=logger.LogLevel.WARNING, message=msg, forced=True) break except ServiceStoppedError: # Not logging here as already logged once, re-raising # Since we already started processing this file, deleting it as we could've already sent some events out # This is a trade-off between data replication vs data loss. raise except Exception as error: msg = "Failed to process event file {0}:{1}".format(event_file, textutil.format_exception(error)) logger.warn(msg) add_log_event(level=logger.LogLevel.WARNING, message=msg, forced=True) finally: # Todo: We should delete files after ensuring that we sent the data to Wireserver successfully # from our end rather than deleting first and sending later. This is to ensure the data reliability # of the agent telemetry pipeline. os.remove(event_file_path) finally: if dropped_events_with_error_count: msg = "Dropped events for Extension: {0}; Details:\n\t{1}".format(handler_name, '\n\t'.join( ["Reason: {0}; Dropped Count: {1}".format(k, v) for k, v in dropped_events_with_error_count.items()])) logger.warn(msg) add_log_event(level=logger.LogLevel.WARNING, message=msg, forced=True) if captured_extension_events_count > 0: logger.info("Collected {0} events for extension: {1}".format(captured_extension_events_count, handler_name)) @staticmethod def _ensure_all_events_directories_empty(extension_events_directories): if not extension_events_directories: return for extension_handler_with_event_dir in extension_events_directories: event_dir_path = extension_handler_with_event_dir[1] if not os.path.exists(event_dir_path): return log_err = True # Delete any residue files in the events directory for residue_file in os.listdir(event_dir_path): try: os.remove(os.path.join(event_dir_path, residue_file)) except Exception as error: # Only log the first error once per handler per run to keep the logfile clean if log_err: logger.error("Failed to completely clear the {0} directory. Exception: {1}", event_dir_path, ustr(error)) log_err = False def _enqueue_events_and_get_count(self, handler_name, event_file_path, captured_events_count, dropped_events_with_error_count): event_file_time = datetime.datetime.fromtimestamp(os.path.getmtime(event_file_path)) # Read event file and decode it properly with open(event_file_path, "rb") as event_file_descriptor: event_data = event_file_descriptor.read().decode("utf-8") # Parse the string and get the list of events events = json.loads(event_data) # We allow multiple events in a file but there can be an instance where the file only has a single # JSON event and not a list. Handling that condition too if not isinstance(events, list): events = [events] for event in events: try: self._send_telemetry_events_handler.enqueue_event( self._parse_telemetry_event(handler_name, event, event_file_time) ) captured_events_count += 1 except InvalidExtensionEventError as invalid_error: # These are the errors thrown if there's an error parsing the event. We want to report these back to the # extension publishers so that they are aware of the issues. # The error messages are all static messages, we will use this to create a dict and emit an event at the # end of each run to notify if there were any errors parsing events for the extension dropped_events_with_error_count[ustr(invalid_error)] += 1 except ServiceStoppedError as stopped_error: logger.error( "Unable to enqueue events as service stopped: {0}. Stopping collecting extension events".format( ustr(stopped_error))) raise except Exception as error: logger.warn("Unable to parse and transmit event, error: {0}".format(error)) if captured_events_count >= self._MAX_NUMBER_OF_EVENTS_PER_EXTENSION_PER_PERIOD: break return captured_events_count def _parse_telemetry_event(self, handler_name, extension_unparsed_event, event_file_time): """ Parse the Json event file and convert it to TelemetryEvent object with the required data. :return: Complete TelemetryEvent with all required fields filled up properly. Raises if event breaches contract. """ extension_event = self._parse_event_and_ensure_it_is_valid(extension_unparsed_event) # Create a telemetry event, add all common parameters to the event # and then overwrite all the common params with extension events params if same event = TelemetryEvent(TELEMETRY_LOG_EVENT_ID, TELEMETRY_LOG_PROVIDER_ID) event.file_type = "json" CollectTelemetryEventsHandler.add_common_params_to_telemetry_event(event, event_file_time) replace_or_add_params = { GuestAgentGenericLogsSchema.EventName: "{0}-{1}".format(handler_name, extension_event[ ExtensionEventSchema.Version.lower()]), GuestAgentGenericLogsSchema.CapabilityUsed: extension_event[ExtensionEventSchema.EventLevel.lower()], GuestAgentGenericLogsSchema.TaskName: extension_event[ExtensionEventSchema.TaskName.lower()], GuestAgentGenericLogsSchema.Context1: extension_event[ExtensionEventSchema.Message.lower()], GuestAgentGenericLogsSchema.Context2: extension_event[ExtensionEventSchema.Timestamp.lower()], GuestAgentGenericLogsSchema.Context3: extension_event[ExtensionEventSchema.OperationId.lower()], GuestAgentGenericLogsSchema.EventPid: extension_event[ExtensionEventSchema.EventPid.lower()], GuestAgentGenericLogsSchema.EventTid: extension_event[ExtensionEventSchema.EventTid.lower()] } self._replace_or_add_param_in_event(event, replace_or_add_params) return event def _parse_event_and_ensure_it_is_valid(self, extension_event): """ Parse the Json event from file. Raise InvalidExtensionEventError if the event breaches pre-set contract. :param extension_event: The json event from file :return: Verified Json event that qualifies the contract. """ def _clean_value(k, v): if v is not None: if isinstance(v, int): if k.lower() in [ExtensionEventSchema.EventPid.lower(), ExtensionEventSchema.EventTid.lower()]: return str(v) return v.strip() return v event_size = 0 key_err_msg = "{0}: {1} not found" # Convert the dict to all lower keys to avoid schema confusion. # Only pick the params that we care about and skip the rest. event = dict((k.lower(), _clean_value(k, v)) for k, v in extension_event.items() if k.lower() in self._EXTENSION_EVENT_REQUIRED_FIELDS) # Trim message and only pick the first 3k chars message_key = ExtensionEventSchema.Message.lower() if message_key in event: event[message_key] = event[message_key][:self._EXTENSION_EVENT_MAX_MSG_LEN] else: raise InvalidExtensionEventError( key_err_msg.format(InvalidExtensionEventError.MissingKeyError, ExtensionEventSchema.Message)) if not event[message_key]: raise InvalidExtensionEventError( "{0}: {1} should not be empty".format(InvalidExtensionEventError.EmptyMessageError, ExtensionEventSchema.Message)) for required_key in self._EXTENSION_EVENT_REQUIRED_FIELDS: # If all required keys not in event then raise if required_key not in event: raise InvalidExtensionEventError( key_err_msg.format(InvalidExtensionEventError.MissingKeyError, required_key)) # If the event_size > _EXTENSION_EVENT_MAX_SIZE=6k, then raise if event_size > self._EXTENSION_EVENT_MAX_SIZE: raise InvalidExtensionEventError( "{0}: max event size allowed: {1}".format(InvalidExtensionEventError.OversizeEventError, self._EXTENSION_EVENT_MAX_SIZE)) event_size += len(event[required_key]) return event @staticmethod def _replace_or_add_param_in_event(event, replace_or_add_params): for param in event.parameters: if param.name in replace_or_add_params: param.value = replace_or_add_params.pop(param.name) if not replace_or_add_params: # All values replaced, return return # Add the remaining params to the event for param_name in replace_or_add_params: event.parameters.append(TelemetryEventParam(param_name, replace_or_add_params[param_name])) class _CollectAndEnqueueEvents(PeriodicOperation): """ Periodic operation to collect telemetry events located in the events folder and enqueue them for the SendTelemetryHandler thread. """ _EVENT_COLLECTION_PERIOD = datetime.timedelta(minutes=1) def __init__(self, send_telemetry_events_handler): super(_CollectAndEnqueueEvents, self).__init__(_CollectAndEnqueueEvents._EVENT_COLLECTION_PERIOD) self._send_telemetry_events_handler = send_telemetry_events_handler def _operation(self): """ Periodically send any events located in the events folder """ try: if self._send_telemetry_events_handler.stopped(): logger.warn("{0} service is not running, skipping iteration.".format( self._send_telemetry_events_handler.get_thread_name())) return self.process_events() except Exception as error: err_msg = "Failure in collecting telemetry events: {0}".format(ustr(error)) add_event(op=WALAEventOperation.UnhandledError, message=err_msg, is_success=False) def process_events(self): """ Returns a list of events that need to be sent to the telemetry pipeline and deletes the corresponding files from the events directory. """ event_directory_full_path = os.path.join(conf.get_lib_dir(), EVENTS_DIRECTORY) event_files = os.listdir(event_directory_full_path) debug_info = CollectOrReportEventDebugInfo(operation=CollectOrReportEventDebugInfo.OP_COLLECT) for event_file in event_files: try: match = EVENT_FILE_REGEX.search(event_file) if match is None: continue event_file_path = os.path.join(event_directory_full_path, event_file) try: logger.verbose("Processing event file: {0}", event_file_path) with open(event_file_path, "rb") as event_fd: event_data = event_fd.read().decode("utf-8") event = parse_event(event_data) # "legacy" events are events produced by previous versions of the agent (<= 2.2.46) and extensions; # they do not include all the telemetry fields, so we add them here is_legacy_event = match.group('agent_event') is None if is_legacy_event: # We'll use the file creation time for the event's timestamp event_file_creation_time_epoch = os.path.getmtime(event_file_path) event_file_creation_time = datetime.datetime.fromtimestamp(event_file_creation_time_epoch) if event.is_extension_event(): _CollectAndEnqueueEvents._trim_legacy_extension_event_parameters(event) CollectTelemetryEventsHandler.add_common_params_to_telemetry_event(event, event_file_creation_time) else: _CollectAndEnqueueEvents._update_legacy_agent_event(event, event_file_creation_time) self._send_telemetry_events_handler.enqueue_event(event) finally: # Todo: We should delete files after ensuring that we sent the data to Wireserver successfully # from our end rather than deleting first and sending later. This is to ensure the data reliability # of the agent telemetry pipeline. os.remove(event_file_path) except ServiceStoppedError as stopped_error: logger.error( "Unable to enqueue events as service stopped: {0}, skipping events collection".format( ustr(stopped_error))) except UnicodeError as uni_err: debug_info.update_unicode_error(uni_err) except Exception as error: debug_info.update_op_error(error) debug_info.report_debug_info() @staticmethod def _update_legacy_agent_event(event, event_creation_time): # Ensure that if an agent event is missing a field from the schema defined since 2.2.47, the missing fields # will be appended, ensuring the event schema is complete before the event is reported. new_event = TelemetryEvent() new_event.parameters = [] CollectTelemetryEventsHandler.add_common_params_to_telemetry_event(new_event, event_creation_time) event_params = dict([(param.name, param.value) for param in event.parameters]) new_event_params = dict([(param.name, param.value) for param in new_event.parameters]) missing_params = set(new_event_params.keys()).difference(set(event_params.keys())) params_to_add = [] for param_name in missing_params: params_to_add.append(TelemetryEventParam(param_name, new_event_params[param_name])) event.parameters.extend(params_to_add) @staticmethod def _trim_legacy_extension_event_parameters(event): """ This method is called for extension events before they are sent out. Per the agreement with extension publishers, the parameters that belong to extensions and will be reported intact are Name, Version, Operation, OperationSuccess, Message, and Duration. Since there is nothing preventing extensions to instantiate other fields (which belong to the agent), we call this method to ensure the rest of the parameters are trimmed since they will be replaced with values coming from the agent. :param event: Extension event to trim. :return: Trimmed extension event; containing only extension-specific parameters. """ params_to_keep = dict().fromkeys([ GuestAgentExtensionEventsSchema.Name, GuestAgentExtensionEventsSchema.Version, GuestAgentExtensionEventsSchema.Operation, GuestAgentExtensionEventsSchema.OperationSuccess, GuestAgentExtensionEventsSchema.Message, GuestAgentExtensionEventsSchema.Duration ]) trimmed_params = [] for param in event.parameters: if param.name in params_to_keep: trimmed_params.append(param) event.parameters = trimmed_params class CollectTelemetryEventsHandler(ThreadHandlerInterface): """ This Handler takes care of fetching the Extension Telemetry events from the {extension_events_dir} and sends it to Kusto for advanced debuggability. """ _THREAD_NAME = "TelemetryEventsCollector" def __init__(self, send_telemetry_events_handler): self.should_run = True self.thread = None self._send_telemetry_events_handler = send_telemetry_events_handler @staticmethod def get_thread_name(): return CollectTelemetryEventsHandler._THREAD_NAME def run(self): logger.info("Start Extension Telemetry service.") self.start() def is_alive(self): return self.thread is not None and self.thread.is_alive() def start(self): self.thread = threading.Thread(target=self.daemon) self.thread.setDaemon(True) self.thread.setName(CollectTelemetryEventsHandler.get_thread_name()) self.thread.start() def stop(self): """ Stop server communication and join the thread to main thread. """ self.should_run = False if self.is_alive(): self.thread.join() def stopped(self): return not self.should_run def daemon(self): periodic_operations = [ _CollectAndEnqueueEvents(self._send_telemetry_events_handler) ] is_etp_enabled = get_supported_feature_by_name(SupportedFeatureNames.ExtensionTelemetryPipeline).is_supported logger.info("Extension Telemetry pipeline enabled: {0}".format(is_etp_enabled)) if is_etp_enabled: periodic_operations.append(_ProcessExtensionEvents(self._send_telemetry_events_handler)) logger.info("Successfully started the {0} thread".format(self.get_thread_name())) while not self.stopped(): try: for periodic_op in periodic_operations: periodic_op.run() except Exception as error: logger.warn( "An error occurred in the Telemetry Extension thread main loop; will skip the current iteration.\n{0}", ustr(error)) finally: PeriodicOperation.sleep_until_next_operation(periodic_operations) @staticmethod def add_common_params_to_telemetry_event(event, event_time): reporter = get_event_logger() reporter.add_common_event_parameters(event, event_time)WALinuxAgent-2.9.1.1/azurelinuxagent/ga/env.py000066400000000000000000000230711446033677600211710ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import re import os import socket import threading import azurelinuxagent.common.conf as conf import azurelinuxagent.common.logger as logger from azurelinuxagent.common.dhcp import get_dhcp_handler from azurelinuxagent.common.event import add_periodic, WALAEventOperation, add_event from azurelinuxagent.common.future import ustr from azurelinuxagent.common.interfaces import ThreadHandlerInterface from azurelinuxagent.common.osutil import get_osutil from azurelinuxagent.common.protocol.util import get_protocol_util from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION from azurelinuxagent.ga.periodic_operation import PeriodicOperation CACHE_PATTERNS = [ re.compile("^(.*)\.(\d+)\.(agentsManifest)$", re.IGNORECASE), # pylint: disable=W1401 re.compile("^(.*)\.(\d+)\.(manifest\.xml)$", re.IGNORECASE), # pylint: disable=W1401 re.compile("^(.*)\.(\d+)\.(xml)$", re.IGNORECASE) # pylint: disable=W1401 ] MAXIMUM_CACHED_FILES = 50 def get_env_handler(): return EnvHandler() class RemovePersistentNetworkRules(PeriodicOperation): def __init__(self, osutil): super(RemovePersistentNetworkRules, self).__init__(conf.get_remove_persistent_net_rules_period()) self.osutil = osutil def _operation(self): self.osutil.remove_rules_files() class MonitorDhcpClientRestart(PeriodicOperation): def __init__(self, osutil): super(MonitorDhcpClientRestart, self).__init__(conf.get_monitor_dhcp_client_restart_period()) self.osutil = osutil self.dhcp_handler = get_dhcp_handler() self.dhcp_handler.conf_routes() self.dhcp_warning_enabled = True self.dhcp_id_list = [] def _operation(self): if len(self.dhcp_id_list) == 0: self.dhcp_id_list = self._get_dhcp_client_pid() return if all(self.osutil.check_pid_alive(pid) for pid in self.dhcp_id_list): return new_pid = self._get_dhcp_client_pid() if len(new_pid) != 0 and new_pid != self.dhcp_id_list: logger.info("EnvMonitor: Detected dhcp client restart. Restoring routing table.") self.dhcp_handler.conf_routes() self.dhcp_id_list = new_pid def _get_dhcp_client_pid(self): pid = [] try: # return a sorted list since handle_dhclient_restart needs to compare the previous value with # the new value and the comparison should not be affected by the order of the items in the list pid = sorted(self.osutil.get_dhcp_pid()) if len(pid) == 0 and self.dhcp_warning_enabled: logger.warn("Dhcp client is not running.") except Exception as exception: if self.dhcp_warning_enabled: logger.error("Failed to get the PID of the DHCP client: {0}", ustr(exception)) self.dhcp_warning_enabled = len(pid) != 0 return pid class EnableFirewall(PeriodicOperation): def __init__(self, osutil, protocol): super(EnableFirewall, self).__init__(conf.get_enable_firewall_period()) self._osutil = osutil self._protocol = protocol self._try_remove_legacy_firewall_rule = False def _operation(self): # If the rules ever change we must reset all rules and start over again. # # There was a rule change at 2.2.26, which started dropping non-root traffic # to WireServer. The previous rules allowed traffic. Having both rules in # place negated the fix in 2.2.26. Removing only the legacy rule and keeping other rules intact. # # We only try to remove the legacy firewall rule once on service start (irrespective of its exit code). if not self._try_remove_legacy_firewall_rule: self._osutil.remove_legacy_firewall_rule(dst_ip=self._protocol.get_endpoint()) self._try_remove_legacy_firewall_rule = True success, is_firewall_rules_updated = self._osutil.enable_firewall(dst_ip=self._protocol.get_endpoint(), uid=os.getuid()) if is_firewall_rules_updated: msg = "Successfully added Azure fabric firewall rules. Current Firewall rules:\n{0}".format(self._osutil.get_firewall_list()) logger.info(msg) add_event(AGENT_NAME, version=CURRENT_VERSION, op=WALAEventOperation.Firewall, message=msg, log_event=False) add_periodic( logger.EVERY_HOUR, AGENT_NAME, version=CURRENT_VERSION, op=WALAEventOperation.Firewall, is_success=success, log_event=False) class LogFirewallRules(PeriodicOperation): """ Log firewall rules state once a day. Goal is to capture the firewall state when the agent service startup, in addition to add more debug data and would be more useful long term. """ def __init__(self, osutil): super(LogFirewallRules, self).__init__(conf.get_firewall_rules_log_period()) self._osutil = osutil def _operation(self): # Log firewall rules state once a day logger.info("Current Firewall rules:\n{0}".format(self._osutil.get_firewall_list())) class SetRootDeviceScsiTimeout(PeriodicOperation): def __init__(self, osutil): super(SetRootDeviceScsiTimeout, self).__init__(conf.get_root_device_scsi_timeout_period()) self._osutil = osutil def _operation(self): self._osutil.set_scsi_disks_timeout(conf.get_root_device_scsi_timeout()) class MonitorHostNameChanges(PeriodicOperation): def __init__(self, osutil): super(MonitorHostNameChanges, self).__init__(conf.get_monitor_hostname_period()) self._osutil = osutil self._hostname = self._osutil.get_hostname_record() def _operation(self): curr_hostname = socket.gethostname() if curr_hostname != self._hostname: logger.info("EnvMonitor: Detected hostname change: {0} -> {1}", self._hostname, curr_hostname) self._osutil.set_hostname(curr_hostname) self._osutil.publish_hostname(curr_hostname) self._hostname = curr_hostname class EnvHandler(ThreadHandlerInterface): """ Monitor changes to dhcp and hostname. If dhcp client process re-start has occurred, reset routes, dhcp with fabric. Monitor scsi disk. If new scsi disk found, set timeout """ _THREAD_NAME = "EnvHandler" @staticmethod def get_thread_name(): return EnvHandler._THREAD_NAME def __init__(self): self.stopped = True self.hostname = None self.env_thread = None def run(self): if not self.stopped: logger.info("Stop existing env monitor service.") self.stop() self.stopped = False logger.info("Starting env monitor service.") self.start() def is_alive(self): return self.env_thread.is_alive() def start(self): self.env_thread = threading.Thread(target=self.daemon) self.env_thread.setDaemon(True) self.env_thread.setName(self.get_thread_name()) self.env_thread.start() def daemon(self): try: # The initialization of the protocol needs to be done within the environment thread itself rather # than initializing it in the ExtHandler thread. This is done to avoid any concurrency issues as each # thread would now have its own ProtocolUtil object as per the SingletonPerThread model. protocol_util = get_protocol_util() protocol = protocol_util.get_protocol() osutil = get_osutil() periodic_operations = [ RemovePersistentNetworkRules(osutil), MonitorDhcpClientRestart(osutil), ] if conf.enable_firewall(): periodic_operations.append(EnableFirewall(osutil, protocol)) periodic_operations.append(LogFirewallRules(osutil)) if conf.get_root_device_scsi_timeout() is not None: periodic_operations.append(SetRootDeviceScsiTimeout(osutil)) if conf.get_monitor_hostname(): periodic_operations.append(MonitorHostNameChanges(osutil)) while not self.stopped: try: for op in periodic_operations: op.run() except Exception as e: logger.error("An error occurred in the environment thread main loop; will skip the current iteration.\n{0}", ustr(e)) finally: PeriodicOperation.sleep_until_next_operation(periodic_operations) except Exception as e: logger.error("An error occurred in the environment thread; will exit the thread.\n{0}", ustr(e)) def stop(self): """ Stop server communication and join the thread to main thread. """ self.stopped = True if self.env_thread is not None: self.env_thread.join() WALinuxAgent-2.9.1.1/azurelinuxagent/ga/exthandlers.py000066400000000000000000003436121446033677600227300ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import copy import datetime import glob import json import os import re import shutil import stat import tempfile import time import zipfile from distutils.version import LooseVersion from collections import defaultdict from functools import partial from azurelinuxagent.common import conf from azurelinuxagent.common import logger from azurelinuxagent.common.utils import fileutil from azurelinuxagent.common import version from azurelinuxagent.common.agent_supported_feature import get_agent_supported_features_list_for_extensions, \ SupportedFeatureNames, get_supported_feature_by_name, get_agent_supported_features_list_for_crp from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator from azurelinuxagent.common.datacontract import get_properties, set_properties from azurelinuxagent.common.errorstate import ErrorState from azurelinuxagent.common.event import add_event, elapsed_milliseconds, WALAEventOperation, \ add_periodic, EVENTS_DIRECTORY from azurelinuxagent.common.exception import ExtensionDownloadError, ExtensionError, ExtensionErrorCodes, \ ExtensionOperationError, ExtensionUpdateError, ProtocolError, ProtocolNotFoundError, ExtensionsGoalStateError, \ GoalStateAggregateStatusCodes, MultiConfigExtensionEnableError from azurelinuxagent.common.future import ustr, is_file_not_found_error from azurelinuxagent.common.protocol.extensions_goal_state import GoalStateSource from azurelinuxagent.common.protocol.restapi import ExtensionStatus, ExtensionSubStatus, Extension, ExtHandlerStatus, \ VMStatus, GoalStateAggregateStatus, ExtensionState, ExtensionRequestedState, ExtensionSettings from azurelinuxagent.common.utils import textutil from azurelinuxagent.common.utils.archive import ARCHIVE_DIRECTORY_NAME from azurelinuxagent.common.utils.flexible_version import FlexibleVersion from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION, \ PY_VERSION_MAJOR, PY_VERSION_MICRO, PY_VERSION_MINOR _HANDLER_NAME_PATTERN = r'^([^-]+)' _HANDLER_VERSION_PATTERN = r'(\d+(?:\.\d+)*)' _HANDLER_PATTERN = _HANDLER_NAME_PATTERN + r"-" + _HANDLER_VERSION_PATTERN _HANDLER_PKG_PATTERN = re.compile(_HANDLER_PATTERN + r'\.zip$', re.IGNORECASE) _DEFAULT_EXT_TIMEOUT_MINUTES = 90 _VALID_HANDLER_STATUS = ['Ready', 'NotReady', "Installing", "Unresponsive"] HANDLER_NAME_PATTERN = re.compile(_HANDLER_NAME_PATTERN, re.IGNORECASE) HANDLER_COMPLETE_NAME_PATTERN = re.compile(_HANDLER_PATTERN + r'$', re.IGNORECASE) HANDLER_PKG_EXT = ".zip" # This is the default value for the env variables, whenever we call a command which is not an update scenario, we # set the env variable value to NOT_RUN to reduce ambiguity for the extension publishers NOT_RUN = "NOT_RUN" # Max size of individual status file _MAX_STATUS_FILE_SIZE_IN_BYTES = 128 * 1024 # 128K # Truncating length of fields. _MAX_STATUS_MESSAGE_LENGTH = 1024 # 1k message allowed to be shown in the portal. _MAX_SUBSTATUS_FIELD_LENGTH = 10 * 1024 # Making 10K; allowing fields to have enough debugging information.. _TRUNCATED_SUFFIX = u" ... [TRUNCATED]" # Status file specific retries and delays. _NUM_OF_STATUS_FILE_RETRIES = 5 _STATUS_FILE_RETRY_DELAY = 2 # seconds # This is the default sequence number we use when there are no settings available for Handlers _DEFAULT_SEQ_NO = "0" class ExtHandlerStatusValue(object): """ Statuses for Extension Handlers """ ready = "Ready" not_ready = "NotReady" class ExtensionStatusValue(object): """ Statuses for Extensions """ transitioning = "transitioning" warning = "warning" error = "error" success = "success" STRINGS = ['transitioning', 'warning', 'error', 'success'] _EXTENSION_TERMINAL_STATUSES = [ExtensionStatusValue.error, ExtensionStatusValue.success] class ExtCommandEnvVariable(object): Prefix = "AZURE_GUEST_AGENT" DisableReturnCode = "{0}_DISABLE_CMD_EXIT_CODE".format(Prefix) DisableReturnCodeMultipleExtensions = "{0}_DISABLE_CMD_EXIT_CODES_MULTIPLE_EXTENSIONS".format(Prefix) UninstallReturnCode = "{0}_UNINSTALL_CMD_EXIT_CODE".format(Prefix) ExtensionPath = "{0}_EXTENSION_PATH".format(Prefix) ExtensionVersion = "{0}_EXTENSION_VERSION".format(Prefix) ExtensionSeqNumber = "ConfigSequenceNumber" # At par with Windows Guest Agent ExtensionName = "ConfigExtensionName" UpdatingFromVersion = "{0}_UPDATING_FROM_VERSION".format(Prefix) WireProtocolAddress = "{0}_WIRE_PROTOCOL_ADDRESS".format(Prefix) ExtensionSupportedFeatures = "{0}_EXTENSION_SUPPORTED_FEATURES".format(Prefix) def validate_has_key(obj, key, full_key_path): if key not in obj: raise ExtensionStatusError(msg="Invalid status format by extension: Missing {0} key".format(full_key_path), code=ExtensionStatusError.StatusFileMalformed) def validate_in_range(val, valid_range, name): if val not in valid_range: raise ExtensionStatusError(msg="Invalid value {0} in range {1} at the node {2}".format(val, valid_range, name), code=ExtensionStatusError.StatusFileMalformed) def parse_formatted_message(formatted_message): if formatted_message is None: return None validate_has_key(formatted_message, 'lang', 'formattedMessage/lang') validate_has_key(formatted_message, 'message', 'formattedMessage/message') return formatted_message.get('message') def parse_ext_substatus(substatus): # Check extension sub status format validate_has_key(substatus, 'status', 'substatus/status') validate_in_range(substatus['status'], ExtensionStatusValue.STRINGS, 'substatus/status') status = ExtensionSubStatus() status.name = substatus.get('name') status.status = substatus.get('status') status.code = substatus.get('code', 0) formatted_message = substatus.get('formattedMessage') status.message = parse_formatted_message(formatted_message) return status def parse_ext_status(ext_status, data): if data is None: return if not isinstance(data, list): data_string = ustr(data)[:4096] raise ExtensionStatusError(msg="The extension status must be an array: {0}".format(data_string), code=ExtensionStatusError.StatusFileMalformed) if not data: return # Currently, only the first status will be reported data = data[0] # Check extension status format validate_has_key(data, 'status', 'status') status_data = data['status'] validate_has_key(status_data, 'status', 'status/status') status = status_data['status'] if status not in ExtensionStatusValue.STRINGS: status = ExtensionStatusValue.error applied_time = status_data.get('configurationAppliedTime') ext_status.configurationAppliedTime = applied_time ext_status.operation = status_data.get('operation') ext_status.status = status ext_status.code = status_data.get('code', 0) formatted_message = status_data.get('formattedMessage') ext_status.message = parse_formatted_message(formatted_message) substatus_list = status_data.get('substatus', []) # some extensions incorrectly report an empty substatus with a null value if substatus_list is None: substatus_list = [] for substatus in substatus_list: if substatus is not None: ext_status.substatusList.append(parse_ext_substatus(substatus)) def migrate_handler_state(): """ Migrate handler state and status (if they exist) from an agent-owned directory into the handler-owned config directory Notes: - The v2.0.x branch wrote all handler-related state into the handler-owned config directory (e.g., /var/lib/waagent/Microsoft.Azure.Extensions.LinuxAsm-2.0.1/config). - The v2.1.x branch original moved that state into an agent-owned handler state directory (e.g., /var/lib/waagent/handler_state). - This move can cause v2.1.x agents to multiply invoke a handler's install command. It also makes clean-up more difficult since the agent must remove the state as well as the handler directory. """ handler_state_path = os.path.join(conf.get_lib_dir(), "handler_state") if not os.path.isdir(handler_state_path): return for handler_path in glob.iglob(os.path.join(handler_state_path, "*")): handler = os.path.basename(handler_path) handler_config_path = os.path.join(conf.get_lib_dir(), handler, "config") if os.path.isdir(handler_config_path): for file in ("State", "Status"): # pylint: disable=redefined-builtin from_path = os.path.join(handler_state_path, handler, file.lower()) to_path = os.path.join(handler_config_path, "Handler" + file) if os.path.isfile(from_path) and not os.path.isfile(to_path): try: shutil.move(from_path, to_path) except Exception as e: logger.warn( "Exception occurred migrating {0} {1} file: {2}", handler, file, str(e)) try: shutil.rmtree(handler_state_path) except Exception as e: logger.warn("Exception occurred removing {0}: {1}", handler_state_path, str(e)) return class ExtHandlerState(object): NotInstalled = "NotInstalled" Installed = "Installed" Enabled = "Enabled" FailedUpgrade = "FailedUpgrade" class GoalStateStatus(object): """ This is an Enum to define the State of the GoalState as a whole. This is reported as part of the 'vmArtifactsAggregateStatus.goalStateAggregateStatus' in the status blob. Note: not to be confused with the State of the ExtHandler which reported as part of 'handlerAggregateStatus' """ Success = "Success" Failed = "Failed" # The following field is not used now but would be needed once Status reporting is moved to a separate thread. Initialize = "Initialize" Transitioning = "Transitioning" def get_exthandlers_handler(protocol): return ExtHandlersHandler(protocol) def list_agent_lib_directory(skip_agent_package=True, ignore_names=None): lib_dir = conf.get_lib_dir() for name in os.listdir(lib_dir): path = os.path.join(lib_dir, name) if ignore_names is not None and any(ignore_names) and name in ignore_names: continue if skip_agent_package and (version.is_agent_package(path) or version.is_agent_path(path)): continue yield name, path class ExtHandlersHandler(object): def __init__(self, protocol): self.protocol = protocol self.ext_handlers = None # The GoalState Aggregate status needs to report the last status of the GoalState. Since we only process # extensions on goal state change, we need to maintain its state. # Setting the status to None here. This would be overridden as soon as the first GoalState is processed self.__gs_aggregate_status = None self.report_status_error_state = ErrorState() def __last_gs_unsupported(self): # Return if the last GoalState was unsupported return self.__gs_aggregate_status is not None and \ self.__gs_aggregate_status.status == GoalStateStatus.Failed and \ self.__gs_aggregate_status.code == GoalStateAggregateStatusCodes.GoalStateUnsupportedRequiredFeatures def run(self): try: gs = self.protocol.get_goal_state() egs = gs.extensions_goal_state # self.ext_handlers needs to be initialized before returning, since status reporting depends on it; also # we make a deep copy of the extensions, since changes are made to self.ext_handlers while processing the extensions self.ext_handlers = copy.deepcopy(egs.extensions) if not self._extension_processing_allowed(): return utc_start = datetime.datetime.utcnow() error = None message = "ProcessExtensionsGoalState started [{0} channel: {1} source: {2} activity: {3} correlation {4} created: {5}]".format( egs.id, egs.channel, egs.source, egs.activity_id, egs.correlation_id, egs.created_on_timestamp) logger.info('') logger.info(message) add_event(op=WALAEventOperation.ExtensionProcessing, message=message) try: self.__process_and_handle_extensions(egs.svd_sequence_number, egs.id) self._cleanup_outdated_handlers() except Exception as e: error = u"Error processing extensions:{0}".format(textutil.format_exception(e)) finally: duration = elapsed_milliseconds(utc_start) if error is None: message = 'ProcessExtensionsGoalState completed [{0} {1} ms]\n'.format(egs.id, duration) logger.info(message) else: message = 'ProcessExtensionsGoalState failed [{0} {1} ms]\n{2}'.format(egs.id, duration, error) logger.error(message) add_event(op=WALAEventOperation.ExtensionProcessing, is_success=(error is None), message=message, log_event=False, duration=duration) except Exception as error: msg = u"ProcessExtensionsInGoalState - Exception processing extension handlers:{0}".format(textutil.format_exception(error)) logger.error(msg) add_event(op=WALAEventOperation.ExtensionProcessing, is_success=False, message=msg, log_event=False) def __get_unsupported_features(self): required_features = self.protocol.get_goal_state().extensions_goal_state.required_features supported_features = get_agent_supported_features_list_for_crp() return [feature for feature in required_features if feature not in supported_features] def __process_and_handle_extensions(self, svd_sequence_number, goal_state_id): try: # Verify we satisfy all required features, if any. If not, report failure here itself, no need to process anything further. unsupported_features = self.__get_unsupported_features() if any(unsupported_features): msg = "Failing GS {0} as Unsupported features found: {1}".format(goal_state_id, ', '.join(unsupported_features)) logger.warn(msg) self.__gs_aggregate_status = GoalStateAggregateStatus(status=GoalStateStatus.Failed, seq_no=svd_sequence_number, code=GoalStateAggregateStatusCodes.GoalStateUnsupportedRequiredFeatures, message=msg) add_event(op=WALAEventOperation.GoalStateUnsupportedFeatures, is_success=False, message=msg, log_event=False) else: self.handle_ext_handlers(goal_state_id) self.__gs_aggregate_status = GoalStateAggregateStatus(status=GoalStateStatus.Success, seq_no=svd_sequence_number, code=GoalStateAggregateStatusCodes.Success, message="GoalState executed successfully") except Exception as error: msg = "Unexpected error when processing goal state:{0}".format(textutil.format_exception(error)) self.__gs_aggregate_status = GoalStateAggregateStatus(status=GoalStateStatus.Failed, seq_no=svd_sequence_number, code=GoalStateAggregateStatusCodes.GoalStateUnknownFailure, message=msg) logger.warn(msg) add_event(op=WALAEventOperation.ExtensionProcessing, is_success=False, message=msg, log_event=False) @staticmethod def get_ext_handler_instance_from_path(name, path, protocol, skip_handlers=None): if not os.path.isdir(path) or re.match(HANDLER_NAME_PATTERN, name) is None: return None separator = name.rfind('-') handler_name = name[0:separator] if skip_handlers is not None and handler_name in skip_handlers: # Handler in skip_handlers list, not parsing it return None eh = Extension(name=handler_name) eh.version = str(FlexibleVersion(name[separator + 1:])) return ExtHandlerInstance(eh, protocol) def _cleanup_outdated_handlers(self): # Skip cleanup if the previous GS was Unsupported if self.__last_gs_unsupported(): return handlers = [] pkgs = [] ext_handlers_in_gs = [ext_handler.name for ext_handler in self.ext_handlers] # Build a collection of uninstalled handlers and orphaned packages # Note: # -- An orphaned package is one without a corresponding handler # directory for item, path in list_agent_lib_directory(skip_agent_package=True): try: handler_instance = ExtHandlersHandler.get_ext_handler_instance_from_path(name=item, path=path, protocol=self.protocol, skip_handlers=ext_handlers_in_gs) if handler_instance is not None: # Since this handler name doesn't exist in the GS, marking it for deletion handlers.append(handler_instance) continue except Exception: continue if os.path.isfile(path) and \ not os.path.isdir(path[0:-len(HANDLER_PKG_EXT)]): if not re.match(_HANDLER_PKG_PATTERN, item): continue pkgs.append(path) # Then, remove the orphaned packages for pkg in pkgs: try: os.remove(pkg) logger.verbose("Removed orphaned extension package {0}".format(pkg)) except OSError as e: logger.warn("Failed to remove orphaned package {0}: {1}".format(pkg, e.strerror)) # Finally, remove the directories and packages of the orphaned handlers, i.e. Any extension directory that # is still in the FileSystem but not in the GoalState for handler in handlers: handler.remove_ext_handler() pkg = os.path.join(conf.get_lib_dir(), handler.get_full_name() + HANDLER_PKG_EXT) if os.path.isfile(pkg): try: os.remove(pkg) logger.verbose("Removed extension package {0}".format(pkg)) except OSError as e: logger.warn("Failed to remove extension package {0}: {1}".format(pkg, e.strerror)) def _extension_processing_allowed(self): if not conf.get_extensions_enabled(): logger.verbose("Extension handling is disabled") return False if conf.get_enable_overprovisioning(): if self.protocol.get_goal_state().extensions_goal_state.on_hold: logger.info("Extension handling is on hold") return False return True @staticmethod def __get_dependency_level(tup): (extension, handler) = tup if extension is not None: return extension.dependency_level_sort_key(handler.state) return handler.dependency_level_sort_key() def __get_sorted_extensions_for_processing(self): all_extensions = [] for handler in self.ext_handlers: if any(handler.settings): all_extensions.extend([(ext, handler) for ext in handler.settings]) else: # We need to process the Handler even if no settings specified from CRP (legacy behavior) logger.info("No extension/run-time settings settings found for {0}".format(handler.name)) all_extensions.append((None, handler)) all_extensions.sort(key=self.__get_dependency_level) return all_extensions def handle_ext_handlers(self, goal_state_id): if not self.ext_handlers: logger.info("No extension handlers found, not processing anything.") return wait_until = datetime.datetime.utcnow() + datetime.timedelta(minutes=_DEFAULT_EXT_TIMEOUT_MINUTES) all_extensions = self.__get_sorted_extensions_for_processing() # Since all_extensions are sorted based on sort_key, the last element would be the maximum based on the sort_key max_dep_level = self.__get_dependency_level(all_extensions[-1]) if any(all_extensions) else 0 depends_on_err_msg = None for extension, ext_handler in all_extensions: handler_i = ExtHandlerInstance(ext_handler, self.protocol, extension=extension) # In case of depends-on errors, we skip processing extensions if there was an error processing dependent extensions. # But CRP is still waiting for some status back for the skipped extensions. In order to propagate the status back to CRP, # we will report status back here with the relevant error message for each of the dependent extension. if depends_on_err_msg is not None: # For MC extensions, report the HandlerStatus as is and create a new placeholder per extension if doesnt exist if handler_i.should_perform_multi_config_op(extension): # Ensure some handler status exists for the Handler, if not, set it here if handler_i.get_handler_status() is None: handler_i.set_handler_status(message=depends_on_err_msg, code=-1) handler_i.create_status_file_if_not_exist(extension, status=ExtensionStatusValue.error, code=-1, operation=WALAEventOperation.ExtensionProcessing, message=depends_on_err_msg) # For SC extensions, overwrite the HandlerStatus with the relevant message else: handler_i.set_handler_status(message=depends_on_err_msg, code=-1) continue # Process extensions and get if it was successfully executed or not extension_success = self.handle_ext_handler(handler_i, extension, goal_state_id) dep_level = self.__get_dependency_level((extension, ext_handler)) if 0 <= dep_level < max_dep_level: extension_full_name = handler_i.get_extension_full_name(extension) try: # Do no wait for extension status if the handler failed if not extension_success: raise Exception("Skipping processing of extensions since execution of dependent extension {0} failed".format( extension_full_name)) # Wait for the extension installation until it is handled. # This is done for the install and enable. Not for the uninstallation. # If handled successfully, proceed with the current handler. # Otherwise, skip the rest of the extension installation. self.wait_for_handler_completion(handler_i, wait_until, extension=extension) except Exception as error: logger.warn( "Dependent extension {0} failed or timed out, will skip processing the rest of the extensions".format( extension_full_name)) depends_on_err_msg = ustr(error) add_event(name=extension_full_name, version=handler_i.ext_handler.version, op=WALAEventOperation.ExtensionProcessing, is_success=False, message=depends_on_err_msg) @staticmethod def wait_for_handler_completion(handler_i, wait_until, extension=None): """ Check the status of the extension being handled. Wait until it has a terminal state or times out. :raises: Exception if it is not handled successfully. """ extension_name = handler_i.get_extension_full_name(extension) # If the handler had no settings, we should not wait at all for handler to report status. if extension is None: logger.info("No settings found for {0}, not waiting for it's status".format(extension_name)) return try: ext_completed, status = False, None # Keep polling for the extension status until it succeeds or times out while datetime.datetime.utcnow() <= wait_until: ext_completed, status = handler_i.is_ext_handling_complete(extension) if ext_completed: break time.sleep(5) except Exception as e: msg = "Failed to wait for Handler completion due to unknown error. Marking the dependent extension as failed: {0}, {1}".format( extension_name, textutil.format_exception(e)) raise Exception(msg) # In case of timeout or terminal error state, we log it and raise # Incase extension reported status at the last sec, we should prioritize reporting status over timeout if not ext_completed and datetime.datetime.utcnow() > wait_until: msg = "Dependent Extension {0} did not reach a terminal state within the allowed timeout. Last status was {1}".format( extension_name, status) raise Exception(msg) if status != ExtensionStatusValue.success: msg = "Dependent Extension {0} did not succeed. Status was {1}".format(extension_name, status) raise Exception(msg) def handle_ext_handler(self, ext_handler_i, extension, goal_state_id): """ Execute the requested command for the handler and return if success :param ext_handler_i: The ExtHandlerInstance object to execute the command on :param extension: The extension settings on which to run the command on :param goal_state_id: ID of the current GoalState :return: True if the operation was successful, False if not """ try: # Ensure the extension config was valid if ext_handler_i.ext_handler.is_invalid_setting: raise ExtensionsGoalStateError(ext_handler_i.ext_handler.invalid_setting_reason) handler_state = ext_handler_i.ext_handler.state # The Guest Agent currently only supports 1 installed version per extension on the VM. # If the extension version is unregistered and the customers wants to uninstall the extension, # we should let it go through even if the installed version doesnt exist in Handler manifest (PIR) anymore. # If target state is enabled and version not found in manifest, do not process the extension. if ext_handler_i.decide_version(target_state=handler_state, extension=extension) is None and handler_state == ExtensionRequestedState.Enabled: handler_version = ext_handler_i.ext_handler.version name = ext_handler_i.ext_handler.name err_msg = "Unable to find version {0} in manifest for extension {1}".format(handler_version, name) ext_handler_i.set_operation(WALAEventOperation.Download) raise ExtensionError(msg=err_msg) # Handle everything on an extension level rather than Handler level ext_handler_i.logger.info("Target handler state: {0} [{1}]", handler_state, goal_state_id) if handler_state == ExtensionRequestedState.Enabled: self.handle_enable(ext_handler_i, extension) elif handler_state == ExtensionRequestedState.Disabled: self.handle_disable(ext_handler_i, extension) elif handler_state == ExtensionRequestedState.Uninstall: self.handle_uninstall(ext_handler_i, extension=extension) else: message = u"Unknown ext handler state:{0}".format(handler_state) raise ExtensionError(message) return True except MultiConfigExtensionEnableError as error: ext_name = ext_handler_i.get_extension_full_name(extension) err_msg = "Error processing MultiConfig extension {0}: {1}".format(ext_name, ustr(error)) # This error is only thrown for enable operation on MultiConfig extension. # Since these are maintained by the extensions, the expectation here is that they would update their status files appropriately with their errors. # The extensions should already have a placeholder status file, but incase they dont, setting one here to fail fast. ext_handler_i.create_status_file_if_not_exist(extension, status=ExtensionStatusValue.error, code=error.code, operation=ext_handler_i.operation, message=err_msg) add_event(name=ext_name, version=ext_handler_i.ext_handler.version, op=ext_handler_i.operation, is_success=False, log_event=True, message=err_msg) except ExtensionsGoalStateError as error: # Catch and report Invalid ExtensionConfig errors here to fail fast rather than timing out after 90 min err_msg = "Ran into config errors: {0}. \nPlease retry again as another operation with updated settings".format( ustr(error)) self.__handle_and_report_ext_handler_errors(ext_handler_i, error, report_op=WALAEventOperation.InvalidExtensionConfig, message=err_msg, extension=extension) except ExtensionUpdateError as error: # Not reporting the error as it has already been reported from the old version self.__handle_and_report_ext_handler_errors(ext_handler_i, error, ext_handler_i.operation, ustr(error), report=False, extension=extension) except ExtensionDownloadError as error: msg = "Failed to download artifacts: {0}".format(ustr(error)) self.__handle_and_report_ext_handler_errors(ext_handler_i, error, report_op=WALAEventOperation.Download, message=msg, extension=extension) except ExtensionError as error: self.__handle_and_report_ext_handler_errors(ext_handler_i, error, ext_handler_i.operation, ustr(error), extension=extension) except Exception as error: error.code = -1 self.__handle_and_report_ext_handler_errors(ext_handler_i, error, ext_handler_i.operation, ustr(error), extension=extension) return False @staticmethod def __handle_and_report_ext_handler_errors(ext_handler_i, error, report_op, message, report=True, extension=None): # This function is only called for Handler level errors, we capture MultiConfig errors separately, # so report only HandlerStatus here. ext_handler_i.set_handler_status(message=message, code=error.code) # If the handler supports multi-config, create a status file with failed status if no status file exists. # This is for correctly reporting errors back to CRP for failed Handler level operations for MultiConfig extensions. # In case of Handler failures, we will retry each time for each extension, so we need to create a status # file with failure since the extensions wont be called where they can create their status files. # This way we guarantee reporting back to CRP if ext_handler_i.should_perform_multi_config_op(extension): ext_handler_i.create_status_file_if_not_exist(extension, status=ExtensionStatusValue.error, code=error.code, operation=report_op, message=message) if report: name = ext_handler_i.get_extension_full_name(extension) handler_version = ext_handler_i.ext_handler.version add_event(name=name, version=handler_version, op=report_op, is_success=False, log_event=True, message=message) def handle_enable(self, ext_handler_i, extension): """ 1- Ensure the handler is installed 2- Check if extension is enabled or disabled and then process accordingly """ uninstall_exit_code = None old_ext_handler_i = ext_handler_i.get_installed_ext_handler() current_handler_state = ext_handler_i.get_handler_state() ext_handler_i.logger.info("[Enable] current handler state is: {0}", current_handler_state.lower()) # We go through the entire process of downloading and initializing the extension if it's either a fresh # extension or if it's a retry of a previously failed upgrade. if current_handler_state == ExtHandlerState.NotInstalled or current_handler_state == ExtHandlerState.FailedUpgrade: self.__setup_new_handler(ext_handler_i, extension) if old_ext_handler_i is None: ext_handler_i.install(extension=extension) elif ext_handler_i.version_ne(old_ext_handler_i): # This is a special case, we need to update the handler version here but to do that we need to also # disable each enabled extension of this handler. uninstall_exit_code = ExtHandlersHandler._update_extension_handler_and_return_if_failed( old_ext_handler_i, ext_handler_i, extension) else: ext_handler_i.ensure_consistent_data_for_mc() ext_handler_i.update_settings(extension) self.__handle_extension(ext_handler_i, extension, uninstall_exit_code) @staticmethod def __setup_new_handler(ext_handler_i, extension): ext_handler_i.set_handler_state(ExtHandlerState.NotInstalled) ext_handler_i.download() ext_handler_i.initialize() ext_handler_i.update_settings(extension) @staticmethod def __handle_extension(ext_handler_i, extension, uninstall_exit_code): # Check if extension level settings provided for the handler, if not, call enable for the handler. # This is legacy behavior, we can have handlers with no settings. if extension is None: ext_handler_i.enable() return # MultiConfig: Handle extension level ops here ext_handler_i.logger.info("Requested extension state: {0}", extension.state) if extension.state == ExtensionState.Enabled: ext_handler_i.enable(extension, uninstall_exit_code=uninstall_exit_code) elif extension.state == ExtensionState.Disabled: # Only disable extension if the requested state == Disabled and current state is != Disabled if ext_handler_i.get_extension_state(extension) != ExtensionState.Disabled: # Extensions can only be disabled for Multi Config extensions. Disable operation for extension is # tantamount to uninstalling Handler so ignoring errors incase of Disable failure and deleting state. ext_handler_i.disable(extension, ignore_error=True) else: ext_handler_i.logger.info("Extension already disabled, not doing anything") else: raise ExtensionsGoalStateError( "Unknown requested state for Extension {0}: {1}".format(extension.name, extension.state)) @staticmethod def _update_extension_handler_and_return_if_failed(old_ext_handler_i, ext_handler_i, extension=None): def execute_old_handler_command_and_return_if_succeeds(func): """ Created a common wrapper to execute all commands that need to be executed from the old handler so that it can have a common exception handling mechanism :param func: The command to be executed on the old handler :return: True if command execution succeeds and False if it fails """ continue_on_update_failure = False exit_code = 0 try: continue_on_update_failure = ext_handler_i.load_manifest().is_continue_on_update_failure() func() except ExtensionError as e: # Reporting the event with the old handler and raising a new ExtensionUpdateError to set the # handler status on the new version msg = "%s; ContinueOnUpdate: %s" % (ustr(e), continue_on_update_failure) old_ext_handler_i.report_event(message=msg, is_success=False) if not continue_on_update_failure: raise ExtensionUpdateError(msg) exit_code = e.code if isinstance(e, ExtensionOperationError): exit_code = e.exit_code # pylint: disable=E1101 logger.info("Continue on Update failure flag is set, proceeding with update") return exit_code disable_exit_codes = defaultdict(lambda: NOT_RUN) # We only want to disable the old handler if it is currently enabled; no other state makes sense. if old_ext_handler_i.get_handler_state() == ExtHandlerState.Enabled: # Corner case - If the old handler is a Single config Handler with no extensions at all, # we should just disable the handler if not old_ext_handler_i.supports_multi_config and not any(old_ext_handler_i.extensions): disable_exit_codes[ old_ext_handler_i.ext_handler.name] = execute_old_handler_command_and_return_if_succeeds( func=partial(old_ext_handler_i.disable, extension=None)) # Else we disable all enabled extensions of this handler # Note: If MC is supported this will disable only enabled_extensions else it will disable all extensions for old_ext in old_ext_handler_i.enabled_extensions: disable_exit_codes[old_ext.name] = execute_old_handler_command_and_return_if_succeeds( func=partial(old_ext_handler_i.disable, extension=old_ext)) ext_handler_i.copy_status_files(old_ext_handler_i) if ext_handler_i.version_gt(old_ext_handler_i): ext_handler_i.update(disable_exit_codes=disable_exit_codes, updating_from_version=old_ext_handler_i.ext_handler.version, extension=extension) else: updating_from_version = ext_handler_i.ext_handler.version old_ext_handler_i.update(handler_version=updating_from_version, disable_exit_codes=disable_exit_codes, updating_from_version=updating_from_version, extension=extension) uninstall_exit_code = execute_old_handler_command_and_return_if_succeeds( func=partial(old_ext_handler_i.uninstall, extension=extension)) old_ext_handler_i.remove_ext_handler() ext_handler_i.update_with_install(uninstall_exit_code=uninstall_exit_code, extension=extension) return uninstall_exit_code def handle_disable(self, ext_handler_i, extension=None): """ Disable is a legacy behavior, CRP doesn't support it, its only for XML based extensions. In case we get a disable request, just disable that extension. """ handler_state = ext_handler_i.get_handler_state() ext_handler_i.logger.info("[Disable] current handler state is: {0}", handler_state.lower()) if handler_state == ExtHandlerState.Enabled: ext_handler_i.disable(extension) def handle_uninstall(self, ext_handler_i, extension): """ To Uninstall the handler, first ensure all extensions are disabled 1- Disable all enabled extensions first if Handler is Enabled and then Disable the handler (disabled extensions wont have any extensions dependent on them so we can just go ahead and remove all of them at once if HandlerState==Uninstall. CRP will only set the HandlerState to Uninstall if all its extensions are set to be disabled) 2- Finally uninstall the handler """ handler_state = ext_handler_i.get_handler_state() ext_handler_i.logger.info("[Uninstall] current handler state is: {0}", handler_state.lower()) if handler_state != ExtHandlerState.NotInstalled: if handler_state == ExtHandlerState.Enabled: # Corner case - Single config Handler with no extensions at all # If there are no extension settings for Handler, we should just disable the handler if not ext_handler_i.supports_multi_config and not any(ext_handler_i.extensions): ext_handler_i.disable() # If Handler is Enabled, there should be atleast 1 enabled extension for the handler # Note: If MC is supported this will disable only enabled_extensions else it will disable all extensions for enabled_ext in ext_handler_i.enabled_extensions: ext_handler_i.disable(enabled_ext) # Try uninstalling the extension and swallow any exceptions in case of failures after logging them try: ext_handler_i.uninstall(extension=extension) except ExtensionError as e: ext_handler_i.report_event(message=ustr(e), is_success=False) ext_handler_i.remove_ext_handler() def __get_handlers_on_file_system(self, goal_state_changed): handlers_to_report = [] # Ignoring the `history` and `events` directories as they're not handlers and are agent-generated for item, path in list_agent_lib_directory(skip_agent_package=True, ignore_names=[EVENTS_DIRECTORY, ARCHIVE_DIRECTORY_NAME]): try: handler_instance = ExtHandlersHandler.get_ext_handler_instance_from_path(name=item, path=path, protocol=self.protocol) if handler_instance is not None: ext_handler = handler_instance.ext_handler # For each handler we need to add extensions to report their status. # For Single Config, we just need to add one extension with name as Handler Name # For Multi Config, walk the config directory and find all unique extension names # and add them as extensions to the handler. extensions_names = set() # Settings for Multi Config are saved as ..settings. # Use this pattern to determine if Handler supports Multi Config or not and add extensions for settings_path in glob.iglob(os.path.join(handler_instance.get_conf_dir(), "*.*.settings")): match = re.search("(?P\\w+)\\.\\d+\\.settings", settings_path) if match is not None: extensions_names.add(match.group("extname")) ext_handler.supports_multi_config = True # If nothing found with that pattern then its a Single Config, add an extension with Handler Name if not any(extensions_names): extensions_names.add(ext_handler.name) for ext_name in extensions_names: ext = ExtensionSettings(name=ext_name) # Fetch the last modified sequence number seq_no, _ = handler_instance.get_status_file_path(ext) ext.sequenceNumber = seq_no # Append extension to the list of extensions for the handler ext_handler.settings.append(ext) handlers_to_report.append(ext_handler) except Exception as error: # Log error once per goal state if goal_state_changed: logger.warn("Can't fetch ExtHandler from path: {0}; Error: {1}".format(path, ustr(error))) return handlers_to_report def report_ext_handlers_status(self, goal_state_changed=False, vm_agent_update_status=None, vm_agent_supports_fast_track=False): """ Go through handler_state dir, collect and report status. Returns the status it reported, or None if an error occurred. """ try: vm_status = VMStatus(status="Ready", message="Guest Agent is running", gs_aggregate_status=self.__gs_aggregate_status, vm_agent_update_status=vm_agent_update_status) vm_status.vmAgent.set_supports_fast_track(vm_agent_supports_fast_track) handlers_to_report = [] # In case of Unsupported error, report the status of the handlers in the VM if self.__last_gs_unsupported(): handlers_to_report = self.__get_handlers_on_file_system(goal_state_changed) # If GoalState supported, report the status of extension handlers that were requested by the GoalState elif not self.__last_gs_unsupported() and self.ext_handlers is not None: handlers_to_report = self.ext_handlers for ext_handler in handlers_to_report: try: self.report_ext_handler_status(vm_status, ext_handler, goal_state_changed) except ExtensionError as error: add_event(op=WALAEventOperation.ExtensionProcessing, is_success=False, message=ustr(error)) logger.verbose("Report vm agent status") try: self.protocol.report_vm_status(vm_status) logger.verbose("Completed vm agent status report successfully") self.report_status_error_state.reset() except ProtocolNotFoundError as error: self.report_status_error_state.incr() message = "Failed to report vm agent status: {0}".format(error) logger.verbose(message) except ProtocolError as error: self.report_status_error_state.incr() message = "Failed to report vm agent status: {0}".format(error) add_event(AGENT_NAME, version=CURRENT_VERSION, op=WALAEventOperation.ExtensionProcessing, is_success=False, message=message) if self.report_status_error_state.is_triggered(): message = "Failed to report vm agent status for more than {0}" \ .format(self.report_status_error_state.min_timedelta) add_event(AGENT_NAME, version=CURRENT_VERSION, op=WALAEventOperation.ReportStatusExtended, is_success=False, message=message) self.report_status_error_state.reset() return vm_status except Exception as error: msg = u"Failed to report status: {0}".format(textutil.format_exception(error)) logger.warn(msg) add_event(AGENT_NAME, version=CURRENT_VERSION, op=WALAEventOperation.ReportStatus, is_success=False, message=msg) return None def get_ext_handlers_status_debug_info(self, vm_status): status_blob_text = self.protocol.get_status_blob_data() if status_blob_text is None: status_blob_text = "" support_multi_config = {} vm_status_data = get_properties(vm_status) vm_handler_statuses = vm_status_data.get('vmAgent', {}).get('extensionHandlers') for handler_status in vm_handler_statuses: if handler_status.get('name') is not None: support_multi_config[handler_status.get('name')] = handler_status.get('supports_multi_config') debug_text = json.dumps({ "agentName": AGENT_NAME, "daemonVersion": str(version.get_daemon_version()), "pythonVersion": "Python: {0}.{1}.{2}".format(PY_VERSION_MAJOR, PY_VERSION_MINOR, PY_VERSION_MICRO), "extensionSupportedFeatures": [name for name, _ in get_agent_supported_features_list_for_extensions().items()], "supportsMultiConfig": support_multi_config }) return '''{{ "__comment__": "The __status__ property is the actual status reported to CRP", "__status__": {0}, "__debug__": {1} }} '''.format(status_blob_text, debug_text) def report_ext_handler_status(self, vm_status, ext_handler, goal_state_changed): ext_handler_i = ExtHandlerInstance(ext_handler, self.protocol) handler_status = ext_handler_i.get_handler_status() # If nothing available, skip reporting if handler_status is None: # We should always have some handler status if requested state != Uninstall irrespective of single or # multi-config. If state is != Uninstall, report error if ext_handler.state != ExtensionRequestedState.Uninstall: msg = "No handler status found for {0}. Not reporting anything for it.".format(ext_handler.name) ext_handler_i.report_error_on_incarnation_change(goal_state_changed, log_msg=msg, event_msg=msg) return handler_state = ext_handler_i.get_handler_state() ext_handler_statuses = [] # For MultiConfig, we need to report status per extension even for Handler level failures. # If we have HandlerStatus for a MultiConfig handler and GS is requesting for it, we would report status per # extension even if HandlerState == NotInstalled (Sample scenario: ExtensionsGoalStateError, DecideVersionError, etc) if handler_state != ExtHandlerState.NotInstalled or ext_handler.supports_multi_config: # Since we require reading the Manifest for reading the heartbeat, this would fail if HandlerManifest not found. # Only try to read heartbeat if HandlerState != NotInstalled. if handler_state != ExtHandlerState.NotInstalled: # Heartbeat is a handler level thing only, so we dont need to modify this try: heartbeat = ext_handler_i.collect_heartbeat() if heartbeat is not None: handler_status.status = heartbeat.get('status') if 'formattedMessage' in heartbeat: handler_status.message = parse_formatted_message(heartbeat.get('formattedMessage')) except ExtensionError as e: ext_handler_i.set_handler_status(message=ustr(e), code=e.code) ext_handler_statuses = ext_handler_i.get_extension_handler_statuses(handler_status, goal_state_changed) # If not any extension status reported, report the Handler status if not any(ext_handler_statuses): ext_handler_statuses.append(handler_status) vm_status.vmAgent.extensionHandlers.extend(ext_handler_statuses) class ExtHandlerInstance(object): def __init__(self, ext_handler, protocol, execution_log_max_size=(10 * 1024 * 1024), extension=None): self.ext_handler = ext_handler self.protocol = protocol self.operation = None self.pkg = None self.pkg_file = None self.logger = None self.set_logger(extension=extension, execution_log_max_size=execution_log_max_size) @property def supports_multi_config(self): return self.ext_handler.supports_multi_config @property def extensions(self): return self.ext_handler.settings @property def enabled_extensions(self): """ In case of Single config, just return all the extensions of the handler (expectation being that there'll only be a single extension per handler). We will not be maintaining extension level state for Single config Handlers """ if self.supports_multi_config: return [ext for ext in self.extensions if self.get_extension_state(ext) == ExtensionState.Enabled] return self.extensions def get_extension_full_name(self, extension=None): """ Get the full name of the extension .. :param extension: The requested extension :return: if MultiConfig not supported or extension == None, else . """ if self.should_perform_multi_config_op(extension): return "{0}.{1}".format(self.ext_handler.name, extension.name) return self.ext_handler.name def __set_command_execution_log(self, extension, execution_log_max_size): try: fileutil.mkdir(self.get_log_dir(), mode=0o755) except IOError as e: self.logger.error(u"Failed to create extension log dir: {0}", e) else: log_file_name = "CommandExecution.log" if not self.should_perform_multi_config_op( extension) else "CommandExecution_{0}.log".format(extension.name) log_file = os.path.join(self.get_log_dir(), log_file_name) self.__truncate_file_head(log_file, execution_log_max_size, self.get_extension_full_name(extension)) self.logger.add_appender(logger.AppenderType.FILE, logger.LogLevel.INFO, log_file) @staticmethod def __truncate_file_head(filename, max_size, extension_name): try: if os.stat(filename).st_size <= max_size: return with open(filename, "rb") as existing_file: existing_file.seek(-1 * max_size, 2) _ = existing_file.readline() with open(filename + ".tmp", "wb") as tmp_file: shutil.copyfileobj(existing_file, tmp_file) os.rename(filename + ".tmp", filename) except (IOError, OSError) as e: if is_file_not_found_error(e): # If CommandExecution.log does not exist, it's not noteworthy; # this just means that no extension with self.ext_handler.name is # installed. return logger.error( "Exception occurred while attempting to truncate {0} for extension {1}. Exception is: {2}", filename, extension_name, ustr(e)) for f in (filename, filename + ".tmp"): try: os.remove(f) except (IOError, OSError) as cleanup_exception: if is_file_not_found_error(cleanup_exception): logger.info("File '{0}' does not exist.", f) else: logger.warn("Exception occurred while attempting to remove file '{0}': {1}", f, cleanup_exception) def decide_version(self, target_state=None, extension=None): self.logger.verbose("Decide which version to use") try: manifest = self.protocol.get_goal_state().fetch_extension_manifest(self.ext_handler.name, self.ext_handler.manifest_uris) pkg_list = manifest.pkg_list except ProtocolError as e: raise ExtensionError("Failed to get ext handler pkgs", e) except ExtensionDownloadError: self.set_operation(WALAEventOperation.Download) raise # Determine the desired and installed versions requested_version = FlexibleVersion(str(self.ext_handler.version)) installed_version_string = self.get_installed_version() installed_version = requested_version if installed_version_string is None else FlexibleVersion(installed_version_string) # Divide packages # - Find the installed package (its version must exactly match) # - Find the internal candidate (its version must exactly match) # - Separate the public packages selected_pkg = None installed_pkg = None pkg_list.versions.sort(key=lambda p: FlexibleVersion(p.version)) for pkg in pkg_list.versions: pkg_version = FlexibleVersion(pkg.version) if pkg_version == installed_version: installed_pkg = pkg if requested_version.matches(pkg_version): selected_pkg = pkg # Finally, update the version only if not downgrading # Note: # - A downgrade, which will be bound to the same major version, # is allowed if the installed version is no longer available if target_state in (ExtensionRequestedState.Uninstall, ExtensionRequestedState.Disabled): if installed_pkg is None: msg = "Failed to find installed version: {0} of Handler: {1} in handler manifest to uninstall.".format( installed_version, self.ext_handler.name) self.logger.warn(msg) self.pkg = installed_pkg self.ext_handler.version = str(installed_version) \ if installed_version is not None else None else: self.pkg = selected_pkg if self.pkg is not None: self.ext_handler.version = str(selected_pkg.version) if self.pkg is not None: self.logger.verbose("Use version: {0}", self.pkg.version) # We reset the logger here incase the handler version changes if not requested_version.matches(FlexibleVersion(self.ext_handler.version)): self.set_logger(extension=extension) return self.pkg def set_logger(self, execution_log_max_size=(10 * 1024 * 1024), extension=None): prefix = "[{0}]".format(self.get_full_name(extension)) self.logger = logger.Logger(logger.DEFAULT_LOGGER, prefix) self.__set_command_execution_log(extension, execution_log_max_size) def version_gt(self, other): self_version = self.ext_handler.version other_version = other.ext_handler.version return FlexibleVersion(self_version) > FlexibleVersion(other_version) def version_ne(self, other): self_version = self.ext_handler.version other_version = other.ext_handler.version return FlexibleVersion(self_version) != FlexibleVersion(other_version) def get_installed_ext_handler(self): latest_version = self.get_installed_version() if latest_version is None: return None installed_handler = copy.deepcopy(self.ext_handler) installed_handler.version = latest_version return ExtHandlerInstance(installed_handler, self.protocol) def get_installed_version(self): latest_version = None for path in glob.iglob(os.path.join(conf.get_lib_dir(), self.ext_handler.name + "-*")): if not os.path.isdir(path): continue separator = path.rfind('-') version_from_path = FlexibleVersion(path[separator + 1:]) state_path = os.path.join(path, 'config', 'HandlerState') if not os.path.exists(state_path) or fileutil.read_file(state_path) == ExtHandlerState.NotInstalled \ or fileutil.read_file(state_path) == ExtHandlerState.FailedUpgrade: logger.verbose("Ignoring version of uninstalled or failed extension: {0}".format(path)) continue if latest_version is None or latest_version < version_from_path: latest_version = version_from_path return str(latest_version) if latest_version is not None else None def copy_status_files(self, old_ext_handler_i): self.logger.info("Copy status files from old plugin to new") old_ext_dir = old_ext_handler_i.get_base_dir() new_ext_dir = self.get_base_dir() old_ext_mrseq_file = os.path.join(old_ext_dir, "mrseq") if os.path.isfile(old_ext_mrseq_file): logger.info("Migrating {0} to {1}.", old_ext_mrseq_file, new_ext_dir) shutil.copy2(old_ext_mrseq_file, new_ext_dir) else: logger.info("{0} does not exist, no migration is needed.", old_ext_mrseq_file) old_ext_status_dir = old_ext_handler_i.get_status_dir() new_ext_status_dir = self.get_status_dir() if os.path.isdir(old_ext_status_dir): for status_file in os.listdir(old_ext_status_dir): status_file = os.path.join(old_ext_status_dir, status_file) if os.path.isfile(status_file): shutil.copy2(status_file, new_ext_status_dir) def set_operation(self, op): self.operation = op def report_event(self, name=None, message="", is_success=True, duration=0, log_event=True): ext_handler_version = self.ext_handler.version name = self.ext_handler.name if name is None else name add_event(name=name, version=ext_handler_version, message=message, op=self.operation, is_success=is_success, duration=duration, log_event=log_event) def _unzip_extension_package(self, source_file, target_directory): self.logger.info("Unzipping extension package: {0}", source_file) try: zipfile.ZipFile(source_file).extractall(target_directory) except Exception as exception: logger.info("Error while unzipping extension package: {0}", ustr(exception)) os.remove(source_file) if os.path.exists(target_directory): shutil.rmtree(target_directory) return False return True def download(self): begin_utc = datetime.datetime.utcnow() self.set_operation(WALAEventOperation.Download) if self.pkg is None or self.pkg.uris is None or len(self.pkg.uris) == 0: raise ExtensionDownloadError("No package uri found") package_file = os.path.join(conf.get_lib_dir(), self.get_extension_package_zipfile_name()) package_exists = False if os.path.exists(package_file): self.logger.info("Using existing extension package: {0}", package_file) if self._unzip_extension_package(package_file, self.get_base_dir()): package_exists = True else: self.logger.info("The existing extension package is invalid, will ignore it.") if not package_exists: is_fast_track_goal_state = self.protocol.get_goal_state().extensions_goal_state.source == GoalStateSource.FastTrack self.protocol.client.download_zip_package("extension package", self.pkg.uris, package_file, self.get_base_dir(), use_verify_header=is_fast_track_goal_state) self.report_event(message="Download succeeded", duration=elapsed_milliseconds(begin_utc)) self.pkg_file = package_file def ensure_consistent_data_for_mc(self): # If CRP expects Handler to support MC, ensure the HandlerManifest also reflects that. # Even though the HandlerManifest.json is not expected to change once the extension is installed, # CRP can wrongfully request send a Multi-Config GoalState even if the Handler supports only Single Config. # Checking this only if HandlerState == Enable. In case of Uninstall, we dont care. if self.supports_multi_config and not self.load_manifest().supports_multiple_extensions(): raise ExtensionsGoalStateError( "Handler {0} does not support MultiConfig but CRP expects it, failing due to inconsistent data".format( self.ext_handler.name)) def initialize(self): self.logger.info("Initializing extension {0}".format(self.get_full_name())) # Add user execute permission to all files under the base dir for file in fileutil.get_all_files(self.get_base_dir()): # pylint: disable=redefined-builtin fileutil.chmod(file, os.stat(file).st_mode | stat.S_IXUSR) # Save HandlerManifest.json man_file = fileutil.search_file(self.get_base_dir(), 'HandlerManifest.json') if man_file is None: raise ExtensionDownloadError("HandlerManifest.json not found") try: man = fileutil.read_file(man_file, remove_bom=True) fileutil.write_file(self.get_manifest_file(), man) except IOError as e: fileutil.clean_ioerror(e, paths=[self.get_base_dir(), self.pkg_file]) raise ExtensionDownloadError(u"Failed to save HandlerManifest.json", e) self.ensure_consistent_data_for_mc() # Create status and config dir try: status_dir = self.get_status_dir() fileutil.mkdir(status_dir, mode=0o700) conf_dir = self.get_conf_dir() fileutil.mkdir(conf_dir, mode=0o700) if get_supported_feature_by_name(SupportedFeatureNames.ExtensionTelemetryPipeline).is_supported: fileutil.mkdir(self.get_extension_events_dir(), mode=0o700) except IOError as e: fileutil.clean_ioerror(e, paths=[self.get_base_dir(), self.pkg_file]) raise ExtensionDownloadError(u"Failed to initialize extension '{0}'".format(self.get_full_name()), e) # Save HandlerEnvironment.json self.create_handler_env() self.set_extension_resource_limits() def set_extension_resource_limits(self): extension_name = self.get_full_name() # setup the resource limits for extension operations and it's services. man = self.load_manifest() resource_limits = man.get_resource_limits(extension_name, self.ext_handler.version) if not CGroupConfigurator.get_instance().is_extension_resource_limits_setup_completed(extension_name, cpu_quota=resource_limits.get_extension_slice_cpu_quota()): CGroupConfigurator.get_instance().setup_extension_slice( extension_name=extension_name, cpu_quota=resource_limits.get_extension_slice_cpu_quota()) CGroupConfigurator.get_instance().set_extension_services_cpu_memory_quota(resource_limits.get_service_list()) def create_status_file_if_not_exist(self, extension, status, code, operation, message): _, status_path = self.get_status_file_path(extension) if status_path is not None and not os.path.exists(status_path): now = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") status_contents = [ { "version": 1.0, "timestampUTC": now, "status": { "name": self.get_extension_full_name(extension), "operation": operation, "status": status, "code": code, "formattedMessage": { "lang": "en-US", "message": message } } } ] # Create status directory if not exists. This is needed in the case where the Handler fails before even # initializing the directories (ExtensionsGoalStateError, Version deleted from PIR error, etc) if not os.path.exists(os.path.dirname(status_path)): fileutil.mkdir(os.path.dirname(status_path), mode=0o700) self.logger.info("Creating a placeholder status file {0} with status: {1}".format(status_path, status)) fileutil.write_file(status_path, json.dumps(status_contents)) def enable(self, extension=None, uninstall_exit_code=None): try: self._enable_extension(extension, uninstall_exit_code) except ExtensionError as error: if self.should_perform_multi_config_op(extension): raise MultiConfigExtensionEnableError(error) raise # Even if a single extension is enabled for this handler, set the Handler state as Enabled self.set_handler_state(ExtHandlerState.Enabled) self.set_handler_status(status=ExtHandlerStatusValue.ready, message="Plugin enabled") def should_perform_multi_config_op(self, extension): return self.supports_multi_config and extension is not None def _enable_extension(self, extension, uninstall_exit_code): uninstall_exit_code = str(uninstall_exit_code) if uninstall_exit_code is not None else NOT_RUN env = { ExtCommandEnvVariable.UninstallReturnCode: uninstall_exit_code } # This check to call the setup if extension already installed and not called setup before self.set_extension_resource_limits() self.set_operation(WALAEventOperation.Enable) man = self.load_manifest() enable_cmd = man.get_enable_command() self.logger.info("Enable extension: [{0}]".format(enable_cmd)) self.launch_command(enable_cmd, cmd_name="enable", timeout=300, extension_error_code=ExtensionErrorCodes.PluginEnableProcessingFailed, env=env, extension=extension) if self.should_perform_multi_config_op(extension): # Only save extension state if MC supported self.__set_extension_state(extension, ExtensionState.Enabled) # start tracking the extension services cgroup. resource_limits = man.get_resource_limits(self.get_full_name(), self.ext_handler.version) CGroupConfigurator.get_instance().start_tracking_extension_services_cgroups( resource_limits.get_service_list()) def _disable_extension(self, extension=None): self.set_operation(WALAEventOperation.Disable) man = self.load_manifest() disable_cmd = man.get_disable_command() self.logger.info("Disable extension: [{0}]".format(disable_cmd)) self.launch_command(disable_cmd, cmd_name="disable", timeout=900, extension_error_code=ExtensionErrorCodes.PluginDisableProcessingFailed, extension=extension) def disable(self, extension=None, ignore_error=False): try: self._disable_extension(extension) except ExtensionError as error: if not ignore_error: raise msg = "[Ignored Error] Ran into error disabling extension:{0}".format(ustr(error)) self.logger.info(msg) self.report_event(name=self.get_extension_full_name(extension), message=msg, is_success=False, log_event=False) # Clean extension state For Multi Config extensions on Disable if self.should_perform_multi_config_op(extension): self.__remove_extension_state_files(extension) # For Single config, dont check enabled_extensions because no extension state is maintained. # For MultiConfig, Set the handler state to Installed only when all extensions have been disabled if not self.supports_multi_config or not any(self.enabled_extensions): self.set_handler_state(ExtHandlerState.Installed) self.set_handler_status(status=ExtHandlerStatusValue.not_ready, message="Plugin disabled") def install(self, uninstall_exit_code=None, extension=None): # For Handler level operations, extension just specifies the settings that initiated the install. # This is needed to provide the sequence number and extension name in case the extension needs to report # failure/status using status file. uninstall_exit_code = str(uninstall_exit_code) if uninstall_exit_code is not None else NOT_RUN env = {ExtCommandEnvVariable.UninstallReturnCode: uninstall_exit_code} man = self.load_manifest() install_cmd = man.get_install_command() self.logger.info("Install extension [{0}]".format(install_cmd)) self.set_operation(WALAEventOperation.Install) self.launch_command(install_cmd, cmd_name="install", timeout=900, extension=extension, extension_error_code=ExtensionErrorCodes.PluginInstallProcessingFailed, env=env) self.set_handler_state(ExtHandlerState.Installed) self.set_handler_status(status=ExtHandlerStatusValue.not_ready, message="Plugin installed but not enabled") def uninstall(self, extension=None): # For Handler level operations, extension just specifies the settings that initiated the uninstall. # This is needed to provide the sequence number and extension name in case the extension needs to report # failure/status using status file. self.set_operation(WALAEventOperation.UnInstall) man = self.load_manifest() # stop tracking extension services cgroup. resource_limits = man.get_resource_limits(self.get_full_name(), self.ext_handler.version) CGroupConfigurator.get_instance().stop_tracking_extension_services_cgroups( resource_limits.get_service_list()) CGroupConfigurator.get_instance().remove_extension_services_drop_in_files( resource_limits.get_service_list()) uninstall_cmd = man.get_uninstall_command() self.logger.info("Uninstall extension [{0}]".format(uninstall_cmd)) self.launch_command(uninstall_cmd, cmd_name="uninstall", extension=extension) def remove_ext_handler(self): try: zip_filename = os.path.join(conf.get_lib_dir(), self.get_extension_package_zipfile_name()) if os.path.exists(zip_filename): os.remove(zip_filename) self.logger.verbose("Deleted the extension zip at path {0}", zip_filename) base_dir = self.get_base_dir() if os.path.isdir(base_dir): self.logger.info("Remove extension handler directory: {0}", base_dir) # some extensions uninstall asynchronously so ignore error 2 while removing them def on_rmtree_error(_, __, exc_info): _, exception, _ = exc_info if not isinstance(exception, OSError) or exception.errno != 2: # [Errno 2] No such file or directory raise exception shutil.rmtree(base_dir, onerror=on_rmtree_error) self.logger.info("Remove the extension slice: {0}".format(self.get_full_name())) CGroupConfigurator.get_instance().remove_extension_slice( extension_name=self.get_full_name()) except IOError as e: message = "Failed to remove extension handler directory: {0}".format(e) self.report_event(message=message, is_success=False) self.logger.warn(message) def update(self, handler_version=None, disable_exit_codes=None, updating_from_version=None, extension=None): # For Handler level operations, extension just specifies the settings that initiated the update. # This is needed to provide the sequence number and extension name in case the extension needs to report # failure/status using status file. if handler_version is None: handler_version = self.ext_handler.version env = { 'VERSION': handler_version, ExtCommandEnvVariable.UpdatingFromVersion: updating_from_version } if not self.supports_multi_config: # For single config, extension.name == ext_handler.name env[ExtCommandEnvVariable.DisableReturnCode] = ustr(disable_exit_codes.get(self.ext_handler.name)) else: disable_codes = [] for ext in self.extensions: disable_codes.append({ "extensionName": ext.name, "exitCode": ustr(disable_exit_codes.get(ext.name)) }) env[ExtCommandEnvVariable.DisableReturnCodeMultipleExtensions] = json.dumps(disable_codes) try: self.set_operation(WALAEventOperation.Update) man = self.load_manifest() update_cmd = man.get_update_command() self.logger.info("Update extension [{0}]".format(update_cmd)) self.launch_command(update_cmd, cmd_name="update", timeout=900, extension_error_code=ExtensionErrorCodes.PluginUpdateProcessingFailed, env=env, extension=extension) except ExtensionError: # Mark the handler as Failed so we don't clean it up and can keep reporting its status self.set_handler_state(ExtHandlerState.FailedUpgrade) raise def update_with_install(self, uninstall_exit_code=None, extension=None): man = self.load_manifest() if man.is_update_with_install(): self.install(uninstall_exit_code=uninstall_exit_code, extension=extension) else: self.logger.info("UpdateWithInstall not set. " "Skip install during upgrade.") self.set_handler_state(ExtHandlerState.Installed) def _get_last_modified_seq_no_from_config_files(self, extension): """ The sequence number is not guaranteed to always be strictly increasing. To ensure we always get the latest one, fetching the sequence number from config file that was last modified (and not necessarily the largest). :return: Last modified Sequence number or -1 on errors """ seq_no = -1 if self.supports_multi_config and (extension is None or extension.name is None): # If no extension name is provided for Multi Config, don't try to parse any sequence number from filesystem return seq_no try: largest_modified_time = 0 conf_dir = self.get_conf_dir() for item in os.listdir(conf_dir): item_path = os.path.join(conf_dir, item) if not os.path.isfile(item_path): continue try: # Settings file for Multi Config look like - ..settings # Settings file for Single Config look like - .settings match = re.search("((?P\\w+)\\.)*(?P\\d+)\\.settings", item_path) if match is not None: ext_name = match.group('ext_name') if self.supports_multi_config and extension.name != ext_name: continue curr_seq_no = int(match.group("seq_no")) curr_modified_time = os.path.getmtime(item_path) if curr_modified_time > largest_modified_time: seq_no = curr_seq_no largest_modified_time = curr_modified_time except (ValueError, IndexError, TypeError): self.logger.verbose("Failed to parse file name: {0}", item) continue except Exception as error: logger.verbose("Error fetching sequence number from config files: {0}".format(ustr(error))) seq_no = -1 return seq_no def get_status_file_path(self, extension=None): """ We should technically only fetch the sequence number from GoalState and not rely on the filesystem at all, But there are certain scenarios where we need to fetch the latest sequence number from the filesystem (For example when we need to report the status for extensions of previous GS if the current GS is Unsupported). Always prioritizing sequence number from extensions but falling back to filesystem :param extension: Extension for which the sequence number is required :return: Sequence number for the extension, Status file path or -1, None """ path = None seq_no = None if extension is not None and extension.sequenceNumber is not None: try: seq_no = int(extension.sequenceNumber) except ValueError: logger.error('Sequence number [{0}] does not appear to be valid'.format(extension.sequenceNumber)) if seq_no is None: # If we're unable to fetch Sequence number from Extension for any reason, # try fetching it from the last modified Settings file. seq_no = self._get_last_modified_seq_no_from_config_files(extension) if seq_no is not None and seq_no > -1: if self.should_perform_multi_config_op(extension) and extension is not None and extension.name is not None: path = os.path.join(self.get_status_dir(), "{0}.{1}.status".format(extension.name, seq_no)) elif not self.supports_multi_config: path = os.path.join(self.get_status_dir(), "{0}.status").format(seq_no) return seq_no if seq_no is not None else -1, path def collect_ext_status(self, ext): self.logger.verbose("Collect extension status for {0}".format(self.get_extension_full_name(ext))) seq_no, ext_status_file = self.get_status_file_path(ext) # We should never try to read any status file if the handler has no settings, returning None in that case if seq_no == -1 or ext is None: return None data = None data_str = None # Extension.name contains the extension name in case of MC and Handler name in case of Single Config. ext_status = ExtensionStatus(name=ext.name, seq_no=seq_no) try: data_str, data = self._read_status_file(ext_status_file) except ExtensionStatusError as e: msg = "" ext_status.status = ExtensionStatusValue.error if e.code == ExtensionStatusError.CouldNotReadStatusFile: ext_status.code = ExtensionErrorCodes.PluginUnknownFailure msg = u"We couldn't read any status for {0} extension, for the sequence number {1}. It failed due" \ u" to {2}".format(self.get_full_name(ext), seq_no, ustr(e)) elif e.code == ExtensionStatusError.InvalidJsonFile: ext_status.code = ExtensionErrorCodes.PluginSettingsStatusInvalid msg = u"The status reported by the extension {0}(Sequence number {1}), was in an " \ u"incorrect format and the agent could not parse it correctly. Failed due to {2}" \ .format(self.get_full_name(ext), seq_no, ustr(e)) elif e.code == ExtensionStatusError.FileNotExists: msg = "This status is being reported by the Guest Agent since no status file was " \ "reported by extension {0}: {1}".format(self.get_extension_full_name(ext), ustr(e)) # Reporting a success code and transitioning status to keep in accordance with existing code that # creates default status placeholder file ext_status.code = ExtensionErrorCodes.PluginSuccess ext_status.status = ExtensionStatusValue.transitioning # This log is periodic due to the verbose nature of the status check. Please make sure that the message # constructed above does not change very frequently and includes important info such as sequence number, # extension name to make sure that the log reflects changes in the extension sequence for which the # status is being sent. logger.periodic_warn(logger.EVERY_HALF_HOUR, u"[PERIODIC] " + msg) add_periodic(delta=logger.EVERY_HALF_HOUR, name=self.get_extension_full_name(ext), version=self.ext_handler.version, op=WALAEventOperation.StatusProcessing, is_success=False, message=msg, log_event=False) ext_status.message = msg return ext_status # We did not encounter InvalidJsonFile/CouldNotReadStatusFile and thus the status file was correctly written # and has valid json. try: parse_ext_status(ext_status, data) if len(data_str) > _MAX_STATUS_FILE_SIZE_IN_BYTES: raise ExtensionStatusError(msg="For Extension Handler {0} for the sequence number {1}, the status " "file {2} of size {3} bytes is too big. Max Limit allowed is {4} bytes" .format(self.get_full_name(ext), seq_no, ext_status_file, len(data_str), _MAX_STATUS_FILE_SIZE_IN_BYTES), code=ExtensionStatusError.MaxSizeExceeded) except ExtensionStatusError as e: msg = u"For Extension Handler {0} for the sequence number {1}, the status file {2}. " \ u"Encountered the following error: {3}".format(self.get_full_name(ext), seq_no, ext_status_file, ustr(e)) logger.periodic_warn(logger.EVERY_DAY, u"[PERIODIC] " + msg) add_periodic(delta=logger.EVERY_HALF_HOUR, name=self.get_extension_full_name(ext), version=self.ext_handler.version, op=WALAEventOperation.StatusProcessing, is_success=False, message=msg, log_event=False) if e.code == ExtensionStatusError.MaxSizeExceeded: ext_status.message, field_size = self._truncate_message(ext_status.message, _MAX_STATUS_MESSAGE_LENGTH) ext_status.substatusList = self._process_substatus_list(ext_status.substatusList, field_size) elif e.code == ExtensionStatusError.StatusFileMalformed: ext_status.message = "Could not get a valid status from the extension {0}. Encountered the " \ "following error: {1}".format(self.get_full_name(ext), ustr(e)) ext_status.code = ExtensionErrorCodes.PluginSettingsStatusInvalid ext_status.status = ExtensionStatusValue.error return ext_status def get_ext_handling_status(self, ext): seq_no, ext_status_file = self.get_status_file_path(ext) # This is legacy scenario for cases when no extension settings is available if seq_no < 0 or ext_status_file is None: return None # Missing status file is considered a non-terminal state here # so that extension sequencing can wait until it becomes existing if not os.path.exists(ext_status_file): status = ExtensionStatusValue.warning else: ext_status = self.collect_ext_status(ext) status = ext_status.status if ext_status is not None else None return status def is_ext_handling_complete(self, ext): status = self.get_ext_handling_status(ext) # when seq < 0 (i.e. no new user settings), the handling is complete and return None status if status is None: return True, None # If not in terminal state, it is incomplete if status not in _EXTENSION_TERMINAL_STATUSES: return False, status # Extension completed, return its status return True, status def report_error_on_incarnation_change(self, goal_state_changed, log_msg, event_msg, extension=None, op=WALAEventOperation.ReportStatus): # Since this code is called on a loop, logging as a warning only on goal state change, else logging it # as verbose if goal_state_changed: logger.warn(log_msg) add_event(name=self.get_extension_full_name(extension), version=self.ext_handler.version, op=op, message=event_msg, is_success=False, log_event=False) else: logger.verbose(log_msg) def get_extension_handler_statuses(self, handler_status, goal_state_changed): """ Get the list of ExtHandlerStatus objects corresponding to each extension in the Handler. Each object might have its own status for the Extension status but the Handler status would be the same for each extension in a Handle :return: List of ExtHandlerStatus objects for each extension in the Handler """ ext_handler_statuses = [] # TODO Refactor or remove this common code pattern (for each extension subordinate to an ext_handler, do X). for ext in self.extensions: # In MC, for disabled extensions we dont need to report status. Skip reporting if disabled and state == disabled # Extension.state corresponds to the state requested by CRP, self.__get_extension_state() corresponds to the # state of the extension on the VM. Skip reporting only if both are Disabled if self.should_perform_multi_config_op(ext) and \ ext.state == ExtensionState.Disabled and self.get_extension_state(ext) == ExtensionState.Disabled: continue # Breaking off extension reporting in 2 parts, one which is Handler dependent and the other that is Extension dependent try: ext_handler_status = ExtHandlerStatus() set_properties("ExtHandlerStatus", ext_handler_status, get_properties(handler_status)) except Exception as error: msg = "Something went wrong when trying to get a copy of the Handler status for {0}".format( self.get_extension_full_name()) self.report_error_on_incarnation_change(goal_state_changed, event_msg=msg, log_msg="{0}.\nStack Trace: {1}".format( msg, textutil.format_exception(error))) # Since this is a Handler level error and we need to do it per extension, breaking here and logging # error since we wont be able to report error anyways and saving it as a handler status (legacy behavior) self.set_handler_status(message=msg, code=-1) break # For the extension dependent stuff, if there's some unhandled error, we will report it back to CRP as an extension error. try: ext_status = self.collect_ext_status(ext) if ext_status is not None: ext_handler_status.extension_status = ext_status ext_handler_statuses.append(ext_handler_status) except ExtensionError as error: msg = "Unknown error when trying to fetch status from extension {0}".format( self.get_extension_full_name(ext)) self.report_error_on_incarnation_change(goal_state_changed, event_msg=msg, log_msg="{0}.\nStack Trace: {1}".format( msg, textutil.format_exception(error)), extension=ext) # Unexpected error, for single config, keep the behavior as is if not self.should_perform_multi_config_op(ext): self.set_handler_status(message=ustr(error), code=error.code) break # For MultiConfig, create a custom ExtensionStatus object with the error details and attach it to the Handler. # This way the error would be reported back to CRP and the failure would be propagated instantly as compared to CRP eventually timing it out. ext_status = ExtensionStatus(name=ext.name, seq_no=ext.sequenceNumber, code=ExtensionErrorCodes.PluginUnknownFailure, status=ExtensionStatusValue.error, message=msg) ext_handler_status.extension_status = ext_status ext_handler_statuses.append(ext_handler_status) return ext_handler_statuses def collect_heartbeat(self): # pylint: disable=R1710 man = self.load_manifest() if not man.is_report_heartbeat(): return heartbeat_file = os.path.join(conf.get_lib_dir(), self.get_heartbeat_file()) if not os.path.isfile(heartbeat_file): raise ExtensionError("Failed to get heart beat file") if not self.is_responsive(heartbeat_file): return { "status": "Unresponsive", "code": -1, "message": "Extension heartbeat is not responsive" } try: heartbeat_json = fileutil.read_file(heartbeat_file) heartbeat = json.loads(heartbeat_json)[0]['heartbeat'] except IOError as e: raise ExtensionError("Failed to get heartbeat file:{0}".format(e)) except (ValueError, KeyError) as e: raise ExtensionError("Malformed heartbeat file: {0}".format(e)) return heartbeat @staticmethod def is_responsive(heartbeat_file): """ Was heartbeat_file updated within the last ten (10) minutes? :param heartbeat_file: str :return: bool """ last_update = int(time.time() - os.stat(heartbeat_file).st_mtime) return last_update <= 600 def launch_command(self, cmd, cmd_name=None, timeout=300, extension_error_code=ExtensionErrorCodes.PluginProcessingError, env=None, extension=None): begin_utc = datetime.datetime.utcnow() self.logger.verbose("Launch command: [{0}]", cmd) base_dir = self.get_base_dir() with tempfile.TemporaryFile(dir=base_dir, mode="w+b") as stdout: with tempfile.TemporaryFile(dir=base_dir, mode="w+b") as stderr: if env is None: env = {} # Always add Extension Path and version to the current launch_command (Ask from publishers) env.update({ ExtCommandEnvVariable.ExtensionPath: base_dir, ExtCommandEnvVariable.ExtensionVersion: str(self.ext_handler.version), ExtCommandEnvVariable.WireProtocolAddress: self.protocol.get_endpoint(), # Setting sequence number to 0 incase no settings provided to keep in accordance with the empty # 0.settings file that we create for such extensions. ExtCommandEnvVariable.ExtensionSeqNumber: str( extension.sequenceNumber) if extension is not None else _DEFAULT_SEQ_NO }) if self.should_perform_multi_config_op(extension): env[ExtCommandEnvVariable.ExtensionName] = extension.name supported_features = [] for _, feature in get_agent_supported_features_list_for_extensions().items(): supported_features.append( { "Key": feature.name, "Value": feature.version } ) if supported_features: env[ExtCommandEnvVariable.ExtensionSupportedFeatures] = json.dumps(supported_features) ext_name = self.get_extension_full_name(extension) try: # Some extensions erroneously begin cmd with a slash; don't interpret those # as root-relative. (Issue #1170) command_full_path = os.path.join(base_dir, cmd.lstrip(os.path.sep)) log_msg = "Executing command: {0} with environment variables: {1}".format(command_full_path, json.dumps(env)) self.logger.info(log_msg) self.report_event(name=ext_name, message=log_msg, log_event=False) # Add the os environment variables before executing command env.update(os.environ) process_output = CGroupConfigurator.get_instance().start_extension_command( extension_name=self.get_full_name(extension), command=command_full_path, cmd_name=cmd_name, timeout=timeout, shell=True, cwd=base_dir, env=env, stdout=stdout, stderr=stderr, error_code=extension_error_code) except OSError as e: raise ExtensionError("Failed to launch '{0}': {1}".format(command_full_path, e.strerror), code=extension_error_code) duration = elapsed_milliseconds(begin_utc) log_msg = "Command: {0}\n{1}".format(cmd, "\n".join( [line for line in process_output.split('\n') if line != ""])) self.logger.info(log_msg) self.report_event(name=ext_name, message=log_msg, duration=duration, log_event=False) return process_output def load_manifest(self): man_file = self.get_manifest_file() try: data = json.loads(fileutil.read_file(man_file)) except (IOError, OSError) as e: raise ExtensionError('Failed to load manifest file ({0}): {1}'.format(man_file, e.strerror), code=ExtensionErrorCodes.PluginHandlerManifestNotFound) except ValueError: raise ExtensionError('Malformed manifest file ({0}).'.format(man_file), code=ExtensionErrorCodes.PluginHandlerManifestDeserializationError) return HandlerManifest(data[0]) def update_settings_file(self, settings_file, settings): settings_file = os.path.join(self.get_conf_dir(), settings_file) try: fileutil.write_file(settings_file, settings) except IOError as e: fileutil.clean_ioerror(e, paths=[settings_file]) raise ExtensionError(u"Failed to update settings file", e) def update_settings(self, extension): if self.extensions is None or len(self.extensions) == 0 or extension is None: # This is the behavior of waagent 2.0.x # The new agent has to be consistent with the old one. self.logger.info("Extension has no settings, write empty 0.settings") self.update_settings_file("{0}.settings".format(_DEFAULT_SEQ_NO), "") return settings = { 'publicSettings': extension.publicSettings, 'protectedSettings': extension.protectedSettings, 'protectedSettingsCertThumbprint': extension.certificateThumbprint } ext_settings = { "runtimeSettings": [{ "handlerSettings": settings }] } # MultiConfig: change the name to ..settings for MC and .settings for SC settings_file = "{0}.{1}.settings".format(extension.name, extension.sequenceNumber) if \ self.should_perform_multi_config_op(extension) else "{0}.settings".format(extension.sequenceNumber) self.logger.info("Update settings file: {0}", settings_file) self.update_settings_file(settings_file, json.dumps(ext_settings)) def create_handler_env(self): handler_env = { HandlerEnvironment.logFolder: self.get_log_dir(), HandlerEnvironment.configFolder: self.get_conf_dir(), HandlerEnvironment.statusFolder: self.get_status_dir(), HandlerEnvironment.heartbeatFile: self.get_heartbeat_file() } if get_supported_feature_by_name(SupportedFeatureNames.ExtensionTelemetryPipeline).is_supported: handler_env[HandlerEnvironment.eventsFolder] = self.get_extension_events_dir() # For now, keep the preview key to not break extensions that were using the preview. handler_env[HandlerEnvironment.eventsFolder_preview] = self.get_extension_events_dir() env = [{ HandlerEnvironment.name: self.ext_handler.name, HandlerEnvironment.version: HandlerEnvironment.schemaVersion, HandlerEnvironment.handlerEnvironment: handler_env }] try: fileutil.write_file(self.get_env_file(), json.dumps(env)) except IOError as e: fileutil.clean_ioerror(e, paths=[self.get_base_dir(), self.pkg_file]) raise ExtensionDownloadError(u"Failed to save handler environment", e) def __get_handler_state_file_name(self, extension=None): if self.should_perform_multi_config_op(extension): return "{0}.HandlerState".format(extension.name) return "HandlerState" def set_handler_state(self, handler_state): self.__set_state(name=self.__get_handler_state_file_name(), value=handler_state) def get_handler_state(self): return self.__get_state(name=self.__get_handler_state_file_name(), default=ExtHandlerState.NotInstalled) def __set_extension_state(self, extension, extension_state): self.__set_state(name=self.__get_handler_state_file_name(extension), value=extension_state) def get_extension_state(self, extension=None): return self.__get_state(name=self.__get_handler_state_file_name(extension), default=ExtensionState.Disabled) def __set_state(self, name, value): state_dir = self.get_conf_dir() state_file = os.path.join(state_dir, name) try: if not os.path.exists(state_dir): fileutil.mkdir(state_dir, mode=0o700) fileutil.write_file(state_file, value) except IOError as e: fileutil.clean_ioerror(e, paths=[state_file]) self.logger.error("Failed to set state: {0}", e) def __get_state(self, name, default=None): state_dir = self.get_conf_dir() state_file = os.path.join(state_dir, name) if not os.path.isfile(state_file): return default try: return fileutil.read_file(state_file) except IOError as e: self.logger.error("Failed to get state: {0}", e) return default def __remove_extension_state_files(self, extension): self.logger.info("Removing states files for disabled extension: {0}".format(extension.name)) try: # MultiConfig: Remove all config/.*.settings, status/.*.status and config/.HandlerState files files_to_delete = [ os.path.join(self.get_conf_dir(), "{0}.*.settings".format(extension.name)), os.path.join(self.get_status_dir(), "{0}.*.status".format(extension.name)), os.path.join(self.get_conf_dir(), self.__get_handler_state_file_name(extension)) ] fileutil.rm_files(*files_to_delete) except Exception as error: extension_name = self.get_extension_full_name(extension) message = "Failed to remove extension state files for {0}: {1}".format(extension_name, ustr(error)) self.report_event(name=extension_name, message=message, is_success=False, log_event=False) self.logger.warn(message) def set_handler_status(self, status=ExtHandlerStatusValue.not_ready, message="", code=0): state_dir = self.get_conf_dir() handler_status = ExtHandlerStatus() handler_status.name = self.ext_handler.name handler_status.version = str(self.ext_handler.version) handler_status.message = message handler_status.code = code handler_status.status = status handler_status.supports_multi_config = self.ext_handler.supports_multi_config status_file = os.path.join(state_dir, "HandlerStatus") try: handler_status_json = json.dumps(get_properties(handler_status)) if handler_status_json is not None: if not os.path.exists(state_dir): fileutil.mkdir(state_dir, mode=0o700) fileutil.write_file(status_file, handler_status_json) else: self.logger.error("Failed to create JSON document of handler status for {0} version {1}".format( self.ext_handler.name, self.ext_handler.version)) except (IOError, ValueError, ProtocolError) as error: fileutil.clean_ioerror(error, paths=[status_file]) self.logger.error("Failed to save handler status: {0}", textutil.format_exception(error)) def get_handler_status(self): state_dir = self.get_conf_dir() status_file = os.path.join(state_dir, "HandlerStatus") if not os.path.isfile(status_file): return None handler_status_contents = "" try: handler_status_contents = fileutil.read_file(status_file) data = json.loads(handler_status_contents) handler_status = ExtHandlerStatus() set_properties("ExtHandlerStatus", handler_status, data) return handler_status except (IOError, ValueError) as error: self.logger.error("Failed to get handler status: {0}", error) except Exception as error: error_msg = "Failed to get handler status message: {0}.\n Contents of file: {1}".format( ustr(error), handler_status_contents).replace('"', '\'') add_periodic( delta=logger.EVERY_HOUR, name=AGENT_NAME, version=CURRENT_VERSION, op=WALAEventOperation.ExtensionProcessing, is_success=False, message=error_msg) raise return None def get_extension_package_zipfile_name(self): return "{0}__{1}{2}".format(self.ext_handler.name, self.ext_handler.version, HANDLER_PKG_EXT) def get_full_name(self, extension=None): """ :return: - if extension is None or Handler does not support Multi Config, else then return - .- """ return "{0}-{1}".format(self.get_extension_full_name(extension), self.ext_handler.version) def get_base_dir(self): return os.path.join(conf.get_lib_dir(), self.get_full_name()) def get_status_dir(self): return os.path.join(self.get_base_dir(), "status") def get_conf_dir(self): return os.path.join(self.get_base_dir(), 'config') def get_extension_events_dir(self): return os.path.join(self.get_log_dir(), EVENTS_DIRECTORY) def get_heartbeat_file(self): return os.path.join(self.get_base_dir(), 'heartbeat.log') def get_manifest_file(self): return os.path.join(self.get_base_dir(), 'HandlerManifest.json') def get_env_file(self): return os.path.join(self.get_base_dir(), HandlerEnvironment.fileName) def get_log_dir(self): return os.path.join(conf.get_ext_log_dir(), self.ext_handler.name) @staticmethod def is_azuremonitorlinuxagent(extension_name): cgroup_monitor_extension_name = conf.get_cgroup_monitor_extension_name() if re.match(r"\A" + cgroup_monitor_extension_name, extension_name) is not None\ and datetime.datetime.utcnow() < datetime.datetime.strptime(conf.get_cgroup_monitor_expiry_time(), "%Y-%m-%d"): return True return False @staticmethod def _read_status_file(ext_status_file): err_count = 0 while True: try: return ExtHandlerInstance._read_and_parse_json_status_file(ext_status_file) except Exception: err_count += 1 if err_count >= _NUM_OF_STATUS_FILE_RETRIES: raise time.sleep(_STATUS_FILE_RETRY_DELAY) @staticmethod def _read_and_parse_json_status_file(ext_status_file): if not os.path.exists(ext_status_file): raise ExtensionStatusError(msg="Status file {0} does not exist".format(ext_status_file), code=ExtensionStatusError.FileNotExists) try: data_str = fileutil.read_file(ext_status_file) except IOError as e: raise ExtensionStatusError(msg=ustr(e), inner=e, code=ExtensionStatusError.CouldNotReadStatusFile) try: data = json.loads(data_str) except (ValueError, TypeError) as e: raise ExtensionStatusError(msg="{0} \n First 2000 Bytes of status file:\n {1}".format(ustr(e), ustr(data_str)[:2000]), inner=e, code=ExtensionStatusError.InvalidJsonFile) return data_str, data def _process_substatus_list(self, substatus_list, current_status_size=0): processed_substatus = [] # Truncating the substatus to reduce the size, and preserve other fields of the text for substatus in substatus_list: substatus.name, field_size = self._truncate_message(substatus.name, _MAX_SUBSTATUS_FIELD_LENGTH) current_status_size += field_size substatus.message, field_size = self._truncate_message(substatus.message, _MAX_SUBSTATUS_FIELD_LENGTH) current_status_size += field_size if current_status_size <= _MAX_STATUS_FILE_SIZE_IN_BYTES: processed_substatus.append(substatus) else: break return processed_substatus @staticmethod def _truncate_message(field, truncate_size=_MAX_SUBSTATUS_FIELD_LENGTH): # pylint: disable=R1710 if field is None: # pylint: disable=R1705 return else: truncated_field = field if len(field) < truncate_size else field[:truncate_size] + _TRUNCATED_SUFFIX return truncated_field, len(truncated_field) class HandlerEnvironment(object): # HandlerEnvironment.json schema version schemaVersion = 1.0 fileName = "HandlerEnvironment.json" handlerEnvironment = "handlerEnvironment" logFolder = "logFolder" configFolder = "configFolder" statusFolder = "statusFolder" heartbeatFile = "heartbeatFile" eventsFolder_preview = "eventsFolder_preview" eventsFolder = "eventsFolder" name = "name" version = "version" class HandlerManifest(object): def __init__(self, data): if data is None or data['handlerManifest'] is None: raise ExtensionError('Malformed manifest file.') self.data = data def get_name(self): return self.data["name"] def get_version(self): return self.data["version"] def get_install_command(self): return self.data['handlerManifest']["installCommand"] def get_uninstall_command(self): return self.data['handlerManifest']["uninstallCommand"] def get_update_command(self): return self.data['handlerManifest']["updateCommand"] def get_enable_command(self): return self.data['handlerManifest']["enableCommand"] def get_disable_command(self): return self.data['handlerManifest']["disableCommand"] def is_report_heartbeat(self): return self.data['handlerManifest'].get('reportHeartbeat', False) def is_update_with_install(self): update_mode = self.data['handlerManifest'].get('updateMode') if update_mode is None: return True return update_mode.lower() == "updatewithinstall" def is_continue_on_update_failure(self): return self.data['handlerManifest'].get('continueOnUpdateFailure', False) def supports_multiple_extensions(self): return self.data['handlerManifest'].get('supportsMultipleExtensions', False) def get_resource_limits(self, extension_name, str_version): """ Placeholder values for testing and monitoring the monitor extension resource usage. This is not effective after nov 30th. """ if ExtHandlerInstance.is_azuremonitorlinuxagent(extension_name): if LooseVersion(str_version) < LooseVersion("1.12"): test_man = { "resourceLimits": { "services": [ { "name": "mdsd.service" } ] } } return ResourceLimits(test_man.get('resourceLimits', None)) else: test_man = { "resourceLimits": { "services": [ { "name": "azuremonitoragent.service" } ] } } return ResourceLimits(test_man.get('resourceLimits', None)) return ResourceLimits(self.data.get('resourceLimits', None)) class ResourceLimits(object): def __init__(self, data): self.data = data def get_extension_slice_cpu_quota(self): if self.data is not None: return self.data.get('cpuQuotaPercentage', None) return None def get_extension_slice_memory_quota(self): if self.data is not None: return self.data.get('memoryQuotaInMB', None) return None def get_service_list(self): if self.data is not None: return self.data.get('services', None) return None class ExtensionStatusError(ExtensionError): """ When extension failed to provide a valid status file """ CouldNotReadStatusFile = 1 InvalidJsonFile = 2 StatusFileMalformed = 3 MaxSizeExceeded = 4 FileNotExists = 5 def __init__(self, msg=None, inner=None, code=-1): # pylint: disable=W0235 super(ExtensionStatusError, self).__init__(msg, inner, code) WALinuxAgent-2.9.1.1/azurelinuxagent/ga/monitor.py000066400000000000000000000314711446033677600220730ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import datetime import os import threading import azurelinuxagent.common.conf as conf import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.networkutil as networkutil from azurelinuxagent.common.cgroup import MetricValue, MetricsCategory, MetricsCounter from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator from azurelinuxagent.common.cgroupstelemetry import CGroupsTelemetry from azurelinuxagent.common.errorstate import ErrorState from azurelinuxagent.common.event import add_event, WALAEventOperation, report_metric from azurelinuxagent.common.future import ustr from azurelinuxagent.common.interfaces import ThreadHandlerInterface from azurelinuxagent.common.osutil import get_osutil from azurelinuxagent.common.protocol.healthservice import HealthService from azurelinuxagent.common.protocol.imds import get_imds_client from azurelinuxagent.common.protocol.util import get_protocol_util from azurelinuxagent.common.utils.restutil import IOErrorCounter from azurelinuxagent.common.utils.textutil import hash_strings from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION from azurelinuxagent.ga.periodic_operation import PeriodicOperation def get_monitor_handler(): return MonitorHandler() class PollResourceUsage(PeriodicOperation): """ Periodic operation to poll the tracked cgroups for resource usage data. It also checks whether there are processes in the agent's cgroup that should not be there. """ def __init__(self): super(PollResourceUsage, self).__init__(conf.get_cgroup_check_period()) self.__log_metrics = conf.get_cgroup_log_metrics() self.__periodic_metrics = {} def _operation(self): tracked_metrics = CGroupsTelemetry.poll_all_tracked() for metric in tracked_metrics: key = metric.category + metric.counter + metric.instance if key not in self.__periodic_metrics or (self.__periodic_metrics[key] + metric.report_period) <= datetime.datetime.now(): report_metric(metric.category, metric.counter, metric.instance, metric.value, log_event=self.__log_metrics) self.__periodic_metrics[key] = datetime.datetime.now() CGroupConfigurator.get_instance().check_cgroups(tracked_metrics) class PollSystemWideResourceUsage(PeriodicOperation): def __init__(self): super(PollSystemWideResourceUsage, self).__init__(datetime.timedelta(hours=1)) self.__log_metrics = conf.get_cgroup_log_metrics() self.osutil = get_osutil() def poll_system_memory_metrics(self): used_mem, available_mem = self.osutil.get_used_and_available_system_memory() return [ MetricValue(MetricsCategory.MEMORY_CATEGORY, MetricsCounter.USED_MEM, "", used_mem), MetricValue(MetricsCategory.MEMORY_CATEGORY, MetricsCounter.AVAILABLE_MEM, "", available_mem) ] def _operation(self): metrics = self.poll_system_memory_metrics() for metric in metrics: report_metric(metric.category, metric.counter, metric.instance, metric.value, log_event=self.__log_metrics) class ResetPeriodicLogMessages(PeriodicOperation): """ Periodic operation to clean up the hash-tables maintained by the loggers. For reference, please check azurelinuxagent.common.logger.Logger and azurelinuxagent.common.event.EventLogger classes """ def __init__(self): super(ResetPeriodicLogMessages, self).__init__(datetime.timedelta(hours=12)) def _operation(self): logger.reset_periodic() class ReportNetworkErrors(PeriodicOperation): def __init__(self): super(ReportNetworkErrors, self).__init__(datetime.timedelta(minutes=30)) def _operation(self): io_errors = IOErrorCounter.get_and_reset() hostplugin_errors = io_errors.get("hostplugin") protocol_errors = io_errors.get("protocol") other_errors = io_errors.get("other") if hostplugin_errors > 0 or protocol_errors > 0 or other_errors > 0: msg = "hostplugin:{0};protocol:{1};other:{2}".format(hostplugin_errors, protocol_errors, other_errors) add_event(op=WALAEventOperation.HttpErrors, message=msg) class ReportNetworkConfigurationChanges(PeriodicOperation): """ Periodic operation to check and log changes in network configuration. """ def __init__(self): super(ReportNetworkConfigurationChanges, self).__init__(datetime.timedelta(minutes=1)) self.osutil = get_osutil() self.last_route_table_hash = b'' self.last_nic_state = {} def log_network_configuration(self): try: route_file = '/proc/net/route' if os.path.exists(route_file): lines = [] with open(route_file) as file_object: for line in file_object: lines.append(line) if len(lines) >= 100: lines.append("= self._last_warning_time + self._LOG_WARNING_PERIOD: logger.warn(warning) self._last_warning_time = datetime.datetime.utcnow() self._last_warning = warning def next_run_time(self): return self._next_run_time def _operation(self): """ Derived classes must override this with the definition of the operation they need to perform """ raise NotImplementedError() @staticmethod def sleep_until_next_operation(operations): """ Takes a list of operations, finds the operation that should be executed next (that with the closest next_run_time) and sleeps until it is time to execute that operation. """ next_operation_time = min([op.next_run_time() for op in operations]) sleep_timedelta = next_operation_time - datetime.datetime.utcnow() # timedelta.total_seconds() is not available on Python 2.6, do the computation manually sleep_seconds = ((sleep_timedelta.days * 24 * 3600 + sleep_timedelta.seconds) * 10.0 ** 6 + sleep_timedelta.microseconds) / 10.0 ** 6 if sleep_seconds > 0: time.sleep(sleep_seconds) WALinuxAgent-2.9.1.1/azurelinuxagent/ga/remoteaccess.py000066400000000000000000000143411446033677600230560ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import os.path from datetime import datetime, timedelta import azurelinuxagent.common.conf as conf import azurelinuxagent.common.logger as logger from azurelinuxagent.common.event import add_event, WALAEventOperation from azurelinuxagent.common.future import ustr from azurelinuxagent.common.osutil import get_osutil from azurelinuxagent.common.utils import textutil from azurelinuxagent.common.utils.cryptutil import CryptUtil from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION REMOTE_USR_EXPIRATION_FORMAT = "%a, %d %b %Y %H:%M:%S %Z" DATE_FORMAT = "%Y-%m-%d" TRANSPORT_PRIVATE_CERT = "TransportPrivate.pem" REMOTE_ACCESS_ACCOUNT_COMMENT = "JIT_Account" MAX_TRY_ATTEMPT = 5 FAILED_ATTEMPT_THROTTLE = 1 def get_remote_access_handler(protocol): return RemoteAccessHandler(protocol) class RemoteAccessHandler(object): def __init__(self, protocol): self._os_util = get_osutil() self._protocol = protocol self._cryptUtil = CryptUtil(conf.get_openssl_cmd()) self._remote_access = None self._check_existing_jit_users = True def run(self): try: if self._os_util.jit_enabled: # Handle remote access if any. self._remote_access = self._protocol.client.get_remote_access() self._handle_remote_access() except Exception as e: msg = u"Exception processing goal state for remote access users: {0}".format(textutil.format_exception(e)) add_event(AGENT_NAME, version=CURRENT_VERSION, op=WALAEventOperation.RemoteAccessHandling, is_success=False, message=msg) def _get_existing_jit_users(self): all_users = self._os_util.get_users() return set(u[0] for u in all_users if self._is_jit_user(u[4])) def _handle_remote_access(self): if self._remote_access is not None: logger.info("Processing remote access users in goal state.") self._check_existing_jit_users = True existing_jit_users = self._get_existing_jit_users() goal_state_users = set(u.name for u in self._remote_access.user_list.users) for acc in self._remote_access.user_list.users: try: raw_expiration = acc.expiration account_expiration = datetime.strptime(raw_expiration, REMOTE_USR_EXPIRATION_FORMAT) now = datetime.utcnow() if acc.name not in existing_jit_users and now < account_expiration: self._add_user(acc.name, acc.encrypted_password, account_expiration) elif acc.name in existing_jit_users and now > account_expiration: # user account expired, delete it. logger.info("Remote access user '{0}' expired.", acc.name) self._remove_user(acc.name) except Exception as e: logger.error("Error processing remote access user '{0}' - {1}", acc.name, ustr(e)) for user in existing_jit_users: try: if user not in goal_state_users: # user explicitly removed self._remove_user(user) except Exception as e: logger.error("Error removing remote access user '{0}' - {1}", user, ustr(e)) else: # There are no JIT users in the goal state; that may mean that they were removed or that they # were never added. Enumerating the users on the current vm can be very slow and this path is hit # on each goal state; we use self._check_existing_jit_users to avoid enumerating the users # every single time. if self._check_existing_jit_users: logger.info("Looking for existing remote access users.") existing_jit_users = self._get_existing_jit_users() remove_user_errors = False for user in existing_jit_users: try: self._remove_user(user) except Exception as e: logger.error("Error removing remote access user '{0}' - {1}", user, ustr(e)) remove_user_errors = True if not remove_user_errors: self._check_existing_jit_users = False @staticmethod def _is_jit_user(comment): return comment == REMOTE_ACCESS_ACCOUNT_COMMENT def _add_user(self, username, encrypted_password, account_expiration): user_added = False try: expiration_date = (account_expiration + timedelta(days=1)).strftime(DATE_FORMAT) logger.info("Adding remote access user '{0}' with expiration date {1}", username, expiration_date) self._os_util.useradd(username, expiration_date, REMOTE_ACCESS_ACCOUNT_COMMENT) user_added = True logger.info("Adding remote access user '{0}' to sudoers", username) prv_key = os.path.join(conf.get_lib_dir(), TRANSPORT_PRIVATE_CERT) pwd = self._cryptUtil.decrypt_secret(encrypted_password, prv_key) self._os_util.chpasswd(username, pwd, conf.get_password_cryptid(), conf.get_password_crypt_salt_len()) self._os_util.conf_sudoer(username) except Exception: if user_added: self._remove_user(username) raise def _remove_user(self, username): logger.info("Removing remote access user '{0}'", username) self._os_util.del_account(username) WALinuxAgent-2.9.1.1/azurelinuxagent/ga/send_telemetry_events.py000066400000000000000000000162731446033677600250160ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2020 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import datetime import threading import time from azurelinuxagent.common import logger from azurelinuxagent.common.event import add_event, WALAEventOperation from azurelinuxagent.common.exception import ServiceStoppedError from azurelinuxagent.common.future import ustr, Queue, Empty from azurelinuxagent.common.interfaces import ThreadHandlerInterface from azurelinuxagent.common.utils import textutil def get_send_telemetry_events_handler(protocol_util): return SendTelemetryEventsHandler(protocol_util) class SendTelemetryEventsHandler(ThreadHandlerInterface): """ This Handler takes care of sending all telemetry out of the agent to Wireserver. It sends out data as soon as there's any data available in the queue to send. """ _THREAD_NAME = "SendTelemetryHandler" _MAX_TIMEOUT = datetime.timedelta(seconds=5).seconds _MIN_EVENTS_TO_BATCH = 30 _MIN_BATCH_WAIT_TIME = datetime.timedelta(seconds=5) def __init__(self, protocol_util): self._protocol = protocol_util.get_protocol() self.should_run = True self._thread = None # We're using a Queue for handling the communication between threads. We plan to remove any dependency on the # filesystem in the future and use add_event to directly queue events into the queue rather than writing to # a file and then parsing it later. # Once we move add_event to directly queue events, we need to add a maxsize here to ensure some limitations are # being set (currently our limits are enforced by collector_threads but that would become obsolete once we # start enqueuing events directly). self._queue = Queue() @staticmethod def get_thread_name(): return SendTelemetryEventsHandler._THREAD_NAME def run(self): logger.info("Start SendTelemetryHandler service.") self.start() def is_alive(self): return self._thread is not None and self._thread.is_alive() def start(self): self._thread = threading.Thread(target=self._process_telemetry_thread) self._thread.setDaemon(True) self._thread.setName(self.get_thread_name()) self._thread.start() def stop(self): """ Stop server communication and join the thread to main thread. """ self.should_run = False if self.is_alive(): self.join() def join(self): self._queue.join() self._thread.join() def stopped(self): return not self.should_run def enqueue_event(self, event): # Add event to queue and set event if self.stopped(): raise ServiceStoppedError("{0} is stopped, not accepting anymore events".format(self.get_thread_name())) # Queue.put() can block if the queue is full which can be an uninterruptible wait. Blocking for a max of # SendTelemetryEventsHandler._MAX_TIMEOUT seconds and raising a ServiceStoppedError to retry later. # Todo: Queue.put() will only raise a Full exception if a maxsize is set for the Queue. Once some size # limitations are set for the Queue, ensure to handle that correctly here. try: self._queue.put(event, timeout=SendTelemetryEventsHandler._MAX_TIMEOUT) except Exception as error: raise ServiceStoppedError( "Unable to enqueue due to: {0}, stopping any more enqueuing until the next run".format(ustr(error))) def _wait_for_event_in_queue(self): """ Wait for atleast one event in Queue or timeout after SendTelemetryEventsHandler._MAX_TIMEOUT seconds. In case of a timeout, set the event to None. :return: event if an event is added to the Queue or None to signify no events were added in queue. This would raise in case of an error. """ try: event = self._queue.get(timeout=SendTelemetryEventsHandler._MAX_TIMEOUT) self._queue.task_done() except Empty: # No elements in Queue, return None event = None return event def _process_telemetry_thread(self): logger.info("Successfully started the {0} thread".format(self.get_thread_name())) try: # On demand wait, start processing as soon as there is any data available in the queue. In worst case, # also keep checking every SendTelemetryEventsHandler._MAX_TIMEOUT secs to avoid uninterruptible waits. # Incase the service is stopped but we have events in queue, ensure we send them out before killing the thread. while not self.stopped() or not self._queue.empty(): first_event = self._wait_for_event_in_queue() if first_event: # Start processing queue only if first event is not None (i.e. Queue has atleast 1 event), # else do nothing self._send_events_in_queue(first_event) except Exception as error: err_msg = "An unknown error occurred in the {0} thread main loop, stopping thread.{1}".format( self.get_thread_name(), textutil.format_exception(error)) add_event(op=WALAEventOperation.UnhandledError, message=err_msg, is_success=False) def _send_events_in_queue(self, first_event): # Process everything in Queue start_time = datetime.datetime.utcnow() while not self.stopped() and (self._queue.qsize() + 1) < self._MIN_EVENTS_TO_BATCH and ( start_time + self._MIN_BATCH_WAIT_TIME) > datetime.datetime.utcnow(): # To promote batching, we either wait for atleast _MIN_EVENTS_TO_BATCH events or _MIN_BATCH_WAIT_TIME secs # before sending out the first request to wireserver. # If the thread is requested to stop midway, we skip batching and send whatever we have in the queue. logger.verbose("Waiting for events to batch. Total events so far: {0}, Time elapsed: {1} secs", self._queue.qsize()+1, (datetime.datetime.utcnow() - start_time).seconds) time.sleep(1) # Delete files after sending the data rather than deleting and sending self._protocol.report_event(self._get_events_in_queue(first_event)) def _get_events_in_queue(self, first_event): yield first_event while not self._queue.empty(): try: event = self._queue.get_nowait() self._queue.task_done() yield event except Exception as error: logger.error("Some exception when fetching event from queue: {0}".format(textutil.format_exception(error)))WALinuxAgent-2.9.1.1/azurelinuxagent/ga/update.py000066400000000000000000002435361446033677600216750ustar00rootroot00000000000000# Windows Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import glob import json import os import platform import re import shutil import signal import stat import subprocess import sys import time import uuid from datetime import datetime, timedelta from azurelinuxagent.common import conf from azurelinuxagent.common import logger from azurelinuxagent.common.protocol.imds import get_imds_client from azurelinuxagent.common.utils import fileutil, textutil from azurelinuxagent.common.agent_supported_feature import get_supported_feature_by_name, SupportedFeatureNames from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator from azurelinuxagent.common.event import add_event, initialize_event_logger_vminfo_common_parameters, \ WALAEventOperation, EVENTS_DIRECTORY from azurelinuxagent.common.exception import UpdateError, ExitException, AgentUpgradeExitException, AgentMemoryExceededException from azurelinuxagent.common.future import ustr from azurelinuxagent.common.osutil import get_osutil, systemd from azurelinuxagent.common.persist_firewall_rules import PersistFirewallRulesHandler from azurelinuxagent.common.protocol.goal_state import GoalStateSource from azurelinuxagent.common.protocol.hostplugin import HostPluginProtocol, VmSettingsNotSupported from azurelinuxagent.common.protocol.restapi import VMAgentUpdateStatus, VMAgentUpdateStatuses, ExtHandlerPackageList, \ VERSION_0 from azurelinuxagent.common.protocol.util import get_protocol_util from azurelinuxagent.common.utils import shellutil from azurelinuxagent.common.utils.archive import StateArchiver, AGENT_STATUS_FILE from azurelinuxagent.common.utils.flexible_version import FlexibleVersion from azurelinuxagent.common.utils.networkutil import AddFirewallRules from azurelinuxagent.common.utils.shellutil import CommandError from azurelinuxagent.common.version import AGENT_LONG_NAME, AGENT_NAME, AGENT_DIR_PATTERN, CURRENT_AGENT, AGENT_VERSION, \ CURRENT_VERSION, DISTRO_NAME, DISTRO_VERSION, get_lis_version, \ has_logrotate, PY_VERSION_MAJOR, PY_VERSION_MINOR, PY_VERSION_MICRO, get_daemon_version from azurelinuxagent.ga.collect_logs import get_collect_logs_handler, is_log_collection_allowed from azurelinuxagent.ga.collect_telemetry_events import get_collect_telemetry_events_handler from azurelinuxagent.ga.env import get_env_handler from azurelinuxagent.ga.exthandlers import HandlerManifest, ExtHandlersHandler, list_agent_lib_directory, \ ExtensionStatusValue, ExtHandlerStatusValue from azurelinuxagent.ga.monitor import get_monitor_handler from azurelinuxagent.ga.send_telemetry_events import get_send_telemetry_events_handler AGENT_ERROR_FILE = "error.json" # File name for agent error record AGENT_MANIFEST_FILE = "HandlerManifest.json" AGENT_PARTITION_FILE = "partition" CHILD_HEALTH_INTERVAL = 15 * 60 CHILD_LAUNCH_INTERVAL = 5 * 60 CHILD_LAUNCH_RESTART_MAX = 3 CHILD_POLL_INTERVAL = 60 MAX_FAILURE = 3 # Max failure allowed for agent before blacklisted GOAL_STATE_PERIOD_EXTENSIONS_DISABLED = 5 * 60 ORPHAN_POLL_INTERVAL = 3 ORPHAN_WAIT_INTERVAL = 15 * 60 AGENT_SENTINEL_FILE = "current_version" # This file marks that the first goal state (after provisioning) has been completed, either because it converged or because we received another goal # state before it converged. The contents will be an instance of ExtensionsSummary. If the file does not exist then we have not finished processing # the goal state. INITIAL_GOAL_STATE_FILE = "initial_goal_state" READONLY_FILE_GLOBS = [ "*.crt", "*.p7m", "*.pem", "*.prv", "ovf-env.xml" ] class ExtensionsSummary(object): """ The extensions summary is a list of (extension name, extension status) tuples for the current goal state; it is used to report changes in the status of extensions and to keep track of when the goal state converges (i.e. when all extensions in the goal state reach a terminal state: success or error.) The summary is computed from the VmStatus reported to blob storage. """ def __init__(self, vm_status=None): if vm_status is None: self.summary = [] self.converged = True else: # take the name and status of the extension if is it not None, else use the handler's self.summary = [(o.name, o.status) for o in map(lambda h: h.extension_status if h.extension_status is not None else h, vm_status.vmAgent.extensionHandlers)] self.summary.sort(key=lambda s: s[0]) # sort by extension name to make comparisons easier self.converged = all(status in (ExtensionStatusValue.success, ExtensionStatusValue.error, ExtHandlerStatusValue.ready, ExtHandlerStatusValue.not_ready) for _, status in self.summary) def __eq__(self, other): return self.summary == other.summary def __ne__(self, other): return not (self == other) def __str__(self): return ustr(self.summary) class AgentUpgradeType(object): """ Enum for different modes of Agent Upgrade """ Hotfix = "Hotfix" Normal = "Normal" def get_update_handler(): return UpdateHandler() class UpdateHandler(object): TELEMETRY_HEARTBEAT_PERIOD = timedelta(minutes=30) CHECK_MEMORY_USAGE_PERIOD = timedelta(seconds=conf.get_cgroup_check_period()) def __init__(self): self.osutil = get_osutil() self.protocol_util = get_protocol_util() self._is_running = True # Member variables to keep track of the Agent AutoUpgrade self.last_attempt_time = None self._last_hotfix_upgrade_time = None self._last_normal_upgrade_time = None self.agents = [] self.child_agent = None self.child_launch_time = None self.child_launch_attempts = 0 self.child_process = None self.signal_handler = None self._last_telemetry_heartbeat = None self._heartbeat_id = str(uuid.uuid4()).upper() self._heartbeat_counter = 0 self._last_check_memory_usage = datetime.min self._check_memory_usage_last_error_report = datetime.min # VM Size is reported via the heartbeat, default it here. self._vm_size = None # these members are used to avoid reporting errors too frequently self._heartbeat_update_goal_state_error_count = 0 self._update_goal_state_error_count = 0 self._update_goal_state_last_error_report = datetime.min self._report_status_last_failed_goal_state = None # incarnation of the last goal state that has been fully processed # (None if no goal state has been processed) self._last_incarnation = None # ID of the last extensions goal state that has been fully processed (incarnation for WireServer goal states or etag for HostGAPlugin goal states) # (None if no extensions goal state has been processed) self._last_extensions_gs_id = None # Goal state that is currently been processed (None if no goal state is being processed) self._goal_state = None # Whether the agent supports FastTrack (it does, as long as the HostGAPlugin supports the vmSettings API) self._supports_fast_track = False self._extensions_summary = ExtensionsSummary() self._is_initial_goal_state = not os.path.exists(self._initial_goal_state_file_path()) if not conf.get_extensions_enabled(): self._goal_state_period = GOAL_STATE_PERIOD_EXTENSIONS_DISABLED else: if self._is_initial_goal_state: self._goal_state_period = conf.get_initial_goal_state_period() else: self._goal_state_period = conf.get_goal_state_period() def run_latest(self, child_args=None): """ This method is called from the daemon to find and launch the most current, downloaded agent. Note: - Most events should be tagged to the launched agent (agent_version) """ if self.child_process is not None: raise Exception("Illegal attempt to launch multiple goal state Agent processes") if self.signal_handler is None: self.signal_handler = signal.signal(signal.SIGTERM, self.forward_signal) latest_agent = None if not conf.get_autoupdate_enabled() else self.get_latest_agent_greater_than_daemon( daemon_version=CURRENT_VERSION) if latest_agent is None: logger.info(u"Installed Agent {0} is the most current agent", CURRENT_AGENT) agent_cmd = "python -u {0} -run-exthandlers".format(sys.argv[0]) agent_dir = os.getcwd() agent_name = CURRENT_AGENT agent_version = CURRENT_VERSION else: logger.info(u"Determined Agent {0} to be the latest agent", latest_agent.name) agent_cmd = latest_agent.get_agent_cmd() agent_dir = latest_agent.get_agent_dir() agent_name = latest_agent.name agent_version = latest_agent.version if child_args is not None: agent_cmd = "{0} {1}".format(agent_cmd, child_args) try: # Launch the correct Python version for python-based agents cmds = textutil.safe_shlex_split(agent_cmd) if cmds[0].lower() == "python": cmds[0] = sys.executable agent_cmd = " ".join(cmds) self._evaluate_agent_health(latest_agent) self.child_process = subprocess.Popen( cmds, cwd=agent_dir, stdout=sys.stdout, stderr=sys.stderr, env=os.environ) logger.verbose(u"Agent {0} launched with command '{1}'", agent_name, agent_cmd) # Setting the poll interval to poll every second to reduce the agent provisioning time; # The daemon shouldn't wait for 60secs before starting the ext-handler in case the # ext-handler kills itself during agent-update during the first 15 mins (CHILD_HEALTH_INTERVAL) poll_interval = 1 ret = None start_time = time.time() while (time.time() - start_time) < CHILD_HEALTH_INTERVAL: time.sleep(poll_interval) try: ret = self.child_process.poll() except OSError: # if child_process has terminated, calling poll could raise an exception ret = -1 if ret is not None: break if ret is None or ret <= 0: msg = u"Agent {0} launched with command '{1}' is successfully running".format( agent_name, agent_cmd) logger.info(msg) add_event( AGENT_NAME, version=agent_version, op=WALAEventOperation.Enable, is_success=True, message=msg, log_event=False) if ret is None: # Wait for the process to exit if self.child_process.wait() > 0: msg = u"ExtHandler process {0} launched with command '{1}' exited with return code: {2}".format( agent_name, agent_cmd, ret) logger.warn(msg) else: msg = u"Agent {0} launched with command '{1}' failed with return code: {2}".format( agent_name, agent_cmd, ret) logger.warn(msg) add_event( AGENT_NAME, version=agent_version, op=WALAEventOperation.Enable, is_success=False, message=msg) except Exception as e: # Ignore child errors during termination if self.is_running: msg = u"Agent {0} launched with command '{1}' failed with exception: \n".format( agent_name, agent_cmd) logger.warn(msg) detailed_message = '{0} {1}'.format(msg, textutil.format_exception(e)) add_event( AGENT_NAME, version=agent_version, op=WALAEventOperation.Enable, is_success=False, message=detailed_message) if latest_agent is not None: latest_agent.mark_failure(is_fatal=True, reason=detailed_message) self.child_process = None return def run(self, debug=False): """ This is the main loop which watches for agent and extension updates. """ try: logger.info("{0} (Goal State Agent version {1})", AGENT_LONG_NAME, AGENT_VERSION) logger.info("OS: {0} {1}", DISTRO_NAME, DISTRO_VERSION) logger.info("Python: {0}.{1}.{2}", PY_VERSION_MAJOR, PY_VERSION_MINOR, PY_VERSION_MICRO) os_info_msg = u"Distro: {dist_name}-{dist_ver}; "\ u"OSUtil: {util_name}; AgentService: {service_name}; "\ u"Python: {py_major}.{py_minor}.{py_micro}; "\ u"systemd: {systemd}; "\ u"LISDrivers: {lis_ver}; "\ u"logrotate: {has_logrotate};".format( dist_name=DISTRO_NAME, dist_ver=DISTRO_VERSION, util_name=type(self.osutil).__name__, service_name=self.osutil.service_name, py_major=PY_VERSION_MAJOR, py_minor=PY_VERSION_MINOR, py_micro=PY_VERSION_MICRO, systemd=systemd.is_systemd(), lis_ver=get_lis_version(), has_logrotate=has_logrotate() ) logger.info(os_info_msg) # # Initialize the goal state; some components depend on information provided by the goal state and this # call ensures the required info is initialized (e.g. telemetry depends on the container ID.) # protocol = self.protocol_util.get_protocol() self._initialize_goal_state(protocol) # Initialize the common parameters for telemetry events initialize_event_logger_vminfo_common_parameters(protocol) # Send telemetry for the OS-specific info. add_event(AGENT_NAME, op=WALAEventOperation.OSInfo, message=os_info_msg) # # Perform initialization tasks # from azurelinuxagent.ga.exthandlers import get_exthandlers_handler, migrate_handler_state exthandlers_handler = get_exthandlers_handler(protocol) migrate_handler_state() from azurelinuxagent.ga.remoteaccess import get_remote_access_handler remote_access_handler = get_remote_access_handler(protocol) self._ensure_no_orphans() self._emit_restart_event() self._emit_changes_in_default_configuration() self._ensure_partition_assigned() self._ensure_readonly_files() self._ensure_cgroups_initialized() self._ensure_extension_telemetry_state_configured_properly(protocol) self._ensure_firewall_rules_persisted(dst_ip=protocol.get_endpoint()) self._add_accept_tcp_firewall_rule_if_not_enabled(dst_ip=protocol.get_endpoint()) self._reset_legacy_blacklisted_agents() self._cleanup_legacy_goal_state_history() # Get all thread handlers telemetry_handler = get_send_telemetry_events_handler(self.protocol_util) all_thread_handlers = [ get_monitor_handler(), get_env_handler(), telemetry_handler, get_collect_telemetry_events_handler(telemetry_handler) ] if is_log_collection_allowed(): all_thread_handlers.append(get_collect_logs_handler()) # Launch all monitoring threads self._start_threads(all_thread_handlers) logger.info("Goal State Period: {0} sec. This indicates how often the agent checks for new goal states and reports status.", self._goal_state_period) while self.is_running: self._check_daemon_running(debug) self._check_threads_running(all_thread_handlers) self._process_goal_state(exthandlers_handler, remote_access_handler) self._send_heartbeat_telemetry(protocol) self._check_agent_memory_usage() time.sleep(self._goal_state_period) except AgentUpgradeExitException as exitException: add_event(op=WALAEventOperation.AgentUpgrade, message=exitException.reason, log_event=False) logger.info(exitException.reason) except ExitException as exitException: logger.info(exitException.reason) except Exception as error: msg = u"Agent {0} failed with exception: {1}".format(CURRENT_AGENT, ustr(error)) self._set_sentinel(msg=msg) logger.warn(msg) logger.warn(textutil.format_exception(error)) sys.exit(1) # additional return here because sys.exit is mocked in unit tests return self._shutdown() sys.exit(0) def _initialize_goal_state(self, protocol): # # Block until we can fetch the first goal state (self._try_update_goal_state() does its own logging and error handling). # while not self._try_update_goal_state(protocol): time.sleep(conf.get_goal_state_period()) # # If FastTrack is disabled we need to check if the current goal state (which will be retrieved using the WireServer and # hence will be a Fabric goal state) is outdated. # if not conf.get_enable_fast_track(): last_fast_track_timestamp = HostPluginProtocol.get_fast_track_timestamp() if last_fast_track_timestamp is not None: egs = protocol.client.get_goal_state().extensions_goal_state if egs.created_on_timestamp < last_fast_track_timestamp: egs.is_outdated = True logger.info("The current Fabric goal state is older than the most recent FastTrack goal state; will skip it.\nFabric: {0}\nFastTrack: {1}", egs.created_on_timestamp, last_fast_track_timestamp) def _get_vm_size(self, protocol): """ Including VMSize is meant to capture the architecture of the VM (i.e. arm64 VMs will have arm64 included in their vmsize field and amd64 will have no architecture indicated). """ if self._vm_size is None: imds_client = get_imds_client(protocol.get_endpoint()) try: imds_info = imds_client.get_compute() self._vm_size = imds_info.vmSize except Exception as e: err_msg = "Attempts to retrieve VM size information from IMDS are failing: {0}".format(textutil.format_exception(e)) logger.periodic_warn(logger.EVERY_SIX_HOURS, "[PERIODIC] {0}".format(err_msg)) return "unknown" return self._vm_size def _get_vm_arch(self): return platform.machine() def _check_daemon_running(self, debug): # Check that the parent process (the agent's daemon) is still running if not debug and self._is_orphaned: raise ExitException("Agent {0} is an orphan -- exiting".format(CURRENT_AGENT)) def _start_threads(self, all_thread_handlers): for thread_handler in all_thread_handlers: thread_handler.run() def _check_threads_running(self, all_thread_handlers): # Check that all the threads are still running for thread_handler in all_thread_handlers: if thread_handler.keep_alive() and not thread_handler.is_alive(): logger.warn("{0} thread died, restarting".format(thread_handler.get_thread_name())) thread_handler.start() def _try_update_goal_state(self, protocol): """ Attempts to update the goal state and returns True on success or False on failure, sending telemetry events about the failures. """ try: max_errors_to_log = 3 protocol.client.update_goal_state(silent=self._update_goal_state_error_count >= max_errors_to_log) self._goal_state = protocol.get_goal_state() if self._update_goal_state_error_count > 0: message = u"Fetching the goal state recovered from previous errors. Fetched {0} (certificates: {1})".format( self._goal_state.extensions_goal_state.id, self._goal_state.certs.summary) add_event(AGENT_NAME, op=WALAEventOperation.FetchGoalState, version=CURRENT_VERSION, is_success=True, message=message, log_event=False) logger.info(message) self._update_goal_state_error_count = 0 try: self._supports_fast_track = conf.get_enable_fast_track() and protocol.client.get_host_plugin().check_vm_settings_support() except VmSettingsNotSupported: self._supports_fast_track = False except Exception as e: self._update_goal_state_error_count += 1 self._heartbeat_update_goal_state_error_count += 1 if self._update_goal_state_error_count <= max_errors_to_log: message = u"Error fetching the goal state: {0}".format(textutil.format_exception(e)) logger.error(message) add_event(op=WALAEventOperation.FetchGoalState, is_success=False, message=message, log_event=False) self._update_goal_state_last_error_report = datetime.now() else: if self._update_goal_state_last_error_report + timedelta(hours=6) > datetime.now(): self._update_goal_state_last_error_report = datetime.now() message = u"Fetching the goal state is still failing: {0}".format(textutil.format_exception(e)) logger.error(message) add_event(op=WALAEventOperation.FetchGoalState, is_success=False, message=message, log_event=False) return False return True def __update_guest_agent(self, protocol): """ This function checks for new Agent updates and raises AgentUpgradeExitException if available. There are 2 different ways the agent checks for an update - 1) Requested Version is specified in the Goal State. - In this case, the Agent will download the requested version and upgrade/downgrade instantly. 2) No requested version. - In this case, the agent will periodically check (1 hr) for new agent versions in GA Manifest. - If available, it will download all versions > CURRENT_VERSION. - Depending on the highest version > CURRENT_VERSION, the agent will update within 4 hrs (for a Hotfix update) or 24 hrs (for a Normal update) """ def log_next_update_time(): next_normal_time, next_hotfix_time = self.__get_next_upgrade_times() upgrade_type = self.__get_agent_upgrade_type(available_agent) next_time = next_hotfix_time if upgrade_type == AgentUpgradeType.Hotfix else next_normal_time message_ = "Discovered new {0} upgrade {1}; Will upgrade on or after {2}".format( upgrade_type, available_agent.name, datetime.utcfromtimestamp(next_time).strftime(logger.Logger.LogTimeFormatInUTC)) add_event(AGENT_NAME, op=WALAEventOperation.AgentUpgrade, version=CURRENT_VERSION, is_success=True, message=message_, log_event=False) logger.info(message_) def handle_updates_for_requested_version(): if requested_version < CURRENT_VERSION: prefix = "downgrade" # In case of a downgrade, we blacklist the current agent to avoid starting it back up ever again # (the expectation here being that if RSM is asking us to a downgrade, # there's a good reason for not wanting the current version). try: # We should always have an agent directory for the CURRENT_VERSION # (unless the CURRENT_VERSION == daemon version, but since we don't support downgrading # below daemon version, we will never reach this code path if that's the scenario) current_agent = next(agent for agent in self.agents if agent.version == CURRENT_VERSION) msg = "Blacklisting the agent {0} since a downgrade was requested in the GoalState, " \ "suggesting that we really don't want to execute any extensions using this version".format( CURRENT_VERSION) logger.info(msg) current_agent.mark_failure(is_fatal=True, reason=msg) except StopIteration: logger.warn( "Could not find a matching agent with current version {0} to blacklist, skipping it".format( CURRENT_VERSION)) else: # In case of an upgrade, we don't need to blacklist anything as the daemon will automatically # start the next available highest version which would be the requested version prefix = "upgrade" raise AgentUpgradeExitException( "Exiting current process to {0} to the request Agent version {1}".format(prefix, requested_version)) # Skip the update if there is no goal state yet or auto-update is disabled if self._goal_state is None or not conf.get_autoupdate_enabled(): return False if self._download_agent_if_upgrade_available(protocol): # The call to get_latest_agent_greater_than_daemon() also finds all agents in directory and sets the self.agents property. # This state is used to find the GuestAgent object with the current version later if requested version is available in last GS. available_agent = self.get_latest_agent_greater_than_daemon() requested_version, _ = self.__get_requested_version_and_agent_family_from_last_gs() if requested_version is not None: # If requested version specified, upgrade/downgrade to the specified version instantly as this is # driven by the goal state (as compared to the agent periodically checking for new upgrades every hour) handle_updates_for_requested_version() elif available_agent is None: # Legacy behavior: The current agent can become unavailable and needs to be reverted. # In that case, self._upgrade_available() returns True and available_agent would be None. Handling it here. raise AgentUpgradeExitException( "Agent {0} is reverting to the installed agent -- exiting".format(CURRENT_AGENT)) else: log_next_update_time() self.__upgrade_agent_if_permitted() def _processing_new_incarnation(self): """ True if we are currently processing a new incarnation (i.e. WireServer goal state) """ return self._goal_state is not None and self._goal_state.incarnation != self._last_incarnation def _processing_new_extensions_goal_state(self): """ True if we are currently processing a new extensions goal state """ egs = self._goal_state.extensions_goal_state return self._goal_state is not None and egs.id != self._last_extensions_gs_id and not egs.is_outdated def _process_goal_state(self, exthandlers_handler, remote_access_handler): protocol = exthandlers_handler.protocol # update self._goal_state if not self._try_update_goal_state(protocol): # agent updates and status reporting should be done even when the goal state is not updated self.__update_guest_agent(protocol) self._report_status(exthandlers_handler) return # check for agent updates self.__update_guest_agent(protocol) try: if self._processing_new_extensions_goal_state(): if not self._extensions_summary.converged: message = "A new goal state was received, but not all the extensions in the previous goal state have completed: {0}".format(self._extensions_summary) logger.warn(message) add_event(op=WALAEventOperation.GoalState, message=message, is_success=False, log_event=False) if self._is_initial_goal_state: self._on_initial_goal_state_completed(self._extensions_summary) self._extensions_summary = ExtensionsSummary() exthandlers_handler.run() # check cgroup and disable if any extension started in agent cgroup after goal state processed. # Note: Monitor thread periodically checks this in addition to here. CGroupConfigurator.get_instance().check_cgroups(cgroup_metrics=[]) # report status before processing the remote access, since that operation can take a long time self._report_status(exthandlers_handler) if self._processing_new_incarnation(): remote_access_handler.run() # lastly, archive the goal state history (but do it only on new goal states - no need to do it on every iteration) if self._processing_new_extensions_goal_state(): UpdateHandler._archive_goal_state_history() finally: if self._goal_state is not None: self._last_incarnation = self._goal_state.incarnation self._last_extensions_gs_id = self._goal_state.extensions_goal_state.id @staticmethod def _archive_goal_state_history(): try: archiver = StateArchiver(conf.get_lib_dir()) archiver.archive() except Exception as exception: logger.warn("Error cleaning up the goal state history: {0}", ustr(exception)) @staticmethod def _cleanup_legacy_goal_state_history(): try: StateArchiver.purge_legacy_goal_state_history() except Exception as exception: logger.warn("Error removing legacy history files: {0}", ustr(exception)) def __get_vmagent_update_status(self, goal_state_changed): """ This function gets the VMAgent update status as per the last GoalState. Returns: None if the last GS does not ask for requested version else VMAgentUpdateStatus """ if not conf.get_enable_ga_versioning(): return None update_status = None try: requested_version, manifest = self.__get_requested_version_and_agent_family_from_last_gs() if manifest is None and goal_state_changed: logger.info("Unable to report update status as no matching manifest found for family: {0}".format( conf.get_autoupdate_gafamily())) return None if requested_version is not None: if CURRENT_VERSION == requested_version: status = VMAgentUpdateStatuses.Success code = 0 else: status = VMAgentUpdateStatuses.Error code = 1 update_status = VMAgentUpdateStatus(expected_version=manifest.requested_version_string, status=status, code=code) except Exception as error: if goal_state_changed: err_msg = "[This error will only be logged once per goal state] " \ "Ran into error when trying to fetch updateStatus for the agent, skipping reporting update satus. Error: {0}".format( textutil.format_exception(error)) logger.warn(err_msg) add_event(op=WALAEventOperation.AgentUpgrade, is_success=False, message=err_msg, log_event=False) return update_status def _report_status(self, exthandlers_handler): vm_agent_update_status = self.__get_vmagent_update_status(self._processing_new_extensions_goal_state()) # report_ext_handlers_status does its own error handling and returns None if an error occurred vm_status = exthandlers_handler.report_ext_handlers_status( goal_state_changed=self._processing_new_extensions_goal_state(), vm_agent_update_status=vm_agent_update_status, vm_agent_supports_fast_track=self._supports_fast_track) if vm_status is not None: self._report_extensions_summary(vm_status) if self._goal_state is not None: agent_status = exthandlers_handler.get_ext_handlers_status_debug_info(vm_status) self._goal_state.save_to_history(agent_status, AGENT_STATUS_FILE) if self._goal_state.extensions_goal_state.is_outdated: exthandlers_handler.protocol.client.get_host_plugin().clear_fast_track_state() def _report_extensions_summary(self, vm_status): try: extensions_summary = ExtensionsSummary(vm_status) if self._extensions_summary != extensions_summary: self._extensions_summary = extensions_summary message = "Extension status: {0}".format(self._extensions_summary) logger.info(message) add_event(op=WALAEventOperation.GoalState, message=message, is_success=True) if self._extensions_summary.converged: message = "All extensions in the goal state have reached a terminal state: {0}".format(extensions_summary) logger.info(message) add_event(op=WALAEventOperation.GoalState, message=message, is_success=True) if self._is_initial_goal_state: self._on_initial_goal_state_completed(self._extensions_summary) except Exception as error: # report errors only once per goal state if self._report_status_last_failed_goal_state != self._goal_state.extensions_goal_state.id: self._report_status_last_failed_goal_state = self._goal_state.extensions_goal_state.id msg = u"Error logging the goal state summary: {0}".format(textutil.format_exception(error)) logger.warn(msg) add_event(op=WALAEventOperation.GoalState, is_success=False, message=msg) def _on_initial_goal_state_completed(self, extensions_summary): fileutil.write_file(self._initial_goal_state_file_path(), ustr(extensions_summary)) if conf.get_extensions_enabled() and self._goal_state_period != conf.get_goal_state_period(): self._goal_state_period = conf.get_goal_state_period() logger.info("Initial goal state completed, switched the goal state period to {0}", self._goal_state_period) self._is_initial_goal_state = False def forward_signal(self, signum, frame): if signum == signal.SIGTERM: self._shutdown() if self.child_process is None: return logger.info( u"Agent {0} forwarding signal {1} to {2}\n", CURRENT_AGENT, signum, self.child_agent.name if self.child_agent is not None else CURRENT_AGENT) self.child_process.send_signal(signum) if self.signal_handler not in (None, signal.SIG_IGN, signal.SIG_DFL): self.signal_handler(signum, frame) elif self.signal_handler is signal.SIG_DFL: if signum == signal.SIGTERM: self._shutdown() sys.exit(0) return @staticmethod def __get_daemon_version_for_update(): daemon_version = get_daemon_version() if daemon_version != FlexibleVersion(VERSION_0): return daemon_version # We return 0.0.0.0 if daemon version is not specified. In that case, # use the min version as 2.2.53 as we started setting the daemon version starting 2.2.53. return FlexibleVersion("2.2.53") def get_latest_agent_greater_than_daemon(self, daemon_version=None): """ If autoupdate is enabled, return the most current, downloaded, non-blacklisted agent which is not the current version (if any) and is greater than the `daemon_version`. Otherwise, return None (implying to use the installed agent). If `daemon_version` is None, we fetch it from the environment variable set by the DaemonHandler """ self._find_agents() daemon_version = self.__get_daemon_version_for_update() if daemon_version is None else daemon_version # Fetch the downloaded agents that are different from the current version and greater than the daemon version available_agents = [agent for agent in self.agents if agent.is_available and agent.version != CURRENT_VERSION and agent.version > daemon_version] return available_agents[0] if len(available_agents) >= 1 else None def _emit_restart_event(self): try: if not self._is_clean_start: msg = u"Agent did not terminate cleanly: {0}".format( fileutil.read_file(self._sentinel_file_path())) logger.info(msg) add_event( AGENT_NAME, version=CURRENT_VERSION, op=WALAEventOperation.Restart, is_success=False, message=msg) except Exception: pass return @staticmethod def _emit_changes_in_default_configuration(): try: def log_event(msg): logger.info("******** {0} ********", msg) add_event(AGENT_NAME, op=WALAEventOperation.ConfigurationChange, message=msg) def log_if_int_changed_from_default(name, current, message=""): default = conf.get_int_default_value(name) if default != current: log_event("{0} changed from its default: {1}. New value: {2}. {3}".format(name, default, current, message)) def log_if_op_disabled(name, value): if not value: log_event("{0} is set to False, not processing the operation".format(name)) log_if_int_changed_from_default("Extensions.GoalStatePeriod", conf.get_goal_state_period(), "Changing this value affects how often extensions are processed and status for the VM is reported. Too small a value may report the VM as unresponsive") log_if_int_changed_from_default("Extensions.InitialGoalStatePeriod", conf.get_initial_goal_state_period(), "Changing this value affects how often extensions are processed and status for the VM is reported. Too small a value may report the VM as unresponsive") log_if_op_disabled("OS.EnableFirewall", conf.enable_firewall()) log_if_op_disabled("Extensions.Enabled", conf.get_extensions_enabled()) log_if_op_disabled("AutoUpdate.Enabled", conf.get_autoupdate_enabled()) if conf.enable_firewall(): log_if_int_changed_from_default("OS.EnableFirewallPeriod", conf.get_enable_firewall_period()) if conf.get_autoupdate_enabled(): log_if_int_changed_from_default("Autoupdate.Frequency", conf.get_autoupdate_frequency()) if conf.get_enable_fast_track(): log_if_op_disabled("Debug.EnableFastTrack", conf.get_enable_fast_track()) if conf.get_lib_dir() != "/var/lib/waagent": log_event("lib dir is in an unexpected location: {0}".format(conf.get_lib_dir())) except Exception as e: logger.warn("Failed to log changes in configuration: {0}", ustr(e)) def _ensure_no_orphans(self, orphan_wait_interval=ORPHAN_WAIT_INTERVAL): pid_files, ignored = self._write_pid_file() # pylint: disable=W0612 for pid_file in pid_files: try: pid = fileutil.read_file(pid_file) wait_interval = orphan_wait_interval while self.osutil.check_pid_alive(pid): wait_interval -= ORPHAN_POLL_INTERVAL if wait_interval <= 0: logger.warn( u"{0} forcibly terminated orphan process {1}", CURRENT_AGENT, pid) os.kill(pid, signal.SIGKILL) break logger.info( u"{0} waiting for orphan process {1} to terminate", CURRENT_AGENT, pid) time.sleep(ORPHAN_POLL_INTERVAL) os.remove(pid_file) except Exception as e: logger.warn( u"Exception occurred waiting for orphan agent to terminate: {0}", ustr(e)) return def _ensure_partition_assigned(self): """ Assign the VM to a partition (0 - 99). Downloaded updates may be configured to run on only some VMs; the assigned partition determines eligibility. """ if not os.path.exists(self._partition_file): partition = ustr(int(datetime.utcnow().microsecond / 10000)) fileutil.write_file(self._partition_file, partition) add_event( AGENT_NAME, version=CURRENT_VERSION, op=WALAEventOperation.Partition, is_success=True, message=partition) def _ensure_readonly_files(self): for g in READONLY_FILE_GLOBS: for path in glob.iglob(os.path.join(conf.get_lib_dir(), g)): os.chmod(path, stat.S_IRUSR) def _ensure_cgroups_initialized(self): configurator = CGroupConfigurator.get_instance() configurator.initialize() def _evaluate_agent_health(self, latest_agent): """ Evaluate the health of the selected agent: If it is restarting too frequently, raise an Exception to force blacklisting. """ if latest_agent is None: self.child_agent = None return if self.child_agent is None or latest_agent.version != self.child_agent.version: self.child_agent = latest_agent self.child_launch_time = None self.child_launch_attempts = 0 if self.child_launch_time is None: self.child_launch_time = time.time() self.child_launch_attempts += 1 if (time.time() - self.child_launch_time) <= CHILD_LAUNCH_INTERVAL \ and self.child_launch_attempts >= CHILD_LAUNCH_RESTART_MAX: msg = u"Agent {0} restarted more than {1} times in {2} seconds".format( self.child_agent.name, CHILD_LAUNCH_RESTART_MAX, CHILD_LAUNCH_INTERVAL) raise Exception(msg) return def _filter_blacklisted_agents(self): self.agents = [agent for agent in self.agents if not agent.is_blacklisted] def _find_agents(self): """ Load all non-blacklisted agents currently on disk. """ try: self._set_and_sort_agents(self._load_agents()) self._filter_blacklisted_agents() except Exception as e: logger.warn(u"Exception occurred loading available agents: {0}", ustr(e)) return def _get_pid_parts(self): pid_file = conf.get_agent_pid_file_path() pid_dir = os.path.dirname(pid_file) pid_name = os.path.basename(pid_file) pid_re = re.compile("(\d+)_{0}".format(re.escape(pid_name))) # pylint: disable=W1401 return pid_dir, pid_name, pid_re def _get_pid_files(self): pid_dir, pid_name, pid_re = self._get_pid_parts() # pylint: disable=W0612 pid_files = [os.path.join(pid_dir, f) for f in os.listdir(pid_dir) if pid_re.match(f)] pid_files.sort(key=lambda f: int(pid_re.match(os.path.basename(f)).group(1))) return pid_files @property def is_running(self): return self._is_running @is_running.setter def is_running(self, value): self._is_running = value @property def _is_clean_start(self): return not os.path.isfile(self._sentinel_file_path()) @property def _is_orphaned(self): parent_pid = os.getppid() if parent_pid in (1, None): return True if not os.path.isfile(conf.get_agent_pid_file_path()): return True return fileutil.read_file(conf.get_agent_pid_file_path()) != ustr(parent_pid) def _load_agents(self): path = os.path.join(conf.get_lib_dir(), "{0}-*".format(AGENT_NAME)) return [GuestAgent.from_installed_agent(agent_dir) for agent_dir in glob.iglob(path) if os.path.isdir(agent_dir)] def _partition(self): return int(fileutil.read_file(self._partition_file)) @property def _partition_file(self): return os.path.join(conf.get_lib_dir(), AGENT_PARTITION_FILE) def _purge_agents(self): """ Remove from disk all directories and .zip files of unknown agents (without removing the current, running agent). """ path = os.path.join(conf.get_lib_dir(), "{0}-*".format(AGENT_NAME)) known_versions = [agent.version for agent in self.agents] if CURRENT_VERSION not in known_versions: logger.verbose( u"Running Agent {0} was not found in the agent manifest - adding to list", CURRENT_VERSION) known_versions.append(CURRENT_VERSION) for agent_path in glob.iglob(path): try: name = fileutil.trim_ext(agent_path, "zip") m = AGENT_DIR_PATTERN.match(name) if m is not None and FlexibleVersion(m.group(1)) not in known_versions: if os.path.isfile(agent_path): logger.info(u"Purging outdated Agent file {0}", agent_path) os.remove(agent_path) else: logger.info(u"Purging outdated Agent directory {0}", agent_path) shutil.rmtree(agent_path) except Exception as e: logger.warn(u"Purging {0} raised exception: {1}", agent_path, ustr(e)) return def _set_and_sort_agents(self, agents=None): if agents is None: agents = [] self.agents = agents self.agents.sort(key=lambda agent: agent.version, reverse=True) return def _set_sentinel(self, agent=CURRENT_AGENT, msg="Unknown cause"): try: fileutil.write_file( self._sentinel_file_path(), "[{0}] [{1}]".format(agent, msg)) except Exception as e: logger.warn( u"Exception writing sentinel file {0}: {1}", self._sentinel_file_path(), str(e)) return def _sentinel_file_path(self): return os.path.join(conf.get_lib_dir(), AGENT_SENTINEL_FILE) @staticmethod def _initial_goal_state_file_path(): return os.path.join(conf.get_lib_dir(), INITIAL_GOAL_STATE_FILE) def _shutdown(self): # Todo: Ensure all threads stopped when shutting down the main extension handler to ensure that the state of # all threads is clean. self.is_running = False if not os.path.isfile(self._sentinel_file_path()): return try: os.remove(self._sentinel_file_path()) except Exception as e: logger.warn( u"Exception removing sentinel file {0}: {1}", self._sentinel_file_path(), str(e)) return def __get_requested_version_and_agent_family_from_last_gs(self): """ Get the requested version and corresponding manifests from last GS if supported Returns: (Requested Version, Manifest) if supported and available (None, None) if no manifests found in the last GS (None, manifest) if not supported or not specified in GS """ family_name = conf.get_autoupdate_gafamily() agent_families = self._goal_state.extensions_goal_state.agent_families agent_families = [m for m in agent_families if m.name == family_name and len(m.uris) > 0] if len(agent_families) == 0: return None, None if conf.get_enable_ga_versioning() and agent_families[0].is_requested_version_specified: return agent_families[0].requested_version, agent_families[0] return None, agent_families[0] def _download_agent_if_upgrade_available(self, protocol, base_version=CURRENT_VERSION): """ This function downloads the new agent if an update is available. If a requested version is available in goal state, then only that version is downloaded (new-update model) Else, we periodically (1hr by default) checks if new Agent upgrade is available and download it on filesystem if available (old-update model) rtype: Boolean return: True if current agent is no longer available or an agent with a higher version number is available else False """ def report_error(msg_, version_=CURRENT_VERSION, op=WALAEventOperation.Download): logger.warn(msg_) add_event(AGENT_NAME, op=op, version=version_, is_success=False, message=msg_, log_event=False) def can_proceed_with_requested_version(): if not gs_updated: # If the goal state didn't change, don't process anything. return False # With the new model, we will get a new GS when CRP wants us to auto-update using required version. # If there's no new goal state, don't proceed with anything msg_ = "Found requested version in manifest: {0} for goal state {1}".format( requested_version, goal_state_id) logger.info(msg_) add_event(AGENT_NAME, op=WALAEventOperation.AgentUpgrade, is_success=True, message=msg_, log_event=False) if requested_version < daemon_version: # Don't process the update if the requested version is lesser than daemon version, # as we don't support downgrades below daemon versions. report_error( "Can't process the upgrade as the requested version: {0} is < current daemon version: {1}".format( requested_version, daemon_version), op=WALAEventOperation.AgentUpgrade) return False return True def agent_upgrade_time_elapsed(now_): if self.last_attempt_time is not None: next_attempt_time = self.last_attempt_time + conf.get_autoupdate_frequency() else: next_attempt_time = now_ if next_attempt_time > now_: return False return True agent_family_name = conf.get_autoupdate_gafamily() gs_updated = False daemon_version = self.__get_daemon_version_for_update() try: # Fetch the agent manifests from the latest Goal State goal_state_id = self._goal_state.extensions_goal_state.id gs_updated = self._processing_new_extensions_goal_state() requested_version, agent_family = self.__get_requested_version_and_agent_family_from_last_gs() if agent_family is None: logger.verbose( u"No manifest links found for agent family: {0} for goal state {1}, skipping update check".format( agent_family_name, goal_state_id)) return False except Exception as err: # If there's some issues in fetching the agent manifests, report it only on goal state change msg = u"Exception retrieving agent manifests: {0}".format(textutil.format_exception(err)) if gs_updated: report_error(msg) else: logger.verbose(msg) return False if requested_version is not None: # If GA versioning is enabled and requested version present in GS, and it's a new GS, follow new logic if not can_proceed_with_requested_version(): return False else: # If no requested version specified in the Goal State, follow the old auto-update logic # Note: If the first Goal State contains a requested version, this timer won't start (i.e. self.last_attempt_time won't be updated). # If any subsequent goal state does not contain requested version, this timer will start then, and we will # download all versions available in PIR and auto-update to the highest available version on that goal state. now = time.time() if not agent_upgrade_time_elapsed(now): return False logger.info("No requested version specified, checking for all versions for agent update (family: {0})", agent_family_name) self.last_attempt_time = now try: # If we make it to this point, then either there is a requested version in a new GS (new auto-update model), # or the 1hr time limit has elapsed for us to check the agent manifest for updates (old auto-update model). pkg_list = ExtHandlerPackageList() # If the requested version is the current version, don't download anything; # the call to purge() below will delete all other agents from disk # In this case, no need to even fetch the GA family manifest as we don't need to download any agent. if requested_version is not None and requested_version == CURRENT_VERSION: packages_to_download = [] msg = "The requested version is running as the current version: {0}".format(requested_version) logger.info(msg) add_event(AGENT_NAME, op=WALAEventOperation.AgentUpgrade, is_success=True, message=msg) else: agent_manifest = self._goal_state.fetch_agent_manifest(agent_family.name, agent_family.uris) pkg_list = agent_manifest.pkg_list packages_to_download = pkg_list.versions # Verify the requested version is in GA family manifest (if specified) if requested_version is not None and requested_version != CURRENT_VERSION: for pkg in pkg_list.versions: if FlexibleVersion(pkg.version) == requested_version: # Found a matching package, only download that one packages_to_download = [pkg] break else: msg = "No matching package found in the agent manifest for requested version: {0} in goal state {1}, skipping agent update".format( requested_version, goal_state_id) report_error(msg, version_=requested_version) return False # Set the agents to those available for download at least as current as the existing agent # or to the requested version (if specified) is_fast_track_goal_state = self._goal_state.extensions_goal_state.source == GoalStateSource.FastTrack agents_to_download = [GuestAgent.from_agent_package(pkg, protocol, is_fast_track_goal_state) for pkg in packages_to_download] # Filter out the agents that were downloaded/extracted successfully. If the agent was not installed properly, # we delete the directory and the zip package from the filesystem self._set_and_sort_agents([agent for agent in agents_to_download if agent.is_available]) # Remove from disk any agent no longer needed in the VM. # If requested version is provided, this would delete all other agents present on the VM except - # - the current version and the requested version if requested version != current version # - only the current version if requested version == current version # Note: # The code leaves on disk available, but blacklisted, agents to preserve the state. # Otherwise, those agents could be downloaded again and inappropriately retried. self._purge_agents() self._filter_blacklisted_agents() # If there are no agents available to upgrade/downgrade to, return False if len(self.agents) == 0: return False if requested_version is not None: # In case of requested version, return True if an agent with a different version number than the # current version is available that is higher than the current daemon version return self.agents[0].version != base_version and self.agents[0].version > daemon_version else: # Else, return True if the highest agent is > base_version (CURRENT_VERSION) return self.agents[0].version > base_version except Exception as err: msg = u"Exception downloading agents for update: {0}".format(textutil.format_exception(err)) report_error(msg) return False def _write_pid_file(self): pid_files = self._get_pid_files() pid_dir, pid_name, pid_re = self._get_pid_parts() previous_pid_file = None if len(pid_files) <= 0 else pid_files[-1] pid_index = -1 \ if previous_pid_file is None \ else int(pid_re.match(os.path.basename(previous_pid_file)).group(1)) pid_file = os.path.join(pid_dir, "{0}_{1}".format(pid_index + 1, pid_name)) try: fileutil.write_file(pid_file, ustr(os.getpid())) logger.info(u"{0} running as process {1}", CURRENT_AGENT, ustr(os.getpid())) except Exception as e: pid_file = None logger.warn( u"Expection writing goal state agent {0} pid to {1}: {2}", CURRENT_AGENT, pid_file, ustr(e)) return pid_files, pid_file def _send_heartbeat_telemetry(self, protocol): if self._last_telemetry_heartbeat is None: self._last_telemetry_heartbeat = datetime.utcnow() - UpdateHandler.TELEMETRY_HEARTBEAT_PERIOD if datetime.utcnow() >= (self._last_telemetry_heartbeat + UpdateHandler.TELEMETRY_HEARTBEAT_PERIOD): dropped_packets = self.osutil.get_firewall_dropped_packets(protocol.get_endpoint()) auto_update_enabled = 1 if conf.get_autoupdate_enabled() else 0 # Include vm architecture in the heartbeat message because the kusto table does not have # a separate column for it. vmarch = self._get_vm_arch() telemetry_msg = "{0};{1};{2};{3};{4};{5}".format(self._heartbeat_counter, self._heartbeat_id, dropped_packets, self._heartbeat_update_goal_state_error_count, auto_update_enabled, vmarch) debug_log_msg = "[DEBUG HeartbeatCounter: {0};HeartbeatId: {1};DroppedPackets: {2};" \ "UpdateGSErrors: {3};AutoUpdate: {4}]".format(self._heartbeat_counter, self._heartbeat_id, dropped_packets, self._heartbeat_update_goal_state_error_count, auto_update_enabled) # Write Heartbeat events/logs add_event(name=AGENT_NAME, version=CURRENT_VERSION, op=WALAEventOperation.HeartBeat, is_success=True, message=telemetry_msg, log_event=False) logger.info(u"[HEARTBEAT] Agent {0} is running as the goal state agent {1}", CURRENT_AGENT, debug_log_msg) # Update/Reset the counters self._heartbeat_counter += 1 self._heartbeat_update_goal_state_error_count = 0 self._last_telemetry_heartbeat = datetime.utcnow() def _check_agent_memory_usage(self): """ This checks the agent current memory usage and safely exit the process if agent reaches the memory limit """ try: if conf.get_enable_agent_memory_usage_check() and self._extensions_summary.converged: if self._last_check_memory_usage == datetime.min or datetime.utcnow() >= (self._last_check_memory_usage + UpdateHandler.CHECK_MEMORY_USAGE_PERIOD): self._last_check_memory_usage = datetime.utcnow() CGroupConfigurator.get_instance().check_agent_memory_usage() except AgentMemoryExceededException as exception: msg = "Check on agent memory usage:\n{0}".format(ustr(exception)) logger.info(msg) add_event(AGENT_NAME, op=WALAEventOperation.AgentMemory, is_success=True, message=msg) raise ExitException("Agent {0} is reached memory limit -- exiting".format(CURRENT_AGENT)) except Exception as exception: if self._check_memory_usage_last_error_report == datetime.min or (self._check_memory_usage_last_error_report + timedelta(hours=6)) > datetime.now(): self._check_memory_usage_last_error_report = datetime.now() msg = "Error checking the agent's memory usage: {0} --- [NOTE: Will not log the same error for the 6 hours]".format(ustr(exception)) logger.warn(msg) add_event(AGENT_NAME, op=WALAEventOperation.AgentMemory, is_success=False, message=msg) @staticmethod def _ensure_extension_telemetry_state_configured_properly(protocol): etp_enabled = get_supported_feature_by_name(SupportedFeatureNames.ExtensionTelemetryPipeline).is_supported for name, path in list_agent_lib_directory(skip_agent_package=True): try: handler_instance = ExtHandlersHandler.get_ext_handler_instance_from_path(name=name, path=path, protocol=protocol) except Exception: # Ignore errors if any continue try: if handler_instance is not None: # Recreate the HandlerEnvironment for existing extensions on startup. # This is to ensure that existing extensions can start using the telemetry pipeline if they support # it and also ensures that the extensions are not sending out telemetry if the Agent has to disable the feature. handler_instance.create_handler_env() events_dir = handler_instance.get_extension_events_dir() # If ETP is enabled and events directory doesn't exist for handler, create it if etp_enabled and not(os.path.exists(events_dir)): fileutil.mkdir(events_dir, mode=0o700) except Exception as e: logger.warn( "Unable to re-create HandlerEnvironment file on service startup. Error: {0}".format(ustr(e))) continue try: if not etp_enabled: # If extension telemetry pipeline is disabled, ensure we delete all existing extension events directory # because the agent will not be listening on those events. extension_event_dirs = glob.glob(os.path.join(conf.get_ext_log_dir(), "*", EVENTS_DIRECTORY)) for ext_dir in extension_event_dirs: shutil.rmtree(ext_dir, ignore_errors=True) except Exception as e: logger.warn("Error when trying to delete existing Extension events directory. Error: {0}".format(ustr(e))) @staticmethod def _ensure_firewall_rules_persisted(dst_ip): if not conf.enable_firewall(): logger.info("Not setting up persistent firewall rules as OS.EnableFirewall=False") return is_success = False logger.info("Starting setup for Persistent firewall rules") try: PersistFirewallRulesHandler(dst_ip=dst_ip, uid=os.getuid()).setup() msg = "Persistent firewall rules setup successfully" is_success = True logger.info(msg) except Exception as error: msg = "Unable to setup the persistent firewall rules: {0}".format(ustr(error)) logger.error(msg) add_event( op=WALAEventOperation.PersistFirewallRules, is_success=is_success, message=msg, log_event=False) def _add_accept_tcp_firewall_rule_if_not_enabled(self, dst_ip): if not conf.enable_firewall(): return def _execute_run_command(command): # Helper to execute a run command, returns True if no exception # Here we primarily check if an iptable rule exist. True if it exits , false if not try: shellutil.run_command(command) return True except CommandError as err: # return code 1 is expected while using the check command. Raise if encounter any other return code if err.returncode != 1: raise return False try: wait = self.osutil.get_firewall_will_wait() # "-C" checks if the iptable rule is available in the chain. It throws an exception with return code 1 if the ip table rule doesnt exist drop_rule = AddFirewallRules.get_wire_non_root_drop_rule(AddFirewallRules.CHECK_COMMAND, dst_ip, wait=wait) if not _execute_run_command(drop_rule): # DROP command doesn't exist indicates then none of the firewall rules are set yet # exiting here as the environment thread will set up all firewall rules logger.info("DROP rule is not available which implies no firewall rules are set yet. Environment thread will set it up.") return else: # DROP rule exists in the ip table chain. Hence checking if the DNS TCP to wireserver rule exists. If not we add it. accept_tcp_rule = AddFirewallRules.get_accept_tcp_rule(AddFirewallRules.CHECK_COMMAND, dst_ip, wait=wait) if not _execute_run_command(accept_tcp_rule): try: logger.info( "Firewall rule to allow DNS TCP request to wireserver for a non root user unavailable. Setting it now.") accept_tcp_rule = AddFirewallRules.get_accept_tcp_rule(AddFirewallRules.INSERT_COMMAND, dst_ip, wait=wait) shellutil.run_command(accept_tcp_rule) logger.info( "Succesfully added firewall rule to allow non root users to do a DNS TCP request to wireserver") except CommandError as error: msg = "Unable to set the non root tcp access firewall rule :" \ "Run command execution for {0} failed with error:{1}.Return Code:{2}"\ .format(error.command, error.stderr, error.returncode) logger.error(msg) else: logger.info( "Not setting the firewall rule to allow DNS TCP request to wireserver for a non root user since it already exists") except Exception as e: msg = "Error while checking ip table rules:{0}".format(ustr(e)) logger.error(msg) def __get_next_upgrade_times(self): """ Get the next upgrade times return: Next Normal Upgrade Time, Next Hotfix Upgrade Time """ def get_next_process_time(last_val, frequency): return now if last_val is None else last_val + frequency now = time.time() next_hotfix_time = get_next_process_time(self._last_hotfix_upgrade_time, conf.get_hotfix_upgrade_frequency()) next_normal_time = get_next_process_time(self._last_normal_upgrade_time, conf.get_normal_upgrade_frequency()) return next_normal_time, next_hotfix_time @staticmethod def __get_agent_upgrade_type(available_agent): # We follow semantic versioning for the agent, if . is same, then . has changed. # In this case, we consider it as a Hotfix upgrade. Else we consider it a Normal upgrade. if available_agent.version.major == CURRENT_VERSION.major and available_agent.version.minor == CURRENT_VERSION.minor: return AgentUpgradeType.Hotfix return AgentUpgradeType.Normal def __upgrade_agent_if_permitted(self): """ Check every 4hrs for a Hotfix Upgrade and 24 hours for a Normal upgrade and upgrade the agent if available. raises: ExitException when a new upgrade is available in the relevant time window, else returns """ next_normal_time, next_hotfix_time = self.__get_next_upgrade_times() now = time.time() # Not permitted to update yet for any of the AgentUpgradeModes if next_hotfix_time > now and next_normal_time > now: return # Update the last upgrade check time even if no new agent is available for upgrade self._last_hotfix_upgrade_time = now if next_hotfix_time <= now else self._last_hotfix_upgrade_time self._last_normal_upgrade_time = now if next_normal_time <= now else self._last_normal_upgrade_time available_agent = self.get_latest_agent_greater_than_daemon() if available_agent is None or available_agent.version <= CURRENT_VERSION: logger.verbose("No agent upgrade discovered") return upgrade_type = self.__get_agent_upgrade_type(available_agent) upgrade_message = "{0} Agent upgrade discovered, updating to {1} -- exiting".format(upgrade_type, available_agent.name) if (upgrade_type == AgentUpgradeType.Hotfix and next_hotfix_time <= now) or ( upgrade_type == AgentUpgradeType.Normal and next_normal_time <= now): raise AgentUpgradeExitException(upgrade_message) def _reset_legacy_blacklisted_agents(self): # Reset the state of all blacklisted agents that were blacklisted by legacy agents (i.e. not during auto-update) # Filter legacy agents which are blacklisted but do not contain a `reason` in their error.json files # (this flag signifies that this agent was blacklisted by the newer agents). try: legacy_blacklisted_agents = [agent for agent in self._load_agents() if agent.is_blacklisted and agent.error.reason == ''] for agent in legacy_blacklisted_agents: agent.clear_error() except Exception as err: logger.warn("Unable to reset legacy blacklisted agents due to: {0}".format(err)) class GuestAgent(object): def __init__(self, path, pkg, protocol, is_fast_track_goal_state): """ If 'path' is given, the object is initialized to the version installed under that path. If 'pkg' is given, the version specified in the package information is downloaded and the object is initialized to that version. 'is_fast_track_goal_state' and 'protocol' are used only when a package is downloaded. NOTE: Prefer using the from_installed_agent and from_agent_package methods instead of calling __init__ directly """ self._is_fast_track_goal_state = is_fast_track_goal_state self.pkg = pkg self._protocol = protocol version = None if path is not None: m = AGENT_DIR_PATTERN.match(path) if m is None: raise UpdateError(u"Illegal agent directory: {0}".format(path)) version = m.group(1) elif self.pkg is not None: version = pkg.version if version is None: raise UpdateError(u"Illegal agent version: {0}".format(version)) self.version = FlexibleVersion(version) location = u"disk" if path is not None else u"package" logger.verbose(u"Loading Agent {0} from {1}", self.name, location) self.error = GuestAgentError(self.get_agent_error_file()) self.error.load() try: self._ensure_downloaded() self._ensure_loaded() except Exception as e: # If we're unable to download/unpack the agent, delete the Agent directory try: if os.path.isdir(self.get_agent_dir()): shutil.rmtree(self.get_agent_dir(), ignore_errors=True) except Exception as err: logger.warn("Unable to delete Agent files: {0}".format(err)) msg = u"Agent {0} install failed with exception:".format( self.name) detailed_msg = '{0} {1}'.format(msg, textutil.format_exception(e)) add_event( AGENT_NAME, version=self.version, op=WALAEventOperation.Install, is_success=False, message=detailed_msg) @staticmethod def from_installed_agent(path): """ Creates an instance of GuestAgent using the agent installed in the given 'path'. """ return GuestAgent(path, None, None, False) @staticmethod def from_agent_package(package, protocol, is_fast_track_goal_state): """ Creates an instance of GuestAgent using the information provided in the 'package'; if that version of the agent is not installed it, it installs it. """ return GuestAgent(None, package, protocol, is_fast_track_goal_state) @property def name(self): return "{0}-{1}".format(AGENT_NAME, self.version) def get_agent_cmd(self): return self.manifest.get_enable_command() def get_agent_dir(self): return os.path.join(conf.get_lib_dir(), self.name) def get_agent_error_file(self): return os.path.join(conf.get_lib_dir(), self.name, AGENT_ERROR_FILE) def get_agent_manifest_path(self): return os.path.join(self.get_agent_dir(), AGENT_MANIFEST_FILE) def get_agent_pkg_path(self): return ".".join((os.path.join(conf.get_lib_dir(), self.name), "zip")) def clear_error(self): self.error.clear() self.error.save() @property def is_available(self): return self.is_downloaded and not self.is_blacklisted @property def is_blacklisted(self): return self.error is not None and self.error.is_blacklisted @property def is_downloaded(self): return self.is_blacklisted or \ os.path.isfile(self.get_agent_manifest_path()) def mark_failure(self, is_fatal=False, reason=''): try: if not os.path.isdir(self.get_agent_dir()): os.makedirs(self.get_agent_dir()) self.error.mark_failure(is_fatal=is_fatal, reason=reason) self.error.save() if self.error.is_blacklisted: msg = u"Agent {0} is permanently blacklisted".format(self.name) logger.warn(msg) add_event(op=WALAEventOperation.AgentBlacklisted, is_success=False, message=msg, log_event=False, version=self.version) except Exception as e: logger.warn(u"Agent {0} failed recording error state: {1}", self.name, ustr(e)) def _ensure_downloaded(self): logger.verbose(u"Ensuring Agent {0} is downloaded", self.name) if self.is_downloaded: logger.verbose(u"Agent {0} was previously downloaded - skipping download", self.name) return if self.pkg is None: raise UpdateError(u"Agent {0} is missing package and download URIs".format( self.name)) self._download() msg = u"Agent {0} downloaded successfully".format(self.name) logger.verbose(msg) add_event( AGENT_NAME, version=self.version, op=WALAEventOperation.Install, is_success=True, message=msg) def _ensure_loaded(self): self._load_manifest() self._load_error() def _download(self): try: self._protocol.client.download_zip_package("agent package", self.pkg.uris, self.get_agent_pkg_path(), self.get_agent_dir(), use_verify_header=self._is_fast_track_goal_state) except Exception as exception: msg = "Unable to download Agent {0}: {1}".format(self.name, ustr(exception)) add_event( AGENT_NAME, op=WALAEventOperation.Download, version=CURRENT_VERSION, is_success=False, message=msg) raise UpdateError(msg) def _load_error(self): try: self.error = GuestAgentError(self.get_agent_error_file()) self.error.load() logger.verbose(u"Agent {0} error state: {1}", self.name, ustr(self.error)) except Exception as e: logger.warn(u"Agent {0} failed loading error state: {1}", self.name, ustr(e)) def _load_manifest(self): path = self.get_agent_manifest_path() if not os.path.isfile(path): msg = u"Agent {0} is missing the {1} file".format(self.name, AGENT_MANIFEST_FILE) raise UpdateError(msg) with open(path, "r") as manifest_file: try: manifests = json.load(manifest_file) except Exception as e: msg = u"Agent {0} has a malformed {1} ({2})".format(self.name, AGENT_MANIFEST_FILE, ustr(e)) raise UpdateError(msg) if type(manifests) is list: if len(manifests) <= 0: msg = u"Agent {0} has an empty {1}".format(self.name, AGENT_MANIFEST_FILE) raise UpdateError(msg) manifest = manifests[0] else: manifest = manifests try: self.manifest = HandlerManifest(manifest) # pylint: disable=W0201 if len(self.manifest.get_enable_command()) <= 0: raise Exception(u"Manifest is missing the enable command") except Exception as e: msg = u"Agent {0} has an illegal {1}: {2}".format( self.name, AGENT_MANIFEST_FILE, ustr(e)) raise UpdateError(msg) logger.verbose( u"Agent {0} loaded manifest from {1}", self.name, self.get_agent_manifest_path()) logger.verbose(u"Successfully loaded Agent {0} {1}: {2}", self.name, AGENT_MANIFEST_FILE, ustr(self.manifest.data)) return class GuestAgentError(object): def __init__(self, path): self.last_failure = 0.0 self.was_fatal = False if path is None: raise UpdateError(u"GuestAgentError requires a path") self.path = path self.failure_count = 0 self.reason = '' self.clear() return def mark_failure(self, is_fatal=False, reason=''): self.last_failure = time.time() self.failure_count += 1 self.was_fatal = is_fatal self.reason = reason return def clear(self): self.last_failure = 0.0 self.failure_count = 0 self.was_fatal = False self.reason = '' return @property def is_blacklisted(self): return self.was_fatal or self.failure_count >= MAX_FAILURE def load(self): if self.path is not None and os.path.isfile(self.path): try: with open(self.path, 'r') as f: self.from_json(json.load(f)) except Exception as error: # The error.json file is only supposed to be written only by the agent. # If for whatever reason the file is malformed, just delete it to reset state of the errors. logger.warn( "Ran into error when trying to load error file {0}, deleting it to clean state. Error: {1}".format( self.path, textutil.format_exception(error))) try: os.remove(self.path) except Exception: # We try best case efforts to delete the file, ignore error if we're unable to do so pass return def save(self): if os.path.isdir(os.path.dirname(self.path)): with open(self.path, 'w') as f: json.dump(self.to_json(), f) return def from_json(self, data): self.last_failure = max(self.last_failure, data.get(u"last_failure", 0.0)) self.failure_count = max(self.failure_count, data.get(u"failure_count", 0)) self.was_fatal = self.was_fatal or data.get(u"was_fatal", False) reason = data.get(u"reason", '') self.reason = reason if reason != '' else self.reason return def to_json(self): data = { u"last_failure": self.last_failure, u"failure_count": self.failure_count, u"was_fatal": self.was_fatal, u"reason": ustr(self.reason) } return data def __str__(self): return "Last Failure: {0}, Total Failures: {1}, Fatal: {2}, Reason: {3}".format( self.last_failure, self.failure_count, self.was_fatal, self.reason) WALinuxAgent-2.9.1.1/azurelinuxagent/pa/000077500000000000000000000000001446033677600200355ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/azurelinuxagent/pa/__init__.py000066400000000000000000000011661446033677600221520ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # WALinuxAgent-2.9.1.1/azurelinuxagent/pa/deprovision/000077500000000000000000000000001446033677600223765ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/azurelinuxagent/pa/deprovision/__init__.py000066400000000000000000000013501446033677600245060ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # from azurelinuxagent.pa.deprovision.factory import get_deprovision_handler __all__ = ["get_deprovision_handler"] WALinuxAgent-2.9.1.1/azurelinuxagent/pa/deprovision/arch.py000066400000000000000000000025021446033677600236640ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import azurelinuxagent.common.utils.fileutil as fileutil from azurelinuxagent.pa.deprovision.default import DeprovisionHandler, \ DeprovisionAction class ArchDeprovisionHandler(DeprovisionHandler): def __init__(self): # pylint: disable=W0235 super(ArchDeprovisionHandler, self).__init__() def setup(self, deluser): warnings, actions = super(ArchDeprovisionHandler, self).setup(deluser) warnings.append("WARNING! /etc/machine-id will be removed.") files_to_del = ['/etc/machine-id'] actions.append(DeprovisionAction(fileutil.rm_files, files_to_del)) return warnings, actions WALinuxAgent-2.9.1.1/azurelinuxagent/pa/deprovision/clearlinux.py000066400000000000000000000023501446033677600251160ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # # pylint: disable=W0611 import azurelinuxagent.common.utils.fileutil as fileutil from azurelinuxagent.pa.deprovision.default import DeprovisionHandler, \ DeprovisionAction # pylint: enable=W0611 class ClearLinuxDeprovisionHandler(DeprovisionHandler): def __init__(self, distro): # pylint: disable=W0231 self.distro = distro def setup(self, deluser): warnings, actions = super(ClearLinuxDeprovisionHandler, self).setup(deluser) # Probably should just wipe /etc and /var here return warnings, actions WALinuxAgent-2.9.1.1/azurelinuxagent/pa/deprovision/coreos.py000066400000000000000000000025111446033677600242410ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import azurelinuxagent.common.utils.fileutil as fileutil from azurelinuxagent.pa.deprovision.default import DeprovisionHandler, \ DeprovisionAction class CoreOSDeprovisionHandler(DeprovisionHandler): def __init__(self): # pylint: disable=W0235 super(CoreOSDeprovisionHandler, self).__init__() def setup(self, deluser): warnings, actions = super(CoreOSDeprovisionHandler, self).setup(deluser) warnings.append("WARNING! /etc/machine-id will be removed.") files_to_del = ['/etc/machine-id'] actions.append(DeprovisionAction(fileutil.rm_files, files_to_del)) return warnings, actions WALinuxAgent-2.9.1.1/azurelinuxagent/pa/deprovision/default.py000066400000000000000000000262231446033677600244010ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import glob import os.path import re import signal import sys import azurelinuxagent.common.conf as conf import azurelinuxagent.common.utils.fileutil as fileutil from azurelinuxagent.common import version from azurelinuxagent.common.cgroupconfigurator import _AGENT_DROP_IN_FILE_SLICE, _DROP_IN_FILE_CPU_ACCOUNTING, \ _DROP_IN_FILE_CPU_QUOTA, _DROP_IN_FILE_MEMORY_ACCOUNTING, LOGCOLLECTOR_SLICE from azurelinuxagent.common.exception import ProtocolError from azurelinuxagent.common.osutil import get_osutil, systemd from azurelinuxagent.common.persist_firewall_rules import PersistFirewallRulesHandler from azurelinuxagent.common.protocol.util import get_protocol_util from azurelinuxagent.ga.exthandlers import HANDLER_COMPLETE_NAME_PATTERN def read_input(message): if sys.version_info[0] >= 3: return input(message) else: # This is not defined in python3, and the linter will thus # throw an undefined-variable error on this line. # Suppress it here. return raw_input(message) # pylint: disable=E0602 class DeprovisionAction(object): def __init__(self, func, args=None, kwargs=None): if args is None: args = [] if kwargs is None: kwargs = {} self.func = func self.args = args self.kwargs = kwargs def invoke(self): self.func(*self.args, **self.kwargs) class DeprovisionHandler(object): def __init__(self): self.osutil = get_osutil() self.protocol_util = get_protocol_util() self.actions_running = False signal.signal(signal.SIGINT, self.handle_interrupt_signal) def del_root_password(self, warnings, actions): warnings.append("WARNING! root password will be disabled. " "You will not be able to login as root.") actions.append(DeprovisionAction(self.osutil.del_root_password)) def del_user(self, warnings, actions): try: ovfenv = self.protocol_util.get_ovf_env() except ProtocolError: warnings.append("WARNING! ovf-env.xml is not found.") warnings.append("WARNING! Skip delete user.") return username = ovfenv.username warnings.append(("WARNING! {0} account and entire home directory " "will be deleted.").format(username)) actions.append(DeprovisionAction(self.osutil.del_account, [username])) def regen_ssh_host_key(self, warnings, actions): warnings.append("WARNING! All SSH host key pairs will be deleted.") actions.append(DeprovisionAction(fileutil.rm_files, [conf.get_ssh_key_glob()])) def stop_agent_service(self, warnings, actions): warnings.append("WARNING! The waagent service will be stopped.") actions.append(DeprovisionAction(self.osutil.stop_agent_service)) def del_dirs(self, warnings, actions): # pylint: disable=W0613 dirs = [conf.get_lib_dir(), conf.get_ext_log_dir()] actions.append(DeprovisionAction(fileutil.rm_dirs, dirs)) def del_files(self, warnings, actions): # pylint: disable=W0613 files = ['/root/.bash_history', conf.get_agent_log_file()] actions.append(DeprovisionAction(fileutil.rm_files, files)) # For OpenBSD actions.append(DeprovisionAction(fileutil.rm_files, ["/etc/random.seed", "/var/db/host.random", "/etc/isakmpd/local.pub", "/etc/isakmpd/private/local.key", "/etc/iked/private/local.key", "/etc/iked/local.pub"])) def del_resolv(self, warnings, actions): warnings.append("WARNING! /etc/resolv.conf will be deleted.") files_to_del = ["/etc/resolv.conf"] actions.append(DeprovisionAction(fileutil.rm_files, files_to_del)) def del_dhcp_lease(self, warnings, actions): warnings.append("WARNING! Cached DHCP leases will be deleted.") dirs_to_del = ["/var/lib/dhclient", "/var/lib/dhcpcd", "/var/lib/dhcp"] actions.append(DeprovisionAction(fileutil.rm_dirs, dirs_to_del)) # For FreeBSD and OpenBSD actions.append(DeprovisionAction(fileutil.rm_files, ["/var/db/dhclient.leases.*"])) # For FreeBSD, NM controlled actions.append(DeprovisionAction(fileutil.rm_files, ["/var/lib/NetworkManager/dhclient-*.lease"])) def del_ext_handler_files(self, warnings, actions): # pylint: disable=W0613 ext_dirs = [d for d in os.listdir(conf.get_lib_dir()) if os.path.isdir(os.path.join(conf.get_lib_dir(), d)) and re.match(HANDLER_COMPLETE_NAME_PATTERN, d) is not None and not version.is_agent_path(d)] for ext_dir in ext_dirs: ext_base = os.path.join(conf.get_lib_dir(), ext_dir) files = glob.glob(os.path.join(ext_base, 'status', '*.status')) files += glob.glob(os.path.join(ext_base, 'config', '*.settings')) files += glob.glob(os.path.join(ext_base, 'config', 'HandlerStatus')) files += glob.glob(os.path.join(ext_base, 'mrseq')) if len(files) > 0: actions.append(DeprovisionAction(fileutil.rm_files, files)) def del_lib_dir_files(self, warnings, actions): # pylint: disable=W0613 known_files = [ 'HostingEnvironmentConfig.xml', 'Incarnation', 'partition', 'Protocol', 'SharedConfig.xml', 'WireServerEndpoint' ] known_files_glob = [ 'Extensions.*.xml', 'ExtensionsConfig.*.xml', 'GoalState.*.xml' ] lib_dir = conf.get_lib_dir() files = [f for f in \ [os.path.join(lib_dir, kf) for kf in known_files] \ if os.path.isfile(f)] for p in known_files_glob: files += glob.glob(os.path.join(lib_dir, p)) if len(files) > 0: actions.append(DeprovisionAction(fileutil.rm_files, files)) def reset_hostname(self, warnings, actions): # pylint: disable=W0613 localhost = ["localhost.localdomain"] actions.append(DeprovisionAction(self.osutil.set_hostname, localhost)) actions.append(DeprovisionAction(self.osutil.set_dhcp_hostname, localhost)) def setup(self, deluser): warnings = [] actions = [] self.stop_agent_service(warnings, actions) if conf.get_regenerate_ssh_host_key(): self.regen_ssh_host_key(warnings, actions) self.del_dhcp_lease(warnings, actions) self.reset_hostname(warnings, actions) if conf.get_delete_root_password(): self.del_root_password(warnings, actions) self.del_dirs(warnings, actions) self.del_files(warnings, actions) self.del_resolv(warnings, actions) if deluser: self.del_user(warnings, actions) self.del_persist_firewall_rules(actions) self.remove_agent_cgroup_config(actions) return warnings, actions def setup_changed_unique_id(self): warnings = [] actions = [] self.del_dhcp_lease(warnings, actions) self.del_lib_dir_files(warnings, actions) self.del_ext_handler_files(warnings, actions) self.del_persist_firewall_rules(actions) self.remove_agent_cgroup_config(actions) return warnings, actions def run(self, force=False, deluser=False): warnings, actions = self.setup(deluser) self.do_warnings(warnings) if self.do_confirmation(force=force): self.do_actions(actions) def run_changed_unique_id(self): ''' Clean-up files and directories that may interfere when the VM unique identifier has changed. While users *should* manually deprovision a VM, the files removed by this routine will help keep the agent from getting confused (since incarnation and extension settings, among other items, will no longer be monotonically increasing). ''' warnings, actions = self.setup_changed_unique_id() self.do_warnings(warnings) self.do_actions(actions) def do_actions(self, actions): self.actions_running = True for action in actions: action.invoke() self.actions_running = False def do_confirmation(self, force=False): if force: return True confirm = read_input("Do you want to proceed (y/n)") return True if confirm.lower().startswith('y') else False def do_warnings(self, warnings): for warning in warnings: print(warning) def handle_interrupt_signal(self, signum, frame): # pylint: disable=W0613 if not self.actions_running: print("Deprovision is interrupted.") sys.exit(0) print ('Deprovisioning may not be interrupted.') return @staticmethod def del_persist_firewall_rules(actions): agent_network_service_path = PersistFirewallRulesHandler.get_service_file_path() actions.append(DeprovisionAction(fileutil.rm_files, [agent_network_service_path, os.path.join(conf.get_lib_dir(), PersistFirewallRulesHandler.BINARY_FILE_NAME)])) @staticmethod def remove_agent_cgroup_config(actions): # Get all service drop in file paths agent_drop_in_path = systemd.get_agent_drop_in_path() slice_path = os.path.join(agent_drop_in_path, _AGENT_DROP_IN_FILE_SLICE) cpu_accounting_path = os.path.join(agent_drop_in_path, _DROP_IN_FILE_CPU_ACCOUNTING) cpu_quota_path = os.path.join(agent_drop_in_path, _DROP_IN_FILE_CPU_QUOTA) mem_accounting_path = os.path.join(agent_drop_in_path, _DROP_IN_FILE_MEMORY_ACCOUNTING) # Get log collector slice unit_file_install_path = systemd.get_unit_file_install_path() log_collector_slice_path = os.path.join(unit_file_install_path, LOGCOLLECTOR_SLICE) actions.append(DeprovisionAction(fileutil.rm_files, [slice_path, cpu_accounting_path, cpu_quota_path, mem_accounting_path, log_collector_slice_path])) WALinuxAgent-2.9.1.1/azurelinuxagent/pa/deprovision/factory.py000066400000000000000000000033401446033677600244170ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # from distutils.version import LooseVersion as Version # pylint: disable=no-name-in-module, import-error from azurelinuxagent.common.version import DISTRO_NAME, DISTRO_VERSION, DISTRO_FULL_NAME from .arch import ArchDeprovisionHandler from .clearlinux import ClearLinuxDeprovisionHandler from .coreos import CoreOSDeprovisionHandler from .default import DeprovisionHandler from .ubuntu import UbuntuDeprovisionHandler, Ubuntu1804DeprovisionHandler def get_deprovision_handler(distro_name=DISTRO_NAME, distro_version=DISTRO_VERSION, distro_full_name=DISTRO_FULL_NAME): if distro_name == "arch": return ArchDeprovisionHandler() if distro_name == "ubuntu": if Version(distro_version) >= Version('18.04'): return Ubuntu1804DeprovisionHandler() else: return UbuntuDeprovisionHandler() if distro_name in ("flatcar", "coreos"): return CoreOSDeprovisionHandler() if "Clear Linux" in distro_full_name: return ClearLinuxDeprovisionHandler() # pylint: disable=E1120 return DeprovisionHandler() WALinuxAgent-2.9.1.1/azurelinuxagent/pa/deprovision/ubuntu.py000066400000000000000000000042141446033677600242730ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import azurelinuxagent.common.utils.fileutil as fileutil from azurelinuxagent.pa.deprovision.default import DeprovisionHandler, \ DeprovisionAction class UbuntuDeprovisionHandler(DeprovisionHandler): def __init__(self): # pylint: disable=W0235 super(UbuntuDeprovisionHandler, self).__init__() def del_resolv(self, warnings, actions): if os.path.realpath( '/etc/resolv.conf') != '/run/resolvconf/resolv.conf': warnings.append("WARNING! /etc/resolv.conf will be deleted.") files_to_del = ["/etc/resolv.conf"] actions.append(DeprovisionAction(fileutil.rm_files, files_to_del)) else: warnings.append("WARNING! /etc/resolvconf/resolv.conf.d/tail " "and /etc/resolvconf/resolv.conf.d/original will " "be deleted.") files_to_del = ["/etc/resolvconf/resolv.conf.d/tail", "/etc/resolvconf/resolv.conf.d/original"] actions.append(DeprovisionAction(fileutil.rm_files, files_to_del)) class Ubuntu1804DeprovisionHandler(UbuntuDeprovisionHandler): def __init__(self): # pylint: disable=W0235 super(Ubuntu1804DeprovisionHandler, self).__init__() def del_resolv(self, warnings, actions): # no changes will be made to /etc/resolv.conf warnings.append("WARNING! /etc/resolv.conf will NOT be removed, this is a behavior change to earlier " "versions of Ubuntu.") WALinuxAgent-2.9.1.1/azurelinuxagent/pa/provision/000077500000000000000000000000001446033677600220655ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/azurelinuxagent/pa/provision/__init__.py000066400000000000000000000012751446033677600242030ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # from azurelinuxagent.pa.provision.factory import get_provision_handler WALinuxAgent-2.9.1.1/azurelinuxagent/pa/provision/cloudinit.py000066400000000000000000000143771446033677600244450ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import os.path import time from datetime import datetime import azurelinuxagent.common.conf as conf import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.fileutil as fileutil import azurelinuxagent.common.utils.shellutil as shellutil # pylint: disable=W0611 from azurelinuxagent.common.event import elapsed_milliseconds, WALAEventOperation # pylint: disable=W0611 from azurelinuxagent.common.exception import ProvisionError, ProtocolError from azurelinuxagent.common.future import ustr from azurelinuxagent.common.protocol.util import OVF_FILE_NAME from azurelinuxagent.common.protocol.ovfenv import OvfEnv from azurelinuxagent.pa.provision.default import ProvisionHandler from azurelinuxagent.pa.provision.cloudinitdetect import cloud_init_is_enabled class CloudInitProvisionHandler(ProvisionHandler): def __init__(self): # pylint: disable=W0235 super(CloudInitProvisionHandler, self).__init__() def run(self): try: if super(CloudInitProvisionHandler, self).check_provisioned_file(): logger.info("Provisioning already completed, skipping.") return utc_start = datetime.utcnow() logger.info("Running CloudInit provisioning handler") self.wait_for_ovfenv() self.protocol_util.get_protocol() # Trigger protocol detection self.report_not_ready("Provisioning", "Starting") thumbprint = self.wait_for_ssh_host_key() # pylint: disable=W0612 self.write_provisioned() logger.info("Finished provisioning") self.report_ready() self.report_event("Provisioning with cloud-init succeeded ({0}s)".format(self._get_uptime_seconds()), is_success=True, duration=elapsed_milliseconds(utc_start)) except ProvisionError as e: msg = "Provisioning with cloud-init failed: {0} ({1}s)".format(ustr(e), self._get_uptime_seconds()) logger.error(msg) self.report_not_ready("ProvisioningFailed", ustr(e)) self.report_event(msg) return def wait_for_ovfenv(self, max_retry=1800, sleep_time=1): """ Wait for cloud-init to copy ovf-env.xml file from provision ISO """ ovf_file_path = os.path.join(conf.get_lib_dir(), OVF_FILE_NAME) logging_interval = 10 max_logging_interval = 320 for retry in range(0, max_retry): if os.path.isfile(ovf_file_path): try: ovf_env = OvfEnv(fileutil.read_file(ovf_file_path)) self.handle_provision_guest_agent(ovf_env.provision_guest_agent) return except ProtocolError as pe: raise ProvisionError("OVF xml could not be parsed " "[{0}]: {1}".format(ovf_file_path, ustr(pe))) else: if retry < max_retry - 1: if retry % logging_interval == 0: logger.info( "Waiting for cloud-init to copy ovf-env.xml to {0} " "[{1} retries remaining, " "sleeping {2}s between retries]".format(ovf_file_path, max_retry - retry, sleep_time)) if not cloud_init_is_enabled(): logger.warn("cloud-init does not appear to be enabled") logging_interval = min(logging_interval * 2, max_logging_interval) time.sleep(sleep_time) raise ProvisionError("Giving up, ovf-env.xml was not copied to {0} " "after {1}s".format(ovf_file_path, max_retry * sleep_time)) def wait_for_ssh_host_key(self, max_retry=1800, sleep_time=1): """ Wait for cloud-init to generate ssh host key """ keypair_type = conf.get_ssh_host_keypair_type() # pylint: disable=W0612 path = conf.get_ssh_key_public_path() logging_interval = 10 max_logging_interval = 320 for retry in range(0, max_retry): if os.path.isfile(path): logger.info("ssh host key found at: {0}".format(path)) try: thumbprint = self.get_ssh_host_key_thumbprint(chk_err=False) logger.info("Thumbprint obtained from : {0}".format(path)) return thumbprint except ProvisionError: logger.warn("Could not get thumbprint from {0}".format(path)) if retry < max_retry - 1: if retry % logging_interval == 0: logger.info("Waiting for ssh host key be generated at {0} " "[{1} attempts remaining, " "sleeping {2}s between retries]".format(path, max_retry - retry, sleep_time)) if not cloud_init_is_enabled(): logger.warn("cloud-init does not appear to be running") logging_interval = min(logging_interval * 2, max_logging_interval) time.sleep(sleep_time) raise ProvisionError("Giving up, ssh host key was not found at {0} " "after {1}s".format(path, max_retry * sleep_time)) WALinuxAgent-2.9.1.1/azurelinuxagent/pa/provision/cloudinitdetect.py000066400000000000000000000035261446033677600256300ustar00rootroot00000000000000"""Module for detecting the existence of cloud-init""" import subprocess import azurelinuxagent.common.logger as logger def _cloud_init_is_enabled_systemd(): """ Determine whether or not cloud-init is enabled on a systemd machine. Args: None Returns: bool: True if cloud-init is enabled, False if otherwise. """ try: systemctl_output = subprocess.check_output([ 'systemctl', 'is-enabled', 'cloud-init-local.service' ], stderr=subprocess.STDOUT).decode('utf-8').replace('\n', '') unit_is_enabled = systemctl_output == 'enabled' # pylint: disable=broad-except except Exception as exc: logger.info('Unable to get cloud-init enabled status from systemctl: {0}'.format(exc)) unit_is_enabled = False return unit_is_enabled def _cloud_init_is_enabled_service(): """ Determine whether or not cloud-init is enabled on a non-systemd machine. Args: None Returns: bool: True if cloud-init is enabled, False if otherwise. """ try: subprocess.check_output([ 'service', 'cloud-init', 'status' ], stderr=subprocess.STDOUT) unit_is_enabled = True # pylint: disable=broad-except except Exception as exc: logger.info('Unable to get cloud-init enabled status from service: {0}'.format(exc)) unit_is_enabled = False return unit_is_enabled def cloud_init_is_enabled(): """ Determine whether or not cloud-init is enabled. Args: None Returns: bool: True if cloud-init is enabled, False if otherwise. """ unit_is_enabled = _cloud_init_is_enabled_systemd() or _cloud_init_is_enabled_service() logger.info('cloud-init is enabled: {0}'.format(unit_is_enabled)) return unit_is_enabled WALinuxAgent-2.9.1.1/azurelinuxagent/pa/provision/default.py000066400000000000000000000262511446033677600240710ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # """ Provision handler """ import os import os.path import re import time from datetime import datetime import azurelinuxagent.common.conf as conf import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.shellutil as shellutil import azurelinuxagent.common.utils.fileutil as fileutil from azurelinuxagent.common.future import ustr from azurelinuxagent.common.event import add_event, WALAEventOperation, \ elapsed_milliseconds from azurelinuxagent.common.exception import ProvisionError, ProtocolError, \ OSUtilError from azurelinuxagent.common.osutil import get_osutil from azurelinuxagent.common.protocol.restapi import ProvisionStatus from azurelinuxagent.common.protocol.util import get_protocol_util from azurelinuxagent.common.version import AGENT_NAME from azurelinuxagent.pa.provision.cloudinitdetect import cloud_init_is_enabled CUSTOM_DATA_FILE = "CustomData" CLOUD_INIT_PATTERN = b".*/bin/cloud-init.*" CLOUD_INIT_REGEX = re.compile(CLOUD_INIT_PATTERN) PROVISIONED_FILE = 'provisioned' class ProvisionHandler(object): def __init__(self): self.osutil = get_osutil() self.protocol_util = get_protocol_util() def run(self): if not conf.get_provision_enabled(): logger.info("Provisioning is disabled, skipping.") self.write_provisioned() self.report_ready() return try: utc_start = datetime.utcnow() thumbprint = None # pylint: disable=W0612 if self.check_provisioned_file(): logger.info("Provisioning already completed, skipping.") return logger.info("Running default provisioning handler") if cloud_init_is_enabled(): raise ProvisionError("cloud-init appears to be installed and enabled, " "this is not expected, cannot continue") logger.info("Copying ovf-env.xml") ovf_env = self.protocol_util.copy_ovf_env() self.protocol_util.get_protocol() # Trigger protocol detection self.report_not_ready("Provisioning", "Starting") logger.info("Starting provisioning") self.provision(ovf_env) thumbprint = self.reg_ssh_host_key() self.osutil.restart_ssh_service() self.write_provisioned() self.report_event("Provisioning succeeded ({0}s)".format(self._get_uptime_seconds()), is_success=True, duration=elapsed_milliseconds(utc_start)) self.handle_provision_guest_agent(ovf_env.provision_guest_agent) self.report_ready() logger.info("Provisioning complete") except (ProtocolError, ProvisionError) as e: msg = "Provisioning failed: {0} ({1}s)".format(ustr(e), self._get_uptime_seconds()) logger.error(msg) self.report_not_ready("ProvisioningFailed", ustr(e)) self.report_event(msg, is_success=False) return @staticmethod def _get_uptime_seconds(): try: with open('/proc/uptime') as fh: uptime, _ = fh.readline().split() return uptime except: # pylint: disable=W0702 return 0 def reg_ssh_host_key(self): keypair_type = conf.get_ssh_host_keypair_type() if conf.get_regenerate_ssh_host_key(): fileutil.rm_files(conf.get_ssh_key_glob()) if conf.get_ssh_host_keypair_mode() == "auto": # pylint: disable=W0105 ''' The -A option generates all supported key types. This is supported since OpenSSH 5.9 (2011). ''' # pylint: enable=W0105 shellutil.run("ssh-keygen -A") else: keygen_cmd = "ssh-keygen -N '' -t {0} -f {1}" shellutil.run(keygen_cmd. format(keypair_type, conf.get_ssh_key_private_path())) return self.get_ssh_host_key_thumbprint() def get_ssh_host_key_thumbprint(self, chk_err=True): cmd = "ssh-keygen -lf {0}".format(conf.get_ssh_key_public_path()) ret = shellutil.run_get_output(cmd, chk_err=chk_err) if ret[0] == 0: return ret[1].rstrip().split()[1].replace(':', '') else: raise ProvisionError(("Failed to generate ssh host key: " "ret={0}, out= {1}").format(ret[0], ret[1])) @staticmethod def provisioned_file_path(): return os.path.join(conf.get_lib_dir(), PROVISIONED_FILE) @staticmethod def is_provisioned(): """ A VM is considered provisioned *anytime* the provisioning sentinel file exists and not provisioned *anytime* the file is absent. """ return os.path.isfile(ProvisionHandler.provisioned_file_path()) def check_provisioned_file(self): """ If the VM was provisioned using an agent that did not record the VM unique identifier, the provisioning file will be re-written to include the identifier. A warning is logged *if* the VM unique identifier has changed since VM was provisioned. Returns False if the VM has not been provisioned. """ if not ProvisionHandler.is_provisioned(): return False s = fileutil.read_file(ProvisionHandler.provisioned_file_path()).strip() if not self.osutil.is_current_instance_id(s): if len(s) > 0: logger.warn("VM is provisioned, " "but the VM unique identifier has changed -- " "clearing cached state") from azurelinuxagent.pa.deprovision \ import get_deprovision_handler deprovision_handler = get_deprovision_handler() deprovision_handler.run_changed_unique_id() self.write_provisioned() self.report_ready() return True def write_provisioned(self): fileutil.write_file( ProvisionHandler.provisioned_file_path(), get_osutil().get_instance_id()) @staticmethod def write_agent_disabled(): logger.warn("Disabling guest agent in accordance with ovf-env.xml") fileutil.write_file(conf.get_disable_agent_file_path(), '') def handle_provision_guest_agent(self, provision_guest_agent): self.report_event(message=provision_guest_agent, is_success=True, duration=0, operation=WALAEventOperation.ProvisionGuestAgent) if provision_guest_agent and provision_guest_agent.lower() == 'false': self.write_agent_disabled() def provision(self, ovfenv): logger.info("Handle ovf-env.xml.") try: logger.info("Set hostname [{0}]".format(ovfenv.hostname)) self.osutil.set_hostname(ovfenv.hostname) logger.info("Publish hostname [{0}]".format(ovfenv.hostname)) self.osutil.publish_hostname(ovfenv.hostname) self.config_user_account(ovfenv) self.save_customdata(ovfenv) if conf.get_delete_root_password(): self.osutil.del_root_password() except OSUtilError as e: raise ProvisionError("Failed to provision: {0}".format(ustr(e))) def config_user_account(self, ovfenv): logger.info("Create user account if not exists") self.osutil.useradd(ovfenv.username) if ovfenv.user_password is not None: logger.info("Set user password.") crypt_id = conf.get_password_cryptid() salt_len = conf.get_password_crypt_salt_len() self.osutil.chpasswd(ovfenv.username, ovfenv.user_password, crypt_id=crypt_id, salt_len=salt_len) logger.info("Configure sudoer") self.osutil.conf_sudoer(ovfenv.username, nopasswd=ovfenv.user_password is None) logger.info("Configure sshd") self.osutil.conf_sshd(ovfenv.disable_ssh_password_auth) self.deploy_ssh_pubkeys(ovfenv) self.deploy_ssh_keypairs(ovfenv) def save_customdata(self, ovfenv): customdata = ovfenv.customdata if customdata is None: return lib_dir = conf.get_lib_dir() if conf.get_decode_customdata() or conf.get_execute_customdata(): logger.info("Decode custom data") customdata = self.osutil.decode_customdata(customdata) logger.info("Save custom data") customdata_file = os.path.join(lib_dir, CUSTOM_DATA_FILE) fileutil.write_file(customdata_file, customdata) if conf.get_execute_customdata(): start = time.time() logger.info("Execute custom data") os.chmod(customdata_file, 0o700) shellutil.run(customdata_file) add_event(name=AGENT_NAME, duration=int(time.time() - start), is_success=True, op=WALAEventOperation.CustomData) def deploy_ssh_pubkeys(self, ovfenv): for pubkey in ovfenv.ssh_pubkeys: logger.info("Deploy ssh public key.") self.osutil.deploy_ssh_pubkey(ovfenv.username, pubkey) def deploy_ssh_keypairs(self, ovfenv): for keypair in ovfenv.ssh_keypairs: logger.info("Deploy ssh key pairs.") self.osutil.deploy_ssh_keypair(ovfenv.username, keypair) def report_event(self, message, is_success=False, duration=0, operation=WALAEventOperation.Provision): add_event(name=AGENT_NAME, message=message, duration=duration, is_success=is_success, op=operation) def report_not_ready(self, sub_status, description): status = ProvisionStatus(status="NotReady", subStatus=sub_status, description=description) try: protocol = self.protocol_util.get_protocol() protocol.report_provision_status(status) except ProtocolError as e: logger.error("Reporting NotReady failed: {0}", e) self.report_event(ustr(e)) def report_ready(self): status = ProvisionStatus(status="Ready") try: protocol = self.protocol_util.get_protocol() protocol.report_provision_status(status) except ProtocolError as e: logger.error("Reporting Ready failed: {0}", e) self.report_event(ustr(e)) WALinuxAgent-2.9.1.1/azurelinuxagent/pa/provision/factory.py000066400000000000000000000030441446033677600241070ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import azurelinuxagent.common.conf as conf from azurelinuxagent.common import logger from azurelinuxagent.common.version import DISTRO_NAME, DISTRO_VERSION, \ DISTRO_FULL_NAME from .default import ProvisionHandler from .cloudinit import CloudInitProvisionHandler, cloud_init_is_enabled def get_provision_handler(distro_name=DISTRO_NAME, # pylint: disable=W0613 distro_version=DISTRO_VERSION, # pylint: disable=W0613 distro_full_name=DISTRO_FULL_NAME): # pylint: disable=W0613 provisioning_agent = conf.get_provisioning_agent() if provisioning_agent == 'cloud-init' or ( provisioning_agent == 'auto' and cloud_init_is_enabled()): logger.info('Using cloud-init for provisioning') return CloudInitProvisionHandler() logger.info('Using waagent for provisioning') return ProvisionHandler() WALinuxAgent-2.9.1.1/azurelinuxagent/pa/rdma/000077500000000000000000000000001446033677600207605ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/azurelinuxagent/pa/rdma/__init__.py000066400000000000000000000012631446033677600230730ustar00rootroot00000000000000# Copyright 2016 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # from azurelinuxagent.pa.rdma.factory import get_rdma_handler WALinuxAgent-2.9.1.1/azurelinuxagent/pa/rdma/centos.py000066400000000000000000000243341446033677600226330ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import glob # pylint: disable=W0611 import os import re import time import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.shellutil as shellutil from azurelinuxagent.common.rdma import RDMAHandler class CentOSRDMAHandler(RDMAHandler): rdma_user_mode_package_name = 'microsoft-hyper-v-rdma' rdma_kernel_mode_package_name = 'kmod-microsoft-hyper-v-rdma' rdma_wrapper_package_name = 'msft-rdma-drivers' hyper_v_package_name = "hypervkvpd" hyper_v_package_name_new = "microsoft-hyper-v" version_major = None version_minor = None def __init__(self, distro_version): v = distro_version.split('.') if len(v) < 2: raise Exception('Unexpected centos version: %s' % distro_version) self.version_major, self.version_minor = v[0], v[1] def install_driver(self): """ Install the KVP daemon and the appropriate RDMA driver package for the RDMA firmware. """ # Check and install the KVP deamon if it not running time.sleep(10) # give some time for the hv_hvp_daemon to start up. kvpd_running = RDMAHandler.is_kvp_daemon_running() logger.info('RDMA: kvp daemon running: %s' % kvpd_running) if not kvpd_running: self.check_or_install_kvp_daemon() time.sleep(10) # wait for post-install reboot or kvp to come up # Find out RDMA firmware version and see if the existing package needs # updating or if the package is missing altogether (and install it) fw_version = self.get_rdma_version() if not fw_version: raise Exception('Cannot determine RDMA firmware version') logger.info("RDMA: found firmware version: {0}".format(fw_version)) fw_version = self.get_int_rdma_version(fw_version) installed_pkg = self.get_rdma_package_info() if installed_pkg: logger.info( 'RDMA: driver package present: {0}'.format(installed_pkg)) if self.is_rdma_package_up_to_date(installed_pkg, fw_version): logger.info('RDMA: driver package is up-to-date') return else: logger.info('RDMA: driver package needs updating') self.update_rdma_package(fw_version) else: logger.info('RDMA: driver package is NOT installed') self.update_rdma_package(fw_version) def is_rdma_package_up_to_date(self, pkg, fw_version): # Example match (pkg name, -, followed by 3 segments, fw_version and -): # - pkg=microsoft-hyper-v-rdma-4.1.0.142-20160323.x86_64 # - fw_version=142 pattern = '{0}-(\d+\.){{3,}}({1})-'.format(self.rdma_user_mode_package_name, fw_version) # pylint: disable=W1401 return re.match(pattern, pkg) @staticmethod def get_int_rdma_version(version): s = version.split('.') if len(s) == 0: raise Exception('Unexpected RDMA firmware version: "%s"' % version) return s[0] def get_rdma_package_info(self): """ Returns the installed rdma package name or None """ ret, output = shellutil.run_get_output( 'rpm -q %s' % self.rdma_user_mode_package_name, chk_err=False) if ret != 0: return None return output def update_rdma_package(self, fw_version): logger.info("RDMA: updating RDMA packages") self.refresh_repos() self.force_install_package(self.rdma_wrapper_package_name) self.install_rdma_drivers(fw_version) def force_install_package(self, pkg_name): """ Attempts to remove existing package and installs the package """ logger.info('RDMA: Force installing package: %s' % pkg_name) if self.uninstall_package(pkg_name) != 0: logger.info('RDMA: Erasing package failed but will continue') if self.install_package(pkg_name) != 0: raise Exception('Failed to install package "{0}"'.format(pkg_name)) logger.info('RDMA: installation completed: %s' % pkg_name) @staticmethod def uninstall_package(pkg_name): return shellutil.run('yum erase -y -q {0}'.format(pkg_name)) @staticmethod def install_package(pkg_name): return shellutil.run('yum install -y -q {0}'.format(pkg_name)) def refresh_repos(self): logger.info("RDMA: refreshing yum repos") if shellutil.run('yum clean all') != 0: raise Exception('Cleaning yum repositories failed') if shellutil.run('yum updateinfo') != 0: raise Exception('Failed to act on yum repo update information') logger.info("RDMA: repositories refreshed") def install_rdma_drivers(self, fw_version): """ Installs the drivers from /opt/rdma/rhel[Major][Minor] directory, particularly the microsoft-hyper-v-rdma-* kmod-* and (no debuginfo or src). Tries to uninstall them first. """ pkg_dir = '/opt/microsoft/rdma/rhel{0}{1}'.format( self.version_major, self.version_minor) logger.info('RDMA: pkgs dir: {0}'.format(pkg_dir)) if not os.path.isdir(pkg_dir): raise Exception('RDMA packages directory %s is missing' % pkg_dir) pkgs = os.listdir(pkg_dir) logger.info('RDMA: found %d files in package directory' % len(pkgs)) # Uninstal KVP daemon first (if exists) self.uninstall_kvp_driver_package_if_exists() # Install kernel mode driver (kmod-microsoft-hyper-v-rdma-*) kmod_pkg = self.get_file_by_pattern( pkgs, "%s-(\d+\.){3,}(%s)-\d{8}\.x86_64.rpm" % (self.rdma_kernel_mode_package_name, fw_version)) # pylint: disable=W1401 if not kmod_pkg: raise Exception("RDMA kernel mode package not found") kmod_pkg_path = os.path.join(pkg_dir, kmod_pkg) self.uninstall_pkg_and_install_from( 'kernel mode', self.rdma_kernel_mode_package_name, kmod_pkg_path) # Install user mode driver (microsoft-hyper-v-rdma-*) umod_pkg = self.get_file_by_pattern( pkgs, "%s-(\d+\.){3,}(%s)-\d{8}\.x86_64.rpm" % (self.rdma_user_mode_package_name, fw_version)) # pylint: disable=W1401 if not umod_pkg: raise Exception("RDMA user mode package not found") umod_pkg_path = os.path.join(pkg_dir, umod_pkg) self.uninstall_pkg_and_install_from( 'user mode', self.rdma_user_mode_package_name, umod_pkg_path) logger.info("RDMA: driver packages installed") if not self.load_driver_module() or not self.is_driver_loaded(): logger.info("RDMA: driver module is not loaded; reboot required") self.reboot_system() else: logger.info("RDMA: kernel module is loaded") @staticmethod def get_file_by_pattern(file_list, pattern): for l in file_list: if re.match(pattern, l): return l return None def uninstall_pkg_and_install_from(self, pkg_type, pkg_name, pkg_path): logger.info( "RDMA: Processing {0} driver: {1}".format(pkg_type, pkg_path)) logger.info("RDMA: Try to uninstall existing version: %s" % pkg_name) if self.uninstall_package(pkg_name) == 0: logger.info("RDMA: Successfully uninstaled %s" % pkg_name) logger.info( "RDMA: Installing {0} package from {1}".format(pkg_type, pkg_path)) if self.install_package(pkg_path) != 0: raise Exception( "Failed to install RDMA {0} package".format(pkg_type)) @staticmethod def is_package_installed(pkg): """Runs rpm -q and checks return code to find out if a package is installed""" return shellutil.run("rpm -q %s" % pkg, chk_err=False) == 0 def uninstall_kvp_driver_package_if_exists(self): logger.info('RDMA: deleting existing kvp driver packages') kvp_pkgs = [self.hyper_v_package_name, self.hyper_v_package_name_new] for kvp_pkg in kvp_pkgs: if not self.is_package_installed(kvp_pkg): logger.info( "RDMA: kvp package %s does not exist, skipping" % kvp_pkg) else: logger.info('RDMA: erasing kvp package "%s"' % kvp_pkg) if shellutil.run("yum erase -q -y %s" % kvp_pkg, chk_err=False) == 0: logger.info("RDMA: successfully erased package") else: logger.error("RDMA: failed to erase package") def check_or_install_kvp_daemon(self): """Checks if kvp daemon package is installed, if not installs the package and reboots the machine. """ logger.info("RDMA: Checking kvp daemon packages.") kvp_pkgs = [self.hyper_v_package_name, self.hyper_v_package_name_new] for pkg in kvp_pkgs: logger.info("RDMA: Checking if package %s installed" % pkg) installed = self.is_package_installed(pkg) if installed: raise Exception('RDMA: package %s is installed, but the kvp daemon is not running' % pkg) kvp_pkg_to_install=self.hyper_v_package_name logger.info("RDMA: no kvp drivers installed, will install '%s'" % kvp_pkg_to_install) logger.info("RDMA: trying to install kvp package '%s'" % kvp_pkg_to_install) if self.install_package(kvp_pkg_to_install) != 0: raise Exception("RDMA: failed to install kvp daemon package '%s'" % kvp_pkg_to_install) logger.info("RDMA: package '%s' successfully installed" % kvp_pkg_to_install) logger.info("RDMA: Machine will now be rebooted.") self.reboot_system() WALinuxAgent-2.9.1.1/azurelinuxagent/pa/rdma/factory.py000066400000000000000000000035451446033677600230100ustar00rootroot00000000000000# Copyright 2016 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # from distutils.version import LooseVersion as Version # pylint: disable=no-name-in-module, import-error import azurelinuxagent.common.logger as logger from azurelinuxagent.common.rdma import RDMAHandler from azurelinuxagent.common.version import DISTRO_FULL_NAME, DISTRO_VERSION from .centos import CentOSRDMAHandler from .suse import SUSERDMAHandler from .ubuntu import UbuntuRDMAHandler def get_rdma_handler( distro_full_name=DISTRO_FULL_NAME, distro_version=DISTRO_VERSION ): """Return the handler object for RDMA driver handling""" if ( (distro_full_name == 'SUSE Linux Enterprise Server' or distro_full_name == 'SLES' or distro_full_name == 'SLE_HPC') and Version(distro_version) > Version('11') ): return SUSERDMAHandler() if distro_full_name in ('CentOS Linux', 'CentOS', 'Red Hat Enterprise Linux Server', 'AlmaLinux', 'CloudLinux', 'Rocky Linux'): return CentOSRDMAHandler(distro_version) if distro_full_name == 'Ubuntu': return UbuntuRDMAHandler() logger.info("No RDMA handler exists for distro='{0}' version='{1}'", distro_full_name, distro_version) return RDMAHandler() WALinuxAgent-2.9.1.1/azurelinuxagent/pa/rdma/suse.py000066400000000000000000000176301446033677600223200ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import glob import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.shellutil as shellutil from azurelinuxagent.common.rdma import RDMAHandler from azurelinuxagent.common.version import DISTRO_VERSION from distutils.version import LooseVersion as Version class SUSERDMAHandler(RDMAHandler): def install_driver(self): # pylint: disable=R1710 """Install the appropriate driver package for the RDMA firmware""" if Version(DISTRO_VERSION) >= Version('15'): msg = 'SLE 15 and later only supports PCI pass through, no ' msg += 'special driver needed for IB interface' logger.info(msg) return True fw_version = self.get_rdma_version() if not fw_version: error_msg = 'RDMA: Could not determine firmware version. ' error_msg += 'Therefore, no driver will be installed.' logger.error(error_msg) return zypper_install = 'zypper -n in %s' zypper_install_noref = 'zypper -n --no-refresh in %s' zypper_lock = 'zypper addlock %s' zypper_remove = 'zypper -n rm %s' zypper_search = 'zypper -n se -s %s' zypper_unlock = 'zypper removelock %s' package_name = 'dummy' # Figure out the kernel that is running to find the proper kmp cmd = 'uname -r' status, kernel_release = shellutil.run_get_output(cmd) # pylint: disable=W0612 if 'default' in kernel_release: package_name = 'msft-rdma-kmp-default' info_msg = 'RDMA: Detected kernel-default' logger.info(info_msg) elif 'azure' in kernel_release: package_name = 'msft-rdma-kmp-azure' info_msg = 'RDMA: Detected kernel-azure' logger.info(info_msg) else: error_msg = 'RDMA: Could not detect kernel build, unable to ' error_msg += 'load kernel module. Kernel release: "%s"' logger.error(error_msg % kernel_release) return cmd = zypper_search % package_name status, repo_package_info = shellutil.run_get_output(cmd) driver_package_versions = [] driver_package_installed = False for entry in repo_package_info.split('\n'): if package_name in entry: sections = entry.split('|') if len(sections) < 4: error_msg = 'RDMA: Unexpected output from"%s": "%s"' logger.error(error_msg % (cmd, entry)) continue installed = sections[0].strip() version = sections[3].strip() driver_package_versions.append(version) if fw_version in version and installed.startswith('i'): info_msg = 'RDMA: Matching driver package "%s-%s" ' info_msg += 'is already installed, nothing to do.' logger.info(info_msg % (package_name, version)) return True if installed.startswith('i'): # A driver with a different version is installed driver_package_installed = True cmd = zypper_unlock % package_name result = shellutil.run(cmd) info_msg = 'Driver with different version installed ' info_msg += 'unlocked package "%s".' logger.info(info_msg % (package_name)) # If we get here the driver package is installed but the # version doesn't match or no package is installed requires_reboot = False if driver_package_installed: # Unloading the particular driver with rmmod does not work # We have to reboot after the new driver is installed if self.is_driver_loaded(): info_msg = 'RDMA: Currently loaded driver does not match the ' info_msg += 'firmware implementation, reboot will be required.' logger.info(info_msg) requires_reboot = True logger.info("RDMA: removing package %s" % package_name) cmd = zypper_remove % package_name shellutil.run(cmd) logger.info("RDMA: removed package %s" % package_name) logger.info("RDMA: looking for fw version %s in packages" % fw_version) for entry in driver_package_versions: if fw_version not in entry: logger.info("Package '%s' is not a match." % entry) else: logger.info("Package '%s' is a match. Installing." % entry) complete_name = '%s-%s' % (package_name, entry) cmd = zypper_install % complete_name result = shellutil.run(cmd) if result: error_msg = 'RDMA: Failed install of package "%s" ' error_msg += 'from available repositories.' logger.error(error_msg % complete_name) msg = 'RDMA: Successfully installed "%s" from ' msg += 'configured repositories' logger.info(msg % complete_name) # Lock the package so it does not accidentally get updated cmd = zypper_lock % package_name result = shellutil.run(cmd) info_msg = 'Applied lock to "%s"' % package_name logger.info(info_msg) if not self.load_driver_module() or requires_reboot: self.reboot_system() return True else: # pylint: disable=W0120 logger.info("RDMA: No suitable match in repos. Trying local.") local_packages = glob.glob('/opt/microsoft/rdma/*.rpm') for local_package in local_packages: logger.info("Examining: %s" % local_package) if local_package.endswith('.src.rpm'): continue if ( package_name in local_package and fw_version in local_package ): logger.info("RDMA: Installing: %s" % local_package) cmd = zypper_install_noref % local_package result = shellutil.run(cmd) if result and result != 106: error_msg = 'RDMA: Failed install of package "%s" ' error_msg += 'from local package cache' logger.error(error_msg % local_package) break msg = 'RDMA: Successfully installed "%s" from ' msg += 'local package cache' logger.info(msg % (local_package)) # Lock the package so it does not accidentally get updated cmd = zypper_lock % package_name result = shellutil.run(cmd) info_msg = 'Applied lock to "%s"' % package_name logger.info(info_msg) if not self.load_driver_module() or requires_reboot: self.reboot_system() return True else: error_msg = 'Unable to find driver package that matches ' error_msg += 'RDMA firmware version "%s"' % fw_version logger.error(error_msg) return WALinuxAgent-2.9.1.1/azurelinuxagent/pa/rdma/ubuntu.py000066400000000000000000000124261446033677600226610ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import glob # pylint: disable=W0611 import os import re import time # pylint: disable=W0611 import azurelinuxagent.common.conf as conf import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.shellutil as shellutil from azurelinuxagent.common.rdma import RDMAHandler class UbuntuRDMAHandler(RDMAHandler): def install_driver(self): #Install the appropriate driver package for the RDMA firmware nd_version = self.get_rdma_version() if not nd_version: logger.error("RDMA: Could not determine firmware version. No driver will be installed") return #replace . with _, we are looking for number like 144_0 nd_version = re.sub('\.', '_', nd_version) # pylint: disable=W1401 #Check to see if we need to reconfigure driver status,module_name = shellutil.run_get_output('modprobe -R hv_network_direct', chk_err=False) if status != 0: logger.info("RDMA: modprobe -R hv_network_direct failed. Use module name hv_network_direct") module_name = "hv_network_direct" else: module_name = module_name.strip() logger.info("RDMA: current RDMA driver %s nd_version %s" % (module_name, nd_version)) if module_name == 'hv_network_direct_%s' % nd_version: logger.info("RDMA: driver is installed and ND version matched. Skip reconfiguring driver") return #Reconfigure driver if one is available status,output = shellutil.run_get_output('modinfo hv_network_direct_%s' % nd_version) if status == 0: logger.info("RDMA: driver with ND version is installed. Link to module name") self.update_modprobed_conf(nd_version) return #Driver not found. We need to check to see if we need to update kernel if not conf.enable_rdma_update(): logger.info("RDMA: driver update is disabled. Skip kernel update") return status,output = shellutil.run_get_output('uname -r') if status != 0: return if not re.search('-azure$', output): logger.error("RDMA: skip driver update on non-Azure kernel") return kernel_version = re.sub('-azure$', '', output) kernel_version = re.sub('-', '.', kernel_version) #Find the new kernel package version status,output = shellutil.run_get_output('apt-get update') if status != 0: return status,output = shellutil.run_get_output('apt-cache show --no-all-versions linux-azure') if status != 0: return r = re.search('Version: (\S+)', output) # pylint: disable=W1401 if not r: logger.error("RDMA: version not found in package linux-azure.") return package_version = r.groups()[0] #Remove the ending . after package_version = re.sub("\.\d+$", "", package_version) # pylint: disable=W1401 logger.info('RDMA: kernel_version=%s package_version=%s' % (kernel_version, package_version)) kernel_version_array = [ int(x) for x in kernel_version.split('.') ] package_version_array = [ int(x) for x in package_version.split('.') ] if kernel_version_array < package_version_array: logger.info("RDMA: newer version available, update kernel and reboot") status,output = shellutil.run_get_output('apt-get -y install linux-azure') if status: logger.error("RDMA: kernel update failed") return self.reboot_system() else: logger.error("RDMA: no kernel update is avaiable for ND version %s" % nd_version) def update_modprobed_conf(self, nd_version): #Update /etc/modprobe.d/vmbus-rdma.conf to point to the correct driver modprobed_file = '/etc/modprobe.d/vmbus-rdma.conf' lines = '' if not os.path.isfile(modprobed_file): logger.info("RDMA: %s not found, it will be created" % modprobed_file) else: with open(modprobed_file, 'r') as f: lines = f.read() r = re.search('alias hv_network_direct hv_network_direct_\S+', lines) # pylint: disable=W1401 if r: lines = re.sub('alias hv_network_direct hv_network_direct_\S+', 'alias hv_network_direct hv_network_direct_%s' % nd_version, lines) # pylint: disable=W1401 else: lines += '\nalias hv_network_direct hv_network_direct_%s\n' % nd_version with open('/etc/modprobe.d/vmbus-rdma.conf', 'w') as f: f.write(lines) logger.info("RDMA: hv_network_direct alias updated to ND %s" % nd_version) WALinuxAgent-2.9.1.1/bin/000077500000000000000000000000001446033677600147605ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/bin/py3/000077500000000000000000000000001446033677600154735ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/bin/py3/waagent000077500000000000000000000030471446033677600170530ustar00rootroot00000000000000#!/usr/bin/env python3 # # Azure Linux Agent # # Copyright 2015 Microsoft Corporation # # 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. # # Requires Python 2.6 and Openssl 1.0+ # # Implements parts of RFC 2131, 1541, 1497 and # http://msdn.microsoft.com/en-us/library/cc227282%28PROT.10%29.aspx # http://msdn.microsoft.com/en-us/library/cc227259%28PROT.13%29.aspx # import os import sys if sys.version_info[0] == 2: import imp else: import importlib if __name__ == '__main__' : import azurelinuxagent.agent as agent """ Invoke main method of agent """ agent.main() if __name__ == 'waagent': """ Load waagent2.0 to support old version of extensions """ if sys.version_info[0] == 3: raise ImportError("waagent2.0 doesn't support python3") bin_path = os.path.dirname(os.path.abspath(__file__)) agent20_path = os.path.join(bin_path, "waagent2.0") if not os.path.isfile(agent20_path): raise ImportError("Can't load waagent") agent20 = imp.load_source('waagent', agent20_path) __all__ = dir(agent20) WALinuxAgent-2.9.1.1/bin/waagent000077500000000000000000000030461446033677600163370ustar00rootroot00000000000000#!/usr/bin/env python # # Azure Linux Agent # # Copyright 2015 Microsoft Corporation # # 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. # # Requires Python 2.6 and Openssl 1.0+ # # Implements parts of RFC 2131, 1541, 1497 and # http://msdn.microsoft.com/en-us/library/cc227282%28PROT.10%29.aspx # http://msdn.microsoft.com/en-us/library/cc227259%28PROT.13%29.aspx # import os import sys if sys.version_info[0] == 2: import imp else: import importlib if __name__ == '__main__' : import azurelinuxagent.agent as agent """ Invoke main method of agent """ agent.main() if __name__ == 'waagent': """ Load waagent2.0 to support old version of extensions """ if sys.version_info[0] == 3: raise ImportError("waagent2.0 doesn't support python3") bin_path = os.path.dirname(os.path.abspath(__file__)) agent20_path = os.path.join(bin_path, "waagent2.0") if not os.path.isfile(agent20_path): raise ImportError("Can't load waagent") agent20 = imp.load_source('waagent', agent20_path) __all__ = dir(agent20) WALinuxAgent-2.9.1.1/bin/waagent2.0000066400000000000000000007551201446033677600165630ustar00rootroot00000000000000#!/usr/bin/env python # # Azure Linux Agent # # Copyright 2015 Microsoft Corporation # # 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. # # Requires Python 2.6 and Openssl 1.0+ # # Implements parts of RFC 2131, 1541, 1497 and # http://msdn.microsoft.com/en-us/library/cc227282%28PROT.10%29.aspx # http://msdn.microsoft.com/en-us/library/cc227259%28PROT.13%29.aspx # import crypt import random import array import base64 import httplib import os import os.path import platform import pwd import re import shutil import socket import SocketServer import struct import string import subprocess import sys import tempfile import textwrap import threading import time import traceback import xml.dom.minidom import fcntl import inspect import zipfile import json import datetime import xml.sax.saxutils from distutils.version import LooseVersion if not hasattr(subprocess,'check_output'): def check_output(*popenargs, **kwargs): r"""Backport from subprocess module from python 2.7""" if 'stdout' in kwargs: raise ValueError('stdout argument not allowed, it will be overridden.') process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs) output, unused_err = process.communicate() retcode = process.poll() if retcode: cmd = kwargs.get("args") if cmd is None: cmd = popenargs[0] raise subprocess.CalledProcessError(retcode, cmd, output=output) return output # Exception classes used by this module. class CalledProcessError(Exception): def __init__(self, returncode, cmd, output=None): self.returncode = returncode self.cmd = cmd self.output = output def __str__(self): return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode) subprocess.check_output=check_output subprocess.CalledProcessError=CalledProcessError GuestAgentName = "WALinuxAgent" GuestAgentLongName = "Azure Linux Agent" GuestAgentVersion = "WALinuxAgent-2.0.16" ProtocolVersion = "2012-11-30" #WARNING this value is used to confirm the correct fabric protocol. Config = None WaAgent = None DiskActivated = False Openssl = "openssl" Children = [] ExtensionChildren = [] VMM_STARTUP_SCRIPT_NAME='install' VMM_CONFIG_FILE_NAME='linuxosconfiguration.xml' global RulesFiles RulesFiles = [ "/lib/udev/rules.d/75-persistent-net-generator.rules", "/etc/udev/rules.d/70-persistent-net.rules" ] VarLibDhcpDirectories = ["/var/lib/dhclient", "/var/lib/dhcpcd", "/var/lib/dhcp"] EtcDhcpClientConfFiles = ["/etc/dhcp/dhclient.conf", "/etc/dhcp3/dhclient.conf"] global LibDir LibDir = "/var/lib/waagent" global provisioned provisioned=False global provisionError provisionError=None HandlerStatusToAggStatus = {"installed":"Installing", "enabled":"Ready", "unintalled":"NotReady", "disabled":"NotReady"} WaagentConf = """\ # # Azure Linux Agent Configuration # Role.StateConsumer=None # Specified program is invoked with the argument "Ready" when we report ready status # to the endpoint server. Role.ConfigurationConsumer=None # Specified program is invoked with XML file argument specifying role configuration. Role.TopologyConsumer=None # Specified program is invoked with XML file argument specifying role topology. Provisioning.Enabled=y # Provisioning.DeleteRootPassword=y # Password authentication for root account will be unavailable. Provisioning.RegenerateSshHostKeyPair=y # Generate fresh host key pair. Provisioning.SshHostKeyPairType=rsa # Supported values are "rsa", "dsa" and "ecdsa". Provisioning.MonitorHostName=y # Monitor host name changes and publish changes via DHCP requests. ResourceDisk.Format=y # Format if unformatted. If 'n', resource disk will not be mounted. ResourceDisk.Filesystem=ext4 # Typically ext3 or ext4. FreeBSD images should use 'ufs2' here. ResourceDisk.MountPoint=/mnt/resource # ResourceDisk.EnableSwap=n # Create and use swapfile on resource disk. ResourceDisk.SwapSizeMB=0 # Size of the swapfile. LBProbeResponder=y # Respond to load balancer probes if requested by Azure. Logs.Verbose=n # Enable verbose logs OS.RootDeviceScsiTimeout=300 # Root device timeout in seconds. OS.OpensslPath=None # If "None", the system default version is used. """ README_FILENAME="DATALOSS_WARNING_README.txt" README_FILECONTENT="""\ WARNING: THIS IS A TEMPORARY DISK. Any data stored on this drive is SUBJECT TO LOSS and THERE IS NO WAY TO RECOVER IT. Please do not use this disk for storing any personal or application data. For additional details to please refer to the MSDN documentation at : http://msdn.microsoft.com/en-us/library/windowsazure/jj672979.aspx """ ############################################################ # BEGIN DISTRO CLASS DEFS ############################################################ ############################################################ # AbstractDistro ############################################################ class AbstractDistro(object): """ AbstractDistro defines a skeleton neccesary for a concrete Distro class. Generic methods and attributes are kept here, distribution specific attributes and behavior are to be placed in the concrete child named distroDistro, where distro is the string returned by calling python platform.linux_distribution()[0]. So for CentOS the derived class is called 'centosDistro'. """ def __init__(self): """ Generic Attributes go here. These are based on 'majority rules'. This __init__() may be called or overriden by the child. """ self.agent_service_name = os.path.basename(sys.argv[0]) self.selinux=None self.service_cmd='/usr/sbin/service' self.ssh_service_restart_option='restart' self.ssh_service_name='ssh' self.ssh_config_file='/etc/ssh/sshd_config' self.hostname_file_path='/etc/hostname' self.dhcp_client_name='dhclient' self.requiredDeps = [ 'route', 'shutdown', 'ssh-keygen', 'useradd', 'usermod', 'openssl', 'sfdisk', 'fdisk', 'mkfs', 'sed', 'grep', 'sudo', 'parted' ] self.init_script_file='/etc/init.d/waagent' self.agent_package_name='WALinuxAgent' self.fileBlackList = [ "/root/.bash_history", "/var/log/waagent.log",'/etc/resolv.conf' ] self.agent_files_to_uninstall = ["/etc/waagent.conf", "/etc/logrotate.d/waagent"] self.grubKernelBootOptionsFile = '/etc/default/grub' self.grubKernelBootOptionsLine = 'GRUB_CMDLINE_LINUX_DEFAULT=' self.getpidcmd = 'pidof' self.mount_dvd_cmd = 'mount' self.sudoers_dir_base = '/etc' self.waagent_conf_file = WaagentConf self.shadow_file_mode=0600 self.shadow_file_path="/etc/shadow" self.dhcp_enabled = False def isSelinuxSystem(self): """ Checks and sets self.selinux = True if SELinux is available on system. """ if self.selinux == None: if Run("which getenforce",chk_err=False): self.selinux = False else: self.selinux = True return self.selinux def isSelinuxRunning(self): """ Calls shell command 'getenforce' and returns True if 'Enforcing'. """ if self.isSelinuxSystem(): return RunGetOutput("getenforce")[1].startswith("Enforcing") else: return False def setSelinuxEnforce(self,state): """ Calls shell command 'setenforce' with 'state' and returns resulting exit code. """ if self.isSelinuxSystem(): if state: s = '1' else: s='0' return Run("setenforce "+s) def setSelinuxContext(self,path,cn): """ Calls shell 'chcon' with 'path' and 'cn' context. Returns exit result. """ if self.isSelinuxSystem(): if not os.path.exists(path): Error("Path does not exist: {0}".format(path)) return 1 return Run('chcon ' + cn + ' ' + path) def setHostname(self,name): """ Shell call to hostname. Returns resulting exit code. """ return Run('hostname ' + name) def publishHostname(self,name): """ Set the contents of the hostname file to 'name'. Return 1 on failure. """ try: r=SetFileContents(self.hostname_file_path, name) for f in EtcDhcpClientConfFiles: if os.path.exists(f) and FindStringInFile(f,r'^[^#]*?send\s*host-name.*?(|gethostname[(,)])') == None : r=ReplaceFileContentsAtomic('/etc/dhcp/dhclient.conf', "send host-name \"" + name + "\";\n" + "\n".join(filter(lambda a: not a.startswith("send host-name"), GetFileContents('/etc/dhcp/dhclient.conf').split('\n')))) except: return 1 return r def installAgentServiceScriptFiles(self): """ Create the waagent support files for service installation. Called by registerAgentService() Abstract Virtual Function. Over-ridden in concrete Distro classes. """ pass def registerAgentService(self): """ Calls installAgentService to create service files. Shell exec service registration commands. (e.g. chkconfig --add waagent) Abstract Virtual Function. Over-ridden in concrete Distro classes. """ pass def uninstallAgentService(self): """ Call service subsystem to remove waagent script. Abstract Virtual Function. Over-ridden in concrete Distro classes. """ pass def unregisterAgentService(self): """ Calls self.stopAgentService and call self.uninstallAgentService() """ self.stopAgentService() self.uninstallAgentService() def startAgentService(self): """ Service call to start the Agent service """ return Run(self.service_cmd + ' ' + self.agent_service_name + ' start') def stopAgentService(self): """ Service call to stop the Agent service """ return Run(self.service_cmd + ' ' + self.agent_service_name + ' stop',False) def restartSshService(self): """ Service call to re(start) the SSH service """ sshRestartCmd = self.service_cmd + " " + self.ssh_service_name + " " + self.ssh_service_restart_option retcode = Run(sshRestartCmd) if retcode > 0: Error("Failed to restart SSH service with return code:" + str(retcode)) return retcode def sshDeployPublicKey(self,fprint,path): """ Generic sshDeployPublicKey - over-ridden in some concrete Distro classes due to minor differences in openssl packages deployed """ error=0 SshPubKey = OvfEnv().OpensslToSsh(fprint) if SshPubKey != None: AppendFileContents(path, SshPubKey) else: Error("Failed: " + fprint + ".crt -> " + path) error = 1 return error def checkPackageInstalled(self,p): """ Query package database for prescence of an installed package. Abstract Virtual Function. Over-ridden in concrete Distro classes. """ pass def checkPackageUpdateable(self,p): """ Online check if updated package of walinuxagent is available. Abstract Virtual Function. Over-ridden in concrete Distro classes. """ pass def deleteRootPassword(self): """ Generic root password removal. """ filepath="/etc/shadow" ReplaceFileContentsAtomic(filepath,"root:*LOCK*:14600::::::\n" + "\n".join(filter(lambda a: not a.startswith("root:"),GetFileContents(filepath).split('\n')))) os.chmod(filepath,self.shadow_file_mode) if self.isSelinuxSystem(): self.setSelinuxContext(filepath,'system_u:object_r:shadow_t:s0') Log("Root password deleted.") return 0 def changePass(self,user,password): Log("Change user password") crypt_id = Config.get("Provisioning.PasswordCryptId") if crypt_id is None: crypt_id = "6" salt_len = Config.get("Provisioning.PasswordCryptSaltLength") try: salt_len = int(salt_len) if salt_len < 0 or salt_len > 10: salt_len = 10 except (ValueError, TypeError): salt_len = 10 return self.chpasswd(user, password, crypt_id=crypt_id, salt_len=salt_len) def chpasswd(self, username, password, crypt_id=6, salt_len=10): passwd_hash = self.gen_password_hash(password, crypt_id, salt_len) cmd = "usermod -p '{0}' {1}".format(passwd_hash, username) ret, output = RunGetOutput(cmd, log_cmd=False) if ret != 0: return "Failed to set password for {0}: {1}".format(username, output) def gen_password_hash(self, password, crypt_id, salt_len): collection = string.ascii_letters + string.digits salt = ''.join(random.choice(collection) for _ in range(salt_len)) salt = "${0}${1}".format(crypt_id, salt) return crypt.crypt(password, salt) def load_ata_piix(self): return WaAgent.TryLoadAtapiix() def unload_ata_piix(self): """ Generic function to remove ata_piix.ko. """ return WaAgent.TryUnloadAtapiix() def deprovisionWarnUser(self): """ Generic user warnings used at deprovision. """ print("WARNING! Nameserver configuration in /etc/resolv.conf will be deleted.") def deprovisionDeleteFiles(self): """ Files to delete when VM is deprovisioned """ for a in VarLibDhcpDirectories: Run("rm -f " + a + "/*") # Clear LibDir, remove nameserver and root bash history for f in os.listdir(LibDir) + self.fileBlackList: try: os.remove(f) except: pass return 0 def uninstallDeleteFiles(self): """ Files to delete when agent is uninstalled. """ for f in self.agent_files_to_uninstall: try: os.remove(f) except: pass return 0 def checkDependencies(self): """ Generic dependency check. Return 1 unless all dependencies are satisfied. """ if self.checkPackageInstalled('NetworkManager'): Error(GuestAgentLongName + " is not compatible with network-manager.") return 1 try: m= __import__('pyasn1') except ImportError: Error(GuestAgentLongName + " requires python-pyasn1 for your Linux distribution.") return 1 for a in self.requiredDeps: if Run("which " + a + " > /dev/null 2>&1",chk_err=False): Error("Missing required dependency: " + a) return 1 return 0 def packagedInstall(self,buildroot): """ Called from setup.py for use by RPM. Copies generated files waagent.conf, under the buildroot. """ if not os.path.exists(buildroot+'/etc'): os.mkdir(buildroot+'/etc') SetFileContents(buildroot+'/etc/waagent.conf', MyDistro.waagent_conf_file) if not os.path.exists(buildroot+'/etc/logrotate.d'): os.mkdir(buildroot+'/etc/logrotate.d') SetFileContents(buildroot+'/etc/logrotate.d/waagent', WaagentLogrotate) self.init_script_file=buildroot+self.init_script_file # this allows us to call installAgentServiceScriptFiles() if not os.path.exists(os.path.dirname(self.init_script_file)): os.mkdir(os.path.dirname(self.init_script_file)) self.installAgentServiceScriptFiles() def GetIpv4Address(self): """ Return the ip of the first active non-loopback interface. """ addr='' iface,addr=GetFirstActiveNetworkInterfaceNonLoopback() return addr def GetMacAddress(self): return GetMacAddress() def GetInterfaceName(self): return GetFirstActiveNetworkInterfaceNonLoopback()[0] def RestartInterface(self, iface, max_retry=3): for retry in range(1, max_retry + 1): ret = Run("ifdown " + iface + " && ifup " + iface) if ret == 0: return Log("Failed to restart interface: {0}, ret={1}".format(iface, ret)) if retry < max_retry: Log("Retry restart interface in 5 seconds") time.sleep(5) def CreateAccount(self,user, password, expiration, thumbprint): return CreateAccount(user, password, expiration, thumbprint) def DeleteAccount(self,user): return DeleteAccount(user) def ActivateResourceDisk(self): """ Format, mount, and if specified in the configuration set resource disk as swap. """ global DiskActivated format = Config.get("ResourceDisk.Format") if format == None or format.lower().startswith("n"): DiskActivated = True return device = DeviceForIdePort(1) if device == None: Error("ActivateResourceDisk: Unable to detect disk topology.") return device = "/dev/" + device mountlist = RunGetOutput("mount")[1] mountpoint = GetMountPoint(mountlist, device) if(mountpoint): Log("ActivateResourceDisk: " + device + "1 is already mounted.") else: mountpoint = Config.get("ResourceDisk.MountPoint") if mountpoint == None: mountpoint = "/mnt/resource" CreateDir(mountpoint, "root", 0755) fs = Config.get("ResourceDisk.Filesystem") if fs == None: fs = "ext3" partition = device + "1" #Check partition type Log("Detect GPT...") ret = RunGetOutput("parted {0} print".format(device)) if ret[0] == 0 and "gpt" in ret[1]: Log("GPT detected.") #GPT(Guid Partition Table) is used. #Get partitions. parts = filter(lambda x : re.match("^\s*[0-9]+", x), ret[1].split("\n")) #If there are more than 1 partitions, remove all partitions #and create a new one using the entire disk space. if len(parts) > 1: for i in range(1, len(parts) + 1): Run("parted {0} rm {1}".format(device, i)) Run("parted {0} mkpart primary 0% 100%".format(device)) Run("mkfs." + fs + " " + partition + " -F") else: existingFS = RunGetOutput("sfdisk -q -c " + device + " 1", chk_err=False)[1].rstrip() if existingFS == "7" and fs != "ntfs": Run("sfdisk -c " + device + " 1 83") Run("mkfs." + fs + " " + partition) if Run("mount " + partition + " " + mountpoint, chk_err=False): #If mount failed, try to format the partition and mount again Warn("Failed to mount resource disk. Retry mounting.") Run("mkfs." + fs + " " + partition + " -F") if Run("mount " + partition + " " + mountpoint): Error("ActivateResourceDisk: Failed to mount resource disk (" + partition + ").") return Log("Resource disk (" + partition + ") is mounted at " + mountpoint + " with fstype " + fs) #Create README file under the root of resource disk SetFileContents(os.path.join(mountpoint,README_FILENAME), README_FILECONTENT) DiskActivated = True #Create swap space swap = Config.get("ResourceDisk.EnableSwap") if swap == None or swap.lower().startswith("n"): return sizeKB = int(Config.get("ResourceDisk.SwapSizeMB")) * 1024 if os.path.isfile(mountpoint + "/swapfile") and os.path.getsize(mountpoint + "/swapfile") != (sizeKB * 1024): os.remove(mountpoint + "/swapfile") if not os.path.isfile(mountpoint + "/swapfile"): Run("umask 0077 && dd if=/dev/zero of=" + mountpoint + "/swapfile bs=1024 count=" + str(sizeKB)) Run("mkswap " + mountpoint + "/swapfile") if not Run("swapon " + mountpoint + "/swapfile"): Log("Enabled " + str(sizeKB) + " KB of swap at " + mountpoint + "/swapfile") else: Error("ActivateResourceDisk: Failed to activate swap at " + mountpoint + "/swapfile") def Install(self): return Install() def mediaHasFilesystem(self,dsk): if len(dsk) == 0 : return False if Run("LC_ALL=C fdisk -l " + dsk + " | grep Disk"): return False return True def mountDVD(self,dvd,location): return RunGetOutput(self.mount_dvd_cmd + ' ' + dvd + ' ' + location) def GetHome(self): return GetHome() def getDhcpClientName(self): return self.dhcp_client_name def initScsiDiskTimeout(self): """ Set the SCSI disk timeout when the agent starts running """ self.setScsiDiskTimeout() def setScsiDiskTimeout(self): """ Iterate all SCSI disks(include hot-add) and set their timeout if their value are different from the OS.RootDeviceScsiTimeout """ try: scsiTimeout = Config.get("OS.RootDeviceScsiTimeout") for diskName in [disk for disk in os.listdir("/sys/block") if disk.startswith("sd")]: self.setBlockDeviceTimeout(diskName, scsiTimeout) except: pass def setBlockDeviceTimeout(self, device, timeout): """ Set SCSI disk timeout by set /sys/block/sd*/device/timeout """ if timeout != None and device: filePath = "/sys/block/" + device + "/device/timeout" if(GetFileContents(filePath).splitlines()[0].rstrip() != timeout): SetFileContents(filePath,timeout) Log("SetBlockDeviceTimeout: Update the device " + device + " with timeout " + timeout) def waitForSshHostKey(self, path): """ Provide a dummy waiting, since by default, ssh host key is created by waagent and the key should already been created. """ if(os.path.isfile(path)): return True else: Error("Can't find host key: {0}".format(path)) return False def isDHCPEnabled(self): return self.dhcp_enabled def stopDHCP(self): """ Stop the system DHCP client so that the agent can bind on its port. If the distro has set dhcp_enabled to True, it will need to provide an implementation of this method. """ raise NotImplementedError('stopDHCP method missing') def startDHCP(self): """ Start the system DHCP client. If the distro has set dhcp_enabled to True, it will need to provide an implementation of this method. """ raise NotImplementedError('startDHCP method missing') def translateCustomData(self, data): """ Translate the custom data from a Base64 encoding. Default to no-op. """ decodeCustomData = Config.get("Provisioning.DecodeCustomData") if decodeCustomData != None and decodeCustomData.lower().startswith("y"): return base64.b64decode(data) return data def getConfigurationPath(self): return "/etc/waagent.conf" def getProcessorCores(self): return int(RunGetOutput("grep 'processor.*:' /proc/cpuinfo |wc -l")[1]) def getTotalMemory(self): return int(RunGetOutput("grep MemTotal /proc/meminfo |awk '{print $2}'")[1])/1024 def getInterfaceNameByMac(self, mac): ret, output = RunGetOutput("ifconfig -a") if ret != 0: raise Exception("Failed to get network interface info") output = output.replace('\n', '') match = re.search(r"(eth\d).*(HWaddr|ether) {0}".format(mac), output, re.IGNORECASE) if match is None: raise Exception("Failed to get ifname with mac: {0}".format(mac)) output = match.group(0) eths = re.findall(r"eth\d", output) if eths is None or len(eths) == 0: raise Exception("Failed to get ifname with mac: {0}".format(mac)) return eths[-1] def configIpV4(self, ifName, addr, netmask=24): ret, output = RunGetOutput("ifconfig {0} up".format(ifName)) if ret != 0: raise Exception("Failed to bring up {0}: {1}".format(ifName, output)) ret, output = RunGetOutput("ifconfig {0} {1}/{2}".format(ifName, addr, netmask)) if ret != 0: raise Exception("Failed to config ipv4 for {0}: {1}".format(ifName, output)) def setDefaultGateway(self, gateway): Run("/sbin/route add default gw" + gateway, chk_err=False) def routeAdd(self, net, mask, gateway): Run("/sbin/route add -net " + net + " netmask " + mask + " gw " + gateway, chk_err=False) ############################################################ # GentooDistro ############################################################ gentoo_init_file = """\ #!/sbin/runscript command=/usr/sbin/waagent pidfile=/var/run/waagent.pid command_args=-daemon command_background=true name="Azure Linux Agent" depend() { need localmount use logger network after bootmisc modules } """ class gentooDistro(AbstractDistro): """ Gentoo distro concrete class """ def __init__(self): # super(gentooDistro,self).__init__() self.service_cmd='/sbin/service' self.ssh_service_name='sshd' self.hostname_file_path='/etc/conf.d/hostname' self.dhcp_client_name='dhcpcd' self.shadow_file_mode=0640 self.init_file=gentoo_init_file def publishHostname(self,name): try: if (os.path.isfile(self.hostname_file_path)): r=ReplaceFileContentsAtomic(self.hostname_file_path, "hostname=\"" + name + "\"\n" + "\n".join(filter(lambda a: not a.startswith("hostname="), GetFileContents(self.hostname_file_path).split("\n")))) except: return 1 return r def installAgentServiceScriptFiles(self): SetFileContents(self.init_script_file, self.init_file) os.chmod(self.init_script_file, 0755) def registerAgentService(self): self.installAgentServiceScriptFiles() return Run('rc-update add ' + self.agent_service_name + ' default') def uninstallAgentService(self): return Run('rc-update del ' + self.agent_service_name + ' default') def unregisterAgentService(self): self.stopAgentService() return self.uninstallAgentService() def checkPackageInstalled(self,p): if Run('eix -I ^' + p + '$',chk_err=False): return 0 else: return 1 def checkPackageUpdateable(self,p): if Run('eix -u ^' + p + '$',chk_err=False): return 0 else: return 1 def RestartInterface(self, iface): Run("/etc/init.d/net." + iface + " restart") ############################################################ # SuSEDistro ############################################################ suse_init_file = """\ #! /bin/sh # # Azure Linux Agent sysV init script # # Copyright 2013 Microsoft Corporation # Copyright SUSE LLC # # 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. # # /etc/init.d/waagent # # and symbolic link # # /usr/sbin/rcwaagent # # System startup script for the waagent # ### BEGIN INIT INFO # Provides: AzureLinuxAgent # Required-Start: $network sshd # Required-Stop: $network sshd # Default-Start: 3 5 # Default-Stop: 0 1 2 6 # Description: Start the AzureLinuxAgent ### END INIT INFO PYTHON=/usr/bin/python WAZD_BIN=/usr/sbin/waagent WAZD_CONF=/etc/waagent.conf WAZD_PIDFILE=/var/run/waagent.pid test -x "$WAZD_BIN" || { echo "$WAZD_BIN not installed"; exit 5; } test -e "$WAZD_CONF" || { echo "$WAZD_CONF not found"; exit 6; } . /etc/rc.status # First reset status of this service rc_reset # Return values acc. to LSB for all commands but status: # 0 - success # 1 - misc error # 2 - invalid or excess args # 3 - unimplemented feature (e.g. reload) # 4 - insufficient privilege # 5 - program not installed # 6 - program not configured # # Note that starting an already running service, stopping # or restarting a not-running service as well as the restart # with force-reload (in case signalling is not supported) are # considered a success. case "$1" in start) echo -n "Starting AzureLinuxAgent" ## Start daemon with startproc(8). If this fails ## the echo return value is set appropriate. startproc -f ${PYTHON} ${WAZD_BIN} -daemon rc_status -v ;; stop) echo -n "Shutting down AzureLinuxAgent" ## Stop daemon with killproc(8) and if this fails ## set echo the echo return value. killproc -p ${WAZD_PIDFILE} ${PYTHON} ${WAZD_BIN} rc_status -v ;; try-restart) ## Stop the service and if this succeeds (i.e. the ## service was running before), start it again. $0 status >/dev/null && $0 restart rc_status ;; restart) ## Stop the service and regardless of whether it was ## running or not, start it again. $0 stop sleep 1 $0 start rc_status ;; force-reload|reload) rc_status ;; status) echo -n "Checking for service AzureLinuxAgent " ## Check status with checkproc(8), if process is running ## checkproc will return with exit status 0. checkproc -p ${WAZD_PIDFILE} ${PYTHON} ${WAZD_BIN} rc_status -v ;; probe) ;; *) echo "Usage: $0 {start|stop|status|try-restart|restart|force-reload|reload}" exit 1 ;; esac rc_exit """ class SuSEDistro(AbstractDistro): """ SuSE Distro concrete class Put SuSE specific behavior here... """ def __init__(self): super(SuSEDistro,self).__init__() self.service_cmd='/sbin/service' self.ssh_service_name='sshd' self.kernel_boot_options_file='/boot/grub/menu.lst' self.hostname_file_path='/etc/HOSTNAME' self.requiredDeps += [ "/sbin/insserv" ] self.init_file=suse_init_file self.dhcp_client_name='dhcpcd' if ((DistInfo(fullname=1)[0] == 'SUSE Linux Enterprise Server' and DistInfo()[1] >= '12') or \ (DistInfo(fullname=1)[0] == 'openSUSE' and DistInfo()[1] >= '13.2')): self.dhcp_client_name='wickedd-dhcp4' self.grubKernelBootOptionsFile = '/boot/grub/menu.lst' self.grubKernelBootOptionsLine = 'kernel' self.getpidcmd='pidof ' self.dhcp_enabled=True def checkPackageInstalled(self,p): if Run("rpm -q " + p,chk_err=False): return 0 else: return 1 def checkPackageUpdateable(self,p): if Run("zypper list-updates | grep " + p,chk_err=False): return 1 else: return 0 def installAgentServiceScriptFiles(self): try: SetFileContents(self.init_script_file, self.init_file) os.chmod(self.init_script_file, 0744) except: pass def registerAgentService(self): self.installAgentServiceScriptFiles() return Run('insserv ' + self.agent_service_name) def uninstallAgentService(self): return Run('insserv -r ' + self.agent_service_name) def unregisterAgentService(self): self.stopAgentService() return self.uninstallAgentService() def startDHCP(self): Run("service " + self.dhcp_client_name + " start", chk_err=False) def stopDHCP(self): Run("service " + self.dhcp_client_name + " stop", chk_err=False) ############################################################ # redhatDistro ############################################################ redhat_init_file= """\ #!/bin/bash # # Init file for AzureLinuxAgent. # # chkconfig: 2345 60 80 # description: AzureLinuxAgent # # source function library . /etc/rc.d/init.d/functions RETVAL=0 FriendlyName="AzureLinuxAgent" WAZD_BIN=/usr/sbin/waagent start() { echo -n $"Starting $FriendlyName: " $WAZD_BIN -daemon & } stop() { echo -n $"Stopping $FriendlyName: " killproc -p /var/run/waagent.pid $WAZD_BIN RETVAL=$? echo return $RETVAL } case "$1" in start) start ;; stop) stop ;; restart) stop start ;; reload) ;; report) ;; status) status $WAZD_BIN RETVAL=$? ;; *) echo $"Usage: $0 {start|stop|restart|status}" RETVAL=1 esac exit $RETVAL """ class redhatDistro(AbstractDistro): """ Redhat Distro concrete class Put Redhat specific behavior here... """ def __init__(self): super(redhatDistro,self).__init__() self.service_cmd='/sbin/service' self.ssh_service_restart_option='condrestart' self.ssh_service_name='sshd' self.hostname_file_path= None if DistInfo()[1] < '7.0' else '/etc/hostname' self.init_file=redhat_init_file self.grubKernelBootOptionsFile = '/boot/grub/menu.lst' self.grubKernelBootOptionsLine = 'kernel' def publishHostname(self,name): super(redhatDistro,self).publishHostname(name) if DistInfo()[1] < '7.0' : filepath = "/etc/sysconfig/network" if os.path.isfile(filepath): ReplaceFileContentsAtomic(filepath, "HOSTNAME=" + name + "\n" + "\n".join(filter(lambda a: not a.startswith("HOSTNAME"), GetFileContents(filepath).split('\n')))) ethernetInterface = MyDistro.GetInterfaceName() filepath = "/etc/sysconfig/network-scripts/ifcfg-" + ethernetInterface if os.path.isfile(filepath): ReplaceFileContentsAtomic(filepath, "DHCP_HOSTNAME=" + name + "\n" + "\n".join(filter(lambda a: not a.startswith("DHCP_HOSTNAME"), GetFileContents(filepath).split('\n')))) return 0 def installAgentServiceScriptFiles(self): SetFileContents(self.init_script_file, self.init_file) os.chmod(self.init_script_file, 0744) return 0 def registerAgentService(self): self.installAgentServiceScriptFiles() return Run('chkconfig --add waagent') def uninstallAgentService(self): return Run('chkconfig --del ' + self.agent_service_name) def unregisterAgentService(self): self.stopAgentService() return self.uninstallAgentService() def checkPackageInstalled(self,p): if Run("yum list installed " + p,chk_err=False): return 0 else: return 1 def checkPackageUpdateable(self,p): if Run("yum check-update | grep "+ p,chk_err=False): return 1 else: return 0 def checkDependencies(self): """ Generic dependency check. Return 1 unless all dependencies are satisfied. """ if DistInfo()[1] < '7.0' and self.checkPackageInstalled('NetworkManager'): Error(GuestAgentLongName + " is not compatible with network-manager.") return 1 try: m= __import__('pyasn1') except ImportError: Error(GuestAgentLongName + " requires python-pyasn1 for your Linux distribution.") return 1 for a in self.requiredDeps: if Run("which " + a + " > /dev/null 2>&1",chk_err=False): Error("Missing required dependency: " + a) return 1 return 0 ############################################################ # centosDistro ############################################################ class centosDistro(redhatDistro): """ CentOS Distro concrete class Put CentOS specific behavior here... """ def __init__(self): super(centosDistro,self).__init__() ############################################################ # eulerosDistro ############################################################ class eulerosDistro(redhatDistro): """ EulerOS Distro concrete class Put EulerOS specific behavior here... """ def __init__(self): super(eulerosDistro,self).__init__() ############################################################ # oracleDistro ############################################################ class oracleDistro(redhatDistro): """ Oracle Distro concrete class Put Oracle specific behavior here... """ def __init__(self): super(oracleDistro, self).__init__() ############################################################ # asianuxDistro ############################################################ class asianuxDistro(redhatDistro): """ Asianux Distro concrete class Put Asianux specific behavior here... """ def __init__(self): super(asianuxDistro,self).__init__() ############################################################ # CoreOSDistro ############################################################ class CoreOSDistro(AbstractDistro): """ CoreOS Distro concrete class Put CoreOS specific behavior here... """ CORE_UID = 500 def __init__(self): super(CoreOSDistro,self).__init__() self.requiredDeps += [ "/usr/bin/systemctl" ] self.agent_service_name = 'waagent' self.init_script_file='/etc/systemd/system/waagent.service' self.fileBlackList.append("/etc/machine-id") self.dhcp_client_name='systemd-networkd' self.getpidcmd='pidof ' self.shadow_file_mode=0640 self.waagent_path='/usr/share/oem/bin' self.python_path='/usr/share/oem/python/bin' self.dhcp_enabled=True if 'PATH' in os.environ: os.environ['PATH'] = "{0}:{1}".format(os.environ['PATH'], self.python_path) else: os.environ['PATH'] = self.python_path if 'PYTHONPATH' in os.environ: os.environ['PYTHONPATH'] = "{0}:{1}".format(os.environ['PYTHONPATH'], self.waagent_path) else: os.environ['PYTHONPATH'] = self.waagent_path def checkPackageInstalled(self,p): """ There is no package manager in CoreOS. Return 1 since it must be preinstalled. """ return 1 def checkDependencies(self): for a in self.requiredDeps: if Run("which " + a + " > /dev/null 2>&1",chk_err=False): Error("Missing required dependency: " + a) return 1 return 0 def checkPackageUpdateable(self,p): """ There is no package manager in CoreOS. Return 0 since it can't be updated via package. """ return 0 def startAgentService(self): return Run('systemctl start ' + self.agent_service_name) def stopAgentService(self): return Run('systemctl stop ' + self.agent_service_name) def restartSshService(self): """ SSH is socket activated on CoreOS. No need to restart it. """ return 0 def sshDeployPublicKey(self,fprint,path): """ We support PKCS8. """ if Run("ssh-keygen -i -m PKCS8 -f " + fprint + " >> " + path): return 1 else : return 0 def RestartInterface(self, iface): Run("systemctl restart systemd-networkd") def CreateAccount(self, user, password, expiration, thumbprint): """ Create a user account, with 'user', 'password', 'expiration', ssh keys and sudo permissions. Returns None if successful, error string on failure. """ userentry = None try: userentry = pwd.getpwnam(user) except: pass uidmin = None try: uidmin = int(GetLineStartingWith("UID_MIN", "/etc/login.defs").split()[1]) except: pass if uidmin == None: uidmin = 100 if userentry != None and userentry[2] < uidmin and userentry[2] != self.CORE_UID: Error("CreateAccount: " + user + " is a system user. Will not set password.") return "Failed to set password for system user: " + user + " (0x06)." if userentry == None: command = "useradd --create-home --password '*' " + user if expiration != None: command += " --expiredate " + expiration.split('.')[0] if Run(command): Error("Failed to create user account: " + user) return "Failed to create user account: " + user + " (0x07)." else: Log("CreateAccount: " + user + " already exists. Will update password.") if password != None: self.changePass(user, password) try: if password == None: SetFileContents("/etc/sudoers.d/waagent", user + " ALL = (ALL) NOPASSWD: ALL\n") else: SetFileContents("/etc/sudoers.d/waagent", user + " ALL = (ALL) ALL\n") os.chmod("/etc/sudoers.d/waagent", 0440) except: Error("CreateAccount: Failed to configure sudo access for user.") return "Failed to configure sudo privileges (0x08)." home = MyDistro.GetHome() if thumbprint != None: dir = home + "/" + user + "/.ssh" CreateDir(dir, user, 0700) pub = dir + "/id_rsa.pub" prv = dir + "/id_rsa" Run("ssh-keygen -y -f " + thumbprint + ".prv > " + pub) SetFileContents(prv, GetFileContents(thumbprint + ".prv")) for f in [pub, prv]: os.chmod(f, 0600) ChangeOwner(f, user) SetFileContents(dir + "/authorized_keys", GetFileContents(pub)) ChangeOwner(dir + "/authorized_keys", user) Log("Created user account: " + user) return None def startDHCP(self): Run("systemctl start " + self.dhcp_client_name, chk_err=False) def stopDHCP(self): Run("systemctl stop " + self.dhcp_client_name, chk_err=False) def translateCustomData(self, data): return base64.b64decode(data) def getConfigurationPath(self): return "/usr/share/oem/waagent.conf" ############################################################ # debianDistro ############################################################ debian_init_file = """\ #!/bin/sh ### BEGIN INIT INFO # Provides: AzureLinuxAgent # Required-Start: $network $syslog # Required-Stop: $network $syslog # Should-Start: $network $syslog # Should-Stop: $network $syslog # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: AzureLinuxAgent # Description: AzureLinuxAgent ### END INIT INFO . /lib/lsb/init-functions OPTIONS="-daemon" WAZD_BIN=/usr/sbin/waagent WAZD_PID=/var/run/waagent.pid case "$1" in start) log_begin_msg "Starting AzureLinuxAgent..." pid=$( pidofproc $WAZD_BIN ) if [ -n "$pid" ] ; then log_begin_msg "Already running." log_end_msg 0 exit 0 fi start-stop-daemon --start --quiet --oknodo --background --exec $WAZD_BIN -- $OPTIONS log_end_msg $? ;; stop) log_begin_msg "Stopping AzureLinuxAgent..." start-stop-daemon --stop --quiet --oknodo --pidfile $WAZD_PID ret=$? rm -f $WAZD_PID log_end_msg $ret ;; force-reload) $0 restart ;; restart) $0 stop $0 start ;; status) status_of_proc $WAZD_BIN && exit 0 || exit $? ;; *) log_success_msg "Usage: /etc/init.d/waagent {start|stop|force-reload|restart|status}" exit 1 ;; esac exit 0 """ class debianDistro(AbstractDistro): """ debian Distro concrete class Put debian specific behavior here... """ def __init__(self): super(debianDistro,self).__init__() self.requiredDeps += [ "/usr/sbin/update-rc.d" ] self.init_file=debian_init_file self.agent_package_name='walinuxagent' self.dhcp_client_name='dhclient' self.getpidcmd='pidof ' self.shadow_file_mode=0640 def checkPackageInstalled(self,p): """ Check that the package is installed. Return 1 if installed, 0 if not installed. This method of using dpkg-query allows wildcards to be present in the package name. """ if not Run("dpkg-query -W -f='${Status}\n' '" + p + "' | grep ' installed' 2>&1",chk_err=False): return 1 else: return 0 def checkDependencies(self): """ Debian dependency check. python-pyasn1 is NOT needed. Return 1 unless all dependencies are satisfied. NOTE: using network*manager will catch either package name in Ubuntu or debian. """ if self.checkPackageInstalled('network*manager'): Error(GuestAgentLongName + " is not compatible with network-manager.") return 1 for a in self.requiredDeps: if Run("which " + a + " > /dev/null 2>&1",chk_err=False): Error("Missing required dependency: " + a) return 1 return 0 def checkPackageUpdateable(self,p): if Run("apt-get update ; apt-get upgrade -us | grep " + p,chk_err=False): return 1 else: return 0 def installAgentServiceScriptFiles(self): """ If we are packaged - the service name is walinuxagent, do nothing. """ if self.agent_service_name == 'walinuxagent': return 0 try: SetFileContents(self.init_script_file, self.init_file) os.chmod(self.init_script_file, 0744) except OSError, e: ErrorWithPrefix('installAgentServiceScriptFiles','Exception: '+str(e)+' occured creating ' + self.init_script_file) return 1 return 0 def registerAgentService(self): if self.installAgentServiceScriptFiles() == 0: return Run('update-rc.d waagent defaults') else : return 1 def uninstallAgentService(self): return Run('update-rc.d -f ' + self.agent_service_name + ' remove') def unregisterAgentService(self): self.stopAgentService() return self.uninstallAgentService() def sshDeployPublicKey(self,fprint,path): """ We support PKCS8. """ if Run("ssh-keygen -i -m PKCS8 -f " + fprint + " >> " + path): return 1 else : return 0 ############################################################ # KaliDistro - WIP # Functioning on Kali 1.1.0a so far ############################################################ class KaliDistro(debianDistro): """ Kali Distro concrete class Put Kali specific behavior here... """ def __init__(self): super(KaliDistro,self).__init__() ############################################################ # UbuntuDistro ############################################################ ubuntu_upstart_file = """\ #walinuxagent - start Azure agent description "walinuxagent" author "Ben Howard " start on (filesystem and started rsyslog) pre-start script WALINUXAGENT_ENABLED=1 [ -r /etc/default/walinuxagent ] && . /etc/default/walinuxagent if [ "$WALINUXAGENT_ENABLED" != "1" ]; then exit 1 fi if [ ! -x /usr/sbin/waagent ]; then exit 1 fi #Load the udf module modprobe -b udf end script exec /usr/sbin/waagent -daemon """ class UbuntuDistro(debianDistro): """ Ubuntu Distro concrete class Put Ubuntu specific behavior here... """ def __init__(self): super(UbuntuDistro,self).__init__() self.init_script_file='/etc/init/waagent.conf' self.init_file=ubuntu_upstart_file self.fileBlackList = [ "/root/.bash_history", "/var/log/waagent.log"] self.dhcp_client_name=None self.getpidcmd='pidof ' def registerAgentService(self): return self.installAgentServiceScriptFiles() def uninstallAgentService(self): """ If we are packaged - the service name is walinuxagent, do nothing. """ if self.agent_service_name == 'walinuxagent': return 0 os.remove('/etc/init/' + self.agent_service_name + '.conf') def unregisterAgentService(self): """ If we are packaged - the service name is walinuxagent, do nothing. """ if self.agent_service_name == 'walinuxagent': return self.stopAgentService() return self.uninstallAgentService() def deprovisionWarnUser(self): """ Ubuntu specific warning string from Deprovision. """ print("WARNING! Nameserver configuration in /etc/resolvconf/resolv.conf.d/{tail,original} will be deleted.") def deprovisionDeleteFiles(self): """ Ubuntu uses resolv.conf by default, so removing /etc/resolv.conf will break resolvconf. Therefore, we check to see if resolvconf is in use, and if so, we remove the resolvconf artifacts. """ if os.path.realpath('/etc/resolv.conf') != '/run/resolvconf/resolv.conf': Log("resolvconf is not configured. Removing /etc/resolv.conf") self.fileBlackList.append('/etc/resolv.conf') else: Log("resolvconf is enabled; leaving /etc/resolv.conf intact") resolvConfD = '/etc/resolvconf/resolv.conf.d/' self.fileBlackList.extend([resolvConfD + 'tail', resolvConfD + 'original']) for f in os.listdir(LibDir)+self.fileBlackList: try: os.remove(f) except: pass return 0 def getDhcpClientName(self): if self.dhcp_client_name != None : return self.dhcp_client_name if DistInfo()[1] == '12.04' : self.dhcp_client_name='dhclient3' else : self.dhcp_client_name='dhclient' return self.dhcp_client_name def waitForSshHostKey(self, path): """ Wait until the ssh host key is generated by cloud init. """ for retry in range(0, 10): if(os.path.isfile(path)): return True time.sleep(1) Error("Can't find host key: {0}".format(path)) return False ############################################################ # LinuxMintDistro ############################################################ class LinuxMintDistro(UbuntuDistro): """ LinuxMint Distro concrete class Put LinuxMint specific behavior here... """ def __init__(self): super(LinuxMintDistro,self).__init__() ############################################################ # fedoraDistro ############################################################ fedora_systemd_service = """\ [Unit] Description=Azure Linux Agent After=network.target After=sshd.service ConditionFileIsExecutable=/usr/sbin/waagent ConditionPathExists=/etc/waagent.conf [Service] Type=simple ExecStart=/usr/sbin/waagent -daemon [Install] WantedBy=multi-user.target """ class fedoraDistro(redhatDistro): """ FedoraDistro concrete class Put Fedora specific behavior here... """ def __init__(self): super(fedoraDistro,self).__init__() self.service_cmd = '/usr/bin/systemctl' self.hostname_file_path = '/etc/hostname' self.init_script_file = '/usr/lib/systemd/system/' + self.agent_service_name + '.service' self.init_file = fedora_systemd_service self.grubKernelBootOptionsFile = '/etc/default/grub' self.grubKernelBootOptionsLine = 'GRUB_CMDLINE_LINUX=' def publishHostname(self, name): SetFileContents(self.hostname_file_path, name + '\n') ethernetInterface = MyDistro.GetInterfaceName() filepath = "/etc/sysconfig/network-scripts/ifcfg-" + ethernetInterface if os.path.isfile(filepath): ReplaceFileContentsAtomic(filepath, "DHCP_HOSTNAME=" + name + "\n" + "\n".join(filter(lambda a: not a.startswith("DHCP_HOSTNAME"), GetFileContents(filepath).split('\n')))) return 0 def installAgentServiceScriptFiles(self): SetFileContents(self.init_script_file, self.init_file) os.chmod(self.init_script_file, 0644) return Run(self.service_cmd + ' daemon-reload') def registerAgentService(self): self.installAgentServiceScriptFiles() return Run(self.service_cmd + ' enable ' + self.agent_service_name) def uninstallAgentService(self): """ Call service subsystem to remove waagent script. """ return Run(self.service_cmd + ' disable ' + self.agent_service_name) def unregisterAgentService(self): """ Calls self.stopAgentService and call self.uninstallAgentService() """ self.stopAgentService() self.uninstallAgentService() def startAgentService(self): """ Service call to start the Agent service """ return Run(self.service_cmd + ' start ' + self.agent_service_name) def stopAgentService(self): """ Service call to stop the Agent service """ return Run(self.service_cmd + ' stop ' + self.agent_service_name, False) def restartSshService(self): """ Service call to re(start) the SSH service """ sshRestartCmd = self.service_cmd + " " + self.ssh_service_restart_option + " " + self.ssh_service_name retcode = Run(sshRestartCmd) if retcode > 0: Error("Failed to restart SSH service with return code:" + str(retcode)) return retcode def checkPackageInstalled(self, p): """ Query package database for prescence of an installed package. """ import rpm ts = rpm.TransactionSet() rpms = ts.dbMatch(rpm.RPMTAG_PROVIDES, p) return bool(len(rpms) > 0) def deleteRootPassword(self): return Run("/sbin/usermod root -p '!!'") def packagedInstall(self,buildroot): """ Called from setup.py for use by RPM. Copies generated files waagent.conf, under the buildroot. """ if not os.path.exists(buildroot+'/etc'): os.mkdir(buildroot+'/etc') SetFileContents(buildroot+'/etc/waagent.conf', MyDistro.waagent_conf_file) if not os.path.exists(buildroot+'/etc/logrotate.d'): os.mkdir(buildroot+'/etc/logrotate.d') SetFileContents(buildroot+'/etc/logrotate.d/WALinuxAgent', WaagentLogrotate) self.init_script_file=buildroot+self.init_script_file # this allows us to call installAgentServiceScriptFiles() if not os.path.exists(os.path.dirname(self.init_script_file)): os.mkdir(os.path.dirname(self.init_script_file)) self.installAgentServiceScriptFiles() def CreateAccount(self, user, password, expiration, thumbprint): super(fedoraDistro, self).CreateAccount(user, password, expiration, thumbprint) Run('/sbin/usermod ' + user + ' -G wheel') def DeleteAccount(self, user): Run('/sbin/usermod ' + user + ' -G ""') super(fedoraDistro, self).DeleteAccount(user) ############################################################ # FreeBSD ############################################################ FreeBSDWaagentConf = """\ # # Azure Linux Agent Configuration # Role.StateConsumer=None # Specified program is invoked with the argument "Ready" when we report ready status # to the endpoint server. Role.ConfigurationConsumer=None # Specified program is invoked with XML file argument specifying role configuration. Role.TopologyConsumer=None # Specified program is invoked with XML file argument specifying role topology. Provisioning.Enabled=y # Provisioning.DeleteRootPassword=y # Password authentication for root account will be unavailable. Provisioning.RegenerateSshHostKeyPair=y # Generate fresh host key pair. Provisioning.SshHostKeyPairType=rsa # Supported values are "rsa", "dsa" and "ecdsa". Provisioning.MonitorHostName=y # Monitor host name changes and publish changes via DHCP requests. ResourceDisk.Format=y # Format if unformatted. If 'n', resource disk will not be mounted. ResourceDisk.Filesystem=ufs2 # ResourceDisk.MountPoint=/mnt/resource # ResourceDisk.EnableSwap=n # Create and use swapfile on resource disk. ResourceDisk.SwapSizeMB=0 # Size of the swapfile. LBProbeResponder=y # Respond to load balancer probes if requested by Azure. Logs.Verbose=n # Enable verbose logs OS.RootDeviceScsiTimeout=300 # Root device timeout in seconds. OS.OpensslPath=None # If "None", the system default version is used. """ bsd_init_file="""\ #! /bin/sh # PROVIDE: waagent # REQUIRE: DAEMON cleanvar sshd # BEFORE: LOGIN # KEYWORD: nojail . /etc/rc.subr export PATH=$PATH:/usr/local/bin name="waagent" rcvar="waagent_enable" command="/usr/sbin/${name}" command_interpreter="/usr/local/bin/python" waagent_flags=" daemon &" pidfile="/var/run/waagent.pid" load_rc_config $name run_rc_command "$1" """ bsd_activate_resource_disk_txt="""\ #!/usr/bin/env python import os import sys if sys.version_info[0] == 2: import imp else: import importlib # waagent has no '.py' therefore create waagent module import manually. __name__='setupmain' #prevent waagent.__main__ from executing waagent=imp.load_source('waagent','/tmp/waagent') waagent.LoggerInit('/var/log/waagent.log','/dev/console') from waagent import RunGetOutput,Run Config=waagent.ConfigurationProvider(None) format = Config.get("ResourceDisk.Format") if format == None or format.lower().startswith("n"): sys.exit(0) device_base = 'da1' device = "/dev/" + device_base for entry in RunGetOutput("mount")[1].split(): if entry.startswith(device + "s1"): waagent.Log("ActivateResourceDisk: " + device + "s1 is already mounted.") sys.exit(0) mountpoint = Config.get("ResourceDisk.MountPoint") if mountpoint == None: mountpoint = "/mnt/resource" waagent.CreateDir(mountpoint, "root", 0755) fs = Config.get("ResourceDisk.Filesystem") if waagent.FreeBSDDistro().mediaHasFilesystem(device) == False : Run("newfs " + device + "s1") if Run("mount " + device + "s1 " + mountpoint): waagent.Error("ActivateResourceDisk: Failed to mount resource disk (" + device + "s1).") sys.exit(0) waagent.Log("Resource disk (" + device + "s1) is mounted at " + mountpoint + " with fstype " + fs) waagent.SetFileContents(os.path.join(mountpoint,waagent.README_FILENAME), waagent.README_FILECONTENT) swap = Config.get("ResourceDisk.EnableSwap") if swap == None or swap.lower().startswith("n"): sys.exit(0) sizeKB = int(Config.get("ResourceDisk.SwapSizeMB")) * 1024 if os.path.isfile(mountpoint + "/swapfile") and os.path.getsize(mountpoint + "/swapfile") != (sizeKB * 1024): os.remove(mountpoint + "/swapfile") if not os.path.isfile(mountpoint + "/swapfile"): Run("umask 0077 && dd if=/dev/zero of=" + mountpoint + "/swapfile bs=1024 count=" + str(sizeKB)) if Run("mdconfig -a -t vnode -f " + mountpoint + "/swapfile -u 0"): waagent.Error("ActivateResourceDisk: Configuring swap - Failed to create md0") if not Run("swapon /dev/md0"): waagent.Log("Enabled " + str(sizeKB) + " KB of swap at " + mountpoint + "/swapfile") else: waagent.Error("ActivateResourceDisk: Failed to activate swap at " + mountpoint + "/swapfile") """ class FreeBSDDistro(AbstractDistro): """ """ def __init__(self): """ Generic Attributes go here. These are based on 'majority rules'. This __init__() may be called or overriden by the child. """ super(FreeBSDDistro,self).__init__() self.agent_service_name = os.path.basename(sys.argv[0]) self.selinux=False self.ssh_service_name='sshd' self.ssh_config_file='/etc/ssh/sshd_config' self.hostname_file_path='/etc/hostname' self.dhcp_client_name='dhclient' self.requiredDeps = [ 'route', 'shutdown', 'ssh-keygen', 'pw' , 'openssl', 'fdisk', 'sed', 'grep' , 'sudo'] self.init_script_file='/etc/rc.d/waagent' self.init_file=bsd_init_file self.agent_package_name='WALinuxAgent' self.fileBlackList = [ "/root/.bash_history", "/var/log/waagent.log",'/etc/resolv.conf' ] self.agent_files_to_uninstall = ["/etc/waagent.conf"] self.grubKernelBootOptionsFile = '/boot/loader.conf' self.grubKernelBootOptionsLine = '' self.getpidcmd = 'pgrep -n' self.mount_dvd_cmd = 'dd bs=2048 count=33 skip=295 if=' # custom data max len is 64k self.sudoers_dir_base = '/usr/local/etc' self.waagent_conf_file = FreeBSDWaagentConf def installAgentServiceScriptFiles(self): SetFileContents(self.init_script_file, self.init_file) os.chmod(self.init_script_file, 0777) AppendFileContents("/etc/rc.conf","waagent_enable='YES'\n") return 0 def registerAgentService(self): self.installAgentServiceScriptFiles() return Run("services_mkdb " + self.init_script_file) def sshDeployPublicKey(self,fprint,path): """ We support PKCS8. """ if Run("ssh-keygen -i -m PKCS8 -f " + fprint + " >> " + path): return 1 else : return 0 def deleteRootPassword(self): """ BSD root password removal. """ filepath="/etc/master.passwd" ReplaceStringInFile(filepath,r'root:.*?:','root::') #ReplaceFileContentsAtomic(filepath,"root:*LOCK*:14600::::::\n" # + "\n".join(filter(lambda a: not a.startswith("root:"),GetFileContents(filepath).split('\n')))) os.chmod(filepath,self.shadow_file_mode) if self.isSelinuxSystem(): self.setSelinuxContext(filepath,'system_u:object_r:shadow_t:s0') RunGetOutput("pwd_mkdb -u root /etc/master.passwd") Log("Root password deleted.") return 0 def changePass(self,user,password): return RunSendStdin("pw usermod " + user + " -h 0 ",password, log_cmd=False) def load_ata_piix(self): return 0 def unload_ata_piix(self): return 0 def checkDependencies(self): """ FreeBSD dependency check. Return 1 unless all dependencies are satisfied. """ for a in self.requiredDeps: if Run("which " + a + " > /dev/null 2>&1",chk_err=False): Error("Missing required dependency: " + a) return 1 return 0 def packagedInstall(self,buildroot): pass def GetInterfaceName(self): """ Return the ip of the active ethernet interface. """ iface,inet,mac=self.GetFreeBSDEthernetInfo() return iface def RestartInterface(self, iface): Run("service netif restart") def GetIpv4Address(self): """ Return the ip of the active ethernet interface. """ iface,inet,mac=self.GetFreeBSDEthernetInfo() return inet def GetMacAddress(self): """ Return the ip of the active ethernet interface. """ iface,inet,mac=self.GetFreeBSDEthernetInfo() l=mac.split(':') r=[] for i in l: r.append(string.atoi(i,16)) return r def GetFreeBSDEthernetInfo(self): """ There is no SIOCGIFCONF on freeBSD - just parse ifconfig. Returns strings: iface, inet4_addr, and mac or 'None,None,None' if unable to parse. We will sleep and retry as the network must be up. """ code,output=RunGetOutput("ifconfig",chk_err=False) Log(output) retries=10 cmd='ifconfig | grep -A2 -B2 ether | grep -B3 inet | grep -A4 UP ' code=1 while code > 0 : if code > 0 and retries == 0: Error("GetFreeBSDEthernetInfo - Failed to detect ethernet interface") return None, None, None code,output=RunGetOutput(cmd,chk_err=False) retries-=1 if code > 0 and retries > 0 : Log("GetFreeBSDEthernetInfo - Error: retry ethernet detection " + str(retries)) if retries == 9 : c,o=RunGetOutput("ifconfig | grep -A1 -B2 ether",chk_err=False) if c == 0: t=o.replace('\n',' ') t=t.split() i=t[0][:-1] Log(RunGetOutput('id')[1]) Run('dhclient '+i) time.sleep(10) j=output.replace('\n',' ') j=j.split() iface=j[0][:-1] for i in range(len(j)): if j[i] == 'inet' : inet=j[i+1] elif j[i] == 'ether' : mac=j[i+1] return iface, inet, mac def CreateAccount(self,user, password, expiration, thumbprint): """ Create a user account, with 'user', 'password', 'expiration', ssh keys and sudo permissions. Returns None if successful, error string on failure. """ userentry = None try: userentry = pwd.getpwnam(user) except: pass uidmin = None try: if os.path.isfile("/etc/login.defs"): uidmin = int(GetLineStartingWith("UID_MIN", "/etc/login.defs").split()[1]) except: pass if uidmin == None: uidmin = 100 if userentry != None and userentry[2] < uidmin: Error("CreateAccount: " + user + " is a system user. Will not set password.") return "Failed to set password for system user: " + user + " (0x06)." if userentry == None: command = "pw useradd " + user + " -m" if expiration != None: command += " -e " + expiration.split('.')[0] if Run(command): Error("Failed to create user account: " + user) return "Failed to create user account: " + user + " (0x07)." else: Log("CreateAccount: " + user + " already exists. Will update password.") if password != None: self.changePass(user,password) try: # for older distros create sudoers.d if not os.path.isdir(MyDistro.sudoers_dir_base+'/sudoers.d/'): # create the /etc/sudoers.d/ directory os.mkdir(MyDistro.sudoers_dir_base+'/sudoers.d') # add the include of sudoers.d to the /etc/sudoers SetFileContents(MyDistro.sudoers_dir_base+'/sudoers',GetFileContents(MyDistro.sudoers_dir_base+'/sudoers')+'\n#includedir ' + MyDistro.sudoers_dir_base + '/sudoers.d\n') if password == None: SetFileContents(MyDistro.sudoers_dir_base+"/sudoers.d/waagent", user + " ALL = (ALL) NOPASSWD: ALL\n") else: SetFileContents(MyDistro.sudoers_dir_base+"/sudoers.d/waagent", user + " ALL = (ALL) ALL\n") os.chmod(MyDistro.sudoers_dir_base+"/sudoers.d/waagent", 0440) except: Error("CreateAccount: Failed to configure sudo access for user.") return "Failed to configure sudo privileges (0x08)." home = MyDistro.GetHome() if thumbprint != None: dir = home + "/" + user + "/.ssh" CreateDir(dir, user, 0700) pub = dir + "/id_rsa.pub" prv = dir + "/id_rsa" Run("ssh-keygen -y -f " + thumbprint + ".prv > " + pub) SetFileContents(prv, GetFileContents(thumbprint + ".prv")) for f in [pub, prv]: os.chmod(f, 0600) ChangeOwner(f, user) SetFileContents(dir + "/authorized_keys", GetFileContents(pub)) ChangeOwner(dir + "/authorized_keys", user) Log("Created user account: " + user) return None def DeleteAccount(self,user): """ Delete the 'user'. Clear utmp first, to avoid error. Removes the /etc/sudoers.d/waagent file. """ userentry = None try: userentry = pwd.getpwnam(user) except: pass if userentry == None: Error("DeleteAccount: " + user + " not found.") return uidmin = None try: if os.path.isfile("/etc/login.defs"): uidmin = int(GetLineStartingWith("UID_MIN", "/etc/login.defs").split()[1]) except: pass if uidmin == None: uidmin = 100 if userentry[2] < uidmin: Error("DeleteAccount: " + user + " is a system user. Will not delete account.") return Run("> /var/run/utmp") #Delete utmp to prevent error if we are the 'user' deleted pid = subprocess.Popen(['rmuser', '-y', user], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE).pid try: os.remove(MyDistro.sudoers_dir_base+"/sudoers.d/waagent") except: pass return def ActivateResourceDiskNoThread(self): """ Format, mount, and if specified in the configuration set resource disk as swap. """ global DiskActivated Run('cp /usr/sbin/waagent /tmp/') SetFileContents('/tmp/bsd_activate_resource_disk.py',bsd_activate_resource_disk_txt) Run('chmod +x /tmp/bsd_activate_resource_disk.py') pid = subprocess.Popen(["/tmp/bsd_activate_resource_disk.py", ""]).pid Log("Spawning bsd_activate_resource_disk.py") DiskActivated = True return def Install(self): """ Install the agent service. Check dependencies. Create /etc/waagent.conf and move old version to /etc/waagent.conf.old Copy RulesFiles to /var/lib/waagent Create /etc/logrotate.d/waagent Set /etc/ssh/sshd_config ClientAliveInterval to 180 Call ApplyVNUMAWorkaround() """ if MyDistro.checkDependencies(): return 1 os.chmod(sys.argv[0], 0755) SwitchCwd() for a in RulesFiles: if os.path.isfile(a): if os.path.isfile(GetLastPathElement(a)): os.remove(GetLastPathElement(a)) shutil.move(a, ".") Warn("Moved " + a + " -> " + LibDir + "/" + GetLastPathElement(a) ) MyDistro.registerAgentService() if os.path.isfile("/etc/waagent.conf"): try: os.remove("/etc/waagent.conf.old") except: pass try: os.rename("/etc/waagent.conf", "/etc/waagent.conf.old") Warn("Existing /etc/waagent.conf has been renamed to /etc/waagent.conf.old") except: pass SetFileContents("/etc/waagent.conf", self.waagent_conf_file) if os.path.exists('/usr/local/etc/logrotate.d/'): SetFileContents("/usr/local/etc/logrotate.d/waagent", WaagentLogrotate) filepath = "/etc/ssh/sshd_config" ReplaceFileContentsAtomic(filepath, "\n".join(filter(lambda a: not a.startswith("ClientAliveInterval"), GetFileContents(filepath).split('\n'))) + "\nClientAliveInterval 180\n") Log("Configured SSH client probing to keep connections alive.") #ApplyVNUMAWorkaround() return 0 def mediaHasFilesystem(self,dsk): if Run('LC_ALL=C fdisk -p ' + dsk + ' | grep "invalid fdisk partition table found" ',False): return False return True def mountDVD(self,dvd,location): #At this point we cannot read a joliet option udf DVD in freebsd10 - so we 'dd' it into our location retcode,out = RunGetOutput(self.mount_dvd_cmd + dvd + ' of=' + location + '/ovf-env.xml') if retcode != 0: return retcode,out ovfxml = (GetFileContents(location+"/ovf-env.xml",asbin=False)) if ord(ovfxml[0]) > 128 and ord(ovfxml[1]) > 128 and ord(ovfxml[2]) > 128 : ovfxml = ovfxml[3:] # BOM is not stripped. First three bytes are > 128 and not unicode chars so we ignore them. ovfxml = ovfxml.strip(chr(0x00)) ovfxml = "".join(filter(lambda x: ord(x)<128, ovfxml)) ovfxml = re.sub(r'.*\Z','',ovfxml,0,re.DOTALL) ovfxml += '' SetFileContents(location+"/ovf-env.xml", ovfxml) return retcode,out def GetHome(self): return '/home' def initScsiDiskTimeout(self): """ Set the SCSI disk timeout by updating the kernal config """ timeout = Config.get("OS.RootDeviceScsiTimeout") if timeout: Run("sysctl kern.cam.da.default_timeout=" + timeout) def setScsiDiskTimeout(self): return def setBlockDeviceTimeout(self, device, timeout): return def getProcessorCores(self): return int(RunGetOutput("sysctl hw.ncpu | awk '{print $2}'")[1]) def getTotalMemory(self): return int(RunGetOutput("sysctl hw.realmem | awk '{print $2}'")[1])/1024 def setDefaultGateway(self, gateway): Run("/sbin/route add default " + gateway, chk_err=False) def routeAdd(self, net, mask, gateway): Run("/sbin/route add -net " + net + " " + mask + " " + gateway, chk_err=False) ############################################################ # END DISTRO CLASS DEFS ############################################################ # This lets us index into a string or an array of integers transparently. def Ord(a): """ Allows indexing into a string or an array of integers transparently. Generic utility function. """ if type(a) == type("a"): a = ord(a) return a def IsLinux(): """ Returns True if platform is Linux. Generic utility function. """ return (platform.uname()[0] == "Linux") def GetLastPathElement(path): """ Similar to basename. Generic utility function. """ return path.rsplit('/', 1)[1] def GetFileContents(filepath,asbin=False): """ Read and return contents of 'filepath'. """ mode='r' if asbin: mode+='b' c=None try: with open(filepath, mode) as F : c=F.read() except IOError, e: ErrorWithPrefix('GetFileContents','Reading from file ' + filepath + ' Exception is ' + str(e)) return None return c def SetFileContents(filepath, contents): """ Write 'contents' to 'filepath'. """ if type(contents) == str : contents=contents.encode('latin-1', 'ignore') try: with open(filepath, "wb+") as F : F.write(contents) except IOError, e: ErrorWithPrefix('SetFileContents','Writing to file ' + filepath + ' Exception is ' + str(e)) return None return 0 def AppendFileContents(filepath, contents): """ Append 'contents' to 'filepath'. """ if type(contents) == str : contents=contents.encode('latin-1') try: with open(filepath, "a+") as F : F.write(contents) except IOError, e: ErrorWithPrefix('AppendFileContents','Appending to file ' + filepath + ' Exception is ' + str(e)) return None return 0 def ReplaceFileContentsAtomic(filepath, contents): """ Write 'contents' to 'filepath' by creating a temp file, and replacing original. """ handle, temp = tempfile.mkstemp(dir = os.path.dirname(filepath)) if type(contents) == str : contents=contents.encode('latin-1') try: os.write(handle, contents) except IOError, e: ErrorWithPrefix('ReplaceFileContentsAtomic','Writing to file ' + filepath + ' Exception is ' + str(e)) return None finally: os.close(handle) try: os.rename(temp, filepath) return None except IOError, e: ErrorWithPrefix('ReplaceFileContentsAtomic','Renaming ' + temp+ ' to ' + filepath + ' Exception is ' + str(e)) try: os.remove(filepath) except IOError, e: ErrorWithPrefix('ReplaceFileContentsAtomic','Removing '+ filepath + ' Exception is ' + str(e)) try: os.rename(temp,filepath) except IOError, e: ErrorWithPrefix('ReplaceFileContentsAtomic','Removing '+ filepath + ' Exception is ' + str(e)) return 1 return 0 def GetLineStartingWith(prefix, filepath): """ Return line from 'filepath' if the line startswith 'prefix' """ for line in GetFileContents(filepath).split('\n'): if line.startswith(prefix): return line return None def Run(cmd,chk_err=True): """ Calls RunGetOutput on 'cmd', returning only the return code. If chk_err=True then errors will be reported in the log. If chk_err=False then errors will be suppressed from the log. """ retcode,out=RunGetOutput(cmd,chk_err) return retcode def RunGetOutput(cmd, chk_err=True, log_cmd=True): """ Wrapper for subprocess.check_output. Execute 'cmd'. Returns return code and STDOUT, trapping expected exceptions. Reports exceptions to Error if chk_err parameter is True """ if log_cmd: LogIfVerbose(cmd) try: output=subprocess.check_output(cmd,stderr=subprocess.STDOUT,shell=True) except subprocess.CalledProcessError,e : if chk_err and log_cmd: Error('CalledProcessError. Error Code is ' + str(e.returncode) ) Error('CalledProcessError. Command string was ' + e.cmd ) Error('CalledProcessError. Command result was ' + (e.output[:-1]).decode('latin-1')) return e.returncode,e.output.decode('latin-1') return 0,output.decode('latin-1') def RunSendStdin(cmd, input, chk_err=True, log_cmd=True): """ Wrapper for subprocess.Popen. Execute 'cmd', sending 'input' to STDIN of 'cmd'. Returns return code and STDOUT, trapping expected exceptions. Reports exceptions to Error if chk_err parameter is True """ if log_cmd: LogIfVerbose(cmd+input) try: me=subprocess.Popen([cmd], shell=True, stdin=subprocess.PIPE,stderr=subprocess.STDOUT,stdout=subprocess.PIPE) output=me.communicate(input) except OSError , e : if chk_err and log_cmd: Error('CalledProcessError. Error Code is ' + str(me.returncode) ) Error('CalledProcessError. Command string was ' + cmd ) Error('CalledProcessError. Command result was ' + output[0].decode('latin-1')) return 1,output[0].decode('latin-1') if me.returncode is not 0 and chk_err is True and log_cmd: Error('CalledProcessError. Error Code is ' + str(me.returncode) ) Error('CalledProcessError. Command string was ' + cmd ) Error('CalledProcessError. Command result was ' + output[0].decode('latin-1')) return me.returncode,output[0].decode('latin-1') def GetNodeTextData(a): """ Filter non-text nodes from DOM tree """ for b in a.childNodes: if b.nodeType == b.TEXT_NODE: return b.data def GetHome(): """ Attempt to guess the $HOME location. Return the path string. """ home = None try: home = GetLineStartingWith("HOME", "/etc/default/useradd").split('=')[1].strip() except: pass if (home == None) or (home.startswith("/") == False): home = "/home" return home def ChangeOwner(filepath, user): """ Lookup user. Attempt chown 'filepath' to 'user'. """ p = None try: p = pwd.getpwnam(user) except: pass if p != None: if not os.path.exists(filepath): Error("Path does not exist: {0}".format(filepath)) else: os.chown(filepath, p[2], p[3]) def CreateDir(dirpath, user, mode): """ Attempt os.makedirs, catch all exceptions. Call ChangeOwner afterwards. """ try: os.makedirs(dirpath, mode) except: pass ChangeOwner(dirpath, user) def CreateAccount(user, password, expiration, thumbprint): """ Create a user account, with 'user', 'password', 'expiration', ssh keys and sudo permissions. Returns None if successful, error string on failure. """ userentry = None try: userentry = pwd.getpwnam(user) except: pass uidmin = None try: uidmin = int(GetLineStartingWith("UID_MIN", "/etc/login.defs").split()[1]) except: pass if uidmin == None: uidmin = 100 if userentry != None and userentry[2] < uidmin: Error("CreateAccount: " + user + " is a system user. Will not set password.") return "Failed to set password for system user: " + user + " (0x06)." if userentry == None: command = "useradd -m " + user if expiration != None: command += " -e " + expiration.split('.')[0] if Run(command): Error("Failed to create user account: " + user) return "Failed to create user account: " + user + " (0x07)." else: Log("CreateAccount: " + user + " already exists. Will update password.") if password != None: MyDistro.changePass(user, password) try: # for older distros create sudoers.d if not os.path.isdir('/etc/sudoers.d/'): # create the /etc/sudoers.d/ directory os.mkdir('/etc/sudoers.d/') # add the include of sudoers.d to the /etc/sudoers SetFileContents('/etc/sudoers',GetFileContents('/etc/sudoers')+'\n#includedir /etc/sudoers.d\n') if password == None: SetFileContents("/etc/sudoers.d/waagent", user + " ALL = (ALL) NOPASSWD: ALL\n") else: SetFileContents("/etc/sudoers.d/waagent", user + " ALL = (ALL) ALL\n") os.chmod("/etc/sudoers.d/waagent", 0440) except: Error("CreateAccount: Failed to configure sudo access for user.") return "Failed to configure sudo privileges (0x08)." home = MyDistro.GetHome() if thumbprint != None: dir = home + "/" + user + "/.ssh" CreateDir(dir, user, 0700) pub = dir + "/id_rsa.pub" prv = dir + "/id_rsa" Run("ssh-keygen -y -f " + thumbprint + ".prv > " + pub) SetFileContents(prv, GetFileContents(thumbprint + ".prv")) for f in [pub, prv]: os.chmod(f, 0600) ChangeOwner(f, user) SetFileContents(dir + "/authorized_keys", GetFileContents(pub)) ChangeOwner(dir + "/authorized_keys", user) Log("Created user account: " + user) return None def DeleteAccount(user): """ Delete the 'user'. Clear utmp first, to avoid error. Removes the /etc/sudoers.d/waagent file. """ userentry = None try: userentry = pwd.getpwnam(user) except: pass if userentry == None: Error("DeleteAccount: " + user + " not found.") return uidmin = None try: uidmin = int(GetLineStartingWith("UID_MIN", "/etc/login.defs").split()[1]) except: pass if uidmin == None: uidmin = 100 if userentry[2] < uidmin: Error("DeleteAccount: " + user + " is a system user. Will not delete account.") return Run("> /var/run/utmp") #Delete utmp to prevent error if we are the 'user' deleted Run("userdel -f -r " + user) try: os.remove("/etc/sudoers.d/waagent") except: pass return def IsInRangeInclusive(a, low, high): """ Return True if 'a' in 'low' <= a >= 'high' """ return (a >= low and a <= high) def IsPrintable(ch): """ Return True if character is displayable. """ return IsInRangeInclusive(ch, Ord('A'), Ord('Z')) or IsInRangeInclusive(ch, Ord('a'), Ord('z')) or IsInRangeInclusive(ch, Ord('0'), Ord('9')) def HexDump(buffer, size): """ Return Hex formated dump of a 'buffer' of 'size'. """ if size < 0: size = len(buffer) result = "" for i in range(0, size): if (i % 16) == 0: result += "%06X: " % i byte = buffer[i] if type(byte) == str: byte = ord(byte.decode('latin1')) result += "%02X " % byte if (i & 15) == 7: result += " " if ((i + 1) % 16) == 0 or (i + 1) == size: j = i while ((j + 1) % 16) != 0: result += " " if (j & 7) == 7: result += " " j += 1 result += " " for j in range(i - (i % 16), i + 1): byte=buffer[j] if type(byte) == str: byte = ord(byte.decode('latin1')) k = '.' if IsPrintable(byte): k = chr(byte) result += k if (i + 1) != size: result += "\n" return result def SimpleLog(file_path,message): if not file_path or len(message) < 1: return t = time.localtime() t = "%04u/%02u/%02u %02u:%02u:%02u " % (t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec) lines=re.sub(re.compile(r'^(.)',re.MULTILINE),t+r'\1',message) with open(file_path, "a") as F : lines = filter(lambda x : x in string.printable, lines) F.write(lines.encode('ascii','ignore') + "\n") class Logger(object): """ The Agent's logging assumptions are: For Log, and LogWithPrefix all messages are logged to the self.file_path and to the self.con_path. Setting either path parameter to None skips that log. If Verbose is enabled, messages calling the LogIfVerbose method will be logged to file_path yet not to con_path. Error and Warn messages are normal log messages with the 'ERROR:' or 'WARNING:' prefix added. """ def __init__(self,filepath,conpath,verbose=False): """ Construct an instance of Logger. """ self.file_path=filepath self.con_path=conpath self.verbose=verbose def ThrottleLog(self,counter): """ Log everything up to 10, every 10 up to 100, then every 100. """ return (counter < 10) or ((counter < 100) and ((counter % 10) == 0)) or ((counter % 100) == 0) def LogToFile(self,message): """ Write 'message' to logfile. """ if self.file_path: try: with open(self.file_path, "a") as F : message = filter(lambda x : x in string.printable, message) F.write(message.encode('ascii','ignore') + "\n") except IOError, e: print e pass def LogToCon(self,message): """ Write 'message' to /dev/console. This supports serial port logging if the /dev/console is redirected to ttys0 in kernel boot options. """ if self.con_path: try: with open(self.con_path, "w") as C : message = filter(lambda x : x in string.printable, message) C.write(message.encode('ascii','ignore') + "\n") except IOError, e: pass def Log(self,message): """ Standard Log function. Logs to self.file_path, and con_path """ self.LogWithPrefix("", message) def LogWithPrefix(self,prefix, message): """ Prefix each line of 'message' with current time+'prefix'. """ t = time.localtime() t = "%04u/%02u/%02u %02u:%02u:%02u " % (t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec) t += prefix for line in message.split('\n'): line = t + line self.LogToFile(line) self.LogToCon(line) def NoLog(self,message): """ Don't Log. """ pass def LogIfVerbose(self,message): """ Only log 'message' if global Verbose is True. """ self.LogWithPrefixIfVerbose('',message) def LogWithPrefixIfVerbose(self,prefix, message): """ Only log 'message' if global Verbose is True. Prefix each line of 'message' with current time+'prefix'. """ if self.verbose == True: t = time.localtime() t = "%04u/%02u/%02u %02u:%02u:%02u " % (t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec) t += prefix for line in message.split('\n'): line = t + line self.LogToFile(line) self.LogToCon(line) def Warn(self,message): """ Prepend the text "WARNING:" to the prefix for each line in 'message'. """ self.LogWithPrefix("WARNING:", message) def Error(self,message): """ Call ErrorWithPrefix(message). """ ErrorWithPrefix("", message) def ErrorWithPrefix(self,prefix, message): """ Prepend the text "ERROR:" to the prefix for each line in 'message'. Errors written to logfile, and /dev/console """ self.LogWithPrefix("ERROR:", message) def LoggerInit(log_file_path,log_con_path,verbose=False): """ Create log object and export its methods to global scope. """ global Log,LogWithPrefix,LogIfVerbose,LogWithPrefixIfVerbose,Error,ErrorWithPrefix,Warn,NoLog,ThrottleLog,myLogger l=Logger(log_file_path,log_con_path,verbose) Log,LogWithPrefix,LogIfVerbose,LogWithPrefixIfVerbose,Error,ErrorWithPrefix,Warn,NoLog,ThrottleLog,myLogger = l.Log,l.LogWithPrefix,l.LogIfVerbose,l.LogWithPrefixIfVerbose,l.Error,l.ErrorWithPrefix,l.Warn,l.NoLog,l.ThrottleLog,l def Linux_ioctl_GetInterfaceMac(ifname): """ Return the mac-address bound to the socket. """ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', (ifname[:15]+('\0'*241)).encode('latin-1'))) return ''.join(['%02X' % Ord(char) for char in info[18:24]]) def GetFirstActiveNetworkInterfaceNonLoopback(): """ Return the interface name, and ip addr of the first active non-loopback interface. """ iface='' expected=16 # how many devices should I expect... is_64bits = sys.maxsize > 2**32 struct_size=40 if is_64bits else 32 # for 64bit the size is 40 bytes, for 32bits it is 32 bytes. s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) buff=array.array('B', b'\0' * (expected*struct_size)) retsize=(struct.unpack('iL', fcntl.ioctl(s.fileno(), 0x8912, struct.pack('iL',expected*struct_size,buff.buffer_info()[0]))))[0] if retsize == (expected*struct_size) : Warn('SIOCGIFCONF returned more than ' + str(expected) + ' up network interfaces.') s=buff.tostring() preferred_nic = Config.get("Network.Interface") for i in range(0,struct_size*expected,struct_size): iface=s[i:i+16].split(b'\0', 1)[0] if iface == b'lo': continue elif preferred_nic is None: break elif iface == preferred_nic: break return iface.decode('latin-1'), socket.inet_ntoa(s[i+20:i+24]) def GetIpv4Address(): """ Return the ip of the first active non-loopback interface. """ iface,addr=GetFirstActiveNetworkInterfaceNonLoopback() return addr def HexStringToByteArray(a): """ Return hex string packed into a binary struct. """ b = b"" for c in range(0, len(a) // 2): b += struct.pack("B", int(a[c * 2:c * 2 + 2], 16)) return b def GetMacAddress(): """ Convienience function, returns mac addr bound to first non-loobback interface. """ ifname='' while len(ifname) < 2 : ifname=GetFirstActiveNetworkInterfaceNonLoopback()[0] a = Linux_ioctl_GetInterfaceMac(ifname) return HexStringToByteArray(a) def DeviceForIdePort(n): """ Return device name attached to ide port 'n'. """ if n > 3: return None g0 = "00000000" if n > 1: g0 = "00000001" n = n - 2 device = None path = "/sys/bus/vmbus/devices/" for vmbus in os.listdir(path): guid = GetFileContents(path + vmbus + "/device_id").lstrip('{').split('-') if guid[0] == g0 and guid[1] == "000" + str(n): for root, dirs, files in os.walk(path + vmbus): if root.endswith("/block"): device = dirs[0] break else : #older distros for d in dirs: if ':' in d and "block" == d.split(':')[0]: device = d.split(':')[1] break break return device class HttpResourceGoneError(Exception): pass class Util(object): """ Http communication class. Base of GoalState, and Agent classes. """ RetryWaitingInterval=10 def __init__(self): self.Endpoint = None def _ParseUrl(self, url): secure = False host = self.Endpoint path = url port = None #"http[s]://hostname[:port][/]" if url.startswith("http://"): url = url[7:] if "/" in url: host = url[0: url.index("/")] path = url[url.index("/"):] else: host = url path = "/" elif url.startswith("https://"): secure = True url = url[8:] if "/" in url: host = url[0: url.index("/")] path = url[url.index("/"):] else: host = url path = "/" if host is None: raise ValueError("Host is invalid:{0}".format(url)) if(":" in host): pos = host.rfind(":") port = int(host[pos + 1:]) host = host[0:pos] return host, port, secure, path def GetHttpProxy(self, secure): """ Get http_proxy and https_proxy from environment variables. Username and password is not supported now. """ host = Config.get("HttpProxy.Host") port = Config.get("HttpProxy.Port") return (host, port) def _HttpRequest(self, method, host, path, port=None, data=None, secure=False, headers=None, proxyHost=None, proxyPort=None): resp = None conn = None try: if secure: port = 443 if port is None else port if proxyHost is not None and proxyPort is not None: conn = httplib.HTTPSConnection(proxyHost, proxyPort, timeout=10) conn.set_tunnel(host, port) #If proxy is used, full url is needed. path = "https://{0}:{1}{2}".format(host, port, path) else: conn = httplib.HTTPSConnection(host, port, timeout=10) else: port = 80 if port is None else port if proxyHost is not None and proxyPort is not None: conn = httplib.HTTPConnection(proxyHost, proxyPort, timeout=10) #If proxy is used, full url is needed. path = "http://{0}:{1}{2}".format(host, port, path) else: conn = httplib.HTTPConnection(host, port, timeout=10) if headers == None: conn.request(method, path, data) else: conn.request(method, path, data, headers) resp = conn.getresponse() except httplib.HTTPException, e: Error('HTTPException {0}, args:{1}'.format(e, repr(e.args))) except IOError, e: Error('Socket IOError {0}, args:{1}'.format(e, repr(e.args))) return resp def HttpRequest(self, method, url, data=None, headers=None, maxRetry=3, chkProxy=False): """ Sending http request to server On error, sleep 10 and maxRetry times. Return the output buffer or None. """ LogIfVerbose("HTTP Req: {0} {1}".format(method, url)) LogIfVerbose("HTTP Req: Data={0}".format(data)) LogIfVerbose("HTTP Req: Header={0}".format(headers)) try: host, port, secure, path = self._ParseUrl(url) except ValueError, e: Error("Failed to parse url:{0}".format(url)) return None #Check proxy proxyHost, proxyPort = (None, None) if chkProxy: proxyHost, proxyPort = self.GetHttpProxy(secure) #If httplib module is not built with ssl support. Fallback to http if secure and not hasattr(httplib, "HTTPSConnection"): Warn("httplib is not built with ssl support") secure = False proxyHost, proxyPort = self.GetHttpProxy(secure) #If httplib module doesn't support https tunnelling. Fallback to http if secure and \ proxyHost is not None and \ proxyPort is not None and \ not hasattr(httplib.HTTPSConnection, "set_tunnel"): Warn("httplib doesn't support https tunnelling(new in python 2.7)") secure = False proxyHost, proxyPort = self.GetHttpProxy(secure) resp = self._HttpRequest(method, host, path, port=port, data=data, secure=secure, headers=headers, proxyHost=proxyHost, proxyPort=proxyPort) for retry in range(0, maxRetry): if resp is not None and \ (resp.status == httplib.OK or \ resp.status == httplib.CREATED or \ resp.status == httplib.ACCEPTED): return resp; if resp is not None and resp.status == httplib.GONE: raise HttpResourceGoneError("Http resource gone.") Error("Retry={0}".format(retry)) Error("HTTP Req: {0} {1}".format(method, url)) Error("HTTP Req: Data={0}".format(data)) Error("HTTP Req: Header={0}".format(headers)) if resp is None: Error("HTTP Err: response is empty.".format(retry)) else: Error("HTTP Err: Status={0}".format(resp.status)) Error("HTTP Err: Reason={0}".format(resp.reason)) Error("HTTP Err: Header={0}".format(resp.getheaders())) Error("HTTP Err: Body={0}".format(resp.read())) time.sleep(self.__class__.RetryWaitingInterval) resp = self._HttpRequest(method, host, path, port=port, data=data, secure=secure, headers=headers, proxyHost=proxyHost, proxyPort=proxyPort) return None def HttpGet(self, url, headers=None, maxRetry=3, chkProxy=False): return self.HttpRequest("GET", url, headers=headers, maxRetry=maxRetry, chkProxy=chkProxy) def HttpHead(self, url, headers=None, maxRetry=3, chkProxy=False): return self.HttpRequest("HEAD", url, headers=headers, maxRetry=maxRetry, chkProxy=chkProxy) def HttpPost(self, url, data, headers=None, maxRetry=3, chkProxy=False): return self.HttpRequest("POST", url, data=data, headers=headers, maxRetry=maxRetry, chkProxy=chkProxy) def HttpPut(self, url, data, headers=None, maxRetry=3, chkProxy=False): return self.HttpRequest("PUT", url, data=data, headers=headers, maxRetry=maxRetry, chkProxy=chkProxy) def HttpDelete(self, url, headers=None, maxRetry=3, chkProxy=False): return self.HttpRequest("DELETE", url, headers=headers, maxRetry=maxRetry, chkProxy=chkProxy) def HttpGetWithoutHeaders(self, url, maxRetry=3, chkProxy=False): """ Return data from an HTTP get on 'url'. """ resp = self.HttpGet(url, headers=None, maxRetry=maxRetry, chkProxy=chkProxy) return resp.read() if resp is not None else None def HttpGetWithHeaders(self, url, maxRetry=3, chkProxy=False): """ Return data from an HTTP get on 'url' with x-ms-agent-name and x-ms-version headers. """ resp = self.HttpGet(url, headers={ "x-ms-agent-name": GuestAgentName, "x-ms-version": ProtocolVersion }, maxRetry=maxRetry, chkProxy=chkProxy) return resp.read() if resp is not None else None def HttpSecureGetWithHeaders(self, url, transportCert, maxRetry=3, chkProxy=False): """ Return output of get using ssl cert. """ resp = self.HttpGet(url, headers={ "x-ms-agent-name": GuestAgentName, "x-ms-version": ProtocolVersion, "x-ms-cipher-name": "DES_EDE3_CBC", "x-ms-guest-agent-public-x509-cert": transportCert }, maxRetry=maxRetry, chkProxy=chkProxy) return resp.read() if resp is not None else None def HttpPostWithHeaders(self, url, data, maxRetry=3, chkProxy=False): headers = { "x-ms-agent-name": GuestAgentName, "Content-Type": "text/xml; charset=utf-8", "x-ms-version": ProtocolVersion } try: return self.HttpPost(url, data=data, headers=headers, maxRetry=maxRetry, chkProxy=chkProxy) except HttpResourceGoneError as e: Error("Failed to post: {0} {1}".format(url, e)) return None __StorageVersion="2014-02-14" def GetBlobType(url): restutil = Util() #Check blob type LogIfVerbose("Check blob type.") timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) blobPropResp = restutil.HttpHead(url, { "x-ms-date" : timestamp, 'x-ms-version' : __StorageVersion }, chkProxy=True); blobType = None if blobPropResp is None: Error("Can't get status blob type.") return None blobType = blobPropResp.getheader("x-ms-blob-type") LogIfVerbose("Blob type={0}".format(blobType)) return blobType def PutBlockBlob(url, data): restutil = Util() LogIfVerbose("Upload block blob") timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) ret = restutil.HttpPut(url, data, { "x-ms-date" : timestamp, "x-ms-blob-type" : "BlockBlob", "Content-Length": str(len(data)), "x-ms-version" : __StorageVersion }, chkProxy=True) if ret is None: Error("Failed to upload block blob for status.") return -1 return 0 def PutPageBlob(url, data): restutil = Util() LogIfVerbose("Replace old page blob") timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) #Align to 512 bytes pageBlobSize = ((len(data) + 511) / 512) * 512 ret = restutil.HttpPut(url, "", { "x-ms-date" : timestamp, "x-ms-blob-type" : "PageBlob", "Content-Length": "0", "x-ms-blob-content-length" : str(pageBlobSize), "x-ms-version" : __StorageVersion }, chkProxy=True) if ret is None: Error("Failed to clean up page blob for status") return -1 if url.index('?') < 0: url = "{0}?comp=page".format(url) else: url = "{0}&comp=page".format(url) LogIfVerbose("Upload page blob") pageMax = 4 * 1024 * 1024 #Max page size: 4MB start = 0 end = 0 while end < len(data): end = min(len(data), start + pageMax) contentSize = end - start #Align to 512 bytes pageEnd = ((end + 511) / 512) * 512 bufSize = pageEnd - start buf = bytearray(bufSize) buf[0 : contentSize] = data[start : end] ret = restutil.HttpPut(url, buffer(buf), { "x-ms-date" : timestamp, "x-ms-range" : "bytes={0}-{1}".format(start, pageEnd - 1), "x-ms-page-write" : "update", "x-ms-version" : __StorageVersion, "Content-Length": str(pageEnd - start) }, chkProxy=True) if ret is None: Error("Failed to upload page blob for status") return -1 start = end return 0 def UploadStatusBlob(url, data): LogIfVerbose("Upload status blob") LogIfVerbose("Status={0}".format(data)) blobType = GetBlobType(url) if blobType == "BlockBlob": return PutBlockBlob(url, data) elif blobType == "PageBlob": return PutPageBlob(url, data) else: Error("Unknown blob type: {0}".format(blobType)) return -1 class TCPHandler(SocketServer.BaseRequestHandler): """ Callback object for LoadBalancerProbeServer. Recv and send LB probe messages. """ def __init__(self,lb_probe): super(TCPHandler,self).__init__() self.lb_probe=lb_probe def GetHttpDateTimeNow(self): """ Return formatted gmtime "Date: Fri, 25 Mar 2011 04:53:10 GMT" """ return time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()) def handle(self): """ Log LB probe messages, read the socket buffer, send LB probe response back to server. """ self.lb_probe.ProbeCounter = (self.lb_probe.ProbeCounter + 1) % 1000000 log = [NoLog, LogIfVerbose][ThrottleLog(self.lb_probe.ProbeCounter)] strCounter = str(self.lb_probe.ProbeCounter) if self.lb_probe.ProbeCounter == 1: Log("Receiving LB probes.") log("Received LB probe # " + strCounter) self.request.recv(1024) self.request.send("HTTP/1.1 200 OK\r\nContent-Length: 2\r\nContent-Type: text/html\r\nDate: " + self.GetHttpDateTimeNow() + "\r\n\r\nOK") class LoadBalancerProbeServer(object): """ Threaded object to receive and send LB probe messages. Load Balancer messages but be recv'd by the load balancing server, or this node may be shut-down. """ def __init__(self, port): self.ProbeCounter = 0 self.server = SocketServer.TCPServer((self.get_ip(), port), TCPHandler) self.server_thread = threading.Thread(target = self.server.serve_forever) self.server_thread.setDaemon(True) self.server_thread.start() def shutdown(self): self.server.shutdown() def get_ip(self): for retry in range(1,6): ip = MyDistro.GetIpv4Address() if ip == None : Log("LoadBalancerProbeServer: GetIpv4Address() returned None, sleeping 10 before retry " + str(retry+1) ) time.sleep(10) else: return ip class ConfigurationProvider(object): """ Parse amd store key:values in waagent.conf """ def __init__(self, walaConfigFile): self.values = dict() if 'MyDistro' not in globals(): global MyDistro MyDistro = GetMyDistro() if walaConfigFile is None: walaConfigFile = MyDistro.getConfigurationPath() if os.path.isfile(walaConfigFile) == False: raise Exception("Missing configuration in {0}".format(walaConfigFile)) try: for line in GetFileContents(walaConfigFile).split('\n'): if not line.startswith("#") and "=" in line: parts = line.split()[0].split('=') value = parts[1].strip("\" ") if value != "None": self.values[parts[0]] = value else: self.values[parts[0]] = None except: Error("Unable to parse {0}".format(walaConfigFile)) raise return def get(self, key): return self.values.get(key) class EnvMonitor(object): """ Montor changes to dhcp and hostname. If dhcp clinet process re-start has occurred, reset routes, dhcp with fabric. """ def __init__(self): self.shutdown = False self.HostName = socket.gethostname() self.server_thread = threading.Thread(target = self.monitor) self.server_thread.setDaemon(True) self.server_thread.start() self.published = False def monitor(self): """ Monitor dhcp client pid and hostname. If dhcp clinet process re-start has occurred, reset routes, dhcp with fabric. """ publish = Config.get("Provisioning.MonitorHostName") dhcpcmd = MyDistro.getpidcmd+ ' ' + MyDistro.getDhcpClientName() dhcppid = RunGetOutput(dhcpcmd)[1] while not self.shutdown: for a in RulesFiles: if os.path.isfile(a): if os.path.isfile(GetLastPathElement(a)): os.remove(GetLastPathElement(a)) shutil.move(a, ".") Log("EnvMonitor: Moved " + a + " -> " + LibDir) MyDistro.setScsiDiskTimeout() if publish != None and publish.lower().startswith("y"): try: if socket.gethostname() != self.HostName: Log("EnvMonitor: Detected host name change: " + self.HostName + " -> " + socket.gethostname()) self.HostName = socket.gethostname() WaAgent.UpdateAndPublishHostName(self.HostName) dhcppid = RunGetOutput(dhcpcmd)[1] self.published = True except: pass else: self.published = True pid = "" if not os.path.isdir("/proc/" + dhcppid.strip()): pid = RunGetOutput(dhcpcmd)[1] if pid != "" and pid != dhcppid: Log("EnvMonitor: Detected dhcp client restart. Restoring routing table.") WaAgent.RestoreRoutes() dhcppid = pid for child in Children: if child.poll() != None: Children.remove(child) time.sleep(5) def SetHostName(self, name): """ Generic call to MyDistro.setHostname(name). Complian to Log on error. """ if socket.gethostname() == name: self.published = True elif MyDistro.setHostname(name): Error("Error: SetHostName: Cannot set hostname to " + name) return ("Error: SetHostName: Cannot set hostname to " + name) def IsHostnamePublished(self): """ Return self.published """ return self.published def ShutdownService(self): """ Stop server comminucation and join the thread to main thread. """ self.shutdown = True self.server_thread.join() class Certificates(object): """ Object containing certificates of host and provisioned user. Parses and splits certificates into files. """ # # 2010-12-15 # 2 # Pkcs7BlobWithPfxContents # MIILTAY... # # def __init__(self): self.reinitialize() def reinitialize(self): """ Reset the Role, Incarnation """ self.Incarnation = None self.Role = None def Parse(self, xmlText): """ Parse multiple certificates into seperate files. """ self.reinitialize() SetFileContents("Certificates.xml", xmlText) dom = xml.dom.minidom.parseString(xmlText) for a in [ "CertificateFile", "Version", "Incarnation", "Format", "Data", ]: if not dom.getElementsByTagName(a): Error("Certificates.Parse: Missing " + a) return None node = dom.childNodes[0] if node.localName != "CertificateFile": Error("Certificates.Parse: root not CertificateFile") return None SetFileContents("Certificates.p7m", "MIME-Version: 1.0\n" + "Content-Disposition: attachment; filename=\"Certificates.p7m\"\n" + "Content-Type: application/x-pkcs7-mime; name=\"Certificates.p7m\"\n" + "Content-Transfer-Encoding: base64\n\n" + GetNodeTextData(dom.getElementsByTagName("Data")[0])) if Run(Openssl + " cms -decrypt -in Certificates.p7m -inkey TransportPrivate.pem -recip TransportCert.pem | " + Openssl + " pkcs12 -nodes -password pass: -out Certificates.pem"): Error("Certificates.Parse: Failed to extract certificates from CMS message.") return self # There may be multiple certificates in this package. Split them. file = open("Certificates.pem") pindex = 1 cindex = 1 output = open("temp.pem", "w") for line in file.readlines(): output.write(line) if re.match(r'[-]+END .*?(KEY|CERTIFICATE)[-]+$',line): output.close() if re.match(r'[-]+END .*?KEY[-]+$',line): os.rename("temp.pem", str(pindex) + ".prv") pindex += 1 else: os.rename("temp.pem", str(cindex) + ".crt") cindex += 1 output = open("temp.pem", "w") output.close() os.remove("temp.pem") keys = dict() index = 1 filename = str(index) + ".crt" while os.path.isfile(filename): thumbprint = (RunGetOutput(Openssl + " x509 -in " + filename + " -fingerprint -noout")[1]).rstrip().split('=')[1].replace(':', '').upper() pubkey=RunGetOutput(Openssl + " x509 -in " + filename + " -pubkey -noout")[1] keys[pubkey] = thumbprint os.rename(filename, thumbprint + ".crt") os.chmod(thumbprint + ".crt", 0600) MyDistro.setSelinuxContext(thumbprint + '.crt','unconfined_u:object_r:ssh_home_t:s0') index += 1 filename = str(index) + ".crt" index = 1 filename = str(index) + ".prv" while os.path.isfile(filename): pubkey = RunGetOutput(Openssl + " rsa -in " + filename + " -pubout 2> /dev/null ")[1] os.rename(filename, keys[pubkey] + ".prv") os.chmod(keys[pubkey] + ".prv", 0600) MyDistro.setSelinuxContext( keys[pubkey] + '.prv','unconfined_u:object_r:ssh_home_t:s0') index += 1 filename = str(index) + ".prv" return self class SharedConfig(object): """ Parse role endpoint server and goal state config. """ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # def __init__(self): self.reinitialize() def reinitialize(self): """ Reset members. """ self.RdmaMacAddress = None self.RdmaIPv4Address = None self.xmlText = None def Parse(self, xmlText): """ Parse and write configuration to file SharedConfig.xml. """ LogIfVerbose(xmlText) self.reinitialize() self.xmlText = xmlText dom = xml.dom.minidom.parseString(xmlText) for a in [ "SharedConfig", "Deployment", "Service", "ServiceInstance", "Incarnation", "Role", ]: if not dom.getElementsByTagName(a): Error("SharedConfig.Parse: Missing " + a) node = dom.childNodes[0] if node.localName != "SharedConfig": Error("SharedConfig.Parse: root not SharedConfig") nodes = dom.getElementsByTagName("Instance") if nodes is not None and len(nodes) != 0: node = nodes[0] if node.hasAttribute("rdmaMacAddress"): addr = node.getAttribute("rdmaMacAddress") self.RdmaMacAddress = addr[0:2] for i in range(1, 6): self.RdmaMacAddress += ":" + addr[2 * i : 2 *i + 2] if node.hasAttribute("rdmaIPv4Address"): self.RdmaIPv4Address = node.getAttribute("rdmaIPv4Address") return self def Save(self): LogIfVerbose("Save SharedConfig.xml") SetFileContents("SharedConfig.xml", self.xmlText) def InvokeTopologyConsumer(self): program = Config.get("Role.TopologyConsumer") if program != None: try: Children.append(subprocess.Popen([program, LibDir + "/SharedConfig.xml"])) except OSError, e : ErrorWithPrefix('Agent.Run','Exception: '+ str(e) +' occured launching ' + program ) def Process(self): global rdma_configured if not rdma_configured and self.RdmaMacAddress is not None and self.RdmaIPv4Address is not None: handler = RdmaHandler(self.RdmaMacAddress, self.RdmaIPv4Address) handler.start() rdma_configured = True self.InvokeTopologyConsumer() rdma_configured = False class RdmaError(Exception): pass class RdmaHandler(object): """ Handle rdma configuration. """ def __init__(self, mac, ip_addr, dev="/dev/hvnd_rdma", dat_conf_files=['/etc/dat.conf', '/etc/rdma/dat.conf', '/usr/local/etc/dat.conf']): self.mac = mac self.ip_addr = ip_addr self.dev = dev self.dat_conf_files = dat_conf_files self.data = ('rdmaMacAddress="{0}" rdmaIPv4Address="{1}"' '').format(self.mac, self.ip_addr) def start(self): """ Start a new thread to process rdma """ threading.Thread(target=self.process).start() def process(self): try: self.set_dat_conf() self.set_rdma_dev() self.set_rdma_ip() except RdmaError as e: Error("Failed to config rdma device: {0}".format(e)) def set_dat_conf(self): """ Agent needs to search all possible locations for dat.conf """ Log("Set dat.conf") for dat_conf_file in self.dat_conf_files: if not os.path.isfile(dat_conf_file): continue try: self.write_dat_conf(dat_conf_file) except IOError as e: raise RdmaError("Failed to write to dat.conf: {0}".format(e)) def write_dat_conf(self, dat_conf_file): Log("Write config to {0}".format(dat_conf_file)) old = ("ofa-v2-ib0 u2.0 nonthreadsafe default libdaplofa.so.2 " "dapl.2.0 \"\S+ 0\"") new = ("ofa-v2-ib0 u2.0 nonthreadsafe default libdaplofa.so.2 " "dapl.2.0 \"{0} 0\"").format(self.ip_addr) lines = GetFileContents(dat_conf_file) lines = re.sub(old, new, lines) SetFileContents(dat_conf_file, lines) def set_rdma_dev(self): """ Write config string to /dev/hvnd_rdma """ Log("Set /dev/hvnd_rdma") self.wait_rdma_dev() self.write_rdma_dev_conf() def write_rdma_dev_conf(self): Log("Write rdma config to {0}: {1}".format(self.dev, self.data)) try: with open(self.dev, "w") as c: c.write(self.data) except IOError, e: raise RdmaError("Error writing {0}, {1}".format(self.dev, e)) def wait_rdma_dev(self): Log("Wait for /dev/hvnd_rdma") retry = 0 while retry < 120: if os.path.exists(self.dev): return time.sleep(1) retry += 1 raise RdmaError("The device doesn't show up in 120 seconds") def set_rdma_ip(self): Log("Set ip addr for rdma") try: if_name = MyDistro.getInterfaceNameByMac(self.mac) #Azure is using 12 bits network mask for infiniband. MyDistro.configIpV4(if_name, self.ip_addr, 12) except Exception as e: raise RdmaError("Failed to config rdma device: {0}".format(e)) class ExtensionsConfig(object): """ Parse ExtensionsConfig, downloading and unpacking them to /var/lib/waagent. Install if true, remove if it is set to false. """ # # # # # # # {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"1BE9A13AA1321C7C515EF109746998BAB6D86FD1", #"protectedSettings":"MIIByAYJKoZIhvcNAQcDoIIBuTCCAbUCAQAxggFxMIIBbQIBADBVMEExPzA9BgoJkiaJk/IsZAEZFi9XaW5kb3dzIEF6dXJlIFNlcnZpY2UgTWFuYWdlbWVudCBmb3IgR #Xh0ZW5zaW9ucwIQZi7dw+nhc6VHQTQpCiiV2zANBgkqhkiG9w0BAQEFAASCAQCKr09QKMGhwYe+O4/a8td+vpB4eTR+BQso84cV5KCAnD6iUIMcSYTrn9aveY6v6ykRLEw8GRKfri2d6 #tvVDggUrBqDwIgzejGTlCstcMJItWa8Je8gHZVSDfoN80AEOTws9Fp+wNXAbSuMJNb8EnpkpvigAWU2v6pGLEFvSKC0MCjDTkjpjqciGMcbe/r85RG3Zo21HLl0xNOpjDs/qqikc/ri43Y76E/X #v1vBSHEGMFprPy/Hwo3PqZCnulcbVzNnaXN3qi/kxV897xGMPPC3IrO7Nc++AT9qRLFI0841JLcLTlnoVG1okPzK9w6ttksDQmKBSHt3mfYV+skqs+EOMDsGCSqGSIb3DQEHATAUBggqh #kiG9w0DBwQITgu0Nu3iFPuAGD6/QzKdtrnCI5425fIUy7LtpXJGmpWDUA==","publicSettings":{"port":"3000"}}}]} # # #https://ostcextensions.blob.core.test-cint.azure-test.net/vhds/eg-plugin7-vm.eg-plugin7-vm.eg-plugin7-vm.status?sr=b&sp=rw& #se=9999-01-01&sk=key1&sv=2012-02-12&sig=wRUIDN1x2GC06FWaetBP9sjjifOWvRzS2y2XBB4qoBU%3D def __init__(self): self.reinitialize() def reinitialize(self): """ Reset members. """ self.Extensions = None self.Plugins = None self.Util = None def Parse(self, xmlText): """ Write configuration to file ExtensionsConfig.xml. Log plugin specific activity to /var/log/azure/.//CommandExecution.log. If state is enabled: if the plugin is installed: if the new plugin's version is higher if DisallowMajorVersionUpgrade is false or if true, the version is a minor version do upgrade: download the new archive do the updateCommand. disable the old plugin and remove enable the new plugin if the new plugin's version is the same or lower: create the new .settings file from the configuration received do the enableCommand if the plugin is not installed: download/unpack archive and call the installCommand/Enable if state is disabled: call disableCommand if state is uninstall: call uninstallCommand remove old plugin directory. """ self.reinitialize() self.Util=Util() dom = xml.dom.minidom.parseString(xmlText) LogIfVerbose(xmlText) self.plugin_log_dir='/var/log/azure' if not os.path.exists(self.plugin_log_dir): os.mkdir(self.plugin_log_dir) try: self.Extensions=dom.getElementsByTagName("Extensions") pg = dom.getElementsByTagName("Plugins") if len(pg) > 0: self.Plugins = pg[0].getElementsByTagName("Plugin") else: self.Plugins = [] incarnation=self.Extensions[0].getAttribute("goalStateIncarnation") SetFileContents('ExtensionsConfig.'+incarnation+'.xml', xmlText) except Exception, e: Error('ERROR: Error parsing ExtensionsConfig: {0}.'.format(e)) return None for p in self.Plugins: if len(p.getAttribute("location"))<1: # this plugin is inside the PluginSettings continue p.setAttribute('restricted','false') previous_version = None version=p.getAttribute("version") name=p.getAttribute("name") plog_dir=self.plugin_log_dir+'/'+name +'/'+ version if not os.path.exists(plog_dir): os.makedirs(plog_dir) p.plugin_log=plog_dir+'/CommandExecution.log' handler=name + '-' + version if p.getAttribute("isJson") != 'true': Error("Plugin " + name+" version: " +version+" is not a JSON Extension. Skipping.") continue Log("Found Plugin: " + name + ' version: ' + version) if p.getAttribute("state") == 'disabled' or p.getAttribute("state") == 'uninstall': #disable zip_dir=LibDir+"/" + name + '-' + version mfile=None for root, dirs, files in os.walk(zip_dir): for f in files: if f in ('HandlerManifest.json'): mfile=os.path.join(root,f) if mfile != None: break if mfile == None : Error('HandlerManifest.json not found.') continue manifest = GetFileContents(mfile) p.setAttribute('manifestdata',manifest) if self.launchCommand(p.plugin_log,name,version,'disableCommand') == None : self.SetHandlerState(handler, 'Enabled') Error('Unable to disable '+name) SimpleLog(p.plugin_log,'ERROR: Unable to disable '+name) else : self.SetHandlerState(handler, 'Disabled') Log(name+' is disabled') SimpleLog(p.plugin_log,name+' is disabled') # uninstall if needed if p.getAttribute("state") == 'uninstall': if self.launchCommand(p.plugin_log,name,version,'uninstallCommand') == None : self.SetHandlerState(handler, 'Installed') Error('Unable to uninstall '+name) SimpleLog(p.plugin_log,'Unable to uninstall '+name) else : self.SetHandlerState(handler, 'NotInstalled') Log(name+' uninstallCommand completed .') # remove the plugin Run('rm -rf ' + LibDir + '/' + name +'-'+ version + '*') Log(name +'-'+ version + ' extension files deleted.') SimpleLog(p.plugin_log,name +'-'+ version + ' extension files deleted.') continue # state is enabled # if the same plugin exists and the version is newer or # does not exist then download and unzip the new plugin plg_dir=None latest_version_installed = LooseVersion("0.0") for item in os.listdir(LibDir): itemPath = os.path.join(LibDir, item) if os.path.isdir(itemPath) and name in item: try: #Split plugin dir name with '-' to get intalled plugin name and version sperator = item.rfind('-') if sperator < 0: continue installed_plg_name = item[0:sperator] installed_plg_version = LooseVersion(item[sperator + 1:]) #Check installed plugin name and compare installed version to get the latest version installed if installed_plg_name == name and installed_plg_version > latest_version_installed: plg_dir = itemPath previous_version = str(installed_plg_version) latest_version_installed = installed_plg_version except Exception as e: Warn("Invalid plugin dir name: {0} {1}".format(item, e)) continue if plg_dir == None or LooseVersion(version) > LooseVersion(previous_version) : location=p.getAttribute("location") Log("Downloading plugin manifest: " + name + " from " + location) SimpleLog(p.plugin_log,"Downloading plugin manifest: " + name + " from " + location) self.Util.Endpoint=location.split('/')[2] Log("Plugin server is: " + self.Util.Endpoint) SimpleLog(p.plugin_log,"Plugin server is: " + self.Util.Endpoint) manifest=self.Util.HttpGetWithoutHeaders(location, chkProxy=True) if manifest == None: Error("Unable to download plugin manifest" + name + " from primary location. Attempting with failover location.") SimpleLog(p.plugin_log,"Unable to download plugin manifest" + name + " from primary location. Attempting with failover location.") failoverlocation=p.getAttribute("failoverlocation") self.Util.Endpoint=failoverlocation.split('/')[2] Log("Plugin failover server is: " + self.Util.Endpoint) SimpleLog(p.plugin_log,"Plugin failover server is: " + self.Util.Endpoint) manifest=self.Util.HttpGetWithoutHeaders(failoverlocation, chkProxy=True) #if failoverlocation also fail what to do then? if manifest == None: AddExtensionEvent(name,WALAEventOperation.Download,False,0,version,"Download mainfest fail "+failoverlocation) Log("Plugin manifest " + name + " downloading failed from failover location.") SimpleLog(p.plugin_log,"Plugin manifest " + name + " downloading failed from failover location.") filepath=LibDir+"/" + name + '.' + incarnation + '.manifest' if os.path.splitext(location)[-1] == '.xml' : #if this is an xml file we may have a BOM if ord(manifest[0]) > 128 and ord(manifest[1]) > 128 and ord(manifest[2]) > 128: manifest=manifest[3:] SetFileContents(filepath,manifest) #Get the bundle url from the manifest p.setAttribute('manifestdata',manifest) man_dom = xml.dom.minidom.parseString(manifest) bundle_uri = "" for mp in man_dom.getElementsByTagName("Plugin"): if GetNodeTextData(mp.getElementsByTagName("Version")[0]) == version: bundle_uri = GetNodeTextData(mp.getElementsByTagName("Uri")[0]) break if len(mp.getElementsByTagName("DisallowMajorVersionUpgrade")): if GetNodeTextData(mp.getElementsByTagName("DisallowMajorVersionUpgrade")[0]) == 'true' and previous_version !=None and previous_version.split('.')[0] != version.split('.')[0] : Log('DisallowMajorVersionUpgrade is true, this major version is restricted from upgrade.') SimpleLog(p.plugin_log,'DisallowMajorVersionUpgrade is true, this major version is restricted from upgrade.') p.setAttribute('restricted','true') continue if len(bundle_uri) < 1 : Error("Unable to fetch Bundle URI from manifest for " + name + " v " + version) SimpleLog(p.plugin_log,"Unable to fetch Bundle URI from manifest for " + name + " v " + version) continue Log("Bundle URI = " + bundle_uri) SimpleLog(p.plugin_log,"Bundle URI = " + bundle_uri) # Download the zipfile archive and save as '.zip' bundle=self.Util.HttpGetWithoutHeaders(bundle_uri, chkProxy=True) if bundle == None: AddExtensionEvent(name,WALAEventOperation.Download,True,0,version,"Download zip fail "+bundle_uri) Error("Unable to download plugin bundle" + bundle_uri ) SimpleLog(p.plugin_log,"Unable to download plugin bundle" + bundle_uri ) continue AddExtensionEvent(name,WALAEventOperation.Download,True,0,version,"Download Success") b=bytearray(bundle) filepath=LibDir+"/" + os.path.basename(bundle_uri) + '.zip' SetFileContents(filepath,b) Log("Plugin bundle" + bundle_uri + "downloaded successfully length = " + str(len(bundle))) SimpleLog(p.plugin_log,"Plugin bundle" + bundle_uri + "downloaded successfully length = " + str(len(bundle))) # unpack the archive z=zipfile.ZipFile(filepath) zip_dir=LibDir+"/" + name + '-' + version z.extractall(zip_dir) Log('Extracted ' + bundle_uri + ' to ' + zip_dir) SimpleLog(p.plugin_log,'Extracted ' + bundle_uri + ' to ' + zip_dir) # zip no file perms in .zip so set all the scripts to +x Run( "find " + zip_dir +" -type f | xargs chmod u+x ") #write out the base64 config data so the plugin can process it. mfile=None for root, dirs, files in os.walk(zip_dir): for f in files: if f in ('HandlerManifest.json'): mfile=os.path.join(root,f) if mfile != None: break if mfile == None : Error('HandlerManifest.json not found.') SimpleLog(p.plugin_log,'HandlerManifest.json not found.') continue manifest = GetFileContents(mfile) p.setAttribute('manifestdata',manifest) # create the status and config dirs Run('mkdir -p ' + root + '/status') Run('mkdir -p ' + root + '/config') # write out the configuration data to goalStateIncarnation.settings file in the config path. config='' seqNo='0' if len(dom.getElementsByTagName("PluginSettings")) != 0 : pslist=dom.getElementsByTagName("PluginSettings")[0].getElementsByTagName("Plugin") for ps in pslist: if name == ps.getAttribute("name") and version == ps.getAttribute("version"): Log("Found RuntimeSettings for " + name + " V " + version) SimpleLog(p.plugin_log,"Found RuntimeSettings for " + name + " V " + version) config=GetNodeTextData(ps.getElementsByTagName("RuntimeSettings")[0]) seqNo=ps.getElementsByTagName("RuntimeSettings")[0].getAttribute("seqNo") break if config == '': Log("No RuntimeSettings for " + name + " V " + version) SimpleLog(p.plugin_log,"No RuntimeSettings for " + name + " V " + version) SetFileContents(root +"/config/" + seqNo +".settings", config ) #create HandlerEnvironment.json handler_env='[{ "name": "'+name+'", "seqNo": "'+seqNo+'", "version": 1.0, "handlerEnvironment": { "logFolder": "'+os.path.dirname(p.plugin_log)+'", "configFolder": "' + root + '/config", "statusFolder": "' + root + '/status", "heartbeatFile": "'+ root + '/heartbeat.log"}}]' SetFileContents(root+'/HandlerEnvironment.json',handler_env) self.SetHandlerState(handler, 'NotInstalled') cmd = '' getcmd='installCommand' if plg_dir != None and previous_version != None and LooseVersion(version) > LooseVersion(previous_version): previous_handler=name+'-'+previous_version if self.GetHandlerState(previous_handler) != 'NotInstalled': getcmd='updateCommand' # disable the old plugin if it exists if self.launchCommand(p.plugin_log,name,previous_version,'disableCommand') == None : self.SetHandlerState(previous_handler, 'Enabled') Error('Unable to disable old plugin '+name+' version ' + previous_version) SimpleLog(p.plugin_log,'Unable to disable old plugin '+name+' version ' + previous_version) else : self.SetHandlerState(previous_handler, 'Disabled') Log(name+' version ' + previous_version + ' is disabled') SimpleLog(p.plugin_log,name+' version ' + previous_version + ' is disabled') try: Log("Copy status file from old plugin dir to new") old_plg_dir = plg_dir new_plg_dir = os.path.join(LibDir, "{0}-{1}".format(name, version)) old_ext_status_dir = os.path.join(old_plg_dir, "status") new_ext_status_dir = os.path.join(new_plg_dir, "status") if os.path.isdir(old_ext_status_dir): for status_file in os.listdir(old_ext_status_dir): status_file_path = os.path.join(old_ext_status_dir, status_file) if os.path.isfile(status_file_path): shutil.copy2(status_file_path, new_ext_status_dir) mrseq_file = os.path.join(old_plg_dir, "mrseq") if os.path.isfile(mrseq_file): shutil.copy(mrseq_file, new_plg_dir) except Exception as e: Error("Failed to copy status file.") isupgradeSuccess = True if getcmd=='updateCommand': if self.launchCommand(p.plugin_log,name,version,getcmd,previous_version) == None : Error('Update failed for '+name+'-'+version) SimpleLog(p.plugin_log,'Update failed for '+name+'-'+version) isupgradeSuccess=False else : Log('Update complete'+name+'-'+version) SimpleLog(p.plugin_log,'Update complete'+name+'-'+version) # if we updated - call unistall for the old plugin if self.launchCommand(p.plugin_log,name,previous_version,'uninstallCommand') == None : self.SetHandlerState(previous_handler, 'Installed') Error('Uninstall failed for '+name+'-'+previous_version) SimpleLog(p.plugin_log,'Uninstall failed for '+name+'-'+previous_version) isupgradeSuccess=False else : self.SetHandlerState(previous_handler, 'NotInstalled') Log('Uninstall complete'+ previous_handler ) SimpleLog(p.plugin_log,'Uninstall complete'+ name +'-' + previous_version) try: #rm old plugin dir if os.path.isdir(plg_dir): shutil.rmtree(plg_dir) Log(name +'-'+ previous_version + ' extension files deleted.') SimpleLog(p.plugin_log,name +'-'+ previous_version + ' extension files deleted.') except Exception as e: Error("Failed to remove old plugin directory") AddExtensionEvent(name,WALAEventOperation.Upgrade,isupgradeSuccess,0,previous_version) else : # run install if self.launchCommand(p.plugin_log,name,version,getcmd) == None : self.SetHandlerState(handler, 'NotInstalled') Error('Installation failed for '+name+'-'+version) SimpleLog(p.plugin_log,'Installation failed for '+name+'-'+version) else : self.SetHandlerState(handler, 'Installed') Log('Installation completed for '+name+'-'+version) SimpleLog(p.plugin_log,'Installation completed for '+name+'-'+version) #end if plg_dir == none or version > = prev # change incarnation of settings file so it knows how to name status... zip_dir=LibDir+"/" + name + '-' + version mfile=None for root, dirs, files in os.walk(zip_dir): for f in files: if f in ('HandlerManifest.json'): mfile=os.path.join(root,f) if mfile != None: break if mfile == None : Error('HandlerManifest.json not found.') SimpleLog(p.plugin_log,'HandlerManifest.json not found.') continue manifest = GetFileContents(mfile) p.setAttribute('manifestdata',manifest) config='' seqNo='0' if len(dom.getElementsByTagName("PluginSettings")) != 0 : try: pslist=dom.getElementsByTagName("PluginSettings")[0].getElementsByTagName("Plugin") except: Error('Error parsing ExtensionsConfig.') SimpleLog(p.plugin_log,'Error parsing ExtensionsConfig.') continue for ps in pslist: if name == ps.getAttribute("name") and version == ps.getAttribute("version"): Log("Found RuntimeSettings for " + name + " V " + version) SimpleLog(p.plugin_log,"Found RuntimeSettings for " + name + " V " + version) config=GetNodeTextData(ps.getElementsByTagName("RuntimeSettings")[0]) seqNo=ps.getElementsByTagName("RuntimeSettings")[0].getAttribute("seqNo") break if config == '': Error("No RuntimeSettings for " + name + " V " + version) SimpleLog(p.plugin_log,"No RuntimeSettings for " + name + " V " + version) SetFileContents(root +"/config/" + seqNo +".settings", config ) # state is still enable if (self.GetHandlerState(handler) == 'NotInstalled'): # run install first if true if self.launchCommand(p.plugin_log,name,version,'installCommand') == None : self.SetHandlerState(handler, 'NotInstalled') Error('Installation failed for '+name+'-'+version) SimpleLog(p.plugin_log,'Installation failed for '+name+'-'+version) else : self.SetHandlerState(handler, 'Installed') Log('Installation completed for '+name+'-'+version) SimpleLog(p.plugin_log,'Installation completed for '+name+'-'+version) if (self.GetHandlerState(handler) != 'NotInstalled'): if self.launchCommand(p.plugin_log,name,version,'enableCommand') == None : self.SetHandlerState(handler, 'Installed') Error('Enable failed for '+name+'-'+version) SimpleLog(p.plugin_log,'Enable failed for '+name+'-'+version) else : self.SetHandlerState(handler, 'Enabled') Log('Enable completed for '+name+'-'+version) SimpleLog(p.plugin_log,'Enable completed for '+name+'-'+version) # this plugin processing is complete Log('Processing completed for '+name+'-'+version) SimpleLog(p.plugin_log,'Processing completed for '+name+'-'+version) #end plugin processing loop Log('Finished processing ExtensionsConfig.xml') try: SimpleLog(p.plugin_log,'Finished processing ExtensionsConfig.xml') except: pass return self def launchCommand(self,plugin_log,name,version,command,prev_version=None): commandToEventOperation={ "installCommand":WALAEventOperation.Install, "uninstallCommand":WALAEventOperation.UnIsntall, "updateCommand": WALAEventOperation.Upgrade, "enableCommand": WALAEventOperation.Enable, "disableCommand": WALAEventOperation.Disable, } isSuccess=True start = datetime.datetime.now() r=self.__launchCommandWithoutEventLog(plugin_log,name,version,command,prev_version) if r==None: isSuccess=False Duration = int((datetime.datetime.now() - start).seconds) if commandToEventOperation.get(command): AddExtensionEvent(name,commandToEventOperation[command],isSuccess,Duration,version) return r def __launchCommandWithoutEventLog(self,plugin_log,name,version,command,prev_version=None): # get the manifest and read the command mfile=None zip_dir=LibDir+"/" + name + '-' + version for root, dirs, files in os.walk(zip_dir): for f in files: if f in ('HandlerManifest.json'): mfile=os.path.join(root,f) if mfile != None: break if mfile == None : Error('HandlerManifest.json not found.') SimpleLog(plugin_log,'HandlerManifest.json not found.') return None manifest = GetFileContents(mfile) try: jsn = json.loads(manifest) except: Error('Error parsing HandlerManifest.json.') SimpleLog(plugin_log,'Error parsing HandlerManifest.json.') return None if type(jsn)==list: jsn=jsn[0] if jsn.has_key('handlerManifest') : cmd = jsn['handlerManifest'][command] else : Error('Key handlerManifest not found. Handler cannot be installed.') SimpleLog(plugin_log,'Key handlerManifest not found. Handler cannot be installed.') if len(cmd) == 0 : Error('Unable to read ' + command ) SimpleLog(plugin_log,'Unable to read ' + command ) return None # for update we send the path of the old installation arg='' if prev_version != None : arg=' ' + LibDir+'/' + name + '-' + prev_version dirpath=os.path.dirname(mfile) LogIfVerbose('Command is '+ dirpath+'/'+ cmd) # launch pid=None try: child = subprocess.Popen(dirpath+'/'+cmd+arg,shell=True,cwd=dirpath,stdout=subprocess.PIPE) except Exception as e: Error('Exception launching ' + cmd + str(e)) SimpleLog(plugin_log,'Exception launching ' + cmd + str(e)) pid = child.pid if pid == None or pid < 1 : ExtensionChildren.append((-1,root)) Error('Error launching ' + cmd + '.') SimpleLog(plugin_log,'Error launching ' + cmd + '.') else : ExtensionChildren.append((pid,root)) Log("Spawned "+ cmd + " PID " + str(pid)) SimpleLog(plugin_log,"Spawned "+ cmd + " PID " + str(pid)) # wait until install/upgrade is finished timeout = 300 # 5 minutes retry = timeout/5 while retry > 0 and child.poll() == None: LogIfVerbose(cmd + ' still running with PID ' + str(pid)) time.sleep(5) retry-=1 if retry==0: Error('Process exceeded timeout of ' + str(timeout) + ' seconds. Terminating process ' + str(pid)) SimpleLog(plugin_log,'Process exceeded timeout of ' + str(timeout) + ' seconds. Terminating process ' + str(pid)) os.kill(pid,9) return None code = child.wait() if code == None or code != 0: Error('Process ' + str(pid) + ' returned non-zero exit code (' + str(code) + ')') SimpleLog(plugin_log,'Process ' + str(pid) + ' returned non-zero exit code (' + str(code) + ')') return None Log(command + ' completed.') SimpleLog(plugin_log,command + ' completed.') return 0 def ReportHandlerStatus(self): """ Collect all status reports. """ # { "version": "1.0", "timestampUTC": "2014-03-31T21:28:58Z", # "aggregateStatus": { # "guestAgentStatus": { "version": "2.0.4PRE", "status": "Ready", "formattedMessage": { "lang": "en-US", "message": "GuestAgent is running and accepting new configurations." } }, # "handlerAggregateStatus": [{ # "handlerName": "ExampleHandlerLinux", "handlerVersion": "1.0", "status": "Ready", "runtimeSettingsStatus": { # "sequenceNumber": "2", "settingsStatus": { "timestampUTC": "2014-03-31T23:46:00Z", "status": { "name": "ExampleHandlerLinux", "operation": "Command Execution Finished", "configurationAppliedTime": "2014-03-31T23:46:00Z", "status": "success", "formattedMessage": { "lang": "en-US", "message": "Finished executing command" }, # "substatus": [ # { "name": "StdOut", "status": "success", "formattedMessage": { "lang": "en-US", "message": "Goodbye world!" } }, # { "name": "StdErr", "status": "success", "formattedMessage": { "lang": "en-US", "message": "" } } # ] # } } } } # ] # }} try: incarnation=self.Extensions[0].getAttribute("goalStateIncarnation") except: Error('Error parsing attribute "goalStateIncarnation". Unable to send status reports') return -1 status='' statuses='' for p in self.Plugins: if p.getAttribute("state") == 'uninstall' or p.getAttribute("restricted") == 'true' : continue version=p.getAttribute("version") name=p.getAttribute("name") if p.getAttribute("isJson") != 'true': LogIfVerbose("Plugin " + name+" version: " +version+" is not a JSON Extension. Skipping.") continue reportHeartbeat = False if len(p.getAttribute("manifestdata"))<1: Error("Failed to get manifestdata.") else: reportHeartbeat = json.loads(p.getAttribute("manifestdata"))[0]['handlerManifest']['reportHeartbeat'] if len(statuses)>0: statuses+=',' statuses+=self.GenerateAggStatus(name, version, reportHeartbeat) tstamp=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) #header #agent state if provisioned == False: if provisionError == None : agent_state='Provisioning' agent_msg='Guest Agent is starting.' else: agent_state='Provisioning Error.' agent_msg=provisionError else: agent_state='Ready' agent_msg='GuestAgent is running and accepting new configurations.' status='{"version":"1.0","timestampUTC":"'+tstamp+'","aggregateStatus":{"guestAgentStatus":{"version":"'+GuestAgentVersion+'","status":"'+agent_state+'","formattedMessage":{"lang":"en-US","message":"'+agent_msg+'"}},"handlerAggregateStatus":['+statuses+']}}' try: uri=GetNodeTextData(self.Extensions[0].getElementsByTagName("StatusUploadBlob")[0]).replace('&','&') except: Error('Error parsing element "StatusUploadBlob". Unable to send status reports') return -1 LogIfVerbose('Status report '+status+' sent to ' + uri) return UploadStatusBlob(uri, status.encode("utf-8")) def GetCurrentSequenceNumber(self, plugin_base_dir): """ Get the settings file with biggest file number in config folder """ config_dir = os.path.join(plugin_base_dir, 'config') seq_no = 0 for subdir, dirs, files in os.walk(config_dir): for file in files: try: cur_seq_no = int(os.path.basename(file).split('.')[0]) if cur_seq_no > seq_no: seq_no = cur_seq_no except ValueError: continue return str(seq_no) def GenerateAggStatus(self, name, version, reportHeartbeat = False): """ Generate the status which Azure can understand by the status and heartbeat reported by extension """ plugin_base_dir = LibDir+'/'+name+'-'+version+'/' current_seq_no = self.GetCurrentSequenceNumber(plugin_base_dir) status_file=os.path.join(plugin_base_dir, 'status/', current_seq_no +'.status') heartbeat_file = os.path.join(plugin_base_dir, 'heartbeat.log') handler_state_file = os.path.join(plugin_base_dir, 'config', 'HandlerState') agg_state = 'NotReady' handler_state = None status_obj = None status_code = None formatted_message = None localized_message = None if os.path.exists(handler_state_file): handler_state = GetFileContents(handler_state_file).lower() if HandlerStatusToAggStatus.has_key(handler_state): agg_state = HandlerStatusToAggStatus[handler_state] if reportHeartbeat: if os.path.exists(heartbeat_file): d=int(time.time()-os.stat(heartbeat_file).st_mtime) if d > 600 : # not updated for more than 10 min agg_state = 'Unresponsive' else: try: heartbeat = json.loads(GetFileContents(heartbeat_file))[0]["heartbeat"] agg_state = heartbeat.get("status") status_code = heartbeat.get("code") formatted_message = heartbeat.get("formattedMessage") localized_message = heartbeat.get("message") except: Error("Incorrect heartbeat file. Ignore it. ") else: agg_state = 'Unresponsive' #get status file reported by extension if os.path.exists(status_file): # raw status generated by extension is an array, get the first item and remove the unnecessary element try: status_obj = json.loads(GetFileContents(status_file))[0] del status_obj["version"] except: Error("Incorrect status file. Will NOT settingsStatus in settings. ") agg_status_obj = {"handlerName": name, "handlerVersion": version, "status": agg_state, "runtimeSettingsStatus" : {"sequenceNumber": current_seq_no}} if status_obj: agg_status_obj["runtimeSettingsStatus"]["settingsStatus"] = status_obj if status_code != None: agg_status_obj["code"] = status_code if formatted_message: agg_status_obj["formattedMessage"] = formatted_message if localized_message: agg_status_obj["message"] = localized_message agg_status_string = json.dumps(agg_status_obj) LogIfVerbose("Handler Aggregated Status:" + agg_status_string) return agg_status_string def SetHandlerState(self, handler, state=''): zip_dir=LibDir+"/" + handler mfile=None for root, dirs, files in os.walk(zip_dir): for f in files: if f in ('HandlerManifest.json'): mfile=os.path.join(root,f) if mfile != None: break if mfile == None : Error('SetHandlerState(): HandlerManifest.json not found, cannot set HandlerState.') return None Log("SetHandlerState: "+handler+", "+state) return SetFileContents(os.path.dirname(mfile)+'/config/HandlerState', state) def GetHandlerState(self, handler): handlerState = GetFileContents(handler+'/config/HandlerState') if (handlerState): return handlerState.rstrip('\r\n') else: return 'NotInstalled' class HostingEnvironmentConfig(object): """ Parse Hosting enviromnet config and store in HostingEnvironmentConfig.xml """ # # # # # # # # # # # # # # # # # # # # # # # # # # def __init__(self): self.reinitialize() def reinitialize(self): """ Reset Members. """ self.StoredCertificates = None self.Deployment = None self.Incarnation = None self.Role = None self.HostingEnvironmentSettings = None self.ApplicationSettings = None self.Certificates = None self.ResourceReferences = None def Parse(self, xmlText): """ Parse and create HostingEnvironmentConfig.xml. """ self.reinitialize() SetFileContents("HostingEnvironmentConfig.xml", xmlText) dom = xml.dom.minidom.parseString(xmlText) for a in [ "HostingEnvironmentConfig", "Deployment", "Service", "ServiceInstance", "Incarnation", "Role", ]: if not dom.getElementsByTagName(a): Error("HostingEnvironmentConfig.Parse: Missing " + a) return None node = dom.childNodes[0] if node.localName != "HostingEnvironmentConfig": Error("HostingEnvironmentConfig.Parse: root not HostingEnvironmentConfig") return None self.ApplicationSettings = dom.getElementsByTagName("Setting") self.Certificates = dom.getElementsByTagName("StoredCertificate") return self def DecryptPassword(self, e): """ Return decrypted password. """ SetFileContents("password.p7m", "MIME-Version: 1.0\n" + "Content-Disposition: attachment; filename=\"password.p7m\"\n" + "Content-Type: application/x-pkcs7-mime; name=\"password.p7m\"\n" + "Content-Transfer-Encoding: base64\n\n" + textwrap.fill(e, 64)) return RunGetOutput(Openssl + " cms -decrypt -in password.p7m -inkey Certificates.pem -recip Certificates.pem")[1] def ActivateResourceDisk(self): return MyDistro.ActivateResourceDisk() def Process(self): """ Execute ActivateResourceDisk in separate thread. Create the user account. Launch ConfigurationConsumer if specified in the config. """ no_thread = False if DiskActivated == False: for m in inspect.getmembers(MyDistro): if 'ActivateResourceDiskNoThread' in m: no_thread = True break if no_thread == True : MyDistro.ActivateResourceDiskNoThread() else : diskThread = threading.Thread(target = self.ActivateResourceDisk) diskThread.start() User = None Pass = None Expiration = None Thumbprint = None for b in self.ApplicationSettings: sname = b.getAttribute("name") svalue = b.getAttribute("value") if User != None and Pass != None: if User != "root" and User != "" and Pass != "": CreateAccount(User, Pass, Expiration, Thumbprint) else: Error("Not creating user account: " + User) for c in self.Certificates: csha1 = c.getAttribute("certificateId").split(':')[1].upper() if os.path.isfile(csha1 + ".prv"): Log("Private key with thumbprint: " + csha1 + " was retrieved.") if os.path.isfile(csha1 + ".crt"): Log("Public cert with thumbprint: " + csha1 + " was retrieved.") program = Config.get("Role.ConfigurationConsumer") if program != None: try: Children.append(subprocess.Popen([program, LibDir + "/HostingEnvironmentConfig.xml"])) except OSError, e : ErrorWithPrefix('HostingEnvironmentConfig.Process','Exception: '+ str(e) +' occured launching ' + program ) class GoalState(Util): """ Primary container for all configuration except OvfXml. Encapsulates http communication with endpoint server. Initializes and populates: self.HostingEnvironmentConfig self.SharedConfig self.ExtensionsConfig self.Certificates """ # # # 2010-12-15 # 1 # # Started # # 16001 # # # # c6d5526c-5ac2-4200-b6e2-56f2b70c5ab2 # # # MachineRole_IN_0 # Started # # http://10.115.153.40:80/machine/c6d5526c-5ac2-4200-b6e2-56f2b70c5ab2/MachineRole%5FIN%5F0?comp=config&type=hostingEnvironmentConfig&incarnation=1 # http://10.115.153.40:80/machine/c6d5526c-5ac2-4200-b6e2-56f2b70c5ab2/MachineRole%5FIN%5F0?comp=config&type=sharedConfig&incarnation=1 # http://10.115.153.40:80/machine/c6d5526c-5ac2-4200-b6e2-56f2b70c5ab2/MachineRole%5FIN%5F0?comp=certificates&incarnation=1 # http://100.67.238.230:80/machine/9c87aa94-3bda-45e3-b2b7-0eb0fca7baff/1552dd64dc254e6884f8d5b8b68aa18f.eg%2Dplug%2Dvm?comp=config&type=extensionsConfig&incarnation=2 # http://100.67.238.230:80/machine/9c87aa94-3bda-45e3-b2b7-0eb0fca7baff/1552dd64dc254e6884f8d5b8b68aa18f.eg%2Dplug%2Dvm?comp=config&type=fullConfig&incarnation=2 # # # # # # # There is only one Role for VM images. # # Of primary interest is: # LBProbePorts -- an http server needs to run here # We also note Container/ContainerID and RoleInstance/InstanceId to form the health report. # And of course, Incarnation # def __init__(self, Agent): self.Agent = Agent self.Endpoint = Agent.Endpoint self.TransportCert = Agent.TransportCert self.reinitialize() def reinitialize(self): self.Incarnation = None # integer self.ExpectedState = None # "Started" self.HostingEnvironmentConfigUrl = None self.HostingEnvironmentConfigXml = None self.HostingEnvironmentConfig = None self.SharedConfigUrl = None self.SharedConfigXml = None self.SharedConfig = None self.CertificatesUrl = None self.CertificatesXml = None self.Certificates = None self.ExtensionsConfigUrl = None self.ExtensionsConfigXml = None self.ExtensionsConfig = None self.RoleInstanceId = None self.ContainerId = None self.LoadBalancerProbePort = None # integer, ?list of integers def Parse(self, xmlText): """ Request configuration data from endpoint server. Parse and populate contained configuration objects. Calls Certificates().Parse() Calls SharedConfig().Parse Calls ExtensionsConfig().Parse Calls HostingEnvironmentConfig().Parse """ self.reinitialize() LogIfVerbose(xmlText) node = xml.dom.minidom.parseString(xmlText).childNodes[0] if node.localName != "GoalState": Error("GoalState.Parse: root not GoalState") return None for a in node.childNodes: if a.nodeType == node.ELEMENT_NODE: if a.localName == "Incarnation": self.Incarnation = GetNodeTextData(a) elif a.localName == "Machine": for b in a.childNodes: if b.nodeType == node.ELEMENT_NODE: if b.localName == "ExpectedState": self.ExpectedState = GetNodeTextData(b) Log("ExpectedState: " + self.ExpectedState) elif b.localName == "LBProbePorts": for c in b.childNodes: if c.nodeType == node.ELEMENT_NODE and c.localName == "Port": self.LoadBalancerProbePort = int(GetNodeTextData(c)) elif a.localName == "Container": for b in a.childNodes: if b.nodeType == node.ELEMENT_NODE: if b.localName == "ContainerId": self.ContainerId = GetNodeTextData(b) Log("ContainerId: " + self.ContainerId) elif b.localName == "RoleInstanceList": for c in b.childNodes: if c.localName == "RoleInstance": for d in c.childNodes: if d.nodeType == node.ELEMENT_NODE: if d.localName == "InstanceId": self.RoleInstanceId = GetNodeTextData(d) Log("RoleInstanceId: " + self.RoleInstanceId) elif d.localName == "State": pass elif d.localName == "Configuration": for e in d.childNodes: if e.nodeType == node.ELEMENT_NODE: LogIfVerbose(e.localName) if e.localName == "HostingEnvironmentConfig": self.HostingEnvironmentConfigUrl = GetNodeTextData(e) LogIfVerbose("HostingEnvironmentConfigUrl:" + self.HostingEnvironmentConfigUrl) self.HostingEnvironmentConfigXml = self.HttpGetWithHeaders(self.HostingEnvironmentConfigUrl) self.HostingEnvironmentConfig = HostingEnvironmentConfig().Parse(self.HostingEnvironmentConfigXml) elif e.localName == "SharedConfig": self.SharedConfigUrl = GetNodeTextData(e) LogIfVerbose("SharedConfigUrl:" + self.SharedConfigUrl) self.SharedConfigXml = self.HttpGetWithHeaders(self.SharedConfigUrl) self.SharedConfig = SharedConfig().Parse(self.SharedConfigXml) self.SharedConfig.Save() elif e.localName == "ExtensionsConfig": self.ExtensionsConfigUrl = GetNodeTextData(e) LogIfVerbose("ExtensionsConfigUrl:" + self.ExtensionsConfigUrl) self.ExtensionsConfigXml = self.HttpGetWithHeaders(self.ExtensionsConfigUrl) elif e.localName == "Certificates": self.CertificatesUrl = GetNodeTextData(e) LogIfVerbose("CertificatesUrl:" + self.CertificatesUrl) self.CertificatesXml = self.HttpSecureGetWithHeaders(self.CertificatesUrl, self.TransportCert) self.Certificates = Certificates().Parse(self.CertificatesXml) if self.Incarnation == None: Error("GoalState.Parse: Incarnation missing") return None if self.ExpectedState == None: Error("GoalState.Parse: ExpectedState missing") return None if self.RoleInstanceId == None: Error("GoalState.Parse: RoleInstanceId missing") return None if self.ContainerId == None: Error("GoalState.Parse: ContainerId missing") return None SetFileContents("GoalState." + self.Incarnation + ".xml", xmlText) return self def Process(self): """ Calls HostingEnvironmentConfig.Process() """ LogIfVerbose("Process goalstate") self.HostingEnvironmentConfig.Process() self.SharedConfig.Process() class OvfEnv(object): """ Read, and process provisioning info from provisioning file OvfEnv.xml """ # # # # # 1.0 # # LinuxProvisioningConfiguration # HostName # UserName # UserPassword # false # # # # EB0C0AB4B2D5FC35F2F0658D19F44C8283E2DD62 # $HOME/UserName/.ssh/authorized_keys # # # # # EB0C0AB4B2D5FC35F2F0658D19F44C8283E2DD62 # $HOME/UserName/.ssh/id_rsa # # # # # # # def __init__(self): self.reinitialize() def reinitialize(self): """ Reset members. """ self.WaNs = "http://schemas.microsoft.com/windowsazure" self.OvfNs = "http://schemas.dmtf.org/ovf/environment/1" self.MajorVersion = 1 self.MinorVersion = 0 self.ComputerName = None self.AdminPassword = None self.UserName = None self.UserPassword = None self.CustomData = None self.DisableSshPasswordAuthentication = True self.SshPublicKeys = [] self.SshKeyPairs = [] def Parse(self, xmlText, isDeprovision = False): """ Parse xml tree, retreiving user and ssh key information. Return self. """ self.reinitialize() LogIfVerbose(re.sub(".*?<", "*<", xmlText)) dom = xml.dom.minidom.parseString(xmlText) if len(dom.getElementsByTagNameNS(self.OvfNs, "Environment")) != 1: Error("Unable to parse OVF XML.") section = None newer = False for p in dom.getElementsByTagNameNS(self.WaNs, "ProvisioningSection"): for n in p.childNodes: if n.localName == "Version": verparts = GetNodeTextData(n).split('.') major = int(verparts[0]) minor = int(verparts[1]) if major > self.MajorVersion: newer = True if major != self.MajorVersion: break if minor > self.MinorVersion: newer = True section = p if newer == True: Warn("Newer provisioning configuration detected. Please consider updating waagent.") if section == None: Error("Could not find ProvisioningSection with major version=" + str(self.MajorVersion)) return None self.ComputerName = GetNodeTextData(section.getElementsByTagNameNS(self.WaNs, "HostName")[0]) self.UserName = GetNodeTextData(section.getElementsByTagNameNS(self.WaNs, "UserName")[0]) if isDeprovision == True: return self try: self.UserPassword = GetNodeTextData(section.getElementsByTagNameNS(self.WaNs, "UserPassword")[0]) except: pass CDSection=None try: CDSection=section.getElementsByTagNameNS(self.WaNs, "CustomData") if len(CDSection) > 0 : self.CustomData=GetNodeTextData(CDSection[0]) if len(self.CustomData)>0: SetFileContents(LibDir + '/CustomData', bytearray(MyDistro.translateCustomData(self.CustomData), 'utf-8')) Log('Wrote ' + LibDir + '/CustomData') else : Error(' contains no data!') except Exception, e: Error( str(e)+' occured creating ' + LibDir + '/CustomData') disableSshPass = section.getElementsByTagNameNS(self.WaNs, "DisableSshPasswordAuthentication") if len(disableSshPass) != 0: self.DisableSshPasswordAuthentication = (GetNodeTextData(disableSshPass[0]).lower() == "true") for pkey in section.getElementsByTagNameNS(self.WaNs, "PublicKey"): LogIfVerbose(repr(pkey)) fp = None path = None for c in pkey.childNodes: if c.localName == "Fingerprint": fp = GetNodeTextData(c).upper() LogIfVerbose(fp) if c.localName == "Path": path = GetNodeTextData(c) LogIfVerbose(path) self.SshPublicKeys += [[fp, path]] for keyp in section.getElementsByTagNameNS(self.WaNs, "KeyPair"): fp = None path = None LogIfVerbose(repr(keyp)) for c in keyp.childNodes: if c.localName == "Fingerprint": fp = GetNodeTextData(c).upper() LogIfVerbose(fp) if c.localName == "Path": path = GetNodeTextData(c) LogIfVerbose(path) self.SshKeyPairs += [[fp, path]] return self def PrepareDir(self, filepath): """ Create home dir for self.UserName Change owner and return path. """ home = MyDistro.GetHome() # Expand HOME variable if present in path path = os.path.normpath(filepath.replace("$HOME", home)) if (path.startswith("/") == False) or (path.endswith("/") == True): return None dir = path.rsplit('/', 1)[0] if dir != "": CreateDir(dir, "root", 0700) if path.startswith(os.path.normpath(home + "/" + self.UserName + "/")): ChangeOwner(dir, self.UserName) return path def NumberToBytes(self, i): """ Pack number into bytes. Retun as string. """ result = [] while i: result.append(chr(i & 0xFF)) i >>= 8 result.reverse() return ''.join(result) def BitsToString(self, a): """ Return string representation of bits in a. """ index=7 s = "" c = 0 for bit in a: c = c | (bit << index) index = index - 1 if index == -1: s = s + struct.pack('>B', c) c = 0 index = 7 return s def OpensslToSsh(self, file): """ Return base-64 encoded key appropriate for ssh. """ from pyasn1.codec.der import decoder as der_decoder try: f = open(file).read().replace('\n','').split("KEY-----")[1].split('-')[0] k=der_decoder.decode(self.BitsToString(der_decoder.decode(base64.b64decode(f))[0][1]))[0] n=k[0] e=k[1] keydata="" keydata += struct.pack('>I',len("ssh-rsa")) keydata += "ssh-rsa" keydata += struct.pack('>I',len(self.NumberToBytes(e))) keydata += self.NumberToBytes(e) keydata += struct.pack('>I',len(self.NumberToBytes(n)) + 1) keydata += "\0" keydata += self.NumberToBytes(n) except Exception, e: print("OpensslToSsh: Exception " + str(e)) return None return "ssh-rsa " + base64.b64encode(keydata) + "\n" def Process(self): """ Process all certificate and key info. DisableSshPasswordAuthentication if configured. CreateAccount(user) Wait for WaAgent.EnvMonitor.IsHostnamePublished(). Restart ssh service. """ error = None if self.ComputerName == None : return "Error: Hostname missing" error=WaAgent.EnvMonitor.SetHostName(self.ComputerName) if error: return error if self.DisableSshPasswordAuthentication: filepath = "/etc/ssh/sshd_config" # Disable RFC 4252 and RFC 4256 authentication schemes. ReplaceFileContentsAtomic(filepath, "\n".join(filter(lambda a: not (a.startswith("PasswordAuthentication") or a.startswith("ChallengeResponseAuthentication")), GetFileContents(filepath).split('\n'))) + "\nPasswordAuthentication no\nChallengeResponseAuthentication no\n") Log("Disabled SSH password-based authentication methods.") if self.AdminPassword != None: MyDistro.changePass('root',self.AdminPassword) if self.UserName != None: error = MyDistro.CreateAccount(self.UserName, self.UserPassword, None, None) sel = MyDistro.isSelinuxRunning() if sel : MyDistro.setSelinuxEnforce(0) home = MyDistro.GetHome() for pkey in self.SshPublicKeys: Log("Deploy public key:{0}".format(pkey[0])) if not os.path.isfile(pkey[0] + ".crt"): Error("PublicKey not found: " + pkey[0]) error = "Failed to deploy public key (0x09)." continue path = self.PrepareDir(pkey[1]) if path == None: Error("Invalid path: " + pkey[1] + " for PublicKey: " + pkey[0]) error = "Invalid path for public key (0x03)." continue Run(Openssl + " x509 -in " + pkey[0] + ".crt -noout -pubkey > " + pkey[0] + ".pub") MyDistro.setSelinuxContext(pkey[0] + '.pub','unconfined_u:object_r:ssh_home_t:s0') MyDistro.sshDeployPublicKey(pkey[0] + '.pub',path) MyDistro.setSelinuxContext(path,'unconfined_u:object_r:ssh_home_t:s0') if path.startswith(os.path.normpath(home + "/" + self.UserName + "/")): ChangeOwner(path, self.UserName) for keyp in self.SshKeyPairs: Log("Deploy key pair:{0}".format(keyp[0])) if not os.path.isfile(keyp[0] + ".prv"): Error("KeyPair not found: " + keyp[0]) error = "Failed to deploy key pair (0x0A)." continue path = self.PrepareDir(keyp[1]) if path == None: Error("Invalid path: " + keyp[1] + " for KeyPair: " + keyp[0]) error = "Invalid path for key pair (0x05)." continue SetFileContents(path, GetFileContents(keyp[0] + ".prv")) os.chmod(path, 0600) Run("ssh-keygen -y -f " + keyp[0] + ".prv > " + path + ".pub") MyDistro.setSelinuxContext(path,'unconfined_u:object_r:ssh_home_t:s0') MyDistro.setSelinuxContext(path + '.pub','unconfined_u:object_r:ssh_home_t:s0') if path.startswith(os.path.normpath(home + "/" + self.UserName + "/")): ChangeOwner(path, self.UserName) ChangeOwner(path + ".pub", self.UserName) if sel : MyDistro.setSelinuxEnforce(1) while not WaAgent.EnvMonitor.IsHostnamePublished(): time.sleep(1) MyDistro.restartSshService() return error class WALAEvent(object): def __init__(self): self.providerId="" self.eventId=1 self.OpcodeName="" self.KeywordName="" self.TaskName="" self.TenantName="" self.RoleName="" self.RoleInstanceName="" self.ContainerId="" self.ExecutionMode="IAAS" self.OSVersion="" self.GAVersion="" self.RAM=0 self.Processors=0 def ToXml(self): strEventid=u''.format(self.eventId) strProviderid=u''.format(self.providerId) strRecordFormat = u'' strRecordNoQuoteFormat = u'' strMtStr=u'mt:wstr' strMtUInt64=u'mt:uint64' strMtBool=u'mt:bool' strMtFloat=u'mt:float64' strEventsData=u"" for attName in self.__dict__: if attName in ["eventId","filedCount","providerId"]: continue attValue = self.__dict__[attName] if type(attValue) is int: strEventsData+=strRecordFormat.format(attName,attValue,strMtUInt64) continue if type(attValue) is str: attValue = xml.sax.saxutils.quoteattr(attValue) strEventsData+=strRecordNoQuoteFormat.format(attName,attValue,strMtStr) continue if str(type(attValue)).count("'unicode'") >0 : attValue = xml.sax.saxutils.quoteattr(attValue) strEventsData+=strRecordNoQuoteFormat.format(attName,attValue,strMtStr) continue if type(attValue) is bool: strEventsData+=strRecordFormat.format(attName,attValue,strMtBool) continue if type(attValue) is float: strEventsData+=strRecordFormat.format(attName,attValue,strMtFloat) continue Log("Warning: property "+attName+":"+str(type(attValue))+":type"+str(type(attValue))+"Can't convert to events data:"+":type not supported") return u"{0}{1}{2}".format(strProviderid,strEventid,strEventsData) def Save(self): eventfolder = LibDir+"/events" if not os.path.exists(eventfolder): os.mkdir(eventfolder) os.chmod(eventfolder,0700) if len(os.listdir(eventfolder)) > 1000: raise Exception("WriteToFolder:Too many file under "+eventfolder+" exit") filename = os.path.join(eventfolder,str(int(time.time()*1000000))) with open(filename+".tmp",'wb+') as hfile: hfile.write(self.ToXml().encode("utf-8")) os.rename(filename+".tmp",filename+".tld") class WALAEventOperation: HeartBeat="HeartBeat" Provision = "Provision" Install = "Install" UnIsntall = "UnInstall" Disable = "Disable" Enable = "Enable" Download = "Download" Upgrade = "Upgrade" Update = "Update" def AddExtensionEvent(name,op,isSuccess,duration=0,version="1.0",message="",type="",isInternal=False): event = ExtensionEvent() event.Name=name event.Version=version event.IsInternal=isInternal event.Operation=op event.OperationSuccess=isSuccess event.Message=message event.Duration=duration event.ExtensionType=type try: event.Save() except: Error("Error "+traceback.format_exc()) class ExtensionEvent(WALAEvent): def __init__(self): WALAEvent.__init__(self) self.eventId=1 self.providerId="69B669B9-4AF8-4C50-BDC4-6006FA76E975" self.Name="" self.Version="" self.IsInternal=False self.Operation="" self.OperationSuccess=True self.ExtensionType="" self.Message="" self.Duration=0 class WALAEventMonitor(WALAEvent): def __init__(self,postMethod): WALAEvent.__init__(self) self.post = postMethod self.sysInfo={} self.eventdir = LibDir+"/events" self.issysteminfoinitilized = False def StartEventsLoop(self): eventThread = threading.Thread(target = self.EventsLoop) eventThread.setDaemon(True) eventThread.start() def EventsLoop(self): LastReportHeartBeatTime = datetime.datetime.min try: while True: if (datetime.datetime.now()-LastReportHeartBeatTime) > \ datetime.timedelta(minutes=30): LastReportHeartBeatTime = datetime.datetime.now() AddExtensionEvent(op=WALAEventOperation.HeartBeat,name="WALA",isSuccess=True) self.postNumbersInOneLoop=0 self.CollectAndSendWALAEvents() time.sleep(60) except: Error("Exception in events loop:"+traceback.format_exc()) def SendEvent(self,providerid,events): dataFormat = u'{1}'\ '' data = dataFormat.format(providerid,events) self.post("/machine/?comp=telemetrydata", data) def CollectAndSendWALAEvents(self): if not os.path.exists(self.eventdir): return #Throtting, can't send more than 3 events in 15 seconds eventSendNumber=0 eventFiles = os.listdir(self.eventdir) events = {} for file in eventFiles: if not file.endswith(".tld"): continue with open(os.path.join(self.eventdir,file),"rb") as hfile: #if fail to open or delete the file, throw exception xmlStr = hfile.read().decode("utf-8",'ignore') os.remove(os.path.join(self.eventdir,file)) params="" eventid="" providerid="" #if exception happen during process an event, catch it and continue try: xmlStr = self.AddSystemInfo(xmlStr) for node in xml.dom.minidom.parseString(xmlStr.encode("utf-8")).childNodes[0].childNodes: if node.tagName == "Param": params+=node.toxml() if node.tagName == "Event": eventid=node.getAttribute("id") if node.tagName == "Provider": providerid = node.getAttribute("id") except: Error(traceback.format_exc()) continue if len(params)==0 or len(eventid)==0 or len(providerid)==0: Error("Empty filed in params:"+params+" event id:"+eventid+" provider id:"+providerid) continue eventstr = u''.format(eventid,params) if not events.get(providerid): events[providerid]="" if len(events[providerid]) >0 and len(events.get(providerid)+eventstr)>= 63*1024: eventSendNumber+=1 self.SendEvent(providerid,events.get(providerid)) if eventSendNumber %3 ==0: time.sleep(15) events[providerid]="" if len(eventstr) >= 63*1024: Error("Signle event too large abort "+eventstr[:300]) continue events[providerid]=events.get(providerid)+eventstr for key in events.keys(): if len(events[key]) > 0: eventSendNumber+=1 self.SendEvent(key,events[key]) if eventSendNumber%3 == 0: time.sleep(15) def AddSystemInfo(self,eventData): if not self.issysteminfoinitilized: self.issysteminfoinitilized=True try: self.sysInfo["OSVersion"]=platform.system()+":"+"-".join(DistInfo(1))+":"+platform.release() self.sysInfo["GAVersion"]=GuestAgentVersion self.sysInfo["RAM"]=MyDistro.getTotalMemory() self.sysInfo["Processors"]=MyDistro.getProcessorCores() sharedConfig = xml.dom.minidom.parse("/var/lib/waagent/SharedConfig.xml").childNodes[0] hostEnvConfig= xml.dom.minidom.parse("/var/lib/waagent/HostingEnvironmentConfig.xml").childNodes[0] gfiles = RunGetOutput("ls -t /var/lib/waagent/GoalState.*.xml")[1] goalStateConfi = xml.dom.minidom.parse(gfiles.split("\n")[0]).childNodes[0] self.sysInfo["TenantName"]=hostEnvConfig.getElementsByTagName("Deployment")[0].getAttribute("name") self.sysInfo["RoleName"]=hostEnvConfig.getElementsByTagName("Role")[0].getAttribute("name") self.sysInfo["RoleInstanceName"]=sharedConfig.getElementsByTagName("Instance")[0].getAttribute("id") self.sysInfo["ContainerId"]=goalStateConfi.getElementsByTagName("ContainerId")[0].childNodes[0].nodeValue except: Error(traceback.format_exc()) eventObject = xml.dom.minidom.parseString(eventData.encode("utf-8")).childNodes[0] for node in eventObject.childNodes: if node.tagName == "Param": name = node.getAttribute("Name") if self.sysInfo.get(name): node.setAttribute("Value",xml.sax.saxutils.escape(str(self.sysInfo[name]))) return eventObject.toxml() class Agent(Util): """ Primary object container for the provisioning process. """ def __init__(self): self.GoalState = None self.Endpoint = None self.LoadBalancerProbeServer = None self.HealthReportCounter = 0 self.TransportCert = "" self.EnvMonitor = None self.SendData = None self.DhcpResponse = None def CheckVersions(self): """ Query endpoint server for wire protocol version. Fail if our desired protocol version is not seen. """ # # # # 2010-12-15 # # # 2010-12-15 # 2010-28-10 # # global ProtocolVersion protocolVersionSeen = False node = xml.dom.minidom.parseString(self.HttpGetWithoutHeaders("/?comp=versions")).childNodes[0] if node.localName != "Versions": Error("CheckVersions: root not Versions") return False for a in node.childNodes: if a.nodeType == node.ELEMENT_NODE and a.localName == "Supported": for b in a.childNodes: if b.nodeType == node.ELEMENT_NODE and b.localName == "Version": v = GetNodeTextData(b) LogIfVerbose("Fabric supported wire protocol version: " + v) if v == ProtocolVersion: protocolVersionSeen = True if a.nodeType == node.ELEMENT_NODE and a.localName == "Preferred": v = GetNodeTextData(a.getElementsByTagName("Version")[0]) Log("Fabric preferred wire protocol version: " + v) if not protocolVersionSeen: Warn("Agent supported wire protocol version: " + ProtocolVersion + " was not advertised by Fabric.") else: Log("Negotiated wire protocol version: " + ProtocolVersion) return True def Unpack(self, buffer, offset, range): """ Unpack bytes into python values. """ result = 0 for i in range: result = (result << 8) | Ord(buffer[offset + i]) return result def UnpackLittleEndian(self, buffer, offset, length): """ Unpack little endian bytes into python values. """ return self.Unpack(buffer, offset, list(range(length - 1, -1, -1))) def UnpackBigEndian(self, buffer, offset, length): """ Unpack big endian bytes into python values. """ return self.Unpack(buffer, offset, list(range(0, length))) def HexDump3(self, buffer, offset, length): """ Dump range of buffer in formatted hex. """ return ''.join(['%02X' % Ord(char) for char in buffer[offset:offset + length]]) def HexDump2(self, buffer): """ Dump buffer in formatted hex. """ return self.HexDump3(buffer, 0, len(buffer)) def BuildDhcpRequest(self): """ Build DHCP request string. """ # # typedef struct _DHCP { # UINT8 Opcode; /* op: BOOTREQUEST or BOOTREPLY */ # UINT8 HardwareAddressType; /* htype: ethernet */ # UINT8 HardwareAddressLength; /* hlen: 6 (48 bit mac address) */ # UINT8 Hops; /* hops: 0 */ # UINT8 TransactionID[4]; /* xid: random */ # UINT8 Seconds[2]; /* secs: 0 */ # UINT8 Flags[2]; /* flags: 0 or 0x8000 for broadcast */ # UINT8 ClientIpAddress[4]; /* ciaddr: 0 */ # UINT8 YourIpAddress[4]; /* yiaddr: 0 */ # UINT8 ServerIpAddress[4]; /* siaddr: 0 */ # UINT8 RelayAgentIpAddress[4]; /* giaddr: 0 */ # UINT8 ClientHardwareAddress[16]; /* chaddr: 6 byte ethernet MAC address */ # UINT8 ServerName[64]; /* sname: 0 */ # UINT8 BootFileName[128]; /* file: 0 */ # UINT8 MagicCookie[4]; /* 99 130 83 99 */ # /* 0x63 0x82 0x53 0x63 */ # /* options -- hard code ours */ # # UINT8 MessageTypeCode; /* 53 */ # UINT8 MessageTypeLength; /* 1 */ # UINT8 MessageType; /* 1 for DISCOVER */ # UINT8 End; /* 255 */ # } DHCP; # # tuple of 244 zeros # (struct.pack_into would be good here, but requires Python 2.5) sendData = [0] * 244 transactionID = os.urandom(4) macAddress = MyDistro.GetMacAddress() # Opcode = 1 # HardwareAddressType = 1 (ethernet/MAC) # HardwareAddressLength = 6 (ethernet/MAC/48 bits) for a in range(0, 3): sendData[a] = [1, 1, 6][a] # fill in transaction id (random number to ensure response matches request) for a in range(0, 4): sendData[4 + a] = Ord(transactionID[a]) LogIfVerbose("BuildDhcpRequest: transactionId:%s,%04X" % (self.HexDump2(transactionID), self.UnpackBigEndian(sendData, 4, 4))) # fill in ClientHardwareAddress for a in range(0, 6): sendData[0x1C + a] = Ord(macAddress[a]) # DHCP Magic Cookie: 99, 130, 83, 99 # MessageTypeCode = 53 DHCP Message Type # MessageTypeLength = 1 # MessageType = DHCPDISCOVER # End = 255 DHCP_END for a in range(0, 8): sendData[0xEC + a] = [99, 130, 83, 99, 53, 1, 1, 255][a] return array.array("B", sendData) def IntegerToIpAddressV4String(self, a): """ Build DHCP request string. """ return "%u.%u.%u.%u" % ((a >> 24) & 0xFF, (a >> 16) & 0xFF, (a >> 8) & 0xFF, a & 0xFF) def RouteAdd(self, net, mask, gateway): """ Add specified route using /sbin/route add -net. """ net = self.IntegerToIpAddressV4String(net) mask = self.IntegerToIpAddressV4String(mask) gateway = self.IntegerToIpAddressV4String(gateway) Log("Route add: net={0}, mask={1}, gateway={2}".format(net, mask, gateway)) MyDistro.routeAdd(net, mask, gateway) def SetDefaultGateway(self, gateway): """ Set default gateway """ gateway = self.IntegerToIpAddressV4String(gateway) Log("Set default gateway: {0}".format(gateway)) MyDistro.setDefaultGateway(gateway) def HandleDhcpResponse(self, sendData, receiveBuffer): """ Parse DHCP response: Set default gateway. Set default routes. Retrieve endpoint server. Returns endpoint server or None on error. """ LogIfVerbose("HandleDhcpResponse") bytesReceived = len(receiveBuffer) if bytesReceived < 0xF6: Error("HandleDhcpResponse: Too few bytes received " + str(bytesReceived)) return None LogIfVerbose("BytesReceived: " + hex(bytesReceived)) LogWithPrefixIfVerbose("DHCP response:", HexDump(receiveBuffer, bytesReceived)) # check transactionId, cookie, MAC address # cookie should never mismatch # transactionId and MAC address may mismatch if we see a response meant from another machine for offsets in [list(range(4, 4 + 4)), list(range(0x1C, 0x1C + 6)), list(range(0xEC, 0xEC + 4))]: for offset in offsets: sentByte = Ord(sendData[offset]) receivedByte = Ord(receiveBuffer[offset]) if sentByte != receivedByte: LogIfVerbose("HandleDhcpResponse: sent cookie:" + self.HexDump3(sendData, 0xEC, 4)) LogIfVerbose("HandleDhcpResponse: rcvd cookie:" + self.HexDump3(receiveBuffer, 0xEC, 4)) LogIfVerbose("HandleDhcpResponse: sent transactionID:" + self.HexDump3(sendData, 4, 4)) LogIfVerbose("HandleDhcpResponse: rcvd transactionID:" + self.HexDump3(receiveBuffer, 4, 4)) LogIfVerbose("HandleDhcpResponse: sent ClientHardwareAddress:" + self.HexDump3(sendData, 0x1C, 6)) LogIfVerbose("HandleDhcpResponse: rcvd ClientHardwareAddress:" + self.HexDump3(receiveBuffer, 0x1C, 6)) LogIfVerbose("HandleDhcpResponse: transactionId, cookie, or MAC address mismatch") return None endpoint = None # # Walk all the returned options, parsing out what we need, ignoring the others. # We need the custom option 245 to find the the endpoint we talk to, # as well as, to handle some Linux DHCP client incompatibilities, # options 3 for default gateway and 249 for routes. And 255 is end. # i = 0xF0 # offset to first option while i < bytesReceived: option = Ord(receiveBuffer[i]) length = 0 if (i + 1) < bytesReceived: length = Ord(receiveBuffer[i + 1]) LogIfVerbose("DHCP option " + hex(option) + " at offset:" + hex(i) + " with length:" + hex(length)) if option == 255: LogIfVerbose("DHCP packet ended at offset " + hex(i)) break elif option == 249: # http://msdn.microsoft.com/en-us/library/cc227282%28PROT.10%29.aspx LogIfVerbose("Routes at offset:" + hex(i) + " with length:" + hex(length)) if length < 5: Error("Data too small for option " + str(option)) j = i + 2 while j < (i + length + 2): maskLengthBits = Ord(receiveBuffer[j]) maskLengthBytes = (((maskLengthBits + 7) & ~7) >> 3) mask = 0xFFFFFFFF & (0xFFFFFFFF << (32 - maskLengthBits)) j += 1 net = self.UnpackBigEndian(receiveBuffer, j, maskLengthBytes) net <<= (32 - maskLengthBytes * 8) net &= mask j += maskLengthBytes gateway = self.UnpackBigEndian(receiveBuffer, j, 4) j += 4 self.RouteAdd(net, mask, gateway) if j != (i + length + 2): Error("HandleDhcpResponse: Unable to parse routes") elif option == 3 or option == 245: if i + 5 < bytesReceived: if length != 4: Error("HandleDhcpResponse: Endpoint or Default Gateway not 4 bytes") return None gateway = self.UnpackBigEndian(receiveBuffer, i + 2, 4) IpAddress = self.IntegerToIpAddressV4String(gateway) if option == 3: self.SetDefaultGateway(gateway) name = "DefaultGateway" else: endpoint = IpAddress name = "Azure wire protocol endpoint" LogIfVerbose(name + ": " + IpAddress + " at " + hex(i)) else: Error("HandleDhcpResponse: Data too small for option " + str(option)) else: LogIfVerbose("Skipping DHCP option " + hex(option) + " at " + hex(i) + " with length " + hex(length)) i += length + 2 return endpoint def DoDhcpWork(self): """ Discover the wire server via DHCP option 245. And workaround incompatibility with Azure DHCP servers. """ ShortSleep = False # Sleep 1 second before retrying DHCP queries. ifname=None sleepDurations = [0, 10, 30, 60, 60] maxRetry = len(sleepDurations) lastTry = (maxRetry - 1) for retry in range(0, maxRetry): try: #Open DHCP port if iptables is enabled. Run("iptables -D INPUT -p udp --dport 68 -j ACCEPT",chk_err=False) # We supress error logging on error. Run("iptables -I INPUT -p udp --dport 68 -j ACCEPT",chk_err=False) # We supress error logging on error. strRetry = str(retry) prefix = "DoDhcpWork: try=" + strRetry LogIfVerbose(prefix) sendData = self.BuildDhcpRequest() LogWithPrefixIfVerbose("DHCP request:", HexDump(sendData, len(sendData))) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) missingDefaultRoute = True try: if DistInfo()[0] == 'FreeBSD': missingDefaultRoute = True else: routes = RunGetOutput("route -n")[1] for line in routes.split('\n'): if line.startswith("0.0.0.0 ") or line.startswith("default "): missingDefaultRoute = False except: pass if missingDefaultRoute: # This is required because sending after binding to 0.0.0.0 fails with # network unreachable when the default gateway is not set up. ifname=MyDistro.GetInterfaceName() Log("DoDhcpWork: Missing default route - adding broadcast route for DHCP.") if DistInfo()[0] == 'FreeBSD': Run("route add -net 255.255.255.255 -iface " + ifname,chk_err=False) else: Run("route add 255.255.255.255 dev " + ifname,chk_err=False) if MyDistro.isDHCPEnabled(): MyDistro.stopDHCP() sock.bind(("0.0.0.0", 68)) sock.sendto(sendData, ("", 67)) sock.settimeout(10) Log("DoDhcpWork: Setting socket.timeout=10, entering recv") receiveBuffer = sock.recv(1024) endpoint = self.HandleDhcpResponse(sendData, receiveBuffer) if endpoint == None: LogIfVerbose("DoDhcpWork: No endpoint found") if endpoint != None or retry == lastTry: if endpoint != None: self.SendData = sendData self.DhcpResponse = receiveBuffer if retry == lastTry: LogIfVerbose("DoDhcpWork: try=" + strRetry) return endpoint sleepDuration = [sleepDurations[retry % len(sleepDurations)], 1][ShortSleep] LogIfVerbose("DoDhcpWork: sleep=" + str(sleepDuration)) time.sleep(sleepDuration) except Exception, e: ErrorWithPrefix(prefix, str(e)) ErrorWithPrefix(prefix, traceback.format_exc()) finally: sock.close() if missingDefaultRoute: #We added this route - delete it Log("DoDhcpWork: Removing broadcast route for DHCP.") if DistInfo()[0] == 'FreeBSD': Run("route del -net 255.255.255.255 -iface " + ifname,chk_err=False) else: Run("route del 255.255.255.255 dev " + ifname,chk_err=False) # We supress error logging on error. if MyDistro.isDHCPEnabled(): MyDistro.startDHCP() return None def UpdateAndPublishHostName(self, name): """ Set hostname locally and publish to iDNS """ Log("Setting host name: " + name) MyDistro.publishHostname(name) ethernetInterface = MyDistro.GetInterfaceName() MyDistro.RestartInterface(ethernetInterface) self.RestoreRoutes() def RestoreRoutes(self): """ If there is a DHCP response, then call HandleDhcpResponse. """ if self.SendData != None and self.DhcpResponse != None: self.HandleDhcpResponse(self.SendData, self.DhcpResponse) def UpdateGoalState(self): """ Retreive goal state information from endpoint server. Parse xml and initialize Agent.GoalState object. Return object or None on error. """ goalStateXml = None maxRetry = 9 log = NoLog for retry in range(1, maxRetry + 1): strRetry = str(retry) log("retry UpdateGoalState,retry=" + strRetry) goalStateXml = self.HttpGetWithHeaders("/machine/?comp=goalstate") if goalStateXml != None: break log = Log time.sleep(retry) if not goalStateXml: Error("UpdateGoalState failed.") return Log("Retrieved GoalState from Azure Fabric.") self.GoalState = GoalState(self).Parse(goalStateXml) return self.GoalState def ReportReady(self): """ Send health report 'Ready' to server. This signals the fabric that our provosion is completed, and the host is ready for operation. """ counter = (self.HealthReportCounter + 1) % 1000000 self.HealthReportCounter = counter healthReport = ("" + self.GoalState.Incarnation + "" + self.GoalState.ContainerId + "" + self.GoalState.RoleInstanceId + "Ready") a = self.HttpPostWithHeaders("/machine?comp=health", healthReport) if a != None: return a.getheader("x-ms-latest-goal-state-incarnation-number") return None def ReportNotReady(self, status, desc): """ Send health report 'Provisioning' to server. This signals the fabric that our provosion is starting. """ healthReport = ("" + self.GoalState.Incarnation + "" + self.GoalState.ContainerId + "" + self.GoalState.RoleInstanceId + "NotReady" + "
" + status + "" + desc + "
" + "
") a = self.HttpPostWithHeaders("/machine?comp=health", healthReport) if a != None: return a.getheader("x-ms-latest-goal-state-incarnation-number") return None def ReportRoleProperties(self, thumbprint): """ Send roleProperties and thumbprint to server. """ roleProperties = ("" + "" + self.GoalState.ContainerId + "" + "" + "" + self.GoalState.RoleInstanceId + "" + "" + "") a = self.HttpPostWithHeaders("/machine?comp=roleProperties", roleProperties) Log("Posted Role Properties. CertificateThumbprint=" + thumbprint) return a def LoadBalancerProbeServer_Shutdown(self): """ Shutdown the LoadBalancerProbeServer. """ if self.LoadBalancerProbeServer != None: self.LoadBalancerProbeServer.shutdown() self.LoadBalancerProbeServer = None def GenerateTransportCert(self): """ Create ssl certificate for https communication with endpoint server. """ Run(Openssl + " req -x509 -nodes -subj /CN=LinuxTransport -days 32768 -newkey rsa:2048 -keyout TransportPrivate.pem -out TransportCert.pem") cert = "" for line in GetFileContents("TransportCert.pem").split('\n'): if not "CERTIFICATE" in line: cert += line.rstrip() return cert def DoVmmStartup(self): """ Spawn the VMM startup script. """ Log("Starting Microsoft System Center VMM Initialization Process") pid = subprocess.Popen(["/bin/bash","/mnt/cdrom/secure/"+VMM_STARTUP_SCRIPT_NAME,"-p /mnt/cdrom/secure/ "]).pid time.sleep(5) sys.exit(0) def TryUnloadAtapiix(self): """ If global modloaded is True, then we loaded the ata_piix kernel module, unload it. """ if modloaded: Run("rmmod ata_piix.ko",chk_err=False) Log("Unloaded ata_piix.ko driver for ATAPI CD-ROM") def TryLoadAtapiix(self): """ Load the ata_piix kernel module if it exists. If successful, set global modloaded to True. If unable to load module leave modloaded False. """ global modloaded modloaded=False retcode,krn=RunGetOutput('uname -r') krn_pth='/lib/modules/'+krn.strip('\n')+'/kernel/drivers/ata/ata_piix.ko' if Run("lsmod | grep ata_piix",chk_err=False) == 0 : Log("Module " + krn_pth + " driver for ATAPI CD-ROM is already present.") return 0 if retcode: Error("Unable to provision: Failed to call uname -r") return "Unable to provision: Failed to call uname" if os.path.isfile(krn_pth): retcode,output=RunGetOutput("insmod " + krn_pth,chk_err=False) else: Log("Module " + krn_pth + " driver for ATAPI CD-ROM does not exist.") return 1 if retcode != 0: Error('Error calling insmod for '+ krn_pth + ' driver for ATAPI CD-ROM') return retcode time.sleep(1) # check 3 times if the mod is loaded for i in range(3): if Run('lsmod | grep ata_piix'): continue else : modloaded=True break if not modloaded: Error('Unable to load '+ krn_pth + ' driver for ATAPI CD-ROM') return 1 Log("Loaded " + krn_pth + " driver for ATAPI CD-ROM") # we have succeeded loading the ata_piix mod if it can be done. def SearchForVMMStartup(self): """ Search for a DVD/CDROM containing VMM's VMM_CONFIG_FILE_NAME. Call TryLoadAtapiix in case we must load the ata_piix module first. If VMM_CONFIG_FILE_NAME is found, call DoVmmStartup. Else, return to Azure Provisioning process. """ self.TryLoadAtapiix() if os.path.exists('/mnt/cdrom/secure') == False: CreateDir("/mnt/cdrom/secure", "root", 0700) mounted=False for dvds in [re.match(r'(sr[0-9]|hd[c-z]|cdrom[0-9]|cd[0-9]?)',x) for x in os.listdir('/dev/')]: if dvds == None: continue dvd = '/dev/'+dvds.group(0) if Run("LC_ALL=C fdisk -l " + dvd + " | grep Disk",chk_err=False): continue # Not mountable else: for retry in range(1,6): retcode,output=RunGetOutput("mount -v " + dvd + " /mnt/cdrom/secure") Log(output[:-1]) if retcode == 0: Log("mount succeeded on attempt #" + str(retry) ) mounted=True break if 'is already mounted on /mnt/cdrom/secure' in output: Log("Device " + dvd + " is already mounted on /mnt/cdrom/secure." + str(retry) ) mounted=True break Log("mount failed on attempt #" + str(retry) ) Log("mount loop sleeping 5...") time.sleep(5) if not mounted: # unable to mount continue if not os.path.isfile("/mnt/cdrom/secure/"+VMM_CONFIG_FILE_NAME): #nope - mount the next drive if mounted: Run("umount "+dvd,chk_err=False) mounted=False continue else : # it is the vmm startup self.DoVmmStartup() Log("VMM Init script not found. Provisioning for Azure") return def Provision(self): """ Responible for: Regenerate ssh keys, Mount, read, and parse ovfenv.xml from provisioning dvd rom Process the ovfenv.xml info Call ReportRoleProperties If configured, delete root password. Return None on success, error string on error. """ enabled = Config.get("Provisioning.Enabled") if enabled != None and enabled.lower().startswith("n"): return Log("Provisioning image started.") type = Config.get("Provisioning.SshHostKeyPairType") if type == None: type = "rsa" regenerateKeys = Config.get("Provisioning.RegenerateSshHostKeyPair") if regenerateKeys == None or regenerateKeys.lower().startswith("y"): Run("rm -f /etc/ssh/ssh_host_*key*") Run("ssh-keygen -N '' -t " + type + " -f /etc/ssh/ssh_host_" + type + "_key") MyDistro.restartSshService() #SetFileContents(LibDir + "/provisioned", "") dvd = None for dvds in [re.match(r'(sr[0-9]|hd[c-z]|cdrom[0-9]|cd[0-9]?)',x) for x in os.listdir('/dev/')]: if dvds == None : continue dvd = '/dev/'+dvds.group(0) if dvd == None: # No DVD device detected Error("No DVD device detected, unable to provision.") return "No DVD device detected, unable to provision." if MyDistro.mediaHasFilesystem(dvd) is False : out=MyDistro.load_ata_piix() if out: return out for i in range(10): # we may have to wait if os.path.exists(dvd): break Log("Waiting for DVD - sleeping 1 - "+str(i+1)+" try...") time.sleep(1) if os.path.exists('/mnt/cdrom/secure') == False: CreateDir("/mnt/cdrom/secure", "root", 0700) #begin mount loop - 5 tries - 5 sec wait between for retry in range(1,6): location='/mnt/cdrom/secure' retcode,output=MyDistro.mountDVD(dvd,location) Log(output[:-1]) if retcode == 0: Log("mount succeeded on attempt #" + str(retry) ) break if 'is already mounted on /mnt/cdrom/secure' in output: Log("Device " + dvd + " is already mounted on /mnt/cdrom/secure." + str(retry) ) break Log("mount failed on attempt #" + str(retry) ) Log("mount loop sleeping 5...") time.sleep(5) if not os.path.isfile("/mnt/cdrom/secure/ovf-env.xml"): Error("Unable to provision: Missing ovf-env.xml on DVD.") return "Failed to retrieve provisioning data (0x02)." ovfxml = (GetFileContents(u"/mnt/cdrom/secure/ovf-env.xml",asbin=False)) # use unicode here to ensure correct codec gets used. if ord(ovfxml[0]) > 128 and ord(ovfxml[1]) > 128 and ord(ovfxml[2]) > 128 : ovfxml = ovfxml[3:] # BOM is not stripped. First three bytes are > 128 and not unicode chars so we ignore them. ovfxml=ovfxml.strip(chr(0x00)) # we may have NULLs. ovfxml=ovfxml[ovfxml.find('.*?<", "*<", ovfxml)) Run("umount " + dvd,chk_err=False) MyDistro.unload_ata_piix() error = None if ovfxml != None: Log("Provisioning image using OVF settings in the DVD.") ovfobj = OvfEnv().Parse(ovfxml) if ovfobj != None: error = ovfobj.Process() if error : Error ("Provisioning image FAILED " + error) return ("Provisioning image FAILED " + error) Log("Ovf XML process finished") # This is done here because regenerated SSH host key pairs may be potentially overwritten when processing the ovfxml fingerprint = RunGetOutput("ssh-keygen -lf /etc/ssh/ssh_host_" + type + "_key.pub")[1].rstrip().split()[1].replace(':','') self.ReportRoleProperties(fingerprint) delRootPass = Config.get("Provisioning.DeleteRootPassword") if delRootPass != None and delRootPass.lower().startswith("y"): MyDistro.deleteRootPassword() Log("Provisioning image completed.") return error def Run(self): """ Called by 'waagent -daemon.' Main loop to process the goal state. State is posted every 25 seconds when provisioning has been completed. Search for VMM enviroment, start VMM script if found. Perform DHCP and endpoint server discovery by calling DoDhcpWork(). Check wire protocol versions. Set SCSI timeout on root device. Call GenerateTransportCert() to create ssl certs for server communication. Call UpdateGoalState(). If not provisioned, call ReportNotReady("Provisioning", "Starting") Call Provision(), set global provisioned = True if successful. Call goalState.Process() Start LBProbeServer if indicated in waagent.conf. Start the StateConsumer if indicated in waagent.conf. ReportReady if provisioning is complete. If provisioning failed, call ReportNotReady("ProvisioningFailed", provisionError) """ SetFileContents("/var/run/waagent.pid", str(os.getpid()) + "\n") reportHandlerStatusCount = 0 # Determine if we are in VMM. Spawn VMM_STARTUP_SCRIPT_NAME if found. self.SearchForVMMStartup() ipv4='' while ipv4 == '' or ipv4 == '0.0.0.0' : ipv4=MyDistro.GetIpv4Address() if ipv4 == '' or ipv4 == '0.0.0.0' : Log("Waiting for network.") time.sleep(10) Log("IPv4 address: " + ipv4) mac='' mac=MyDistro.GetMacAddress() if len(mac)>0 : Log("MAC address: " + ":".join(["%02X" % Ord(a) for a in mac])) # Consume Entropy in ACPI table provided by Hyper-V try: SetFileContents("/dev/random", GetFileContents("/sys/firmware/acpi/tables/OEM0")) except: pass Log("Probing for Azure environment.") self.Endpoint = self.DoDhcpWork() while self.Endpoint == None: Log("Azure environment not detected.") Log("Retry environment detection in 60 seconds") time.sleep(60) self.Endpoint = self.DoDhcpWork() Log("Discovered Azure endpoint: " + self.Endpoint) if not self.CheckVersions(): Error("Agent.CheckVersions failed") sys.exit(1) self.EnvMonitor = EnvMonitor() # Set SCSI timeout on SCSI disks MyDistro.initScsiDiskTimeout() global provisioned global provisionError global Openssl Openssl = Config.get("OS.OpensslPath") if Openssl == None: Openssl = "openssl" self.TransportCert = self.GenerateTransportCert() eventMonitor = None incarnation = None # goalStateIncarnationFromHealthReport currentPort = None # loadBalancerProbePort goalState = None # self.GoalState, instance of GoalState provisioned = os.path.exists(LibDir + "/provisioned") program = Config.get("Role.StateConsumer") provisionError = None lbProbeResponder = True setting = Config.get("LBProbeResponder") if setting != None and setting.lower().startswith("n"): lbProbeResponder = False while True: if (goalState == None) or (incarnation == None) or (goalState.Incarnation != incarnation): try: goalState = self.UpdateGoalState() except HttpResourceGoneError as e: Warn("Incarnation is out of date:{0}".format(e)) incarnation = None continue if goalState == None : Warn("Failed to fetch goalstate") continue if provisioned == False: self.ReportNotReady("Provisioning", "Starting") goalState.Process() if provisioned == False: provisionError = self.Provision() if provisionError == None : provisioned = True SetFileContents(LibDir + "/provisioned", "") lastCtime = "NOTFIND" try: walaConfigFile = MyDistro.getConfigurationPath() lastCtime = time.ctime(os.path.getctime(walaConfigFile)) except: pass #Get Ctime of wala config, can help identify the base image of this VM AddExtensionEvent(name="WALA",op=WALAEventOperation.Provision,isSuccess=True, message="WALA Config Ctime:"+lastCtime) executeCustomData = Config.get("Provisioning.ExecuteCustomData") if executeCustomData != None and executeCustomData.lower().startswith("y"): if os.path.exists(LibDir + '/CustomData'): Run('chmod +x ' + LibDir + '/CustomData') Run(LibDir + '/CustomData') else: Error(LibDir + '/CustomData does not exist.') # # only one port supported # restart server if new port is different than old port # stop server if no longer a port # goalPort = goalState.LoadBalancerProbePort if currentPort != goalPort: try: self.LoadBalancerProbeServer_Shutdown() currentPort = goalPort if currentPort != None and lbProbeResponder == True: self.LoadBalancerProbeServer = LoadBalancerProbeServer(currentPort) if self.LoadBalancerProbeServer == None : lbProbeResponder = False Log("Unable to create LBProbeResponder.") except Exception, e: Error("Failed to launch LBProbeResponder: {0}".format(e)) currentPort = None # Report SSH key fingerprint type = Config.get("Provisioning.SshHostKeyPairType") if type == None: type = "rsa" host_key_path = "/etc/ssh/ssh_host_" + type + "_key.pub" if(MyDistro.waitForSshHostKey(host_key_path)): fingerprint = RunGetOutput("ssh-keygen -lf /etc/ssh/ssh_host_" + type + "_key.pub")[1].rstrip().split()[1].replace(':','') self.ReportRoleProperties(fingerprint) if program != None and DiskActivated == True: try: Children.append(subprocess.Popen([program, "Ready"])) except OSError, e : ErrorWithPrefix('SharedConfig.Parse','Exception: '+ str(e) +' occured launching ' + program ) program = None sleepToReduceAccessDenied = 3 time.sleep(sleepToReduceAccessDenied) if provisionError != None: incarnation = self.ReportNotReady("ProvisioningFailed", provisionError) else: incarnation = self.ReportReady() # Process our extensions. if goalState.ExtensionsConfig == None and goalState.ExtensionsConfigXml != None : reportHandlerStatusCount = 0 #Reset count when new goal state comes goalState.ExtensionsConfig = ExtensionsConfig().Parse(goalState.ExtensionsConfigXml) # report the status/heartbeat results of extension processing if goalState.ExtensionsConfig != None : ret = goalState.ExtensionsConfig.ReportHandlerStatus() if ret != 0: Error("Failed to report handler status") elif reportHandlerStatusCount % 1000 == 0: #Agent report handler status every 25 seconds. Reduce the log entries by adding a count Log("Successfully reported handler status") reportHandlerStatusCount += 1 if not eventMonitor: eventMonitor = WALAEventMonitor(self.HttpPostWithHeaders) eventMonitor.StartEventsLoop() time.sleep(25 - sleepToReduceAccessDenied) WaagentLogrotate = """\ /var/log/waagent.log { monthly rotate 6 notifempty missingok } """ def GetMountPoint(mountlist, device): """ Example of mountlist: /dev/sda1 on / type ext4 (rw) proc on /proc type proc (rw) sysfs on /sys type sysfs (rw) devpts on /dev/pts type devpts (rw,gid=5,mode=620) tmpfs on /dev/shm type tmpfs (rw,rootcontext="system_u:object_r:tmpfs_t:s0") none on /proc/sys/fs/binfmt_misc type binfmt_misc (rw) /dev/sdb1 on /mnt/resource type ext4 (rw) """ if (mountlist and device): for entry in mountlist.split('\n'): if(re.search(device, entry)): tokens = entry.split() #Return the 3rd column of this line return tokens[2] if len(tokens) > 2 else None return None def FindInLinuxKernelCmdline(option): """ Return match object if 'option' is present in the kernel boot options of the grub configuration. """ m=None matchs=r'^.*?'+MyDistro.grubKernelBootOptionsLine+r'.*?'+option+r'.*$' try: m=FindStringInFile(MyDistro.grubKernelBootOptionsFile,matchs) except IOError, e: Error('FindInLinuxKernelCmdline: Exception opening ' + MyDistro.grubKernelBootOptionsFile + 'Exception:' + str(e)) return m def AppendToLinuxKernelCmdline(option): """ Add 'option' to the kernel boot options of the grub configuration. """ if not FindInLinuxKernelCmdline(option): src=r'^(.*?'+MyDistro.grubKernelBootOptionsLine+r')(.*?)("?)$' rep=r'\1\2 '+ option + r'\3' try: ReplaceStringInFile(MyDistro.grubKernelBootOptionsFile,src,rep) except IOError, e : Error('AppendToLinuxKernelCmdline: Exception opening ' + MyDistro.grubKernelBootOptionsFile + 'Exception:' + str(e)) return 1 Run("update-grub",chk_err=False) return 0 def RemoveFromLinuxKernelCmdline(option): """ Remove 'option' to the kernel boot options of the grub configuration. """ if FindInLinuxKernelCmdline(option): src=r'^(.*?'+MyDistro.grubKernelBootOptionsLine+r'.*?)('+option+r')(.*?)("?)$' rep=r'\1\3\4' try: ReplaceStringInFile(MyDistro.grubKernelBootOptionsFile,src,rep) except IOError, e : Error('RemoveFromLinuxKernelCmdline: Exception opening ' + MyDistro.grubKernelBootOptionsFile + 'Exception:' + str(e)) return 1 Run("update-grub",chk_err=False) return 0 def FindStringInFile(fname,matchs): """ Return match object if found in file. """ try: ms=re.compile(matchs) for l in (open(fname,'r')).readlines(): m=re.search(ms,l) if m: return m except: raise return None def ReplaceStringInFile(fname,src,repl): """ Replace 'src' with 'repl' in file. """ try: sr=re.compile(src) if FindStringInFile(fname,src): updated='' for l in (open(fname,'r')).readlines(): n=re.sub(sr,repl,l) updated+=n ReplaceFileContentsAtomic(fname,updated) except : raise return def ApplyVNUMAWorkaround(): """ If kernel version has NUMA bug, add 'numa=off' to kernel boot options. """ VersionParts = platform.release().replace('-', '.').split('.') if int(VersionParts[0]) > 2: return if int(VersionParts[1]) > 6: return if int(VersionParts[2]) > 37: return if AppendToLinuxKernelCmdline("numa=off") == 0 : Log("Your kernel version " + platform.release() + " has a NUMA-related bug: NUMA has been disabled.") else : "Error adding 'numa=off'. NUMA has not been disabled." def RevertVNUMAWorkaround(): """ Remove 'numa=off' from kernel boot options. """ if RemoveFromLinuxKernelCmdline("numa=off") == 0 : Log('NUMA has been re-enabled') else : Log('NUMA has not been re-enabled') def Install(): """ Install the agent service. Check dependencies. Create /etc/waagent.conf and move old version to /etc/waagent.conf.old Copy RulesFiles to /var/lib/waagent Create /etc/logrotate.d/waagent Set /etc/ssh/sshd_config ClientAliveInterval to 180 Call ApplyVNUMAWorkaround() """ if MyDistro.checkDependencies(): return 1 os.chmod(sys.argv[0], 0755) SwitchCwd() for a in RulesFiles: if os.path.isfile(a): if os.path.isfile(GetLastPathElement(a)): os.remove(GetLastPathElement(a)) shutil.move(a, ".") Warn("Moved " + a + " -> " + LibDir + "/" + GetLastPathElement(a) ) MyDistro.registerAgentService() if os.path.isfile("/etc/waagent.conf"): try: os.remove("/etc/waagent.conf.old") except: pass try: os.rename("/etc/waagent.conf", "/etc/waagent.conf.old") Warn("Existing /etc/waagent.conf has been renamed to /etc/waagent.conf.old") except: pass SetFileContents("/etc/waagent.conf", MyDistro.waagent_conf_file) SetFileContents("/etc/logrotate.d/waagent", WaagentLogrotate) filepath = "/etc/ssh/sshd_config" ReplaceFileContentsAtomic(filepath, "\n".join(filter(lambda a: not a.startswith("ClientAliveInterval"), GetFileContents(filepath).split('\n'))) + "\nClientAliveInterval 180\n") Log("Configured SSH client probing to keep connections alive.") ApplyVNUMAWorkaround() return 0 def GetMyDistro(dist_class_name=''): """ Return MyDistro object. NOTE: Logging is not initialized at this point. """ if dist_class_name == '': if 'Linux' in platform.system(): Distro=DistInfo()[0] else : # I know this is not Linux! if 'FreeBSD' in platform.system(): Distro=platform.system() Distro=Distro.strip('"') Distro=Distro.strip(' ') dist_class_name=Distro+'Distro' else: Distro=dist_class_name if not globals().has_key(dist_class_name): print Distro+' is not a supported distribution.' return None return globals()[dist_class_name]() # the distro class inside this module. def DistInfo(fullname=0): if 'FreeBSD' in platform.system(): release = re.sub('\-.*\Z', '', str(platform.release())) distinfo = ['FreeBSD', release] return distinfo if 'linux_distribution' in dir(platform): distinfo = list(platform.linux_distribution(full_distribution_name=fullname)) distinfo[0] = distinfo[0].strip() # remove trailing whitespace in distro name if os.path.exists("/etc/euleros-release"): distinfo[0] = "euleros" return distinfo else: return platform.dist() def PackagedInstall(buildroot): """ Called from setup.py for use by RPM. Generic implementation Creates directories and files /etc/waagent.conf, /etc/init.d/waagent, /usr/sbin/waagent, /etc/logrotate.d/waagent, /etc/sudoers.d/waagent under buildroot. Copies generated files waagent.conf, into place and exits. """ MyDistro=GetMyDistro() if MyDistro == None : sys.exit(1) MyDistro.packagedInstall(buildroot) def LibraryInstall(buildroot): pass def Uninstall(): """ Uninstall the agent service. Copy RulesFiles back to original locations. Delete agent-related files. Call RevertVNUMAWorkaround(). """ SwitchCwd() for a in RulesFiles: if os.path.isfile(GetLastPathElement(a)): try: shutil.move(GetLastPathElement(a), a) Warn("Moved " + LibDir + "/" + GetLastPathElement(a) + " -> " + a ) except: pass MyDistro.unregisterAgentService() MyDistro.uninstallDeleteFiles() RevertVNUMAWorkaround() return 0 def Deprovision(force, deluser): """ Remove user accounts created by provisioning. Disables root password if Provisioning.DeleteRootPassword = 'y' Stop agent service. Remove SSH host keys if they were generated by the provision. Set hostname to 'localhost.localdomain'. Delete cached system configuration files in /var/lib and /var/lib/waagent. """ #Append blank line at the end of file, so the ctime of this file is changed every time Run("echo ''>>"+ MyDistro.getConfigurationPath()) SwitchCwd() ovfxml = GetFileContents(LibDir+"/ovf-env.xml") ovfobj = None if ovfxml != None: ovfobj = OvfEnv().Parse(ovfxml, True) print("WARNING! The waagent service will be stopped.") print("WARNING! All SSH host key pairs will be deleted.") print("WARNING! Cached DHCP leases will be deleted.") MyDistro.deprovisionWarnUser() delRootPass = Config.get("Provisioning.DeleteRootPassword") if delRootPass != None and delRootPass.lower().startswith("y"): print("WARNING! root password will be disabled. You will not be able to login as root.") if ovfobj != None and deluser == True: print("WARNING! " + ovfobj.UserName + " account and entire home directory will be deleted.") if force == False and not raw_input('Do you want to proceed (y/n)? ').startswith('y'): return 1 MyDistro.stopAgentService() # Remove SSH host keys regenerateKeys = Config.get("Provisioning.RegenerateSshHostKeyPair") if regenerateKeys == None or regenerateKeys.lower().startswith("y"): Run("rm -f /etc/ssh/ssh_host_*key*") # Remove root password if delRootPass != None and delRootPass.lower().startswith("y"): MyDistro.deleteRootPassword() # Remove distribution specific networking configuration MyDistro.publishHostname('localhost.localdomain') MyDistro.deprovisionDeleteFiles() if deluser == True: MyDistro.DeleteAccount(ovfobj.UserName) return 0 def SwitchCwd(): """ Switch to cwd to /var/lib/waagent. Create if not present. """ CreateDir(LibDir, "root", 0700) os.chdir(LibDir) def Usage(): """ Print the arguments to waagent. """ print("usage: " + sys.argv[0] + " [-verbose] [-force] [-help|-install|-uninstall|-deprovision[+user]|-version|-serialconsole|-daemon]") return 0 def main(): """ Instantiate MyDistro, exit if distro class is not defined. Parse command-line arguments, exit with usage() on error. Instantiate ConfigurationProvider. Call appropriate non-daemon methods and exit. If daemon mode, enter Agent.Run() loop. """ if GuestAgentVersion == "": print("WARNING! This is a non-standard agent that does not include a valid version string.") if len(sys.argv) == 1: sys.exit(Usage()) LoggerInit('/var/log/waagent.log','/dev/console') global LinuxDistro LinuxDistro=DistInfo()[0] global MyDistro MyDistro=GetMyDistro() if MyDistro == None : sys.exit(1) args = [] conf_file = None global force force = False for a in sys.argv[1:]: if re.match("^([-/]*)(help|usage|\?)", a): sys.exit(Usage()) elif re.match("^([-/]*)version", a): print(GuestAgentVersion + " running on " + LinuxDistro) sys.exit(0) elif re.match("^([-/]*)verbose", a): myLogger.verbose = True elif re.match("^([-/]*)force", a): force = True elif re.match("^(?:[-/]*)conf=.+", a): conf_file = re.match("^(?:[-/]*)conf=(.+)", a).groups()[0] elif re.match("^([-/]*)(setup|install)", a): sys.exit(MyDistro.Install()) elif re.match("^([-/]*)(uninstall)", a): sys.exit(Uninstall()) else: args.append(a) global Config Config = ConfigurationProvider(conf_file) logfile = Config.get("Logs.File") if logfile is not None: myLogger.file_path = logfile logconsole = Config.get("Logs.Console") if logconsole is not None and logconsole.lower().startswith("n"): myLogger.con_path = None verbose = Config.get("Logs.Verbose") if verbose != None and verbose.lower().startswith("y"): myLogger.verbose=True global daemon daemon = False for a in args: if re.match("^([-/]*)deprovision\+user", a): sys.exit(Deprovision(force, True)) elif re.match("^([-/]*)deprovision", a): sys.exit(Deprovision(force, False)) elif re.match("^([-/]*)daemon", a): daemon = True elif re.match("^([-/]*)serialconsole", a): AppendToLinuxKernelCmdline("console=ttyS0 earlyprintk=ttyS0") Log("Configured kernel to use ttyS0 as the boot console.") sys.exit(0) else: print("Invalid command line parameter:" + a) sys.exit(1) if daemon == False: sys.exit(Usage()) global modloaded modloaded = False while True: try: SwitchCwd() Log(GuestAgentLongName + " Version: " + GuestAgentVersion) if IsLinux(): Log("Linux Distribution Detected : " + LinuxDistro) global WaAgent WaAgent = Agent() WaAgent.Run() except Exception, e: Error(traceback.format_exc()) Error("Exception: " + str(e)) Log("Restart agent in 15 seconds") time.sleep(15) if __name__ == '__main__' : main() WALinuxAgent-2.9.1.1/ci/000077500000000000000000000000001446033677600146035ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/ci/2.7.pylintrc000066400000000000000000000065331446033677600167060ustar00rootroot00000000000000# python2.7 uses pylint 1.9.5, whose docs can be found here: http://pylint.pycqa.org/en/1.9/technical_reference/features.html#messages # python3.4 uses pylint 2.3.1, whose docs can be found here: http://pylint.pycqa.org/en/pylint-2.3.1/technical_reference/features.html [MESSAGES CONTROL] disable=C, # (C) convention, for programming standard violation consider-using-dict-comprehension, # (R1717): *Consider using a dictionary comprehension* consider-using-in, # (R1714): *Consider merging these comparisons with "in" to %r* consider-using-set-comprehension, # (R1718): *Consider using a set comprehension* consider-using-with, # (R1732): *Emitted if a resource-allocating assignment or call may be replaced by a 'with' block* duplicate-code, # (R0801): *Similar lines in %s files* no-init, # (W0232): Class has no __init__ method no-else-break, # (R1723): *Unnecessary "%s" after "break"* no-else-continue, # (R1724): *Unnecessary "%s" after "continue"* no-else-raise, # (R1720): *Unnecessary "%s" after "raise"* no-else-return, # (R1705): *Unnecessary "%s" after "return"* no-self-use, # (R0201): *Method could be a function* protected-access, # (W0212): Access to a protected member of a client class simplifiable-if-expression, # (R1719): *The if expression can be replaced with %s* simplifiable-if-statement, # (R1703): *The if statement can be replaced with %s* super-with-arguments, # (R1725): *Consider using Python 3 style super() without arguments* too-few-public-methods, # (R0903): *Too few public methods (%s/%s)* too-many-ancestors, # (R0901): *Too many ancestors (%s/%s)* too-many-arguments, # (R0913): *Too many arguments (%s/%s)* too-many-boolean-expressions, # (R0916): *Too many boolean expressions in if statement (%s/%s)* too-many-branches, # (R0912): *Too many branches (%s/%s)* too-many-instance-attributes, # (R0902): *Too many instance attributes (%s/%s)* too-many-locals, # (R0914): *Too many local variables (%s/%s)* too-many-nested-blocks, # (R1702): *Too many nested blocks (%s/%s)* too-many-public-methods, # (R0904): *Too many public methods (%s/%s)* too-many-return-statements, # (R0911): *Too many return statements (%s/%s)* too-many-statements, # (R0915): *Too many statements (%s/%s)* useless-object-inheritance, # (R0205): *Class %r inherits from object, can be safely removed from bases in python3* useless-return, # (R1711): *Useless return at end of function or method* bad-continuation, # Buggy, **REMOVED in pylint-2.6.0** bad-option-value, # pylint does not recognize the error code/symbol (needed to supress breaking changes across pylint versions) bad-whitespace, # Used when a wrong number of spaces is used around an operator, bracket or block opener. broad-except, # Used when an except catches a too general exception, possibly burying unrelated errors. deprecated-lambda, # Used when a lambda is the first argument to “map†or “filterâ€. It could be clearer as a list comprehension or generator expression. (2.7 only) missing-docstring, # Used when a module, function, class or method has no docstring old-style-class, # Used when a class is defined that does not inherit from another class and does not inherit explicitly from “objectâ€. (2.7 only) fixme, # Used when a warning note as FIXME or TODO is detected WALinuxAgent-2.9.1.1/ci/3.6.pylintrc000066400000000000000000000054761446033677600167130ustar00rootroot00000000000000# python 3.6+ uses the latest pylint version, whose docs can be found here: http://pylint.pycqa.org/en/stable/technical_reference/features.html [MESSAGES CONTROL] disable=C, # (C) convention, for programming standard violation broad-except, # (W0703): *Catching too general exception %s* consider-using-dict-comprehension, # (R1717): *Consider using a dictionary comprehension* consider-using-in, # (R1714): *Consider merging these comparisons with "in" to %r* consider-using-set-comprehension, # (R1718): *Consider using a set comprehension* consider-using-with, # (R1732): *Emitted if a resource-allocating assignment or call may be replaced by a 'with' block* duplicate-code, # (R0801): *Similar lines in %s files* fixme, # Used when a warning note as FIXME or TODO is detected no-else-break, # (R1723): *Unnecessary "%s" after "break"* no-else-continue, # (R1724): *Unnecessary "%s" after "continue"* no-else-raise, # (R1720): *Unnecessary "%s" after "raise"* no-else-return, # (R1705): *Unnecessary "%s" after "return"* no-init, # (W0232): Class has no __init__ method no-self-use, # (R0201): *Method could be a function* protected-access, # (W0212): Access to a protected member of a client class raise-missing-from, # (W0707): *Consider explicitly re-raising using the 'from' keyword* redundant-u-string-prefix, # The u prefix for strings is no longer necessary in Python >=3.0 simplifiable-if-expression, # (R1719): *The if expression can be replaced with %s* simplifiable-if-statement, # (R1703): *The if statement can be replaced with %s* super-with-arguments, # (R1725): *Consider using Python 3 style super() without arguments* too-few-public-methods, # (R0903): *Too few public methods (%s/%s)* too-many-ancestors, # (R0901): *Too many ancestors (%s/%s)* too-many-arguments, # (R0913): *Too many arguments (%s/%s)* too-many-boolean-expressions, # (R0916): *Too many boolean expressions in if statement (%s/%s)* too-many-branches, # (R0912): *Too many branches (%s/%s)* too-many-instance-attributes, # (R0902): *Too many instance attributes (%s/%s)* too-many-locals, # (R0914): *Too many local variables (%s/%s)* too-many-nested-blocks, # (R1702): *Too many nested blocks (%s/%s)* too-many-public-methods, # (R0904): *Too many public methods (%s/%s)* too-many-return-statements, # (R0911): *Too many return statements (%s/%s)* too-many-statements, # (R0915): *Too many statements (%s/%s)* unspecified-encoding, # (W1514): Using open without explicitly specifying an encoding use-a-generator, # (R1729): *Use a generator instead '%s(%s)'* useless-object-inheritance, # (R0205): *Class %r inherits from object, can be safely removed from bases in python3* useless-return, # (R1711): *Useless return at end of function or method* WALinuxAgent-2.9.1.1/ci/nosetests.sh000077500000000000000000000014201446033677600171660ustar00rootroot00000000000000#!/usr/bin/env bash set -u EXIT_CODE=0 echo "=========================================" echo "nosetests -a '!requires_sudo' output" echo "=========================================" nosetests -a '!requires_sudo' tests $NOSEOPTS || EXIT_CODE=$(($EXIT_CODE || $?)) echo EXIT_CODE no_sudo nosetests = $EXIT_CODE [[ -f .coverage ]] && \ sudo mv .coverage coverage.$(uuidgen).no_sudo.data echo "=========================================" echo "nosetests -a 'requires_sudo' output" echo "=========================================" sudo env "PATH=$PATH" nosetests -a 'requires_sudo' tests $NOSEOPTS || EXIT_CODE=$(($EXIT_CODE || $?)) echo EXIT_CODE with_sudo nosetests = $EXIT_CODE [[ -f .coverage ]] && \ sudo mv .coverage coverage.$(uuidgen).with_sudo.data exit "$EXIT_CODE" WALinuxAgent-2.9.1.1/config/000077500000000000000000000000001446033677600154555ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/config/66-azure-storage.rules000066400000000000000000000034451446033677600215600ustar00rootroot00000000000000# Azure specific rules. ACTION!="add|change", GOTO="walinuxagent_end" SUBSYSTEM!="block", GOTO="walinuxagent_end" ATTRS{ID_VENDOR}!="Msft", GOTO="walinuxagent_end" ATTRS{ID_MODEL}!="Virtual_Disk", GOTO="walinuxagent_end" # Match the known ID parts for root and resource disks. ATTRS{device_id}=="?00000000-0000-*", ENV{fabric_name}="root", GOTO="wa_azure_names" ATTRS{device_id}=="?00000000-0001-*", ENV{fabric_name}="resource", GOTO="wa_azure_names" # Gen2 disk. ATTRS{device_id}=="{f8b3781a-1e82-4818-a1c3-63d806ec15bb}", ENV{fabric_scsi_controller}="scsi0", GOTO="azure_datadisk" # Create symlinks for data disks attached. ATTRS{device_id}=="{f8b3781b-1e82-4818-a1c3-63d806ec15bb}", ENV{fabric_scsi_controller}="scsi1", GOTO="azure_datadisk" ATTRS{device_id}=="{f8b3781c-1e82-4818-a1c3-63d806ec15bb}", ENV{fabric_scsi_controller}="scsi2", GOTO="azure_datadisk" ATTRS{device_id}=="{f8b3781d-1e82-4818-a1c3-63d806ec15bb}", ENV{fabric_scsi_controller}="scsi3", GOTO="azure_datadisk" GOTO="walinuxagent_end" # Parse out the fabric n ame based off of scsi indicators. LABEL="azure_datadisk" ENV{DEVTYPE}=="partition", PROGRAM="/bin/sh -c 'readlink /sys/class/block/%k/../device|cut -d: -f4'", ENV{fabric_name}="$env{fabric_scsi_controller}/lun$result" ENV{DEVTYPE}=="disk", PROGRAM="/bin/sh -c 'readlink /sys/class/block/%k/device|cut -d: -f4'", ENV{fabric_name}="$env{fabric_scsi_controller}/lun$result" ENV{fabric_name}=="scsi0/lun0", ENV{fabric_name}="root" ENV{fabric_name}=="scsi0/lun1", ENV{fabric_name}="resource" # Don't create a symlink for the cd-rom. ENV{fabric_name}=="scsi0/lun2", GOTO="walinuxagent_end" # Create the symlinks. LABEL="wa_azure_names" ENV{DEVTYPE}=="disk", SYMLINK+="disk/azure/$env{fabric_name}" ENV{DEVTYPE}=="partition", SYMLINK+="disk/azure/$env{fabric_name}-part%n" LABEL="walinuxagent_end" WALinuxAgent-2.9.1.1/config/99-azure-product-uuid.rules000066400000000000000000000005271446033677600225440ustar00rootroot00000000000000SUBSYSTEM!="dmi", GOTO="product_uuid-exit" ATTR{sys_vendor}!="Microsoft Corporation", GOTO="product_uuid-exit" ATTR{product_name}!="Virtual Machine", GOTO="product_uuid-exit" TEST!="/sys/devices/virtual/dmi/id/product_uuid", GOTO="product_uuid-exit" RUN+="/bin/chmod 0444 /sys/devices/virtual/dmi/id/product_uuid" LABEL="product_uuid-exit" WALinuxAgent-2.9.1.1/config/alpine/000077500000000000000000000000001446033677600167255ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/config/alpine/waagent.conf000066400000000000000000000050641446033677600212270ustar00rootroot00000000000000# # Windows Azure Linux Agent Configuration # # Enable extension handling. Do not disable this unless you do not need password reset, # backup, monitoring, or any extension handling whatsoever. Extensions.Enabled=y # Which provisioning agent to use. Supported values are "auto" (default), "waagent", # "cloud-init", or "disabled". Provisioning.Agent=auto # Password authentication for root account will be unavailable. Provisioning.DeleteRootPassword=y # Generate fresh host key pair. Provisioning.RegenerateSshHostKeyPair=y # Supported values are "rsa", "dsa", "ecdsa", "ed25519", and "auto". # The "auto" option is supported on OpenSSH 5.9 (2011) and later. Provisioning.SshHostKeyPairType=rsa # Monitor host name changes and publish changes via DHCP requests. Provisioning.MonitorHostName=n # Decode CustomData from Base64. Provisioning.DecodeCustomData=y # Execute CustomData after provisioning. Provisioning.ExecuteCustomData=y # Format if unformatted. If 'n', resource disk will not be mounted. ResourceDisk.Format=y # File system on the resource disk # Typically ext3 or ext4. FreeBSD images should use 'ufs2' here. ResourceDisk.Filesystem=ext4 # Mount point for the resource disk ResourceDisk.MountPoint=/mnt/resource # Create and use swapfile on resource disk. ResourceDisk.EnableSwap=n # Size of the swapfile. ResourceDisk.SwapSizeMB=0 # Comma-separated list of mount options. See mount(8) for valid options. ResourceDisk.MountOptions=None # Respond to load balancer probes if requested by Windows Azure. LBProbeResponder=y # Enable logging to serial console (y|n) # When stdout is not enough... # 'y' if not set Logs.Console=y # Enable verbose logging (y|n) Logs.Verbose=n # Preferred network interface to communicate with Azure platform Network.Interface=eth0 # Is FIPS enabled OS.EnableFIPS=n # Root device timeout in seconds. OS.RootDeviceScsiTimeout=300 # If "None", the system default version is used. OS.OpensslPath=None # Set the path to SSH keys and configuration files OS.SshDir=/etc/ssh # Enable or disable goal state processing auto-update, default is enabled # AutoUpdate.Enabled=y # Determine the update family, this should not be changed # AutoUpdate.GAFamily=Prod # Determine if the overprovisioning feature is enabled. If yes, hold extension # handling until inVMArtifactsProfile.OnHold is false. # Default is enabled # EnableOverProvisioning=y # Allow fallback to HTTP if HTTPS is unavailable # Note: Allowing HTTP (vs. HTTPS) may cause security risks # OS.AllowHTTP=n # Add firewall rules to protect access to Azure host node services OS.EnableFirewall=y WALinuxAgent-2.9.1.1/config/arch/000077500000000000000000000000001446033677600163725ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/config/arch/waagent.conf000066400000000000000000000056421446033677600206760ustar00rootroot00000000000000# # Microsoft Azure Linux Agent Configuration # # Which provisioning agent to use. Supported values are "auto" (default), "waagent", # "cloud-init", or "disabled". Provisioning.Agent=auto # Password authentication for root account will be unavailable. Provisioning.DeleteRootPassword=n # Generate fresh host key pair. Provisioning.RegenerateSshHostKeyPair=y # Supported values are "rsa", "dsa", "ecdsa", "ed25519", and "auto". # The "auto" option is supported on OpenSSH 5.9 (2011) and later. Provisioning.SshHostKeyPairType=rsa # Monitor host name changes and publish changes via DHCP requests. Provisioning.MonitorHostName=y # Decode CustomData from Base64. Provisioning.DecodeCustomData=n # Execute CustomData after provisioning. Provisioning.ExecuteCustomData=n # Algorithm used by crypt when generating password hash. #Provisioning.PasswordCryptId=6 # Length of random salt used when generating password hash. #Provisioning.PasswordCryptSaltLength=10 # Allow reset password of sys user Provisioning.AllowResetSysUser=n # Format if unformatted. If 'n', resource disk will not be mounted. ResourceDisk.Format=y # File system on the resource disk # Typically ext3 or ext4. FreeBSD images should use 'ufs2' here. ResourceDisk.Filesystem=ext4 # Mount point for the resource disk ResourceDisk.MountPoint=/mnt/resource # Create and use swapfile on resource disk. ResourceDisk.EnableSwap=n # Size of the swapfile. ResourceDisk.SwapSizeMB=0 # Comma-separated list of mount options. See mount(8) for valid options. ResourceDisk.MountOptions=None # Respond to load balancer probes if requested by Windows Azure. LBProbeResponder=y # Enable verbose logging (y|n) Logs.Verbose=n # Enable Console logging, default is y # Logs.Console=y # Is FIPS enabled OS.EnableFIPS=n # Root device timeout in seconds. OS.RootDeviceScsiTimeout=300 # If "None", the system default version is used. OS.OpensslPath=None # Set the path to SSH keys and configuration files OS.SshDir=/etc/ssh # If set, agent will use proxy server to access internet #HttpProxy.Host=None #HttpProxy.Port=None # Detect Scvmm environment, default is n # DetectScvmmEnv=n # # Lib.Dir=/var/lib/waagent # # DVD.MountPoint=/mnt/cdrom/secure # # Pid.File=/var/run/waagent.pid # # Extension.LogDir=/var/log/azure # # Home.Dir=/home # Enable RDMA management and set up, should only be used in HPC images # OS.EnableRDMA=y # Enable or disable goal state processing auto-update, default is enabled # AutoUpdate.Enabled=y # Determine the update family, this should not be changed # AutoUpdate.GAFamily=Prod # Determine if the overprovisioning feature is enabled. If yes, hold extension # handling until inVMArtifactsProfile.OnHold is false. # Default is enabled # EnableOverProvisioning=y # Allow fallback to HTTP if HTTPS is unavailable # Note: Allowing HTTP (vs. HTTPS) may cause security risks # OS.AllowHTTP=n # Add firewall rules to protect access to Azure host node services OS.EnableFirewall=y WALinuxAgent-2.9.1.1/config/bigip/000077500000000000000000000000001446033677600165475ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/config/bigip/waagent.conf000066400000000000000000000054541446033677600210540ustar00rootroot00000000000000# # Windows Azure Linux Agent Configuration # # Specified program is invoked with the argument "Ready" when we report ready status # to the endpoint server. Role.StateConsumer=None # Specified program is invoked with XML file argument specifying role # configuration. Role.ConfigurationConsumer=None # Specified program is invoked with XML file argument specifying role topology. Role.TopologyConsumer=None # Enable extension handling. Do not disable this unless you do not need password reset, # backup, monitoring, or any extension handling whatsoever. Extensions.Enabled=y # Which provisioning agent to use. Supported values are "auto" (default), "waagent", # "cloud-init", or "disabled". Provisioning.Agent=auto # Password authentication for root account will be unavailable. Provisioning.DeleteRootPassword=y # Generate fresh host key pair. Provisioning.RegenerateSshHostKeyPair=y # Supported values are "rsa", "dsa", "ecdsa", "ed25519", and "auto". # The "auto" option is supported on OpenSSH 5.9 (2011) and later. Provisioning.SshHostKeyPairType=rsa # Monitor host name changes and publish changes via DHCP requests. # waagent cannot do this on BIG-IP VE Provisioning.MonitorHostName=n # Format if unformatted. If 'n', resource disk will not be mounted. ResourceDisk.Format=y # File system on the resource disk # Typically ext3 or ext4. FreeBSD images should use 'ufs2' here. ResourceDisk.Filesystem=ext4 # Mount point for the resource disk ResourceDisk.MountPoint=/mnt/resource # Create and use swapfile on resource disk. ResourceDisk.EnableSwap=n # Size of the swapfile. ResourceDisk.SwapSizeMB=0 # Respond to load balancer probes if requested by Windows Azure. LBProbeResponder=y # Enable verbose logging (y|n) Logs.Verbose=n # Enable Console logging, default is y # Logs.Console=y # Is FIPS enabled OS.EnableFIPS=n # Root device timeout in seconds. OS.RootDeviceScsiTimeout=300 # If "None", the system default version is used. OS.OpensslPath=None # Set the path to SSH keys and configuration files OS.SshDir=/etc/ssh # Specify location of waagent lib dir on BIG-IP Lib.Dir=/shared/vadc/azure/waagent/ # Specify location of sshd config file on BIG-IP OS.SshdConfigPath=/config/ssh/sshd_config # Disable RDMA management and set up OS.EnableRDMA=n # Enable or disable goal state processing auto-update, default is enabled AutoUpdate.Enabled=y # Determine the update family, this should not be changed # AutoUpdate.GAFamily=Prod # Determine if the overprovisioning feature is enabled. If yes, hold extension # handling until inVMArtifactsProfile.OnHold is false. # Default is enabled # EnableOverProvisioning=y # Allow fallback to HTTP if HTTPS is unavailable # Note: Allowing HTTP (vs. HTTPS) may cause security risks # OS.AllowHTTP=n # Add firewall rules to protect access to Azure host node services OS.EnableFirewall=y WALinuxAgent-2.9.1.1/config/clearlinux/000077500000000000000000000000001446033677600176235ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/config/clearlinux/waagent.conf000066400000000000000000000047471446033677600221340ustar00rootroot00000000000000# # Microsoft Azure Linux Agent Configuration # # Specified program is invoked with the argument "Ready" when we report ready status # to the endpoint server. Role.StateConsumer=None # Specified program is invoked with XML file argument specifying role # configuration. Role.ConfigurationConsumer=None # Specified program is invoked with XML file argument specifying role topology. Role.TopologyConsumer=None # Which provisioning agent to use. Supported values are "auto" (default), "waagent", # "cloud-init", or "disabled". Provisioning.Agent=auto # Password authentication for root account will be unavailable. Provisioning.DeleteRootPassword=y # Generate fresh host key pair. Provisioning.RegenerateSshHostKeyPair=y # Supported values are "rsa", "dsa", "ecdsa", "ed25519", and "auto". # The "auto" option is supported on OpenSSH 5.9 (2011) and later. Provisioning.SshHostKeyPairType=rsa # Monitor host name changes and publish changes via DHCP requests. Provisioning.MonitorHostName=y # Decode CustomData from Base64. Provisioning.DecodeCustomData=y # Execute CustomData after provisioning. Provisioning.ExecuteCustomData=n # Allow reset password of sys user Provisioning.AllowResetSysUser=n # Format if unformatted. If 'n', resource disk will not be mounted. ResourceDisk.Format=y # File system on the resource disk # Typically ext3 or ext4. FreeBSD images should use 'ufs2' here. ResourceDisk.Filesystem=ext4 # Mount point for the resource disk ResourceDisk.MountPoint=/mnt/resource # Create and use swapfile on resource disk. ResourceDisk.EnableSwap=n # Size of the swapfile. ResourceDisk.SwapSizeMB=0 # Enable verbose logging (y|n) Logs.Verbose=n # Enable Console logging, default is y # Logs.Console=y # Is FIPS enabled OS.EnableFIPS=n # Root device timeout in seconds. OS.RootDeviceScsiTimeout=300 # If "None", the system default version is used. OS.OpensslPath=None # Set the path to SSH keys and configuration files OS.SshDir=/etc/ssh # Enable or disable self-update, default is enabled AutoUpdate.Enabled=y AutoUpdate.GAFamily=Prod # Determine if the overprovisioning feature is enabled. If yes, hold extension # handling until inVMArtifactsProfile.OnHold is false. # Default is enabled # EnableOverProvisioning=y # Allow fallback to HTTP if HTTPS is unavailable # Note: Allowing HTTP (vs. HTTPS) may cause security risks # OS.AllowHTTP=n # Add firewall rules to protect access to Azure host node services # Note: # - The default is false to protect the state of existing VMs OS.EnableFirewall=y WALinuxAgent-2.9.1.1/config/coreos/000077500000000000000000000000001446033677600167475ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/config/coreos/waagent.conf000066400000000000000000000061171446033677600212510ustar00rootroot00000000000000# # Microsoft Azure Linux Agent Configuration # # Enable extension handling. Do not disable this unless you do not need password reset, # backup, monitoring, or any extension handling whatsoever. Extensions.Enabled=y # Which provisioning agent to use. Supported values are "auto" (default), "waagent", # "cloud-init", or "disabled". Provisioning.Agent=auto # Password authentication for root account will be unavailable. Provisioning.DeleteRootPassword=n # Generate fresh host key pair. Provisioning.RegenerateSshHostKeyPair=n # Supported values are "rsa", "dsa", "ecdsa", "ed25519", and "auto". # The "auto" option is supported on OpenSSH 5.9 (2011) and later. Provisioning.SshHostKeyPairType=ed25519 # Monitor host name changes and publish changes via DHCP requests. Provisioning.MonitorHostName=y # Decode CustomData from Base64. Provisioning.DecodeCustomData=n # Execute CustomData after provisioning. Provisioning.ExecuteCustomData=n # Algorithm used by crypt when generating password hash. #Provisioning.PasswordCryptId=6 # Length of random salt used when generating password hash. #Provisioning.PasswordCryptSaltLength=10 # Allow reset password of sys user Provisioning.AllowResetSysUser=n # Format if unformatted. If 'n', resource disk will not be mounted. ResourceDisk.Format=y # File system on the resource disk # Typically ext3 or ext4. FreeBSD images should use 'ufs2' here. ResourceDisk.Filesystem=ext4 # Mount point for the resource disk ResourceDisk.MountPoint=/mnt/resource # Create and use swapfile on resource disk. ResourceDisk.EnableSwap=n # Size of the swapfile. ResourceDisk.SwapSizeMB=0 # Comma-separated list of mount options. See mount(8) for valid options. ResourceDisk.MountOptions=None # Respond to load balancer probes if requested by Windows Azure. LBProbeResponder=y # Enable verbose logging (y|n) Logs.Verbose=n # Enable Console logging, default is y # Logs.Console=y # Is FIPS enabled OS.EnableFIPS=n # Set the path to SSH keys and configuration files OS.SshDir=/etc/ssh # Root device timeout in seconds. OS.RootDeviceScsiTimeout=300 # If "None", the system default version is used. OS.OpensslPath=None # If set, agent will use proxy server to access internet #HttpProxy.Host=None #HttpProxy.Port=None # Detect Scvmm environment, default is n # DetectScvmmEnv=n # # Lib.Dir=/var/lib/waagent # # DVD.MountPoint=/mnt/cdrom/secure # # Pid.File=/var/run/waagent.pid # # Extension.LogDir=/var/log/azure # # Home.Dir=/home # Enable RDMA management and set up, should only be used in HPC images # OS.EnableRDMA=y # Enable or disable goal state processing auto-update, default is enabled # AutoUpdate.Enabled=y # Determine the update family, this should not be changed # AutoUpdate.GAFamily=Prod # Determine if the overprovisioning feature is enabled. If yes, hold extension # handling until inVMArtifactsProfile.OnHold is false. # Default is enabled # EnableOverProvisioning=y # Allow fallback to HTTP if HTTPS is unavailable # Note: Allowing HTTP (vs. HTTPS) may cause security risks OS.AllowHTTP=y # Add firewall rules to protect access to Azure host node services OS.EnableFirewall=y WALinuxAgent-2.9.1.1/config/debian/000077500000000000000000000000001446033677600166775ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/config/debian/waagent.conf000066400000000000000000000064211446033677600211770ustar00rootroot00000000000000# # Microsoft Azure Linux Agent Configuration # # Enable extension handling. Do not disable this unless you do not need password reset, # backup, monitoring, or any extension handling whatsoever. Extensions.Enabled=y # Which provisioning agent to use. Supported values are "auto" (default), "waagent", # "cloud-init", or "disabled". Provisioning.Agent=auto # Password authentication for root account will be unavailable. Provisioning.DeleteRootPassword=y # Generate fresh host key pair. Provisioning.RegenerateSshHostKeyPair=y # Supported values are "rsa", "dsa", "ecdsa", "ed25519", and "auto". # The "auto" option is supported on OpenSSH 5.9 (2011) and later. Provisioning.SshHostKeyPairType=auto # Monitor host name changes and publish changes via DHCP requests. Provisioning.MonitorHostName=y # Decode CustomData from Base64. Provisioning.DecodeCustomData=n # Execute CustomData after provisioning. Provisioning.ExecuteCustomData=n # Algorithm used by crypt when generating password hash. #Provisioning.PasswordCryptId=6 # Length of random salt used when generating password hash. #Provisioning.PasswordCryptSaltLength=10 # Allow reset password of sys user Provisioning.AllowResetSysUser=n # Format if unformatted. If 'n', resource disk will not be mounted. ResourceDisk.Format=y # File system on the resource disk # Typically ext3 or ext4. FreeBSD images should use 'ufs2' here. ResourceDisk.Filesystem=ext4 # Mount point for the resource disk ResourceDisk.MountPoint=/mnt/resource # Create and use swapfile on resource disk. ResourceDisk.EnableSwap=n # Size of the swapfile. ResourceDisk.SwapSizeMB=0 # Comma-separated list of mount options. See mount(8) for valid options. ResourceDisk.MountOptions=None # Enable verbose logging (y|n) Logs.Verbose=n # Enable Console logging, default is y # Logs.Console=y # Enable periodic log collection, default is y Logs.Collect=y # How frequently to collect logs, default is each hour Logs.CollectPeriod=3600 # Is FIPS enabled OS.EnableFIPS=n # Root device timeout in seconds. OS.RootDeviceScsiTimeout=300 # If "None", the system default version is used. OS.OpensslPath=None # Set the SSH ClientAliveInterval # OS.SshClientAliveInterval=180 # Set the path to SSH keys and configuration files OS.SshDir=/etc/ssh # If set, agent will use proxy server to access internet #HttpProxy.Host=None #HttpProxy.Port=None # Detect Scvmm environment, default is n # DetectScvmmEnv=n # # Lib.Dir=/var/lib/waagent # # DVD.MountPoint=/mnt/cdrom/secure # # Pid.File=/var/run/waagent.pid # # Extension.LogDir=/var/log/azure # # Home.Dir=/home # Enable RDMA management and set up, should only be used in HPC images # OS.EnableRDMA=y # Enable or disable goal state processing auto-update, default is enabled # AutoUpdate.Enabled=y # Determine the update family, this should not be changed # AutoUpdate.GAFamily=Prod # Determine if the overprovisioning feature is enabled. If yes, hold extension # handling until inVMArtifactsProfile.OnHold is false. # Default is enabled # EnableOverProvisioning=y # Allow fallback to HTTP if HTTPS is unavailable # Note: Allowing HTTP (vs. HTTPS) may cause security risks # OS.AllowHTTP=n # Add firewall rules to protect access to Azure host node services # Note: # - The default is false to protect the state of existing VMs OS.EnableFirewall=n WALinuxAgent-2.9.1.1/config/devuan/000077500000000000000000000000001446033677600167375ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/config/devuan/waagent.conf000066400000000000000000000064721446033677600212450ustar00rootroot00000000000000# # Microsoft Azure Linux Agent Configuration # # Enable extension handling. Do not disable this unless you do not need password reset, # backup, monitoring, or any extension handling whatsoever. Extensions.Enabled=y # Which provisioning agent to use. Supported values are "auto" (default), "waagent", # "cloud-init", or "disabled". Provisioning.Agent=auto # Password authentication for root account will be unavailable. Provisioning.DeleteRootPassword=y # Generate fresh host key pair. Provisioning.RegenerateSshHostKeyPair=y # Supported values are "rsa", "dsa", "ecdsa", "ed25519", and "auto". # The "auto" option is supported on OpenSSH 5.9 (2011) and later. Provisioning.SshHostKeyPairType=auto # Monitor host name changes and publish changes via DHCP requests. Provisioning.MonitorHostName=y # Decode CustomData from Base64. Provisioning.DecodeCustomData=n # Execute CustomData after provisioning. Provisioning.ExecuteCustomData=n # Algorithm used by crypt when generating password hash. #Provisioning.PasswordCryptId=6 # Length of random salt used when generating password hash. #Provisioning.PasswordCryptSaltLength=10 # Allow reset password of sys user Provisioning.AllowResetSysUser=n # Format if unformatted. If 'n', resource disk will not be mounted. ResourceDisk.Format=y # File system on the resource disk # Typically ext3 or ext4. FreeBSD images should use 'ufs2' here. ResourceDisk.Filesystem=ext4 # Mount point for the resource disk ResourceDisk.MountPoint=/mnt/resource # Create and use swapfile on resource disk. ResourceDisk.EnableSwap=n # Size of the swapfile. ResourceDisk.SwapSizeMB=0 # Comma-separated list of mount options. See mount(8) for valid options. ResourceDisk.MountOptions=None # Enable verbose logging (y|n) Logs.Verbose=n # Enable Console logging, default is y # Logs.Console=y # Is FIPS enabled OS.EnableFIPS=n # Root device timeout in seconds. OS.RootDeviceScsiTimeout=300 # If "None", the system default version is used. OS.OpensslPath=None # Set the SSH ClientAliveInterval # OS.SshClientAliveInterval=180 # Set the path to SSH keys and configuration files OS.SshDir=/etc/ssh # If set, agent will use proxy server to access internet #HttpProxy.Host=None #HttpProxy.Port=None # Detect Scvmm environment, default is n # DetectScvmmEnv=n # # Lib.Dir=/var/lib/waagent # # DVD.MountPoint=/mnt/cdrom/secure # # Pid.File=/var/run/waagent.pid # # Extension.LogDir=/var/log/azure # # Home.Dir=/home # Enable RDMA management and set up, should only be used in HPC images # OS.EnableRDMA=y # Enable or disable goal state processing auto-update, default is enabled # AutoUpdate.Enabled=y # Determine the update family, this should not be changed # AutoUpdate.GAFamily=Prod # Determine if the overprovisioning feature is enabled. If yes, hold extension # handling until inVMArtifactsProfile.OnHold is false. # Default is enabled # EnableOverProvisioning=y # Allow fallback to HTTP if HTTPS is unavailable # Note: Allowing HTTP (vs. HTTPS) may cause security risks # OS.AllowHTTP=n # Add firewall rules to protect access to Azure host node services # Note: # - The default is false to protect the state of existing VMs OS.EnableFirewall=y # Enforce control groups limits on the agent and extensions CGroups.EnforceLimits=n # CGroups which are excluded from limits, comma separated CGroups.Excluded=customscript,runcommand WALinuxAgent-2.9.1.1/config/freebsd/000077500000000000000000000000001446033677600170675ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/config/freebsd/waagent.conf000066400000000000000000000060011446033677600213610ustar00rootroot00000000000000# # Microsoft Azure Linux Agent Configuration # # Enable extension handling. Do not disable this unless you do not need password reset, # backup, monitoring, or any extension handling whatsoever. Extensions.Enabled=y # Which provisioning agent to use. Supported values are "auto" (default), "waagent", # "cloud-init", or "disabled". Provisioning.Agent=auto # Password authentication for root account will be unavailable. Provisioning.DeleteRootPassword=y # Generate fresh host key pair. Provisioning.RegenerateSshHostKeyPair=y # Supported values are "rsa", "dsa", "ecdsa", "ed25519", and "auto". # The "auto" option is supported on OpenSSH 5.9 (2011) and later. Provisioning.SshHostKeyPairType=rsa # Monitor host name changes and publish changes via DHCP requests. Provisioning.MonitorHostName=y # Decode CustomData from Base64. Provisioning.DecodeCustomData=n # Execute CustomData after provisioning. Provisioning.ExecuteCustomData=n # Algorithm used by crypt when generating password hash. #Provisioning.PasswordCryptId=6 # Length of random salt used when generating password hash. #Provisioning.PasswordCryptSaltLength=10 # Format if unformatted. If 'n', resource disk will not be mounted. ResourceDisk.Format=y # File system on the resource disk # Typically ext3 or ext4. FreeBSD images should use 'ufs' here. ResourceDisk.Filesystem=ufs # Mount point for the resource disk ResourceDisk.MountPoint=/mnt/resource # Create and use swapfile on resource disk. ResourceDisk.EnableSwap=y # Size of the swapfile. ResourceDisk.SwapSizeMB=16384 # Comma-separated list of mount options. See mount(8) for valid options. ResourceDisk.MountOptions=None # Enable verbose logging (y|n) Logs.Verbose=n # Enable Console logging, default is y # Logs.Console=y # Is FIPS enabled OS.EnableFIPS=n # Root device timeout in seconds. OS.RootDeviceScsiTimeout=300 # If "None", the system default version is used. OS.OpensslPath=None # Set the path to SSH keys and configuration files OS.SshDir=/etc/ssh OS.PasswordPath=/etc/master.passwd OS.SudoersDir=/usr/local/etc/sudoers.d # If set, agent will use proxy server to access internet #HttpProxy.Host=None #HttpProxy.Port=None # Detect Scvmm environment, default is n # DetectScvmmEnv=n # # Lib.Dir=/var/lib/waagent # # DVD.MountPoint=/mnt/cdrom/secure # # Pid.File=/var/run/waagent.pid # # Extension.LogDir=/var/log/azure # # Home.Dir=/home # Enable RDMA management and set up, should only be used in HPC images # OS.EnableRDMA=y # Enable or disable goal state processing auto-update, default is enabled # AutoUpdate.Enabled=y # Determine the update family, this should not be changed # AutoUpdate.GAFamily=Prod # Determine if the overprovisioning feature is enabled. If yes, hold extension # handling until inVMArtifactsProfile.OnHold is false. # Default is enabled # EnableOverProvisioning=y # Allow fallback to HTTP if HTTPS is unavailable # Note: Allowing HTTP (vs. HTTPS) may cause security risks # OS.AllowHTTP=n # Add firewall rules to protect access to Azure host node services OS.EnableFirewall=y WALinuxAgent-2.9.1.1/config/gaia/000077500000000000000000000000001446033677600163565ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/config/gaia/waagent.conf000066400000000000000000000060111446033677600206510ustar00rootroot00000000000000# # Microsoft Azure Linux Agent Configuration # # Enable extension handling. Do not disable this unless you do not need password reset, # backup, monitoring, or any extension handling whatsoever. Extensions.Enabled=y # Which provisioning agent to use. Supported values are "auto" (default), "waagent", # "cloud-init", or "disabled". Provisioning.Agent=auto # Password authentication for root account will be unavailable. Provisioning.DeleteRootPassword=n # Generate fresh host key pair. Provisioning.RegenerateSshHostKeyPair=n # Supported values are "rsa", "dsa", "ecdsa", "ed25519", and "auto". # The "auto" option is supported on OpenSSH 5.9 (2011) and later. Provisioning.SshHostKeyPairType=rsa # Monitor host name changes and publish changes via DHCP requests. Provisioning.MonitorHostName=n # Decode CustomData from Base64. Provisioning.DecodeCustomData=y # Execute CustomData after provisioning. Provisioning.ExecuteCustomData=n # Algorithm used by crypt when generating password hash. Provisioning.PasswordCryptId=1 # Length of random salt used when generating password hash. #Provisioning.PasswordCryptSaltLength=10 # Allow reset password of sys user Provisioning.AllowResetSysUser=y # Format if unformatted. If 'n', resource disk will not be mounted. ResourceDisk.Format=y # File system on the resource disk # Typically ext3 or ext4. FreeBSD images should use 'ufs2' here. ResourceDisk.Filesystem=ext3 # Mount point for the resource disk ResourceDisk.MountPoint=/mnt/resource # Create and use swapfile on resource disk. ResourceDisk.EnableSwap=y # Size of the swapfile. ResourceDisk.SwapSizeMB=1024 # Comma-separated list of mount options. See mount(8) for valid options. ResourceDisk.MountOptions=None # Enable verbose logging (y|n) Logs.Verbose=n # Enable Console logging, default is y # Logs.Console=y # Is FIPS enabled OS.EnableFIPS=n # Root device timeout in seconds. OS.RootDeviceScsiTimeout=300 # If "None", the system default version is used. OS.OpensslPath=/var/lib/waagent/openssl # Set the path to SSH keys and configuration files OS.SshDir=/etc/ssh # If set, agent will use proxy server to access internet #HttpProxy.Host=None #HttpProxy.Port=None # Detect Scvmm environment, default is n # DetectScvmmEnv=n # # Lib.Dir=/var/lib/waagent # # DVD.MountPoint=/mnt/cdrom/secure # # Pid.File=/var/run/waagent.pid # # Extension.LogDir=/var/log/azure # # Home.Dir=/home # Enable RDMA management and set up, should only be used in HPC images OS.EnableRDMA=n # Enable or disable goal state processing auto-update, default is enabled AutoUpdate.Enabled=n # Determine the update family, this should not be changed # AutoUpdate.GAFamily=Prod # Determine if the overprovisioning feature is enabled. If yes, hold extension # handling until inVMArtifactsProfile.OnHold is false. # Default is enabled # EnableOverProvisioning=y # Allow fallback to HTTP if HTTPS is unavailable # Note: Allowing HTTP (vs. HTTPS) may cause security risks # OS.AllowHTTP=n # Add firewall rules to protect access to Azure host node services OS.EnableFirewall=y WALinuxAgent-2.9.1.1/config/iosxe/000077500000000000000000000000001446033677600166045ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/config/iosxe/waagent.conf000066400000000000000000000057241446033677600211110ustar00rootroot00000000000000# # Microsoft Azure Linux Agent Configuration # # Which provisioning agent to use. Supported values are "auto" (default), "waagent", # "cloud-init", or "disabled". Provisioning.Agent=auto # Password authentication for root account will be unavailable. Provisioning.DeleteRootPassword=y # Generate fresh host key pair. Provisioning.RegenerateSshHostKeyPair=n # Supported values are "rsa", "dsa", "ecdsa", "ed25519", and "auto". # The "auto" option is supported on OpenSSH 5.9 (2011) and later. Provisioning.SshHostKeyPairType=rsa # Monitor host name changes and publish changes via DHCP requests. Provisioning.MonitorHostName=n # Decode CustomData from Base64. Provisioning.DecodeCustomData=n # Execute CustomData after provisioning. Provisioning.ExecuteCustomData=n # Algorithm used by crypt when generating password hash. #Provisioning.PasswordCryptId=6 # Length of random salt used when generating password hash. #Provisioning.PasswordCryptSaltLength=10 # Allow reset password of sys user Provisioning.AllowResetSysUser=n # Format if unformatted. If 'n', resource disk will not be mounted. ResourceDisk.Format=n # File system on the resource disk # Typically ext3 or ext4. FreeBSD images should use 'ufs2' here. ResourceDisk.Filesystem=ext4 # Mount point for the resource disk ResourceDisk.MountPoint=/mnt/resource # Create and use swapfile on resource disk. ResourceDisk.EnableSwap=n # Size of the swapfile. ResourceDisk.SwapSizeMB=0 # Comma-separated list of mount options. See mount(8) for valid options. ResourceDisk.MountOptions=None # Enable verbose logging (y|n) Logs.Verbose=n # Enable Console logging, default is y # Logs.Console=y # Is FIPS enabled OS.EnableFIPS=n # Root device timeout in seconds. OS.RootDeviceScsiTimeout=300 # If "None", the system default version is used. OS.OpensslPath=None # Set the SSH ClientAliveInterval # OS.SshClientAliveInterval=180 # Set the path to SSH keys and configuration files OS.SshDir=/etc/ssh # If set, agent will use proxy server to access internet #HttpProxy.Host=None #HttpProxy.Port=None # Detect Scvmm environment, default is n # DetectScvmmEnv=n # # Lib.Dir=/var/lib/waagent # # DVD.MountPoint=/mnt/cdrom/secure # # Pid.File=/var/run/waagent.pid # # Extension.LogDir=/var/log/azure # # Home.Dir=/home # Enable RDMA management and set up, should only be used in HPC images # OS.EnableRDMA=y # Enable or disable goal state processing auto-update, default is enabled AutoUpdate.Enabled=y # Determine the update family, this should not be changed # AutoUpdate.GAFamily=Prod # Determine if the overprovisioning feature is enabled. If yes, hold extension # handling until inVMArtifactsProfile.OnHold is false. # Default is enabled # EnableOverProvisioning=y # Allow fallback to HTTP if HTTPS is unavailable # Note: Allowing HTTP (vs. HTTPS) may cause security risks # OS.AllowHTTP=n # Add firewall rules to protect access to Azure host node services # Note: # - The default is false to protect the state of existing VMs OS.EnableFirewall=y WALinuxAgent-2.9.1.1/config/mariner/000077500000000000000000000000001446033677600171125ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/config/mariner/waagent.conf000066400000000000000000000045601446033677600214140ustar00rootroot00000000000000# Microsoft Azure Linux Agent Configuration # # Enable extension handling. Do not disable this unless you do not need password reset, # backup, monitoring, or any extension handling whatsoever. Extensions.Enabled=y # Which provisioning agent to use. Supported values are "auto" (default), "waagent", # "cloud-init", or "disabled". Provisioning.Agent=auto # Specified program is invoked with the argument "Ready" when we report ready status # to the endpoint server. Role.StateConsumer=None # Specified program is invoked with XML file argument specifying role # configuration. Role.ConfigurationConsumer=None # Specified program is invoked with XML file argument specifying role topology. Role.TopologyConsumer=None # Enable instance creation Provisioning.Enabled=n # Password authentication for root account will be unavailable. Provisioning.DeleteRootPassword=y # Generate fresh host key pair. Provisioning.RegenerateSshHostKeyPair=y # Supported values are "rsa", "dsa" and "ecdsa". Provisioning.SshHostKeyPairType=rsa # Monitor host name changes and publish changes via DHCP requests. Provisioning.MonitorHostName=y # Decode CustomData from Base64. Provisioning.DecodeCustomData=y # Execute CustomData after provisioning. Provisioning.ExecuteCustomData=n # Allow reset password of sys user Provisioning.AllowResetSysUser=n # Format if unformatted. If 'n', resource disk will not be mounted. ResourceDisk.Format=n # File system on the resource disk # Typically ext3 or ext4. FreeBSD images should use 'ufs2' here. ResourceDisk.Filesystem=ext4 # Mount point for the resource disk ResourceDisk.MountPoint=/mnt/resource # Create and use swapfile on resource disk. ResourceDisk.EnableSwap=n # Size of the swapfile. ResourceDisk.SwapSizeMB=0 # Enable verbose logging (y|n) Logs.Verbose=n # Is FIPS enabled OS.EnableFIPS=n # Root device timeout in seconds. OS.RootDeviceScsiTimeout=300 # If "None", the system default version is used. OS.OpensslPath=None # Set the path to SSH keys and configuration files OS.SshDir=/etc/ssh # Enable or disable self-update, default is enabled AutoUpdate.Enabled=y AutoUpdate.GAFamily=Prod # Determine if the overprovisioning feature is enabled. If yes, hold extension # handling until inVMArtifactsProfile.OnHold is false. # Default is disabled # EnableOverProvisioning=n # Add firewall rules to protect access to Azure host node services OS.EnableFirewall=y WALinuxAgent-2.9.1.1/config/nsbsd/000077500000000000000000000000001446033677600165665ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/config/nsbsd/waagent.conf000066400000000000000000000056441446033677600210740ustar00rootroot00000000000000# # Microsoft Azure Linux Agent Configuration # # Which provisioning agent to use. Supported values are "auto" (default), "waagent", # "cloud-init", or "disabled". Provisioning.Agent=auto # Password authentication for root account will be unavailable. Provisioning.DeleteRootPassword=n # Generate fresh host key pair. Provisioning.RegenerateSshHostKeyPair=n # Supported values are "rsa", "dsa", "ecdsa", "ed25519", and "auto". # The "auto" option is supported on OpenSSH 5.9 (2011) and later. Provisioning.SshHostKeyPairType=rsa # Monitor host name changes and publish changes via DHCP requests. Provisioning.MonitorHostName=y # Decode CustomData from Base64. Provisioning.DecodeCustomData=n # Execute CustomData after provisioning. Provisioning.ExecuteCustomData=n # Algorithm used by crypt when generating password hash. #Provisioning.PasswordCryptId=6 # Length of random salt used when generating password hash. #Provisioning.PasswordCryptSaltLength=10 # Format if unformatted. If 'n', resource disk will not be mounted. ResourceDisk.Format=n # File system on the resource disk # Typically ext3 or ext4. FreeBSD images should use 'ufs' here. ResourceDisk.Filesystem=ufs # Mount point for the resource disk ResourceDisk.MountPoint=/mnt/resource # Create and use swapfile on resource disk. ResourceDisk.EnableSwap=n # Size of the swapfile. ResourceDisk.SwapSizeMB=0 # Comma-separated list of mount options. See mount(8) for valid options. ResourceDisk.MountOptions=None # Enable verbose logging (y|n) TODO set n Logs.Verbose=n # Enable Console logging, default is y # Logs.Console=y # Is FIPS enabled OS.EnableFIPS=n # Root device timeout in seconds. OS.RootDeviceScsiTimeout=300 # If "None", the system default version is used. OS.OpensslPath=None # Set the path to SSH keys and configuration files OS.SshDir=/etc/ssh OS.PasswordPath=/etc/master.passwd OS.SudoersDir=/usr/local/etc/sudoers.d # If set, agent will use proxy server to access internet #HttpProxy.Host=None #HttpProxy.Port=None # Detect Scvmm environment, default is n # DetectScvmmEnv=n # Lib.Dir=/usr/Firewall/var/waagent # # DVD.MountPoint=/mnt/cdrom/secure # # Pid.File=/var/run/waagent.pid # Extension.LogDir=/log/azure # # Home.Dir=/home # Enable RDMA management and set up, should only be used in HPC images # OS.EnableRDMA=y # Enable or disable goal state processing auto-update, default is enabled AutoUpdate.Enabled=n # Determine the update family, this should not be changed # AutoUpdate.GAFamily=Prod # Determine if the overprovisioning feature is enabled. If yes, hold extension # handling until inVMArtifactsProfile.OnHold is false. # Default is disabled # EnableOverProvisioning=n # Allow fallback to HTTP if HTTPS is unavailable # Note: Allowing HTTP (vs. HTTPS) may cause security risks # OS.AllowHTTP=n # Add firewall rules to protect access to Azure host node services # Note: # - The default is false to protect the state of existing VMs OS.EnableFirewall=n WALinuxAgent-2.9.1.1/config/openbsd/000077500000000000000000000000001446033677600171075ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/config/openbsd/waagent.conf000066400000000000000000000056311446033677600214110ustar00rootroot00000000000000# # Microsoft Azure Linux Agent Configuration # # Which provisioning agent to use. Supported values are "auto" (default), "waagent", # "cloud-init", or "disabled". Provisioning.Agent=auto # Password authentication for root account will be unavailable. Provisioning.DeleteRootPassword=y # Generate fresh host key pair. Provisioning.RegenerateSshHostKeyPair=y # Supported values are "rsa", "dsa", "ecdsa", "ed25519", and "auto". # The "auto" option is supported on OpenSSH 5.9 (2011) and later. Provisioning.SshHostKeyPairType=auto # Monitor host name changes and publish changes via DHCP requests. Provisioning.MonitorHostName=y # Decode CustomData from Base64. Provisioning.DecodeCustomData=n # Execute CustomData after provisioning. Provisioning.ExecuteCustomData=n # Algorithm used by crypt when generating password hash. #Provisioning.PasswordCryptId=6 # Length of random salt used when generating password hash. #Provisioning.PasswordCryptSaltLength=10 # Format if unformatted. If 'n', resource disk will not be mounted. ResourceDisk.Format=y # File system on the resource disk # Typically ext3 or ext4. OpenBSD images should use 'ufs2' here. ResourceDisk.Filesystem=ufs2 # Mount point for the resource disk ResourceDisk.MountPoint=/mnt/resource # Create and use swapfile on resource disk. ResourceDisk.EnableSwap=y # Max size of the swap partition in MB ResourceDisk.SwapSizeMB=65536 # Comma-separated list of mount options. See mount(8) for valid options. ResourceDisk.MountOptions=None # Enable verbose logging (y|n) Logs.Verbose=n # Enable Console logging, default is y # Logs.Console=y # Is FIPS enabled OS.EnableFIPS=n # Root device timeout in seconds. OS.RootDeviceScsiTimeout=300 # If "None", the system default version is used. OS.OpensslPath=/usr/local/bin/eopenssl # Set the path to SSH keys and configuration files OS.SshDir=/etc/ssh OS.PasswordPath=/etc/master.passwd # If set, agent will use proxy server to access internet #HttpProxy.Host=None #HttpProxy.Port=None # Detect Scvmm environment, default is n # DetectScvmmEnv=n # # Lib.Dir=/var/lib/waagent # # DVD.MountPoint=/mnt/cdrom/secure # # Pid.File=/var/run/waagent.pid # # Extension.LogDir=/var/log/azure # # Home.Dir=/home # Enable RDMA management and set up, should only be used in HPC images # OS.EnableRDMA=y # Enable or disable goal state processing auto-update, default is enabled # AutoUpdate.Enabled=y # Determine the update family, this should not be changed # AutoUpdate.GAFamily=Prod # Determine if the overprovisioning feature is enabled. If yes, hold extension # handling until inVMArtifactsProfile.OnHold is false. # Default is enabled # EnableOverProvisioning=y # Allow fallback to HTTP if HTTPS is unavailable # Note: Allowing HTTP (vs. HTTPS) may cause security risks # OS.AllowHTTP=n # Add firewall rules to protect access to Azure host node services # Note: # - The default is false to protect the state of existing VMs OS.EnableFirewall=y WALinuxAgent-2.9.1.1/config/photonos/000077500000000000000000000000001446033677600173265ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/config/photonos/waagent.conf000066400000000000000000000040401446033677600216210ustar00rootroot00000000000000# Microsoft Azure Linux Agent Configuration # # Specified program is invoked with the argument "Ready" when we report ready status # to the endpoint server. Role.StateConsumer=None # Specified program is invoked with XML file argument specifying role # configuration. Role.ConfigurationConsumer=None # Specified program is invoked with XML file argument specifying role topology. Role.TopologyConsumer=None # Enable instance creation Provisioning.Enabled=n # Rely on cloud-init to provision Provisioning.UseCloudInit=y # Password authentication for root account will be unavailable. Provisioning.DeleteRootPassword=y # Generate fresh host key pair. Provisioning.RegenerateSshHostKeyPair=y # Supported values are "rsa", "dsa" and "ecdsa". Provisioning.SshHostKeyPairType=rsa # Monitor host name changes and publish changes via DHCP requests. Provisioning.MonitorHostName=y # Decode CustomData from Base64. Provisioning.DecodeCustomData=y # Execute CustomData after provisioning. Provisioning.ExecuteCustomData=n # Allow reset password of sys user Provisioning.AllowResetSysUser=n # Format if unformatted. If 'n', resource disk will not be mounted. ResourceDisk.Format=n # File system on the resource disk # Typically ext3 or ext4. FreeBSD images should use 'ufs2' here. ResourceDisk.Filesystem=ext4 # Mount point for the resource disk ResourceDisk.MountPoint=/mnt/resource # Create and use swapfile on resource disk. ResourceDisk.EnableSwap=n # Size of the swapfile. ResourceDisk.SwapSizeMB=0 # Enable verbose logging (y|n) Logs.Verbose=n # Is FIPS enabled OS.EnableFIPS=n # Root device timeout in seconds. OS.RootDeviceScsiTimeout=300 # If "None", the system default version is used. OS.OpensslPath=None # Set the path to SSH keys and configuration files OS.SshDir=/etc/ssh # Enable or disable self-update, default is enabled AutoUpdate.Enabled=y AutoUpdate.GAFamily=Prod # Determine if the overprovisioning feature is enabled. If yes, hold extension # handling until inVMArtifactsProfile.OnHold is false. # Default is disabled # EnableOverProvisioning=n WALinuxAgent-2.9.1.1/config/suse/000077500000000000000000000000001446033677600164345ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/config/suse/waagent.conf000066400000000000000000000064571446033677600207450ustar00rootroot00000000000000# # Microsoft Azure Linux Agent Configuration # # Enable extension handling. Do not disable this unless you do not need password reset, # backup, monitoring, or any extension handling whatsoever. Extensions.Enabled=y # Which provisioning agent to use. Supported values are "auto" (default), "waagent", # "cloud-init", or "disabled". Provisioning.Agent=auto # Password authentication for root account will be unavailable. Provisioning.DeleteRootPassword=y # Generate fresh host key pair. Provisioning.RegenerateSshHostKeyPair=y # Supported values are "rsa", "dsa", "ecdsa", "ed25519", and "auto". # The "auto" option is supported on OpenSSH 5.9 (2011) and later. Provisioning.SshHostKeyPairType=rsa # Monitor host name changes and publish changes via DHCP requests. Provisioning.MonitorHostName=y # Decode CustomData from Base64. Provisioning.DecodeCustomData=n # Execute CustomData after provisioning. Provisioning.ExecuteCustomData=n # Algorithm used by crypt when generating password hash. #Provisioning.PasswordCryptId=6 # Length of random salt used when generating password hash. #Provisioning.PasswordCryptSaltLength=10 # Allow reset password of sys user Provisioning.AllowResetSysUser=n # Format if unformatted. If 'n', resource disk will not be mounted. ResourceDisk.Format=y # File system on the resource disk # Typically ext3 or ext4. FreeBSD images should use 'ufs2' here. ResourceDisk.Filesystem=btrfs # Mount point for the resource disk ResourceDisk.MountPoint=/mnt/resource # Create and use swapfile on resource disk. ResourceDisk.EnableSwap=n # Size of the swapfile. ResourceDisk.SwapSizeMB=0 # Comma-separated list of mount options. See mount(8) for valid options. ResourceDisk.MountOptions=compress=lzo # Respond to load balancer probes if requested by Microsoft Azure. LBProbeResponder=y # Enable verbose logging (y|n) Logs.Verbose=n # Enable Console logging, default is y # Logs.Console=y # Enable periodic log collection, default is y Logs.Collect=y # How frequently to collect logs, default is each hour Logs.CollectPeriod=3600 # Is FIPS enabled OS.EnableFIPS=n # Root device timeout in seconds. OS.RootDeviceScsiTimeout=300 # If "None", the system default version is used. OS.OpensslPath=None # Set the path to SSH keys and configuration files OS.SshDir=/etc/ssh # If set, agent will use proxy server to access internet #HttpProxy.Host=None #HttpProxy.Port=None # Detect Scvmm environment, default is n # DetectScvmmEnv=n # # Lib.Dir=/var/lib/waagent # # DVD.MountPoint=/mnt/cdrom/secure # # Pid.File=/var/run/waagent.pid # # Extension.LogDir=/var/log/azure # # Home.Dir=/home # Enable RDMA management and set up, should only be used in HPC images # OS.EnableRDMA=y # Enable checking RDMA driver version and update # OS.CheckRdmaDriver=y # Enable or disable goal state processing auto-update, default is enabled # AutoUpdate.Enabled=y # Determine the update family, this should not be changed # AutoUpdate.GAFamily=Prod # Determine if the overprovisioning feature is enabled. If yes, hold extension # handling until inVMArtifactsProfile.OnHold is false. # Default is enabled # EnableOverProvisioning=y # Allow fallback to HTTP if HTTPS is unavailable # Note: Allowing HTTP (vs. HTTPS) may cause security risks # OS.AllowHTTP=n # Add firewall rules to protect access to Azure host node services OS.EnableFirewall=y WALinuxAgent-2.9.1.1/config/ubuntu/000077500000000000000000000000001446033677600167775ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/config/ubuntu/waagent.conf000066400000000000000000000063261446033677600213030ustar00rootroot00000000000000# # Microsoft Azure Linux Agent Configuration # # Enable extension handling. Do not disable this unless you do not need password reset, # backup, monitoring, or any extension handling whatsoever. Extensions.Enabled=y # Which provisioning agent to use. Supported values are "auto" (default), "waagent", # "cloud-init", or "disabled". Provisioning.Agent=auto # Password authentication for root account will be unavailable. Provisioning.DeleteRootPassword=y # Generate fresh host key pair. Provisioning.RegenerateSshHostKeyPair=n # Supported values are "rsa", "dsa", "ecdsa", "ed25519", and "auto". # The "auto" option is supported on OpenSSH 5.9 (2011) and later. Provisioning.SshHostKeyPairType=rsa # Monitor host name changes and publish changes via DHCP requests. Provisioning.MonitorHostName=n # Decode CustomData from Base64. Provisioning.DecodeCustomData=n # Execute CustomData after provisioning. Provisioning.ExecuteCustomData=n # Algorithm used by crypt when generating password hash. #Provisioning.PasswordCryptId=6 # Length of random salt used when generating password hash. #Provisioning.PasswordCryptSaltLength=10 # Allow reset password of sys user Provisioning.AllowResetSysUser=n # Format if unformatted. If 'n', resource disk will not be mounted. ResourceDisk.Format=n # File system on the resource disk # Typically ext3 or ext4. FreeBSD images should use 'ufs2' here. ResourceDisk.Filesystem=ext4 # Mount point for the resource disk ResourceDisk.MountPoint=/mnt # Create and use swapfile on resource disk. ResourceDisk.EnableSwap=n # Size of the swapfile. ResourceDisk.SwapSizeMB=0 # Comma-separated list of mount options. See mount(8) for valid options. ResourceDisk.MountOptions=None # Respond to load balancer probes if requested by Microsoft Azure. LBProbeResponder=y # Enable verbose logging (y|n) Logs.Verbose=n # Enable Console logging, default is y # Logs.Console=y # Enable periodic log collection, default is y Logs.Collect=y # How frequently to collect logs, default is each hour Logs.CollectPeriod=3600 # Is FIPS enabled OS.EnableFIPS=n # Root device timeout in seconds. OS.RootDeviceScsiTimeout=300 # If "None", the system default version is used. OS.OpensslPath=None # Set the path to SSH keys and configuration files OS.SshDir=/etc/ssh # If set, agent will use proxy server to access internet #HttpProxy.Host=None #HttpProxy.Port=None # Detect Scvmm environment, default is n # DetectScvmmEnv=n # Enable RDMA management and set up, should only be used in HPC images # OS.EnableRDMA=y # Enable RDMA kernel update, this value is effective on Ubuntu # OS.UpdateRdmaDriver=y # Enable checking RDMA driver version and update # OS.CheckRdmaDriver=y # Enable or disable goal state processing auto-update, default is enabled # AutoUpdate.Enabled=y # Determine the update family, this should not be changed # AutoUpdate.GAFamily=Prod # Determine if the overprovisioning feature is enabled. If yes, hold extension # handling until inVMArtifactsProfile.OnHold is false. # Default is enabled # EnableOverProvisioning=y # Allow fallback to HTTP if HTTPS is unavailable # Note: Allowing HTTP (vs. HTTPS) may cause security risks # OS.AllowHTTP=n # Add firewall rules to protect access to Azure host node services OS.EnableFirewall=y WALinuxAgent-2.9.1.1/config/waagent.conf000066400000000000000000000076441446033677600177650ustar00rootroot00000000000000# # Microsoft Azure Linux Agent Configuration # # Enable extension handling. Do not disable this unless you do not need password reset, # backup, monitoring, or any extension handling whatsoever. Extensions.Enabled=y # How often (in seconds) to poll for new goal states Extensions.GoalStatePeriod=6 # Which provisioning agent to use. Supported values are "auto" (default), "waagent", # "cloud-init", or "disabled". Provisioning.Agent=auto # Password authentication for root account will be unavailable. Provisioning.DeleteRootPassword=y # Generate fresh host key pair. Provisioning.RegenerateSshHostKeyPair=y # Supported values are "rsa", "dsa", "ecdsa", "ed25519", and "auto". # The "auto" option is supported on OpenSSH 5.9 (2011) and later. Provisioning.SshHostKeyPairType=rsa # Monitor host name changes and publish changes via DHCP requests. Provisioning.MonitorHostName=y # How often (in seconds) to monitor host name changes. Provisioning.MonitorHostNamePeriod=30 # Decode CustomData from Base64. Provisioning.DecodeCustomData=n # Execute CustomData after provisioning. Provisioning.ExecuteCustomData=n # Algorithm used by crypt when generating password hash. #Provisioning.PasswordCryptId=6 # Length of random salt used when generating password hash. #Provisioning.PasswordCryptSaltLength=10 # Allow reset password of sys user Provisioning.AllowResetSysUser=n # Format if unformatted. If 'n', resource disk will not be mounted. ResourceDisk.Format=y # File system on the resource disk # Typically ext3 or ext4. FreeBSD images should use 'ufs2' here. ResourceDisk.Filesystem=ext4 # Mount point for the resource disk ResourceDisk.MountPoint=/mnt/resource # Create and use swapfile on resource disk. ResourceDisk.EnableSwap=n # Size of the swapfile. ResourceDisk.SwapSizeMB=0 # Comma-separated list of mount options. See mount(8) for valid options. ResourceDisk.MountOptions=None # Enable verbose logging (y|n) Logs.Verbose=n # Enable Console logging, default is y # Logs.Console=y # Enable periodic log collection, default is y Logs.Collect=y # How frequently to collect logs, default is each hour Logs.CollectPeriod=3600 # Is FIPS enabled OS.EnableFIPS=n # Root device timeout in seconds. OS.RootDeviceScsiTimeout=300 # How often (in seconds) to set the root device timeout. OS.RootDeviceScsiTimeoutPeriod=30 # If "None", the system default version is used. OS.OpensslPath=None # Set the SSH ClientAliveInterval # OS.SshClientAliveInterval=180 # Set the path to SSH keys and configuration files OS.SshDir=/etc/ssh # If set, agent will use proxy server to access internet #HttpProxy.Host=None #HttpProxy.Port=None # Detect Scvmm environment, default is n # DetectScvmmEnv=n # # Lib.Dir=/var/lib/waagent # # DVD.MountPoint=/mnt/cdrom/secure # # Pid.File=/var/run/waagent.pid # # Extension.LogDir=/var/log/azure # # Home.Dir=/home # Enable RDMA management and set up, should only be used in HPC images # OS.EnableRDMA=y # Enable checking RDMA driver version and update # OS.CheckRdmaDriver=y # Enable or disable goal state processing auto-update, default is enabled # AutoUpdate.Enabled=y # Determine the update family, this should not be changed # AutoUpdate.GAFamily=Prod # Determine if the overprovisioning feature is enabled. If yes, hold extension # handling until inVMArtifactsProfile.OnHold is false. # Default is enabled # EnableOverProvisioning=y # Allow fallback to HTTP if HTTPS is unavailable # Note: Allowing HTTP (vs. HTTPS) may cause security risks # OS.AllowHTTP=n # Add firewall rules to protect access to Azure host node services OS.EnableFirewall=y # How often (in seconds) to check the firewall rules OS.EnableFirewallPeriod=30 # How often (in seconds) to remove the udev rules for persistent network interface names (75-persistent-net-generator.rules and /etc/udev/rules.d/70-persistent-net.rules) OS.RemovePersistentNetRulesPeriod=30 # How often (in seconds) to monitor for DHCP client restarts OS.MonitorDhcpClientRestartPeriod=30 WALinuxAgent-2.9.1.1/config/waagent.logrotate000066400000000000000000000016141446033677600210270ustar00rootroot00000000000000/var/log/waagent.log { # Old versions of log files are compressed with gzip by default. compress # Rotate log files > 20 MB, and keep last 50 archived files. With an compression ratio ranging from 5-10%, The # archived files would take an average of around 50-100 MB. Even for extremely chatty agent, the average size of # the compressed files would not go beyond ~2 MB per day. size 20M rotate 50 # Add date as extension when rotating logs dateext # Formatting the date extension to YYYY-MM-DD-SSSSSSS format. logrotate does not provide hours, mins and seconds # option. Adding the %s (system clock epoch time) to differentiate rotated log files within the same day. dateformat -%Y-%m-%d-%s # Do not rotate the log if it is empty notifempty # If the log file is missing, go on to the next one without issuing an error message. missingok }WALinuxAgent-2.9.1.1/init/000077500000000000000000000000001446033677600151535ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/init/arch/000077500000000000000000000000001446033677600160705ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/init/arch/waagent.service000066400000000000000000000005371446033677600211050ustar00rootroot00000000000000[Unit] Description=Azure Linux Agent Wants=network-online.target sshd.service sshd-keygen.service After=network-online.target ConditionFileIsExecutable=/usr/bin/waagent ConditionPathExists=/etc/waagent.conf [Service] Type=simple ExecStart=/usr/bin/python -u /usr/bin/waagent -daemon Restart=always RestartSec=5 [Install] WantedBy=multi-user.target WALinuxAgent-2.9.1.1/init/azure-vmextensions.slice000066400000000000000000000002141446033677600220570ustar00rootroot00000000000000[Unit] Description=Slice for Azure VM Extensions DefaultDependencies=no Before=slices.target [Slice] CPUAccounting=yes MemoryAccounting=yes WALinuxAgent-2.9.1.1/init/azure.slice000066400000000000000000000001471446033677600173240ustar00rootroot00000000000000[Unit] Description=Slice for Azure VM Agent and Extensions DefaultDependencies=no Before=slices.target WALinuxAgent-2.9.1.1/init/clearlinux/000077500000000000000000000000001446033677600173215ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/init/clearlinux/waagent.service000066400000000000000000000005661446033677600223400ustar00rootroot00000000000000[Unit] Description=Azure Linux Agent Wants=network-online.target sshd.service sshd-keygen.service After=network-online.target ConditionFileIsExecutable=/usr/bin/waagent ConditionPathExists=/usr/share/defaults/waagent/waagent.conf [Service] Type=simple ExecStart=/usr/bin/python -u /usr/bin/waagent -daemon Restart=always RestartSec=5 [Install] WantedBy=multi-user.target WALinuxAgent-2.9.1.1/init/coreos/000077500000000000000000000000001446033677600164455ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/init/coreos/cloud-config.yml000066400000000000000000000023511446033677600215420ustar00rootroot00000000000000#cloud-config coreos: units: - name: etcd.service runtime: true drop-ins: - name: 10-oem.conf content: | [Service] Environment=ETCD_PEER_ELECTION_TIMEOUT=1200 - name: etcd2.service runtime: true drop-ins: - name: 10-oem.conf content: | [Service] Environment=ETCD_ELECTION_TIMEOUT=1200 - name: waagent.service command: start runtime: true content: | [Unit] Description=Microsoft Azure Agent Wants=network-online.target sshd-keygen.service After=network-online.target sshd-keygen.service [Service] Type=simple Restart=always RestartSec=5s ExecStart=/usr/share/oem/python/bin/python /usr/share/oem/bin/waagent -daemon - name: oem-cloudinit.service command: restart runtime: yes content: | [Unit] Description=Cloudinit from Azure metadata [Service] Type=oneshot ExecStart=/usr/bin/coreos-cloudinit --oem=azure oem: id: azure name: Microsoft Azure version-id: 2.1.4 home-url: https://azure.microsoft.com/ bug-report-url: https://github.com/coreos/bugs/issues WALinuxAgent-2.9.1.1/init/devuan/000077500000000000000000000000001446033677600164355ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/init/devuan/default/000077500000000000000000000000001446033677600200615ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/init/devuan/default/walinuxagent000066400000000000000000000001321446033677600225060ustar00rootroot00000000000000# To disable the Microsoft Azure Agent, set WALINUXAGENT_ENABLED=0 WALINUXAGENT_ENABLED=1 WALinuxAgent-2.9.1.1/init/devuan/walinuxagent000066400000000000000000000226701446033677600210750ustar00rootroot00000000000000#!/bin/bash # walinuxagent # script to start and stop the waagent daemon. # # This script takes into account the possibility that both daemon and # non-daemon instances of waagent may be running concurrently, # and attempts to ensure that any non-daemon instances are preserved # when the daemon instance is stopped. # ### BEGIN INIT INFO # Provides: walinuxagent # Required-Start: $remote_fs $syslog $network # Required-Stop: $remote_fs # X-Start-Before: cloud-init # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Microsoft Azure Linux Agent ### END INIT INFO DESC="Microsoft Azure Linux Agent" INTERPRETER="/usr/bin/python3" DAEMON='/usr/sbin/waagent' DAEMON_ARGS='-daemon' START_ARGS='--background' NAME='waagent' # set to 1 to enable a lot of debugging output DEBUG=0 . /lib/lsb/init-functions debugmsg() { # output a console message if DEBUG is set # (can be enabled dynamically by giving "debug" as an extra argument) if [ "x${DEBUG}" == "x1" ] ; then echo "[debug]: $1" >&2 fi return 0 } check_non_daemon_instances() { # check if there are any non-daemon instances of waagent running local NDPIDLIST i NDPIDCT declare -a NDPIDLIST debugmsg "check_non_daemon_instance: after init, #NDPIDLIST=${#NDPIDLIST[*]}" readarray -t NDPIDLIST < <( ps ax | grep "${INTERPRETER}" | grep "${DAEMON}" | grep -v -- "${DAEMON_ARGS}" | grep -v "grep" | awk '{ print $1 }') NDPIDCT=${#NDPIDLIST[@]} debugmsg "check_non_daemon_instances: NDPIDCT=${NDPIDCT}" debugmsg "check_non_daemon_instances: NDPIDLIST[0] = ${NDPIDLIST[0]}" if [ ${NDPIDCT} -gt 0 ] ; then debugmsg "check_non_daemon_instances: WARNING: non-daemon instances of waagent exist" else debugmsg "check_non_daemon_instances: no non-daemon instances of waagent are currently running" fi for (( i = 0 ; i < ${NDPIDCT} ; i++ )) ; do debugmsg "check_non_daemon_instances: WARNING: process ${NDPIDLIST[${i}]} is a non-daemon waagent instance" done return 0 } get_daemon_pid() { # (re)create PIDLIST, return the first entry local PID create_pidlist PID=${PIDLIST[0]} if [ -z "${PID}" ] ; then debugmsg "get_daemon_pid: : WARNING: no waagent daemon process found" fi echo "${PID}" } recheck_status() { # after an attempt to stop the daemon, re-check the status # and take any further actions required. # (NB: at the moment, we only re-check once. Possible improvement # would be to iterate the re-check up to a given maximum tries). local STATUS NEWSTATUS get_status STATUS=$? debugmsg "stop_waagent: status is now ${STATUS}" # ideal if stop has been successful: STATUS=1 - no daemon process case ${STATUS} in 0) # stop didn't work # what to do? maybe try kill -9 ? debugmsg "recheck_status: ERROR: unable to stop waagent" debugmsg "recheck_status: trying again with kill -9" kill_daemon_from_pid 1 # probably need to check status again? get_status NEW_STATUS=$? if [ "x${NEW_STATUS}" == "x1" ] ; then debugmsg "recheck_status: successfully stopped." log_end_msg 0 || true else # could probably do something more productive here debugmsg "recheck_status: unable to stop daemon - giving up" log_end_msg 1 || true exit 1 fi ;; 1) # THIS IS THE EXPECTED CASE: daemon is no longer running and debugmsg "recheck_status: waagent daemon stopped successfully." log_end_msg 0 || true ;; 2) # so weird that we can't figure out what's going on debugmsg "recheck_status: ERROR: unable to determine waagent status" debugmsg "recheck_status: manual intervention required" log_end_msg 1 || true exit 1 ;; esac } start_waagent() { # we use start-stop-daemon for starting waagent local STATUS get_status STATUS=$? # check the status value - take appropriate action debugmsg "start_waagent: STATUS=${STATUS}" case "${STATUS}" in 0) debugmsg "start_waagent: waagent is already running" log_daemon_msg "waagent is already running" log_end_msg 0 || true ;; 1) # not running (we ignore presence/absence of pidfile) # just start waagent debugmsg "start_waagent: waagent is not currently running" log_daemon_msg "Starting ${NAME} daemon" start-stop-daemon --start --quiet --background --name "${NAME}" --exec ${INTERPRETER} -- ${DAEMON} ${DAEMON_ARGS} log_end_msg $? || true ;; 2) # get_status can't figure out what's going on. # try doing a stop to clean up, then attempt to start waagent # will probably require manual intervention debugmsg "start_waagent: unable to determine current status" debugmsg "start_waagent: trying to stop waagent first, and then start it" stop_waagent log_daemon_msg "Starting ${NAME} daemon" start-stop-daemon --start --quiet --background --name ${NAME} --exec ${INTERPRETER} -- ${DAEMON} ${DAEMON_ARGS} log_end_msg $? || true ;; esac } kill_daemon_from_pidlist() { # check the pidlist for at least one waagent daemon process # if found, kill it directly from the entry in the pidlist # Ignore any pidfile. Avoid killing any non-daemon # waagent processes. # If called with "1" as first argument, use kill -9 rather than # normal kill local i PIDCT FORCE FORCE=0 if [ "x${1}" == "x1" ] ; then debugmsg "kill_daemon_from_pidlist: WARNING: using kill -9" FORCE=1 fi debugmsg "kill_daemon_from_pidlist: killing daemon using pid(s) in PIDLIST" PIDCT=${#PIDLIST[*]} if [ "${PIDCT}" -eq 0 ] ; then debugmsg "kill_daemon_from_pidlist: ERROR: no pids in PIDLIST" return 1 fi for (( i=0 ; i < ${PIDCT} ; i++ )) ; do debugmsg "kill_daemon_from_pidlist: killing waagent daemon process ${PIDLIST[${i}]}" if [ "x${FORCE}" == "x1" ] ; then kill -9 ${PIDLIST[${i}]} else kill ${PIDLIST[${i}]} fi done return 0 } stop_waagent() { # check the current status and if the waagent daemon is running, attempt # to stop it. # start-stop-daemon is avoided here local STATUS PID RC get_status STATUS=$? debugmsg "stop_waagent: current status = ${STATUS}" case "${STATUS}" in 0) # - ignore any pidfile - kill directly from process list log_daemon_msg "Stopping ${NAME} daemon (using process list)" kill_daemon_from_pidlist recheck_status ;; 1) # not running - we ignore any pidfile # REVISIT: should we check for a pidfile and remove if found? debugmsg "waagent is not running" log_daemon_msg "waagent is already stopped" log_end_msg 0 || true ;; 2) # weirdness - call for help debugmsg "ERROR: unable to determine waagent status - manual intervention required" log_daemon_msg "WARNING: unable to determine status of waagent daemon - manual intervention required" log_end_msg 1 || true ;; esac } check_daemons() { # check for running waagent daemon processes local ENTRY ps ax | grep "${INTERPRETER}" | grep "${DAEMON}" | grep -- "${DAEMON_ARGS}" | grep -v 'grep' | while read ENTRY ; do debugmsg "check_daemons(): ENTRY='${ENTRY}'" done return 0 } create_pidlist() { # initialise the list of waagent daemon processes # NB: there should only be one - both this script and waagent itself # attempt to avoid starting more than one daemon process. # However, we use an array just in case. readarray -t PIDLIST < <( ps ax | grep "${INTERPRETER}" | grep "${DAEMON}" | grep -- "${DAEMON_ARGS}" | grep -v 'grep' | awk '{ print $1 }') if [ "${#PIDLIST[*]}" -eq 0 ] ; then debugmsg "create_pidlist: WARNING: no waagent daemons found" elif [ "${#PIDLIST[*]}" -gt 1 ] ; then debugmsg "create_pidlist: WARNING: multiple waagent daemons running" fi return 0 } get_status() { # simplified status - ignoring any pidfile # Possibilities: # 0 - waagent daemon running # 1 - waagent daemon not running # 2 - status unclear # (NB: if we find that multiple daemons exist, we just ignore the fact. # It should be virtually impossible for this to happen) local FOUND RPID ENTRY STATUS DAEMON_RUNNING PIDCT PIDCT=0 DAEMON_RUNNING= RPID= ENTRY= # assume the worst STATUS=2 check_daemons create_pidlist # should only be one daemon running - but we check, just in case PIDCT=${#PIDLIST[@]} debugmsg "get_status: PIDCT=${PIDCT}" if [ ${PIDCT} -eq 0 ] ; then # not running STATUS=1 else # at least one daemon process is running if [ ${PIDCT} -gt 1 ] ; then debugmsg "get_status: WARNING: more than one waagent daemon running" debugmsg "get_status: (should not happen)" else debugmsg "get_status: only one daemon instance running - as expected" fi STATUS=0 fi return ${STATUS} } waagent_status() { # get the current status of the waagent daemon, and return it local STATUS get_status STATUS=$? debugmsg "waagent status = ${STATUS}" case ${STATUS} in 0) log_daemon_msg "waagent is running" ;; 1) log_daemon_msg "WARNING: waagent is not running" ;; 2) log_daemon_msg "WARNING: waagent status cannot be determined" ;; esac log_end_msg 0 || true return 0 } ######################################################################### # MAINLINE # Usage: "service [scriptname] [ start | stop | status | restart ] [ debug ] # (specifying debug as extra argument enables debugging output) ######################################################################### export PATH="${PATH}:+$PATH:}/usr/sbin:/sbin" declare -a PIDLIST if [ ! -z "$2" -a "$2" == "debug" ] ; then DEBUG=1 fi # pre-check for non-daemon (e.g. console) instances of waagent check_non_daemon_instances case "$1" in start) start_waagent ;; stop) stop_waagent ;; status) waagent_status ;; restart) stop_waagent start_waagent ;; esac exit 0 WALinuxAgent-2.9.1.1/init/freebsd/000077500000000000000000000000001446033677600165655ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/init/freebsd/waagent000077500000000000000000000005151446033677600201420ustar00rootroot00000000000000#!/bin/sh # PROVIDE: waagent # REQUIRE: sshd netif dhclient # KEYWORD: nojail . /etc/rc.subr PATH=$PATH:/usr/local/bin:/usr/local/sbin name="waagent" rcvar="waagent_enable" pidfile="/var/run/waagent.pid" command="/usr/local/sbin/${name}" command_interpreter="python" command_args="start" load_rc_config $name run_rc_command "$1" WALinuxAgent-2.9.1.1/init/gaia/000077500000000000000000000000001446033677600160545ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/init/gaia/waagent000077500000000000000000000014561446033677600174360ustar00rootroot00000000000000#!/bin/bash # # Init file for AzureLinuxAgent. # # chkconfig: 2345 60 80 # description: AzureLinuxAgent # # source function library . /etc/rc.d/init.d/functions RETVAL=0 FriendlyName="AzureLinuxAgent" WAZD_BIN=/usr/sbin/waagent.sh start() { echo -n $"Starting $FriendlyName: " $WAZD_BIN -start & success echo } stop() { echo -n $"Stopping $FriendlyName: " killproc -p /var/run/waagent.pid $WAZD_BIN RETVAL=$? echo return $RETVAL } case "$1" in start) start ;; stop) stop ;; restart) stop start ;; reload) ;; report) ;; status) status $WAZD_BIN RETVAL=$? ;; *) echo $"Usage: $0 {start|stop|restart|status}" RETVAL=1 esac exit $RETVAL WALinuxAgent-2.9.1.1/init/mariner/000077500000000000000000000000001446033677600166105ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/init/mariner/waagent.service000066400000000000000000000006201446033677600216160ustar00rootroot00000000000000[Unit] Description=Azure Linux Agent Wants=systemd-networkd-wait-online.service sshd.service sshd-keygen.service After=systemd-networkd-wait-online.service cloud-init.service ConditionFileIsExecutable=/usr/bin/waagent ConditionPathExists=/etc/waagent.conf [Service] Type=simple ExecStart=/usr/bin/python -u /usr/bin/waagent -daemon Restart=always RestartSec=5 [Install] WantedBy=multi-user.target WALinuxAgent-2.9.1.1/init/openbsd/000077500000000000000000000000001446033677600166055ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/init/openbsd/waagent000066400000000000000000000002311446033677600201520ustar00rootroot00000000000000#!/bin/sh daemon="python2.7 /usr/local/sbin/waagent -start" . /etc/rc.d/rc.subr pexp="python /usr/local/sbin/waagent -daemon" rc_reload=NO rc_cmd $1 WALinuxAgent-2.9.1.1/init/openwrt/000077500000000000000000000000001446033677600166515ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/init/openwrt/waagent000077500000000000000000000027061446033677600202320ustar00rootroot00000000000000#!/bin/sh /etc/rc.common # Init file for AzureLinuxAgent. # # Copyright 2018 Microsoft Corporation # Copyright 2018 Sonus Networks, Inc. (d.b.a. Ribbon Communications Operating Company) # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # # description: AzureLinuxAgent # START=60 STOP=80 RETVAL=0 FriendlyName="AzureLinuxAgent" WAZD_BIN=/usr/sbin/waagent WAZD_CONF=/etc/waagent.conf WAZD_PIDFILE=/var/run/waagent.pid test -x "$WAZD_BIN" || { echo "$WAZD_BIN not installed"; exit 5; } test -e "$WAZD_CONF" || { echo "$WAZD_CONF not found"; exit 6; } start() { echo -n "Starting $FriendlyName: " $WAZD_BIN -start RETVAL=$? echo return $RETVAL } stop() { echo -n "Stopping $FriendlyName: " if [ -f "$WAZD_PIDFILE" ] then kill -9 `cat ${WAZD_PIDFILE}` rm ${WAZD_PIDFILE} RETVAL=$? echo return $RETVAL else echo "$FriendlyName already stopped." fi } WALinuxAgent-2.9.1.1/init/photonos/000077500000000000000000000000001446033677600170245ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/init/photonos/waagent.service000066400000000000000000000006211446033677600220330ustar00rootroot00000000000000[Unit] Description=Azure Linux Agent Wants=systemd-networkd-wait-online.service sshd.service sshd-keygen.service After=systemd-networkd-wait-online.service cloud-init.service ConditionFileIsExecutable=/usr/bin/waagent ConditionPathExists=/etc/waagent.conf [Service] Type=simple ExecStart=/usr/bin/python3 -u /usr/bin/waagent -daemon Restart=always RestartSec=5 [Install] WantedBy=multi-user.target WALinuxAgent-2.9.1.1/init/redhat/000077500000000000000000000000001446033677600164225ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/init/redhat/py2/000077500000000000000000000000001446033677600171345ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/init/redhat/py2/waagent.service000066400000000000000000000006321446033677600221450ustar00rootroot00000000000000[Unit] Description=Azure Linux Agent Wants=network-online.target sshd.service sshd-keygen.service After=network-online.target ConditionFileIsExecutable=/usr/sbin/waagent ConditionPathExists=/etc/waagent.conf [Service] Type=simple ExecStart=/usr/bin/python -u /usr/sbin/waagent -daemon Restart=always RestartSec=5 Slice=azure.slice CPUAccounting=yes MemoryAccounting=yes [Install] WantedBy=multi-user.target WALinuxAgent-2.9.1.1/init/redhat/waagent.service000066400000000000000000000006331446033677600214340ustar00rootroot00000000000000[Unit] Description=Azure Linux Agent Wants=network-online.target sshd.service sshd-keygen.service After=network-online.target ConditionFileIsExecutable=/usr/sbin/waagent ConditionPathExists=/etc/waagent.conf [Service] Type=simple ExecStart=/usr/bin/python3 -u /usr/sbin/waagent -daemon Restart=always RestartSec=5 Slice=azure.slice CPUAccounting=yes MemoryAccounting=yes [Install] WantedBy=multi-user.target WALinuxAgent-2.9.1.1/init/sles/000077500000000000000000000000001446033677600161215ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/init/sles/waagent.service000066400000000000000000000005421446033677600211320ustar00rootroot00000000000000[Unit] Description=Azure Linux Agent Wants=network-online.target sshd.service sshd-keygen.service After=network-online.target ConditionFileIsExecutable=/usr/sbin/waagent ConditionPathExists=/etc/waagent.conf [Service] Type=simple ExecStart=/usr/bin/python3 -u /usr/sbin/waagent -daemon Restart=always RestartSec=5 [Install] WantedBy=multi-user.target WALinuxAgent-2.9.1.1/init/suse/000077500000000000000000000000001446033677600161325ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/init/suse/waagent000077500000000000000000000062011446033677600175050ustar00rootroot00000000000000#! /bin/sh # # Microsoft Azure Linux Agent sysV init script # # Copyright 2013 Microsoft Corporation # Copyright SUSE LLC # # 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. # # /etc/init.d/waagent # # and symbolic link # # /usr/sbin/rcwaagent # # System startup script for the waagent # ### BEGIN INIT INFO # Provides: MicrosoftAzureLinuxAgent # Required-Start: $network sshd # Required-Stop: $network sshd # Default-Start: 3 5 # Default-Stop: 0 1 2 6 # Description: Start the MicrosoftAzureLinuxAgent ### END INIT INFO PYTHON=/usr/bin/python WAZD_BIN=/usr/sbin/waagent WAZD_CONF=/etc/waagent.conf WAZD_PIDFILE=/var/run/waagent.pid test -x "$WAZD_BIN" || { echo "$WAZD_BIN not installed"; exit 5; } test -e "$WAZD_CONF" || { echo "$WAZD_CONF not found"; exit 6; } . /etc/rc.status # First reset status of this service rc_reset # Return values acc. to LSB for all commands but status: # 0 - success # 1 - misc error # 2 - invalid or excess args # 3 - unimplemented feature (e.g. reload) # 4 - insufficient privilege # 5 - program not installed # 6 - program not configured # # Note that starting an already running service, stopping # or restarting a not-running service as well as the restart # with force-reload (in case signalling is not supported) are # considered a success. case "$1" in start) echo -n "Starting MicrosoftAzureLinuxAgent" ## Start daemon with startproc(8). If this fails ## the echo return value is set appropriate. startproc -f ${PYTHON} ${WAZD_BIN} -start rc_status -v ;; stop) echo -n "Shutting down MicrosoftAzureLinuxAgent" ## Stop daemon with killproc(8) and if this fails ## set echo the echo return value. killproc -p ${WAZD_PIDFILE} ${PYTHON} ${WAZD_BIN} rc_status -v ;; try-restart) ## Stop the service and if this succeeds (i.e. the ## service was running before), start it again. $0 status >/dev/null && $0 restart rc_status ;; restart) ## Stop the service and regardless of whether it was ## running or not, start it again. $0 stop sleep 1 $0 start rc_status ;; force-reload|reload) rc_status ;; status) echo -n "Checking for service MicrosoftAzureLinuxAgent " ## Check status with checkproc(8), if process is running ## checkproc will return with exit status 0. checkproc -p ${WAZD_PIDFILE} ${PYTHON} ${WAZD_BIN} rc_status -v ;; probe) ;; *) echo "Usage: $0 {start|stop|status|try-restart|restart|force-reload|reload}" exit 1 ;; esac rc_exit WALinuxAgent-2.9.1.1/init/ubuntu/000077500000000000000000000000001446033677600164755ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/init/ubuntu/walinuxagent000066400000000000000000000001321446033677600211220ustar00rootroot00000000000000# To disable the Microsoft Azure Agent, set WALINUXAGENT_ENABLED=0 WALINUXAGENT_ENABLED=1 WALinuxAgent-2.9.1.1/init/ubuntu/walinuxagent.conf000066400000000000000000000007321446033677600220540ustar00rootroot00000000000000description "Microsoft Azure Linux agent" author "Ben Howard " start on runlevel [2345] stop on runlevel [!2345] pre-start script [ -r /etc/default/walinuxagent ] && . /etc/default/walinuxagent if [ "$WALINUXAGENT_ENABLED" != "1" ]; then stop ; exit 0 fi if [ ! -x /usr/sbin/waagent ]; then stop ; exit 0 fi #Load the udf module modprobe -b udf end script exec /usr/sbin/waagent -daemon respawn WALinuxAgent-2.9.1.1/init/ubuntu/walinuxagent.service000066400000000000000000000011161446033677600225640ustar00rootroot00000000000000# # NOTE: # This file hosted on WALinuxAgent repository only for reference purposes. # Please refer to a recent image to find out the up-to-date systemd unit file. # [Unit] Description=Azure Linux Agent After=network-online.target cloud-init.service Wants=network-online.target sshd.service sshd-keygen.service ConditionFileIsExecutable=/usr/sbin/waagent ConditionPathExists=/etc/waagent.conf [Service] Type=simple ExecStart=/usr/bin/python3 -u /usr/sbin/waagent -daemon Restart=always Slice=azure.slice CPUAccounting=yes MemoryAccounting=yes [Install] WantedBy=multi-user.target WALinuxAgent-2.9.1.1/init/waagent000077500000000000000000000014761446033677600165370ustar00rootroot00000000000000#!/bin/bash # # Init file for AzureLinuxAgent. # # chkconfig: 2345 60 80 # description: AzureLinuxAgent # # source function library . /etc/rc.d/init.d/functions RETVAL=0 FriendlyName="AzureLinuxAgent" WAZD_BIN=/usr/sbin/waagent start() { echo -n $"Starting $FriendlyName: " $WAZD_BIN -start RETVAL=$? echo return $RETVAL } stop() { echo -n $"Stopping $FriendlyName: " killproc -p /var/run/waagent.pid $WAZD_BIN RETVAL=$? echo return $RETVAL } case "$1" in start) start ;; stop) stop ;; restart) stop start ;; reload) ;; report) ;; status) status $WAZD_BIN RETVAL=$? ;; *) echo $"Usage: $0 {start|stop|restart|status}" RETVAL=1 esac exit $RETVAL WALinuxAgent-2.9.1.1/init/waagent.service000066400000000000000000000005411446033677600201630ustar00rootroot00000000000000[Unit] Description=Azure Linux Agent Wants=network-online.target sshd.service sshd-keygen.service After=network-online.target ConditionFileIsExecutable=/usr/sbin/waagent ConditionPathExists=/etc/waagent.conf [Service] Type=simple ExecStart=/usr/bin/python -u /usr/sbin/waagent -daemon Restart=always RestartSec=5 [Install] WantedBy=multi-user.target WALinuxAgent-2.9.1.1/makepkg.py000077500000000000000000000103261446033677600162060ustar00rootroot00000000000000#!/usr/bin/env python3 import argparse import glob import logging import os.path import shutil import subprocess import sys from azurelinuxagent.common.version import AGENT_NAME, AGENT_VERSION, AGENT_LONG_VERSION from azurelinuxagent.ga.update import AGENT_MANIFEST_FILE MANIFEST = '''[{{ "name": "{0}", "version": 1.0, "handlerManifest": {{ "installCommand": "", "uninstallCommand": "", "updateCommand": "", "enableCommand": "python -u {1} -run-exthandlers", "disableCommand": "", "rebootAfterInstall": false, "reportHeartbeat": false }} }}]''' PUBLISH_MANIFEST = ''' Microsoft.OSTCLinuxAgent {1} {0} VmRole Microsoft Azure Guest Agent for Linux IaaS true https://github.com/Azure/WALinuxAgent/blob/2.1/LICENSE.txt https://github.com/Azure/WALinuxAgent/blob/2.1/LICENSE.txt https://github.com/Azure/WALinuxAgent true Microsoft Linux ''' PUBLISH_MANIFEST_FILE = 'manifest.xml' def do(*args): try: return subprocess.check_output(args, stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: # pylint: disable=C0103 raise Exception("[{0}] failed:\n{1}\n{2}".format(" ".join(args), str(e), e.output)) def run(agent_family, output_directory, log): output_path = os.path.join(output_directory, "eggs") target_path = os.path.join(output_path, AGENT_LONG_VERSION) bin_path = os.path.join(target_path, "bin") egg_path = os.path.join(bin_path, AGENT_LONG_VERSION + ".egg") manifest_path = os.path.join(target_path, AGENT_MANIFEST_FILE) publish_manifest_path = os.path.join(target_path, PUBLISH_MANIFEST_FILE) pkg_name = os.path.join(output_path, AGENT_LONG_VERSION + ".zip") if os.path.isdir(target_path): shutil.rmtree(target_path) elif os.path.isfile(target_path): os.remove(target_path) if os.path.isfile(pkg_name): os.remove(pkg_name) os.makedirs(bin_path) log.info("Created {0} directory".format(target_path)) setup_path = os.path.join(os.path.dirname(__file__), "setup.py") args = ["python3", setup_path, "bdist_egg", "--dist-dir={0}".format(bin_path)] log.info("Creating egg {0}".format(egg_path)) do(*args) egg_name = os.path.join("bin", os.path.basename( glob.glob(os.path.join(bin_path, "*"))[0])) log.info("Writing {0}".format(manifest_path)) with open(manifest_path, mode='w') as manifest: manifest.write(MANIFEST.format(AGENT_NAME, egg_name)) log.info("Writing {0}".format(publish_manifest_path)) with open(publish_manifest_path, mode='w') as publish_manifest: publish_manifest.write(PUBLISH_MANIFEST.format(AGENT_VERSION, agent_family)) cwd = os.getcwd() os.chdir(target_path) try: log.info("Creating package {0}".format(pkg_name)) do("zip", "-r", pkg_name, egg_name) do("zip", "-j", pkg_name, AGENT_MANIFEST_FILE) do("zip", "-j", pkg_name, PUBLISH_MANIFEST_FILE) finally: os.chdir(cwd) log.info("Package {0} successfully created".format(pkg_name)) if __name__ == "__main__": logging.basicConfig(format='%(message)s', level=logging.INFO) parser = argparse.ArgumentParser() parser.add_argument('family', metavar='family', nargs='?', default='Test', help='Agent family') parser.add_argument('-o', '--output', default=os.getcwd(), help='Output directory') arguments = parser.parse_args() try: run(arguments.family, arguments.output, logging) except Exception as exception: logging.error(str(exception)) sys.exit(1) sys.exit(0) WALinuxAgent-2.9.1.1/requirements.txt000066400000000000000000000000461446033677600174740ustar00rootroot00000000000000distro; python_version >= '3.8' pyasn1WALinuxAgent-2.9.1.1/setup.py000077500000000000000000000331511446033677600157300ustar00rootroot00000000000000#!/usr/bin/env python # # Microsoft Azure Linux Agent setup.py # # Copyright 2013 Microsoft Corporation # # 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. # import os import subprocess import sys import setuptools from setuptools import find_packages from setuptools.command.install import install as _install from azurelinuxagent.common.osutil import get_osutil from azurelinuxagent.common.version import AGENT_NAME, AGENT_VERSION, \ AGENT_DESCRIPTION, \ DISTRO_NAME, DISTRO_VERSION, DISTRO_FULL_NAME root_dir = os.path.dirname(os.path.abspath(__file__)) # pylint: disable=invalid-name os.chdir(root_dir) def set_files(data_files, dest=None, src=None): data_files.append((dest, src)) def set_bin_files(data_files, dest, src=None): if src is None: src = ["bin/waagent", "bin/waagent2.0"] data_files.append((dest, src)) def set_conf_files(data_files, dest="/etc", src=None): if src is None: src = ["config/waagent.conf"] data_files.append((dest, src)) def set_logrotate_files(data_files, dest="/etc/logrotate.d", src=None): if src is None: src = ["config/waagent.logrotate"] data_files.append((dest, src)) def set_sysv_files(data_files, dest="/etc/rc.d/init.d", src=None): if src is None: src = ["init/waagent"] data_files.append((dest, src)) def set_systemd_files(data_files, dest, src=None): if src is None: src = ["init/waagent.service"] data_files.append((dest, src)) def set_freebsd_rc_files(data_files, dest="/etc/rc.d/", src=None): if src is None: src = ["init/freebsd/waagent"] data_files.append((dest, src)) def set_openbsd_rc_files(data_files, dest="/etc/rc.d/", src=None): if src is None: src = ["init/openbsd/waagent"] data_files.append((dest, src)) def set_udev_files(data_files, dest="/etc/udev/rules.d/", src=None): if src is None: src = ["config/66-azure-storage.rules", "config/99-azure-product-uuid.rules"] data_files.append((dest, src)) def get_data_files(name, version, fullname): # pylint: disable=R0912 """ Determine data_files according to distro name, version and init system type """ data_files = [] osutil = get_osutil() systemd_dir_path = osutil.get_systemd_unit_file_install_path() agent_bin_path = osutil.get_agent_bin_path() if name in ('redhat', 'rhel', 'centos', 'almalinux', 'cloudlinux', 'rocky'): if version.startswith("8") or version.startswith("9"): # redhat8+ default to py3 set_bin_files(data_files, dest=agent_bin_path, src=["bin/py3/waagent", "bin/waagent2.0"]) else: set_bin_files(data_files, dest=agent_bin_path) set_conf_files(data_files) set_logrotate_files(data_files) set_udev_files(data_files) if version.startswith("8") or version.startswith("9"): # redhat 8+ uses systemd and python3 set_systemd_files(data_files, dest=systemd_dir_path, src=["init/redhat/waagent.service", "init/azure.slice", "init/azure-vmextensions.slice" ]) elif version.startswith("6"): set_sysv_files(data_files) else: # redhat7.0+ use systemd set_systemd_files(data_files, dest=systemd_dir_path, src=[ "init/redhat/py2/waagent.service", "init/azure.slice", "init/azure-vmextensions.slice" ]) if version.startswith("7.1"): # TODO this is a mitigation to systemctl bug on 7.1 set_sysv_files(data_files) elif name == 'arch': set_bin_files(data_files, dest=agent_bin_path) set_conf_files(data_files, src=["config/arch/waagent.conf"]) set_udev_files(data_files) set_systemd_files(data_files, dest=systemd_dir_path, src=["init/arch/waagent.service"]) elif name in ('coreos', 'flatcar'): set_bin_files(data_files, dest=agent_bin_path) set_conf_files(data_files, dest="/usr/share/oem", src=["config/coreos/waagent.conf"]) set_logrotate_files(data_files) set_udev_files(data_files) set_files(data_files, dest="/usr/share/oem", src=["init/coreos/cloud-config.yml"]) elif "Clear Linux" in fullname: set_bin_files(data_files, dest=agent_bin_path) set_conf_files(data_files, dest="/usr/share/defaults/waagent", src=["config/clearlinux/waagent.conf"]) set_systemd_files(data_files, dest=systemd_dir_path, src=["init/clearlinux/waagent.service"]) elif name == 'mariner': set_bin_files(data_files, dest=agent_bin_path) set_conf_files(data_files, dest="/etc", src=["config/mariner/waagent.conf"]) set_systemd_files(data_files, dest=systemd_dir_path, src=["init/mariner/waagent.service"]) set_logrotate_files(data_files) set_udev_files(data_files) elif name == 'ubuntu': set_conf_files(data_files, src=["config/ubuntu/waagent.conf"]) set_logrotate_files(data_files) set_udev_files(data_files) if version.startswith("12") or version.startswith("14"): # Ubuntu12.04/14.04 - uses upstart if version.startswith("12"): set_bin_files(data_files, dest=agent_bin_path) else: set_bin_files(data_files, dest=agent_bin_path, src=["bin/py3/waagent", "bin/waagent2.0"]) set_files(data_files, dest="/etc/init", src=["init/ubuntu/walinuxagent.conf"]) set_files(data_files, dest='/etc/default', src=['init/ubuntu/walinuxagent']) else: set_bin_files(data_files, dest=agent_bin_path, src=["bin/py3/waagent", "bin/waagent2.0"]) # Ubuntu15.04+ uses systemd set_systemd_files(data_files, dest=systemd_dir_path, src=[ "init/ubuntu/walinuxagent.service", "init/azure.slice", "init/azure-vmextensions.slice" ]) elif name == 'suse' or name == 'opensuse': # pylint: disable=R1714 set_bin_files(data_files, dest=agent_bin_path) set_conf_files(data_files, src=["config/suse/waagent.conf"]) set_logrotate_files(data_files) set_udev_files(data_files) if fullname == 'SUSE Linux Enterprise Server' and \ version.startswith('11') or \ fullname == 'openSUSE' and version.startswith( '13.1'): set_sysv_files(data_files, dest='/etc/init.d', src=["init/suse/waagent"]) else: # sles 12+ and openSUSE 13.2+ use systemd set_systemd_files(data_files, dest=systemd_dir_path) elif name == 'sles': # sles 15+ distro named as sles set_bin_files(data_files, dest=agent_bin_path, src=["bin/py3/waagent", "bin/waagent2.0"]) set_conf_files(data_files, src=["config/suse/waagent.conf"]) set_logrotate_files(data_files) set_udev_files(data_files) # sles 15+ uses systemd and python3 set_systemd_files(data_files, dest=systemd_dir_path, src=["init/sles/waagent.service"]) elif name == 'freebsd': set_bin_files(data_files, dest=agent_bin_path) set_conf_files(data_files, src=["config/freebsd/waagent.conf"]) set_freebsd_rc_files(data_files) elif name == 'openbsd': set_bin_files(data_files, dest=agent_bin_path) set_conf_files(data_files, src=["config/openbsd/waagent.conf"]) set_openbsd_rc_files(data_files) elif name == 'debian': set_bin_files(data_files, dest=agent_bin_path, src=["bin/py3/waagent", "bin/waagent2.0"]) set_conf_files(data_files, src=["config/debian/waagent.conf"]) set_logrotate_files(data_files) set_udev_files(data_files, dest="/lib/udev/rules.d") if debian_has_systemd(): set_systemd_files(data_files, dest=systemd_dir_path) elif name == 'devuan': set_bin_files(data_files, dest=agent_bin_path, src=["bin/py3/waagent", "bin/waagent2.0"]) set_files(data_files, dest="/etc/init.d", src=['init/devuan/walinuxagent']) set_files(data_files, dest="/etc/default", src=['init/devuan/default/walinuxagent']) set_conf_files(data_files, src=['config/devuan/waagent.conf']) set_logrotate_files(data_files) set_udev_files(data_files, dest="/lib/udev/rules.d") elif name == 'iosxe': set_bin_files(data_files, dest=agent_bin_path) set_conf_files(data_files, src=["config/iosxe/waagent.conf"]) set_logrotate_files(data_files) set_udev_files(data_files) set_systemd_files(data_files, dest=systemd_dir_path) if version.startswith("7.1"): # TODO this is a mitigation to systemctl bug on 7.1 set_sysv_files(data_files) elif name == 'openwrt': set_bin_files(data_files, dest=agent_bin_path) set_conf_files(data_files) set_logrotate_files(data_files) set_sysv_files(data_files, dest='/etc/init.d', src=["init/openwrt/waagent"]) elif name == 'photonos': set_bin_files(data_files, dest=agent_bin_path) set_conf_files(data_files, src=["config/photonos/waagent.conf"]) set_systemd_files(data_files, dest=systemd_dir_path, src=["init/photonos/waagent.service"]) elif name == 'fedora': set_bin_files(data_files, dest=agent_bin_path) set_conf_files(data_files) set_logrotate_files(data_files) set_udev_files(data_files) set_systemd_files(data_files, dest=systemd_dir_path) else: # Use default setting set_bin_files(data_files, dest=agent_bin_path) set_conf_files(data_files) set_logrotate_files(data_files) set_udev_files(data_files) set_sysv_files(data_files) return data_files def debian_has_systemd(): try: return subprocess.check_output( ['cat', '/proc/1/comm']).strip().decode() == 'systemd' except subprocess.CalledProcessError: return False class install(_install): # pylint: disable=C0103 user_options = _install.user_options + [ ('lnx-distro=', None, 'target Linux distribution'), ('lnx-distro-version=', None, 'target Linux distribution version'), ('lnx-distro-fullname=', None, 'target Linux distribution full name'), ('register-service', None, 'register as startup service and start'), ('skip-data-files', None, 'skip data files installation'), ] def initialize_options(self): _install.initialize_options(self) # pylint: disable=attribute-defined-outside-init self.lnx_distro = DISTRO_NAME self.lnx_distro_version = DISTRO_VERSION self.lnx_distro_fullname = DISTRO_FULL_NAME self.register_service = False # All our data files are system-wide files that are not included in the egg; skip them when # creating an egg. self.skip_data_files = "bdist_egg" in sys.argv # pylint: enable=attribute-defined-outside-init def finalize_options(self): _install.finalize_options(self) if self.skip_data_files: return data_files = get_data_files(self.lnx_distro, self.lnx_distro_version, self.lnx_distro_fullname) self.distribution.data_files = data_files self.distribution.reinitialize_command('install_data', True) def run(self): _install.run(self) if self.register_service: osutil = get_osutil() osutil.register_agent_service() osutil.stop_agent_service() osutil.start_agent_service() # Note to packagers and users from source. # In version 3.5 of Python distribution information handling in the platform # module was deprecated. Depending on the Linux distribution the # implementation may be broken prior to Python 3.7 wher the functionality # will be removed from Python 3 requires = [] # pylint: disable=invalid-name if float(sys.version[:3]) >= 3.7: requires = ['distro'] # pylint: disable=invalid-name modules = [] # pylint: disable=invalid-name if "bdist_egg" in sys.argv: modules.append("__main__") setuptools.setup( name=AGENT_NAME, version=AGENT_VERSION, long_description=AGENT_DESCRIPTION, author='Microsoft Corporation', author_email='walinuxagent@microsoft.com', platforms='Linux', url='https://github.com/Azure/WALinuxAgent', license='Apache License Version 2.0', packages=find_packages(exclude=["tests*", "dcr*"]), py_modules=modules, install_requires=requires, cmdclass={ 'install': install } ) WALinuxAgent-2.9.1.1/test-requirements.txt000066400000000000000000000012241446033677600204500ustar00rootroot00000000000000codecov coverage mock==2.0.0; python_version == '2.6' mock==3.0.5; python_version >= '2.7' and python_version <= '3.5' mock==4.0.2; python_version >= '3.6' distro; python_version >= '3.8' nose nose-timer; python_version >= '2.7' # Pinning the wrapt requirement to 1.12.0 due to the bug - https://github.com/GrahamDumpleton/wrapt/issues/188 wrapt==1.12.0; python_version > '2.6' and python_version < '3.6' pylint; python_version > '2.6' and python_version < '3.6' pylint==2.8.3; python_version >= '3.6' # Requirements to run pylint on the end-to-end tests source code assertpy azure-core azure-identity azure-mgmt-compute>=22.1.0 azure-mgmt-resource>=15.0.0 WALinuxAgent-2.9.1.1/tests/000077500000000000000000000000001446033677600153525ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/__init__.py000066400000000000000000000011651446033677600174660ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # WALinuxAgent-2.9.1.1/tests/common/000077500000000000000000000000001446033677600166425ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/common/__init__.py000066400000000000000000000011651446033677600207560ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # WALinuxAgent-2.9.1.1/tests/common/dhcp/000077500000000000000000000000001446033677600175605ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/common/dhcp/__init__.py000066400000000000000000000011651446033677600216740ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # WALinuxAgent-2.9.1.1/tests/common/dhcp/test_dhcp.py000066400000000000000000000120441446033677600221100ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import mock import azurelinuxagent.common.dhcp as dhcp import azurelinuxagent.common.osutil.default as osutil from tests.tools import AgentTestCase, open_patch, patch class TestDHCP(AgentTestCase): DEFAULT_ROUTING_TABLE = "\ Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT \n\ eth0 00345B0A 00000000 0001 0 0 5 00000000 0 0 0 \n\ lo 00000000 01345B0A 0003 0 0 1 00FCFFFF 0 0 0 \n" def setUp(self): AgentTestCase.setUp(self) def tearDown(self): AgentTestCase.tearDown(self) def test_wireserver_route_exists(self): # setup dhcp_handler = dhcp.get_dhcp_handler() self.assertTrue(dhcp_handler.endpoint is None) self.assertTrue(dhcp_handler.routes is None) self.assertTrue(dhcp_handler.gateway is None) # execute routing_table_with_wireserver_route = TestDHCP.DEFAULT_ROUTING_TABLE + \ "eth0 00000000 10813FA8 0003 0 0 5 00000000 0 0 0 \n" with patch("os.path.exists", return_value=True): open_file_mock = mock.mock_open(read_data=routing_table_with_wireserver_route) with patch(open_patch(), open_file_mock): self.assertTrue(dhcp_handler.wireserver_route_exists) # test self.assertTrue(dhcp_handler.endpoint is not None) self.assertTrue(dhcp_handler.routes is None) self.assertTrue(dhcp_handler.gateway is None) def test_wireserver_route_not_exists(self): # setup dhcp_handler = dhcp.get_dhcp_handler() self.assertTrue(dhcp_handler.endpoint is None) self.assertTrue(dhcp_handler.routes is None) self.assertTrue(dhcp_handler.gateway is None) # execute with patch("os.path.exists", return_value=True): open_file_mock = mock.mock_open(read_data=TestDHCP.DEFAULT_ROUTING_TABLE) with patch(open_patch(), open_file_mock): self.assertFalse(dhcp_handler.wireserver_route_exists) # test self.assertTrue(dhcp_handler.endpoint is None) self.assertTrue(dhcp_handler.routes is None) self.assertTrue(dhcp_handler.gateway is None) def test_dhcp_cache_exists(self): dhcp_handler = dhcp.get_dhcp_handler() dhcp_handler.osutil = osutil.DefaultOSUtil() with patch.object(osutil.DefaultOSUtil, 'get_dhcp_lease_endpoint', return_value=None): self.assertFalse(dhcp_handler.dhcp_cache_exists) self.assertEqual(dhcp_handler.endpoint, None) with patch.object(osutil.DefaultOSUtil, 'get_dhcp_lease_endpoint', return_value="foo"): self.assertTrue(dhcp_handler.dhcp_cache_exists) self.assertEqual(dhcp_handler.endpoint, "foo") def test_dhcp_skip_cache(self): handler = dhcp.get_dhcp_handler() handler.osutil = osutil.DefaultOSUtil() open_file_mock = mock.mock_open(read_data=TestDHCP.DEFAULT_ROUTING_TABLE) with patch('os.path.exists', return_value=False): with patch.object(osutil.DefaultOSUtil, 'get_dhcp_lease_endpoint')\ as patch_dhcp_cache: with patch.object(dhcp.DhcpHandler, 'send_dhcp_req') \ as patch_dhcp_send: endpoint = 'foo' patch_dhcp_cache.return_value = endpoint # endpoint comes from cache self.assertFalse(handler.skip_cache) with patch("os.path.exists", return_value=True): with patch(open_patch(), open_file_mock): handler.run() self.assertTrue(patch_dhcp_cache.call_count == 1) self.assertTrue(patch_dhcp_send.call_count == 0) self.assertTrue(handler.endpoint == endpoint) # reset handler.skip_cache = True handler.endpoint = None # endpoint comes from dhcp request self.assertTrue(handler.skip_cache) with patch("os.path.exists", return_value=True): with patch(open_patch(), open_file_mock): handler.run() self.assertTrue(patch_dhcp_cache.call_count == 1) self.assertTrue(patch_dhcp_send.call_count == 1) WALinuxAgent-2.9.1.1/tests/common/mock_cgroup_environment.py000066400000000000000000000127371446033677600241620ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright 2020 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import contextlib import os from tests.tools import patch, data_dir from tests.common.mock_environment import MockEnvironment, MockCommand _MOCKED_COMMANDS = [ MockCommand(r"^systemctl --version$", '''systemd 237 +PAM +AUDIT +SELINUX +IMA +APPARMOR +SMACK +SYSVINIT +UTMP +LIBCRYPTSETUP +GCRYPT +GNUTLS +ACL +XZ +LZ4 +SECCOMP +BLKID +ELFUTILS +KMOD -IDN2 +IDN -PCRE2 default-hierarchy=hybrid '''), MockCommand(r"^mount -t cgroup$", '''cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd) cgroup on /sys/fs/cgroup/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,rdma) cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset) cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio) cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event) cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb) cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer) cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory) cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids) cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices) cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct) cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio) '''), MockCommand(r"^mount -t cgroup2$", '''cgroup on /sys/fs/cgroup/unified type cgroup2 (rw,nosuid,nodev,noexec,relatime) '''), MockCommand(r"^systemctl show walinuxagent\.service --property Slice", '''Slice=system.slice '''), MockCommand(r"^systemctl show walinuxagent\.service --property CPUAccounting$", '''CPUAccounting=no '''), MockCommand(r"^systemctl show walinuxagent\.service --property CPUQuotaPerSecUSec$", '''CPUQuotaPerSecUSec=infinity '''), MockCommand(r"^systemctl show walinuxagent\.service --property MemoryAccounting$", '''MemoryAccounting=no '''), MockCommand(r"^systemctl show extension\.service --property ControlGroup$", '''ControlGroup=/system.slice/extension.service '''), MockCommand(r"^systemctl daemon-reload", ""), MockCommand(r"^systemctl stop ([^\s]+)"), MockCommand(r"^systemd-run (.+) --unit=([^\s]+) --scope ([^\s]+)", ''' Running scope as unit: TEST_UNIT.scope Thu 28 May 2020 07:25:55 AM PDT '''), ] _MOCKED_FILES = [ ("/proc/self/cgroup", os.path.join(data_dir, 'cgroups', 'proc_self_cgroup')), (r"/proc/[0-9]+/cgroup", os.path.join(data_dir, 'cgroups', 'proc_pid_cgroup')), ("/sys/fs/cgroup/unified/cgroup.controllers", os.path.join(data_dir, 'cgroups', 'sys_fs_cgroup_unified_cgroup.controllers')) ] _MOCKED_PATHS = [ r"^(/lib/systemd/system)", r"^(/etc/systemd/system)" ] class UnitFilePaths: walinuxagent = "/lib/systemd/system/walinuxagent.service" logcollector = "/lib/systemd/system/azure-walinuxagent-logcollector.slice" azure = "/lib/systemd/system/azure.slice" vmextensions = "/lib/systemd/system/azure-vmextensions.slice" extensionslice = "/lib/systemd/system/azure-vmextensions-Microsoft.CPlat.Extension.slice" slice = "/lib/systemd/system/walinuxagent.service.d/10-Slice.conf" cpu_accounting = "/lib/systemd/system/walinuxagent.service.d/11-CPUAccounting.conf" cpu_quota = "/lib/systemd/system/walinuxagent.service.d/12-CPUQuota.conf" memory_accounting = "/lib/systemd/system/walinuxagent.service.d/13-MemoryAccounting.conf" extension_service_cpu_accounting = '/lib/systemd/system/extension.service.d/11-CPUAccounting.conf' extension_service_cpu_quota = '/lib/systemd/system/extension.service.d/12-CPUQuota.conf' extension_service_memory_accounting = '/lib/systemd/system/extension.service.d/13-MemoryAccounting.conf' extension_service_memory_limit = '/lib/systemd/system/extension.service.d/14-MemoryLimit.conf' @contextlib.contextmanager def mock_cgroup_environment(tmp_dir): """ Creates a mocks environment used by the tests related to cgroups (currently it only provides support for systemd platforms). The command output used in __MOCKED_COMMANDS comes from an Ubuntu 18 system. """ data_files = [ (os.path.join(data_dir, 'init', 'walinuxagent.service'), UnitFilePaths.walinuxagent), (os.path.join(data_dir, 'init', 'azure.slice'), UnitFilePaths.azure), (os.path.join(data_dir, 'init', 'azure-vmextensions.slice'), UnitFilePaths.vmextensions) ] with patch('azurelinuxagent.common.cgroupapi.CGroupsApi.cgroups_supported', return_value=True): with patch('azurelinuxagent.common.osutil.systemd.is_systemd', return_value=True): with MockEnvironment(tmp_dir, commands=_MOCKED_COMMANDS, paths=_MOCKED_PATHS, files=_MOCKED_FILES, data_files=data_files) as mock: yield mock WALinuxAgent-2.9.1.1/tests/common/mock_command.py000077500000000000000000000011051446033677600216430ustar00rootroot00000000000000#!/usr/bin/env python3 import os import sys if len(sys.argv) != 4: sys.stderr.write("usage: {0} ".format(os.path.basename(__file__))) # W0632: Possible unbalanced tuple unpacking with sequence: left side has 3 label(s), right side has 0 value(s) (unbalanced-tuple-unpacking) # Disabled: Unpacking is balanced: there is a check for the length on line 5 stdout, return_value, stderr = sys.argv[1:] # pylint: disable=W0632 if stdout != '': sys.stdout.write(stdout) if stderr != '': sys.stderr.write(stderr) sys.exit(int(return_value)) WALinuxAgent-2.9.1.1/tests/common/mock_environment.py000066400000000000000000000154441446033677600226010ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright 2020 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import re import shutil import subprocess from azurelinuxagent.common.future import ustr from azurelinuxagent.common.utils import fileutil from tests.tools import patch, patch_builtin class MockCommand: def __init__(self, command, stdout='', return_value=0, stderr=''): self.command = command self.stdout = stdout self.return_value = return_value self.stderr = stderr def __str__(self): return ' '.join(self.command) if isinstance(self.command, list) else self.command class MockEnvironment: """ A MockEnvironment is a Context Manager that can be used to mock a set of commands, file system paths, and/or files. It can be useful in tests that need to execute commands or access/modify files that are not available in all platforms, or require root privileges, or can change global system settings. For a sample usage see the mock_cgroup_environment() function. Currently, MockEnvironment mocks subprocess.Popen(), fileutil.mkdir(), os.path.exists() and the builtin() open function (it mocks fileutil.mkdir() instead od os.mkdir() because the agent's code users the former since it provides backwards compatibility with Python 2). The mock for Popen looks for a match in the given 'commands' and, if found, forwards the call to the mock_command.py, which produces the output specified by the matching item. Otherwise it forwards the call to the original Popen function. The mocks for the other functions first look for a match in the given 'files' array and, if found, map the file to the corresponding path in the matching item (if the mapping points to an Exception, the Exception is raised). If there is no match, then it checks if the file is included in the given 'paths' array and maps the path to the given 'tmp_dir' (e.g. "/lib/systemd/system" becomes "/lib/systemd/system".) If there no matches, the path is not changed. Once this mapping has completed the mocks invoke the corresponding original function. Matches are done using regular expressions; the regular expressions in 'paths' must create group 0 to indicate the section of the path that needs to be mapped (i.e. use parenthesis around the section that needs to be mapped.) The items in the given 'data_files' are copied to the 'tmp_dir'. The add_*() methods insert new items int the list of mock objects. Items added by these methods take precedence over the items provided to the __init__() method. """ def __init__(self, tmp_dir, commands=None, paths=None, files=None, data_files=None): # make a copy of the arrays passed as arguments since individual tests can modify them self.tmp_dir = tmp_dir self.commands = [] if commands is None else commands[:] self.paths = [] if paths is None else paths[:] self.files = [] if files is None else files[:] self._data_files = data_files # get references to the functions we'll mock so that we can call the original implementations self._original_popen = subprocess.Popen self._original_mkdir = fileutil.mkdir self._original_path_exists = os.path.exists self._original_open = open self.patchers = [ patch_builtin("open", side_effect=self._mock_open), patch("subprocess.Popen", side_effect=self._mock_popen), patch("os.path.exists", side_effect=self._mock_path_exists), patch("azurelinuxagent.common.utils.fileutil.mkdir", side_effect=self._mock_mkdir) ] def __enter__(self): if self._data_files is not None: for items in self._data_files: self.add_data_file(items[0], items[1]) try: for patcher in self.patchers: patcher.start() except Exception: self._stop_patchers() raise return self def __exit__(self, *_): self._stop_patchers() def _stop_patchers(self): for patcher in self.patchers: try: patcher.stop() except Exception: pass def add_command(self, command): self.commands.insert(0, command) def add_path(self, mock): self.paths.insert(0, mock) def add_file(self, actual, mock): self.files.insert(0, (actual, mock)) def add_data_file(self, source, target): shutil.copyfile(source, self.get_mapped_path(target)) def get_mapped_path(self, path): for item in self.files: match = re.match(item[0], path) if match is not None: return item[1] for item in self.paths: mapped = re.sub(item, r"{0}\1".format(self.tmp_dir), path) if mapped != path: mapped_parent = os.path.split(mapped)[0] if not self._original_path_exists(mapped_parent): os.makedirs(mapped_parent) return mapped return path def _mock_popen(self, command, *args, **kwargs): if isinstance(command, list): command_string = " ".join(command) else: command_string = command for cmd in self.commands: match = re.match(cmd.command, command_string) if match is not None: mock_script = os.path.join(os.path.split(__file__)[0], "mock_command.py") if 'shell' in kwargs and kwargs['shell']: command = "{0} '{1}' {2} '{3}'".format(mock_script, cmd.stdout, cmd.return_value, cmd.stderr) else: command = [mock_script, cmd.stdout, ustr(cmd.return_value), cmd.stderr] break return self._original_popen(command, *args, **kwargs) def _mock_mkdir(self, path, *args, **kwargs): return self._original_mkdir(self.get_mapped_path(path), *args, **kwargs) def _mock_open(self, path, *args, **kwargs): mapped_path = self.get_mapped_path(path) if isinstance(mapped_path, Exception): raise mapped_path return self._original_open(mapped_path, *args, **kwargs) def _mock_path_exists(self, path): return self._original_path_exists(self.get_mapped_path(path)) WALinuxAgent-2.9.1.1/tests/common/osutil/000077500000000000000000000000001446033677600201615ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/common/osutil/__init__.py000066400000000000000000000011651446033677600222750ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # WALinuxAgent-2.9.1.1/tests/common/osutil/test_alpine.py000066400000000000000000000022221446033677600230400ustar00rootroot00000000000000# Copyright 2019 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import unittest from azurelinuxagent.common.osutil.alpine import AlpineOSUtil from tests.tools import AgentTestCase from .test_default import osutil_get_dhcp_pid_should_return_a_list_of_pids class TestAlpineOSUtil(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) def tearDown(self): AgentTestCase.tearDown(self) def test_get_dhcp_pid_should_return_a_list_of_pids(self): osutil_get_dhcp_pid_should_return_a_list_of_pids(self, AlpineOSUtil()) if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/common/osutil/test_arch.py000066400000000000000000000022041446033677600225050ustar00rootroot00000000000000# Copyright 2019 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import unittest from azurelinuxagent.common.osutil.arch import ArchUtil from tests.tools import AgentTestCase from .test_default import osutil_get_dhcp_pid_should_return_a_list_of_pids class TestArchUtil(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) def tearDown(self): AgentTestCase.tearDown(self) def test_get_dhcp_pid_should_return_a_list_of_pids(self): osutil_get_dhcp_pid_should_return_a_list_of_pids(self, ArchUtil()) if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/common/osutil/test_bigip.py000066400000000000000000000270651446033677600226760ustar00rootroot00000000000000# Copyright 2016 F5 Networks Inc. # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import socket import time import unittest import azurelinuxagent.common.logger as logger import azurelinuxagent.common.osutil.bigip as osutil import azurelinuxagent.common.osutil.default as default import azurelinuxagent.common.utils.shellutil as shellutil from azurelinuxagent.common.exception import OSUtilError from azurelinuxagent.common.osutil.bigip import BigIpOSUtil from tests.tools import AgentTestCase, patch from .test_default import osutil_get_dhcp_pid_should_return_a_list_of_pids class TestBigIpOSUtil_wait_until_mcpd_is_initialized(AgentTestCase): @patch.object(shellutil, "run", return_value=0) @patch.object(logger, "info", return_value=None) def test_success(self, *args): result = osutil.BigIpOSUtil._wait_until_mcpd_is_initialized( osutil.BigIpOSUtil() ) self.assertEqual(result, True) # There are two logger calls in the mcpd wait function. The second # occurs after mcpd is found to be "up" self.assertEqual(args[0].call_count, 2) @patch.object(shellutil, "run", return_value=1) @patch.object(logger, "info", return_value=None) @patch.object(time, "sleep", return_value=None) def test_failure(self, *args): # pylint: disable=unused-argument self.assertRaises( OSUtilError, osutil.BigIpOSUtil._wait_until_mcpd_is_initialized, osutil.BigIpOSUtil() ) class TestBigIpOSUtil_save_sys_config(AgentTestCase): @patch.object(shellutil, "run", return_value=0) @patch.object(logger, "error", return_value=None) def test_success(self, *args): result = osutil.BigIpOSUtil._save_sys_config(osutil.BigIpOSUtil()) self.assertEqual(result, 0) self.assertEqual(args[0].call_count, 0) @patch.object(shellutil, "run", return_value=1) @patch.object(logger, "error", return_value=None) def test_failure(self, *args): result = osutil.BigIpOSUtil._save_sys_config(osutil.BigIpOSUtil()) self.assertEqual(result, 1) self.assertEqual(args[0].call_count, 1) class TestBigIpOSUtil_useradd(AgentTestCase): @patch.object(osutil.BigIpOSUtil, 'get_userentry', return_value=None) @patch.object(shellutil, "run_command") def test_success(self, *args): args[0].return_value = (0, None) result = osutil.BigIpOSUtil.useradd( osutil.BigIpOSUtil(), 'foo', expiration=None ) self.assertEqual(result, 0) @patch.object(osutil.BigIpOSUtil, 'get_userentry', return_value=None) def test_user_already_exists(self, *args): args[0].return_value = 'admin' result = osutil.BigIpOSUtil.useradd( osutil.BigIpOSUtil(), 'admin', expiration=None ) self.assertEqual(result, None) @patch.object(shellutil, "run", return_value=1) def test_failure(self, *args): # pylint: disable=unused-argument self.assertRaises( OSUtilError, osutil.BigIpOSUtil.useradd, osutil.BigIpOSUtil(), 'foo', expiration=None ) class TestBigIpOSUtil_chpasswd(AgentTestCase): @patch.object(shellutil, "run_command") @patch.object(osutil.BigIpOSUtil, 'get_userentry', return_value=True) @patch.object(osutil.BigIpOSUtil, 'is_sys_user', return_value=False) @patch.object(osutil.BigIpOSUtil, '_save_sys_config', return_value=None) def test_success(self, *args): result = osutil.BigIpOSUtil.chpasswd( osutil.BigIpOSUtil(), 'admin', 'password', crypt_id=6, salt_len=10 ) self.assertEqual(result, 0) self.assertEqual(args[0].call_count, 1) self.assertEqual(args[0].call_count, 1) @patch.object(osutil.BigIpOSUtil, 'is_sys_user', return_value=True) def test_is_sys_user(self, *args): # pylint: disable=unused-argument self.assertRaises( OSUtilError, osutil.BigIpOSUtil.chpasswd, osutil.BigIpOSUtil(), 'admin', 'password', crypt_id=6, salt_len=10 ) @patch.object(shellutil, "run_get_output", return_value=(1, None)) @patch.object(osutil.BigIpOSUtil, 'is_sys_user', return_value=False) def test_failed_to_set_user_password(self, *args): # pylint: disable=unused-argument self.assertRaises( OSUtilError, osutil.BigIpOSUtil.chpasswd, osutil.BigIpOSUtil(), 'admin', 'password', crypt_id=6, salt_len=10 ) @patch.object(shellutil, "run_get_output", return_value=(0, None)) @patch.object(osutil.BigIpOSUtil, 'is_sys_user', return_value=False) @patch.object(osutil.BigIpOSUtil, 'get_userentry', return_value=None) def test_failed_to_get_user_entry(self, *args): # pylint: disable=unused-argument self.assertRaises( OSUtilError, osutil.BigIpOSUtil.chpasswd, osutil.BigIpOSUtil(), 'admin', 'password', crypt_id=6, salt_len=10 ) class TestBigIpOSUtil_get_dvd_device(AgentTestCase): @patch.object(os, "listdir", return_value=['tty1','cdrom0']) def test_success(self, *args): # pylint: disable=unused-argument result = osutil.BigIpOSUtil.get_dvd_device( osutil.BigIpOSUtil(), '/dev' ) self.assertEqual(result, '/dev/cdrom0') @patch.object(os, "listdir", return_value=['foo', 'bar']) def test_failure(self, *args): # pylint: disable=unused-argument self.assertRaises( OSUtilError, osutil.BigIpOSUtil.get_dvd_device, osutil.BigIpOSUtil(), '/dev' ) class TestBigIpOSUtil_restart_ssh_service(AgentTestCase): @patch.object(shellutil, "run", return_value=0) def test_success(self, *args): # pylint: disable=unused-argument result = osutil.BigIpOSUtil.restart_ssh_service( osutil.BigIpOSUtil() ) self.assertEqual(result, 0) class TestBigIpOSUtil_stop_agent_service(AgentTestCase): @patch.object(shellutil, "run", return_value=0) def test_success(self, *args): # pylint: disable=unused-argument result = osutil.BigIpOSUtil.stop_agent_service( osutil.BigIpOSUtil() ) self.assertEqual(result, 0) class TestBigIpOSUtil_start_agent_service(AgentTestCase): @patch.object(shellutil, "run", return_value=0) def test_success(self, *args): # pylint: disable=unused-argument result = osutil.BigIpOSUtil.start_agent_service( osutil.BigIpOSUtil() ) self.assertEqual(result, 0) class TestBigIpOSUtil_register_agent_service(AgentTestCase): @patch.object(shellutil, "run", return_value=0) def test_success(self, *args): # pylint: disable=unused-argument result = osutil.BigIpOSUtil.register_agent_service( osutil.BigIpOSUtil() ) self.assertEqual(result, 0) class TestBigIpOSUtil_unregister_agent_service(AgentTestCase): @patch.object(shellutil, "run", return_value=0) def test_success(self, *args): # pylint: disable=unused-argument result = osutil.BigIpOSUtil.unregister_agent_service( osutil.BigIpOSUtil() ) self.assertEqual(result, 0) class TestBigIpOSUtil_set_hostname(AgentTestCase): @patch.object(os.path, "exists", return_value=False) def test_success(self, *args): result = osutil.BigIpOSUtil.set_hostname( # pylint: disable=assignment-from-none osutil.BigIpOSUtil(), None ) self.assertEqual(args[0].call_count, 0) self.assertEqual(result, None) class TestBigIpOSUtil_set_dhcp_hostname(AgentTestCase): @patch.object(os.path, "exists", return_value=False) def test_success(self, *args): result = osutil.BigIpOSUtil.set_dhcp_hostname( # pylint: disable=assignment-from-none osutil.BigIpOSUtil(), None ) self.assertEqual(args[0].call_count, 0) self.assertEqual(result, None) class TestBigIpOSUtil_get_first_if(AgentTestCase): @patch.object(osutil.BigIpOSUtil, '_format_single_interface_name', return_value=b'eth0') def test_success(self, *args): # pylint: disable=unused-argument ifname, ipaddr = osutil.BigIpOSUtil().get_first_if() self.assertTrue(ifname.startswith('eth')) self.assertTrue(ipaddr is not None) try: socket.inet_aton(ipaddr) except socket.error: self.fail("not a valid ip address") @patch.object(osutil.BigIpOSUtil, '_format_single_interface_name', return_value=b'loenp0s3') def test_success(self, *args): # pylint: disable=unused-argument,function-redefined ifname, ipaddr = osutil.BigIpOSUtil().get_first_if() self.assertFalse(ifname.startswith('eth')) self.assertTrue(ipaddr is not None) try: socket.inet_aton(ipaddr) except socket.error: self.fail("not a valid ip address") class TestBigIpOSUtil_mount_dvd(AgentTestCase): @patch.object(shellutil, "run", return_value=0) @patch.object(time, "sleep", return_value=None) @patch.object(osutil.BigIpOSUtil, '_wait_until_mcpd_is_initialized', return_value=None) @patch.object(default.DefaultOSUtil, 'mount_dvd', return_value=None) def test_success(self, *args): osutil.BigIpOSUtil.mount_dvd( osutil.BigIpOSUtil(), max_retry=6, chk_err=True ) self.assertEqual(args[0].call_count, 1) self.assertEqual(args[1].call_count, 1) class TestBigIpOSUtil_route_add(AgentTestCase): @patch.object(shellutil, "run", return_value=0) def test_success(self, *args): osutil.BigIpOSUtil.route_add( osutil.BigIpOSUtil(), '10.10.10.0', '255.255.255.0', '10.10.10.1' ) self.assertEqual(args[0].call_count, 1) class TestBigIpOSUtil_device_for_ide_port(AgentTestCase): @patch.object(time, "sleep", return_value=None) @patch.object(os.path, "exists", return_value=False) @patch.object(default.DefaultOSUtil, 'device_for_ide_port', return_value=None) def test_success_waiting(self, *args): osutil.BigIpOSUtil.device_for_ide_port( osutil.BigIpOSUtil(), '5' ) self.assertEqual(args[0].call_count, 1) self.assertEqual(args[1].call_count, 99) self.assertEqual(args[2].call_count, 99) @patch.object(time, "sleep", return_value=None) @patch.object(os.path, "exists", return_value=True) @patch.object(default.DefaultOSUtil, 'device_for_ide_port', return_value=None) def test_success_immediate(self, *args): osutil.BigIpOSUtil.device_for_ide_port( osutil.BigIpOSUtil(), '5' ) self.assertEqual(args[0].call_count, 1) self.assertEqual(args[1].call_count, 1) self.assertEqual(args[2].call_count, 0) class TestBigIpOSUtil(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) def tearDown(self): AgentTestCase.tearDown(self) def test_get_dhcp_pid_should_return_a_list_of_pids(self): osutil_get_dhcp_pid_should_return_a_list_of_pids(self, BigIpOSUtil()) if __name__ == '__main__': unittest.main()WALinuxAgent-2.9.1.1/tests/common/osutil/test_clearlinux.py000066400000000000000000000022341446033677600237410ustar00rootroot00000000000000# Copyright 2019 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import unittest from azurelinuxagent.common.osutil.clearlinux import ClearLinuxUtil from tests.tools import AgentTestCase from .test_default import osutil_get_dhcp_pid_should_return_a_list_of_pids class TestClearLinuxUtil(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) def tearDown(self): AgentTestCase.tearDown(self) def test_get_dhcp_pid_should_return_a_list_of_pids(self): osutil_get_dhcp_pid_should_return_a_list_of_pids(self, ClearLinuxUtil()) if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/common/osutil/test_coreos.py000066400000000000000000000022161446033677600230650ustar00rootroot00000000000000# Copyright 2019 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import unittest from azurelinuxagent.common.osutil.coreos import CoreOSUtil from tests.tools import AgentTestCase from .test_default import osutil_get_dhcp_pid_should_return_a_list_of_pids class TestAlpineOSUtil(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) def tearDown(self): AgentTestCase.tearDown(self) def test_get_dhcp_pid_should_return_a_list_of_pids(self): osutil_get_dhcp_pid_should_return_a_list_of_pids(self, CoreOSUtil()) if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/common/osutil/test_default.py000066400000000000000000001665641446033677600232400ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import contextlib import glob import os import socket import subprocess import tempfile import unittest import mock import azurelinuxagent.common.conf as conf import azurelinuxagent.common.osutil.default as osutil import azurelinuxagent.common.utils.shellutil as shellutil import azurelinuxagent.common.utils.textutil as textutil from azurelinuxagent.common.exception import OSUtilError from azurelinuxagent.common.future import ustr from azurelinuxagent.common.osutil import get_osutil from azurelinuxagent.common.utils import fileutil from azurelinuxagent.common.utils.flexible_version import FlexibleVersion from azurelinuxagent.common.utils.networkutil import AddFirewallRules from tests.common.mock_environment import MockEnvironment from tests.tools import AgentTestCase, patch, open_patch, load_data, data_dir, is_python_version_26_or_34, skip_if_predicate_true actual_get_proc_net_route = 'azurelinuxagent.common.osutil.default.DefaultOSUtil._get_proc_net_route' def fake_is_loopback(_, iface): return iface.startswith('lo') class TestOSUtil(AgentTestCase): def test_restart(self): # setup retries = 3 ifname = 'dummy' with patch.object(shellutil, "run") as run_patch: run_patch.return_value = 1 # execute osutil.DefaultOSUtil.restart_if(osutil.DefaultOSUtil(), ifname=ifname, retries=retries, wait=0) # assert self.assertEqual(run_patch.call_count, retries) self.assertEqual(run_patch.call_args_list[0][0][0], 'ifdown {0} && ifup {0}'.format(ifname)) def test_get_dvd_device_success(self): with patch.object(os, 'listdir', return_value=['cpu', 'cdrom0']): osutil.DefaultOSUtil().get_dvd_device() def test_get_dvd_device_failure(self): with patch.object(os, 'listdir', return_value=['cpu', 'notmatching']): try: osutil.DefaultOSUtil().get_dvd_device() self.fail('OSUtilError was not raised') except OSUtilError as ose: self.assertTrue('notmatching' in ustr(ose)) @patch('time.sleep') def test_mount_dvd_success(self, _): msg = 'message' with patch.object(osutil.DefaultOSUtil, 'get_dvd_device', return_value='/dev/cdrom'): with patch.object(shellutil, 'run_command', return_value=msg): with patch.object(os, 'makedirs'): try: osutil.DefaultOSUtil().mount_dvd() except OSUtilError: self.fail("mounting failed") @patch('time.sleep') def test_mount_dvd_failure(self, _): msg = 'message' exception = shellutil.CommandError("mount dvd", 1, "", msg) with patch.object(osutil.DefaultOSUtil, 'get_dvd_device', return_value='/dev/cdrom'): with patch.object(shellutil, 'run_command', side_effect=exception) as patch_run: with patch.object(os, 'makedirs'): try: osutil.DefaultOSUtil().mount_dvd() self.fail('OSUtilError was not raised') except OSUtilError as ose: self.assertTrue(msg in ustr(ose)) self.assertEqual(patch_run.call_count, 5) def test_empty_proc_net_route(self): routing_table = "" mo = mock.mock_open(read_data=routing_table) with patch(open_patch(), mo): self.assertEqual(len(osutil.DefaultOSUtil().read_route_table()), 0) def test_no_routes(self): routing_table = 'Iface\tDestination\tGateway \tFlags\tRefCnt\tUse\tMetric\tMask\t\tMTU\tWindow\tIRTT \n' mo = mock.mock_open(read_data=routing_table) with patch(open_patch(), mo): raw_route_list = osutil.DefaultOSUtil().read_route_table() self.assertEqual(len(osutil.DefaultOSUtil().get_list_of_routes(raw_route_list)), 0) def test_bogus_proc_net_route(self): routing_table = 'Iface\tDestination\tGateway \tFlags\t\tUse\tMetric\t\neth0\t00000000\t00000000\t0001\t\t0\t0\n' mo = mock.mock_open(read_data=routing_table) with patch(open_patch(), mo): raw_route_list = osutil.DefaultOSUtil().read_route_table() self.assertEqual(len(osutil.DefaultOSUtil().get_list_of_routes(raw_route_list)), 0) def test_valid_routes(self): routing_table = \ 'Iface\tDestination\tGateway \tFlags\tRefCnt\tUse\tMetric\tMask\t\tMTU\tWindow\tIRTT \n' \ 'eth0\t00000000\tC1BB910A\t0003\t0\t0\t0\t00000000\t0\t0\t0 \n' \ 'eth0\tC0BB910A\t00000000\t0001\t0\t0\t0\tC0FFFFFF\t0\t0\t0 \n' \ 'eth0\t10813FA8\tC1BB910A\t000F\t0\t0\t0\tFFFFFFFF\t0\t0\t0 \n' \ 'eth0\tFEA9FEA9\tC1BB910A\t0007\t0\t0\t0\tFFFFFFFF\t0\t0\t0 \n' \ 'docker0\t002BA8C0\t00000000\t0001\t0\t0\t10\t00FFFFFF\t0\t0\t0 \n' known_sha1_hash = b'\x1e\xd1k\xae[\xf8\x9b\x1a\x13\xd0\xbbT\xa4\xe3Y\xa3\xdd\x0b\xbd\xa9' mo = mock.mock_open(read_data=routing_table) with patch(open_patch(), mo): raw_route_list = osutil.DefaultOSUtil().read_route_table() self.assertEqual(len(raw_route_list), 6) self.assertEqual(textutil.hash_strings(raw_route_list), known_sha1_hash) route_list = osutil.DefaultOSUtil().get_list_of_routes(raw_route_list) self.assertEqual(len(route_list), 5) self.assertEqual(route_list[0].gateway_quad(), '10.145.187.193') self.assertEqual(route_list[1].gateway_quad(), '0.0.0.0') self.assertEqual(route_list[1].mask_quad(), '255.255.255.192') self.assertEqual(route_list[2].destination_quad(), '168.63.129.16') self.assertEqual(route_list[1].flags, 1) self.assertEqual(route_list[2].flags, 15) self.assertEqual(route_list[3].flags, 7) self.assertEqual(route_list[3].metric, 0) self.assertEqual(route_list[4].metric, 10) self.assertEqual(route_list[0].interface, 'eth0') self.assertEqual(route_list[4].interface, 'docker0') @patch('azurelinuxagent.common.osutil.default.DefaultOSUtil.get_primary_interface', return_value='eth0') @patch('azurelinuxagent.common.osutil.default.DefaultOSUtil._get_all_interfaces', return_value={'eth0':'10.0.0.1'}) @patch('azurelinuxagent.common.osutil.default.DefaultOSUtil.is_loopback', fake_is_loopback) def test_get_first_if(self, get_all_interfaces_mock, get_primary_interface_mock): # pylint: disable=unused-argument """ Validate that the agent can find the first active non-loopback interface. This test case used to run live, but not all developers have an eth* interface. It is perfectly valid to have a br*, but this test does not account for that. """ ifname, ipaddr = osutil.DefaultOSUtil().get_first_if() self.assertEqual(ifname, 'eth0') self.assertEqual(ipaddr, '10.0.0.1') @patch('azurelinuxagent.common.osutil.default.DefaultOSUtil.get_primary_interface', return_value='bogus0') @patch('azurelinuxagent.common.osutil.default.DefaultOSUtil._get_all_interfaces', return_value={'eth0':'10.0.0.1', 'lo': '127.0.0.1'}) @patch('azurelinuxagent.common.osutil.default.DefaultOSUtil.is_loopback', fake_is_loopback) def test_get_first_if_nosuchprimary(self, get_all_interfaces_mock, get_primary_interface_mock): # pylint: disable=unused-argument ifname, ipaddr = osutil.DefaultOSUtil().get_first_if() self.assertTrue(ifname.startswith('eth')) self.assertTrue(ipaddr is not None) try: socket.inet_aton(ipaddr) except socket.error: self.fail("not a valid ip address") def test_get_first_if_all_loopback(self): fake_ifaces = {'lo':'127.0.0.1'} with patch.object(osutil.DefaultOSUtil, 'get_primary_interface', return_value='bogus0'): with patch.object(osutil.DefaultOSUtil, '_get_all_interfaces', return_value=fake_ifaces): self.assertEqual(('', ''), osutil.DefaultOSUtil().get_first_if()) def test_get_all_interfaces(self): loopback_count = 0 non_loopback_count = 0 for iface in osutil.DefaultOSUtil()._get_all_interfaces(): if iface == 'lo': loopback_count += 1 else: non_loopback_count += 1 self.assertEqual(loopback_count, 1, 'Exactly 1 loopback network interface should exist') self.assertGreater(loopback_count, 0, 'At least 1 non-loopback network interface should exist') def test_isloopback(self): for iface in osutil.DefaultOSUtil()._get_all_interfaces(): if iface == 'lo': self.assertTrue(osutil.DefaultOSUtil().is_loopback(iface)) else: self.assertFalse(osutil.DefaultOSUtil().is_loopback(iface)) def test_isprimary(self): routing_table = "\ Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT \n\ eth0 00000000 01345B0A 0003 0 0 5 00000000 0 0 0 \n\ eth0 00345B0A 00000000 0001 0 0 5 00000000 0 0 0 \n\ lo 00000000 01345B0A 0003 0 0 1 00FCFFFF 0 0 0 \n" mo = mock.mock_open(read_data=routing_table) with patch(open_patch(), mo): self.assertFalse(osutil.DefaultOSUtil().is_primary_interface('lo')) self.assertTrue(osutil.DefaultOSUtil().is_primary_interface('eth0')) def test_sriov(self): routing_table = "\ Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT \n" \ "bond0 00000000 0100000A 0003 0 0 0 00000000 0 0 0 \n" \ "bond0 0000000A 00000000 0001 0 0 0 00000000 0 0 0 \n" \ "eth0 0000000A 00000000 0001 0 0 0 00000000 0 0 0 \n" \ "bond0 10813FA8 0100000A 0007 0 0 0 00000000 0 0 0 \n" \ "bond0 FEA9FEA9 0100000A 0007 0 0 0 00000000 0 0 0 \n" mo = mock.mock_open(read_data=routing_table) with patch(open_patch(), mo): self.assertFalse(osutil.DefaultOSUtil().is_primary_interface('eth0')) self.assertTrue(osutil.DefaultOSUtil().is_primary_interface('bond0')) def test_multiple_default_routes(self): routing_table = "\ Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT \n\ high 00000000 01345B0A 0003 0 0 5 00000000 0 0 0 \n\ low1 00000000 01345B0A 0003 0 0 1 00FCFFFF 0 0 0 \n" mo = mock.mock_open(read_data=routing_table) with patch(open_patch(), mo): self.assertTrue(osutil.DefaultOSUtil().is_primary_interface('low1')) def test_multiple_interfaces(self): routing_table = "\ Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT \n\ first 00000000 01345B0A 0003 0 0 1 00000000 0 0 0 \n\ secnd 00000000 01345B0A 0003 0 0 1 00FCFFFF 0 0 0 \n" mo = mock.mock_open(read_data=routing_table) with patch(open_patch(), mo): self.assertTrue(osutil.DefaultOSUtil().is_primary_interface('first')) def test_interface_flags(self): routing_table = "\ Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT \n\ nflg 00000000 01345B0A 0001 0 0 1 00000000 0 0 0 \n\ flgs 00000000 01345B0A 0003 0 0 1 00FCFFFF 0 0 0 \n" mo = mock.mock_open(read_data=routing_table) with patch(open_patch(), mo): self.assertTrue(osutil.DefaultOSUtil().is_primary_interface('flgs')) def test_no_interface(self): routing_table = "\ Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT \n\ ndst 00000001 01345B0A 0003 0 0 1 00000000 0 0 0 \n\ nflg 00000000 01345B0A 0001 0 0 1 00FCFFFF 0 0 0 \n" mo = mock.mock_open(read_data=routing_table) with patch(open_patch(), mo): self.assertFalse(osutil.DefaultOSUtil().is_primary_interface('ndst')) self.assertFalse(osutil.DefaultOSUtil().is_primary_interface('nflg')) self.assertFalse(osutil.DefaultOSUtil().is_primary_interface('invalid')) def test_no_primary_does_not_throw(self): with patch.object(osutil.DefaultOSUtil, 'get_primary_interface') \ as patch_primary: exception = False patch_primary.return_value = '' try: osutil.DefaultOSUtil().get_first_if()[0] except Exception as e: # pylint: disable=unused-variable print(textutil.format_exception(e)) exception = True self.assertFalse(exception) def test_dhcp_lease_default(self): self.assertTrue(osutil.DefaultOSUtil().get_dhcp_lease_endpoint() is None) def test_dhcp_lease_ubuntu(self): with patch.object(glob, "glob", return_value=['/var/lib/dhcp/dhclient.eth0.leases']): with patch(open_patch(), mock.mock_open(read_data=load_data("dhcp.leases"))): endpoint = get_osutil(distro_name='ubuntu', distro_version='12.04').get_dhcp_lease_endpoint() # pylint: disable=assignment-from-none self.assertTrue(endpoint is not None) self.assertEqual(endpoint, "168.63.129.16") endpoint = get_osutil(distro_name='ubuntu', distro_version='12.04').get_dhcp_lease_endpoint() # pylint: disable=assignment-from-none self.assertTrue(endpoint is not None) self.assertEqual(endpoint, "168.63.129.16") endpoint = get_osutil(distro_name='ubuntu', distro_version='14.04').get_dhcp_lease_endpoint() # pylint: disable=assignment-from-none self.assertTrue(endpoint is not None) self.assertEqual(endpoint, "168.63.129.16") def test_dhcp_lease_custom_dns(self): """ Validate that the wireserver address is coming from option 245 (on default configurations the address is also available in the domain-name-servers option, but users may set up a custom dns server on their vnet) """ with patch.object(glob, "glob", return_value=['/var/lib/dhcp/dhclient.eth0.leases']): with patch(open_patch(), mock.mock_open(read_data=load_data("dhcp.leases.custom.dns"))): endpoint = get_osutil(distro_name='ubuntu', distro_version='14.04').get_dhcp_lease_endpoint() # pylint: disable=assignment-from-none self.assertEqual(endpoint, "168.63.129.16") def test_dhcp_lease_multi(self): with patch.object(glob, "glob", return_value=['/var/lib/dhcp/dhclient.eth0.leases']): with patch(open_patch(), mock.mock_open(read_data=load_data("dhcp.leases.multi"))): endpoint = get_osutil(distro_name='ubuntu', distro_version='12.04').get_dhcp_lease_endpoint() # pylint: disable=assignment-from-none self.assertTrue(endpoint is not None) self.assertEqual(endpoint, "168.63.129.2") def test_get_total_mem(self): """ Validate the returned value matches to the one retrieved by invoking shell command """ cmd = "grep MemTotal /proc/meminfo |awk '{print $2}'" ret = shellutil.run_get_output(cmd) if ret[0] == 0: self.assertEqual(int(ret[1]) / 1024, get_osutil().get_total_mem()) else: self.fail("Cannot retrieve total memory using shell command.") def test_get_processor_cores(self): """ Validate the returned value matches to the one retrieved by invoking shell command """ cmd = "grep 'processor.*:' /proc/cpuinfo |wc -l" ret = shellutil.run_get_output(cmd) if ret[0] == 0: self.assertEqual(int(ret[1]), get_osutil().get_processor_cores()) else: self.fail("Cannot retrieve number of process cores using shell command.") def test_conf_sshd(self): new_file = "\ Port 22\n\ Protocol 2\n\ ChallengeResponseAuthentication yes\n\ #PasswordAuthentication yes\n\ UsePAM yes\n\ " expected_output = "\ Port 22\n\ Protocol 2\n\ ChallengeResponseAuthentication no\n\ #PasswordAuthentication yes\n\ UsePAM yes\n\ PasswordAuthentication no\n\ ClientAliveInterval 180\n\ " with patch.object(fileutil, 'write_file') as patch_write: with patch.object(fileutil, 'read_file', return_value=new_file): osutil.DefaultOSUtil().conf_sshd(disable_password=True) patch_write.assert_called_once_with( conf.get_sshd_conf_file_path(), expected_output) def test_conf_sshd_with_match(self): new_file = "\ Port 22\n\ ChallengeResponseAuthentication yes\n\ Match host 192.168.1.1\n\ ChallengeResponseAuthentication yes\n\ " expected_output = "\ Port 22\n\ ChallengeResponseAuthentication no\n\ PasswordAuthentication no\n\ ClientAliveInterval 180\n\ Match host 192.168.1.1\n\ ChallengeResponseAuthentication yes\n\ " with patch.object(fileutil, 'write_file') as patch_write: with patch.object(fileutil, 'read_file', return_value=new_file): osutil.DefaultOSUtil().conf_sshd(disable_password=True) patch_write.assert_called_once_with( conf.get_sshd_conf_file_path(), expected_output) def test_conf_sshd_with_match_last(self): new_file = "\ Port 22\n\ Match host 192.168.1.1\n\ ChallengeResponseAuthentication yes\n\ " expected_output = "\ Port 22\n\ PasswordAuthentication no\n\ ChallengeResponseAuthentication no\n\ ClientAliveInterval 180\n\ Match host 192.168.1.1\n\ ChallengeResponseAuthentication yes\n\ " with patch.object(fileutil, 'write_file') as patch_write: with patch.object(fileutil, 'read_file', return_value=new_file): osutil.DefaultOSUtil().conf_sshd(disable_password=True) patch_write.assert_called_once_with( conf.get_sshd_conf_file_path(), expected_output) def test_conf_sshd_with_match_middle(self): new_file = "\ Port 22\n\ match host 192.168.1.1\n\ ChallengeResponseAuthentication yes\n\ match all\n\ #Other config\n\ " expected_output = "\ Port 22\n\ match host 192.168.1.1\n\ ChallengeResponseAuthentication yes\n\ match all\n\ #Other config\n\ PasswordAuthentication no\n\ ChallengeResponseAuthentication no\n\ ClientAliveInterval 180\n\ " with patch.object(fileutil, 'write_file') as patch_write: with patch.object(fileutil, 'read_file', return_value=new_file): osutil.DefaultOSUtil().conf_sshd(disable_password=True) patch_write.assert_called_once_with( conf.get_sshd_conf_file_path(), expected_output) def test_conf_sshd_with_match_multiple(self): new_file = "\ Port 22\n\ Match host 192.168.1.1\n\ ChallengeResponseAuthentication yes\n\ Match host 192.168.1.2\n\ ChallengeResponseAuthentication yes\n\ Match all\n\ #Other config\n\ " expected_output = "\ Port 22\n\ Match host 192.168.1.1\n\ ChallengeResponseAuthentication yes\n\ Match host 192.168.1.2\n\ ChallengeResponseAuthentication yes\n\ Match all\n\ #Other config\n\ PasswordAuthentication no\n\ ChallengeResponseAuthentication no\n\ ClientAliveInterval 180\n\ " with patch.object(fileutil, 'write_file') as patch_write: with patch.object(fileutil, 'read_file', return_value=new_file): osutil.DefaultOSUtil().conf_sshd(disable_password=True) patch_write.assert_called_once_with( conf.get_sshd_conf_file_path(), expected_output) def test_conf_sshd_with_match_multiple_first_last(self): new_file = "\ Match host 192.168.1.1\n\ ChallengeResponseAuthentication yes\n\ Match host 192.168.1.2\n\ ChallengeResponseAuthentication yes\n\ " expected_output = "\ PasswordAuthentication no\n\ ChallengeResponseAuthentication no\n\ ClientAliveInterval 180\n\ Match host 192.168.1.1\n\ ChallengeResponseAuthentication yes\n\ Match host 192.168.1.2\n\ ChallengeResponseAuthentication yes\n\ " with patch.object(fileutil, 'write_file') as patch_write: with patch.object(fileutil, 'read_file', return_value=new_file): osutil.DefaultOSUtil().conf_sshd(disable_password=True) patch_write.assert_called_once_with( conf.get_sshd_conf_file_path(), expected_output) def test_correct_instance_id(self): util = osutil.DefaultOSUtil() self.assertEqual( "12345678-1234-1234-1234-123456789012", util._correct_instance_id("78563412-3412-3412-1234-123456789012")) self.assertEqual( "D0DF4C54-4ECB-4A4B-9954-5BDF3ED5C3B8", util._correct_instance_id("544CDFD0-CB4E-4B4A-9954-5BDF3ED5C3B8")) self.assertEqual( "d0df4c54-4ecb-4a4b-9954-5bdf3ed5c3b8", util._correct_instance_id("544cdfd0-cb4e-4b4a-9954-5bdf3ed5c3b8")) @patch('os.path.isfile', return_value=True) @patch('azurelinuxagent.common.utils.fileutil.read_file', return_value="33C2F3B9-1399-429F-8EB3-BA656DF32502") def test_get_instance_id_from_file(self, mock_read, mock_isfile): # pylint: disable=unused-argument util = osutil.DefaultOSUtil() self.assertEqual( util.get_instance_id(), "B9F3C233-9913-9F42-8EB3-BA656DF32502") @patch('os.path.isfile', return_value=True) @patch('azurelinuxagent.common.utils.fileutil.read_file', return_value="") def test_get_instance_id_empty_from_file(self, mock_read, mock_isfile): # pylint: disable=unused-argument util = osutil.DefaultOSUtil() self.assertEqual( "", util.get_instance_id()) @patch('os.path.isfile', return_value=True) @patch('azurelinuxagent.common.utils.fileutil.read_file', return_value="Value") def test_get_instance_id_malformed_from_file(self, mock_read, mock_isfile): # pylint: disable=unused-argument util = osutil.DefaultOSUtil() self.assertEqual( "Value", util.get_instance_id()) @patch('os.path.isfile', return_value=False) @patch('azurelinuxagent.common.utils.shellutil.run_get_output', return_value=[0, '33C2F3B9-1399-429F-8EB3-BA656DF32502']) def test_get_instance_id_from_dmidecode(self, mock_shell, mock_isfile): # pylint: disable=unused-argument util = osutil.DefaultOSUtil() self.assertEqual( util.get_instance_id(), "B9F3C233-9913-9F42-8EB3-BA656DF32502") @patch('os.path.isfile', return_value=False) @patch('azurelinuxagent.common.utils.shellutil.run_get_output', return_value=[1, 'Error Value']) def test_get_instance_id_missing(self, mock_shell, mock_isfile): # pylint: disable=unused-argument util = osutil.DefaultOSUtil() self.assertEqual("", util.get_instance_id()) @patch('os.path.isfile', return_value=False) @patch('azurelinuxagent.common.utils.shellutil.run_get_output', return_value=[0, 'Unexpected Value']) def test_get_instance_id_unexpected(self, mock_shell, mock_isfile): # pylint: disable=unused-argument util = osutil.DefaultOSUtil() self.assertEqual("", util.get_instance_id()) @patch('os.path.isfile', return_value=True) @patch('azurelinuxagent.common.utils.fileutil.read_file') def test_is_current_instance_id_from_file(self, mock_read, mock_isfile): # pylint: disable=unused-argument util = osutil.DefaultOSUtil() mock_read.return_value = "11111111-2222-3333-4444-556677889900" self.assertFalse(util.is_current_instance_id( "B9F3C233-9913-9F42-8EB3-BA656DF32502")) mock_read.return_value = "B9F3C233-9913-9F42-8EB3-BA656DF32502" self.assertTrue(util.is_current_instance_id( "B9F3C233-9913-9F42-8EB3-BA656DF32502")) mock_read.return_value = "33C2F3B9-1399-429F-8EB3-BA656DF32502" self.assertTrue(util.is_current_instance_id( "B9F3C233-9913-9F42-8EB3-BA656DF32502")) mock_read.return_value = "b9f3c233-9913-9f42-8eb3-ba656df32502" self.assertTrue(util.is_current_instance_id( "B9F3C233-9913-9F42-8EB3-BA656DF32502")) mock_read.return_value = "33c2f3b9-1399-429f-8eb3-ba656df32502" self.assertTrue(util.is_current_instance_id( "B9F3C233-9913-9F42-8EB3-BA656DF32502")) @patch('os.path.isfile', return_value=False) @patch('azurelinuxagent.common.utils.shellutil.run_get_output') def test_is_current_instance_id_from_dmidecode(self, mock_shell, mock_isfile): # pylint: disable=unused-argument util = osutil.DefaultOSUtil() mock_shell.return_value = [0, 'B9F3C233-9913-9F42-8EB3-BA656DF32502'] self.assertTrue(util.is_current_instance_id( "B9F3C233-9913-9F42-8EB3-BA656DF32502")) mock_shell.return_value = [0, '33C2F3B9-1399-429F-8EB3-BA656DF32502'] self.assertTrue(util.is_current_instance_id( "B9F3C233-9913-9F42-8EB3-BA656DF32502")) @patch('azurelinuxagent.common.conf.get_sudoers_dir') def test_conf_sudoer(self, mock_dir): tmp_dir = tempfile.mkdtemp() mock_dir.return_value = tmp_dir util = osutil.DefaultOSUtil() # Assert the sudoer line is added if missing util.conf_sudoer("FooBar") waagent_sudoers = os.path.join(tmp_dir, 'waagent') self.assertTrue(os.path.isfile(waagent_sudoers)) count = -1 with open(waagent_sudoers, 'r') as f: count = len(f.readlines()) self.assertEqual(1, count) # Assert the line does not get added a second time util.conf_sudoer("FooBar") count = -1 with open(waagent_sudoers, 'r') as f: count = len(f.readlines()) print("WRITING TO {0}".format(waagent_sudoers)) self.assertEqual(1, count) @staticmethod def _command_to_string(command): return " ".join(command) if isinstance(command, list) else command @staticmethod @contextlib.contextmanager def _mock_iptables(version=osutil._IPTABLES_LOCKING_VERSION, destination='168.63.129.16'): """ Mock for the iptable commands used to set up the firewall. Returns a patch of subprocess.Popen augmented with these properties: * wait - True if the iptable commands use the -w option * destination - The target IP address * uid - The uid used for the -owner option * command_calls - A list of the iptable commands executed by the mock (the --version and -L commands are omitted) * set_command - By default all the mocked commands succeed and produce no output; this method can be used to override the return value and output of these commands (or to add other commands) """ mocked_commands = {} def set_command(command, output='', exit_code=0): command_string = TestOSUtil._command_to_string(command) mocked_commands[command_string] = (output.replace("'", "'\"'\"'"), exit_code) return command_string wait = "-w" if FlexibleVersion(version) >= osutil._IPTABLES_LOCKING_VERSION else "" uid = 42 version_command = set_command(osutil.get_iptables_version_command(), output=str(version)) list_command = set_command(osutil.get_firewall_list_command(wait), output="Mock Output") set_command(osutil.get_firewall_packets_command(wait)) set_command(AddFirewallRules.get_wire_non_root_drop_rule(AddFirewallRules.CHECK_COMMAND, destination, wait=wait)) set_command(AddFirewallRules.get_wire_non_root_drop_rule(AddFirewallRules.APPEND_COMMAND, destination, wait=wait)) set_command(AddFirewallRules.get_wire_root_accept_rule(AddFirewallRules.CHECK_COMMAND, destination, uid, wait=wait)) set_command(AddFirewallRules.get_wire_root_accept_rule(AddFirewallRules.APPEND_COMMAND, destination, uid, wait=wait)) set_command(AddFirewallRules.get_accept_tcp_rule(AddFirewallRules.APPEND_COMMAND, destination, wait=wait)) set_command(AddFirewallRules.get_accept_tcp_rule(AddFirewallRules.INSERT_COMMAND, destination, wait=wait)) set_command(AddFirewallRules.get_accept_tcp_rule(AddFirewallRules.CHECK_COMMAND, destination, wait=wait)) # the agent assumes the rules have been deleted when these commands return 1 set_command(osutil.get_firewall_delete_conntrack_accept_command(wait, destination), exit_code=1) set_command(osutil.get_delete_accept_tcp_rule(wait, destination), exit_code=1) set_command(osutil.get_firewall_delete_owner_accept_command(wait, destination, uid), exit_code=1) set_command(osutil.get_firewall_delete_conntrack_drop_command(wait, destination), exit_code=1) command_calls = [] def mock_popen(command, *args, **kwargs): command_string = TestOSUtil._command_to_string(command) if command_string in mocked_commands: if command_string != version_command and command_string != list_command: command_calls.append(command_string) output, exit_code = mocked_commands[command_string] command = "echo '{0}' && exit {1}".format(output, exit_code) kwargs["shell"] = True return mock_popen.original(command, *args, **kwargs) mock_popen.original = subprocess.Popen with patch("azurelinuxagent.common.cgroupapi.subprocess.Popen", side_effect=mock_popen) as popen_patcher: with patch('os.getuid', return_value=uid): popen_patcher.wait = wait popen_patcher.destination = destination popen_patcher.uid = uid popen_patcher.set_command = set_command popen_patcher.command_calls = command_calls yield popen_patcher def test_get_firewall_dropped_packets_returns_zero_if_firewall_disabled(self): with patch.object(osutil, '_enable_firewall', False): util = osutil.DefaultOSUtil() self.assertEqual(0, util.get_firewall_dropped_packets("not used")) def test_get_firewall_dropped_packets_returns_negative_if_error(self): with TestOSUtil._mock_iptables() as mock_iptables: with patch.object(osutil, '_enable_firewall', True): mock_iptables.set_command(osutil.get_firewall_packets_command(mock_iptables.wait), exit_code=1) self.assertEqual(-1, osutil.DefaultOSUtil().get_firewall_dropped_packets()) def test_get_firewall_dropped_packets_should_ignore_transient_errors(self): with TestOSUtil._mock_iptables() as mock_iptables: with patch.object(osutil, '_enable_firewall', True): mock_iptables.set_command(osutil.get_firewall_packets_command(mock_iptables.wait), exit_code=3, output="can't initialize iptables table `security': iptables who? (do you need to insmod?)") self.assertEqual(0, osutil.DefaultOSUtil().get_firewall_dropped_packets()) def test_get_firewall_dropped_packets_should_ignore_returncode_4(self): with TestOSUtil._mock_iptables() as mock_iptables: with patch.object(osutil, '_enable_firewall', True): mock_iptables.set_command(osutil.get_firewall_packets_command(mock_iptables.wait), exit_code=4, output="iptables v1.8.2 (nf_tables): RULE_REPLACE failed (Invalid argument): rule in chain OUTPUT") self.assertEqual(0, osutil.DefaultOSUtil().get_firewall_dropped_packets()) def test_get_firewall_dropped_packets(self): destination = '168.63.129.16' with TestOSUtil._mock_iptables() as mock_iptables: with patch.object(osutil, '_enable_firewall', True): mock_iptables.set_command(osutil.get_firewall_packets_command(mock_iptables.wait), output=''' Chain OUTPUT (policy ACCEPT 104 packets, 43628 bytes) pkts bytes target prot opt in out source destination 0 0 ACCEPT tcp -- any any anywhere 168.63.129.16 owner UID match daemon 32 1920 DROP tcp -- any any anywhere 168.63.129.16 ''') self.assertEqual(32, osutil.DefaultOSUtil().get_firewall_dropped_packets(destination)) def test_enable_firewall_should_set_up_the_firewall(self): with TestOSUtil._mock_iptables() as mock_iptables: with patch.object(osutil, '_enable_firewall', True): # fail the rule check to force enable of the firewall mock_iptables.set_command(AddFirewallRules.get_accept_tcp_rule(AddFirewallRules.CHECK_COMMAND, mock_iptables.destination, wait=mock_iptables.wait), exit_code=0) mock_iptables.set_command(AddFirewallRules.get_wire_root_accept_rule(AddFirewallRules.CHECK_COMMAND, mock_iptables.destination, mock_iptables.uid, wait=mock_iptables.wait), exit_code=0) mock_iptables.set_command(AddFirewallRules.get_wire_non_root_drop_rule(AddFirewallRules.CHECK_COMMAND, mock_iptables.destination, wait=mock_iptables.wait), exit_code=1) success, _ = osutil.DefaultOSUtil().enable_firewall(dst_ip=mock_iptables.destination, uid=mock_iptables.uid) tcp_check_command = TestOSUtil._command_to_string(AddFirewallRules.get_accept_tcp_rule(AddFirewallRules.CHECK_COMMAND, mock_iptables.destination, wait=mock_iptables.wait)) accept_check_command = TestOSUtil._command_to_string(AddFirewallRules.get_wire_root_accept_rule(AddFirewallRules.CHECK_COMMAND, mock_iptables.destination, mock_iptables.uid, wait=mock_iptables.wait)) drop_check_command = TestOSUtil._command_to_string(AddFirewallRules.get_wire_non_root_drop_rule(AddFirewallRules.CHECK_COMMAND, mock_iptables.destination, wait=mock_iptables.wait)) delete_conntrack_accept_command = TestOSUtil._command_to_string(osutil.get_firewall_delete_conntrack_accept_command(mock_iptables.wait, mock_iptables.destination)) delete_accept_tcp_rule = TestOSUtil._command_to_string(osutil.get_delete_accept_tcp_rule(mock_iptables.wait, mock_iptables.destination)) delete_owner_accept_command = TestOSUtil._command_to_string(osutil.get_firewall_delete_owner_accept_command(mock_iptables.wait, mock_iptables.destination, mock_iptables.uid)) delete_conntrack_drop_command = TestOSUtil._command_to_string(osutil.get_firewall_delete_conntrack_drop_command(mock_iptables.wait, mock_iptables.destination)) accept_tcp_command = TestOSUtil._command_to_string(AddFirewallRules.get_accept_tcp_rule(AddFirewallRules.APPEND_COMMAND, mock_iptables.destination, wait=mock_iptables.wait)) accept_command = TestOSUtil._command_to_string(AddFirewallRules.get_wire_root_accept_rule(AddFirewallRules.APPEND_COMMAND, mock_iptables.destination, mock_iptables.uid, wait=mock_iptables.wait)) drop_add_command = TestOSUtil._command_to_string(AddFirewallRules.get_wire_non_root_drop_rule(AddFirewallRules.APPEND_COMMAND, mock_iptables.destination, wait=mock_iptables.wait)) self.assertTrue(success, "Enabling the firewall was not successful") # Exactly 10 calls have to be made. # First is the check rule check which was mocked to fail, and delete call and then append calls self.assertEqual(len(mock_iptables.command_calls), 10, "Incorrect number of calls to iptables: [{0}]". format(mock_iptables.command_calls)) self.assertEqual(mock_iptables.command_calls[0], tcp_check_command, "The first command should check the tcp rule") self.assertEqual(mock_iptables.command_calls[1], accept_check_command, "The second command should check the accept rule") self.assertEqual(mock_iptables.command_calls[2], drop_check_command, "The third command should check the drop rule") self.assertEqual(mock_iptables.command_calls[3], delete_conntrack_accept_command, "The fourth command should delete the conntrack accept rule: {0}".format( mock_iptables.command_calls[3])) self.assertEqual(mock_iptables.command_calls[4], delete_accept_tcp_rule, "The fifth command should delete the dns tcp accept rule: {0}".format( mock_iptables.command_calls[4])) self.assertEqual(mock_iptables.command_calls[5], delete_owner_accept_command, "The sixth command should delete the owner accept rule: {0}".format( mock_iptables.command_calls[5])) self.assertEqual(mock_iptables.command_calls[6], delete_conntrack_drop_command, "The seventh command should delete the conntrack accept rule : {0}".format( mock_iptables.command_calls[6])) self.assertEqual(mock_iptables.command_calls[7], accept_tcp_command, "The eighth command should add the dns tcp accept rule") self.assertEqual(mock_iptables.command_calls[8], accept_command, "The ninth command should add the accept rule") self.assertEqual(mock_iptables.command_calls[9], drop_add_command, "The tenth command should add the drop rule") self.assertTrue(osutil._enable_firewall, "The firewall should not have been disabled") def test_enable_firewall_should_not_use_wait_when_iptables_does_not_support_it(self): with TestOSUtil._mock_iptables(version=osutil._IPTABLES_LOCKING_VERSION - 1) as mock_iptables: with patch.object(osutil, '_enable_firewall', True): # fail the rule check to force enable of the firewall mock_iptables.set_command(AddFirewallRules.get_accept_tcp_rule(AddFirewallRules.CHECK_COMMAND, mock_iptables.destination, wait=mock_iptables.wait), exit_code=1) mock_iptables.set_command(AddFirewallRules.get_wire_root_accept_rule(AddFirewallRules.CHECK_COMMAND, mock_iptables.destination, mock_iptables.uid, wait=mock_iptables.wait), exit_code=1) mock_iptables.set_command(AddFirewallRules.get_wire_non_root_drop_rule(AddFirewallRules.CHECK_COMMAND, mock_iptables.destination, wait=mock_iptables.wait), exit_code=1) success, _ = osutil.DefaultOSUtil().enable_firewall(dst_ip=mock_iptables.destination, uid=mock_iptables.uid) self.assertTrue(success, "Enabling the firewall was not successful") # Exactly 8 calls have to be made. # First check rule, delete 4 rules, # and Append the IPTable 3 rules. self.assertEqual(len(mock_iptables.command_calls), 8, "Incorrect number of calls to iptables: [{0}]".format(mock_iptables.command_calls)) for command in mock_iptables.command_calls: self.assertNotIn("-w", command, "The -w option should have been used in {0}".format(command)) self.assertTrue(osutil._enable_firewall, "The firewall should not have been disabled") def test_enable_firewall_should_not_set_firewall_if_the_all_the_rules_exists(self): with TestOSUtil._mock_iptables() as mock_iptables: with patch.object(osutil, '_enable_firewall', True): tcp_check_command = mock_iptables.set_command(AddFirewallRules.get_accept_tcp_rule(AddFirewallRules.CHECK_COMMAND, mock_iptables.destination, wait=mock_iptables.wait), exit_code=0) accept_check_command = mock_iptables.set_command(AddFirewallRules.get_wire_root_accept_rule(AddFirewallRules.CHECK_COMMAND, mock_iptables.destination, mock_iptables.uid, wait=mock_iptables.wait), exit_code=0) drop_check_command = mock_iptables.set_command(AddFirewallRules.get_wire_non_root_drop_rule(AddFirewallRules.CHECK_COMMAND, mock_iptables.destination, wait=mock_iptables.wait), exit_code=0) success, _ = osutil.DefaultOSUtil().enable_firewall(dst_ip=mock_iptables.destination, uid=mock_iptables.uid) self.assertTrue(success, "Enabling the firewall was not successful") self.assertEqual(len(mock_iptables.command_calls), 3, "Incorrect number of calls to iptables: [{0}]". format(mock_iptables.command_calls)) self.assertEqual(mock_iptables.command_calls[0], tcp_check_command, "Unexpected command: {0}".format(mock_iptables.command_calls[0])) self.assertEqual(mock_iptables.command_calls[1], accept_check_command, "Unexpected command: {0}".format(mock_iptables.command_calls[1])) self.assertEqual(mock_iptables.command_calls[2], drop_check_command, "Unexpected command: {0}".format(mock_iptables.command_calls[2])) self.assertTrue(osutil._enable_firewall) def test_enable_firewall_should_check_for_invalid_iptables_options(self): with TestOSUtil._mock_iptables() as mock_iptables: with patch.object(osutil, '_enable_firewall', True): # iptables uses the following exit codes # 0 - correct function # 1 - other errors # 2 - errors which appear to be caused by invalid or abused command # line parameters tcp_check_command = mock_iptables.set_command(AddFirewallRules.get_accept_tcp_rule(AddFirewallRules.CHECK_COMMAND, mock_iptables.destination, wait=mock_iptables.wait), exit_code=0) accept_check_command = mock_iptables.set_command(AddFirewallRules.get_wire_root_accept_rule(AddFirewallRules.CHECK_COMMAND, mock_iptables.destination, mock_iptables.uid, wait=mock_iptables.wait), exit_code=0) drop_check_command = mock_iptables.set_command(AddFirewallRules.get_wire_non_root_drop_rule(AddFirewallRules.CHECK_COMMAND, mock_iptables.destination, wait=mock_iptables.wait), exit_code=2) success, _ = osutil.DefaultOSUtil().enable_firewall(dst_ip=mock_iptables.destination, uid=mock_iptables.uid) delete_conntrack_accept_command = TestOSUtil._command_to_string(osutil.get_firewall_delete_conntrack_accept_command(mock_iptables.wait, mock_iptables.destination)) delete_accept_tcp_rule = TestOSUtil._command_to_string(osutil.get_delete_accept_tcp_rule(mock_iptables.wait, mock_iptables.destination)) delete_owner_accept_command = TestOSUtil._command_to_string(osutil.get_firewall_delete_owner_accept_command(mock_iptables.wait, mock_iptables.destination, mock_iptables.uid)) delete_conntrack_drop_command = TestOSUtil._command_to_string(osutil.get_firewall_delete_conntrack_drop_command(mock_iptables.wait, mock_iptables.destination)) self.assertFalse(success, "Enable firewall should have failed") self.assertEqual(len(mock_iptables.command_calls), 7, "Incorrect number of calls to iptables: [{0}]". format(mock_iptables.command_calls)) self.assertEqual(mock_iptables.command_calls[0], tcp_check_command, "The first command should check the tcp rule: {0}".format(mock_iptables.command_calls[0])) self.assertEqual(mock_iptables.command_calls[1], accept_check_command, "The second command should check the accept rule: {0}".format(mock_iptables.command_calls[1])) self.assertEqual(mock_iptables.command_calls[2], drop_check_command, "The third command should check the drop rule: {0}".format(mock_iptables.command_calls[2])) self.assertEqual(mock_iptables.command_calls[3], delete_conntrack_accept_command, "The fourth command should delete the conntrack accept rule: {0}".format(mock_iptables.command_calls[3])) self.assertEqual(mock_iptables.command_calls[4], delete_accept_tcp_rule, "The fifth command should delete the dns tcp accept rule: {0}".format( mock_iptables.command_calls[4])) self.assertEqual(mock_iptables.command_calls[5], delete_owner_accept_command, "The sixth command should delete the owner accept rule: {0}".format(mock_iptables.command_calls[5])) self.assertEqual(mock_iptables.command_calls[6], delete_conntrack_drop_command, "The seventh command should delete the conntrack accept rule : {0}".format(mock_iptables.command_calls[6])) self.assertFalse(osutil._enable_firewall) def test_enable_firewall_skips_if_disabled(self): with TestOSUtil._mock_iptables() as mock_iptables: with patch.object(osutil, '_enable_firewall', False): success, _ = osutil.DefaultOSUtil().enable_firewall(dst_ip=mock_iptables.destination, uid=mock_iptables.uid) self.assertFalse(success, "The firewall should not have been disabled") self.assertEqual(len(mock_iptables.command_calls), 0, "iptables should not have been invoked: [{0}]". format(mock_iptables.command_calls)) self.assertFalse(osutil._enable_firewall) def test_remove_firewall(self): with TestOSUtil._mock_iptables() as mock_iptables: with patch.object(osutil, '_enable_firewall', True): delete_commands = {} def mock_popen(command, *args, **kwargs): command_string = TestOSUtil._command_to_string(command) if AddFirewallRules.DELETE_COMMAND in command_string: # The agent invokes the delete commands continuously until they return 1 to indicate the rules has been removed # The mock returns 0 (success) the first time it is invoked and 1 (rule does not exist) thereafter if command_string not in delete_commands: exit_code = 0 delete_commands[command_string] = 1 else: exit_code = 1 delete_commands[command_string] += 1 command = "echo '' && exit {0}".format(exit_code) kwargs["shell"] = True return mock_popen.original(command, *args, **kwargs) mock_popen.original = subprocess.Popen with patch("azurelinuxagent.common.cgroupapi.subprocess.Popen", side_effect=mock_popen): success = osutil.DefaultOSUtil().remove_firewall(mock_iptables.destination, mock_iptables.uid, mock_iptables.wait) delete_conntrack_accept_command = TestOSUtil._command_to_string(osutil.get_firewall_delete_conntrack_accept_command(mock_iptables.wait, mock_iptables.destination)) delete_accept_tcp_rule = TestOSUtil._command_to_string( osutil.get_delete_accept_tcp_rule(mock_iptables.wait, mock_iptables.destination)) delete_owner_accept_command = TestOSUtil._command_to_string(osutil.get_firewall_delete_owner_accept_command(mock_iptables.wait, mock_iptables.destination, mock_iptables.uid)) delete_conntrack_drop_command = TestOSUtil._command_to_string(osutil.get_firewall_delete_conntrack_drop_command(mock_iptables.wait, mock_iptables.destination)) self.assertTrue(success, "Removing the firewall should have succeeded") self.assertEqual(len(delete_commands), 4, "Expected 4 delete commands: [{0}]".format(delete_commands)) # delete rules < 2.2.26 self.assertIn(delete_accept_tcp_rule, delete_commands, "The delete dns tcp accept command was not executed") self.assertEqual(delete_commands[delete_accept_tcp_rule], 2, "The delete dns tcp accept command should have been executed twice") self.assertIn(delete_conntrack_accept_command, delete_commands, "The delete conntrack accept command was not executed") self.assertEqual(delete_commands[delete_conntrack_accept_command], 2, "The delete conntrack accept command should have been executed twice") self.assertIn(delete_owner_accept_command, delete_commands, "The delete owner accept command was not executed") self.assertEqual(delete_commands[delete_owner_accept_command], 2, "The delete owner accept command should have been executed twice") # delete rules >= 2.2.26 self.assertIn(delete_conntrack_drop_command, delete_commands, "The delete conntrack drop command was not executed") self.assertEqual(delete_commands[delete_conntrack_drop_command], 2, "The delete conntrack drop command should have been executed twice") self.assertTrue(osutil._enable_firewall) def test_remove_firewall_should_not_retry_invalid_rule(self): with TestOSUtil._mock_iptables() as mock_iptables: with patch.object(osutil, '_enable_firewall', True): command = osutil.get_firewall_delete_conntrack_accept_command(mock_iptables.wait, mock_iptables.destination) # Note that the command is actually a valid rule, but we use the mock to report it as invalid (exit code 2) delete_conntrack_accept_command = mock_iptables.set_command(command, exit_code=2) success = osutil.DefaultOSUtil().remove_firewall(mock_iptables.destination, mock_iptables.uid, mock_iptables.wait) self.assertFalse(success, "Removing the firewall should not have succeeded") self.assertEqual(len(mock_iptables.command_calls), 1, "Expected a single call to iptables: [{0}]". format(mock_iptables.command_calls)) self.assertEqual(mock_iptables.command_calls[0], delete_conntrack_accept_command, "Expected call to delete conntrack accept command: {0}".format(mock_iptables.command_calls[0])) self.assertFalse(osutil._enable_firewall) @skip_if_predicate_true(is_python_version_26_or_34, "Disabled on Python 2.6 and 3.4 for now. Need to revisit to fix it") def test_get_nic_state(self): state = osutil.DefaultOSUtil().get_nic_state() self.assertNotEqual(state, {}) self.assertGreater(len(state.keys()), 1) another_state = osutil.DefaultOSUtil().get_nic_state() name = list(another_state.keys())[0] another_state[name].add_ipv4("xyzzy") self.assertNotEqual(state, another_state) as_string = osutil.DefaultOSUtil().get_nic_state(as_string=True) self.assertNotEqual(as_string, '') def test_get_used_and_available_system_memory(self): memory_table = "\ total used free shared buff/cache available \n\ Mem: 8340144128 619352064 5236809728 1499136 2483982336 7426314240 \n\ Swap: 0 0 0 \n" with patch.object(shellutil, 'run_command', return_value=memory_table): used_mem, available_mem = osutil.DefaultOSUtil().get_used_and_available_system_memory() self.assertEqual(used_mem, 619352064/(1024**2), "The value didn't match") self.assertEqual(available_mem, 7426314240/(1024**2), "The value didn't match") def test_get_used_and_available_system_memory_error(self): msg = 'message' exception = shellutil.CommandError("free -d", 1, "", msg) with patch.object(shellutil, 'run_command', side_effect=exception) as patch_run: with self.assertRaises(shellutil.CommandError) as context_manager: osutil.DefaultOSUtil().get_used_and_available_system_memory() self.assertEqual(patch_run.call_count, 1) self.assertEqual(context_manager.exception.returncode, 1) def test_get_dhcp_pid_should_return_a_list_of_pids(self): osutil_get_dhcp_pid_should_return_a_list_of_pids(self, osutil.DefaultOSUtil()) def test_get_dhcp_pid_should_return_an_empty_list_when_the_dhcp_client_is_not_running(self): original_run_command = shellutil.run_command def mock_run_command(cmd): # pylint: disable=unused-argument return original_run_command(["pidof", "non-existing-process"]) with patch("azurelinuxagent.common.utils.shellutil.run_command", side_effect=mock_run_command): pid_list = osutil.DefaultOSUtil().get_dhcp_pid() self.assertTrue(len(pid_list) == 0, "the return value is not an empty list: {0}".format(pid_list)) @patch('os.walk', return_value=[('host3/target3:0:1/3:0:1:0/block', ['sdb'], [])]) @patch('azurelinuxagent.common.utils.fileutil.read_file', return_value='{00000000-0001-8899-0000-000000000000}') @patch('os.listdir', return_value=['00000000-0001-8899-0000-000000000000']) @patch('os.path.exists', return_value=True) def test_device_for_ide_port_gen1_success( self, os_path_exists, # pylint: disable=unused-argument os_listdir, # pylint: disable=unused-argument fileutil_read_file, # pylint: disable=unused-argument os_walk): # pylint: disable=unused-argument dev = osutil.DefaultOSUtil().device_for_ide_port(1) self.assertEqual(dev, 'sdb', 'The returned device should be the resource disk') @patch('os.walk', return_value=[('host0/target0:0:0/0:0:0:1/block', ['sdb'], [])]) @patch('azurelinuxagent.common.utils.fileutil.read_file', return_value='{f8b3781a-1e82-4818-a1c3-63d806ec15bb}') @patch('os.listdir', return_value=['f8b3781a-1e82-4818-a1c3-63d806ec15bb']) @patch('os.path.exists', return_value=True) def test_device_for_ide_port_gen2_success( self, os_path_exists, # pylint: disable=unused-argument os_listdir, # pylint: disable=unused-argument fileutil_read_file, # pylint: disable=unused-argument os_walk): # pylint: disable=unused-argument dev = osutil.DefaultOSUtil().device_for_ide_port(1) self.assertEqual(dev, 'sdb', 'The returned device should be the resource disk') @patch('os.listdir', return_value=['00000000-0000-0000-0000-000000000000']) @patch('os.path.exists', return_value=True) def test_device_for_ide_port_none( self, os_path_exists, # pylint: disable=unused-argument os_listdir): # pylint: disable=unused-argument dev = osutil.DefaultOSUtil().device_for_ide_port(1) self.assertIsNone(dev, 'None should be returned if no resource disk found') def osutil_get_dhcp_pid_should_return_a_list_of_pids(test_instance, osutil_instance): """ This is a very basic test for osutil.get_dhcp_pid. It is simply meant to exercise the implementation of that method in case there are any basic errors, such as a typos, etc. The test does not verify that the implementation returns the PID for the actual dhcp client; in fact, it uses a mock that invokes pidof to return the PID of an arbitrary process (the pidof process itself). Most implementations of get_dhcp_pid use pidof with the appropriate name for the dhcp client. The test is defined as a global function to make it easily accessible from the test suites for each distro. """ original_run_command = shellutil.run_command def mock_run_command(cmd): # pylint: disable=unused-argument return original_run_command(["pidof", "pidof"]) with patch("azurelinuxagent.common.utils.shellutil.run_command", side_effect=mock_run_command): pid = osutil_instance.get_dhcp_pid() test_instance.assertTrue(len(pid) != 0, "get_dhcp_pid did not return a PID") class TestGetPublishedHostname(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) self.__published_hostname = os.path.join(self.tmp_dir, "published_hostname") self.__patcher = patch('azurelinuxagent.common.osutil.default.conf.get_published_hostname', return_value=self.__published_hostname) self.__patcher.start() def tearDown(self): self.__patcher.stop() AgentTestCase.tearDown(self) def __get_published_hostname_contents(self): with open(self.__published_hostname, "r") as file_: return file_.read() def test_get_hostname_record_should_create_published_hostname(self): actual = osutil.DefaultOSUtil().get_hostname_record() expected = socket.gethostname() self.assertEqual(expected, actual, "get_hostname_record returned an incorrect hostname") self.assertTrue(os.path.exists(self.__published_hostname), "The published_hostname file was not created") self.assertEqual(expected, self.__get_published_hostname_contents(), "get_hostname_record returned an incorrect hostname") def test_get_hostname_record_should_use_existing_published_hostname(self): expected = "a-sample-hostname-used-for-testing" with open(self.__published_hostname, "w") as file_: file_.write(expected) actual = osutil.DefaultOSUtil().get_hostname_record() self.assertEqual(expected, actual, "get_hostname_record returned an incorrect hostname") self.assertEqual(expected, self.__get_published_hostname_contents(), "get_hostname_record returned an incorrect hostname") def test_get_hostname_record_should_initialize_the_host_name_using_cloud_init_info(self): with MockEnvironment(self.tmp_dir, files=[('/var/lib/cloud/data/set-hostname', os.path.join(data_dir, "cloud-init", "set-hostname"))]): actual = osutil.DefaultOSUtil().get_hostname_record() expected = "a-sample-set-hostname" self.assertEqual(expected, actual, "get_hostname_record returned an incorrect hostname") self.assertEqual(expected, self.__get_published_hostname_contents(), "get_hostname_record returned an incorrect hostname") if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/common/osutil/test_default_osutil.py000066400000000000000000000017171446033677600246230ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.4+ and Openssl 1.0+ # from azurelinuxagent.common.osutil.default import DefaultOSUtil, shellutil # pylint: disable=unused-import from tests.tools import AgentTestCase, patch # pylint: disable=unused-import class DefaultOsUtilTestCase(AgentTestCase): def test_default_service_name(self): self.assertEqual(DefaultOSUtil().get_service_name(), "waagent") WALinuxAgent-2.9.1.1/tests/common/osutil/test_factory.py000066400000000000000000000364161446033677600232530ustar00rootroot00000000000000# Copyright 2019 Microsoft Corporation # # 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. # # Requires Python 2.4+ and Openssl 1.0+ # from azurelinuxagent.common.osutil.alpine import AlpineOSUtil from azurelinuxagent.common.osutil.arch import ArchUtil from azurelinuxagent.common.osutil.bigip import BigIpOSUtil from azurelinuxagent.common.osutil.clearlinux import ClearLinuxUtil from azurelinuxagent.common.osutil.coreos import CoreOSUtil from azurelinuxagent.common.osutil.debian import DebianOSBaseUtil, DebianOSModernUtil from azurelinuxagent.common.osutil.devuan import DevuanOSUtil from azurelinuxagent.common.osutil.default import DefaultOSUtil from azurelinuxagent.common.osutil.factory import _get_osutil from azurelinuxagent.common.osutil.freebsd import FreeBSDOSUtil from azurelinuxagent.common.osutil.gaia import GaiaOSUtil from azurelinuxagent.common.osutil.iosxe import IosxeOSUtil from azurelinuxagent.common.osutil.openbsd import OpenBSDOSUtil from azurelinuxagent.common.osutil.openwrt import OpenWRTOSUtil from azurelinuxagent.common.osutil.photonos import PhotonOSUtil from azurelinuxagent.common.osutil.redhat import RedhatOSUtil, Redhat6xOSUtil from azurelinuxagent.common.osutil.suse import SUSEOSUtil, SUSE11OSUtil from azurelinuxagent.common.osutil.ubuntu import UbuntuOSUtil, Ubuntu12OSUtil, Ubuntu14OSUtil, \ UbuntuSnappyOSUtil, Ubuntu16OSUtil, Ubuntu18OSUtil from tests.tools import AgentTestCase, patch class TestOsUtilFactory(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) def tearDown(self): AgentTestCase.tearDown(self) @patch("azurelinuxagent.common.logger.warn") def test_get_osutil_it_should_return_default(self, patch_logger): ret = _get_osutil(distro_name="", distro_code_name="", distro_version="", distro_full_name="") self.assertTrue(isinstance(ret, DefaultOSUtil)) self.assertEqual(patch_logger.call_count, 1) self.assertEqual(ret.get_service_name(), "waagent") def test_get_osutil_it_should_return_ubuntu(self): ret = _get_osutil(distro_name="ubuntu", distro_code_name="", distro_version="10.04", distro_full_name="") self.assertTrue(isinstance(ret, UbuntuOSUtil)) self.assertEqual(ret.get_service_name(), "walinuxagent") ret = _get_osutil(distro_name="ubuntu", distro_code_name="", distro_version="12.04", distro_full_name="") self.assertTrue(isinstance(ret, Ubuntu12OSUtil)) self.assertEqual(ret.get_service_name(), "walinuxagent") ret = _get_osutil(distro_name="ubuntu", distro_code_name="trusty", distro_version="14.04", distro_full_name="") self.assertTrue(isinstance(ret, Ubuntu14OSUtil)) self.assertEqual(ret.get_service_name(), "walinuxagent") ret = _get_osutil(distro_name="ubuntu", distro_code_name="xenial", distro_version="16.04", distro_full_name="") self.assertTrue(isinstance(ret, Ubuntu16OSUtil)) self.assertEqual(ret.get_service_name(), "walinuxagent") ret = _get_osutil(distro_name="ubuntu", distro_code_name="bionic", distro_version="18.04", distro_full_name="") self.assertTrue(isinstance(ret, Ubuntu18OSUtil)) self.assertEqual(ret.get_service_name(), "walinuxagent") ret = _get_osutil(distro_name="ubuntu", distro_code_name="focal", distro_version="20.04", distro_full_name="") self.assertTrue(isinstance(ret, Ubuntu18OSUtil)) self.assertEqual(ret.get_service_name(), "walinuxagent") ret = _get_osutil(distro_name="ubuntu", distro_code_name="", distro_version="10.04", distro_full_name="Snappy Ubuntu Core") self.assertTrue(isinstance(ret, UbuntuSnappyOSUtil)) self.assertEqual(ret.get_service_name(), "walinuxagent") def test_get_osutil_it_should_return_arch(self): ret = _get_osutil(distro_name="arch", distro_code_name="", distro_version="", distro_full_name="") self.assertTrue(isinstance(ret, ArchUtil)) self.assertEqual(ret.get_service_name(), "waagent") def test_get_osutil_it_should_return_clear_linux(self): ret = _get_osutil(distro_name="clear linux", distro_code_name="", distro_version="", distro_full_name="Clear Linux") self.assertTrue(isinstance(ret, ClearLinuxUtil)) self.assertEqual(ret.get_service_name(), "waagent") def test_get_osutil_it_should_return_alpine(self): ret = _get_osutil(distro_name="alpine", distro_code_name="", distro_version="", distro_full_name="") self.assertTrue(isinstance(ret, AlpineOSUtil)) self.assertEqual(ret.get_service_name(), "waagent") def test_get_osutil_it_should_return_kali(self): ret = _get_osutil(distro_name="kali", distro_code_name="", distro_version="", distro_full_name="") self.assertTrue(isinstance(ret, DebianOSBaseUtil)) self.assertEqual(ret.get_service_name(), "waagent") def test_get_osutil_it_should_return_coreos(self): ret = _get_osutil(distro_name="coreos", distro_code_name="", distro_version="", distro_full_name="") self.assertTrue(isinstance(ret, CoreOSUtil)) self.assertEqual(ret.get_service_name(), "waagent") ret = _get_osutil(distro_name="flatcar", distro_code_name="", distro_version="", distro_full_name="") self.assertTrue(isinstance(ret, CoreOSUtil)) self.assertEqual(ret.get_service_name(), "waagent") def test_get_osutil_it_should_return_suse(self): ret = _get_osutil(distro_name="suse", distro_code_name="", distro_version="10", distro_full_name="") self.assertTrue(isinstance(ret, SUSEOSUtil)) self.assertEqual(ret.get_service_name(), "waagent") ret = _get_osutil(distro_name="suse", distro_code_name="", distro_full_name="SUSE Linux Enterprise Server", distro_version="11") self.assertTrue(isinstance(ret, SUSE11OSUtil)) self.assertEqual(ret.get_service_name(), "waagent") ret = _get_osutil(distro_name="suse", distro_code_name="", distro_full_name="openSUSE", distro_version="12") self.assertTrue(isinstance(ret, SUSE11OSUtil)) self.assertEqual(ret.get_service_name(), "waagent") def test_get_osutil_it_should_return_debian(self): ret = _get_osutil(distro_name="debian", distro_code_name="", distro_full_name="", distro_version="7") self.assertTrue(isinstance(ret, DebianOSBaseUtil)) self.assertEqual(ret.get_service_name(), "waagent") ret = _get_osutil(distro_name="debian", distro_code_name="", distro_full_name="", distro_version="8") self.assertTrue(isinstance(ret, DebianOSModernUtil)) self.assertEqual(ret.get_service_name(), "walinuxagent") def test_get_osutil_it_should_return_devuan(self): ret = _get_osutil(distro_name="devuan", distro_code_name="", distro_full_name="", distro_version="4") self.assertTrue(isinstance(ret, DevuanOSUtil)) self.assertEqual(ret.get_service_name(), "waagent") def test_get_osutil_it_should_return_redhat(self): ret = _get_osutil(distro_name="redhat", distro_code_name="", distro_full_name="", distro_version="6") self.assertTrue(isinstance(ret, Redhat6xOSUtil)) self.assertEqual(ret.get_service_name(), "waagent") ret = _get_osutil(distro_name="rhel", distro_code_name="", distro_full_name="", distro_version="6") self.assertTrue(isinstance(ret, Redhat6xOSUtil)) self.assertEqual(ret.get_service_name(), "waagent") ret = _get_osutil(distro_name="centos", distro_code_name="", distro_full_name="", distro_version="6") self.assertTrue(isinstance(ret, Redhat6xOSUtil)) self.assertEqual(ret.get_service_name(), "waagent") ret = _get_osutil(distro_name="oracle", distro_code_name="", distro_full_name="", distro_version="6") self.assertTrue(isinstance(ret, Redhat6xOSUtil)) self.assertEqual(ret.get_service_name(), "waagent") ret = _get_osutil(distro_name="redhat", distro_code_name="", distro_full_name="", distro_version="7") self.assertTrue(isinstance(ret, RedhatOSUtil)) self.assertEqual(ret.get_service_name(), "waagent") ret = _get_osutil(distro_name="rhel", distro_code_name="", distro_full_name="", distro_version="7") self.assertTrue(isinstance(ret, RedhatOSUtil)) self.assertEqual(ret.get_service_name(), "waagent") ret = _get_osutil(distro_name="centos", distro_code_name="", distro_full_name="", distro_version="7") self.assertTrue(isinstance(ret, RedhatOSUtil)) self.assertEqual(ret.get_service_name(), "waagent") ret = _get_osutil(distro_name="oracle", distro_code_name="", distro_full_name="", distro_version="7") self.assertTrue(isinstance(ret, RedhatOSUtil)) self.assertEqual(ret.get_service_name(), "waagent") ret = _get_osutil(distro_name="almalinux", distro_code_name="", distro_full_name="", distro_version="8") self.assertTrue(isinstance(ret, RedhatOSUtil)) self.assertEqual(ret.get_service_name(), "waagent") ret = _get_osutil(distro_name="cloudlinux", distro_code_name="", distro_full_name="", distro_version="8") self.assertTrue(isinstance(ret, RedhatOSUtil)) self.assertEqual(ret.get_service_name(), "waagent") ret = _get_osutil(distro_name="rocky", distro_code_name="", distro_full_name="", distro_version="8") self.assertTrue(isinstance(ret, RedhatOSUtil)) self.assertEqual(ret.get_service_name(), "waagent") def test_get_osutil_it_should_return_euleros(self): ret = _get_osutil(distro_name="euleros", distro_code_name="", distro_version="", distro_full_name="") self.assertTrue(isinstance(ret, RedhatOSUtil)) self.assertEqual(ret.get_service_name(), "waagent") def test_get_osutil_it_should_return_uos(self): ret = _get_osutil(distro_name="uos", distro_code_name="", distro_version="", distro_full_name="") self.assertTrue(isinstance(ret, RedhatOSUtil)) self.assertEqual(ret.get_service_name(), "waagent") def test_get_osutil_it_should_return_freebsd(self): ret = _get_osutil(distro_name="freebsd", distro_code_name="", distro_version="", distro_full_name="") self.assertTrue(isinstance(ret, FreeBSDOSUtil)) self.assertEqual(ret.get_service_name(), "waagent") def test_get_osutil_it_should_return_openbsd(self): ret = _get_osutil(distro_name="openbsd", distro_code_name="", distro_version="", distro_full_name="") self.assertTrue(isinstance(ret, OpenBSDOSUtil)) self.assertEqual(ret.get_service_name(), "waagent") def test_get_osutil_it_should_return_bigip(self): ret = _get_osutil(distro_name="bigip", distro_code_name="", distro_version="", distro_full_name="") self.assertTrue(isinstance(ret, BigIpOSUtil)) self.assertEqual(ret.get_service_name(), "waagent") def test_get_osutil_it_should_return_gaia(self): ret = _get_osutil(distro_name="gaia", distro_code_name="", distro_version="", distro_full_name="") self.assertTrue(isinstance(ret, GaiaOSUtil)) self.assertEqual(ret.get_service_name(), "waagent") def test_get_osutil_it_should_return_iosxe(self): ret = _get_osutil(distro_name="iosxe", distro_code_name="", distro_version="", distro_full_name="") self.assertTrue(isinstance(ret, IosxeOSUtil)) self.assertEqual(ret.get_service_name(), "waagent") def test_get_osutil_it_should_return_openwrt(self): ret = _get_osutil(distro_name="openwrt", distro_code_name="", distro_version="", distro_full_name="") self.assertTrue(isinstance(ret, OpenWRTOSUtil)) self.assertEqual(ret.get_service_name(), "waagent") def test_get_osutil_it_should_return_photonos(self): ret = _get_osutil(distro_name="photonos", distro_code_name="", distro_version="", distro_full_name="") self.assertTrue(isinstance(ret, PhotonOSUtil)) self.assertEqual(ret.get_service_name(), "waagent") WALinuxAgent-2.9.1.1/tests/common/osutil/test_freebsd.py000066400000000000000000000113241446033677600232050ustar00rootroot00000000000000# Copyright 2019 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import unittest import azurelinuxagent.common.utils.shellutil as shellutil from azurelinuxagent.common.osutil.freebsd import FreeBSDOSUtil from azurelinuxagent.common.utils import textutil from tests.tools import AgentTestCase, patch from .test_default import osutil_get_dhcp_pid_should_return_a_list_of_pids class TestFreeBSDOSUtil(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) def tearDown(self): AgentTestCase.tearDown(self) def test_get_dhcp_pid_should_return_a_list_of_pids(self): osutil_get_dhcp_pid_should_return_a_list_of_pids(self, FreeBSDOSUtil()) def test_empty_proc_net_route(self): route_table = "" with patch.object(shellutil, 'run_command', return_value=route_table): # Header line only self.assertEqual(len(FreeBSDOSUtil().read_route_table()), 1) def test_no_routes(self): route_table = """Routing tables Internet: Destination Gateway Flags Netif Expire """ with patch.object(shellutil, 'run_command', return_value=route_table): raw_route_list = FreeBSDOSUtil().read_route_table() self.assertEqual(len(FreeBSDOSUtil().get_list_of_routes(raw_route_list)), 0) def test_bogus_proc_net_route(self): route_table = """Routing tables Internet: Destination Gateway Flags Netif Expire 1.1.1 0.0.0 """ with patch.object(shellutil, 'run_command', return_value=route_table): raw_route_list = FreeBSDOSUtil().read_route_table() self.assertEqual(len(FreeBSDOSUtil().get_list_of_routes(raw_route_list)), 0) def test_valid_routes(self): route_table = """Routing tables Internet: Destination Gateway Flags Netif Expire 0.0.0.0 10.145.187.193 UGS em0 10.145.187.192/26 0.0.0.0 US em0 168.63.129.16 10.145.187.193 UH em0 169.254.169.254 10.145.187.193 UHS em0 192.168.43.0 0.0.0.0 US vtbd0 """ with patch.object(shellutil, 'run_command', return_value=route_table): raw_route_list = FreeBSDOSUtil().read_route_table() self.assertEqual(len(raw_route_list), 6) route_list = FreeBSDOSUtil().get_list_of_routes(raw_route_list) self.assertEqual(len(route_list), 5) self.assertEqual(route_list[0].gateway_quad(), '10.145.187.193') self.assertEqual(route_list[1].gateway_quad(), '0.0.0.0') self.assertEqual(route_list[1].mask_quad(), '255.255.255.192') self.assertEqual(route_list[2].destination_quad(), '168.63.129.16') self.assertEqual(route_list[1].flags, 1) self.assertEqual(route_list[2].flags, 33) self.assertEqual(route_list[3].flags, 5) self.assertEqual((route_list[3].metric - route_list[4].metric), 1) self.assertEqual(route_list[0].interface, 'em0') self.assertEqual(route_list[4].interface, 'vtbd0') def test_get_first_if(self): """ Validate that the agent can find the first active non-loopback interface. This test case used to run live, but not all developers have an eth* interface. It is perfectly valid to have a br*, but this test does not account for that. """ freebsdosutil = FreeBSDOSUtil() with patch.object(freebsdosutil, '_get_net_info', return_value=('em0', '10.0.0.1', 'e5:f0:38:aa:da:52')): ifname, ipaddr = freebsdosutil.get_first_if() self.assertEqual(ifname, 'em0') self.assertEqual(ipaddr, '10.0.0.1') def test_no_primary_does_not_throw(self): freebsdosutil = FreeBSDOSUtil() with patch.object(freebsdosutil, '_get_net_info', return_value=('em0', '10.0.0.1', 'e5:f0:38:aa:da:52')): try: freebsdosutil.get_first_if()[0] except Exception as e: # pylint: disable=unused-variable print(textutil.format_exception(e)) exception = True # pylint: disable=unused-variable if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/common/osutil/test_nsbsd.py000066400000000000000000000064771446033677600227210ustar00rootroot00000000000000# Copyright 2019 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import unittest from os import path from azurelinuxagent.common.osutil.nsbsd import NSBSDOSUtil from azurelinuxagent.common.utils.fileutil import read_file from tests.tools import AgentTestCase, patch class TestNSBSDOSUtil(AgentTestCase): dhclient_pid_file = "/var/run/dhclient.pid" def setUp(self): AgentTestCase.setUp(self) def tearDown(self): AgentTestCase.tearDown(self) def test_get_dhcp_pid_should_return_a_list_of_pids(self): with patch.object(NSBSDOSUtil, "resolver"): # instantiating NSBSDOSUtil requires a resolver original_isfile = path.isfile def mock_isfile(path): # pylint: disable=redefined-outer-name return True if path == self.dhclient_pid_file else original_isfile(path) original_read_file = read_file def mock_read_file(file, *args, **kwargs): # pylint: disable=redefined-builtin return "123" if file == self.dhclient_pid_file else original_read_file(file, *args, **kwargs) with patch("os.path.isfile", mock_isfile): with patch("azurelinuxagent.common.osutil.nsbsd.fileutil.read_file", mock_read_file): pid_list = NSBSDOSUtil().get_dhcp_pid() self.assertEqual(pid_list, [123]) def test_get_dhcp_pid_should_return_an_empty_list_when_the_dhcp_client_is_not_running(self): with patch.object(NSBSDOSUtil, "resolver"): # instantiating NSBSDOSUtil requires a resolver # # PID file does not exist # original_isfile = path.isfile def mock_isfile(path): # pylint: disable=redefined-outer-name return False if path == self.dhclient_pid_file else original_isfile(path) with patch("os.path.isfile", mock_isfile): pid_list = NSBSDOSUtil().get_dhcp_pid() self.assertEqual(pid_list, []) # # PID file is empty # original_isfile = path.isfile def mock_isfile(path): # pylint: disable=redefined-outer-name,function-redefined return True if path == self.dhclient_pid_file else original_isfile(path) original_read_file = read_file def mock_read_file(file, *args, **kwargs): # pylint: disable=redefined-builtin return "" if file == self.dhclient_pid_file else original_read_file(file, *args, **kwargs) with patch("os.path.isfile", mock_isfile): with patch("azurelinuxagent.common.osutil.nsbsd.fileutil.read_file", mock_read_file): pid_list = NSBSDOSUtil().get_dhcp_pid() self.assertEqual(pid_list, []) if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/common/osutil/test_openbsd.py000066400000000000000000000022251446033677600232250ustar00rootroot00000000000000# Copyright 2019 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import unittest from azurelinuxagent.common.osutil.openbsd import OpenBSDOSUtil from tests.tools import AgentTestCase from .test_default import osutil_get_dhcp_pid_should_return_a_list_of_pids class TestAlpineOSUtil(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) def tearDown(self): AgentTestCase.tearDown(self) def test_get_dhcp_pid_should_return_a_list_of_pids(self): osutil_get_dhcp_pid_should_return_a_list_of_pids(self, OpenBSDOSUtil()) if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/common/osutil/test_openwrt.py000066400000000000000000000022261446033677600232720ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import unittest from azurelinuxagent.common.osutil.openwrt import OpenWRTOSUtil from tests.tools import AgentTestCase from .test_default import osutil_get_dhcp_pid_should_return_a_list_of_pids class TestOpenWRTOSUtil(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) def tearDown(self): AgentTestCase.tearDown(self) def test_get_dhcp_pid_should_return_a_list_of_pids(self): osutil_get_dhcp_pid_should_return_a_list_of_pids(self, OpenWRTOSUtil()) if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/common/osutil/test_photonos.py000066400000000000000000000022251446033677600234440ustar00rootroot00000000000000# Copyright 2021 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import unittest from azurelinuxagent.common.osutil.photonos import PhotonOSUtil from tests.tools import AgentTestCase from .test_default import osutil_get_dhcp_pid_should_return_a_list_of_pids class TestPhotonOSUtil(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) def tearDown(self): AgentTestCase.tearDown(self) def test_get_dhcp_pid_should_return_a_list_of_pids(self): osutil_get_dhcp_pid_should_return_a_list_of_pids(self, PhotonOSUtil()) if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/common/osutil/test_redhat.py000066400000000000000000000022301446033677600230360ustar00rootroot00000000000000# Copyright 2019 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import unittest from azurelinuxagent.common.osutil.redhat import Redhat6xOSUtil from tests.tools import AgentTestCase from .test_default import osutil_get_dhcp_pid_should_return_a_list_of_pids class TestRedhat6xOSUtil(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) def tearDown(self): AgentTestCase.tearDown(self) def test_get_dhcp_pid_should_return_a_list_of_pids(self): osutil_get_dhcp_pid_should_return_a_list_of_pids(self, Redhat6xOSUtil()) if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/common/osutil/test_suse.py000066400000000000000000000022201446033677600225450ustar00rootroot00000000000000# Copyright 2019 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import unittest from azurelinuxagent.common.osutil.suse import SUSE11OSUtil from tests.tools import AgentTestCase from .test_default import osutil_get_dhcp_pid_should_return_a_list_of_pids class TestSUSE11OSUtil(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) def tearDown(self): AgentTestCase.tearDown(self) def test_get_dhcp_pid_should_return_a_list_of_pids(self): osutil_get_dhcp_pid_should_return_a_list_of_pids(self, SUSE11OSUtil()) if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/common/osutil/test_ubuntu.py000066400000000000000000000027311446033677600231170ustar00rootroot00000000000000# Copyright 2019 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import unittest from azurelinuxagent.common.osutil.ubuntu import Ubuntu12OSUtil, Ubuntu18OSUtil from tests.tools import AgentTestCase from .test_default import osutil_get_dhcp_pid_should_return_a_list_of_pids class TestUbuntu12OSUtil(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) def tearDown(self): AgentTestCase.tearDown(self) def test_get_dhcp_pid_should_return_a_list_of_pids(self): osutil_get_dhcp_pid_should_return_a_list_of_pids(self, Ubuntu12OSUtil()) class TestUbuntu18OSUtil(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) def tearDown(self): AgentTestCase.tearDown(self) def test_get_dhcp_pid_should_return_a_list_of_pids(self): osutil_get_dhcp_pid_should_return_a_list_of_pids(self, Ubuntu18OSUtil()) if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/common/test_agent_supported_feature.py000066400000000000000000000056151446033677600252000ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # from azurelinuxagent.common.agent_supported_feature import SupportedFeatureNames, \ get_agent_supported_features_list_for_crp, get_supported_feature_by_name, \ get_agent_supported_features_list_for_extensions from tests.tools import AgentTestCase, patch class TestAgentSupportedFeature(AgentTestCase): def test_it_should_return_features_properly(self): with patch("azurelinuxagent.common.agent_supported_feature._MultiConfigFeature.is_supported", True): self.assertIn(SupportedFeatureNames.MultiConfig, get_agent_supported_features_list_for_crp(), "Multi-config should be fetched in crp_supported_features") with patch("azurelinuxagent.common.agent_supported_feature._MultiConfigFeature.is_supported", False): self.assertNotIn(SupportedFeatureNames.MultiConfig, get_agent_supported_features_list_for_crp(), "Multi-config should not be fetched in crp_supported_features as not supported") self.assertEqual(SupportedFeatureNames.MultiConfig, get_supported_feature_by_name(SupportedFeatureNames.MultiConfig).name, "Invalid/Wrong feature returned") # Raise error if feature name not found with self.assertRaises(NotImplementedError): get_supported_feature_by_name("ABC") def test_it_should_return_extension_supported_features_properly(self): with patch("azurelinuxagent.common.agent_supported_feature._ETPFeature.is_supported", True): self.assertIn(SupportedFeatureNames.ExtensionTelemetryPipeline, get_agent_supported_features_list_for_extensions(), "ETP should be in supported features list") with patch("azurelinuxagent.common.agent_supported_feature._ETPFeature.is_supported", False): self.assertNotIn(SupportedFeatureNames.ExtensionTelemetryPipeline, get_agent_supported_features_list_for_extensions(), "ETP should not be in supported features list") self.assertEqual(SupportedFeatureNames.ExtensionTelemetryPipeline, get_supported_feature_by_name(SupportedFeatureNames.ExtensionTelemetryPipeline).name, "Invalid/Wrong feature returned") WALinuxAgent-2.9.1.1/tests/common/test_cgroupapi.py000066400000000000000000000267741446033677600222640ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.4+ and Openssl 1.0+ # from __future__ import print_function import os import re import subprocess import tempfile from azurelinuxagent.common.cgroupapi import CGroupsApi, SystemdCgroupsApi from azurelinuxagent.common.cgroupstelemetry import CGroupsTelemetry from azurelinuxagent.common.osutil import systemd from azurelinuxagent.common.utils import fileutil from tests.common.mock_cgroup_environment import mock_cgroup_environment from tests.tools import AgentTestCase, patch, mock_sleep from tests.utils.cgroups_tools import CGroupsTools class _MockedFileSystemTestCase(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) self.cgroups_file_system_root = os.path.join(self.tmp_dir, "cgroup") os.mkdir(self.cgroups_file_system_root) os.mkdir(os.path.join(self.cgroups_file_system_root, "cpu")) os.mkdir(os.path.join(self.cgroups_file_system_root, "memory")) self.mock_cgroups_file_system_root = patch("azurelinuxagent.common.cgroupapi.CGROUPS_FILE_SYSTEM_ROOT", self.cgroups_file_system_root) self.mock_cgroups_file_system_root.start() def tearDown(self): self.mock_cgroups_file_system_root.stop() AgentTestCase.tearDown(self) class CGroupsApiTestCase(_MockedFileSystemTestCase): def test_cgroups_should_be_supported_only_on_ubuntu16_centos7dot4_redhat7dot4_and_later_versions(self): test_cases = [ (['ubuntu', '16.04', 'xenial'], True), (['ubuntu', '16.10', 'yakkety'], True), (['ubuntu', '18.04', 'bionic'], True), (['ubuntu', '18.10', 'cosmic'], True), (['ubuntu', '20.04', 'focal'], True), (['ubuntu', '20.10', 'groovy'], True), (['centos', '7.8', 'Source'], False), (['redhat', '7.8', 'Maipo'], False), (['redhat', '7.9.1908', 'Core'], False), (['centos', '8.1', 'Source'], False), (['redhat', '8.2', 'Maipo'], False), (['redhat', '8.2.2111', 'Core'], False), (['centos', '7.4', 'Source'], False), (['redhat', '7.4', 'Maipo'], False), (['centos', '7.5', 'Source'], False), (['centos', '7.3', 'Maipo'], False), (['redhat', '7.2', 'Maipo'], False), (['bigip', '15.0.1', 'Final'], False), (['gaia', '273.562', 'R80.30'], False), (['debian', '9.1', ''], False), ] for (distro, supported) in test_cases: with patch("azurelinuxagent.common.cgroupapi.get_distro", return_value=distro): self.assertEqual(CGroupsApi.cgroups_supported(), supported, "cgroups_supported() failed on {0}".format(distro)) class SystemdCgroupsApiTestCase(AgentTestCase): def test_get_systemd_version_should_return_a_version_number(self): with mock_cgroup_environment(self.tmp_dir): version_info = systemd.get_version() found = re.search(r"systemd \d+", version_info) is not None self.assertTrue(found, "Could not determine the systemd version: {0}".format(version_info)) def test_get_cpu_and_memory_mount_points_should_return_the_cgroup_mount_points(self): with mock_cgroup_environment(self.tmp_dir): cpu, memory = SystemdCgroupsApi().get_cgroup_mount_points() self.assertEqual(cpu, '/sys/fs/cgroup/cpu,cpuacct', "The mount point for the CPU controller is incorrect") self.assertEqual(memory, '/sys/fs/cgroup/memory', "The mount point for the memory controller is incorrect") def test_get_service_cgroup_paths_should_return_the_cgroup_mount_points(self): with mock_cgroup_environment(self.tmp_dir): cpu, memory = SystemdCgroupsApi().get_unit_cgroup_paths("extension.service") self.assertIn(cpu, '/sys/fs/cgroup/cpu,cpuacct/system.slice/extension.service', "The mount point for the CPU controller is incorrect") self.assertIn(memory, '/sys/fs/cgroup/memory/system.slice/extension.service', "The mount point for the memory controller is incorrect") def test_get_cpu_and_memory_cgroup_relative_paths_for_process_should_return_the_cgroup_relative_paths(self): with mock_cgroup_environment(self.tmp_dir): cpu, memory = SystemdCgroupsApi.get_process_cgroup_relative_paths('self') self.assertEqual(cpu, "system.slice/walinuxagent.service", "The relative path for the CPU cgroup is incorrect") self.assertEqual(memory, "system.slice/walinuxagent.service", "The relative memory for the CPU cgroup is incorrect") def test_get_cgroup2_controllers_should_return_the_v2_cgroup_controllers(self): with mock_cgroup_environment(self.tmp_dir): mount_point, controllers = SystemdCgroupsApi.get_cgroup2_controllers() self.assertEqual(mount_point, "/sys/fs/cgroup/unified", "Invalid mount point for V2 cgroups") self.assertIn("cpu", controllers, "The CPU controller is not in the list of V2 controllers") self.assertIn("memory", controllers, "The memory controller is not in the list of V2 controllers") def test_get_unit_property_should_return_the_value_of_the_given_property(self): with mock_cgroup_environment(self.tmp_dir): cpu_accounting = systemd.get_unit_property("walinuxagent.service", "CPUAccounting") self.assertEqual(cpu_accounting, "no", "Property {0} of {1} is incorrect".format("CPUAccounting", "walinuxagent.service")) def assert_cgroups_created(self, extension_cgroups): self.assertEqual(len(extension_cgroups), 2, 'start_extension_command did not return the expected number of cgroups') cpu_found = memory_found = False for cgroup in extension_cgroups: match = re.match( r'^/sys/fs/cgroup/(cpu|memory)/system.slice/Microsoft.Compute.TestExtension_1\.2\.3\_([a-f0-9-]+)\.scope$', cgroup.path) self.assertTrue(match is not None, "Unexpected path for cgroup: {0}".format(cgroup.path)) if match.group(1) == 'cpu': cpu_found = True if match.group(1) == 'memory': memory_found = True self.assertTrue(cpu_found, 'start_extension_command did not return a cpu cgroup') self.assertTrue(memory_found, 'start_extension_command did not return a memory cgroup') @patch('time.sleep', side_effect=lambda _: mock_sleep()) def test_start_extension_command_should_return_the_command_output(self, _): original_popen = subprocess.Popen def mock_popen(command, *args, **kwargs): if command.startswith('systemd-run --property'): command = "echo TEST_OUTPUT" return original_popen(command, *args, **kwargs) with mock_cgroup_environment(self.tmp_dir): with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as output_file: with patch("azurelinuxagent.common.cgroupapi.subprocess.Popen", side_effect=mock_popen) as popen_patch: # pylint: disable=unused-variable command_output = SystemdCgroupsApi().start_extension_command( extension_name="Microsoft.Compute.TestExtension-1.2.3", command="A_TEST_COMMAND", cmd_name="test", shell=True, timeout=300, cwd=self.tmp_dir, env={}, stdout=output_file, stderr=output_file) self.assertIn("[stdout]\nTEST_OUTPUT\n", command_output, "The test output was not captured") @patch('time.sleep', side_effect=lambda _: mock_sleep()) def test_start_extension_command_should_execute_the_command_in_a_cgroup(self, _): with mock_cgroup_environment(self.tmp_dir): SystemdCgroupsApi().start_extension_command( extension_name="Microsoft.Compute.TestExtension-1.2.3", command="test command", cmd_name="test", shell=False, timeout=300, cwd=self.tmp_dir, env={}, stdout=subprocess.PIPE, stderr=subprocess.PIPE) tracked = CGroupsTelemetry._tracked self.assertTrue( any(cg for cg in tracked.values() if cg.name == 'Microsoft.Compute.TestExtension-1.2.3' and 'cpu' in cg.path), "The extension's CPU is not being tracked") self.assertTrue( any(cg for cg in tracked.values() if cg.name == 'Microsoft.Compute.TestExtension-1.2.3' and 'memory' in cg.path), "The extension's Memory is not being tracked") @patch('time.sleep', side_effect=lambda _: mock_sleep()) def test_start_extension_command_should_use_systemd_to_execute_the_command(self, _): with mock_cgroup_environment(self.tmp_dir): with patch("azurelinuxagent.common.cgroupapi.subprocess.Popen", wraps=subprocess.Popen) as popen_patch: SystemdCgroupsApi().start_extension_command( extension_name="Microsoft.Compute.TestExtension-1.2.3", command="the-test-extension-command", cmd_name="test", timeout=300, shell=True, cwd=self.tmp_dir, env={}, stdout=subprocess.PIPE, stderr=subprocess.PIPE) extension_calls = [args[0] for (args, _) in popen_patch.call_args_list if "the-test-extension-command" in args[0]] self.assertEqual(1, len(extension_calls), "The extension should have been invoked exactly once") self.assertIn("systemd-run", extension_calls[0], "The extension should have been invoked using systemd") class SystemdCgroupsApiMockedFileSystemTestCase(_MockedFileSystemTestCase): def test_cleanup_legacy_cgroups_should_remove_legacy_cgroups(self): # Set up a mock /var/run/waagent.pid file daemon_pid_file = os.path.join(self.tmp_dir, "waagent.pid") fileutil.write_file(daemon_pid_file, "42\n") # Set up old controller cgroups, but do not add the daemon's PID to them legacy_cpu_cgroup = CGroupsTools.create_legacy_agent_cgroup(self.cgroups_file_system_root, "cpu", '') legacy_memory_cgroup = CGroupsTools.create_legacy_agent_cgroup(self.cgroups_file_system_root, "memory", '') with patch("azurelinuxagent.common.cgroupapi.get_agent_pid_file_path", return_value=daemon_pid_file): legacy_cgroups = SystemdCgroupsApi().cleanup_legacy_cgroups() self.assertEqual(legacy_cgroups, 2, "cleanup_legacy_cgroups() did not find all the expected cgroups") self.assertFalse(os.path.exists(legacy_cpu_cgroup), "cleanup_legacy_cgroups() did not remove the CPU legacy cgroup") self.assertFalse(os.path.exists(legacy_memory_cgroup), "cleanup_legacy_cgroups() did not remove the memory legacy cgroup") WALinuxAgent-2.9.1.1/tests/common/test_cgroupconfigurator.py000066400000000000000000001717631446033677600242140ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.4+ and Openssl 1.0+ # from __future__ import print_function import contextlib import os import random import re import subprocess import tempfile import time import threading from nose.plugins.attrib import attr from azurelinuxagent.common import conf from azurelinuxagent.common.cgroup import AGENT_NAME_TELEMETRY, MetricsCounter, MetricValue, MetricsCategory, CpuCgroup from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator, DisableCgroups from azurelinuxagent.common.cgroupstelemetry import CGroupsTelemetry from azurelinuxagent.common.event import WALAEventOperation from azurelinuxagent.common.exception import CGroupsException, ExtensionError, ExtensionErrorCodes, \ AgentMemoryExceededException from azurelinuxagent.common.future import ustr from azurelinuxagent.common.utils import shellutil, fileutil from tests.common.mock_environment import MockCommand from tests.common.mock_cgroup_environment import mock_cgroup_environment, UnitFilePaths from tests.tools import AgentTestCase, patch, mock_sleep, i_am_root, data_dir, is_python_version_26_or_34, skip_if_predicate_true from tests.utils.miscellaneous_tools import format_processes, wait_for class CGroupConfiguratorSystemdTestCase(AgentTestCase): @classmethod def tearDownClass(cls): CGroupConfigurator._instance = None AgentTestCase.tearDownClass() @contextlib.contextmanager def _get_cgroup_configurator(self, initialize=True, enable=True, mock_commands=None): CGroupConfigurator._instance = None configurator = CGroupConfigurator.get_instance() CGroupsTelemetry.reset() with mock_cgroup_environment(self.tmp_dir) as mock_environment: if mock_commands is not None: for command in mock_commands: mock_environment.add_command(command) configurator.mocks = mock_environment if initialize: if not enable: with patch.object(configurator, "enable"): configurator.initialize() else: configurator.initialize() yield configurator def test_initialize_should_enable_cgroups(self): with self._get_cgroup_configurator() as configurator: self.assertTrue(configurator.enabled(), "cgroups were not enabled") def test_initialize_should_start_tracking_the_agent_cgroups(self): with self._get_cgroup_configurator() as configurator: tracked = CGroupsTelemetry._tracked self.assertTrue(configurator.enabled(), "Cgroups should be enabled") self.assertTrue(any(cg for cg in tracked.values() if cg.name == AGENT_NAME_TELEMETRY and 'cpu' in cg.path), "The Agent's CPU is not being tracked. Tracked: {0}".format(tracked)) self.assertTrue(any(cg for cg in tracked.values() if cg.name == AGENT_NAME_TELEMETRY and 'memory' in cg.path), "The Agent's Memory is not being tracked. Tracked: {0}".format(tracked)) def test_initialize_should_start_tracking_other_controllers_when_one_is_not_present(self): command_mocks = [MockCommand(r"^mount -t cgroup$", '''cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd) cgroup on /sys/fs/cgroup/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,rdma) cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset) cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio) cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event) cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb) cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer) cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids) cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices) cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct) cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio) ''')] with self._get_cgroup_configurator(mock_commands=command_mocks) as configurator: tracked = CGroupsTelemetry._tracked self.assertTrue(configurator.enabled(), "Cgroups should be enabled") self.assertFalse(any(cg for cg in tracked.values() if cg.name == 'walinuxagent.service' and 'memory' in cg.path), "The Agent's memory should not be tracked. Tracked: {0}".format(tracked)) def test_initialize_should_not_enable_cgroups_when_the_cpu_and_memory_controllers_are_not_present(self): command_mocks = [MockCommand(r"^mount -t cgroup$", '''cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd) cgroup on /sys/fs/cgroup/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,rdma) cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset) cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio) cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event) cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb) cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer) cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids) cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices) cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio) ''')] with self._get_cgroup_configurator(mock_commands=command_mocks) as configurator: tracked = CGroupsTelemetry._tracked self.assertFalse(configurator.enabled(), "Cgroups should not be enabled") self.assertEqual(len(tracked), 0, "No cgroups should be tracked. Tracked: {0}".format(tracked)) def test_initialize_should_not_enable_cgroups_when_the_agent_is_not_in_the_system_slice(self): command_mocks = [MockCommand(r"^mount -t cgroup$", '''cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd) cgroup on /sys/fs/cgroup/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,rdma) cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset) cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio) cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event) cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb) cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer) cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids) cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices) cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio) ''')] with self._get_cgroup_configurator(mock_commands=command_mocks) as configurator: tracked = CGroupsTelemetry._tracked agent_drop_in_file_cpu_quota = configurator.mocks.get_mapped_path(UnitFilePaths.cpu_quota) self.assertFalse(configurator.enabled(), "Cgroups should not be enabled") self.assertEqual(len(tracked), 0, "No cgroups should be tracked. Tracked: {0}".format(tracked)) self.assertFalse(os.path.exists(agent_drop_in_file_cpu_quota), "{0} should not have been created".format(agent_drop_in_file_cpu_quota)) def test_initialize_should_not_create_unit_files(self): with self._get_cgroup_configurator() as configurator: # get the paths to the mocked files azure_slice_unit_file = configurator.mocks.get_mapped_path(UnitFilePaths.azure) extensions_slice_unit_file = configurator.mocks.get_mapped_path(UnitFilePaths.vmextensions) agent_drop_in_file_slice = configurator.mocks.get_mapped_path(UnitFilePaths.slice) agent_drop_in_file_cpu_accounting = configurator.mocks.get_mapped_path(UnitFilePaths.cpu_accounting) agent_drop_in_file_memory_accounting = configurator.mocks.get_mapped_path(UnitFilePaths.memory_accounting) # The mock creates the slice unit files; delete them os.remove(azure_slice_unit_file) os.remove(extensions_slice_unit_file) # The service file for the agent includes settings for the slice and cpu accounting, but not for cpu quota; initialize() # should not create drop in files for the first 2, but it should create one the cpu quota self.assertFalse(os.path.exists(azure_slice_unit_file), "{0} should not have been created".format(azure_slice_unit_file)) self.assertFalse(os.path.exists(extensions_slice_unit_file), "{0} should not have been created".format(extensions_slice_unit_file)) self.assertFalse(os.path.exists(agent_drop_in_file_slice), "{0} should not have been created".format(agent_drop_in_file_slice)) self.assertFalse(os.path.exists(agent_drop_in_file_cpu_accounting), "{0} should not have been created".format(agent_drop_in_file_cpu_accounting)) self.assertFalse(os.path.exists(agent_drop_in_file_memory_accounting), "{0} should not have been created".format(agent_drop_in_file_memory_accounting)) def test_initialize_should_create_unit_files_when_the_agent_service_file_is_not_updated(self): with self._get_cgroup_configurator(initialize=False) as configurator: # get the paths to the mocked files azure_slice_unit_file = configurator.mocks.get_mapped_path(UnitFilePaths.azure) extensions_slice_unit_file = configurator.mocks.get_mapped_path(UnitFilePaths.vmextensions) agent_drop_in_file_slice = configurator.mocks.get_mapped_path(UnitFilePaths.slice) agent_drop_in_file_cpu_accounting = configurator.mocks.get_mapped_path(UnitFilePaths.cpu_accounting) agent_drop_in_file_memory_accounting = configurator.mocks.get_mapped_path(UnitFilePaths.memory_accounting) # The mock creates the service and slice unit files; replace the former and delete the latter configurator.mocks.add_data_file(os.path.join(data_dir, 'init', "walinuxagent.service.previous"), UnitFilePaths.walinuxagent) os.remove(azure_slice_unit_file) os.remove(extensions_slice_unit_file) configurator.initialize() # The older service file for the agent did not include settings for the slice and cpu parameters; in that case, initialize() should # create drop in files to set those properties self.assertTrue(os.path.exists(azure_slice_unit_file), "{0} was not created".format(azure_slice_unit_file)) self.assertTrue(os.path.exists(extensions_slice_unit_file), "{0} was not created".format(extensions_slice_unit_file)) self.assertTrue(os.path.exists(agent_drop_in_file_slice), "{0} was not created".format(agent_drop_in_file_slice)) self.assertTrue(os.path.exists(agent_drop_in_file_cpu_accounting), "{0} was not created".format(agent_drop_in_file_cpu_accounting)) self.assertTrue(os.path.exists(agent_drop_in_file_memory_accounting), "{0} was not created".format(agent_drop_in_file_memory_accounting)) def test_initialize_should_update_logcollector_memorylimit(self): with self._get_cgroup_configurator(initialize=False) as configurator: log_collector_unit_file = configurator.mocks.get_mapped_path(UnitFilePaths.logcollector) original_memory_limit = "MemoryLimit=30M" # The mock creates the slice unit file with memory limit configurator.mocks.add_data_file(os.path.join(data_dir, 'init', "azure-walinuxagent-logcollector.slice"), UnitFilePaths.logcollector) if not os.path.exists(log_collector_unit_file): raise Exception("{0} should have been created during test setup".format(log_collector_unit_file)) if not fileutil.findre_in_file(log_collector_unit_file, original_memory_limit): raise Exception("MemoryLimit was not set correctly. Expected: {0}. Got:\n{1}".format( original_memory_limit, fileutil.read_file(log_collector_unit_file))) configurator.initialize() # initialize() should update the unit file to remove the memory limit self.assertFalse(fileutil.findre_in_file(log_collector_unit_file, original_memory_limit), "Log collector slice unit file was not updated correctly. Expected no memory limit. Got:\n{0}".format( fileutil.read_file(log_collector_unit_file))) def test_setup_extension_slice_should_create_unit_files(self): with self._get_cgroup_configurator() as configurator: # get the paths to the mocked files extension_slice_unit_file = configurator.mocks.get_mapped_path(UnitFilePaths.extensionslice) configurator.setup_extension_slice(extension_name="Microsoft.CPlat.Extension", cpu_quota=5) expected_cpu_accounting = "CPUAccounting=yes" expected_cpu_quota_percentage = "5%" expected_memory_accounting = "MemoryAccounting=yes" self.assertTrue(os.path.exists(extension_slice_unit_file), "{0} was not created".format(extension_slice_unit_file)) self.assertTrue(fileutil.findre_in_file(extension_slice_unit_file, expected_cpu_accounting), "CPUAccounting was not set correctly. Expected: {0}. Got:\n{1}".format(expected_cpu_accounting, fileutil.read_file( extension_slice_unit_file))) self.assertTrue(fileutil.findre_in_file(extension_slice_unit_file, expected_cpu_quota_percentage), "CPUQuota was not set correctly. Expected: {0}. Got:\n{1}".format(expected_cpu_quota_percentage, fileutil.read_file( extension_slice_unit_file))) self.assertTrue(fileutil.findre_in_file(extension_slice_unit_file, expected_memory_accounting), "MemoryAccounting was not set correctly. Expected: {0}. Got:\n{1}".format(expected_memory_accounting, fileutil.read_file( extension_slice_unit_file))) def test_remove_extension_slice_should_remove_unit_files(self): with self._get_cgroup_configurator() as configurator: with patch("os.path.exists") as mock_path: mock_path.return_value = True # get the paths to the mocked files extension_slice_unit_file = configurator.mocks.get_mapped_path(UnitFilePaths.extensionslice) CGroupsTelemetry._tracked['/sys/fs/cgroup/cpu,cpuacct/azure.slice/azure-vmextensions.slice/' \ 'azure-vmextensions-Microsoft.CPlat.Extension.slice'] = \ CpuCgroup('Microsoft.CPlat.Extension', '/sys/fs/cgroup/cpu,cpuacct/azure.slice/azure-vmextensions.slice/azure-vmextensions-Microsoft.CPlat.Extension.slice') configurator.remove_extension_slice(extension_name="Microsoft.CPlat.Extension") tracked = CGroupsTelemetry._tracked self.assertFalse( any(cg for cg in tracked.values() if cg.name == 'Microsoft.CPlat.Extension' and 'cpu' in cg.path), "The extension's CPU is being tracked") self.assertFalse( any(cg for cg in tracked.values() if cg.name == 'Microsoft.CPlat.Extension' and 'memory' in cg.path), "The extension's Memory is being tracked") self.assertFalse(os.path.exists(extension_slice_unit_file), "{0} should not be present".format(extension_slice_unit_file)) def test_enable_should_raise_cgroups_exception_when_cgroups_are_not_supported(self): with self._get_cgroup_configurator(enable=False) as configurator: with patch.object(configurator, "supported", return_value=False): with self.assertRaises(CGroupsException) as context_manager: configurator.enable() self.assertIn("Attempted to enable cgroups, but they are not supported on the current platform", str(context_manager.exception)) def test_enable_should_set_agent_cpu_quota_and_track_throttled_time(self): with self._get_cgroup_configurator(initialize=False) as configurator: agent_drop_in_file_cpu_quota = configurator.mocks.get_mapped_path(UnitFilePaths.cpu_quota) if os.path.exists(agent_drop_in_file_cpu_quota): raise Exception("{0} should not have been created during test setup".format(agent_drop_in_file_cpu_quota)) configurator.initialize() expected_quota = "CPUQuota={0}%".format(conf.get_agent_cpu_quota()) self.assertTrue(os.path.exists(agent_drop_in_file_cpu_quota), "{0} was not created".format(agent_drop_in_file_cpu_quota)) self.assertTrue( fileutil.findre_in_file(agent_drop_in_file_cpu_quota, expected_quota), "CPUQuota was not set correctly. Expected: {0}. Got:\n{1}".format(expected_quota, fileutil.read_file(agent_drop_in_file_cpu_quota))) self.assertTrue(CGroupsTelemetry.get_track_throttled_time(), "Throttle time should be tracked") def test_enable_should_not_track_throttled_time_when_setting_the_cpu_quota_fails(self): with self._get_cgroup_configurator(initialize=False) as configurator: if CGroupsTelemetry.get_track_throttled_time(): raise Exception("Test setup should not start tracking Throttle Time") configurator.mocks.add_file(UnitFilePaths.cpu_quota, Exception("A TEST EXCEPTION")) configurator.initialize() self.assertFalse(CGroupsTelemetry.get_track_throttled_time(), "Throttle time should not be tracked") def test_disable_should_reset_cpu_quota(self): with self._get_cgroup_configurator() as configurator: if len(CGroupsTelemetry._tracked) == 0: raise Exception("Test setup should have started tracking at least 1 cgroup (the agent's)") if not CGroupsTelemetry._track_throttled_time: raise Exception("Test setup should have started tracking Throttle Time") configurator.disable("UNIT TEST", DisableCgroups.AGENT) agent_drop_in_file_cpu_quota = configurator.mocks.get_mapped_path(UnitFilePaths.cpu_quota) self.assertTrue(os.path.exists(agent_drop_in_file_cpu_quota), "{0} was not created".format(agent_drop_in_file_cpu_quota)) self.assertTrue( fileutil.findre_in_file(agent_drop_in_file_cpu_quota, "^CPUQuota=$"), "CPUQuota was not set correctly. Expected an empty value. Got:\n{0}".format(fileutil.read_file(agent_drop_in_file_cpu_quota))) self.assertEqual(len(CGroupsTelemetry._tracked), 1, "Memory cgroups should be tracked after disable. Tracking: {0}".format(CGroupsTelemetry._tracked)) self.assertFalse(any(cg for cg in CGroupsTelemetry._tracked.values() if cg.name == 'walinuxagent.service' and 'cpu' in cg.path), "The Agent's cpu should not be tracked. Tracked: {0}".format(CGroupsTelemetry._tracked)) def test_disable_should_reset_cpu_quota_for_all_cgroups(self): service_list = [ { "name": "extension.service", "cpuQuotaPercentage": 5 } ] extension_name = "Microsoft.CPlat.Extension" extension_services = {extension_name: service_list} with self._get_cgroup_configurator() as configurator: with patch.object(configurator, "get_extension_services_list", return_value=extension_services): # get the paths to the mocked files agent_drop_in_file_cpu_quota = configurator.mocks.get_mapped_path(UnitFilePaths.cpu_quota) extension_slice_unit_file = configurator.mocks.get_mapped_path(UnitFilePaths.extensionslice) extension_service_cpu_quota = configurator.mocks.get_mapped_path(UnitFilePaths.extension_service_cpu_quota) configurator.setup_extension_slice(extension_name=extension_name, cpu_quota=5) configurator.set_extension_services_cpu_memory_quota(service_list) CGroupsTelemetry._tracked['/sys/fs/cgroup/cpu,cpuacct/system.slice/extension.service'] = \ CpuCgroup('extension.service', '/sys/fs/cgroup/cpu,cpuacct/system.slice/extension.service') CGroupsTelemetry._tracked['/sys/fs/cgroup/cpu,cpuacct/azure.slice/azure-vmextensions.slice/' \ 'azure-vmextensions-Microsoft.CPlat.Extension.slice'] = \ CpuCgroup('Microsoft.CPlat.Extension', '/sys/fs/cgroup/cpu,cpuacct/azure.slice/azure-vmextensions.slice/azure-vmextensions-Microsoft.CPlat.Extension.slice') configurator.disable("UNIT TEST", DisableCgroups.ALL) self.assertTrue(os.path.exists(agent_drop_in_file_cpu_quota), "{0} was not created".format(agent_drop_in_file_cpu_quota)) self.assertTrue( fileutil.findre_in_file(agent_drop_in_file_cpu_quota, "^CPUQuota=$"), "CPUQuota was not set correctly. Expected an empty value. Got:\n{0}".format( fileutil.read_file(agent_drop_in_file_cpu_quota))) self.assertTrue(os.path.exists(extension_slice_unit_file), "{0} was not created".format(extension_slice_unit_file)) self.assertTrue( fileutil.findre_in_file(extension_slice_unit_file, "^CPUQuota=$"), "CPUQuota was not set correctly. Expected an empty value. Got:\n{0}".format( fileutil.read_file(extension_slice_unit_file))) self.assertTrue(os.path.exists(extension_service_cpu_quota), "{0} was not created".format(extension_service_cpu_quota)) self.assertTrue( fileutil.findre_in_file(extension_service_cpu_quota, "^CPUQuota=$"), "CPUQuota was not set correctly. Expected an empty value. Got:\n{0}".format( fileutil.read_file(extension_service_cpu_quota))) self.assertEqual(len(CGroupsTelemetry._tracked), 0, "No cgroups should be tracked after disable. Tracking: {0}".format( CGroupsTelemetry._tracked)) @patch('time.sleep', side_effect=lambda _: mock_sleep()) def test_start_extension_command_should_not_use_systemd_when_cgroups_are_not_enabled(self, _): with self._get_cgroup_configurator() as configurator: configurator.disable("UNIT TEST", DisableCgroups.ALL) with patch("azurelinuxagent.common.cgroupapi.subprocess.Popen", wraps=subprocess.Popen) as patcher: configurator.start_extension_command( extension_name="Microsoft.Compute.TestExtension-1.2.3", command="date", cmd_name="test", timeout=300, shell=False, cwd=self.tmp_dir, env={}, stdout=subprocess.PIPE, stderr=subprocess.PIPE) command_calls = [args[0] for args, _ in patcher.call_args_list if len(args) > 0 and "date" in args[0]] self.assertEqual(len(command_calls), 1, "The test command should have been called exactly once [{0}]".format(command_calls)) self.assertNotIn("systemd-run", command_calls[0], "The command should not have been invoked using systemd") self.assertEqual(command_calls[0], "date", "The command line should not have been modified") @patch('time.sleep', side_effect=lambda _: mock_sleep()) def test_start_extension_command_should_use_systemd_run_when_cgroups_are_enabled(self, _): with self._get_cgroup_configurator() as configurator: with patch("azurelinuxagent.common.cgroupapi.subprocess.Popen", wraps=subprocess.Popen) as popen_patch: configurator.start_extension_command( extension_name="Microsoft.Compute.TestExtension-1.2.3", command="the-test-extension-command", cmd_name="test", timeout=300, shell=False, cwd=self.tmp_dir, env={}, stdout=subprocess.PIPE, stderr=subprocess.PIPE) command_calls = [args[0] for (args, _) in popen_patch.call_args_list if "the-test-extension-command" in args[0]] self.assertEqual(len(command_calls), 1, "The test command should have been called exactly once [{0}]".format(command_calls)) self.assertIn("systemd-run", command_calls[0], "The extension should have been invoked using systemd") @patch('time.sleep', side_effect=lambda _: mock_sleep()) def test_start_extension_command_should_start_tracking_the_extension_cgroups(self, _): # CPU usage is initialized when we begin tracking a CPU cgroup; since this test does not retrieve the # CPU usage, there is no need for initialization with self._get_cgroup_configurator() as configurator: configurator.start_extension_command( extension_name="Microsoft.Compute.TestExtension-1.2.3", command="test command", cmd_name="test", timeout=300, shell=False, cwd=self.tmp_dir, env={}, stdout=subprocess.PIPE, stderr=subprocess.PIPE) tracked = CGroupsTelemetry._tracked self.assertTrue( any(cg for cg in tracked.values() if cg.name == 'Microsoft.Compute.TestExtension-1.2.3' and 'cpu' in cg.path), "The extension's CPU is not being tracked") self.assertTrue( any(cg for cg in tracked.values() if cg.name == 'Microsoft.Compute.TestExtension-1.2.3' and 'memory' in cg.path), "The extension's Memory is not being tracked") def test_start_extension_command_should_raise_an_exception_when_the_command_cannot_be_started(self): with self._get_cgroup_configurator() as configurator: original_popen = subprocess.Popen def mock_popen(command_arg, *args, **kwargs): if "test command" in command_arg: raise Exception("A TEST EXCEPTION") return original_popen(command_arg, *args, **kwargs) with patch("azurelinuxagent.common.cgroupapi.subprocess.Popen", side_effect=mock_popen): with self.assertRaises(Exception) as context_manager: configurator.start_extension_command( extension_name="Microsoft.Compute.TestExtension-1.2.3", command="test command", cmd_name="test", timeout=300, shell=False, cwd=self.tmp_dir, env={}, stdout=subprocess.PIPE, stderr=subprocess.PIPE) self.assertIn("A TEST EXCEPTION", str(context_manager.exception)) @patch('time.sleep', side_effect=lambda _: mock_sleep()) def test_start_extension_command_should_disable_cgroups_and_invoke_the_command_directly_if_systemd_fails(self, _): with self._get_cgroup_configurator() as configurator: configurator.mocks.add_command(MockCommand("systemd-run", return_value=1, stdout='', stderr='Failed to start transient scope unit: syntax error')) with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as output_file: with patch("azurelinuxagent.common.cgroupconfigurator.add_event") as mock_add_event: with patch("subprocess.Popen", wraps=subprocess.Popen) as popen_patch: CGroupsTelemetry.reset() command = "echo TEST_OUTPUT" command_output = configurator.start_extension_command( extension_name="Microsoft.Compute.TestExtension-1.2.3", command=command, cmd_name="test", timeout=300, shell=True, cwd=self.tmp_dir, env={}, stdout=output_file, stderr=output_file) self.assertFalse(configurator.enabled(), "Cgroups should have been disabled") disabled_events = [kwargs for _, kwargs in mock_add_event.call_args_list if kwargs['op'] == WALAEventOperation.CGroupsDisabled] self.assertTrue(len(disabled_events) == 1, "Exactly one CGroupsDisabled telemetry event should have been issued. Found: {0}".format(disabled_events)) self.assertIn("Failed to start Microsoft.Compute.TestExtension-1.2.3 using systemd-run", disabled_events[0]['message'], "The systemd-run failure was not included in the telemetry message") self.assertEqual(False, disabled_events[0]['is_success'], "The telemetry event should indicate a failure") extension_calls = [args[0] for (args, _) in popen_patch.call_args_list if command in args[0]] self.assertEqual(2, len(extension_calls), "The extension should have been invoked exactly twice") self.assertIn("systemd-run", extension_calls[0], "The first call to the extension should have used systemd") self.assertEqual(command, extension_calls[1], "The second call to the extension should not have used systemd") self.assertEqual(len(CGroupsTelemetry._tracked), 0, "No cgroups should have been created") self.assertIn("TEST_OUTPUT\n", command_output, "The test output was not captured") @patch('time.sleep', side_effect=lambda _: mock_sleep()) def test_start_extension_command_should_disable_cgroups_and_invoke_the_command_directly_if_systemd_times_out(self, _): with self._get_cgroup_configurator() as configurator: # Systemd has its own internal timeout which is shorter than what we define for extension operation timeout. # When systemd times out, it will write a message to stderr and exit with exit code 1. # In that case, we will internally recognize the failure due to the non-zero exit code, not as a timeout. configurator.mocks.add_command(MockCommand("systemd-run", return_value=1, stdout='', stderr='Failed to start transient scope unit: Connection timed out')) with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stdout: with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stderr: with patch("subprocess.Popen", wraps=subprocess.Popen) as popen_patch: CGroupsTelemetry.reset() configurator.start_extension_command( extension_name="Microsoft.Compute.TestExtension-1.2.3", command="echo 'success'", cmd_name="test", timeout=300, shell=True, cwd=self.tmp_dir, env={}, stdout=stdout, stderr=stderr) self.assertFalse(configurator.enabled(), "Cgroups should have been disabled") extension_calls = [args[0] for (args, _) in popen_patch.call_args_list if "echo 'success'" in args[0]] self.assertEqual(2, len(extension_calls), "The extension should have been called twice. Got: {0}".format(extension_calls)) self.assertIn("systemd-run", extension_calls[0], "The first call to the extension should have used systemd") self.assertNotIn("systemd-run", extension_calls[1], "The second call to the extension should not have used systemd") self.assertEqual(len(CGroupsTelemetry._tracked), 0, "No cgroups should have been created") @skip_if_predicate_true(is_python_version_26_or_34, "Disabled on Python 2.6 and 3.4 for now. Need to revisit to fix it") @attr('requires_sudo') @patch('time.sleep', side_effect=lambda _: mock_sleep()) def test_start_extension_command_should_not_use_fallback_option_if_extension_fails(self, *args): self.assertTrue(i_am_root(), "Test does not run when non-root") with self._get_cgroup_configurator() as configurator: pass # release the mocks used to create the test CGroupConfigurator so that they do not conflict the mock Popen below command = "ls folder_does_not_exist" with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stdout: with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stderr: with patch("azurelinuxagent.common.cgroupapi.subprocess.Popen", wraps=subprocess.Popen) as popen_patch: with self.assertRaises(ExtensionError) as context_manager: configurator.start_extension_command( extension_name="Microsoft.Compute.TestExtension-1.2.3", command=command, cmd_name="test", timeout=300, shell=True, cwd=self.tmp_dir, env={}, stdout=stdout, stderr=stderr) extension_calls = [args[0] for (args, _) in popen_patch.call_args_list if command in args[0]] self.assertEqual(1, len(extension_calls), "The extension should have been invoked exactly once") self.assertIn("systemd-run", extension_calls[0], "The first call to the extension should have used systemd") self.assertEqual(context_manager.exception.code, ExtensionErrorCodes.PluginUnknownFailure) self.assertIn("Non-zero exit code", ustr(context_manager.exception)) # The scope name should appear in the process output since systemd-run was invoked and stderr # wasn't truncated. self.assertIn("Running scope as unit", ustr(context_manager.exception)) @skip_if_predicate_true(is_python_version_26_or_34, "Disabled on Python 2.6 and 3.4 for now. Need to revisit to fix it") @attr('requires_sudo') @patch('time.sleep', side_effect=lambda _: mock_sleep()) @patch("azurelinuxagent.common.utils.extensionprocessutil.TELEMETRY_MESSAGE_MAX_LEN", 5) def test_start_extension_command_should_not_use_fallback_option_if_extension_fails_with_long_output(self, *args): self.assertTrue(i_am_root(), "Test does not run when non-root") with self._get_cgroup_configurator() as configurator: pass # release the mocks used to create the test CGroupConfigurator so that they do not conflict the mock Popen below long_output = "a"*20 # large enough to ensure both stdout and stderr are truncated long_stdout_stderr_command = "echo {0} && echo {0} >&2 && ls folder_does_not_exist".format(long_output) with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stdout: with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stderr: with patch("azurelinuxagent.common.cgroupapi.subprocess.Popen", wraps=subprocess.Popen) as popen_patch: with self.assertRaises(ExtensionError) as context_manager: configurator.start_extension_command( extension_name="Microsoft.Compute.TestExtension-1.2.3", command=long_stdout_stderr_command, cmd_name="test", timeout=300, shell=True, cwd=self.tmp_dir, env={}, stdout=stdout, stderr=stderr) extension_calls = [args[0] for (args, _) in popen_patch.call_args_list if long_stdout_stderr_command in args[0]] self.assertEqual(1, len(extension_calls), "The extension should have been invoked exactly once") self.assertIn("systemd-run", extension_calls[0], "The first call to the extension should have used systemd") self.assertEqual(context_manager.exception.code, ExtensionErrorCodes.PluginUnknownFailure) self.assertIn("Non-zero exit code", ustr(context_manager.exception)) # stdout and stderr should have been truncated, so the scope name doesn't appear in stderr # even though systemd-run ran self.assertNotIn("Running scope as unit", ustr(context_manager.exception)) @attr('requires_sudo') def test_start_extension_command_should_not_use_fallback_option_if_extension_times_out(self, *args): # pylint: disable=unused-argument self.assertTrue(i_am_root(), "Test does not run when non-root") with self._get_cgroup_configurator() as configurator: pass # release the mocks used to create the test CGroupConfigurator so that they do not conflict the mock Popen below with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stdout: with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stderr: with patch("azurelinuxagent.common.utils.extensionprocessutil.wait_for_process_completion_or_timeout", return_value=[True, None, 0]): with patch("azurelinuxagent.common.cgroupapi.SystemdCgroupsApi._is_systemd_failure", return_value=False): with self.assertRaises(ExtensionError) as context_manager: configurator.start_extension_command( extension_name="Microsoft.Compute.TestExtension-1.2.3", command="date", cmd_name="test", timeout=300, shell=True, cwd=self.tmp_dir, env={}, stdout=stdout, stderr=stderr) self.assertEqual(context_manager.exception.code, ExtensionErrorCodes.PluginHandlerScriptTimedout) self.assertIn("Timeout", ustr(context_manager.exception)) @patch('time.sleep', side_effect=lambda _: mock_sleep()) def test_start_extension_command_should_capture_only_the_last_subprocess_output(self, _): with self._get_cgroup_configurator() as configurator: pass # release the mocks used to create the test CGroupConfigurator so that they do not conflict the mock Popen below original_popen = subprocess.Popen def mock_popen(command, *args, **kwargs): # Inject a syntax error to the call # Popen can accept both strings and lists, handle both here. if isinstance(command, str): systemd_command = command.replace('systemd-run', 'systemd-run syntax_error') elif isinstance(command, list) and command[0] == 'systemd-run': systemd_command = ['systemd-run', 'syntax_error'] + command[1:] return original_popen(systemd_command, *args, **kwargs) expected_output = "[stdout]\n{0}\n\n\n[stderr]\n" with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stdout: with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stderr: with patch("azurelinuxagent.common.cgroupapi.subprocess.Popen", side_effect=mock_popen): # We expect this call to fail because of the syntax error process_output = configurator.start_extension_command( extension_name="Microsoft.Compute.TestExtension-1.2.3", command="echo 'very specific test message'", cmd_name="test", timeout=300, shell=True, cwd=self.tmp_dir, env={}, stdout=stdout, stderr=stderr) self.assertEqual(expected_output.format("very specific test message"), process_output) def test_it_should_set_extension_services_cpu_memory_quota(self): service_list = [ { "name": "extension.service", "cpuQuotaPercentage": 5 } ] with self._get_cgroup_configurator() as configurator: # get the paths to the mocked files extension_service_cpu_accounting = configurator.mocks.get_mapped_path(UnitFilePaths.extension_service_cpu_accounting) extension_service_cpu_quota = configurator.mocks.get_mapped_path(UnitFilePaths.extension_service_cpu_quota) configurator.set_extension_services_cpu_memory_quota(service_list) expected_cpu_accounting = "CPUAccounting=yes" expected_cpu_quota_percentage = "CPUQuota=5%" # create drop in files to set those properties self.assertTrue(os.path.exists(extension_service_cpu_accounting), "{0} was not created".format(extension_service_cpu_accounting)) self.assertTrue( fileutil.findre_in_file(extension_service_cpu_accounting, expected_cpu_accounting), "CPUAccounting was not enabled. Expected: {0}. Got:\n{1}".format(expected_cpu_accounting, fileutil.read_file(extension_service_cpu_accounting))) self.assertTrue(os.path.exists(extension_service_cpu_quota), "{0} was not created".format(extension_service_cpu_quota)) self.assertTrue( fileutil.findre_in_file(extension_service_cpu_quota, expected_cpu_quota_percentage), "CPUQuota was not set. Expected: {0}. Got:\n{1}".format(expected_cpu_quota_percentage, fileutil.read_file(extension_service_cpu_quota))) def test_it_should_set_extension_services_when_quotas_not_defined(self): service_list = [ { "name": "extension.service" } ] with self._get_cgroup_configurator() as configurator: # get the paths to the mocked files extension_service_cpu_accounting = configurator.mocks.get_mapped_path(UnitFilePaths.extension_service_cpu_accounting) extension_service_cpu_quota = configurator.mocks.get_mapped_path(UnitFilePaths.extension_service_cpu_quota) extension_service_memory_accounting = configurator.mocks.get_mapped_path(UnitFilePaths.extension_service_memory_accounting) extension_service_memory_quota = configurator.mocks.get_mapped_path(UnitFilePaths.extension_service_memory_limit) configurator.set_extension_services_cpu_memory_quota(service_list) self.assertTrue(os.path.exists(extension_service_cpu_accounting), "{0} was not created".format(extension_service_cpu_accounting)) self.assertFalse(os.path.exists(extension_service_cpu_quota), "{0} should not have been created during setup".format(extension_service_cpu_quota)) self.assertTrue(os.path.exists(extension_service_memory_accounting), "{0} was not created".format(extension_service_memory_accounting)) self.assertFalse(os.path.exists(extension_service_memory_quota), "{0} should not have been created during setup".format(extension_service_memory_quota)) def test_it_should_start_tracking_extension_services_cgroups(self): service_list = [ { "name": "extension.service" } ] with self._get_cgroup_configurator() as configurator: configurator.start_tracking_extension_services_cgroups(service_list) tracked = CGroupsTelemetry._tracked self.assertTrue( any(cg for cg in tracked.values() if cg.name == 'extension.service' and 'cpu' in cg.path), "The extension service's CPU is not being tracked") self.assertTrue( any(cg for cg in tracked.values() if cg.name == 'extension.service' and 'memory' in cg.path), "The extension service's Memory is not being tracked") def test_it_should_stop_tracking_extension_services_cgroups(self): service_list = [ { "name": "extension.service" } ] with self._get_cgroup_configurator() as configurator: with patch("os.path.exists") as mock_path: mock_path.return_value = True CGroupsTelemetry.track_cgroup(CpuCgroup('extension.service', '/sys/fs/cgroup/cpu,cpuacct/system.slice/extension.service')) configurator.stop_tracking_extension_services_cgroups(service_list) tracked = CGroupsTelemetry._tracked self.assertFalse( any(cg for cg in tracked.values() if cg.name == 'extension.service' and 'cpu' in cg.path), "The extension service's CPU is being tracked") self.assertFalse( any(cg for cg in tracked.values() if cg.name == 'extension.service' and 'memory' in cg.path), "The extension service's Memory is being tracked") def test_it_should_remove_extension_services_drop_in_files(self): service_list = [ { "name": "extension.service", "cpuQuotaPercentage": 5 } ] with self._get_cgroup_configurator() as configurator: extension_service_cpu_accounting = configurator.mocks.get_mapped_path( UnitFilePaths.extension_service_cpu_accounting) extension_service_cpu_quota = configurator.mocks.get_mapped_path(UnitFilePaths.extension_service_cpu_quota) extension_service_memory_accounting = configurator.mocks.get_mapped_path( UnitFilePaths.extension_service_memory_accounting) configurator.remove_extension_services_drop_in_files(service_list) self.assertFalse(os.path.exists(extension_service_cpu_accounting), "{0} should not have been created".format(extension_service_cpu_accounting)) self.assertFalse(os.path.exists(extension_service_cpu_quota), "{0} should not have been created".format(extension_service_cpu_quota)) self.assertFalse(os.path.exists(extension_service_memory_accounting), "{0} should not have been created".format(extension_service_memory_accounting)) def test_it_should_start_tracking_unit_cgroups(self): with self._get_cgroup_configurator() as configurator: configurator.start_tracking_unit_cgroups("extension.service") tracked = CGroupsTelemetry._tracked self.assertTrue( any(cg for cg in tracked.values() if cg.name == 'extension.service' and 'cpu' in cg.path), "The extension service's CPU is not being tracked") self.assertTrue( any(cg for cg in tracked.values() if cg.name == 'extension.service' and 'memory' in cg.path), "The extension service's Memory is not being tracked") def test_it_should_stop_tracking_unit_cgroups(self): def side_effect(path): if path == '/sys/fs/cgroup/cpu,cpuacct/system.slice/extension.service': return True return False with self._get_cgroup_configurator() as configurator: with patch("os.path.exists") as mock_path: mock_path.side_effect = side_effect CGroupsTelemetry._tracked['/sys/fs/cgroup/cpu,cpuacct/system.slice/extension.service'] = \ CpuCgroup('extension.service', '/sys/fs/cgroup/cpu,cpuacct/system.slice/extension.service') configurator.stop_tracking_unit_cgroups("extension.service") tracked = CGroupsTelemetry._tracked self.assertFalse( any(cg for cg in tracked.values() if cg.name == 'extension.service' and 'cpu' in cg.path), "The extension service's CPU is being tracked") self.assertFalse( any(cg for cg in tracked.values() if cg.name == 'extension.service' and 'memory' in cg.path), "The extension service's Memory is being tracked") def test_check_processes_in_agent_cgroup_should_raise_a_cgroups_exception_when_there_are_unexpected_processes_in_the_agent_cgroup(self): with self._get_cgroup_configurator() as configurator: pass # release the mocks used to create the test CGroupConfigurator so that they do not conflict the mock Popen below # The test script recursively creates a given number of descendant processes, then it blocks until the # 'stop_file' exists. It produces an output file containing the PID of each descendant process. test_script = os.path.join(self.tmp_dir, "create_processes.sh") stop_file = os.path.join(self.tmp_dir, "create_processes.stop") AgentTestCase.create_script(test_script, """ #!/usr/bin/env bash set -euo pipefail if [[ $# != 2 ]]; then echo "Usage: $0 " exit 1 fi echo $$ >> $1 if [[ $2 > 1 ]]; then $0 $1 $(($2 - 1)) else timeout 30s /usr/bin/env bash -c "while ! [[ -f {0} ]]; do sleep 0.25s; done" fi exit 0 """.format(stop_file)) number_of_descendants = 3 def wait_for_processes(processes_file): def _all_present(): if os.path.exists(processes_file): with open(processes_file, "r") as file_stream: _all_present.processes = [int(process) for process in file_stream.read().split()] return len(_all_present.processes) >= number_of_descendants _all_present.processes = [] if not wait_for(_all_present): raise Exception("Timeout waiting for processes. Expected {0}; got: {1}".format( number_of_descendants, format_processes(_all_present.processes))) return _all_present.processes threads = [] try: # # Start the processes that will be used by the test. We use two sets of processes: the first set simulates a command executed by the agent # (e.g. iptables) and its child processes, if any. The second set of processes simulates an extension. # agent_command_output = os.path.join(self.tmp_dir, "agent_command.pids") agent_command = threading.Thread(target=lambda: shellutil.run_command([test_script, agent_command_output, str(number_of_descendants)])) agent_command.start() threads.append(agent_command) agent_command_processes = wait_for_processes(agent_command_output) extension_output = os.path.join(self.tmp_dir, "extension.pids") def start_extension(): original_sleep = time.sleep original_popen = subprocess.Popen # Extensions are started using systemd-run; mock Popen to remove the call to systemd-run; the test script creates a couple of # child processes, which would simulate the extension's processes. def mock_popen(command, *args, **kwargs): match = re.match(r"^systemd-run --property=CPUAccounting=no --property=MemoryAccounting=no --unit=[^\s]+ --scope --slice=[^\s]+ (.+)", command) is_systemd_run = match is not None if is_systemd_run: command = match.group(1) process = original_popen(command, *args, **kwargs) if is_systemd_run: start_extension.systemd_run_pid = process.pid return process with patch('time.sleep', side_effect=lambda _: original_sleep(0.1)): # start_extension_command has a small delay; skip it with patch("azurelinuxagent.common.cgroupapi.subprocess.Popen", side_effect=mock_popen): with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stdout: with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stderr: configurator.start_extension_command( extension_name="TestExtension", command="{0} {1} {2}".format(test_script, extension_output, number_of_descendants), cmd_name="test", timeout=30, shell=True, cwd=self.tmp_dir, env={}, stdout=stdout, stderr=stderr) start_extension.systemd_run_pid = None extension = threading.Thread(target=start_extension) extension.start() threads.append(extension) extension_processes = wait_for_processes(extension_output) # # check_processes_in_agent_cgroup uses shellutil and the cgroups api to get the commands that are currently running; # wait for all the processes to show up # if not wait_for(lambda: len(shellutil.get_running_commands()) > 0 and len(configurator._cgroups_api.get_systemd_run_commands()) > 0): raise Exception("Timeout while attempting to track the child commands") # # Verify that check_processes_in_agent_cgroup raises when there are unexpected processes in the agent's cgroup. # # For the agent's processes, we use the current process and its parent (in the actual agent these would be the daemon and the extension # handler), and the commands started by the agent. # # For other processes, we use process 1, a process that already completed, and an extension. Note that extensions are started using # systemd-run and the process for that commands belongs to the agent's cgroup but the processes for the extension should be in a # different cgroup # def get_completed_process(): random.seed() completed = random.randint(1000, 10000) while os.path.exists("/proc/{0}".format(completed)): # ensure we do not use an existing process completed = random.randint(1000, 10000) return completed agent_processes = [os.getppid(), os.getpid()] + agent_command_processes + [start_extension.systemd_run_pid] other_processes = [1, get_completed_process()] + extension_processes with patch("azurelinuxagent.common.cgroupconfigurator.CGroupsApi.get_processes_in_cgroup", return_value=agent_processes + other_processes): with self.assertRaises(CGroupsException) as context_manager: configurator._check_processes_in_agent_cgroup() # The list of processes in the message is an array of strings: "['foo', ..., 'bar']" message = ustr(context_manager.exception) search = re.search(r'unexpected processes: \[(?P.+)\]', message) self.assertIsNotNone(search, "The event message is not in the expected format: {0}".format(message)) reported = search.group('processes').split(',') self.assertEqual( len(other_processes), len(reported), "An incorrect number of processes was reported. Expected: {0} Got: {1}".format(format_processes(other_processes), reported)) for pid in other_processes: self.assertTrue( any("[PID: {0}]".format(pid) in reported_process for reported_process in reported), "Process {0} was not reported. Got: {1}".format(format_processes([pid]), reported)) finally: # create the file that stops the test processes and wait for them to complete open(stop_file, "w").close() for thread in threads: thread.join(timeout=5) def test_check_agent_throttled_time_should_raise_a_cgroups_exception_when_the_threshold_is_exceeded(self): metrics = [MetricValue(MetricsCategory.CPU_CATEGORY, MetricsCounter.THROTTLED_TIME, AGENT_NAME_TELEMETRY, conf.get_agent_cpu_throttled_time_threshold() + 1)] with self.assertRaises(CGroupsException) as context_manager: CGroupConfigurator._Impl._check_agent_throttled_time(metrics) self.assertIn("The agent has been throttled", ustr(context_manager.exception), "An incorrect exception was raised") def test_check_cgroups_should_disable_cgroups_when_a_check_fails(self): with self._get_cgroup_configurator() as configurator: checks = ["_check_processes_in_agent_cgroup", "_check_agent_throttled_time"] for method_to_fail in checks: patchers = [] try: # mock 'method_to_fail' to raise an exception and the rest to do nothing for method_to_mock in checks: side_effect = CGroupsException(method_to_fail) if method_to_mock == method_to_fail else lambda *_: None p = patch.object(configurator, method_to_mock, side_effect=side_effect) patchers.append(p) p.start() with patch("azurelinuxagent.common.cgroupconfigurator.add_event") as add_event: configurator.enable() tracked_metrics = [ MetricValue(MetricsCategory.CPU_CATEGORY, MetricsCounter.PROCESSOR_PERCENT_TIME, "test", 10)] configurator.check_cgroups(tracked_metrics) if method_to_fail == "_check_processes_in_agent_cgroup": self.assertFalse(configurator.enabled(), "An error in {0} should have disabled cgroups".format(method_to_fail)) else: self.assertFalse(configurator.agent_enabled(), "An error in {0} should have disabled cgroups".format(method_to_fail)) disable_events = [kwargs for _, kwargs in add_event.call_args_list if kwargs["op"] == WALAEventOperation.CGroupsDisabled] self.assertTrue( len(disable_events) == 1, "Exactly 1 event should have been emitted when {0} fails. Got: {1}".format(method_to_fail, disable_events)) self.assertIn( "[CGroupsException] {0}".format(method_to_fail), disable_events[0]["message"], "The error message is not correct when {0} failed".format(method_to_fail)) finally: for p in patchers: p.stop() def test_check_agent_memory_usage_should_raise_a_cgroups_exception_when_the_limit_is_exceeded(self): metrics = [MetricValue(MetricsCategory.MEMORY_CATEGORY, MetricsCounter.TOTAL_MEM_USAGE, AGENT_NAME_TELEMETRY, conf.get_agent_memory_quota() + 1), MetricValue(MetricsCategory.MEMORY_CATEGORY, MetricsCounter.SWAP_MEM_USAGE, AGENT_NAME_TELEMETRY, conf.get_agent_memory_quota() + 1)] with self.assertRaises(AgentMemoryExceededException) as context_manager: with self._get_cgroup_configurator() as configurator: with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_tracked_metrics") as tracked_metrics: tracked_metrics.return_value = metrics configurator.check_agent_memory_usage() self.assertIn("The agent memory limit {0} bytes exceeded".format(conf.get_agent_memory_quota()), ustr(context_manager.exception), "An incorrect exception was raised")WALinuxAgent-2.9.1.1/tests/common/test_cgroups.py000066400000000000000000000232251446033677600217410ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.4+ and Openssl 1.0+ # from __future__ import print_function import errno import os import random import shutil from azurelinuxagent.common.cgroup import CpuCgroup, MemoryCgroup, MetricsCounter, CounterNotFound from azurelinuxagent.common.exception import CGroupsException from azurelinuxagent.common.osutil import get_osutil from azurelinuxagent.common.utils import fileutil from tests.tools import AgentTestCase, patch, data_dir def consume_cpu_time(): waste = 0 for x in range(1, 200000): # pylint: disable=unused-variable waste += random.random() return waste class TestCGroup(AgentTestCase): def test_is_active(self): test_cgroup = CpuCgroup("test_extension", self.tmp_dir) self.assertEqual(False, test_cgroup.is_active()) with open(os.path.join(self.tmp_dir, "tasks"), mode="wb") as tasks: tasks.write(str(1000).encode()) self.assertEqual(True, test_cgroup.is_active()) @patch("azurelinuxagent.common.logger.periodic_warn") def test_is_active_file_not_present(self, patch_periodic_warn): test_cgroup = CpuCgroup("test_extension", self.tmp_dir) self.assertEqual(False, test_cgroup.is_active()) test_cgroup = MemoryCgroup("test_extension", os.path.join(self.tmp_dir, "this_cgroup_does_not_exist")) self.assertEqual(False, test_cgroup.is_active()) self.assertEqual(0, patch_periodic_warn.call_count) @patch("azurelinuxagent.common.logger.periodic_warn") def test_is_active_incorrect_file(self, patch_periodic_warn): open(os.path.join(self.tmp_dir, "tasks"), mode="wb").close() test_cgroup = CpuCgroup("test_extension", os.path.join(self.tmp_dir, "tasks")) self.assertEqual(False, test_cgroup.is_active()) self.assertEqual(1, patch_periodic_warn.call_count) class TestCpuCgroup(AgentTestCase): @classmethod def setUpClass(cls): AgentTestCase.setUpClass() original_read_file = fileutil.read_file # # Tests that need to mock the contents of /proc/stat or */cpuacct/stat can set this map from # the file that needs to be mocked to the mock file (each test starts with an empty map). If # an Exception is given instead of a path, the exception is raised # cls.mock_read_file_map = {} def mock_read_file(filepath, **args): if filepath in cls.mock_read_file_map: mapped_value = cls.mock_read_file_map[filepath] if isinstance(mapped_value, Exception): raise mapped_value filepath = mapped_value return original_read_file(filepath, **args) cls.mock_read_file = patch("azurelinuxagent.common.utils.fileutil.read_file", side_effect=mock_read_file) cls.mock_read_file.start() @classmethod def tearDownClass(cls): cls.mock_read_file.stop() AgentTestCase.tearDownClass() def setUp(self): AgentTestCase.setUp(self) TestCpuCgroup.mock_read_file_map.clear() def test_initialize_cpu_usage_should_set_current_cpu_usage(self): cgroup = CpuCgroup("test", "/sys/fs/cgroup/cpu/system.slice/test") TestCpuCgroup.mock_read_file_map = { "/proc/stat": os.path.join(data_dir, "cgroups", "proc_stat_t0"), os.path.join(cgroup.path, "cpuacct.stat"): os.path.join(data_dir, "cgroups", "cpuacct.stat_t0") } cgroup.initialize_cpu_usage() self.assertEqual(cgroup._current_cgroup_cpu, 63763) self.assertEqual(cgroup._current_system_cpu, 5496872) def test_get_cpu_usage_should_return_the_cpu_usage_since_its_last_invocation(self): osutil = get_osutil() cgroup = CpuCgroup("test", "/sys/fs/cgroup/cpu/system.slice/test") TestCpuCgroup.mock_read_file_map = { "/proc/stat": os.path.join(data_dir, "cgroups", "proc_stat_t0"), os.path.join(cgroup.path, "cpuacct.stat"): os.path.join(data_dir, "cgroups", "cpuacct.stat_t0") } cgroup.initialize_cpu_usage() TestCpuCgroup.mock_read_file_map = { "/proc/stat": os.path.join(data_dir, "cgroups", "proc_stat_t1"), os.path.join(cgroup.path, "cpuacct.stat"): os.path.join(data_dir, "cgroups", "cpuacct.stat_t1") } cpu_usage = cgroup.get_cpu_usage() self.assertEqual(cpu_usage, round(100.0 * 0.000307697876885 * osutil.get_processor_cores(), 3)) TestCpuCgroup.mock_read_file_map = { "/proc/stat": os.path.join(data_dir, "cgroups", "proc_stat_t2"), os.path.join(cgroup.path, "cpuacct.stat"): os.path.join(data_dir, "cgroups", "cpuacct.stat_t2") } cpu_usage = cgroup.get_cpu_usage() self.assertEqual(cpu_usage, round(100.0 * 0.000445181085968 * osutil.get_processor_cores(), 3)) def test_initialize_cpu_usage_should_set_the_cgroup_usage_to_0_when_the_cgroup_does_not_exist(self): cgroup = CpuCgroup("test", "/sys/fs/cgroup/cpu/system.slice/test") io_error_2 = IOError() io_error_2.errno = errno.ENOENT # "No such directory" TestCpuCgroup.mock_read_file_map = { "/proc/stat": os.path.join(data_dir, "cgroups", "proc_stat_t0"), os.path.join(cgroup.path, "cpuacct.stat"): io_error_2 } cgroup.initialize_cpu_usage() self.assertEqual(cgroup._current_cgroup_cpu, 0) self.assertEqual(cgroup._current_system_cpu, 5496872) # check the system usage just for test sanity def test_initialize_cpu_usage_should_raise_an_exception_when_called_more_than_once(self): cgroup = CpuCgroup("test", "/sys/fs/cgroup/cpu/system.slice/test") TestCpuCgroup.mock_read_file_map = { "/proc/stat": os.path.join(data_dir, "cgroups", "proc_stat_t0"), os.path.join(cgroup.path, "cpuacct.stat"): os.path.join(data_dir, "cgroups", "cpuacct.stat_t0") } cgroup.initialize_cpu_usage() with self.assertRaises(CGroupsException): cgroup.initialize_cpu_usage() def test_get_cpu_usage_should_raise_an_exception_when_initialize_cpu_usage_has_not_been_invoked(self): cgroup = CpuCgroup("test", "/sys/fs/cgroup/cpu/system.slice/test") with self.assertRaises(CGroupsException): cpu_usage = cgroup.get_cpu_usage() # pylint: disable=unused-variable def test_get_throttled_time_should_return_the_value_since_its_last_invocation(self): test_file = os.path.join(self.tmp_dir, "cpu.stat") shutil.copyfile(os.path.join(data_dir, "cgroups", "cpu.stat_t0"), test_file) # throttled_time = 50 cgroup = CpuCgroup("test", self.tmp_dir) cgroup.initialize_cpu_usage() shutil.copyfile(os.path.join(data_dir, "cgroups", "cpu.stat_t1"), test_file) # throttled_time = 2075541442327 throttled_time = cgroup.get_cpu_throttled_time() self.assertEqual(throttled_time, float(2075541442327 - 50) / 1E9, "The value of throttled_time is incorrect") def test_get_tracked_metrics_should_return_the_throttled_time(self): cgroup = CpuCgroup("test", os.path.join(data_dir, "cgroups")) cgroup.initialize_cpu_usage() def find_throttled_time(metrics): return [m for m in metrics if m.counter == MetricsCounter.THROTTLED_TIME] found = find_throttled_time(cgroup.get_tracked_metrics()) self.assertTrue(len(found) == 0, "get_tracked_metrics should not fetch the throttled time by default. Found: {0}".format(found)) found = find_throttled_time(cgroup.get_tracked_metrics(track_throttled_time=True)) self.assertTrue(len(found) == 1, "get_tracked_metrics should have fetched the throttled time by default. Found: {0}".format(found)) class TestMemoryCgroup(AgentTestCase): def test_get_metrics(self): test_mem_cg = MemoryCgroup("test_extension", os.path.join(data_dir, "cgroups", "memory_mount")) memory_usage = test_mem_cg.get_memory_usage() self.assertEqual(150000, memory_usage) max_memory_usage = test_mem_cg.get_max_memory_usage() self.assertEqual(1000000, max_memory_usage) swap_memory_usage = test_mem_cg.try_swap_memory_usage() self.assertEqual(20000, swap_memory_usage) def test_get_metrics_when_files_not_present(self): test_mem_cg = MemoryCgroup("test_extension", os.path.join(data_dir, "cgroups")) with self.assertRaises(IOError) as e: test_mem_cg.get_memory_usage() self.assertEqual(e.exception.errno, errno.ENOENT) with self.assertRaises(IOError) as e: test_mem_cg.get_max_memory_usage() self.assertEqual(e.exception.errno, errno.ENOENT) with self.assertRaises(IOError) as e: test_mem_cg.try_swap_memory_usage() self.assertEqual(e.exception.errno, errno.ENOENT) def test_get_memory_usage_counters_not_found(self): test_mem_cg = MemoryCgroup("test_extension", os.path.join(data_dir, "cgroups", "missing_memory_counters")) with self.assertRaises(CounterNotFound): test_mem_cg.get_memory_usage() swap_memory_usage = test_mem_cg.try_swap_memory_usage() self.assertEqual(0, swap_memory_usage) WALinuxAgent-2.9.1.1/tests/common/test_cgroupstelemetry.py000066400000000000000000000501421446033677600236720ustar00rootroot00000000000000# Copyright 2019 Microsoft Corporation # # 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. # # Requires Python 2.4+ and Openssl 1.0+ # import errno import os import random import time from azurelinuxagent.common.cgroup import CpuCgroup, MemoryCgroup from azurelinuxagent.common.cgroupstelemetry import CGroupsTelemetry from azurelinuxagent.common.utils import fileutil from tests.tools import AgentTestCase, data_dir, patch def raise_ioerror(*_): e = IOError() from errno import EIO e.errno = EIO raise e def median(lst): data = sorted(lst) l_len = len(data) if l_len < 1: return None if l_len % 2 == 0: return (data[int((l_len - 1) / 2)] + data[int((l_len + 1) / 2)]) / 2.0 else: return data[int((l_len - 1) / 2)] def generate_metric_list(lst): return [float(sum(lst)) / float(len(lst)), min(lst), max(lst), median(lst), len(lst)] def consume_cpu_time(): waste = 0 for x in range(1, 200000): # pylint: disable=unused-variable waste += random.random() return waste def consume_memory(): waste = [] for x in range(1, 3): # pylint: disable=unused-variable waste.append([random.random()] * 10000) time.sleep(0.1) waste *= 0 return waste class TestCGroupsTelemetry(AgentTestCase): NumSummarizationValues = 7 @classmethod def setUpClass(cls): AgentTestCase.setUpClass() # CPU Cgroups compute usage based on /proc/stat and /sys/fs/cgroup/.../cpuacct.stat; use mock data for those # files original_read_file = fileutil.read_file def mock_read_file(filepath, **args): if filepath == "/proc/stat": filepath = os.path.join(data_dir, "cgroups", "proc_stat_t0") elif filepath.endswith("/cpuacct.stat"): filepath = os.path.join(data_dir, "cgroups", "cpuacct.stat_t0") return original_read_file(filepath, **args) cls._mock_read_cpu_cgroup_file = patch("azurelinuxagent.common.utils.fileutil.read_file", side_effect=mock_read_file) cls._mock_read_cpu_cgroup_file.start() @classmethod def tearDownClass(cls): cls._mock_read_cpu_cgroup_file.stop() AgentTestCase.tearDownClass() def setUp(self): AgentTestCase.setUp(self) CGroupsTelemetry.reset() def tearDown(self): AgentTestCase.tearDown(self) CGroupsTelemetry.reset() @staticmethod def _track_new_extension_cgroups(num_extensions): for i in range(num_extensions): dummy_cpu_cgroup = CpuCgroup("dummy_extension_{0}".format(i), "dummy_cpu_path_{0}".format(i)) CGroupsTelemetry.track_cgroup(dummy_cpu_cgroup) dummy_memory_cgroup = MemoryCgroup("dummy_extension_{0}".format(i), "dummy_memory_path_{0}".format(i)) CGroupsTelemetry.track_cgroup(dummy_memory_cgroup) def _assert_cgroups_are_tracked(self, num_extensions): for i in range(num_extensions): self.assertTrue(CGroupsTelemetry.is_tracked("dummy_cpu_path_{0}".format(i))) self.assertTrue(CGroupsTelemetry.is_tracked("dummy_memory_path_{0}".format(i))) def _assert_polled_metrics_equal(self, metrics, cpu_metric_value, memory_metric_value, max_memory_metric_value, swap_memory_value): for metric in metrics: self.assertIn(metric.category, ["CPU", "Memory"]) if metric.category == "CPU": self.assertEqual(metric.counter, "% Processor Time") self.assertEqual(metric.value, cpu_metric_value) if metric.category == "Memory": self.assertIn(metric.counter, ["Total Memory Usage", "Max Memory Usage", "Swap Memory Usage"]) if metric.counter == "Total Memory Usage": self.assertEqual(metric.value, memory_metric_value) elif metric.counter == "Max Memory Usage": self.assertEqual(metric.value, max_memory_metric_value) elif metric.counter == "Swap Memory Usage": self.assertEqual(metric.value, swap_memory_value) def test_telemetry_polling_with_active_cgroups(self, *args): # pylint: disable=unused-argument num_extensions = 3 self._track_new_extension_cgroups(num_extensions) with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_max_memory_usage") as patch_get_memory_max_usage: with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_memory_usage") as patch_get_memory_usage: with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_memory_usage") as patch_get_memory_usage: with patch("azurelinuxagent.common.cgroup.MemoryCgroup.try_swap_memory_usage") as patch_try_swap_memory_usage: with patch("azurelinuxagent.common.cgroup.CpuCgroup.get_cpu_usage") as patch_get_cpu_usage: with patch("azurelinuxagent.common.cgroup.CGroup.is_active") as patch_is_active: patch_is_active.return_value = True current_cpu = 30 current_memory = 209715200 current_max_memory = 471859200 current_swap_memory = 20971520 # 1 CPU metric + 1 Current Memory + 1 Max memory + 1 swap memory num_of_metrics_per_extn_expected = 4 patch_get_cpu_usage.return_value = current_cpu patch_get_memory_usage.return_value = current_memory # example 200 MB patch_get_memory_max_usage.return_value = current_max_memory # example 450 MB patch_try_swap_memory_usage.return_value = current_swap_memory # example 20MB num_polls = 12 for data_count in range(1, num_polls + 1): # pylint: disable=unused-variable metrics = CGroupsTelemetry.poll_all_tracked() self.assertEqual(len(metrics), num_extensions * num_of_metrics_per_extn_expected) self._assert_polled_metrics_equal(metrics, current_cpu, current_memory, current_max_memory, current_swap_memory) @patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_max_memory_usage", side_effect=raise_ioerror) @patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_memory_usage", side_effect=raise_ioerror) @patch("azurelinuxagent.common.cgroup.CpuCgroup.get_cpu_usage", side_effect=raise_ioerror) @patch("azurelinuxagent.common.cgroup.CGroup.is_active", return_value=False) def test_telemetry_polling_with_inactive_cgroups(self, *_): num_extensions = 5 no_extensions_expected = 0 # pylint: disable=unused-variable self._track_new_extension_cgroups(num_extensions) self._assert_cgroups_are_tracked(num_extensions) metrics = CGroupsTelemetry.poll_all_tracked() for i in range(num_extensions): self.assertFalse(CGroupsTelemetry.is_tracked("dummy_cpu_path_{0}".format(i))) self.assertFalse(CGroupsTelemetry.is_tracked("dummy_memory_path_{0}".format(i))) self.assertEqual(len(metrics), 0) @patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_max_memory_usage") @patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_memory_usage") @patch("azurelinuxagent.common.cgroup.CpuCgroup.get_cpu_usage") @patch("azurelinuxagent.common.cgroup.CGroup.is_active") def test_telemetry_polling_with_changing_cgroups_state(self, patch_is_active, patch_get_cpu_usage, # pylint: disable=unused-argument patch_get_mem, patch_get_max_mem, *args): num_extensions = 5 self._track_new_extension_cgroups(num_extensions) patch_is_active.return_value = True no_extensions_expected = 0 # pylint: disable=unused-variable expected_data_count = 1 # pylint: disable=unused-variable current_cpu = 30 current_memory = 209715200 current_max_memory = 471859200 patch_get_cpu_usage.return_value = current_cpu patch_get_mem.return_value = current_memory # example 200 MB patch_get_max_mem.return_value = current_max_memory # example 450 MB self._assert_cgroups_are_tracked(num_extensions) CGroupsTelemetry.poll_all_tracked() self._assert_cgroups_are_tracked(num_extensions) patch_is_active.return_value = False patch_get_cpu_usage.side_effect = raise_ioerror patch_get_mem.side_effect = raise_ioerror patch_get_max_mem.side_effect = raise_ioerror CGroupsTelemetry.poll_all_tracked() for i in range(num_extensions): self.assertFalse(CGroupsTelemetry.is_tracked("dummy_cpu_path_{0}".format(i))) self.assertFalse(CGroupsTelemetry.is_tracked("dummy_memory_path_{0}".format(i))) # mocking get_proc_stat to make it run on Mac and other systems. This test does not need to read the values of the # /proc/stat file on the filesystem. @patch("azurelinuxagent.common.logger.periodic_warn") def test_telemetry_polling_to_not_generate_transient_logs_ioerror_file_not_found(self, patch_periodic_warn): num_extensions = 1 self._track_new_extension_cgroups(num_extensions) self.assertEqual(0, patch_periodic_warn.call_count) # Not expecting logs present for io_error with errno=errno.ENOENT io_error_2 = IOError() io_error_2.errno = errno.ENOENT with patch("azurelinuxagent.common.utils.fileutil.read_file", side_effect=io_error_2): poll_count = 1 for data_count in range(poll_count, 10): # pylint: disable=unused-variable CGroupsTelemetry.poll_all_tracked() self.assertEqual(0, patch_periodic_warn.call_count) @patch("azurelinuxagent.common.logger.periodic_warn") def test_telemetry_polling_to_generate_transient_logs_ioerror_permission_denied(self, patch_periodic_warn): num_extensions = 1 num_controllers = 1 is_active_check_per_controller = 2 self._track_new_extension_cgroups(num_extensions) self.assertEqual(0, patch_periodic_warn.call_count) # Expecting logs to be present for different kind of errors io_error_3 = IOError() io_error_3.errno = errno.EPERM with patch("azurelinuxagent.common.utils.fileutil.read_file", side_effect=io_error_3): poll_count = 1 expected_count_per_call = num_controllers + is_active_check_per_controller # get_max_memory_usage memory controller would generate a log statement, and each cgroup would invoke a # is active check raising an exception for data_count in range(poll_count, 10): # pylint: disable=unused-variable CGroupsTelemetry.poll_all_tracked() self.assertEqual(poll_count * expected_count_per_call, patch_periodic_warn.call_count) def test_telemetry_polling_to_generate_transient_logs_index_error(self): num_extensions = 1 self._track_new_extension_cgroups(num_extensions) # Generating a different kind of error (non-IOError) to check the logging. # Trying to invoke IndexError during the getParameter call with patch("azurelinuxagent.common.utils.fileutil.read_file", return_value=''): with patch("azurelinuxagent.common.logger.periodic_warn") as patch_periodic_warn: expected_call_count = 1 # 1 periodic warning for memory for data_count in range(1, 10): # pylint: disable=unused-variable CGroupsTelemetry.poll_all_tracked() self.assertEqual(expected_call_count, patch_periodic_warn.call_count) @patch("azurelinuxagent.common.cgroup.MemoryCgroup.try_swap_memory_usage") @patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_max_memory_usage") @patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_memory_usage") @patch("azurelinuxagent.common.cgroup.CpuCgroup.get_cpu_usage") @patch("azurelinuxagent.common.cgroup.CGroup.is_active") def test_telemetry_calculations(self, patch_is_active, patch_get_cpu_usage, patch_get_memory_usage, patch_get_memory_max_usage, patch_try_memory_swap_usage, *args): # pylint: disable=unused-argument num_polls = 10 num_extensions = 1 cpu_percent_values = [random.randint(0, 100) for _ in range(num_polls)] # only verifying calculations and not validity of the values. memory_usage_values = [random.randint(0, 8 * 1024 ** 3) for _ in range(num_polls)] max_memory_usage_values = [random.randint(0, 8 * 1024 ** 3) for _ in range(num_polls)] swap_usage_values = [random.randint(0, 8 * 1024 ** 3) for _ in range(num_polls)] self._track_new_extension_cgroups(num_extensions) self.assertEqual(2 * num_extensions, len(CGroupsTelemetry._tracked)) for i in range(num_polls): patch_is_active.return_value = True patch_get_cpu_usage.return_value = cpu_percent_values[i] patch_get_memory_usage.return_value = memory_usage_values[i] patch_get_memory_max_usage.return_value = max_memory_usage_values[i] patch_try_memory_swap_usage.return_value = swap_usage_values[i] metrics = CGroupsTelemetry.poll_all_tracked() # 1 CPU metric + 1 Current Memory + 1 Max memory + 1 swap memory self.assertEqual(len(metrics), 4 * num_extensions) self._assert_polled_metrics_equal(metrics, cpu_percent_values[i], memory_usage_values[i], max_memory_usage_values[i], swap_usage_values[i]) def test_cgroup_tracking(self, *args): # pylint: disable=unused-argument num_extensions = 5 num_controllers = 2 self._track_new_extension_cgroups(num_extensions) self._assert_cgroups_are_tracked(num_extensions) self.assertEqual(num_extensions * num_controllers, len(CGroupsTelemetry._tracked)) def test_cgroup_is_tracked(self, *args): # pylint: disable=unused-argument num_extensions = 5 self._track_new_extension_cgroups(num_extensions) self._assert_cgroups_are_tracked(num_extensions) self.assertFalse(CGroupsTelemetry.is_tracked("not_present_cpu_dummy_path")) self.assertFalse(CGroupsTelemetry.is_tracked("not_present_memory_dummy_path")) @patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_memory_usage", side_effect=raise_ioerror) def test_process_cgroup_metric_with_no_memory_cgroup_mounted(self, *args): # pylint: disable=unused-argument num_extensions = 5 self._track_new_extension_cgroups(num_extensions) with patch("azurelinuxagent.common.cgroup.CpuCgroup.get_cpu_usage") as patch_get_cpu_usage: with patch("azurelinuxagent.common.cgroup.CGroup.is_active") as patch_is_active: patch_is_active.return_value = True current_cpu = 30 patch_get_cpu_usage.return_value = current_cpu poll_count = 1 for data_count in range(poll_count, 10): # pylint: disable=unused-variable metrics = CGroupsTelemetry.poll_all_tracked() self.assertEqual(len(metrics), num_extensions * 1) # Only CPU populated self._assert_polled_metrics_equal(metrics, current_cpu, 0, 0, 0) @patch("azurelinuxagent.common.cgroup.CpuCgroup.get_cpu_usage", side_effect=raise_ioerror) def test_process_cgroup_metric_with_no_cpu_cgroup_mounted(self, *args): # pylint: disable=unused-argument num_extensions = 5 self._track_new_extension_cgroups(num_extensions) with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_max_memory_usage") as patch_get_memory_max_usage: with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_memory_usage") as patch_get_memory_usage: with patch("azurelinuxagent.common.cgroup.MemoryCgroup.try_swap_memory_usage") as patch_try_swap_memory_usage: with patch("azurelinuxagent.common.cgroup.CGroup.is_active") as patch_is_active: patch_is_active.return_value = True current_memory = 209715200 current_max_memory = 471859200 current_swap_memory = 20971520 patch_get_memory_usage.return_value = current_memory # example 200 MB patch_get_memory_max_usage.return_value = current_max_memory # example 450 MB patch_try_swap_memory_usage.return_value = current_swap_memory # example 20MB num_polls = 10 for data_count in range(1, num_polls + 1): # pylint: disable=unused-variable metrics = CGroupsTelemetry.poll_all_tracked() # Memory is only populated, CPU is not. Thus 3 metrics for memory. self.assertEqual(len(metrics), num_extensions * 3) self._assert_polled_metrics_equal(metrics, 0, current_memory, current_max_memory, current_swap_memory) @patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_memory_usage", side_effect=raise_ioerror) @patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_max_memory_usage", side_effect=raise_ioerror) @patch("azurelinuxagent.common.cgroup.CpuCgroup.get_cpu_usage", side_effect=raise_ioerror) def test_extension_telemetry_not_sent_for_empty_perf_metrics(self, *args): # pylint: disable=unused-argument num_extensions = 5 self._track_new_extension_cgroups(num_extensions) with patch("azurelinuxagent.common.cgroup.CGroup.is_active") as patch_is_active: patch_is_active.return_value = False poll_count = 1 for data_count in range(poll_count, 10): # pylint: disable=unused-variable metrics = CGroupsTelemetry.poll_all_tracked() self.assertEqual(0, len(metrics)) @patch("azurelinuxagent.common.cgroup.CpuCgroup.get_cpu_usage") @patch("azurelinuxagent.common.cgroup.CpuCgroup.get_cpu_throttled_time") @patch("azurelinuxagent.common.cgroup.CGroup.is_active") def test_cgroup_telemetry_should_not_report_cpu_negative_value(self, patch_is_active, path_get_throttled_time, patch_get_cpu_usage): num_polls = 5 num_extensions = 1 # only verifying calculations and not validity of the values. cpu_percent_values = [random.randint(0, 100) for _ in range(num_polls-1)] cpu_percent_values.append(-1) cpu_throttled_values = [random.randint(0, 60 * 60) for _ in range(num_polls)] dummy_cpu_cgroup = CpuCgroup("dummy_extension_name", "dummy_cpu_path") CGroupsTelemetry.track_cgroup(dummy_cpu_cgroup) self.assertEqual(1, len(CGroupsTelemetry._tracked)) for i in range(num_polls): patch_is_active.return_value = True patch_get_cpu_usage.return_value = cpu_percent_values[i] path_get_throttled_time.return_value = cpu_throttled_values[i] CGroupsTelemetry._track_throttled_time = True metrics = CGroupsTelemetry.poll_all_tracked() # 1 CPU metric + 1 CPU throttled # ignore CPU metrics from telemetry if cpu cgroup reports negative value if i < num_polls-1: self.assertEqual(len(metrics), 2 * num_extensions) else: self.assertEqual(len(metrics), 0) for metric in metrics: self.assertGreaterEqual(metric.value, 0, "telemetry should not report negative value") WALinuxAgent-2.9.1.1/tests/common/test_conf.py000066400000000000000000000135421446033677600212050ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os.path import azurelinuxagent.common.conf as conf from azurelinuxagent.common.utils import fileutil from tests.tools import AgentTestCase, data_dir class TestConf(AgentTestCase): # Note: # -- These values *MUST* match those from data/test_waagent.conf EXPECTED_CONFIGURATION = { "Extensions.Enabled": True, "Provisioning.Agent": "auto", "Provisioning.DeleteRootPassword": True, "Provisioning.RegenerateSshHostKeyPair": True, "Provisioning.SshHostKeyPairType": "rsa", "Provisioning.MonitorHostName": True, "Provisioning.DecodeCustomData": False, "Provisioning.ExecuteCustomData": False, "Provisioning.PasswordCryptId": '6', "Provisioning.PasswordCryptSaltLength": 10, "Provisioning.AllowResetSysUser": False, "ResourceDisk.Format": True, "ResourceDisk.Filesystem": "ext4", "ResourceDisk.MountPoint": "/mnt/resource", "ResourceDisk.EnableSwap": False, "ResourceDisk.EnableSwapEncryption": False, "ResourceDisk.SwapSizeMB": 0, "ResourceDisk.MountOptions": None, "Logs.Verbose": False, "OS.EnableFIPS": True, "OS.RootDeviceScsiTimeout": '300', "OS.OpensslPath": '/usr/bin/openssl', "OS.SshClientAliveInterval": 42, "OS.SshDir": "/notareal/path", "HttpProxy.Host": None, "HttpProxy.Port": None, "DetectScvmmEnv": False, "Lib.Dir": "/var/lib/waagent", "DVD.MountPoint": "/mnt/cdrom/secure", "Pid.File": "/var/run/waagent.pid", "Extension.LogDir": "/var/log/azure", "OS.HomeDir": "/home", "OS.EnableRDMA": False, "OS.UpdateRdmaDriver": False, "OS.CheckRdmaDriver": False, "AutoUpdate.Enabled": True, "AutoUpdate.GAFamily": "Prod", "EnableOverProvisioning": True, "OS.AllowHTTP": False, "OS.EnableFirewall": False } def setUp(self): AgentTestCase.setUp(self) self.conf = conf.ConfigurationProvider() conf.load_conf_from_file( os.path.join(data_dir, "test_waagent.conf"), self.conf) def test_get_should_return_default_when_key_is_not_found(self): self.assertEqual("The Default Value", self.conf.get("this-key-does-not-exist", "The Default Value")) self.assertEqual("The Default Value", self.conf.get("this-key-does-not-exist", lambda: "The Default Value")) def test_get_switch_should_return_default_when_key_is_not_found(self): self.assertEqual(True, self.conf.get_switch("this-key-does-not-exist", True)) self.assertEqual(True, self.conf.get_switch("this-key-does-not-exist", lambda: True)) def test_get_int_should_return_default_when_key_is_not_found(self): self.assertEqual(123456789, self.conf.get_int("this-key-does-not-exist", 123456789)) self.assertEqual(123456789, self.conf.get_int("this-key-does-not-exist", lambda: 123456789)) def test_key_value_handling(self): self.assertEqual("Value1", self.conf.get("FauxKey1", "Bad")) self.assertEqual("Value2 Value2", self.conf.get("FauxKey2", "Bad")) self.assertEqual("delalloc,rw,noatime,nobarrier,users,mode=777", self.conf.get("FauxKey3", "Bad")) def test_get_ssh_dir(self): self.assertTrue(conf.get_ssh_dir(self.conf).startswith("/notareal/path")) def test_get_sshd_conf_file_path(self): self.assertTrue(conf.get_sshd_conf_file_path( self.conf).startswith("/notareal/path")) def test_get_ssh_key_glob(self): self.assertTrue(conf.get_ssh_key_glob( self.conf).startswith("/notareal/path")) def test_get_ssh_key_private_path(self): self.assertTrue(conf.get_ssh_key_private_path( self.conf).startswith("/notareal/path")) def test_get_ssh_key_public_path(self): self.assertTrue(conf.get_ssh_key_public_path( self.conf).startswith("/notareal/path")) def test_get_fips_enabled(self): self.assertTrue(conf.get_fips_enabled(self.conf)) def test_get_provision_agent(self): self.assertTrue(conf.get_provisioning_agent(self.conf) == 'auto') def test_get_configuration(self): configuration = conf.get_configuration(self.conf) self.assertTrue(len(configuration.keys()) > 0) for k in TestConf.EXPECTED_CONFIGURATION.keys(): self.assertEqual( TestConf.EXPECTED_CONFIGURATION[k], configuration[k], k) def test_get_agent_disabled_file_path(self): self.assertEqual(conf.get_disable_agent_file_path(self.conf), os.path.join(self.tmp_dir, conf.DISABLE_AGENT_FILE)) def test_write_agent_disabled(self): """ Test writing disable_agent is empty """ from azurelinuxagent.pa.provision.default import ProvisionHandler disable_file_path = conf.get_disable_agent_file_path(self.conf) self.assertFalse(os.path.exists(disable_file_path)) ProvisionHandler.write_agent_disabled() self.assertTrue(os.path.exists(disable_file_path)) self.assertEqual('', fileutil.read_file(disable_file_path)) def test_get_extensions_enabled(self): self.assertTrue(conf.get_extensions_enabled(self.conf)) WALinuxAgent-2.9.1.1/tests/common/test_errorstate.py000066400000000000000000000074011446033677600224470ustar00rootroot00000000000000import unittest from datetime import timedelta, datetime from azurelinuxagent.common.errorstate import ErrorState from tests.tools import Mock, patch class TestErrorState(unittest.TestCase): def test_errorstate00(self): """ If ErrorState is never incremented, it will never trigger. """ test_subject = ErrorState(timedelta(seconds=10000)) self.assertFalse(test_subject.is_triggered()) self.assertEqual(0, test_subject.count) self.assertEqual('unknown', test_subject.fail_time) def test_errorstate01(self): """ If ErrorState is never incremented, and the timedelta is zero it will not trigger. """ test_subject = ErrorState(timedelta(seconds=0)) self.assertFalse(test_subject.is_triggered()) self.assertEqual(0, test_subject.count) self.assertEqual('unknown', test_subject.fail_time) def test_errorstate02(self): """ If ErrorState is triggered, and the current time is within timedelta of now it will trigger. """ test_subject = ErrorState(timedelta(seconds=0)) test_subject.incr() self.assertTrue(test_subject.is_triggered()) self.assertEqual(1, test_subject.count) self.assertEqual('0.0 min', test_subject.fail_time) @patch('azurelinuxagent.common.errorstate.datetime') def test_errorstate03(self, mock_time): """ ErrorState will not trigger until 1. ErrorState has been incr() at least once. 2. The timedelta from the first incr() has elapsed. """ test_subject = ErrorState(timedelta(minutes=15)) for x in range(1, 10): mock_time.utcnow = Mock(return_value=datetime.utcnow() + timedelta(minutes=x)) test_subject.incr() self.assertFalse(test_subject.is_triggered()) mock_time.utcnow = Mock(return_value=datetime.utcnow() + timedelta(minutes=30)) test_subject.incr() self.assertTrue(test_subject.is_triggered()) self.assertEqual('29.0 min', test_subject.fail_time) def test_errorstate04(self): """ If ErrorState is reset the timestamp of the last incr() is reset to None. """ test_subject = ErrorState(timedelta(minutes=15)) self.assertTrue(test_subject.timestamp is None) test_subject.incr() self.assertTrue(test_subject.timestamp is not None) test_subject.reset() self.assertTrue(test_subject.timestamp is None) def test_errorstate05(self): """ Test the fail_time for various scenarios """ test_subject = ErrorState(timedelta(minutes=15)) self.assertEqual('unknown', test_subject.fail_time) test_subject.incr() self.assertEqual('0.0 min', test_subject.fail_time) test_subject.timestamp = datetime.utcnow() - timedelta(seconds=60) self.assertEqual('1.0 min', test_subject.fail_time) test_subject.timestamp = datetime.utcnow() - timedelta(seconds=73) self.assertEqual('1.22 min', test_subject.fail_time) test_subject.timestamp = datetime.utcnow() - timedelta(seconds=120) self.assertEqual('2.0 min', test_subject.fail_time) test_subject.timestamp = datetime.utcnow() - timedelta(seconds=60 * 59) self.assertEqual('59.0 min', test_subject.fail_time) test_subject.timestamp = datetime.utcnow() - timedelta(seconds=60 * 60) self.assertEqual('1.0 hr', test_subject.fail_time) test_subject.timestamp = datetime.utcnow() - timedelta(seconds=60 * 95) self.assertEqual('1.58 hr', test_subject.fail_time) test_subject.timestamp = datetime.utcnow() - timedelta(seconds=60 * 60 * 3) self.assertEqual('3.0 hr', test_subject.fail_time) WALinuxAgent-2.9.1.1/tests/common/test_event.py000066400000000000000000001414361446033677600214050ustar00rootroot00000000000000# coding=utf-8 # # Copyright 2017 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # from __future__ import print_function import json import os import re import shutil import threading import xml.dom from datetime import datetime, timedelta from mock import MagicMock from azurelinuxagent.common.utils import textutil, fileutil from azurelinuxagent.common import event, logger from azurelinuxagent.common.AgentGlobals import AgentGlobals from azurelinuxagent.common.event import add_event, add_periodic, add_log_event, elapsed_milliseconds, \ WALAEventOperation, parse_xml_event, parse_json_event, AGENT_EVENT_FILE_EXTENSION, EVENTS_DIRECTORY, \ TELEMETRY_EVENT_EVENT_ID, TELEMETRY_EVENT_PROVIDER_ID, TELEMETRY_LOG_EVENT_ID, TELEMETRY_LOG_PROVIDER_ID, \ report_metric from azurelinuxagent.common.future import ustr from azurelinuxagent.common.osutil import get_osutil from azurelinuxagent.common.telemetryevent import CommonTelemetryEventSchema, GuestAgentGenericLogsSchema, \ GuestAgentExtensionEventsSchema, GuestAgentPerfCounterEventsSchema from azurelinuxagent.common.version import CURRENT_AGENT, CURRENT_VERSION, AGENT_EXECUTION_MODE from azurelinuxagent.ga.collect_telemetry_events import _CollectAndEnqueueEvents from tests.protocol import mockwiredata from tests.protocol.mocks import mock_wire_protocol, MockHttpResponse from tests.protocol.HttpRequestPredicates import HttpRequestPredicates from tests.tools import AgentTestCase, data_dir, load_data, patch, skip_if_predicate_true, is_python_version_26_or_34 from tests.utils.event_logger_tools import EventLoggerTools class TestEvent(HttpRequestPredicates, AgentTestCase): # These are the Operation/Category for events produced by the tests below (as opposed by events produced by the agent itself) _Message = "ThisIsATestEventMessage" _Operation = "ThisIsATestEventOperation" _Category = "ThisIsATestMetricCategory" def setUp(self): AgentTestCase.setUp(self) self.event_dir = os.path.join(self.tmp_dir, EVENTS_DIRECTORY) EventLoggerTools.initialize_event_logger(self.event_dir) threading.current_thread().setName("TestEventThread") osutil = get_osutil() self.expected_common_parameters = { # common parameters computed at event creation; the timestamp (stored as the opcode name) is not included # here and is checked separately from these parameters CommonTelemetryEventSchema.GAVersion: CURRENT_AGENT, CommonTelemetryEventSchema.ContainerId: AgentGlobals.get_container_id(), CommonTelemetryEventSchema.EventTid: threading.current_thread().ident, CommonTelemetryEventSchema.EventPid: os.getpid(), CommonTelemetryEventSchema.TaskName: threading.current_thread().getName(), CommonTelemetryEventSchema.KeywordName: '', # common parameters computed from the OS platform CommonTelemetryEventSchema.OSVersion: EventLoggerTools.get_expected_os_version(), CommonTelemetryEventSchema.ExecutionMode: AGENT_EXECUTION_MODE, CommonTelemetryEventSchema.RAM: int(osutil.get_total_mem()), CommonTelemetryEventSchema.Processors: osutil.get_processor_cores(), # common parameters from the goal state CommonTelemetryEventSchema.TenantName: 'db00a7755a5e4e8a8fe4b19bc3b330c3', CommonTelemetryEventSchema.RoleName: 'MachineRole', CommonTelemetryEventSchema.RoleInstanceName: 'b61f93d0-e1ed-40b2-b067-22c243233448.MachineRole_IN_0', # common parameters CommonTelemetryEventSchema.Location: EventLoggerTools.mock_imds_data['location'], CommonTelemetryEventSchema.SubscriptionId: EventLoggerTools.mock_imds_data['subscriptionId'], CommonTelemetryEventSchema.ResourceGroupName: EventLoggerTools.mock_imds_data['resourceGroupName'], CommonTelemetryEventSchema.VMId: EventLoggerTools.mock_imds_data['vmId'], CommonTelemetryEventSchema.ImageOrigin: EventLoggerTools.mock_imds_data['image_origin'], } self.expected_extension_events_params = { GuestAgentExtensionEventsSchema.IsInternal: False, GuestAgentExtensionEventsSchema.ExtensionType: "" } @staticmethod def _report_events(protocol, event_list): def _yield_events(): for telemetry_event in event_list: yield telemetry_event protocol.client.report_event(_yield_events()) @staticmethod def _collect_events(): def append_event(e): for p in e.parameters: if p.name == 'Operation' and p.value == TestEvent._Operation \ or p.name == 'Category' and p.value == TestEvent._Category \ or p.name == 'Message' and p.value == TestEvent._Message \ or p.name == 'Context1' and p.value == TestEvent._Message: event_list.append(e) event_list = [] send_telemetry_events = MagicMock() send_telemetry_events.enqueue_event = MagicMock(wraps=append_event) event_collector = _CollectAndEnqueueEvents(send_telemetry_events) event_collector.process_events() return event_list def _collect_event_files(self): files = [os.path.join(self.event_dir, f) for f in os.listdir(self.event_dir)] return [f for f in files if fileutil.findre_in_file(f, TestEvent._Operation)] @staticmethod def _is_guest_extension_event(event): # pylint: disable=redefined-outer-name return event.eventId == TELEMETRY_EVENT_EVENT_ID and event.providerId == TELEMETRY_EVENT_PROVIDER_ID @staticmethod def _is_telemetry_log_event(event): # pylint: disable=redefined-outer-name return event.eventId == TELEMETRY_LOG_EVENT_ID and event.providerId == TELEMETRY_LOG_PROVIDER_ID def test_parse_xml_event(self, *args): # pylint: disable=unused-argument data_str = load_data('ext/event_from_extension.xml') event = parse_xml_event(data_str) # pylint: disable=redefined-outer-name self.assertIsNotNone(event) self.assertNotEqual(0, event.parameters) self.assertTrue(all(param is not None for param in event.parameters)) def test_parse_json_event(self, *args): # pylint: disable=unused-argument data_str = load_data('ext/event.json') event = parse_json_event(data_str) # pylint: disable=redefined-outer-name self.assertIsNotNone(event) self.assertNotEqual(0, event.parameters) self.assertTrue(all(param is not None for param in event.parameters)) def test_add_event_should_use_the_container_id_from_the_most_recent_goal_state(self): def create_event_and_return_container_id(): # pylint: disable=inconsistent-return-statements event.add_event(name='Event', op=TestEvent._Operation) event_list = self._collect_events() self.assertEqual(len(event_list), 1, "Could not find the event created by add_event") for p in event_list[0].parameters: if p.name == CommonTelemetryEventSchema.ContainerId: return p.value self.fail("Could not find Contained ID on event") with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: contained_id = create_event_and_return_container_id() # The expect value comes from DATA_FILE self.assertEqual(contained_id, 'c6d5526c-5ac2-4200-b6e2-56f2b70c5ab2', "Incorrect container ID") protocol.mock_wire_data.set_container_id('AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE') protocol.client.update_goal_state() contained_id = create_event_and_return_container_id() self.assertEqual(contained_id, 'AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE', "Incorrect container ID") protocol.mock_wire_data.set_container_id('11111111-2222-3333-4444-555555555555') protocol.client.update_goal_state() contained_id = create_event_and_return_container_id() self.assertEqual(contained_id, '11111111-2222-3333-4444-555555555555', "Incorrect container ID") def test_add_event_should_handle_event_errors(self): with patch("azurelinuxagent.common.utils.fileutil.mkdir", side_effect=OSError): with patch('azurelinuxagent.common.logger.periodic_error') as mock_logger_periodic_error: add_event('test', message='test event', op=TestEvent._Operation) # The event shouldn't have been created self.assertTrue(len(self._collect_event_files()) == 0) # The exception should have been caught and logged args = mock_logger_periodic_error.call_args exception_message = args[0][1] self.assertIn("[EventError] Failed to create events folder", exception_message) def test_event_status_event_marked(self): es = event.__event_status__ self.assertFalse(es.event_marked("Foo", "1.2", "FauxOperation")) es.mark_event_status("Foo", "1.2", "FauxOperation", True) self.assertTrue(es.event_marked("Foo", "1.2", "FauxOperation")) event.__event_status__ = event.EventStatus() event.init_event_status(self.tmp_dir) es = event.__event_status__ self.assertTrue(es.event_marked("Foo", "1.2", "FauxOperation")) def test_event_status_defaults_to_success(self): es = event.__event_status__ self.assertTrue(es.event_succeeded("Foo", "1.2", "FauxOperation")) def test_event_status_records_status(self): es = event.EventStatus() es.mark_event_status("Foo", "1.2", "FauxOperation", True) self.assertTrue(es.event_succeeded("Foo", "1.2", "FauxOperation")) es.mark_event_status("Foo", "1.2", "FauxOperation", False) self.assertFalse(es.event_succeeded("Foo", "1.2", "FauxOperation")) def test_event_status_preserves_state(self): es = event.__event_status__ es.mark_event_status("Foo", "1.2", "FauxOperation", False) self.assertFalse(es.event_succeeded("Foo", "1.2", "FauxOperation")) event.__event_status__ = event.EventStatus() event.init_event_status(self.tmp_dir) es = event.__event_status__ self.assertFalse(es.event_succeeded("Foo", "1.2", "FauxOperation")) def test_should_emit_event_ignores_unknown_operations(self): event.__event_status__ = event.EventStatus() self.assertTrue(event.should_emit_event("Foo", "1.2", "FauxOperation", True)) self.assertTrue(event.should_emit_event("Foo", "1.2", "FauxOperation", False)) # Marking the event has no effect event.mark_event_status("Foo", "1.2", "FauxOperation", True) self.assertTrue(event.should_emit_event("Foo", "1.2", "FauxOperation", True)) self.assertTrue(event.should_emit_event("Foo", "1.2", "FauxOperation", False)) def test_should_emit_event_handles_known_operations(self): event.__event_status__ = event.EventStatus() # Known operations always initially "fire" for op in event.__event_status_operations__: self.assertTrue(event.should_emit_event("Foo", "1.2", op, True)) self.assertTrue(event.should_emit_event("Foo", "1.2", op, False)) # Note a success event... for op in event.__event_status_operations__: event.mark_event_status("Foo", "1.2", op, True) # Subsequent success events should not fire, but failures will for op in event.__event_status_operations__: self.assertFalse(event.should_emit_event("Foo", "1.2", op, True)) self.assertTrue(event.should_emit_event("Foo", "1.2", op, False)) # Note a failure event... for op in event.__event_status_operations__: event.mark_event_status("Foo", "1.2", op, False) # Subsequent success events fire and failure do not for op in event.__event_status_operations__: self.assertTrue(event.should_emit_event("Foo", "1.2", op, True)) self.assertFalse(event.should_emit_event("Foo", "1.2", op, False)) @patch('azurelinuxagent.common.event.EventLogger') @patch('azurelinuxagent.common.logger.error') @patch('azurelinuxagent.common.logger.warn') @patch('azurelinuxagent.common.logger.info') def test_should_log_errors_if_failed_operation_and_empty_event_dir(self, mock_logger_info, mock_logger_warn, mock_logger_error, mock_reporter): mock_reporter.event_dir = None add_event("dummy name", version=CURRENT_VERSION, op=WALAEventOperation.Download, is_success=False, message="dummy event message", reporter=mock_reporter) self.assertEqual(1, mock_logger_error.call_count) self.assertEqual(1, mock_logger_warn.call_count) self.assertEqual(0, mock_logger_info.call_count) args = mock_logger_error.call_args[0] self.assertEqual(('dummy name', 'Download', 'dummy event message', 0), args[1:]) @patch('azurelinuxagent.common.event.EventLogger') @patch('azurelinuxagent.common.logger.error') @patch('azurelinuxagent.common.logger.warn') @patch('azurelinuxagent.common.logger.info') def test_should_log_errors_if_failed_operation_and_not_empty_event_dir(self, mock_logger_info, mock_logger_warn, mock_logger_error, mock_reporter): mock_reporter.event_dir = "dummy" with patch("azurelinuxagent.common.event.should_emit_event", return_value=True) as mock_should_emit_event: with patch("azurelinuxagent.common.event.mark_event_status"): with patch("azurelinuxagent.common.event.EventLogger._add_event"): add_event("dummy name", version=CURRENT_VERSION, op=WALAEventOperation.Download, is_success=False, message="dummy event message") self.assertEqual(1, mock_should_emit_event.call_count) self.assertEqual(1, mock_logger_error.call_count) self.assertEqual(0, mock_logger_warn.call_count) self.assertEqual(0, mock_logger_info.call_count) args = mock_logger_error.call_args[0] self.assertEqual(('dummy name', 'Download', 'dummy event message', 0), args[1:]) @patch('azurelinuxagent.common.event.EventLogger.add_event') def test_periodic_emits_if_not_previously_sent(self, mock_event): event.__event_logger__.reset_periodic() event.add_periodic(logger.EVERY_DAY, "FauxEvent") self.assertEqual(1, mock_event.call_count) @patch('azurelinuxagent.common.event.EventLogger.add_event') def test_periodic_does_not_emit_if_previously_sent(self, mock_event): event.__event_logger__.reset_periodic() event.add_periodic(logger.EVERY_DAY, "FauxEvent") self.assertEqual(1, mock_event.call_count) event.add_periodic(logger.EVERY_DAY, "FauxEvent") self.assertEqual(1, mock_event.call_count) @patch('azurelinuxagent.common.event.EventLogger.add_event') def test_periodic_emits_if_forced(self, mock_event): event.__event_logger__.reset_periodic() event.add_periodic(logger.EVERY_DAY, "FauxEvent") self.assertEqual(1, mock_event.call_count) event.add_periodic(logger.EVERY_DAY, "FauxEvent", force=True) self.assertEqual(2, mock_event.call_count) @patch('azurelinuxagent.common.event.EventLogger.add_event') def test_periodic_emits_after_elapsed_delta(self, mock_event): event.__event_logger__.reset_periodic() event.add_periodic(logger.EVERY_DAY, "FauxEvent") self.assertEqual(1, mock_event.call_count) event.add_periodic(logger.EVERY_DAY, "FauxEvent") self.assertEqual(1, mock_event.call_count) h = hash("FauxEvent"+WALAEventOperation.Unknown+ustr(True)) event.__event_logger__.periodic_events[h] = \ datetime.now() - logger.EVERY_DAY - logger.EVERY_HOUR event.add_periodic(logger.EVERY_DAY, "FauxEvent") self.assertEqual(2, mock_event.call_count) @patch('azurelinuxagent.common.event.EventLogger.add_event') def test_periodic_forwards_args(self, mock_event): event.__event_logger__.reset_periodic() event.add_periodic(logger.EVERY_DAY, "FauxEvent", op=WALAEventOperation.Log, is_success=True, duration=0, version=str(CURRENT_VERSION), message="FauxEventMessage", log_event=True, force=False) mock_event.assert_called_once_with("FauxEvent", op=WALAEventOperation.Log, is_success=True, duration=0, version=str(CURRENT_VERSION), message="FauxEventMessage", log_event=True) @patch("azurelinuxagent.common.event.datetime") @patch('azurelinuxagent.common.event.EventLogger.add_event') def test_periodic_forwards_args_default_values(self, mock_event, mock_datetime): # pylint: disable=unused-argument event.__event_logger__.reset_periodic() event.add_periodic(logger.EVERY_DAY, "FauxEvent", message="FauxEventMessage") mock_event.assert_called_once_with("FauxEvent", op=WALAEventOperation.Unknown, is_success=True, duration=0, version=str(CURRENT_VERSION), message="FauxEventMessage", log_event=True) @patch("azurelinuxagent.common.event.EventLogger.add_event") def test_add_event_default_variables(self, mock_add_event): add_event('test', message='test event') mock_add_event.assert_called_once_with('test', duration=0, is_success=True, log_event=True, message='test event', op=WALAEventOperation.Unknown, version=str(CURRENT_VERSION)) def test_collect_events_should_delete_event_files(self): add_event(name='Event1', op=TestEvent._Operation) add_event(name='Event1', op=TestEvent._Operation) add_event(name='Event3', op=TestEvent._Operation) event_files = self._collect_event_files() self.assertEqual(3, len(event_files), "Did not find all the event files that were created") event_list = self._collect_events() event_files = os.listdir(self.event_dir) self.assertEqual(len(event_list), 3, "Did not collect all the events that were created") self.assertEqual(len(event_files), 0, "The event files were not deleted") def test_save_event(self): add_event('test', message='test event', op=TestEvent._Operation) self.assertTrue(len(self._collect_event_files()) == 1) # checking the extension of the file created. for filename in os.listdir(self.event_dir): self.assertTrue(filename.endswith(AGENT_EVENT_FILE_EXTENSION), 'Event file does not have the correct extension ({0}): {1}'.format(AGENT_EVENT_FILE_EXTENSION, filename)) @staticmethod def _get_event_message(evt): for p in evt.parameters: if p.name == GuestAgentExtensionEventsSchema.Message: return p.value return None def test_collect_events_should_be_able_to_process_events_with_non_ascii_characters(self): self._create_test_event_file("custom_script_nonascii_characters.tld") event_list = self._collect_events() self.assertEqual(len(event_list), 1) self.assertEqual(TestEvent._get_event_message(event_list[0]), u'World\u05e2\u05d9\u05d5\u05ea \u05d0\u05d7\u05e8\u05d5\u05ea\u0906\u091c') @skip_if_predicate_true(is_python_version_26_or_34, "Disabled on Python 2.6 and 3.4 for now. Need to revisit to fix it") def test_collect_events_should_ignore_invalid_event_files(self): self._create_test_event_file("custom_script_1.tld") # a valid event self._create_test_event_file("custom_script_utf-16.tld") self._create_test_event_file("custom_script_invalid_json.tld") os.chmod(self._create_test_event_file("custom_script_no_read_access.tld"), 0o200) self._create_test_event_file("custom_script_2.tld") # another valid event with patch("azurelinuxagent.common.event.add_event") as mock_add_event: event_list = self._collect_events() self.assertEqual( len(event_list), 2) self.assertTrue( all(TestEvent._get_event_message(evt) == "A test telemetry message." for evt in event_list), "The valid events were not found") invalid_events = [] total_dropped_count = 0 for args, kwargs in mock_add_event.call_args_list: # pylint: disable=unused-variable match = re.search(r"DroppedEventsCount: (\d+)", kwargs['message']) if match is not None: invalid_events.append(kwargs['op']) total_dropped_count += int(match.groups()[0]) self.assertEqual(3, total_dropped_count, "Total dropped events dont match") self.assertIn(WALAEventOperation.CollectEventErrors, invalid_events, "{0} errors not reported".format(WALAEventOperation.CollectEventErrors)) self.assertIn(WALAEventOperation.CollectEventUnicodeErrors, invalid_events, "{0} errors not reported".format(WALAEventOperation.CollectEventUnicodeErrors)) def test_save_event_rollover(self): # We keep 1000 events only, and the older ones are removed. num_of_events = 999 add_event('test', message='first event') # this makes number of events to num_of_events + 1. for i in range(num_of_events): add_event('test', message='test event {0}'.format(i)) num_of_events += 1 # adding the first add_event. events = os.listdir(self.event_dir) events.sort() self.assertTrue(len(events) == num_of_events, "{0} is not equal to {1}".format(len(events), num_of_events)) first_event = os.path.join(self.event_dir, events[0]) with open(first_event) as first_fh: first_event_text = first_fh.read() self.assertTrue('first event' in first_event_text) add_event('test', message='last event') # Adding the above event displaces the first_event events = os.listdir(self.event_dir) events.sort() self.assertTrue(len(events) == num_of_events, "{0} events found, {1} expected".format(len(events), num_of_events)) first_event = os.path.join(self.event_dir, events[0]) with open(first_event) as first_fh: first_event_text = first_fh.read() self.assertFalse('first event' in first_event_text, "'first event' not in {0}".format(first_event_text)) self.assertTrue('test event 0' in first_event_text) last_event = os.path.join(self.event_dir, events[-1]) with open(last_event) as last_fh: last_event_text = last_fh.read() self.assertTrue('last event' in last_event_text) def test_save_event_cleanup(self): for i in range(0, 2000): evt = os.path.join(self.event_dir, '{0}.tld'.format(ustr(1491004920536531 + i))) with open(evt, 'w') as fh: fh.write('{0}{1}'.format(TestEvent._Operation, i)) test_events = self._collect_event_files() self.assertTrue(len(test_events) == 2000, "{0} events found, 2000 expected".format(len(test_events))) add_event('test', message='last event', op=TestEvent._Operation) events = os.listdir(self.event_dir) self.assertTrue(len(events) == 1000, "{0} events found, 1000 expected".format(len(events))) def test_elapsed_milliseconds(self): utc_start = datetime.utcnow() + timedelta(days=1) self.assertEqual(0, elapsed_milliseconds(utc_start)) def _assert_event_includes_all_parameters_in_the_telemetry_schema(self, actual_event, expected_parameters, assert_timestamp): # add the common parameters to the set of expected parameters all_expected_parameters = self.expected_common_parameters.copy() if self._is_guest_extension_event(actual_event): all_expected_parameters.update(self.expected_extension_events_params.copy()) all_expected_parameters.update(expected_parameters) # convert the event parameters to a dictionary; do not include the timestamp, # which is verified using assert_timestamp() event_parameters = {} timestamp = None for p in actual_event.parameters: if p.name == CommonTelemetryEventSchema.OpcodeName: # the timestamp is stored in the opcode name timestamp = p.value else: event_parameters[p.name] = p.value if self._is_telemetry_log_event(actual_event): # Remove Context2 from event parameters and verify that the timestamp is correct telemetry_log_event_timestamp = event_parameters.pop(GuestAgentGenericLogsSchema.Context2, None) self.assertIsNotNone(telemetry_log_event_timestamp, "Context2 should be filled with a timestamp") assert_timestamp(telemetry_log_event_timestamp) self.maxDiff = None # the dictionary diffs can be quite large; display the whole thing self.assertDictEqual(event_parameters, all_expected_parameters) self.assertIsNotNone(timestamp, "The event does not have a timestamp (Opcode)") assert_timestamp(timestamp) @staticmethod def _datetime_to_event_timestamp(dt): return dt.strftime(logger.Logger.LogTimeFormatInUTC) def _test_create_event_function_should_create_events_that_have_all_the_parameters_in_the_telemetry_schema(self, create_event_function, expected_parameters): """ Helper to tests methods that create events (e.g. add_event, add_log_event, etc). """ # execute the method that creates the event, capturing the time range of the execution timestamp_lower = TestEvent._datetime_to_event_timestamp(datetime.utcnow()) create_event_function() timestamp_upper = TestEvent._datetime_to_event_timestamp(datetime.utcnow()) event_list = self._collect_events() self.assertEqual(len(event_list), 1) # verify the event parameters self._assert_event_includes_all_parameters_in_the_telemetry_schema( event_list[0], expected_parameters, assert_timestamp=lambda timestamp: self.assertTrue(timestamp_lower <= timestamp <= timestamp_upper, "The event timestamp (opcode) is incorrect") ) def test_add_event_should_create_events_that_have_all_the_parameters_in_the_telemetry_schema(self): self._test_create_event_function_should_create_events_that_have_all_the_parameters_in_the_telemetry_schema( create_event_function=lambda: add_event( name="TestEvent", op=TestEvent._Operation, is_success=True, duration=1234, version="1.2.3.4", message="Test Message"), expected_parameters={ GuestAgentExtensionEventsSchema.Name: 'TestEvent', GuestAgentExtensionEventsSchema.Version: '1.2.3.4', GuestAgentExtensionEventsSchema.Operation: TestEvent._Operation, GuestAgentExtensionEventsSchema.OperationSuccess: True, GuestAgentExtensionEventsSchema.Message: 'Test Message', GuestAgentExtensionEventsSchema.Duration: 1234, GuestAgentExtensionEventsSchema.ExtensionType: ''}) def test_add_periodic_should_create_events_that_have_all_the_parameters_in_the_telemetry_schema(self): self._test_create_event_function_should_create_events_that_have_all_the_parameters_in_the_telemetry_schema( create_event_function=lambda: add_periodic( delta=logger.EVERY_MINUTE, name="TestPeriodicEvent", op=TestEvent._Operation, is_success=False, duration=4321, version="4.3.2.1", message="Test Periodic Message"), expected_parameters={ GuestAgentExtensionEventsSchema.Name: 'TestPeriodicEvent', GuestAgentExtensionEventsSchema.Version: '4.3.2.1', GuestAgentExtensionEventsSchema.Operation: TestEvent._Operation, GuestAgentExtensionEventsSchema.OperationSuccess: False, GuestAgentExtensionEventsSchema.Message: 'Test Periodic Message', GuestAgentExtensionEventsSchema.Duration: 4321, GuestAgentExtensionEventsSchema.ExtensionType: ''}) @skip_if_predicate_true(lambda: True, "Enable this test when SEND_LOGS_TO_TELEMETRY is enabled") def test_add_log_event_should_create_events_that_have_all_the_parameters_in_the_telemetry_schema(self): self._test_create_event_function_should_create_events_that_have_all_the_parameters_in_the_telemetry_schema( create_event_function=lambda: add_log_event(logger.LogLevel.INFO, 'A test INFO log event'), expected_parameters={ GuestAgentGenericLogsSchema.EventName: 'Log', GuestAgentGenericLogsSchema.CapabilityUsed: 'INFO', GuestAgentGenericLogsSchema.Context1: 'log event', GuestAgentGenericLogsSchema.Context3: '' }) def test_add_log_event_should_always_create_events_when_forced(self): self._test_create_event_function_should_create_events_that_have_all_the_parameters_in_the_telemetry_schema( create_event_function=lambda: add_log_event(logger.LogLevel.WARNING, TestEvent._Message, forced=True), expected_parameters={ GuestAgentGenericLogsSchema.EventName: 'Log', GuestAgentGenericLogsSchema.CapabilityUsed: 'WARNING', GuestAgentGenericLogsSchema.Context1: TestEvent._Message, GuestAgentGenericLogsSchema.Context3: '' }) def test_add_log_event_should_not_create_event_if_not_allowed_and_not_forced(self): add_log_event(logger.LogLevel.WARNING, 'A test WARNING log event') event_list = self._collect_events() self.assertEqual(len(event_list), 0, "No events should be created if not forced and not allowed") def test_report_metric_should_create_events_that_have_all_the_parameters_in_the_telemetry_schema(self): self._test_create_event_function_should_create_events_that_have_all_the_parameters_in_the_telemetry_schema( create_event_function=lambda: report_metric(TestEvent._Category, "%idle", "total", 12.34), expected_parameters={ GuestAgentPerfCounterEventsSchema.Category: TestEvent._Category, GuestAgentPerfCounterEventsSchema.Counter: '%idle', GuestAgentPerfCounterEventsSchema.Instance: 'total', GuestAgentPerfCounterEventsSchema.Value: 12.34 }) def _create_test_event_file(self, source_file): source_file_path = os.path.join(data_dir, "events", source_file) target_file_path = os.path.join(self.event_dir, source_file) shutil.copy(source_file_path, target_file_path) return target_file_path @staticmethod def _get_file_creation_timestamp(file): # pylint: disable=redefined-builtin return TestEvent._datetime_to_event_timestamp(datetime.fromtimestamp(os.path.getmtime(file))) def test_collect_events_should_add_all_the_parameters_in_the_telemetry_schema_to_legacy_agent_events(self): # Agents <= 2.2.46 use *.tld as the extension for event files (newer agents use "*.waagent.tld") and they populate # only a subset of fields; the rest are added by the current agent when events are collected. self._create_test_event_file("legacy_agent.tld") event_list = self._collect_events() self.assertEqual(len(event_list), 1) self._assert_event_includes_all_parameters_in_the_telemetry_schema( event_list[0], expected_parameters={ GuestAgentExtensionEventsSchema.Name: "WALinuxAgent", GuestAgentExtensionEventsSchema.Version: "9.9.9", GuestAgentExtensionEventsSchema.IsInternal: False, GuestAgentExtensionEventsSchema.Operation: TestEvent._Operation, GuestAgentExtensionEventsSchema.OperationSuccess: True, GuestAgentExtensionEventsSchema.Message: "The cgroup filesystem is ready to use", GuestAgentExtensionEventsSchema.Duration: 1234, GuestAgentExtensionEventsSchema.ExtensionType: "ALegacyExtensionType", CommonTelemetryEventSchema.GAVersion: "WALinuxAgent-1.1.1", CommonTelemetryEventSchema.ContainerId: "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE", CommonTelemetryEventSchema.EventTid: 98765, CommonTelemetryEventSchema.EventPid: 4321, CommonTelemetryEventSchema.TaskName: "ALegacyTask", CommonTelemetryEventSchema.KeywordName: "ALegacyKeywordName"}, assert_timestamp=lambda timestamp: self.assertEqual(timestamp, '1970-01-01 12:00:00', "The event timestamp (opcode) is incorrect") ) def test_collect_events_should_use_the_file_creation_time_for_legacy_agent_events_missing_a_timestamp(self): test_file = self._create_test_event_file("legacy_agent_no_timestamp.tld") event_creation_time = TestEvent._get_file_creation_timestamp(test_file) event_list = self._collect_events() self.assertEqual(len(event_list), 1) self._assert_event_includes_all_parameters_in_the_telemetry_schema( event_list[0], expected_parameters={ GuestAgentExtensionEventsSchema.Name: "WALinuxAgent", GuestAgentExtensionEventsSchema.Version: "9.9.9", GuestAgentExtensionEventsSchema.IsInternal: False, GuestAgentExtensionEventsSchema.Operation: TestEvent._Operation, GuestAgentExtensionEventsSchema.OperationSuccess: True, GuestAgentExtensionEventsSchema.Message: "The cgroup filesystem is ready to use", GuestAgentExtensionEventsSchema.Duration: 1234, GuestAgentExtensionEventsSchema.ExtensionType: "ALegacyExtensionType", CommonTelemetryEventSchema.GAVersion: "WALinuxAgent-1.1.1", CommonTelemetryEventSchema.ContainerId: "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE", CommonTelemetryEventSchema.EventTid: 98765, CommonTelemetryEventSchema.EventPid: 4321, CommonTelemetryEventSchema.TaskName: "ALegacyTask", CommonTelemetryEventSchema.KeywordName: "ALegacyKeywordName"}, assert_timestamp=lambda timestamp: self.assertEqual(timestamp, event_creation_time, "The event timestamp (opcode) is incorrect") ) def _assert_extension_event_includes_all_parameters_in_the_telemetry_schema(self, event_file): # Extensions drop their events as *.tld files on the events directory. They populate only a subset of fields, # and the rest are added by the agent when events are collected. test_file = self._create_test_event_file(event_file) event_creation_time = TestEvent._get_file_creation_timestamp(test_file) event_list = self._collect_events() self.assertEqual(len(event_list), 1) self._assert_event_includes_all_parameters_in_the_telemetry_schema( event_list[0], expected_parameters={ GuestAgentExtensionEventsSchema.Name: 'Microsoft.Azure.Extensions.CustomScript', GuestAgentExtensionEventsSchema.Version: '2.0.4', GuestAgentExtensionEventsSchema.Operation: TestEvent._Operation, GuestAgentExtensionEventsSchema.OperationSuccess: True, GuestAgentExtensionEventsSchema.Message: 'A test telemetry message.', GuestAgentExtensionEventsSchema.Duration: 150000, GuestAgentExtensionEventsSchema.ExtensionType: 'json'}, assert_timestamp=lambda timestamp: self.assertEqual(timestamp, event_creation_time, "The event timestamp (opcode) is incorrect") ) def test_collect_events_should_add_all_the_parameters_in_the_telemetry_schema_to_extension_events(self): self._assert_extension_event_includes_all_parameters_in_the_telemetry_schema('custom_script_1.tld') def test_collect_events_should_ignore_extra_parameters_in_extension_events(self): self._assert_extension_event_includes_all_parameters_in_the_telemetry_schema('custom_script_extra_parameters.tld') def test_report_event_should_encode_call_stack_correctly(self): """ The Message in some telemetry events that include call stacks are being truncated in Kusto. While the issue doesn't seem to be in the agent itself, this test verifies that the Message of the event we send in the HTTP request matches the Message we read from the event's file. """ def get_event_message_from_event_file(event_file): with open(event_file, "rb") as fd: event_data = fd.read().decode("utf-8") # event files are UTF-8 encoded telemetry_event = json.loads(event_data) for p in telemetry_event['parameters']: if p['name'] == GuestAgentExtensionEventsSchema.Message: return p['value'] raise ValueError('Could not find the Message for the telemetry event in {0}'.format(event_file)) def get_event_message_from_http_request_body(event_body): # The XML for the event is sent over as a CDATA element ("Event") in the request's body http_request_body = event_body if ( event_body is None or type(event_body) is ustr) else textutil.str_to_encoded_ustr(event_body) request_body_xml_doc = textutil.parse_doc(http_request_body) event_node = textutil.find(request_body_xml_doc, "Event") if event_node is None: raise ValueError('Could not find the Event node in the XML document') if len(event_node.childNodes) != 1: raise ValueError('The Event node in the XML document should have exactly 1 child') event_node_first_child = event_node.childNodes[0] if event_node_first_child.nodeType != xml.dom.Node.CDATA_SECTION_NODE: raise ValueError('The Event node contents should be CDATA') event_node_cdata = event_node_first_child.nodeValue # The CDATA will contain a sequence of "" nodes, which # correspond to the parameters of the telemetry event. Wrap those into a "Helper" node # and extract the "Message" event_xml_text = '{0}'.format(event_node_cdata) event_xml_doc = textutil.parse_doc(event_xml_text) helper_node = textutil.find(event_xml_doc, "Helper") for child in helper_node.childNodes: if child.getAttribute('Name') == GuestAgentExtensionEventsSchema.Message: return child.getAttribute('Value') raise ValueError('Could not find the Message for the telemetry event. Request body: {0}'.format(http_request_body)) def http_post_handler(url, body, **__): if self.is_telemetry_request(url): http_post_handler.request_body = body return MockHttpResponse(status=200) return None http_post_handler.request_body = None with mock_wire_protocol(mockwiredata.DATA_FILE, http_post_handler=http_post_handler) as protocol: event_file_path = self._create_test_event_file("event_with_callstack.waagent.tld") expected_message = get_event_message_from_event_file(event_file_path) event_list = self._collect_events() self._report_events(protocol, event_list) event_message = get_event_message_from_http_request_body(http_post_handler.request_body) self.assertEqual(event_message, expected_message, "The Message in the HTTP request does not match the Message in the event's *.tld file") def test_report_event_should_encode_events_correctly(self): def http_post_handler(url, body, **__): if self.is_telemetry_request(url): http_post_handler.request_body = body return MockHttpResponse(status=200) return None http_post_handler.request_body = None with mock_wire_protocol(mockwiredata.DATA_FILE, http_post_handler=http_post_handler) as protocol: test_messages = [ 'Non-English message - æ­¤æ–‡å­—ä¸æ˜¯è‹±æ–‡çš„', "Ξεσκεπάζω τὴν ψυχοφθόÏα βδελυγμία", "The quick brown fox jumps over the lazy dog", "El pingüino Wenceslao hizo kilómetros bajo exhaustiva lluvia y frío, añoraba a su querido cachorro.", "Portez ce vieux whisky au juge blond qui fume sur son île intérieure, à côté de l'alcôve ovoïde, où les bûches", "se consument dans l'âtre, ce qui lui permet de penser à la cænogenèse de l'être dont il est question", "dans la cause ambiguë entendue à Moÿ, dans un capharnaüm qui, pense-t-il, diminue çà et là la qualité de son Å“uvre.", "D'fhuascail Ãosa, Úrmhac na hÓighe Beannaithe, pór Éava agus Ãdhaimh", "ÃrvíztűrÅ‘ tükörfúrógép", "Kæmi ný öxi hér ykist þjófum nú bæði víl og ádrepa", "Sævör grét áðan því úlpan var ónýt", "ã„ã‚ã¯ã«ã»ã¸ã¨ã¡ã‚Šã¬ã‚‹ã‚’ ã‚ã‹ã‚ˆãŸã‚Œãã¤ã­ãªã‚‰ã‚€ ã†ã‚ã®ãŠãã‚„ã¾ã‘ãµã“ãˆã¦ ã‚ã•ãゆã‚ã¿ã—ã‚‘ã²ã‚‚ã›ã™", "? דג סקרן שט ×‘×™× ×ž×וכזב ולפתע ×ž×¦× ×œ×• חברה ×יך הקליטה" "Pchnąć w tÄ™ łódź jeża lub oÅ›m skrzyÅ„ fig", "Normal string event" ] for msg in test_messages: add_event('TestEventEncoding', message=msg, op=TestEvent._Operation) event_list = self._collect_events() self._report_events(protocol, event_list) # In Py2, encode() produces a str and in py3 it produces a bytes string. # type(bytes) == type(str) for Py2 so this check is mainly for Py3 to ensure that the event is encoded properly. self.assertIsInstance(http_post_handler.request_body, bytes, "The Event request body should be encoded") self.assertIn(textutil.str_to_encoded_ustr(msg).encode('utf-8'), http_post_handler.request_body, "Encoded message not found in body") class TestMetrics(AgentTestCase): @patch('azurelinuxagent.common.event.EventLogger.save_event') def test_report_metric(self, mock_event): event.report_metric("cpu", "%idle", "_total", 10.0) self.assertEqual(1, mock_event.call_count) event_json = mock_event.call_args[0][0] self.assertIn(event.TELEMETRY_EVENT_PROVIDER_ID, event_json) self.assertIn("%idle", event_json) event_dictionary = json.loads(event_json) self.assertEqual(event_dictionary['providerId'], event.TELEMETRY_EVENT_PROVIDER_ID) for parameter in event_dictionary["parameters"]: if parameter['name'] == GuestAgentPerfCounterEventsSchema.Counter: self.assertEqual(parameter['value'], '%idle') break else: self.fail("Counter '%idle' not found in event parameters: {0}".format(repr(event_dictionary))) def test_cleanup_message(self): ev_logger = event.EventLogger() self.assertEqual(None, ev_logger._clean_up_message(None)) self.assertEqual("", ev_logger._clean_up_message("")) self.assertEqual("Daemon Activate resource disk failure", ev_logger._clean_up_message( "Daemon Activate resource disk failure")) self.assertEqual("[M.A.E.CS-2.0.7] Target handler state", ev_logger._clean_up_message( '2019/10/07 21:54:16.629444 INFO [M.A.E.CS-2.0.7] Target handler state')) self.assertEqual("[M.A.E.CS-2.0.7] Initializing extension M.A.E.CS-2.0.7", ev_logger._clean_up_message( '2019/10/07 21:54:17.284385 INFO [M.A.E.CS-2.0.7] Initializing extension M.A.E.CS-2.0.7')) self.assertEqual("ExtHandler ProcessGoalState completed [incarnation 4; 4197 ms]", ev_logger._clean_up_message( "2019/10/07 21:55:38.474861 INFO ExtHandler ProcessGoalState completed [incarnation 4; 4197 ms]")) self.assertEqual("Daemon Azure Linux Agent Version:2.2.43", ev_logger._clean_up_message( "2019/10/07 21:52:28.615720 INFO Daemon Azure Linux Agent Version:2.2.43")) self.assertEqual('Daemon Cgroup controller "memory" is not mounted. Failed to create a cgroup for the VM Agent;' ' resource usage will not be tracked', ev_logger._clean_up_message('Daemon Cgroup controller "memory" is not mounted. Failed to ' 'create a cgroup for the VM Agent; resource usage will not be ' 'tracked')) self.assertEqual('ExtHandler Root directory /sys/fs/cgroup/memory/walinuxagent.extensions does not exist.', ev_logger._clean_up_message("2019/10/08 23:45:05.691037 WARNING ExtHandler Root directory " "/sys/fs/cgroup/memory/walinuxagent.extensions does not exist.")) self.assertEqual("LinuxAzureDiagnostic started to handle.", ev_logger._clean_up_message("2019/10/07 22:02:40 LinuxAzureDiagnostic started to handle.")) self.assertEqual("VMAccess started to handle.", ev_logger._clean_up_message("2019/10/07 21:56:58 VMAccess started to handle.")) self.assertEqual( '[PERIODIC] ExtHandler Root directory /sys/fs/cgroup/memory/walinuxagent.extensions does not exist.', ev_logger._clean_up_message("2019/10/08 23:45:05.691037 WARNING [PERIODIC] ExtHandler Root directory " "/sys/fs/cgroup/memory/walinuxagent.extensions does not exist.")) self.assertEqual("[PERIODIC] LinuxAzureDiagnostic started to handle.", ev_logger._clean_up_message( "2019/10/07 22:02:40 [PERIODIC] LinuxAzureDiagnostic started to handle.")) self.assertEqual("[PERIODIC] VMAccess started to handle.", ev_logger._clean_up_message("2019/10/07 21:56:58 [PERIODIC] VMAccess started to handle.")) self.assertEqual('[PERIODIC] Daemon Cgroup controller "memory" is not mounted. Failed to create a cgroup for ' 'the VM Agent; resource usage will not be tracked', ev_logger._clean_up_message('[PERIODIC] Daemon Cgroup controller "memory" is not mounted. ' 'Failed to create a cgroup for the VM Agent; resource usage will ' 'not be tracked')) self.assertEqual('The time should be in UTC', ev_logger._clean_up_message( '2019-11-26T18:15:06.866746Z INFO The time should be in UTC')) self.assertEqual('The time should be in UTC', ev_logger._clean_up_message( '2019-11-26T18:15:06.866746Z The time should be in UTC')) self.assertEqual('[PERIODIC] The time should be in UTC', ev_logger._clean_up_message( '2019-11-26T18:15:06.866746Z INFO [PERIODIC] The time should be in UTC')) self.assertEqual('[PERIODIC] The time should be in UTC', ev_logger._clean_up_message( '2019-11-26T18:15:06.866746Z [PERIODIC] The time should be in UTC')) WALinuxAgent-2.9.1.1/tests/common/test_logcollector.py000066400000000000000000000575131446033677600227560ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2020 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import shutil import tempfile import zipfile from azurelinuxagent.common.logcollector import LogCollector from azurelinuxagent.common.utils import fileutil from azurelinuxagent.common.utils.fileutil import rm_dirs, mkdir, rm_files from tests.tools import AgentTestCase, is_python_version_26, patch, skip_if_predicate_true, data_dir SMALL_FILE_SIZE = 1 * 1024 * 1024 # 1 MB LARGE_FILE_SIZE = 5 * 1024 * 1024 # 5 MB @skip_if_predicate_true(is_python_version_26, "Disabled on Python 2.6") class TestLogCollector(AgentTestCase): @classmethod def setUpClass(cls): AgentTestCase.setUpClass() prefix = "{0}_".format(cls.__class__.__name__) cls.tmp_dir = tempfile.mkdtemp(prefix=prefix) cls.root_collect_dir = os.path.join(cls.tmp_dir, "files_to_collect") mkdir(cls.root_collect_dir) cls._mock_constants() cls._mock_cgroup() @classmethod def _mock_constants(cls): cls.mock_manifest = patch("azurelinuxagent.common.logcollector.MANIFEST_NORMAL", cls._build_manifest()) cls.mock_manifest.start() cls.log_collector_dir = os.path.join(cls.tmp_dir, "logcollector") cls.mock_log_collector_dir = patch("azurelinuxagent.common.logcollector._LOG_COLLECTOR_DIR", cls.log_collector_dir) cls.mock_log_collector_dir.start() cls.truncated_files_dir = os.path.join(cls.tmp_dir, "truncated") cls.mock_truncated_files_dir = patch("azurelinuxagent.common.logcollector._TRUNCATED_FILES_DIR", cls.truncated_files_dir) cls.mock_truncated_files_dir.start() cls.output_results_file_path = os.path.join(cls.log_collector_dir, "results.txt") cls.mock_output_results_file_path = patch("azurelinuxagent.common.logcollector.OUTPUT_RESULTS_FILE_PATH", cls.output_results_file_path) cls.mock_output_results_file_path.start() cls.compressed_archive_path = os.path.join(cls.log_collector_dir, "logs.zip") cls.mock_compressed_archive_path = patch("azurelinuxagent.common.logcollector.COMPRESSED_ARCHIVE_PATH", cls.compressed_archive_path) cls.mock_compressed_archive_path.start() @classmethod def _mock_cgroup(cls): # CPU Cgroups compute usage based on /proc/stat and /sys/fs/cgroup/.../cpuacct.stat; use mock data for those # files original_read_file = fileutil.read_file def mock_read_file(filepath, **args): if filepath == "/proc/stat": filepath = os.path.join(data_dir, "cgroups", "proc_stat_t0") elif filepath.endswith("/cpuacct.stat"): filepath = os.path.join(data_dir, "cgroups", "cpuacct.stat_t0") return original_read_file(filepath, **args) cls._mock_read_cpu_cgroup_file = patch("azurelinuxagent.common.utils.fileutil.read_file", side_effect=mock_read_file) cls._mock_read_cpu_cgroup_file.start() @classmethod def tearDownClass(cls): cls.mock_manifest.stop() cls.mock_log_collector_dir.stop() cls.mock_truncated_files_dir.stop() cls.mock_output_results_file_path.stop() cls.mock_compressed_archive_path.stop() cls._mock_read_cpu_cgroup_file.stop() shutil.rmtree(cls.tmp_dir) AgentTestCase.tearDownClass() def setUp(self): AgentTestCase.setUp(self) self._build_test_data() def tearDown(self): rm_dirs(self.root_collect_dir) rm_files(self.compressed_archive_path) AgentTestCase.tearDown(self) @classmethod def _build_test_data(cls): """ Build a dummy file structure which will be used as a foundation for the log collector tests """ cls._create_file_of_specific_size(os.path.join(cls.root_collect_dir, "waagent.log"), SMALL_FILE_SIZE) # small text file cls._create_file_of_specific_size(os.path.join(cls.root_collect_dir, "waagent.log.1"), LARGE_FILE_SIZE) # large text file cls._create_file_of_specific_size(os.path.join(cls.root_collect_dir, "waagent.log.2.gz"), SMALL_FILE_SIZE, binary=True) # small binary file cls._create_file_of_specific_size(os.path.join(cls.root_collect_dir, "waagent.log.3.gz"), LARGE_FILE_SIZE, binary=True) # large binary file mkdir(os.path.join(cls.root_collect_dir, "another_dir")) cls._create_file_of_specific_size(os.path.join(cls.root_collect_dir, "less_important_file"), SMALL_FILE_SIZE) cls._create_file_of_specific_size(os.path.join(cls.root_collect_dir, "another_dir", "least_important_file"), SMALL_FILE_SIZE) @classmethod def _build_manifest(cls): """ Files listed in the manifest will be collected, others will be ignored """ files = [ os.path.join(cls.root_collect_dir, "waagent*"), os.path.join(cls.root_collect_dir, "less_important_file*"), os.path.join(cls.root_collect_dir, "another_dir", "least_important_file"), os.path.join(cls.root_collect_dir, "non_existing_file"), ] manifest = "" for file_entry in files: manifest += "copy,{0}\n".format(file_entry) return manifest @staticmethod def _create_file_of_specific_size(file_path, file_size, binary=False): binary_descriptor = "b" if binary else "" data = b'0' if binary else '0' with open(file_path, "w{0}".format(binary_descriptor)) as fh: # pylint: disable=bad-open-mode fh.seek(file_size - 1) fh.write(data) @staticmethod def _truncated_path(normal_path): return "truncated_" + normal_path.replace(os.path.sep, "_") def _assert_files_are_in_archive(self, expected_files): with zipfile.ZipFile(self.compressed_archive_path, "r") as archive: archive_files = archive.namelist() for file in expected_files: # pylint: disable=redefined-builtin if file.lstrip(os.path.sep) not in archive_files: self.fail("File {0} was supposed to be collected, but is not present in the archive!".format(file)) # Assert that results file is always present if "results.txt" not in archive_files: self.fail("File results.txt was supposed to be collected, but is not present in the archive!") self.assertTrue(True) # pylint: disable=redundant-unittest-assert def _assert_files_are_not_in_archive(self, unexpected_files): with zipfile.ZipFile(self.compressed_archive_path, "r") as archive: archive_files = archive.namelist() for file in unexpected_files: # pylint: disable=redefined-builtin if file.lstrip(os.path.sep) in archive_files: self.fail("File {0} wasn't supposed to be collected, but is present in the archive!".format(file)) self.assertTrue(True) # pylint: disable=redundant-unittest-assert def _assert_archive_created(self, archive): with open(self.output_results_file_path, "r") as out: error_message = out.readlines()[-1] self.assertTrue(archive, "Failed to collect logs, error message: {0}".format(error_message)) def _get_uncompressed_file_size(self, file): # pylint: disable=redefined-builtin with zipfile.ZipFile(self.compressed_archive_path, "r") as archive: return archive.getinfo(file.lstrip(os.path.sep)).file_size def _get_number_of_files_in_archive(self): with zipfile.ZipFile(self.compressed_archive_path, "r") as archive: # Exclude results file return len(archive.namelist())-1 def test_log_collector_parses_commands_in_manifest(self): # Ensure familiar commands are parsed and unknowns are ignored (like diskinfo and malformed entries) file_to_collect = os.path.join(self.root_collect_dir, "waagent.log") folder_to_list = self.root_collect_dir manifest = """ echo,### Test header ### unknown command ll,{0} copy,{1} diskinfo,""".format(folder_to_list, file_to_collect) with patch("azurelinuxagent.common.logcollector.MANIFEST_NORMAL", manifest): with patch('azurelinuxagent.common.logcollector.LogCollector._initialize_telemetry'): log_collector = LogCollector(cpu_cgroup_path="dummy_cpu_path", memory_cgroup_path="dummy_memory_path") archive = log_collector.collect_logs_and_get_archive() with open(self.output_results_file_path, "r") as fh: results = fh.readlines() # Assert echo was parsed self.assertTrue(any(line.endswith("### Test header ###\n") for line in results)) # Assert unknown command was reported self.assertTrue(any(line.endswith("ERROR Couldn\'t parse \"unknown command\"\n") for line in results)) # Assert ll was parsed self.assertTrue(any("ls -alF {0}".format(folder_to_list) in line for line in results)) # Assert copy was parsed self._assert_archive_created(archive) self._assert_files_are_in_archive(expected_files=[file_to_collect]) no_files = self._get_number_of_files_in_archive() self.assertEqual(1, no_files, "Expected 1 file in archive, found {0}!".format(no_files)) def test_log_collector_uses_full_manifest_when_full_mode_enabled(self): file_to_collect = os.path.join(self.root_collect_dir, "less_important_file") manifest = """ echo,### Test header ### copy,{0} """.format(file_to_collect) with patch("azurelinuxagent.common.logcollector.MANIFEST_FULL", manifest): with patch('azurelinuxagent.common.logcollector.LogCollector._initialize_telemetry'): log_collector = LogCollector(is_full_mode=True, cpu_cgroup_path="dummy_cpu_path", memory_cgroup_path="dummy_memory_path") archive = log_collector.collect_logs_and_get_archive() self._assert_archive_created(archive) self._assert_files_are_in_archive(expected_files=[file_to_collect]) no_files = self._get_number_of_files_in_archive() self.assertEqual(1, no_files, "Expected 1 file in archive, found {0}!".format(no_files)) def test_log_collector_should_collect_all_files(self): # All files in the manifest should be collected, since none of them are over the individual file size limit, # and combined they do not cross the archive size threshold. with patch('azurelinuxagent.common.logcollector.LogCollector._initialize_telemetry'): log_collector = LogCollector(cpu_cgroup_path="dummy_cpu_path", memory_cgroup_path="dummy_memory_path") archive = log_collector.collect_logs_and_get_archive() self._assert_archive_created(archive) expected_files = [ os.path.join(self.root_collect_dir, "waagent.log"), os.path.join(self.root_collect_dir, "waagent.log.1"), os.path.join(self.root_collect_dir, "waagent.log.2.gz"), os.path.join(self.root_collect_dir, "waagent.log.3.gz"), os.path.join(self.root_collect_dir, "less_important_file"), os.path.join(self.root_collect_dir, "another_dir", "least_important_file") ] self._assert_files_are_in_archive(expected_files) no_files = self._get_number_of_files_in_archive() self.assertEqual(6, no_files, "Expected 6 files in archive, found {0}!".format(no_files)) def test_log_collector_should_truncate_large_text_files_and_ignore_large_binary_files(self): # Set the size limit so that some files are too large to collect in full. with patch("azurelinuxagent.common.logcollector._FILE_SIZE_LIMIT", SMALL_FILE_SIZE): with patch('azurelinuxagent.common.logcollector.LogCollector._initialize_telemetry'): log_collector = LogCollector(cpu_cgroup_path="dummy_cpu_path", memory_cgroup_path="dummy_memory_path") archive = log_collector.collect_logs_and_get_archive() self._assert_archive_created(archive) expected_files = [ os.path.join(self.root_collect_dir, "waagent.log"), self._truncated_path(os.path.join(self.root_collect_dir, "waagent.log.1")), # this file should be truncated os.path.join(self.root_collect_dir, "waagent.log.2.gz"), os.path.join(self.root_collect_dir, "less_important_file"), os.path.join(self.root_collect_dir, "another_dir", "least_important_file") ] unexpected_files = [ os.path.join(self.root_collect_dir, "waagent.log.3.gz") # binary files cannot be truncated, ignore it ] self._assert_files_are_in_archive(expected_files) self._assert_files_are_not_in_archive(unexpected_files) no_files = self._get_number_of_files_in_archive() self.assertEqual(5, no_files, "Expected 5 files in archive, found {0}!".format(no_files)) def test_log_collector_should_prioritize_important_files_if_archive_too_big(self): # Set the archive size limit so that not all files can be collected. In that case, files will be added to the # archive according to their priority. # Specify files that have priority. The list is ordered, where the first entry has the highest priority. must_collect_files = [ os.path.join(self.root_collect_dir, "waagent*"), os.path.join(self.root_collect_dir, "less_important_file*") ] with patch("azurelinuxagent.common.logcollector._UNCOMPRESSED_ARCHIVE_SIZE_LIMIT", 10 * 1024 * 1024): with patch("azurelinuxagent.common.logcollector._MUST_COLLECT_FILES", must_collect_files): with patch('azurelinuxagent.common.logcollector.LogCollector._initialize_telemetry'): log_collector = LogCollector(cpu_cgroup_path="dummy_cpu_path", memory_cgroup_path="dummy_memory_path") archive = log_collector.collect_logs_and_get_archive() self._assert_archive_created(archive) expected_files = [ os.path.join(self.root_collect_dir, "waagent.log"), os.path.join(self.root_collect_dir, "waagent.log.1"), os.path.join(self.root_collect_dir, "waagent.log.2.gz") ] unexpected_files = [ os.path.join(self.root_collect_dir, "waagent.log.3.gz"), os.path.join(self.root_collect_dir, "less_important_file"), os.path.join(self.root_collect_dir, "another_dir", "least_important_file") ] self._assert_files_are_in_archive(expected_files) self._assert_files_are_not_in_archive(unexpected_files) no_files = self._get_number_of_files_in_archive() self.assertEqual(3, no_files, "Expected 3 files in archive, found {0}!".format(no_files)) # Second collection, if a file got deleted, delete it from the archive and add next file on the priority list # if there is enough space. rm_files(os.path.join(self.root_collect_dir, "waagent.log.3.gz")) with patch("azurelinuxagent.common.logcollector._UNCOMPRESSED_ARCHIVE_SIZE_LIMIT", 10 * 1024 * 1024): with patch("azurelinuxagent.common.logcollector._MUST_COLLECT_FILES", must_collect_files): second_archive = log_collector.collect_logs_and_get_archive() expected_files = [ os.path.join(self.root_collect_dir, "waagent.log"), os.path.join(self.root_collect_dir, "waagent.log.1"), os.path.join(self.root_collect_dir, "waagent.log.2.gz"), os.path.join(self.root_collect_dir, "less_important_file"), os.path.join(self.root_collect_dir, "another_dir", "least_important_file") ] unexpected_files = [ os.path.join(self.root_collect_dir, "waagent.log.3.gz") ] self._assert_files_are_in_archive(expected_files) self._assert_files_are_not_in_archive(unexpected_files) self._assert_archive_created(second_archive) no_files = self._get_number_of_files_in_archive() self.assertEqual(5, no_files, "Expected 5 files in archive, found {0}!".format(no_files)) def test_log_collector_should_update_archive_when_files_are_new_or_modified_or_deleted(self): # Ensure the archive reflects the state of files on the disk at collection time. If a file was updated, it # needs to be updated in the archive, deleted if removed from disk, and added if not previously seen. with patch('azurelinuxagent.common.logcollector.LogCollector._initialize_telemetry'): log_collector = LogCollector(cpu_cgroup_path="dummy_cpu_path", memory_cgroup_path="dummy_memory_path") first_archive = log_collector.collect_logs_and_get_archive() self._assert_archive_created(first_archive) # Everything should be in the archive expected_files = [ os.path.join(self.root_collect_dir, "waagent.log"), os.path.join(self.root_collect_dir, "waagent.log.1"), os.path.join(self.root_collect_dir, "waagent.log.2.gz"), os.path.join(self.root_collect_dir, "waagent.log.3.gz"), os.path.join(self.root_collect_dir, "less_important_file"), os.path.join(self.root_collect_dir, "another_dir", "least_important_file") ] self._assert_files_are_in_archive(expected_files) no_files = self._get_number_of_files_in_archive() self.assertEqual(6, no_files, "Expected 6 files in archive, found {0}!".format(no_files)) # Update a file and its last modified time to ensure the last modified time and last collection time are not # the same in this test file_to_update = os.path.join(self.root_collect_dir, "waagent.log") self._create_file_of_specific_size(file_to_update, LARGE_FILE_SIZE) # update existing file new_time = os.path.getmtime(file_to_update) + 5 os.utime(file_to_update, (new_time, new_time)) # Create a new file (that is covered by the manifest and will be collected) and delete a file self._create_file_of_specific_size(os.path.join(self.root_collect_dir, "less_important_file.1"), LARGE_FILE_SIZE) rm_files(os.path.join(self.root_collect_dir, "waagent.log.1")) second_archive = log_collector.collect_logs_and_get_archive() self._assert_archive_created(second_archive) expected_files = [ os.path.join(self.root_collect_dir, "waagent.log"), os.path.join(self.root_collect_dir, "waagent.log.2.gz"), os.path.join(self.root_collect_dir, "waagent.log.3.gz"), os.path.join(self.root_collect_dir, "less_important_file"), os.path.join(self.root_collect_dir, "less_important_file.1"), os.path.join(self.root_collect_dir, "another_dir", "least_important_file") ] unexpected_files = [ os.path.join(self.root_collect_dir, "waagent.log.1") ] self._assert_files_are_in_archive(expected_files) self._assert_files_are_not_in_archive(unexpected_files) file = os.path.join(self.root_collect_dir, "waagent.log") # pylint: disable=redefined-builtin new_file_size = self._get_uncompressed_file_size(file) self.assertEqual(LARGE_FILE_SIZE, new_file_size, "File {0} hasn't been updated! Size in archive is {1}, but " "should be {2}.".format(file, new_file_size, LARGE_FILE_SIZE)) no_files = self._get_number_of_files_in_archive() self.assertEqual(6, no_files, "Expected 6 files in archive, found {0}!".format(no_files)) def test_log_collector_should_clean_up_uncollected_truncated_files(self): # Make sure that truncated files that are no longer needed are cleaned up. If an existing truncated file # from a previous run is not collected in the current run, it should be deleted to free up space. # Specify files that have priority. The list is ordered, where the first entry has the highest priority. must_collect_files = [ os.path.join(self.root_collect_dir, "waagent*") ] # Set the archive size limit so that not all files can be collected. In that case, files will be added to the # archive according to their priority. # Set the size limit so that only two files can be collected, of which one needs to be truncated. with patch("azurelinuxagent.common.logcollector._UNCOMPRESSED_ARCHIVE_SIZE_LIMIT", 2 * SMALL_FILE_SIZE): with patch("azurelinuxagent.common.logcollector._MUST_COLLECT_FILES", must_collect_files): with patch("azurelinuxagent.common.logcollector._FILE_SIZE_LIMIT", SMALL_FILE_SIZE): with patch('azurelinuxagent.common.logcollector.LogCollector._initialize_telemetry'): log_collector = LogCollector(cpu_cgroup_path="dummy_cpu_path", memory_cgroup_path="dummy_memory_path") archive = log_collector.collect_logs_and_get_archive() self._assert_archive_created(archive) expected_files = [ os.path.join(self.root_collect_dir, "waagent.log"), self._truncated_path(os.path.join(self.root_collect_dir, "waagent.log.1")), # this file should be truncated ] self._assert_files_are_in_archive(expected_files) no_files = self._get_number_of_files_in_archive() self.assertEqual(2, no_files, "Expected 2 files in archive, found {0}!".format(no_files)) # Remove the original file so it is not collected anymore. In the next collection, the truncated file should be # removed both from the archive and from the filesystem. rm_files(os.path.join(self.root_collect_dir, "waagent.log.1")) with patch("azurelinuxagent.common.logcollector._UNCOMPRESSED_ARCHIVE_SIZE_LIMIT", 2 * SMALL_FILE_SIZE): with patch("azurelinuxagent.common.logcollector._MUST_COLLECT_FILES", must_collect_files): with patch("azurelinuxagent.common.logcollector._FILE_SIZE_LIMIT", SMALL_FILE_SIZE): with patch('azurelinuxagent.common.logcollector.LogCollector._initialize_telemetry'): log_collector = LogCollector(cpu_cgroup_path="dummy_cpu_path", memory_cgroup_path="dummy_memory_path") second_archive = log_collector.collect_logs_and_get_archive() expected_files = [ os.path.join(self.root_collect_dir, "waagent.log"), os.path.join(self.root_collect_dir, "waagent.log.2.gz"), ] unexpected_files = [ self._truncated_path(os.path.join(self.root_collect_dir, "waagent.log.1")) ] self._assert_files_are_in_archive(expected_files) self._assert_files_are_not_in_archive(unexpected_files) self._assert_archive_created(second_archive) no_files = self._get_number_of_files_in_archive() self.assertEqual(2, no_files, "Expected 2 files in archive, found {0}!".format(no_files)) truncated_files = os.listdir(self.truncated_files_dir) self.assertEqual(0, len(truncated_files), "Uncollected truncated file waagent.log.1 should have been deleted!") WALinuxAgent-2.9.1.1/tests/common/test_logger.py000066400000000000000000000710771446033677600215460ustar00rootroot00000000000000# Copyright 2016 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import json # pylint: disable=unused-import import os import tempfile from datetime import datetime, timedelta from azurelinuxagent.common.event import __event_logger__, add_log_event, MAX_NUMBER_OF_EVENTS, EVENTS_DIRECTORY import azurelinuxagent.common.logger as logger from azurelinuxagent.common.utils import fileutil from tests.tools import AgentTestCase, MagicMock, patch, skip_if_predicate_true _MSG_INFO = "This is our test info logging message {0} {1}" _MSG_WARN = "This is our test warn logging message {0} {1}" _MSG_ERROR = "This is our test error logging message {0} {1}" _MSG_VERBOSE = "This is our test verbose logging message {0} {1}" _DATA = ["arg1", "arg2"] class TestLogger(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) self.lib_dir = tempfile.mkdtemp() self.event_dir = os.path.join(self.lib_dir, EVENTS_DIRECTORY) fileutil.mkdir(self.event_dir) self.log_file = tempfile.mkstemp(prefix="logfile-")[1] logger.reset_periodic() def tearDown(self): AgentTestCase.tearDown(self) logger.reset_periodic() logger.DEFAULT_LOGGER.appenders *= 0 fileutil.rm_dirs(self.event_dir) @patch('azurelinuxagent.common.logger.Logger.verbose') @patch('azurelinuxagent.common.logger.Logger.warn') @patch('azurelinuxagent.common.logger.Logger.error') @patch('azurelinuxagent.common.logger.Logger.info') def test_periodic_emits_if_not_previously_sent(self, mock_info, mock_error, mock_warn, mock_verbose): logger.periodic_info(logger.EVERY_DAY, _MSG_INFO, logger.LogLevel.INFO, *_DATA) self.assertEqual(1, mock_info.call_count) logger.periodic_error(logger.EVERY_DAY, _MSG_ERROR, logger.LogLevel.ERROR, *_DATA) self.assertEqual(1, mock_error.call_count) logger.periodic_warn(logger.EVERY_DAY, _MSG_WARN, logger.LogLevel.WARNING, *_DATA) self.assertEqual(1, mock_warn.call_count) logger.periodic_verbose(logger.EVERY_DAY, _MSG_VERBOSE, logger.LogLevel.VERBOSE, *_DATA) self.assertEqual(1, mock_verbose.call_count) @patch('azurelinuxagent.common.logger.Logger.verbose') @patch('azurelinuxagent.common.logger.Logger.warn') @patch('azurelinuxagent.common.logger.Logger.error') @patch('azurelinuxagent.common.logger.Logger.info') def test_periodic_does_not_emit_if_previously_sent(self, mock_info, mock_error, mock_warn, mock_verbose): # The count does not increase from 1 - the first time it sends the data. logger.periodic_info(logger.EVERY_DAY, _MSG_INFO, *_DATA) self.assertIn(hash(_MSG_INFO), logger.DEFAULT_LOGGER.periodic_messages) self.assertEqual(1, mock_info.call_count) logger.periodic_info(logger.EVERY_DAY, _MSG_INFO, *_DATA) self.assertIn(hash(_MSG_INFO), logger.DEFAULT_LOGGER.periodic_messages) self.assertEqual(1, mock_info.call_count) logger.periodic_warn(logger.EVERY_DAY, _MSG_WARN, *_DATA) self.assertIn(hash(_MSG_WARN), logger.DEFAULT_LOGGER.periodic_messages) self.assertEqual(1, mock_warn.call_count) logger.periodic_warn(logger.EVERY_DAY, _MSG_WARN, *_DATA) self.assertIn(hash(_MSG_WARN), logger.DEFAULT_LOGGER.periodic_messages) self.assertEqual(1, mock_warn.call_count) logger.periodic_error(logger.EVERY_DAY, _MSG_ERROR, *_DATA) self.assertIn(hash(_MSG_ERROR), logger.DEFAULT_LOGGER.periodic_messages) self.assertEqual(1, mock_error.call_count) logger.periodic_error(logger.EVERY_DAY, _MSG_ERROR, *_DATA) self.assertIn(hash(_MSG_ERROR), logger.DEFAULT_LOGGER.periodic_messages) self.assertEqual(1, mock_error.call_count) logger.periodic_verbose(logger.EVERY_DAY, _MSG_VERBOSE, *_DATA) self.assertIn(hash(_MSG_VERBOSE), logger.DEFAULT_LOGGER.periodic_messages) self.assertEqual(1, mock_verbose.call_count) logger.periodic_verbose(logger.EVERY_DAY, _MSG_VERBOSE, *_DATA) self.assertIn(hash(_MSG_VERBOSE), logger.DEFAULT_LOGGER.periodic_messages) self.assertEqual(1, mock_verbose.call_count) self.assertEqual(4, len(logger.DEFAULT_LOGGER.periodic_messages)) @patch('azurelinuxagent.common.logger.Logger.verbose') @patch('azurelinuxagent.common.logger.Logger.warn') @patch('azurelinuxagent.common.logger.Logger.error') @patch('azurelinuxagent.common.logger.Logger.info') def test_periodic_emits_after_elapsed_delta(self, mock_info, mock_error, mock_warn, mock_verbose): logger.periodic_info(logger.EVERY_DAY, _MSG_INFO, *_DATA) self.assertEqual(1, mock_info.call_count) logger.periodic_info(logger.EVERY_DAY, _MSG_INFO, *_DATA) self.assertEqual(1, mock_info.call_count) logger.DEFAULT_LOGGER.periodic_messages[hash(_MSG_INFO)] = datetime.now() - \ logger.EVERY_DAY - logger.EVERY_HOUR logger.periodic_info(logger.EVERY_DAY, _MSG_INFO, *_DATA) self.assertEqual(2, mock_info.call_count) logger.periodic_warn(logger.EVERY_DAY, _MSG_WARN, *_DATA) self.assertEqual(1, mock_warn.call_count) logger.periodic_warn(logger.EVERY_DAY, _MSG_WARN, *_DATA) self.assertEqual(1, mock_warn.call_count) logger.DEFAULT_LOGGER.periodic_messages[hash(_MSG_WARN)] = datetime.now() - \ logger.EVERY_DAY - logger.EVERY_HOUR logger.periodic_warn(logger.EVERY_DAY, _MSG_WARN, *_DATA) self.assertEqual(2, mock_info.call_count) logger.periodic_error(logger.EVERY_DAY, _MSG_ERROR, *_DATA) self.assertEqual(1, mock_error.call_count) logger.periodic_error(logger.EVERY_DAY, _MSG_ERROR, *_DATA) self.assertEqual(1, mock_error.call_count) logger.DEFAULT_LOGGER.periodic_messages[hash(_MSG_ERROR)] = datetime.now() - \ logger.EVERY_DAY - logger.EVERY_HOUR logger.periodic_error(logger.EVERY_DAY, _MSG_ERROR, *_DATA) self.assertEqual(2, mock_info.call_count) logger.periodic_verbose(logger.EVERY_DAY, _MSG_VERBOSE, *_DATA) self.assertEqual(1, mock_verbose.call_count) logger.periodic_verbose(logger.EVERY_DAY, _MSG_VERBOSE, *_DATA) self.assertEqual(1, mock_verbose.call_count) logger.DEFAULT_LOGGER.periodic_messages[hash(_MSG_VERBOSE)] = datetime.now() - \ logger.EVERY_DAY - logger.EVERY_HOUR logger.periodic_verbose(logger.EVERY_DAY, _MSG_VERBOSE, *_DATA) self.assertEqual(2, mock_info.call_count) @patch('azurelinuxagent.common.logger.Logger.verbose') @patch('azurelinuxagent.common.logger.Logger.warn') @patch('azurelinuxagent.common.logger.Logger.error') @patch('azurelinuxagent.common.logger.Logger.info') def test_periodic_forwards_message_and_args(self, mock_info, mock_error, mock_warn, mock_verbose): logger.periodic_info(logger.EVERY_DAY, _MSG_INFO, *_DATA) mock_info.assert_called_once_with(_MSG_INFO, *_DATA) logger.periodic_error(logger.EVERY_DAY, _MSG_ERROR, *_DATA) mock_error.assert_called_once_with(_MSG_ERROR, *_DATA) logger.periodic_warn(logger.EVERY_DAY, _MSG_WARN, *_DATA) mock_warn.assert_called_once_with(_MSG_WARN, *_DATA) logger.periodic_verbose(logger.EVERY_DAY, _MSG_VERBOSE, *_DATA) mock_verbose.assert_called_once_with(_MSG_VERBOSE, *_DATA) def test_logger_should_log_in_utc(self): file_name = "test.log" file_path = os.path.join(self.tmp_dir, file_name) test_logger = logger.Logger() test_logger.add_appender(logger.AppenderType.FILE, logger.LogLevel.INFO, path=file_path) before_write_utc = datetime.utcnow() test_logger.info("The time should be in UTC") with open(file_path, "r") as log_file: log = log_file.read() try: time_in_file = datetime.strptime(log.split(logger.LogLevel.STRINGS[logger.LogLevel.INFO])[0].strip() , logger.Logger.LogTimeFormatInUTC) except ValueError: self.fail("Ensure timestamp follows ISO-8601 format + 'Z' for UTC") # If the time difference is > 5secs, there's a high probability that the time_in_file is in different TZ self.assertTrue((time_in_file-before_write_utc) <= timedelta(seconds=5)) @patch("azurelinuxagent.common.logger.datetime") def test_logger_should_log_micro_seconds(self, mock_dt): # datetime.isoformat() skips ms if ms=0, this test ensures that ms is always set file_name = "test.log" file_path = os.path.join(self.tmp_dir, file_name) test_logger = logger.Logger() test_logger.add_appender(logger.AppenderType.FILE, logger.LogLevel.INFO, path=file_path) ts_with_no_ms = datetime.utcnow().replace(microsecond=0) mock_dt.utcnow = MagicMock(return_value=ts_with_no_ms) test_logger.info("The time should contain milli-seconds") with open(file_path, "r") as log_file: log = log_file.read() try: time_in_file = datetime.strptime(log.split(logger.LogLevel.STRINGS[logger.LogLevel.INFO])[0].strip() , logger.Logger.LogTimeFormatInUTC) except ValueError: self.fail("Ensure timestamp follows ISO-8601 format and has micro seconds in it") self.assertEqual(ts_with_no_ms, time_in_file, "Timestamps dont match") def test_telemetry_logger(self): mock = MagicMock() appender = logger.TelemetryAppender(logger.LogLevel.WARNING, mock) appender.write(logger.LogLevel.WARNING, "--unit-test-WARNING--") mock.assert_called_with(logger.LogLevel.WARNING, "--unit-test-WARNING--") mock.reset_mock() appender.write(logger.LogLevel.ERROR, "--unit-test-ERROR--") mock.assert_called_with(logger.LogLevel.ERROR, "--unit-test-ERROR--") mock.reset_mock() appender.write(logger.LogLevel.INFO, "--unit-test-INFO--") mock.assert_not_called() mock.reset_mock() for i in range(5): # pylint: disable=unused-variable appender.write(logger.LogLevel.ERROR, "--unit-test-ERROR--") appender.write(logger.LogLevel.INFO, "--unit-test-INFO--") self.assertEqual(5, mock.call_count) # Only ERROR should be called. @patch('azurelinuxagent.common.event.EventLogger.save_event') def test_telemetry_logger_not_on_by_default(self, mock_save): appender = logger.TelemetryAppender(logger.LogLevel.WARNING, add_log_event) appender.write(logger.LogLevel.WARNING, 'Cgroup controller "memory" is not mounted. ' 'Failed to create a cgroup for extension ' 'Microsoft.OSTCExtensions.DummyExtension-1.2.3.4') self.assertEqual(0, mock_save.call_count) @patch("azurelinuxagent.common.logger.StdoutAppender.write") @patch("azurelinuxagent.common.logger.TelemetryAppender.write") @patch("azurelinuxagent.common.logger.ConsoleAppender.write") @patch("azurelinuxagent.common.logger.FileAppender.write") def test_add_appender(self, mock_file_write, mock_console_write, mock_telem_write, mock_stdout_write): lg = logger.Logger(logger.DEFAULT_LOGGER, "TestLogger1") lg.add_appender(logger.AppenderType.FILE, logger.LogLevel.INFO, path=self.log_file) lg.add_appender(logger.AppenderType.TELEMETRY, logger.LogLevel.WARNING, path=add_log_event) lg.add_appender(logger.AppenderType.CONSOLE, logger.LogLevel.WARNING, path="/dev/null") lg.add_appender(logger.AppenderType.STDOUT, logger.LogLevel.WARNING, path=None) counter = 0 for appender in lg.appenders: if isinstance(appender, logger.FileAppender): counter += 1 elif isinstance(appender, logger.TelemetryAppender): counter += 1 elif isinstance(appender, logger.ConsoleAppender): counter += 1 elif isinstance(appender, logger.StdoutAppender): counter += 1 # All 4 appenders should have been included. self.assertEqual(4, counter) # The write for all the loggers will get called, but the levels are honored in the individual write method # itself. Each appender has its own test to validate the writing of the log message for different levels. # For Reference: tests.common.test_logger.TestAppender lg.warn("Test Log") self.assertEqual(1, mock_file_write.call_count) self.assertEqual(1, mock_console_write.call_count) self.assertEqual(1, mock_telem_write.call_count) self.assertEqual(1, mock_stdout_write.call_count) lg.info("Test Log") self.assertEqual(2, mock_file_write.call_count) self.assertEqual(2, mock_console_write.call_count) self.assertEqual(2, mock_telem_write.call_count) self.assertEqual(2, mock_stdout_write.call_count) lg.error("Test Log") self.assertEqual(3, mock_file_write.call_count) self.assertEqual(3, mock_console_write.call_count) self.assertEqual(3, mock_telem_write.call_count) self.assertEqual(3, mock_stdout_write.call_count) @patch("azurelinuxagent.common.logger.StdoutAppender.write") @patch("azurelinuxagent.common.logger.TelemetryAppender.write") @patch("azurelinuxagent.common.logger.ConsoleAppender.write") @patch("azurelinuxagent.common.logger.FileAppender.write") def test_set_prefix(self, mock_file_write, mock_console_write, mock_telem_write, mock_stdout_write): lg = logger.Logger(logger.DEFAULT_LOGGER) prefix = "YoloLogger" lg.set_prefix(prefix) self.assertEqual(lg.prefix, prefix) lg.add_appender(logger.AppenderType.FILE, logger.LogLevel.INFO, path=self.log_file) lg.add_appender(logger.AppenderType.TELEMETRY, logger.LogLevel.WARNING, path=add_log_event) lg.add_appender(logger.AppenderType.CONSOLE, logger.LogLevel.WARNING, path="/dev/null") lg.add_appender(logger.AppenderType.STDOUT, logger.LogLevel.WARNING, path=None) lg.error("Test Log") self.assertIn(prefix, mock_file_write.call_args[0][1]) self.assertIn(prefix, mock_console_write.call_args[0][1]) self.assertIn(prefix, mock_telem_write.call_args[0][1]) self.assertIn(prefix, mock_stdout_write.call_args[0][1]) @patch("azurelinuxagent.common.logger.StdoutAppender.write") @patch("azurelinuxagent.common.logger.TelemetryAppender.write") @patch("azurelinuxagent.common.logger.ConsoleAppender.write") @patch("azurelinuxagent.common.logger.FileAppender.write") def test_nested_logger(self, mock_file_write, mock_console_write, mock_telem_write, mock_stdout_write): """ The purpose of this test is to see if the logger gets correctly created when passed it another logger and also if the appender correctly gets the messages logged. This is how the ExtHandlerInstance logger works. I initialize the default logger(logger), then create a new logger(lg) from it, and then log using logger & lg. See if both logs are flowing through or not. """ parent_prefix = "ParentLogger" child_prefix = "ChildLogger" logger.add_logger_appender(logger.AppenderType.FILE, logger.LogLevel.INFO, path=self.log_file) logger.add_logger_appender(logger.AppenderType.TELEMETRY, logger.LogLevel.WARNING, path=add_log_event) logger.add_logger_appender(logger.AppenderType.CONSOLE, logger.LogLevel.WARNING, path="/dev/null") logger.add_logger_appender(logger.AppenderType.STDOUT, logger.LogLevel.WARNING) logger.set_prefix(parent_prefix) lg = logger.Logger(logger.DEFAULT_LOGGER, child_prefix) lg.error("Test Log") self.assertEqual(1, mock_file_write.call_count) self.assertEqual(1, mock_console_write.call_count) self.assertEqual(1, mock_telem_write.call_count) self.assertEqual(1, mock_stdout_write.call_count) self.assertIn(child_prefix, mock_file_write.call_args[0][1]) self.assertIn(child_prefix, mock_console_write.call_args[0][1]) self.assertIn(child_prefix, mock_telem_write.call_args[0][1]) self.assertIn(child_prefix, mock_stdout_write.call_args[0][1]) logger.error("Test Log") self.assertEqual(2, mock_file_write.call_count) self.assertEqual(2, mock_console_write.call_count) self.assertEqual(2, mock_telem_write.call_count) self.assertEqual(2, mock_stdout_write.call_count) self.assertIn(parent_prefix, mock_file_write.call_args[0][1]) self.assertIn(parent_prefix, mock_console_write.call_args[0][1]) self.assertIn(parent_prefix, mock_telem_write.call_args[0][1]) self.assertIn(parent_prefix, mock_stdout_write.call_args[0][1]) @patch("azurelinuxagent.common.event.send_logs_to_telemetry", return_value=True) @patch("azurelinuxagent.common.conf.get_lib_dir") def test_telemetry_logger_add_log_event(self, mock_lib_dir, *_): mock_lib_dir.return_value = self.lib_dir __event_logger__.event_dir = self.event_dir prefix = "YoloLogger" logger.add_logger_appender(logger.AppenderType.TELEMETRY, logger.LogLevel.WARNING, path=add_log_event) logger.set_prefix(prefix) logger.warn('Test Log - Warning') event_files = os.listdir(__event_logger__.event_dir) self.assertEqual(1, len(event_files)) log_file_event = os.path.join(__event_logger__.event_dir, event_files[0]) try: with open(log_file_event) as logfile: logcontent = logfile.read() # Checking the contents of the event file. self.assertIn("Test Log - Warning", logcontent) except Exception as e: self.assertFalse(True, "The log file looks like it isn't correctly setup for this test. Take a look. " # pylint: disable=redundant-unittest-assert "{0}".format(e)) @skip_if_predicate_true(lambda: True, "Enable this test when SEND_LOGS_TO_TELEMETRY is enabled") @patch("azurelinuxagent.common.logger.StdoutAppender.write") @patch("azurelinuxagent.common.logger.ConsoleAppender.write") @patch("azurelinuxagent.common.event.send_logs_to_telemetry", return_value=True) def test_telemetry_logger_verify_maximum_recursion_depths_doesnt_happen(self, *_): logger.add_logger_appender(logger.AppenderType.FILE, logger.LogLevel.INFO, path="/dev/null") logger.add_logger_appender(logger.AppenderType.TELEMETRY, logger.LogLevel.WARNING, path=add_log_event) for i in range(MAX_NUMBER_OF_EVENTS): logger.warn('Test Log - {0} - 1 - Warning'.format(i)) exception_caught = False # #1035 was caused due to too many files being written in an error condition. Adding even one more here broke # the camels back earlier - It would go into an infinite recursion as telemetry would call log, which in turn # would call telemetry, and so on. # The description of the fix is given in the comments @ azurelinuxagent.common.logger.Logger#log.write_log. try: for i in range(10): logger.warn('Test Log - {0} - 2 - Warning'.format(i)) except RuntimeError: exception_caught = True self.assertFalse(exception_caught, msg="Caught a Runtime Error. This should not have been raised.") @skip_if_predicate_true(lambda: True, "Enable this test when SEND_LOGS_TO_TELEMETRY is enabled") @patch("azurelinuxagent.common.logger.StdoutAppender.write") @patch("azurelinuxagent.common.logger.ConsoleAppender.write") @patch("azurelinuxagent.common.event.send_logs_to_telemetry", return_value=True) @patch("azurelinuxagent.common.conf.get_lib_dir") def test_telemetry_logger_check_all_file_logs_written_when_events_gt_MAX_NUMBER_OF_EVENTS(self, mock_lib_dir, *_): mock_lib_dir.return_value = self.lib_dir __event_logger__.event_dir = self.event_dir no_of_log_statements = MAX_NUMBER_OF_EVENTS + 100 exception_caught = False prefix = "YoloLogger" logger.add_logger_appender(logger.AppenderType.FILE, logger.LogLevel.INFO, path=self.log_file) logger.add_logger_appender(logger.AppenderType.TELEMETRY, logger.LogLevel.WARNING, path=add_log_event) logger.set_prefix(prefix) # Calling logger.warn no_of_log_statements times would cause the telemetry appender to writing # 1000 events into the events dir, and then drop the remaining events. It should not generate the RuntimeError try: for i in range(0, no_of_log_statements): logger.warn('Test Log - {0} - 1 - Warning'.format(i)) except RuntimeError: exception_caught = True self.assertFalse(exception_caught, msg="Caught a Runtime Error. This should not have been raised.") self.assertEqual(MAX_NUMBER_OF_EVENTS, len(os.listdir(__event_logger__.event_dir))) try: with open(self.log_file) as logfile: logcontent = logfile.readlines() # Checking the last log entry. # Subtracting 1 as range is exclusive of the upper bound self.assertIn("WARNING {1} Test Log - {0} - 1 - Warning".format(no_of_log_statements - 1, prefix), logcontent[-1]) # Checking the 1001st log entry. We know that 1001st entry would generate a PERIODIC message of too many # events, which should be captured in the log file as well. self.assertRegex(logcontent[1001], r"(.*WARNING\s*{0}\s*\[PERIODIC\]\s*Too many files under:.*{1}, " r"current count\:\s*\d+,\s*removing oldest\s*.*)".format(prefix, self.event_dir)) except Exception as e: self.assertFalse(True, "The log file looks like it isn't correctly setup for this test. " # pylint: disable=redundant-unittest-assert "Take a look. {0}".format(e)) class TestAppender(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) self.lib_dir = tempfile.mkdtemp() self.event_dir = os.path.join(self.lib_dir, EVENTS_DIRECTORY) fileutil.mkdir(self.event_dir) self.log_file = tempfile.mkstemp(prefix="logfile-")[1] logger.reset_periodic() def tearDown(self): AgentTestCase.tearDown(self) logger.reset_periodic() fileutil.rm_dirs(self.event_dir) logger.DEFAULT_LOGGER.appenders *= 0 @patch("azurelinuxagent.common.event.send_logs_to_telemetry", return_value=True) @patch("azurelinuxagent.common.logger.sys.stdout.write") @patch("azurelinuxagent.common.event.EventLogger.add_log_event") def test_no_appenders_added(self, mock_add_log_event, mock_sys_stdout, *_): # Validating no logs are written in any appender logger.verbose("test-verbose") logger.info("test-info") logger.warn("test-warn") logger.error("test-error") # Validating Console and File logs with open(self.log_file) as logfile: logcontent = logfile.readlines() self.assertEqual(0, len(logcontent)) # Validating telemetry call self.assertEqual(0, mock_add_log_event.call_count) # Validating stdout call self.assertEqual(0, mock_sys_stdout.call_count) def test_console_appender(self): logger.add_logger_appender(logger.AppenderType.CONSOLE, logger.LogLevel.WARNING, path=self.log_file) logger.verbose("test-verbose") with open(self.log_file) as logfile: logcontent = logfile.readlines() # Levels are honored and Verbose should not be written. self.assertEqual(0, len(logcontent)) logger.info("test-info") with open(self.log_file) as logfile: logcontent = logfile.readlines() # Levels are honored and Info should not be written. self.assertEqual(0, len(logcontent)) # As console has a mode of w, it'll always only have 1 line only. logger.warn("test-warn") with open(self.log_file) as logfile: logcontent = logfile.readlines() self.assertEqual(1, len(logcontent)) self.assertRegex(logcontent[0], r"(.*WARNING\s\w+\s*test-warn.*)") logger.error("test-error") with open(self.log_file) as logfile: logcontent = logfile.readlines() # Levels are honored and Info, Verbose should not be written. self.assertEqual(1, len(logcontent)) self.assertRegex(logcontent[0], r"(.*ERROR\s\w+\s*test-error.*)") def test_file_appender(self): logger.add_logger_appender(logger.AppenderType.FILE, logger.LogLevel.INFO, path=self.log_file) logger.verbose("test-verbose") logger.info("test-info") logger.warn("test-warn") logger.error("test-error") with open(self.log_file) as logfile: logcontent = logfile.readlines() # Levels are honored and Verbose should not be written. self.assertEqual(3, len(logcontent)) self.assertRegex(logcontent[0], r"(.*INFO\s\w+\s*test-info.*)") self.assertRegex(logcontent[1], r"(.*WARNING\s\w+\s*test-warn.*)") self.assertRegex(logcontent[2], r"(.*ERROR\s\w+\s*test-error.*)") @patch("azurelinuxagent.common.event.send_logs_to_telemetry", return_value=True) @patch("azurelinuxagent.common.event.EventLogger.add_log_event") def test_telemetry_appender(self, mock_add_log_event, *_): logger.add_logger_appender(logger.AppenderType.TELEMETRY, logger.LogLevel.WARNING, path=add_log_event) logger.verbose("test-verbose") logger.info("test-info") logger.warn("test-warn") logger.error("test-error") self.assertEqual(2, mock_add_log_event.call_count) @patch("azurelinuxagent.common.logger.sys.stdout.write") def test_stdout_appender(self, mock_sys_stdout): logger.add_logger_appender(logger.AppenderType.STDOUT, logger.LogLevel.ERROR) logger.verbose("test-verbose") logger.info("test-info") logger.warn("test-warn") logger.error("test-error") # Validating only test-error gets logged and not others. self.assertEqual(1, mock_sys_stdout.call_count) def test_console_output_enabled_should_return_true_when_there_are_console_appenders(self): my_logger = logger.Logger() my_logger.add_appender(logger.AppenderType.STDOUT, logger.LogLevel.INFO, None) my_logger.add_appender(logger.AppenderType.CONSOLE, logger.LogLevel.INFO, None) self.assertTrue(my_logger.console_output_enabled(), "Console output should be enabled, appenders = {0}".format(my_logger.appenders)) def test_console_output_enabled_should_return_false_when_there_are_no_console_appenders(self): my_logger = logger.Logger() my_logger.add_appender(logger.AppenderType.STDOUT, logger.LogLevel.INFO, None) self.assertFalse(my_logger.console_output_enabled(), "Console output should not be enabled, appenders = {0}".format(my_logger.appenders)) def test_disable_console_output_should_remove_all_console_appenders(self): my_logger = logger.Logger() my_logger.add_appender(logger.AppenderType.STDOUT, logger.LogLevel.INFO, None) my_logger.add_appender(logger.AppenderType.CONSOLE, logger.LogLevel.INFO, None) my_logger.add_appender(logger.AppenderType.STDOUT, logger.LogLevel.INFO, None) my_logger.add_appender(logger.AppenderType.CONSOLE, logger.LogLevel.INFO, None) my_logger.disable_console_output() self.assertTrue( len(my_logger.appenders) == 2 and all(isinstance(a, logger.StdoutAppender) for a in my_logger.appenders), "The console appender was not removed: {0}".format(my_logger.appenders)) WALinuxAgent-2.9.1.1/tests/common/test_persist_firewall_rules.py000066400000000000000000000571461446033677600250600ustar00rootroot00000000000000# Copyright 2016 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import contextlib import os import shutil import subprocess import sys import uuid import azurelinuxagent.common.conf as conf from azurelinuxagent.common.future import ustr from azurelinuxagent.common.osutil.default import DefaultOSUtil from azurelinuxagent.common.persist_firewall_rules import PersistFirewallRulesHandler from azurelinuxagent.common.utils import fileutil, shellutil from azurelinuxagent.common.utils.networkutil import AddFirewallRules, FirewallCmdDirectCommands from tests.tools import AgentTestCase, MagicMock, patch class TestPersistFirewallRulesHandler(AgentTestCase): original_popen = subprocess.Popen def __init__(self, *args, **kwargs): super(TestPersistFirewallRulesHandler, self).__init__(*args, **kwargs) self._expected_service_name = "" self._expected_service_name = "" self._binary_file = "" self._network_service_unit_file = "" def setUp(self): AgentTestCase.setUp(self) # Override for mocking Popen, should be of the form - (True/False, cmd-to-execute-if-True) self.__replace_popen_cmd = lambda *_: (False, "") self.__executed_commands = [] self.__test_dst_ip = "1.2.3.4" self.__test_uid = 9999 self.__test_wait = "-w" self.__systemd_dir = os.path.join(self.tmp_dir, "system") fileutil.mkdir(self.__systemd_dir) self.__agent_bin_dir = os.path.join(self.tmp_dir, "bin") fileutil.mkdir(self.__agent_bin_dir) self.__tmp_conf_lib = os.path.join(self.tmp_dir, "waagent") fileutil.mkdir(self.__tmp_conf_lib) conf.get_lib_dir = MagicMock(return_value=self.__tmp_conf_lib) def tearDown(self): shutil.rmtree(self.__systemd_dir, ignore_errors=True) shutil.rmtree(self.__agent_bin_dir, ignore_errors=True) shutil.rmtree(self.__tmp_conf_lib, ignore_errors=True) AgentTestCase.tearDown(self) def __mock_popen(self, cmd, *args, **kwargs): self.__executed_commands.append(cmd) replace_cmd, replace_with_command = self.__replace_popen_cmd(cmd) if replace_cmd: cmd = replace_with_command return TestPersistFirewallRulesHandler.original_popen(cmd, *args, **kwargs) @contextlib.contextmanager def _get_persist_firewall_rules_handler(self, systemd=True): osutil = DefaultOSUtil() osutil.get_firewall_will_wait = MagicMock(return_value=self.__test_wait) osutil.get_agent_bin_path = MagicMock(return_value=self.__agent_bin_dir) osutil.get_systemd_unit_file_install_path = MagicMock(return_value=self.__systemd_dir) self._expected_service_name = PersistFirewallRulesHandler._AGENT_NETWORK_SETUP_NAME_FORMAT.format( osutil.get_service_name()) self._network_service_unit_file = os.path.join(self.__systemd_dir, self._expected_service_name) self._binary_file = os.path.join(conf.get_lib_dir(), PersistFirewallRulesHandler.BINARY_FILE_NAME) # Just for these tests, ignoring the mode of mkdir to allow non-sudo tests orig_mkdir = fileutil.mkdir with patch("azurelinuxagent.common.persist_firewall_rules.fileutil.mkdir", side_effect=lambda path, **mode: orig_mkdir(path)): with patch("azurelinuxagent.common.persist_firewall_rules.get_osutil", return_value=osutil): with patch('azurelinuxagent.common.osutil.systemd.is_systemd', return_value=systemd): with patch("azurelinuxagent.common.utils.shellutil.subprocess.Popen", side_effect=self.__mock_popen): yield PersistFirewallRulesHandler(self.__test_dst_ip, self.__test_uid) def __assert_firewall_called(self, cmd, validate_command_called=True): if validate_command_called: self.assertIn(AddFirewallRules.get_wire_root_accept_rule(command=AddFirewallRules.APPEND_COMMAND, destination=self.__test_dst_ip, owner_uid=self.__test_uid, firewalld_command=cmd), self.__executed_commands, "Firewall {0} command not found".format(cmd)) self.assertIn(AddFirewallRules.get_wire_non_root_drop_rule(command=AddFirewallRules.APPEND_COMMAND, destination=self.__test_dst_ip, firewalld_command=cmd), self.__executed_commands, "Firewall {0} command not found".format(cmd)) else: self.assertNotIn(AddFirewallRules.get_wire_root_accept_rule(command=AddFirewallRules.APPEND_COMMAND, destination=self.__test_dst_ip, owner_uid=self.__test_uid, firewalld_command=cmd), self.__executed_commands, "Firewall {0} command found".format(cmd)) self.assertNotIn(AddFirewallRules.get_wire_non_root_drop_rule(command=AddFirewallRules.APPEND_COMMAND, destination=self.__test_dst_ip, firewalld_command=cmd), self.__executed_commands, "Firewall {0} command found".format(cmd)) def __assert_systemctl_called(self, cmd="enable", validate_command_called=True): systemctl_command = ["systemctl", cmd, self._expected_service_name] if validate_command_called: self.assertIn(systemctl_command, self.__executed_commands, "Systemctl command {0} not found".format(cmd)) else: self.assertNotIn(systemctl_command, self.__executed_commands, "Systemctl command {0} found".format(cmd)) def __assert_systemctl_reloaded(self, validate_command_called=True): systemctl_reload = ["systemctl", "daemon-reload"] if validate_command_called: self.assertIn(systemctl_reload, self.__executed_commands, "Systemctl config not reloaded") else: self.assertNotIn(systemctl_reload, self.__executed_commands, "Systemctl config reloaded") def __assert_firewall_cmd_running_called(self, validate_command_called=True): cmd = PersistFirewallRulesHandler._FIREWALLD_RUNNING_CMD if validate_command_called: self.assertIn(cmd, self.__executed_commands, "Firewall state not checked") else: self.assertNotIn(cmd, self.__executed_commands, "Firewall state not checked") def __assert_network_service_setup_properly(self): self.__assert_systemctl_called(cmd="is-enabled", validate_command_called=True) self.__assert_systemctl_called(cmd="enable", validate_command_called=True) self.__assert_systemctl_reloaded() self.__assert_firewall_called(cmd=FirewallCmdDirectCommands.PassThrough, validate_command_called=False) self.assertTrue(os.path.exists(self._network_service_unit_file), "Service unit file should be there") self.assertTrue(os.path.exists(self._binary_file), "Binary file should be there") @staticmethod def __mock_network_setup_service_enabled(cmd): if "firewall-cmd" in cmd: return True, ["echo", "not-running"] if "systemctl" in cmd: return True, ["echo", "enabled"] return False, [] @staticmethod def __mock_network_setup_service_disabled(cmd): if "firewall-cmd" in cmd: return True, ["echo", "not-running"] if "systemctl" in cmd: return True, ["echo", "not enabled"] return False, [] @staticmethod def __mock_firewalld_running_and_not_applied(cmd): if cmd == PersistFirewallRulesHandler._FIREWALLD_RUNNING_CMD: return True, ["echo", "running"] # This is to fail the check if firewalld-rules are already applied cmds_to_fail = ["firewall-cmd", FirewallCmdDirectCommands.QueryPassThrough, "conntrack"] if all(cmd_to_fail in cmd for cmd_to_fail in cmds_to_fail): return True, ["exit", "1"] if "firewall-cmd" in cmd: return True, ["echo", "enabled"] return False, [] @staticmethod def __mock_firewalld_running_and_remove_not_successful(cmd): if cmd == PersistFirewallRulesHandler._FIREWALLD_RUNNING_CMD: return True, ["echo", "running"] # This is to fail the check if firewalld-rules are already applied cmds_to_fail = ["firewall-cmd", FirewallCmdDirectCommands.QueryPassThrough, "conntrack"] if all(cmd_to_fail in cmd for cmd_to_fail in cmds_to_fail): return True, ["exit", "1"] # This is to fail the remove if firewalld-rules fails to remove rule cmds_to_fail = ["firewall-cmd", FirewallCmdDirectCommands.RemovePassThrough, "conntrack"] if all(cmd_to_fail in cmd for cmd_to_fail in cmds_to_fail): return True, ["exit", "2"] if "firewall-cmd" in cmd: return True, ["echo", "enabled"] return False, [] def __setup_and_assert_network_service_setup_scenario(self, handler, mock_popen=None): mock_popen = TestPersistFirewallRulesHandler.__mock_network_setup_service_disabled if mock_popen is None else mock_popen self.__replace_popen_cmd = mock_popen handler.setup() self.__assert_systemctl_called(cmd="is-enabled", validate_command_called=True) self.__assert_systemctl_called(cmd="enable", validate_command_called=True) self.__assert_systemctl_reloaded(validate_command_called=True) self.__assert_firewall_cmd_running_called(validate_command_called=True) self.__assert_firewall_called(cmd=FirewallCmdDirectCommands.QueryPassThrough, validate_command_called=False) self.__assert_firewall_called(cmd=FirewallCmdDirectCommands.RemovePassThrough, validate_command_called=False) self.__assert_firewall_called(cmd=FirewallCmdDirectCommands.PassThrough, validate_command_called=False) self.assertTrue(os.path.exists(handler.get_service_file_path()), "Service unit file not found") def test_it_should_skip_setup_if_firewalld_already_enabled(self): self.__replace_popen_cmd = lambda cmd: ("firewall-cmd" in cmd, ["echo", "running"]) with self._get_persist_firewall_rules_handler() as handler: handler.setup() # Assert we verified that rules were set using firewall-cmd self.__assert_firewall_called(cmd=FirewallCmdDirectCommands.QueryPassThrough, validate_command_called=True) # Assert no commands for adding rules using firewall-cmd were called self.__assert_firewall_called(cmd=FirewallCmdDirectCommands.RemovePassThrough, validate_command_called=False) self.__assert_firewall_called(cmd=FirewallCmdDirectCommands.PassThrough, validate_command_called=False) # Assert no commands for systemctl were called self.assertFalse(any("systemctl" in cmd for cmd in self.__executed_commands), "Systemctl shouldn't be called") def test_it_should_skip_setup_if_agent_network_setup_service_already_enabled_and_version_same(self): with self._get_persist_firewall_rules_handler() as handler: # 1st time should setup the service self.__setup_and_assert_network_service_setup_scenario(handler) # 2nd time setup should do nothing as service is enabled and no version updated self.__replace_popen_cmd = TestPersistFirewallRulesHandler.__mock_network_setup_service_enabled # Reset state self.__executed_commands = [] handler.setup() self.__assert_systemctl_called(cmd="is-enabled", validate_command_called=True) self.__assert_systemctl_called(cmd="enable", validate_command_called=False) self.__assert_systemctl_reloaded(validate_command_called=False) self.__assert_firewall_cmd_running_called(validate_command_called=True) self.__assert_firewall_called(cmd=FirewallCmdDirectCommands.QueryPassThrough, validate_command_called=False) self.__assert_firewall_called(cmd=FirewallCmdDirectCommands.RemovePassThrough, validate_command_called=False) self.__assert_firewall_called(cmd=FirewallCmdDirectCommands.PassThrough, validate_command_called=False) self.assertTrue(os.path.exists(handler.get_service_file_path()), "Service unit file not found") def test_it_should_always_replace_binary_file_only_if_using_custom_network_service(self): def _find_in_file(file_name, line_str): try: with open(file_name, 'r') as fh: content = fh.read() return line_str in content except Exception: # swallow exception pass return False test_str = 'os.system("{py_path} {egg_path} --setup-firewall --dst_ip={wire_ip} --uid={user_id} {wait}")' current_exe_path = os.path.join(os.getcwd(), sys.argv[0]) self.__replace_popen_cmd = TestPersistFirewallRulesHandler.__mock_network_setup_service_disabled with self._get_persist_firewall_rules_handler() as handler: self.assertFalse(os.path.exists(self._binary_file), "Binary file should not be there") self.assertFalse(os.path.exists(self._network_service_unit_file), "Unit file should not be present") handler.setup() orig_service_file_contents = "ExecStart={py_path} {binary_path}".format(py_path=sys.executable, binary_path=self._binary_file) self.__assert_systemctl_called(cmd="is-enabled", validate_command_called=True) self.__assert_firewall_called(cmd=FirewallCmdDirectCommands.PassThrough, validate_command_called=False) self.assertTrue(os.path.exists(self._binary_file), "Binary file should be there") self.assertTrue(_find_in_file(self._binary_file, test_str.format(py_path=sys.executable, egg_path=current_exe_path, wire_ip=self.__test_dst_ip, user_id=self.__test_uid, wait=self.__test_wait)), "Binary file not set correctly") self.assertTrue(_find_in_file(self._network_service_unit_file, orig_service_file_contents), "Service Unit file file not set correctly") # Change test params self.__test_dst_ip = "9.8.7.6" self.__test_uid = 5555 self.__test_wait = "" # The service should say its enabled now self.__replace_popen_cmd = TestPersistFirewallRulesHandler.__mock_network_setup_service_enabled with self._get_persist_firewall_rules_handler() as handler: # The Binary file should be available on the 2nd run self.assertTrue(os.path.exists(self._binary_file), "Binary file should be there") handler.setup() self.assertTrue(_find_in_file(self._binary_file, test_str.format(py_path=sys.executable, egg_path=current_exe_path, wire_ip=self.__test_dst_ip, user_id=self.__test_uid, wait=self.__test_wait)), "Binary file not updated correctly") # Unit file should NOT be updated self.assertTrue(_find_in_file(self._network_service_unit_file, orig_service_file_contents), "Service Unit file file should not be updated") def test_it_should_use_firewalld_if_available(self): self.__replace_popen_cmd = self.__mock_firewalld_running_and_not_applied with self._get_persist_firewall_rules_handler() as handler: handler.setup() self.__assert_firewall_cmd_running_called(validate_command_called=True) self.__assert_firewall_called(cmd=FirewallCmdDirectCommands.QueryPassThrough, validate_command_called=True) self.__assert_firewall_called(cmd=FirewallCmdDirectCommands.RemovePassThrough, validate_command_called=True) self.__assert_firewall_called(cmd=FirewallCmdDirectCommands.PassThrough, validate_command_called=True) self.assertFalse(any("systemctl" in cmd for cmd in self.__executed_commands), "Systemctl shouldn't be called") def test_it_should_add_firewalld_rules_if_remove_raises_exception(self): self.__replace_popen_cmd = self.__mock_firewalld_running_and_remove_not_successful with self._get_persist_firewall_rules_handler() as handler: handler.setup() self.__assert_firewall_cmd_running_called(validate_command_called=True) self.__assert_firewall_called(cmd=FirewallCmdDirectCommands.QueryPassThrough, validate_command_called=True) self.__assert_firewall_called(cmd=FirewallCmdDirectCommands.RemovePassThrough, validate_command_called=True) self.__assert_firewall_called(cmd=FirewallCmdDirectCommands.PassThrough, validate_command_called=True) self.assertFalse(any("systemctl" in cmd for cmd in self.__executed_commands), "Systemctl shouldn't be called") def test_it_should_set_up_custom_service_if_no_firewalld(self): self.__replace_popen_cmd = TestPersistFirewallRulesHandler.__mock_network_setup_service_disabled with self._get_persist_firewall_rules_handler() as handler: self.assertFalse(os.path.exists(self._network_service_unit_file), "Service unit file should not be there") self.assertFalse(os.path.exists(self._binary_file), "Binary file should not be there") handler.setup() self.__assert_network_service_setup_properly() def test_it_should_cleanup_files_on_error(self): orig_write_file = fileutil.write_file files_to_fail = [] def mock_write_file(path, _, *__): if files_to_fail[0] in path: raise IOError("Invalid file: {0}".format(path)) return orig_write_file(path, _, *__) self.__replace_popen_cmd = TestPersistFirewallRulesHandler.__mock_network_setup_service_disabled with self._get_persist_firewall_rules_handler() as handler: test_files = [self._binary_file, self._network_service_unit_file] for file_to_fail in test_files: files_to_fail = [file_to_fail] with patch("azurelinuxagent.common.persist_firewall_rules.fileutil.write_file", side_effect=mock_write_file): with self.assertRaises(Exception) as context_manager: handler.setup() self.assertIn("Invalid file: {0}".format(file_to_fail), ustr(context_manager.exception)) self.assertFalse(os.path.exists(file_to_fail), "File should be deleted: {0}".format(file_to_fail)) # Cleanup remaining files for test clarity for test_file in test_files: try: os.remove(test_file) except Exception: pass def test_it_should_execute_binary_file_successfully(self): # A bare-bone test to ensure no simple syntactical errors in the binary file as its generated dynamically self.__replace_popen_cmd = TestPersistFirewallRulesHandler.__mock_network_setup_service_disabled with self._get_persist_firewall_rules_handler() as handler: self.assertFalse(os.path.exists(self._binary_file), "Binary file should not be there") handler.setup() self.assertTrue(os.path.exists(self._binary_file), "Binary file not set properly") shellutil.run_command([sys.executable, self._binary_file]) def test_it_should_not_fail_if_egg_not_found(self): self.__replace_popen_cmd = TestPersistFirewallRulesHandler.__mock_network_setup_service_disabled test_str = str(uuid.uuid4()) with patch("sys.argv", [test_str]): with self._get_persist_firewall_rules_handler() as handler: self.assertFalse(os.path.exists(self._binary_file), "Binary file should not be there") handler.setup() output = shellutil.run_command([sys.executable, self._binary_file], stderr=subprocess.STDOUT) expected_str = "{0} file not found, skipping execution of firewall execution setup for this boot".format( os.path.join(os.getcwd(), test_str)) self.assertIn(expected_str, output, "Unexpected output") def test_it_should_delete_custom_service_files_if_firewalld_enabled(self): with self._get_persist_firewall_rules_handler() as handler: # 1st run - Setup the Custom Service self.__setup_and_assert_network_service_setup_scenario(handler) # 2nd run - Enable Firewalld and ensure the agent sets firewall rules using firewalld and deletes custom service self.__executed_commands = [] self.__replace_popen_cmd = self.__mock_firewalld_running_and_not_applied handler.setup() self.__assert_firewall_cmd_running_called(validate_command_called=True) self.__assert_firewall_called(cmd=FirewallCmdDirectCommands.QueryPassThrough, validate_command_called=True) self.__assert_firewall_called(cmd=FirewallCmdDirectCommands.RemovePassThrough, validate_command_called=True) self.__assert_firewall_called(cmd=FirewallCmdDirectCommands.PassThrough, validate_command_called=True) self.__assert_systemctl_called(cmd="is-enabled", validate_command_called=False) self.__assert_systemctl_called(cmd="enable", validate_command_called=False) self.__assert_systemctl_reloaded(validate_command_called=False) self.assertFalse(os.path.exists(handler.get_service_file_path()), "Service unit file found") self.assertFalse(os.path.exists(os.path.join(conf.get_lib_dir(), handler.BINARY_FILE_NAME)), "Binary file found") def test_it_should_reset_service_unit_files_if_version_changed(self): with self._get_persist_firewall_rules_handler() as handler: # 1st step - Setup the service with old Version test_ver = str(uuid.uuid4()) with patch.object(handler, "_UNIT_VERSION", test_ver): self.__setup_and_assert_network_service_setup_scenario(handler) self.assertIn(test_ver, fileutil.read_file(handler.get_service_file_path()), "Test version not found") # 2nd step - Re-run the setup and ensure the service file set up again even if service enabled self.__executed_commands = [] self.__setup_and_assert_network_service_setup_scenario(handler, mock_popen=self.__mock_network_setup_service_enabled) self.assertNotIn(test_ver, fileutil.read_file(handler.get_service_file_path()), "Test version found incorrectly") WALinuxAgent-2.9.1.1/tests/common/test_singletonperthread.py000066400000000000000000000154571446033677600241700ustar00rootroot00000000000000import uuid from multiprocessing import Queue from threading import Thread, currentThread from azurelinuxagent.common.singletonperthread import SingletonPerThread from tests.tools import AgentTestCase, clear_singleton_instances class TestClassToTestSingletonPerThread(SingletonPerThread): """ Since these tests deal with testing in a multithreaded environment, we employ the use of multiprocessing.Queue() to ensure that the data is consistent. This test class uses a uuid to identify an object instead of directly using object reference because Queue.get() returns a different object reference than what is put in it even though the object is same (which is verified using uuid in this test class) Eg: obj1 = WireClient("obj1") obj1 <__main__.WireClient object at 0x7f5e78476198> q = Queue() q.put(obj1) test1 = q.get() test1 <__main__.WireClient object at 0x7f5e78430630> test1.endpoint == obj1.endpoint True """ def __init__(self): # Set the name of the object to the current thread name self.name = currentThread().getName() # Unique identifier for a class object self.uuid = str(uuid.uuid4()) class TestSingletonPerThread(AgentTestCase): THREAD_NAME_1 = 'thread-1' THREAD_NAME_2 = 'thread-2' def setUp(self): super(TestSingletonPerThread, self).setUp() # In a multi-threaded environment, exceptions thrown in the child thread will not be propagated to the parent # thread. In order to achieve that, adding all exceptions to a Queue and then checking that in parent thread. self.errors = Queue() clear_singleton_instances(TestClassToTestSingletonPerThread) def _setup_multithread_and_execute(self, func1, args1, func2, args2, t1_name=None, t2_name=None): t1 = Thread(target=func1, args=args1) t2 = Thread(target=func2, args=args2) t1.setName(t1_name if t1_name else self.THREAD_NAME_1) t2.setName(t2_name if t2_name else self.THREAD_NAME_2) t1.start() t2.start() t1.join() t2.join() errs = [] while not self.errors.empty(): errs.append(self.errors.get()) if len(errs) > 0: raise Exception("Errors: %s" % ' , '.join(errs)) @staticmethod def _get_test_class_instance(q, err): try: obj = TestClassToTestSingletonPerThread() q.put(obj) except Exception as e: err.put(str(e)) def _parse_instances_and_return_thread_objects(self, instances, t1_name=None, t2_name=None): obj1, obj2 = instances.get(), instances.get() def check_obj(name): if obj1.name == name: return obj1 elif obj2.name == name: return obj2 else: return None t1_object = check_obj(t1_name if t1_name else self.THREAD_NAME_1) t2_object = check_obj(t2_name if t2_name else self.THREAD_NAME_2) return t1_object, t2_object def test_it_should_have_only_one_instance_for_same_thread(self): obj1 = TestClassToTestSingletonPerThread() obj2 = TestClassToTestSingletonPerThread() self.assertEqual(obj1.uuid, obj2.uuid) def test_it_should_have_multiple_instances_for_multiple_threads(self): instances = Queue() self._setup_multithread_and_execute(func1=self._get_test_class_instance, args1=(instances, self.errors), func2=self._get_test_class_instance, args2=(instances, self.errors)) self.assertEqual(2, instances.qsize()) # Assert that there are 2 objects in the queue obj1, obj2 = instances.get(), instances.get() self.assertNotEqual(obj1.uuid, obj2.uuid) def test_it_should_return_existing_instance_for_new_thread_with_same_name(self): instances = Queue() self._setup_multithread_and_execute(func1=self._get_test_class_instance, args1=(instances, self.errors), func2=self._get_test_class_instance, args2=(instances, self.errors)) t1_obj, t2_obj = self._parse_instances_and_return_thread_objects(instances) new_instances = Queue() # The 2nd call is to get new objects with the same thread name to verify if the objects are same self._setup_multithread_and_execute(func1=self._get_test_class_instance, args1=(new_instances, self.errors), func2=self._get_test_class_instance, args2=(new_instances, self.errors)) new_t1_obj, new_t2_obj = self._parse_instances_and_return_thread_objects(new_instances) self.assertEqual(t1_obj.name, new_t1_obj.name) self.assertEqual(t1_obj.uuid, new_t1_obj.uuid) self.assertEqual(t2_obj.name, new_t2_obj.name) self.assertEqual(t2_obj.uuid, new_t2_obj.uuid) def test_singleton_object_should_match_thread_name(self): instances = Queue() t1_name = str(uuid.uuid4()) t2_name = str(uuid.uuid4()) test_class_obj_name = lambda t_name: "%s__%s" % (TestClassToTestSingletonPerThread.__name__, t_name) self._setup_multithread_and_execute(func1=self._get_test_class_instance, args1=(instances, self.errors), func2=self._get_test_class_instance, args2=(instances, self.errors), t1_name=t1_name, t2_name=t2_name) singleton_instances = TestClassToTestSingletonPerThread._instances # pylint: disable=no-member # Assert instance names are consistent with the thread names self.assertIn(test_class_obj_name(t1_name), singleton_instances) self.assertIn(test_class_obj_name(t2_name), singleton_instances) # Assert that the objects match their respective threads # This function matches objects with their thread names and returns the respective object or None if not found t1_obj, t2_obj = self._parse_instances_and_return_thread_objects(instances, t1_name, t2_name) # Ensure that objects for both the threads were found self.assertIsNotNone(t1_obj) self.assertIsNotNone(t2_obj) # Ensure that the objects match with their respective thread objects self.assertEqual(singleton_instances[test_class_obj_name(t1_name)].uuid, t1_obj.uuid) self.assertEqual(singleton_instances[test_class_obj_name(t2_name)].uuid, t2_obj.uuid) WALinuxAgent-2.9.1.1/tests/common/test_telemetryevent.py000066400000000000000000000055041446033677600233330ustar00rootroot00000000000000# Copyright 2019 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # from azurelinuxagent.common.telemetryevent import TelemetryEvent, TelemetryEventParam, GuestAgentExtensionEventsSchema, \ CommonTelemetryEventSchema from tests.tools import AgentTestCase def get_test_event(name="DummyExtension", op="Unknown", is_success=True, duration=0, version="foo", evt_type="", is_internal=False, message="DummyMessage", eventId=1): event = TelemetryEvent(eventId, "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX") event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.Name, name)) event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.Version, str(version))) event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.IsInternal, is_internal)) event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.Operation, op)) event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.OperationSuccess, is_success)) event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.Message, message)) event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.Duration, duration)) event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.ExtensionType, evt_type)) return event class TestTelemetryEvent(AgentTestCase): def test_contains_works_for_TelemetryEvent(self): test_event = get_test_event(message="Dummy Event") self.assertTrue(GuestAgentExtensionEventsSchema.Name in test_event) self.assertTrue(GuestAgentExtensionEventsSchema.Version in test_event) self.assertTrue(GuestAgentExtensionEventsSchema.IsInternal in test_event) self.assertTrue(GuestAgentExtensionEventsSchema.Operation in test_event) self.assertTrue(GuestAgentExtensionEventsSchema.OperationSuccess in test_event) self.assertTrue(GuestAgentExtensionEventsSchema.Message in test_event) self.assertTrue(GuestAgentExtensionEventsSchema.Duration in test_event) self.assertTrue(GuestAgentExtensionEventsSchema.ExtensionType in test_event) self.assertFalse(CommonTelemetryEventSchema.GAVersion in test_event) self.assertFalse(CommonTelemetryEventSchema.ContainerId in test_event)WALinuxAgent-2.9.1.1/tests/common/test_version.py000066400000000000000000000247621446033677600217530ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # from __future__ import print_function import os import textwrap import mock import azurelinuxagent.common.conf as conf from azurelinuxagent.common.future import ustr from azurelinuxagent.common.event import EVENTS_DIRECTORY from azurelinuxagent.common.version import set_current_agent, \ AGENT_LONG_VERSION, AGENT_VERSION, AGENT_NAME, AGENT_NAME_PATTERN, \ get_f5_platform, get_distro, get_lis_version, PY_VERSION_MAJOR, \ PY_VERSION_MINOR, get_daemon_version, set_daemon_version, __DAEMON_VERSION_ENV_VARIABLE as DAEMON_VERSION_ENV_VARIABLE from azurelinuxagent.common.utils.flexible_version import FlexibleVersion from tests.tools import AgentTestCase, open_patch, patch def freebsd_system(): return ["FreeBSD"] def freebsd_system_release(x, y, z): # pylint: disable=unused-argument return "10.0" def openbsd_system(): return ["OpenBSD"] def openbsd_system_release(x, y, z): # pylint: disable=unused-argument return "20.0" def default_system(): return [""] def default_system_no_linux_distro(): return '', '', '' def default_system_exception(): raise Exception def is_platform_dist_supported(): # platform.dist() and platform.linux_distribution() is deprecated from Python 3.8+ if PY_VERSION_MAJOR == 3 and PY_VERSION_MINOR >= 8: return False return True class TestAgentVersion(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) @mock.patch('platform.system', side_effect=freebsd_system) @mock.patch('re.sub', side_effect=freebsd_system_release) def test_distro_is_correct_format_when_freebsd(self, platform_system_name, mock_variable): # pylint: disable=unused-argument osinfo = get_distro() freebsd_list = ['freebsd', "10.0", '', 'freebsd'] self.assertListEqual(freebsd_list, osinfo) @mock.patch('platform.system', side_effect=openbsd_system) @mock.patch('re.sub', side_effect=openbsd_system_release) def test_distro_is_correct_format_when_openbsd(self, platform_system_name, mock_variable): # pylint: disable=unused-argument osinfo = get_distro() openbsd_list = ['openbsd', "20.0", '', 'openbsd'] self.assertListEqual(openbsd_list, osinfo) @mock.patch('platform.system', side_effect=default_system) def test_distro_is_correct_format_when_default_case(self, *args): # pylint: disable=unused-argument default_list = ['', '', '', ''] unknown_list = ['unknown', 'FFFF', '', ''] if is_platform_dist_supported(): with patch('platform.dist', side_effect=default_system_no_linux_distro): osinfo = get_distro() self.assertListEqual(default_list, osinfo) else: # platform.dist() is deprecated in Python 3.7+ and would throw, resulting in unknown distro osinfo = get_distro() self.assertListEqual(unknown_list, osinfo) @mock.patch('platform.system', side_effect=default_system) def test_distro_is_correct_for_exception_case(self, *args): # pylint: disable=unused-argument default_list = ['unknown', 'FFFF', '', ''] if is_platform_dist_supported(): with patch('platform.dist', side_effect=default_system_exception): osinfo = get_distro() else: # platform.dist() is deprecated in Python 3.7+ so we can't patch it, but it would throw # as well, resulting in the same unknown distro osinfo = get_distro() self.assertListEqual(default_list, osinfo) def test_get_lis_version_should_return_a_string(self): """ On a Hyper-V guest with the LIS drivers installed as a module, this function should return a string of the version, like '4.3.5'. Anywhere else it should return 'Absent' and possibly return 'Failed' if an exception was raised, so we check that it returns a string'. """ lis_version = get_lis_version() self.assertIsInstance(lis_version, ustr) def test_get_daemon_version_should_return_the_version_that_was_previously_set(self): set_daemon_version("1.2.3.4") try: self.assertEqual( FlexibleVersion("1.2.3.4"), get_daemon_version(), "The daemon version should be 1.2.3.4. Environment={0}".format(os.environ) ) finally: os.environ.pop(DAEMON_VERSION_ENV_VARIABLE) def test_get_daemon_version_should_return_zero_when_the_version_has_not_been_set(self): self.assertEqual( FlexibleVersion("0.0.0.0"), get_daemon_version(), "The daemon version should not be defined. Environment={0}".format(os.environ) ) class TestCurrentAgentName(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) @patch("os.getcwd", return_value="/default/install/directory") def test_extract_name_finds_installed(self, mock_cwd): # pylint: disable=unused-argument current_agent, current_version = set_current_agent() self.assertEqual(AGENT_LONG_VERSION, current_agent) self.assertEqual(AGENT_VERSION, str(current_version)) @patch("os.getcwd", return_value="/") def test_extract_name_root_finds_installed(self, mock_cwd): # pylint: disable=unused-argument current_agent, current_version = set_current_agent() self.assertEqual(AGENT_LONG_VERSION, current_agent) self.assertEqual(AGENT_VERSION, str(current_version)) @patch("os.getcwd") def test_extract_name_in_path_finds_installed(self, mock_cwd): path = os.path.join(conf.get_lib_dir(), EVENTS_DIRECTORY) mock_cwd.return_value = path current_agent, current_version = set_current_agent() self.assertEqual(AGENT_LONG_VERSION, current_agent) self.assertEqual(AGENT_VERSION, str(current_version)) @patch("os.getcwd") def test_extract_name_finds_latest_agent(self, mock_cwd): path = os.path.join(conf.get_lib_dir(), "{0}-{1}".format( AGENT_NAME, "1.2.3")) mock_cwd.return_value = path agent = os.path.basename(path) version = AGENT_NAME_PATTERN.match(agent).group(1) current_agent, current_version = set_current_agent() self.assertEqual(agent, current_agent) self.assertEqual(version, str(current_version)) class TestGetF5Platforms(AgentTestCase): def test_get_f5_platform_bigip_12_1_1(self): version_file = textwrap.dedent(""" Product: BIG-IP Version: 12.1.1 Build: 0.0.184 Sequence: 12.1.1.0.0.184.0 BaseBuild: 0.0.184 Edition: Final Date: Thu Aug 11 17:09:01 PDT 2016 Built: 160811170901 Changelist: 1874858 JobID: 705993""") mocked_open = mock.mock_open(read_data=version_file) with patch(open_patch(), mocked_open): platform = get_f5_platform() self.assertTrue(platform[0] == 'bigip') self.assertTrue(platform[1] == '12.1.1') self.assertTrue(platform[2] == 'bigip') self.assertTrue(platform[3] == 'BIG-IP') def test_get_f5_platform_bigip_12_1_0_hf1(self): version_file = textwrap.dedent(""" Product: BIG-IP Version: 12.1.0 Build: 1.0.1447 Sequence: 12.1.0.1.0.1447.0 BaseBuild: 0.0.1434 Edition: Hotfix HF1 Date: Wed Jun 8 13:41:59 PDT 2016 Built: 160608134159 Changelist: 1773831 JobID: 673467""") mocked_open = mock.mock_open(read_data=version_file) with patch(open_patch(), mocked_open): platform = get_f5_platform() self.assertTrue(platform[0] == 'bigip') self.assertTrue(platform[1] == '12.1.0') self.assertTrue(platform[2] == 'bigip') self.assertTrue(platform[3] == 'BIG-IP') def test_get_f5_platform_bigip_12_0_0(self): version_file = textwrap.dedent(""" Product: BIG-IP Version: 12.0.0 Build: 0.0.606 Sequence: 12.0.0.0.0.606.0 BaseBuild: 0.0.606 Edition: Final Date: Fri Aug 21 13:29:22 PDT 2015 Built: 150821132922 Changelist: 1486072 JobID: 536212""") mocked_open = mock.mock_open(read_data=version_file) with patch(open_patch(), mocked_open): platform = get_f5_platform() self.assertTrue(platform[0] == 'bigip') self.assertTrue(platform[1] == '12.0.0') self.assertTrue(platform[2] == 'bigip') self.assertTrue(platform[3] == 'BIG-IP') def test_get_f5_platform_iworkflow_2_0_1(self): version_file = textwrap.dedent(""" Product: iWorkflow Version: 2.0.1 Build: 0.0.9842 Sequence: 2.0.1.0.0.9842.0 BaseBuild: 0.0.9842 Edition: Final Date: Sat Oct 1 22:52:08 PDT 2016 Built: 161001225208 Changelist: 1924048 JobID: 734712""") mocked_open = mock.mock_open(read_data=version_file) with patch(open_patch(), mocked_open): platform = get_f5_platform() self.assertTrue(platform[0] == 'iworkflow') self.assertTrue(platform[1] == '2.0.1') self.assertTrue(platform[2] == 'iworkflow') self.assertTrue(platform[3] == 'iWorkflow') def test_get_f5_platform_bigiq_5_1_0(self): version_file = textwrap.dedent(""" Product: BIG-IQ Version: 5.1.0 Build: 0.0.631 Sequence: 5.1.0.0.0.631.0 BaseBuild: 0.0.631 Edition: Final Date: Thu Sep 15 19:55:43 PDT 2016 Built: 160915195543 Changelist: 1907534 JobID: 726344""") mocked_open = mock.mock_open(read_data=version_file) with patch(open_patch(), mocked_open): platform = get_f5_platform() self.assertTrue(platform[0] == 'bigiq') self.assertTrue(platform[1] == '5.1.0') self.assertTrue(platform[2] == 'bigiq') self.assertTrue(platform[3] == 'BIG-IQ') WALinuxAgent-2.9.1.1/tests/daemon/000077500000000000000000000000001446033677600166155ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/daemon/__init__.py000066400000000000000000000011651446033677600207310ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # WALinuxAgent-2.9.1.1/tests/daemon/test_daemon.py000066400000000000000000000134301446033677600214720ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import unittest from multiprocessing import Process import azurelinuxagent.common.conf as conf from azurelinuxagent.daemon.main import OPENSSL_FIPS_ENVIRONMENT, get_daemon_handler from azurelinuxagent.pa.provision.default import ProvisionHandler from tests.tools import AgentTestCase, Mock, patch class MockDaemonCall(object): def __init__(self, daemon_handler, count): self.daemon_handler = daemon_handler self.count = count def __call__(self, *args, **kw): self.count = self.count - 1 # Stop daemon after restarting for n times if self.count <= 0: self.daemon_handler.running = False raise Exception("Mock unhandled exception") class TestDaemon(AgentTestCase): @patch("time.sleep") def test_daemon_restart(self, mock_sleep): # Mock daemon function daemon_handler = get_daemon_handler() mock_daemon = Mock(side_effect=MockDaemonCall(daemon_handler, 2)) daemon_handler.daemon = mock_daemon daemon_handler.check_pid = Mock() daemon_handler.run() mock_sleep.assert_any_call(15) self.assertEqual(2, daemon_handler.daemon.call_count) @patch("time.sleep") @patch("azurelinuxagent.daemon.main.conf") @patch("azurelinuxagent.daemon.main.sys.exit") def test_check_pid(self, mock_exit, mock_conf, _): daemon_handler = get_daemon_handler() mock_pid_file = os.path.join(self.tmp_dir, "pid") mock_conf.get_agent_pid_file_path = Mock(return_value=mock_pid_file) daemon_handler.check_pid() self.assertTrue(os.path.isfile(mock_pid_file)) daemon_handler.check_pid() mock_exit.assert_any_call(0) @patch("azurelinuxagent.daemon.main.DaemonHandler.check_pid") @patch("azurelinuxagent.common.conf.get_fips_enabled", return_value=True) def test_set_openssl_fips(self, _, __): daemon_handler = get_daemon_handler() daemon_handler.running = False with patch.dict("os.environ"): daemon_handler.run() self.assertTrue(OPENSSL_FIPS_ENVIRONMENT in os.environ) self.assertEqual('1', os.environ[OPENSSL_FIPS_ENVIRONMENT]) @patch("azurelinuxagent.daemon.main.DaemonHandler.check_pid") @patch("azurelinuxagent.common.conf.get_fips_enabled", return_value=False) def test_does_not_set_openssl_fips(self, _, __): daemon_handler = get_daemon_handler() daemon_handler.running = False with patch.dict("os.environ"): daemon_handler.run() self.assertFalse(OPENSSL_FIPS_ENVIRONMENT in os.environ) @patch('azurelinuxagent.common.conf.get_provisioning_agent', return_value='waagent') @patch('azurelinuxagent.ga.update.UpdateHandler.run_latest') @patch('azurelinuxagent.pa.provision.default.ProvisionHandler.run') def test_daemon_agent_enabled(self, patch_run_provision, patch_run_latest, gpa): # pylint: disable=unused-argument """ Agent should run normally when no disable_agent is found """ with patch('azurelinuxagent.pa.provision.get_provision_handler', return_value=ProvisionHandler()): # DaemonHandler._initialize_telemetry requires communication with WireServer and IMDS; since we # are not using telemetry in this test we mock it out with patch('azurelinuxagent.daemon.main.DaemonHandler._initialize_telemetry'): self.assertFalse(os.path.exists(conf.get_disable_agent_file_path())) daemon_handler = get_daemon_handler() def stop_daemon(child_args): # pylint: disable=unused-argument daemon_handler.running = False patch_run_latest.side_effect = stop_daemon daemon_handler.run() self.assertEqual(1, patch_run_provision.call_count) self.assertEqual(1, patch_run_latest.call_count) @patch('azurelinuxagent.common.conf.get_provisioning_agent', return_value='waagent') @patch('azurelinuxagent.ga.update.UpdateHandler.run_latest', side_effect=AgentTestCase.fail) @patch('azurelinuxagent.pa.provision.default.ProvisionHandler.run', side_effect=ProvisionHandler.write_agent_disabled) def test_daemon_agent_disabled(self, _, patch_run_latest, gpa): # pylint: disable=unused-argument """ Agent should provision, then sleep forever when disable_agent is found """ with patch('azurelinuxagent.pa.provision.get_provision_handler', return_value=ProvisionHandler()): # file is created by provisioning handler self.assertFalse(os.path.exists(conf.get_disable_agent_file_path())) daemon_handler = get_daemon_handler() # we need to assert this thread will sleep forever, so fork it daemon = Process(target=daemon_handler.run) daemon.start() daemon.join(timeout=5) self.assertTrue(daemon.is_alive()) daemon.terminate() # disable_agent was written, run_latest was not called self.assertTrue(os.path.exists(conf.get_disable_agent_file_path())) self.assertEqual(0, patch_run_latest.call_count) if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/daemon/test_resourcedisk.py000066400000000000000000000064461446033677600227420ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import unittest from tests.tools import AgentTestCase, patch, DEFAULT from azurelinuxagent.daemon.resourcedisk.default import ResourceDiskHandler class TestResourceDisk(AgentTestCase): def test_mount_flags_empty(self): partition = '/dev/sdb1' mountpoint = '/mnt/resource' options = None expected = 'mount -t ext3 /dev/sdb1 /mnt/resource' rdh = ResourceDiskHandler() mount_string = rdh.get_mount_string(options, partition, mountpoint) self.assertEqual(expected, mount_string) def test_mount_flags_many(self): partition = '/dev/sdb1' mountpoint = '/mnt/resource' options = 'noexec,noguid,nodev' expected = 'mount -t ext3 -o noexec,noguid,nodev /dev/sdb1 /mnt/resource' rdh = ResourceDiskHandler() mount_string = rdh.get_mount_string(options, partition, mountpoint) self.assertEqual(expected, mount_string) @patch('azurelinuxagent.common.utils.shellutil.run_get_output') @patch('azurelinuxagent.common.utils.shellutil.run') @patch('azurelinuxagent.daemon.resourcedisk.default.ResourceDiskHandler.mkfile') @patch('azurelinuxagent.daemon.resourcedisk.default.os.path.isfile', return_value=False) @patch( 'azurelinuxagent.daemon.resourcedisk.default.ResourceDiskHandler.check_existing_swap_file', return_value=False) def test_create_swap_space( self, mock_check_existing_swap_file, # pylint: disable=unused-argument mock_isfile, # pylint: disable=unused-argument mock_mkfile, # pylint: disable=unused-argument mock_run, mock_run_get_output): mount_point = '/mnt/resource' size_mb = 128 rdh = ResourceDiskHandler() def rgo_side_effect(*args, **kwargs): # pylint: disable=unused-argument if args[0] == 'swapon -s': return (0, 'Filename\t\t\t\tType\t\tSize\tUsed\tPriority\n/mnt/resource/swapfile \tfile \t131068\t0\t-2\n') return DEFAULT def run_side_effect(*args, **kwargs): # pylint: disable=unused-argument # We have to change the default mock behavior to return a falsey value # (instead of the default truthy of the mock), because we are testing # really for the exit code of the the swapon command to return 0. if 'swapon' in args[0]: return 0 return None mock_run_get_output.side_effect = rgo_side_effect mock_run.side_effect = run_side_effect rdh.create_swap_space( mount_point=mount_point, size_mb=size_mb ) if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/data/000077500000000000000000000000001446033677600162635ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/cgroups/000077500000000000000000000000001446033677600177455ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/cgroups/cpu.stat000066400000000000000000000000561446033677600214320ustar00rootroot00000000000000nr_periods 1 nr_throttled 1 throttled_time 50 WALinuxAgent-2.9.1.1/tests/data/cgroups/cpu.stat_t0000066400000000000000000000000561446033677600220350ustar00rootroot00000000000000nr_periods 1 nr_throttled 1 throttled_time 50 WALinuxAgent-2.9.1.1/tests/data/cgroups/cpu.stat_t1000066400000000000000000000001011446033677600220250ustar00rootroot00000000000000nr_periods 66927 nr_throttled 25803 throttled_time 2075541442327 WALinuxAgent-2.9.1.1/tests/data/cgroups/cpu_mount/000077500000000000000000000000001446033677600217565ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/cgroups/cpu_mount/cpuacct.stat000066400000000000000000000000311446033677600242670ustar00rootroot00000000000000user 50000 system 100000 WALinuxAgent-2.9.1.1/tests/data/cgroups/cpuacct.stat000066400000000000000000000000301446033677600222550ustar00rootroot00000000000000user 42380 system 21383 WALinuxAgent-2.9.1.1/tests/data/cgroups/cpuacct.stat_t0000066400000000000000000000000301446033677600226600ustar00rootroot00000000000000user 42380 system 21383 WALinuxAgent-2.9.1.1/tests/data/cgroups/cpuacct.stat_t1000066400000000000000000000000301446033677600226610ustar00rootroot00000000000000user 42390 system 21390 WALinuxAgent-2.9.1.1/tests/data/cgroups/cpuacct.stat_t2000066400000000000000000000000301446033677600226620ustar00rootroot00000000000000user 42417 system 21428 WALinuxAgent-2.9.1.1/tests/data/cgroups/dummy_proc_cmdline000066400000000000000000000000751446033677600235430ustar00rootroot00000000000000python-ubin/WALinuxAgent-2.2.45-py2.7.egg-run-exthandlersWALinuxAgent-2.9.1.1/tests/data/cgroups/dummy_proc_comm000066400000000000000000000000061446033677600230550ustar00rootroot00000000000000pythonWALinuxAgent-2.9.1.1/tests/data/cgroups/dummy_proc_statm000066400000000000000000000000361446033677600232550ustar00rootroot00000000000000980608 81022 30304 4 0 93606 0WALinuxAgent-2.9.1.1/tests/data/cgroups/memory_mount/000077500000000000000000000000001446033677600224775ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/cgroups/memory_mount/memory.max_usage_in_bytes000066400000000000000000000000071446033677600275730ustar00rootroot000000000000001000000WALinuxAgent-2.9.1.1/tests/data/cgroups/memory_mount/memory.stat000066400000000000000000000012771446033677600247130ustar00rootroot00000000000000cache 50000 rss 100000 rss_huge 4194304 shmem 8192 mapped_file 540672 dirty 0 writeback 0 swap 20000 pgpgin 42584 pgpgout 24188 pgfault 71983 pgmajfault 402 inactive_anon 32854016 active_anon 12288 inactive_file 47472640 active_file 1290240 unevictable 0 hierarchical_memory_limit 9223372036854771712 hierarchical_memsw_limit 9223372036854771712 total_cache 48771072 total_rss 32845824 total_rss_huge 4194304 total_shmem 8192 total_mapped_file 540672 total_dirty 0 total_writeback 0 total_swap 0 total_pgpgin 42584 total_pgpgout 24188 total_pgfault 71983 total_pgmajfault 402 total_inactive_anon 32854016 total_active_anon 12288 total_inactive_file 47472640 total_active_file 1290240 total_unevictable 0WALinuxAgent-2.9.1.1/tests/data/cgroups/missing_memory_counters/000077500000000000000000000000001446033677600247305ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/cgroups/missing_memory_counters/memory.stat000066400000000000000000000012511446033677600271340ustar00rootroot00000000000000cache 50000 rss_huge 4194304 shmem 8192 mapped_file 540672 dirty 0 writeback 0 pgpgin 42584 pgpgout 24188 pgfault 71983 pgmajfault 402 inactive_anon 32854016 active_anon 12288 inactive_file 47472640 active_file 1290240 unevictable 0 hierarchical_memory_limit 9223372036854771712 hierarchical_memsw_limit 9223372036854771712 total_cache 48771072 total_rss 32845824 total_rss_huge 4194304 total_shmem 8192 total_mapped_file 540672 total_dirty 0 total_writeback 0 total_swap 0 total_pgpgin 42584 total_pgpgout 24188 total_pgfault 71983 total_pgmajfault 402 total_inactive_anon 32854016 total_active_anon 12288 total_inactive_file 47472640 total_active_file 1290240 total_unevictable 0WALinuxAgent-2.9.1.1/tests/data/cgroups/proc_pid_cgroup000066400000000000000000000014311446033677600230450ustar00rootroot0000000000000012:devices:/system.slice/Microsoft.A.Sample.Extension_1.0.1_aeac05dc-8c24-4542-95f2-a0d6be1c5ba7.scope 11:perf_event:/ 10:rdma:/ 9:blkio:/system.slice/Microsoft.A.Sample.Extension_1.0.1_aeac05dc-8c24-4542-95f2-a0d6be1c5ba7.scope 8:net_cls,net_prio:/ 7:freezer:/ 6:hugetlb:/ 5:memory:/system.slice/Microsoft.A.Sample.Extension_1.0.1_aeac05dc-8c24-4542-95f2-a0d6be1c5ba7.scope 4:cpuset:/ 3:cpu,cpuacct:/system.slice/Microsoft.A.Sample.Extension_1.0.1_aeac05dc-8c24-4542-95f2-a0d6be1c5ba7.scope 2:pids:/system.slice/Microsoft.A.Sample.Extension_1.0.1_aeac05dc-8c24-4542-95f2-a0d6be1c5ba7.scope 1:name=systemd:/system.slice/Microsoft.A.Sample.Extension_1.0.1_aeac05dc-8c24-4542-95f2-a0d6be1c5ba7.scope 0::/system.slice/Microsoft.A.Sample.Extension_1.0.1_aeac05dc-8c24-4542-95f2-a0d6be1c5ba7.scope WALinuxAgent-2.9.1.1/tests/data/cgroups/proc_self_cgroup000066400000000000000000000006121446033677600232220ustar00rootroot0000000000000012:blkio:/system.slice/walinuxagent.service 11:cpu,cpuacct:/system.slice/walinuxagent.service 10:devices:/system.slice/walinuxagent.service 9:pids:/system.slice/walinuxagent.service 8:memory:/system.slice/walinuxagent.service 7:freezer:/ 6:hugetlb:/ 5:perf_event:/ 4:net_cls,net_prio:/ 3:cpuset:/ 2:rdma:/ 1:name=systemd:/system.slice/walinuxagent.service 0::/system.slice/walinuxagent.service WALinuxAgent-2.9.1.1/tests/data/cgroups/proc_stat_t0000066400000000000000000000047371446033677600223040ustar00rootroot00000000000000cpu 1242996 18547 360544 3839094 7843 0 27848 0 0 0 cpu0 305718 4380 129988 2580650 5164 0 11050 0 0 0 cpu1 316342 4742 80358 416652 1001 0 8333 0 0 0 cpu2 311233 4916 75691 419501 720 0 4268 0 0 0 cpu3 309702 4508 74507 422289 956 0 4196 0 0 0 intr 78869641 14 5118 0 0 0 0 0 0 1 17275956 0 0 977671 0 0 0 0 0 0 0 1285764 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 24614913 290 40 7069 56 9208 8660 8574436 0 0 0 0 0 0 0 0 0 0 0 0 10238 107667 2597 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ctxt 145952189 btime 1575311513 processes 49655 procs_running 2 procs_blocked 0 softirq 36699327 11497379 8043346 4993 1111315 54 54 1755674 7556345 0 6730167 WALinuxAgent-2.9.1.1/tests/data/cgroups/proc_stat_t1000066400000000000000000000047371446033677600223050ustar00rootroot00000000000000cpu 1251670 18548 368052 3877822 7926 0 28103 0 0 0 cpu0 307860 4380 135060 2583922 5171 0 11173 0 0 0 cpu1 318474 4742 81276 428425 1019 0 8384 0 0 0 cpu2 313521 4916 76464 431225 748 0 4297 0 0 0 cpu3 311814 4508 75250 434248 986 0 4247 0 0 0 intr 81910494 14 5118 0 0 0 0 0 0 1 18573886 0 0 977671 0 0 0 0 0 0 0 1291486 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 25913782 290 40 7152 56 9383 8954 8604958 0 0 0 0 0 0 0 0 0 0 0 0 10367 107667 2672 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ctxt 150920611 btime 1575311513 processes 49693 procs_running 4 procs_blocked 0 softirq 37131388 11544829 8149486 5023 1127332 54 54 1776947 7667169 0 6860494 WALinuxAgent-2.9.1.1/tests/data/cgroups/proc_stat_t2000066400000000000000000000047411446033677600223010ustar00rootroot00000000000000cpu 1293131 18554 388040 3961230 8246 0 28928 0 0 0 cpu0 317730 4381 146935 2591197 5184 0 11535 0 0 0 cpu1 329039 4746 84213 453402 1178 0 8587 0 0 0 cpu2 324213 4917 79154 456426 826 0 4410 0 0 0 cpu3 322147 4509 77737 460203 1057 0 4395 0 0 0 intr 89815534 14 5118 0 0 0 0 0 0 1 21544211 0 0 977671 0 0 0 0 0 0 0 1306547 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 29282036 290 40 7566 56 10241 9584 8929545 0 0 0 0 0 0 0 0 0 0 0 0 11076 107667 2867 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ctxt 162816290 btime 1575311513 processes 49900 procs_running 2 procs_blocked 0 softirq 38518020 11917848 8439326 5100 1170153 54 54 1834614 7944771 0 7206100 WALinuxAgent-2.9.1.1/tests/data/cgroups/sys_fs_cgroup_unified_cgroup.controllers000066400000000000000000000000521446033677600302010ustar00rootroot00000000000000io memory pids perf_event rdma cpu freezerWALinuxAgent-2.9.1.1/tests/data/cloud-init/000077500000000000000000000000001446033677600203325ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/cloud-init/set-hostname000066400000000000000000000001261446033677600226630ustar00rootroot00000000000000{ "fqdn": "a-sample-set-hostname.domain.com", "hostname": "a-sample-set-hostname" } WALinuxAgent-2.9.1.1/tests/data/dhcp000066400000000000000000000005101446033677600171200ustar00rootroot00000000000000ƪÑ] >` >* >]8ª8RD008CFA06B61Cc‚Sc56 >* >Œõ >Œ"test-cs12.h1.internal.cloudapp.netÿÿþ:ÿÿÿÿ;ÿÿÿÿ3ÿÿÿÿ >ÿWALinuxAgent-2.9.1.1/tests/data/dhcp.leases000066400000000000000000000035721446033677600204060ustar00rootroot00000000000000lease { interface "eth0"; fixed-address 10.0.1.4; server-name "RDE41D2D9BB18C"; option subnet-mask 255.255.255.0; option dhcp-lease-time 4294967295; option routers 10.0.1.1; option dhcp-message-type 5; option dhcp-server-identifier 168.63.129.16; option domain-name-servers invalid; option dhcp-renewal-time 4294967295; option rfc3442-classless-static-routes 0,10,0,1,1,32,168,63,129,16,10,0,1,1; option unknown-245 a8:3f:81:10; option dhcp-rebinding-time 4294967295; option domain-name "qylsde3bnlhu5dstzf3bav5inc.fx.internal.cloudapp.net"; renew 0 2152/07/23 23:27:10; rebind 0 2152/07/23 23:27:10; expire 0 never; } lease { interface "eth0"; fixed-address 10.0.1.4; server-name "RDE41D2D9BB18C"; option subnet-mask 255.255.255.0; option dhcp-lease-time 4294967295; option routers 10.0.1.1; option dhcp-message-type 5; option dhcp-server-identifier 168.63.129.16; option domain-name-servers expired; option dhcp-renewal-time 4294967295; option unknown-245 a8:3f:81:10; option dhcp-rebinding-time 4294967295; option domain-name "qylsde3bnlhu5dstzf3bav5inc.fx.internal.cloudapp.net"; renew 4 2015/06/16 16:58:54; rebind 4 2015/06/16 16:58:54; expire 4 2015/06/16 16:58:54; } lease { interface "eth0"; fixed-address 10.0.1.4; server-name "RDE41D2D9BB18C"; option subnet-mask 255.255.255.0; option dhcp-lease-time 4294967295; option routers 10.0.1.1; option dhcp-message-type 5; option dhcp-server-identifier 168.63.129.16; option domain-name-servers 168.63.129.16; option dhcp-renewal-time 4294967295; option rfc3442-classless-static-routes 0,10,0,1,1,32,168,63,129,16,10,0,1,1; option unknown-245 a8:3f:81:10; option dhcp-rebinding-time 4294967295; option domain-name "qylsde3bnlhu5dstzf3bav5inc.fx.internal.cloudapp.net"; renew 0 2152/07/23 23:27:10; rebind 0 2152/07/23 23:27:10; expire 0 2152/07/23 23:27:10; } WALinuxAgent-2.9.1.1/tests/data/dhcp.leases.custom.dns000066400000000000000000000035641446033677600225030ustar00rootroot00000000000000lease { interface "eth0"; fixed-address 10.0.1.4; server-name "RDE41D2D9BB18C"; option subnet-mask 255.255.255.0; option dhcp-lease-time 4294967295; option routers 10.0.1.1; option dhcp-message-type 5; option dhcp-server-identifier 168.63.129.16; option domain-name-servers invalid; option dhcp-renewal-time 4294967295; option rfc3442-classless-static-routes 0,10,0,1,1,32,168,63,129,16,10,0,1,1; option unknown-245 a8:3f:81:01; option dhcp-rebinding-time 4294967295; option domain-name "qylsde3bnlhu5dstzf3bav5inc.fx.internal.cloudapp.net"; renew 0 2152/07/23 23:27:10; rebind 0 2152/07/23 23:27:10; expire 0 never; } lease { interface "eth0"; fixed-address 10.0.1.4; server-name "RDE41D2D9BB18C"; option subnet-mask 255.255.255.0; option dhcp-lease-time 4294967295; option routers 10.0.1.1; option dhcp-message-type 5; option dhcp-server-identifier 168.63.129.16; option domain-name-servers expired; option dhcp-renewal-time 4294967295; option unknown-245 a8:3f:81:02; option dhcp-rebinding-time 4294967295; option domain-name "qylsde3bnlhu5dstzf3bav5inc.fx.internal.cloudapp.net"; renew 4 2015/06/16 16:58:54; rebind 4 2015/06/16 16:58:54; expire 4 2015/06/16 16:58:54; } lease { interface "eth0"; fixed-address 10.0.1.4; server-name "RDE41D2D9BB18C"; option subnet-mask 255.255.255.0; option dhcp-lease-time 4294967295; option routers 10.0.1.1; option dhcp-message-type 5; option dhcp-server-identifier 168.63.129.16; option domain-name-servers 8.8.8.8; option dhcp-renewal-time 4294967295; option rfc3442-classless-static-routes 0,10,0,1,1,32,168,63,129,16,10,0,1,1; option unknown-245 a8:3f:81:10; option dhcp-rebinding-time 4294967295; option domain-name "qylsde3bnlhu5dstzf3bav5inc.fx.internal.cloudapp.net"; renew 0 2152/07/23 23:27:10; rebind 0 2152/07/23 23:27:10; expire 0 2152/07/23 23:27:10; } WALinuxAgent-2.9.1.1/tests/data/dhcp.leases.multi000066400000000000000000000037161446033677600215370ustar00rootroot00000000000000lease { interface "eth0"; fixed-address 10.0.1.4; server-name "RDE41D2D9BB18C"; option subnet-mask 255.255.255.0; option dhcp-lease-time 4294967295; option routers 10.0.1.1; option dhcp-message-type 5; option dhcp-server-identifier 168.63.129.16; option domain-name-servers first; option dhcp-renewal-time 4294967295; option rfc3442-classless-static-routes 0,10,0,1,1,32,168,63,129,16,10,0,1,1; option unknown-245 a8:3f:81:01; option dhcp-rebinding-time 4294967295; option domain-name "qylsde3bnlhu5dstzf3bav5inc.fx.internal.cloudapp.net"; renew 0 2152/07/23 23:27:10; rebind 0 2152/07/23 23:27:10; expire 0 2152/07/23 23:27:10; } lease { interface "eth0"; fixed-address 10.0.1.4; server-name "RDE41D2D9BB18C"; option subnet-mask 255.255.255.0; option dhcp-lease-time 4294967295; option routers 10.0.1.1; option dhcp-message-type 5; option dhcp-server-identifier 168.63.129.16; option domain-name-servers second; option dhcp-renewal-time 4294967295; option rfc3442-classless-static-routes 0,10,0,1,1,32,168,63,129,16,10,0,1,1; option unknown-245 a8:3f:81:02; option dhcp-rebinding-time 4294967295; option domain-name "qylsde3bnlhu5dstzf3bav5inc.fx.internal.cloudapp.net"; renew 0 2152/07/23 23:27:10; rebind 0 2152/07/23 23:27:10; expire 0 2152/07/23 23:27:10; } lease { interface "eth0"; fixed-address 10.0.1.4; server-name "RDE41D2D9BB18C"; option subnet-mask 255.255.255.0; option dhcp-lease-time 4294967295; option routers 10.0.1.1; option dhcp-message-type 5; option dhcp-server-identifier 168.63.129.16; option domain-name-servers expired; option dhcp-renewal-time 4294967295; option rfc3442-classless-static-routes 0,10,0,1,1,32,168,63,129,16,10,0,1,1; option unknown-245 a8:3f:81:03; option dhcp-rebinding-time 4294967295; option domain-name "qylsde3bnlhu5dstzf3bav5inc.fx.internal.cloudapp.net"; renew 0 2152/07/23 23:27:10; rebind 0 2152/07/23 23:27:10; expire 0 2012/07/23 23:27:10; } WALinuxAgent-2.9.1.1/tests/data/events/000077500000000000000000000000001446033677600175675ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/events/1478123456789000.tld000066400000000000000000000006271446033677600220610ustar00rootroot00000000000000{"eventId": 1, "providerId": "69B669B9-4AF8-4C50-BDC4-6006FA76E975", "parameters": [{"name": "Name", "value": "Test Event"}, {"name": "Version", "value": "2.2.0"}, {"name": "IsInternal", "value": false}, {"name": "Operation", "value": "Some Operation"}, {"name": "OperationSuccess", "value": true}, {"name": "Message", "value": ""}, {"name": "Duration", "value": 0}, {"name": "ExtensionType", "value": ""}]}WALinuxAgent-2.9.1.1/tests/data/events/1478123456789001.tld000066400000000000000000000006701446033677600220600ustar00rootroot00000000000000{"eventId": 1, "providerId": "69B669B9-4AF8-4C50-BDC4-6006FA76E975", "parameters": [{"name": "Name", "value": "Linux Event"}, {"name": "Version", "value": "2.2.0"}, {"name": "IsInternal", "value": false}, {"name": "Operation", "value": "Linux Operation"}, {"name": "OperationSuccess", "value": false}, {"name": "Message", "value": "Linux Message"}, {"name": "Duration", "value": 42}, {"name": "ExtensionType", "value": "Linux Event Type"}]}WALinuxAgent-2.9.1.1/tests/data/events/1479766858966718.tld000066400000000000000000000007571446033677600221260ustar00rootroot00000000000000{"eventId": 1, "providerId": "69B669B9-4AF8-4C50-BDC4-6006FA76E975", "parameters": [{"name": "Name", "value": "WALinuxAgent"}, {"name": "Version", "value": "2.3.0.1"}, {"name": "IsInternal", "value": false}, {"name": "Operation", "value": "Enable"}, {"name": "OperationSuccess", "value": true}, {"name": "Message", "value": "Agent WALinuxAgent-2.3.0.1 launched with command 'python install.py' is successfully running"}, {"name": "Duration", "value": 0}, {"name": "ExtensionType", "value": ""}]}WALinuxAgent-2.9.1.1/tests/data/events/collect_and_send_events_unreadable_data/000077500000000000000000000000001446033677600276065ustar00rootroot00000000000000IncorrectExtension.tmp000077500000000000000000000017101446033677600341000ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/events/collect_and_send_events_unreadable_data { "eventId": 1, "providerId": "69B669B9-4AF8-4C50-BDC4-6006FA76E975", "parameters": [ { "name": "Name", "value": "Microsoft.EnterpriseCloud.Monitoring.OmsAgentForLinux" }, { "name": "Version", "value": "1.11.5" }, { "name": "Operation", "value": "Install" }, { "name": "OperationSuccess", "value": false }, { "name": "Message", "value": "HelloWorld" }, { "name": "Duration", "value": 300000 } ] }WALinuxAgent-2.9.1.1/tests/data/events/collect_and_send_events_unreadable_data/UnreadableFile.tld000077500000000000000000000017101446033677600331570ustar00rootroot00000000000000 { "eventId": 1, "providerId": "69B669B9-4AF8-4C50-BDC4-6006FA76E975", "parameters": [ { "name": "Name", "value": "Microsoft.EnterpriseCloud.Monitoring.OmsAgentForLinux" }, { "name": "Version", "value": "1.11.5" }, { "name": "Operation", "value": "Install" }, { "name": "OperationSuccess", "value": false }, { "name": "Message", "value": "HelloWorld" }, { "name": "Duration", "value": 300000 } ] }WALinuxAgent-2.9.1.1/tests/data/events/custom_script_1.tld000066400000000000000000000012351446033677600234130ustar00rootroot00000000000000{ "eventId": 1, "providerId": "69B669B9-4AF8-4C50-BDC4-6006FA76E975", "parameters": [ { "name": "Name", "value": "Microsoft.Azure.Extensions.CustomScript" }, { "name": "Version", "value": "2.0.4" }, { "name": "Operation", "value": "ThisIsATestEventOperation" }, { "name": "OperationSuccess", "value": true }, { "name": "Message", "value": "A test telemetry message." }, { "name": "Duration", "value": 150000 } ] }WALinuxAgent-2.9.1.1/tests/data/events/custom_script_2.tld000066400000000000000000000012351446033677600234140ustar00rootroot00000000000000{ "eventId": 1, "providerId": "69B669B9-4AF8-4C50-BDC4-6006FA76E975", "parameters": [ { "name": "Name", "value": "Microsoft.Azure.Extensions.CustomScript" }, { "name": "Version", "value": "2.0.4" }, { "name": "Operation", "value": "ThisIsATestEventOperation" }, { "name": "OperationSuccess", "value": true }, { "name": "Message", "value": "A test telemetry message." }, { "name": "Duration", "value": 150000 } ] }WALinuxAgent-2.9.1.1/tests/data/events/custom_script_extra_parameters.tld000066400000000000000000000027411446033677600266240ustar00rootroot00000000000000{ "eventId": 1, "providerId": "69B669B9-4AF8-4C50-BDC4-6006FA76E975", "parameters": [ { "name": "Name", "value": "Microsoft.Azure.Extensions.CustomScript" }, { "name": "Version", "value": "2.0.4" }, { "name": "Operation", "value": "ThisIsATestEventOperation" }, { "name": "OperationSuccess", "value": true }, { "name": "Message", "value": "A test telemetry message." }, { "name": "Duration", "value": 150000 }, { "name": "IsInternal", "value": true }, { "name": "ExtensionType", "value": "XML" }, { "name": "GAVersion", "value": "WALinuxAgent-9.9.99" }, { "name": "ContainerId", "value": "11111111-2222-3333-4444-555555555555" }, { "name": "OpcodeName", "value": "2099-12-31 11:59:59.505791" }, { "name": "EventTid", "value": 54321 }, { "name": "EventPid", "value": 98765 }, { "name": "TaskName", "value": "NOT_A_VALID_TASK" }, { "name": "KeywordName", "value": "NOT_A_VALID_KEYWORD" } ] }WALinuxAgent-2.9.1.1/tests/data/events/custom_script_invalid_json.tld000066400000000000000000000012731446033677600257340ustar00rootroot00000000000000{ "eventId": 1, "providerId": "69B669B9-4AF8-4C50-BDC4-6006FA76E975", "parameters": [ { "name": "Name", "value": "Microsoft.Azure.Extensions.CustomScript" }, { "name": "Version", "value": "2.0.4" }, { "name": "Operation", "value": "ThisIsATestEventOperation" }, { "name": "OperationSuccess", "value": true }, { "name": "Message", value: "THIS IS NOT VALID JSON - this object's name is not quoted" }, { "name": "Duration", "value": 150000 } ] }WALinuxAgent-2.9.1.1/tests/data/events/custom_script_no_read_access.tld000066400000000000000000000012351446033677600262030ustar00rootroot00000000000000{ "eventId": 1, "providerId": "69B669B9-4AF8-4C50-BDC4-6006FA76E975", "parameters": [ { "name": "Name", "value": "Microsoft.Azure.Extensions.CustomScript" }, { "name": "Version", "value": "2.0.4" }, { "name": "Operation", "value": "ThisIsATestEventOperation" }, { "name": "OperationSuccess", "value": true }, { "name": "Message", "value": "A test telemetry message." }, { "name": "Duration", "value": 150000 } ] }WALinuxAgent-2.9.1.1/tests/data/events/custom_script_nonascii_characters.tld000066400000000000000000000012421446033677600272530ustar00rootroot00000000000000{ "eventId": 1, "providerId": "69B669B9-4AF8-4C50-BDC4-6006FA76E975", "parameters": [ { "name": "Name", "value": "Microsoft.Azure.Extensions.CustomScript" }, { "name": "Version", "value": "2.0.4" }, { "name": "Operation", "value": "ThisIsATestEventOperation" }, { "name": "OperationSuccess", "value": true }, { "name": "Message", "value": "Worldעיות ×חרותआज" }, { "name": "Duration", "value": 150000 } ] }WALinuxAgent-2.9.1.1/tests/data/events/custom_script_utf-16.tld000066400000000000000000000024741446033677600243030ustar00rootroot00000000000000ÿþ{ "eventId": 1, "providerId": "69B669B9-4AF8-4C50-BDC4-6006FA76E975", "parameters": [ { "name": "Name", "value": "Microsoft.Azure.Extensions.CustomScript" }, { "name": "Version", "value": "2.0.4" }, { "name": "Operation", "value": "ThisIsATestEventOperation" }, { "name": "OperationSuccess", "value": true }, { "name": "Message", "value": "A test telemetry message." }, { "name": "Duration", "value": 150000 } ] }WALinuxAgent-2.9.1.1/tests/data/events/event_with_callstack.waagent.tld000066400000000000000000000042751446033677600261260ustar00rootroot00000000000000{"eventId": 1, "providerId": "69B669B9-4AF8-4C50-BDC4-6006FA76E975", "parameters": [{"name": "Name", "value": "WALinuxAgent"}, {"name": "Version", "value": "2.2.46"}, {"name": "Operation", "value": "ThisIsATestEventOperation"}, {"name": "OperationSuccess", "value": false}, {"name": "Message", "value": "An error occurred while retrieving the goal state: Traceback (most recent call last):\n File \"bin/WALinuxAgent-2.2.47/azurelinuxagent/common/protocol/wire.py\", line 715, in try_update_goal_state\n self.update_goal_state()\n File \"bin/WALinuxAgent-2.2.47/azurelinuxagent/common/protocol/wire.py\", line 708, in update_goal_state\n WireClient._UpdateType.GoalStateForced if forced else WireClient._UpdateType.GoalState)\n File \"bin/WALinuxAgent-2.2.47/azurelinuxagent/common/protocol/wire.py\", line 771, in _update_from_goal_state\n raise ProtocolError(\"Exceeded max retry updating goal state\")\nazurelinuxagent.common.exception.ProtocolError: [ProtocolError] Exceeded max retry updating goal state\n"}, {"name": "Duration", "value": 0}, {"name": "GAVersion", "value": "WALinuxAgent-2.2.46"}, {"name": "ContainerId", "value": "ebd8bf98-8a26-4607-b82f-41333b65b587"}, {"name": "OpcodeName", "value": "2020-03-19T15:30:15.326391Z"}, {"name": "EventTid", "value": 140524239595328}, {"name": "EventPid", "value": 3264}, {"name": "TaskName", "value": "ExtHandler"}, {"name": "KeywordName", "value": ""}, {"name": "ExtensionType", "value": ""}, {"name": "IsInternal", "value": false}, {"name": "OSVersion", "value": "Linux:ubuntu-18.04-bionic:5.0.0-1032-azure"}, {"name": "ExecutionMode", "value": "IAAS"}, {"name": "RAM", "value": 7976}, {"name": "Processors", "value": 2}, {"name": "VMName", "value": "_nam-u18"}, {"name": "TenantName", "value": "1feb87ac-f8b5-427e-a5b3-563b34a3931a"}, {"name": "RoleName", "value": "_nam-u18"}, {"name": "RoleInstanceName", "value": "1feb87ac-f8b5-427e-a5b3-563b34a3931a._nam-u18"}, {"name": "Location", "value": "northcentralus"}, {"name": "SubscriptionId", "value": "2588be01-bc36-4aa0-bf22-db6efc2b58ae"}, {"name": "ResourceGroupName", "value": "narrieta-rg"}, {"name": "VMId", "value": "06a6333f-410e-4a8f-a93a-c5b6075dfdad"}, {"name": "ImageOrigin", "value": 2}], "file_type": ""}WALinuxAgent-2.9.1.1/tests/data/events/extension_events/000077500000000000000000000000001446033677600231675ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/events/extension_events/different_cases/000077500000000000000000000000001446033677600263135ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/events/extension_events/different_cases/1591918616.json000066400000000000000000000021641446033677600303070ustar00rootroot00000000000000[ { "eventlevel": "INFO", "Message": "Files downloaded. Asynchronously executing command: 'SecureCommand_11'", "Version": "1", "TASKNAME": "Downloading files", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "tIMEsTAMP": "2019-12-12T01:21:05.1960563Z" }, { "EventLevel": "INFO", "MESSAGE": "HandlerEnvironment = Version: 1, HandlerEnvironment: [LogFolder: \"C:\\WindowsAzure\\Logs\\Plugins\\Microsoft.Compute.CustomScriptExtension\\1.10.3\", ConfigFolder: \"C:\\Packages\\Plugins\\Microsoft.Compute.CustomScriptExtension\\1.10.3\\RuntimeSettings\", StatusFolder: \"C:\\Packages\\Plugins\\Microsoft.Compute.CustomScriptExtension\\1.10.3\\Status\", HeartbeatFile: \"C:\\Packages\\Plugins\\Microsoft.Compute.CustomScriptExtension\\1.10.3\\Status\\HeartBeat.Json\"]", "version": "1", "TaskName": "Downloading files", "EventPID": "3228", "EVENTTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:21:05.1960563Z" } ]WALinuxAgent-2.9.1.1/tests/data/events/extension_events/empty_message/000077500000000000000000000000001446033677600260315ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/events/extension_events/empty_message/1592350454.json000066400000000000000000000011501446033677600300060ustar00rootroot00000000000000[ { "EventLevel": "INFO", "Message": null, "Version": "1", "TaskName": "Downloading files", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:21:05.1960563Z", "BadEvent": true }, { "EventLevel": "INFO", "Message": "", "Version": "1", "TaskName": "Downloading files", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:21:05.1960563Z", "BadEvent": true } ]WALinuxAgent-2.9.1.1/tests/data/events/extension_events/extra_parameters/000077500000000000000000000000001446033677600265355ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/events/extension_events/extra_parameters/1592273009.json000066400000000000000000000030551446033677600305200ustar00rootroot00000000000000[ { "EventLevel": "INFO", "Message": "Files downloaded. Asynchronously executing command: 'SecureCommand_11'", "Version": "1", "TaskName": "Downloading files", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:21:05.1960563Z", "SomethingNewButNotCool": "This is a random new param" }, { "EL": "INFO", "msg": "HandlerEnvironment = Version: 1, HandlerEnvironment: [LogFolder: \"C:\\WindowsAzure\\Logs\\Plugins\\Microsoft.Compute.CustomScriptExtension\\1.10.3\", ConfigFolder: \"C:\\Packages\\Plugins\\Microsoft.Compute.CustomScriptExtension\\1.10.3\\RuntimeSettings\", StatusFolder: \"C:\\Packages\\Plugins\\Microsoft.Compute.CustomScriptExtension\\1.10.3\\Status\", HeartbeatFile: \"C:\\Packages\\Plugins\\Microsoft.Compute.CustomScriptExtension\\1.10.3\\Status\\HeartBeat.Json\"]", "ver": "1", "TaskName": "Downloading files", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "Time": "2019-12-12T01:21:05.1960563Z", "BadEvent": true }, { "EventLevel": "INFO", "Message": "Hello World", "Version": "1", "TaskName": "Downloading files", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:21:05.1960563Z", "SomethingVeryWeird": "Weirdly weird but satisfying" } ]WALinuxAgent-2.9.1.1/tests/data/events/extension_events/int_type/000077500000000000000000000000001446033677600250225ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/events/extension_events/int_type/1519934744.json000066400000000000000000000005071446033677600270150ustar00rootroot00000000000000{ "EventLevel": "INFO", "Message": "Accept int value for eventpid and eventtid", "Version": "1", "TaskName": "Downloading files", "EventPid": 3228, "EventTid": 1, "OpErAtiOnID": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2023-03-13T01:21:05.1960563Z" }WALinuxAgent-2.9.1.1/tests/data/events/extension_events/large_messages/000077500000000000000000000000001446033677600261505ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/events/extension_events/large_messages/1591921510.json000066400000000000000000002132161446033677600301310ustar00rootroot00000000000000[ { "EventLevel": "INFO", "Message": "[MESSAGE_START] Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 Files downloaded. Asynchronously executing command: 'SecureCommand_11 [MESSAGE_END]", "Version": "1", "TaskName": "Downloading files", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:21:05.1960563Z" } ]WALinuxAgent-2.9.1.1/tests/data/events/extension_events/malformed_files/000077500000000000000000000000001446033677600263175ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/events/extension_events/malformed_files/1592008079.json000066400000000000000000000003721446033677600303040ustar00rootroot00000000000000[ { "EventLevel": "" , "Message": null, "Version": "", "TaskName": null, "EventPid": null, "EventTid": null, "OperationId": null, "TimeStamp": null, "BadEvent": true } ]WALinuxAgent-2.9.1.1/tests/data/events/extension_events/malformed_files/1594857360.tld000066400000000000000000000005131446033677600301220ustar00rootroot00000000000000{ "EventLevel": "INFO", "Message": "Enabling Handler", "Version": "1", "TaskName": "Handler Operation", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:20:05.0950244Z", "BadEvent": true }WALinuxAgent-2.9.1.1/tests/data/events/extension_events/malformed_files/bad_json_files/000077500000000000000000000000001446033677600312605ustar00rootroot000000000000001591816395.json000066400000000000000000000000301446033677600331640ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/events/extension_events/malformed_files/bad_json_files{ "BadEvent": true ]WALinuxAgent-2.9.1.1/tests/data/events/extension_events/malformed_files/bad_name_file.json000066400000000000000000000022041446033677600317350ustar00rootroot00000000000000[ { "EventLevel": "INFO", "Message": "Starting IaaS ScriptHandler Extension v1", "Version": "1", "TaskName": "Extension Info", "EventPid": "5676", "EventTid": "1", "OperationId": "e1065def-7571-42c2-88a2-4f8b4c8f226d", "TimeStamp": "2019-12-12T01:11:38.2298194Z", "BadEvent": true }, { "EventLevel": "INFO", "Message": "HandlerEnvironment = Version: 1, HandlerEnvironment: [LogFolder: \"C:\\WindowsAzure\\Logs\\Plugins\\Microsoft.Compute.CustomScriptExtension\\1.10.3\", ConfigFolder: \"C:\\Packages\\Plugins\\Microsoft.Compute.CustomScriptExtension\\1.10.3\\RuntimeSettings\", StatusFolder: \"C:\\Packages\\Plugins\\Microsoft.Compute.CustomScriptExtension\\1.10.3\\Status\", HeartbeatFile: \"C:\\Packages\\Plugins\\Microsoft.Compute.CustomScriptExtension\\1.10.3\\Status\\HeartBeat.Json\"]", "Version": "1", "TaskName": "Extension Info", "EventPid": "5676", "EventTid": "1", "OperationId": "e1065def-7571-42c2-88a2-4f8b4c8f226d", "TimeStamp": "2019-12-12T01:11:38.2318168Z", "BadEvent": true } ]WALinuxAgent-2.9.1.1/tests/data/events/extension_events/missing_parameters/000077500000000000000000000000001446033677600270635ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/events/extension_events/missing_parameters/1592273793.json000066400000000000000000000045411446033677600310610ustar00rootroot00000000000000[ { "EventLevel": "INFO", "msg": "Random names for param keys should generate MissingKeyError, message missing", "Version": "1", "TaskName": "Downloading files", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:21:05.1960563Z", "BadEvent": true }, { "EventLevel": "INFO", "msg": "Random names for param keys should generate MissingKeyError, message missing", "Version": "1", "TaskName": "Downloading files", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:21:05.1960563Z", "BadEvent": true }, { "EventLevel": "INFO", "Message": "MissingKeyError: EventPid missing", "Version": "1", "TaskName": "Downloading files", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:21:05.1960563Z", "BadEvent": true }, { "EventLevel": "INFO", "Message": "MissingKeyError: EventPid missing", "Version": "1", "TaskName": "Downloading files", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:21:05.1960563Z", "BadEvent": true }, { "EventLevel": "INFO", "Message": "MissingKeyError: Version missing", "TaskName": "Downloading files", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:21:05.1960563Z", "BadEvent": true }, { "EventLevel": "INFO", "Message": "MissingKeyError: Version missing", "EventPid": "3228", "TaskName": "Downloading files", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:21:05.1960563Z", "BadEvent": true }, { "EventLevel": "INFO", "Message": "MissingKeyError: Version missing", "EventPid": "3228", "TaskName": "Downloading files", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:21:05.1960563Z", "BadEvent": true } ]WALinuxAgent-2.9.1.1/tests/data/events/extension_events/mix_files/000077500000000000000000000000001446033677600251465ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/events/extension_events/mix_files/1591835369.json000066400000000000000000000000261446033677600271400ustar00rootroot00000000000000{ "BadEvent": true }WALinuxAgent-2.9.1.1/tests/data/events/extension_events/mix_files/1591835848.json000066400000000000000000000073511446033677600271520ustar00rootroot00000000000000[ { "EventLevel": "INFO", "Message": "Starting IaaS ScriptHandler Extension v1", "Version": "1", "TaskName": "Extension Info", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:20:05.0950244Z" }, { "EventLevel": "INFO", "Message": "HandlerEnvironment = Version: 1, HandlerEnvironment: [LogFolder: \"C:\\WindowsAzure\\Logs\\Plugins\\Microsoft.Compute.CustomScriptExtension\\1.10.3\", ConfigFolder: \"C:\\Packages\\Plugins\\Microsoft.Compute.CustomScriptExtension\\1.10.3\\RuntimeSettings\", StatusFolder: \"C:\\Packages\\Plugins\\Microsoft.Compute.CustomScriptExtension\\1.10.3\\Status\", HeartbeatFile: \"C:\\Packages\\Plugins\\Microsoft.Compute.CustomScriptExtension\\1.10.3\\Status\\HeartBeat.Json\"]", "Version": "1", "TaskName": "Extension Info", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:20:05.0950244Z" }, { "EventLevel": "INFO", "Message": "Comamnd Executed: enable", "Version": "1", "TaskName": "Handler Command", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:20:05.0950244Z" }, { "EventLevel": "INFO", "Message": "Enabling Handler", "Version": "1", "TaskName": "Handler Operation", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:20:05.0950244Z" }, { "EventLevel": "INFO", "Message": "Successfully enabled TLS.", "Version": "1", "TaskName": "TLS", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:20:05.1106498Z" }, { "EventLevel": "INFO", "Message": "Handler successfully enabled", "Version": "1", "TaskName": "Handler Operation", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:20:05.1731503Z" }, { "EventLevel": "INFO", "Message": "Loading configuration for sequence number 11", "Version": "1", "TaskName": "Sequence Number", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:20:05.1731503Z" }, { "EventLevel": "INFO", "Message": "HandlerSettings = ProtectedSettingsCertThumbprint: C8F2B56B0E79B592334BA0AFFFE172EB3DEF6753, ProtectedSettings: {MIIB0AYJKoZIhvcNAQcDoIIBwTCCAb0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEGPedX2PQQm0SE3jOKJHGtUwDQYJKoZIhvcNAQEBBQAEggEAqCIhARxqObDt9esAHszPzPonOyNyDpS2f/bsaA6TiZXyUg08JARIAZYAvR3TMJaH+Q3IkTC2tcQHmlBFVx7v9Dxm0RtwGM3CJve8Xq/Jf2X6gsq79nKPiFrZ1BRDp/TB6lZdC6ahIiRA+DOeL9p1wap8R7j67s+oQiEVi0nI0zqSOPH+nXsKNhi2xaW466zdgXdfsy2amp9pO/p9/mg+W/qyMFKcnFI8d26bqaWxYBQVYqxWnXSUE7Ul7hHEKyXQeF2QwE7QVBEMJIgLyx6u58M2FYtoWopU37gOMk8MWfTgIumP0WZOPHS/n6AffPgaypStiu9Q3HYTYnqH28OtjTBLBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECCzHZJQzFhdSgCgV25G1ZQKeyJPTLZu+u8bIjtPQ5yOOLFQZj8XrJj4HhQNTndIDwyNz}, PublicSettings: {}", "Version": "1", "TaskName": "Handler Configuration", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:20:16.7047734Z" }, { "BadEvent": true } ]WALinuxAgent-2.9.1.1/tests/data/events/extension_events/mix_files/1591835859.json000066400000000000000000000005061446033677600271470ustar00rootroot00000000000000 { "EventLevel": "INFO", "Message": "Downloading files specified in configuration...", "Version": "1", "TaskName": "Downloading files", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:20:52.9247262Z" } WALinuxAgent-2.9.1.1/tests/data/events/extension_events/special_chars/000077500000000000000000000000001446033677600257675ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/events/extension_events/special_chars/1591918939.json000066400000000000000000000005201446033677600277650ustar00rootroot00000000000000{ "EventLevel": "INFO", "Message": "Non-English message - æ­¤æ–‡å­—ä¸æ˜¯è‹±æ–‡çš„", "Version": "1", "TaskName": "Downloading files", "EventPid": "3228", "EventTid": "1", "OpErAtiOnID": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:21:05.1960563Z" }WALinuxAgent-2.9.1.1/tests/data/events/extension_events/well_formed_files/000077500000000000000000000000001446033677600266505ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/events/extension_events/well_formed_files/1591905451.json000066400000000000000000000073031446033677600306350ustar00rootroot00000000000000[ { "EventLevel": "INFO", "Message": "Starting IaaS ScriptHandler Extension v1", "Version": "1", "TaskName": "Extension Info", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:20:05.0950244Z" }, { "EventLevel": "INFO", "Message": "HandlerEnvironment = Version: 1, HandlerEnvironment: [LogFolder: \"C:\\WindowsAzure\\Logs\\Plugins\\Microsoft.Compute.CustomScriptExtension\\1.10.3\", ConfigFolder: \"C:\\Packages\\Plugins\\Microsoft.Compute.CustomScriptExtension\\1.10.3\\RuntimeSettings\", StatusFolder: \"C:\\Packages\\Plugins\\Microsoft.Compute.CustomScriptExtension\\1.10.3\\Status\", HeartbeatFile: \"C:\\Packages\\Plugins\\Microsoft.Compute.CustomScriptExtension\\1.10.3\\Status\\HeartBeat.Json\"]", "Version": "1", "TaskName": "Extension Info", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:20:05.0950244Z" }, { "EventLevel": "INFO", "Message": "Comamnd Executed: enable", "Version": "1", "TaskName": "Handler Command", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:20:05.0950244Z" }, { "EventLevel": "INFO", "Message": "Enabling Handler", "Version": "1", "TaskName": "Handler Operation", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:20:05.0950244Z" }, { "EventLevel": "INFO", "Message": "Successfully enabled TLS.", "Version": "1", "TaskName": "TLS", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:20:05.1106498Z" }, { "EventLevel": "INFO", "Message": "Handler successfully enabled", "Version": "1", "TaskName": "Handler Operation", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:20:05.1731503Z" }, { "EventLevel": "INFO", "Message": "Loading configuration for sequence number 11", "Version": "1", "TaskName": "Sequence Number", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:20:05.1731503Z" }, { "EventLevel": "INFO", "Message": "HandlerSettings = ProtectedSettingsCertThumbprint: C8F2B56B0E79B592334BA0AFFFE172EB3DEF6753, ProtectedSettings: {MIIB0AYJKoZIhvcNAQcDoIIBwTCCAb0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEGPedX2PQQm0SE3jOKJHGtUwDQYJKoZIhvcNAQEBBQAEggEAqCIhARxqObDt9esAHszPzPonOyNyDpS2f/bsaA6TiZXyUg08JARIAZYAvR3TMJaH+Q3IkTC2tcQHmlBFVx7v9Dxm0RtwGM3CJve8Xq/Jf2X6gsq79nKPiFrZ1BRDp/TB6lZdC6ahIiRA+DOeL9p1wap8R7j67s+oQiEVi0nI0zqSOPH+nXsKNhi2xaW466zdgXdfsy2amp9pO/p9/mg+W/qyMFKcnFI8d26bqaWxYBQVYqxWnXSUE7Ul7hHEKyXQeF2QwE7QVBEMJIgLyx6u58M2FYtoWopU37gOMk8MWfTgIumP0WZOPHS/n6AffPgaypStiu9Q3HYTYnqH28OtjTBLBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECCzHZJQzFhdSgCgV25G1ZQKeyJPTLZu+u8bIjtPQ5yOOLFQZj8XrJj4HhQNTndIDwyNz}, PublicSettings: {}", "Version": "1", "TaskName": "Handler Configuration", "EventPid": "3228", "EventTid": "1", "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", "TimeStamp": "2019-12-12T01:20:16.7047734Z" } ]WALinuxAgent-2.9.1.1/tests/data/events/extension_events/well_formed_files/1592355539.json000066400000000000000000000052361446033677600306470ustar00rootroot00000000000000[ { "EventLevel": "INFO", "Message": "Starting IaaS ScriptHandler Extension v1", "Version": "1", "TaskName": "Extension Info", "EventPid": "5676", "EventTid": "1", "OperationId": "e1065def-7571-42c2-88a2-4f8b4c8f226d", "Timestamp": "2019-12-12T01:11:38.2298194Z" }, { "EventLevel": "INFO", "Message": "HandlerEnvironment = Version: 1, HandlerEnvironment: [LogFolder: \"C:\\WindowsAzure\\Logs\\Plugins\\Microsoft.Compute.CustomScriptExtension\\1.10.3\", ConfigFolder: \"C:\\Packages\\Plugins\\Microsoft.Compute.CustomScriptExtension\\1.10.3\\RuntimeSettings\", StatusFolder: \"C:\\Packages\\Plugins\\Microsoft.Compute.CustomScriptExtension\\1.10.3\\Status\", HeartbeatFile: \"C:\\Packages\\Plugins\\Microsoft.Compute.CustomScriptExtension\\1.10.3\\Status\\HeartBeat.Json\"]", "Version": "1", "TaskName": "Extension Info", "EventPid": "5676", "EventTid": "1", "OperationId": "e1065def-7571-42c2-88a2-4f8b4c8f226d", "Timestamp": "2019-12-12T01:11:38.2318168Z" }, { "EventLevel": "INFO", "Message": "Comamnd Executed: enable", "Version": "1", "TaskName": "Handler Command", "EventPid": "5676", "EventTid": "1", "OperationId": "e1065def-7571-42c2-88a2-4f8b4c8f226d", "Timestamp": "2019-12-12T01:11:38.2328180Z" }, { "EventLevel": "INFO", "Message": "Enabling Handler", "Version": "1", "TaskName": "Handler Operation", "EventPid": "5676", "EventTid": "1", "OperationId": "e1065def-7571-42c2-88a2-4f8b4c8f226d", "Timestamp": "2019-12-12T01:11:38.2338180Z" }, { "EventLevel": "INFO", "Message": "Successfully enabled TLS.", "Version": "1", "TaskName": "TLS", "EventPid": "5676", "EventTid": "1", "OperationId": "e1065def-7571-42c2-88a2-4f8b4c8f226d", "Timestamp": "2019-12-12T01:11:38.2368192Z" }, { "EventLevel": "INFO", "Message": "Handler successfully enabled", "Version": "1", "TaskName": "Handler Operation", "EventPid": "5676", "EventTid": "1", "OperationId": "e1065def-7571-42c2-88a2-4f8b4c8f226d", "Timestamp": "2019-12-12T01:11:38.2448190Z" }, { "EventLevel": "INFO", "Message": "Loading configuration for sequence number 11", "Version": "1", "TaskName": "Sequence Number", "EventPid": "5676", "EventTid": "1", "OperationId": "e1065def-7571-42c2-88a2-4f8b4c8f226d", "Timestamp": "2019-12-12T01:11:38.2498176Z" } ]WALinuxAgent-2.9.1.1/tests/data/events/extension_events/well_formed_files/9999999999.json000066400000000000000000000043521446033677600307200ustar00rootroot00000000000000[ { "EventLevel": "INFO", "Message": "This is the latest event", "Version": "1", "TaskName": "Extension Info", "EventPid": "999", "EventTid": "999", "OperationId": "", "TimeStamp": "2019-12-12T01:20:05.0950244Z" }, { "EventLevel": "INFO", "Message": "This is the latest event", "Version": "1", "TaskName": "Extension Info", "EventPid": "999", "EventTid": "999", "OperationId": "", "TimeStamp": "2019-12-12T01:20:05.0950244Z" }, { "EventLevel": "INFO", "Message": "This is the latest event", "Version": "1", "TaskName": "Extension Info", "EventPid": "999", "EventTid": "999", "OperationId": "", "TimeStamp": "2019-12-12T01:20:05.0950244Z" }, { "EventLevel": "INFO", "Message": "This is the latest event", "Version": "1", "TaskName": "Extension Info", "EventPid": "999", "EventTid": "999", "OperationId": "", "TimeStamp": "2019-12-12T01:20:05.0950244Z" }, { "EventLevel": "INFO", "Message": "This is the latest event", "Version": "1", "TaskName": "Extension Info", "EventPid": "999", "EventTid": "999", "OperationId": "", "TimeStamp": "2019-12-12T01:20:05.0950244Z" }, { "EventLevel": "INFO", "Message": "This is the latest event", "Version": "1", "TaskName": "Extension Info", "EventPid": "999", "EventTid": "999", "OperationId": "", "TimeStamp": "2019-12-12T01:20:05.0950244Z" }, { "EventLevel": "INFO", "Message": "This is the latest event", "Version": "1", "TaskName": "Extension Info", "EventPid": "999", "EventTid": "999", "OperationId": "", "TimeStamp": "2019-12-12T01:20:05.0950244Z" }, { "EventLevel": "INFO", "Message": "This is the latest event", "Version": "1", "TaskName": "Extension Info", "EventPid": "999", "EventTid": "999", "OperationId": "", "TimeStamp": "2019-12-12T01:20:05.0950244Z" } ]WALinuxAgent-2.9.1.1/tests/data/events/legacy_agent.tld000066400000000000000000000027231446033677600227220ustar00rootroot00000000000000{ "eventId": 1, "providerId": "69B669B9-4AF8-4C50-BDC4-6006FA76E975", "parameters": [ { "name": "Name", "value": "WALinuxAgent" }, { "name": "Version", "value": "9.9.9" }, { "name": "IsInternal", "value": false }, { "name": "Operation", "value": "ThisIsATestEventOperation" }, { "name": "OperationSuccess", "value": true }, { "name": "Message", "value": "The cgroup filesystem is ready to use" }, { "name": "Duration", "value": 1234 }, { "name": "ExtensionType", "value": "ALegacyExtensionType" }, { "name": "GAVersion", "value": "WALinuxAgent-1.1.1" }, { "name": "ContainerId", "value": "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE" }, { "name": "OpcodeName", "value": "1970-01-01 12:00:00" }, { "name": "EventTid", "value": 98765 }, { "name": "EventPid", "value": 4321 }, { "name": "TaskName", "value": "ALegacyTask" }, { "name": "KeywordName", "value": "ALegacyKeywordName" } ] }WALinuxAgent-2.9.1.1/tests/data/events/legacy_agent_no_timestamp.tld000066400000000000000000000025611446033677600255010ustar00rootroot00000000000000{ "eventId": 1, "providerId": "69B669B9-4AF8-4C50-BDC4-6006FA76E975", "parameters": [ { "name": "Name", "value": "WALinuxAgent" }, { "name": "Version", "value": "9.9.9" }, { "name": "IsInternal", "value": false }, { "name": "Operation", "value": "ThisIsATestEventOperation" }, { "name": "OperationSuccess", "value": true }, { "name": "Message", "value": "The cgroup filesystem is ready to use" }, { "name": "Duration", "value": 1234 }, { "name": "ExtensionType", "value": "ALegacyExtensionType" }, { "name": "GAVersion", "value": "WALinuxAgent-1.1.1" }, { "name": "ContainerId", "value": "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE" }, { "name": "EventTid", "value": 98765 }, { "name": "EventPid", "value": 4321 }, { "name": "TaskName", "value": "ALegacyTask" }, { "name": "KeywordName", "value": "ALegacyKeywordName" } ] }WALinuxAgent-2.9.1.1/tests/data/ext/000077500000000000000000000000001446033677600170635ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/ext/dsc_event.json000066400000000000000000000015631446033677600217350ustar00rootroot00000000000000{ "eventId":"1", "parameters":[ { "name":"Name", "value":"Microsoft.Azure.GuestConfiguration.DSCAgent" }, { "name":"Version", "value":"1.18.0" }, { "name":"IsInternal", "value":true }, { "name":"Operation", "value":"GuestConfigAgent.Scenario" }, { "name":"OperationSuccess", "value":true }, { "name":"Message", "value":"[2019-11-05 10:06:52.688] [PID 11487] [TID 11513] [Timer Manager] [INFO] [89f9cf47-c02d-4774-b21a-abdf2beb3cd9] Run pull refresh for timer 'dsc_refresh_timer'\n" }, { "name":"Duration", "value":0 }, { "name":"ExtentionType", "value":"" } ], "providerId":"69B669B9-4AF8-4C50-BDC4-6006FA76E975" }WALinuxAgent-2.9.1.1/tests/data/ext/event.json000066400000000000000000000013501446033677600210760ustar00rootroot00000000000000{ "eventId":1, "providerId":"69B669B9-4AF8-4C50-BDC4-6006FA76E975", "parameters":[ { "name":"Name", "value":"CustomScript" }, { "name":"Version", "value":"1.4.1.0" }, { "name":"IsInternal", "value":false }, { "name":"Operation", "value":"RunScript" }, { "name":"OperationSuccess", "value":true }, { "name":"Message", "value":"(01302)Script is finished. ---stdout--- hello ---errout--- " }, { "name":"Duration", "value":0 }, { "name":"ExtensionType", "value":"" } ] }WALinuxAgent-2.9.1.1/tests/data/ext/event_from_agent.json000066400000000000000000000037511446033677600233060ustar00rootroot00000000000000{ "eventId": 1, "providerId": "69B669B9-4AF8-4C50-BDC4-6006FA76E975", "parameters": [ { "name": "Name", "value": "dummy_name" }, { "name": "Version", "value": "2.2.46" }, { "name": "Operation", "value": "Unknown" }, { "name": "OperationSuccess", "value": true }, { "name": "Message", "value": "" }, { "name": "Duration", "value": 0 }, { "name": "GAVersion", "value": "WALinuxAgent-2.2.46" }, { "name": "ContainerId", "value": "UNINITIALIZED" }, { "name": "OpcodeName", "value": "2020-01-31T00:06:54.074757Z" }, { "name": "EventTid", "value": 139628215564096 }, { "name": "EventPid", "value": 10681 }, { "name": "TaskName", "value": "MainThread" }, { "name": "KeywordName", "value": "" }, { "name": "ExtensionType", "value": "" }, { "name": "IsInternal", "value": false }, { "name": "OSVersion", "value": "TEST_OSVersion" }, { "name": "ExecutionMode", "value": "TEST_ExecutionMode" }, { "name": "RAM", "value": 512 }, { "name": "Processors", "value": 2 }, { "name": "VMName", "value": "TEST_VMName" }, { "name": "TenantName", "value": "TEST_TenantName" }, { "name": "RoleName", "value": "TEST_RoleName" }, { "name": "RoleInstanceName", "value": "TEST_RoleInstanceName" }, { "name": "Location", "value": "TEST_Location" }, { "name": "SubscriptionId", "value": "TEST_SubscriptionId" }, { "name": "ResourceGroupName", "value": "TEST_ResourceGroupName" }, { "name": "VMId", "value": "TEST_VMId" }, { "name": "ImageOrigin", "value": 1 } ], "file_type": "json" }WALinuxAgent-2.9.1.1/tests/data/ext/event_from_extension.xml000066400000000000000000000023071446033677600240470ustar00rootroot00000000000000 WALinuxAgent-2.9.1.1/tests/data/ext/sample-status-invalid-format-emptykey-line7.json000066400000000000000000000100241446033677600303500ustar00rootroot00000000000000[ { "status": { "status": "success", "code": 1, "snapshotInfo": null, "" "name": "Microsoft.Azure.Extension.VMExtension", "commandStartTimeUTCTicks": "636953997844977993", "taskId": "e5e5602b-48a6-4c35-9f96-752043777af1", "formattedMessage": { "lang": "en-US", "message": "Aenean semper nunc nisl, vitae sollicitudin felis consequat at. In lobortis elementum sapien, non commodo odio semper ac." }, "uniqueMachineId": "e5e5602b-48a6-4c35-9f96-752043777af1", "vmHealthInfo": null, "storageDetails": { "totalUsedSizeInBytes": 10000000000, "partitionCount": 3, "isSizeComputationFailed": false, "isStoragespacePresent": false }, "telemetryData": null, "substatus": [ { "status": "success", "formattedMessage": null, "code": "0", "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" } ], "operation": "Enable" }, "version": "1.0", "timestampUTC": "2019-06-06T23:52:59Z" } ]WALinuxAgent-2.9.1.1/tests/data/ext/sample-status-invalid-json-format.json000066400000000000000000000101271446033677600264420ustar00rootroot00000000000000[ { "_comment": "This is an invalid status file, it's missing a brace at line 37", "status": { "status": "success", "code": 1, "snapshotInfo": null, "name": "Microsoft.Azure.Extension.VMExtension", "commandStartTimeUTCTicks": "636953997844977993", "taskId": "e5e5602b-48a6-4c35-9f96-752043777af1", "formattedMessage": { "lang": "en-US", "message": "Aenean semper nunc nisl, vitae sollicitudin felis consequat at. In lobortis elementum sapien, non commodo odio semper ac." }, "uniqueMachineId": "e5e5602b-48a6-4c35-9f96-752043777af1", "vmHealthInfo": null, "storageDetails": { "totalUsedSizeInBytes": 10000000000, "partitionCount": 3, "isSizeComputationFailed": false, "isStoragespacePresent": false }, "telemetryData": null, "substatus": [ { "status": "success", "formattedMessage": null, "code": "0", "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" } ], "operation": "Enable" }, "version": "1.0", "timestampUTC": "2019-06-06T23:52:59Z" ]WALinuxAgent-2.9.1.1/tests/data/ext/sample-status-invalid-status-no-status-status-key.json000066400000000000000000000077441446033677600316030ustar00rootroot00000000000000[ { "status": { "code": 1, "snapshotInfo": null, "name": "Microsoft.Azure.Extension.VMExtension", "commandStartTimeUTCTicks": "636953997844977993", "taskId": "e5e5602b-48a6-4c35-9f96-752043777af1", "formattedMessage": { "lang": "en-US", "message": "Aenean semper nunc nisl, vitae sollicitudin felis consequat at. In lobortis elementum sapien, non commodo odio semper ac." }, "uniqueMachineId": "e5e5602b-48a6-4c35-9f96-752043777af1", "vmHealthInfo": null, "storageDetails": { "totalUsedSizeInBytes": 10000000000, "partitionCount": 3, "isSizeComputationFailed": false, "isStoragespacePresent": false }, "telemetryData": null, "substatus": [ { "status": "success", "formattedMessage": null, "code": "0", "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" } ], "operation": "Enable" }, "version": "1.0", "timestampUTC": "2019-06-06T23:52:59Z" } ]WALinuxAgent-2.9.1.1/tests/data/ext/sample-status-very-large-multiple-substatuses.json000066400000000000000000016264531446033677600310700ustar00rootroot00000000000000[ { "status": { "status": "success", "code": 1, "snapshotInfo": null, "name": "Microsoft.Azure.Extension.VMExtension", "commandStartTimeUTCTicks": "636953997844977993", "taskId": "e5e5602b-48a6-4c35-9f96-752043777af1", "formattedMessage": { "lang": "en-US", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "uniqueMachineId": "e5e5602b-48a6-4c35-9f96-752043777af1", "vmHealthInfo": null, "storageDetails": { "totalUsedSizeInBytes": 10000000000, "partitionCount": 3, "isSizeComputationFailed": false, "isStoragespacePresent": false }, "telemetryData": null, "substatus": [ { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" }, { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" } ], "operation": "Enable" }, "version": "1.0", "timestampUTC": "2019-06-06T23:52:59Z" } ]WALinuxAgent-2.9.1.1/tests/data/ext/sample-status-very-large.json000066400000000000000000004276061446033677600246520ustar00rootroot00000000000000[ { "status": { "status": "success", "code": 1, "snapshotInfo": null, "name": "Microsoft.Azure.Extension.VMExtension", "commandStartTimeUTCTicks": "636953997844977993", "taskId": "e5e5602b-48a6-4c35-9f96-752043777af1", "formattedMessage": { "lang": "en-US", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "uniqueMachineId": "e5e5602b-48a6-4c35-9f96-752043777af1", "vmHealthInfo": null, "storageDetails": { "totalUsedSizeInBytes": 10000000000, "partitionCount": 3, "isSizeComputationFailed": false, "isStoragespacePresent": false }, "telemetryData": null, "substatus": [ { "status": "success", "formattedMessage": { "lang": "eng", "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non lacinia urna, sit amet venenatis orci. Praesent maximus erat et augue tincidunt, quis fringilla urna mollis. Fusce id lacus velit. Praesent interdum, nulla eget sagittis lobortis, elit arcu porta nisl, eu volutpat ante est ut libero. Nulla volutpat nisl arcu, sed vehicula nulla fringilla id. Mauris sollicitudin viverra nunc, id sodales dui tempor eu. Morbi sit amet placerat felis. Pellentesque nunc leo, sollicitudin eu ex ac, varius lobortis tortor. Etiam purus ipsum, venenatis nec sagittis non, commodo non ipsum. Ut hendrerit a erat ut vehicula. Nam ullamcorper finibus metus, non iaculis metus molestie id. Vivamus blandit commodo metus. Fusce pellentesque, nunc sed lobortis laoreet, neque leo pulvinar felis, ac fermentum ligula ante a lacus. Aliquam interdum luctus elit, nec ultrices quam iaculis a. Aliquam tempor, arcu vel consequat molestie, ligula nisi gravida lacus, sed blandit magna felis nec dolor. Pellentesque dignissim ac diam a posuere. Etiam vulputate nisi vel dui imperdiet, et cursus dolor pellentesque." }, "code": 0, "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"Message\", \"Value\": \"Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Quisque eu nisl consectetur, accumsan lacus et, laoreet purus. Proin eget arcu ligula. Aliquam erat volutpat. Ut laoreet velit eget malesuada maximus. Duis dictum lectus ligula, nec congue ante lobortis sed. Cras sed sapien lobortis, maximus erat lacinia, lacinia mauris. Proin sodales vulputate libero. Nullam nisi nunc, malesuada eget aliquet eu, porttitor vitae neque. Nunc interdum malesuada eleifend. Nam in enim placerat, ornare nunc quis, sollicitudin magna. Cras rutrum vel ipsum dignissim commodo. Nulla facilisi. Integer quam nibh, varius in luctus eu, volutpat accumsan ligula. Donec id blandit urna, nec tempus erat. Nullam at arcu vel nunc consectetur lobortis id ultrices purus. Suspendisse ac diam auctor, luctus risus nec, molestie urna. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor. Integer tincidunt sem nisl. Praesent dictum sit amet augue ut convallis. Maecenas lobortis lorem massa, in lobortis sapien auctor a. Duis lobortis euismod ligula non accumsan. Morbi risus turpis, interdum eu varius quis, consectetur tristique risus. Integer fermentum arcu sit amet dui faucibus pulvinar. Integer egestas quam at mi tempor, ac varius est iaculis. Morbi at nisi pretium, semper lacus vel, suscipit nisl. Mauris vitae ipsum tellus. Aenean blandit dapibus tortor, ac varius odio feugiat in. Etiam mi risus, auctor sit amet vehicula ut, maximus ut risus. Suspendisse nec interdum magna. Proin hendrerit turpis arcu, vitae dictum magna blandit egestas. Morbi vitae purus nunc. Duis vehicula, massa sed feugiat tempus, quam libero vehicula sapien, a volutpat massa nulla non est. Maecenas fermentum lectus eu lectus molestie, sed sagittis neque ornare. Proin id arcu non magna pretium semper vel sed nulla. Integer porttitor, ipsum vitae iaculis eleifend, eros ligula ornare lorem, at pulvinar urna risus vel diam. Curabitur tempus elementum tristique. Etiam non erat in nunc efficitur finibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris id pellentesque felis. Pellentesque vel magna nec mi rhoncus laoreet a eget nibh. Proin vitae nulla non ante interdum lobortis. Nam eu ex eget neque ultrices cursus ut sed enim. Nulla facilisi. Pellentesque maximus, est ac facilisis rutrum, justo erat hendrerit est, vitae tempor tortor justo fringilla lectus. Fusce est augue, posuere porttitor sapien sed, pellentesque viverra neque. Etiam euismod suscipit egestas. Vivamus cursus dui ac massa feugiat viverra. Suspendisse luctus, ex vitae auctor volutpat, magna elit porttitor mauris, in commodo urna mi ut dolor.\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" } ], "operation": "Enable" }, "version": "1.0", "timestampUTC": "2019-06-06T23:52:59Z" } ]WALinuxAgent-2.9.1.1/tests/data/ext/sample-status.json000066400000000000000000000100051446033677600225540ustar00rootroot00000000000000[ { "status": { "status": "success", "code": 1, "snapshotInfo": null, "name": "Microsoft.Azure.Extension.VMExtension", "commandStartTimeUTCTicks": "636953997844977993", "taskId": "e5e5602b-48a6-4c35-9f96-752043777af1", "formattedMessage": { "lang": "en-US", "message": "Aenean semper nunc nisl, vitae sollicitudin felis consequat at. In lobortis elementum sapien, non commodo odio semper ac." }, "uniqueMachineId": "e5e5602b-48a6-4c35-9f96-752043777af1", "vmHealthInfo": null, "storageDetails": { "totalUsedSizeInBytes": 10000000000, "partitionCount": 3, "isSizeComputationFailed": false, "isStoragespacePresent": false }, "telemetryData": null, "substatus": [ { "status": "success", "formattedMessage": null, "code": "0", "name": "[{\"status\": {\"status\": \"success\", \"code\": \"1\", \"snapshotInfo\": [{\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/p3w4cdkggwwl/abcd?snapshot=2019-06-06T23:53:14.9090608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/l0z0cjhf0fbr/abcd?snapshot=2019-06-06T23:53:14.9083776Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/lxqfz15mlw0s/abcd?snapshot=2019-06-06T23:53:14.9137572Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/m04dplcjltlt/abcd?snapshot=2019-06-06T23:53:14.9087358Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-rvr1sst1m0s0.blob.core.windows.net/nkx4dljgcppt/abcd?snapshot=2019-06-06T23:53:14.9089608Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}, {\"snapshotUri\": \"https://md-sqfzxrbqwwkg.blob.core.windows.net/sl2lt5k20wwx/abcd?snapshot=2019-06-06T23:53:14.8338051Z\", \"errorMessage\": \"\", \"isSuccessful\": \"true\"}], \"name\": \"Microsoft.Azure.RecoveryServices.VMSnapshotLinux\", \"commandStartTimeUTCTicks\": \"636953997844977993\", \"taskId\": \"767baff0-3f1e-4363-a974-5d801189250d\", \"formattedMessage\": {\"lang\": \"en-US\", \"message\": \" statusBlobUploadError=true, snapshotCreator=backupHostService, hostStatusCodeDoSnapshot=200, \"}, \"uniqueMachineId\": \"cf2545fd-fef4-250b-d167-f2edb86da1ac\", \"vmHealthInfo\": {\"vmHealthStatusCode\": 1, \"vmHealthState\": 0}, \"storageDetails\": {\"totalUsedSizeInBytes\": 10795593728, \"partitionCount\": 3, \"isSizeComputationFailed\": false, \"isStoragespacePresent\": false}, \"telemetryData\": [{\"Key\": \"kernelVersion\", \"Value\": \"4.4.0-145-generic\"}, {\"Key\": \"networkFSTypePresentInMount\", \"Value\": \"True\"}, {\"Key\": \"extensionVersion\", \"Value\": \"1.0.9150.0\"}, {\"Key\": \"guestAgentVersion\", \"Value\": \"2.2.32.2\"}, {\"Key\": \"FreezeTime\", \"Value\": \"0:00:00.258313\"}, {\"Key\": \"ramDisksSize\", \"Value\": \"626944\"}, {\"Key\": \"platformArchitecture\", \"Value\": \"64bit\"}, {\"Key\": \"pythonVersion\", \"Value\": \"2.7.12\"}, {\"Key\": \"snapshotCreator\", \"Value\": \"backupHostService\"}, {\"Key\": \"tempDisksSize\", \"Value\": \"60988\"}, {\"Key\": \"statusBlobUploadError\", \"Value\": \"true\"}, {\"Key\": \"ThawTime\", \"Value\": \"0:00:00.143658\"}, {\"Key\": \"extErrorCode\", \"Value\": \"success\"}, {\"Key\": \"osVersion\", \"Value\": \"Ubuntu-16.04\"}, {\"Key\": \"hostStatusCodeDoSnapshot\", \"Value\": \"200\"}, {\"Key\": \"snapshotTimeTaken\", \"Value\": \"0:00:00.325422\"}], \"substatus\": [], \"operation\": \"Enable\"}, \"version\": \"1.0\", \"timestampUTC\": \"\\/Date(1559865179380)\\/\"}]" } ], "operation": "Enable" }, "version": "1.0", "timestampUTC": "2019-06-06T23:52:59Z" } ]WALinuxAgent-2.9.1.1/tests/data/ext/sample_ext-1.3.0.zip000066400000000000000000000040371446033677600224110ustar00rootroot00000000000000PK |cSRHýõ]exit.shUT K0`¼bux èè#!/usr/bin/env sh exit $*PKlQŸ.h`ª“HandlerManifest.jsonUT ÀÆF_ÙÀbux èè}ÎM Â0†á}N²Ö¢ÛîD=¸˜Ò)òG2•ŠôîÆ$J»¨]æ}æ£×ãñ4Šš‹ÃÚ)<‚iú³4ý V™<ÐiMTÛjSÞîÙ]ÀÈÅ–÷R”&(µ·ZGöYi¼rO¾.±Œ'ß›¿¿<»q-.¤6Õh Q :·©neXæ%N½ÇÆZÚu„þTþ´æ¨€3䬧#‚§¾"‘7ÆÞPK NXT”oN ¨ì python.shUT çÄbìÄbux èèuŽÍ ‚@ÄïûWL*˜-ä1Š.A§êÐ%¢ƒO]Ðg¸®ôAÿ{+zí6ox¿™qgÒèV&Š%q$Ö¥p…‹ý“RÓ‘†ê4â¶05±UF+.Еÿñêʆ}¤M]Çœ- rûŒ4f$„¼1ƒG•¦ š€È_Ú‚Qo¼ùD#ì1zXm!3ê%›ª „°©7„o8Þùz9œŽîëa àJˆ\ á}ÆókñóPKwIXT»C% sample.pyUT Q¼bS¼bux èèWKoã6¾ûW0ÊÁR×Q“[ÔYE=4(ôä„"mîJ¤–¤âAþ{‡/‹z8[]"æñÍë#sy‘ÿÚ¾éƒà¹:,XÓ ©É7%xx*¼IoêM-‹ vdš*ø‘JøÑÒPQxÕ” ¬‡ìvAða;²üSðÛ?=^ÂC×<ƒ\Æ1@ü…IÁ²y.Éõ±xS¤S@ôˆ×h€kòRHV<×`Ü/«Í“©ÝIŽŽuÚ{Þ̇ßf kWÁ›DÈš\Ýx™Üa#*¶c˜˜f àçkûµ´îhÅ$Š0N[èCþM0n‚bQÊc•f+’8½$ eªL¡iÚ{ÉúÜwB¦¡ñµ©™Ògu­WÔ¥ÆéIo±²JÙÐ,³c5¤'G£æi ]šr…,i’¦_ÿù=tûîééø%{zʳ_Œ+I¹@ae… ´f|¯’U6›Ä@D. S„ MÌM‘˜'DE@Ö"ßKѵé2È—Sç>ÀtNãh¤àUïüb=£>È&ÙÁ¬BÙII]U´Îxâ¾$ÙÛB©ÅOÒW"[üPK |cSRHýõ]ýexit.shUTK0`ux èèPKlQŸ.h`ª“´[HandlerManifest.jsonUTÀÆF_ux èèPK NXT”oN ¨ì ýSpython.shUTçÄbux èèPKwIXT»C% ý>sample.pyUTQ¼bux èèPKEÄWALinuxAgent-2.9.1.1/tests/data/ext/sample_ext-1.3.0/000077500000000000000000000000001446033677600216615ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/ext/sample_ext-1.3.0/HandlerManifest.json000066400000000000000000000006231446033677600256210ustar00rootroot00000000000000[{ "name": "ExampleHandlerLinux", "version": 1.0, "handlerManifest": { "installCommand": "sample.py -install", "uninstallCommand": "sample.py -uninstall", "updateCommand": "sample.py -update", "enableCommand": "sample.py -enable", "disableCommand": "sample.py -disable", "rebootAfterInstall": false, "reportHeartbeat": false } }] WALinuxAgent-2.9.1.1/tests/data/ext/sample_ext-1.3.0/exit.sh000077500000000000000000000000321446033677600231640ustar00rootroot00000000000000#!/usr/bin/env sh exit $*WALinuxAgent-2.9.1.1/tests/data/ext/sample_ext-1.3.0/python.sh000077500000000000000000000003551446033677600235440ustar00rootroot00000000000000#!/usr/bin/env bash # # Executes its arguments using the 'python' command, if it can be found, else using 'python3'. # python=$(command -v python 2> /dev/null) if [ -z "$PYTHON" ]; then python=$(command -v python3) fi ${python} "$@" WALinuxAgent-2.9.1.1/tests/data/ext/sample_ext-1.3.0/sample.py000077500000000000000000000064451446033677600235300ustar00rootroot00000000000000#!./python.sh import json import os import re import sys def get_seq(requested_ext_name=None): if 'ConfigSequenceNumber' in os.environ: # Always use the environment variable if available return int(os.environ['ConfigSequenceNumber']) latest_seq = -1 largest_modified_time = 0 config_dir = os.path.join(os.getcwd(), "config") if os.path.isdir(config_dir): for item in os.listdir(config_dir): item_path = os.path.join(config_dir, item) if os.path.isfile(item_path): match = re.search("((?P\\w+)\\.)*(?P\\d+)\\.settings", item_path) if match is not None: ext_name = match.group('ext_name') if requested_ext_name is not None and ext_name != requested_ext_name: continue curr_seq_no = int(match.group("seq_no")) curr_modified_time = os.path.getmtime(item_path) if curr_modified_time > largest_modified_time: latest_seq = curr_seq_no largest_modified_time = curr_modified_time return latest_seq def get_extension_state_prefix(): requested_ext_name = None if 'ConfigExtensionName' not in os.environ else os.environ['ConfigExtensionName'] seq = get_seq(requested_ext_name) if seq >= 0: if requested_ext_name is not None: seq = "{0}.{1}".format(requested_ext_name, seq) return seq return None def read_settings_file(seq_prefix): settings_file = os.path.join(os.getcwd(), "config", "{0}.settings".format(seq_prefix)) if not os.path.exists(settings_file): print("No settings found for {0}".format(settings_file)) return None with open(settings_file, "rb") as file_: return json.loads(file_.read().decode("utf-8")) def report_status(seq_prefix, status="success", message=None): status_path = os.path.join(os.getcwd(), "status") if not os.path.exists(status_path): os.makedirs(status_path) status_file = os.path.join(status_path, "{0}.status".format(seq_prefix)) with open(status_file, "w+") as status_: status_to_report = { "status": { "status": status } } if message is not None: status_to_report['status']["formattedMessage"] = { "lang": "en-US", "message": message } status_.write(json.dumps([status_to_report])) if __name__ == "__main__": prefix = get_extension_state_prefix() if prefix is None: print("No sequence number found!") sys.exit(-1) try: settings = read_settings_file(prefix) except Exception as error: msg = "Error when trying to fetch settings {0}.settings: {1}".format(prefix, error) print(msg) report_status(prefix, status="error", message=msg) else: status_msg = None if settings is not None: print(settings) try: status_msg = settings['runtimeSettings'][0]['handlerSettings']['publicSettings']['message'] except Exception: # Settings might not contain the message. Ignore error if not found pass report_status(prefix, message=status_msg) WALinuxAgent-2.9.1.1/tests/data/ga/000077500000000000000000000000001446033677600166525ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/ga/WALinuxAgent-0.0.0.0.zip000066400000000000000000023355171446033677600225130ustar00rootroot00000000000000PKdRebK+ª¶ à "bin/WALinuxAgent-9.9.9.9-py2.7.eggUT ê$@`ê$@`ux œûc°¨1³.ŠÛ¶mÛ¶mÛ¶mÛö¶mÛ¶=Ï\ëܺµ¾]gŸÚ÷þèÔÛQuÒy;Îi`€ýª{¹ß9?ÞZ@€* è¹úú¶†–vúútž¦I‚öÙ¡w_QäŸItnNU§"¸ê¢²Ž¦9"œuñ ][;¥?³FêR*pýLö{>Oë”i£pqWÆý;‡¯È~'1ìá æ±Ê±re(ñ:‰¯ÍâåoÇq!ÓURxé“0sÇÝãŽét¨ ÉÒ–N¾ôpÁóU€› .yÃA¼pÛç*‰°±“Í­†¥ÔBs­%F^ ]"µ"ö¦®ÕÁPÄÝq„ësOž{o•X†ÊG—žÎh{uPO.ƒâµó\„w‡þÄåÆtbÔÜ«X‘§Ä^â¨ÜHX ˆìî:èÎ!0_¥;n4Áǹëz ¯òseþ­8TÂJ®Ñ“»'¥~r"ãÿûºõanÓ4eÛKÕ°kÐA‡÷hÛKgò‚ªùuçùÀ,©²&·*êxŒA|clFºÀÌ ì)ŠßžÔá²S™¥½wÜ,]gŠï_†ÞÝÓβí„!üë_á"cï7st†wÄøîìnl+zŒ½]­Hd®ð±Äi¥áNnY'̵[Xbì#™ô´¦æ÷¼âúˆ®ðÿÕ*( ‰Ò{]xö?núÁü§V¦§L г§@e®†Gˆ@#000010‹00•0130²010‹°J3ûåƒ1ÇiA‘"UKE+93=KQ#/*=I½ CPI€Í4;"3=±ºD#d¨f¦%ä¥&h耱 µ ÑìÍŠò³òêðñáÑðÿ¨+hªØ$gç+e%[‘ƒ,Â:Aò#üç ØqYˆþ-Í‹„ø/WT\œVRNLž^AúÿþPŸ‘b€»ó…êë×Ú“Wq0¢–èw{ÝlnIV=Œ‰/»‘*%Mùñÿêf¢…²{%1Ò´õK^9— ·’æóUïyHª}úôéÝðv‚’‡Ì‘þöªh2w3c6ú3+hcªøL"Ìu ÛUô!;€¢ž¨š( Ém$~ ‚ǃ£Ðl‘i#ÜHêS˜É¿Ýg(5ÛºŒßÒ‚Õ€ Cã¯q ‡îÅÔÉ2Ù-6õWáe”uZa©IÐ…Œo E¸ü™6äøŒÝ} «uO”€?›.3ödùTK1åºä\®5´XÑ¥yµ¹ "ý#íÐ2–¸ü#Ta).²&àÄaÇø+È»üú¯ÕÜ.ÔØ@*ô)þNè&–…þ#0€Ë¿,”ÿ9¡ÊòªJ¢Êt..©éÒñ[lcwŸº4è¼ ãŽ vA²J&ÚÏóÇdiD |_çåw I-­qëSF3¿ò>Óx_¯_ßSåék}X¦y~½<¿?7¥'ÈÆ¼êa fí|žŽ¿_O_áLÛ°.;r™«·¿_žo'Ÿåø _Æöøý½=mIQ 1 Åt2ÕNg-ê—€x™¾öÎÈcÈmæ2˜¨d²õ¬örlö´›ðÑ?jn­å –‘˜™~ øà‚]I¬ÁA"éEήM ‘öƒ7MBXk/‹Ž:e×Áçç…Ô¡F0!5û!ã '»)*€ƒœWÿö›’=s_µC`5†l¶@›µ§AضæÖkÔúÄò`®Ác?¹ Ìp@-IXx.§¤š#ÜQéÑSÙsxL0¯;Šîdf*­êSá,:‡Çß>Ã`é·pF·añtAs%ˆë ü=§ž½ú“¡úõ="ÌB¨ÔpkSŸÁoUdbÈ=ź%Uí R7´ÌM©øøb8ß ¼ «g±"ME„— %ïRš P&µb83˜TjeV_Ž6]æ’t!ÔYЪ©¸ÈÒ;‰œÁ”Áe+O±í ­#ÑÂ@Ix×P÷·*^«´nÙAÖ†ûJÂüj·ŠL~· è±¹Ž8«D N[v7¦ä6#¡‡ã…·ð¨ÃƨäÑžuŠ÷^Ärü@$püð„)q(ÒGgZóeŸÆ6R23㘫—tÎs(Ž:»è6¡ôÆÐxOyú/9eºÃ±˜UL¨'àÃûb¯"ŽIaL³¯â({ny*Ò௠ôÍT1v«Îõ´¢:uD%ªfÝ#„ZöùÏ1Ûäðj2Ðñž<ïÇfSùÛ誥˜Ø~|‘ºX‘Q¡’´ÛÒ^­¿‚™ÿi±×ïïŸzù¥dÏ߃!»³3GU”ìKWe~¿U–¢k#•GÄÌr›Å¹Œ2Ìk4†,»Áç¼ ¿d#ê˜?i5xðžž15,¼â‹:Z^D&¡ª ª§ájØÞ¿5Léé5li9 ߊRìN€oüYß¹`ø1¿&6_òbxCvì¶%¯^Hê­9`w±±5¶Ïõý“(wÁòøàw°Ì±Óh™u÷•ú\B{=4ð¦£s_¼ƒQƒ¹ Η>¿ÿ‹ÑM[cúg"þÙ\Üÿi#LLLíLLíŒ=õm,í¬ÿËXý/­3–ð°ÿq˜ÿí¶v±wз1u3µù¯fÑñs²ÓÓÑñgR5T´R3ÓÔµ2ò3ÔÿkwÿS˜ÿؼ,h ÍLÿC€ÿrg´þ8øÇþ«îúÏù/A ½\LÿÉëêahnjçB¯¯oigéòÿqoì¶DîþFúZ"ª»9QD½–pÑQ±¤MY&ÓÖ–€š$‘VR}|Ò™¢K(èB…±Ó5°º9m»¶egË¿O¤2ﲂÍb•aû¶tkæQ/æYÁt’…Ï#*USiÒ%£ú%>Ù6tQhèˆE¿é2ËY±“7ćyõ¨˜k3‘o5-¢ÏϬéÈ 5xôjd+©µ5Zë] ÂàôÑ|zöà1*2ÛétÝÎ õõεvuoû"ìÿ/¾ðæøà­O®ƒœ„’/Ú¨Fl‰3A“9ÆÅ_½jhRäDJÏòuðÁÌnüY<]ª”¶¤YDÒìè_kžžLõà1 ¿9”¿ðË? ·Q/Z7Ö>vß ¤vÚðÎâ?OÛ«PJÕ¦ÿoïFƒx¥¡)éVÀxV`WiV=Iº-Ø ²ö¯°œ4ކù냹×@õHérø_Ö÷_aRgïOg[±NNFÉ*Úøe¨û´ÓÕ Pb3‘é®'KVêÖ qÍ×U × ß›-bÙ<¾¾ßç9¸Š oSlèö|yŽÊ}h{H¯Žéývz£òØDY<<ñEA2½wb8É›Ž'¯ÑzhƒÀ}…ΫûŠ-¯D`~WÃ>-$솙Û7³Eanm>5Haì9iÅo Í¿ÃßÎ3›ÏÄ®5xBgŽÍ¯ˆÍ ªì“ª,Ò– ¼ 7?vo[¸È(^Ã_M-//Ç–{’P¯–B#$˜‘§3v´Q´”0gØ¿”BúÅߑ߯dß6)ˆ·ÖJ l>TÂ9"¬VØ=<÷å°aZÑ›ì1ò´Dó`ÿ$£¬Fˆ[£I àÊIÈ ÆÌ–…y}¼a±ÊÙsÚ'Ç7·‘>àÂ…1'>„™-³±aâ×Ofüþ;ª¿Á/~ôDwT@e0„rNè«Bü¦`Î<Ë ˆ–ˆ@ÛqEŠJçl>±ÁÔy& 1eÈ(¿ê•ä7ônÍOl-‘Ý(á­\Øš¢kׄš9¯wdÁH»®}5c]ÀÃst®||R ƒˆ‡&ÙlÖÚ3Î\¯¼5J*"9z8 Ïl«Ë4B¼S:‚|®ÃWú×6G„®¢Àf#-~ƒê <šiì¤-Î[}öbò> A ㈠ê@¾Ý@nòþŽ¹ä ¯—€–—GGdŠô±b$y¾Ψ0Mˆ’c>ù˜¡ÐÙ†ãœH+ÕVlÕõ¯§|xÓúSæ·¡ c3=ø“~~›ÔÓ4|d:O“èTrpíè¾çž}H Ä{0<ø\oºŸ¨Üƒ`«´{Ó˜ ᛢSù$ß}7¼A 69D™AÄErÓ2†NÜ٫ǽåzêzòƒøgÛ ˜y“uŸ~!5þÌéR$iß²]íkù?ÙRÝ*–±t²“ [®ËÆØÄþ`Þ øSáÎ$ÞÛ¤3GM™ù{˜všÑX³ëqèÉCh2¸faôTÑ‘:Ç ÆÃB"6Ç~ÅpNàO/kñ–ðŽ«œ$Ïí»{ ÀO!9d?yqøw?@qÁÐDIþ@Š㙀òf|¢•¬Ç¬4'¹CóÇô}UÙ‚K˜a¬Äk²ZEtžÈ³ÁÈ|Æa°Eö#©Äl2¦üí¦‘=Q¶W¥Erˆ'‡OhbšRÑÔÞŸ,MDáBýÛÈãž(6óý€/RH€=/:ßÑ4•¾G‚ÑÔ/TÚÇá¡ ò¼EÊç~~‰0æ¼GG ;›sZ“h‚N•–Òs©-ßä¹þìÏÂüÖÌGƒÓú‚-ˆ@ )»bòzcrí—1Åñ Wöæ ‡^ä÷ßÔ ÀE ¡‡û]ù"‡Ø× .$M-.b=ûùÈRç+j¿™kœT–?ifz)æçSyݼqÈvÛ@ºúÑ â÷wÉ £.bÀïõsíX~*ÉsãÎ6À ³­ úæÚÛÌžœïu …•µ ‰M­…ñmµ:é&¢ái bâèŒ8·{Óß O¨&[Zš[ùÐ)´RÓ$‘î%DÌ/‰³¬UÛJæÝû|‚8$˜‹A,¾[¯8~ýo‰Gõ;¢uÅÊ‹RÔΡÁ Y]šSÝQ!Á±h“ó<¾~@ÂÊ ¨õŽX ôùED`ɿҚœÿä@J¸¾ŒsNà rlŸuV·}-«•1¥ëäQn‰Žw À.× ¶#5*£5Ïzô¯ñH·¾TÂ/ßøx0~±äÈ÷¹iÇ6Ÿ(ïi-¼íYD•$°ù*§Áäåoòò'úEºüa¡Nƒäšg€ŠV‚Þè§KФ¶Þ”?|7¨žl2h“9ÚʾóðÃ2 •Øœh–är ³FUF¹·Ì]nZug=¤ÓÇ»ë‹üùZ’×E³Ê{Ž%‰b*.>wT2Z©5ëO&‘šó.ÆÂê°Ë3jÓ4ì¼"KPDð'`„*æÄG—rðœÐ/¿ÞÚº³­ ©Yr'B—àDéU½ã˜ôµ… ̦{$Xgê‹ñÆ ^Žk¢£a‹^ehä@-0À ªAvÅÝÿ`0Í™V\MØý,Ô`à>îŒÉÊLN4ÆñåÊwUä¼­’ð[`éí×çxÖ©$rŸ¢e¸Þ¤â…ªF½^ =#Σlʯk_õ;g,­·–ЬƒòlÉ_YÍ*Ï(TeVó=@=ûö» G{Ç9<  ÏƬUð£rÃBö„ë"KÕ“£´ÂF¥wŠw@¨lÞ?¹zIÛá4“Y\U ÆqââQ±ÌËJ¥~±Q¼=7>n [ê#æ·Á\N… ë,Êß8+¾¸·áËô»¹F8£É\–ôŒhᆽ~¶áàe£ac‹¥Ž$¼Ñ(hSÛ£­Ï¹ß/§‡u7O~°QŠ÷ÃêV‘ Ÿåv *°†D½“Иs—¡U¿\V> ¡ÿ_B  <,˜ ]¦Õê1ÜÆ€i4E9ìoÊ ™÷€vØŒmv+ " ˃~v˜¦ô¨YÔl™ÖœXÎz‚ä­Ìì]ΑQŸ\ðj™ó*F†ƒžïÖ¬,Ñç¥ÊÖŠ9{퓺0ÚúkV]×acCÓµ@¬¾k¡Ž¶w^üRí6û2òow?{ü'ïož8,«?¼BŽR(óL¤Z~¨¼ß¹´¸ –)Ü(ÞnJO!Z•ýU¿JóG E­l¥U¾!—Y”â?0IÒº¨¼·À:a‘ÓÏ©®B*áð`Ý£E!èEhR>cš*z7ÅQÂøûu­ÑîqðÌÐÌèßèiAÍ cÙ‚×7“>—Ey˜ÀóŠl¥ØÐéX ¬ÍótO˜Œ9¸«‰­Ф Klz·Á}•û²„¿DõûRoº ”rJ,0X6š@°Àù¦@ªD¿ÃRËûcIpŒ€Ç š‘cîç6µÊ#Û sžw¥#Þ/†t~¡,ļO;† È#V9ˆx§Â4ŽxG¥CU\ ?É’DggðÙSÞNHÌ¡Ñ&‰Üܤ•ú(œÖ#q¯E„Üw{¢›- ÈHH·ÚQÁ±kh‡!xš q` ÿ12ú>˜‘ßâèKÖc«&^xäÛ³hƒ”í(Q¶¥D<Ï\-fWžÁ¤ÃÌ~zâ7o¶â~wN'ŽCØY.ˆ$¦¢B–ä@ß&S„bÈ"/ißoOTEΈ^OÔ£ûZ‚šPs²û¤ ”ª2QÁÚ² ÑìBs–²O¬…å(nŸF(n¯²]¸+¦“z8?h+»bµZYvûAª?@¶SZ–qîY&f¾ó@kBËä‡Íë¦?Åu×Éõ[Ý•GÔCs—)­]ÎV–Âf¶ …Î`%OýYYÓR”\m—œÓ¨Âª .ïŽBXG¡ÀkDbà§{iÍ®QÖÊ`›‘ Ð}V”¤.kP¹Ù0fSUiÔ´Dö#JÊfç–ÕåQ!%§¾’àâ•à(V‡|(¨%™#*.²mf „•§/\)É})æµ4 2¹*-è¾·í G•×ü§^eJص Û(õ$¥RtW(«{ÉRuç/ß”2Ù–±ê»d[%¨'óžÔÐ_ì²í^è#TÏÆQi˜®öa†hpìqœšø~I‹’”9”9h:}Ù7x+¬Â®WìêR9®ÝyÙ³,­ª©—ªXkËnëà ª?¶M- ¶ §ÚªˆHc¥’Ô¨XÚY±Yªt¯]~·ƒa©ÛBfÒü3Õ{’³÷ÜŒpP£Ås_ÉRkNÖÁÓ,0:=»Î=r"yÑyþrͶÖt—/FÊn«Ú÷Xi· Š]ÕÖž"™4Q¢Ò½‘ÂPÊ*}icÉW4â+ŠýŠïd4ÒNÑðð¥ÕcBÆVT¶ž"¬yÅ͹,ÓB³wŸYñüÅ?’M´óoýÀÝ‘æ@JÙÊ&TüEèÃÊZÏv¶ÝŽŸ( þûNAê¾›éô}cÈ ƒa£Î§ú ¨ãu±ŠR¾AF'¼&]RJ°ÉoȧÏpC·ØÚ»"œãdDÖÇô%q÷ðý–6·GW; I§“Œòþ#é|/§xÒ»`§ñx¤]n´ôí.ëZ_ˆ,t™Ý§Ó‹ϯ¯«ï½:ÓÆïÞçî¦qy[”%áS:Œ ]¿‰jçî:.Ns@ÿ LÐ|÷X1ݲ¶™·O¤.’ŒKÓÓá)â±ùwg—)‰`pewTa}…ú~Oík›l»ƒÀ1œî“¿e"§´æå<ª“RS´CHl¶ «Žr÷¬â/êçë­÷ ö'å(a©Â 8=†z·0IC‰HÇa¹‚Js³€žtjÛƒs¼o›êcQ ùR$ÝÕØe®À&tÏrç8™)Uaso“²¡Š‘}dQ{ïê¸RØ«.ÚžÞ&' 1Ẫ|Ö!,O’ã!pX öwPé‹lÅ[ʘ€Ó-[Rl>l¼zÿªáßì1½opÜx^ËÒx^ÅCÏkEʇ2©p<•÷QcùoQ0:88ÄúeÿÀ›Å¶^|ÝŸi¯ó˜]¼Ü_Üìd=xÝÁ™ØÙózTÐöÆ"ùÚÚ9_ª¶†ŠM%Fô»”Š»mÆQ²UêyÂ0=OðØðnJþ\DLÌkðaõC:`Fn#ì^"̧ùåV—1¯"†ºx¯“4œô“Ç~­yé³´ö&öx÷nwó¦4k©Ü´>Þf:I“œFêômúPtkúÞ£…‡Ñ©yÍ#y¼O_7›-Eí1™ñ5´Üâ_Àÿ Ç yö夡 3ÿß‘½ÿ] Ã¸VKÛG]‹—ÝR¬¸ëZ£Õ¸¾a,=‰üêìOS)7ÔäøJÒ‘‰Íw–$sÖEžÌÛyRLš)rU»`”©&¨4*Šý”Úƒª*м Êò=¼ö0ýý 0èמÿ¬å‹²}•Bç¥ó¬ÿíïïwºsÒ/gÎgW_çýL•Â/éŬaľ=,!ád Ö dDa¤¢ðß QNÊew©ånÚIËï\Ä“˜<„Ào^ŠÉànêKNE|¤Bݤ³$CE¼URb&ªÉb$¤XÆôGÎìf ™ˆÓ ù¨Ó ù‰Å‹„™ä"¥Sp‘#ÐÒÉr?–2ѧfŠü´¿59ˆ3ME>j§ÎEEyÈ'ÎEžz)sÑe”“ez'ÐEžŠ)ôQ§¤Š½$Yé¢-zI&é#OQ{‰²ÔEÿVY ‰t|b!ÅZyê¦üQæ£ßÆ6Ê• )ò!?ÉÄ\(-V´DŒv.£˜ƒ2ÇA=IeÀÿažÄoǸ’ÜCµ½â,dC*òùÖ 3`Dê€׎AHòVwï‚E§©ñÀ˰îµïY?pV;¤Â¦ÅZÏâ{·•ÆSOR„â"ä a N^©ÿQÞžEûʼ}xy…Áß…Ë!ñêÖ–úG Kב޾öfþm_m·„b“!X[þó–0쓞Ux)1ÊŽ=wkkØ7Þ6Üy“ Rç8ç¾U7wªØ0Ñ&x(áΣpèRÕ.–·[!DÓô4iøë7´šÔ‘%Ô+$–<}Í–4Ó¡Äàn²xþyÍÏ÷´ZâÐèR¿Ð·ÓÝð)gé×ÉËíË î¯MøòC6߹ݴè“Ã}‰‡<›Ï-V”݇œ°­4è!ÆÝ§lY*h"E±ì6‚É»…pi£zr!„hÛ¤Ö‘ ,3Ä™z‡¤v3BâàÁLäL£LôDÃZ/ ñÌç½K3Z†‹_‡%Vì–o×d¸©šªJEQÒãÍÒ¬µ V|¢ÃlÁò"1¨öbi×í4½ëX9iÜKZæñ'¾PËiâïf›.l–Î-:¥ê­Ø~µ›8uwK°n{AS+oàÝYÿr—ž—M±r]À+æ%ûÎX5¿?Rfôl_±¡q•KTuÔq0M4\Vv¡Š3{­7ªYu¶Ìù›nÉ ñÀ9®_T¾Á?†ñÏFgð´n? Ô4ìÍËFêÇZ™ñµqm•RYXÔÍËÞÝJz‡[Í"xòÌ’ný>-A¨»ñô F}@×½BZå«fk8Y³2Àà:Ûß—¶Ÿ¯ÖM·££6'År['J`¹šræZÞ«(-Ý¡trÊI’f1?¿+ÓT¼Q˜<<÷8_ÊÂdÆP9qñ,¸ø˜pûþ‡òÈÓ×–tsN†aÙg€8AûÁ ÞEl(ŸÆKU`nä.ðK™1×d"épý GÙR0 ½Tƒ%å"¼´Ý5?³ß$þð\—š ²Qò[JSýíÚš u™/_‰»%»oÈ&‹,T33ŽZ{'EˆulÇø~‰âŒL7¸Yñ…É!‡Ìp2_ý!+>HzŸRXÊ<êÀ®2ïC*òM³ÅäÛèÏåÏDØfx½kä.Ë=‹(ÞTBÌE¾k*ú(:)Ÿ7ÝbïqfëýôRç÷A°¤¤”SÁ÷˜¡‹ü5-†QàÒfŸ‰ ñ¼ÙE£:7'#_õ1Fý´îaèJL÷vXZsÔ"ÞǦx:Ò=°+4`=N PDÔ¬ø‡ö@f$”&u€ï„ÇÚ”ŽAÿX× ûGëÑÖ¨bµÄ§±AwgµvîºÕÞxËFHˆÌ4nV…dš<¥¿¶ "C¥TChmoö!_6U¼sãBLnõâƒÙ/Œ Ú­Ø®ËôiÝ4oÕßÚ(Y7mºAÞ3ÜÚ{!Ø'+̺H<î¼É`]Ã>©¿vô9K;—ºÚÒ0ÙÊ ŽnÓ(0hHŠÎå/o€Éš)hÁJß¶n%d4ô´t7ö÷Ð¥ÝÒ‡'?+:âh³ÕSצ]q.#½s—3U{Ó¦{ëjæ’'c§ûÌ™ºBÇNj[¾ÉEW* ³|EClú%Wjt±îãZ¹ÏÀ·ÆE6·\g[|°Á÷h%{™[â´]…˜¾Ç´ÆŠR}«®[Øw3˜ÇB™ŽÚ‡Û ÙvzÇ+z¸Z섪^9«Y6< jÛæÑvaª.Ÿ(qÍ3 ˆïÌ©@{npø;ŒtîÙ© å©Ð¤¡õrlm ‰ól¹e»¼rHD¤žT°]$ÈŒö°·°âaxk¾M4—Ñv‹·ÎGñöYÐnkë–…â\ ;N³ŒÞS™ÀlâÊW1• Ú–/Ù Ö{ܲ9`_iåmפ¿¨`O[gHùÀ`NN-T\s¨d.¾uÃxêî†WÖ$Êç1Ë•Éa:8 r˜ëÈÈaGòœ<2|Ëb!R~dª™²óqHª ‰¡–‡êk]¡!SDzp*Èm•FƒqÞü[.bTˆM'LWí[MQ[PëôЫ± ÃÚîöÍÝÍý¯= Ñ ^¿,<ÎÎþi¿¸Æ"†*Ž”¥+ž•{IÒ°°‘åÝjâ¦ш_«ÌQ-o«}‘ðö)-bjãTD›ì)zÇ"ÎVK£(ó%ƒ—ž1t(l¶&alHØÈ:1ÂlšjPÕ¥'ž Éz%k4@~»±žšá6þ¦å –j¤n×RÞÄ ޶«“ÂÆXï൭]z[¦xp콸Aöcn‡–š“âÒϤ‚šT55…˜q¦ ™³™m–$%ã ãÞÉ´‡´ï®ãGІS^F0 åâD ˜ƒ@Œ§Ç¥à$© à:¨ŠÎA ‘òÿ Œ…—&×>`]2Ééðú¤ òYã“VÅâ®Aºê 6è9G’Æ~ß‹µR1#v´Dújý÷ˆ`ï&êÅ›’ÊüRuŽßË=þ¢&ˆ<^|K/þ¦’н"Üä?Ž”•J`Pƒ>cêpw§3®]{vý 2ó{ŸÄl”¼K@îuÝûâk™:û¸sªÁ¶¦Þ¯PYAQ;[Þ›ú Ã7@K7ŒiÖ Q4#~”3wÔè/#dÈwö†¸nŒ^ÎH”U,Žº‰aÍ,ìr´>‚v!7Urâ!7 ªPä¸ï/ðFsFõ8¬ãg}º:­®wó…ä[ggEoV¾j‘‘×g¬Æ¾s™& T á‚5ŸÌB6,<01Ó± ©;ãËl×*ñÁºÃ˜ wá²é 9(Ëô.Ów"åŠ=òA¡ÅéCr}¤•«…Jb- ZO³:Gð ©lˆ‰¶]mÈ»aAóiÕ×Ìô©U¡Åñè jÝ7F;“Õš;¨J¢§±ŸãVË<ØZs /ëÝÑ¡#*t£Ac¿pŒÒÌÉèîÔè¡„1Cò¹b‡¢÷ÀÈ SgÝV@$‡Œ3ïÝö'sBòÔ¨–N]™«öë ~.z̦J(I p`|íÀ¤Ñë@Ùà;âé vªáƒ°/؇Êf7­ ]ƒ¶p: u NÑ^šÆôJÃMQÆ»üõ ñ#ÙŸÖ±HüèЗ¹Ò` ËIˆÉ»Íà^ª2›1bQbÙ|¢,´:×çš6¥®+:…½:²¼(’aHT—M]8b–#“âr œ©¿ÜIÏ$‘)\ŵ}O¤b@çŽk ôlÉÃð…Ç_Ú’Kuœk#¸cžˆ|ÙÕlºƒ-³«N³Ö#á͵O8A1ñôü Ý0G-\<¹È-å5Õ·Õ3ÖÈ­ûýžùèk,;âIß ¦ñ|@ô¾Î„¿‹“ºH¡áy×r‘-îx°„É„ì[“$Fy̰©©KþÎwYô~Ý´.Œû†eKªóíÑ‘rè«Ðõ‹´“{¤&—0Ÿ5])ú tÂ<7LiXÊËgÅ>:£r 5ëä5JÈjŒÎùÛ%ýoÖo AR–<©»l;÷Èö7£c›Ñ! 6¿%)yV*ó5 Üsã#AÓ£:¨xçøŠe&—Å\'$ÔýCNwb†2Í6ËA0£ô}嵊Ua·„P×ê!òÕrÎ>W\§ö¶1‹¶º*`Ü¢8SÁæ8:Ÿ™à„z0¢>㌅/Y@PöCÒ e’¾ÈSȨÉ4Àß«¨·È‡9ꨔ".S©y y†Þ|1O:Yñí ¿ ñ¡O[aU§s¢i"ž¢•ݼ:¼ÃK|õýéRÎ΃TIØA¤áÓJÃkøË±ö™¾Èºnx ¦ 2¬¡OxY–Yb1ø…’lY– ‘µL )¹¯±N¹]¥½³ÎäÆÛÏÎ[Ð:í£NåF;ù5Yçz‡ËÒ ÑÎuÿÙsÓ¥®Î[ѺOæ:îrá—]O]ß¼êßÈjÙÜù6¬øž+atŒ00äæþ{"} ÐôXA¥9®Ϙ›ðT6ÍÊIj¶È }ipÀŸý2Èñô‚%Èhè5™E5íÔ…_Qû­='ÙòÖÞ[¢vc©VrçN­—-¸ì¿®[ß5˜ ¦`õvç(ÓUVëùn©vçTã¯ÒATÚü'Ø5c CŒIû&üùÁ ¹tUS’óÕÎ"ÇhÒLt“†T€±|Ó© ê¹ÅߺVÖQs‰:K’z:±G{1GÊ àt"™öªÀ cf d| ò#“©Ýj•6@¦nY5‡TÖv½ÜnUlŠ`ðøDŸö„#tÞ7ví¾²ô 'Œ)vîÔ¬7úóS>.iá+"·ÀVñ>Æ/¤Âx,ò{ŽWìèïÜ¢?4„ÌÂë 8ƒI”¶+w΀¯Üƈ‹(»éD¡> ªGgd!mä‚þU^ÙýQ"A&ooI.bOs~ ÛsQvhÀ›øtÔãÕM[Rªí\S‘vp¼‡QOú±õu1ëe¡Í ˆ£¦Éï’I³s˜ õHäÕvü<*èq"k®žÉ|_’IbêÂgËó¶­äãòe¬â-º·8'G•°KøODbœVH¿)ÉSö˜L$ý'+1Nðÿ»$ãdï¿?&0Nü±)J™Qˆ™‘‡Ÿ†ÁÃ-³<Ë$q 1ãl˜d‹öjäFÆýˆ ¥›œÄ\nòIˆðn–æ\jòýeÆ“øª±c¿á稖þ´-16 )¥=_ØÒˆ’öUPuVž?R·s‡{G6mæ“õU@õ¸ÐG÷ oÜç'Ï’,•’äî¶…§xÒì8.þÛ!—ÏÍ4ÇïósþÅâ' r°¾ ¯² ÄY£¸v9àÑéšF×ÉU ŒÝ¯”P÷C«ÔFhÖõÓ‚¬ÎþjYx妵FY(¥¥ç@{N€Ê:"* ~ñ-ü¨´ž—z*Ñ µúl#rõR¥VüÛ GÑQZsºVÁöy)­¨Tù¥³²Ó¹ªæ>.Y©ÍùµÈƒ†üÍ^iš× mµö²_¨°ˆQàßaBÆhsŒŒ¹ÕnžKPKÜ6žÿ´‡Y´ÈLntòx‡¹XdƒýŒ-gëBäƒâÙ]ÄÞ?é ÍÕO’ò$uªýS ö ÞÂï0Â_]Šߦ’ Ê«›Ü½ÄÔæ©_!!”»CëiÉ*ËýÞYÙ»L»oÚ“‹ö·ÎÖ7ÊH>Í5 ÏI›Q‹N{2æä†1e…:Øüy"]õÀ‰‡ùS÷{WŠ¹Ë däÍøþ9äàB³ÍãýŨ­b¶S;E½n‡h/Áô.YŒŠÿ1¬A;¦O{8rÎ"aw,Z¥nÿföp¨Ë QÁíláðK·j=“ Ð`.¯šð¨ñ\£ý>iÀMìÆø¨f™`L'±±ÒÑ¡?Qâ9l^äò«u´¤/‹°Y$¦~ fÝ àfÊ£,ëâB€†ê„qw`Z†¢îµr ~,U*O$i<]ï!À,–c"Äp˜Ì´Òž½€•Èâ•û FÄÂâ6œž“‡ yöïöžœaz%ÿN3PÌ÷róCøƒæ›YM×/Z‡=ŒØ3»3ÖNNö‡%}Žñ¸ÀÏ`^S9ÙöwÂÙ¼ù1i“.ql“/Ž›W!v½ÃKxm¬ú|×KqnŽ,=šd­ÅÚ¶Äé‹0 _Üg"‹áKâþ\k/ûÝp03=î»î •—½ñ6ðr„¾,)>ýØ;ö,a‚uà¾ôì­# Çètÿá‰+þîXºûŒL$ÐgEõ‹R…ùêR¶×/a¶ÍèzŸ¯-ó,ŒÛÏ;º|pÚ/ý½*ôÖ“a‘³#¿Êã7æÏ½S~ÞQ<ƒŽ{Ï¢eÈ£'¿‚NõR™”ðugø•3 ½ K0,eœN§œèض¯8XßÔŸoúlÉ}EgzFC™úbÿ$¿Ç=%/O§Ã㳿ØîC‰>|ö_øÿ‰Z[u%zsh ûæÛÛÚÚÛÑ þ#ncodhãLçàé«4õ_`ÉîOj´ÇAÛ™ÂpÐ*.Kj–è)ËeEëÈP‘FÔ‘ï:JÎúypxJ8sópæÐ?Aئ+‚Õ¬mB­s );&\™ƒZň¦'ûN®\ÁR&ÈS`úµµh RA±P…Š0ÿÜ»0…ßYÐ 0é LPtzHȯ Ť¯)B@ƒ+ï3‹ßHìW-´$» ªN ! :AÌ@ü±tç¼â~*v’Ó§M×]¡‰ˆM¥Lw…ožÚrñmš°C#áÐéHfÕ‚óoT¨þ‹˜AÆ›sRÒh†K\Ã"a.çÙ“‹(7î·Û‡I’&¤a.²Û™¨IîºüÄpm„TD̽/œTº‘Ãq»ô}v³ƒw›¼C·q½” ÓÊ­aϾvÁ·cgñ†˜4ì#ɾæ,Ð5¹ßĦlj²+FÓmŽy(Ñ/øT#F¤¬ßh£©PEÎ’´ [†¸Ê_Fê Ú°\„HW2𳡂³½õÒªùîæ¢1­þÃfÃhûçh¥zÂdÐZŒH„…ʼ%·ò ÜÉÝ‘õ!MTñýÖ—Ï‹V+Ÿ÷ÏNÑSZ›%tï‰:.)]VVr3¼R+ß"ËÒW…ÐvEœÎÔÈ»ódïNH©rÅ£¡ÆÛç+7}ƒ›£.;àêeÖÏù †]5!°i´²_l­ ¯à¦R¤\Ú –°µ*á„Gó03³®˜µ7&è¯'˯5\ð…ƒ p3ä9O$¨·Ê6Òy-v?V(çfZ*o¸Yç© RäÈÕÆ NR ROªî‹"8*ÙþƒµPýžzuï1ë€ØèJõ+Ésu¸I“ìÓM)€¼xž‹ôîÐB~\®/Œ‚š”+Â¤Ú \•I“Ø¿U£gt[¥?Tj„ƒ×}WYåÎ5ù¡HèLå™MGPP:Æÿ½ÌÿÍè;»:8Ø;¹˜šè›™ºü«øo3ê{ö‡b~g08é ŠånñªQ•,¼*ÐÖ;žUTWGc ±»º®PNüï®?È#œ:xRÀc662íÍzqýùŸHv {µ¨ß^ˆuH˜ Qi#XeHns\äö™áÒÚChôŠZÄ0Ji ¤Ãé)H2‡À?a:7û©Òjvé ÓMê ¹#A3EœHCx„ú?”AyÂ6JÍêO¬lQälœZ·)ÄVc®•;¡³ÝúSkø­K..ܺZ2­É\±NŸŸŽ·3;gåAÖm:¢ˆS@~²+w¯JCÈ ØC˜Œ´%@êS´”#G»PuT¨ ‚ß*çß!IÖL‡Kë2£ÛÁ dœ˜FÂy ºUGÕ ªáDe.¢Œ†JíÊH<¤‘`ÕÃ`=•Z+w‰è³CþÊÕÂÂúù•»Ñcx^ãjºq C”e—3P ‚2¥Bæ4ÂNÚ9=ÁƒU±ŸŸÿh¦™ÖÉåØÂÜ“†m,/Dû°e™¢‰"œþJ1Bµz>¦èÕœ"žäÝMÑ(}ñD ÷jœ²ÅÎw¨‡hq‹?D.F( L¢Âo uqÊ32^~‡«×R=%Š=±|¿N“7!6¼-ùçYìÆqt…ï1§PÅ+šÚ|0üÊ–åP ¥Ug]д¯¤ðéOWߟ‚‹‚ì•L¾u!×Ã3¦vÁ»ÿ5Ä×ZV­ÖïÂÒ«—0J ý9—m½»ÒUÁ÷õìTi/Z9\SÆ:«‡K9údE…lP½¡jæ Üì'H Be/ÄÙÔ%à¸-ñìOg=<ŠÃš@Y¾Çv¡‰\B´Ìùøqq¹P0ÔŠOÞqPÏðwD£‹E˜QÝé<Ô³â˧å1—ù͆óðojÀ,ݱÜf¿x¹¶*¦{¯™Ÿx£N§ÿMy¶~Ó:~ÇcMÜÒU½\áuîþ«ý|yîáxÅ“ØÓíÌzý‡Jò½‹–ZKÙÚW„î …ï-Ê(O n†É›a±ž´žF»Ÿb#¹Ž²¶IŒïÚ+¿÷c!ƒÑyó m~¬K{nÉüž]n5ïeâ;Ö&¬ÅQÿT.ž õµɶNì§ÇlZ…§ä¸Ý·‘‰¹I{÷"+H¯w’.ï³(ýeC…×üU}ñt7{ !^½„tWá«­2à¢\™•0X„µxäy‘Q§:‰H¬ã:~ïðjql—‘`ÖhãbŸ±I¯ýGѲX)oMû#‹‹ïæ8Ñ-Ÿ"d®¶y1„YˆàL=RpÍB¹ûA{QA†J~~©oEˆ„Ru¾)ö×:¤¡)2W¬›Ž’™¬]z“!9táCfÊ8÷Ÿù¦5làS%ÍÔìã)ŒÁ40PÞ°Öez–8wõŠÀIµ—zûHnX*uÎú¹ƒ±ßü$Þ‰V´Ì[yRk—±¢š4J8ÜûPÓ‚« ÄÌw¼7îÚg_4¡† —v‘J¥+OA§.Œìc[Y'ý>¨»IŸ´ŠÏÀËZ‹Kì3—ïZퟤEúìª&„¸ÕýLÈOcjæ|†¸Š‡bæoP.8îÿG"=Ò‘wüϦž 0ýÿcSg4œ¶Dø&Z‘¸!-Ķ€¡Ž¦›% 2äȇ\°°P›íÄÜ gðUgã4Ïgm/ð×ö5=wƒÐ Ü哌×`1 èäš{¹sïÜÞ>íÜKó[øëvàº7ÖB‹ 6ÁD,°9œCÄ* t¾&ˆ²H=(©â´ èæ:åéÕ™ÚÍ[P„ôÈîÊ­FdëÝ‹!öê ƒØÿiÁú[G_r˜-là$].Ôfh•ñX?mÏ;ÇLuàž‹»©Väª_W÷iú> ]Ø¥ êhà0& {žVaXåzl¼.ùå8Æ¥„,¤Ëk<ÈœHT½Ln>MiSQœŠ¢˜Õ ˆD£_åß-«dšÞœrõ˜£3]1˜oob¾=žÅô 6+|±ÕÉú´ V6çh+’•y¶ÚÀ%ˆ{f0‘ê_`lëÄEõlLþfó\[ü‡ ®q©p=â0]ò`ú¬°1Œ7¢ ™i³ƒ!S"âG{´Åš )ë)õ¡‹‹¼è邆I }”çsAÈ|°Q($tq®Ü$×l¡|Cr1G*ñ_T.àÊÅœ~-õBoBTlÓ˜ê¨ÅHjLÏù?±Ê~Þð=¦'~`Ù¢—Œ›é£Ù'±3à­pºG·|­¹§¯Cae3âÜÜð¹i=¬•Á«ÛX ™¦LÅ&CØp»n -œÑtE“;Œ€~…Wˆš‘,mÓT&ZS%Íw›µ¹î#T1ÃT6ÕõŽxjœÐ|4®ŽëI1ÐÅÅœhâöÝȇ6¹ÁçjàñÑÁ/x¤kj›è³ Áv½¼Ry@GJqdÅ8 øZ4ºý`.ô¬ñî8Õæ íË*!¿€€p _¿VçH`°vpLôùáVùUç°?SÒxäPR~9+¸(R:ùÛ3´m-;ÍmÛÞ¯fW‰Æì®‰G›ü-×`@ünpy±bE­]¼5……×Ð×, äL Ÿ®¼¢% Éž¡TmÙ³tCI¸Œ§e]î"[üfš£5Œ¶‡¬.É|î\”S{±5í=ðR%¼eI¥Xjéõ~ü²Ü\X(M/&ç±è+Ë–N"q|Z’ïa•K¢:2®Ê{Êiþ´Yù ‘>ñHêñü<~ýUoJԈמxÄAA}hb5Š °ÅòòFd2ÓÞ—"liàü@4H)ç^¿F‰µÜò? ùêÀúd3}™±' 8 ®ñëû#¶wuvêØzÆ’oy¾&•AÞLí:9Z$¾Ö©’¯ñ7p¥GÖ™Oô3Ê:¹…ùýceÎÖë(¨táðùpñðúPg¢]!°Z§Võìè?ýðwp;h€|:‚ÿýžalîdïêðo‹¸Ñ²MÞCú™ý“±/ȸ„Ûe¤ôioÍEß@~¦m_i6Z2Ó-­ô÷n—9IÌc몺«sKгÿùùÙ›Ÿw˜)îø­[6Œ?Izï(1‚ ÍÇ‘ê›Â†×‚—àO|hP­$R&¹Óظ¨ ï{¾æE²>º€'ð¢hP«¦Õ›\8¡·!‰$Fý=èÆ; ñõ¬q‰ŸD·>8,¯˜o‡ò\½‡µ†{+èF¯­ —P)Ư›ÒhqSÓÖ–v8•an„ý,ŬžìXò†Ó#/DMk7$0ÈÁõÛðYÁåLà<ÆñÃ9¬æ¼6’£P[Î@VŒÂ®ö­4ÿö+äæuš˜ÎjoÂvŸ¤É€F¥KA“,Ài¡¥>̈́˙—†\ºïþÕˋә¨_ Yõ…‰ä)4SÞœ_^]Ù¦·™ýÄ^ySþgH`å>†ºs ~ÈóBŒM,›6“‚²e È4ÿ‘wz»ìyZ¢ísàÂ2!ã‘·~3hD]`ß[i˜¢ê¤âª¶Eäã;LßðŽ p"Äø<&­G3x(à ;ëaRˆËÜ*S$ðªUf>Æ™aŠ_¤ÈdLMÇ!ü8LM´dÛê­9àó ü Cž= Bn}n=<Ìu‘2tnrY¦¡t7î0,rí*©Ãc¾ECá›q[±:—¯ÓÊC…ÉVþ¾ôXt’Sþ·‚Ä´Û× §Kw­Ú4ÅpÃ.ݨ™5{ç_Ø 2v+C+}9º³è¶Ûw#VOƵMýFÞ„–•Ç÷—ìA,{u kÎ(}úÓ£ 'Nýö?äÑŒYž¬=9ÑöÖxž ©}æ+>®“ –ÛìÒÐHuD’ÀE¥„ö ÂÉzz_Å’wVãjôèO+—+Çg×Ç+qâJ`û¼¾vÕ#™Ð»ÇuM¯u!ù  NaÈÂ9.žào;†wº‹ö¹Û£\‡ÑdM°ŽºüÏ¥Wn Ðl¾'+|­×£*·0”?-[7×O(lBüë*KÔÊÌCÇ,` É8ëmÞ(ÊrnÅ/›ã4‘]´¦¡ Iç#ºÃü•?¸Ns{Ã^œŠÓ⢜pF@á¡W-i§ÞGåÒŸ K¾Ïöé^Îîÿ\µÜU^›‰¿8ŸòFã«9#‘u;ø¹8ª¿ÄHPàÍÚ'â»1§½¦!¹· ± ‹¿oiL HŽ×D{7Xì‹•«>ÂfÓ8«WÙÅÇâj•o+J¾R!ÇwDËAê’ò/y`hj?" qj_•¨ 1Ž»–r·+eV]6ŽÐ—·¬ÄÔ±º&ZnŒoXYäèpû9êV¬MÇî;ü䀒\UsU^ 8—‰¸#N%Åõ`«ÈÁéêHl[hPÐ׃¡§c›€éO³Hì/wôUÜ¥ôW9äàùŽPIMG‰ì#)a>HÙè*¬µ¬‚N' R釟ftýÆL\ÏSž**îåJ¢H¹xƒÊm8f¬ìcmÙLl‡²`DÙ´ L(÷ë»#Ïë¡4`$”e­ˆ{I®aÃæà„»lÍ-èjŒ´7úÒÀƒP#<Ô|ÁNIi«èIº^B« j¾yD4(Œf)h4#­o¢SÞž ¸–;›NóeÒ¯„ëɨRHÍα"¹’5qî"V¾ä%‡Ón–¢[NÌðË]œÓó(“²Ggk‚¶Í–²Àæß}ñH‰è&tòzC´ÐšÈùÓØQx|ÓŒEß}·zrQV$Às(Çw3NÍvúßH|C%r©‚:,Ææ^WîÄë—‘ÔB4·\;9dÄÀ°€éð¼öÃ(¨s)^y®Mñ& üu\4pPödØÆRT“ûä4°àDvI€žôÐPÞáà˜OÔ¸ó“i**pI×!ƒtp^­ÃÝܪ›•ñkªÌì˱ÕÚ§JÈ™{Çq1YíKvýˆ +¢`ß‹*‚˜ÚÙ ‹ª–e¥Ã’ùŠ\V†:“w×é¢Y¬ w’OÌ ›'׃ÝÃ.ÖÞx’¡jO…ˆÅÏKÀ¦ÉĆXÎE3yne´¨ø~î›æÎË~¦íÖ)ä€êkô©ÖÅÃýÅ~Ð>º}¹]yUºZœÖõ¦[¥k.‘µ“,«žZ×a,à, ½µ°…ïVËáa¬ ¨oºS™þ½.e¯M¸pŠ{jsœØ¾Œïc½¡‡nå¸Gª”cƒŽ?ŠGõ¹"ŒFÇ…*Ë$eD xuHÑ>–Ì6F²,ö-¡\5cV#\Gua!ÁñĽ%%mÕt”Éûuxù PˆIé(‡ÏãùF²y0~>ï×ûd‘ŸR:¢ŸÇpÙ{:y˜$ð.eÞ·Šzknïázõ f&ë*ò眈¦o Ûqzª†1VUÇXx˜fJ‹®%õ_$æFô,oiÎ2œE䛋QÞ.)$€B»ê^þl»lÀ’_ج,„Q ÙçSH’F%cަ…ÎT€òWn“R¼‡2 øNbˆ "œž9E#"œ*åîʹÛbôbÚ¯rÏ*µ-=¯“È ²_ù.”e!êd“þ¿,ã1Í@mchâ/ >›)ùg@ OÞÈ¢Lã,D¿:·Jé¨åsÉBÆ87G"Y‘ÊàÂPÌíÕ!*üý,ª»Üz4ñ7:¹Ñ]æ*$ïxg >*7rí` áj¿ s•Ws§î¶âá<â^åê~ê ѸM€#ŒK i€CoV3y[,*CáMZ9÷µ¤¯6½Y1—ÊÕo‘ó)0úô„X&Óö¿Êc©%¶ê§×˜”©rYÕÙIÇÎÈ/Ô%ú|ˆçCk4 ϻǟfÎ'‹–žJzÞ^k¸¯Ù¦uõÓkUûÉG¾fèƒë˜ wU“£ÀèÊ€2`"ÙA­fÔëÄæ§êÚ*óñÒ'Ç%¶5”œaKo |n¢ŸÖ Ü õÛâAh?è%æø a? {|·ÕcŠAg}R*èkŒ áÉæå;¹½}¥>ÔPC)½¼äê oö.Òôå¯Ðäß‹ØdrßÒ¤ß_-ÀO *bÞžÞ­ÅUÅ 4•Ûl媊ïòþÝUå=t¼¥ PÜ,aŽ;ß6¼Ñê–¯Ô_ý^6·$Œ{ê‚écXtщçzŠ8@³*Ú#ÊÜ}\ìá4´—Güá”®Å/‚-žn£ˆ±i™¾+0ê7^ZKmLä+­ð3]­DïTôÉ)€&ûÏŠÈQ{å}r6êZä´ù—hÂ1ãú£ê—S àÁP¬¾ >¬]'¹‡Ÿó”½Àµâ¢ïÈV׎„*¶hϤåé»o×Ü8W¢hïôÿ3øÔñ§qPúßA‚ðÿà a<£%팣ŽÚ»ó” –FÃoJMÏEiœcKUB*Âä@š¹:OZh)«ëŲÄ{qÛÀ*VÄ!]µo®J ¥VPDÕ…ò°j³® ­àÐëüùú<ž”Œb %i·×ë»÷ƽ÷÷ö‚·‰ùe }ïÃP¹„ßôã í;ZŸ-äþ|—°3l!$Ð[ÙE¦Á0ŠE4—.*–¨ºHi§q¢šKt!aG²±òVºHq‡²±ôvªÈYÄ¥|d´¹3!s—Š-n‘W5uAOÔCy›¨Ûò`&BM‘§$yW‘0>š–Ššå°ÁMâ.c–7ï$¥‘b³<Ä]ˆ§2mWR÷\EÍBΣ<4çh»ƒæ-æ~ö|»IóVSÆ»–Kþêä7Û¯ªÙŒ(³Hzß­4ýÙ·£ˆ©¡.·~%)±“_,½”Zqýnäe§ìšÔ£‚\´óùˆ‹m5)yô{ø®¢[²Óæ/ndÎ;Êζ3·ßüž«6“„UCÌí‡ì/ÝÉc¶NáY#ªX° »!¿„É0üÜvUÊùiÝÖÅ” [5}é S€È À|J?¨-&€!Ô«ÖÊò“nš®‹F¿Š}z´×u\ð‰#N^Α‹ðŸ½Zwm}½–;éèØT)’¥*§4ÅŒ°ôwm—ëOÜ»Á²³1ÉXôÕûîÛîŸMÐ +]‡»7bN<è™ú-Ø`è´ÕÞUìuñŠÐ³”Qu¶I1'vß…øÙ].‡Wð LQHê@4ãëgéÕX ›I8âêwSŸ &lÃC7š‰U/™¡£Æ©Û[ ²°¥õ¸A’ ‰øjõ†N¶GÛM&ñtì×K%_[Û7»å›Ò©!Kíº§,65ìè5\-Ms_Û)ÞLAúè6gÎíUï–…v=ÿlê8ÿ\Æ“ô…e“ÎÙ÷Ø|£æ©ñªÞ«\þ7;øC·Ü!‰LÉVвݠ»Ó¶}áh‰{1‚ï@Éq&sáÍ2H&ò •zMô~+ÂÈ\Ë7˜qÒ HàoŠœ±¾Ò©”Ôè¯arínzÚ1gä5«°Ü8›ÁÖZÚÍíËZÛ0A¤Ë”½óŽÄ-ÝzèU(ÝZ‡è« T’-íò3²-èUØÎËGpV¼‰|PhÍѨÆ%#)µé• Ù8Só)¾hî ‰º&ÇA"R|µYG:KfdÔbË’Wµñ;ÐZmôÍnJ[d=›)«x:¾úÝ¿ ¥àw•‘ÍϺì•ëûVfŠŸ•ŒÀ9‘+ޏg°ÍL†,3(rÄK@¶Êsà KýýjËýФ1žÝÝØ7óðY<÷ Ê¡ðT¸W,ý‚p:rGç'3¾O˜Œ‰æ!›l(N5ð2‰™íªMQþéì(Ø×Tw“0µ,IëRBй+e¯„;E¬-p²Àš«¼ü¯ÀVã²5tɯGûö+ÁÃñN¾ehÁeN¡É‚ñ…vM<»(Ÿ :R^ßÄ^Ý<—„¯#aä†\'‰Ò¸UÃîç!»„zȼ©#Ódæ²dL%îø.–`èb­ÑûâF§‚ˆ[¨+Ãzˆllø( E‹mA{D…ŽæR"øC mm ­Òû[nš×© ’þ`˜C½5ð0ˆÍ•¥’vwƒ‡ iŽ`…QšFÊp¾9o,óúx‰Ý\ન3N>óúÌ{­Ia½ÿ(Å9b%Iˆ:l_½CŠ¡=Ý´ô'w½P@›¦ÐĺMxÎÍ-‡e¹Ž«~s$w×'XWoÔ«¯©pÙFŒÂü*êk’òŽ &ÞUø6}æ˜òám˜×aO´ïIrT ®rKÁm1èee¬°$¾ Ñ[÷¢)›ä°å1™ÅhÏrH¯ýÁ@íä+Æ¡/®é8‚wÁÌ‘ÖR^“£® ºLåMê¹K˜Ùò›õlô—n•_•5DèiަÔí¯±zó‡ÿÝÎÛ¿"Év å6äSØ="l B쌕svîSžñ½Í-ñ@E¯¹Ÿ%³ëF¦^¯3ŽÂä³l`:Š‚ÖÛ†žÜU’VŸê(X•x[8#9ÅôIñ¤½[[ÀNKbÛà‹ÏQwðDM×Ó>È‹ùšÊžåe‡ò¸ºd¿rAèìôøntvuÝQK釯lgÌž²#Ÿõ.Xsô´Ý>, ;j'1»›¤mþ‹3$Xf“CD‘Ë}ÊRzF þË_âxø{W8“ÄwlÏ7}þ¿Ö u懲€2gÕ©Ä#JÔÖ!lNTZéºìAË~¯w"1Ù<K䞀ÜßýNÍбZÚä•î•„§, gU˜Á¼:"’Ï0íò%Ç•/X֥аrb>dvsÆ ¤ÖŒÎ2h3.û9“H‹¿VyÓâ€(kHcZ̤J9÷üìsBblT¹¦o…ëçŸ3go–?=ýl’†·°üWLJƒ14KœùYVy%.Hmp ˆÉç\>¸ÀDàÔÁíÉ47c>R2·â|8EGPhêÉÒÖÓ1¿fõÇ­•g_9 jµÞN8“@íð™Ès þ³muFÏØõý< "9ˆ_ß„_ýçZB8#R~Ÿ+ùËø? ¼¼¶'¿DJ §‘ÅUõùdúgL˜Pu98/­d ú¾~Þû´¥>µ:©¨/8docÇ{æo(HìQn"T"å„ ç•IÅ”y'‘8zã}\¥=Šw÷™ R¤”WåDEòhT{C›É%gßç¶½kû™/PÏù$è^i$ff—‚kÌs o Š #< ómVlÌ@¿oÉ&T_Z:otvœ.F;C¿›«pd©\r‚qb>u‹&fóH¹^° ÷åÈ»Ï@ ‘ß²·. ÅâÄÍKÉ$û‡ò`éÈð%©jq‹Ñò·lÈX•T"OBE¶îKœÐíRJMù÷$V3ÉN£í+æña=«÷Y)‚¹Ô’5¼“sbáŽÒEž™Îüjm‰^e%[²J°";Ã9ÃROÇF»¦êâÚ'§ùÍÕëD¢Û„‰Lj±¾“Éy_zƒ1‰ó‡NªÛqޝ6Šy²9H¶üIp¯3[Í®¦¿]“OÙù¬!ø¸’Êñª1¸)ž¿;^é'h£‚%›ä ðÊu.;:·öp™ëµÔAwÆäCWá‘AH.B«p(âMÓf. )¢ÐcÕeWá5"$gà~K¦öý&Ÿ†ï9`œÆ™•’?7d%–vÚ 6ìûÞ`áÄΑ«¥‰âyýL›øà¡Á,eNkœo¾ŸÑj¡4[™µU>>ÛìAß"Bž5QUžSlŸû? YšÅž^×G“ë/+…Qí‘W† ‚c÷}ë¨qxKCU2N!òû°óÏÖ+…˜öŽÁêÇióc\›iòAðX# 9Uâ£Y>P^%úý$-@¡´!TI0éa̰‘æzó0Í·ZR 9rÑŠ“¼‡¥ZxÝh½•€\8ýJÕaª^ÿNÆ8¹\krá$¶²F…ÌoÊ<ì§ ©¼7‹‘ì {¹ñjWNFŠž­¯-I½><WS‘Ü|KÂ{Ä«­5y&àŠÈõvwç«8ãr”–$L佪)g.·¡²£_Dç0k'G>š\ì;9b´3âyÌXõsšälÅ#òü†q ëÌ´–¡×ió¦?ëŸ.G'З#w¨q!ƒÉZÒ?µ6@õe¾…±{ð·äFt’0"ïžÃXÎFúZMnÀ …¸om©çoïæs™ÔázN‹¢”CŽô;»Ó[|1ż#€裞=¾r“Ê™C(<\Gâ@D E‰¬šGf ÖœÖC ÍkÖ7F2Ä«ÏÃâ—G(«:*ËÜ—÷E¥6ÙŸˆ.*­“’æàá &5_N ÛCš»è¡ÜßJ?LQè §Ô¹oÆÇ'£Yþ’Ýå!:¾}Ò§ÚÑ'zD4>^³^1IRt\Vü•Zå X‚Âó7e¯íXµþóØô¼²Î »)ñ:^Z} ÓéñPº]¯®†ýz…°îϨŒ¥µxyæ"‘<´^¦wæÛdùÓñ”6Çgh¶|º>™Æb¹ioÍm2Õd­‚㺣»*Ó<˜Ž[ïýQ[ŒªReUÜ9q¿^Ì’ÈeàÙ(| ŽY‰vÓu.ÃÕhƒø䆈Œ‚Ú(Ë…; $Ÿ1v^òL!ýÇG{暉2«j=Wï(4Ÿ޹xZ´¬ZÈ…FÃû¡cÎRú¨ÿX"YX„m4dÜWüÐ9FâïÃX´” üÄm땟ÝäâGäÍÒ˜{ÌÄt ³™D Zð !Â0¨Ì⣘*|4Ñè¹L:þ@©ùR¦ù>£m#Ç;Ì!Fùò.«À_ºV¸ž?e-ï«ÿáÏçí>VˇQtJ¹pYŽÆz9=ÍŠ~Ho½¿~;DõÑïhl×qÇz¹z9ñêÚ7ÐD²kµü€"ø±m?µÒ¦Û«x«%oàÅ‘ù,û¨y•%H5Þ{·a”%~^£É–G?•rFn íÀvöþŸà—ÿºÈÔV^¶@ÀáøAþßñCK:Ïm[ç­vÄŸ:ý@ªÂL…ç)]û:)×Ú+uy\ncît³$p2ÂLô(Èù(ºÿ»[ ’ô‰×fg}9æHZ$ š×“ ]ƒïÙ<9”1ÉÖŒF¥rþýäd·œ?l¢íª×¿ûà¯T‘Q:§Î ôQªz©Çç0<ÒD‰’ˆMLÑ¢˵âÔxðO£æüÑBƒn×Ìèà13Õ o86I´ÃL;ŒJQˆmòJ‚"ç5(ÓLúRÉx¢A´É/ÓÒE€fÆíQÂpFÿìomãµ…=8W/[u¶µéý–›ólX—~èpwóòqòse ³œÌ¦AUjúê’Z¿œ {@½G~ÀÞg:±NC¡‡¼S‰ûª‹ ”ñ cÒ~HåH¬’°“ZARÁ½ÞÀ8<ÄÄÆ×mXªfüü\X¹H…gÙÍÓû=:œaõövñôtñóõr¡£Ûxù½tµíüþš„›ÛgüËÃëÀ Xd:|¦øœ ¥8LÑDM¨3í<“ I‘IŽxM­âC¸NÔ‡, ¶eH­Hƒ9™Ò¯£²h’IѶ±§"°Øµl”|ý"¶“„éó cÞ “DÉ ñš+Ö¯¥G‘6uÉŽõWv7JòÊþd¦jœx]ñEd-¨V~’)R¼Ø3‘ó·…F$ËäÄ|]ª ¨¾„”Á¬ì¯a”.q°…âñIU–C7UüÅð©©RJÕúr“ÑÙÚÊÓŽÈ£ž¬êâ”Cê0!—xûá¥Û™Ï ¯d“fÓ‡ä˶9y>j€/eT7Ǭ1a|í]ýÍû³ÑÙæ“ÒÀ“ÕÒµ×™ƒÊŸ±£Û{@޹Z e¥ïždö0icì÷æ"èÕ0¤%{! äBÒ€0äÓR­ÔIs|1¸5rsò?ruô{¡ÿÒº8?.H}ÜojnNô'9”Aî`ϰàôÓŽÀÁÜ.eÅ—˜¿._K'#ÙkêSKMÉ÷—;+/Õ÷‡‡Y3MäìNötoÍ7œf6뾞ŽnnNž Å»ñŪœ¯”iž ˜þ8±bŠ“€8uluSïË›®]£‡²’G~z ‚þ¾ÔLd/Ë’Šf6³‚0µ‰ M 2"¡…Ô¢\oJtfgžd0²_ãH1S7 €hG=ypÈ=YÆC TŽOÙú"%0}’ %8×lñÖh1ÒaõQªÃu-iô5Ls‚±Ãº¸Iޤ…k ïÞøWƒûK±‹Vk¤J›êÛÙ ƒãgœÈž¿§—”h6] RÒyR–=-±Jþb _6`bHx*›U¹@xmù€Jÿ@üÆ3»Ý[„èË&–h{ë¼ #YÈ#`èR‘ÌhØ[y3øu*`®Gw}ˆËë´´0ÑiÆúÛb=CLfrób–áòÄQßæÐêãÆ6ãõ ˜¼à„P?5|§` ¯If):?K@TÙlo‰¾`Zã6ÒƒëŽM¤npÜ"‹t²ŽåÝ, G m¦Rn+Aùµ°òq¾Àtž®ŸR0”èì‰sq³G}²TÐ’pHj£ÕÒ"Ys’ ÚÑIÖ Ð{&m¾SUÿSò¾ ã« …ß¹õ]²L‚/o£é{裒µZ@Ãð $ešSO<7+ Iž!ϦÊçd5„õMÕ âW7X ¹'e¦£ŽæI;#zÆJ{ø€áý͈¿OÛ VõäcRPŸo —uxd=¼.Lïm/×…KÈBÅˉgðRzÆS”»«ünAi=‚4ÊRgT-¸“‘ÝHç)'N¼#¾¶:üº"•sß3.[JÒeÉÒz­<õé1œgæhj´Ì…WÑ"¬¶›´%«\,33ïx]xeâðã‰^ÚYk#áõ¦-î¯Î9wHC¢²È[«Ú€À, gŠ’74]€øÃæÙ-âN”{õówAÓôÛïpµP—*!¿íg«ù²!Í¥Ý D„'`*`xà£ÀõDÚìŽ7ÀÿN_]õý¶¿\fV{~Ê“z݇ˆ&Xð?p°(~«ã“ÀK/ ¤[Áñ9‡ó#¸ÁÃØ`tòJX.9ÞÎ['NÒê$ªP©>ŸÞAÀêSæNʪq³6÷²]H@¤¸è}-ìõ®~r™ú¤ê/ÌŸú– ×ÎîÜÓšÐÄ‹H/æ@bè&° ¶*à’O+í{„…V©4;´‘Y›ÍHçê9c‘Æ/Ù Ud4ÁDeEVT×ü@À+Öƒ½šWs\/³ㆱ„¾ ùãfV£»—œù‘ž ´:è«] mØØÒçÆÑÎÖDÎ6Xä’‡nd­à$CÝns‹‹ƒ÷UÏŽ‚ ÷Z®å«Œ ¹ææ-}“¹'G+á÷lƒ­(fPÕ0 ÎFwœaÜc-‘2Sv%,4õèX÷Lmpþ¾Rذ°Ê&‹TžÇ^ûÀìq kt2ä°Ø˜§ÄfvïÄÝ V9û'—w¬ÚÓÞп?>œ¡?æŽÍ¸ 쌰¥vPPf7n,Mw~š.NíÎ×;scù¼œ^ÖÏÆO½"t3Н¥5L/¨Šò•—*$VËZ T°'åeZϰ"ä\¡ ¦ÜÝmQ×f+Ž×ÑêSÁ».ÄDnK¢ŒMo3tWç´Q™¸~¼yê‡v¯ xªMö;w'è¢0¼Pµ»ãWØqÝ£àá¡K¸‹îùji±Ì„ n‘ ?›šmGFˆnËUŲ[<¨À>± à;%!<Øtõ@ WÕl‚±–èñþGŠ:¹¡ÿ‰Ì4tŒ<ñG1uîàvñÌÐt®•ḛ́° –¾ÒÉæ.%0—0X¼ƒ6ž­¢F¸.'ÀméyËÌðjá"‘ÑolÞL ^¤U»YÏðLDB]ì¯ÔüßxBV4QÃü·àz‡‰7®xCk#°×ãU3´9s¢¥\æp®ßþ€iÇjç¡by¦“ÇÕíU­Îùòð¨3—Zã#vu˜JjL ^ØËç– ™»÷Vaˆ±; œÊL5Pßï:É´s¼Å^`v¹2çÒ×Ü&}\ø\Î15|'E†8E8w«ï 4¦"€Æ† XIÐ?KN‰&öòš½³zV+žÊ« î‹€!¯µ}t©ø>wÊôÚ¿¾g›k´ñü5L•™+‚èÝeŸN!Ó+`û² ¬ [Xæöð?Í¿ÿÎÏ17Êk ©Â;òd^c÷ÈÔU˜/l²·i/n%t-Z¸,¥«qgojÄ´3ÍuMqæ}?4J'Ó.ç¬îf(;swM]˜ZÃýµ)ý³M˜ (R›è"žÕ\UÜÍR×ÕjX™ŸÓ³Ì§ ÏÙK0Möê£êçCV€Åé [`®õ1¾fŸz²ÅîÒo%¾qp†tq°Ä²ÊåxcÍXölçaÝ2ªŸýâv€}­†Tw¶n‡6ùh–Nµ§5æÄJ¦_Í­â–ïÍå5úqú}zæ<öY!.ê"]¡Î]Ü:j|¥ø ÃFÛÅ=]CUv.®Þ›§W®†¹z Jð«ÍÔNˆk¦œi‡áÊ,üg<àA(¡I hä뺬ßP5{XG”£=°Ì²yñªÂ֠î;ê‚e–W,Á¬±8aÙ”£CŽ+++é ]ò¶õü7¿3©8}'‘upUó,ÃJ´ŒÝÆ}¬ÕmC˜ŸšÄÞ5Ùáæ²<¶¯áĶÚÿº T¹åà!ÔòeØØÉN©{ƒµ‡»EÀÌM±ì€\‡ÎÛƒ¸á(>Û °Ñ]¨´wF/j"GÔ5i4%à!ºøÚ*ÅØzî @­o¨¨#ÌuFóŠëÉN„ Å5´ ݬšÓâ¤Í‘õE}Ú#ñˆDФž”ô¯êÙÊ.ŸíèfìÆÙzH–ÐÈ*Z¦ô„ºÌ~Å‚hp:Eài+„ûLyYÒ1Ù”µ‘ªkæT¢™»!·mµü:- h·•«vÀa4p’ö.ñƒŽÐ0CÄ˵Î^Ë0ÜúÓÕ)KÒÚz _Ónd ÿ] ÕäÓˆ2…­(³QIˆÚ°ÄfÞÙ »ŒÇ2ôÌsºJÂÃfHï"„.ù ¯Y‹WQa[û­[fÐt DW?³Ý̱¼XmLÌA…i°§0¤¥:éOŒû ›7¹¢óûí)<Öá8‚£ùr-@Ï*6Ú/á}ÁV•¸á¾µûàeŠ—“Œj+î¿#Ð)Q{]¹)²çö4Ug´g½w~Ñ]™~%×Íáõ]¶d­é~$b‰]ÛLL[~6Ó2hêïÆY`g¹¼Àa JEa`W®»â"¡8F5Æß¼ÓºLu3ŠÌÎÞ>œ‘/H‹DžX´BˆÀâ#m¹¢ö¶‘¶z”Boo4›¦,å׳àÿ¿¼ÒaêYáEûÇj2üŸ*Œo´fœqÆ\ý™¡¯@ƒˆ %q”Ú"C%&…%c,R2‰­ ý– §ÜÌ8y£á-×»®ƒ©ÈÓ©;NŠ'Ë¿@+¥à8ËæHÑ÷œ ÇŽ{Övš€b’¨“ ØÊ½½:õûüðûÞ:¥÷db¾¯²ñ¿‡Á¿•ýñø154YÍ¡±Ò<[ ³ÏX¤N=!°„O=1°†¾l„UÂä@±Pë+ô‚,@¸Šç1T_#F œ°ÈU|1ŠÏeŒ…V¦d…Ê¡b©\Ê As“ç9 s--dªñP#Ì œ¡°ÊU|AŠ4ÏsÂF-Ï»bÕU®(DÌ œÑ°Ì á°Î ˜Ác9_ì`ŠÔ Ÿ°Xô”_zàrŸæ9â_-8d ±ÛÙ *‚*„ 7AåáðXé†ÝÛ]¹ì}‘ƒ0ì®*»_hæ¥lÑ:pÈhÖNR"’ba«>(ÐØ»z²±-¯ªÊ(­>2.TEŠˆ]ä{¡Ãý´ëQˆî_ÛD­'Z‡6£sÓÒǼÝÛòrøz,É$äƒú¢àbo²=ÓrÔkH¨{xNEå-÷„wÊaß²gâ˜Í”mÂoâR¡Ñ·³[5#Ñ‘X~{ÑǸ*òBíÞÞÂïZ÷¾Ý­¯k<÷ž—ikv¢9sIÖtœ¹y¹Ïñ†ÓÌ[°`kLµâ6¬!ÎÙ‹4mH鉬@©™ÙqF‚Â3Ü.ŠÛd–/XT9UÚ0rjÚ¹ ™»»óîéŒÆ5¯“/³U_"¨Û*ãžZM¨Léà2ýë!\Ö5¤$$d–Ìcýú ¿2Wܹ¾ù/ÇÁ†/ô«œ¥a?æÜ!Â'[ÍÈCÊF=+ØÅ§&Úíý¤0Jò+‚o\³ÍFKAü!³L³ lñù¼È¡,öikyÛnt“žDXP¥“$ä¶d±p¾§¢<…¶€þÞ+å;¥”[ˆÕ•‡m3MBÉ€Ô‘Œ¶dƒÜa?šØZ[Kkï6q‡ ›"k§DDÝâ}r÷\ñj†j£œ&òjý‰¶8ûþ˜ÆµnÀçákíœ9@G<öbk fpKË»7ç~`h—à¶øè‘a“%òd@P\÷˜<\{³4^ãGR íT8K1ú˜ä©‡à>ETn´~oÿ¹4¯?u {æCo2Tæ³’>o$µ»Û …Óù’k¡WTÁí=ž®sow£>ñ·³æïXež¬í³ØFžµø~P"·VKsRó‘鶘Üpiáò*’qèˆÇÄJšÙyzO-²8Aᘧ´t{f“Ú –:¶[*{Ê0V[ãLgÈepW¡Ì³“Ø7Z» Uº`%êÎøS¨UO³ Ò~z Ó‹¬“²8çTíIZKJôe„æ}àO¡îâéaÿjõׇmÒlÀ6Y^‚‚|òw¬òá_cì¼nhjkŠì| @Ó[kTJ×Çå$ÌÔU*1£$ººH én{¬-IŒ'`OÇ“ÜNÅŒ†·v`ìBtI¢´XôKä!ЧFOÛú$0<¡¢ÈÅíá#¢~?aȉÄ|Ch'¤¿]…'„µsøŒä<ûx;¾¯@ïî Ÿ¤OV¤F®\ÂxضÂZóvó§õ…ý^Ò˜QäÌþÖú_@nþbÙîWO¼6K2Áˆü^(8Fþ 3òO žû¯u­cëù0ÈG[G_1«–@¦^[øaµL%{ dWì÷ÇÐÚÚ½Ÿ¾¢cDKІ\Ë\Tf¸T7TȬBO¸‹È[ZÞj˜³©XÅ¥I£¢ÏO¨[†‹Éqn ‹’Zr­”9ýÂhÜ-Õ/Õ/îu4óþw½¼w×%’dÂ\ð·ÛDÜž‹´~‘·ƒNŸÑÖì·IÛj»s” ™éf&fÚ–‹Œ&<{¨æâØêd‘#“‰D;p 0á MG°Ãµ|â!g Íqè³zîv"L9Kýó„†2.χ%`z§™Ž¥‹‰Ù’ôP13è]ª~¢Þräb’xÀ]VôB:°Šîj œ‘ #*×ó¯ ¯É-©|°¨ »eŠÝß ýÀ 3]4ñ§øµw`Jƒ|˜÷Z(¿h/ Î ˆ¢–„ïÌHØxüå‘ÉqE~ÓsëÛÐIíÏÊÈÇa\"þfq¥; ¼1Vf!×#ÚÏ ƒÀýÀr$ŽÕÊ ðJ-•{TRŠ“síÈ,âT ñ…ÏZýqíY¾‚1ëJëSìF«Fÿ”«¸ M 9³ÇÃk½ÝžÈ“‘;µ…<«:*Åî‹ í.C 0TÈADŠñ “ü;LÇÉöVòð ˜lrnGÞ^Ãc¼šZè I⿪kÛŽH^i5ˆÒ(F±ù‡jr±óf±Êl‚ÃïvçÿaPêù9»Sz:YœºRê-ûöó­*JîÀˆ¿ý'ºŽØaÁl?"ÞD3†¶ë6DZg˜¢Ó”c0x\Žñ‚ñƤgÌjùš‘êòÑ÷UÞ @ýßå Éb{j[Ñd§Š­’½!³ ÕB;$¯Ï¤¿À,‘€^Ñ/ÿÔ#Ù,‹F›y5‡ÕVVó zF£é•å°Ð‘æìÓ†×Wê­^Ú»%/Á'›¢ ÿTK4³™øöNY0ÆÅ^Qи]ÚAqËV4c”—­ 6d“×8·Ö íC*]×(Pš°Bš`ï;!J¾¢ÝW%ÉF0–oJ͵†K wt¯ ƒèê9Pj‡x;Žš2”t‰#¥¤¾Ñ‡"NED,aYí¦X§6ãâõù<Š’’Q“áÃ*éP.iCÐMH9R ÖÚñO®×èwÅuˆàrkb—ƒƒÐ<°¬Ebuä  ®î¬m‚fΖ pª+$»”œÚ…ŠZ|“RoM¥¤UQ~Iˆ"¸m±ÉÎ,Û„OÁ…ÑÑÁ®ì-hUþ›(óàšL,·7L fŒ/V, ¼FñW63C<£ä‹ŠÄã ‘˜ýb€6ˆm.¡c ƒÑUéwox¬1­iòLR.?øO™Ûmƒ¨¬ÇTñ&=ö¢¾-„ÐçE¨Dcªp”fÚ¿|}{e$°·=èÈ<%)›û-,u ºƒNH )‘ ¤òÔ`í ¤n‰°B£yн¡¢HüiéVþ÷ûÀQ»uô|z*Ê./‰2soMÿŽq‘‘¤®Å÷7Ö<"Wâ´ã¦~⎃r«Ãá)¶†K0£N¾@Ó¿°g ¤ÓcwÐ!|(‘W0ù'SrÕe4ZLgY=ùI&'y¬PÝiŸqÊ2ZÉT›þA ý¾ ,5Ï/.ùüÓ\6Œ$þ®¦Ø™V²ÅG³ƒšv+-Ö)­Õéš ,Œò·©¤kyo¡ !=bl9’{±¯ô>áyÿîõé»¶­€¬¾`ivR_ëYü‚1íºÂǺÍdX“·›ud3¹˜*^å-'-p}n º0Æ.k(‚¡Ý¢yÜÙäÔòÜÙ¡ˆ’Ð%êÃ9Ðéì²Ò¦®ðþ~]‹]ùx”””uÒR­Ì1˜Ž>þÄ fŠJù%öcÁâ }uŒ²^4Ëq¡M_ÒâÇÑáëâÁ åJ,ùjoÅJû÷öë«Egã*ýtÇ»à`‚Ö*o¦ÅErNçÑØ. H¨üt<:°w’³»Ö- ÞÁ+—¨« â°³+>ù±1õ–#…‚iÒv8î"Á_x ‘'<²È©¯xyëTWK"®Ý?×›)q™ °@ˆßìFU+ Kª{’’ô©¦qÚövþŒF:OÞºòòÕYn(ãl€|hSà\žªdè;JI[ §zsõwŽz³ÑIÄ–*H.|fò¥áÏ&®hr¿üpcGà9ÿ›¯ª¿Î=ׯó~¶‘óË=Ï×ßgæ¡§†FÃ>âÉa®gxhJ~ãi-¯ò縙…E‡ÊT´.ÿÊäqüZÃ|‡6¾`ÌÅò²Ï´0ˈ³—ʘ0O¶ÏV$À”V©ÔÍãc.UÖ¸L—Ëü«éʪCuF¦VjùÙ!¬CžÜ½¯³_ÚQìˆVµ¼›jQwÎ9 X‡ž´na †|å}Š’hqÖP9,¢^˜J:$— ¥`ÐsŒj§×y~Î\÷Zô2EÁ±G'ÛAþ·§!»CòŽK¹ËxhÅ¿‘Û¤æŠÞ|V2³€f…³sŽBCÔ³Cÿþ„’R ŽÆôQh7½³ØÊjõv+°%õ]>‹á^k˜Ò,r³‚æ•n’­kœÌ ížI€gÖMýõõ©+(Y$«R6pk!zE’LÊØ˜Ò‚æWøË=ûÍ8µÊë/-–·½\nœô.Ï(TîC-½ëêúñç'‚Vnµc¯Üh°Ðhx}W¾SHôæ² €ü$ê;¹f"¸÷úæ`[˜âI#…Îh_½¹˜Í˜}”œÈòOٌ۳Ín›E|ív;?|Ê摺ð½+0ûԾӇ̧õöŸÞFÅ¡fësÊ]œƒä&ü¼úÎúÉOrE6 ¹z c.¸¡5õ×1JüÕ¼‚h™oG¾ð iO¾XH¹óÕoH<«IƒÉõ?BþINÌ(Š¿³1LupUô°aýrÿ  æõ›Í ¸E¯nå3oZÚ¡…7hòCù[k¾“ ^ä…>YvrŠ2ÊJÁóU‚ƒ3k¤ÒÓ뫹Û=qNaàŸT`Ÿ‚üÐx¾ó ìo‘@•‡³öI²ï,}ÉØö/`žÅgNR[Þ­q¾q∵#a2|1 Oú4õ!ò{¤€+F¿-½ìúÄÒÛ#ʯ'NØ›6jíd‚¼¼è)·Šëd gý«u@Å«Ñïžîn½^üŠkœÈi[g÷ð"äj&礧 y¹ÛY˜³ËWìï6Å…<Á°R$­t±8Ã÷âèï±î ÏÀxöQöOAÃðP³ úØxÛ¶mÛ¶mÛ¶mÛ¶mÛ¶­ùþssj滋=S•«Ü¤RÉêÕ½ÒIp(RŽOßho·ñ¶q‡#qûÄ@=Ò^ð”l±ÕBï¼2ô)…$Âêt¯¡Ó<*m‹òÉà*®¼Õ‡Ç|X¯¡B‘‚vøJçÊß‘(}%GÔ”¿£¡ßÎôo·æ»ä*n¤,‹ r¡ç,ÞNq‰;?ŠÇeøÃæ/U“Æ>/D >ï®ø 0hu9ßïž#Ùæ<‡p…ìg2¨^¸lsZÆ®)ºŒ‰ÔÛÆj(¼V9Ãr-e4̸Õ(&4›+K,º´+ ñrê«^-°Ï’yLy¼âùÇ Å^ä^ªnýÔÿü¤ú;–0µø›±ý*Ðîp84æà¾ËÑ œ,ö!²;MýG5’ñìx ×0FûMV[õ ¨·ÿÉ[ž,ÇjË΋zó <- O¥ýsÖ‹’ï ÂÕ˜@ïÿ÷‰1;Ú„f"û]^À‹EžM¨1Y^vÄîL²õ(pì² |÷©’-òønÝLN|v4rÄE]ˆI'šåð^ý™h+BÅÒ[œÕ鬔9—·-jk)§Âz‰²ÕˆÃjz%°¼q$wTþaG…ýÚù нt@1Âì•?„ßÏ¥‰[=«r&*§jV•rù‚™åûÉŽý5YÒõ¡ w5\H˜¸´dtÙÙoAë­£2LÞëx·Ù‘w±ñrÉr¼íûÀÐòfÌå »MÞ‹u›æ%ïSý@­Ôê‰n ÍÙ­¢»U#,ŽŒý>™ls®9ƒã÷³T¹Mµ ¹/™ ±Ã*.©ÛËÂê¹¼½¿¢û~6 ­8TNî”^ìöðóXéñ{»Mìb¼ßœðÄšIZìM1B”)y½ÌýtÉÔX\?Rýµ’Ó¸|úå²é‘tÁÚ1¼x¿ÀûAû_ P®rØ”˜¢öÔÿ§º¢‘­©…™‹£³ãÿ<¦Æcå´Íš¸÷BU´‰˜2-Ÿhd‡øaj×’˜]¾šŠÍ“85Á‹:2 ÍqŒOBUQÓÙ¹÷võš¦íŒˆxlÿ]™åã ‚#Ú7¿ãÛ0†4ºDž5r1ƒKõqŒåÝúŽc<ÆÝÛA³~”³c·>Å>É”0íž@×ÄM}H½ã«7Œ š×À˜óÒ–}C7ñ«`7z-– m>‹ûÅ5=FôbÙüƒÏmL¢ O=¥ ‡o8pê0ˆá‡ö`2p˜,²a˜1l€éàŠ‡ÖÆ tž‡~Ý Qàq“voeÛjd qtÖJžêµµõööæ+>þ©áÛ”3jø5ò0°0ñ±ò¯”6Íb0DŸã¯%Ú)ýò:Í_Y8UÂ}®1;„#ÌM$ô}÷̇©xÃ'ÝÉŸ1J$alÍÄAÊ܉  c09æunãá#üÇçïû݆ޗ³÷ý÷ù1ð§ÙϵÕÏÑÁÛyVvn}àbâàaäáòÛsöUÎ×ô—› ®8Ðà½Ë$ö8‘!1a.Þô,[ bš)"^ý‘ 0™XwÔ!È\n’ì2G¢òõ² „õ}*m=+‡¶ A|½¶ïéd,?õt*øLXiOØ£gº â´¾a® ÚÊ)wØâî^ oÝ0^å‰\êLh“dcvîKósA&7Ejç@_w>ˆaëïöü˜}ÃÏY»‡ÝÓÃÙáõ)½ç¸Þ?}|²„C{åþî>`]oÔÈõª7}b ZÌÌÄ3ùlãßú<ïtÿyÃpˆï.´0”žvÒÕ¹à(g‡–22Êî Ü]د¡Hþ®5vwçva²í|è8í±s1#;7— vÈ\œúóÞN†¼^‹¢ÜÖ©7ntsP·–l~2=—òÐ@avX³°±}ÍŠÀ‰ozÇIº¯nÎÓwì=™t)˜”ó!<ì+<œ,ÝÀx¶·ÒFŽúÒ]FüÎGÓ´õû$¾#Ÿv®îõR1îQ±/y¡6Þ¥¢ ‰»¯ç«ãß!Ò\ø}æo#é¯ÜËPÔÝœ,­.6Ú˜yb/çßïóAó†O÷Ð:Ñk: 2zL F^@µ>¬È¿²§iàV½`ZNZ=€³¨šXç4Ôüªy„tƪ:?FŠ–dÒ2ÿÊ4o :-Ù-Ï)5×ctÎØ÷%(A,“ûŠy¢gu¢™}Ž n^_IKâ™Û±óNK­PUB*$u=Úƒézèw3²þO GQbgŒý`á‰ìÚ¤÷)5;|ŽFAÜÈÊÒžZšõ¼’öS¡å«Få+>™ƒ¡é+Žòï'[µáp'oY“÷° ‚pÂðƒT'g™îW£‚‚^qN¶U³Rj¡2ãáËÐ|ÿ[eßÁ¿‘;åúÞ'â+´â3i&U¤ZÑ.ÄÁ[N-kB_DÄ™ àüäÀL`BžR”šc 6ÑXð@ÓøÝ¿4H(ËíŽâß7Þ„¿GšÛ ¶j+L9Å7¾…ª¥¶´ÆÃéëé®yéç1¬Áíè;i6P¼©Uƒ ÆÂÛïóÜa¬,dœ[ÀPÔ5A2«\ËíÝó¼±q‘iÎõ*-ÅÂi/Ÿ¡ÈÀˆeÛØ²,b ±Q7Ïqmé(d(„9GA™‚R”Pµµ ð$'Êú,§é2 Ï¼™î~ „´÷£tF©=EÒðþyEWãý¿ ÒöÁ¹s-ï\o%¯êt»¤F·çMó»SÔPû{Õâþ4‚ú*€p‹ž…ãçæèéê\ïç{lj/é\rÍ"~&îëòFrug;Q€U÷^WzPœL7¾8ŒñèNƸà\§M#8k(í®´jc$¢gær³6šÞœ%DYÅÞ)½7xÊŽÓ’ÉÑQµÒ·U ×Pf%ù0ø«ùø°ò „O)ïšFn5_Mñ¼Ýw¦‘1;=Åþ/$ÒxWx±#±‘wæÄ"t:û~™ Õ:J¦ÌºŒçcöEž³Fé4e|ŽtA†u‡Ñª5°M³å7Ô8ˆ¿ok#£Û—-¯ï#ñôY³S n­4\//÷¥$®¥eÉ*¿Í#¼Ñ'§Âæ‚>!S,l¯Ž}DŠ‚]Îk¨š:ÕD .Èz\ó½uqYÄpÚ¢²¦øyÖ £­Õìš1¦ÈzÔ Á½ JÒ9jÓ‹ŠùÛ›°„u\óÕZ “µiAsÖš`¨ÅÓJ¬¢›fÑôô+ÔëyÄwWŸ»ÖQVíšMú=Ÿ0¯ê¶.¿ÍëÌþžl~gK»§fTáÏxÀÔ³˜¤€G?ì˜Å¢Œ¦”¥yIÞ7¾hUy/dÜÊt±dÉF«‡ý\DéßUÝ,A7,ø:v2o–øM@2œNÒf^ÞøèsèBû£¹F ®˜­Õ:]$3ðƒRÝ%òœ˜»Ô7Êø¹m[Ã9ój~®HMtwlm ¾·E­>‹e 8Ø‹î¬vCQÜI'v°¹}r*8AÂ….D W MlzmlöÕœEàm”&$No^¶M÷دÑ_ÕäYÔΙ†€ ™Ã¬.!QÁìFn” Çñë]½öTÌ _Û@¢k6k×*|Þó”.bÿ X‡Òdàø6 0?»™Ip´+8W-F¼”± ¼ä$ÒÌ ásÆ®M&$Hí[®Jô€´Ãƪîåúsõ ¬hýR"ni}æ »ÚÖHÿÏÊÉž‹hð±§2á Ã2$nò±< ªn?ÆZ±*Ü!H¾WÈm=Hª@wKkÂç|Œ§Ñ¯‚ÿi‡–éóÅÙ=o?©ƒ¬zš|ŠZz!ÚiŠCôrOOÓÁS»”3ßHÎ`~<]›:ÙÒ%zEþ­ÕŠ^ül<ÉüYÁkÇYÛnq¼? t‡WÿÖçèMº/ˆþD ó¶aBùè²ÐT‰÷6"–P®UìB/§gÜÔ´.tn]€ç· ¨ëÉ”—» Œ'¥Q]H7¬ã/öŸKŸD•Ðy£§Ðº‹ò¢¶çe7£ŠˆŠn€ UI‘ {“ºép9|ÑVh_)=mÚN¥ö~Lå¤öß¾ "ª¯ÊAñ˜ü Ô·Nx‡/½©„kßI“Ïüƒò­Dˆ,;ÑÛ:}M6“fçäo 6‹foO²˜c;Ï(iåbù„éãSßß7Û<#Þü´t„3ë70f&Šì­+æ±yò…[OÙÔ_¹­\Ÿï'Í­H¯NÙ–¥õO¤Œä‡ôþЗ<í¯°æ. ÿ~¾ŸÇ Œ:ÄȳÇx„ççŠñÇýúßæ›5P4C@I¼I¬É’ªX€Žœ±€«ö€qO$ìÑÛ¯E’õí´påxËDzñÚÞÀŸµ”Þ´¯5³1j h_»Uáp¤Ñ¤ˆÒ‡Œ°£©Ã84€cåÊã%Ì Ðd)’¦6öo©ÚO´Nh¥~ÿ~ÀŒrÆZ—º?'4PDQEŠ0?]ÉŸJ ŒÜs»þ¾©IŠå¢‰£, N9—•éQ:»K•díà5k÷Ñä k!1µŸ,û oyÀz”¼?-Ö ”³(P¥'Ô=ÇèР£hJÝTöÞˆ…æEÍN}·#¼  ¨¥©£5¢áKhtÑb 2ãÊœƒ½5´lßs¿‰šÌ4È^mÕc….„Óò¤K«æ®yšS6á_†údi‰Öh=¤(ºµV¢sh°f ¦¬ÿ€á<oëƒ`Õuǰf‡9V:·Õ 5Úë}³ÛßN†'asU«ƒ+Ö³ þwÎ’° £‹òK-6ON=êÄöm»¨†q1t2w³’å­ ¼Œ¦¸;Ø» l¹Šêô¹I“‚ ×V4P‡˜ÝÓ¦x3×F<‰ÎýÝÜ‘sªS¾nìušGÅ…yÕ^®T”3ÃQ¼Žì˜¥«€³XgZ°tŒ71åÊn^æf«¶¯r#¿ý»²ð^¤>nÃ{*×òóø½ÌïªîmV˜{º¨jFU|<|ádDæ<'p'5o© †ûÍõ2¹«˜Ëz@'¦·>üâLd`¨žrH*ŸµŠåËjFúq£á%^ˆ"MBEoqæÐ/Sò—1NaL%æÜ¹Êä]‹QmÊDÕ½–åg¡åT($5¥Ý?çÇ$” NÏ‘‚j{¥µÂR„ðú›ŽÜ·sòx4%½#ôê¸ÒSƒúf8È¿òS V&wK+—ÐR¶‚NAgëè4Ù³ ¡¾Ž×ÄÉQ’`8xÿð>w®('¹¸æqSïÎ$ŸŸg &ÐÐ}fÊwnÊ~÷ä´ºUø›rŒyµÑ´) [j1v® LVÎx.Ys"tÑïTfųR'm¢"°’KWù2¯U³ÆRu‡´®³±µ¢ÉQôºÕªEÝ›B¹èfM‘†«_WU˜JÛûJê5Xþô´ÛöÝUöV¨%0á¬GчІܨ#³ëÎÁËsânWGcèN{~3[æNCT~‚Â{ëc&Qà6ÙãÒ¥Øtõê.‹ZÀ+Ì’cT…° ·X)3ˆ='ñ˯ȳ3îÎ࿎Q¤¦¸e ÂgÐY•üÈÿÀxÉé-H³F®f2göaº—6J呾¶òÐ*”A1ŸqÝ\¿iØK‘-ÈøŽ'åó»Oé“Ó«]£– øÍcxR26¥Z´)מB^uÚ§ÕWâGÆ“\j&V³T¢†ˆø'óí¯,[»^P¹× Oá‚`2Lj*™ÇFá«ZÙm¼à*¹Ä¤6§WxÍ]¨¸êÍúìsÂ3ó*¦1dV‰eÛý™“pÈ‚aŸò¿-Úq´,F¾PgÃY©º €sÆjÃÿ¹)O_ÙêQ?Ën¹YšÏ­¹Ó½<‚uƒæ¾]?ÛŠèý&Oéd^ãTrª°ABM®šÄšl»V`V‰iÉ6ìÍ[³^wžç>ã…€)ÛˉUíµn¹ÑëÏᬳGAÏIÜnîéb\ Ä]tp¬«µ¶@‰LÝž÷ ôƒ¡Nì[.›Ô»gû¤AeÊf‰CWtºÛ?S›ñë'â®®d0?ÿPÛÉû–,™¦#2¶¼µ³ñÁäIÙµÞn |,þ©~ “bNO'(9Sù/[·%£ÛU´/?ß87àÔÀRÔ@Ðb;סàäl,mqõ@+V•ïÐÝ(f&3Y*¥CVw”¦G9|; $@êO»¡‚ø$}¬{’ø>3‘h» åæ’ ˆ©}u-xåƒm’`)jÄ&ÔÞU~¸~Oô…1~J{{΂Ð\‚*ÑXµ')È"·¯ÕÓ&’ô ßÌÇv¶¨ÌLÛ¹¾ÿ|HÕÓÉÉJ—\m_)õ<`'Db‚ŠÓ$UÞÃ9·ÜÛükhCp¯’1˜Ž“ÝyœÝ®ì¯FRÛÌ#ÔNµD¶ô‡' û…<(³|(‘êÔ»+°ï xÐyø2Åä?öQÒ”¸ÑŽGqçP?”ÃÎH‹›¬Ù‡¬ËS\ß¹«©[¾~4ûªýq()¸ÐÐ00–Ÿb$þ^á6>l:½îD+º¦Û@ŠH—sZsÑEšˆX*h5ÿØdPÕ8Ñè †˜¿FûyîUÞtù\~8ÄãAªDU´påLürœrQÇJy) ÉFegk3"‰<3|`„˜«ºJ“±møê¬c|Y ,¸Æ-+å¦ÒÇ{\¼œ@ÅRÖ‘B­ ß}>˜\+ZNë%¼Uòàx†˜‚‘s ˜SCLwÕê—Ùˆƒ1£¯L;tºŠË¨l$¹K4A2uS¡–ƒ6¥¬“£f.WSXE´å踣&CWy ©»’ÌSÓØyäÿÄ‹¯I]¿/;O‘-ëþÞ³¹×›&ø¯ÎùiË«1¨xÆ&t«¥â ÐEû«p̃IÚ,¤1&ʪÒʤJ””SºB“¾?ÌtJï™ãI´ýqå¡ ?%°ä¬/ý8A­Æ?S–ê(OtÞŠKjŽfÖmžô‘­ !„òùÏ|•y‹zS®Lô iE‚¬UöE¡9}á]©µ#fy€Š¢ueH¦çD“ÖŠ\¥Õœ à ,P¥Ÿ¿'CÔï–é2©›3(2i©ÇïÞŽWãÃ;ø<®=øü>®7's!pÒT’_vö¯3cãøÙf‘‡šœæú±tÓE–€5¯·“¥Ü?ðd»Kªç¥XA`ï³+$KýµgÊ) ¢1ÌúŠŸ3,ï/E¶‹+íÇ:둘íë&Eµ\©y Õù=YPƸ7„pK ™b°K¡þ“œQQ„Jê€êÿömI"™bÁǵ»mkÄ òN¹[õ)´>m¿¶—þàIе¤ÄŠ_ ˆº¹j¡ÝRT*?Rë ï…¡ h g³X G½B ëýTN-X¡ZM ëüZ¯¶O áöZè®"„ezŽèç¦|ÔÀʜɅ´‹Û¤ê ÷ÄHÚÛ}|™Û}~úÚHôÙ‡º¨Pw·ååûÁoUqw„’‹ö¨ÍTwE¾ÖWå#k¶ŒœÍÚJƒ<· )L¥9h+ŒÓ„ZeU'tÆ¿ n «mý}û…ü Üœ‡Z±€uôQPўğés:𻎪°Â\. ´µüšùf©„1ß7‚\¼x,®@“o„µû7Ëó/j²·êIAÕÇÕ îêç\nBø½Cn©CJ±Á¬e+Ì.nWP‰Æ)\¯Xß®n 1Ô'Iïe/÷åŒõª6¦î?ä«Úݪ¿üpïÉ{ B“Ú·ƒzÍçδ–œ„Y» âÀ®cZ<òí%Íá2Íé²Ðr9›ˆx<;ñÏ4 ®Ja=.ª~=ÿÅ^›Ls1é»Lg'%@gã9ªñYν]Ü¥Ùã ;àæ¿ÿÕËîÀnóNठ@óÿñÌɨÇ[Ç{#e·æÖ4i‡XD™Â♉d&Zd&\ÆB”©•¥ª¡ƒ˜…”:!“%™Âbª¦–pCÙEE…&Áˆ¢pÈ8@Ú_¦ûÊÚ·›¿²šX71m„PÜŸ›˜ŸÕÑ,¨%) ²ž·~öc¶£­²SB2 D[Zu§óï-ûî{Û‰ê¯lŠÏç)k¯†â‡N®o¯xïí2á&¤¦fÂS¿­Â‰uuÄW7æZó\Íh¯ ïný!tE…’k«1HÕúK7úã€É6«#C#Ø‘9ô´NÆÕùº¼Ë¡ŸÚ¿¿®'ÊÖ{ÑvDåBà†Ü«ÿfû{w¥Ç±|°\s7á‘áý¸óš²EBMêG¤äÕ‘²uÙTöætZJÃ÷\H»}8Z¬r5 Õð^Ì*Š£A¢ZIç,,>\>se]â½·ºÛ>;üK©4¯Œ-;H‚#M=ý4N 1E.ÕQ0CŠY.mìÆs(ËƵŽùRÀËËñË ¯O;ïqm­,޽%ò"“°Ö¶5°“5¿w—t×&$¥G?íßrÊTÖkF³¬ÆøUÒôÕ÷ãÏóUœvq9ž¹ø Z%>cØ’šÀ—¨HDZÈÖ|ø<(—áSZ¿4ÇA{猘v"›³?ýe§’•0¯l°Ÿ£.c3ÎL¬ËôÞƒÆü'{VÂø9Y%oBy¾À[_³¨niDMsÅ?Ñ€‚o®!-|4ðQbˆœ² )Jº2¤‰°0b’YÈ$ë—!ls…¼ñŠÿ•–Ô0v³¼ÎNšÕÜË•JÀ± e=“þ²¯Q·áj8åŲèìÐ{ÂÍW»KûTõ¿¨YŸ»z[»Æâ0m[½·gtEÒõ·@ÜÖ#8…õ6æ‡LL¢‰ëDŸ/º WÃQ½R(½L™vh[­Ú¹G‡ÅeÓl!s¥£u"‘F½åŽá˸Îθ^«™6ö³=Åbó8A›NÖš?½OŸfIbl|Œ’Á3#Ï„!ñ½f‰3ñ½fŠû™_–·˜Ä°+H98EGf؆š¡Oã1Œ½?Š÷àž:u@¦ÄÌñW Íb˜¥·dŒ~:‚|rOÄ›>¢8ÉSͪpúÚô'vY(/1³cr†#ß×<%5³}L—Ý~NÏ*ì¸P™þ,·2’Œ¥ÃíÇ$Y>õš¦Í,ò‡ÐÒª‰ ìO‘x º<# róX¯ýÒQæëwWôußÒ´èþ£-ñ~]«É™G€óõ…Ä.d;æ¨c…vOUáßÅEáðß›Ñ;ãíöI0Òñ‹¨W=£âµ³Wñƒ3Œpϧºcušd1NTø¡GûÀZäìÁxÊÁˆ’ý7&¿Ëƒ ’9ÌfÅñR¬ÂwdIÍíŒU~B¹_9fÿ?yº¢0Æ;ÝåÑ"årú¥Ã7±¦”}@üùP\,8'èçåRxqm¥íæ-j“«òÀ’'·'ä Úü>Ö_'î“#U¢§²®*7Iù-3µIô×C[ñ¸y#@q Eíâ¢K稗Ü9· 7ÐúOgÊAb9L\zĽ¿6xp°¸G¸®JxÌ,"ƒ›ïÕ*ƒ›ŒÑ#÷ƒ€ÿL—þ¸Aq™D£YÑ;1Ò%ƒ{3>+¿ gzo7Œnm$³˜ü²XgCpœ-ñ´EÇØJ·;á9§&[Ù_¿<“‰cnkÿ'ªÝÿT#r0ðâМȀ˕ÝΙ¸þû>ò E¬ 3¤uužî,za`>;·å½;¡VkëÞJn;ÿ„õûÅr¦_0ùYžø³KYÂ5æÜY–@*RX©›Hž¹Çsb;77=R¶ˆJÈGšmPlïQ’”vAß 3žiÅ\·qhAæ¼–-…Ž0é!e£|:wÁ´¦‚™‚lïw%nGisJrZ¶d—oÇa¥xåï¸q}ƒëå—HzÔÍ0OÊã6¤JeŠÍJ¾õC†ø;Ÿ I€@G ¡¢ª{iÝXWØ¥ðÉ¥«>”̪‰½Ï6va;Ùq´²ý¯‡JË̉ìüV:Ö%²QÔBô<Q¢院¥ : ÝÐŽg5x hôF%Ÿ° uìžáË<÷ÅÌV@hû `âÝõ l¡–‰ò…Ф ~^4„â)“mé*ò·®$ ²–\/áêŽ"*~©Ð_¾He‹ÆÁª+€Ò Ϧ‘DzBëJ…ë?wF›‚ÒÿN—DïÈó ò«x××XpÅGÛE·’´’ÀÇ4ÍS븹ìO¾&þßž‰Kr6ÊïðæÇ´ô2Cnvâæë²-‡TÆcS ç\¾¶]l¡ƒb09[e´n?*F¤02D†=væ?ꎲ¼ý¿ÉÉ¥<õ ‹O¶Uƒ%á[pIg´þÝÀv5t5K" Pæpi²,„«é<;±ýâ Ë"ÀŠIöcƒçH߯}5\ѸÝ^é9—îZrtzc/×íF…<›ýœ+û0g:wÄæ\ðY.r׆¢õ¥V¼ÛõЏ Hù+/„÷KT3Üz]iËÖNŠxЇ¤i“e„€‰MO¢ÈYE ϯ˕‰°-+«¸Ýnä…GzO­NBóö3^c¤eaË'‘3= } ù3Œe¸­ŒCÚMËe#Ö1¡<¤ñàZú¡9¿îúóéiº™’võ@­0¦È¸FÙ ŸêÆñçl„ÃhiudÀåøí"êéyÙ9Sãø­r\5Ѝ¿‡ÄF÷Ñí>Tô' ½NRlþ¸9WE\ÒfûpYdt/fê gS`”—2"§ ”Ð2š0@Oš×¥CNFG?߰̑ʆ´v9 £5J µiòr§¢5…)eÉW,@KGÎ|Ãws§´ó^`ªáu‰€«# êF­AtÂ#Ûò€Ö§SÍŠ4Iôiû†iû€#…«AnH4rÍ„L¬ÄÊÓ¬,ÝÁuÑÛ BÌ›½áëÆ'¯ äõJmóDúßBø›Ã~ÒYç{ÂPC ý\w‚^ËÄ ü¿‹ütÿb ’Tœõ)‘gånë¸<gi¨,Êà7–JøÔÞ.ÜBª‡ÕÁpÛQT×ðÚ¥Ná©ì•b>(f~>ïlIÍ´˜=.‡Eº‘76 @ÛûAšŠñgÀ·!ª 骒Ô(ŠB”Q7¶-M»ÃâŠáhUÀ^pÌ*JÖá~*Óú·AšØ¯Ä†4c–†Œ\§ðœ^Û©Å WË¥ŠßA@ÁŠ*Ç^iÔ²²æ§Âg´jØM6‘èòsÙX…C2`Ä(2"³)ËL­àËÓÌÐÙl¯HŽ~³r²÷X:“ÿþ(2sÿý’ÝÎ"éÅiüñÆpoEÏÁ_¾FK>3ìˆbÀãÒÒ;Á{!3yÈÊîÒ$²c$™hó¢fÑðÓßZÖü“>|Œ½du’šúrÝZm»-ß¾/?ÜÖ#¼ŒËB_·ßÔãHÔô-¨Ÿ¸jÂ%;Yš½1FTG†uÖï2ŒÐ´±¿æU#]ì½ëI"¤Ák¤ Úð¹ —¢5®â6îc-†XMTW?1¥W«.`º8Š…‹ ão"$­ùÓ@Ø£PFM—ƒÉ*ZÕø4Y|ÄV û§Í[+=P\.@F%}«÷U€Œ9­ÉõðÃaÀ|ˆv¬ Ñ#V¬w ÷A5=~Q¯øj*sÿ «Uþ-ŸC…2ûm‹dÎçÒ¤ò¨VV[å¤T¤&ôˆµ”nìdO¼Ocñ…ϪÇG/J7,!%®ƒð)*!¦AoÕ6@§®Gj…ï.Lål>¸ZÆ  Ì÷È®¤Ï¼G¨ qî3*ˆ˜¡æ¾+¾ô=‘D:³¬ù¾ÚªÙ låJÜœ`½¾‰[rW4Â¥AwiÕxþ«Æ1mn_{;óc³Šfš„.2ÏÉj÷.nFBožOÈ­87Ïê kGüEŸ `S1ˆŽÞR`´úØd2= »Ò¦IfcÝà©PýŽòê¾kœ°!Tl‡Ê€æD”Ò$ª`L›|òáÛCcÌ€¦g׫GÙL`Yýóœ©2Òl‹úE r'vèJå|HôÎFþI˜H ’£HŠ»«Gj­ÐÖ¯LL‰Ö¼r.•”¬Õ蔼¼_P9eRáVÖØÖ#!ÉË¢ŒêŸ—N![øA«ö|ïèȉÜì Þ«c¢~p]•ɤèÕB|zi_…í:´PýwRÝÃXshUž«ÐeRn›—ö9º¢ë×yÙõS8÷ I‘%Òâìw&ƒ’§’uÖü5@¨¤ÿäBž¥+Q#¦–Ûɱ´ÔÜáhæ”qcYé 4)õÌÅ£ãÈõ¢2Yjo¬7†¦q`%ÆÛ¹”ƒK!¾Ü/YŠˆ8’—óâþïœ1 ‡öRÕ«K¬bF…ÿ4CJ¤5‹„v·le@eY3’’Ñc©-%Õ=[Céz$nȦ9G§vmX!ŽN#êl¿ÅLݸ‘Q€Dàçà·=4"ÙQôÍì6ÔJÖi‹÷~g¦÷=:×ûþÄœv6žúB—ù=™Îò?kVé­+s0£ŽAGñè£sýS×ûçâU‰¢w5€°'Jå>—k-¥<|¦¢ñ-¡à¬«p¦}¾v#­SNRµÞPúþ땤Åu§Ú«IàÝJAOjAA\L­»(ÎHRƒ­9ì šÐSì­9ö¥÷õùò£ÄúRÙ¸PVfu9_JZ§R{h&‰,›È~B›Rf£³(A„Iáš#!4rC˘ç"'2ضOÍæY[¬ÈR3^?(ù˜zözäj½¦q±×ÀƒÃÓ(“øyÓ¹8V-ÆëøQRÞ%Q%|ŸœwcIdI2o\¨±ø‹ýá&AeËÓC¤Nz±!ú’Q×âÑ—$«€$úOIÏðÝÕåÚ•…º+qCo '„· °ØX˜òWÓºmœ¹qM±´MðO£g%öCj0#§ç“ÔWûêy×ɦñÉ4ܺîµy‚6 OøóÄ}œô ÷N½_a¥L|¼0GoÊÎäùžô^/4­©”ªHtÃØà–¢!%³›<m€½rç趇npë¤>ÏÅ«oîGc7<õõ—X© ì³Üd·_–êîêÓE¢–ûaÃGÿ"àÁ‡“ÈpûL? <;ÀVQOgÈ9înIÑ£õ¶By#™rÈ›L+CÇ]½ô•¤–µäòÞ爖 £Ogx¡LãE’U¬+‰(gPa9ñÌô¤ö£ÅëË£i‰4B† ¬ŽHÒ+þ;j!>"€ñö ¾¼G̸Nòëz0ªÖgbÁÕÁcí®ÌŒÍ•¿P˜ºÆ/cÁ¹ù>ÐÇûÜŸt±çÄ*GAqTòÖ…†#@—ÛðÕRÛïtŠ4¾&)t·,qrƒêCŠò*Ó™—ù2CÖ̦ZÚØ˜K¯ðw–|böèâ+Ú‘!µg ­âÔ‘ÿúdh¿÷¼â €_:µÌÑh•b‘ ìâ­Ù…5ìÇâûù’Q,£Y)¹­¼N¿ÚÉP³û•t3%E¤fbç¤Ä+V–!Á1Ï(˜÷ íö,ËõGøÊsíR þܤúÝwQJ8#wÀ8a¯ Y¹>;–K÷• ø^#üL=ܨ̊“Úž µ£ÆCu©Å¾îl¨×_ºŸ¿G«±-üÞAQHðÈ7z™jw"ÑS¸­¸ÀWú[. ±‡Ú’:Aî–†Vïç¿)–lþ™¶É•o æc] ž*ƒÏ5T1ìä’«qI§pì|©q&§¯a_ËkJ¹ƒ§ÈZKmžä)8U9çÔˆ5ÑÏt±²7Ñ_¾¯ˆ5—2ÁI‹ŸiºžÍ ãyrÏ`û4 l’ðxrë\ ¸æ["VÂ÷Å/wóPdb:7›Iö£æ:?¯!þD£Â’IÐjž(“Íf1­JcP âNŠ‹êõ~'캆{ͼÕóò%Þíré=¾4 ¬yTöKº7Èâ×·ykŽâRÊjwy¾Ð|Ê|a2êÝ>ËN$ ¼"B Ï6Å ¦ð˜o˜ב?NS9,'Í¿Ïׂq¹©T±®{¡¬òð1n^à$Ì9¼DÀǩΌiôʾ‘wu»§e«fÀ»|¶Òc{Ø9°O'OÈ™‚íÛ!Îøq'Œ¶röÃ"'•sù/ÇO cÙ×°“"ºy.ɹg*áp±å<”Ø<5oõåæKà‰+ŠYUPö•)cþL–Ë/ÿÙ_’ ¿ã)£F¢>D’1íM¾ “Лì–}I.pº* AÜCFÁ—|, £P¥`ÇJ PÙ ÚùYïÓé|áû#úÓÆ7˜Ì¶½0æ—©w  ÿ‡ΖådZ£ñ³bÖ¬á-ÑBä8ÖŒSÃüÿ)ìqë;³&%ð9Øû‡”é{moi; ¸ôHHpÌ$§š6b¦–Ÿ£æÖ\`K"“*{'Ë»!Ø=¼¿¯Œ½KY‰ïió¨3Í®4 × hk±àý§Kû<2+̇ã*Gb8…·ÕB'3O}ê’Ÿ»3® æ‡0(ÛÂì=ÕÏ‚«Hºe|Ðqc¼Ó?<ò·`UÜ]rUõcŒ¶‚ÛdµƒºBsŠ&*ó®5Ok9Fß:DÂÆ¬²‡ô!ã®$öÌtìaú‹5å‘ô·äO&†Hm•B{¿j¼Ô Câ…·ò¹bcBJ‹W}‰Jr~ZaCXÅ*˜hÆi M AâäùÙ ŒÝþ6ºgò¦à = æ‚c²X+qõS’ˆ»pìI¦iØ:Œp¿ngv$@Í&åT°8€H]Dz¶ñ2à Qa™?©bd`ÌYaùdçobþŽåË pö¼¢ËL‚~bïu¢#ލ(•p†­Ë³Å)œËïçÅÙûIëèëÁcÝò¤`wì£îMùsá3u (àkêö±8òc"m~8¾Ub}£´]º>ÃpaÏ‘”Ÿ 9(žr~ UQНŸË¯\öˆ†?eØÎŠûN>~“40çHÉO}ªTœV²!|‡GtAœ¥ õTÛü ?Åb!(W·K‘"° ))ûP ãÿ˜ÄW_%“ýÒ#|AX',â‚5'ƒ5~#CÂ@ÓŠBºT5¥Åßö&\(ðò> |jrw†X µÂªW‚4‹h8Ê–Ô‘OÛuû ¯þÄcň’ƒRöŽpºù¦½Ä[‡b5ñ!¶r²‘‘rëñUdnDµh²Fa-&‹3ždí¥¸š¤ñ£à”Û#‚xò­>¡aÀ‘™XÃ^h**q,"(›¨ V„à *º·+(h¿è™ºku%”ðUæ°¿NkKBÖ3ùºó#ÌÚ^lsŽ/ËiŸ-G°æz,“9˜H‚s803—ɾ®‘ŒD© ª\vâÚZ/q˜n/R$ÖD³ØŽ™ç '/ÏÍ*ÉÑÎ)~T“Ò²8Þ&*ä‘ 6ü›Ï"ÄwLüòèsƒïüº”Â0ä3+”¶,?­°%nÔQ'°˜ˆ¸é ðUS–æpÌu 6JHŠp²9:·˜W““ÍTÜe_ š±+.¿}*oàiœÜË`6BcԲ㨠ôhÙ,cÎÀœÎj kÅ9—É{‚Pè…ù΢·‡:ɰjÔ•>š"z]HÈtb¯ô-¬óË`¬âtž×”Ñ öOÂwC»žšÔë%©¿›a‰¤y9×À{ÇV<õÛù¡ˆJÙ'À“Lm²‡¥K^ÊUZòî?UK-ËûHô³0ðP!|dÓräàí¬3•„ê=î ¬Ü×JÙ—‡ÆK ”{q}Š}%à^îJ޵åt Æ2ޱ„3,DýÄH€.däí%Í*WŸß•7|œá†_ýMý9^~z¬Øjõj´ÉZùv?wÛÐPÕP÷4/¶'œU<ôÙÅ{µƒ ×³-”4MWߪ¯ñ.άا5ò2„¢B°¯Îæ§ì‹aÊÜöú?t¤<rn“d»Ø.¾¡aÎóp¸.nnm…?@Ña° =%O†KÒêa¼)·àC”R_:ÒdT x_.—† UË(xè« ž¸÷™Q‘¹%äšsÊà——t¿õ&Üǹú ë¼Cþ1s),Ïh(²fSD¼Ÿ£Ï-(¿ wÄx+ÍP ø|hÍ>>Þth¸yüÎbs5æª`ÙðY%53Á㞥ö¶¸{©NCÈ]'ÿå.ƪ3ÇÂù¥YbÍbð©ÙÎwêÏ’aŒž©Þô4}RŽþÉ’ééÇäÑp.LJ ø ÀÕ/ퟣÞÁ<½1<±!Ê1¯Ðu ,>¹¢CˆGÎ35xm P«5œ­5zX®ðçᢑ’2 ÌâHè¨ÝDœa½l‚i·`J$•w…3ÃÖÉÖ"árdÒÆ¥”Ï" T7Ò`G2ÀPº :F³4áøeSî™xš”EÙÆ"›eû ¶äHÛ—ŒíO’Œ¦`’#ŸÔDþ•2öÞƒ[žÝ¬3?Ú¦ñ%´LíÊg×Î9Å ®þ3 ú|G0^=Ž’¢’óãŽ7#|×v îY«êÏ'@’î[«Ó"ÕüÇžDlŠ!¢³Š;«W3By{/·¸y”oÍóêîó«$/€\ë¬)%)ߎŸí”5Õƒ%²|nÄ~…c|uTå‘3—A[![ê”I–»1CpiÉ8=LJa{‡àߌÌc¿fœÁVŽñ%9™/'zÙ׸Šð“‡dgÖ¼yí™·å%½åŒà'ä©#tFá쬥ú9= ˜á[“á³ ©^ 냄ï-~ò½¿ÍÎÏïò\‡²¯EîY$xv.uª^8ÈÏ,\?bØìƦM9'»ˆó•“C~º/ ¹øQ@6½ûGŽ‹Ð¯ÝùnH¬@¹6Œ‡A´U®£t¼†Ó2¦ŸËD¿Px¯—©Œ*Ìèî$5²Á/¾ëoˆÆ¸5ð¯dóVöœÊÓ. 5 rúŸ¹dûiºäˆ©R\Ðl{GÍ<¸B—UÀxvêñ¾n Õ×ý¢‘C˜wÉŒÄ ?kü~ÞQiá«‘Q¼¾áíãÄY¸‰uÛ2øe9vì¶[õÅ72Ý­_8t¬6ç\ZÞþãÃo¼pÞ¯Œöï>„ óY\ykb86þÉQà‡ÒjÄ øê°FÑMûö\>Ã~ÇúqÎ%§maÀŽòæ"T¸rø‘Wt: ’Úwb ñp ü˜ëÙ¼%HÆܵ+Ýãa†Í~ †„6 í£Êô&{oø¤-yCð¼½Þ©^eènŠ#Œ…kÆ ëæÅ3¿¨Tö‰çÂéŽ=x'Rµ¢ÎÙç|pþBÑïâyþ¦`Q¾Â¹$¹,ϰÂÎÜÞ\ß¼˜YÓÓËܸÜí|ý×GÜYþNHR|‡ô3pI'ï† ”ƺ¤/ú˜ô“fLK‡ýò€é§¿”áFölZV N3Ì\–oì¤]ÔM§N…(S˜v@|£þWÃá—Šöïg< ®i””̱B½yÔ¯à·==kߎ¼¾-lÊO•½¦ê1Ry6$é®,—‹YÉdo¬z}»^O%¿SÅì/Á›*ºàa±jR˜b륧0áXKuZôPÅsu“!½òeãxhƒÃñ÷¾ûsÎï_Lœv"<‹h£ÁØÁášn O_ÑB^óc¤êûUM†Ý!>m¿Gè*í};-W»þÝÖLÚÞÀÉÝá#‘ö/ -¶ú€mןú³¹BÔОÄ÷†û ˜ãÐj½KªohÍõœ°ÞuÑžáé%PMäøÿzm;ÝÏTÉ`€òÿd¡qr6±6±1qvô µ÷hQÓ±ÛbEöy«kwÂ5¬ËM|6@çFO¿êÌj˜¥µnQ@£ÑÑÒ6Q ª`·]øë’”<~ìDÇNJë{;º?£Ü²èˆ¥áéëýùM,P­¡Fu.m®W,‰‰íI%( %š\ gÙH¯È‹ø‡w"¶ÛÀJ_v6ÀÒ’-ФmÙê7É•²$zM®’ê8Ä.ˆ©9q'8K=‘,Ø2H±oAes½“Â~(䬩 hø³§êÂ…¸ ìJÔÊz¥«cÓ¶Åï^°xUULϬV'tàÎú éÿ“Úf18êçXá°r+ Æ] dBØZÌÑbZ­qroÛǪ̀ã6>bMÅ|”,ŽYuÂØæ5P90ë,U¼|=gÎlµõkÍ|µýVô—Ôæä¹—'ûuôaÎ^4‘ –ô0µàÍŒq´EuÇ7ŸWô0Ç Êæf†‡_M$FkÊtÊ@¥ÀˆæH"´M¥sf8Ó¬ÚV­¤ƸŠe¡Í»/ÂN’€eáÃ’>$Êë¹Dhº{m}=µffË&Èžlxe‹¯´‰h•SyË“¢Š…¨ç8!ýÄÕ/iã_¹¾ß¿È©pÅrå”*оJ9W D÷Ñä¥Ú1âÝB&‰#šâ|ßµesš}Z³Û²ühgÓµe/²jÍúÆ‹¦uÏä¸U—gÉ­­¤‘eß\ï8¤îK‚8"«üŽ¡ôVQZǰdÉöýi7 Êü¦.âKzˆ‚Ñ*g»I|†˜‹G÷/[ð˜-שƒ÷¤ñT†‰0ƒwá‚Ã,Ä'aZõAÁIid+ÆC/ÁÊcªy k­À–4±€J«ºÜI)Û4—Tí\1ѽüyË£¼„V³»Œ~ TÄ µÈW^ÛËu„#@z0Aϰ+¤­ªñ:´¬ghþãsÝ &=VÞº¤‚²~×xp4N>¯jL=nÉÒëKßh! }R²%“‰ªÙˆ±o-AlÃ[¯®…KÇtŠ¥;Î5‰ñE$„ø*{g«ŠÓ5 r½%×¢í;þL‚Ê& ¨Ïƒú\ÄÚ–ŠÜgd Êoá(´Uº¡t GèåÙšc³ÆüÂØÎÞ9d{%$Ä“§²äi~6¦ñé´ÓòpÓX€õl}¸ÒZÁäÁ»kFÿ3Þ^ïK„o§VÄR±áà.zë‚p¥{ È¥Y¥\ÃO,À”RˆO%cµð~bYûï/edì¶l(zhß#9¨· Þ{[‰-½­ÙÇèxw)¼Ëgº$ë¾ $„€î!ð4žº«â.z_*Tgå³Ò“@¹ŸÌ ñEœ 6{‰Ó±ÜÛ?Γä.ßn'„`àä<奠ûƒHǹ¸$+ɬýò¾GœJžŒw½|•Ñ&¡¤Œ¿¢×Îd`ºïó0Ý€ÿœ{® Î¥5ˆ¼I‡,.g÷ÖζÖ.ÖÓ¹Èúê èË&%iÈmè" ä¹Y( o¦¬hbjaé·ÌÁÀ^_¨™yý§×ÇÊÆÝcñDiŒ$$¿0»óùåtËÄÿ‰mP5çܼáˆÄÚÆy]ÌD˜jDñÔUÎheL ×ä1¼ û‹ã4(ÓLËí ïžõg½ÈJ¯$kú¶²Ö¥îUé5qáèµ;rªÇËÖü‡ÕݰTÿŸ°Ú¨EuÇqKùß.Ñ”ë%{ÃV)Ì<*³t Üt2r©R&&9=\ ´ÅÎEy¤óõøBN‰D–vÊ—î=r2ø~bÚ~bÛ> ¾ÉFF¼½Ùgç×ï—¥Ëo§óSôˆÊÀéî)î/(@ÐE“À¾,ì  %¾x-¾zˆKš „å¿Uy±(GÀÐwX_0wÀ{}ûÈÖ«,壿 µ9ä÷rKvsâ$mÞ¡Mb-,Ö‰2gìFe’J+½%AO90ÆdÊ,ÌŸ(ˆ ` Wþ „¦HŠA9÷0¨üŒ„a¬¬êË’ #’¬ËR#–´MêT#¨Q/ŠÖA+lhê(a¡iR¦@O˜44õq¥@ë0]s©oÒž„‘ËØÖ)ì·­5­‰êüž­B_᯻Ò™Çи‹uëÓßýZ;ÏßÊ.@|“®|Û?Ï™>7—•Ø1ª9f„«â5è£ •S½r*‡³Ær=²X’.Ôm2Fp¥Öዌ‡X3˜Q›Æ$˸9Ÿ9BùRœ—$æA¤¢ ÝóøüsF1„bÂìZ ptuAùe”t«á7Ïu+ÝüIìDô…c°‘̉,¿ÞÀÀ7·>ØŸ¤?­00½ÏÝËòZ‹è\ŒŒÞ$BB#ÜGµGã¯Ñîæõ*‹LPSeXÅg]½Žˆ!Q߉x1‘mÑgx8ÏÓ"ŒuéŸ •Ùã)¨b P3"½9ý~¤6n‰‘¶³BñŠ+²ë;´¼­ÐÙHb6!ɇÔ=wÒ¤ŒÔäOëq¡ÔNòN†}šéÞ/<ŠüIò6RšBÙ6©ì'¿Oý½Ý@†oˆÐ˜Û¾Ï`Øô_@£(ë£åêYŸrªúFÀ=^xMõs9¿Žé R•ÔñËÄI¡|Ü’µy˜¾ÃI€™Þéš¹¶¢wX¤p cí)߄ϧëè“e$§ê”,¹’FS 9¨P„€o0+†ÇÁøÙ%kƒIOþ~Ó¢üoø}c.7í¤ñ»Ã!a|Î5\WmwÕ<Œ«‡Ô÷Ñ䪡Xà€§ñ¼/;Üô¦ÚnѶ‰ûÓ.Gcà§“ÝS:!-r9<Û!ò $n|¼·În:Ù­åÞ”‘ï\WÕþyk_L$¥ÒˆÂbYn^W;»=Ýõ‰¢ÕоѰƒ¾—Äkg·r:W?3Ÿq¨~þ¨Á±‡Üeí­ÍŠ<«à_P¢ä9 ëFÕmÕäÆg.­ø HÏm7û:î\Š‚?X͘©—K…„0 •™êÍt‘¥îN®‹é¨áTM@ ÐÁ± QŸ¦VË)ö%B¸XÞÄðÏÞ­žÒÄ»§¾ÂŽ>ýI+G‘…4¹¤Ví9›Âi6Le­8£º¨ñêf Q:[¼i#üi±as”Õw VM6Û¨ gT{"ÿ´Ì6 w¨'@3ˆ:¥‡ºž\¸ràÓêä—,}ejJŽì:Z–9W8ÍÉÐÌßý©óÙâÌÏ1ÛÝ=ÉÄâñ\ÏZ BqºÄåvå1®¸ƒÎ ÉÞÁH@߆I=‹bDºt>þ·‡•J«ìSÎ`¦™pÿPßÎÖôX9¦ŽÓeëcŽ^Á7@M]Rsg|rT/u`ÈïíÄ}|- 5ùqõâ%X-Òˆ¿Ciµ-]€0€ÊÀÉH3þ"{y¿bAÿU‘Æÿ!!ç ÆcÓ~’µvx8À‚pñ˜1²ë-%nßÚ°UÁd!íÅ%ŠúÊÕ»ëêñ}_¢”×  \¿¸uÉ{X†EY&ùï­cάØÐ¡ºöé‰é·Óæç2K¨UÚLbBh•8*±@–õ¯Âxêoz3æl¥õÜÒI´é‘hÔÝçÆ3'åHÏq8}J:ζ¹Û÷t"ü)ƒžïd»¬‘¤ý€‘ÏGh,*q}ûW‘²~ ù«!8ƒ/¿æÑù¾Ìk÷b”$ÑìŸÔ‘¨™íüÚf=E eÿêb‡:ú5öqÆ Îæó}_e²ÃsÀ0”óÝ6÷é7ÊóZ»>eŽ-†å„ž`å³{´Ã²^¯O¤O7 Xž“)¨‰Ë"±Ú«•ëh¬×Ï‹vùïu§×\¾L°l¯Ê´™Nf~MÏ“¸‡¹Ž#r5jA…ÕN™ÕÁôËõÐdØTOθ4\+‚œŸÉL.Ó´2dTÙ|"´mm\¼óêºî›lüà¹çÉ^x2SŸ4{}Ry?2ç1÷Ñ2¬jŸò\¯!ˆx ;ŠTBPÞçÏÖ»'ÔË+}*{nå8Æ¥]qàÑ÷ –׌rÞóúé~Ÿ¨ï„|œõ§€­µ6ΩjЬ„èxØiù¤Ar½%ËÑõxŒÍ½+4È•ãSOif?¦A¦P è Pè<žìqTFq?M–ÆÓÃÞ}ELÆ&ß|W¸¼±¿€1l·uE8•¬Ù¦‰B†îh‰k­2Ç¾ÕÆ‘-ùXɱáÍ´ÄYˆ“xA©=“FáWY ¤Q¿7£íçÒƒ–‡Q#?Ïëž²ÒÍÏ#ôŒU>#g™F©FSšÀ¤W¤Sè B¯CàFZ†ÒÁ5 e•²ßMñw“羿 V)¥TµZpS­æò:ÀKqˆ6_`ol¾Vñ½a¾ëÿ Ñyð×H®i=|z:ÿ9Ÿ0 ¥òž³à©L•QGuàþë*íÛÚQ×° e(’ȵmóþºÍŠŠòA0~i|V•·«×"¤æ¥ÿ©¯Þ²†ÒhoÊ2‰‡+x—#GŠÂ¦¢GÃþç;úï’i¼àüq+ µ·1©Ï@Æ‚úN£Qíô±Ð@‰<èsÈâüë@5Þ «}ÄV ˆ9°nÏFÅl§­¾–ºx:‹jCm.•ÚѦœϯ¯þðØ"£†E)NGUÙúGãolQÒDMt¦PM:ÕQIÁ‚-|µÛ«´eó•­œÅV©ŠÉP£h£_¡H#”WHDÔ!¾ È Kz\ ²„¿÷o?4eäÖ2%XÃR£Oz»°>µNè¶ 6î,YÂrÀ`Hü0Ü;¾U¡'ùœÏÓ¦kìÜävãÂ$îãωÌ`RNˆ¥§ååízÅ®¦ÄÞÞ%Õ4²B´_.ÉRü¥‹-µ}ßI†3#’4¼‚Ì7°ÏŸ#äk BÖ§ä(OpùEÁŸÅM¶àW†©íq&?˜SÆ£Ò‘ %LæIoZs¹µNa§¥K÷aÅz”=®Nh«alsJ7EÐßÛœ³ +Uö _?]°£ã^ÏùÃùUrǪCÊ!?jï!MÅ)速 êÂÝeÆÆ`]Øû9¤þ˜¾vÒ€’0ÂÔ½EÑÐNk¨#sV8ó´‰Q õSjüñ¾$ì>M§a/ê|Å㲫xKx¥"„Ž€‘úù*ÜU¬b%³¶f•9¢\èS^ã–Sü†JÅM®Dóá³Nrí†e±ÜnGÔg‹TQÞ¦›rug4‘çý½ëL2²y& ^Ë•‹ŽÒšO½Ep»Jíª¶D³ŽÇP#-Z÷ŸJcsikÙ¡CÃa…’eMÙ '$)ç5vH5o»ÉS#]C=çfà•tÛf[y¶nElRíÖk'ËB*ì+¸y;Š˾j6M¯ù}ªœõ!ïHç;OêhþW¡UQ‚ Lpù—|“ ®+3Á„Ôc […ˆº¸Eò‡c’åÚöÝ[D Ïs÷ßX8ˆìýváµõ;á¿:»¢–.³ZKâg­œ\¼ó1e£_$ e¦@é _Y=â¾T¸­’Ÿ±ñZ$öh8}I@Å&ý‹ÓŒhÙ£½ñ“:Ø^à©´maãŠnÔö•©'f–7КÊöáÁ%øPDßÚØæÝ ‰Çø®È* 9K·96áa–¥%92š [KZ¤Æª™ìOÀ,7›wí‚ï÷ñ3« óC-s‚uåMÿ¨Oί,Ô§¦÷ÇFÄBBkš®ÊÏ¢µÞµ¨>ƒtM¾Â§ø%=÷•Δa·¢œq½tëéô5y}©â ïèpSy=»oaf÷ºÆ5ѰPv€´ÖˆæÞAœr”ÿ•#¸*A.ßϤO»§ƒSo»Ÿ1xÅÜS4ÆÊNÞbFËÇÈ5ÔR§2ë  ‰y ³q‹Â= 8VkÀŸ×cñÔõ©S!;ò‡ø\ó¾Fj`8T›ØŽô{ •פûüÓ‰¯)Å•7T÷2µ‘¼j+çhÝ.àl( âbçy0k£˜uE^q}«`µ³.uà*N/j>ëc5·$\M…ª8˜²m{>!wYÖ…T/JÄ5 ù/aÁkl*Öó°¸GI/\ YeæÊN¹7Ûƒð¼k÷ê¼cǤý³c(àcŽ•iØùDþ¢14t¡œqD´­Ð¢ m ËãO/é¡ö¡#(Bøø^Øìðœx•±ÖQ”j3Òò8³tÞ»eoÍèÓšþ~ûÇNÕÆ]gaúãÁáeKGquܹ±Bc‹Ü;Î6´åÀñÃ"L©&îe­dÿÖ<›š›·)gý±Ì91³ö”bú%ØcÍ)ñò¢¦ C‘’Àl€Xø]v¹ȧ¾h<‹…‹¼°ì5F:X0Ú–tõ÷¿ýi¥ÔCȧ€P  €÷”HF=Ú2ÖØb®µ¹Š„"Í¿<‰Ô­ÕÖ4S­ä‰‰í©äa‰Öµ=7§Ê ÖfžË;B ¨¯š»Öˆw¦IÖ‰2ÿ@8ˆùá¼ ‰ÕAþýÿœÜÿôqÇ Áy„Íxé¹ÌñùqF‹Vvv–^úÔéªôTÖæϹ¾þ~A¯ñMDœë%þÖ.éÙwJAP #‡„ÆC`N†;ŽJŠð’”*î_%Ûw ÷jI»,¹¤¥o’·ÉoÀìl‘ö"VüÓ}X2¯T+×%h‹¢«¸Çä W΋ç9„ëáÜ,(âŹY^ Å¹)5/‹ÿù¼äðiÓ»YYø¯{mgygugeAgÝ'g~áépr™öà‘æXÂA$ñ‘Y„Â#´°I0·ÈœÂSbü ¹‡ú4’QNÑYa\\âƒR$-¸[zÁJP·àÜ…`oþm·ø¼2Þî¢Ê ¹‡æ˜ ¹‡êØ%±@x&>žÜF}!R4¤»¢H‹³³¦ˆ!²—àØëóNÄ-{d¯È\pü‚1dúÒ<‘ÒîÆïFÐÏ~Jæå‘ Ÿ—€`=Îp pŸb?¨„äáÞ`?. Àú¼Ôæiÿ¨Ÿõrßbÿ,À©ßBÒQ0(–úèûƒçGÔ°q.°îÚÚ5@d¸D€þ°F¤à‚ø¼-@h_Í_ª¿ˆHÈÆÃòeßdŸª?¨îäveŸ-@- ÈëfοÐî›õÞÍðè]g¿¬- è—öv_ì÷âž(èñ^f_¬ôåÞfÿ8ðà«¿è÷>ààò^g_ü×eß«Ÿ(àvÿ-Àiä—fŸú>.€,à¿Ayã‚7öQ <ï<+g©¦‹uæR6iÇ^QðW2h>\ÏÝCž£*uÏΞí¼ûóïByß'SQ÷$~af*Ù£ëb?{ϸ(8xn(§N³|%1ji½ßPq*¾ãgKvô)ö ù!avIÝÌŽäÀ(pç0œøgóµæñgõ³è›€w´ÇUJ3Ký¤aƒæŸT<hôtšw\ .PNÁð¶â†Ž™ÞgÃA$ë×µq—`èª! š©‘éyû=‘ÑÏã­ÖN¬~îcø;˜€§!dÓ°Õàd“£¯%¥ÐRwÛ u:¸ËMÍÀoä¤z)A°¦=>ºÕЃ½GNì¹|ö³‘4W1héNØ/•æÿ¤" ¨9+*zé…ÂKøÎž=¦“Ùð"W‚Õ%Â?@¨Ò?¿™ªu$5ÏQƒ?pIeŸ—‚Õe"é)TãSJgA£Ä0h³-àx¸ó MjJíwDÖD•“J•:Ã]"RhsÐÿ GCˆ­Ò•yöhT1Qdþð¯zª_ö™“š¡Ï [>JUzÑi×6íûcæ±Ý1¶àÂà®&eKdt`­‰ô /:ß]\ã«`·àƒÆdÙÑ߇ð…ιD’˜Ò½!¯˜8 :ðIŒŸÎ+Pzå!0ˆõéµÌ Êc?YÓP¯ƒ\œ±ºb€GŒà}:c|ª¤D[Y›¯´íµêúuU&‹ßBpò ³u$’eø0–À²4P€+ÔÁOq{Û‚¾B3¤lÍ”ŽKܯÞ3¹;µO÷;Ë Û*`ÛuL¹óŒŠsã÷‘ZTbA¹õ±>÷?G ô§  7ÿ©³ÅÃE huí…þ$²†gµb•sÖd€³³£W™[]ªµ%Ž˜fgyE]G1»ŠÀgõÕª;I_BÀPÉãÆ hú=ó}ÕÛk—‹ y†ä!„Ô³* Ç!íÆ ñ–cÒN"FúÒ·HÓ¢¥äò«ÏƒoSØWÜT«²h•§Ø$¨Šç_¡¤ÇÊ*ŽËùƒo›/Ó¬{˜hÄÏ5>¦K^8bÑ–¤5Q#õ:váíÇôk=U'9SÛº5r-$]_\I¼XqÇWÚÌ֕߀°a³ahÜ‹´:4¾}Ô’÷p‹Pã¡1§] «4¿³æ‚ž/º«³ËÅôâØ|WЗãahKR†–…k'ðæ;ðA_Áц›«ö %¹ ¬'‰Ï½ëþRÖh¨‹pe õî£ûÕ"È´Îrl|MŒ¬ìÂûH” ÁÃÉ”lW%œ<'©©Ž¯w ªF£åß9À¦Wåø;ejÇá¾p8Œ>=¹šG¶S,ô½53<® éy¥t“§&Mï4„Píí 2íP=çÛ^xJ˜”Ã¥ÍìÃað0O’ÅðqDzÁ¤öX›Ž—œ½ ßgË¢ÓÔ[¹.=¬m¹å°T!ml";ßÊ׫•mÓIr)—Noõ6•{/K_ 9Q¶É§L³ì=˜MŸÈyk¦"CÃp·‘¸¨Ð KI1ë€qÛ‰¹Líƒ i3ÏŒ*@5M üøSr)å(ó‹„6lÛX>¾-?ß¼<½pˆ%W…Ç… ¹þã~õÆÀ¶›Nz*ƤMÎÀkêR(^ I¡ Yш]1ëBàȧžf! ÏG¨äD3uŠúø67@ $ÕÕ˜«@ S߯¿Zj±lŒ»!œö‹ñ* ÉîÓF®Ä¡ ɵíQo!ˆ+ÂÜ+b­BeÓÇÂí=ñ*1,!YÇÕnþXˈÍ9õ÷Å1ãdÜ4¹Âœn:‘lœðO>¬Æ‹Ž“ ±,R@¥…”|” iüÒq6îº&bb›¨ÑÉCs —oM€É€Wƒî àc¿Aë!ÜÑöθ~ÌíÍ·‹<¸О/ºKò±GàV†›õð@y¾|Þ þ}t;µ¾à Õ=wµ¦&wSã“ð4ŽÅÀxË«–Ý„»@nmFh9?VfEcO’Õõ ¹ÁÝ\æ5/×4»ŒGâ«Çši[íöø ~s(Yø5ŒÛ %îÅ ™lnÎ>ÄgΜ‹ƒ©\x¬Ü¼·_òzkHЄ¢cA (ÕL#­Ô|¬º~Z—áºkŸ‚Ku³ Ñʽ1šd-ðKš=»[‚2ÓánÃÑ}¡ïš¤MŽ\`kÿ¬ M7ã/;lUô°K¨‚,G¶ ux´³è…/Ô&[µXØ ,ìåcL£‚ë‚K†:X&žËkà›z-o‚é`Ž]¸ Sã`o°GL v™„¾bõIž窤ÇÚ°!¿Y¤* %=Z ¾ò®¼Ýµÿt¡‚²­M´­ÍêõTV Ùfœnó”^34`Ñ9Rô%©ÿÔd^ð"%Â+;06?ãÔc"€ã Ù8ß,Ê9$0“Q6“!´*ä½Ââ´ªä´Bk,½l: l0‹TÁRª¯¾j˜8ØZ퉘hð:Òx¼ŒUÛÉbµSÙ úëD{%³‹ÉTa;øðãÁãBÔ]ëœr‡îÁ ÚȨqHõ•ÖBï†Û¶Èö;‰‰5Þf¾.¬é×9ùë¿áˆmHÒ}ÅkqoËí©|ðßHèJq¢¿aΨ!ºòfÕüxÜ.ûŽV@[Ħ÷`ú(]W<ã][j.ö$qÂóu-R€N³læÂ#R¡ó´–lø±‰¡­ôÊ BºQ÷UнY¦<‘«Li¬ŠPä´Ä¬kÑC ~±lWÅàÛüéÎãß––C2é•ɱÞ^ª02XM¨P TÖH°ÒçÅZ‰åŽ‹Sœ¹–¨ž(Üàr.”~ÉW-öâhäF¥©„K1á˜6Ü{ËTgƵàå¦ìY'‡ò§drȳUÇT#P‹DýºÜXß÷N—Åz¤IˆÕ:ºàúÀ®|p´É‚ñù×"Âú’E˜c÷[þ·€ÉYêÒó÷Zý(æÅžc†ùáÔ¹ûçîúÑÀ}QüëÔ)yÿ>'3¥ÇUΦ§k ³zªð­„ºPÿ£G“X±ËnÙÚTìqm‡V„Ӡñ”êüý§Z73õufpý:e¹5Ø®F-Ó%K_VÌ"vå諜¯ãµ›ÇìA8;Ö}†<ÝàúÀùïÁ2ùÄOÓºœôËM†íss[ß‹ÈÐQ!‘I P]ä?éLÓX¹a¡mÅ×8“|¨’…º¢æ‘yßG³hÙ)°Ùž°ijØEdh )êVÿWEH\è'* QFýÈÞS§¹ÛαÆ#¸ÅV²AMÃ68ÇVó·IQz  Iv–ŽQ†.,EšÁ¬ywãòu:óºÆe̯`£JK×Ö"n¾£rÝÓš1õ.õoz/Ëd¢¡HÄ´%޾2ÏÖÙüÖXt¹Å­Üo§(õ‘)UjMbN¥áÝK ¹o£(¿sLÄÆ´‘ÎPÑ¡’4.ˆ†ÑÒÛråݼJ‡¯2nëìÜÓšõ(Ó¡(¾bÒ\®{–ŒiïH¿Q'ø£—š¿$#×OÔÐç6èú@ƒŽ¦ü$sª¦op¦p³Fc-‹w'Xù£c®_³øTÉ›½²ÛO{¤¡/ó()ªJ;Wk%˜ä5rhHÅðÀê9¥ðæ]µ][Ùf‹º\UxÁ õF¾í‘£èS{SµÜX)Åx|OOæ”Kÿ0“‡ŽÇÿÏQéÑ*Z¤0óØsnªEN…Wu UqŒP‘R_B\”8ÅC×Â!zç½ÂÌÕDήŸVs«OUŒQAæÑšhB»&ùÓzƒ"›{ìI¥²+b šO.ÕÂ(WbªYg)£MšèÇŽqç&¡åÕ¥¤WwÊ6U7ËlžE¬¯›le¼´¦Õ¯õÁZ•ìõïðF¥–G÷`Û¶ÜÖ>·¸ë§‡Î„KJEK <(eÏ"aqówÜtSú^šh€ƒªÆ_JؽPÄkû]^Ÿluç¡ÙÎéä›9ªkÇQ?¥üw5Ãæ“Úˆ åÁiR=4Óà$•ÃöÙ”,K®Ð• µè“Ï2a34LŒKÀÚ/}ÍæD¾£x’o£òvÕ~¿©™R ²îD ›“Ä¢—r“Woú°ÂR*o3ʼnÆÑÒ7¸y·Ìòª(¬ró¥µ² Í'ù¼â§ëh Ôý¢Xï°Š’GücKN ùöi‚‘°ì;0‘îQ嘰8'£Ùq㤌‚_á„n^Æ&ûÈ©íl17-Å•6)·xƒòg½-J`óÅ4Ûg ®‚Õ׌þ™Gc’"bÑôÜ‹©VÓ±¼Õ™å®Œpå[Å*gl:Ü=.Ÿ G8n_µ¿îf~½õ}•ë³îË9P²Û­ï\ „Zý³¤ ¯eÜ4'-™sà\Itá£ÆßøÝ!ªY¶V¦êÑW… "wËÿ@={ÏÆaX÷Î&/Ô-hóZ ×FmŒ×þòèM ûnÕÇ¢ièíÅCÄ.ä{@•E÷„‡êI1CÆl˪”zÏ¥ªR-A8Cj‹;¬´&d5FÕ÷“ŒæQó(„íÉñK«Òû¼ü(Åj.ÿ“Ï7tç`tMŠ«ƒK[„TÓæÛ"ЦiµË&‡ÝÄ£2pÓ®”ñ„~ª7ÊÑ>6~çÔô{º§ÁÔsðô÷ÿ¯Æ¹v‹+ê v<³ÔBþßW㌠œ Œìl Œœií=rTìEÿùž©Eg‡1q^Æ+óT({ì‹W*† .ž;sŒÀ˜jy­íè„Í“ÿý˜®‚Yã'hâÆ?Ìz?ͰíQ›Ý‡EÇ´z^äÿÙüUD™›—“ïXÕ¸‚ãÜCϨÆÖv¥ó­ ožhAµ<c5q ÑÌl_óò¥70!oC:Ò”rµÖ¿N§ÌÚgEý™ÕAüêbÑYèµVS±žµàñÚ‚óyƒö˜ýg¿Îì7ÿFîÛ&†x¸_ïn?¾fIÝèÓ£ëÂÞ†&Ò} ~ÐÞ}Ø{o••Áòœ†¾#Š12e°@R;‰¢OG—Ó$¡ð‰?!c ]ñÅ’$¦†š…íÈ+¦‹ŸlëÇdDt{ºY¡Û°ÚYߟš •âe/}‹ÇWÏa@¶LØ X„“æš#%©ê£ÊAm°·1oœ«—°Œš-TèöÄe]ÿ1x["Ty 9ÔŽ>!×áv¡íäi ükæ…V$’¥ͤ6â£0¾¦ž^¨A@!Õ<ÛR¿¸,°g,iP*3”&Ï@«°/™ìX Îå#öЇÓ6‚ž4)…T‰Q¬Rqì Å ¢$Z~ê£w½ÑÚÉ^z˜¨GÀ ”¾¨‚ÒÞG4eª(ΚŒºh^4`RÚºÌFÙl˶˜³ñ|¢.(‹b6Y»7^÷‹ÿYÛÉöC‡0UwŽ«jpŸÿ«êYÓŠÕq¨Ç ^ÓÇï®ua:Pôq5@Ÿ%Oò¬»@Ú̳aúÑÇ|U´Ž÷‹²ãpÇeÔ¬÷©t»ô–}pÊô]ûUUÿ(¤dÂKŠ>v±UF(»™;Çù…8#ÛDyæ¯æÄ®uýyê%ý3 àѹ÷¿ø!†yþ=8ÿAÙÿkè1jQqÄùw«qs9öç8I Š¯² JU UŽ :ŒŒ6)RåHß>léF­ª{3ÙU$[}ÚÀ¶?.t—í¥ëîÿë©Ê*©E-¥ÄÔÃóçýãRGåG7ûó1¶s'Dɇòë3ØgØ/4¬'¹À¿ÐKÒ"!ôõ¢³éFQÿz.¯p¼('l&¼w­Fñ(¾6¬ÉÓ¶JQiа—_N›Ä‰É˜!¶¼³Š‡þ7”—z˜¡q|PÅͤ‘N’wæ‘z—ÊjNêžXB /苉Š|É Ÿ °ºgs 1%@ΕÌ~.;6=a/Rœ¶zåMž{?3"±|+ê`КȎ³‡ãw_þtn õæy³?B¾fëÉÉrCÓØŒÃP[?ÜÂÂýxį’‘’Vï71 7âwƒÍçk‹­ '4œñÐ €$‚ÔS<ùy£þj´~.@& 5ÅÃÉñÜCÕa?á=Zºäaæê HC>9¶¦q³xQ—æ¤ýp=;$Iq~ÝhGc"½ªóèeŒ ²ëƘ1ø&&B‘¤‚`ŠÃéT¾ŽÄVælD¤ÄpÖ49‚*·EqêÁÝÃg3*âD&åâP¦•T`‹Êg\Dléêòô<МaZêñ¹Û|2à Ë+Îà“~ÔnxD( T\€Ê7œxd j‚U ‹ ïbз–wMâ¥:íºƒæA¿.üÎ!ñ¨©6Uiða÷ö'³Zš€L_2móË…!¦(O¡¤ab¸ònFý‰»9ö:ý9[`RZ[,¸M†2œîéìˆãL`´ªXÒóŒÛQ**æ”ü'(I‘jÖàÊò°_»W¦¼ Tš ‹´Kö(ˤfH¤_„‹å*µ ä%Øê…ñZòC€ºo®9û‹HK_ƒ{ˆî’¹|\ÞÉ­\~ÔwÍÑí¡Ö|‘šj†}¹Í̦!’%23iõ¢Ô=ðT·TÄì•ËV²%&ãKYÁ×¥iÎÇk½˜þm“á]ÈÎ€Ú ÿ¢z!¾uG½ ‚M†wõt GVè#Ý #•© d±Ð£‘ÒDôÌõ8¨ fK?.u“€#ü#EôÏúj£ï†Ô›#µQo3Øê·Gz²ÔEtgó ðo›LVá`À2ɬL$%®  M;´Ýh×òľ,ˆhZn½jûÝk¢F%ßÕüÆj‚ÒopÊke¡xî8[´€\§òÔ’dáiýóáµí.TŲºÍ\+g[ýý_­žLFîœÿVOcs#ûÿ±zrý8m±6ö}Õõ'‘¡U*K¤êˆEp•­Oª, Ï&e2Ö† %ħº›hÜlÿÜØ¹&HPHÔÉÕà$ôÓ¾~ûÃo„„·6ô©—V¯]ä6À÷bE O+¡‡!pUÐ   ½îºy¤ÛÇÕÅQë8²Fc£ý*ѼRÅ!¥ÏzÞÀ5Ù Á²dÖ©VoLn4Ê[çØóYß‚g?¥‰Q< Fª ¿Ëž;¶>,‘ÐÅ7Ž˜…N+‚›rÑó–uðÆqP Dç™dŠ͘.¹†¯·†£‰d§c÷àéE×–¥ñÓxCçSŸþi~1Ôsn1’yj›)ƒÍ†F¤sD¥©SÆïÊ\—ù¶›3$oŒFpè?|Çülp²§(€s_!ñkËŽcQ`Éa]ߨs涌ݼŸž]ɱ]»²gÀÔÙŽQtåÖG]UM=̼Båî6¬˜½N½_6ް‡C¥È)D?'ðF&x‘7Qœ:@JÞ~:íöŽljØ·þ‹ã† ÀA}p?!+ŸÚ¯ûN®&¨`\!½)^G|uÈ¢€‘ÕÃgîŠÕWD¬·íz:ÜGr˜ÌfwmÝo´‹°ÇsÏä)ùDû×Wg„û~ð »Äº>)|ÃOÊ›Dÿq£µR;±¸8/޲8U‚s¨~ Ž4Áùc: ¤Õ»Œ”È A¼à×ÑÙÛÓh7–´àÌüáÂÜ0†×(5™C-õkìu¥²<Ù÷¢yÝÜèñrÈîÀ šj1×J!ׯ¸´.BcH—U7À†{ÑWïqðfì›~¨Ìw¹{6žo¼ü ¬ ¯N½jþEMrΉ÷OV4wƒ‡Ç*·­«GíæöBvïõüÒô1#§élgVãí}çþúc` `ìŒØžÙˆnÀWkùÉ'¬¸9¯ À|Z×ÊT¯=‹²\´HàNaÙ“Ö¯÷}<˜9X÷$‡C¹ß—ØßêTìýv ^‚kŒ_/ž}ëUˆ¯Å†ê`@›&Ï{šúD› !=‘Îq¶Vx|f $éV¸½“™½]m'>†šz&wdðA›Làú.ŸbF ÷{Ü•ÂÚìˆC)YŽO)?«@1]“ ¯ãßÁ¶^¾5`L\¿2ÚùOâ%PÌSx)·ïpyÊ”7\¤¡ÔÍ%½8 ~1H3dÃL~¦ât‘C#¦¡| ò42˜³<"T<þ ,;•Õèé±"ä¦&iu 0)@Á2œG'ÂÃïÄΠ6T—%m;þ;æ:Ì#àzâÈïzGîW>õL禹V†’å³mjºô‰·}åY2Dc™lÖ¯—í¤Pèt+ÞlÈ ?çB‹âVL)°#${\µmYÖŠL·È¯¿„*ÆV¾1îNZZÃe7‚8­ŠÓÀÒî£z'M³ŠËW²ë„’t úU¢’§K1‘2¨P$Ëtæ`:ÒðöC«4í!?4j彃â´Z8IÓ¯à›:êÀŸEwi‹³ôä¤ò÷§C§k ¶Ç6ÿæ«ÿ>~ué‚’7ŠÍ «Y»*G|zd(??XRÝ‹ØÙ^ k·G…J“m“¼¦Ô®ÄÔ¤·a%ƈ…ÕÉ€íPJ[®$#>å°éñd©´/% Ó¥"ô ?Dpàä¶¼J¤l|eä„x&ʺ£o9ª¥k1¬‘¬Ïçz#sá$¶Ð’ÌÁdz¯ÂÝû¾‚J@h~<\Íhм§¢°OšMEªaòš×¾]7ë«Cõo5E t÷b¡$(‹3ì:ÞN`;ç÷²Uì3-Œv¦ý}ÖtÓ–¿ÀSòqí)ñÄ”šÕÍ:E† œ'ËC·RèجX.g|û’JC[UÙæàºa¢¹/À•Sý@а‹âÝ 1#FgUvËÌ3ÖØtZÕ(7Ò ]q“v$ÅÈ6G¡ £qy òX{@ŸƒšujK5bZ%ø¯+ ðåx7©dî#ÌÞm½%’øuÉUEsŠnq¥·×’sm YÕa Ñ„\á ~) ßæˆ¿™ÏvÈÆ(ÿ;5ei°7œÎ@’p™ðó•D"Wïm¡™ÄÔÜDC¶_¢¹Á`iO×8³™Í1òUþþ$2ÓZ™ÂÒMüßReîr詤۸¸¡¯€+ÞAC@x¦° ˆf ;õ³‹‹’ä”#![®"aR(pg.ÁzŽ=qâ }nÔÕÖ°MÇ”çê*Kó!SÂÕ#A˜gw0}‹õÌÿJGùçSg÷ø©ÂnðÅmɯxÿÁa‡†\t/òï‘ x¼P´äÑzºä»þ¦™­ßAJm0TÊz÷Í4À|IÖv»i;©º2¸H®àÇPÂ.ÖN(Ô>-K†=ÈŠ-èäcy¢h4«u¯ÔË´Äbá1á˜ß¨†0ìà-¾Ð)™stúa©-H™Ö;vHURL(-òªV¸§ç¹Ás(ÐŒ'?] «{t5X2i~-óŸ{4 ¿jÃ&‰Z šFkšÕiÍOË %¹· àäHa“<¿ŽLÉQ~e’ù<Ž3â¿WÂ’ü¦ÈÏã:5¨OZè+h‹á-­N›<ª Â;ê$ºëº&k÷̯áatk ûsõÝ\Ž;ºwc»ww×GÇŸ¥;7ÜW=–\ÏÊX6'µ NФ§NäÙåjI.}ÅŽü“…ó7kTaÇÚ$WÜ*»;f¬¤c:9ד*i²—ü•¡¯ (”*%І” ãàýhý7MhÕbrÞÐøm÷%šP¢,F:A°D¯Óe5¥‡Íkó;i_¦3ß5Æ#ÔHcЯFI?1?à¾ÏH\¬€ŸýÔR^žÀθoëRI¤3Ò}õhgÕY|¡ØW“í¦zš$žY`÷Ylg)(UxëOvgÌ#Åà “c‘5?uñ«t«tß ×Å7·vêüÆÅ)v§$sh¯|g^ËV™‹d•Æ'î… ew&d¡CŸ Ö}&L‡ñÕHô}†ÕùÕõ>úù}fe} “`õßR-à嬹¯$¿‹`½r‰=J8w¼=ñoè ÛEfkTÔß*ßËΞ6¸™ßm¬¯ÞÌ#é-­Õø[Lïâ–¬Ðÿcÿ+ l ƒÛ¶mÛ¶mÛ¶ñlÛ¶mÛ¶mÛ¶æ=É$óݛ̙ùÕéêþÑéN¥ªV¯ªÇJG» ᢠ²ØS Ž>·Á>ŸdTÒB<…M]Y‚Ö¯X¹ªUYÆ‰È 7 ÚÝã~½¤"À÷ —Æ‹i–`òÀÈG ügoRU©LTÚÔmª¢tѼA«.îç±Øjç¤ÚiËô&ÌY™H|‰R!2xFr'á\NŸšfhýƒG%)Û7Ó€¯d›ðÉ̯œG^Ç\¸L9×°PÞQ¸pÕGõš´Ví?¿¢6Ý„+$~!Í„H^!TG áF¹|¢?9g(¹ÓEIášÅíÐMÐÜ+¡Ë^,1<­Cb5>e–{{C_IsbåIWGf7¢D–àœìa;Î41Ä\Þãok â98£Õ”Î-šzÐaö§:óà½à駯Ûbbì3Hý§C·NO²ÔªÈ@›œT]ýhÚXLåDy!E÷/2¥méBL)J¥ƒxL‰sM“Šõ&WɤÇLÌîs_aœ}Š|‚GÚgàýC¤ý‹í|•V°¤Öàž|µDÁx)çDÇQh|„! +uÞú=¦$?äþúnå²4)V¶†Þ·žö—Ãmšfú4†ß ØÞʯëkƒ}Fõ -Ôà´}g;lOcÞ—£OOªì±ñ“DAV$¿´pÿè¤$ÊøÔ@eSŽm´OYf>3IUY¨ØŒeÿ¸3”‡p8F%y&—uTª1Ò_i®¡ ’¤ÜLMsæáÛÞ¤mãЭªpîÛÉ4û¾Éòò™"4µ® î|tÕiÜ!å]¥/} …PP¾ŽJ…¹q[ô¯xVF3¨ëͿȫÙÍú1]2–L,>A3×ìËa+< ¸äaS6CÖ߯\ðÕòŸ«3˜~±ºÇ·k\¬¯?c~J]=YÑßKæ®(>ïß´/ÕÑ×Fo7{‹øh/)«‹-èWiÝ`_YúWXJS•ßhþ×TܺU\Vx¢Ö. ¨M–€ñÑy­ßß&À¿ÇÀ ÙsAR\'õ`=§µ®:¸yHwwÚ/:´ޱ@€@8.yó¹°Êîm½Êwºòdq©L.bÿ¹E— -)¾(Whñø-ÞxåK·šŒë€ìûþ 8tWàc#øâã&n÷e1¾4¤Çd'­¼Òž¨jó‘;¹¯R¦xPí‚þ6¡â»| ²õÿ,`1ÐA,¸åÕyøÙA÷“ß[]WÅQ½£jo˜üëk >4ŽÞ` .ù²1 nÞwW'|X–ãçÆ>$®d ú8Ïùûš#!¥Í’?à«~9™Kq&÷ª]¦Öa·»\G¸åƶA’ NgÛÄìºjùííacÑF÷þ·hˆi€A©Pµ^¬CÔ±@P#Ávr& ¶ÄO$}ÍÍAé¾°Œ>B5 Ó&Ŭî8Ó†¿)敌±`s‰«Mry°¯Ñ¨o¢Oügèó¯Wl™ÏMñfŽ«õg|C„C¿º]i–t-G/2\C)«É{ƒ4;öüÉI ÕNÌYJŒ%òjokyuÀž+€½KºΊ=[‚Jw<®¡oÍ«”Ö}lþwóç–1bÓ…bšŽª/ h (6Ñ­Àá%´05´áÕ´ ÈÒ‚° Â(IóNÁ ®N·l;—Ý¢0š6JÈ8Œ›Ìù‚Þ.®A/MZ«âœWî¤rZL8ßðçw$4)ãÏìÞ¶Ÿ°§;t§~­%cGò­"‹zÒè-ùÏ¿üÿ'F·mþ†Dû¿’Õÿß q–¶ýÿÕos¹Í9º%ŠM$ÕîFg‘àd%[¯_œHk(ZQµ“[ÅÙLÉÄì;9mK½—w†¢xu@®âßäñœ=wúA!€0 EwŸ  gȲҎÿ•Uz™QxöR$–TÞV*§9ÍV´þ)~•©ïþ(j—û¡ýôHû]4Ë÷…žC5ä¬Á6ØžwR¶Êê¶ ·–\>4S䦻Eä¥j£õD®×BÛJ»Tª#o±nÐ…ß&Ÿ¯Œ¸é¶Fn!ôX6ðÒzÉ6ì ô`6ð{5y«=Ÿ¿fŸÏDÝx.p“·Î-`d^tßO¾Y@Ö…(^/RUy<– äÙé#øÆèë£ü¢¨ë!ùæP§ïíOšÈ;ŸDÞgéxj¾3ëtDÚ¶¤š]è þ•øeI»pU)5ô®ÝÚ“Hf‹³Rq žŽ^@w=nð âÛñmoº´Ýrc×Àž%ÁŽõüYTD§¡R½¯á?Ê(°ï¦[ôd°–¯ÄzûÐCnçÑHxÖ²HoÓ¸ ÏÕw .¿m~ÝÊ­;Ã÷PXÍ(†Êþ•^}ø:ï¶ÿÎÆ°ïi{“›ó¤zÁ£xjþ³£ÕëÛ¼NÞ¼×;v24ýô¡r3\ëk= ³l]„-X¸¿ë¾éõyÒš¿ÐyâÃ'äáLÖ-êy><Ï”'QÿÛˆæâhÞáå£&°y<đɫC»=î³¶pŽiZ&õèœ}n1Ww³>×6¸L°wž…öeÈŸ¥ài –#s¾ÿ°»‘ɨ(}\œ&_ddy)A‰ìë<ÊU£ú£q´Bª\fwf­ÌŽøqäãPŸ…@,™žèTa¼lÔO¦Ä¯9Ú†«Ð¨ÉaDeÆáÄü<Ó ÿS ‚±·¡Hd/º‰wñq. p< ÕÖ—â?Œ â’YñDþûNÉý±ÖåÓVB¬Åt^©Ö3ÓÖ4&T[N±ÂzÛÏ™DÀ9ÜâxÄa¦h XpÃDØn;º¼ö’q0Éf+þQL[›z2+Û¢ÊâkÆ ´¿çèkòN'a®î’TæMÓø€ª{ôòÑæ«í}FwŽŠÝØYq9òšÝÌ(Ôkš°›MI÷(tOh=±Ÿ9ò¬Ë(3˜[E\±.;›½ÿJ½ŠÙ ?ò¯¶³²8hñé1š¬üLœ!½Nû¸è‹Ú+u9p]æ¦p˜Û°Ï!&ሞÜ:·Lè¹Üüq,J‡žÍZú:œÎI'cŠÜúýCæX ޵Æa%uóšÜX•?€9 ßÝBeö¿ñüãaR 4Ä–4ê‹ëè1ÑgÒÊu ó4“Bf”ül,¸:Ê$±æ?Ð~¯‹œÆôDµ2 ·¢~*%ðòk TA‡ˆ Ä•4CŽ©ºÜüfçFoú{züu"NUR‚@‚+fø´™q­äð¸žÃ[¸É™‘:鷂ʱbqh-xÓ?¨]°žœõý‚9á&’ÎýÆÑ×a»]šýŸ¡äµ~m—%‘dóÞFÝŸ¸Iúóléþ8ÏrÓ (¢Œ`™9Õée&©¤oì üh5;0„“ ã¶UùÞaFî ¯E;¤üKMØ$öI¶u0²Ñ:úR{o* N¤Y5’Øf¾HQ.YJPœêa&®dÛû›`âNÐYHäX–Çó"‹ÚV’^æR š? *ÛÃÿ"ûÛ!°æ~lD…´ô9šf\BgRÄZ˜K^ÞHëuCd«,™Ó 1ý…²ÅÂ4*e˜xع›¦ýpìÒ;{d%u@sK~Œ´I`¡ò“{‰g‘š¹èÞñƒÐ( ª§±ŸN§FM¢V£g2¾Ä™1ø¶ &¢äÕŽ(ÌL5Å,8ô2üÛú+Õ²Q@2L*á¡/@6#}ƒÄÇüÄÿ‹¿Î"¤‡ˆÜ— ž›ïƒD.7L› ÐÑZ)ãð®!÷hmé÷žé,¹ýÉÑcÅTï¡Ð‰_’<?^;wÞ¬Ž(*×–Ú{ä½ ˆp¾qH^%‘\þ%¼â&4yc‚š¹÷–lÌL%gj›ÑHïsê¾)™¯á“à]^9…—em÷£˜0Ÿf8ñnGH\À º˜ðH‰ú¹~VÅód3›•tÃÒ"âcÎ0 ï=oÇ) ´@LXÊ‚°F w,^ 4E¼§V–A.J"]‹}D³â©ÎÏPÚ¡êN蟇¹¦³£õ£«Pïd›ªÖeIÊI%õød1{Úg–T¬ñsóIGµEJè17z¢VÿˆtÎ9{HÏìµ,äóÌáÏ"‰³LÍu)>ÐôŸw#ÙôÇ$ÄVË28©¾‚mOvÙ—Ö Ì>–¹Bn z œÓ)=ª¥¢9ëf¤¸E™OZÆmp)Õò¤^yÚbNÍX̬E€rJ+ùOJü)•ïFw… õ5¶[FgЛ þL¦LL¤°tè[9©ÊÖ²HÖºDê܈—:à8áŸ÷°°p~P‘—een?‡ã|]“}]ëC¤¤'8™ÌâšZC4Ž5ô`ãlf­M˜/ƒÞtFÍ^”MöPcù›üÔƒ”_èáÜ…j‹DâlyW±bLo-;v”–¶Ëš{/§2RKH?ÄôA8ôçÛR´>r<.[ÙîéÿFìzÚ×ñëCda¤6×yì·Ü°6’ADö:î²}ó—š4ŸVb‹,n/álÖg*‡·"—1ïBÇâĪc|¢´¬$3NSAÍ^‘0g\›íì|{³üRûpÇP*¿7èÐä–y%ÅÛ-M[§Ö¶4¨i´ðh¾ñÓäOÈ”,—3(ø§»“\ï÷c¤æ€3ŒbJýÄq8‹öIôžÛû…Ê"Î1˜‰"5²{š–QnvçósTôí·ª£ßv{+™V+¢î‘j*Æà©Õ—&eb<’²JÍé¾+ðaª/·ï s(éDg)¹vc3!¤…*©ÜáGÖ(_µ‰<¦¼êÌ9*$4ãÞÚ3û¨žkL¼2qïî2BÈø´Š7¸,O¼ªIr‡ò‡·¯õLs­÷ˆ÷gÙep¬„fÀº’\òÑáœ#(ˆ‰¦è|¸ñ™©Ðç›W¥Ó}Ûc‘û°N=£àŸ%ÎÐ „éÖ¯Ý%q5Œ% N±K2©3OʰÚ|ˆú¾$7-ñÒï”qð;¹ Am—‘çÞ‡˜ òÁFF? cú¼óAjz|ýÁdÖ4rŠóõ‰p1Ù/G‹F~Ÿž—•Nb¥Ñ#þæ%l2üq Ó9&0‚t” º&„}"@­ÿE'Ô)åy,âŽû*Ý"½$Ejùö«îÍWà$ƹºþº´³— |˜u=óšm•¶€p„•û¿Í"£HDrmQôÛ™á s¥Nñ u1ëŸåÚbBBÂ66ü¡Ocb–î5G;rÑ®¦eÃ?g °QÐÒAïã;žœc0A÷¯õ-‚URÀª@ê„d~•‹®\‰÷Ü«d4Räuƒ…çW—{ÄJeØZ …·~zó–ò\Biz üDÛª„xÿtuÞá6v`9åžM½"üt0Å¿†Ši•ujz%b »±À;I{O¢ý{d=’åí…ËÈÅé+Þcíu\Ú&(o~×åz5gŸèot%M!*Ú:¤ñøLP< f3LòÓçÈ~ÄéÒWPNtÜvÞr4qÓ6“äL Ÿ!ÜO«yP·×/ µŒÔ>}Èú—~“€J^Äk9¼Çiõ±vE0THу-aö¶øÔ2þyüÂèEc ð=èWˆŒ"é«ÛÏ>ŽÂcâÈ}{ú…‹Ÿ W4[óS¿'¾éfóSeõS†fg|P”eÜÁíQ³”ìüwOŒò¾+äÖ[ã?½Ûè“w:jô%cÛñ“(Ð*’P:@*P¡s¸¬ñ‹¹¨Srm‚q9JúÙe sÚh¥2|p³ mGîG‡Q‘¤ûþ?üçôž_‡Äì©<@Ãnî‹ö!1Å„EJ¦§2ªÎ„ÍŠ*æ 5/€7F/‰­z¦=é´ë¬ÊÍgm›‰J$~{¿âsÂ÷!½#GðÌ0ŒL:‹üG]ç-wæ”[ïWÑäg˜ßµ ·öÐû¤– ÞŽ|X|üÃÆ±áÅ×·ÅÔ‹61„º­–´ÔãsôÉaFâ>}x¯|ð÷Q8q²„^c`©7Eωþ•×ò¶ý•…‡i؆·û5¯ë·§éݰÿ[$>;4¿·†?§,É­r§?àj×éžf}õ¶ªõl~t´:ºùÙ9ÄgOý“©¸¡´ÂG‰$¥ÇÿèŽÿxnÐä…kÝî†NáqÐV~à°84ÇösIÀ šëÙÖ­t ÍRd§]‚v1?t!+û»ÛhÕ” 'ªÚJö£wàâÎ|xNRߥ˜4Và©W„O"Zß°ŽÊ/ªp¨ô‡(úŸà¶×k–ÑÕy&žB"°Þ©MÇ+Ð}Q¿ ) š=ï’6H±Jb~y0/uSîëH RöqTú|Œ6É]ÑFd™òQÓʲŸø•öqéqøUwW6ö­J¦—s¡òÂד9jÌ_!jÚÏtųxT »hWK—ÁI0Ú†%o­”Wqžu¡"ßÙ¨Ã8Š • ¥!šþWU]éT^ßÉ7oM7Sô©à(Ï7$ÔúB„Ä«q¶¨Š_šm”•]’›°¾^¯·"¹ƒœ(Í(*í¦^¢Ç’Áo«‚åòY/Q’Òœ1‹§ÌMNÁòu >Ö§Ô‡6l“–&ô¨)²»W”Yż@U·A5rJã´3”ŒÄ¤ý[d‘=ÒarÊ?¬û£]Mò‡{ÈÅ<Œ±8éõgÿCg”^uL€?J-êÄÕÈÏb ’<†#¦¯+qÏ%€ €_Ôæ“·Gï!õ¾^l®j¦[»_­¦} ·6'DãŒCi¼§îC‡õJ­XU÷\°›hæül|Ì 1d8M®÷sŸÀjÕ—©2Ð{+^úgÓ29‡s˜¿štš+ƒƒyª}º=¶*iUgUwøÿ k¿ò^®"¾¼ÌÀ €äÿ;†eêädïäìbèbJçàY¢¤-Ä€â{Ww¾E gBöi0,A´DjÂÍËaZ%Ô•Šaü}‡vÚNÿ?t­0ç^mxÊpò„ÂÈ”¦ô+/(iîB̨„Ç TWó°¼ûUw]Â×,Æb³å.fê§F Ë)wqD^þe+\$s¿§Ž $Îîö@w(0QÅÃxþþ-‹§°RƦ»ÕòBÊ2G2ˆF™%ÊÕ Å§Nrb_„òM\ ¶eÊE·64PµÏÑ;?:±Ò¨'Þ;ZPrж TÞ´ª©nã,ÚKØ%<µ³T*Ó#]†±\Î÷Õˆ`T—ÔŒA$PënÒde,dÔìH ªcOIÍŒ~ÿùþíÛ^á*¸ÏŽ¿Š‰ She “¿y#xC]Áe‹ÑTIü¥¹)ݳã†É‡FÞ0Fïe¥7ìG £¿o¶^Sö²„Å9Vy¡ñÝÔ=†åjTøó(‰±Ú¿Â$à.çÂÙŽâº@²Dôþ_4ÙüÆOQ)`XÒÿ?ŸÏ¸EEÖ~K !GWªñdÓ5@Ì[*JÇ¥"%(MKÃödÕŸúCx lŠ4%•šÅ2Gù1î xÓSØgYÒ»ÉÔ™ák Ãó–üÓÚþìüý^UhJxŽ‘…šò:ùDÝU—BIN±’2æ·ZvÊT8uáÆ4h‘¬ƒL÷Á,jÙ~±VªÍbyãfcïûÊâ(ÕGgÜ }sÝÜõ !ŒVïD³¢vòõA yŒÛæs7ÍwX} 0Є~ƒChBxÒ›Žˆ 8ÅÒ1S¨LÔÚµHª·CÖlY0W„>èð0¿©CÑEØ$„€ s_ÄA È2½ Wxävá,y˜]¢& ö’jøÃuj–нɫ™I5næ„(«lgŠ¥jð ûÂóçåÊ>oÃ\\—=ÃåˆmwÂŽÇQôœÌ47Mt- *W¯q¶f&’F#êXz@U»MµpŸ–[È? ê4ƒìTTVÑú8„v{þ9¦Uű¦c|9øÌrr6øv?v£¾]"5boO`GþëRëX ZšÚðE|PÌ'Üuo„8Á=:¡IZuÊ¥Š»ok\„e‹ì/>°T1ÜbSCjÄ<¯Ou /—Ír•äG(ÚA™†[›Z&Pn1oøi2 ?ï¹8žëš|Â¥ƒZÖ÷ŒäZh`,O–l¶ë:xW—jc~„¬ªU·Íœ†~ÞWºi ç bIVyq.SÙ/T“ÍÖðL÷ç6æóM71Ì™ölpOAl­0ÝÑ=É‹6Ž™!¥GÀã÷éb‹Þ“EBxðoæý-W\´ó@HŽ2ª9‘þ¼òÙç27LV^•ÂóR7* ÂÕÜÓJáMYdm~?bSÕ»Z¼¬‡jh“h6§…©­ffÊ…q é5†“Ùå6æëuì×`ÛØ{‘?V=¯_Àôiƒj³…¾.Þ~z ÆþîS!BÊ®bG­kÅ®Ù=U?”‚PŸà>r‰°,èþ=h)oNÌøkjÿ»l©€÷"nVV_„†¾Œ2ñCÒ•ÞêR¿ð ¼ãOôàÿ$}’çaÂïâ\úþ¯ÿ¦nÿMþÓÃÞç­ÖÄÿa}RÞ*Q1Ëe–Æ`ŸåOEœÚPO\\I;ád“ô*Çæ_·[þ½mÁûƒôÇ gte,5ÃAÒ¸¡GÖkÐôƒñkê•ØäFoÅù!Žä‘Qe˜Å'Ð%-O ’ìzÔ?à„ôF§À SÁ™P5v xKÏ…ù¹Èéw'U9< o é‚œŸWËÝ©÷;¤Tv!9&F¤$ú©a‰)Ïõê˜E‡äѤ*žô¢¤²‘3aESø®Q$>M[Àï§Ý³µ§!½g™o8-MMI›åäðFb(ðraÝÂËѤGf$¨H?ö©B©òö!í"½?ô8ï3=Ò‰&BÙ§JO!¿1Á61ä¾€N^/J“@ ¥”|PQCN9€»¶ èèÑpy3â¸Wõ<8lƒãôÁ’Û‹™#?lí^\ìN˜p`D-T¬ãçàÀ-@vãFö$íøÁÉÃ} –ûGwË$ø0‰0N˜²¥6÷³†.+}°<è=؜̧“¢3ÅpQ.‰–×3¢Ö£¤RècZUªO^ä4®'{x§ÌHÂæþø—ŒX@ P!ÅåÜü$§K/ÂlJúÂ1?½ÒƒF–æ!›æ¦&¤A?Á­Ê_ROžV<ísËC sê¤zõò¼}Ëc/±h†”µÎoœöì_Æ©R1èù€% gcuH½“çBÉC~¿aÍSdÌðLËË´ƒzÀëë‰Ot S%å´¶2¼a_;uï9[fû0b噊Ð['ȨccÃP+Ì´ì0ÀûÍÈdLo›fú¯š¨ÌcJÝY/Zêl†s˜ÚȈ96=KùÈDH’Rv¿(Ù/Ô‘¼jÜ,3ÂÀ@×ÑóÙì§?x‘ªŽÊg„þ*2)ƒãü›¨| .ܯ‘)ûš š5± ª®ÁXåQÆÁb, Ï/2Yƒ ™ä[&ÐôªH´‹÷éG×CyÆ7½1倫ñ*s‰±û»~vq`r܃•c€{y îY»o£?Ç3¹ó˜Gz8vQñ˜ºxûYvXàû2çî‹oIe¯§yH‡Dhd…ØÓÅíãØZÇèn6~3ðàZÏ‹–óCï*¶*âÂÆ¦;é;ZK—™i&6²'tï³ù[AŸpz›2~w,¢ŠßýyÑ D‹ìÒ÷ý®jbÄÄOZÚc­l++r ¨àæHžËíóûãßóêtŸZ]úÉkmÓýæßñjíù¹v¸;3:ãÿSôƒ2¦Fþƒ{s{]‚¥þ³xî”ú»¿wøzx2ÓXú©) êª9óöæ·ýù~j2—ÏÅÑãå½VQÐ8÷ÛÍOøiëˆ~qÁõdÎ4‰V6Hê7qaŒðôaDg¹<`X¿‰À8• I:á”™ôJ‚ñEeh$Uáº-ô”æäË|æœEAÐ!>˜dâp#'f7ß8,%^æÑ\\”ä`nIÞB ë…ùóþsŸÕ˜ Óùb”òõöö6¶&8vð`ÍñŸ€‚"Èš-£Ê8 uåòà}(Fjq…äíȲsûû} è*ö§çO?ìéó€ñÛº¶µq‚:2Çi'à?1‹È‚wÛÙ…ƒU¨ì’ÅÁÆÆö=@¼ßÚ]À£d™î­®×ñš›„N–+‘R« ‡zd-¡ì"QG”)@Ä ›—€Ð•l"×b…Iö]C` –1L)FÚfÈ%CƒéM}Œ›ßTü2€ªBjTê¤KIxn2:UÑ—®Î)ÈiìP \JŽ);‰ãûr®ß$I«$µöHŸJœ^Iís¼ Èöª¢jˆ†r‘½(f’ªÅËs¡šJKüP,#O{ yše¤ÕKö˜4Ÿ]àõ[‹äZ™nÉÇå‹&°($’àíÒg„Ì”nd¬ÎøxZ‹@1ì$™¤Ä‚‚Ù“NסÔçwžÅ¾ÝøçGçsi¶Z•ÃÏ¥ÎÔ“OF“_=ŒÃ6£'I¿Äð]ø„”Ò£Öxl¿¢Cåfç°œ¤} ©èƺÑÉCÓ 5B·Ëáì§¡Z Z‰/ËUÝÞMñ-œj½aQ™°¯ë~aZ.E.ICÍ9Iñ–Öe‡¡`ÉL†Ý›x¤;nW'0¸Ýø"×þÙì{ Ýã0,š¢·U5EäѦ¦ŽÄƬšh  c'’²ÒÅN˜•ÉJ褮æAÉ íšYL½¡#O÷œ;ï3üpúù”!­r°C ã¾°s¼ŸÉ5ÇX°emý"Âù~üM ydP`o§¬Ü®Š©eäöd4ÔÙ*Ö*¹åð“¢d´^;ÿÂXX î²XêjœrÝé­ö¥]?~ÇqîüÜH&MER%õHÝXsó#IaÙ¦pÐY¦ÝlY á×Ì%—ÊÔÚÿiļs]Ðæ¨E?ßK×ÀG{\Ž{´qn‚‰ûÅd8’9º#˜þþK%ªÏY8L¢­û† (4BéÝc-¢<žšù̵Ñ~Ô¨Š£òfТáf»éó`K Ø­OÛ¶\§ƒ.b ÒC%`D†ê“÷´Ì» 9p`hêN0ྡྷD{æ¯Õ‘Xäÿ(›GW—Ñ™­éöåÆmš/—'ý2+8âÞTE:óËsdXz›¢lÔ—ð”趬"zdUç« ¸ë¹Y3ÌÎG_(ƳF¡x"¥Õë5OÔØˆ`YËiš"ØÆs)[GQÜ'ÂïžöÛɨѪ…ÃáÀáL^º€žƒ<~ÃP‡®¡hÒƒ›iR0À¸×¬ ý>< cÀ *B87Ø@͆¾1KÝd.Ï ™Q¿;e½!&‹1›DbE7Á.ÌjkàT)1­¿çÄà 6¸¦}åµ"ZNðàpÛi¿¨„…9˜¿ u¾vwùès3{ûl 4SxŽ´f SGù£´‚Ø ZÒ+;@”U÷êE_˜ß&9ØT.Ó!|Îd ‰1é“(Ž8™Š9½Û,Ix^{A’²™F@±P‘ εÓîŒ>$¾ëÚB×^? §ÓS6/º$Éoízî’þyÅ?Hz“—ë-ÅçMÕžÃQ²Çĵøt1p`éÂ$^`º®ÒÌךD‰ž(ì{àscèÂõau0ô¦8f2´§8pŸ :Aå ò7΂ÚÌLã^@P‡3õgF ­ƒG¼ƒy[9†œ/3*N’ö„:4Ž¥á1xÁò¬® ZÁ™qĪ-‰ÍÓnžïs¯O^Çåµ0¤qiu±0ë^AD…˜¦´?Û!Cáo&ˆ"oŸjj锲„Îù³>«"}­² î˜+_×u‰%`Ì–G¦˜¬sˆ}› “[Ã燼˜GnôêŒP™–u(_¬ ÈC?´otç/‘Î'µOÊ©D$!+ÂÜ.Ü€e&~ü 4==;Í8«~îá…h>?ÙDÛ‚vÂdJqKÎm¦Õ™½ŒÂÎ@ñ)$Nàbnx-JF¼æË¬(b°™Ñüâ6cÅN*ab¹jm;ðB!%ÜÆ ]Ùœ„­Á)s$¨ø€ádyáÁw#GÔ,<¢ô¢QU…SÞõqHªQ_°øìˆ\ÕÏaý *…+TD}}‡^@þ¨K #‹Xï@)1™m…ÊM¯ç6ý²#!¯Ì“¤ ¬Òð±îÒ ¢n•¶j¢sƒ)J a­ê¡§'Df°7uk¿5×ê7½I-®&!Ÿµ‘¶T 9ÍÚhjæñŸÍôÈÜÆ‰qܱ軧hÑ üÇ*\æ°‘az ×¢ƒ…§Æ!r¤sê-ÀêæuIY0ÔÐy §2"¬h¡pý£ãO®Ów¯i,×–;”óÀ´,¬19Ã= C$*unL×Bí5ñ«h2UG é«˜o}¶/VC¾|‰SíMSódqü!¶Ö*’²X!Ç\{•¢AÆ‚W™¯%y’<|ÒÙþÇÅÁ‹›³(wz±sî´t/ŸÌë¥]lág=fr ö…l}U=£ºJOV¸ƒc<“th5Ñ®"ž V4«Å Š×Ò˜Ÿxâê#0øÊw‚s4.Nê4›¥ƒz“°ÚÉQí=”J³‹`м±ƒº`ûæÊj¹ÃÐã¬zùÕ"æhï,Ù¡‹%×DTf&<ÀåA \Z’î”ÆRP×_’t\Yùî 0®[ž1-°IišŒ##’ Ú,ö'%_Jö¯’éáäA?S ÷{…ò Ì0䟥|Ùéh¦i½´ûÔ÷ vô73 “iéÌ¡ªº÷Æ%Kp=ƒÿßzEÅÕ²öX8+µ¢KØJÉ*«ŒqÚÍKYî …Wá"Ãøs–xꩉp¢Å/8W˜¾Ù§'Šñ^d«LCl×¹0™Ö’®pøo‹OÛêø °¡æeZ†ôá\ÖÒ3º­à¼(ù5•ÃȃàöK”†\ÎUàÍÊÍ‹ÍÓQÍ~ïy¯©æåÄ€T×EqÆÕñ¿¯S½¢µ¶Ï•Š•½r;>z¶µN6ïöÁ5w"Ù¦nþQEcÓ¤ËMZúWNÃÍöÐ!õtÿ.ªàÄxtxªrøKø¡±cÌZPƒ_+6dtÐ"ñi<Š ‰©¿p…QS÷ 0v´»TmÕ ^! ¥ ÂÆ3…†óH¶ðÃ@«´ÆµZ¨ ¥s{UÃ¥Þ©W&žQ45z‰aåæ—ØxèøÔ76•<ñžwº° i¬9²eÅóvˆ\4­ˆyGÈj'W‚!™Ì( ÖÈ_XÖt‰ î5o褸ÎS;ø[¨PzS[ºJRgFègœ$k=mÑ( {ƒH…An¦hí¦[lI¥ ~§)é`Âi›ë×Ùa˜•…*6—TÔµYÎs¡Ä½„F©ûCž2V,Þ¸ÇÀÿ”΄SJ©îÔ@´oXaýUL~âà` *Ä¢(gk¸ r°™•Îæ‚%0‘&~ W¿&ú™VI× ÃB¡”»Ê£ñè&ŸŸçüJoJ‚(9EKI—^J ÝóF+yÖ4YgÑ·>ê4b•þö³5•UªØÂõ™Tò2*oC¡9ÕnÞ[è çèÁhµýJ•£orÇH‹ùüÌ*c¸2ô µµ¤!›áy“µr)KXÁ¦Cã%k e\n _¶~MƒL7mcyTܨg)\Ã^Ká•uU à‘°í£CÛØóðÓ¦úž^çÕª¡ïìÖÄ¡œ`(•G–™¼}k ¿¼<€ÀHji&z"NßGÙѦösE¸ërL ‹)Ù'™‘¡<ÂüA¡!U+ô+)gÔcÚc…ƒ~íYU×wøØÖ\)ÕP塃ó3ŸcGæ§/÷–LºßP£ëg™F‘"õŒê#zÚáÜÄŒ¾¶LÕ‰›•ؤäÍXš³ÍÐY¨ÊñžSVè]¢³¹LØ5 Í#Ðb[¿Ð·)%üú^ä‹#?¥bßx ‰8+Á»†(Ó¢Vì÷&tÜ/LICä$hº÷[“ýa­ª˜o#–ñf±¶VꞎZœÀ=ņEWº_Å.íV¯>Hõ”×+u–´†BšeäÓjAþiqVÂq'Y0z‘ƒ2ŠI^g'"_tsçÓfaÉŽ«BÒ]*µf–«æs#@¾ÊýÓ~õAÕêîÇ! ŸÊZu‡o¦±æì¨ºe!<>âM¿ìÃ;¥¤iØ'ÈVó‰. "?Â"{ëÁhkBlÁËokõ ¬v3±z= -Ë\º`®³öÊçļ¥á3”ñȘéÕè_twçþðôî"»½uHÖ‡€ïë GÉ¡$©-#¸¡Ë CÜýpr—HåyUµ³RfTmÀ‚dY‹á|t.w(êr8!›(*{ùTÃêÑW¤ÛC¦w]êØûŒM·ÛÛN“­A.€Á›ÂËÍ–íß¼îöãæÀ… ýÂ!¨zÌ”ÚK¬\Ÿãä»Û‡ä2Ü­Xèù¨Kš"[g ZÊOÞÒ:ѶemþÉ4ÑSüVÀ©j´Ã¾:i¤q¯*Ù½³áõûý6®AÑb×1î4›ÆÁ–w®ç±@•Tv›:£¤âÒÍ¿ñ)¢UIêÎRôßq¿rÒÀÄe鮽\£‚[…ôï¾}/ÛB†¿`áý¶!Õ@žËc~ :çØ0Ú¨ÿ}Âç¨÷‹íx9-2§?N@a¸rðWmkš,Õîs¡ZÌ-E&‚m®.~}úÛ+?ˆ0\š½—K©ÂAo·ävú 9¸:¨t\«öÑ^•Js3=¦Uõí§ŠW<}éîÌK›•Pm¦ÿV¨hƒ¼þù&–ȲxŽÚùÖÃ#—£»iâ=£W¥3Ÿv¾aÛ*Bo)g½b‹»A[YÒU¹š¼!ï94‹‡³Åã(S~úP #DajÒr–ŸéÒH$ý>KzZ *æg“€ •JѰ¯e›Ôî¸' °O‰—¦óÞë̵NÎKfrš¾‚:8–‡- ÀJ6ê¡ZÝR§îÿlµ+÷ó¾ÚRœ·ÛqŠ7Ž[ýöŒp2Þ!XÔ‰·…\²ï².¥_Õ1U†MëËÀ~ÈœJÄqË+¬§ü­@Kþ³-?ÆV’h»÷Yh$ªLÐjìv2^2¼(*xy_§ Z‹V|¹ÊŸwåa1t®ê½qÍLž§W=]¢0zAö9®ù¦ÙÆ$ÄšwÕœÿ± ÄǨÎÙåS¢ ãÇÒ¦†644aàP¨Ž}ôèTtRžÎ«„V—ŠØ-²RDÎŽÅ~Í]x­Xäô³¼v)róÊÖï1oqx_õR/ç¼§íufˆb¦nÎ0ð*+ 8Œ5)/F^7v'™@2 'K€š»·³ÂÄ/ð<ÃeH}C‹;H›•èª/!òǰ,¼8bøm£O|Ô7Øtàôïå ÿÒD½rÛ‘“KäM Ÿ#Ô£rè¬ ôëß.ùëL†ðRÅfîëè`ï†ĺîɳÝ'D*Üâ —TaV]| Vp˜8¬1÷¿O©¾³8¯ŠôØ>€†eÍ~'öžÆo=6ªZLãI{Ê ‡˜ò B'"¬—¾0A’,E,Ì›ð¡bh™cÙ¼û D0Y(j;-ÜÇߢVú Œkbüçéeb±X†að™=XdÊ!Ø[L@õ •Ø ö$ж³e`EëlÁV6Ϭک§íÕüq˜„ÓØq :ß?iä~1êX§Š[k•Zþmf_hãp{Zë 6ë7[+Òðw+r¶MÈf€ÄÐÙ:1&½¾&Ö%æ$rÇï'¿å°Of¦{°};ø=îy&õåNMæ½õêèÑ6&¹uLÕH¹}adƒï^Ãb‰‰˜`Ó  ™E˜`¯D¹ ¯¼#`¤¶zdäQt·£éƸ¥Cë‹g£:Vz ð]³C»{4Јuâ¾ÞW³EðÚI~¾ ³caøÉ‚†Ž¿júSFÃÅYJ˜Ø.Xc¤ñ™¬Òó´v!/+Júâ’ô/g'K'Z=XЬTZÄÅÌCá·I¹³¨Hl]%ÆKk˸7÷ ÏõíÊT6”k_f¬µË ½°®tk¯››lkëBZ— fŒ«fIc ©g§ÄfS@6ƒêNóˆ€\ϸ¢ŸÇaêÙbƒ»$Æ¢aíoLj4ò…âH$É58[ùIó2¢S(g„PýF6cDó+ÀÌTÑ©í!¡tè¥g,«Ã±ØEImÑ”>=i_ÒkuÜžÒH_§"P`Ó›{êó„â\k°A·›À¥m¼p Su¥¨mŽùÿ¸CÏ=Óžñg\§(wr…¯öìeŸi‘d%v§ìiN"Êø¢×T Î‰¦ÙI¿ó¡Ì:ü-íí^ùRe€ôXö ýÍ÷´ë>Êôf¥i|\•œ­K´qËß:z±hµákn]_>lŠòDªD«q þ]S [Κ}PŽƒbÉ]"c8¿^ð—fc[ñXìsRŠ)æÂI £ƒƒîŒ&vÚÞV•\¨€ÊµÙî^•î  ×7 ¿|öÄñ*>è¯Së6)¹®Ñ³zHÔúÈÀ#ð|öWÿšˆjk9¤Äª$œOé=÷6tÑš,ɹòÞ,¦ŒœRu/NÝÀÖœ¦ÿ‡X-’c9œ«Z1‡sóìØíƒì¦ Á$•ÝŸm°ý}‚ÄšÍ'îÃ}„†‚á@†£¢#ÄAìnÌÒï/Š?{FèožÑ6ÕtulÔ×D±Dæ˜O-©œ²yOÌxߣæ_¦z‘fµ=QGÄtÝ’27 p¹ìch•§Ú Ð–8*èÐ#§Jå´ˆÿÀõ>ûâvhß(¦hûzliθÛí¬dhštä^œ¦Ì†Xµzàô$Ôõ8°EÕöéNÑ–‡2=y³²´æå*Ra4‹÷t<Ïè\†ë±ùÇbÌWht£\&A,Þ Yw©·¯Ûf‹^k‹ûÒk}_Ýô¾HN´í*Šóï–Ðìý3‹iõ|bFv“ÛÆXi[g„´ÐÖC¬°GbéD’sî¾*ŒÉ"ŠéЉX®޽³×ÞWÓ*õ›´µÑY-]×2³ùk\I’nkeÕ}òò¾Ëê)~›9¿n¾nX±=ô¦jræpf+ŽžµR´ßa}n{J‰1¹ÂP±°æE6…óÐáG;¥uK*Rû ¤žQçÔ±][Í»C=l¡k¿ëìôÍ ÍÜk ÷3=×(Ïôº?™MO‰O³ók»Gi©ÚÃëë5qÖ§W?wŒ*yeзƬc§ŒÜÃzªÄqR†k]((°µ…âÅGlÄýÉ`jèî]m¡Ewßôœ—=xQ…¥ûzO‹`ñ‹•ñ›Jæ«K‰ªðfÊ„q;7¦G?Å®{_àè2Ã%{’ìh¯6q?n¢'¿¤¿cJSSÁùRQ·§Wp±eÎj™7¿*U² Mëjy'¸©²ËÝd^-¬ÌùÙÒ±Ç8úwg\ˆ8ü—;аœZsãÒ¼*KT#˽ _¤ÍŽ•-ÿ²ÿå:4˜5oXˆYµÇî`'¬ðäQTQ6\äFŽÒp(¶ïº§L«Tî“!Xe Õ°z9^÷é̋ߢ)-žF×k³ä¿¿çò'g×›‚Ëï9·ˆTá6ºXbùጛó±+ýK™Õ›mýõÅ Z/~€|;ù*ˆ sÕ4-_ÑYþôY×ôÇh+cÛ# ‡x/^ú´j–®Ë?S!˜½¦›\iÛOŸ\?”\»Ÿ•õ:Ô:"°fµ¼¯yÅe‚?áæôú…¶ÖÎ5µN¦Çv½Án‹ 2Tã¥pæ3!¬u“ŒökÊHÌq‡¢*VzÏ®á9ŒŸð¶Ø)TŸP¶ËÕöȱ½íáÿJôF+ФØt øÿM 0îññ¶ÑQ÷Úë`" #½¼¤Õ—öU€Wj¤YJ]¥Ä™Ô‘.~ML¼Ñô+êH;&=;ñ¾“(”µÝ—],' Ì_G±t jŒä[4Rn–P|Fm¦ˆ'ÂfÈìe÷CÝä‡jyýòÙã<›UH^ÅŽ^c‰»å=Û~úÞÝs‡kúsö¸‡Ïõþ:ª\üC=³ýŒñ#®J–ÿã®,U™àã®p‘ÀJO!YÅÏ«c¹‰7'úí—!ŽŒÊ˜×  ð:Ù´2z}ˆÕU:>[qÀ³G:ÌvúûûR-ímU–o§˜üöl–©ˆïÓYL<ëÆ¼Å73»é{;ÄÁ+wÙS¬J¿ \ MöôÖ¡k®¡h®)®±éޏ §\L'þw†‡þEmKxÌõ{â%¾©’«ßÂZ¿ÈÝ;ŠHî~8þ§ó(Ó$øcîÓL…ôÓøéòŽ>3¬3ûò0Ü ëþX`!Oλ@!€*°@XÀ{ ìB0â…|Ä2Î'£Wà<¥—²%ŠqxÐhNXHlôÇBÌ@ˆ-ÑKz6l@$ò xU%V3;—*›’ÀYUõ$wL 6œ<æ)kÁ©È xþ# £X8:sXäÄeT½ÃÆ š#ªÝF¦~C#éø ‚[˜s…Å„?M û§3!Aéºì&fk¾RT÷eÖk½ cHDǼpàYï;Tt6"Îg?ÅkˆÉgÓêÅÙD­ò"ÕŒùÞ—_ý<µtu•½›¢[öÆPÖêl–øC•©À2kêYHüÜÊ9ꣷb‡³©±9 ’—ÑqF _Ys‚øê´ILI× 6\á ?=–H~¦ÂŒ…Õdö› 9gMDJÐ`@\†gŠ!öåùÈm˜ì*¿Û–N>Uc¶~ß«Dàö—WXw’B&½ÿ[C|YTaól|¨"ÞëwúÐò†ý<ß^ì$rÅ®úœ4é™N¨Û™Ï6€ÃþjP‚÷`{ÓzPþ†ÉÆÐ>2m.7¾’°i«VuCõý] 0o*ÉÅ´ª¶³yY-ž-ay:0ŒQ…W°gIá31!^ƒ‚¿žOåü:Ž¿šum50Üz»N–~`Ö„–Ê(½*¬g»™Œõ~m~‘Â^+Õcã‡GØÄ’ggз)Ľ!$M #øüR–k2k¹!m+ûnœ®gCX{yLû9a—hWÁfA` í¦‘ùíȹëÆq‡ŒújG<÷/=æ~ë%Õf½L‚O¸1¦Ø» 'L¢wÃx áƒÛÂRþ)Ý90Õˆ*ä 0),1.Á  ‡•Û HUi*=e(Wú.Ä;/_ÊfB{I‰måÓßç³ä%*xM†PœCÈׯv^üNË +ôžAË|S?‘hpxî_1Üyºp¾ý ŽRÏŸÞÀužƒÍî_Œ@Cr㦼@§Sã ”Èérè#”¢º e÷sv"°ù0Ì1š×oÜ’.¡ ë#Y¸‘>:–leMV’…cêž5#†ô,å˜cb4%LfacõÁ`J0å±~y,‚ØÇyvr?¤½¿ÛÌfeTƒV1¢aÎGYòé‹;UKëùMëø6Þ²5«`vô&üÕà`Þ/’C¨ ®X¿æRÌíÊy’ë+×Ps?qc»O¥°Ë§'`—mIr¿œów&c¦ï Ð1†Y¯­™J3„µJÄ#ö’Îò˜(!-ר¨'Ý'Cã¢]“Æ'ÁÎIŒ+aø¦YKÆdͰÿ—S .[âÅ”c_÷Ž-›5Œôs·”‚¼h غ‚ÈJ‡ìò1[G&»A|¨{ ¨ºLÇÂ3ðéo4ýÛ<»1øî~·Áõ[{õ‘”4O¶_Uóo¹º¸6^áò÷c-œKá4?Ðä÷Š~}•Œ|+,ºTÛÚ©9÷c“—_«é–³—ß¿]üDR|ðÇÛ¢£KC¼.±rQ"¶ ‘-Ï¡<¾É[ƒŠ&ÿd‡WÏÁ) £wHú/Hzð{Û·»ÀXO0Ài Cœys­1–¼u~©×Л’$AãåëNpïºù^Ý/l’âöEË„ñï¾p®<ê¡;ßM£kÿÕ·zâ{[BuE…ê9Z^/óAË{Å ¥ëŸï4ÿgûæÀÈ¢‰[ßÍAûƒ.‚.º©»/„š Š¿ò<Á´ß®_Ëu'jؽ툠‹)»yÿ„Å v̳/º Š ¯}¶ps«ø>½!5êÕJÄÑWn/™L»žFÌû´{¤Ž«¯’1Á‘°‰ì.Xì­­3®m£ì÷7§€_ÈÂqݘLWv´íæ/Iר.“®úŒÂ=V êˆfÍ‚]Oÿ3 ð³¯+Cל0´.ÃÃσK+ Û5¤ã´ÄpØ&Ìö hý×xÊn—Í3 ò®V*òêux|w¹Û.wàvÔPÕiPû;Ý ¾IØ6óIå¦_î¶°H EùÆOT´g;þjÍ›Ýè¾6ÌäÃgU3Z5žøöb—g•ÿ®:ç:]{.zß;£WÏJwÿ-ì°10\,9Së|-ç|/§ÕñY´Z´nƒY~¦[P´ÞFX_Ê=Ô1‰-Œê¸Ö,ÝucËÔÈvÒDtlön€åVÝÊŠºÉ–¨¡ÌS±ÔvZƒ´Ð/Jàëì±8âñ2Œ‹h­—/˲£0i¯éù9+j@A(Ǭbat~Ï.Þ`;Ÿ}Ù£¥¥™×¢OI·u#‡<Ì*É„ˆhi°6‹»/[«)§b¦º>ý c¾˜Ùô%MÁ,دnœÁ˜oÌDÃʈuâå-:U…T îˆsM"ÒMݪµ.à GKÑ(5”èWÓ³§Õ1ïÁaWÉ^ôjbÞ©Ï,b¦uaH5à8ÖÍ0ÜKG…»ö3Š…Ûìræ¾%¼òÓŠâÂevìÛyÃú–Ù®0¬óïðâ¯KÝX³ÂÌýPaÌ¿^ˆUݲ·^ÚV½¦j6DÍqgâÔù«Y§p€‰Z=×"¨SÓý#`&à;d<š‹×¥að"`WBՠŧÒlùekî×-©AHÜgµŒ î¤%)-Ho(”@ŽŸoõhÐÙýJS¶´Qt»ÏÅUèË!e-ôFe·Û‹þÕJÛOrµh[‘-ͼ¬éq⛳”=íãvª¶€©7YxS ³øæ”T{b ïáôI_ãóÉ\뎸9gWjBÌà>9ñ—×BÈê¡Â0ô–ä (äã¿·8S®üð^>ó„ûÆ9dƒ¿¢©Ç?&¼aÆΧ¸Œæ 5ÝhY)÷ÝØ 1‘ÅtdûªÉÎq¸ŽôxP lmÅóïGž Eœã²9êRÇeU}û™¯üj)plkÞœn×Þ´ ß®±‘Á&p§Æ“ã/¸F`ûƒ4 „—§à¤]3•A8@×LÓ‡ÄJºrùx=<œœŽ QrŒ5¾è6£]/9/¦3‚ºqûûk¢2rGÿdªàY¡0¹Û ì+ŠázOzq³Ä?÷8;•õî×¾î¾Ûì@1\‚¯Ò ßw› |>Y.¼l:NK`Ãa‰¡¥½RºÓ§Æ5×OUF4E ‚˜= Êßݶüö]vд?-}kš :äNÿ·ÐËlºeS⤋=’—Ucè¾<;³eö¡)p¥ç€›Ða±Á jZÙª@Ù>¼Z-ëÿ?Ø»”âšÁŒ’=¨A¨í Ç9–ã<ÛqíøùÖ^üllYMÊ‘˜L‰˜Ž÷àENCNfl†÷¢¹‡!s‰á²=|lN¿ôdzƒ&= Öb•â'b3"©ð†Àt급8ÔhaD2Î{Æó'×€òŸ–ëQ@ˆjQt]2=§uV.%%“›„c W,i1+ud:”Õjçl³ÁŽîûĸÞElN»[²ïC€ažwe»íå@¢‘ë¼þÉ]tsô>¾×õ½·6Ê x2¹ƒOø,XO8Øõ˜-Ÿ½0‰ÆëŽæX·‚ç¶F?g´x¯!ë¾&»ûnåsQZú·ò%/…_Ý•^õ–—Ûíæ¤q×G»ê\'‡úGÍõ«¹äªæý¶—ü°‘JË™)Ù´Yí¹§ÆÅT=R-™& ×ö4¯?ÐÝ4UàB4Â’¨l¿p¸<êæãi¾¼‘`ýðƒÄ[Ðöó‰ËO„^Ô:`îÍ,6Eš¿Ž£PEcÊöx½ª(+ê23µ{Խ摅²¿S´×\ø!cXì¥Ä°`ÄÀ0ž‹{5²÷IZ]•^ËžûL Ö63åCÅÕmÆ6I&´ÕYñ® ŽŸ+‘íŽâ Xˆ î*dþÅÉÛï"ÁòúÍïчˆ<Ü›³‰;vºúzÎF_…sˆP’È5îýù[° ±d[Ke‚C >A)¡ù’ð…µ?yÞ×82+žBÀÅqùŸÈÉØ2a,¢Î.ã·X(²ø-“ɪ^h cý¬Ö=41ƒ¾·¹îêXËYøþ³`ŠÖ¥Ø×Äé= @Y'…:+”½¨ Þz™X8ŸlñH(Ë«‡ÏÚ‚¶F¦vúϸ®å`ÚÞºbZ× ˜ÏÈM÷TBÔµ‘ÑSÝT˜MQþX3¹×õ·£Çñßï>8M~±ðá% Õ–aÃÙŸ¥0Ì^/ÑjQEñn$!ÿvŸ7Ø™yURÝHYWg;~ÈEñ®q‚ã¹õîU²ë¼ìëb~Ï'Ù25ßY+‰£‰‡žÖéÙr-…  Dö‰N¡ñt‚ P©×Ã댵h]7 =Ý’)eÍb(‚fiŠÊ¹BoG½‡w ŘÄþÉ$ó̈’£ vKHo£&3 п½èŒ3êTÛW쫸BØYüZ³>ÚÁ“©‘7õÈ(ŽxÚ†Rž_Ibï/õ¸5’Û?À™h¶µótŒúkç ÷tÎŒò¶²l2\û·w q‘¾Ûs6Ô±µ~FßeLçXÎ"™ø`#^ þ¡YM]¦Cž#“ÇÕþQ™jµ5G¬ëdض×.y=ï½!“Þä"fï!“UƒG{%좖æ-ÅÃím71åîß.c 1\B,·ñÊø,¹¶å~KÖk±'_ñl6·x+šƒ =n”A ùZœeªŽ{?ò˜8Žj^€)TlXLƒºŒÛ¼D¢„aFöQ‚‘0Èn4‡åÁÍØ¶ŒzqørÙî~›Âm3ó_¶<ö-3öÿVòÏ¿p1Žú÷É“ÒP½»Çðah9ø†3'œ€!<6A¸f»«]ˆ1ÃÌÚ7ݦßÝtc«8ÑÇ2êl£4»î¢Ø]ÊÅ˪÷ñ Z ™ƒdfƒ÷?® ÎëìaÀ8À~µÎ€Pù*n”œ#¥Ìõs$5_“üœv7’aƱƒ®ŽœÕ¥:íÓX¬ ¢;JÖW— ˜® £²¯d,Í•‡¤®5Œ—\)ÑM£xf©Wa2Dàza›€i^ŠÄIdamb =BÅChÄ7I0¬â»XK¯h^³1U§@Â.혶\&0t8æÛý‹}›‹ol)pýÈeÌ‚Œ bhçëŠmM)íÎ&q[ÃØ‘W¸,"o~ßÑxäác ŠfOÅè˜Ñ¹ÉÊhíT‚;m)f%\2ñ;×çøúc,’:ž²¡é‰`à躕ˆ|O$3™FÃ7Dè³Ó~2%+S3þRá\ýt}J4tVc½[ yÄ—°# ðÜÒî–47Øùx~=#cU·uN¹è•±9³q@nÄUC£ õ _ šeÁcM¶æ¨’Ÿ†fhlPèîHð²€utq\O!Úé˜k!ܱ£Ä\t¡÷@3x ÎÜïw8þQB˜ÝÖu7#Ö v¬­ØþRA‰bÇ”[|Ã-ÇÑVÆÅúì[gð]D…d4ãÑ®øÃÑÅýXµg› ãwè·j馬}[दµ; 褚­a|Pµ¨^· VýØ ú:訵´ƒÔ{*øîa˜Öè¡¶§.ÓApî*¤Ú xÔY2݆‰tb_þWâù#(Ó¹¦ºŽÞ¦­M)i=è[;§‚ý×Rse“–èÑš ƒFÂô+õ¥0©7¿ôc¡úÂËÞž»ž"¼AðCr¨M¸×Ãaš÷t‹>ôw<… TSå‡ÌÌAb¹G ÖYè=HdÌA\‡¿Â>èØ[ŠÆáÖ&¹JÆéÖ&yJÆô[ îùŠÑ¥Ø”pçõ¾FíX”pB²¡œùY ¸FÅI  ëÎÑ£`¥£©–šhËeòÃ{Î\6>M…ƒ&c•¶%¬"3µH"ªƒb×jJÈ™*éXͶçñ†@jmR15¬†fGå6?¡þÂÿ°Ôv áxùTm[äsÜåW$C"[år6Z(;”¤µ"µYuJî\™ÃåaH‘vÝ ³|ùž­neC7ùÜWH§ì{íXøæn2lØŸïÚ±¹6Q¸ÍÞñ¨[´mÑO—Îv*äxWqó;.q"Æá]ع­-b¬Ž*»RŠØ\=7¿'Õܲks9а­JÜV‹¦‹:&PÐn^^æèx¿Ç}†~®OÂdD€ÑÍH¯?M]ˆR{ñnˆ §Kv™ è¹%.ê cš¼âxÖƒ{b¤c6–‡h¡ðnI€Ì,%3Ò!f÷_ªAü¥w9øtÜgzÔ¸½X¿¦ÅtÀ:# æà³a­¤(¶Ògεі v9eŽíMØú³ùš´H[ͲF|‚õI_“«‹æ Te´0o@Î24‚Ã{+A![µ*M‰l-a_“EXo蜕‚¼ K£iFÛ±O±•ÍÉÑÐÎÒZ÷²Ð"¦lã÷Ø&L×RS£Ìù¢çädrxŸpö¼bêô³»^Ã’¨Kà*âf}1œ5ÇÍ“8#†­QÎy+Û ª ´PU /¹š*X5ZC\ÈÂFX§{]X 2æ¢E¦ÍWs ÚU*È>©‡Ť¼üÌú»Džx2ÏàNstö™G2š %™u »çŒÕ@¶"Ó}MCKygØ’r4Ù*MãÈ~:“Y)Dâ46¥ŸÄ»8âgyæ? ÜÒS¯W?1sqîɈ•Žþ¸ûðB©Šõ›ª1Ù‰Z JÈe”“•Hç”-9W6B³Pé,÷, æ"AwÛ…üj¹Ò×#qÜ…’SAv׬¸tðÉ›Kiè,†q$æÀÍhø‰‘ôŸ(™‰2/Ž'wü{Sša*ÒãÔZ’(·®Œ˜!‡‘SÕ‚ëÅLJvåà >Cµè[ ¾‚8(Jnê º8'e±|F‡ýÜNfÙ Ç›÷½ÿA£°=`_e×¥[o„å+¦Ôí[n†§ÑÔ;i 4ÆœâQÙJÓIÓ< =wiУ­vGç—ÄaÕªΈ:oAŸ<1‡þÚÕ»öa~´F«G—:.2žÁ¼DL³#&Qê÷ŽPUǺ3¢·Û¶ñùŽçÑ C\ÇHÍ|O2î1‹. ò™¤ï¦µEnÝ<“¿<‚À87^a? Æq’Gl-s¦rfð^ãLDs!ãÜ$ä.ýaLô ®ÃÀ±ˆw¼ýÑóœcX£Z“ÿK@Õç;t¢Ìõ •ÓZEåÑ ¾ò¤€4JI•ñÝ¥ÌÆŸˆ‰†A‰E—Co ÙŽw¶1˜ÆäÔÀÀäbs:Ú)† · Ü6T¤{óÚF‹âêúàxz×%f#zmV¡sj$Eà÷É®[Myâ ï$Íÿ$hÐýÝ9Ý¿6þh€'›eL—|¡ÔKò— ïxð)±îV³÷»²…ä‹’_½ÛW—µLÖÿ|©Çbu VÞÚ’l™fßhˆH²ð­ŽiíзnQˆë+„{($ åKÃtÑ £ÌïI4¯~ 5ÕÈ·ŸI Ù_°ö1 ¬#0N]8+Ë™rÃO½\‡!”‡"ö@ï”°z3‰Ý8ëëÄáÈ&ìs#}MOi' nè¹2ª‡Ö²Cß·Vt’£ª»i…aR,…îªÈjëJ+XKØôv¼óÍ­ÆXŒê'ó÷]1ì ekÜã{ãs›íëâÌ[w\DN2Â0ÈBQ 'Î6Äð§êvÍÔ¾’ï#8% Wyá³Ø\z½þqöÁ¬Ç¸oá÷õMÛíT±b>uxc’æ/vÏþ®j‡P«p-Ï+fQÓÑ"B3J(HI«˜5î”_ëP.YÊ!Ae§Þ3Ky™6žq¹ÜRproÍg½Úéû5ªt­T¼j•¡lƒ8é9DR°íôEÙ…n.Ô•T½™å{È[m`Üõ–ŸoÊ«6®s¹X—Ÿ¯Þ‘ºÞ…ï”sÞמëƒ)l¶#ÆÙušÔíœ_´èÞµY#°l4 ¿ÝSc›Àºý<ÁþF@öwW-<ÏŸ<@›†»ÑÓ©–[¶â(QkÄó#2–C†,òYçÜ?–Pç—Ã7¯n…“uÙZ7·‘ùÖ†”#¢þkO‚Í{ÊÞžÓBìQ)é¥D²B_ê/ Õ…ô`­ÛãÙty‘ܹ(…,õ-Ì>”+:­Gg°5k¹(éÂüɦϴœ3a¶ìë7ÛzâÂé£)Ç”/[yõv Ôøôjq 0ôî©T¼ùCN²÷±OK1½; ¸kãCþ¢ðäÕ!vÕz"5½ƒ~¯ø‚AæâGXUÓÿiÔŽvoSölHÏ3ÈÛ9ÊùŒ»å# ²mÅ[d¿©OSBŸ8\pÃwoñ/mm¾ÿ[qB?àßÈ–t3YMJÇ%¤©~d{þO@ÙŽâ‚æh¬{^"¢!$§ÔŠK±]ûå­¯íï0&@p]T›×çðá¢ÔCå•d0Š~C£VUu‡lüGY ÊxÎ 7 ’.Í2Sbíë»èíεKÚ>†u%ÛpC$¹Ò¶õh®¥eñÊ9'IA—~’ap>DŸáÌ{^á¶W›®×Ä9ŒØxº3ßõ’@1è_t‚‡³ ì#×jÿY¼už./ß-Ïä³;ŽÈáÑ.È@x~O ÞìýÍv£Ý#A#û@«ªß€?’]Ç™wxÃw鎰4,™2—AGgß X÷LwdîZë®P=i¿%28xrW-1m;hÊ”.U/=s ŠÆëÖý§Heµì¯å¨'ìçå‹ËéO%ëœFPÏ/@¦«éJüìŒëÉâ£ÙMg=aWìÏ0£‚QñwƒL—ÜϯÛh®uè)ëo°F—í*_—üǤ ­xPsßÖ¡”€ñ5ÛÔ÷,\“ÌšÅÇlc,5!~—Q–ú2÷áj·àéÝ #Å£?æC]%1øáÁo£çŒKŸÙŒ5Y »Þ椑mcʉTgŒ¸ï#êZ/H}Óûs½ü”^~áOKIt¶M›) K1Ѹ­oÐiL Žtçâ\jNÎ+x«}P­ºÑ6 …¡ä¦KÉßr›|rëO :Ð8j ’õ” 6ÇÆ7¤®Ø´Q4k½¹§-É[ü¿±ç½©šÐçàø§­ï 0ÊŸÞøì¤ÏÓÄ^WP}è:µf.‹սь ÍÒÜgzH<È5WÕöƒ"»æ±F}cÊÂÝÑ´;IÔÆ½{òž>%[«ôS쨈‰š³ÂC©»H×åÃŽõo¥&0Êÿ iÍhîðÄSr=œ| ‘‚?'V¸þ‚3Þe·ÉÕåc$Å^Ã` [b¶2Žù¢Ê`Ý»$é‹8šŸø‹ 9&Ƀ“Îòtf¤² Mëµ.!¡ÑqàÆ°8½âø;p]МË{¶k_“é:.“?”Íœ¼IðŸO¶ˆ?^ç“äµòôÑÈAy´àÎù¨Ì3ò¼¨OãQäp' ¨¢…?¦œ¡õÝL'¾ú¿XÉ82»ëhõw9Çâàˆ¦]!&²—6R‡½G®FÞDŸÁ Q6~œ:±GD?ž(¾Åßá³îó°>¨ÁÒ²ÜÙ€ugÎ>|H”Çq=v’aNFéQ™„; ëëÛ”8xU0ü<¿Á­šý³ŠÁßÐÅÀÄFÜÂr~†ˆÁG³„ðWFp—†þŸÕÿ§Á=Ël58€ñÿBËõ06up±´·£sð¬ÑÜQÆfGð;«›OGO÷`±]ryÜ_à ˜—)½ñÞÙTãV— cÌø¾ÛI¹]Âwð÷$Ì~=j«ÚY¹5Ëʉ .KmÁG‚½ŽŠŸàµŽܬ¹©±]U|Љzn,ZíBò¿Ð°´ IŒ½‚À‚Ð$žŸ–óo1Æ“!àÞ0ö `“˜Í ³ ƒÀºP4þO!Ô!³9“&‡"BcD@`nÃi‘A­°Ä§ÖeÖ#”“ , Ãí+ \9•œ‹ãê3bí1dªcöðq¦ÍšÈÜ ²¤âà‰ä-àzŸ¹Æ~ø€ÜhÖ˜€ŒÓCÿ –³8!ßa £ß£)gÞ5€s5E=̵4Ǽ£ÿ±a)HQD¯’5 š›žÏŸ2vFI¯ZÀkØÏàoíÀ›þ»Ñõáêë5ÒßJK»ùU½ó蟥×<ëÞ»Ñ}™ýz_ƒ´~ëk˜ëñuì„Zé÷©†äD9ƒ˜0ö‡òOxô— Àˆ¸I< Ž ˆYÁµˆ8­ÇxVÁè” ºJFЍ¨§ÃšnT”=둾c¢;u;Üocž™5&aB´f…ß#8Q+ZX¿”(>QLÖãp×Ö^€A/Ó¢îqÎV¼îÝ+ô&eúüI(ÌÅ òG>´85¹Å'Œ^w (®:ˆ °h+³—•€üÖ—”]‚Ï­‡§±üþ9º?뻿2/ŠàËrê2>cäxÝ9u ø@ &·`?C%¦OªÍ8ÌAOœZT|ço<³À=|C•íj•¼×rƒª3˜/QPÂIŒ|ã’Š^ÿª7Ðq8\ƒT8D$U…HK üNì*õÈ| WYq¨~:ǺK­¦(Ð6•¾õ”ô©g.±¿Z'áµ4ƒé;"*”Ù-†5åh‰>϶J&‹;a;&Q±ÆiÂ0LtF‹Î¡QA,# ¬È´™¬ý×ÍÕÛ–~(¡{¦%æÛë?È ¢Z$Dv ªvmøzoØ×b"‡ºQ4z-±Hâ™^öl½!Ecñk:]©¬9F•T¡v¸ÄVbe1‚r•+Es’–eASqÛp„QêôÈâ vÀyRç4¦Gx®x¦ë•‘FÞ¡ÅCk£étìŠc°v_ø5Ê‚¾0]©˜ÌAhh¡h¬>cRìñ.•cH5ÚcFÏX[ѧ“WÁ›–‹vÕ­t6â)y¢æ^h§7Šéhufv)‰*Tcå)FÊå_Ë ¿òÁü/ŸÆ,Á<öÒ­è<-™)ŸòÜ?︀eaÐ&! &¢ÖKs•>`Îpȇ¯½•¨Uaž1%îÓƒo’ŸÓf8©Q+Õ`\µ¼ÌæX1é‹ÎƘQÀí¸·‘|ÙÙó•S~5LÔÛƒ:`#ÝIú¤€|§M5Œ­n$Ž’LÉ,<Õ*ί¿|”5íùß ãuÄ»G@]ÐBꌎXm÷ªèó1nœH¸i&™Ì<žÝÑ„ô8fF~˜`gEjîäýüè`•†/ñí£§¹¼Ë99ÎŒåí¶É<ÚåÕKƒ„˜ˆë<è›w’ôKË|*vL‹/"·+ã:]èÄ  Ç*µGä ^Ì ut-‹3ÔS›%§›7 aµ!w!ä oZg„#/¶÷×¶O+Çã¢LT@ݺ­I)ÂF–µÞÆöòY±Õ‚èÊ~Z½[aƒß¤t‡¦‹QzàŠ3ÝR¤‹éÈJ'‡®ß¾¶9+|µýž3e©æBÇLí)c;ìˆ×xoVANŽ_hÇXD­ö{²cÌÍ7Lôñ8eŸ×æ÷æ$¯@AdxZ_/Sçš¶ÚxþpÕ!Æà™~a®2ë0\08`M¿âœ(C7‹ž›è¢t.ਲ਼¸òüuyÖÂ:(27‡—4¥*l #„%¯µc ãÌlº]‘;ðª‘Jç ŽY;̾\b”²Ü—–*Bh)­ÐþIDó,>’r h3BOÊñ1òÁ×S&t^ 9†òWM´u|»4b™~ü%ñ¥CÔÛáª_G t+Ápàzc¾-.Þév®Ùˆ€Z¢Ãh †¡buô?=¹*B]o’ïÌ©—Ñ \RE÷Û¯c.—œ k_vKó«sÚì5—æü ¢m´tVø{ VÝñö¨ªoSAS¾ªÄÄunºÓÚՈƾŸ´iMË¢;^]„ðYÕžDv¨jT¯kL3Í뺌õÜáð X»\^^ ¡‚é_o¼âß ½Ã×=u澩ô¸’­zƯ»ªŒ¿ñtG*™¼jûÁlu`3 Úzþ‰ª‚k|ŽHj¢ü•äe*œó¼ògKwï€g~ÑGñŠlm`4ˆIŸÑ¼O0)R†Q©Ö—ÿ+ïE¢,T  ‰þ//üÿ0°Æ#Z²8⨷º5Å)&’­¤ãü¶ÜŽÇÒn‡P„¦JŽ$à“Ö졃”RÑ¥ÞêÈ3rö¥Þ&5ÆÊ ’¨àt8އƒÈèp:ñÐ:íNû‚þ‡äÛ=¹»˜?ŠÍ›+Z¶7Y¹k?Ÿ_Ÿ/W·êß.Õ_qs>cÄòˆ¾{úž-ÄLjÉppÜÁ¨ãâÁÿ$ÆÛðP!¨‘ôæSÐ ±SR!°Y ©q lÖ%¨Õ´Èl‚ïScµ. żƥ °Ù!©qÉl%àµ-Cê‰K[ŠÔ¸„Ö“ä´/( Ç¢ŸÛSbÝ%äµ.Í¢Ÿ“Sb‘Ý2ïiyKƒsX•¬J’‚s˜±fµ/ é|5¤õ뉃{,.õš‹‚{LŠ›Ô¸D×I‰}6¤Ê ¿¼7¤âÝF–sš‚g°/T©y×Yå­.¨Õ¸„׫ŠÙ¼q3œq"'‰ê¾Ó–a&Ùyw•z!LEU÷fÀàW”P"ÆáÆÅvGÓä…ÜbÉô9Á¿¿Æ ЬlÑ.(õûÁ(¾»o¤äzúS#¹\iOÊÝåø_#) Ü÷¾Á\|ãNâËæÝß»r¹Ž‡€<ÁZ;ùd ±×T¼+C&–v©YóõƒËL …q<È/6œx`Nºðu|^m?ZÌßÁô©Ü‡âÅ%çvûÁ›‚îR Ê£Ê/¨ ÷¶hkC÷ãRRæmSPiuÑÃÈ î£ÄUqªšñ?­LEÛª¬¥ð–Q}­'C}‡ÆÔÌ`u%²W«ðë?TO¥YÑß »=MŸ^ÖÔôºøæ°c<ß$qiW„9ï"Ì”ƒŸÐz;Ôº"sø‚6둇3År(Í=Ѷª.kU\Î2G7Êû˜jªUÒ´Ë”u¯1­¸6ë3œtC­¡‰ª¿bxŽ"CËÜ t„:…R]ê°}œc–—° ”›oÁ³?C…:Ã…ÅDE²ìüÇZ±M55$4Óh’‚ö¯J«¼Äoö-1c­QËáðû5Œ¸é‹ZÅ8ƒìQjb ë {èO÷³Yèœ_4D®¾†œÙÂÌÓËY:±kñÛ«ïð|{FïV~Ã^gm³b¡ÛˆÛŽ+>B¼‡úÍUI[úëÚ*°[+ÖæmÏ+«8€mL·Eà_°æ±@Qn~œà’il©ð¤ÊóbHÉw Ìÿ¤'Œ"ËõzkÉLpÉ ÀÒeoG"‹ïÍ7ÓkÏþà}»•63EŸâÿi‰\H%˜ÐjJŒÍ\‚® G“Ã?­«‘ÃAì“2ws. ‡/ü¶9!àO ýÃ'BP·òê7ð1T¯Fn—Öl¿;ÝW³FŒÓÑàOÙ îW†R_ ¯â€"õ¹®b½ t†«ã9@l*Ä¡ hfN…zÊ‹‚’—þŒSJENEN2f½†‚4Ø’«¼é½<.÷YV“£dÚFOÜ÷ÿG+žX¤™ÈcÃxßêÊJÖÊPœÝöIä'VKoøÈ?˨a½{ Žô—…Üð Aá)¡D®CWyQ½u舑v³ðX2gÌ)íªœ‡d&RWäoÑ—…‚óÉ-º™Ç f˶:ÊrØzÐSïmýBáÈÙ)ŽpS}¢–pSí^h‰¯:øU ¹ q†â£A€ˆåYÔ]¸|ªý°-{yy—"-æ§Ü_jÙ_Ñ¢‘{¤‰tkPÇ~†TÊ7‘]ÒG¼Â‡.Þͬªþñ']º¤ó\qÍìôBon:ïÅݬ°‡‡ù.ÛnŸy“r Þ³YÜlwÓÇL+eyÌ–xZ’ŸÓRóˆN¸$À®»-[Ôí‡gHÓÆÖÒx+g}WÀR·35<Ÿ­³;7û†ÕÚoCµ°§o¼M[/|³¦öÕ«ÜÖ7áê¯+ý†U¯ï–^Ïâ®ÎÉðˆwêµþ ¬^³§F=$+Çú­ÖЭv-ý§µ/½ §oÂ0•·¸vЇÈ\ô×á÷ .*˜· Vd½'D¶^pQÓ/vNwÕQކÏuéj­VotO=\.¹9‚IÖðɲ3XXeÀ_oŠ î­1{#’Z 8ZÖN—ëéÌ[6lÑ{šo³ÌØ­ù㇔¡^>(d¶fË9uæ£fÛÂÌÂÒ¨žh¦úX_²Éa#ðtüM’®XÁ:Á‡ŒT >‡Ysx÷˜ÊJ¹6m[N@PW2CШ]PþÎ]µ#WêÖ©-CdŠ>ïöCi0òÇuwUöJ³~٦╗Ž4Z•hd*å ›'ñ9ÓŸî=º‡l¿É×Ü<ñàÁõ‰ >×Ùâ_A›ôDg#@^ð`D`U¡Åà\'ÈîÑÛ¼ÁÇkJé2Ã-%Y.IfIÓå®0)ósôø^ýÕ·ê Õ’Þ§EtŠÏsݼÓCŠó¤ºcÈ9äD[N7º žóø‡À‡[®5µêÞ1ÞZÎpÂEËöÄá5ö†+.::ô2ñ²°—2&^&ÝÕ<Ã/É^̉ñçõçù?Àÿ³¸:8<ù·8@êÿšAmæêòŸüšThhÛc·!ù®ÕaŒÃ§"xZO‘OpŠ¡€»AóJ:S~Ç¿C,ßJuµ‘¬5*<÷¹‡ïº4ä¨Õ—èÒéðé×BÙâ t88ÛªñbÞÞŒª¥uö2­_á€ß ˜‰íÂ@«`VºK4êT‚rÀêMÁ@e–g£pN‹xµ¡‘_¨ÔAËœ.Ú hi‚ÊJ8ñ‰ ?]Lr›—$imä2gEáÍ“_îçYùS°5gwí#2N9:¹~Ö-5áñkp´?ò#CÒ^õÒ:;{¹}6®Ý½yß5ˆЅ€:Ï8K׌ °\9ŸLxV(Õé &Ί®ÝLpv1vˆ[¾ªTvüºyA'Af»Ëhq©“XÒmxhåÙíàQÛËéçäÄCbÅ„eb÷.›(A7•58üÑT',’KUß‹T&¥ÌNfÍÞ›œ™ƒß[?ƒ‰ë.ÒsÒsy”x"ÉhäϰÁ™“Že‰©·/š9Ó;ââá ÀæËpÔÈ—“®¾jÄG‚‘„HžrI!kWƒzFÖh”aNìAg4¡!&ŠñwñÔÇ=°HbEš¥Kgpvͱq!3ŤªjEz??×g¦©…c‰7’³ˆã,³žz*ajT.¹!™õ>(zp_Bô>L|£Ñ[¸¦‘zm‘Ú®,Àì`UàEɦ4ÉjMKepWØœE.BD+DõH.(Þ –pÍ1. €~.˜¨ôn”ÈÀ—Ùß{G˜ææ½›¨WÒxêM¾1ðëèå\Vzt6vO‰kG®ž^&üt޲ )×ñ÷çÿeu*{Õ´yúeÿnÈ·ë"ÚÕÛ~8Hè´÷í$«Ñà—ÞÄõ=1Í•á$%Гk!0óOÁÑWþÜîD‹l6‹MàÕ7Ëö{Q.‘’s¾ƒ) :L³{éqÒãÍ&¥åæÂÞx¾´_’×u@t¸Ž åiñFzÔŒ»H[zÕ,ԇΧî b ý0øá1ÛÒKçôDk{}õl¤¾Ôæ¸àö·”Ü÷ÞMÛLc¼^N“Õ&÷¡°¹¤×§ÚI8f©œâWX÷||£v›”ÈhE}ÃÏu±HÇI ‹íME2m-k4§„e÷Ú”ózZÔ— ´”ôNÓ"ÿ3°ÿÉ›/á·LÔÅfgM/ª_Ç×s‰®#«­>U3ƒ×‚#üwzÍ‘GÕž˜Ýßvt„ü·E×)¨Žåš/Sªsa(â.]!Á»<;ã3ïÃyðxøh¢öŸÓ¬x?aTßbs}»×á›KŽÕôÛ*ٓꢰ¯ˆä Ry—·à^œs $f¶zxµaäZ!ƒ°¹XlDÜžëœ%YàZ1¾V¾¢J¯E ²€›Òíº˜‹FÈÁ¹*†âÛhNc„7 ãŒÄË5Åz“¢CÏ¢œæ Fmx÷ýOÊ-r±òÒ”×gƒÉd$°‹j³·ÀûË+æÖI~Þ]¿Ö…é™sxñ0ì´+'#lÍêÌTz ¸Â‚ÛíªQðo \ÚoWc©”"šîD’´sJìø¼Z·¢"Ÿâ % ½'d–OæÒ) L‹S‘Õ†¦)Nÿ;ƒòÂ]X’çAZ*çô‹­„H…JF‚è  œVæï/mM¿×·÷÷îùi…^ý®WG¶×ïn®×œ¾—üyäO˜ßmŸŠn@W_ìÿÅÙ?D ,]¢à±mÛ¶mÛ6¾cÛ¶mÛ¶mÛ¶­wë ºûÖZõ¯Z=Êå("wÄŽÅv{käm°¡à&³AG>jLkG„äOÞöÀ^>g,úd` }ÕÜ ˜ !cëAŽi UtÆ#^-ÙI6w$¿þ‚¡¸·xïÇù$Œ_«º9™Žcì¹Ù4‘šèËø¹­Î›!;9ÊE£‹o–V:~ê%(yãùø57mД2.$7ÉÊNR<¬9N!2?ˆþ+ÚÙÊ9«ÝO…¡Ne@õ)ý^B_ù;^£^ ´ˆ!UA²Ä>¹×ü«oø¶Ügsèš>ÓgXÐø}Á³©ÓÊžL¶ƒH9D5e )FÆ_å‰Xž@åcÈð‡è×+Š>ß!KIÔ7ï6ëí§â >&!ŒFÌ.ž“gØÜûA3õ\Ä«5w~—¡éS–§&-ùf†!,ÄGÌÁc Äüð>ª9ÇNúx|ü «¤–h^T•g»__³švd÷wgnyª‘ ÿÍò|[oŸ¢ÿ›÷áÿ/°Ù¸FMß[!¯§)gÝ!³Ø6§”ö™T§€Kâµp^HI‹½ÄQyúÄöFÇÑo»§‡ P\"=7þ3ô"]ïÒý0ÇnÜ?£ƒ{Ìò€öaÇÌÎÍÕü•¹÷éncS_ÝÖûDèW}ô ¬w¢¨H\`÷ ÔYt=ß Å¹“ìz=ô<Õ€WË?‘fHV<!ÁßK©ß”8•ÙFÁͪÔÀ «Sõ—FP!Ȭ“™À·³‰@^Ð-ölaZ¸`B^ø«F`ÞÿÆyÎdO!;¤r‡,8j^ñy²LóCàxrNÖ"—’åì‹%îŽ8m=÷É$®[l<ÊD¦×•½n@syÇÆë݆ñY‡%øƒÇ¹Þ‡rï@aþ wP'ìÑ º[ó¶`—òVÌ.–›>}.‡Ú9\Î ÷ì/ÈÏÍÛ±ÔÎhIH<0w”Lu`^1“EÈÉ·˜ÕC‡sAÀ,y½šRžê¢æ½vÖDòôI¥4Ñ«ê@áî‹å` É/òðˆM_„[±ȧU|•¸‰?v·¥×㦟õq¿rÿ ‘ö7ë'OØòZƒ‡A›Çd1@L  ÓLÈÑ nÀ5à ’¤ÌÊÜ’ #CAÞõ¿“Ñ L3¾$À6ÌŠkã5 GRzKUO¯Pt‹#EB¤ºH] TôÖ¥C¼ê®À L´LÏÈɉyaÒ¡…g'Nuî›bçʃ/ö+«B @Kv ÈÊmÊ­†¿‡SeÑ¥ù™†¼áÐwfò—oÊÞ«ç§ôD$·Äƒ_“Ÿ+ÓC›]ÑŠ6ИÊ3YŠW\­dȰÚÞ*W>‘™£•¦¥ Þ´¹ÊY~B€+b‡`¹ùRÔÅ|þé}Áëy0A.ßLN™|ÅÄ›ós*Yô1=)1˜Òöh$ÝòœíÚUÇ´˜Q,W&Öcé,BÛÃ3e ¾9ôq¥Rÿ„ìÜ™7“xô, uòŸÃá¸OFùhSíhÖ k6eþûTö‰è醵×]ª·Uì)G ×^9D,0¦Å)[œP‡æmyÖØ6FÈÌ?¯NT¥{ž FYFzETTźl4—£ÂòˆR „&£Q{Ê> EiÑUoñðŒÁqåcÒ°qpxNËÔF„±àÁåM:$Cƒ¸pÒi£¢¸’ÖçÔf2Ú&dT®mr|ÉaQŒÐ†4°FõQB‘þ-\k2e × ‰ŸÔLѳ¨y›l úq­•O¢bé«3d‡ãi[οL‘~ñ +ß‚‘éÎHÒqŠÄjLñòp°ïà…Ø‡QJ³¸^ˆc“A¸Ì»ë©¦!þüŸÙŠ ËÆÿ"†Üí‡Ežw…±Å”óÛqçd¯_‚k ³Ä:Èý ß«ëŠnòö†äªÄÂ>1±Í³rÃä„s7bVb‹Q:@|"›‹Jª~, j j¶Üj „ä;¹ØbEÝävd„;æúûgte<Æâ˜| YÿF˜ÔöÆl’ª,öÒ¨Û¸Ýn¶ð_¯WäÕì\ÕêöüLƒVCß.۱=ëÈÇ4>_¥èÜýŒß6ƒ„Žý*bÅvŠ)_>š¥œøÑOÊ{–ŒŒò·;‘Å!e$цö± OxÄ¥ÓI’y`2ð ‘nÑÞÀ êYuEú%±,ÈŽëqf9««[úR¬bX.'3Ø&!xõ·Œ}ʳF[FÖ#JüD´uY2‘ß™ãÓÊõ+Û» © "—þ“wÏ1Æ¥2xP¸§]Há; L ·UÄe$͸’ 7eyÖ Ð'¹Ÿ­‹G馤°éøÚû§;±&]“wò`4q—„ƉäY;2ŸÕº¼·ìI–j »l ùíE¼ÐJ¥]³!Ž´¾épg}“Ø…‰¶V‹c. c{ú,n'Ƴğ0¬ôCšÅ0nм~úüà Mµõ7­ûj§ÙC\9 ‹?2L^f•ÌFòB9½ùÄd²·Å^ÀE€E×Dz¿±Ž–P^äÐ0eN.SdwëÈz¢æ„ËP¹iYASÈ{â¸ç×ÍÖo@Tb™ÂÏ?È­‹WÀB ìœh9ÛLÜŠBg{>î<Þ„9XÖÐ`e±·A+REíéüL¤v$ÂÁ4f@{ 4S<ÙëÐ?ªöŸ¹ÜKB´®ù1A«DÄÉ!:ªï ù,Êì)HZDΓ)>Y¼$&Ã;x†­rõZõ½Ê¿*àFéÓü%N3#Λ.ºžMFZ°BÜ=±gQ$ºÝ†C>1µóZßT‡Šmܰ_ FStÔ „ „{œG?®ÅÙ‹# Þf9&R¥ÕÄ-6;Mõx;Óò 8ió"”ÖøE,räÓí1kêSY¬ÛbUªÉÕG¢PS¨Ói¦Ý±iy|‚ÉÛiÙŽ*Û`øX¿ÅŒF§ÌÒˆy˜ã3ãlªµ$s%Qj(!⋞U„lÐ{®µ×kû;u¡¦CDÚ[ö®£“ j6µÒB º …B‘-“!à ¿rôb¿À`‚óxµ^_…•í=u¡ŽmçÀ/°Vzp}CäÏbümº}/[?*b­Ù/vLÓm˜XÖÍåÝnGøH‘Â2ÑQœFÕáãóîŠ+uŸEÐrâahË— #TüÍê}‘Ê®ÄT.qâ ð¤uœøŽâ à?üñ¥œ‘ã}~ÀÿßÉÙ2.¼€ý[&ÿËG’çl—FÇ’šË%ÊEýÙN¼QÁƒ) éy­÷èÌŽrHÄZÙµêªR‰ïAxÐꆠšl‘¨T +ÕZZŠ0ÿ>Nßf&áüv𾿿“6=%«zå úipûÝüíðÉUœ pžœlã¿BDKö*¤Ð-à©Ã\K™t®\,zŒz€ï¾ùFà Ú•` (N’èÎçÉyçä?ó»ø6€8c½h^ælÉÍF¡TQ=°,þ%(^(@WŠV¹lïÊðŸ’è ç­<¹rÑóÀXýý·ÂʼV)KÀX€ìÿl:ÖvæÆvÖÖ¦ÆÎvŽÿ1žî§,‰Ô¾«ºó¡3(W&/m·Þ•UNÅã.s3s&J´ˆÔa€P°­Éåß»·Ý+[‹ü„”žJUR±ÀÛ·•»®,âWBM”Žñ4Rî?-Ôl]RÍ—áYujqñ"b×;7ÑF­´ë™‹,eãwQ=ë'"ª$ž¢õ‰@óÀ±êI#½zÇ™6­êßÃ&|B/äl&tpŽË„£võÔ-îÔÒ*p„ƒ)’“h~Ú÷VÉQ´³,2ÅÖýkFʵ`1@H€á9(>zb ÜwÚo­Ì# Ρ¹Nš6srò¸¸8A‘a0.ÉGÖžt9²:ƒÌGFé„êý>uIÀêY7šM Þ( 8•Òâ)h´(¸¹Iœ šD¦ 5>á ^1Pp ¢I¢ uSiL®ý €ÃH¦Óì¹uçÓzüYó¥ xßÌݾ¾÷9_®¾.î¾Þ<9³ëÓXÝm¾œ]y»Úƒ5Rî¾ßŒŸ»çÏš9Žp¨ùû2›€gàÈÈ„1Š&Ì3Z Ô ë"M2è}jmÕ…È&Õßþ(˜uCm=k¤€(JÿnŒüâ 2JIl‘¼ ÿb7ò²õ›Z$¬ïÒY3vm ¥W.Hbq½dë”S§Þujj¿S!?äoíché=Ê$ص½>2m•ü…ÆqvšX©Xø Ïr¿©2+<¿µ¸û±V$+}ß©6bN !lÉ¢æx€e‡$sû‰TÏËÈ4ÃÌé!Jïϼ}±Þ¶ó*+ólîš)ÉPΠÿÜ8úuŽ-'ÑMõ‚³(¤Su’…äÿì}y{²§o‡®²ºàÏÔ}ŽVV ÔÎÙN-0øfÑò맨Lã¬àþaŒµ?Çà¬Òœn¼€ð’kï;¤$âî²ó/$BÄ'ð›5Çæc·_ޫƧ”‹SÀäEãÛ>JÈ0Ð`Ä NìÝ ¶ Ue¹l]àb»•\yÝïòugþÀ(Vòôz”ᵤ‰EC|̬¬þžÊ NˆÖÇLºÜkçšØb‡Íá(_Âtô}¹û8øò{`ëÉÕ‘1SGã¡ù+£©¶äDŽCD½ù¸³gøtwæ!ß¿~ÿþÆ÷W6£~¨UGeÁ·˜ƒäìévçý¾ÃÆÛ'Õ‘P~ß#:‹ÆÄwFË;ǘø|öèÝ=Rä´í¸ ,‘¹t “ÉæJÙ#ŸÐõo'®´MûäA;%]R>ZGýÞ0˜YÕ‡¾où”Á>ÜSÆ%¨È2UŠÜg7 ‘n ÝâˆP§Í°U™KsdŸz?¹;%>]ðÞe“Q툑òŒÆö» 1ˆˆì+‘D7Ž?‚:yÔCy? Þsiø¼ˆå ¾Hü™/é“õ_‡]ðžï¼•]s¿~>Ïä=ƒÚ÷÷ÀóG 1ôõN¶‘õýÂOYwC.M€vgåÏT–#ûWµ„¢Ù¸crF£Y‚  àéX\¹=}ØÝpcKYx"HtfÿÂâq9ZL§ÑÀ$ÑPÍ£òè~ÆCï{-!lSàë‹Uì¤UÀÃA 9Aø³ú¹Šýà V¤­›K4µÂ:ò/æì¬'€ ™< q¼½D4ÁØß¤%îoi  ‚ûs g?Ïþ!‹z+ŽþŸ!Ô´æ0w n™Aw¾½ÇWE@´©r$ºJî´+¤Ï>Ÿ'~=Ÿ"5§û“˜”q‚ˆn¾è8j€Ò%%w¥Pp–Áè¸âVÓ€_«˜<©úsîþy~.ÙðgGeQX¼üò´ûs—–Ù’Oèð†¥=`Ò ,ŽbYuôîKsÛ)¨礙k‘É_\r╯7f?ç)ÕÇÇ8q? ÙÓJ<€'‹…hõtø~ª:š©:zÿ}ÿÙxk:Ú §§q6ô…Èp’Ô!Fø'a®O8^SÐj2K  ^iº:} l¯>^SÊÜÓÛí!ÖVEƒbØ$p]IÂh Éeëx»ïêÉý xIȳ>ØT[äˆSöë«nØÑÁ¡,€™b +¢‚Aë®x2¿‘‡R©a Ù P›@®Yƒ`i(½Î½A’É –X>%(+Îç1L#7Îkÿ·kÿîÖ5«‘—/æ$öÆ2ÎÌfÕ!P¾3xÈüŠœl†3¼™³ïÇÞ"šäÐZЦh,»‡á¹·øhã@ÜÂgœ¯'^pu á2½JêÂ-ªvýûôa{K̪¿½ƒ ÃKaŒúÌ?OÏFв÷Ÿ“y îƒG,bxɬK!ýv.lˆ£òèP£ò'Å|t^4П®ËIa»¶‹/Y¬-“ ‹±‰žÛ/A€ËÓ®€¿Óœ†}L?˜°$æi7{N-ï¶’(‹õt±µÛqNÿ§Y`‡x€Ü0f’0º`®¨Öö¦‹ ?Õ‹86&©\앤b• ]›²1O‹å¿ŠÓ.ì=9ÕlAÿPt£‚azpE>?¥ÿmµºfw[Õ4­é™mOŸü»'šœ=ÕLú>F}añô·²;´Š+©ðœ‘D} K*º Jïñ$µ‚ 9$$ *Ækóg˜vŸ =RCc]v5ër!Uì~À0ëø]X@ôB/ÅŽÏrNáÉÚÈ2àB°3˜Piý‚¹¿ãåâ?|cÂÒ¸ ña 7+ÔÃ!ÇÔ®ÜfŸŽ_ø#CÂkÖÃ? ©‚áT‹h¡ü‡=s~‹)¥Ú(ëXÈL'-…Š/N‹Òb?æhJ{ÄÕ»ä4LÙÒ}lïEo÷Ùû ñ-ù!ß®°]8&Ly”eÓA¬¾?%ˆ8Ÿk´åC*bçt!¹°Jb9‹x›bp…V¾(ÏüÀóºf«.á·ÙÒã” n͈]-`ÿZåDãëR/¾ßrÌ;Aµ‹šÿˆOÄ2O,Êb9%î{Ìø´¸b‰\1³¹DŒd>'Ty3 <ê‡fZ­Ã BEz¹6 ª’öÖ½ÝÆºKÁèêk’®¼GQÝÇ •z÷ºÖE˜Û„5qa|óUä1e*Hbº¤Ûr´Glk!v‘bƒG36§!ñˆ3â['“þx ŒtRf²ÀÒ-ÿéz£ëTEþæ%;n>›Ô›j =‚ëèQSfƒ¬¸€’Cnäd;}Eo­lQbh|ø •°S¾Žßmõ à%/Ûàõ ÖY N^?gÎ'e¾õcáƒ~Í#+\TEaa½N ÁœáŒ9ùëÛ|+v“óŒ?É]”7®L\[ºm;Õ¹Q7t.Wð 2Íyjµcß!8&7a‡t)U±ªQC\(•Z!@¢&&«²T™¨ -ÔÃT• R-ñË W7‰c¯cQI·Â¢‚NKTmý»GÖ߯jäö$åV£˜ùRcƒ[oR¶‚æ9sÆÍt °9”]ûm¼ÝëOév—~ͧ1÷î"E1QIÝî`ÿÅ8ç‰&·`ðåwÀ}C÷wúŠðŸø¹ÛƒÅüeð„4AM“oŒ2oR©õÎá&ãÇ}  !Q@ Û%ôNRAÛɸÃ8‚è’ÔŽP+]]ƒ[M•‡vží2—DxJƒ˜©k©í ouéÚÄ¿„â¢ÏTÕ,Noù+D1áÇ@÷[¤7¬+…¨‘ï Ö÷º™8 jK—N‡›Ù ë“Û¾ºÎ ¡´QàÅð`ÿaÄCtu'â¾ç²&=ÏX‘òÓʵڋ»Dp¹w0!ü`z.6!:S|Ñê€öÜã`É)ÅŒ·Œþuht}¥âÕ8÷ó¬-ÞÍBÿàW8Û÷´mXÿe (ü®ph+A’.êelÓ8í ‹ÙvfMCÅÕ^}Š)£Ÿ;׆`å­¸i¿®Ì Ò$y‘EÑaJŽ“w’Ö6÷3íT³æ)i¨Íô+AN}ÄÒÕ+Q²c·3…IO3˪™cq÷@oR´äì?<Ʊ<}«Yg>‘ªi^r8-â°ß¶Æoí VãÛA[Ç+/.­ø˜ÀÐGÂbØ,k˜êº7àv ¿·sG,ÇlT,`Í+6,¤TKzh¢/å27’»øï” ¦c(ýõ‰ÿ ‡ØR'ìêkt‰õ5éY_­H º[îP{Ýbœj^vhÅ0Í1(èÕèá(NÑû´.É/‚Æõ>SàŸfý˜nm«im i¾e¿šÄØèòØ…‡AÜH«ò£YÛžîøÏØâ®Û¼åzI‡µô15•x Y|U½¢߀´Œ²Šb¼2LðçYÃ_^·ð`ÙãâÇhòŠ—y4µ¥,³‰ÁøÕÎ&2Ï¥õ$ì¤Ë B\þž¢~Z—#Yâ‡*”ãÆ]p÷¢’M¯áu•ôNE ï +‘¿—óGPÃòŒ %êÎÀyŽKCÚ¦+唲CªØñ¸Ò)ç;æDq²HôVÚ;Mö_ ªFÐÖ#M‡?õŒ•ÊÒ¤l„ê¿ý^:E†’p‹pQ'5Cþ«ÄXÀ*(Ê @Fµ#QÒžš_;EhíAÞI14Ð*ƒ/ æ uí­©Ù1áZÝ” q1ß«=çº=Ê!líºÁ#vÂäñÔžY20y“¬LÍ`Ÿ˜@‹÷¥ì×ÀfØ WçH:Îmàîœüñ},x!”³óÑ7 }Êé_Të”(° \9—Ôƒäqü:Î_&'«2u—À±.Ï÷LÑÛSâì¤9+É¥´å»Þ$ýn"{XªÍ zïê*#-*„QUé+@W9!šÂçNß”s E¦dµ²ÃzTBÜÕ{Lô¤‘ûûvmS4Ù2º2È#ƒ úŠ‚ƒZ~ôà uaâIåÊ9g²˜ã|ò6õ xJ0“Iäü×[¨— ã+ÙÚîr×ͪ‹8VD2ú2}aóîêèbÃõ/ CQÖ°íX¬3—iØ~>”Á7—_ÃaÿH$U1ùeRÌÜõí_R„å¯"µU?j¶· ó¼ÿ'Mp¶Ô“ky6§› ß }î”EË=`Ý+÷ˆ|5ËõÈl—zÈõk~É"JÄšÁŸTó¢³6qxA®þ£çøåLùãÌ4†Ç™ ¯ÈÕór Éì6TÏ9xB®çlØ3Ã$BãîA(³B hï8ú‚<Ñdz ¤ËÅZXã_N±¶TPÎs=•›K kØsÓÊ–[ .n£ WŠËÑæ×Q3§‘W¿áóé´µ,en ÝÚ%yûåÏ»ÿ*ËçÿÖ˜A[€ˆ 0À @þÍË·hi[ãhjñ©ú4tí¸rÇÒO«%Egìêdgè6s[¹š“_­c½ªpgÉ܉¥¸ª»tmî¬äT^Ï®$@¼2JP#©C¦fíBæ ðÜŠ®ðìîÀHðÜ·ïùÍVÐmaæŽ:èðÕ)Ÿ}ŸõýýqfüaÒÿíõ~.÷A~þøûq*C¤ýb‹ÐZbš·Rþ+µ5¡-ÚÒTœºt•[vÜf%§ïôPœ‚þ‰>¿-/‰´\†BÊDÍ)<ØL?O©!é<*d£Û;1rž*TKýé>—‰¼ÅTä&îT.d#…©#ÑCj4a±<ˆq2s/µ˜‹|;/ á&ÒCº=SêwÄ5Ýÿ(êÞ〢aº¼•å{G”¸1ù19)öbòrkW>4%å¡ÖùýÓ"ØN²\è…hÖÇ*é%Ù†.tÕlO¹‰¼‡Yh ~#Û]­…J;w’e‰Ø:¡dòÕñ‚æ©Q£Kòç/ð»B Ê| àÝö¼ñ]«JÇj÷]À;‚|:L™¸G‚Ç^-^®•eèÎ?0½EsqKì2îô í"ÝÉ9¼m¿Í=¹ ¾mtíID#ïQº×´@Ùwš§f™4Ÿç€o<‚=˜µ? ¬¯B½ç§+5·Ày¹Cï‰Æ8°'ÎF." 6J ÀäÙÒ¤@ïä¼=]`Ç:@s(œtìÓ Pi´æö=fω]ºéÈ…­‡0(pm±Ë?»y¹ ¸r–4Ä!ý©–RôNÏ ÎìFôl¦4ïÂó˜ OYàYI-s†DöÈ!‹RÃ*ƒŽÄG¤hà½ç¾½±I#q-8êÑöºóÙøx”ß{ÕªÙzAš7¦˜ú¸«Ûºm[Úìœß3Ï:äðÝ‹m Œô1D‰Æ,²F‰“0)®øSî—p™Ž­e")êë•MBïŠxतßüñìF>s7gÈ×Þ{€³ pº}¹µ´æ˜ûÞ(½5Ö2 ™¦ìP4à,dÿíXví8¦äë!ƒǶÉQIu•ϔډÅ_ôå÷Î ¾êÞ;Ûb4Ží;<­y+Îõ×Ëcùö8O —Ó›C†¯0T*X–`äõ¬ILx=†zE,´PNµÿsüI¯‰)à’<ÂX³¡HñÌëgá¶Â~Ë.‰ii9)©Ýêèú7¶`·¿švkM»ç¢¦¶–Zåθ}vÖ©‰=ùaß>äÐ!|MWæ¶.u7oªÖÇ-\ ’í.‡–Q]e”(‚ ¼Å9’„¾Ø‹#Q@0Û­E>Oùˆc,¬S_ýï6É JÆÐÒõÈ"÷wVýR8 Ȫwñ!×·ü­Se¡´Ar„ðKHVàþ#ÎnÆÍn¦Kû ŸnvO?»n•Š´Æ ž½¯É/äý—0£)}ô¬¼ïÁÀêi Õ˜^¿ÚF5ƒzö4§~¡öyЖ7Ö’‚ší`øpëÒ™«g³§ô”Íäá> ‘ì-Õ†íÉem:n²Áj[¾§³·‘p8§´©H ͤm¢)70^€¢/’\ƒÃZl£,,¨¬Õ¢iâ“t1‡ÐW1|(úñ,L³¡¯˜ä¢ '3I#þÀHÈ_Ü)Œ„‹ó¤¦×xpç39ŒøÐð6ôóMôØ'6qÿXs˜)=$âÜ•aÒ,œ®_=!¥8:&ü²(  ñ·cœjƒ‡Æ‘{î†Qô‚ó" ‹!ôüÕÅ /È{*ý`\ÜÓ–@é0ntXÒâYtÍ'B­y™ß ADm'Òµ[n ˆ?|,€eý6¬ òMš±Hþ•–ÆËÐd„—z÷ Õ ÉVKOaŒÜŠUBŒ`õ›+R·1ë9,Ûöͬ‘Dm’1ìÙR¦n‚7¦¿áƒ¶O’£ƒœdõ•¯Ï;ð\Ó÷wørMXI<ñ>Ä$C4ô‚½æ2v 埆Q¯ïÖ½ Œñ{áh^Q«YÍÒ„ã>Ý]œ­ÌŒ«ṳrøH¸Ûž˜Ú!˜Ñ‰½BS¾Û屢Á”,‹ÉÛ\’]ÝþE6޵SXk"Æu¾¸nl^l —Ë7ääìê(|Q‡ãß/^)ø¬Æa´*`ž¬—ïš Û_‰¡5ÃÜÕu®Rjæ»´dôUÌN¹rœ«PÝ^»‰§©qü¶™—³Ñ e»Óþí¸†Ðj„Ç6SAˆ,;¤œ„2ø]?6•Ç›)o\Mý­8Éw?‘¾Vtæ®ÞÐÕXríb¼<~ÝWw‚ ¿nòÞn³=^phñò~VDµßWýâ3ðs¯ó;G'y2„7q̸Q_š- [3Rô¸BÃÌÊŒ)XÍ2÷±Úh²,Ъѯšhñz ÍÕƒdq;ÃÚ5½aK¶“üøŒ^ÈPÖÛC{Õæ¿|X¡ui­ZÔT¶Ä4w̓B4$H½ 1ÚTÍ ñ5Åj.0 ©|~-¨Ä‚‡íâˆ!6&¸¨´r²ÔöRèÂ$Æ]¦ÒqÎÔ˜©çé R`~Sba~Ð@“žNLKPC`N)Â×Zá½[¾ÞIÄV¸^ §›hW*¦bO.:<<iZŽƒÎ¿üÎciL1©Ú&¹ì'3½ª¥-<°M®†¦¹(å΄÷&²a#¢$òH 3#gYXäÖÙ.?Îmâûfrd;·ïôÕg“Z“¹l¨YËd”å’›Ÿ–5ŒéêÑ$.³T°¤×…C…Œ$!ŽT ¢Ø>»nêWñ~\%š¤¸ÉøÖq7ØÓ¦5Bþ˜Æ{ŠçP¤_ì Û™$Ìû “6ªV·8è×ÁÝe¬‰ËºÀgåõOp=ã²²·û c¯ñrýÝ¡®ðB7϶w@Î7·GM‡óÁeaAiœ»Kš‹\ÄÄNW 1L×Ý©J (p‰ká1Z‹¼77ìž?>=?{r*³4I“9ŒHD$F]Iå:Ó§¤ßáA8Ê.uñ/ ºÜu"ÝÖqްù Þ¤SÀNa2u—€ew•· (È.±W†8¼oG¸_6ƒ±ä¼ a@ê}vyÒÿÁ¤Ð5sÑß_‹ÓkëÍ)êª8]çóAàãhë%¯yCÇž#’êƒO^ëë÷9rLo(˜.¨aÕHñ¯X=ÜŸ=ôNV%¶Úyy5(¶™>×ÂðF @ÂÝD¡îQ¡éêümªÔBk"nî}¦•Ûpóÿý?6áÇJ+ž4’ÏøßäРÎi” 1dJ~“[Æ´ë:ˬñu\váº-¯SI7ÖŠtDéH&üTž–ž¤z¸“>e¥<«Åy`Þ°Ô´:ˆãñoÎU‘yRGS§ít¿Z)3qŒGjK~æÕ€qS·Ü˦Èü8ÿ÷ÑJ··‡Êø˧ƒnÙFZÜeI§,$u¥œÇ„#uÔI#¤©”?A<¤‰ /«›—ìŠæž3Q1^ßF¯ïQÜo©km ]E‰2Ü—Ä×öù()ÃbRe•»“u¦Þ-ßV= ·å&Äc·V •o”Ý.%8Pg×í/í³+©ý^|Õ*SÙÔ÷ÓØÎr‚áîè•D¹&ƒ úÆ–lýI`?éáÞ¥ïgÑ*§ Í×&°©9iFÀ…6B`R°sá=~`qw|¿ˆT4àšåmÔVXœ àœÍ’Û ïxðãUB6”ÝÄ—0û¤kÆ®M@öµ+ßhJÏ¡ùýÞSÇŠ1œãÂû®”À¿Ð£õáRÒ[°¾ìƒw¿{1¸^ì“w¶õÉ©îÃíX` ~” !ÊÛÏœ¢O™2µµ4/`¸ìîÜb „ÕUO ª˜ª˜B¶ý˜jd¡±Ä­y¸f=HTEî‹·­YýëwõÛ¶û‡ ËLçEÃÀø|œË»ÈT¶Æ’Ù2Sdrç^LüÂÚ¹œñàùó<±úns—9–ä^R¸„$Õ?Sýøt÷ ¦ÄæÌ¦jÑMb»4ŠxÌ–ã{ÒJzs ´uèøcqB(sSÊ@m$®ODßTeŒ:Œ£ÊbQ—U<Ù&†*sÊû,^¥`h)Ïa~儯\ÖSÅJÂ×µ £ÄDïñ‰Zá¿KYÕTG…¼€ ÎÐפ©=&-V¦ãZ&寰Ó ít(ÄùœæÅð`¯¬!ÄÂP…¦¤ŠwrxÁó«>ó´tÍzöËsæ}0ë^ˆº}n?åŠðŠß­\/õ'š[(Á_lø÷8c|}„Ò³.}C?Ú°³]Eì ÞX´Ðn“N SD²C óæDJ«zº¡ZEZ¬ÉüîÝx`åNdKÐ<¯/‹/C"[«ˆvq l‡šZzÓ)[HâŽÙÑ*,j¨˜=ªêVSÇ·u¶FØ+¦Ë@ÅPŽá;±¿é G×'åe© 8ûâÂ<š9y|œz½œYܯ©)Ö‰òÏpO!èu"˜N©.´V½´>"­&Ñ&¥ÑاåŠlf1Ç(xÛ…c¿Ï ¦©.@HË”H*I -Dñ^XZœ@o~ì}ÌLº‹©cð:…dÙ·mAé·LIA6Øf,Ö=rS~­Ë€8TyîèwqØ’3Æ2ØGfÂwük!ÕùéFw¸˜ojH‰ºbÀŽŃàÖ™LG\‘,V?¼<ógDeÛž‰œöhF% Ií|¸îÀs¿j˜?(ÎrKç@ˆÄlVç&UÔ¦Ær‹PPIÃè˜Ä¨YŽ«³d­#âl^ü§Q @}¥ñ<iR'XöPGÙÆ™§Ã868„"ýIè&Yî3a]ÖCˆh¨ aC8îý—fÁˆÝeÄxOŽ$• gµX•¥“WÊhKêúòô‘a—Aˆ)ßÎ)>¯\gkµºMl”ŠÕaÖ^ñå­Lš†Wt[Ê))š,™Æ– ÛÜ—²ÛW 3¶;Í:h‰šÞQœµ!d/³B«ï¥¨ôóõ­EVCm6ž;éàÉ…Î_„ŽXEŒG„¾ÃEíL<Ñ6ÊYSµøÅ×_áÍÓï‹¿|õ³p¤KØo”µj”E½Æô>íŸ5Íš­;c¿<¾ä c8?߀¥éCEèI#¸ÅõÎõ`c3ÃXeÊ}*™à!FѾWÎÓ÷!€dpŠ.»6˜—®aãÑØ‘þP&qô ×±P·œž³™¦y%¾ƒÙ8¥G’ºÖ‚çŽR\áêà±8¤¤¤ý¸”>ðqüƒGò OÖFA éû ì'D¥åL "ÏÔ±¢òÞ4D—@ðKµk<3oÈY9â Ý4N&ã&Hì¿]„6­„zßA UbBŒÂ«Û˪P–<*`Ãâ½tYP¨bÓ%'ЖÐDÞ@OÈ&r{œâ Ó Â%%ácñÁ ç í –ÞŸ}Ö0}T>]“ÍS›Hż”˘½c–Å…#eü$I̤›æ¢^XIÒ[%hxÆÔ¦¥ìlƒà3¥nÓû]¼Qõ›¸i}þÓœnBls™Ó8¥´ç–äŒaÙw.ÉœCc«‹Ïˆ»šÊîGäâ攓¤Á”îîqaO8<ßé…ÛD– ¾Âñ·8(ª®£QúÇÀŽD§ô ¶6áâé  }Åu~cŒÌPš8øà:Wõ'aÈZJ[knžs÷N>ÁcÀöÜÎî¿PÂ2GŠìÑñrjà°ñˆS7egÉ·ãZŸºáðìÄÍo´CR|ïÏÀ½_͇à¾ÍOË‚ÁÍÛoŒì@V&xjì\¾ˆ®¼´x§4ÿðzSë-|g õÁN·¤˜Lûq °ékÍ+xxlAzÎ*‘ÎhÖ­AE¿æš«ˆ´H6Yûâ¾ø®kõŽnßsáaÔl\–:ººCc¢¬Ùpêcì}Î%nøWWàRþ$Ä){±ttû_QúG[¸½tË«ÅNÙóúµÒü&W‚‹ùní#™RŒø7¼F8k³oä;–%ºŽ’ŽJ¸SÙku8÷šð‚]ø>G sMò—b8ñsbù;Æ!&© ºƒ1õgSËkÓºÃH<˜‰™Ì¼í'8ìt¥“auN%Ö˜ÊÆñÓ¢ jB´Ô9õ3 ÞŒÍN Ñ ÅW×q55†U§ 5y8Ü)%#Ç|…±È{;ÉMãD’޽ܫ¡RÆ/JihHM‰ja%õ;ÖIv¸É2ÌIbsC–»ˆI~½çô ¼a±$w§³Jcý"1Ízu(L&5?1)å“ǦNOO„ htè¢?ŒàÕ{Fú0zÝM _Š =ïÖbDÐÓ)ÚÞ§D$ óìÿ(ÉU™B6ñ…½ eýKgÂøÞ¹zŸ7†²€Ÿðª¼¯šÂJ"PúˆzWiè9ðüÇæH¥fLaE[\Ïêjjò7~‘ „Ô7Y{û¬ÿzæßHßS§\±?9|=·M~3QÎßóÀŸãÙŽçHFjЦQa>Í£1¿•äxS> ÃfÓ' niÍáÿ[¢­R­Äh@ ÀðÇ¢êÛÚZš™:9ÿ/‘ìŒÚŒÓ!Jß“ºAXêÛL3oópVü°2wº³¹<&!ÿUë±Ü×ÚÛ„k8`RéÎ$ŒAßÛÒ×ï¼ÿ¶SiqjaR]ÓQ¡;QFð¬'°#éSΡƒÑøÉ»ãv ¾dŠ´(lZ!Ð,p!I„ b@Æq2éý4f¿€ep’´›Lo“¿ïХߵÔúD ‰³x0Kñ¼?Q;¢|@?rÐ6D’f®#F|bV±Ò„ƒâpšX°v¤Eä;Î^![ìü&ó—dô„ßCw#®æ•ª—%ìòît_ælPE˜º8ž”2¨ªNAÕnçæÐ9VŒ¥d1‹š?qMOÉah”»ˆõ´T%@ÉY á„Xmr9wÂö?( |&|âûà€ü6fâûªêŠôÎ¥IKú˜Ì3i Qúhé{Dwòî"Lú@ìâïÃCìW(ÀrŸLiÁ9f‹FØLid“8ëwÆü$= 0ê%ÇM šloX¶Ð WRÊuPV âs™¬)l«*(Ë‘¬dzÿâ•Eã t–òµÃGØèÊ›þɲ¬÷̦O}KÂüˆyþò1ˆÀ~”œhi9À2e$F¸+¹ßR:)ajCgþ;:jé»~y%·«€ âõ">*¤Ów bU›ª?azìg°;§øg±p‹6ð¯#¯U"bYl¾:†Š²eçˆiYñê–ÅìÈx=iëÂû…C{Û#†_/= 6.³ìá.] :‰¥:ÙDx¶n2us|zAæaÛÒŒ ôdÓ>÷ÍèÓ„Á WЮÝ9¯ñ9|5ø\ÑxeóGù†“ëd…‡G?û^3Eh ÜXûšæ°Ö•D€ñlº‚ªImOy€xþ4VJrå$Cµ/> 'ÛJ£;ù;åLÖ¾aP;Äï‰ákµ±ÍY;VB[5‡ýû­§¾SÝqʦÐzÅ3xB³m·EîšÝ»úhÓ~U ‹^mÄSã§ÓÚ}r\ÅögÇÎZ°4ùß‘ƒ¡€hr€oÊEh:ÚHX>ÎYîÛÒºaP´í6®Ý V1+aêrͯÐ&ù’MV%”áÖŠ›N?ýª{s©–QX5Õ,÷Ž ]k¥._gÒFû'ÖÜ¢çm¿¯EveÄÕ¹*íÌþ¼]r{˜zâÜPQº˜Æ©ìÒZa¸ÔoìôÀ³¬Xïë`ðÉ+8:>ÂÁzͤtÞ!Õøïg-ç–À¹j7Ô©-Ü\¿šÓ]Ωß[¹úŽÉ6*VP~ܶήa/gÁMäuúó"ÆÎÖZÝu z›ÿAÎSOÛ !Ü×Ä€¶q¨¿Û–¥WpmXÙVËñ¯8õµÚb3×¶å9.œÂ2ªvmq¿ 5V—·c“´+CÈ]ÛÁFS;•0¿èöu¶áÛîü×s^².võËšŽ]8=‘&)îÝv,ýŠë/öW}ŽÈ¹íÄ¡ÿÿø¯ŒgÔvl·ØPÔ´ò±aPnˆÝ%耈‘¢ h]a¦ÐÈ͸…svžG‹$¤e€ü®ÛáëÁ×_H?¼ð¤"kÆSk:4Ýør‡œÜ}ŸŸïÎzâü|ž¸!/û€Ý}ƒÇºƒCÀ¼4àßÀ}¦có<'ÊÈû%Jåààá¿úþ¡:e¼Îµ› SÍd\ï"‚Ð6ˆ •üþXð|Zú²y®]Ù¸êd1Þ¼ýV› *—5œH½Îu”» ¨VË#4À Í€ …²ÖOUm¼<;3Y d0©vOhf3Y±QmåˆcT5Û¼Ÿ?qôäÆÕѵônâ‹8™-²Y¬‚ý¶(^[«‹çO„À*€7!šWO ³ÐÓLÖ^Œ‡9õ¡aO_@Ú¼/tNˆBW ³°£i›òG'Þè–'¼^šŠËMR?;šÑâ˜Ò5:š€ý[æè*-|¬7 $•Oo e\Òº CK4 ‚±’d‚ Xµšÿ@w±ŸÉÓ÷ŒõÚ>Ë-²ŽhÊ ?ï“ÿž„®IÝÞ£*Š–Û';µ3fú›÷@ÖB¢6U¬Ôå¡X ‡qåL¢Cj+øôLš®MÖ# £]‚$›Þ¦Ž¦Î!¹ÐU"¬±Ó ¡¦ëˆY}g³£Nd¤¶•T[yA[›¦h°.Ü”ÔÏJ=iNe7CÛY†z1«ešµR.£\&ÁoÜ\ù2 'ß÷gÃ5‚—¥6¿kSrƒ|ˆÁö¬ðà÷95qô yo°Ÿ˜ä¿ß¶l‰a7aVzµ—ª+dꙦ«+¶ÖüÙF%‡S¤¤¶,hCz4úè´vŒ[¾îúê¼ 5dÀ€´<‚.= *… Eã^ß<±±Ðư‰¨»ò _¥Ë6‹ãͼ}ÌÏ*|ĈcëgƒQf+·*ü‰;] ,ÜÞö˜—¶È7ð¡HÓ¡†ŒY_ÕsyG9y¿B˜eªÆÜéB%5!ÅUdè¿áÂq†ÿøcàùûØ*°êvÕýÁdú• «ìv†êãøæ¸;pb®§7ÈÈ–?Úxñö|±uäjËZÿ°;zhø Ü¡3’¾nϧ aCÒé̺?íj{j«C”.`â@L¶¸{ܨ,¦G( ¦ûùf»&£Z}µš÷©¥€yÁßuGþµŸóý€ùïf ßHÿ¸®íÿ8å?îknú¿Êµ7Zv[ˆ;~ gåÝ\ÿkG´_yúé´Œ+t×åºZë,2_¨¤3%V^®>ûîb‹MQÊ ñØò²œôÝyvøˆÒ»e¿O¤âŸ–šòÆ©÷d‰ð<€¸9¤Êï‹ê]–¹ˆØ{§„fÑH`ÑiF$øKé¼Ïsé<†àŽ·ùHÑš]Ô»žØÖÅ(5—h!‰•fφŸ€ÅÖÑÓFß Š\pÞYœ17ÞH7,° v-Ã-è(”óã NP/ P• ¼Õ„†Ñås­Ù°³¤ o(4ù Qý™»q¢Kƒ4à:`G9%×û”øUÏ´…ŒãÇJœâ™ò±‘HƒéÁ|ˆ„<Ø#è¿í7H-š`e1òi¼éür¡†42œó¦E…oå'ì7ÉúžË†,ÑváÌéÇÚ˜[ù3žÅ› ¤F‘9ñta͇ ´ÖŠù=Åqt] •±_Iå|Å™A7­'Ó ýÑ)ܧuöžtç©uJ'Æ™>§– ¥WùÆ&Ž v€ün /`»+“ikØÍŽŽ_Ôª‹±yü¢‹$Q€á³91®§ù|`–o\»\i%©¾ý§Ð$pHïií<$›É ©Òï×¢Ïc«wHßý€Q 4H£éþÉ^”J 1&ÝiG†ÿæ’ÑÜ=CS`Aa[ㆴ¦A¹çÁÊjiÕ d ç8­ø‰{r«­ÚzÜUyé¿ÚÕR“TQÈúŸògÂý]ô“È•z•çžIÔÃì«j‘>ì.-EßzºóbÈÍVœ»²¥ß–ðy¥*RWþˆä¨U€¡|CYL Qgþê,tXDV ʯˆ“îöá$dè¬+6–íøÈu8>8{?.ýÇ è<à­`|PK“¶6Hv›HC®ȸ0ƒ0!Üh C ‘^$>Ïßw&>ݹ}=¾/Ýÿá{ÝáSÎBŸ"Ž=ësÄŽhî»HŒ\Ç"à€sxþ¢oÌoî#àö‹Tq2|Ü/\+cÌÐza¡¨ S i¨^æp"¨%àÓz¯iU‚ô ß×íI±(‘+è»Fæ»' J¸L QçºX;’D/´(ªE«QG9ц a ™üùàÐm¤" …AS‘Pcr´KE&Z¨ûPð®û„¯Š;n8Ü$ä{Œ7[½Da«N‚¬—ëçkê8¤Ó|*)òn–þ•I"¥·h¢®¦Ð×ä.”Û½ð’ªâ5Û0FðŽŒ®¡Òz•^®ÊIú*@Ϩd§¬Jž-oƒh¶»ÇË­wš&žÅ ®åŠg¾î<ªÍöh$‰H7¤JÈìÈ,íI‚çˆxà µø›rç²0S¸Oû*€_;2;<Þ†ÇBÊ[°.Äìá€} —!;’dæX¼’ cqŠï¢ÇÁ …æÛRäBo'Õ–à-õM—~ÝÍåãçfGómm0›ƒY¬‚Ø‘†>GbÏI®zNúšŠÈc@ݲ‘âúƒlÅðXWÃf ~™sì+ .s‚›Àj¶O@C"Î 1I`:.dÖ‘i¤TB†³$Ð>Ò8ÏaƼH¨Ÿ·ÅÔ*ÁÿÊ.CÊ1[¬ÄñCNØùõª- 9¿jÆ7§%Ú0³³yL$eJ ÝÎð)ˆ¿fB˜7hdËï>Vk)„>R£ —ºj§Îi½UÜŠžyÿþ©@‘ Ó`õ(ëÜ8PMpµ´â¥Òše[aû>Ì4š DV^¾ ×LlÔòÍ<Ó…#ï}Ç1ħ)Ôa¬<5Ž÷Âã›OVy?Uª‹¯$ÿ‰Ó¼þ;òåL©H ?X$è?dAM/'õwP C•]Zòå)›Àã§›x3Ðs³éGÉÀ _1؆-?›Ð\¯Æ(Ç„Z2-ó}kà_ŸºMý_wdž­—Ø‘Rv†¿ghO¸³Nq¼C8B°#e„Ð= €¼Fu©Ý—€Ã#êqdÊ‚íu`i|±À ·!Œ JaÑ;­—yVú¨Añ¢7Êœo)ø0a¡TMp ¬ªåEY¶a»%gy!åƒ}Z¬‚¥°´6G^žqq¡ôà³¾x /è&Æw [7¥Ý@)+¸ƒÖA$,N#ùiÕ8 ·™Ž$U€ë5’R7XLÚË B¾$q两¢ø¾llUT¥i6Qý•“£¾÷ï j^ S­Ì«G°ÃKw›L*¢LRWSf¿=À¾"‹ó(1ÇåW]€˜džº°ò¸zºÞÎ!)W¦"/N¬sÕ1L‰Î«TËè¤ZK«[M=³x{][²;¾ÂüÎÚS~ÿšjÓr5£uñL…Þ¡\¦uLª°MÒÌJÇ›Vx´¸ ù›Ødƒ^ ­Oîêý+—~uWL[%ãÄ:Ô¬£kƒÆyÀaÑøPÅë“ ¹k Ž›Fˈ©E7†ì§’†ð­•pð™ª_ç@ÊÓUL@G‰1Î71<~ ÆÉáõ{]‹•õ5Ì''Ž‚êÔ8pœ&”_¼bÑ›™,Ó³4®:q8‚8ãÅ®ÍÎzˆª`Ææn˜Ù_1`ó›¢ñB3=ŽìºêÜ—¿¬ŸÉîA.UNÊôèºÛV}Yášâ†¤Ö˜ƒïH¢L2. fEÌÏÛà›ï®-ŽÅp Èsˆn÷èÁTV䑜T½C éPRòänËÎ<’û©Ù×éȮ罧0KŽä@Íx4q˜Ê÷Ñ$ÖÔ¬$Æ…ô¦3Í ‡ð=»JR=7E鈣C]›Õ?.‚Ç´loñŽmòí€Úa;‹-Ñ›×L;‹­ðjšµJ·—9dÒ×]ù B}5R“/v:Èb@Ê ª~dä?›ÓË ÿù’[Z(¸Œ!&H8Ê—öEƒ ;¤Ék@¹7îÖ/¦™°;‰d‰ÒÖç‰1t†”½ñÃåW¨L•„˜;8“:.Å:0·e–»™á:9€ØÍÑät[ÿe» h”ð:ÿ%Qt» 7·>}?¿Z=p÷ÐDdðjÀú¤·Ñ þücä𩦉åÛU[;;Yÿ"`Ë‹ÉýÒê¹-~ø¾hZIwQ~×ȶã4Ù¦˜Z¯QÚ»­ovéfq¦¶ÕäwsóßèÅ7RMêSøÃÛÂK ,¿Xêÿóq›®~K[]Ùïôóbª¾1(„"ìfa³M055¹¥û4ËždÓ#8’a{>ào^Áìà© [UÞXDE»êºQ>jéêR‡Ð´¶Z>¤†¬fÛ½tåz\CøôtoJzwäÉŒ5ý’‰r é[7Ù-é£p«J)ñ ;¶îY„…+ASn#Ý O6¸íàJyLBÝ5Ê–ˆœ¸Jܼ‚HÕM^¯[>“Æ¢¢0N¾œŒi§í0¿(áYQàßà øWð‰¹d€kr)Ÿ³4aïmŠ­•,ãœO:é:uKå—~‹`œ\SvËì·~k«œLK^Çå‹q+'mÑeš«i^‰äÎÛ1š$x(P¥C2ÂPªFL Ï…P«a¦è‹£×øi˜‡0›^¾po|È,r8ðÙ^?¦z1Qü\\Oì”(-OZ‚<˜«RR–´¨<˜«Tbž¸(i˜§Zr–¸¸4ˆ)në#µN_n8G¾büSHåCIYâ;^ã„Ðä#è-VÈKU®ç§¸f®‡ôJ:«ƒðj:«ƒòŠ:‹1è›ÁœÌ;“15è¬ì“ÂLíʼ4Ø;š¡üCe^ õÅ:;íœðÀ•äû±8Mà8(ÕdÛL¼ }û·8w S "2FÑP3±Š|¢(„¹õ@d%6´DAºâŠÀ᫲ ƒ3•:¢)µiˆb?Úê*œF±aÝ4[%¼Á@ »f'.w®'p!¥(в’C²÷&xÛ§x1Z  ™¢šiõ}9½ƒP3êäOÏ4ÔOË4 aò€Îš”q$@ÇDÜOÃTÔOÀDî¯l2ê¯`2 (i"72<ÊéDJìÏ2±4H3#øN9¾™?‘£øRÍpøjY™éËÀ½“žÓ:µÔvZf7'ŸvZjuµ"3ÍWz æ™QVL7ŒLH•ÜHбÏ#õ #¥P­ÞZ€”ìÙLó€S3kÀæ.W£6õ`CáÑ©–Õ;ôø–>xNbª-·ÏfÓ¦eÉah±’Ý TvÊ—;QŽ [g†Faläi,5èF¹aÑ•ëã3ä:d1[ôáËóõne§¾ë{Õ©\§ßÖ»‡CW2ûsîÙ²aÁòs•رJwT¯äàÃ^©¢'œF ¥Ç-¿Gõ¾Ðy GÀx§ýœ»:ú)AèEÇ"þ%{ yIh/D^I1áŠ{%ã¬');>òþa«Õ"’ÚIÔôWH{?X“eç²Ðïjç/Ç‹v•JÖ&zåy–P9‹ˆmN¤kmZÿgcI½‘¼. ƒP`××1`tê´¿C:¨åsâ„à8ä.FkÞ"4öKÅ5ÇÔ‡¬…š¡3ý"»¼ ¤:ÌÒPÑCxªzÔñŸxù’HÃÄcþs2r޵€4»HWͶ`‡Û÷Ÿ’Ó¿k­ƒRΊbÃ\`æ´iˆ8±ãÀj÷YòXuËV÷M,Ã%+0XêC¸š|¼Ì²ÏøkÈ迆¦W1 ƒ"$ o‰ˆkÒ´Üàf°‰s§sd^è‡l¤Ð«µL$ȹSLó¯øOùWf˜®yà¾l‹<Ž1fçØÐ‚ÛÐÃô¯)d½½ ?Îi„О }ÛûJd™}‹uÎ…¸¸/y" ŠÜôÉÕþû.m–=}jTnì+fýb<Û·ô'¦Dj(«©*i+à2¼aŸª½eáï7´é•“•,`s€KÇŠ=Y§Ž°øÍ¦F‡Êƒã Œ¡¸ˆ~üÅ:²Õ'á=4øc\±Æ‡–ÜôuĤõ6ö2V^ùÈì‹GÊY0þ7Õx…°¸³ë ±ŽüaH“"Þ K=)Cß6¸GM|à;îU¾$Ã=†ˆ¸gh j#»Œ©—U;"v†ÚùV—<Š- ˆ¡ÃüVƒüÐYÄ+þ•åD‹~"ÊÌ`Ú ËW“B«,•>Þ]XèQðeÃzz<„‘rDža™ÿâB[¼,̶0«­âê 4“ÛvWn)S@¾þòßð×û ÷Òö~!Iklbq\>»Ä•†¼jzì™§ E´mÚr´:/Ë{,1&W(>d®Øjº ÁưQ‰-³µÝjç¬F‡oJçóµ2ôè½,¸Ç~-jðÎ×G×÷ rÅm>¯) !ÝêôŲߴ)º± I6‰Ñ+Y“ø,YîëVîØÃôa:?œAUôF¾|,? a´v¸I+v‰e‰5/ÉpÀÆhO¼d‡¼oÔ{oQpãÁb€vU:϶Ÿ€À¹·xyX³jå ƒ”‚mYß"<ñlúRª]ôÈŽY½ àÔ±ñuѯeÎL­)­¼{0E©5N°—šoÌN˜r£»5/^ØùÉçìæv{rµçµ×ÙNÌ”lÈM< q:($ÖïQ€ÛœÍ?2Þq‚m0áÝn3'A¬søãÌèÏýe*áå™·ænNø’üäH^…„/QŠªŠã«N=Y€¯[ˆƒaŠvƒä“=(˜è0ƲWïà¼Í¯±£ø>X' K HÁŒN+w _¯¸£¹²á-ÅI0>_­Áå…ú™%û0ÞÌ…7·™ d À¶Oºÿ`Öó»l?šq›`,à…ý²uÀUj7—-i:`4—;±?¸Ò ²™ÓˆO <,Z©mE¹v½:$*mçU“RœèÌírï5U%ùí*°”jƒÉ-Ã}{÷¶©Ä8g׋&'âXM 0^YMÓŸid.‚Wñ€]ò^Ì‹¿Ö¯ø{z¶,Èë" ‹¬®-£p9Ö™6w³U4mů¹Ž›]…ªîÀñRoÃd§š«â¢×lÝGxßÅðyéÜe=ÿ€ö¶¨[ÄÝÕQ0ˆ«i¸èžŽ?T™˜¼d(O¦†»jÇn4¤…Û"î-‡œ¹ª¼dqo‹b€²¶wÈR\³ÚÊ€JÏ6ú•y]§M’]|’*«ƒÇ©Fí†~Ït_æ]8Ê<ÛõRÉÏÉ*6˜+{¶zè.lA\%BŸø§Á¼¯E®_è=X ”òfX«-¿Ÿuü–_3 ãåW©k˜æ¹› q×ÈZ {ÊqWÅü3~î(Î[‘·òðÑÉâá+ÍIäk)X§ê(Ø±ŽŸ¨IKåŽN¦y¾ó¥3Q®@jCÒQ´N¥’ÖL­7׿BQ†K`¨²_h,”u*˜ƒ­ãšjEÞô%êü‰’Z˜¥ÐÌNh$¨ù`FŒ2HhÿcY(Ä](Íê–Ò´œx¸°u¡¯jžð=ùT­³9º.Öj¹Ã`Ïô]¿Æ—ÜS=0—çëE/F¼·Wº¶|­~Ù[Á3õÔ½¼Žs–ä&5ݬãB,äàžp•8Ý=ïÚ®Žq+Ÿô؆ÚýÊŸ™ÌÖéN¦ñKK`"Èã'âVp(ÙˆÕº)?|_ËAæW]·&:! ñg:‰oiy¿çÎÒ&¡Bb»ÉOþÓ@ƒ¢ˆ÷?báÀš‘gØÊ]ãž(v™]:P:Çá´~’¥ô.BðÑgBÀéRT]ú^‹ÝW3Ÿ§‰6ìªRÁpL‘*²lãå{*Ú©V£:][9µü2Š!“ƒ¿° CŸç‰™~"J”–ØŸ7êCøR[&Ô%––àhL4êÓïE˜÷µ‰í€ñynÐáγÖû½ùõöWJ É< ‘‚òvHFÑ!Q\œSÉGÉ€ Lˆ¿üXNގ͵Ÿ§‰ zåŠBðgà´+ÕO¿iÓ`ðèC˜€Ë¼.feOçy<Ï@=íú!ƒ§G¶þ·#^Þ‘)p»°ò;™GÝê¤Å7UõT—åC:yÌÐId®Ïåeq•€'Œ3êðÎÑrÕ¤hCcÝvÓ›b¯Çc$0ZÇ=ž~ æ‰ûŽ–ÔßßJ¤|Ý‹çÔ6òH€÷"Çî ekjÔVè—þ}ÿÛ#^:žXX¯‹0P9Ï“{G݆yÜ6É8‰[aM¢%š´Jì4°ÏkéQ³Žl-¡0ñò»æü ¦Sï>*ñÇW“CˆzÑ%ò]‰]Uã²U8ÛF&'÷t» üú6‡ÞØm¨YçÂ`•ŠRµHE´ä_Y=’É PüûÀIŒjô÷†¢Ä¨AÙ( {1™eð"ܰé5É$rG= î3ù¶îwG¼ ¸wrÒ½žm,¥KàÍ’×gºç î›C-îX'òwPÛ-ŽÒ˜ßàŽröæu’c0-‹G{ÈPçw)¤Z§yódÂ{ûHL ªÖ²a®VÑòóïÄÇ !üÛn¡K•˜ŒûÍÉ3ÏHæR~È„ldtð~nÕ?}óïÈ •W r·ÊÎ.˜^ûÒˆ7ÊŸÔVË ñ7¹ÍÿJS§ƒç°ÝÀOÿÛP:-ä“)Ñ’¤ÞyºÊÀ’cæPvú#æ/.üú´ù½ÈœÅ[C[º¡{Ëñ•^ì:D¸pÊcˆÚ0«e¬ä$…è*˜}UMÚ*YOmô(ïuÍï’vº!ý Í–:Ûð+¾áÀ 0EÔ!ÿ¶¦'$L=¶¶ÏÞÕÉ¿«‡¶ŸÓØ<EØm†ø4.¹ TŒ ¿'·`Ç+M"׆z·“Ú“\L Œú“AWZÁQdš#ÆÞŽ;@¿L×áÂ]ø=Iï’p»¦ûd.¬ãôì߰톪bª4u»{³ÛoM÷þÕïΖúÆaQcIEËß“ÕMܦa£ˆ¬'udߨ)9rSé)é#‡G/¹ £+ÝÕq$¶’,lm9ÖC ê°ÅããmÙÒã"kõDIy°DC(—À~]Á9ŠŒ¢qy>–äoß\_¦ïƒÿüÄC÷ލ·Úœ6ÃÙJb®Ž>ÓW_&d~Ü©ø]ùˆ•É3**"¬õ|bGòŠÿ¹‚ÿµ­ÀLÓ° ‚áÙ›::Y:9ë›Y:šºZ[ë;ºXÿ¿ÓóN´lœ¶Ø~kõmKU sXnvJÊf+®ÿeŽcNÈ•&™id“  LqÅý}ÝÐ ëå×âÇ2Gˆ"iмžl ßKAzQ¦]©LŸfòrû|²¿jW¨8Vˆv&[æ‡È`ÉìS#>Š<„›J+—že¿¤ Bø]gÁyM)ƒm0ß%·Ø ,õþkõ²Êh­7‹X¦QÆ‘€)%:Þ ñ®ÈР@ŸÒ‘ß:ç¤5Žp8d—Ú=gð…tSvÃfaŒ^Ó¹:‰JJZùw:\M¾´!i V™¤>”´`2j9é‚äT¢1ù—ÒêÙd6€ô„½Ï§&SJ“^‚³kñ]«T ÊtżA%Á©¼XÑ>á,ªÁéVoîN-¡ÑÄo8$™ô³CŽ›ÿ`ëþ2uò‚굆÷UTu`0H7v}rrníÎÚ3°ÃyçkÜ'nŽ;*¨RÓ§]:ÎÏ@©Í#”!ËQÇÀJØr\‰ðš2Ýh0º!³ûÈpSO*.TÀ„Z˜¢ALf¥•õ(áiLÌ6²[ý’=½fçÀÖ¥3H€ÚT¶{ÑÆ€äžTa¦tÒ·¬Åе¤B§qGsÒ ÑȽO¾€—>D±áÐ=ãÞiWFÈB¸ÌKÀ³Ö··±Qªã™^í×ÂþåÖñ›ú}påÛoÙöÖžÓw‡œr'ÏÙÉŠ˜ÕsÒºÙR";†~ï}(£‡U.øÉápi‰Ž~Œ_®ô£ñlèàúg%ƒ¿/&´úïäŠ9ý©7#K‚]GdÎ*ð ô霙ùPY¡€õÃWúÁ}Óp4‘ &¼;sº-ý Û$œc=6b à]32AdxƱºÒ‰¦BL`>zD2¼›]éß êrX¦`y0'‚-ç0K‹X'ç:²rè<¸¥™ L(ZÍŽmZE7*Y%!Å1O?ŒÑ´¦kdëÚÀ‡h«Ú>N¾éW[ÕÌ:»¾)C^+øv’’Ê=ƒÀzÿ~‘ò;䱜¸ëÚ¿&‹èqžCwÝz— €ÒSn ô ~ˆZÝ&o›uÞ@Z©Gñ†‚w‡ 7‚ºìi>GytÛÀ.&;;#L_ ÿf5Øî®¶‡Ú³àNµ.÷¨Ô£5¾ˆ±6šÏzr¸§éxà߃€X´Û%§ñæÍÒÉ‘Hét}½û p_n¯àÈËÑUOE¯h8z-gX+¦Nä/ê@Ao³J`?BãégžcûdV,Ì,J–é%”Zl˜ÔÅ¡<Ó,% áÎBE¥¨sÕf’ï"ƒô’É÷ioŠ‘ ÍÁÛaÔÌ%¼x"[oeø›`Ú¿ă aË T `ÍÊ×ìê¶™¤éÁ­8ý\¦³CòçGt¡¶ &H\ŽøQ@p˜1ÌÈV ¢$Â'² ¢°û§”*0÷Å^¼œœegäò¾LkQï&@Ú èš_èùŒ¾…îë¤`ÞŒ´ï1ñSUaMÙ¬àMèïEƒU Úæ*? 4‡>p3ÀwÝÝ7åúÀW%êPdñd¦¾=j6­iF *—‚ç Ñ©öSm€?@ýAƳº‹Åwxýs1*Ý! q¨,‘zõ_eð3Ûw}†{K•MKÑÝ¢Ò͸(õ^\ì˜2¶Æ&×(³8AHKH̱Œa~ 2¦U˜~ûÀ_âÜÞ_AÒÆ.ãýõ…P‰DËÉE™7“Dòë2\þ\° |ñn2 ݉šÑþ9“)aG*h³ põŸz7SÇØ°¶“ÊQ£¦©«E­×°ðÀÕ“ R ¡:Pû€‹V@qLþG¶¬fvK°’„ã ^CŒ“µ:ž) ´@Êö&Gïh´Ëÿì6ĦU%L *r€ˆSš=y'÷ÑŽ#±/ó“)ä  Jûi ‘yP&7oB­”ÚÇ[Û•zü³¡í,YçØö—í@#2ÀÔr9ü6¼;w¹ŠV\—øíåuÓä#ï9Vv,°é°‘3ÀÛºÇ{l9ޱREH“ )¤…€`é÷»Ç%²ìq³è†Õ§ÙÎ™ŽÆÈ4ˆP^ñ¨#éËï=º[sbîÄ<®c21?—³æŽ¼Ë¢¯nèA'¹™ö„Ê_†7&—˜L”dÁiæ—3nâ5;S ³f—ªXœêŸó‡n7€Í\`WM²Ý­Ijãý%YâÄ£¹ Ÿû €ˆ¬g_ Ç—éýƒ#tn]–ffú³i¡ÁàšFêa³†ØVØ8oY[&åÈ41KJ1Q ]áSŽgÅ4[Ì‘³ êquá€ve^7£@ K®ï²À…q]¨TÖb­\áÐPñù˜WR¬.d{ÍvªvÌÖáÆôÊ3h/e¯+’w1Z5õ&ÇnšÂö’"bMJN¹€N=.:°ßí%ÐpÕŠåÚ{b W˜! O¾# Kr¸ß,Xš²ð„©èh–$Âl @Ôgîáì†Ek`ÙgQ88’Ancü¬úȯ‡è¶ZˆHèxòãÇÉ“ÛÞQ"šSèûü)¹Sü~~¿*ŸP‡&öc†uGá¤ÿµøÇOqÝM¿ÅZ(â’¿ò¦Ù|¼Qm¬§!µœõU|9²x™r7ô€ÃÏé< :«Ó `ùÑÁv)u!¡oås@ð±B8Å[¼k|›vUж8ìø±#öcǾÕvm%´f|‚‹o}µ&-òœ>|Ö­óêéï½>Tnü©XÎiz­OЉnï¸à8D½nT||8‘[ö_A&¿ ˜Ù´î‡ ÖAÜ“‘;ÑsMU€KGîŸ#mÕ ÙêšÔ¨¦h>)¢¢êaŸÆ­Ý&V2=¿AÑkËë ÏÐS“ž”ðq g{VÝ`«¢Ûv¼L§jïÌ/kïñº÷‘SÑ·çòÏÇP¶÷î)Û/èÁ¯Ri/ÿµsY_unY5}W¾!U |z ô4”äXôy›Ó¿…U¨Ó²n°AÕÕ A:ÍDÎ6Û´º——LͪƓюáLˆìXþ6?x¦XH“ý†&÷Š’m¹²¬D1}^ž*8ã/'€Lðƒòì–©*†/RIìCð‹G cCÚX«³…Èi¬\ÉÈ6êhšVÑ4°ãH%£xìp¡4[ò®,ƒ[4Ó¬fº°}äÎp-Ä.½*+—;€\%¹ã¢ ÏAÜqMç.Ó1)+ŠÿBdÛÛ{Ogà&¹`Nɬ€¥ãjà(I|þ£i ýeܪhM­ýºyÆê†ÁóŸK¢Yù:4לN“}Ôwo~ÖL±^ªÈ$m5gÊ—Æ!lÙÑéîiÀTPçX¦æÁBªúYªZ—M/˜Žuh£dk7ŒÃÎàÇäNôÜÇhèŽë±Ù]·‡¯¥éŠžoû½7v²•—÷‹…‡å‡øýø«Ž·ž¶Ž^X,¹×tŠyw/tôÜ–.k¤Ç¾mŒ¶5æ³3P[ÏÞZP½v¡Ázèy‘‘®Ü(;ƒB¼—QLÏžÀíXt6’8ª9žŽy{ïCý©€x†Ms¤ÛÝËQ¥R¢ß‘ÏÎEõ¼w ú²9 pC"B™½èöM·NäH,ç]¥Wæ³ óYU÷ô-Pmî͸HαG°—û~ÕX¯¿ßŽv|­(J‚šå°á˜ïž'âþ ö 8N[IYdð"ä°ó±Ä3é,X¸ ­‡lõ0*ËÑïžRŠrbú©¥Ñ¥0²»ÿ¾ ×®!pØñcHm=a‘û©v&­qš—€vj; ãn(¶A⃂ÝaçƒÐØl€cᾂ<@7# Oú©·JƒZ»úvSŠ4W‡\Ñ¿–^ ^LÚÙ'Ï…š÷êiÙ\yCؘÆ7ÜdÚ°9hëàì:'ñ@”¶`°î³1ÁÈ8êË­F†níÕù#aGâ”ëºSñ3ó«0ˆêï@øÛ©_\và¯ä© jlìM|ÜÏm*›È šçùJ CzÜÛ”t[ªRVZÀWÅ_¥L_„»:ÝûŒö< vôuHç  m9³ñotjVCoZS‹eq>mMÛ®"4Lzºxï½ùðÞ³ä7çÍ=cÞ›PµÛÚHN(yM¢U¼¶†€âéD+CkÙQ–#Ä¥˜Ÿ6Yžkíº˜j„A¾Á²;Hç ß<É#ûƒ­™H²€[ÜŠ›jÕY²"€á~À`KÓ‚.YCPËQ¨†}(V¹sïô´Üʧø¸º>Ì÷<ö|ÿv¤œÓpŠõ<¥—ô*…ÛÁÒX >µU,Ü(}C d½Ë`ºFÓýóx¾eŠ/c8­”ÐTãn5Áÿ!yj³2KÐ PËȬbÓ?zk ÌaðaÖ·œB0ÁΣƒÐÎtß‚“€¡SÄôäùãVÇWW•#ƒÀ¢ŠN™éŠ*› ªv­Íœ©{" Ó:¡Q® e±n¹¥Ö@ ƒ½¥Z_)¶têôXíõö™DO,èhlj‚› H ÷L6q•¥ ÎSh_ì£Íà •$,l ÖUG’ïoÿ,Ÿ»Usùè“ ÉFÁ» ñEž<Šmr@{æ2_R° ›ƒÁ¯-¬¡ôÌ|¦©˜ìƒ;Êc4ÖËS.óúN$b!ë 37 +½ÙGåaÒ(,'-$Ž0M:Ň_à‡#JÞ<üpæ‹Q.è ˆ@’’™ìÙ‘öòsh –b¤XTè\‚Ì¢­#9Ãá(¥ñh‡ØâbO8U¤M ¤I;‰:Ìàˆˆ¬3ÞÔ˜/|V{ÐhÀ¸Ò[G*£Â’Mµ|¥ A«·²Æ‚nF‡ŒšôPœTî~)Q•`­v¬wÇN6ŠèrЫŻf(²H]&HžDg¨,p$%aÆÄœ÷–f§È9€c)EbZ ˜æGÂ(ŽðÊ¢¢<úO |XxΗ¦˜Sç˜W#lWg›$­åÅ×sc,<,Ù~áqð¡ê±¼b¨çóƒZ± yàŠï–jÛâXä„«Š\¶Î«%£!*¬²#kÄpEHˆ¯n;P_¹çÓÔ-ì¼lœš4Ù†3Õ48oºÃ®˜g35VôDß? gãšâ€ÂúãNáXj²K’û°lžßwH[˜›6ae¾È’¯÷s„vá8«ml@rò[´OºVBÉ^ Þ-ÓÙJPçp-‹7¤ã W‹ÈæïqXÄk>Ã<‚I}Å'ÿBÈR,Q’§†…†˜ctm2¶×߬’>·E¬ äOKW(OZå95Ä|‡&ܹ)o*±-ÂÄoÇĈ jtÀ1¡5#oK÷óbDdEÄ„Î&ö;«ðÛøCºR8÷#y‹¡5•æÃ£Xë_ :þ öÜ;Ä0ÖR4šA2“0-;Ô ©è«âÞ"”¬•ЫÈpÕ¥÷ê¯ )#B”´\ên,‡q£„Mg¤›íÁéÂÅ©{… ^„âB¾X©„Q\Þ½VCsµ)DÉé@R"€A8æ.\L‡ææHtOît†li#¯´^BhpA]Ö#@Ù€Ô×êD6- ‡8‹ö“læ£`©êð²b¬l‘Õ¥›Ò›HcužïM°‘kú g^ìÃìß„aÞ8Á¦©ŒbÒÉÁ“ ¥Àž|¾ÕÕ¶®÷ŒÐÿ 2º—G’ð·¹íKòÅ<äOWqÔüwO™¸BÚêÊÀzfsÞ H­v}v»""Èïw§ÿC69–×™GŠŠ;¸á×ï±@¡zõïƒ_z<Üd÷‘Á…«¨»œ AÅFowœƒ,§Õ7Äf)/è‘‘uÄ/)öf :ŒLOŸœ°{$V¦cyõoø0šÚM`4Di|ì51ô>Úš8ˆ¬¼–ëaÙûÈë(TKGÌm£xPÏ´ò÷}nn`\l|ÜV,”xJ©;“ØI,³:”F´SóyŸžéAbĤ³ˆbA@ÿ2@ì³)ˆõÖÄØjgYÖ ˆ·Çñf¿ 1j¿½ €;T½³jþ&_НSHìöÝ}Ø¿›iOñ¯z ð‰wÜcZ ¥õf¤<èo¹­ÌÝͳÇߌœ·°,íefxËè”…CÌ8¨Ö¼ÞÞZý?‡¬ñÁ- ßê©K÷"©Fú–»Sgü¾Ww‰jU èNiKRj”rûB •"ÍiÐsŸ«™¹‘ÁÄñχ´Æ-Oäõ$¤ÂÛÐy’a¾R D/`ýž'J “0Dêð®ý‰'ÌåÑ1á×=Í=Ç‹¨#)ù@]à,yK^ŸÌbß}ö„ Ћævo/Ï0·ÎYÉ&›ˆ°;“­vœ½U;zÜ"ÿ0åè`–Žc|qgT´Ó ï*ÍîМ8MwcÉÆà$´¬ÿ E”±ƒ“„õXJÕQ­BPZNÖĉSEv‚4ªòGE„’/“þ4úãÑå;'/cfž)¥ÑðºÃZ¶)kY$‹ÿ½Wƒ½9¼í湄{-!¼eimÒ…‘þN>w+ }òä,ÞBsµl”Y>qI”ø‹Ä/2n¼®{{€ŽÄùX­Vã.|roÜošhQzfð0’_C¶Ìø÷†~Ü›…¢ȪàýŸ)GÃÿÕÊ£ë´Å‘x÷‹ÊQªTÈÆD둸B'—3&g«“ÄAÉ1­–mªHDb,›À˜x”³ñ×»"¡ÐȼÑå¥Á§ÁÉòù× OÓ¤=û×JÇáE¼ø¢îÚ}lÙñfq‡È=ñØ%DS'íÊ‘Ëcn7Ò=nÒJq ˜`.ªIS`7œ]–8f3ÚmpÂj>æÁÒwº’„ø‚ŽÚ4d0í³!¾{Ï8¨ÛjVu[¢9±š A¸!D’ýPç”›&Sl[ó("r‘ݤ­¨·Ä„%@ð[-hÒĸ€=wÐÂ+ÉÔ];Å”‡‡³½» †Hˆm2òH |2Õ”¤~–V÷ h˜ùÈ(Ûž\½ÿׄÏÕe¬÷>]„i!ÒFC­ÑLÃÁCÍ<üÖŠP"‘FÊÓ§`D8%4½Z2R Å¿p©tЫߨ$÷ãîíáÊ „‘7Ë÷ý|{þzñ~xøþ=´Q%ýz˜ÓÍÍÏûeÿz‹óë5;÷7íËÇÍËWhú¾Ö-ð (`}Ìøxè ZŠÈ™’¬{áˆKË©m†­W,7åÑ€„B ؃‘ObD*µ¶A •º±õÄcwGŽ“ ¤¶ 9¥¦Ù ÖJ4ä.ÝÍáã%c‘ý3®¤·k6K#ð ðPM¡IVeׯôSƒB­®…¬r á 0¢iJé] &šÇJ¶áiÒoðb3vïB‰„™ßyÈž­æÓ·&â1“Tœ©ù×ÔFѸòAú§µÈÓ#…‡šæÀ†dÉc$ ç(è³¹UÄ™àÜóTÞzî³+,Œsïè‹ Nê`ÁÀd‹ö@uɧ9uH0qªh$¿ýÕ>ušÔ­sF8 kÜ Èz¿Ä±M°j[Š€L5D)¨Âg0L“´ý¶ ŒYn™ÆòloÖ×ë”H™ˆÌB €¼à£K*Á« æ4Èž }Ïù(ƒå „±ë‘ê€DY}ß“d3|œa‹Îèß h†ß e ”P¢¤Y¸å1¨î39º&—îVŠïŪ՞ÈjÉn‘ÔS.Ð#ògl¸Gø#?ˆôqj‰1âȱšy¤ÐÉñ|ýszÁgdl3t!ØÕ¥žŠæ^Zà~[_GH'Ûé–$“|ß#¡+úô¥øÉ–ù†¬%ø¡Æ…½¿—7-Ã%9Oä'šöýúr·ÈŸ ˆG>—›ó{?tµ–¤Üú§4‡W–d¾d^¶„ï›ÞëË›“ —“-> Ïœð™WCž7¢RO~}%ê÷&DŒV^ ‚y'NsPa€»~p?5ÿŒD¤kÒûÆ$`Üáï_°ŠóÃöËèñ¼u|§@]]ؘT“Cœ•Po´MFBB{:QQ Œ‘yÏ)f.2ÅRäÁñœ,V)¡þ®†Ý0J ï¦ø '„xÙ<°L²Ý mœûÄ’ZÍJOß®ú¯ AÆÜŽÀæ@RRTÔ Z MPøryQudí5Ýìw† Þî]—bd/>iŽ,lµH6¹ÚãåÞþû™ÿ²L !Ü…¢É…‘\Çš¢ÜÈÕÞ|bå”@€À#•ôÀ]A>Ga…‘°=Íž4ãͼx ‘J!rØ?yn`±è0Öü d?=zY’À`à0¼.~ʽAŽŒŒüA xšˆñá¡ËÎ,ɯZBÊ·b]³ïMÃVò›úx8@0°Þ Á¶µÛ‘ù©AO8Œˆ%=>à•SbØŸñ)4}(á:)ërËŠäV¸Áv‡¼çÀ-½U9(P¼Áîr£ž9dyš·b¡ÿ–“.ô‚öê°ðr ‡O°Y”Rù;ÃíÅ…ç54oÌö¬Ö ̘}mÛgØ;wdûŽHsI<èbkh~>é ÑRp/DF“›Ëh×Ã[êpM´f­Oj*„¥êeöü÷Æ'71+m›…ꊡ퓊¼ÀjÃ¦Ž±Å®gé߯´‘)‡þÃ#WÔ•‘Iø¿ÊnéòH;Hû5»ÆëPG!xÃóÄpSº ¾þ‰UÓ^:É4Ö½ùN8=ñ~\dµŸDÁàÁæéãðîìÇŽz1ƒóÕÑþئzhÕ Ùù@âµõM„寸Wj .ÔWWØLQpb¨á™òtaW}Ñ.N Oá|ÔÓÕChFj¼÷\£ˆØÀI5€õ>܆TZ”©STõùÒf[‹Q:‚’ÇdÞT`„V­ž€èfãësú€AHè2·–ççÇÐêŸ1Çðq+ðÃW›%„.\üì×…J–/tÖB]+Þ|¥­‚$šHaê™ÅŽ„š…¥zlÜfËo Ö®èb°0pÔa.‚”<U\!2,öiãøŒFNå/Ü Òjæ!-mÂ0{x²·%YlY¶q—EE ?€ D󝝸ã G|nf­W Ðû®Ï ¥©B£Uނ̪€»™t0t«ûgŸ / è˜Ã@ý'WHC`ú—Ð|f¾ðE;8S)‰ [#œ4)1Ü˰٫8Kƒ#\’·‚â ‰Xâ6”ªh Löþ[«·º˜¬Óœ"¥©$ÒJnªc‰ÙÝÄPºŽÈŽ9wøÇé¾'ð€Â’Ì}":týj¾n¾Õ ½e°ú’r{JÈ8Ûz~!` 0Ñ>¥"#0˜&d®CrPLhâ‘iÿ±ý6®à;?£jŒ‚øiF:‡7¾˜3Á9¢ rN§™û’yæê[ëó† ?í$ cQÈù5V™`i€ÀÏžá¾’O ¸úx‹–~ó˜ìTj×£–Ø‚| áÉà5p—¸h… Õ¥˜h´[‘Œ~ìy?V”:ôPcûÕ*…~€VRžrùJh‘EÝåáõ½i×:ILi&ÕŠ¨ŠŸ½î‘àvØ”’å)/zò¶¨¤ÝàÚ Q§¥›`ñ¿ÒêVâÂÎÚo›íæ³ùÔ-´hÍÅköýœ º#ù¤ÔcþŸXjÜkcs—T¼ä²N¼¥~Ôý™l%j5)Œae˜rž2» Û4$dË.§õ ­FýUWH;8÷œ™²D ó•äõ•sªjèÊ*<æª÷ ½=ý¶×ð{ó0ÅKM—ÁŠRýݬ¯7™•–h4QTÆ fœú8¤ÄcA,Rç©]#/‚ÜÅœæ[q\Îp¥ƒª†ÝÆÀè=A@¦›\çáø@ðD±‘ïÍ7*x/Ò¼wµ‡¤}ŸlÏà5ˆ²ìŸ·®(5ò±´ºm×ã ÷E×Ó8oæûôÔ^åAõ†à%œûVòvŒCk»¢…ͱ•íÀ&¬…–Ï$Yµc¦™íSOFž3¨œœæ)AÌŠ"ÔêÑŸ¾£ˆÍ1É¢c—ž:¡{1˯c;psEìʬùú¦8–}#©»E¦„­šä%þÒQ×fûŒ:ÁöS?ZKªâ´JNdö¨þòÓÞ"¹Ï*FÓd§¼ã‹ÇÂ5!é™å~«>Ñ„’w4AÏWTÖǪC~©æ+(M0Ó%é1µoÍp;#Ûå_Ë1 =LœPƒší©Ñ ¨ /YÖ˜¢á¡åÄð|¹¯Ž¸|¹#1¨/ª¬{Üqó®íð;Ÿn&¬O¸É‘ȰÈÜfW“c’Â/K•:'í£U-¶Vð¾^ï×]Ö‡Z_§fwMMøCJõÁ´Z§{½7ukUÏôê¦è¸î9/¯;^wÃ{q¨)¹Kó !ÙW=¡û‘æ֊3$¸þ°øÁ€xÝVÊ(ˆ—wfFÀ™`Sš(Lr Ë }Ø¥µj ˜aû<¬¾ã¯Ãö—tâ1)ò–â®9;ƒ…¸ì¬˜õkË þ”gø{½|¼]íÙ½ù̺Ý, 4gk•Ñùu¾+AÖ÷¢3DªpC6`Àö9›KC´.žì0)¥ÿœZ»3äõ°åL…d¡ Îí Yè×ÔU"Üë‘E5ÊP&Ê5í×Ûz¸Ä_’cœ_—µ»!ÉÛk _M°bmíh›gd-¯ó¤Co@bÅÆÛÆÈÁë”áFpmÿó¦–¯@x=Ù½ì^‹™gñ}ôKôr¡$¿UýŸFæAç’ˈ–EÄ›ªQµ¬šÕ7I·öKƒc1¾{v—d%¾üØöYÞ¬AU¥ÝZµ…´jiÖøc(‚!ä©ñZdО^À‹‰œ\I£ÖþàO B÷ü6£¹q*°Šî@(œŒx~âý_Óù ܤ¬Œ¸ºkÍûŠ ê<çÉkOÖ9©*!²%v‘ÅEÄX«ãµì›8CÐ0ƃ·rœ­Àwk] ] ‘ྠW<+¹‡O hq”I«Õ¡ï–Aê7Å™ÏæèTÃóxÕ[*éaͶ÷íB>Új„å# d©÷ŠvÏðÛ6žqéFÕÅ̈Á½¹ËõHë¯ò È"6eÉÊä3³¾Óþ“XzCŒu}¿›pîâå†`³àÞð4óÈÞn"Å+‘—Ž60*jIåªl¸­k Kœñu«*à&5Ÿ|b‡›š38厳¹Q½·Ôp[Ë«âÇÔgAÞ+?K„I·fäI•uúk¸à¨ää¿+7+´`POÀ•|µcjkÖ-.N@†QÌ‘ÖÿL^ã³_l¨Qù!±ïÈ·ãtâ¨Jb­ìÐ^ §&,ðbvy[“þs ºRΖ‘4Ý7:ß…îÑÀ Üœ›…6î<o ³£¹3 "¾EŒö4ùó‘ŠX¿5}íãBÞpf¹×8ËÃRžAycUÑŽ9^?¤‘;ök?`®Qy\ˆüÕ/ä¾þL¸Ò¹kœÅ\LÁfùvPc|9V©»Ödyå uóHæm}ÏRâ©` T…†´§¦Á¼V ;v ôk£8¢‹““ý‡ýDÔ_ÌÖµ§iŽÇ<ƒóìçAcêD8¶J|:M–ñ5îÉ•Mt.‘`µ\}aæ·…–R•úÅ>âF©»Q¢kš?.´ºT“¼ H˜O–+½™€,V[÷¹¾y «­œ±¯:sÁr òàœ#Ö0dÌ ~ÀÕC5$E¹´MH¨ÏH¦X§”ñÀcž2¦†dœ²ø‡š7·È&µ/^XšÉ诛9µýåœ7½6S²4˽€[qݲûŽyòD÷5CûºNÂOÕŸ$ºI“-ùYÏXY>öCU÷¶v- UÎ WlºÑ0^jJ6±ûYW'Ó CÍù‚[lŒÂ ›UȰ©D+ðù<¶ì–ÏxŸ5âJ[áˆW÷p‘½ßR+Œ\“/š/eX„¥T±±˜ý‚´`Û®€-nšloTh]½…ÏËQÊÆîö–‡¯þ†œdÙ€öȬßngú'CdêÂälXW…õÙ'Ó“y— ­Ea)cÜ¡†µ¨åLÝæOj™rì@yöª/b¿]ža,‡‘-¾"}³)ueɃÎ!#˜Öè¼pèÈ@(]Yãk¸z ¢YžCÔ=Ÿ«Mbrý“оßj=Ô‡¦æN°ð'u”·©[ìº ]Ÿ«B³ÂBBš6í±F¼±ƒ{[`®yP3TLwyL\ÒP~7'2u/…g‚†ÃüEÞ¿§¾»|„ÄÖóؽm"ê¼k$òäÕnx*äÐ…£×@™ï”ø$=Ü\¶’'›%. s¸[¨ØÊ}$j4ø¢S—Ý3I¨R©t5{•z Üx,Ç$ÛzæFÑ٭ٌѺ~vdȄδÌV±5£šü^Ü"Z3Ñ@#Äh‡¯ÌÜ´ ´i‹ôzòØ\M øáB+wÒýcÚ§3~fî±sKm«Tû‚‰>üùgºé‘罪·ù3Ïà0$´Ê9¯aû¾óäãj'×Hˆ¥š¿Ë‘¼«oŽïR‚4ëÀ[ÕbeP±+r-ÝX¥K €Ï(鵎ÇYlttïÝRªà¸Š@Š Ë8ô/N¼µhÜÛHB&2ÄÓNîlŸsÍ ù…Í>r Ù>Զϲ¢„tæÀ–ÖßV|¢ñðVkeçXf§Ìf–ŒyYÛ¿^³nÛ7ÅÞèËϘ6d'¬Îá*5ž1ÿ“Ç™Z|¹þÏDãž¶êí;ŸuR_eVË(B©'üEWi¬%°É‡ª™“úVÒwlÍÞ5ŒoæÈnÎEPDÜFÅ”¹m*kñœ¤³þ2ïMäyõžBCË è¹¾üðb¦£’Ù÷áõ¹}`pvÑr]ëĹmŒ¯¯# šq+ë ñKH'Ò…G»bW̨Wk¨HÎãæþÆë+į\M½¨¨W¹¤…¯]ò¡Gä(FÒˆ^Î zO{dh_îW./>©ÏéÛfª€åXàßÜéÆ\T•\¸“²=È•»žíØzo ÎN#ËâwÝ»R÷ „æAX,™„ŸWAïMéÿ³ÿËÄS€Š¢ûŸ³u’³V6\s?<`'ï€ï\ïõðÿÜOö<ó]•‡ à( €ÿÿ·xl4¢­e³æÁËÜ¥(¥XL‘h=†I™ ³±šèˆ»åp3iLnI#й™Z4¾Kãz?µs2MCϸ¦°H PYÜÖ,“ƒfïC¡™ÏïÏÛ ú¸Ð ÐC ¢„âs.„©û{Çu6=MWÑ)€¹ÌÆ|Æ{öý÷¦Vâ—Uî{¶uÆ»¢|Ú—øÍMÏ7Á7E*Ùª‹Ú^yR¾Ûã¶b³¶›¶|“ØC_ÕBØ¢ªÐFÝjœNJ|ùN‹i"¡RS±eÔj¿&[¾…LCS±ÊØÂXa+WCéâ%iƒ£ÖGMNQ¥/ŠÚB„,W¥¯î£ªê!JLÅ]‘ZrF!yKõ±U¶¬UiÃ’Êš9„f6ö°ˆ/õBã:Õc[Ô±«‰,‡×#;o5á³ù0èj$Qùcõ¯Ö}*g½ë—°·t+ÇDzN¹è/ñÍ´!æ¢\»3†òJ ,#õĂÜFrI4stA‘‰PÎÉ`3Ë¢fž|©•Ž ËãÁå-ѽvÅ‚MÓG'³v6+¯Ê=A 'C¯‚5w]GŒ£¢ê§Eup¨w»‹}ûµ/ï&´Òœ¼$Ê"äeqUå‹´EMQb§•ê¬&T¸+JÕ¤EMuâ̺jR¿Ê KÑKˆêÃfË’ÄŠÔŒŒ¥q'‰Y²ÊÜ=m̹­¸¶ÇWóFM¾YmàOÑós»vó›mÏ4ü’1Æ×KyÏsûަÙFì6ýyã¹iÌx-qìÄSiVjvAKÛfØÀ^…íŸæƒn×C"ùWvè ËAÆÓ=0ÇPy zÃEŽ.õýfÎòQþ5ÊüVüïExsï®ì¢h¦ãh7uùתªoÔæÏ‘±‘´ä?„±—,-9mÈ@véÄcdð0,óC‹È«’‚5 /W,zëŒå+š·Õ­ óf[X=LÔâ¥?ZeŽ”U(k5^›È°ó²ÆÖmWUùC @¡Þ)Ž¡¯zšøÁN϶x¦+»QW#NH£êòú·|[åAQ-_AróQ•ø?ƒREõ+§-ÓEµ¹¦ƒÊw.žr-°c;qx*W™õèt•Zý{Tɲw5c*P6!w°-eOý¼¸"O²»ŽvùÃç¾OÉ€|›v4 ¦EÚ%e»\©û»7U5ø °»%pû|5šSClg‘¶£dáue° §Õ!ŒF pì¾Y¾iþÛ¶aj9µ¼ÔØöl]4_>M)þ)¼" #Õø·×¶ßÈ×óX wé:NôèBÀ7Çtç#Úêtúm[µ‰Ü/'†1ƒü'ÉZ—cžÐŸ>ŒàÞW´ÄÐ8´é(±Ñ­H°ß*w»ãY±:#ªˆ¬Æh8îAD×s‚ýWmŠ«@š),½ýøLê—XÛß·¡=Ãbú+ù,‚ÑÍî¬]íÙ®ÒƒkNP"Ñ_]y¶kÈÊ„ 1¡Z±jtË£ò‘@µÝq³4äÚ™)S;–~$ñ tô;{ªíM]_‡ƒ†#–˜PÒ˜ïá’æ¬"ú5ÎâÈÊš{ËãŸÆî›0tïíÖkØ?õV2N_Ú Ž›™©Ú!AxM’N9éJTܤ=qÜÂcMž„¤?»/?#õåK,oÛûK}.%¯HQùâ„Ðô‘šø|GÕî¸áñz_?—Dß’&_"#·$[mk`Ú¯©¹Ú¡–Ñl}Õw$ÖâK'\:º—/ª•¬¨|º‹iXÖdº¥×.Jvt%U¸Ð"Úxæ·£bT‘L·e+6B¡(@ʨ=×A„KÅH˜åÍVƒíN5ÐZarèÂ=WPWY½; bY¶Å{Žrc ¬§ÚHKH« ªV¤ŠRS'z\qÍêöâ`u×-BuêƒO‰î«æ,ØAÂìx¢„HoŽïÔÎZäAĤ+|M¤ùQÌñ r‰†Ð_Ûlw]¡‹ò†qRÞP(kó7•àHM*ÄS¸*õ£ùyÛC}Y:B˜ æyhÆB•°©ãskq^­á}tZ‡T‚q^X6 0 “’ð&êͯYÇe¸=¤#?AÁ2!Ò¡S2ÈD.ˆ^÷Ï“ç{4Åw]“kÃ(+¯èšÐ{btn@&6²Žf‘h m$=áõÄñdgÅk¸‹¨žét ÒŠÓ͉Q€VP¾ñT–µÜÁ#ž<ë3Ó¸C<<Þ°n»×oîŒ.È W°âL¬†ñB®Óž3,¦zT­ÚD ç*ÚÂg=yŠ•þ$–SXåÕßïÚše¿ÖÁS}G4Ï[ÿK¶‹õh ±UﴶЙ6isœMƒ-ÚPÏšµÐ°ÈnÇfš¹S,<â°OdžóÞ¹¡¥XÈÿñ™³`Y¶‘_;Ù*>… ¿Kz–eU8í`0{F „(í Þ§§U‹éH)`íyp^ù¹Ûy±¢ØÃ¶BöPwQw)åÔ'âfK=—ý`ºÐS<âD½¯È­~dÌq¾ÄÝ÷Ïâ¤×ãÑFááAÉø¢"ü¥@ð3¢oS¾íàªëÂ{ºÖ¿L”2™ºU‰<®­5w–·ébW­·c³”WèðõÐ ú׫ŠuÚŽ)d†ÎÓsè:©ìnE±unv–4ªIá)øœJ×üݸsk¤1¨§»0þ§?mÿŸÿXaŽÁÛ#NÉv9ƒÉ>1e^6ûÛ…–DQ‚`ºfˆ¾ŽÐm$P]‚]0sÌ,{ᙾ‹LŠè–Ç)ÊÙI¼¬ô).Ø@ü`µ"Xž^»Sد=íÂqæDXÀA™ÓfÜ=ë±2tÿᔳ3«¨RQò}Ãq?å]ñÒ¶Z¡¬a-ðÛÍÄø“å@ýà0Ë“Œv?à5øRnÚGçžþc` Jæ:ÈF2.ú±Aœ¥Í}k ’NZä]Û"Ë3íºQCiè×.~"O 4HmPñ}Ž…#†Z¹n/æ%¤†½wD[ÎŒa“ƨ[cKèÀžbÅ•›ƒbX"g/2nI˹iuƒ„i«¹ì¨^ŠZ† \?8¡×….“rç5’k»±ã*y …®¼ù¦Å%œ³ì›¢5£ rÚöÌrÜ:è$Ûª ñù›÷Ý–7Y^ÆŠ„Ïkƒß‹ü#¢ëÿ#·÷¨€wóÑ×ðC`_:uÃÏåfääàäädá<€çô‘e–ÅcO«ÍCúæL ç€Y¸P*;céá.»8ä€ý‹}x—V›c|W²g¶0k+Û¿íŸq@&à×ßà swR )S¶ŸþL7}g·À—{r˜äÍ=8rørO[z}àåÍ­{{z(öñö+»>êzg7ÂÇî‚êOé&DwŒÃ'Ói($QÀ#AI}ÉB1ýW«ý‚5ÊCM¯™=XÓ±RãÊ0—@” úÐHPûZMâ_÷k iÄQ0ÝaZ•@âd˜"vc.Qž#æÌ ¤‘¹Ê;sºã¸áTÝ.é½SP@ùFŠFñ5¢C yÙö'ƒäÇí'«Ý{[ñ ñ±Ð”Ñ£¯Y÷b8É3| ¢lS¼Áü ¡†œ¸3¨bÖâ¬ÅüQ¾ë3¯%ñ$b–jˆp¦Ù4çê´y…ÉÈûý#W:œ)'"°‹œˆrùsû¡ÎïZ[¢n5qó€=ÚÒ1Fvÿ—în¢‡=tzîþô»·5UöJ‡ƳÐI,‡öQå³¶7ÒŸEë]NëãHîšÐÚË=X¬‹âçOSƒg²C.üçùcHO”ú¯1ÞèRój<Å©’$,½ÂHNvª32ÖTÏi7›qÈòVDn aú<4Ñ8­³+Ö)Œ“ç*šô‡ô}nì<ÇÕš'ÃôÜ ´¦ÊÊ`Áö7›‡Eš¸cÇéY—Çä2ÏiQ¢Eô€ÐªÿÇA»Eï7›8µ|»:G¿~yà…F¹*«c Õù0j[ƒ¾ªø$Ý+ãlê~ßh–évæë‚¢!Ùzã4-g¦n+Ò(ƒ‘j.Yá˜RŒÔo®'ï 6ïô¦Î¬ÁruG­œ:ƒYPG ÿá`ŒK$w*g¦Y!K•’~³s”÷à€¿óXz¯+¡%³êÊþÜÒçfÂzþ$°7ÕYS+#µü´'1ÜMŒZ¡kaQw´áðÞžÛ}\þ¹*Ý¡XBÅ- jÃj&2¯#‰›Ó‹Üýù0Gs¯ÎJóp¡â§\íáR"iúK„Ü[Zn™ŽiÞ2<[Fwꪈմ^OÄë—Q¡$ilÔ¯MokÀ=G•@” ¶zàŸ6äÒtï Õ„>¸iÕŒ OÜ(ƒ`ö8Е㡸2HŒm”Ü‚$®B€pÔNUv#þÑì±ÐºÓˆà”ÅgSÓ$ˆðQy•t´9¸Ûá)ÕÊÖ¾î{úßà u½üU*â†!éís'ä cç¿Vl‡¬ÔWbɨ I§¯iÖKÇ#€€¶ê%ä­×£k}Ò´¾ó4/PTêå¢, .S<Ø/Sàîù6¡ÔU-ÑÄ´¶9tz°°[{Y÷혚©Ïµ#±ðÇ:ñ?¿~þ9ä¹öðõYv“Q’]Z×Ñb‚_›“;fE_Õßt^=·Éý)ÌäNpŸ>=…U?²~má;.iØy©²ŸÐÅA¾ß ßOÄËP\J¥I¢»8J¼öX«ýÌú£dÇ©+ˆ§0©ß]êá™ÀS?øGïèS4RŸ‚/øÅgFaMÑZê±âØzÄå"[rM 5V¨iç?B¨ CÍEm÷íJ4’OQ¯¶¸CŒ}17>þ0\SGÞ˜TNÁRÌ´ÒúšG8DZŒ€N"§ã_?¸4—bɳ©E?Añ˜ ¼É4jÇÁŒ@_¢e®÷àRán´:¾Óù:V„¼5,èo-N¼?Þ•³¤Ž> .ÒC¼5B“Ã{iÏ««è!¿‡w?…;œ°?ráU‡wK²ÂÄìH ‡çïž/LɈâ¿w[éÞ»bÔè~b ‚o˜‚yYY{jYu“ujˆ^j—Gu'Eʸ90-דR*!€þÐ|ÂÛ_¿ÑáY1¯CàŽ/¼Ÿ—2ð/»Ýð'+aÞ+æƒ/3³/ð+uŒJJ:êõ}傟,~³fA¨Îšò¥a1žÜ…·¼ÍХ旂·)rôÔŵ~˜è$*-ø¨7©ÍaÊ_ ¥;_å5?<×±6ª:¥—àI˜bzIªÉ#¬&÷Å…(ž[–ÃÓcË'nNZÍ?úµŒE½Æà °®¾©t‹*e%w-koü—¦Ñ€Ð6èhlG0µPÕò4Ž.X‘Rçx’ºK§øë/Ñé/öçÞ<‹û©õg¾*7ÝÁÓ(ÃÍ!Q½Ç×kÙäÿŒ¶%x¡ kVã†ÔëCMQÝä¦ä³Ô²µL7-é(ÆÉ›ë^ËÏLtÜOFg¨×~FÌÊ-Y_6pNœïÌÚ‡tíÞØÒkVÍKyÎËzbH‘sLš ?76:.G—; —´á„èŽHç‹Ìø>F™ëø<¦:”®º´§ÁG{N.µy 1ãr³£§¾Rà“‡‚‰š(ZÖlD*âÄþbi‘·Z;.×^+|uÃ<•@€ª°u„ )=àÐ%9‰AÅçV †èšÜX‰üw.Žƒ³Ä?YÛ¦26 l 7d*¤¼EK¥:ûÍÊS=/dºÌÆ s¨¯Ÿ7ÅÎ÷^¸ÿ<ÆÔÙ&kÚ“f“¥H% ŽªûG|€(ÞŽƒ¿³ÎCæ™hY ƒ­(2 eÇú´iµ‰WaàçU¨Ÿê—µ½~ðqÅad(¦…¶¸á8Ô¶Œ ¼3õ"DíròݱÁÔvMŒˆÍ¨¨>Døƒí™L§ï38X„Û/ŒJÉùéEû&Ülþ –ôÖN… 0Ìå‡:EÑVõÛ”B|¼n\Ö*løÑ- ké Ñ(`z¿°¬= ný¢Lz˜@’Åã*Z$ôC> Õ¢@iE™W¡ÒèêÉÙce©S€yl[öj&)2 ã.«b›øèÚ™àÔOp^2Á0«Oâp»OX¸>Î=™\%Gß .@¾Àc7Ò’ÂÞò&ã ÜšŽÇÿÚŒ‡l%.i´[ 7ò¸¬Gˆ–âAcSAHÆ1TufbÞ‹ÛÏ<½õ»V†©v{›`Ÿ)Uôàªz—è@š¢0—ÃäRQ¡ÕK•™´+ †AZ]™aìÒ=c¥>C}%1Ío“‚S§ÞAíðge‰•&¯è?„RclAIDäŸr›È«G‡rl!-ùáÁù‘ 8áY ¤.Â$ää™Vù'Y —‰Ö (–ޅϪ>AIЗ2Tê\`üˆ€g|º¨¡ÕÁ·[]—¥¿çŒ!0Y!cq6»I˜/YÂU™†_÷¼{My8u]Ï?,¬ 柈±xôiB)`"f3\-â;(YjÂ3]CÉG(;ʆ¥Û N¯Yf@çr\yÚ¨l²Üeª¹˜uññ¾# ãÒ=©„”mP®¿Yä ¸ÁøØ(*\6.ížC† ÞU«æH¥ÄØÅk@.º»¤¹Íe*‚Nx‡é+³lLêÒǽ¤G€‡ìxI¢y‹ùŽÃ‡>¦¿€Š°6*Gév¨úIR9àX–û –«ÀØXy«(”3F.ཡ÷S1òŒ·Ì-vµ N΃³Ú‚åâÍ Àh­Ò"Ü* ÄñÂA­Œ%A;äüæ»Ud/Á³ÂÒäøí¦˜9|u«enkÈÇó£C&`°cŒs$ë •`±ãý%ÙT´©M›¡Òd§ÍB°sÓD9ñ£ ç´Ô)d­ÓÈìf„M˜»N§¸/µ[´k“à!2„cÖÞçüÓx¢`æT™eBw›ObÓgáh+õs”s M É&AöÓ‰h"×»Mmº&ÌYòÞú웥ñ¤þÚŽãâ'ÈÓË@GEGH~äÇÍÙÁ^Ýön¸)Jd°='×ßåV°ï¾Ke~R´U"õœ`íN:"87|þ=joöºf‘C_Ëãa UÊæA 2îIbjf²ñ³wT¢Ù•qˆñ›­M{?à/Z=ÔĈ¹U§¦v1äpÂ%.U6Ïö„wú ªD66<2Üêä;èCÔ©1 ¥_l¦dÑ›X?>šp>F•z/ä, |'‚ÓCï@mœ5zO©‚g‰¸óŒ.B)~Æ_”äøÎå§[¸Å¹¼Êí¢’Áœì„ vÄlæ{ØONÞnk ˆ1‰6Þ•3írE sÎ6–o%òL{Ÿ»|ab‚»â7·g^¡Üu˜å¨ž5ÎRjê2^·ê¤}³xO¾ÐÚcyÿ«d€g>Cù€Ý»ì¯c;€±(*¢?Ñšw×€WŸ8é k¥_œëN0_Ô°ÕÞôüFhys·Ñ-Ê©5OeRTJÃ6ˆ¢PÀZK”½q€‡geáó3{P°ûýN€]Ïå«óIUBÓi!ÚÐZ„³3Ó—Üó¦žçTh÷ .»¯Zü£ÇÍ:²[,*!Iø`\cfYÚ ÿ_ZW‚ËÙ+ x?1êÿ1•DÈW©J;ŠjØ•òüN% µ÷Ýh2ó˜*i•>ߣª%wL[Ävpð£ ÒP‘”ß[Ô ®¹ë„„Ìç6ºSz…s²Ò8zLä†Úðã0ÞÔሯ.¾k^­[|£žJŸ~ÖâT³i¥þú¢ŸÂe­ (ÙáäÌBòyŽ)g”­ØTNé4.8JX®ÿæ9äï?æxJž=ržÌ Œï<‚í^µÿ¼be(]˜gß"ÔÏn¬0ÝÓe{ž*±~DR  ÉxGò®¿ÊE¾Ï+Ø |œÀ»“üüÛ>Ò;S*·3n‹7wøß®t'ª/™ce…±Rõ¦!PPOÝŒU)@7ï£4K½N §œâ›~ñÅMÑï?n/,®&>ÿñš_.®jP$g1q@\P £QP‹T~áG^j‚V~EpÊ8Ž‚i]¬¤”0KýýºA€Æ£°Æ¢¼ ¬›ßy®9¥ÌÈ–3sæîE¸Ý ä|¨Q–ʽÂê÷5¾þI¯x!^¤Ùw›‚•ÇšdÙcD(rˆ‰—ë\üÝ,?‘,C1ײ6È*Fa¬«NîUúžÕĸw‘ŽQ¨¤‚½IŠ$ ¯_ÍɆ:Vø¦™[àáp4 ªäÑòêä™'X ™|ÑJ­æ7Ôƒ<ák;ÕûÔMÑ¥KùX û‰æÇð­ÐC6ÖGüMM¶#=\ÀñsÖõÛ\ðQNECÈ’!p"æzÆÓõZœe;õÍÊ^ÕÞUøâóï­Õon±ÎÕHq¼Üjè¬' ¯= Žl“fåÛ§Ïrâf®ø©<áØ°ÛeÀk¾ìuz­ãšúIŠÀ'~CfÿóbSŒ\pã¸>7JýhèP…ë\œÃÛ]˜KÕªü5K™€H‡]ZáÙ$nÂïÜL:cèMˆDÿJœ.4Éî4U'ÄœÁýÆÆÂA;5qtÊöÛÚû‘¥O" êÜ &]„ 8ÉзŽÅ0+Z1¤QÌZ“G柴7þè+Ž~ÊÑôƒú;§xq+þãX?±¬[iþÎ2F[)~P£^í ˜ ô™áüë¹=Ò‚Àß«=ùàäíæóµÔs'¸ÙËc2Pe{rËØñÆ$£c¸¿ëˆ£Ÿ¿0,z‘;J"ƒjñ¨ÔA±8þ×;©àTü‘Ê„8/Ù‘è¢ç0ÝÃÅ'(J‹‡ÏÃ÷¹~¾”ô&“›‹“[-›J'—›{ÒÆ×WWmÈ_è/Ûï\ðaåéË9Z~xiVJÇtó‰wó¢E(\Š~»£›_œú„Q,ðÇêì}žÃišså>Ë%ܧ0tÊõM¿ôþþEÁ !Ëe‘¶* úØÔ€OĪîÏã€Õv›¼Yl`~Pl‘¸€³¸&Îùr‹"­ûëõ&[’Á>&›/Pæãáj—·³˜ýdL¼îX²_¾æ¤6®ý2ˆNtH1z´*p@—!g$,~ä%O üþ®÷[ L_O'& OC;?«×r“7?Z†;¬-@E™©„FSÌoç“I¡ÏøÎ-)(j˜7˜Y‘â„í0?\µ ’G¸r‚ì­¹|âO¬þW“,)Ûµóâyƒ¼z`¸Ë,°Ñƒ“?v{ˆ’Å^¬˜ïW&­_þòðnK!ÔFçO:âiÕm5€•†;Ï¡…,° Ù_Œd[YcŒzÎpÚ P5<×Îà¸u¿ÕÛÛüàÈâÅÖ‘105à̉1'*„N¦ÄžM/Ú´`-'gÀà :'v'/ÚYÐ1ÿŽH²dQ0›!uæ:0üi=àM­[j,a0±§„hÄ/ÈXA¯DÔ PLc8©´ ;ɸ‘ej‘dߣ@­FßÂ&„’Høœ³;¤ ]àPà?¥jEèr,De>~øŒÆ8<9€.Ï…ýá(1Â+A^…§zó¼U€àÝ·K¤â®KQâNî5ÂLò~?bÿ9é$æçî.§äVgZ©N-¢sÿ™¿b©ï‹_Áfza,úÉ­«¿íTçýÒo«#G~–Ë…=]Û]V4uànÏúœ+[ª„yÙÒ±ïûftç¤vø#|#ýý}yèÈ‚w«v_0°(hâø‚ Ιí z6úÙ«ÜÊ(ÏG¢žªDåAåíIÌÈ…–÷'âc’'(…Ì* x)ªmÝ:Ò– U£×}*†rEË@¨ÓŸHEÏDÖ­,åõiË ;ÅLÙø,w1ôµlŽ òé3$GÈ|žBŸ¼®¬›kŽ+gÄàH¡ô¦†5©µ[_­C`ûemF#:½G{]·jnXÙ Œ¼ö]cûC‘ä|ÅB­ Î1ãÞï7më£Ð‡kÞ׋'+–µð¡uýc‘Îã€Qx¥[IäO$ é‚ý½þ·}Ëjw°àðû)·?ñ7tttD\¬Bé’¹3a;ïY?û/R²Œñí“­);/eg1$×·ñÖ6­+Äh+vX ‚ÆkÊSÔ­ÈüàÊå¯Ö€8Û§m~ÍÈû²SÖªl×ñ?ßúÀD¯jÅwã·wqLZd”¤ðÍR“¼­xËÕþS4R¬©ã'Þ•v8eâ0§›Öú³ Ú­È }mšGŠ"7B½­”åˆr룱œœ»{H'"nZ5„Lò|î¼çX¡‰é Cטz°ð9 ï}Üôz›ü9åH²å¥ƒ-%xxnž&šÂ¾iG…Š#ÁM´óúÅñÒÝçÉ¿Š‹zû `F>DX9wü,+£\¶±£Oå=˜eÓÎÓ¦ßOïÝ’Åkã­3Óe6úâÆk‚ž§/øô4!ˆ©fAjí<7¦¢Àìz¦^;ªÛk)ÖmkânkòòÆ@Jð;â%ïê ±½’j¶éã0ŸîìÑ6þ=ÄðŸëdáýíˆÝÎè/h‡,‡¦%ßÏåË#faà‰ôE¦*ós!i¤!sHà~ûœ¸€mýÊË|2pmO„”Z7 ¾ÄøðF¿OnÍšº¦þ‹ÜuäôF!¢fõ lª2Ö׿ˆ–ƒß@3ª÷Âkè CÉÐÁ–E!óîtmY—u¹VÿÍ|¤îodѽMëQo³Ç¶Œ•ÈIë'Ø,0Åm‹$¸«Œ‘?-›º.öt°tæjŒ ìæ&£ïzæ¢X ¨Ê¯d\G¦…ù£JÐyÊݺìE'‹¯ÉGjñA!p‚vnL‡ª‘ƒtȇŽT‡ôCÓˆed=ð¦ÿMòë¼¢í°íÿ¾1©-րЀ €âÿ>yõ¨Û8m"êúÓ7šh$’nÙl²÷´ç®'’²Jz2)˜t. 'j‹Ë"›rR€àHŒ»ìÇ6¤ãc¹pæ\ð]•ýý†û»ºÁHôg£‹°ÌuíºÝy¸¾Õ>Þ¬Lü}Íô}Æÿ©Ü!ú®ø5@Þç"òO}·6´˜‰ü@ £†WÍWfú¢b »˜¯Èô-€ §å»L<_!脺#i&éKøQ#H÷7€ÖPBدª@Ð Yae3 šÏ2€>b5ÓBÔîÆòÚÍ0À:ÆŠ™M.x@˜Fu¬M.\!LT?ž{sx]oÚh‹aýì¹ æuÆÊ$»r21ÿHX¯r1-ÌÐH=5P±ù”<í°ÍÈeÍžÝÞžšÝŽqÍeë<í#±Ý0$úàŠzBquaß.½ð±9èVU˜oÒKýŒÔáwk+o.5t²±Ž  )ÊÄÅ•:T›ÊÑŠúÔæ«lÎyH{àjÜRTœæ Ô•Å% ùÛÓ4Õ‡KaübŒV*pÞe°(‹ëØ7$NÜÉ3¢&^Âö0qÚÌuÃųSŽÒ}•a‹Áà£Þ¢‹QïÙ\õd_ E$6-’6¶ãš::Êz-˜Ÿi¤¼ZÉj7£þ¯ºó¥Óe‹ê@•r¿Uò”aA <Çêý._2B ¬_†€Da¦Sûß» p2;å#_Sü4þéÖœ J9ëA™Ÿlò=%–‘gy[g›ÝJÅ+QÀW:4ª/iܤÙyDH>¦_q'#Ãah‘&žåÂýÿ.êIÅ >fôàÏôXÑÒ 4|´0m_jyg„ÇùHx-l‹”ÒÏBÚÃæ¡]ÈÝÅ6¸5> 6zòSê®–Ž‡\wWc3UjZê*~u7=^xJJŠÉƒþ{f‹ô¥oju›fþÖ+¸feˆ££øÖA8×°²©YOI­Ÿˆ¹«+ÛöNÏKíÛÃiÒéë§ê’O÷‘uÍʬƒ¼—B9+!‘4ËÅ@ÑNµ1r÷E© ÜkDßpI¢øpÐÇ¿j.JNkjRSœ ¤Ï†å{Ûù¢‰ï{d+ºv!¤ªlÓÒç”N\nÏg+”’'Ûœ®”B.‰BMh®?&¯3©×TŽÀ#6™mÓ@N»ùä‹É׌°æ¦Ùï‘ö(b›YÉ=9¨½£{£Úl£Ëø¼Ü ¯Çµ_‹i’Å‚T¿â>X™ÃüÈ'û²ðfWr]ÂH·ð&=GNr¦ oµGK’½@ƒÈ}EvÔËö O í<¥X‚ú˜«ùñ¥zôŠc£«EäY[3ÌIàg8gd3yûÃC^fŠP– NâÉC*8è(Á8Dˆa޲ì¾ÅÒ®„SìÝû÷’d~‡Ú}ùý²)¿e$ ÍA˜Õ™·(*I3Õµ~vªØdJÊ+JÕµ˜ªk³f-¬ƒ—›UUñiz3N1¥D ‘DzÇBô*êóÑü'6#îHzž z»k:Ãvå·Ñ@fÍ@©Ÿjöd0Ö´á:¡9H ™ÎáÅ:Çû»Ÿ·ä·£³«©Qëˆ1%+]BXY’ò–±`ã ¾‡KÔ¾$) wÈ$\A" *ã„“Ûî5Ç`¯;Ãö»tñ±ÑìyðÁte $ ³«Æ“Èt¨ÒýNÛã\t½ºñrH²ÈJÃQØ«ÖÑ`¬4ÙZË–¶I˜jéûyÝüÄ© ãÐÔóåÜï Y 4ó–ï¤]îןáÔÌŠ«Ê¦ê©-Ö’×ßÞ³[®“C"ÚlÔÖðyÀÆ1—Ä-ºÎ&Dºâ¡³ã T³Û*6?t¤ã ‰ˆq”dí;F«;Èìfµ{¾ŠÔrVmÒÇ ¥ÂƒþFJœ‡ãèê" ÃaÊpÃ>ÜWàVÉ -W™Úèãå31*ajÁDMQë8•g )Pê[äП«ôý›“&3­JÑïSèèŽS|ùågÔ.]ÑÏR)¥{ä$´§DÙ_Þhêï•7«Ì[>ыۼÞc,NgcÿP³³æ2DÝ<&q@'²,óB[p9!8ûòCeég¹ž¿×Ù½[½˜bbÄ:ý|fÜ ¼_ |3Â]´ |Ìmßx‹å+Ç7)½V'ùR0£¹¿+¶«w½¯Nïþàÿ§ç¦] ­ ûÏwÓü?Ë' [3kg;[{GgsGãÿ´‹ò„íBÏ5\ xÍÛb3ļP¼.‹mYY!‚¹¥1c©DJq5˜È_ÏÈnb(úTS÷¿'ÓñÑsØöU‚ªÍ€uçMö÷ÂN€]ì I;c¦ƒD è7¨äóæÏP²ÏÛ…­»ém§°sé2¥öØÃ\« `Ù8Z÷ )SDÃûùB ÙâáÎgqÀ¤IÎRö¿@>¿"ÅŠç5µì“$ýª†€ÊÇ • èvcÓ?ºµ ?IÔOfˆ5¤2ZàùÝÛ8]^•H›øý²³ÖÊÙ”YBè…r"Á6µÔ3³{—àí¢Âà]4BFÉÛ¾8 ‘&3•Íù†v³1ÔO§ë?ÆÒ?Þ-7óÆ60Õ÷çîí ªeÄ,µ¬¥À×à.X‰7SØ gÑÝOz¨qç{T§¹¥÷ÙýÔW{+odß…{ý›uE;ÄlÙ*ônW0ñ¼æÊI,'®¢¶Â׫J$b-M3 ·@GÈ5þ*pxºeÕ:â÷ŸØMK÷Ë|Ã(Qv¿¾m<ï¡UŒH™ùnÔFYÞWψЗMÃâ˜r±æuF¡šI3óf@ãîjƒa4dîÒŽÜ6q†°J°§œžÜ9‹vïlëb<ÉÚOz<š{­9z>ížn/?Ü’ÏÌl[*\È–«<]ªäHsE&Ô8e®o•8AñŽÙ Wöë½wd½D¼FÛ€23 æ(yJMì³HÎBòÜ77`Þ±LÛ±oõÇHŽu§€’’{Jäµ!®î°n2’"+D×b°Ëëü¸y)õ–¿deò~R‘ú#{ƒ`ÿùÈ|¯Ì$È‚²ƒØ)ÑrÉ Aƒè(¨ýö· b¯:’ÄŸ¾¨¬]Uªâ¤@h±®CçBžæt®º÷cüÿ©ÔwŸ¡Õµÿƒ<<(íÿKȵ(ÉØ-ñ#ä´ —Å„S± VÆs€œ:  †UùÇ€‚ƒ7î¬kEðÝ©Û(°§0ÓË~£ûLW'ù/C§­ï.Ü^_O{qÍÝÖÉ~Å}™Vç‘ųL{1!ú)úK÷ óŸö"räë°£#tÜ£îcí/*³l“Ý‘6+€{lÀ¢Ey%s|ÈÈí`²wS ‚ºš#©šT8yÿ£n‰¼çvÌ>v¨lß;o|í[-Ï-›L§µ8Â,òå"½IÕ—Ö “1Ò‡gh™fúUâ+Š&4{v(‡¯€²<¢þ3uð«Œ~½Úʘý‘/©ˆó'H—Ž¥$§¯8Ø›;ß Öà[˜Öi=ƒsîk¯w¼ÑF£¥ ÷—áãYƒ›Öª/L‚órÅ?û£¹åCØø/Õ.W‹Žo1+¾F÷Ø×N&Óg¤)žR-)‚Ec`ÚV;ÓY?Ù&b+{”Çð˜V–ÐoU|Ò<¥4ÎI/œ¤–Œ¾Ê§4Ðu·×+ÅÒ•:iUÍÉ4Žv“0×$²`É ïÈ<^>œ{<›—¹—󪕴ê¢&¡„$˜ˆð²dÉ"à4ç`ÐÔÑ5ÎYl©‚Ù8PÀ Ë=g(ëjJ2QR‘N¦Ö$+ "ù€aßwø{¬kûÜÕ4©†(æî»í p5·W0w·¹äÉžqÿ%üíªþ’ØO nh ÏopôOºf-9*Ü­Ž»vð¸ÀàVÖiYÿzuk^jjéÖ ý'ЯQÊSNh#G 1¤Ön\þ'3Û=–(üÑu¸ÿ£¡t6±6±1qvô0qýoõ?ˆ¶¨ÙÄaýû_åÐtG|O…s]Ç*ÖŸ|œŽÂ® %$•i&ÿ,“ž†ûç5·Ñ×[OH…âGЭ'w]¼¹õ&3{z¥šhMó?-Ë`®Œ4¹y=#[.³fŒòÃüO£ƒÇ‘©=¢wþ A ƒ’IÜ{ÙÒtlmÁ z… ‚gèÛúÄç}\K!ûviÝ~àNeŸW. ör¤ÖC€ŽFy,ÔÅ~ISˆ÷dCÓøJ›‰„2/‹³Òò‰ñ‘ð!šêæï¦Uàƒb£}1-#WQQ?¾ygÖ…ÏŸDOüNÿ°`Æ–èë·Ùææ1æß+’3upøQŽå¨ÐŽMÈ^óYÂ[D5F~o[š™ôÞü9¶ cŠM(cV6Ñ4RÜPœ`:…áv~QD—è¾òbKy|Y¾œ]Áî¾ââ~¬Y²Á’^ÌÙ2£-^Á>àí¹°ôÞ"¨˜‚äBLÿ&$&X9²ˆZqÔ)ïâ)R¨3µ?:Q¬‚[6 f)q(äíJ†XŽw›©k¦Àž<ä#Œî–L÷ôÿÑÁúúÌv("Œß3yW=²%ÎIÔ í¸P{‘(ô<4™&GE¹LØÂéñ&›kX'‚®jH“ŽT¦mHde¦ïÒ½0!-5xcy¬È Ïe å8¸•(&×½Oûå‘:Ÿ›Ão–¬JfëÞžDIÚ^uXnÊ—2qÁYiOˆv4ú°7î>Mp£ra ·¼&]r4P¤…Ö} iS $e©\¦Ü5WAk©8‰óq»{¹ïÓô¡|#wÎ’:ãg¹Y½ðe"êõë@ÉÄbïØ#·p*F]$1A•å¬C4]ÄÛ}¼ÜXÓ´@BÉ»ž¢âòØNÔMœJ=V!ñp‰ /ªü!dŠuöNЇâåB!l+ ±Ï4^ã§crK™–¢f+ò6§ªPsþºä¤¼šH°Ö,Õ¦¢W÷ÂzÀðgx¾Z3)êÆÊ#ʸk[ybñûIýfÚ6p¨y(KÃW“8\~Çqê÷-·óÁÏQí4ùgnh3vOмž;f‡:2´•Öø¡}º8{vÖ¤ïÈ9TÕ0Y²Ù ûÎþ—‘jâ'àIO«ú·ýOÖ˜o å ±ë3î}ï]/—6¬›¸½šºM'x/=O¿â _XÌ`Q"÷| Ô¥Å4’UZ´4îÀ¬AR¿2G±m²¦)§æÚQ`³7ijR†×.4†åº•iwAñî«#ö黨.ÔÊÌ?Ýâ`»*vÞ[ù >‘˜[Äû˜qig™ê…Y¡$oïî?#Šƒ€Ï\óØŸ³Ã»ÿ} }ÂÊwû´ì‹@ùÿ‚–ZÔ¬œ¶”t6b‘¹$ŽŠIåñ|ä5¥ $´$)ëd…°CGa×a ù)cªFSµ/8Ir^•¾¼Þ ½ûîõç&žGކwt.jo9¼\ï\î..õürfŠ~õúÚÞ³¾FÏâDl cEZEr€6@÷iå»Äs6HñÐ%âbE|#r‘x?'è’øD;ivoqÉý¸Ÿdw!©3Ú2½¨å[‡¹R¼ªŽåÌv:Ü¿1-W­º÷ñ«Ðß'¥:n°Ü8 d®=G—cÙoA"*´Ü›'M-\J.XRWÕ¦³¸µc9s(¶—mvÈxÕ7– ,<ßœÇf:—ëV„Ø2ù†P-\Iy¦cyÀ,eû2ü=²ýý+ûåI½ Êo,ø[ã«x€ó]a!n4­Œçø¤^ÏÆy*5øà ­°ø0¬óƒVJhqÉóP¬ ‚äǬ.вl·ì1ÉÝÛ·^ˆô¦æ|Ëà}ã õFÃÛ2D2G¥^[Ö˜¹5Ï8À¤®L‰c¹sH²Çí•ÂlÍI vË ]?#5ýêÑÈFayrøÈµAØÆÜç¯mÈuqœöYç e u¯/VØ/]LT#z´®¼±§t¯Úi}þÁi ¨ûÑ9úÎ;‹Ä.E|ëf…A1¯`HcšâˆªBöCÁS+Ë'Y‡ˆT`b@g Ð-Cbt«Ì4U,ÅK¨®[¿¶öµß4¡o ¬oõ5ûu:ýlÚ¶ í„õ¨G¥1›. SÒeJGaeèœãQÜÿ€ êê+Ñ¥Ô¨ýeµå‡æÊN#ÖYª ìïDhND ûðEf…@@‰òH@…kºvݺÎÃMÀGÈ¢‚ƒ^ú8Üï_ Œ¹L ^1ø—Ë'AE©Ù u;Y_ç‹«! Q ;ÉÁ3¶7Ú9‡{"LÓ?ŒþÙ"NS»_¤iý™dï4k®%²žRÃí´žë±ƒ;i$HN¸ºd¯*ÞÜ—o<É<¾sê1à&lõ<ûöpf03˜ì†p€?áM{`Œ ÷ð-û¿Ó#4²%ÇûjΕ˜qM)Àows3šéf`ÈòS^LÕ.ÔPð",~>œˆ®öÁXêOÙ¼9÷pKï3 ð˜+î¶Ap8„S²tÙ´7lan~Ç%*ÆÃ/,Þ,™X¦£pß ~-ÕÑQqîGá9DjC´ÉlXb_ìÂ.D…ÔòT¥ ïì*@ï›;??ÿ "€qó ¦‹ £JU;7¢4uŸŒægBã¡”wÆ\{ÅØÚÙVB‘CÌ…©SA!ÐÿDò‡?йdÊFNè(;J  ƒ6üéwhEh'pÇ^lÇ’ÑG±]¾Ü'…"êk§×b&Ý =ù¨fªƒKÖHœcž4î84„Y‰Šò6óT­‚ž[©¤DÈ3Î-þE:2¤Ya8e‹¾”,‡ áu#XÚaß„öήFa! XÂ.g™¹!JÈíF¾Ÿ²GG߉By…xÙ(•ô›ÕjÖm0Uì1-‰Nr¢$§Eâx²½Ÿ‚á¡óÏùBûÑÌ_jÖ£2}ñ »L4&§wÂŒÞV"²¥çY!‹mâ‰o7Ã¥=—Û¸Ö›]È)K]®#á­Ùk§ÊEŠtlŸWù\€áƒHµ¡ÜARX¨ŒG}úg˵¤¦S™5#æàìÊn;ë¤S©á´zíTîOòJªœí‹‰*8XÀ¦ë yæˆÄ‹yiSkį•ÝóÜŽ ÷ ;¤ztΈ-Õµ%“®Å Å2Éë–C¼±ÑÙ>£™"Y7ƒ…àJ2/ˆe>p†_©ûFÕJ‘èk(Ûã.š¤ì/.×ì£÷dL9™ä‘x©¡•˜ÓvÝ WÊ×`{®øÊk·ùD~Võ´ô”ò1Ù÷?ˬDt:uüO‘ÃPþ?Ó¿ëÿ:ÏÄÎö?ÞŸÁ²rÛ}åê3Tý%çoM2•˪õ‚khZC¯OZ}¸„°ð×€Ž©Ä °ûûÚå‚' °>³þ*8©?í=í=ƒ?N.ËŠžAOìæàüÕn`8 œ#¡1¦œšp_d7lj—B[ž?–f&BOv^"†oZ$kcAÝu+ŽˆÕpÔÜ›`Z–J„†"I'`FSÄÊ”DBz4Áj"€¤Ô`î·†A„ÿ¶¿¯€àxGùÍL9'Hé×ÏèåãÖ®[ÓÍ~qgã‹fÍgï¾Î{;ÒQ¥ø'!B€ùÃj†iíÒI(õ<€RÄz€ yŠÂ:¡°ë2J"ujÈ©Ž J…ˆÌd´¤­šlRÄ“ȯ# ˜}›þ<÷1û6f“ÿPÁ÷ÁÞ/·³PΕþìeÄ„“°t°_Íw~¿ÆqçKÆ÷ nf#( ÒÊ[Š9 /ȤL#of$I 4ÝŽ&^ÌbÌŒ6Ø›è7M™ñ„:ƒ´'aÑj CÉß Q>l=QüÖÐG)gaW¼B6þ"J—ŒýýÄj˜dÅOD6¶»`åoíoÏ ý»lÚ[â@ Ð_ò£e- ˆÿjÆb*ñ 1H¶¬§ñ›ˆ 20‡Îï4axcô©>‚£1š6Gš$Ô|ŬÁ£Ï<¥‰UÞaAx~EñÀù’¨£ÊÇIÉl6âÏI…Ê2Ú%eMh‚åÆânœ˜^Pxïe5UAåiŽØ#£ÞÀcJ”HÚC.»t„h®7ÐÓ7 vÊ ÚŠ©{".ª´dˆËVª¬h¢ÒK‘q;›Fc·‘yauàßE5ßš\ßÅ:éÝ–2à›«Ñ èñ‚ ƒ?bF_±¼¹ [ü^©ZEýh¾ð½p¯ëýÄDþ­"«åÿ×?£zâKO§¬¤x8ËŸZ¾½Hïþ ­†Z†>gï‡Þ±vuÜ:7å¨wØqIZ½ùBÞpŠˆˆ’½c“b?šÓ%‰¤P†ð³\)4_#Ï´¥Ü£G›I›_L½“œîô­u·e>À&-è³8ßî´óåy±‡Ûcõ'åÅP”—û©bëdö/|H®Ò„=,mÏ~qg$€Se°;ÞV °ušèßR±ÝxÁpÜff²Í ¦¥K֛﨑ø©%UàªÔM¤H§Ö?c)þþkv†)¶ù>O¶ý°æð™ÚøAcݘ֙ծ\sª0'Wt•·ÐÌ–G»²®Ì1–£CLßÎIÌvôrÚƒí¤ùkK°Ù¾ë¢®D>›¼'79lAšÀhѰ*¨·7}G“ï‡nÍÉ•7HËÒ?‹JèÞâ°§ìA=öbNeäÍC7º£ÃCî`".^NNÍwv¢ãb>¿[ ½ °iqÎâ@ÿ‘òÞ—À .XÁÊXÚ¾SíÊ˺(ŒÍÕõLJe÷5›¥¤w¨Ô&Çû%Š˜Ñ‹ûËJ.ö§.üÑžaY²ì[e~.x›`¾»Wz/³‘Oq©¢IJÄÇy¥£HÈ\„ƒ}òév¤‹SÈmÒ8y4ô±Y©í¦cŠ,{‚@2.’p+,±‘ïeêïÇLè1y~8"ñ¸^ú/£\˜­\´‘X÷ô#ng ‰[Qo!¹„3ôÅ ;ì&Älv›¯v B;ÂðC CKÓ«L7lMTž7ýÆô>¹…FA0ÆsÅ2zjn€î8â8’@Ã:#qS¦ò¶íÁ¢ºœhƒrG”!õ­Í¥Ä%<jüÄ-×ã¢2ëåqì‚™iXANxÍìZ´3ë¢AŸŠŽ*ŽbÔöh³£øl2–BË)9F’á*H.™ºbk–²øE phȸÒ!k»ïNNEãùðQ@ÉËSÍè-j¹“á@XN}xL†ÿà¥X3; ­˜-3{¾¹ê­€‹©LÕíàœwÆ4ó ¯ÝÓC#ç}ô;Ã5 Øtå<û°Ãé×u|÷¶%‹÷¨šâÚâ‹ ­,Ì%¹$ƒ, ŠAÝžæ¼F'B¥Ù&ÜϦ[Þ˜‹@H¬¿4j 6ÂÏ„÷Ó^’ÓIŠÄùÅ9È”‚Íüðâ ®·]ÒUI¾…¾¾Í}€y¼è>A(]µ  TW3–p3.Þ½F¥ùJ £Î=8À¬ á”D’A¾OÞ€‡9êÍ ¦/é']Ö'Þk[_D¦oXˆ«í&³±ðâ4ñ‘´µ¡^¦C±ÍK`¢3ÒÑrí8biAö’¼†Z?yI|Á“œÈbÚxáðD‰’¡3ñêžlÓhâZ‰ùÒmfì2`—8k7ÿ¤(9&…E"HÞ°P-‹Ku–ÊSnp'AÈ#‹BC´ óà.ÒføËÑæ9HËc‡ï\wo8þ±¬‚Z…l>V­ŽÊî©$ ƒ²‹2I¾¥Ãïì}Vá÷žBô0++¶o|¥¯´¬JFE<Ò“ÒqœÃcŸ~W-hî%hܱ +˜äueá‘ ŒœµŠð°}R<ÂÚÔ¡ñ9z='+'nEâ:ŵGÇÕ‡ÿ³ðñó?¶ô´~ªêf£X°Â2ëy´¤ðçixO8dx¸»Dˆèé@v=î;"/¯LIê­ëTÕ¦6¸J"#%c,]೬;iù¯Ízý»ä&ËÒ~Ô  deE³¤…»b‘^§Ïý“wöºŽï‘ž. ¿ÚQw¥|ôòðs,Šªú [Êê€VÐé"jä_ZÙøÑ’¤Å©vjê©ÉùÚ,:é ³Ùe ‘åÇn•P¼¯˜=5̵JžÝ91ì #(²µ£«×¤±w˸§;Tf6禴9ºÂ¡ H 9±úµÍÕ#ð÷TjFˆ³þÅýÀ'HÖ~BšWØ}B2?0Ðgö»w°õS•úV L·X·Ï)bÆgn’§,Ÿ±Pî¶ð¥­"ƒCÛ=°¾÷OŒ€ Á*ûe,iaþ´ˆÇÔ£©5f}ñÑ¡Á*²|ÀO {O[w ;xþeŽô2¾“‡+UM,é¦ÒoåçÍ^8È0uà3ȳþ·~i¡îVHÎÉ“«`%åFå¶^®zaÇrY¯ÓoÿþÙC–‡ `Þ0Ü-çÈë1.é°˜Æ\óòLÎZŠKKµ›O!I{”Å"K}ÍÛ[é…ŠOèêäÆœÔ)“x­±ò\ë¡u^‹|O|Åìaµû×He¨f oJÜJäänxkò¼vv2j¾Ý¤r[AI%Šž3¬¢i¸8„OMH÷¶j°æúäáÊ1¯QÇ{U{jLã\+’ÈMg…ÓÊ"½ò)w¸OAçT$¾–%Ý軪'Ù¼n7e-LŠ"ÚÏ!®wå‘凜kvQl•ý6šbŠˆ\îFeÖ®¦ã馛z.æcé‰1õ“‡Í›b?å¢âNG'mw~Nç+g…àô蘮™âôë­·ë ñ?0Ë@“°~]Ç߀&–—»:¢ÈŒª‹]éY®½Î¯â²ú‘°g¿v'ó~ô]v!Úh½ïY`C¾j·o[zÓºÐ)?î YÏ_[z­»ˆ­üÊ÷:½æðU‡EyYK§™?€ÿY¯t“…S@ð% ú¿1¬F-š<öØc¨»Àüñ‰D¤’É4WÂÔDÂÈùÆbìxBDC¥üü L4˜ûhû a\ç·ü‘hê”Ì”ÜÈ]™Ïd2Æ£Ì d"‘ÓÅ’,tŠí¾»^]^¤ׂD#»[;m~w>wgnd?oÐw¿íÇr8¼wƒäí8FÓ|1Ñ*E£•™•cꬥ—ª1Už70 9$Ud¨&éÄÖä#•”D£éI¦7 ª¸&X¼cÌ£›˜ª3õ¯ìU±À¬•ÈÓX†|ä)Å“Œ¦°J&‘Äʦª‘Tc˜s Ô3xCXrò>É—ÌÓXEpÿ=U0Ii:€Ó4©¿²4i¼Â<Í9êªkèúÀ|Íx}MÓTªþbb*#ÅÙÈ9V}ÇÙ$ Ãk¦mÄåW6¦ö· ÃQ hœSl° Á§¿J©Ú3^­¡š£6tZA9õîðŠæñ 5Í9cZ­ÌÜ¢VñŠ´ŒVáŠfÉ1zdM>W)Õ(hDWŽã*³?¾–þ”3©bšS4y5½Xñv¤æpëeg×x©5Í)÷l­WÄú¯á$ì]בèJ2ö‹ÿÜ„¥<I¡Šû;_ËåôpÓ±ÁF’V€?Ë_22”iÔµÆ:–©s:ϘTªYà9F±j»K©Êf’5ò(jS"ßÝ¢[kƒ# Ù‹Í® -y ›¼mÅhÄCÁÚ-ù²ºƒÂ+ ÷"odýr~-ĶÎtÀÚFT3$€gëä3çÜ©ý–ù|¿µØÑeö•»uj aä³ <^qfo¾áµ½rö6œ0}»´r¥ýï…NêÍ›à0£6}Fe.ö|{Ñæßs.žÈRr“wË®¸~—€y'¬m~$¶3k»=DØwøï±jäÉ̦éjmêÈÎBÇ£=Ò¥ ‡— R;$ê—ürW«íʃå“óTGØÚnuÏÇ'’]Kê´\/yåF`“¦¥ûMë„ìǬ{ˆ7??M’PT&0(-?¡áÄÓ&â=¢:z/²Q—Äé•ûÜ!ây“ý*: àãâ_þ„Ô?öì{GsO§+#ж­V°ÌÊÌå]hNá1‘[¡ZOBífÅÿ:{ã¤/òŸcïXÝüýi.]êgJ2oôýÏ›J•¬O –•nuªñ7ÚñhÂÂ<[g±Mÿí/ Tƒj¦½¬=Öìµw†#EÑZ~•âP¹)òal_h²ìíCp¬ñÓsÁ´/u ›®ÙpžóÂwõbŸ;¥==s‰E¡ÈJLzcˆ3„Öóû0mxÿ*nzJJ:ã–™#o2–? 9~rŽã`œ×¥è‡IZ‡Ø)GŒaðŒÊÇhº€˜}S»b˜ñ2ô x½ÿ[•ªOùò3 ·×wóbašðl3XîyçHÕ¯¤%â4ùÀ]Å6åMÛL´Öðž DÜ5þ¡Æ2 )à¡…=¢·¬Ü¢pù£­¿d{¶Å¼tUnªîŒwØ[EËžs О ºùSdЩ—á·åñ[{ºAŽ Ã)­wzM¦¯ÚàVòîqfVëìQþ´ƒÝy G¸lm‹4ýJh\·] F袗°åXxà/PÀxFÒ§ýW3"^¯‚ŽàÚqxÖfå œ¿Ü¬#èÌ6ŒÜcAýajЕ'î§5·Û¤Û“¦6R@zŸ´ÈÓìëmÇç†?Na©Þ·¥¾é@šyˆ"¸ß¹ÓÈÅU˵øm°üM mð„–í´ôð:²ÔëMe–bCïEGŽ l)<Ÿw¼Ù w.¹) Ý >Œ'[Ô Ó‚¶­OE Ÿ´L{ÎI¨ÑxŠ-Ù²\Ä-ŸYÕA{æ(”p*øz2nÀš j*œ‰Se>‡ŽHFB´…Ë<|å‹­AŸ¸¹HÖ±žÌVjR_JÚ‚ù.m•¤‰EñóØ ³L`þ ”P,Ž%®„çÊ È ľp$þ÷h0ËfÌ~ $ÊâJ{oÁÉ]8GÀæÿXdÉ+È9ý¬Ÿ.Aä+ \„”QNˆøËo†ynŒD˜( h ¸ªÒèu?‹­\µ01°áȼO²xê®#E©åŸöÓ*›FëµhvJ‡ž©&Vœj‘EòêaK (²QÔ{®í"Ž3Pe³ ›‚Ím?y­m²¸¥êYšZþÞ»éÀmX:>økˆu¾‰®”$ËM;æT'ìK¶L\|þ3gs°k›oQb1üO±Þæs[s“¹f”æh«Ä¦n&MS‡k€Í §à¥Yf ï¡ ñ]N¯·GH¦%(´,m•Ú-Êð&˜ÆIùRãZÀa~5øžkLÐTœëf•r‘ø©°| § ©@óÁ È ám\¦´5ÓY)üZ!4–&µž_»(%¨çŒWDÿâøÙ*—þ·„ôWÁ²_¨2ò±´ùi3¢<©EÛMßžŒ1„ †Ægß‚†Œ—üUÓª/?ÉEY9*_;xR…ÎÉ€t@=ó?`¡MAù+˜L彇#{£`ôƒg›œÐù‡>ì_¥?«ý›WEW1sø¤{Ó¹ÿôÞjºÇ8sá†qÆ"}3‘ŽÉæ.ä¸Gè ÞÖ1fkëQÛ²ZëÈ>.抷ä‘RJ¡bÙ|´Áñ£¤O-,®[iGÝ–gpØÚUb>rŸˆ[/·ZŒ’çŠÇ&äX¨ó¹ËÎlª¸ÛŠ-²Ë2Er¢¿|©…P¹‘·Ë?v_cd _¸š'´g)* ]s¡±_ŠÑQ¨'–ÚÿN¥/@®]$ÔKn¤ŽùÌnÿá‰m÷W·µT¹ z<é$¶Ó%3¤£#©n½ðº?J#ÇÇ•£DiòªuB ßÉ~|çÚ?ulXQôçQ‹Ä4‰ÕÇV ¿…Pû2…‡)Vw+8>0o–³¾G*€Ái#Omð –‹ˆ4ÇBág†ècWÔ½Z yDð†ð|»ŸÌ$ VIº»‚¹'òW‚`ë°ëP1HGÚpt÷Ð NÚA sGÉi©ÕJš…kvúY­ô(Qm×¥LýM‹U\‡&i5‚šˆs'§ýÔiP†Ý‹Á‡o¯2BÖ'¼ÊNŸmeÖ_'G" œ –N ’B °„äë-Íy ÀÖd CÔÉðì ´ †˜ÎŽAŸÐ#‹¨(ÄjØ ™ç˜€#%‘„[Ú>£>vØ:ø–´nÎvÊþQxçÕû@è:ü!…(ö}}Ÿ„ž :]€‘1H“eæ®yõp3›­Pfà»y¿ˆPÐ#ƒ–ÀR.0û× ×·â÷üÑàv!eh_Š9ù¯,ô\ méÁ;>tÞant„3?Ò?\÷9¬äŸ¨d (=”ɾëô(5*3,„¥˜=¼³¬4´YKVëÖ<=‚>Xn(ÏàI­Wòÿ!c\èq)ÚùbEêßò­kãäzXI…ÿæ÷˜ª7Bê#¢¢ÁLp!Þï-aš›)#ßøP3°°Õõ+“!}¸¤¹ôUL÷O–Ãטs€KJÔ; ’míò_pDoN7?_‘61ˆ^lš™8–éìøpÖÇeI&5»µºÌ‚ší}J0dù :"¯€[2§“—-ª“©°G;×ïÝÿ4:’|aõé?aÔîªk ‰8W/I¡À”X4ð‰aþ4Ú¨€m¹Eº ª±e¸žK¢qixG £F9@Gf^5kÖ)Vc99ât£# öêiaïæùp½F0:h“ÙÉZ2ÔÙSdMÏ“*¸ç*Ò”ŒöÇITC½Ë‚ìœJ[”Hü§…iJJFè—™Þ³¤eŠC…–¥©~ýþ‰ýÊ{Y¤$‹ú@€& ÿã*;'g kz== [ g==:{“D»,~„»_Tâ¯E*:WÇòÊSa\5“la+ }’õY¸‘ã5·þÚäríyËÝÞŸÝ-þÑ€ŠJPÛÕµ¼}ð“ë@jðk\lB3³¨  [J1ìzÿY´qVÁk™Ú¾)cȸq{䚃Št=ѧ·¶K†v8Mf þ–|RV÷Éèøí–^B#§àÜâÜØŽtaËï{1ÆJ¶Ê·N*½«þqnß–ð¿nÍü H6GíšÌ&†êÛ¤¶™V|÷ÍG}l@ ?·a¬vÿ‡É¤¬"E¶Rð1}*Ñœl/2B-ZƒëêÑ—Šò¥Fäv“ŸbezÐÍtFxfñÆNïHf­ÊqeÇ÷6oöþeî}qKB3úma3t=Rpt++jáKfï¹ÅnRü5’qIg z„ÕˆQ M0°ü³üF²¤'™Sª9Toà–8÷ã´!LA‡¨—‹òRL=¡÷~åæ–v~µ´ýs—˜ÞÝXnC³U,ö¸5uÊÔ-úºœ£¡Ön‹E5žO5M Ð9òÈ)dècCéC ü!WÜ? ÿ³e›iP÷ßÝËÕÿ›oo¤Ó5È€šäšõ%ˆ–$$N.¾iƒ,D`SÜÁeõàÙBç¹íƒ³ýðsá”kå[3ц” DNƒ•…$&.4‚T‰ähJ‰w ¥°M „»±Š»AáZè=,˵5ÚËÒ ªûUú@›÷û^S=à6d1mÜ.ŒZ£T¨Èð'¾úÖö!jn4œ§)mÉX’ÜÚB@ÚÏ ´Rû@ÅÙ£}˸ÿ¿ ¶pÔqpápû?¶øÿ¾¶µ½…íÿê ÎQ°=bû_ý5¥¯l«Ôä4‘(훨™-è Kú (@Ä1ʼn¤¶ ~wò‰Ä\#…ü.Â3GYcb²ûÈ&ª\:îê?ZÁ\Š/ÐՒʾ,˜2ûšY=%ö®¿7ˆßšZE5÷Å*ÐÝE0)C8–)E z„ÕÑãTÜÓ4ûfͱöˆ94£?ËÌ×.°\ à%<Ëp8($ˆ÷¤BG(Æžf$)„,ÇU®¹$†‘ôÈvG3™b|ý–? ¾‘kÿ ù[#ÓЂ|Ñ$D§ƒÌ #üݳ¢¡&ÛéUV …)^˜5GnüºˆÑÙ–º³5HnÆÛ,ìÿl©,wºî¬î=îªØÀðwC,×1À8 Ï6¸DcÕ³[ñ„!ŠaáÝo$:ÇU(LÞa\ê‚ ^n­B6AÅ“-/ ôNJ±mÃäù&‹õ…Ôão÷®AûpbçÓA“Q>Xr8’AåøÿÚ3o²qìµHÞ×>ú'?“#S•)޶‚ i º4­FŠDãíøâR¯¨ž­7Ÿ‚›_þMk¯sÏ8=µÄ;ÂK|…Íô*1æ‰âüäÜ7ÙŽ|ès^™ômªÊrb"(7çÛÕ˜c‘¼È_ iU­"èx)}m<»%"í6¡¡}Vlœ:ùC¦óˆ-é…JZO¯m„=E РϪXŒŽe0’ð—ù =ÞäJ?îLH©z¿»Íˆ UeŸôÄ`,ýè>-ú[6Ïü¿ñýOÞIºøêRù/}!þ½ÿ{µ¨LÄ.ñ#Ô´"›1Á\"êB²A²#ôw`Äuå\Ì7»ed!{ã’ÿ†ÀûW:2a²9F(Y¨¥Uéõh^ûmõ}¾Lu¨QNóòþÚÍñ > éŒâÁôtŒšÉÊ`°i\@ê/@? ´ `ßcî#÷O@ñü;g[¨úÍóUÛ_¼sÐ3‘0ÙN€.‹ÑšÁe^é¨Då#ì’¿-’/ò $u?@†Ódáóþ¸Çtg&eóhZÈߣ@`~{ÿ! Ió ß)êg"üà}`u0€ïÄÓ|»1H.p´hÏ!!NéÒÕ<ÖøG ‡ÒPSZ;ŠâPíts¸Ž;‚Ý7Á;Û¬ðÀâ åÒÿÔ¨h‹O7j^õh¼D¡‘$Ú¥šQ-Õ\¯/ËFmcÜNÙâÂSôty3º…½vtµ&ÈŠ2ñU©\}<Ôœ.ÉMc‚‰¼›ÕªtœÐ¤ŽnÓ •®´\N‰ÅÍ™„8¢Q§é¤ü‡žÉ%ÞbdÔ‘"{æ]Ì-úéE´¦ _:ªÝ¨ŽA,M²B€ùÌA¯D5­P|Ì’•-6¹B^0ã8T¸çµùÁȸ0°›Ž·6mô&a;h8(%_Ì«r ·jùÔBLÕïÀäbdÚ³Œs dÊv­’\ÒHG™3+–Ï•SK+¹FÙHCâ¢b8Ý5»¿FŸ»è+³F*µ®geĉÝ ç ÅXPŠ£¡©* T =ÒX‚*òßußøDìÎ2mçûÛÈ]ï×í® {€“ ›j55üœ\ÞÔhÛæÅâR(ë&}3V6Â}Î~9Ú|9í;”%FYž¯Œ_¡ HØÙ% 0ÁE[â‚Ýpýì²5°¤—9Rˆ½ø´ØÀ’‡®,™ÍV)M¤L‹Žœ1¸s’[ÓÓ”‡ëG2ÈX¼éGý{ bª¤Ž?æâÐF¤²äj`kÿ*šÅÜU~[Ë t žÐ}µ ZD ¬Vƒåw€†–‹ò¯ÿµ!T¢XMÀ^Ù 6òXØÆãÌ¡'$~©šÚÒ‡£kÆÀ!Ô,úSÔ†ckúÒ%«ôþÁµ/#Iì.&ös’8¢õ0‘{*!… ù8æq!Jö¥âUh âê'ZM¡c~MtØ…¸®âòà˜­=|”d‘—ZŽ Üïú;Uë@9!Z(¿ì`Óó3$!8‰¬üõÝh!JeGÑñd™ók‹ªÅQˆsE•üä=zzÐŽ¡¼¦>ô2ÿ"Ôª8¼Oj”mÇx­Ð·~?)qTiªÚÁÀ½¾×³#Bg(Á\ï\œÉ<þZü¿!ö¯uY·óžx¶Å¾sÂÊyÐìE6öp5:÷ +’¢HYÖÏm›ì±Îñ¬~ôРtHÛôºP/IšÏQÅ+i,³Šµ$Þæ?ßdJÏ´ºàO^fÀÄö&h4:z*ÛÖŸy|=ëpBÔÕ3(Ÿª›™„ðʲV…¡bê ¯ P!½Ý¥ÜbñÑþª,œ¯™â¡6ËÁt¹dÆ á õ3§šóÌBgÕü+5ZdÞß¾c#IÏÃ0ÍÞ„í*7s; á^ºôP&/d[ ²ØE{z[ ?Üù‘¨(Ðt+ù™¦jïNbÆå±/0«%Ü©„l>6¬¼‡vêGL¿Gõ®d=^ª3¾²?2Ê Ö§‚›ÇyÒ¢;H‚$üŒ<íß¾‹¡eÑ¡v6—:>_És­´c«Çþ«9¯kí*&›…9˜ž’Aﳂ9œç»Õ>wsêQ»Î4 ãЛls¢ƒ¶ßÿ­õ ~°€ìÿ6TFÔ¶·„nŒï6»²vª!#‰¤ ÐU¬T¨äQÒì)ñÒ*Üûé_£mlŒ'J0‚§”A½Ñ価ñvj¢N† ¹×õµj0-Xœf/le¶~žÊ¿ÆîìþàáŸ8°BøûKü¥Àét0:¼ÖƒD Ei&Aw+N”üÉöâ6Ÿ·ë÷9 ûîÐ×›ÅÌ{W—ânÚ†î¬Q¶|ºEØ2”ÊF÷H ÞacÀ0 5QÎð8òu23WÛ¯”wÐаVé¢_ùqUúиE¾qåmzW@[Æb€å¿¶ÙAVÀàn Êmq‚8G!Ç þ 3 ^QxˆCß‘ï¡Ä–ÕÓ>ÄPBi†@¯ 'Ò½+˜'6ã‘H`¤ ¶çmàë"Òœ‘bj 0/jšå IÖØã OâTŒ¯ b¿>ŠânÁÜîÞ=Ùž†ò¥ŸVìK‘¯äGˆd¾—Ë»ñ ¡Á¦ÆÇ»EŸ< _#ÊÍq]k…”§]š}m™øbG{=¬«uË»Û\ò_Éd™—Ø(Ô`m°F°PYØ¢ô17?²š‰@ÎJ„_©‰Ž(6H—8â íE=Wù5EV±»"ñkQk^eõñ”.3ùgȧ¨Þw+#±ŠÔ ®Q1D2 %5£} C}ÒFL<¯+#bææ2x¨uÜrÍ¿fð(ºÄ,XýÎiŽÆ¬Ãr3¯ØÉŒSéL<=žd0°{¤>ŸK Ó ï\ƒºÕܨÕ²ŽdHØ×¢Ú;;Dy–²&žô%6èà#¢ÔšŒ -èX “-²DØ!aÝ1D]¿–»º%x3{Û.®(›¡¤×¤_±Æ£3ÀEÍøD‘}z Ý~M´wÝÉè–NœrŠÁáõ:ÄðnÒ/Tàúœ5Žõ&ž‚vN÷þŸ›g›È–¸þ†ŸE“(mÛ» ‹õoX5ÞØê?~ô  kX ³M—X´‚Wû@äwo´ƒui(“£üÚqQeóâÐà ôH~úž¸ÀÞ±Žâþ1Ô^Q+F¨ b0GÚSh<ÖÖG+s²’³î^µÐñozq¬Ú?Óa&æÏ´ðÚØ%Ó{$p¢²IE#@Ù ªÓ`%/¿.ÙrI$Qöh®ô`Õ8-œÝÔ”ËQâ×óÞ8š%Tâ‚j³¡Y aËØWe–I.º’^0J(IUfzluŠ#ÓRt(÷ žbaVrŸ\—Û'ÙÕ|˜T¹ÐMH;Øîò/¡áÞaôá¶åvâò¢ïñ*Ô·Ís3ßä§»ò'eTŠZ4…:G'Ú9ùn¸éqKØäGò‰&º3õã¢æ'ÈA­ù¬VÞe²;üýϼOØZ׈àÅöC&†föÿ±É‰––vGƒïZ]ÿ"Œ¸!+Ž4@4ÅíI_YàtLòµKë£ã;S(ÑBZ½^ƒ“Šu‹v- žœìúIÛµäzÌ¢[èÌ# §»ë}ôrvB#“\ˆ©+éºÆòûØ“¡ú¦xJÓ‘ù4{þ™ß%ì Jk¥±U¯IGc5 ¸ø²47Ia½ô)ÂQ·È–xW¡ 7¢~ÐÏP’¼Ó”KØ(7mæFÛ0ƒ·ê*ÚRQÑŒ)²Ã!«¢§Gr‡ÃéùØÆGUU–ZX?ýŒ{šÙ FéHŸT…AÙíh}Ò=CƬRæC Ú}–Êø­¨õÓI8fK=v-¨¼DIƒæ¬ÔV¢Š>\ë6Àâ!ÇUÎNý†L\wWÆN´ÿèׯ^ÝÞNã›™Ý^®ÎάîNýUÛõ÷祡+WWo`™”«ë ükÛÆÊH8d< 1TÌ7pR …"ÊF -Ú$£1TÂxˆ´¾ÞGã±Ô@ê¥î9NëTÁÚb>l}PŠtmÉIêîµõ$æ¸9ÛЮõMdõþ±UöL-4†1[ĨÞÐ8°¾¬ÅTûª76š„mÓ}¾ô»ªñ6ÐÐÕ= Ðë^´ämpMn5]ÒåÄd ;»ºy‹é÷™b”ôé ð5CâúšæÉBaó}.+GDMä™õô@ÀÊÖà&<Åá5ÌYC_¿+Ù´'¶é yNÅ Òð¬Ó¯‘¤…‰)Q’F¨žƒí‹=ÿSÞf”Þ3WßäË Wú»¤U ‡¦6‰äH`Z, Ýv±ó~“hâF­óïÝóÒáã‡`€wÉ8® B@Ú¸¯ &8¼eZÓ`v³(LY,Áè¡úéâèC¸5cvaw$mµUY8eÙÝØ}o ä;#éûN¾¶>h™(j‰AÐgèPS˜Á'å¢:©™Z…^sáÎÕËîÈ£/Nì;+º>@Ú›¶éø¬ë'Í€ÆSˆvjfvn Ä@â“y§‚5±úm‚ ÿþªA' jI${5˜tv‹rõü¥.Q/Á&–M êUøŒú Qš?±…ÌäâxÈ2A!ÿKrýí´Æƒ×˜EpGTŸ^}‹àE?t1{4g›8ö«"‘ªG)(´¢K@ñˆN¥>Fhõ©)µ¡[«V§:ãSQ¿£HkHEG«Šr”g%(¿Â쩤‰R¿¼7õj!À@“„¿¼wBWf)ìÒvWè=ŽêSñEc~>ÕSäænœê‰#è¶/—©•=²¿0[zôØ5‡#XÈøL½…­E¿o3Š1%Q†´(Œ)ß)yÒÚ? 1?LÖ-  †Ô Å( «Ð $ñShÖóœ:aG1Õ|H e]´«BÙu‹êº'»r–Osº'â.6j×ñ·+a¦°ú^h Ïvu¡Öç´"åmüÒôë:¤ @Tj8U>‰æ‚ÝpÒª.usÏ fâMk€¨ñ Jl9·çÅM­ž„:N®9mhß5Ï(¦ „‘ýwõ¬m#hO)á­*ð¤Ö›ùÌ£ô@ž3-J¨vqáóT®3MBvU ÝgFaæýÆpN×ù¥zãœÀ^N¬AŽb¼edÝbüú눃j\‚÷HèÍ+xÇzjMUø¥À9' >Y¶þO›åÈô9Ë]BÞ€G11#¢ç^ãÙØÏMxMãÚHü‘€ÎkÍ•¨1*"Ò„·ntæö¬|!?L¿1/DÓCí¤X ¬³š0y†€mßàŽᴩ!¿Ž¼þko„rJÇgQì”O|©iÊÆzÓSŠëì:¤"Úã9Æ„ÕqLJ«ýð¹Ðïµhú^<ï P1Û¯¼l+pJª“êÆæqêîÖe$ÿAõ†UÊ…óµ ÈYÑtØš¬¹ ¿ BFGŒ UP*©ÆÎPÓî«%u¨$u(¢3Íêƒí0>E㬕3k!‰! Ë¢ñ»(ž@jGç*p¶Õs·a¨…ª©“ õo¤,D¨˜ãîF²{² Z\__Y±i”grÜ÷Ë…¤½Îyæ&%©Vð:tS­¤ûº8 " †_D[ù»D B´Å”P3«b¾.ÛeÏÝôÐ:aYhþó(á‘ÓP‡Ö“þò<öëñðÿÊÞ)X K»¶mÛ¶mÛ¶mÛ¶íïÚ¶mÛ¶oþMURµ[©dó<ó2§«ûÌéÓ“{wV!ëÑRÅ3Ï»&Œ-¥ÌV­p ý`JÞ`h5ЉZ*L¤IˆÞ}HžªÅ¸ÀW [ÇŒ/³:u*òÙçZß $;Pr€ Î6:JäºZ#hýÔVt†ž xfzÖ× bvºJ:^R ª ¾†™ÐûInø¼<<"[7­hÚyƒ è8ÖÉ¡Kì«0>–b6zn9ÓUã‚aô6"ÉP:oFg¥«õhe•û^Qi[Ôõ™>á܃fÓÒõð@³Ck5L#Q˜5õ)aÁËãû5V'} k |4 ¤šªSžkjžÈðX‹¡5Ô©Ñlpˆ.¾tœö]ïWÙV23”lJ¿+!­þ‰O8Øô˜sº… rš†²AH°ö¯Q—!ëÇîò.!Ý’êC¹‡!ˆ˜°±y›@á0‹^Cû’A'È< ÎÊË[ít¬Ô*dÉ´…QF‘ÙìTÓÌ&)è>³z³~9=€g`ÚÈ 5£B’èi)rÐýzº¹œÆ/ó)\‡÷~С"º‡ýõGŒYjªÐ ÏÇž£`EúÉhIf* @#^ø¹ÐÏ4™ŠÞI ­QTÜY°FîY°ò® ýŸð–H D+¤*Ö3‰Uùš—Gˆ à!Õ<¬,*ùT7ÞE·Z»Èd f2JN‘ëy©ÇYdTètÞýÇ€#Ï/n L>EÞáY£!ñæ.ÐÚá1ó3ÛDžÿZïå$×\a¿Û„ûT9ŸÅkî®Ñú±K6í;åñMLH£Ž” 劗Pþ>B{Í7 YÁün&÷b©œ¯ VÒ÷ªøSNÒ9ÁëøVw&{ xFðNiFú³ÍÁ¼ñ׸ô²O,#’tõóØ”G¤|†fƒvˆlÊ+0×2’kŽÙº „ÿ¥‚­"õÐÚÐ,-!ÏÌÙäD~»0ÝÌAaƒ ‘ø)˾‰ÐÞw$º°lÈ;8í=]ˆ8ØÂ$0,ÂÛ^‚¨Ã&W“ö$áy§‚:€Byh Ó­ÇTY§vªRzW*7˜ÝZa{ }ÖÚa¬s Ëæ+)Êò†CÓ ¿]yÖQµ¯ð33`¸ÍK!îÍЮ5QK¬CtKŸ®ï° fŸd€0'î­¹EÝ´–eb¤§¶çÜÅ» ÷ô†Gþkˆ-}Dð!ƒ²#Xd‰“òº“ƒ'þ~Ö96©_9ÕI%Ü&uS˜ÁÞM™A›N>§ò‘7,Yš ëª*’¦Ç¸+¿( ͈`'Éyã ã® ¥PÛd+PƒŸ[‡Eýy²MU¸iÒ^,¡g¡ÂÏÍX4r‡·Ij°çÍŽûç †¿µÞÞ¬P¾¦ÐO &L¡~8WÅòâܳîn½0ݜӹ ËÔûí¥U¡ê3¶d7ÿ$ÝÆ%ÿ‚rO£–+¶-7W^‡À¶ 5?¯WÉÐ?öôGmºÖp¿ÀšxõÃ~@ªy=²)qãàL¨ÃžÙFòñSM˜_œ[n$g>©û=‡fízgºÿ&{Æd`ƒ-¡ž×ÿ}; &ý(¿N楺iP Ï[=iÊ[Vì]op­h¹Ê–垪,'ù%_°6f—~Í«ËzZ …w¨r¹ÄÇö÷e»Õ™’`×nŽ”ùMÌyùÚp2Ù#°¿e%›Ž%`îR‘Pâ¡qqyâÞòí»rEÙÞÒoß¼fQ BâÂ4®zÎSÉ'ôƒàÐ~ ˆw»òرê˜${ïâ„ʳ@<¤¹/©\æ—ï{iL_ý"üøþ,¾o‘îbåmÈïª6yåU0"Ȥ‹ƒÉ¤d¨ž,NlÒà¬ÿ°Ÿ™ï{=Ž‹€¯èôpÂNe0¾„üËÌ%,sA©º#›t$O@MàžVkr„˜ÿÇSŒh$Y(ð,²n‰áfÞï&» 4*Ý C6¨-ÃÒKŠÂ´û¼h 5¦M•‡LƒÈ`;ÿ8l–²5š-àÚë˜2ŒÎ ]ÄDÑ œ|šö[ gnx’æfÎ7û¼P×Z)®”´€_^ ¹ùöávÝRÂÙÎÀÐbH™ Ÿ•+•áE½å˜LT ¯ù-—²ó#ƒ‘yl§Åò ¬G1[$8jr‘˜úïy ï«YĪ¥C¯ µº ”ð圀¦>58P¬t§•©è %ÌB˜ª‡Níiáû‡3y†ø?uù¬Ù×MvïmP@ J‡j•NÇKúØ(’@ÕÛ=þ(Hc¸{ÛE—ó°O‘…Èr%[Þ&7RFÿâ\»ª´û2oibežXåÔ[ô=ÀôØYõÚŽy= ¼H¤28YIwí½Zf5Rw‚HyܶSxƼSûòkVÿ„+§™G'^pzåèË ÿ!ÏB½§.¹h7eÝ)™Ó‚¬\£Y\#Ù†byõù %5éµ]„öïASE9ŸÉzü]³‡Hj6îŽóbt«¦Z˜ÖYÆqOl6\¹ë{œÞ\nZgsŸÙ=á/üÄ8*.wu…TÀ½ Úºqž‰Ià.Iø˜Ø¬ú2¢À%ù™8ñ;N¤÷-Ýa=!0Fg7ÏšeWÉ èj¼Vl³ßª<´hȤÞ×â_zK_1Dr ®Íÿä5ûõ€ïy%çUùÎ{ˆÞû×'áu¨N“#ôÈ] ѽo:¼ùzaí3.¤Ì—+hæÃ]!ôúI90q_T‹·*}Ú³Còæ~lÇ×R½ ÇßûAÑÔ¬*¥‚3âIG'ØŸFcËê±·éû14øG¨,n]'Ý´B(~3fJ­[>`ºé4•šs.³Ôg6*èTÒz}ÆàÌ1º¾oGÙÌëe¢¼ñ=°îq(Sô ”Ïú&ÜÉï©é¬Õ)xŽ]}ĉSšòßïw$}gÞöøWÄXsýrƒ#Á#Î-`Êmœ‚!þA×2BÞwèõ¿dâc‰ÆÒvˆË¥«í{èHB+ŽÁÅÂi) /êöåV°®øOöbkŸ±¬VúaÚßNZ·äNG”Gñ6 éY€Ðyˆ¢{¹(³;ýÁ|/m˜òty¹v-ÑÙûïg_=õ ówózÿx™Ûïv÷ôöÄíévW2zÁ9€mÏëîEüÂ}Ò^g¿ýqÞ6Ù(T>z:/ÅÚ¬.Tþ±Rõ‚û~ÙÍõ½@šä³ÛþM`Ž[†ø‡Ä](€Ø Ú@v2àJ¦®Ç˜`V% ­#bóû¼•O<Œ?leµpÜsSăqÀ ªÆð†ÍŒ ¦3v_‡¡‰Í°aˆÌ7‡ãÕTœ%Ü)ýÕ-®˜YvYt>Ë®¤`ji»›£ëJÝìÎ¯ä³ }ß ]fhKõÜ ówïÁ濉æK%lT¤b`Æyfæ1åÌ)8œ—ÉѱLÇ?^;¢üþ8=rÐ'Šû36T^¦ò®3õ=ȸý奕˜ŒÎáX12ìWr§§&ľ«ü:»š˜5?>"f#d¨E uZ‰jçc §è‚­i a2j»)€]Q*(Þˆ}zØé=—íºKøž•îÛ4Ýv4{«ªâ¿få5ï©+òÞ·ï“ñ3ž‚ý:yoÛeX¾õ~í!Þ¹£Oëh‡Š%ô`T|ùwogOvìÍv«—Üòîïºý×’ú8ê·k Dèîú‚ô.È<yÀûmNl•dj{Ý£Ëmh Ò€ßìîzáù'ƒúsðV Ì4@©|zÑ >)ÀÓX=ÞÒv߸¿Š3#íìÒ7Úç÷«hÿNù76ç—c¡CžÃß÷ºa?€÷û?¶Ä'º þ ¼rþo&Aþ¯®—ñ‰¶¬ó¶˜—LŸHR¥½eO+!gWQ÷±BÒÚl›i­ŒÜjiÀ¢Uz:X`f†ANv•”hÊ aøw€ÊIN_¢²“ÔI’‚t\ÇIR÷ÜÖNÿtB£3B²Jh‘ÀÒÏë×Ï /òŸŸÙmþïçz!¿òó_ú³~ÏâÂ?b$ÂÖjoqî×^ÂVìW?!ùõ/É^Ë»òá×”ž|´­¥W?1›¹{_a’rw5›9(óû޹ë<´®'#ïÝ^Î!ûÙ¨Іž~zr‘FŠÎ³áW΃}O%’3Ñ· õ^‚œ³Jr²ÑvòGÐp?˜ébzü@S˜CUhF×aû"JBú4F-êajB¿._Ø€Ö4³óÿŹS$F9ó ¤cO†ûèé=L¥‹·ò‹G+MDòüSÏ´«tóóF:mÁL”Of¼WÉíÝó‹3)dø‡oh݆AÈp{²~åm™ ‚m™@¤m„·òw³Q…z_)ffê"efò"Í^NÚ"Î^B;Ý“½¥Cm¤MÄ…lf"®©øáP¼V ¢O^ò^×þ¾sž>ø0Qô÷QDÝãôt—H{ºès¦=ÄÔÞªîÚRkmˆQްCVМ"}q¤+l£6@@9íé‘RÚ‚þªë‰í£1e—‡ŽXè½µé¼a€Ìæ´~ù^Šþ=Ê#­H#U–dZx58¿×Ä¿yuÓ®,ÝÓ¤=0ÛL´jÃÕ³ «2)¤ÙK¹í8ï™Ýþ¡Íj€u òæwyM¬ß$:pÂqÃÄŒˆ˜zÞ‡E@MÈëTâ#¡k9! :¢óÔ£n›ˆ!°Ö5„fn¯0q~ÉÈ*Ën`ÄÑhчn¼x2Ô¦jßOt¸ãÎXå9È…3AöZ÷.Cz§èéD‘<—๠\‘Ù™ŽÓ3Á¤vôÖC·LpÑœR;òó“ªmŽ•%ÕÓ×Òl©‰„O] #¬* T­ØÕ´Œ•Ùm½¼›±(€1ËìS uXÂ.Y÷!X`­œÓ…‰",•bmÇ:(ö«—Æn Émà¬%È×Ò.”Ôжß+×¶ìTË@ôjÍO¨uhI-Í’Rix–‘7âuQLXé&£u¨u þðqÚ%*#Ñ0n*éI!2à¬,´f³ÕM_útÑÌò[¹4Ò7xü”/zH‘xŒ“ $ÂGÁˆb½¡eî)¼hêÛ‹ªãM%Sy[ñ‘-– 7ëù<Ô[¸ƒ¯C[vé2:Y].g¯¶o¾oÁ[uD¥’Ä®Õ@ˆÂGZ¯lË}ÄC¼hÂbƦ¡Ýוæ 9J'&RV ª¸î¤ªäW€ ¢¨‚€„S$Z–®Ðo·cÕ‰Ûú²Ò‹QÂm¹ÿ¹ñ Dj3¼[†° ÈóúúƒiθÕõÜ·çñå·û{¾—áBÖLš>-.éÝzõíêV+!ȾŠpâçót1Ç•Ë3¡•t£ArÕn*W6§•‰‰W«ˆ8d·(@˜àÚcvˆ@1 ±’õ«N$©þjðµ_ÔÍ€™³ÓSÆ ’<9Ü~7õnÐ\(×WCê0˜Ù¯æ; ©3ú…Îx…9Ûí$Ú€áRQŸøzÚ‘$Ó -b(Z•œVŸþ¼Bg1TCâÀ(šqOåÆ9˜zúÕò½Z…üÑŒ ´º#À'$˜tÉèXÏ€ÕŒÆgpPi/^øãªaSè¢k(Eé¹˜Æ TEN¶GG•`£ßµ¯ãgQUtuiY.VaŠLú·e„ç ŒgL>¸ŽÇSN ? s®˜†ØïË Ãñ„5†  òŽŠníwŒE`Ê?äžß›9D¿‚|Ï~ÚÏ”4‚ˆaß?ûyªæÛˆðIñò'¹„/iâô²A%Oþo–ñYýƒŸŒek»Vžœf¦œ:ËÛ1¥…ã.žË ù½sü‘Rèë¹ól©™ÙWO.1ŒV/ Ó5²íÒØk;‰ëGd9ÜŽ\ô;qF„ò…Ðmeãl[= &M.VÂ6eV*PEPÀŒ+G“¿ä¬fšZ¡‡éÎtËv5n:ä6á›VYngn2䑤ÒhvÜ"’ˆ>s‹j ÁsQè à[k“*Èá­^©›«xPò•€ÚÕWRñý¾cÛ1ß¶=dø.±KC}þX0`ú: Û‚ãKPIM§ÒæÑmƒô€ÁÍub¾G5ä“ôX—aÊìK%|x<úëSÍX8ãþ¬Š|(IA¯=(ˆ"mL†¥&v!²ÒPAJ“qLjcÿ~O¡%¦ã»^ÈØÈ®^n§ƒq2÷qTm~|Õ‘‰„u-¥¹ú0Bmç 7³sŸ'¬«¤,„Ëô’¥¬LÆ|>õ<3ÃOæÃβDO:@j4À$º‘l5TT SJSÐ[qñq˜wo'@b!‚gÅÕÐ’@qqî34ÇV)m³.+x À,[4q0ˆ¶ª¾Ÿ?=i°_ºåú6ÅXÌÀ\u¿Bìj+)bB„)LŽe§…iÎ’ÅÊÄ “Þ}¸NÂ|‘xÝ1D8·!fÒc¶ÓŽ^ÊF–šN$ÒºÆA”M ð5(gt ô–ñ_ÕIŒ¥ùE=ŶàÎÅú´Ò„hyƒC4Ú(¤'¯ÐT›´ Õ?÷¯îÝ=ˆW¯Íšµ?IQë™$Z*œ>¬°?Tmµ„$З$‚^ÈG°¸Iÿú,Î/°ÌK]ð%ª0/v92Ÿæ˜ã{ǯ§¨lfÂŒp‚^‹Ž*¨øÎæÉ½&]"]Öä\tmDöLë‹ìLÄ·X";1çóÇÂ>smŸÑ§$À%W¤ž^ Åk¹LQŠ ‡ûf»‘\bC'#÷üz)R me„°óÎy`o”G’ ¼ôŽ­©$œzêÙmyšÏÌÜòõÎv£¡ïþ“£¦ÔXòcĉþE8vGòq:LëX"æŠl®ÍÁXþx/~‡âö[jÃ]£þQês3ð!–ýà i¾™º 諎 Q)+œ­¸î¨ä O6œ¨x Êùš–e¾Cd[â~=n +Mñl7,•Ÿk1²d(èTï`æpòŸº8îëÓw- ¥ko>ªé£ÒÏÔ§6-½ZðNù2Ñ3].Ì|6ã4r¶?<BpG³M2l{N{N~—ÓŒˆÑ~Z].SÞmýxT€ÍøHÙBåå<ÆÝšK:Í»e†Ø‡t;¨:Û»”ñ>Ÿ­Ã Ç‚„pÁ9U„òæ>nÙ_¸C%u²ùà„d‹îJŽÏç&žÃÝËŒ¹­¨•Ïq•Z{FÜKÆ5^¨$ÌCì+#†õP¹º4–×Ñ8³PœYµõl4œŽ‚‡K¥]¾@×&‰“™f¹lðk˜# رÏi©˜blRœq’þœ4]ìÍÜNÈôÉØ`ÕP?¡Ò`ÄÄØNÕ.¿£ÆÈ5“D:²LD’ÐUe]‰EަúÜ6p0Ø©‹˜%³{k;[²îÑîdƒ7;8²o4¸Ü²g[å<Ã,S¾>¸†eåL°O‚ð¸'ãl6¶¤qÊÓÆA N°ކôOɸ•¥Kð. pò ’äL?~,²TF(OzøPÍË6#’~¼taßï»™®¶û?³e®rh ñì »˜9ª#\÷0=1ù8eÁjâLpASÀ)ìk©_; ˜AŽ£9»;ˆ­ÜN[ŸúvÖk\º¥çáÞþõƒûàSλ`9ŸïêzCÏ"þÔ6þ¿ƒ,«#’ÏñèXíù„ìÚ¤GK«~qWu§©3ÐIÖ¶òæÇVmiƒ½Á?Ö6GU Sî ZC)"˜aD-Ц)´Ù¶ :nM¾CÀÂìwVIQ¯”þïpø,½´}ûÏÅã“õ۶üãê”ì´Ñvw¬eè %VKÌ+‡ðcKPõ546^Œ*ÏÛ¼™ Íb³ìNïÞ†Q"ŒþØ6 —r‘…²/èBö´¬Ãö"Ú16 µþžªÝ9 ØÒ(ðä ?u„z:Åsií­†3»Ù¨á >ËlÉÏl(Z1ô|(s”Þ‘d7¿(”|…Y!ÓCFMt—ÿjÿ%{v ŽeO}’J…\(Éž…1Ñq†ÎL3L6ÕY¢ˆ ê TîÿTE®@G=zÖˆª.l¶Zx¼e´E]í8| í ôúæÊ$ü­ õ:Ù=É=Ái!ÃÄ·t‚Ja*§î…ÁšbP(£>üöv}¼_Dä†i櫳 ¢-)a­¾E7zßf]èutS€IòcPC-ÇfI³»Œ…|ñ Š®kŒØù¹ÌøÚôPÔos›pÚ¤ÈÃZ¨;ÙhϬ¼s¬1ò–[¥@‰%¤Ÿ·n¼p;®ö'Ñl‹—)V]QìŽë(} ·©ûýž’é||N —Eêïô•ÊaÊê·„vr!µTp8ÿŒìš Og”(7 ¥ß/~ uê]€‰®/,@׿ërÏMæíê×I‡`² –%ù‚^éL® ö¬ú¿#×ÿ4•]ž÷’]ç‡îüæä»ñ•Ü¿÷P4Œ½o×¢!9UG…ñⱢ߫¿º$xeŸò2­tðA±òÔü›ô“s¦²­y˜hGÌÚ3~Ü™É2ñö+ˬ=Ò¨\ÿ@™îüË—׈ÿR=jâ÷Ã"Ÿª¨Ô·AEtê&¦ ×BmùØò8'HèB£ä2ú\¢”ôð'„~‹@^`Ät`ç¸w\5p˜óEÁÞi¼*3uŸNªá Øî„ß‚}Å*÷¯:bÿwÄy*Uï°-¼‡E«™ë¦ÜØŸsXëŠÄ»DÚ6Õ†Œ|b5µ,8ßûø?ãT%UP!ì«¢óú)¸«Fîs-1Û·2`O¡[§\À¯a æÄ´Ê m8Iîú6©R]y»¼åÆÑ0Óz;&ÇxJ.ã^ËE"2b¥$–ôU3t/AÛÑW6©F gÜtÐÈ­£š¶ƒ˜LÜzͺ#Oqoc[¸=­Bãj®ëmÜTÈÒü¶DE‰@mE¿°NìeärÈázð¤”ØÅ\ññìÛ©ÄíX"›ÏD=í6&±°°9œ)[ËêSòHR*®T™™,YZ³Òóoø5ê|-(OþëQ š¥WÚr‰Jþ³FáÊu¼yGðPB™+†+ÀŠ?¸ÜZ÷ ‹ ŽlѲ/QtCGü”…‚NŸ¤ïÇKÑ4…kþô™ 6»IÊè)†âðî5æú0ÑcØ^îñŠ™ç å—·P…¥ð£ŽÓ¿£+Àü• ¯yÈ¢Ø@aAºÎ¥<Â9]ŠÉrHôCx~¥ÿ+Òsà„?kH§ÁoÐÈNûŸƒÞôî+ô—¿Ôµm‘ÒwØßä½ 5EýÞDP§S¯U/ëÒµ¼7ÀæjU¶5•2ߤp÷LëœN~;àÖúHÖC,ødfÆêÕ¼_ðìtî)©¤þ°¶xÎŽÑ$ØvfŠg^:¨ÆUï×ÉìbµpIÓœ%„Á©`ž› åêàçÀÂ/tò8 ëN¾ K¥Ì°ª ¢¯€{Rè]7UÙº!>$ȉ3 ضžC€Kä !aÔa¯5²cà‡š<|n:^H‡ÿsMü‰2U%·}•éÀ~»3¿™Ø69-bkúv·fˆJ†,ìæ¸: mXÃè è?¨ž<Ò5÷—Gòãܹ·éÑýº1:Ô2– /‹&Ýî⸙ª£7é{Ó(^‚öC³W=RC9bî|[·IÄ‘ÔFÚlîÚ5ݶÄs= ªÉ>”\4N={ŽÇóÌÎÆï;É»W;fÿu•ï çØÓòpùö~aü·ñ‰¼³1šéÊ›2ÂW->xºÀ}|azûT)cj™x¯›ê2&^IaN”±Å4"³Ó÷‹Jg<öAµh ½-îš)Ñg€ßÃTJ¥„üjÆø0!M³Ÿ<Åõ4˜1pÎK¤/&S–V)¯c8ž"@~zîÇ«8ìÀt>”fýœ>ö$¸ŠÌCTÄ<ÑÎ* ]Ùu]Vž{1JTÚÝkÙ)çüÈ}5ÍžÓ–35K´3÷ÍÙ–û„üli< I.£Ä㤱½TÈgp‘@¼‰ZÖ[ÎIYgë¸Ù7ñyóCiáRSv`oóœÔ^/èÕ ëר&=q’„ŸØy³nz-f.Š™6YÕ “lÖ…|â|ÆZ ¬x‰à9ÖY+W 7¸¾3^\ b¹ÕS Ù°Éý؈‹’t¹|g±PÊèç¶b¼@†úZ½·íyÑúuËî>ƒI;+ÔñÆáÆÔD·&€h( æVÓVœ¼9ÈÈ´È'šO@Y@$wceÝt¹¼Š¦ wë{t€FŽESwÙmßK“×­!×ÌmªEû-ôª¼”Ëd‹[Ûu‰/‚*' .€ÿGIÁåê¢2Ü@$Gn_Æ%ké: êhË!³‰È|÷¬Ù.L^ M¿¼ AZ¶Í_%ßû½V=1èékç\ æ.ÕÒ¹7² wÒÓPªy%Þ£•¬®TZ·åݹsPXðŠ6,Áê¨K•ÏB²&}.ï›KÏ^áu&'âß ä¼×Ðòs}2êiÞè8»qüï'ãîJ ¦w.nû­KSEÉãþ㊵S@Ø| 8½ÿî\ß ‚:/zÊ»ý;°^Ö;‡ÿÍ/pÊW‡ÍA0÷¤áË;D: z;^ýЉAè7ûî_Ánù”‹±›ÑëÍfçê_Ýì,ަ‡½›á/ÖO~¾oþî =cËVû÷,Ôžwt/òéÁ{ó(_ø:£OV~])ŽÿÓ(Ø®ì{gÌÇÕy€ÌJMß5@â÷«a·ëOVæJºJ;©ê÷ù 1+s¶ø@ðÔB¥ zÖÔ֞Ϩ›OgáéŽ1–ø×&£1Hüìe‚9e0ñÔGˆKíI|³­Ïtù8aš§ù±¾q£à«kdRdôlæh3ž;ò蟛DÑcâÐ… þÿì Û˜:ýŸ§ÿåšP·ý/×Äï­¾£2Ÿˆîe”Í=‚æa³w΍6D³KëÆx‰”Rj៯x”Ÿ¢ÍØÍ%|"Æ.ïÛ6ô]Eò(¤Î„O廋پéÙ5ú{0¹”'cs¡SeÆ‹œÄ}9K™B jæ é,’×{.Úw¨T2Gw ‚V`¿=èŒ>¤!%tF)„eÁ¼‚ÎPˆ9D»{?(‰SžyN$Há±Ùé´GBÁ®7'[L æÄYs–Í s\ýÏLÌD×›f¢³ qeÙM ÒòÍÖ¯ª=ÃeÁ ÜIí¸‚µÂ{é~”.}àÉ0¹¡±QfR ›ª„·˜VoéaX´%´Í´TY‚‚R]ìáÑàrP‹‘8Ÿ=´B{FÀ¾È"@Ÿ~Üg1ša÷Õ^^oO¸{º»{zð—Ÿ=Èw;\Ý×ÛÏA€»§¼ýgðAhÔô>‘zâ1[LÆfF,‚5Y#Q‰³Ì{ùqzšMCª¾"¤¸“Šs-ë‚4$鑳ë¹n9zO• Ö¯NV&uðÓ™v'äÌlѬs’ g%Œ¬ÐÓÑ ‹­‚M ý®ëìܬT|Ô±©—ŠØÔPÓ‘“§€³J ¥(©Êt'›dçìôýǃDLæè“ºøˆ%áZï·.É‘ (ŒÈ™=(ï0HØQÖÄEJU`ÈÏÖÂýÍXæb¢-8zìëO6Ê L“Mi‡žOrÕ@¨¦eݼ£¢“„¼U5v)‡¡¨Y?v_]ÚЂöÔ9åOü&Ú¥*unž’+–ä蹃¾éør­j‹¶!l÷†-‹Í1˜ŽòXá¨ö¾gÁîªb…¯f¡EíÒvĒرÄh+†Qv ‡ùrý~³®ÅÊ*³•·í]“]ú´ä¿¼º¹+‡ü 0(쥅[Õ@Ô›½æ† œJßkñ2 £že°3K ¯8 t’Ó8©ö“_ñ¶¹‰.ZÔ›Ö”r°)Ìi<*éTá0Õ†U„}ZÊÓF¤¥šÁünc'f ‹ôKZح/»jØ{Ñ-¡âa‹’Õ•Ø”&¨jïmÁƒä·júà[¬j#5ìs3ïKžèH£1] ‹²inæÿªŒrnÿýf‡v‰ô Ô³·—ç¨U°—Hcúe5ümCð*EÖx[ÖÙá6<åÂ\w ä­’‰ûuØò}麻»¤ñ9£üÀÒ†ß6m¬d›ï4©ŸåÊ­ýÉ»¢Ÿ‹àH‘ ,##Z~b0\ˆ;vfwv~„~ñh¬ËcÿÉçoŠk…µê›Š2YøyÑAÆN†àNÎæ_Ú¬¦šóW9ø_ ®‹#Šä“¢Œª9;_)[#RÈ©"ÚêÖ î‡=ÔdÑ»à-hôÖs%íܰ…j—Õôÿzjžúÿ÷oFjž¿ú†º‡îgŸõš3ë‰Põ?ÚóÂýbIo%m6è^ËIb3§ŽïË:š3,7én ¿¿ÜǨ¦ «$²ÈÞN•,e *5RZÛ³{uÑØÅ%@.IëL ¼8DG µ$6Û1)².´³®ª›•R¡u?š‘¨ÚáØxX9¹{£N ¯dŠÝL3”xGa%ý‰P¾ID/sú&™?œ0ØÁ9î!„ªÙöVfà—¬ 0²í` Š/HµÍZ)í°³F¥¿ª¿’*£L¤‚¼'-©¿µÒQñ¨‹ *x¹`dÈÒ/„*G*…~_tÙ’5}4žOy‹Ÿ¨ôǬTßXnKK¿¦dÅqLåõ¦zuí¡¡w¶Æa§ÿw®þÆnu¿Ñ}ÕÃlEóÚÞâ&3%Þ°ƒÝêuˆ¹áßãšž¦É˜ºX‹J®+Gq=ЙS.22mõ=FÀûc×QÙ‹ûD®¨O¯àðßd ä7zK6¾ÆÃùˆ nD}̰Wž¶£¾Ãïé×¾Øî¿,$õp°r³üwÚ.|ó°%©?×ÎgBÈ ã1gdmu-$Ðly„H,»Àà¯SojLK×÷»ÆÐy‹”U4ÊKûå )~/ øÕfÉ;˜Á³û„ ¼ðì %ç¢\.ñ[L‡¬ 'Rm xÒ ³^)àp8âh Tž¿ÁHx1@]xìãótÁØ#òî6å\IHµ VP)€¾¯"\”ƒB¨\æ åZZ_ù…ØÏCVžæ\å|0ˆ~è+˜`8z/$½O—þFòB]2br9· ZÎ ŽLCºžH¯ñ‰ŒãÍ"^÷êuÙï1Ïô#"w¸A«çÁåè »ºí®f»³eÒŸÒQKZ‹ØÀ}qá01\ø'+ʺ¦4‰õòŸåÞ?#º§›ˆ9ècÚÞd…æ†(~rÂtÅØxãm/M~|žÞÍñÛèÀ9KÅié£ÍÏÑÍOfµõu% oÂw˜jõNì}Ÿ¤H†Ø¦¼&oþÀ«aAèžÙeibÆ´õÌb±f)çÓF #±iwgeÏ_Ï×ö”£íúRºã„è¹–- #(ÿ¦" ¤è"ª(£Í TWFr:ã+Ͻòú>ùE·à· (õΆ=qfÝÞ1Ó®¤G•ùaäC·ë­c®lÛNMçͯ+GÛÇÞ‰M*Šãkh’C`*+•m\Í»QðÒ–C®ž¤Z±ë-Ó&µ&þ§xUrPÑKÇIxÀ%§Ý)Þ,;çþF v÷’C€ç¤á­9,ÕE{³ØZ³¡S!P ´;ÏJcd$Q”nÃA'3 ý7Mþê-uɿljOÃm;㜥ïwÓÁ|:x¥ø§ä§_`ð7’ݪ=þÄ$ï(òÃëÅTw˜©ï¶œ?úá2 …–ä•=åW„³ ›Τõ˜¹Æœö9óÒøú ¤1ߪwÍ㹜 æßÿaÖÆ=^Ô„þߌ¯Û;™Ú;ÿ—ðPS´?ä@ð;S9V‘î‚/W–—kÑEÝ¡S Ý­ÒRMÌ–‰øyÓ”[¶mõ÷› ™¢{“ ñÀμ瓇yVá ‚®TË¢_†¶›Ÿ÷•ïQRšY [[b–&½ÀI´/ŽWÈ€!Lh´l Ò`„ø³Ì üÝBÈÑ /Wx°ãUíýí©1äÔ DH‹6Êžp¨O¤‚Ö qJu$#AÚV ¯Ì 9>â{#ħõ_†mÑê’H`3^Ã7ÉZÕ³!CÍ4À'àÊô¡ ¬ƒ±‰‘?W¡ž‰ÜñÿJ ”`-ýl*‹Æ‹4•JÀZW"¾Ñ¼xOËÒ¦KvÈJ•!öŒìhÈâGk%]ÍÂ=ÙIБpç Ÿè†ö9KþðñÉ£©3w‡~çéíÁ•GO®æœ¶;0bëÉ¡!k¢¬åë÷ð‚÷—+KHŠÜ^Œûì‹É5sF Q ‰Œ ç)ên4)õ£¥ÀU¨p¶Û6Ä*BON9Ûþ´bø’ÕëÙ’bf§Îþdz?Ém Ë$J˜Ø§áð‡KLÞ b¹"µ/d<*ò ÄSí@4ô4J6Jë7c‹–+2õWÀ‡4’ÑHÔ¬(ÖQ›ÚøžÝpçR• ©r®ù½l;Ë£&t2m3\gÏ m|C!Qñ¿ œì ‚ýõ€.íO†!¦Lš_ñ­ÀH}‡ðÑÍT¯ü³ÓÁçL²x(L·š¤\ Ãv­[@Ë*[ökqm“”µP#+™¨ó͇ÙNX :Y¿aNû Ñá]rnš×ƒŸ >=¨©5d©z°U´RÂǘh™zV0fã´[oQŠX’¨¥ökÛ±>ë…fc#)o-Жúqé7bnÒðK§Ð˜&ÎVU¨=JÞ”ÜÆHá ­x7_38Z;’¬e”×+®DqTæ˜YX®Nr¼ÂÏF¾BHÞ|àKWÂN#Ùe†ýÛ ¹èÌÔïrfeòâÚ4WÓwYñP†,ê^sìØÁë-êØFѨºÔ€þnWú‰UM²~OÓ ­Áº—e´Üµ1‹ÍGmi›Wÿ|ƒH·‘§úÛ»·ûö@å_vÝÕ]òÛ#·í"K-ýâU¼ýBî!Ø•¡û½äaúÚÊ âªÃŽÒ›Õy ¯ƒ€Äm‹òn¤;„Y6?»sîuÍŸÒ š6¢lDEýüެ¾4…®õ–ß®|øŸ+Íçkg¸PAØþ7Û\þo2Q×vÞAÙM5!uÊ‘6¦ƒe ¡Mg.c–1ä ¤d.•Ù‰ÁÁ»D/Ô¸Òˆ£ñl¾*+œPþ÷?pïGþ Å8ËI5é€Ì4,ƒkmáó^ظãN_IüXsz™ã·ûÙ6Þ ¹âÓy·zwDc\¥5™ Ãβ±h¨ ‹¦²1iª›ñ0`2lâ3Ñcr›]_ºÚ]³TÐg÷Ù„…+Ò 3Sþ‘,ÉsñS2,©bx$ ½E‹ iïé"€³ô^39iG&ç‹UÇØ˜u ;ùV7¤—]uÕ0XRI…&¡i0H Á‚ÆA2pL AE£I¬Ÿ ‚´ž¾aQPˆ7ºüÆÝˆoRî7-èé‡ã)è5'’@¡‰ ¥Ì©FÛ˜7g“ÂUkBÌØ ‚ªâ9¢q˸ ò™,v XU³ÿ™a›‘9 /"#^3)„Qå68‘3\o>éšo%c²ÌR#«5ÞãGi°zMj§ßëàðGýÀêptôò)“Œ©Fáò¸Ëþ GzózØØèAwŸ¥ÀxÍ»å÷Y­EÂB–%r¥ B+|èf€K=«d/Îö„¿ O¼íŪÀ’1áx¨Zìºâ³ëË—‘…ÌŒø –™ ð#B-Bzhb5r@1þ!Áý¨ ü-ó.ø$Ih]”ËÂÆèY×µ€#3Di† R¨2CM>Ûšëë¾Úùé&…ƒ×þ¥OV™§ ´}IZ€˜N‡,zVJ?}E.h8ñ®H_ëHöUÿ(;¸75N¥Ê&@½zj‰H‰ëpùQJ—eœìä…}lf7àn\b» ,ÈXâ/ΑÅ~€í‡I«34s%hII4O\:¥¹]¯Lø˜/qh¨>ûÁ‘’Ï“°'•zë ö9P¸ŽæéôgÝn¥•;÷CõJs‘¹ZtE€žsœb­Þ¤(`ŸEÞ¤WNQ‹k¬S„›`u( žå¼SÐj>A£ãL”Šñ¢ÚLš”_I»ßjP“ôBQ¨zÏ“PΕW‚A!d ¾ q»r>‹‹'<7]¿MÍÐÆ´&6÷+žÆŠ_â¬L¾|…¹SÑ™Õß–µ¹7)‘XŒpD|eüçªT°„¢áb…TÝ_‚ÿÒ×=|Ú@6ÁÊ´¹«L¯—ä¼'`Íåuìs1øëßö!eH~½k·'Ft]Ca§€Hnˆ¹¸õQ¾÷/êÔ;ÖμzSwÜ¡qƽ¾ÞUËtÿAÚUP±¨JÕ¬ÊxW!0Aà™î¶fèaðC2Ûâ÷âEÓèÃ8RÇìïñÍÕ È?ø~Gšu›ÅŒÆ9žh„”ҩ0‚§þLŒ§ÞúìHD^eÝ à$«Ý2æ’Àÿ2ì~ªÓ¡VK“;:tÚ rÂ{bÓõlëIÂ|Ä:Ÿtv×j& Ž·¡(/Qà!(N­l Kæ8Gñˆu ΋èÄ ²ê–ÊUD’HW­že>©®—oDj÷ÀN¹Õ^7Дžµ&opØ#ŒñD‡gzCcÃ7x"dË7xºˆ±×s® +å)TÇÞr¸¬N3æ.3®ËŽ ýûÜؤëÉ5(ýŒz-[ÝÙi䤵µa"•S ”ZJI½,öW/"dþ™½&Òâl³Â­¼±À÷“%G¬{`ÿÈú€ÿﯦº¸ êCðLL, ÿ+3¢EmÁn‹ Å÷M]‡¾E][.Šƒ ##»$£aÏÈ\0D«3m]‚Ä]Rô!ìç‹Î”³™Äªs>’/ÏÛÃþ¢x1Ǻ‰,Ó n7ÿ$ßDfó2õ)+$YË5ã]q´‚†L¡ R'ŒèÔFÿW &Üý€Ñ…—À",hwnº~•ÅVŠ›‚–)´¹>‰hwÌG´N†.4 ‘ꢱSF‰iä,Lˆ>×à?Rù |….¤ôKþ4‡®º J”–m9î vŽ_n–¾üi º®\§§t_Z²{ù+Ä·A£qRG¥RDjÜXλU’\uüN“"“4q5Q'í¶¿¦–Kï‰/à *è‡ì `7þ,Ù3ŸÓŽ,Zß| ºS_7¶uZ³-¹Öèd3•S³™»MÞß&ýO]rüè„÷®Ã‰P¼nžjÚ5ë± z¦ÍþøAnk~¬óUdžèWõ·oÜ‚yú‰ödhH]ÖÕ㮕¿—ŸÚL´Ó#N^›Ÿ]Ætr[PùÛè‰#[ß[4W.Zî”îƒ {fï©õ=ð°Æ}õâ9ª’ ª$‰.ªí ^à©ô¬þ+#| ßìÅ Ùƒà{^ñ””!4µ½zøÂ%HÇÄM³„;îî¨(¤Ž¤Å™£Dɸ«.:CÛ•á½(=Xˆœ#©r1$„s²JÃé·ü:mB`© \‡þ\8gGL‚å“À)º'—êª9Ò*_ŸÒ د[“u/ú¤t¯¹MåœJ¶-^ó6õ–ê!®â8šâq µÞÛ^Lð~ÐGç<E7Ť!ÒéØïHL*æ4Ç<Ážƒß4:ÚÇ‹ÍÙÖ$dªÓ>Ì=è}RŽ·öo™ü2Ü!ØkÉ;)C~[§$k‡íïʸ‰ø+l'†} Ÿr üÁ¢íõçmÇlË¿á·1ú8›“á ágx¡©kªWX /è sÿîÈ?(ÁŨ©<úò?s£úN>Hêý¿“(£ø’ÿH¹cøÿ ‰ú¿+ý?$ÊÖ~K ¦Œd+r©Äñl§î% –j®•h=ÀŽ¢Óè‹ôضÿY4¹´‰V<-¦ËYïõ±òËõ«ù¯¿>1EÎM/«Ht[“Mxá7?œ'Éñwcóýë_¼d¡ç»g¾j‘v€G©] 9æØ['pžñ3”j±ú@5V˜Þ̹õ~-¦/n·kñÚ@5¶/0Y›A#änYƒMà ›LëÀÛhãm@›qL×è:È¿ 1ì$n†`{øšqÀpa¥@~@= ïëaІ¹U-Ëéê—-k Éoú^¸­qèsZ”¹† òOÑ?q\òÎ4AœúÏ9 ÒqF«£3̆Od(@ÐðvpD‹†Ã„ðW8ê•An¢ÚI>M¬KëbË«oÜm¿®8éðãÕëÍzÑÝí =ÄDXälÍÍBïü¡es‰ð*^)¡s¡ÎgÜл1Ë_}õûSpNôO4*d–H ¢IÊBûT‰0[ i!^qÆŒ1Éel‘†Å¹³òÜ¥9øïà1^F¬— ý3¥zVŒH•·A®™E½²Óq=z#®®&æô}žTKäú,5Èf£6çähQ*V –¿l?¯ŠŒoæÒ–eÉê¥L“àY–èáôŠ™’s#²E‰Cé©&¸ÎTPEòä J\‰%«JØÁ‡Dú’ãPÖĺQ"ÉqCY2NùÌaû H³ÑZL!»4TÙè;ŸçYu!¼±B‰ìu™åìE—xãñêf:%®ìþ+%³v½‚-©ÀÛE«öž¹x̜ܶ±ƒŽÄ¹?H‹ä@%ž9B_P¥)Òqér µÛcµ ªgI²x>íörÉ07ÅǺ—6>'ÎÄ' ŒbU0IÆ™¸Ð¾æ¥¤ö§M[™£EÍ×èê&¶<¥¬ùgÞ’7“§4}ñî8ãzHð^ 9‰FÛ½_õÙ6JTô;.8bA—ž,SFÓ]…Óe~¿!üÐÏ-®mëy:Óî´¼žý;eGê,%,P6Ø>Á®8‘¾·Ht¤É]’AŽöÉë„‚”û»œàa](‰ÏŒµŠMW©ÉúÙŽPY€Þ6¼L=^"ï(t±ƒ”e 0t3à­™ð9F|ý¯ò^?oûŸ¯ò–ʤÈ;«iO¹PSr¥¼:xÈX¢ãQ@#€ ߆¥šCxŒ†¬rñeªmRLsø‡"õáäFЕCx½áоÚdõ¾’;á¥d¡……(&V©'a#¸õ9úè­~ÞÏ⻥Ñt¥‘q†dån{áî -ï;î¢^Ð93Ж÷^  oÀHwD+IC Ãß.׉ëìåÄòþ¸çÃM ³DqE(µ43ç>|¥¨‘X4Ü+¶4<¶›:Ém™‡üJ÷\Ü‘ÂËäjjM÷xZÀ”ÁMÛ'<Ó±C +ño\b&Ï7ÖÏÆ·ØèÝ×§znÉ6,HIø³´5h«òz?ÜR®•g þÍüÙí÷é§àÿ´ r&8õõú ÑʳÔOÇffXù§Šçcÿu£(ÖüãF«„fvy'¾vÔ}T´(Øj§ÖêZÆÔ‡6úÞðÿ{ž³ð¢P:€ÑùÿÍ ]m\þƒƒ7¾¶î[í çîP*·z’‘"Ò×›®ùc 'Ãå‘7ÖÜ]7Gí<(’š6$J'Ñú;~æ` $æKÞ^·Vk‰ 0f`ÁÞÖÙ¸âSgíù¥6óWøõòØâ’,²¢Ý1Fä°EÖØP~0:ü2>ñ¦…gc4¹×í÷ñK†ü_Ž@#ý— hh걯ñœ“냞f†)4€" õ– o™ÇŸS) „ÎÅ#Nx…WMc¹ŠÌ@ ¿T"ÉË‹p)úƒÿ*µâ8) cò5³èĹ¹ãããӄǵ}ú˜Ï@“sãÍÇŽž®Ñʨò‡:òIh^ÿA]\§sH÷¸ûny)Ìf}¯‡ºX„|ð`¿à„³x¹àº $ ’J±HÜæÍ[¦ 5γruÙ׻Нš®àúßO®O‡Fΰ_§VV>f6Þ³tuŽ¢Ÿ’›²›¢ß]'ãfÿ&}Üü ù ²òÓá%À$Äö„³t¿zÃúY!H¥÷`Ë¡ î˜ÏvÍß‚ñÃçq[ŸDö ÈòÀNˆ$"±“¬ÊiŠvQɵéò—0~­kfèÒ(2›zW¼aIÛ:(ä6†%¿ ¿O8+óÁ õ_8bŽã+±Ëœ?ŽüðdßDÙäƒMq:æítøâ>l9ݕΈþ~Î'þ9îª1M-„ç‹ÜªóÏø—:ž¢õå9‡\³Ä¡A$Kn»|œOŸy’çr¸åEëÿMùã…l ÏsËм‰¢ ò¤pBù¯¡äŠ¿½ øPL÷ÕRÆýHÑ䘼ÃVOÜa«Ê˜ýúÐYŸò³Ue}ÊWÖPöÙ;z­,)€Fø;þ`RÅqD©«å˜Qò¤·0a4À•w劽s¾$§®pöÚ}wC@sŒ½ ®<ºÜòƒ_ƒÆ(?èHŒÂ„ú}bŒè«ŠÔ% 1Äæs_çôñA+óçqÕù_ë…ãzîRNó˜tWÛô*ºG'¦î„¶1hr¶´Ù|]>=´øz…ùÛºÛa°“sgžñ_¯GœÜto\Ñ5˜KBÝwx¡c= ZŠ4ˆšbåÒªý°¸}/Ó ^¦D~ºkkÇŠ_ çƒΚ«HÔEò;ãYÈÔC9ŸÁóÇK-õÁ‡w¯£Eñ$$kØÀpšçbYÂå¼s`7qBX`(¥<ú¡ê0˜V¡4E§ùºy:?ÃA¾>½_b 7ûzº83÷r?Žð-úC&ÿ[yߺÙëõЯéR¯Õþ|sÝòzËn°mõ ”€ºÌÒÿÒÓü~8ÄùõÞ kШÞh [-سù0ŽN»ñÌù¹5‡úsU:ælcacÿñf7’õÚùuF2ÏŸ*ÐÆÏi››U­‹­„Ï݃ãsøf•E‚Àd=³® 0Nj ¦4)à:n^ºk­[,ycDÐÀ™"n€#T·ÚHÌ2j§XFÖEdµÁjÖ:t±8)ZS™Eß´S8;)`Er1BZ¸/zá¢ñYoiî_ܤžå•|ÊÞÇfx96oÌJ'l¡” æsÊ Õ6î:eÞH %î »3ghÎ=ÿÀæãa¹Ÿ‚ ø·äø¼± Lw~û ežäûá¤æ`Ø6MaËÎår9£`p@KÈ1"ÈMÒŠùŒƒÒ©b£G÷£HÌ_¾¬Ë‡`ü½ÒŒÚ=®Ä¥oWù8œb ÓLÎös M@ O“ Ð@K.qÒõ³;@‚±@ ê LÃhLÑ“Be…°k˯&#AöˆE®`ïöJè~–wõ÷qNšå>–l¹.Ä(àOåçþùO/ÏÖù0ÿ|ÜxÄŒ|Ç)¹'’rÃAA­ÑІ&䜯~Þ} óÚeáɺìá>åÚîða:‡‚×.ººv~tï Úîº~z&J uÆgÖíÌ’[Qeo»”¾[‚$l»@³,6éj¨¾-6xZ Á¥˜s-ù¹œä²½zBèQ Î=I¦Û¾s Âq¸@ʸÝN.¦“•ú€r²yÞdÜÎi|(çð*ç´û)5úÁnåÍÓÝû´: ¤aFµ8XW}Ú´VãÌi¡Øjlç¶ ¡¶b„ƒÖí80$N#¤0ÚÒÖxg,O©õm[$àžáa2èÁ.R"wúAO«¬DΓ¯´yx—´ÀÁoÜŽ+ùá“ÔBnÙ·Êe€0„ΓB¨€doJn+<òÅ„ ÊÂÊLüeÝeÎ[h+qÄðŽéïg¹õµYƒ£ulD™óA/Á`œn0 Nut}yŸÓ¤dÔŽRÊò³Ü#zñ €ÀÇad $­.¶hÉlÈ鮣ßámë埬.··äª,m˜ùz7´uŒÃdD+–ÿÅx ®üœq ŸÏ+ïÌNM0gø9h­3¶$'Kгïg*@”ïæˆO„(ÇR’ J6ðÂhéÊ)à Þ.,o‰dc ¯‚e€æt”¥ã¼2v3@ÀµTy7RˆHÒ#SóG/¢G$B¢qT"ìËY•«ŒÚûOŽ ùÌuÝ…MOfl´Ã=hÞ¨¤DÖ†MuFÝhèïßÇrïá² ?Ë™îát p÷ç øò°Ÿb‘*s–!ÉäðÁgì¨Ø[{b“hÊFÆv…B7t dÊ{=KÚ³»àÅp>äì€wìf)ÃÚËÇ]#¤µ Vn#i™Sц㺀tÆ‚ÙÔ׺ ²]„ÂôÉÒšdë0 ÝJטQÛNM€]¨V­Òy¶nAŒ/ ƒ<$<’=Qº½EyÁä*Ó-艵1Q=ÿªwfUQå¶¥ª•J&0w¯*õ&Öf@f]u{Ï &F2‡ƒkdKo×\C§­7?Ç`9–P1}¡ae7 ¦zrócÿOë—Ò[( +7þ/0M¯ŽÜ÷–ï©. AóXZ¯2÷½ÖäV:€ù2I¬‘áßÕõuHø×¹b?¦Xï@ÖÞ“e±Bˆ/õX¶d–s9¢XfTi"-AÛ• Ázû¡ä ¢jì±.ÜÓ‡culk E6?P‘e´ª0k/S”ØB;Mí\Æk÷Ô0ÐÆ&¯®Qf¾Ú¤q?Ú(ªˡ&òŒç‡2R‡b]±Bã;.’Jb¼9,€Ä!–z !Å*{<Œ"uG!¹Ñ°Ã ÖøöÂöЋu1eP{Þp)]cY&s9MI2*‰òæÏ2±¬”¬§Æhfð˵GkCR¨,ÝŠ*Üç‚4Òâ ÇNë àa«¼4tÒ cn×5|xM8´äVwp¡C Õ*Ó£%‘w3[!°î¡±„;tŸ!N°Ð`T*%–Ÿ.CÀ£hOcËùŸ§k(DÔ­‚(E=!!ñq ¶¬Ò߬S!“_΢ „ʆxPááR!¤—x¨b^PLIßAsñ–LcU#‹c ™c WðÑ0økyÓWë›D’RÛòóFM¬4ô›O…Ê€ F_‘@#Mø¢ X*z“#NÊFP¿cú>–ŒD³|ÿéÉŽ2BûåPð­à-x!¨-Y줎"‚¦D¼0D”Ô ‡e°«ÂÁD´)gïÜÜ¢"›•󵂼JÃ$ãå›—V®Ïùou¢>½?Ô+Ä—¤ ›SЩÃ8Yq<úÀÐ`#K!—õ¾¶JÃq-`¼ÓwÛEM†<Þ˜ë€Æ)µµT»¬qúSXÍæÂZºaJC©â0ÿYoűu}ŠŠш÷Œ„J|`×ý9`ÞX0š‰ ãnS€k›Àš`×Ö¢)dìšÔ1Â_͵=¶n["üCnÑaû«['AÑÖ¤c9ZÀeý°É[¸Ôu„¶iDb/ê°š ‰{˜óEÃdâ3Ç °ˆË;$ËúL>38âqxøOÞ`ê¼ÿ Ž*&˜© °t/y ;TSÊ‹¨ùH”¶V³†HR1S¢„vlkáR#Ü]¯3Œÿ´:©ÁÇ@ª¥‚ÂþHñ(§%¢£Ü …)(|h­ÕlLARPíU»€T@É(K9ºêU–Í1ÉSQ”ï|àþ/7ÉÃ"6ðw:VªT ò´F„.u|jh¬Ï§WJºy­M*.O­HhÖÍ$TŽqÑÓ1ò*}}!ÇéY€µ§(`5ƒ7¢2(°ðønkˆD—6K¸›Q‰ ÿ¸|Ý3ºþÔ€º¤s¯ÎÆPÖ6æL: Õ,®rr)¬­—"[ÏãÃ1Ž6nG7Öçûú´J8íû’ÙzºøÚZ_N̽¦«Û…ÞZ”›&¨'o7°Õó>ópwój%'¬ü;î*e+ ¸4aV× _ d™!ÖE’7 øñ¬€˜šÀsÑA}yFªIëÉ-q%¿Ö[–GO§­bí¦h±>>?A½]ÌÕ’1€bØé´IyX-mÏè ²d-¹ÍDù Bñ»ç1¼Ç‹K[ðù³ÕLÅAB%P=¶!ÓA,¬ÚKoÑ¿áñ3—µ¤²2»1Å”S«Îš:*aR•¯mBuæ÷ÚÖÊËIAº`Á[Ô¬­ÚTøqîÅqÿ<(Ì~’E§úBèðŠÅ¾êÀ’xÄ·‚JŠþ1h<¬Tðoß½ˆ=[.ˈ„²Nþ¨d[Tci(Ì¡S†0õvà239‰Î IAŒ¼gM²0‰>5EÄ¥ûôI·Ž²aóÓ:r¾.eðr8 tI1dˆw)cv€E7&5‚"Îo‚—ÖÇ©ÅC ×ðÛ´^2®üÄâ©DåÝdnèÿœ•"«ÈÒ>t›ÅzÌb‘ÃÑ:|¨[gÊÆ ¾Ÿ—5Ù0jŽ>ôXn«Ú½ ¥¬~àŸ[¦[ÅŸËÄéÕ›œ{á‚iõó_‹Ú{ñ:n>ÖWAŸ ”ØðD‰OÐ…3Q ·•¬n­ÕX ~”µ¾³VhA:ß\–9^¬CSi¥XCðœé†Â{t"B;¯YкfȱclP†3Ì‘$á§ÑœÃ:,±È«/L^ bÒcƒüEÅAM®^m"!AI]”ÆØ´2˜Ç’ø˜}p`átEŽh‹…FØ%[ÿ)mÖ®¾qüÖ_ú”w?ϰ…%µWix ®¤*¿êIȶ™Å¼ˆnJ±Ç²TX„Aº5s¡·¯èÖâÞOüξ`ž‡B… å)P|6þ Èåõ[…þšÉÑ¥O9ð\Ó¬1ÐÞ뤟ÂìþY)ìéý-¡tà y󛽩ϷT2(Ô4óÕ÷-{މ PBe9™­¾¤WÇ:?@Õpä¿©¢uØD(Áq®°fGÉ:"+àóä <ž0 >Á`S“ã;R˶s„oj†”–ÙŠUÿaÿÙÕbbßÁ°ˆÂe™¨Ü.)/ªSzÿU;'É.ïK>'}b_×b­·™ýtº9åS/RQVÿ)ek×â©ýaøœœ.ü×Ä‚ä£pËQ!«G”e¬ípbaÇpTïdÛæñOÄ=û½B}÷HÂÌÉ[Ƭ+A2t‘|“p4‘ÍÁaÒt„sÞœŸ¬+)ºx8PlÊPúÆŠ·I[êß›j´Ï.+Ú6º+7Ì{H±_Š¡Š"V{³%”P²û«iœ¹DÊ~j>M1ÃCü2gÞ/8<ß /H—¢ ·’¤e—Ê´[}nD-«ÈšÉƹ‹J®ÞG¶Õ:΀x<Ÿ&@Pˆ®ŒÅå‚Så9ó‘_õ0“½óäÆ:ÇAÍqía5ã\Íb…ðy†Eºá©Öp4rí‘…ÐC"Tx„Oº¹©‘fì"[áÛþOnlò/pVn¸ì´S±Yµ“1‹¶¤O4gÀ/ë“ÝÎ-ÿÝţxÝØŸï½ù}•n;ƒ7 Ú\¬î·˜4Ån“³ËÔÎhoÄ>B”­Þ°?ÔvW”)CµÎã0MF±Rj–‚rÂe1BˆÇûâ“r¸Zal6¦›ñ0"’ž ˜Q¼G\Pž§…e å2,Fýã†ËÉR.¯ä…vÿ)‰§þŽ­b%wm;.u˜µ%=&²¤‡moC‘«Ì•tž„C’aè(%¶£S€&‡<[î×6{OŽiK7=GÚØj†NCÓÒ&ÓYDl¤/üÛózx`Ó•ç?‘m dIù…}¥d!AÊ ˜òn¥€Çr#´•l ™¶Ùq–}È\§î6¨qÜÒô¥ã¸EîÊÖ'  ³«ìÔ©À -ÏÓ’ñgXÔH¹ÝûÍõ¢K½ÞűÆÊl 8(R-JRV±;á¦Ê 2#}ZU¡ÙåQaìØä¸Ñ’òQ-©?6mÎtÎSÒ•v>¾n[Ê;Ò9¿ çgÓÝfd’…­Ÿ½3ÃË’XôÐÚsëºF5 …&œjþfRL so‡àÔl¯ï‰õ£ä6 ãb~HP¬ðqL÷çpÆáÒ04OWûì"oudBÉU™ú':ƒ5ötJã¤Â£*»ò $O¿4«m*S ÄÛï=Øñ`Šx±qó bŠš’Àç@=DÙ®_?¥¥£~>·Ž0+z”¿¬‰Rš®ÎþORŸ¦†™A$çè)Œ2¯²þö345Q4\ʲ|$OÇb<«” Íõp %¨Ñ¶·¸/¬µE˜aªÕöÙ?‡ÁYE [Í|¢×‹N ê•’8bj%Ržõ?H{‘S£ù’Ãu¾ºM¶d:S*CúÿQÙŠï•<æaЫû,€vÒ`~ö‚ìa› )gÚ/Û“éÓ9un܃Y³b$Cçæ_y‚T«’¹¦<)Íe!À´KW£--Ë5ãG… ®ÏOÏc<k\ÕBój„µ·(J+ÚzÍù¤ ¤p¶ÇÀ) ç|ÿ1-1AÝ]ŒÆ¶Î¾[Öz(&ÁQ z¬@7°ÝбPv‘v4õͼlêÝÄs|!Ò_Y³5Tñï']ð>ðcGZùÆ®ÞÎLáa¼¢jQxÓwkÏÊ8)@5-YØî?À ‚T mÞ¸Ê-Á>ªÛò¥ètï3$9Îh k';??H¾]&\‹…šQg´hØÝÔÈMêPµÕxµÀÎ8¦*³lªc±fÌö¤–9{ŠhÅr.œè_ó½¦Ë4*ŽÜÙ× ²7ðò;öèÖ"ékVF´^RRËŒbg†AŒ z\©EIÙ9ÚM×akQZ†ÅÖŽ0¢4‘ã¡ÒÒ&”à?«¹?¢÷J;Öѳß-Ÿ=„wQüª&â^>'¢Y&M‚úNÀÜQ±¸¥îò»'úÎO{wu±ÊÏ·_­Y9¸Š>âß"¿ûÇg«ö{ýøxyx:øëãT™7ƒ;Í4c¯ÜÌ`îQ͹¾nJ†Ü܃ûòvÛOÂjòoB¯a ~ÏN›¯òNÍù`ÊýKÖvÍëÀf¸‹°–Ài‰ó%ÞŽ¦X­k=Ü…ƒÊ6V.{Œ†b¥„‹©\î)|wŸ^ö5¸Q¾¢™üÔ;þ6ÂÈm‹6ÌݽZžqþ”uD#8œäoù¶¹â’&Œ ’ΉͲ!:á¾+¹ ýiBº÷•QöÑ©&qg « XohÚDºóbÉMbUë,hÙýAmTí«U@DòppÔÃt”ÐúŽÌ÷ÆÞûQÀæBeøØêãùŽ«ÂBà çøª´¹¤Ïæ×ô:ÃÆ,~˜ŒŒ )»ô.N(„h]„ú@¿¯ ¬žíŸ%äs¡¶ÒçW 3‹Éà™O›†‡øœˆâT£UE….ºñŠ“ áþ¹™Œ"çØ‘jjó{¦‰²Œ¼ + ÝJÖÁ¬Ü0 m@mæÕÀ˜™8åá8Úïà QMs¢â2Âìqº HmŠÐ¶w}e?tÕ!Äb‹È_£Âa÷éBWbtzt0—ʸÒÝ’ ëŠuò£éQ”o¨ª{«ÉŒæŠ{4Úw®ÆŒù ,ÿ!ŽëóS—Šö»Š+Û‡Ißo¦Jà霢¸ža³Tw¶0ö¹ÔSÛ@)½g'r Ü°óÅ|u Cmв™?S‡ûynóÙEøˆ39¥Ö¨ÂÔœÞco¶ûÖ{×VÑ¢îÀ `LnYâÒ¢ÔÎsEWGUd+„Á‰ŒLçŽÀöCñQ•Á‘fœó™½¨%Í‘ SC!Jü¡ÐÛ‘Åœýi Äa&'þ7º¦QìÊ^>'åMwFÏÖY,ZìÃoüº8±ϵޭ\~z¡úöÄ6›ï=N7ƒþ¶E­Æœÿ¢)©x–_!lé‹:ªÝxT7M#aÁÏ궦 OøŒɳÉ=Õ8‹m³ôvy‘ËTõø³)EU8—»p¼>Uàê~J})ÈZûì½åQ’‡Ôó ùÈJ|Õ[|*Ô]Klh¹ó_e ÿHyØ"t÷æx-ÊŸH¼ìIî±u MÅ‚*µSc$­þô÷ÙmÁÃ(µÒÏB%ÒI^>itr`%4˜é¼ë@ÁH¨úÝ,bYµaþigt~’q˜QyÙpÅ"åd’ûI‚ºBÇ|Im£µu¬Œ€Ú4—Þ ›yGu¶%ü-~K>‡C683êKD®‹Ä0¶;˜dÎ’‚ÉŒÅÉOÅkÑ5¦Lá^7Ed«õ°Ðácwœ ?{E «¥Ž„’j˜“]š™Äú)ÔÝ»P=*/«>Iz\¨å}ÄTŸId°n¡ ò§âðÔöOåj‘v(žshN˜Ä¡“öÈ2MŒ¢2†¥âLG<9ç}†Fµf–ë ›Õ¾ºÌ¥HìJ¨rq½ÑpXÇøõ9+¥\þr2š¯'Fp>>î1d,Ž|iPÆÇ¤­£iZÒR é a|YNqRÚ ½Â)†~åLˆ/lñXÂWy‚áðGè? #,7Cì{ROØ6(¶ÚGÅØ‡ØÍn9¢ÇŸðPdšÒØ!!þTv×—D½B`ü–#EÎõK[µê¨)ˆàjQ5ü.%n='¡X Ò0š)މ0Õbÿ©Óžö´DÇ-J_À‚¡wÈþ€¯†ùëbͧ|oò ꜖Rì„k`ñD0´§z9 )Ði«’P7…DŒÈþŒ#” 1Úpj©Ü/-cê«+›ŒI†Ùò\£á+TI):;9›wñv£}8?¨°óåzˆîNL|¼Ÿß£|˜úGµã±yï˜vÔC„Ê–•0¹ôN1èÀÿÆ18ýœÎý†˜”‹ 1Å_Úk5Qùj.Gh]s‰DšUõªS>–”ìpÇ2KcªC€"D˲èswÃ/åI…—ayÈKQidQ {ÑÛ]¥z´ˆ‹¶:#?”YŒJo0%ˆ QŒPvŽO¤ ’$K"Û –W)ug 4¶4ÔM`óRûiÏS½Fe‘ßËn‘¼ ÎDÕ‚ysWJÆ¥£¸kTHÝìTo$¿+nûÍ5¶±­B©·—PÚ–ß ƒ6¦a£$L:Ù8F`4?«AJ!‘ú:J2÷Ü4 Ãòý5o w´ËiN OU ;]–>œa©ïXõ“ÿSÊ–`!&y'¼µ÷%H½3·k?jn–U–æj•ÖnØsŸ]Jd›e¼¥w`K®Ô;Ê,yE[í†cؘªË.€Sx£¿.ûÙ’Ü“CW&‚h¨ä㨕yÝÿhEACjÅz¶­Ûš©Ã?få#U´fÐnÞv÷ö×¥ÚzÖoÔÜTÞ^U‡`à+ÒÛ„/_…äøñzún+`)+ ¹GÂâ·>¾×'mýý—ùÒ&“¸£à=}JÁ9HÀ÷†-Ђû‡žçÛg¶7ÿnìRul¨†ÀJÁ'eN{îÆáÐsœ½WnHÑK¥ÃmïcDêoÂñø+ÁzÛÓ–o"XÑö(Tn÷»0ñ’ï ¬N/‰w“nÇà™"Ë"Fp`ÞŽ;±‰ú*!`ÁI.@¶¹G›°Ê(cÚ¤ÆWÆKðqŸ‘-&4a—Zë9®Öþ!qÍ™ )Ò2q€ú³‚tZÆ~€Ç&Y¬9¥Z*»,«Á‡j¨Lˆ$²›:R8 fx°ëªa§5TŸgx«XMd®Ñ`?àŠ“Q½Ÿ{ÅP*úù:z¯k `hJPÓñÔª`ü<^G(}º‚^ŒŒÄnîÛ_VWË‹/ø8¿FîþÞ¯åâ£qó{\‡ÃãóÅúµÓI9Ó s‹ѽÒ>[¶'Û­²÷‘üó8 Ÿ°µ|_l:—s÷ZæíñÞÁë9>‹·ê´´µNþªî6“vÇ×|\:½®Ëw„ÔP€‡ÖxžYf³?LÈ.=i­„£l Ä8œ²à´Iß(בÁ„æÑSc&_<ÐÜ'™@ëõQ¬x&h¡r<†39P¬Þ²7õ*§-nô”­EwYÛÆ5ftš?OêBÊP‚¥xgÂW®¡¯.Vì¡“qùTýYÆÃ¨e3w6g¢5Žž!]=¯7üjÝêCÆ–ˆÅ·vÂÆ&8,BŒGäw%ü*H{/-eÝ¡„Žy¨&ÑögjN-¥ƒˆàZe2îÍ{î¦áò¶¾åñ\®±Ux;ëøh„G‘!*C jþç§Ñ)Ÿç4½Lø»lò¨•¶‚KI·¹1P¬‚ÛŒ#æ‚£¥`ŽHŠGí­‘å[¥ ¨u|oÝŠoÈ:I(t£—·GÍìoaBE"9:i…UùÎI¢‹>>FýŒßÉ“½—[é¶‹óÖ‚Ÿã¹ÿ0Ð?2€%†Ÿ¯Ên߬÷¬ðß…Ibzi'l¥ƒšçÇ?¼Ôªv±Þ¶€?Á‰z=–€ÝQ‘c÷‹!¤Š¾¾ÍQT°=ÀÑ[÷Æc†fä «òàѱœKQÚÙ5ë Ï–¯ TDÂñØÉOÊ|¯K+ÝëD z*}ü:^»iõÄÇ­úãLú}L¿À“ecË&}Û•\–©T&Ùñ[aäß‘™EÓ(id; V^½¹¢¿°_z_B²¨—ª¨I‘1v‹+ñÚ´“žõ,D;Þ^esE#¿p\ªÚ© £I<3ÚÕÍNy°kõY³©¾‘hxÎì4Ô—#˜¨CdÑÉD·IE¿½«°zÀô”øïîGv?É\šï4-88îàL8öje #9àŸËzo<s¹Ø¨(·ØÛâ9e}Lh5³ ©V< òDãW Ïû.AÙfê[1Ãö/¦{`[yÕõ_4Úº„üoÈ Éˆ ƒÚ j`‡ëíhÙfu¶v€V”@Ŧ0 _)®Ä¼ô@Il½M(¶EžÍ©Ë•‰Í—­-°ïØ2Ls[SážKîBj$–µo¨¿J³$†’7›ÅX‹FåÊtõj½r¸§qê_^ÍjE€ù¸éð|äîW n,R>Lø‡§{v­¾o—}÷oéǨi·FŽW+þ-~I®Ïÿ¼É~ZW,­/X@?ƒÚr€"¦ã|,2a>gõoVû”ó. Dƒ´dÚ*[¬c¢éÉæ–ªíüúU»–i^Ÿ¸gRl’rF¿>êJ(EG0yÉ™ö.Ó0U¨[`#Oœ\I½’¢tz¡„°9S0¬/Ûm|Æ4âë ­¿3èœý¬ÕY#9X‰f¸ºBÝûp_ÏA*Ó€’ÝVÇi€¢´öI7¬Iqp4žiÓ*Ó@+5±’ÓI’ì@”é½` ‹'\p:êÌZuTçRï³›Ôñ"0î*±ñÓňÕH*8JBô˜úÒ%ð‰š¼:£7ôÔÍß’Šßi5{W_Ž`©`Â‹é© ÏD´È“;;i”¢Sc^“'õ®-,ó¤#WË"á™W¤àÁ}c™å4s+í¯•€=§Æv4' Œ˜*5SÎå ;¯ s_Ç铿[dcÓ”_*p…÷Œç\’•Ÿ¥I=ºË‘Ù!m¶ å(VHêóÎ#ÞU'H™Ø©9 °¹'œ™3Åo×ý?᫨K2=dƒ—ç¸å D؆pðß5 ŠíiòÉU™\ƒŽJ¸–Çÿh V¹7—¯ï¥ÒžáìýÅû”™Òo;5]à gNBq¶Á ™ÍK§(Gø¶lïf£F" ˜nžªü9R~ª“vM(+WÓ¯.:ôR9#Zi³zxêØýƒ~KS­šÌê§0ãS¿Å‰nå=mâÈû ÔbàãÊDQsQRDxrøÀd®b¸Áa“á4©ýhÄÅÅèP‹èUzU¡€âe(:BÑã©H oHáWXî¹QÑç }ÎøîáÞ¸\W~ ÊÕåѧ,¹3i=l½jPE<2‹ÛQê„ÿV®¬+vt•7qKDη†ÿ”3€¾£Qx»ƒ.ƒ¥ØAcÉsF£vPL®Öº\Å©ñy•°œ¾-À /uóÅÊíªÇ5²ÎjËšÀXé´7§¥¥Ì˧¥,µywº°½ßåHŠÎȰ©'uI¬.ÝÈ‹#>€cðîQ`6ÕÞÃ8[ñÍ ùÅ’5"æú«¹ëXÒD/ O¡œn}3¡Þ®IgãpFÞJSCñVˆnõ¢sJ!x£ìí™2J F\~ní†ýôº§†WDµšJuЩÒSìŒ:z;Ú¹¼ø[åGùóqão¦Š³áÁÒ<5dKJ6V׎‡E|€æhVZ¶…Àpw™ÜÙ-0b²¿b°l'Dòú~Õ:ºÑøçùßH j:£9Ö’ýUåÄÁµÞüÖÓ»b~,ïAú¿î¥a»tEŒÜµ,“¬á'X’Í÷±ÅÄ¡î¥Àª-q7‰_ñvëu¤Œ ÿjUò(PI›µ®ÞS©œêûð®UŽ~7Ú)mvd> HJ+( "GF°D +­%Ü@ÎäóS~×*êMÙ%ŸóQê„àŠ§fÓ³²c(l¦?.Òç“çT:´¦äèZ›D}÷“ˆTjŒÄ¼.rìá§%°D›ô¦£Ò"ì:'¦Ç”=3ÿYeùÕs•á­ti^óî_Àü °Ÿ¤XgåfU¸Í–PÃ2ÆÐ€Á‰|sžA¢ßYßî£ ¹©"¤ ãT+ìÐÄ-ô¢ÈLìšR&òˆrS!y[’ÁkÙ+Ù§Ø6‘ܶ\ä¤JäI.ë¸C®ÆŠôë$éàëç'Vb‰?uàkSô=é’K‹|w$ åÿëÏõDz`N…x?:s†náçaÐDô®‡ý{Ñ@'vh¦žz)ÜN¥âQKø8J®Ž–o)µ°Ö0†Œi-ò‘d ûÖk|y€~ò§xâ™ka¥ž7¡J;­Ø˜m¸È¶!xIž¢Í‘8©]–*ÞÞÝwµ’®‹wØ2[ÀA&ï—¸"¡žZ”JÑÉ Øÿô`è6±_.ydƒíÕ5ò  ôr¿2<(/ˆ×êêË’{4.K¿7k¡«›ð_ITom ëósï›ÀÕ«ŠŽŽ‰‚6XÕžvݺÆP±Ôüêù%oó"÷2œxðŽ®„Ú„ ß/âè[£òÎuu(}Õ79T‚w÷â›ÛÝ„¹ú'ŒBÚm½¼ð|ã+Ï/a¢ü˜héúùÙbиhø[;ºÈžAÍóY ]ÀbºM-¡ÊŠé2£e]RÛKÜ;ÈÆåÃχ/ñãᖔβgºÊ£*C uÃŽº&^€[´Ø¡÷t’«K¿É=Éåžs‡ ×ê&Þ)“44\Ö"cKM›¦W)%{SYcÚŽÜRKÒÖ!v’Í é,øIj0òƒ`ðëµ ‰vXÏb+CŽ2h[£ ›aSÅÑ]r-žºZ¹™à—€*ÿ²E©ž7Aƒ(ê—î0ýP@ÌKí&#Õ¬©?ïÀgÏ/POû ½)ð-s±=Öß²lUñqu»Çê]f™ÒÁ¼½¸ãépˆKÑ•\mzT÷ƒD!kƒS?€;Aw#ívd¨„‰1çžhŽêra€ï¸U!d°=\¯« µ0è¬$Îå~`íº†¥üs _;ñ«pUläyØN“ëŸE ™ VÉ܆¾‚ÒæIì3³ï:Ì+•¢ï¾¬ZŸtó6¤þì1EcDõðëy–ÊR  ¥`ªÍX#¬?éøóÚt†Í[ÕóãàßšÊ!Öîm=Ûˆ^Q°ý<å9d¸Á¼•™ìÓ\rôà¸æ-Sçվŋà”ËCÉòÅÀäz8é…°Ë4]°Réó9u=Mï9ÃË£áÃ<èöày‡§Þã¿S¯€²æä¢ûu7ÂcèMÃ7|߆Ñô£jùñkjqñ £Æ_lŠîç£gNrÊéŠÌ”´9ÌdÓ,‘|¼˜»[[ (R÷Á5W=îÌȡڜ·Q-+,Ÿ}àQ¸Ï.oû½Æ¶!<ï-oîõ-ÓŸžƒÝÛì¾`~po_ ­›;ÜîP/©M–ª=ã!½¾77×Ï/˜ª¯¯ë™ììö.ð{ï{7ô«›ÛWð²K_¶køW/oœk¡‡Ÿ„ª;x¯vŽó¾R¯.y•¹«³³û8 &V yÇ_Q3”J óÌ@+1®Ç£ÉX㈿ʬRM–íšÙ@¬jU ú<Æog‹XzÝÜçˆl t,C4 «¨VGЊ€ënæ:ãÙz«•ñ¸©`«¥äŸ½´­eÔ¨®{HX¬ö¹œQ(Isº b:Á²†x—V}•u~½¸^‘Òõž––:Ówô‰etƒU IóçPžpuUÆ‹—:\Gv9è²"µ‰š} ¹€V|ȈÛaÐk„º},7(í©5AdIÃ’c^Æ¥ÔÅÐ"?x#4m;…Jy 2êñíS¹QI·R»¾^Qß ·Ä3ù=êúY¯ÄMòn¯àÃ]äyA·†ã†Úsoš\Vð^Ÿ'—¿˜¬Òõ:!ÆwpŒäLÓ~–µÚkȪ[Mjq•u,#Nð(œj0XÞ|0 `[E~`ÓÛZú7•½>•½ZgMzB üc©SW”q*-ÈÂÃN Ç=/U³x³u£R®0ù õû$F`%~]‹Žžró¿õé½öé~…)ŸK÷7ŠfNö±wÏÿN6 øþ?5W²õáû{ú¡?ó¸+¤G.eä²Í¨nñŸþA@«ïÙE·]•ËÓ(]þø‡½ÅÌ ’ Ä×Là ¶Œ[î{¶Zš ú),TF$Ųëéêäàâäëì½ð:ž=×. ÿáµuwx€ßh›Bw‹.—ŠéY¨cÞ¼‰ÝÞYÈöÍ’ÿüë+”ï*ˆZXA3n›¥ïWW7?ªŽëO» vo4[§ÚÁŠ ^0±?¾9(xî‰íU sÜßY ¬¡¹';èxšF–ÄSTÁ 6ÅÐõ‹Ä¸ï ŸŸ„ £w_„‹ ÿð1G\¿˜£[‡[Ú.$pÈh£@÷þÜO³c1ï \ùÛ³ŽÈ)âêúO©ðùrqñ7Ðóíéøõ"®Öÿ+ßÙX©Úãÿ¸úfq,Åp=WÛ…1@ ½èU€Ý¬¦O©LuŸZŸÙ¿ÛŽç®JÕ5~ö#sˆêŸY ù6ÌÛÎA­‚g¬’J«çɈP«æ4p>Z\c³·–SË6­P¸ †/)áŒ\×Ô¨#ºò‘“ß`D6›Î=¤î‚â¼›„Sfë0uî$¡h+Ù.yÉ;CGçN$c„—þ7@c¡ØÈÇRÛ¨–½ož-›+ÊàO¶‰ý躊> o‰ñ-¤úÌî’ÕÑçÜÄ®õyìޤ/B:ij0WŽ¢С@q2͢ﭵ”‘ð-ê<¿è0w‰ßݼ;k’þéb/¿ˆé–éúÇ׎‚Ô%pFQôlÝ ‚ÃÉ4ÒÅTô{7jþÂqë?$pÛ´PÑ·„oQün9ä«ñ€ó,V H©¶ÌÌG¹ÆknîO¡Už:gû£­@¨ùÌ?d¿ž] ȬRÀfêˆp ½¹o¥zS%rô’Ê÷"=2àâÅ`EhbwHâÁe-Íã"묋. ÔÏ'_AžwÞä‹]”Ì zpÊmÅMõböÀ¾‹¶ „äO-Á2[wüþ àì(c)1 쟫íS*ƒÀÆ'Tã˜(Þè3ƒ " Ð[u7ˆÁá\N8Áä½Ub{ãO"yÕ?nÞ &ûÖk5%Ò4Å2 rþ_sñB·^%}¤ƒÂC#A¨ÏÆ—çß>¡§—1xR?zÅ1óXsЇ!ÝÒìÆv Úì® K"§(Á¥W¢Ž³ð‰ ñ´5[†^†xÞ¶ ‹AqöÑÆo¦¨yÿá&s=V8$NMK}BC(2&@rN•"«8y{–ßPÏðǺ‘TæsâEªYUwÏ`' ÝJ;ÒäúËzèºØ…%Ão‚.·H9_}îó³3ñoÏö;Ñ5y•VBòLŒû‹×躛ڿÿ‘6¹"¦¦æ­z@ùÿÃÜl<Óí‚£é…çVÝõ@"p?sÀc‡„ÃóÀIrÚŒ´ã&™°EΜqðÌÖ %,àÞT â8Ñ¥cÝ¢Öíd2Á•æzÚ’V›šjLÙŠ2¢F2B®„4áDrfCŸš#Òê‡ÊùØ:#™ò÷õë]]è`¶6Ö£D¡²ãµÓùæßg÷ÿéýwmïSerÝWóÝKØ·oyÜg®›ÿÆË|×ñ“9×þS“9×ù““9×ñǼwðÊ=ãS¾uÇô¤k×·5íÞõƒÇ¼wôê=ãóokÏøô­ý‡yïìõkƧokÏúô͉?ؼ6ãðhÃùkú›Ï}ǯɩ¿‡å \wа‡í)\w ±‡•:>ócLçˆqí½=Ó›•ï+ŸåI¾‘ÑZ-ŸÉZ>û£üå…³é3Fói¦Û~5õiÎÊ:\Ów¶†×þ4íH®é3H“Ûî4þÊ1]Ów¡&—á‰|z³ ŸÅš|æ[’w-¶·&—é©üXc´zf¤Ûd¯‰É$ˆ÷´pz©2>» þ‹˜ìœ=?=‡u@ï6‡mÀ}6Ãß}6Ë€ß}6sÀ}TÆ!ûLÖ!‚]Æ!ƒ›>{?"³ï|×#žÃdÐQÙg„Éâ_Ö~F¦ÿŒî=‡»;|i>>ó²Ù/,òx„lÆ>%³ÿ“ŽúöÈܽe2Yc6S`̆*2ù ûLÀÞÓy¿°ñn®?42÷{P¿õ‡üñÞtyW¬ÝMhìfðáeUëuâLìÆVìã¥J^-¼Ù|F²,³Õ2¶·i¹Æ?tv¨X+CÃbD1/õx›ºev+Øí YàÆŽN¬ºttµ¡¶!/•:5°@¦X½ãê1UËev¬ðc2žŠÕ;9u,Cß ¨(W‚Æù9AÕ'^·'ÙÕ·¶¾Ú®¶µëç9,1uièbÙ‚þÍqÔâA ésðk÷‡ˆjXC©õŒÝ(jƒ^ еñ÷Yç$·ÐØ¥¶o¹»nù=Ÿõ:?ÞøÝøÝüNgËûÞß_‹‰Êô"¹N§Óãëã̤1ê£JáÔpòfKŸŒM™¨DA¹2uêŒY'"w“ekéÿ#ç, ték¼×¶mÛ¶mÛ¶mÛ¶mÛ¶m[ó­«ù³J¥+YösN¥ëø-EýF€8ëÅ~k½˜²£á?bMĹöiZ« ÁÉ~ÆPn¥M¾ EêÕ²D2®'è-6<¨M¥ÄÏQ­:[½Àxj× ô$LÓçîž'âxýá—c³-$[£fž¬è;Ä;`$±­…­Ñé’k£cÁ¤¥ŠFåÙ® µ¢šÄc¹ ·0jÆdÁ ´«[ײŠZb'zËÚ}«I×.q³ÁÕ“ªW±å»ìax ½ÑøðØJí1¤€ ¤zy ô¶é‹YoÂ4=H¢+|»¶Ûq:°šŽ³Û™N›®rOO¸Hš²#xxkAŠLóŒ(RT³l[=lsÓ³ìÙ³B‰v(‚˜ÛGWÁ,CÍ8z™>H°!F¡rµ½Ü{Zâ7¢b9ÚâÃÞb‡‡m般¿*@\®hrƒm¤2ŒßÙ­óõ)-í¼_ -ø}»vÀù(ò9/ Ô×sØΦrò˜‚~2}ô08²Bl)¨[®NÅÀØt¡ì‡R]½qðøzñþj=OT=ò¿M´¹}ë¾MŒá½L 67Ü0Jí|Þìlõñøïö‰~:½ÖdµÙ%ðBÃùqV«õ½=,Ç&”£H¹ ‰ƒí,ë³/öíÁŒ¾‡A‡ö÷@‡†“Q&‡ã>†‰ã:QÜCÀxCÈœâE½ÿ(YÅ÷¤Zz¶‹2ÊWªOIæÑP³Š*¬³ŠZ{žÞDX-% 1)9yNý ó9œL©ÈjSÊŽ`¨±gíFš\©G@=üµ#¢=b[»àÉ‚Ïp¿Þ{/í-PoJÚ7Õ¿cãþ;*mƒà« ÷rW7¨Ç½Ò?Ë‹GšÞ@ Y¿gø¾Á|JŠÃt2Á©9Â9Üáˆ}‹ø{ŽÏÅ=ãN?aÛ9ÿ3þ±£±î{^¨Ï9%§ª‰ÇɉïÛ“…éÅëv~ü9'yCÃ#ô•õ9}F ^ä" |ò–¯dùIPZXjÄ«N ªW†ôsWç¯ÒÖx,‚A°Z ÍŠé6VÆ•ª!ÇL©›0¯Ñ…É, HËn³ó³3|¿¼8rãur°º7:ºƒ3Ò"ãs²¥Z±?½NÕþüèG‘ä Ÿ´ÆŸQI¬íež½s¿_”¸Îæüt^~1)ÀˆÙÕR5~t¤ˆ%2§‰BC›íñ~vx—v¸¾_aB ‹2“< ¥Ë†¢Íí…OÍÖS7×—$s9V ‹ ‘„>7qè_„ ¯²„}¡6¤ÎGÍLwÁÃp[ªæB&U3öÛ-"Ü {˜lX‚â›Åõçrýx^¿8sòÜqz‚™ç0îŸå-s½=KñÚIÏ~ôˆ¯£UE™¹‡W–Wà-p5-©Úô{À›4 ý÷Њwd×9Æ ;ƒt#Çÿ1<›4ÏÝð„ì,{Í Ó‚Ä5Çÿ4 wòßFð 9éèÛ‘÷®óúynÎ-ýMûèéz··_K—yÀ÷,R÷x6ÒØ/ÞLîVÛ;‘-÷ð—"×s„îá“€×#Ñô›¸il› Œ}°«šë±' ’C1#’ïÆådo]ô»– •ba/~³Ié² È-˜>ä‹2Z',2ËPX[i û6aFvoª“eU¨ Z¾%8ÒL’©EU)¬û¥c'W â¤b(Ì^¡ .S«MƪH´Lø§ˆà4wÈSÔ†0c-Æ/©Ñ´†”:A…8Štª¾6„½¬ç¥_{Ùn;qÞºÂsÀº~oZ1ç%’ùLHP‘>)ð9J+¥2^öÿÚ4™q°Õ}. "I½S¨ î.©ÿöÚráÇǰ}š7ËÛ¶éœúßô övÇõü@UìÂ铲Žrß)裴 ä±ýR ÌÚ˜L†ZOK¢òW[|ï%Áâ—ÌI²knå2])ÕW?ÏןeLp™Jɳ›©zΘ‚Ó!2¤ƒýtñ”þÒR§;¶/ÂMæ1zÝé0±©u˜:ÆôœÚÖUüÅ>´vF¯¡ÓgéˆÞËÒù™ðÄwq ïa1ÏG‘žÓ•=´G?fø€—Q¢šñÞ\0Áez÷ìŽÅIØžøyµ9hzf<‰ðä¥3ª¤œIðˆ¥˜|ûíªJྉ°–tp`&DÙ%…ïz’š»óm;„ÁI´£‚9îáߘ೧~?:…çÜ©çûÊ‚Jùë4c×Ö‰Š÷^άs§…‰Tª—‡ÍÎk$Áåk|§ÈIïPng·»XQò ɸ7hêQ±Š“mZj¼—4g—åÉך?%ÂÏbêñµÀŽ@º²Ã—8 CW&rË6ÊìîV«á§=±T­””ÔÛÊÔ¿HærBûÆúúŽ«{ÙÀ ŠÚ„Œ VXÉ´¬ÿëfô ™ó_i °ÐÅyëæ½‹˜ösf%ÃɰݸKnõ±”½”.º¨›4= oAœæ!6Û}œÖ6ì甪÷U“Å¿ËHÁ¶o5é47C©s;Ù=ãVz˜æ±`ÃÁ´[DqkÛZN ³ñ}62÷­|#§ößUú#Á·$Úá8·ã‹• „5ð‘!i[5îëDßzŒ\°HRïþÉÇ~O±R#´£TCÖ6Ñ{ 5y²ƒ8ñh—|4Z翘í ±†p±Óg2¨~h¡÷yZ>±Rj i+{lZb3Åv=.¸CL¯ü®âºkw=š$+5Ûuv{œ‚œô¶þ• ã³ZPü­´(‚¡óSÍ}~‹fÃòß7¨ÛÛJ$‡ßåÕpŒ€… é››4ýYè[·bò(“Fu€g-J‘"tz{ï …Ï`Âí' Ó¨1Àkj•€|ËgÁWŠØˆè_î!šóó=³‡ £õ4¯öƒø«…pH CëÞºz©'&Äúá·T¿¯bqï@–é°MùÏ•’Í“ËÇ‚ÄS2×|®pÒiÑÔ¥`ý:2<¢ Öº¡HMÐ#î•=óá«õÛ+°Ñ©Ý¢©RYÎúîÿØg`ÊAJ'(®F¥”Ѓ†áG}ĵé‹I¿;TI.LJvKäµÎ³Œ7Îò­ýʹs§Ç_°šôÑëÊúZ8\‹/UÒa‘)2ã+vK%[E)ö¤5f[%’ϵº®«ºË:·êÒï>>Ùnè)Ò'­¡\é075†=ŠÐŽÌ-¸)rŸØl»×džŽ ¡þ6Ñ(¼ h¬ZfƒR~·Â'Bn'òµ£ ÐÀý§LŸºãfœla&–!}‡X IšÉ2ž¾Mİ3Oøcœl²ìà~~ÓS~œº˜¯&q&°ŸbÙES6Ê’ G:ÞxÑW¢²MJÆeÆšY™*8²«¢G eO騚ˆ˜ ‚˜Ä™²29ê+–‡g®Yªš¦ñö]Çýäk²Íßc>CuÁ¶òô]*Ôk£ß³®ˆ]ï]¸OÌ´Þƒ8q8@ŒàUóqž…>s"Æî¾ßܱâWéâ¼Ò Må<=}ç­f²t1ÙØF£ÉOqååë%ÉDÆ•^ñÔ©¯ÁPw5®¨˧ Ãò­Áè¤&QfqÖãeåîñ'½åûv€gþþ‘$Ɖ/¤ âÓ;)î®ÎµýÔ®@½ã#Äuì|e+”&Q­ÀÚ-û†‹1hÛ¸`y))ûÏØ¾„ÈøÌÑÄé•ܤÚÇÛ]­ƒeq¢£nõÎí–Kì-ölÍ@J\+ÉwâÞò~Š'x‡!ç5Ž¿0âø›Ú°P‹ìUÓ¬Dí›Èr#v“ *u¬ß™Ö¨Ö°1¾GÂäçK2nQ§ ’kZ&¡ñ;g@ò—ö©k—§IÕ°£(pôm.2ïB}@+ßjbÀ¾—Ø´åT ³­ö<¨ý€Ëï uÉëª;(Z§£J«`†|xS5…`¥ærÁˆË#d!xmÀPb&"bÇp8ËQ‘´£$gçó¿%ÚŸÕ<µ64êm¿³²4 5Å#.=X¯rá/êö­É°öʉ‹ô¹ÉkÆM.ÔQ-Á‘Œ#<,-ïjÜŽýºe*ƒñ7„^OS*@rWùšá|»KÿÂñÐYܬ·°¤YŽ8”q”ú6I ®Lá=**Ú.—¯ …¸”y|.% ŘôA°(׬ )‰½§†ûˆ÷Íþó2F. -? d#úUáÀ¨E[³Ô§voä–•Ha­ ‰1”·Fö±ú‡}“â—TFV«ÃÁÇÊÍêròx¿>¼~r •v|ÿ}2QuVJ:§Çáöaèô>bÙ„ø>b¾54£“ÑÍéáù>yÒPqÐ0e„„÷Z¯5u"ªûuÁÉ9ñBNCU:EÅúu„Ê‘æ½F6Èh²j )7N‚—4ÊH¡n(JT¢8\víñõúl\•ê2!Eµâ:'Ú¾›ï»>¢iÀ¡­ö– ætS`_â¤ïv¸“=Ñšk®0kMð²éÔÏ'Ü$'lj¸„ðë\ñ}¬ý:\^É@3˜D©)ÐÀ{·¡ºyj3±U\ûß ÜIDÝ®Ê ÑÕ`þBïÓõõT*R§™ä€xùŸ)š¾Ž°‹Rµ½Žþˆ–I(^S«Š§þ«¨s¤ò`´‰U(a,Š39‚æËTJazy.lï™–éÓ¯¿ÒhBÌÁ[ymËWQâ9¾^aÌ«œï5¦`/â-¤>'”³TD0Æ<)¸—z¶ÌM’Rø³èìfç>[½· VïA É3«Ã°Þ~ÏÐùZ|¹+ß…>kDjãdûÒ6íUƒ ¨‘ôÀî1ö‡(éEIVò¨€Ó78@y´¦A#½²¹/ÐÔZ:XÀªPn ¿Ü‘4P­±Y´ž±È·P¸Œ%kFÑ;Îwíz((0B6ŸOЮ­)îý³Êªä¤Þyíï~>$µ Æ&æ»Å©BJà=(þhÄÓe&.e‘MµÈ3.ö[`ßÀ…å~¯7Û•þ%p1´N˜žAUÿßãR0Ô¼¤&\%BÅat` ½pzÖVR[Ý6%d/§Mä0*™„5ÁÚòa¿…jV}whDT2z¨)0Ë#êÁçóñr V’RÕÙ^>[ĉ5Öê‚vZß©û ‘ÿYìYrI-ð®ÑÆÐn“`šQqd I{5ëk;á<ŸÆ_û÷ðªÖ‡Î¬‘dNÏ+¾øà$Õ ®7ávþäœà\Ö ãƒ5xè«ôˆ”`p¦Úý‡;M´ÿ—È3–eÛ_ƒã¿Éå7@CÓT£oŒpÃy ½†Ýüuzs˜ñçäòñ3ÿ.¸ŒS{é˜`£SJp£•†ŠJ"dŠò¼œeœ>pÃ<Ý®‰5ÈÂ5¿À/BªI7˜XгžwKsí(ßÂ<ê0ech_¬ß £Ïð‡›M« FŽ‹¹ív..6ŽXÖÝOÜ›š¯Ü(/…!çè‡Õ§B:µ–¬Óµ¹ôä[›.ÕøÝã»ÃkJÖqíüOùXûÂKصÛI‚ Áõ1 Ê‹~›ôªJ7r4 '; j5 Aa›Õ&²í!É)Á†G–£cLèÍ Ï„K—½ƒñÄÎ{M Ù’JóÙÞó°F¬Š9…©$_~ëerþ-aÈQG.¼‹‚}£—uÇêÐ6 i švuÏÂ*:{Ié»KÄ0>‚ÜLàŒ?dæP·ÉyÀ×<¤K×€ UúL¼²•Nj݈‚„Y1wÒ ˆs†JÕlå,”ö@üÙÕø³Hª÷о`ƒŒ¡Bû³é0XÍŠ(ò`r7…Û' Ð5D£%“úÕjßíç(9Ž6tc¸W¦’@zŠÞ Y¾–- µºb;ÁÙ˜|L®Ï¿EuD XþQF¨âet¢aB¨8ÔQ‘5Ä ?6ô’ Ü…ò³ÌxŽèIþÕ’‚:°¶­? Ár[æS†¨™|—ºjxë¶-p§ü†ÆÉÿÐ ÓÏþ<­±PÚ/‰Œa"¿‘‡T¥zúP!*bÖ{ªíO»GÒWÐv;|$A¡fEórbN.5×%3¨×ÖØu„7--7[™B‡GÜ쬧ê <ÿÃ=·ñ¤!']Lq×Àêâ;aW<ŠÙ`2vÚÕ8g¸Òm‹×øæù{—„ÛÓÛ ±^}œ‰d¡Àë*ô :uwÖ™ øpG¦¢ b-\<ª/œð(=•ÕKì#áSÀCýÈZŒÁ Ë÷Œ±ïøðëØ[3`Lº¥ZL1kÕØÄcœJGš©…¬ˆ.X ,“2Ò#ÖžîØ™ôF GY8Ê›«26íŽÿ‰ìæªÖFÆN!YŠ\lTñÄœœn Öľ/ØþÔ F»¿2)–ï­!q›Óž:q]aÕ3O¾¿(oVŠ6™ô¹ùÇ냒‚?ºôs‘_§ûH«×}‡x©øIÆ¦Ìæ¢¥äS jœ“YÖ/! ÕŒŠÌ"™qý5ÙBK½€ýSïKGŽé¬ï/GìOªÆÝJ¨ZYmEp²'îYÚ9ñ;ÁHw–þ\¹ÎÓJm™Þ”#ŸÈk¼tTaTeh-Z»˜&g B>™äl z0ÈÒ‘ð,%qÏf^øF-‰áau'þtèmaR€8\;jôMÄé¸îk’ž¢°ÀÞÔµ¼Â©Tû&•(ë= #rW•ȼ œ¸:eÙ‘ÔõÚŽøÉwá˜g,õá¿j{¾$*× `ôU±®ó,Ègßô€pô?K‘>úâÞ+Ó[8o³km3Û¹iÿG¬>³5#Ä3ðà×Ï ˆ's%è15êФóÓ4/ÊW3ì Dwmê¨×¤ýX¼~0èä'•û-<©iV÷·N' L‹ ЋºSJ+˜ô ždéJ°fö["ÆZæ|}½¾ÖªpûT›J)©—}M}tuhÁø¼¯)|+~f Š&åÀUºPÙýy¢Ÿ)ÿqÆqVx&JÁ¤ ÿµ9*e«l[røXÿž;ˆÔ8¥ã¤ÔÑ4}l’¬ƒ¼Sþ¤t³Òû­mð\õñVýE[…?úžë‚ `TÿeþK´¨²Ç@2»9ZãÉJ¨1í€FÆ(™ÌC¸÷–ÅéáÕÜ9r™xÕ¤ûB´’¥Œ*LÑíÍN)dF¯0,Ú£¥/N¡°}£e:OùJV“zÛà2íê#$¬¸\7©ýµ¢>JCûÙ¨°²½ë5'€.­T­f ;áoäQ§MKMÈ@$dí5vâM½CổŒ…‘òcâ|âϺš‰äD85¯ëø—‘Õ›B\­Ãx´½Ç!çî“ÑK KñJ—¡¼,ìC‘é^~ ª¥ £ÚÈÊb ZjÈ:ÞñgƒñwxYÍ©hvø)@–XÀ5¡'<7A‘‹¢Í—éy¡âÝä–z¤ð0„yõ…e;üô‚^”ñšÑPgó`¦9Æí¬ãEqäL¸†ýüÉÔðŒXõ©EÔ›°HÃ~» ’{.3¨¼?½~_#Æ7¾Y¿OÒhÈùγp€‡†Ó ié-tD€~ÐÖBtsCÅkù*›œb›W‹OÓdÝâhɨãÁUvà‰Ñ ƒVÎ}Bú.+C„9~Á)Á(]ÝQa<[£šö¬õ}MΙp¬ê ý%¬ûF܃ҳEÅ­û’¾ù˜× pw• œ}ø#Zi…ë3X×€¼€ÎkGbPkŸ”±â…wÚ7¤—„idÒkU#¤‘UÁ‰Ê¬g¢Òx³~¯¸ÂÈ Ihäaß– ø±ù²xØ Â‹Ã ‘b=ô‘¸ø—ÀºÀÇÁO—\Ô€ÂÈð6µJ€YP—«¢b pÀ@’)…¨ %œNŠq OãêG³§³Ÿ½ºRœÒ]aŒ`ÀácAš‚2U,¦åDtj •<Ïæ•Êåvž¾ríéñQ—hPPPª6ç‡ü^'¤ñç=¹"J|·ÌÑ I;×õÍ4ÿë–eµ;}ÐsQÆÆ©µ\F%Ôœòˆ;0߇P;9ÏŒÜû\§°RltiÔý) Z©ÚP;¢©rÕn8uq Fœl¦€>‰—è ¬¦bÙt½w} ùá¹@¦ÑgdÔò}B]Â/*¼8Åò_@9]ZæÔ)WbQ?£ }eO('iæd¿gñ¾ìÎ:(¬¥ù|ïkÀ Êñ’’×y9ñè×ac[STŸj ´u2â¿zË] †¾:<ûši9UµcäÔýónbè·iejüÒ&לøì øK¦&‘kùD>À~ëf¼¡…E&]zß@N„,—¨Nî$¢'E5ýžw†:‰´–wìÚ;ªX0öåîÇU¬·>8Æ0Ëù¶µï8ôfšÌ¨óÆ%2˜‹ÿýIÎËä‘ös÷Èß~€¨x{žL‘â3©,L·^¿œw(R½7©‹Ó0lFÜTG¹…»ˆÄØânŠrF‡‡•PT¯t÷2Y×óŸ“|öšGiÀUf÷“H&\ÌR÷Q¤Uíí"¢ÊØ$Én™o÷ŠM‹Ä5Oô/Ör¥ÈÁÎuÃÀUœ@˜*|ð3“½i•\ÛÎÑßÙþèw?!]gÝy3§ge RÚkBÝüI=䌒Ïd¢ï(ŽE4Ø\à3ÀôÑÔÞèI³* J½%”¨Â®X”ÐI¹çÅ`È’ÐZTÌ7È9Â"þX3±W{T?}û1ç7òöB1ÌÏq…€—h ¸o3å7К÷éu*9y —4’”|Üœ4ÞïÀE˜aöçPz ¶tÕå2üNŽøPƒ£«t„æ÷Ϩe¶ÕH×½—¨Ç²îô–QˆÅÌiILS·éêFäjCA¢±wM»î>™Ó>ùÙ»0·Ü‹è‹»r€ÜmŒ\Ò?y*½µlôD „æ$ºùioÉ\Ð`ïÖ&‹¸cªÝ9ö’ö¦¹¥î½ ]@7¸H~·þ­4gO¸n_î-ºšð’ÉHÍm’±0mMÙâdCóú¬Œ`ÃaTcÕ­™MsFÔð'ø>ÛŽörêW,Ž*Xʲû`-EümM» Xáä%-J:l·üÔ?"#‘[R»y¹c n¾?ëü»¶™”¥h²C9³Ãó^RC<ûŠ5Ä=¦H@û§Ö&ÔWà9žÀ–nñµ^ÇÞ2ßš¾À \¸‹Qtö¼áç^ïÁ´U…­ê/+ÝæŸÚ—/}ýY^м,¢• {î w÷½Ç@³v|gi §÷XŸC/ă(=ÇRn€HRôêŒ7J6gõM{(€ÜÉøêÂEŒçï4úÇb·ª°ÅÕ—ž}p+P ÑáTj„¯RôItf®­¹ßù@{)ß1W¨`O~ÖÚ9«ÑÈÍ­Ûýü`„º âí$fx/]ðžPþ•úŒHR¹Q‘Œ‰Ý÷¯EÒn|§•ô™‘–H„Íšð4ú0{EÏ` Îõ•©¶É÷p•­=[Ô¦ú¾3FiçôEŒ¯kÑ.þ 4r‰B!!qoŸ“Îcôϯ”[)AN–Ðò"ßrcO-Pg³nCCkG%Hµ-¨É”èÈÛùÈo¿… ¬NŒ¶^]ζ¾t¼qö¼\=¹ÒqnÊî¯ ÿÀ»{ºÔç&šòMqGúIqš¸s ³#–c£´ÜQâc@¢ÍÄôÆÛ¦èm™püxpËölÑ<Ë|‰^·!¾|$aF¸¶-Æ"Ç~™~<·’eXH:¨kè¹/äæ¶»}¦ðj?fòOáàÀË÷õÚÏáêÞ5‡µ$$¯l–ð|²LKZãY¿úÕ;3¸+TÕË¡’(QŠFíF¥[ã"×g³iܽØxÊ‚qÀ@×WÅ“®þ0j3ºŸ>„é7¢e™¨y¥±¼´©‰ K[KÓÿÉb3¯†Þ* \ÈGyûY1÷üÅÆ³#â÷7!5:ÁwN'äà( ëÖ°A?¬•–ø\v*ðåûó[Sl†|ÜWÇÔƒíÙʼn?vSpd•¿Ø†Q@áÒ ðV{\Jþ|¾„Ï ïe)o(šéê߉Î8ƒes(õ~_³ª¾âÎ'.È… £Z¥>â·‰»þãª=øpoYã¥=îpr€j}K-€¢!@°OTúÆè>´Æ|_x`è^8úÇp²kq~ë[K57Ù_ßu$7Çä¶CÆgxoŠ y ec›&xìã¦;‘ÏÔWb3Že³±Ó–ÏT XàúcµÀ¥‚JJh…%å›?õа ÝëÕóDÌÈ]Ýk‘$Èá_PÔ å¶UÉ‚–U+–6êWi;‚¢d*HÆ:Ù† el©håPC 8Û„RÚ)”ÓÓ•#è_ó5­(BE0ꃉ‘Ð)gøÆ*Þ˜ÄFF†½Ïãá“y°UQ½°=zºÂ7¢#}ô¬Ýò¶ï,NÈÇá‡U¥19¦>qç)ï +‘~·×aÛ·Õ» ‹šÃþCÞ·ÕÛj¯ è5/ƒåú_ä„ÔÔ'¢ê‰µaž¦Šõšã3'nÈ÷«­ò°éjêнåÝå´†{ü« ³>µ¥c»Ì^—P¯ŠHUç8Ȳ2h•öâNw,جãiðaÚ÷é¦Ñdnš³Ñ5`y¯ìK¶u3‰ž@ŽKv#lÁ3¦b÷Ú`GZÓàóäªvI¢TþËWùª*[Ò×°íÃ}¸ä”Z=¦?sÃàšO 8In¬å ËQ§ª4ý+×û‰X¿ ÷[ÈÑñHº*u¼:4M~(µmöŠÈ“–nX5o×R¶ëÓÈžRó‡wì‰Uh´ñZÅøI:pª]já¦ü€ø½zt$#’|(v þ:ÓeB 1ÝìÀFüãÜÁ—@3QĽ¹M LÔþ¥"НV÷‰N1­¡*L9-HÔ ~­žH– ®ViÂÜ›x¬ö4™ÚÁ¨A˜V ¥†4K„¶"˜òM2È¡ úA`Ìÿ zŸR9¼lè™bäñ4˜êd¢9ªhÛA’øê{Ý0]½}Àpý†"8·ÁªŒG$@á6Sæä÷ !Åtxsšo©¢®>uoÙms[W&&s?×%1e{WãΞ_‚î£\ gñ ú{7äÔ“6ÙmºÜ¿w>»ÖÃGÓì2áBÖÍšÞ§Áç¾81ðóëqr€hQkö"¬ä]^`Ís&îú®“#±æµ]ÔÓ´êrä’™©äîì÷Øe‘%&ŠØüLV“+ÂÙ)cÿ\ O®å Zl=ÞÏÝ`¨q ÍVP Íç-Ã#“ˆz¸$“E“_É ão/n…!‘™ÀÁ\ ¡‰Þk‘©:EÄù˜b|Ñe…7K$­»ËËQ¢“HYGK^ÅC•P„§¢‘XÕ÷aì*ˆ{Ë| P¿¬ã¨ÙŸl¬4ÐÆ—ƒ6áé&ïŽ2rT'¸à‰£UÌR”­Ø•ò’‰"ö2bCÒ¶9(øng/*R@Zñ<á?ýü½„Ã… °"¿»R.ƒ"3Û˜tÉ’N3ô¬šK×”MÕ\7èfȘn2/"°^ì±o“b‚<÷¬YK¿HöM‘ÎÎ+.a¥fQW½µªe+$è®'“}ªõÙ‡zéž`¼ýhñQ¨öíéJzD9ZÄÞ›®ã3”È YÉŠDºäè=!LY>-#×8¯Øp-¡†Dƈ* ýUùv<üc¼å?^±n$]K±‚@’ØC"¹ÔíDšj-¦¢äüWYaÎÒˆ„¢å"sS×ÕvÂy9¢:¾ôŠ÷3£t%BkÑŒ³?Ýhd¬I¬…ªÊ/Zjy¼7c8³r¤]¨fs?íæus÷ÚÙ¤ ‘¿|EÒwx,óOšxï{ '7»“Óák³fðxùšæÐþwue0 ©­ô”t×FìºÊ¼µõ„JãÈ2G°øLr~T¿ûý‹ñ¡L€Yù‚îÅæŸe–­„îËrÑÅÑúXPhaO=\Wò" ´Öu²+ZH=B>ÜígÍúŒIcŠ (Ö¦KÍ?¥/kKopñEuo^Ðs o€bQ}G^íjvëø„²Ýÿ¼²Ê…>UÄÔÈ€‡LoäÔÇúÖ+Õ®\_©V(§É[9ûøQ‘¾2A#ºYœÐ`žË† æÓ+G¡N¹ä J÷¬› ¥ÃKyw¯MG–’š¶Ê¬§!Z0¢¿5 Í{ó+î²_@æî¿áuÙ_Ú¬TŒ]Eï”ÿõb°ŽÌËÿ»*Éô«ûwAÖ >¯Ž7JõKzÒ¶-£|¡®¤¤À#±Ò©s±Ù¹}yPÑ£aú³µRa&á&0¾S{ÌMþóp¨·{ÿ9>~ƒ`c+¦¿sÚBR{"Šüø•@YUá'”È(‚¨6gb ÿÍs¾ƒIÿolFì×M§~© H"Xdt•¬ Ï‚›oíª‘Gu8ÌL¤Ü}Oä7ù!tU²ÕCµG;õ0rÏ_C&p‰h9ˆ‘Ðú*z(Ķ(£õw£áI6@´&€U”,!µ!3œ´sèŽÊÈÉ-Òÿòåö€GmÕâ_eÉðÛú¿Êÿ„ÅkÈQG³ØÑÃÑ¿Ú8½¨þ5Ù¡6} ÔŒµ»ÖòåQ NÀ(Li5ušsç–ˆeªƒÖQ‰+½·e›*™$7ß¹ ô=PÏï=Ã÷S8æ»Ç“žíÆS8é5?(Œ¨ñÀ¬Ó:÷U÷ž1Kª½æ·ó•7ßtÍïmîf¸®¿>A×aÐ:cÖÝS¤Ï‚êRÏÑõo€OàJÄ…W¸è­V¾4|„˜óÀ28 wz þ&ñr䢅;QÁÙž \—öÝïn³0k::c ¢è.MÖ•Z‚u³ÏËÿÞöºÁØ Ã´Ã̹áºÿWû\b·»™ÄÝú;Dº>FB [€°{ñ£|[¿o½ínß6*ÔÅòJÞz;2?õ×áu‡YmZ`כՀû½y Ù÷•HôÙ“Ýü¡fAi/ẻ\I-VB9^Ë4òy‘«oP"B!%Â\K4gî?Z²rXÈÅZ 6qSŒÜ•.È>üÚƒš™?Õ^/¥é6]û%±5[?O¡e[Dº'AÚ2RØ6¶‡ÃÓËêÉ}×Çâûýùþ°ûV¤ôéÿV”¦_$`ÜŠ†FZ·ëqqüÎæôþOÄíËm‡SÉÄ$Cð8ê;V’ü©_÷S-ÂJìé%ÓF²A–`¦0/ä¢KÕ9C¡VlÒ¡tË‚M(õHzÅb4d*qäüQñÁ¿‰HÙ\Nê=J ¾6Ž–ÜÉŒˆ†BNþ½Ô—O¯|Ž£uÊÃ9gmÚÞÃJ…2ÝÃÏ€¬ª4À¼p6þ¯Ú¬fF,Süh#BHHê_äôÔ²ö$aŒg x4y²±íJéH²_­¿¿µŸ‹½ù¨‰šžT’o_ G vVfzNa¤!–ívwK  ÓÄÕ2[Ù“R÷Ãe VÛ!øz|/Adûùn[iÜ+(?7hÇxlg‹»žôu·^›©Ì²³â[“g êÍZÄ\Ð#ͦ¼üÞV €”” ¿¶Y l†/tRé/(:Õ*ÊùÒ µ_°'¦§w|”ö«ù¤éÏΩçè8ë¹Ñ#ñ%ßÄ¡^·Û:W„£^Æ/1 }êBoC|²ÀH'Œ®Fl´FŸ±óG(›öˆ¨Í}+±R]àûì.—Ñ)H“Ùå™aÛܦŠÕÊÍbJ»Ž¡×2KHÏ(ÀŠùÍüþÐÉ%pVºØÑ„Ÿ7.;Nd*ùU’z#)gÕS¥‡gpƒàbŒYëy‘xI|â·d%"‚REw[­N–cmA°yWäúÓ+yÿ›°IS«­™"p¿™ÂáªXwY-màû4­Á²::clßжÞè¶[R=º–fõAª&ÿFìÕJxvÜàfü ú¥)3å*»f¥ÐØBŽKj’r &âóô±ëNÍWq±‚pnªnöôH…å° )šó VFë¦Ãø>|, Bø³½­»vIágõškÁNHu²•ôoZ”ŒëC —C2‰êÍ6{F~Ï寸è¥tÊACíÒò¡\ƒeñZÙóUxñ ÔT™?gŸ/ñK•ÅÉÒCu+© S å…\óT{€àoÂÓÄJ› ¶ÎxÉb®Ôj„%FO„÷swÇä¯û³îÈy‹*ì:«»@ûçøÀ—§­ý\§r¶ôŽUôNí?/£­Lü½N¨k{j¹Rúc¬Ëó6|$[üV÷§¢Î Êë^! –J–›"—ç£=]î~^Ô«˜TYÝ¿×Kc¿m‰«÷Þ›•¾ ‡?ç)®È¦=Ýîýþ†’$;?/ÄM]íAÎú—ÕØÞ¯›ˆÄ™É[Ÿ¿­ª*Z;x€ÈÎÈÖ´e:Ë¢ܘڑ@çœgnò£YÁë ÿu$5ý$N qÅPÂ]RßÀô±3¿I†Ï¯“BLg¶Ú]M{¡ñ Ó Ô¢.äà4fÃDÈ?ª™"kÝâŽôº3Í÷Ϊš„:g 0¹]~œA‹„ñ+»3¥®À Ÿ'ãöÏ °<É/ ¤Øâ–•IÀ#sU#ÿ’Qøh&¬8®¼ÔÒxN¨þqÔ$ àìUóÁ›«BG ²_»3f'tü6+šâEñ+‘­vÖË©±µûéš9ãŽsŸèe·~-ó0ô&ã&3‡€Õû¤3e´•íæê”«¿/»œÞ£Þ³VŸ2Pnƒâh9ö"L­Ñ‡Š•×R§@¢1PI´jE¢ÕÞÀ¨ùóß ¥:Nðæè4W›ÅdmµG¯„k}«´ 9OÚWb^_ÙRò×R‘hcͺýë6E’3ˆP5}E(êv¤ÄŽ$G–ʱY^r¥¨–Ó‚®/YšÊ“4fTÕ£  M£I_§ˆ$‰' ¼îwAgwÙŸêȪÿ5Þó7zN^ÇK£ ºÎ:w'È¿—ýž‚$7ÙÆl$3ÈzúK|©j Ô<®¶›ã€å å¦Ô‡ßmÐÕ ›Ç¼¯é‹š3]3ËD«¾Óü‡Cr¸6Íò¬VôŒ¤+Ám)ê„Üð“‘3¢(uÇAÚà6"¡~´Õ›v¡ê'¡\ˆHŠdDÍ!y<… زª[9`ecèX“žÊý¦?o:%Ù¤]߬ÝXçj"‚¡[ùkÅöŒnwÎ(yRhBƒÏdCUÂ1_wt¯W‹Ë6¥kÊÇ;B_'­òÁ ­X•ƒœO5†ÇWý{ÏpÒêé”Å2ÌhöE¯ÉP ÷Gñ¢O†¿µys×Ö†%Òy¥«@~)H SÔ…ÄäF:Òvƒn¤çißèÆŸkÉ|k¿…ÌÒ¡²ÊåŠD¯[Á´©¥§† ÂLE럿Bòôê!j(—Ü¡¹ßºàIÀ¸5ùl0¬ùŽái0¹;I´Ý¼îI6ØfŒ££¢¥k0º)ô;Î0ž Sé”VáÉo`ˆC™UlÅž÷}·Ÿá¦^lÑnfócIéùÒ)ɱΠ‚_¥#±^öbœåÈÇ}ñ™PWµ’*pP&ç”ÏÄŽa-\QQFпÍ7‘üüÅÜ­ˆ×_v/¨™Šl DÜšjU±1¹Ao#j¥S  DƒŸCèÈý†‰ÆmÚkV¶6ó¼–?JÍ¿`x™ëllØ 5¯<õðŒèb7žÍر¡g=Êgma<4ó*›"›+fÔ¼6ÔÜoɳ©¡šrŽ1’Ï÷Vs]öM(Õ2²6 «’7_m+Ó@Z££1kevg¨ÀÏ]ÿ¤£ÕÄF«qÃKJUöÌŒhÀVU4gý«l°nФ£K¡yEfù2{<Ì“¦þ‹Ó“À Šg¯¡ ažâ¬y‰¯z 5ý±©m74|nϦæC_Ë®ã“ÐËpˆ‚5ÓÉöU xr%x5j‡M®ˆr ö›ªÈ o…wwQ¨’ÊÈ›;Ä»šx€6Œõ1V|/ˆùz'Ӫ槡;„¥;Þ…µFÅXX„j±Ky¼ÍåkÀZ?;iÕ©ÅÓ¤¯ËÁ\ ¬Óa÷ê|yÕ´aèàÒÔ]êÚØv$À e½¹$­ã)› ¾p©rGB«ñ¬}!¡n¾ãÍ"o WW¨¥®eÙ—k"J°]ÈpFsŽ^é þPÎzQ|/P]#áÑW+fœ–÷¯]‡¯SÝ3äžúøi71@1Â/lÍîK!û¡–œõñ¹ŒžÅ^kæ»°‡W‰–@¥ŠjñÍŠáØbô0 œðœfiUc“^*fñž ÕY¶ÉˆdTȉöØêzÛfÍ~¿"AJ9ÕTë§sÌ•W“äG™ÈÍV¤Ez))GÑA×nòqÈ^•*År÷lùÝ­)ÍÏ…0~!8¾Ùé`ºíëqí°@k°ô°©D3[hlp„7xçmZá* fZ üE3" ÝÐb{D²“E!-ÛÍíeTiçp¸¢9‰…´ˆ¡jµÔD8ýÝdRlÉ®£Ç”Y¤n1˜Œ?<ë…÷Û«--â­Îyª¦ÂZit¯’æñÕ]?¥³^ñÖ t÷9¶Àÿ¸7I™ÿ΢‰å…˜ضÿ[×Òú: ×ùÆÂäf'â+ó—U•†cˆ€¶Â¨q_?ÌéRê+¼rQÏòÚi²êÔºù‚›&¾—pŠ×T=;q™ÒùŠØ k„Ê bá}zuò7‚ ˜Èdûøâ˜Wtô›å8WAh\öî±…kÏ»äYa³ Iã*÷Ö 9yøGmfóXÚÇä4-„±IÖ}Ö`K,ÄK’©G»u‡ƒ6sWî¶þä¦Ñm%d¡5hˆo¡Šv¿¹ŒŽfÁ3GÚ~ÝtJ5XBr)K=®ãÊbZkÎø@©§>±ìQ.zªýS²63ËíÅ©IG‘ƒ€îºXCFY±òf=ÁwOM^Êô4+¿Ï‚ŠŽŠêÿŠgØùcŽæÉBü`¶Ü"þ 2É÷Zíq5S‘ÑË"#Wzü!li0“J‰üàï¹»hásöëjÑŽ…Ÿà@¹ÿ³³H¢£Öx{%¶˜Ó°eÁìK‰jv(X𠈫«X¹TKVÓa±U=й;…€D'𯄠ڰåKK|öÏÈã §¦·+H+ëÈÒf´ô3¸ÝñBEBئËåtͰ'eÌ&h½$'µ ŒÁ¬3XoµÒ ™,ÕG\Ÿ¥T·<<¡Uy%Oª¸¢7GÓԺ،”Õ°‚´F§-9JîZ'ýý##‡y"R¦ÕÕ/zð3œ/ YJæ[&axàÈ\9«ÄÄ¥|›nUìjfÒ\©™b;bž¬ö´¹Üï²Zµ:5`#Ivø<} N?®@¶&‹EçĺEžš¡ã¯$–}Ù¢Ÿ+ƒ|ÚÔÌ÷0½ÆYòð„›Q_93fä Š²ýHJ¡÷c0]Õ±—¾4â߇ÝOÂdlTŒáÖõß禠„rø%殳-Ö‹¹"/Ž_©oΙ3b>çõê8àâ¬.ŠèrÆÆ…GÌ›èÖÝl£K+tf1Ãë8õ¢]lZ%yÐÒ±| °±µ2ÖÈÊ f»+Áïûôl}Ø.ó…´W›„4³DŸîH§ó™aD‹©”p3·`TÝÃË dHið‚»%¥Û0 í˜Õ2'a!ì7JZ$a¡½ß’%þQx r[Ô=-†Ô­ í¿¢m NY@ÍujZop® Iè._’\kÔP$JA.Þ4Ø2穃®…%"Ì_>žª‡Ð—Îyìk™ìI=µÁøõ°5$È`°añµr­o]È( ¦^ÿ(ïÞUƒDëjWùc8e¶Iœ®}%¿@9𠑤šÂº, #;þ)JKx‘ÕÈ+êËMÝyÃoâ6šd¬%ŠkúÔÏ‘ûaõGêj,p-Õ²TT“%ç%TcE‚Œù"V|*2($Ê.Œ4“ÌGWŠA…Mb|Kd¥–¬¼7P`Ÿ¿Ã §3:Ç9L®=NŸ‹ë§ßŒ'£³9oë@º¬¨³¹÷²¨3:ÝD¾Ç%Ï? ¿ë猥´Ñã\&4ïŽ›ç ¨ß)º<ŸÁ©3Úɇ ©3û½'³Ù[×/ ¿ëé§²¹/³±Ùܤ)ºlœ£´Ù\ÞŸF>××i’]7Ù\3»½ *Öƒ âêÐÖoemécàú¢ÌØíÔòÁç±á5K2b-¢T'IYÍ|†g²Prü¢Ê|;ô$M˜)|ŠUlžµëÃpMö]TñXE{åzÿ[=ðîFÿç\ÙN 8¨»ÛÉæuFÈ.u, üâR#•ïý¶Šô—+WÍb]™Ë¬­åÅ´þE›1Á'!£#ÊÜïóùýÓž´èï<¥i ’µ^ 'ͪ†BÄKXæ5TAÕïn®ºn–;6*“Ëvõ­%=Ú]¸ûõ¨ôAY&òøÔ*Y8Q%ž,‘ÍÔñ RqçO])ÐTjˆç—©ì§†~Ò¡I·{wÿtžv_oM–r‰žþ¯Gg›2l¸Jó¢z©ÐB®Y½¬QÅʺTšNCÿ`¼(•½0Áæûõ[´!­Õ¢ÔnwóhËõp¼À*$o²®™{*4Ô¬-îÙ×à—Ë¢a1ÍnmÄw"2€ Šiwb¡°§€J›0¨âurf ‡7TŸyy!bY§½’ñE¥”&a #­© „Y]3KáHŸ•êÎ/® PÖÕa>4¤‡Ã[,’~†¡‰Zdà¹È1âδ[6º" ±¼q0³ >U0¿oàψ®"–§ÊËl¶Ž q…§ˆZ2;)øØ ||Ò9§#ŒýR5>~,{ª/~‹€üsÈ™W^Ò`α²ÀZú¨Ñ=EF¡¢ÝT²a¨ž-71Ù"Ï2jÐÒLI)!­ÕÚ 4)q%¨t²Š¿yeú %Ïúw²ùÓð<bC…0æ_…z¸Ü¯ð½Û§£¦MžouÞƒv˜ªë%Õ²?˜Í—Ñ WÒ"k_—‘ç2ÀÚ¬Ò=VË!:ÈÉè ‡/ÇÛ ]¶¶!¹$gúáïhæõÞ¨‘×ô†,PŠ„—‰oéUõÖµ­=—r:µQ¾Œ^üì–DUÑíY‡°Y¡c’&ÑHºAµµG­Ýáðó9¼šXr€Ct ³TŽ@ÒœSâCµ’¼Ï£9¢¤qõq÷Ôßת`›oñkß B%1¾çʬáEpêÄÃÐÙ¸ªÄ}x¼ìÊ]CÀŒ ®cË8pÒCªò(¹V¼aeÙG ÷»keÎÒ£Q î…ÜóÕ]ÆHA²cA†š‰'S<\$»Ì³’X.%•2úi'µêA“ ×þzº&Å‹vׇ¢Ž†5‚­ílZÝDXìöCÉkõ8{•ÿ;‘fº¤çYGõ»(áM¸¶K¿¨ ¸i²›°×aw¼ä[¥%áNÑ< ÿhbP © Qá³!yÿÉx4ˆ"âe3×ó¾¢þ5—¯&Š0ñÃ%öø%c™£š$Ú(k! 2•¹h#ê§·FaSJäV¢Û(ØË„º‚V-ËCúF)ð·{<šÝÃpn•Û¡O¾½˜É<«p‹_ ™rúœÚ% -¡3˜JèfÑ>úƒ9ÓñE#Ó͵c[ÇÁ5ª–2‰)W¹ïÞð_#rvK',=2¯]µ¼š&’ÜŽFô·ÛDR/ªœƒêšÞŒ'Y0á4t-»:M<-Ê?b§@Ú%òã ¢atŒ‚–“ÄÆ˜o EkùE;ƒ Kß9C`&3_ج·zÈzŠ$ZCà9mÜÐËÏ𙼡S¾±PUëÎF_ ¬¼ÁÓ¾‘Sµúk_à¬?Œ¬ÿªzOaÑîèïÔ¬=ëÏA&»´ŠÓ-Žp†O ¬rOܬê 1W8¨5ýŸ«šËÉ%Ò ÎŒ Öò9tf XëßĬÑ:Zëß̬~XY}A3súOMëßjëߨ¬,¬~hY}a3sFO{úOo ¾Ø‹£}‘3uFOqÚæÞ¿yúOtõÁ´Fˆ,Ì`hX.Múî9ëφMhY?ëØS탢/–2ï뢿ÌK—O„¬hõÍâ½…´£lõo:4óŸxXÆ^ƒX„Òõ?:ÀßÏ»a?õµnüRþ&¯#¾¹É¯ÕkK4‚Žªæ‚³¿ÎˆM\\žõ'cÁ‰í‚ÆèÄœM|ÿ'ÁòÀXq®ñî`€üÿ›`ij`älçèAkïQ£ncGÈÿ;[‡nà ïl l‘B<\g] ƒW® Õ\¶o"¤{åÕ\Y=Ú>OIšE>Ÿ?Љ&_}Ž€«¸NßÓhËHÄ›ùžœÏ$/WÔL~äo.µî­ª9Ævv–53J–ZMwüÈ-pùí`¿‡öÃDl­RÊ](PìG*}¨l¦…Ïð9­!j ,¤!i¤‰Þ‹5Á|4Åšžn^ln2›œjjN¦ÎÁ•í„á@ˆÝ­ ÔÔ¥K±i~t¤ÿødÎ×óÐΖ ý =jÏ‡É M´_ò6å¨6Æ*ÒY@×Àü@ÌdPì„Ôä¯Ö/X¬,-ÔªO0E‡ÔX\¬W~ÌŽ^¡Íd‚ªDÐᱞi‘SÉI±'j—*3P§ÕÓhaÿ2j>D.™嶋߸ .ÞЪ Î•üG2…ELS*F€<ãQNX³rh3ÜÞËÐi*aÛÁ~d,3‡Î§åh€X*:[ž,ÈŠ™!ñ¨FHQIJÇ.Wë#ÝÔ²ïKœ*• ù)ž§hÞïE6`Ìí‰ûšûy£ÝŽÇó¼Ã†÷y°V¿'‡‡GCèr’Dnš}Êꥊ ª-P‰>z¶8M£‘·Óp‚ïÊ<ŽhQ–`t;“%öT ”øO…_Þ±zúºÍˆÁ‚*&«°°%ŠMÈ)+*QسÌBdìˆmQzÆiÜ ù.¼‘8úøˆï ‰i<Ø“Â>©›±ˆÂ©_‰’¾Vò7l ×- B (»6Ⱥ˜Vˆ7– „j"9¼!E‘Ÿ+©VÔ¤Ò ®T€<ëÌv-€´)øŠ\®&yH©Â«LïôñF‰p© pL°mGH÷fb-ãJ -U™ÊÄõ¾‚ÆüpÃzdë±·V2Ͱ–}§—#?á¦g«GÇ G¼p¼AïºsUfèR?`ÄIððïc{™+Ð,±"aãP[nš&a~™«°:u:Wâ=ÎÞ&ò:e¦Â?Î Á·õÄ~€=âþ]žôaI‰Â©MÜÝ’šXáXH¿#EÙ =ÕNŒï>ú#0‚ ÔAX°ºö]”XÝ·b±Ò=$˜­…$Ã35!½ ¬¸–™Ä)jxcÐ)GL©ä†:'†ÁÏ´}Ž :\PŽG šìý57=ÒN[¢ibŠtªb¾¶ÚÍËŽg+ÅÊdâÅ/sz5›&ßÒÖn€id¥‹¢‘ìmczæöa4ù^0ý[ìŽ6£óJ‹L 5´NoáÛòsçý,Ÿ%x‚ZJêáº3›P"¥¥Øfè93Âa·ê ñ×Nel×ãÙºrÚú¤Fš3ÕŒÂl¯ÆÌ¸çî·H(}ÍVøÓìγ >ÑUcóàHæI¼\§ÒhKå]â»z¢ö’ãûP U‡iÿù@v-RQ©ÛɈ£ÀÊ_y{Kë(¼²+WÙY’5‰prýª¨âvË ½Þ%£Òã<åArê^Sçá00SÜfæ'D|z”|æã@<ÎÝdíŸo%íú…øƒpx7ËÇ+rÑ÷4eÈ“^BS}×2Å­u‚ºÜ¡KXãøÿ& +{®å€ÔÀþOÿ?ŒjT¥œ¶Dmù“Å%–¿l6m¦Xà)&47f­p¶'UZ•AE76+6A„‡rÕÓFe³'8Ž3¿ÃŽñŠ)ö§õÖ'óÄO¥w€¡ ¨f)ˆ®?J»Êþ[XÀ}Ñ1ß~/³~[ñ>`/Nb|Zàøó|àìþöþûûë ¼rR÷ÎðëøÐx@ `ª` €ª€ ÐÖèÃõ5Júðïäá”õ•p€xÀ °ÖÛÑSýdüÒ@YÌGÿî)à—úKgþSƒÐáúM¦Ü@Þ°ÃÜõðPý¨Ðœÿ¼†ëI˜jø@ 1Ö1âù`¡ë™L¹a½A‡5ôÍfÜÜa†9ôϦÝß=àÑüuè(ö«ðiºÌG—úéÚ¡¯1æê‚Ú®ÿ…Wõ×b:£¾³À¯è€7€YÏä;õcágögâ¯Ô­´"­“†cõÛú¯µ‚ʪ‡ÖÐÁO¡‘èÿùëöèo¹c×|òÖ»ýT\1«¬+$Žßѧ( Þ­JAÄY:þ\ ÙÝé¿ýŒqY6òõ¶¹ùùùÜáÔ½ØÛµ‘¿éÿ-%FÙüZÞ=Ù"‡yÕø 8©ÂËlÉ}BÐú±]r_£ŽØwxq?/‰)l{펉ÁË-nG¡ àŠúv¯u^MÁ›-;zgƒ`¾ÅD/l'ìU¶“Â-Òwouê;LÀ}Óž? óÂé­ù]—y]\5vÕ¬E^ë²EïüM}Õ· A— É}Oëý‹lÍC]X[õµ"Cª2Љ‘6'okõ[R©ÓŸÀÊ"+¨Îl•§zíÚA##i“lƒ^¦2Q9ÆUMÀxÙ/ùå¸_ I¾¯,VBªß‚Û S¸.xbÕ¯dæ÷AX<0;jå5Ž}øËP¦›Äya'ëà\e!tKÒ:%da/ÝÙ|ÒÚßÒš©_²øÌ{^Œ—ïC­Ü ?,u¨ñ8nSIì5wÖ›GxÎé }0Ï74’Ÿ5º¯’œOy΀º€=Já­÷/|°†X”ÊÇÔUd©XŠudЭÞ®EÇ` —#iùóÇÄo1*âP¢ ÷…¬tVlNdâ`ä­Âã‡É]Ƙɰœ>™vŸ•ŽOh<ä´¿¶ ¢¼µ™U01j7tãa’ƒÑ2ÏÌuÁ'Î(rš¸¯¸;Wž([3ÁÃѶL(íãÄ%1ν úˆ5–M˜†3Ie®aè´X´ßÞ‡¶<_9¦kQï¹°‹••[¼íÚÃÿ³m÷+,X뺭„,b°`¡÷„Ûj–œïÓa1)ˆqPÛÛÁîž?«Íl˜7 ìu]©¹ñ9ªä€Ò!8÷R›Ñ:§àë°\ƒøÜP‰="q÷üx•~Z÷ì¨IÚ¾;¬U{ÏøÞ‰6¾Ð½¹V¾ME­ÄC‰5:jjäCuƺ ÓfÒ@e£e½eÈFãZÕÒÔFãÊ5ƺ†0div©=“܈يó8]V*ºË÷ä!¬®%GšH¿‘T0ÈEëá~_¼ÅŠGòÒ+¬æ–_Ö´iݪ½¡’îº$Ú¢š~YY¢½Y§"¨QÊjËÌj1ã¿™Õ{¹éL^ÁfAX2ËU*'h‹½,"Ånøic« íÅ-Íý`ëƈ‹&Œ{ä€îRH‹quN+.\rá<ÚXþëPI<$ ÿÆ]]Q<ŠÄWž©%œ&øuL¥“c%&¡ùåüÓ$j£'êP†š~ú v@žJùÆ’¦*Hû?e8ãÍÇÏ .þ ¬<†ôh]€²©þzxLCàù2G 1d¯:4̧K´Ñßò´ƒ-ÌÒÕ iFh —å’x½º›‚x™t<×™“söüqÌÓk}ØBhWƒ˜…ø,>{Îkso{½ÅÆK†’ùZ5ÇñµšÓÄsõ¹éZúÊnc £1E£ø¯²)ÜѶTè/jìø÷2Ãù’]IƒHì͘Èê·óøéìZwKièº"ad~[M¸›«¼áÑ4f›{—Ž®8¬m¦¨]¯Q¬ÇéÄÕ7ýs~4QT”"¨»S&¢t&~»¥1»,èÅvóÙ}Êtt‹•g·ã±V&Y‹5mwêit«9C”Ãmä”ÀäX$«îÇZ—7w-¾[ª¼ïËø)VH'|W¨ì¤“4é¶Ùi7£yï(èI¶ˆy¿MóãÿJùwíW´ÍÜíÆøqÿ#ähbbèdüãn¸-þC\êÞ •çþL|vü¾Á„*ûÁ2y£qÓhªça±1   "„> <òÚ«ßÞÛ'@g˜Ëº×ò*!BLÑ×K×'ŽÑ]L“{jRПzIr7Lá GúùžGëS4öè)ÌiTv4ØOÉ»A6©Ãå©vC pœ(ÆË壅ä#( xÄMù÷"Iß"sС±1œ8”úaÒÊÿú0U£3q¤Sã$¹?##” äyŽXR6Âá "˜í±B#ì#¨•(@àð°wN—üfÈ#$÷̲ãðIòAËø“ñö]ÝšÕ–Éø¦ç]X·þª7'×%à`õ¤ò"5ÅÏ›J Î‚÷É,EÆXsŒ?vâ¼3†1ù4ôü $:uP¶…*Ù¤é6׉õÔx˜L¯¾åý¡XµÌ.™<9ÔÄ<îw{pùˆ,ô“­¹.÷ÉÅ$UnmÐøL7~üm¥mxæUááf>É{£š¼)«%ü.BšÒ._¹ak¾¹*\eæR—öjòÛ›2ôÑv;ÊûÎ,uLšLˆGX/±/_—?iì"ó®.Ã,<ÊV“Êiã+Õïx€u ŸÖ‰9‰pJ¸‰™Ù”Î|¥‘ü—Þ¬®¾ Œ±YwSÐL&I,’°À-ûMt(Ó;ÿpJ Å&&ü9‹ú(m¥óäâý€+#ö9 [F¸f#YGF¼Ût¹î»œ??Ñu9/2»œZòÑóôʪ–â¼D_¿£¬S­-GDÝæ¿Z PZþºëºÆf¼Ñ\¬W&r³e ÌYÂÙÇqEQ‹´q1”س|QÆwå*9§º<ÏþkóUJÉ$*p‘b - Ÿ×Æ ‹uŒç«nŸÈêD E`›áêøÃú»´n$ MÈÂP"Uæ³æ=åp0›~£Ê@7Ê%)µ„à+ÝA£X#)``%>‚û•Q>å+H¤‹²ôÄYýM <= RJ*¥…W›þ]$ÿÊÏ¥ÈkhPÙÖkpî…+®¾Ã߃Ë]ÒÁ¶dd!tñ-òé¯%«á‹S•À‚ÛsÑê ³»Rƒ·W‡ê8wüÁèu =Qé¶'îõK—ãiQ(£°0±¶—[iá+æ‚ê†aÚXùñÛ  ?÷eâÅiÕIÀ91Ég-[~ùf Ü¥®£ÒTÁúU{`Ü7„BÈ ²J\Tø ð«Ç†” *µ@’²–ÆÁEnl åBíÂÖN˜öGÇ¡cé ¼'Ô¹_¯al[iPƒ õ#VÀµiˆ½¦gî˜ØÀšïÆŽ«mõ¢N•È•'hØÙï•ó@¢%æÒˆÕ )µXà6Ò°˜|Î4áF6È§Ï cŽ?L×xKb~ýô×ÊK;Ô4Mr"pƺܑ?eÒÀ¦:¬æ?„+RŽÇÍ_Ì Éü~kœÀÊlÄvìñ'\¿¶Fñ˜† š1jGv<²×æ¬-‘U‹oÍ ¨C hÃ÷§ÚOmˆæÔ¦! `3-OÄpìš’0›2 G%ø’eÀ‚‚è0,Ñèþ-:Âr¤ªò®æ–-Âþ$œKy«ÃŠŽW ¨]Ëÿ°r+Jå‚ÕGA:¢>ªëïDtt’ß#‚f0­—NJ¾™Ç }ÝÉéI˜§ÓÍCÉÿÎ[¸•ú£ C ¬Ð ˆùÚËõ6ñÃ>ÌSP¾‹Ó O‹oÝáÆ²ŒÑÆN}¾1…F,ÐçîÆ…ûfðaØÝ»œ‰b³ã›œ+¢&¶jïR¬~¬åû¬àw)FÝŠ´Lɳ0/ïÅözË´Æ„}ÅT¸‰s™²va÷´€Ûç¡ UC³ËÄu (;45Nbï,‰çØôF›VÊì´ž†‚­M/"ðg£Íàˆ„ˆÜî$ëV1YPˆ½s˜—X§BpdvÙ»€®f߮Б¿ñj\Èãé¯ë)ÒgåÜíØˆ ~¾ýœ[ ––››£š™VêäÛ;xŒréÓ”Eñ»æÜ]ÛUÛ_$ÇêE?¯ë¢|ëÐôƒivÅÚW½ßñéƒVi@ {K×pk^ƒp«‹¼{x:àõûÐvs{¯±‡ê7Âåž@ ž¤Ùv½X•LÕÆuš3Í£¨l<`z+Iû4"-–ú -0k†A‹â™¤‡›Ö¢á´8Æ·å¶ŸØL1ŒÑºß<gºÀ«f àå§š!Ów²ÞADYô¢‚F}i—N†·)V/V%—C¶+1ñ\77«B?>öBÈüaÒ³æÇÈî—/J”Ž„#O2æ«YskgK;b'Ðt䵬Já<6{Ô^£í†îˆÐèÄ(™‚öYf}K?Ʋ¢ÊMs7bŠ1wSW ¥“·ÖðÿꨢDPE Ö¤­]þŠ=Î_ž(%WÚÿçÀÝ¥î/»”l /xdtWäfzû‘pÎ0ÌôèEwéÀž.¹ïÎíš?ëøxöïûÞZ©” †×ž²¾ÜHÕ&-­_.}{nCþe±ÇJ¯°pÿÊôA{5?®®ÖyùTº9ñ$ä^¨¸TÇêw÷Tò3¿EÕ±Øù—²WCDÔŰV˜­R^9(0zQ/³9HK€úï£êc&~u9æ´™ç|5ó<Á`,‘r]ªgÒÄ´ƒÓKLî¶(“Ê FŒuÉ“}y¨ –¨Ìê«ÒS}…ÿ#iž9€í> ƈY)ÿ²¤`ËÒ'c¬*rtE2r¥KõL·g#Y’J~MR B;EG攄.’± @Óõ£O‡ ä|j`eýÐYè3û&YO“;¦=Wå÷M¼¾ÇÓ´ þÜÿkuW5Eš½@5[uNyšã¯V¨68šZÔ¸é$öê‹ýïüJ§ë÷$]±8“ýD®¯…OGh“‚ò)’h<¨i蹇v| °eJ»hqÙbá0Vû“WØÛ_^AÈ*y¥LúF7Hë8ÈVæa÷ÕöÒ“#–cþ–@_·˜C×Pcü9ˆ!ÿÈ¥õ$X@ˆt•ò> š—]#ïØ°ë=Ú„ôAƒúÕ¯in^ã±U{Sx—Àý±3ãPw/ÿ½ÔæÇÄÒå$KexOf+1”Z6qLA«›Ýµ”LìLÇ0ÞüS¤l‰d ' Ñ# SJ|“”r—WУq…e_¤^µ‚%lI­l(r ßùă×3su<¹n“©_U €Ó¡ó{/_I‘Œn4è§Ë³Pƹ`M¹ˆ§=+"êû›É‚¯Àfñd•ÀÄ„¡tÄÝ; ÷ýÄ?Nüd…ÈY B¥Xš ZÏ<à–tÌœx8½Ý=µfEt¦b=ØdâäA¹…úX9„ŠgiÞ(ª¿¾¦uyD6)õ¤ ÄÐS>¿¬Kèì%%Ö\Y=T)+èÜðl;w§Rx½0i%¿}ÌßeòùɳÌrL 7¾U%Žˆ2„L¾}\È©3Äq…LíŠ s€œÎ8w§íò!¸“ˆG<,jÆÆÆïq˜éÔµí úÌÓ2òx¹Ò’†òÙ:¤$a~ßbúDFJºÜµY£ÁΣó}ú½ÕZ{×hµ´[)bÐzRD2G'‚½S§Tk¤mñÑ[ÌøÄVîE¶O(sâa·6ÑGS¥¡\-&Ó Ë}d4ë¨áú ³^ j«%ÎEÊÐ2uF'ŸŒ¢dv_…Laœo³Í[›Éªñ±»"j"¤*Ë¢uDÞêHL–™Ägúûù{½ãÝ¿îÎ>1ÿõæeàca,hÏ$@¡¦?ëÍììñÊöæªjOvWN$à ßr¬ ª¥‡4¡sØ…±Wä”Å¿®2¿ ìe†M@w…?8^”*ݪÑUóCr/½$Y׬—Ó•cVIœŒtòµ71Š—9˜¨ý­A±À Q :"<ãb‹tìWœ¿Êé;´ÂA+¼,lnðÌÁEÆ, MšÆRÉ.]r°êüQ æ‰'Ú½2ŒüнÇa~»Ø:ÔU§G¿ ·ú«û›u­ÂËJˆWÞB!ƒ°µö’ à93É”Nü4£ª¿ÒÕ¾+®¼ßøiE%üײc›"ç¥Kei’ï(6åm÷4qÎmM<º?õ£² ƒ%˜Íz±;V7CÁ÷'Ù÷pÂ/÷hŽö÷ú`êÛ#‚ïëý ‰Š{=Ñ5¹š÷ÒBžÜƒàìSL&õe®S?³7Èø2Oya;νâE&Æ8“×!ÅG$CE44J̶dùaY¯fÑŠŽ÷D²ì39 öÂLó›â,Š–÷ #ùxìáÞË÷â ê¿£öýÁNʬ–…“¤…†F7ZZˆÃÒ+kX"GX°øŒˆËJ)©Î‘MDi·MÄ^ÞÐ{„’^ué™%P„wùÚ!.SM—Õ…'h§¹¦¹팣 è (ÿÁÙIþ°˜¡ùÑëj X(ý—hX,Å™+7KYP [åÌý»å|3Ûè­Ó»edpSërÅè49´¿(Ûwm£Æ^D EáÍ‚‘ÓAÅ8—–:ü/@€®Aô¯*Ó¥žN©å‰ $è)K¹+&wƒ½³ËCƧåtÉ*S¨h£ð¯ûÑôƒ”I¥/K0bÄm¢µºŒV³ch¶ ä&¿ÐYÁ#“K¬öÝõî)±ëKvg¢kþöc ­²i–Ò©Öç–@Boz§ã^gv“³a¦ Ìd–U®âÃp8C*«Xíí‘þ ˜÷3ÒÈ5„;ÔùwL’3\2ÛKÚ)a‹vmÂà.,”ªžõø¦Axì›çA­@e ùáW’XN_I ø+ÚöNi ‹55þä[aÑœóleØÔçFiõó³ËöZï E™ø|¥†Hfj~9Ò¸0öòZº¨ ¸rKèyÖøÍÙJˆBŸQ™$š¨ /ðDfõÎtíñø–Î~”1«I´ù#OÖŒ:H€âh‘oÕ–…:™QËÉ–©§móY¶ð Ó¥öPðßTyÚ~ €šXÄJqjí]sôº…(“W© Æ6B¤ã÷ÉNM.QVklOgoû‡×m—a°0é!g–\¡Gµ°yÝ^ Šì ßøá|3BòR…›õUØ«óYCAü¦49e¤Œgl‘ˆcÔ´OSxu’qìæV‹lasü?S5SbeWuè-Õ/µØ¸™1Ê)SrŒ}]ÜÖzà ŒcKH„E,+ùåjPÒAP è1ê¼$Ù˜°t²J—'5žÄ…›„ ðëí¡?øÒäñ ìä(¶ÐYøo+S‰.Pu¯:ÀŒøØ¨aÍÆÇšHÒ$Ü;‘Q¾þò‹˜Ã‹BüvŒ\C¥ì+‚²a<Ûmºò®³¡Žž`ë1Ü#¾T©™aT†[Ú3Éž ˜èûŽ{´ÛÖs!úú±Y?ôAÚ%îÿø {o#Y$àUÞ µtT)M)ˆ|ýeðÕLMTâqjÁ¶Ò•Z¿ÂGú®çsôäbgDwÀ,°h?ÍÄÄŽ÷o6¾€ðwoø)/ºù88¿Nî÷¯»­Šß´„âÃ< "ž~^XE.òKg ±YžƒIЂ«“&Õü›‚¨±‰\É4kŠ©^gÓ7ÐÊÁtâ×6ÿrkSŒéÖ Ï °ƒfj¤‡¢×Ü-²AÖ¶ö‰ÔÌÑü7œêFåÿO¹B\¨.× ¥iž½*}êı|®¡i”ðÌ“I¼¶Q~d}™F¦î(:²¦Ý›È‘Ôf_èí þWTKUí¦:Ö 5Éò€ÚVþ}y`ˆ¹0šAþk:a~´êI€Ð†PÕތʒÅÅfèCÌE©Íó8Ñ› A6dE3gRÒSÃBqûeü|‡Ÿ]H?(çÄÁ3Ëã*2!žX2,Jl¶¼?oÛm65â¹(Pw´T¤Äùö^0ƒg>º0m\§¼–|˜)Ñšÿ®? r`rblÀ¾Þ YAtgY+~ÕŽ†—bCq¼Ù¯Éž8’ËÅÜ‹w'V—ïåUÏ}­gGÇmùëÂsîêæûàöþä©ggCw’b ÷_Y[ßß’χºö©¹á;D`¾À v,7eK",· leÀ­+n'b\çj™mÝ™§fÁ¥˜az‡›ô5£¼oí)K\³uűŽÀÙ,lý¥Î.@œ€ÃüKÈÔB$}[ñz̘¸º/­9b(oÍm²Šûˆ‚‰¿¹ÊéÙ×O¯W D$o¬77Sk54áÞN' ÝÎV›šŠØJ…œŒº.à‰ük‡*¶@h6WÀ D¦Ø,5j6ÏDxµó/y,#ßü¬âÙëx›¦ÅKÍ©ÖJdÊî¸ë8ó˜ãx3ý³*ÿûÚãù)Ï£Ö™[Dùݱþd– Ïzh"°Åüä ²5}vRµ>[ÊòÈY¯BúžáÍQŸø94í“ÅÞppŠWb,d%¾:<­§¿ÏEiAPÀCa;ñ’]Ž ÿLoç½ãÄ@QÁTÑí¦Æ!Ðíñ@Ž¡•{^dkÿâ“Bãvörøð<^= ê‡3>„ß©åT©Òˆ2Εž]¨Á<£‰}š8©b쳩åèø„^±ÎÄÀ–‰J§ÇÍlÀî…¸àyGæžGÎ9ìËvçJZºõÙ¡ºæ•-û °ÿô¯ÿú85óÇ mءXC0€sjP0ˆ»B0ˆ{J¯»–YDçÛ&º.ú¶—^»r0À; ó ñÉÆ“…øÒõEG{…ôm—cƒòÒî–ÙáÉç;ãÅ} ìÍ›â&¾?00à‡+­Ç; KâÞ{ñ¯¨¸À;zNâüAPÐÌ}G—q•gX®Ö¨ÎL?ŸƒçRf%beèE0yÐ(ÃCÇJÃ(—ŠFH–mÉ‚!#/¨)³=i¼êPè˜ö€R¥q.ôJÀVl³î:0UÝî®õIaAeO$¦:Lmý^‹ÆP •©‹¡dåáê1³àpZÛ¦ ´N&ßL*ù Ñ–?HóÀýT>@æÚt™ô,Ìûæ+:Øí8ï¸NÁ ö  Ñ¢ Ä,@ dsAt„kËØ~£ŠÍ½fÃd?8þ¯BXKcºÜO·ývÙ’»K¼#‰Éè<ãbR ÏÄü©|’y¸óV:zM/K ÍïofÝ 22DßU˜>„c]_£!1•„—èÏ$Ÿž€ÎdqîɆÎÕ+˜-u²pQ«ñ."WjQRÛ‘‘›#÷8!˜`(ó@ÁB~ )½ëb@™O ¢hÌzq,Sµä­gD5³,d?/v*4î?Jþqú\k±ÂI³´QjG6c¿dcÏ•jC'’^?Š*¤Vò€ërÜ×ù­vòéÈ£ %ÇèEZÜ:3ÎFå%Æ*ö#IËW¬ïY.a=ñ–MNá9Én3õ[–ÈÓ…›‹‘ËB~A—£„Ëþ£o¹Æœ°Î!¶BWî_R{ÓŽ=0•£œ!‹¸‘ˆì((HíòMmxTXyÉ“9â2‰#Ñh>ƒåñ¹¯Ÿiôí Ñêt:‚k °AR|ƒþ0C"¥°¡u“ѳ¢‡¨Ša×»ŠÃx:øã‚P8°Êö|ót=Ѝlγ]{KæÅG‚×0>ÎRLä´QŸ††™*£ËÖ¿RY¥ó.s·Vy1(DMÆ‘Š¯60ä(k6j%½hÌ%Á éC©1gŒÓ<ƒ¥× èýÉFeÆ¥Cºó4Þ¶êx^ƒJ¢aÅ8Ì1´½}šŢvé:°íÙþ°}ëМRLJTOÉdÓ~ìp¬ò…n@ä{CþÚ{}ôê”ÒÌIqvTôX&‹Î;Sâ(PéD Ó¡C4|õQ%à ™€Ë>#¹Qý#e%ÈU[ĬX3åÏô¼¬Ÿrl¦ˆ …N1J퇒ó’bós¦óÂÆ1(µiýõˆÿLç1®¿!5a÷Iš-:Dí±çGŒ•t n}t^¹3q•Ïåó¯Ò%5d†¢ÁЩMìÓ­ƒÄð-‹iIü ÒÂwGS+òõÁx.¯;=X»Y÷"9´ææ²3ŽÙ°Xß =›Tkæ)ÙJ¾ì»ô„H ÈLýáowÒû‰"‹éÏÑŸ©UóÅÄõ pÓQ*ZF´¥%›‹ñSœÃ:öØgŽLEAÐRF9b9LcÑèøR‘~E¾h3M½s£ÞŸúM5XS²¹WÿUñ" ì3[ªŠX&Ì€­’Á»9޺禿VQk«•]ÜLf¿@QpîXvتèŠùpkÀ㾑DêD;]\F0®Lõé^ÏEÖZ™Y®«°AxÜ*LÙß)A#!aúNÀ[†ôR-?~óÑÌíMEºA?Ȫ*{œÜ{HNà—Lj¢ë:MnÞçÂßÍ㜠³@áJtÛS¬¹Ó,aú€ š$;F®™Æ' Xá*‚[XÀÔà²y‰Ü{éóWûÖ4-Dò¥ü­qúáÏâIŸý‰ŸÁ~]|ë ÀpÄœ(o a?â“P¶ž)|]^9Á#Ô€H¹z¦× €©Wœt”`µ‘Æ`œšò0Jxí^Ü¡tþg†Øº™Íüè]½,^F_ ½@õËÔèv¸…ºUœ‘í-_J”‘X€ã‚¥bèGž«LÑA‹ÓCH„‰ôIf¢W 2‡Ã«'d¨˜uâ”òÀa3”búë¤d&#¨2©”I`V¶œ¹!,÷×dL¾ º\, ù±C[%S²Ïa”."Ež8¯ä=¬r"óÿR?0³DÊ~ú¬0µS-Ÿ­C©yj¦®;ÿ”Ö•­ÉNqÃâ:¾:•oÆÛr;Ña·'ãöèÍÓãRV&|žj:M:.äif—Ä%ß /¯Aj9Ж)Õõøµ»Î3¡¢mŽy-ÑeM„vÜ‹vŽ{ ñ@êŠdšV¹%˜K²D1cúi"R‡ŠneãÒB= ŒÓÅpÓÈÆFÆá«kC‰VŸ¯ ÇlnêŸé_„Ÿ¬OiÑH†EÓ“™ig8_'íy±&3ôjÙ[sy¥ÜŒ®rÄp +ëÒêÄ ÕÄ“óÖ )pfnn–FNVëYv”íLNYæZž×oèlî5B‹Ó¯ ØÐEGV¦%î¥uçzÐMö¬Oþë2Ù哲ôÒôsp'‹—.Éo²PN†O#»LEûu’óOGfõdöaØF¡ÇÈ›í’d"*ù|µ¡œ¸ˆ0©bt?™óåt,÷RÇŠ³Ö<þ=‘¦³°øÛÀ1qŸ¢ç³Àq—:IJ@î"ñÿbì‚…–¥ÁcÛ¶mÛ¶mÛ¶mÛ¶mß±m[ó^ü11sg3wÓ‹^wwefgVe”ñʺˆ8«W–u ãe£.û B±»JÓ×蔓¦›i—ujhi^œ›Ñ:ÐÍê9œÀÏK}=ÅäU¾æ‡JÛ^àÞpéq8;võÖ†~\<ÉËÉë«ÝäI¯åÙþà-Ø{Ðû<Ìíx,ó4èrî‚þvf?Ìéq4ì@çéjt|¹ÍónäºÍÏ·ÜŒÁ΃ÒìœÍxÆ/,GŽºýCJ#Š}úeRV5­]ô)žÚ%h-TuV¯ c•Ks$›3luT©ŒVçËCœò Dõ_ßyvŠFáU‚/UT¦ë2€3”ß …Ðz»~¾Q­%.ÈBcCv¿·T½ÐiGWuyÇ5–Çã¥ë?¡<àþéº^nÏc²kIO»q¾0LDQ›”Ç;l ëä&i#æm!®2ÐÀxmX÷ƒz©ïã#vWö-Dd”C=—â¥$lfŽI)·ÿIc 9$§u¹*w;X(A?kï“ß.YBEî³-qÑúÆ)úI”“„¾$áMø$_K=F*k1,ý²­ýµç¢­(òPÑ"F‹·Õ-&=N°âêjðzPb%×\+S­œMšm´¡ˆÕá&°E£/©PzƒMu£DU~}²*h›Æj5‘›Ö“o]ñýI(Ù[tŒjZ¼Èl³Ñs­ªª$´ ªz†MkPKÏ#LGíº§Í^p¼$T T oI[2œˆR=»¿è¶Ø U>ø°ÜÙ5X卵I' k‰ ½Q¢|bÕ¦—.Þ\òßÒ/i†b8!Gú™cC¯ÐÞÊQÚ²CäMk?m` òn€æÉN7/%w)Âá®<1ÎlÅÏçi$ ã/è‘›8ÉÁ«\ª^RD©oh­½ºVVYVû×É"°Ž7u6 !rêZ;j+å·AqäôÒ¦&Œ—¬~+sI3®O»uÅzô›+ñz™j%/n/߬i<ÃaƒaìÂk3¤”sÁ>‡o*vAk¿Þ9Na&1Þä‰ñMi:qÎJcx$AÖvülýöì“ì÷IJõ8Œ~Þùp¸[Žsç¾.Åä'.Վ׃¯o–mű2y‘Ò„#¯çéð¹}ME9—f‹ ¯@mwß9=­Ž†nȘ†ur…‚lû£ÔBH¬élÝb{(}5ç¦S\«\çkå65™ ]>GÑ<(¨s™¤©ÙTd¸z¬,4Ü$5TsÜ‘(OÛ×B¿•èºÙ´Îúwþ\ªž1Ÿ£sì?O#¥Mˆ ÿ™Ñ,“¸Û#eå«á¨{ªÆ’’¥ªkÕ§™81âÂÎY{1_¨¿áRR)¢j'2íêYØÓÁÒOŒ* £¶-® XD{ª²»¥Îù)ÕÞO¨²DCF6Ö§ñÑYˆˆLA‰Z³~B2)¿pÔ EúT£‰ðº»x[vŸ8¼ÀhS·Ä’éJeÄÓÓäÑ*²Íéôssñaû:|X¸lÜcp‘:1' åO¾¥ùåôyû¹ôF7´JQntÂÊŠräŸÝ…¹£ƒ0xevP1I¬5Mu‡›ÂX»+èpÜ8Ú[qýñõå´ôõP… Ù=IX?Q£u:«÷úç¹MrŠC|¾³Í?4K1S;éÚTï  D˜Ú­3 C#ÌúÁšcå¯÷u*±¯¿Ozë‹û‘G@‡ºx÷õiŠp]–b^ë=>¼°&wëùÞ°¦+ü~ò™™,Ĭ5ŸB…Ñ*Â,"±«v=8‘¹³¨vù³”B¯+“ž6£ˆ¨ÔÎÖË’Ö¹0¬ˆ>r–ÉŽŒb³¢sÀ~•~Q”þµ´:O䯉ÇDf/ Ûì~†™‘Ñn AÑaŠ„F`ŒáxÔ´J¨8¢µqþ­êr‰ù8ÅJ†¹”a”$ùdü{£I$…ûJ‡}þqIÚóåªÃS—À>|²‡LÒ\ø¯\Ê$¶‚X¬ØÔ¢¢’÷=štžì„®,³ ÓÌÁ#ì]ÆÄ$)ojEWŽÊë˜÷gäfr™2t.=aN>áòš¼•;5°ÆÛ‘f’ƒî¸-×饀Æj•ËÍj€@3(-¤ÛjnÐ+|¨›ì«™d!nêBècò¢×ð7wã‹î? 7MËõÔX!›SÑ{îv¦CÑÉ"DnRf½Ã›œÅrf¹‰QLйsÔAí)ù2&7\­—òÜ ä'ç“8#oZ ‡P‰÷NÁM‡fòYÍQ×N=xx£8-ßh‚4NgÄœ™‘ ;í.•Ì7ƒ¨œÚ™Ôºg_¬}Óq¦”gœ`Ó\•“¡þTô5+éU(qcšúþ‘œxõºz™ýŽªc’=üo‡R9ž/«¥Óô4zQbƒ0æó~V3q¿x( ÉýÁ36IÑ‹N º3.~ª Ý‚ €†ÝK†¦¬cÊ Zð˜éUÇ =+LŽÞ ùˆ¨PwtâIwhï»óo€û1InÌCS¹<[mŸ[ £6Û˜ŽÓö85Î÷Iªz‹À1jS—ÌÍÜ#;'¾‹.¹rþ‡|vü¤”«[;ÞßDýËAðñË„v1lîúµ[7 jß|ÈŽk1™ÅÒû̦Pœ~„Ôgñ1&àML“ih€ë’ƒ‹n¡ìæçY¥W8ºg]k¢]y%WHºò…3ÏuÇ— 7ä¥MdÆXiø6iƒZ@R²Ç{ÿ´ÀQ@F«¤û ÐÜšõìS˧mk'JÌÎ'0Ìʸ°[”6^*&7m£sUpЉ· ¢Jàž£;l^;é€B:RW½7)ˆÅeq±)›pÖ†/xòÜ÷ìqÈ!Y§`ŠøÍP7|ý†A©æãz… º@A7€„[ p½B¯ •È€¶=CÑÕêÐKðëØó@øB .p·wGoxe`ÊΘI.\Ù3 L™ˆ{ÈPàTîk¼—޾@Õ}xì pž˜Cëà jÔià!ì 7ö‘f¢Jľ, ƒùNZ8>Ó“MüºY¸qÆ'»$#ƒã:ÚB·ÐмêBà¿üX˜Ý­å.\‡M*úÔ²amö”E³¡jî"ëÑŠóËïÍ#ül(Gå€Cí3(+> Õ6‡T Üî& Þ•CeþQ˜ŒCô‹qû:z“0‹üÇ)€… £®¤¢VÐÍLc‰3‹j±ÆìP‘¥aÀ‘x -J}œ¹æÅ¾Š¹ŒTJ!¥s¢Ñ}ßµ‰‰iµ*éÁB'íY¼ ”Ù{"=AH—†ó”þ̰Jˆ×‘ÿò0³šF"m1ˈ`y®æ Ò¬Sµ’qs¢ð]»ªÛàïý扂lrÜÛ¶%…X+µÑ–S2ÊZÁ€£©Ö!ôqBDfÈðCZb`•NǤPrwmk2"8æž6Qjsg™œî¬Ê ŸW]«§~aqP$àSw Â!\±âÓ䵇ô2•Ýñþ{Ù;èÐýWcAŠÎWØ®e²ìMèD5€¬è#†t~g³ÍÝ“mS¬ŽiqkÁ ÛHŒI.N…ç¯èЊ¶~·I=| 2yҸŨ¹ÐÀö%rO¯!Nr4Mdo,Àéð Dò^kÖ‘ÊÚþ[HÒ°E€I ÍhÛ¸E•®v\ÇÉ„K'P›ÄÔnJËœv”Lµ-à ä;ŸèÍ{Nj'amž¤SCÑDu¥DÖ1r©k—ùÎãá›;ÕÉ0Û² I~Ô¶¬ÑnRÎê]’¥DyKÊý s XýpÙeûCb2V½f+3ɉ†¥–[q p‚xì}ZJ{šÊúþçIš)Å3¯nÅø¤Ù»—ðòØö°)H—ëÍb舨f_7G¯Ó÷û-õqùý(uQK·€±;me%†ƒ;-%´…þ¸„IA¢ùf¬±%.ø|/%[e­¤üÝt®dQ'‘– :¹°¡ÜtÑ"ÅW/èö1›Uˇp±ðUÀIXŠÊqVHhÑ ”Rû.¸¯B´»x¯dλl¸’hÅkoEPRTÝÚ÷Ϩf];º"Ò5‹ôõOZžŠ-k¡&`–Š-ÆWbýpœ[©ä¯ó2½¯b/ø©¸] 6½¯ä£ûySÉ~iµ˜Õé:+µO¿möé•£ASް Œ´tôìÑü£gtäÑðÇð‚Z(´öŠ Éó_ |ì–6¿v¾²Â³œM‡Y7.}ó‡L ÉÍ‚%ó‚7ù£X`K6á’kœqrÍ.1â{1j ñiXøžt5I4 8ªõº–ûÀzÉMûÊv•´N=ì¥ÚõŠ˜Éþï6Z‹Ã¿¸2òÈ?Ù¥e¿xï8Ò`×a•QÕ¥7xcÿ$Ž~ª¥l:)õ.Ñê7 ¶fp ûq•ì=h˜‚pÝo6£Ö ¨|Éa#«yºu ‘¸OúO?cUª2eqî˜ÿ|&]6‚˜nŠ3 [7dâ¯Ò('Ä%=öp?Óðä,mBÆMu…o÷—ìÞWÒ6Œµ¢¦f´ë-Lí—¦úXèžÙ É~M®`›ó2` påo,Â9«W¨ªŠ©Ÿ€ÃžÞ Ã^'1Ç‘¦m{ÀÏXÇÉÛ Û•$2IŠ×,ÂÚ¢3òªëùǶֽ<º˜z(©¸i.¤çF“§Û£Á&õM'ÿÚYÂhÎÕÓj'Ù¼´`}¦ë4JRk#ð§÷Ìn²#[k%ѸÐþQ›û‚ü’¥áœ5¥\§›ò-IÙjUhë%kYƳ†Gyš½Ÿ”~ "i¶ï ¼5$² >|x>=>MÓÏ.ÛÊäðÓõ©ø9Zú}$«ÓÑ×ËÜCOÚÚ'Ð+¥ ÈSþ¤‰!u^0n`¾©Íœ’·I¥eí$’“¼œ»vfYQzùÑê¡!¼1™ð<“Âåµ-#Ã@ÿvÜÚ‰c%·¯QÃúòÜ‘EMïî<ÄG®ôõÆÆlz‡«Ï°Çy›“¾ _ÖÌÓ7”WägAǘÀ›d Èsÿ8ÓH[£wœ‹7ºúÉ<éqZ}ßÝ1Þ”k8 ïà“ˆ #æ÷“‚c"ßã—…uš÷?ç:0³Œlz¼Ûöx½…]2ÍwÑ Uœ¼ É ]#h½(`3L·à\fÄÖã±aŸ4'Ží-?ÄTÄ–þ哃nÅŸ°=ŸX‡+²Ã$PYˆ„ÇOÃ>.Á´Pií³˜€TƒÉË®L*Šâ Mü°é|o*Tv‰ï3Þk褋|¥m¼ÏÄÖ°å94@¿Ñ_$S5y͉57&®Ž··Ø*œNlT¯38úÁJ»žsÍÕ.{ÜL±d\ ŸÂ,÷ÙÎz¾Â¸…:óŒ4üæm±A"ßçËÙ 1f1²^iˆaYEÛ©<ÜÜâTíDˆÇ=ø.KØvñÆL‹nÂÍ|¤„Ù>}I@¹¦Lê"h0‘žkɃҔ_‚À¶s“ ¶ñW«o_¶}''Vk…ž^¬#jéªt3zp™Sܧlõ£¯j'ï „D;ÐîÝã!sIƒÚB˜üm†y†Vj¸†×6 ­Î¼Ô÷¢þËíê4¶µÍ*ß+aE”ý"nî.ßb@ËÒ;W›-Yò[È–Ññ)X¸]>î°+hx^,‰ã0ð†)»2šxšEªà-DÌ,P£rÖ?'V—žz&Ý‹KeÑŠÅ?Jls¼A˜/<+iÔ”ã‰ÐçÙõt´ÆÈ'Þˆ JÞÖü SVÃ…qEÈ™aãðLsÆ'àm—¸·O$“¿ ‰’ÐìÁæ # ñUgÆ S¿°g}O’ý%ÅÀï/*ÊVYqOùàî3¯ÐýdIä˜]àÓù=ɘž)ØZïimWÜ!b~ÿ©Ë]øç†2j ©Ã–;@­VãµØ¥õ¦^ÞX^žT9.I0­ž†.ËW=«RªÔ¼·”±1* C1³¡rC'|Ø‘ õo—øôw2bþƒ£sFX†Ý›µV9üɤÞÌÈ%ÜÁ2ätClæÚôzÿ«Ze†­ÀÚÅ©A¬ª†;U¶Ý´út×Í/A‰Éa ï÷Ô¼4Üœ½ ¦.&Y*uÛï Ë8¦–/Ô¾ÁùÚ߆Ë \¾¨‡8eƒ Æq2O%S yžE|s»ÛŸ_ÿlÓ72Ÿ™SØPÖ•4—[]ß_“¢]A¯ÓãP-ì7ÙSbÝ.LãèîÜI¸Òiï‚"]ó³[¢g1<Ö›ëðL.nÞ ‰~µf7ø5ŸÈôMCS=Í£¯³çý0Õ:Z]OG+âÉw¾¦* ¿™¬ t’Ö²L;u|ƒÔ-"i3¥ Î,ƒY×Ò.Üú„Gü[Þ©Ãv§Š¤üÔ2gšsOnxh형¹ƒÊ=íÙq8±ì­á”¶•Î"s ´Ä·Ñ5Æy•1;vå~]`«¯Ë³Í¬I7=2ˆóuš7S5+ mo},bytè žV)Ê’p[3T–KoÕœ^Ù»»ž<Å»Q*ñu‰Æ<Ù‘‡­1#ƒ§²”Ó9ÈJ¾Sxíú3KÞ±Ëgæ"£S^$¾QZ⺠¹æ³09'~Ò€š·ü"@ÕK«YØBšZ×(ÞGo†{І*Øó)ó+u¨…ßèÙoLû”C²¦Øe§ž[jºœyéĪ!Ý Å<ŸŽÚ™ÈÚK¢wà Óubò¢\Õ‰¶~TS愺)VOM c¬±_-‘÷.§7ù ƒ‡ï\í6{çfUl{—÷Æ7t«Æä͈º–f}MÁL¥§˜ýÔ¤T·oaƒ¡g†Ä—¸¸öV¡Ê®¿nËß`çÆYrm_p8É7ö‰opמ'¨ä~¬—nï³ÅñHmÄ|.¦Œ koaÑãtÞ‰Çy-ø;çXÞ"á&‰Ì/V9Á[>3‘Zä¢ÚÒÙi+nãÞrÃe“;×mgÊLµlÃåù&[gxC¤¹°,Úð¼}8ª3¸÷¶ù€5ƒ¾B}£ÓšøX<¶‚–³Ûor$,²4r¼Õ#}³c¯Î©©g§ÎÛY¬Þ„¹gÝa0µ+ÒMŸî¡…›üEdžô>6)‚9äîR_uÀbÊXÝÎþÁ«ëö/F»ýá+y‡á´àż×óv_èV{(Ü;õö/X{ÿðÍãŸÝ_öˆÃ…xÇOG¶ó ¹ãdü9C>åsÌ1ToèõD¢Ð„~ayBù™?ng¹CÙ‚ø|çaö7Üi¥ ÿZÅJå2rÃÑô,U1¢ î0åÔ cFFþ »±8Þhø6ƒ×õÏf„"­|…Ïýþác‡VeC&8ºó¡ƒÆß4¹k+ OÉŸU²I1§q[õ X‰ÖÒ9ÇԎɘCŒÿ¸ ‰õëÿ»Æÿ¦ƒƒŽÕ!vqHÿ=%憖†tž­˜¶Î[mÝþ U×q*´‹IkÙg±õ‘;Ñ9'^uñà‰ Y@1µ§{_<”AH¤Üšåðd€|wwú8=gM”}0œx±bsÌiÛÉù¤èa2'Û< £°ÒL„Îû2ø=%í<†BÍ))D¦Q1‰ÍAd÷}0þK:PN8±½—Êõò I¤šK¨ˆK J̃¬™O9ˆ¯ CtDé¢s•žûdÍ6TDˆ ï¥D„æa@, ï@jÎøq Æ cúf2Ž^.Ï÷÷ŠÔ6$«‡îŸç7'£«ÏF@Ú)ðDæZÈ£¿ÚN Usj!hòBæ¦rø*!t(ÁöbE=ðQØthÀEÔÀ©)ÖÅï5,´Æ«Ks|ÑPKÜÙIÓ–ñß_6^œ¸±ñéìöuüÛ ñæw{ùº;:z2“/ÞN~¾/—§ŸPjõ“«ôÇåÓÕêÀC*a¼hP'à„œY‰¥͔ƴ€§K%Ði÷œòôA‰¡ß#)®ƒ+IÖ.e`JIO¤ì+ë¹´¥DÖ½os6Yß=L™°ÁÉÂ#dk 91Å‹„xJ˜LÅY¹ß õ-2·™šõ1TÉ/S÷¤ÚÅ‹{Ò£NtSâ šBf” <#9 ²÷ž êNÐ  Øc–¢÷^Ÿƒ÷ßB”•¯DsÒÀ_¬jDpB0Ù©OœÁî n!4,Dô@P§(¹±‹ÌFÎt\9©<ážBø½ ˜,NöÞ¢É] ¨DûÓÛ+D}NÒå{z>#ÿ‹ÔBײ8í4Žš¸\dL£euµÒçEW0§Ÿ°œ{=Ъ ãËÆjöàáà’r„ð¾%8]§Âi•Ÿåáá¸:%pœeq³Nž~-ÓÏøââþ>—ˆöåÓp …±”ð‹Y ûÄuR‰äœê˜¥ý’L¡f¬•¢0™‹_´Ò:n_þagTáLÇÀÖ:ðl’îO¾{Ćð«•×ÂJÇBg[ðÉLAvÞàÎvp4=¾€•°¯¼ÚùÏÓ Ò¦­„P¼î ñ’ßnê'ªþS„×îÃbÉÞ®Hzi—°Õ*úE)~«Ý©ReљDžÏîõÑvϽ§I•0™r;"²Uò¸9ôh¤ÃþÊ€ç–0î„ÐDÅ~‰Ÿ—tx±û4óbÆÂí‡YX°õõ!g°c®Œ-v·€{•P™æ)ú3«©HœÃ¤:êÃO0 ðQH K(Ô¥Î*SE9‚Å2?˜r LEiÄŸ¼žŠvQ"²H ‚¡2Z‡tD âqï½göCóÀL15yf3ï¦EI«Õb”''OˆbawœQ6MæFwÆc-PܲÁ\Ą̊§#FTÉî03æÛ’ÁN£¥†»<¨$®‡jñA°0'„Ør¬«¯„¯%ä|µž^(Èïo/VP„»Ù¹23\÷–-DØ2Í\űÑ ËOš?¢ï­uØxñúéBQ[¨Z{Û‘eÿgJUÈbç%%Q>vÌ:ë(¥bé¦}ã¢$|§i˜ÔÄ£¦”ÕÚ_å´+ë Íå‘öôúuõÕ¾vÆï‹ÿœO·  0ïOÀÂS°m=׫Vo(*‡ŒV’Š eÕ4†šÇ)ò vGUì¸ÜßÓéçŽø¼EDˆ’AÄ1uœÔ™À²q3½ÌMïÀáÅà*”‚Ӛݷ‡··u²QÀÿ6¿0Ô¶­uì„j;úf£ªÐ‡«»úk§n¯t/R]YD×À[¹èDFÞ,t Â^ÉÙ*EñÛoøJ.Ǫ-gÑË~ò˜¬ÐÅÈÍÆäVÿ²2(ª˜…!΄½ù2Ô¬Éü!›UH?BBeq˜Å4hØÏ­®¥0Ú³å‚LN[€§˜•d¨8ä &DLˆfÊ[.¸N­·ðUvˆ¡"”Ð9Íè½;ÒXNç&PqÇz]*3Ý3¢Ðo[„EàÂ…¿K—aìm`\4P…§o>UoÄ|$b]zÊ=þé‚Üv  ¹ö‚*×W`E´z3 ­dž˜ÄK9,ZzGheüí©D ‹£†X\-Ï6¤ìŠøZL…9¼Â¥Šð.7Â3¨1D‚x‚„ûj>äG ‰é.ç¥ÑJð…p鿢ÎÙìÏÝg]}*¦)óp¡ë/0Fž§S„ÅcÕIåÏv¼&`k¶—‚àAU*µ¨º¾÷ 5® æ ~]Q0é‹ñ}…ª®À–àgМ›Ñ v~Q-U€GQVèß¼Ãáe¿~)<ö•òãÚ¢=¿£ÂÎh侨×H¿x} nOd芠6€çåw_¹‘„îEêÓàîÖl×Ú}-ÒI8G˜i¹)?6Š5=j„”bÆ÷í½pg·¬Ð8úÚbמ¿X‚»ßþrø!ò„Dˆ›úg_^ÇâkW\ò4;æg¹,ÿr@6yûp7Á='‡v¦Iü ”¦à†s’â{)aúv(¤åir¿Õå)Ì("×¢ ¹“wåÍǃJÍqXú¸öˆONNì^ Ìâ&paç‰{+vðR‰{êÎåjÙì{é¶¹3Ô|Þ§±€iùô™¦ã¾µbõ¤2ï–Zòxwz…ü2öë'ª½òú±À ¶eTWxÚé±LCy˜™Ä"[Áæ=/¡ áîCXrÇáÞ ¬<×R+×ÅooŽô~œgìãcðº†`¹øW2â6ÔøS ¶Ë"uCáß 7–Ý  àE,£µ%ß¡\ç.UL)diƒÆQ!ZJtëÞК~X€?33$±yi×{GçZ4xv%ÔÈé_#A_ÙwGÛ+8LàÂQÈóú¥htD‡¡nù!xþÒúß”aæèÌLÂ0‡h(€¶ÒÞÕ|ZÏèxž'v¶s·Y û/ö/©­SÛt3¼æÕ‚IsåIªKÄBÐ6©÷ ( ð±Â‚«ö:ÈêÚ[ªªÌÊ´.í¬¸'‘>âJZ‰½Áâwü³›ËØZÓ†ˆw´žð"Éç˸¬lt`ø¢ìüº}ç\ÐÝOaBÉÈþ[df<«µc£ŽÚ߬¿Ûº¶É´)S )£ºe))œ¶ð >:Teȵ›¹“ûr=cvjg„\OelÏC8\!E@‚€dA^"%á{ꌾ;õ‘Ëíîv3b©´Ôöv›Ëm>7ÃyæÞwËñ—ÿ°(ÏÇ÷%¨[ÂïùÙ#–¬Œ¥óÉ,B B›]dvî)·â䕇ÞÚ“‡.ÒM±IªHý %B\¤ŒÖÚÄàƒ.(ê"&j«â2&b+ãRU¤†šë¨ˆ­Ž2bä)µIòDa åR&j«¥ˆ"Ê@ÌÕ\Ä–­ëTÏA¹é\ÄVŽoªGO±¤§³].r“â =”›d%r ÙH=oU.ØÞ§B†ûž¿4¿™$¨Ì)6\ûr'ÅïÁWìQ Ó+2æ”B†½µ´uΊ¥Gí‚Ám¸œ¼P‰/5jœ†­7|ªÔ¨4ŠU7¾¥IjöRvÄe”ïØxô&_RV† “—€(÷ /wè¥Àn&Œä âO 7]9Óå0̓‰jJn<„{"©Ú㉾âPm4oPœs8ìxÊLº Y–‰F—›e”…™'ÿä£ÖÁ,ã ÔÑÌÃI¹ÌC¨œ{<Ùãpæ!ä–u6Yä€î!tHóxÜÒð/].ùÞ•bVBÕ —*3íbÛd$ÇâÌ@*À8—µ§ D©MjéÊ«‘5HXc½íg!½­ЙLoA#hÈS¢à16¿£»."uÞ[{Z›^·^_ë%kVõL¼wýHµü"Ô©Ï•K±‹”·i×*¦si§ÍàÐy?X½8ŽžGD2úÞ~ðýúÌ ×OÏæ)ÐÖñè5_´Ÿ M_²Vd«UR ¸<t‘JÒèlTñ‰ lpEÜž½8Š€Jv1Ñ.²X?IL.Nêœ1SPvØYDÍ K¿2KAìKØ;ß 19=›4ÑovŸ¨Ä%6ê #AD,wôao.Îg“ªÚ}h²'ˆ@žÆÎýðª×>9­Ú¬SQß%dŒëÁïè/¨[<Õ&Þwœn’¯ÉÍé§öÕ^ÏûP4ݶ†9E+â(®øc«ä44F°/Çm(›ÍqÝÖ×NÇ™¤Ž·.m“û¬‘üëa^'ž³L ö ÊøZû¥žú§¢…iC¢ß¸24O}7È––Á`Á®ãG6Ç#ûû¾T>±Ð0|.7Y·8œÚ„ÙF¡uƒÜ3Šô3·õqìò˜dj,f±‘J¬d–üI ZŒe1ääe‡µ4˜îXÁ#kŽž´ \”ÞuGݹ†drO´Ö‹3Ô,{ýÇZFʪTš­ÒÈ›·Ž“Žju¹P¸J4ª}õÕ8Ü3]¡>„~oî{þÒ@íð܉ iMܘ kÉî\ƒðnFx´’ƒúþÞqé“È Ÿ'ç>DN4/3ý!¬壓“ŒÓs.¢sÑw#¡Üð•´Gë?ÙÞUפã¼lžQCé,³eGn?‹ÇL ·tm¹%¦‡$úÅû°#pPÌÌåÌcZ²MÔ§ß'_1FgE ¾bÂÛðØ{š^ÚÉ_”<ÛÙñ±šrGÔ[ Ù&S[…«š¶ª2u#‚§>.‰MÀL3™äRôÁý”¶çÄ1³‘ë}öA77»€ÌdU(9à”%ØÄyZûÄAhÜ.=ƒqEæküÈCó½N×íy*þ^øé‘™ä˜$£?»±ÈY£¦¶—®l]þ01øE`~«(‘¤B‡0•I¨œ”aQ МºâÕPZ€q,¢–ÔÚ$¸óÉm§’“\‘Ž€R]Ò¾¯‘7 VÍ (⟦/Ò&Ãn˜§JîòÏ•›5n ®t:‡ðñ"]Êh7Š9‡ŸÂn°¢ìC.B}ÆaðEU˜äqÛ7Фötረí§LR Îÿóv;ìw±}¶T\…J"eÀ¢ö"ú(x‘`ŸXx2;y Òw&U(uc‡‚ÜRÏœErJ³DàÚU@4 'ØŠ|ÛlÂ#É¡3Ó©HÁÕWÌ{ÜÌÔEÕqæøÔÍ>ŒøpûK•HfE»WÉÑÀŦ3ôD8‡ñ‰ßü=”Xž/§%â¾rmèéç1¢ÁÌ­«ïÉ!¨SðÐ&3¾hêSý”L“©£áqañÛ8ùOŽ=JKç€Is-%@@ËÊoV¶Ú\ÐÔH®HX¿›È»y;)\>œX›Ð•fÕG´Œ¸wŒªRŠù¥`tæÍE"ϱ%'W¨óªoWO³ü­°Ó¦m×$âÀéd0œ„%{¤ÓSÚ¥Ó7&äÊ‚…Ø!üóòr>Ñ»–žCj·—NÖÌÞ'AÎ7¥Æ\eÌpn…R > ½ X B-Ïhí«Kb{­YkQºª,”ÙçZ2l-sÖŽtzæÈÂôÀw)/«…½áP¦tOKª±=Ài”Àøì¶Ê™WR-ΪÆ0·0´È+¾ã¤²§²@y²ä¢¼LQB?=õÉÊ^ž;Xb&û5S5M+¤DI¤Ã'Ü›“ƒÄ §?6¿BMüÇãž[àþ¬ÊÁ?¬(Ty¨yæn‘$̰Þ0G¦ÌÚYãÆ…ëáüë7á$¸µ6š‹ë†”P(bY_œWud›"åvrfJà.2TG:;Û®G­Ý猕:iTîkl¹qHIZ^ðòÓg¿w„e3 z3 j]•5‰é ]bfƱfûê¢èÑŠ¾‹ÊYOªÖÜ©‰ÆÝ@ö7cLuŽ:oO8‚¶ÁMi¤u ×áÓ|8 ¨›øˆŸl«û,«lòô¢±›æï]QTÁÎÞb.‰›vÂôÙÒè.° DuŸ+ @×£Á4Œ«ò#’ìË+,L;¼®Æµûö_¦’<¡ª¬·ïQ†GU´13õ@,­WO}Yçæ¬n…0ý ¥—QÐFxãž½U/œùv0¨K-|wi(¥»*ÙîâM¼qD— U”+Ù­Ç$ ²;àÙÅÁ£»ž¡}^Ó ‚:/Uk^‚à46öIôý}3éY1®ÉN6¢ºÍwyͶeLÈ¡B©›BX(¡f/¢]•žûºÈ…{]õù'Ÿ\{±ñŒ£Mw¦PÔòºgNÐ9¶æÎ¥ßKÈbÌR–nÔR$zÀ¼\!Ù_º¸nKèx½«Àñ¯ñá!-ã~Öb9Íüñÿn×? ç_ÎR‘º¿VTNŽþ’”á=küýô0óÑ.{ÔÏ*>EsyÑÆ^ùÉ'ÿ¥õøŽ´¶ÚV»°Ý™,þÍyÙµ£ 9LÝMÕ@ ‹ÂxLzY]3ißE·”·MLmYC0¸0¸ŒöQ¼Ãxcø”cøµÉã‹oB0c > !*I™„R£‚jBQ›‰†Óe$JGJ;YÇͽ²Xð×Û<-Ž]¡×ã"×»ïÃl~_-DÊ ã[›ŽˆºAãq¦eNËœ!åC.òdj`úw”üP¸Ò©2‘©9ˆè8óEs55\pnµÞ^Çc]Œds™:Ä…¤÷´•:ÓO’‘fVÌ´wàÖGUǪé\”ÇLÔȪUñƒ3—w¿ÚýmyqØï½x³³›¡ëEüèÀ….¬}½9¼§{æìèkR½w£ÆŠ<Ì^Öðfyææ·*SðåÒ÷ïeT´úe{TôOõêû»‡uüöcy#ÿˆËÒ1æŸÌD–W]FÌŠ¸]˿Նþ:kZu"S«p'ÇÔrD1k³Q™ Óx8ZeÅÚhúÅ´&Z§C6Çý -ŠÌR>Ats’ÁB-°évžN¢F#'Z  F=â8Ä[Kg~/îž+:ŒÅŒS«ØsÑ¿uÏ63¢B¨Í뫱æà¦Ö!#0ýKZúîm¾KOðàQü!  “!Aí'’[¾½âtþþ&¨çeаM>†Ø¡*=ãÊ妡~ˬ!†#Å#ûIÙ÷Í{(¨$r—2(rÉæf+õÅEyÇî$¹ú¨I‡Åj•ê~¹êÆêÅ[)«äÆ”- ~økθåœD¡QbqtDÖßÖv׫'lûèœ<“–F ëÁ¬ø’Ø\ÅÁH#Lš(RD[T"\i·=_tºD„§Œì G¦ûƒcc蛺ó`>ˆÅIT¸ß³Žÿ;Ÿús}PË7i´Ž¿›Ü*ݾ®6ÞÖX*‰ ,ëÑ7Èí¹b¥›áÆ_áa0™1øÚ4‹Ã©É­¹ñùŸùMÄ 7»ÇR’®ùt|ÝðNœµ•e,qâÆƒ2_8s½ý¥y#‡ZX6OråÎÇo»k÷âŠz4H…êü%Þq®¢SÙ͉—i¦1i‚4)l ûõ´ÊV­­æ¶¸«ý7*ÍþVZ©x¹síã’Ró%µïH”ÉÕIj œµ.òÄ-ãf*(*ÊøBô:¼`ë»ÕÖiçÂ5—܆]à(/ §5#kòËòŒj¢ðXOs<ºEúÅÊ]‡‡‹T6Ü’çV©Æ»RO=—OûÈ_Ÿ®ó’ ­yÉL‡D"™Ü·k5Q…ä×ÈÛ_£ç„›`Yq¯ EÎÈ­wívÌ%S~çñüÿ§€˜PÊÖ_€„@þ_—ã^õ;lQ>cnÇë‘Ê–R­‡#©È\© QR¹Ä@Óå#i3w›úf~3ó6I gÕÛŽý(ô}D˜:(½ðg³ër+A Œq¼yœùGÃoªÃ3àï‰|‡ûûY€óSg°a ˜èZ³¤¤´™A¼®'-€˜ ô€Å.AÐ!*—j:!wÄ!Òs:A`»Ùk Á˜(uÐ[*©3•Âñìð;@<`ŸEÝâ§×KÓ–™?dý\®áJªƒ©ÔË"+Ž‘yIàÖîöúoÚ‚ã¸Ö­Í½.ëví×›×ò-³5¹îÖÞÿÙjkwÓ²‚$ýúzÿÊÕv¿³ÓÎõ l¡] ˆS•вX|.Dî°ÔÈP~X2ÆW°Å ‹S”YµIä!pÇp‘F¡HšW ±`’,Ä\ðÍ¢‰Ù¹S ¾’DíåÕ±®×dÖÂ.·‹–ë“;Ê$æ©ø|W,xsárݬ:µ°çBŠTÓiµåLµ•¢]¨NÖ÷J(ù#x®÷ÜÿPbâ—ŠgÛë3Üó¨¦üü){ÒÍ‚ uý$R’e‡æ°K ú÷Li»)¦OŠBgàù¬q œ[Û,‹ÍµYjF•l¶Ïr©Bô<¶‡|õëͽªº²Œ>…‡oYŽ‚7&EEqX vÕIšÞ7ñÞ9“¼â4é7B±üƒ”ƒ¶o«Ä2‚‚ {­÷ ™¢W‹XI# Ù£“×§oVO5\L$‹—-Îè< Uú™ˆÖ1èÛIñˆŽSåó½ô§p/â,þ€{ôbã×ì8ÿM°¯DÁQ•£pÞ—L¯ƒzì¥U¹Ou Ç/ñµÿÎÁò3d1ætdfìœûÅ[ÖwLõ†ÃŸpõŽ çq%Ä»™P·¶2é$i¬hkÂ$é°³>€C±—ºæøp2<»\ÿB9$¬¾M™ìþ>|U~µ­h±/ñ¡Èß.€¸îQ®GZƒLp""è×™ÌÒ‰*w|¡ß¯u(1{Dñå›xNítL"ö/:ažpƒá•8Àð"›¥mw®fMÅTµÙ:=’UUÖÓ¶»°¼…¹#èLqžŒ’äÈà‡º‰µmèRF¸ƒA–ÂKH°…(õ*à§YZ¾âW–¶‰–KÚ i”[wtò ¹Pòî#Rå[À`¿<<%oæïÛó#*NïqÀ}4Y ©¸[¹üÊŒ69W4™;È:Ã5ü0¥ FÝL÷_tÅYoÌTŠ1§bzS;®zL^ô¬ ÷#èf=šP`ô'¿ÓsaŽ3x8›‘ŽýZŸv:“^ÿ ˆ’Úß\&¥Uäy1TâºW9uÔÚ_étú\GÌ«ì—M¯ˆ\Žë¿.™&ÍOIM‰ârïÏÅ”òчÐIÞÞ o«ŠÌ‰n2(t'?Žpa¤Ä”ÀZ£-ð1õY$–÷E ÖQñ'åœ*^àç¾(/>?)¢y Ö‡ÐúIb‰-ìâ-Ïù÷uÏ!™(-=_ÿ¹f†•œPžÔëáI.Wv õXij6‰r.åa&Òi‰ûò^ï½[cC½…Ðfƒ˜ƒO¿ê±¨ ÈígÕ@©ÏÊ9ơψGp¢C[™XWxt‚D„þ·aÞ„~ùæSÚ(ˆÇllò¼)é^Jw4•A×;DBì6íß­,¹@xNW1Yv‡¯Â½çÞSŠÈÞU·¬ÏÝ’©¶J>ã‡q$ÜO%¢¯ºäßãñlŽº¤žôBq\¼ÍƒÈ ðb Ë.2¶´ž8š¹££ü» ë• –È–ZWy޶³Nÿ]ðd4!¸UÑ a³2áÒéJzÈ­´>Ò–ªˆqæ·vúŠ7¼ñÌ4«`Òš66{Ë$»;el+a¦ÊØ6V3?ϸԦ=‘/[¸Ì\Ž$etË剴‘ÜÀÊ”Ww ~N°S*ÙóÍÑ5cSᄀ©¹Q¯¦åNf[YD6Ü#<ÆîVWp[ϸ]¢C§FÞhˆáuß]<šôË'ª{îÑ]ºÖÈfÿ䮜d°HwyªQÔ¥RµJu(³ú¹"k×&œš®!Ka‹[íz!ÄŒi+Øó~@0 ±“_ã˜jMÁPýÕèE  w&+×ò{gß7îgþr³–aíÈÏ»¾áO51çž(fƒ!™î°zä÷—vö”p­RuMÑÓþ€þ“ %óVÔ‹ÌBþ7Ãl ,íLþ×Ñ”ªk¿Å†v÷B —‚ízÒ‚.¼H8ÐC2ÓúŠÀMeD`q!q(–¸I¨9ŠýºçNQÛëÝæÑ×@ûpâ÷DxIÃ,½u+©Ú~v6¯7€šìÚöÜ#PKz‘Ê:‰ ­_; D™0°­vúÍ\7³/N2îä‘—;LµäH™ë7 ç]èÍðOØamJ°’h÷dÝKÌi«EK2™ØejÍ7c‘Ä8Ú½8tÇsÒjò3­~JWbx\þÆ÷ˉﮋ.™×Ƭ2ëJðü´>=}­zöEì¶}+vð ^±¼k+Ø&“<»l¥ Xš€ZHï‰íÑ­¤‡iÆh–Kr‚.Ô)o…î½ØÈ·ä(T‡Î’@ׯ Sò`kÔ¬3³q|ëäµûåm—ÎέúæÑ ø »C§¾.mëÄkT½Þ Ñ |^]Y€5|°;ˆ_¸JCŒÆh–-B 7@'º“JuÎ3“jª° ?¦˜V©XǦ̸'H·Å*å®|Ð7OåŸúEv0þkßíø~5¹#{6'ì|ÓyŒTìÄP¡Ù¶£Å󭱬™î½”9h“þSÃñU‡Ž½'£d’ŽôÜS,‰iBXw2ñì³)Ò×<AÓ4£cÅôƒßwÇ€¦\OG¬b²ï&S¯ú“Ð+óžrxaôºÒ@ ºÏ‘‘å#çÝ¡ò>T¯ƒ÷Y#¥H„Ÿã¿ÌQ­hµO/§ásà’™`â¼…Ÿm®ß2xÑËj ·©ïffh±êENÅÏ/ÆÊþ4‹mÏ|‹íaãPÁ=ÍÏ“æ¢1&H(±?§'QŸÛû '­ä·‹…[W®à÷«?˜ >ÜØèçrL mH’&—)«ðeo¼f<áTÞAK“`tjëw#;¥ó±j9ª½?_˜3Œûé}ç:4¸ `~‚øºECuüqçÝ6axyTZ®ß¯0Í|ŒcþØ1&8rñŸïx–#ä–Ä>öÙ1"¹½g@ºÛ•ÊÆ05Þ ¨†ÃvÄ¥ÈÉ0à“D›Áoy—o >@=ׯ@ûÕ½ÜàÄ{ß©½6Ÿ{‚ÉZÊ#J¥ð퉙ònç:¸ QíÉüÿÄŸeì) VÿM»ªÿç.Ϫk'o± ¤]_“nÌÚuR(Ø,AÊ`3øp/(yÏPÜôA'áÒÆå6š´<¶üP~cå½Ópœ­Ë5I£yh¸³EÊ›hþûè»;??üÛ¸ù~øº¹+¯ù|wðJ÷E Â7‡4vž‹ã<š2ÎjŽ`>i.Ée|ÝiW‰Ž×^ØþÍ Oº ò,)nØ^;²Ï•¬8­f`¯œ›Î*FÓÉ/œÞ°Ë`|†÷µÆG, X(Ò®ïÛ©2À±'¡¹fpÕøk[WÎÇmÏÜ,Ç-s«h_¡C}Í´R.œV ‡—¹QÔÛ+Н¤Cµcp%& ÆÈ%’×£|åòÇŽoÖ(—ãQ‰àÆ¿3…–Œ•ÌE¬Ñ‘s{m“kÕÜx]¯8¾tù-μê^o6“îHªD§mSµ.®¦'«ŽÃ2ÿ!Z>óåèJgºz³È¤½vÑ×x«&ûvË‘· ÅS_ù•®<úröæG»ÑfÕ–R­˜6/|v/ *jôÈMú)M:åZñøg¨¬´Ý£øÔñ"óî܃Ï2’=¤S—»ôóÄ2:º97O}¯¨ µ/RQ B/ /Ú¥Ž-¢a÷ü….ùå+Ör ‘*šN¿Ö¢™ÐñV¼®Å.ƒjªçþvÅûý´>Šû`•,éу•wU[ñ¥j˜jÂl÷Z¦‰uUƒW Váïª3¤‰'Û•i^ ’Çpbr)d^Öe*µÿŽüž5Pjge‰ŠgZ‰6oˆJ:`H¨'ì-ý…µ½e±+ä|¦“S÷ ÖDŒôE•a»k‡;Ô~ÓþCÜï{À]Äx¶ æ\øe¸ñÓpíW1ÕÜ!Pq\Ði KõíˆþÃeÃûgÿŸ¥¿=O{"€ í¿QAíœÿOo¼V mû#64¿·ú}*è 1\ÎÌp#ÆPü%p9Ds.Ц@é”ÙX+Iª¸­9ÿ½»s#é4ÞL[ó—‰ãÎü;¼O‡!:×>F§N§ëþõmËäTKŽÁWpLHSÑ(Tèám©†èñwõ»˜ ú!JToôœFBD—4öO@$¶.Ì=†R\êšKM"åÁk§ñ£Ä¹G'Tȶ4;ÕªÊP„kZ!zpVp߸úÙñjØ2ó_?&Íú5@úXØM¹¥¨9¾m=½ùÓµr†“ %CZæCþP×FCžp^Dý©”úL¶úؼe“»M òU§™¹í,l@ÊÆ–¹ Ñ®A«šày[ hêSÝ™›mc~½û3ã‰w—¯ûåqð׋Ãß_£Wo^s÷·ëG;7×/oöá5ØîŒŸ©_Oîn,æ@ÁXYØ xbÒPÅ”æN\`$]LgÄI±{iZè>›JýE‡ð“!“%%&Ú` *æ‹ju/9ÁY€ÔM޲›×Ô?5ŒD)Õ¡.'À¤lKÔ¯Nþö/ïà¦VÀƒÝˆZÏ?*±n$ËM;~°ÈÅ Æ1¨¸c­ÁobºØÅÓè˜Í¤k@46»ûû£ï*$& ˜¢bÉæ7ÛõBŒ^|ˆn‰R¹D6SæZ‘VŒèÐB•¥Ðƒ…(@Öj|mÛá4‚$Š Úa‹Ò4ÉNL›V€ô3ò¸¼ä)4kÃÔ»õ¨þ‚¾Œ ò¦ÚPã”B©c$ÆA³>Gê;k§'Žæ (úÙ !êîÖae÷°­¤ý°¤»‹ó ‡P¡ . Ûð~‡L%Fðë$j­ÉØ8…9ÿ'1]ëîÁL7˜Û+t–iã ÁŸ÷•ˆ&Œè]%(”l”2v †ÿxdÉÓM¯í,¹ÑV¬IOàÃðÒ›”Gû¼Ï\G0ÆdÐéèqFìq3„BÕ´…-p¡ž`.w•·×Êóçúøý×%h ™ò¼´Fñ4\ØŸK{{Ha‡]¦|ú忇 ~··,ß/ÓÇßÊÏä–"`nÀg'¸Nåð´‘…‰Nÿ~7Ì‘Eœ›–þiÒcºKFøŽó„Ä×ç fbsÌÿúýn¼}N L}×à9ÊL†F…3í Bsf®û]-¸7ˆò®Oh„¨&†ÎŽ5»¶Xƒ£)aHÈK²˜Íšws"¸KÌ)Ö ÉÃ_ø„&¦Â{ýÞ/Þ.Ñ•d­C5ýfÜÜÌI_‘M«Ð{Hm­ÁhýäÊÿÜ $Ü÷…â¨|^a3ßµ¼“=Ú|†RüÛäIGBNê¥gºíp,l­NFvï UÔ…‡7Mž»TRB©Í&°õV¦Öú¤Ø)¡©kTqe/E>JÓÓÅMª˜bŽçâ£DF˜íq¶_Óz³i±wù¬Èœ÷™Ê58û}7h6üÂó…NågÏ+8`‘-{ôz^«ñ F~VYV$P- ß½3Ï—ŽÎZÏ£ã€=êÀ(Ã![aBÍØ4Â~ÖÆåßUM­x—ÖìVC¬…•¹ ‡e:Žò‘ .g)UÎ"´À›`ÊPõk8òä¦sä È% y Xírá|üŠÕù#ÐÇEÙ´:÷%¹ I|sÖ(1¤õ›¿NÓõÍ4@7U#U2 `êU Ðc¬k⬋oåjg™#Œ:¥Ï¹¬ÖV%òîÊŠ!Ü5KîV[¾c|zX ­TX~j¦ýë"½¼0p—ú—úÛÄy²+w·ÜˆÅñ pbh–®|–,ë.Äiw’ê Ï\¹¹¼>i··ä9KL®ÁeÝHy6´Ð!Öñkƒi->íy½Èï•Ô*Ý(©‚žŽޝŽýg9ƒWi¢üwq! ÒLäÊ$ÉŠå­Û¿¡|X¹Þj TìL#R¡ê haÈ!ÿRÆøhô@ÃæK’2-NuSeB¯ÊöÈŽ^À+‘éÜŽud¤nídƒÕŒž‡Xƒ‚“Oæ¾Qôzm‚™qºÙ<·-µ'Ø/' nðu3ÀpÑeÚ üÊ«ÑNˆQ»Ó¶w“_–»Ô³Ö:Oïz™§®èws›.%›*„²ÿZ—ãþÑ&w毛9×/†;\Z¥óZº €f¢ó[<: ÍŽl‘ÏÂdã•Ë]ÑH—–Æú´Z¼´¸ýÉüÄa“| ”§h¡œ¿%!å¿ÒVŸ;eäz¸*—ŒoHU‹mÔ~RþÞ±!½l™íÁ“Qd‹4XÈŸ?Yrš¹jâ‹ ˜8ôOßšÎÛü²}§æè2Ç®‚½ÍàÄŠ[ÚÖÛ‡'*,Ü^UÎCŠpwÿ_=ȘXÿ3}õ57„¤0MLìØ…X¢èüІYZK„òn¼€®\Gç§}6§´¹xK-é ÃÛâÇKoý °´æP/¯íH½¿˜69 „í­8Èø{~l` è‰SÌ;¿õý’†wÔ AR(h¼ÿF´þ¿«¸ñ¨¦¶óÖê.(…R åÕ©µ](wíhæJI®äŽLy²¥‘èòóPWÁK2H ‚@#´tÓh¦Üa8ßs!üÌŸð¯dþ§þçRûkN3€„”šMû ‰ZwtoŽó+÷^žnÖŸŽõ÷Ñ;ߛƯùГ±Ç1zÂûý Iþ)'6¶MSŸæŠJ…Yhlu>-•µè ÕU½RéàW6‹Fÿ Gš,šžA2Ù{¡ŽaÑdXÎõ5¦±>Ó«¥?ìøEkؽýûU‡É*¨„߆wEÐ}1K¬²n'ؤÕHx¯îO\–«·i§J=Ð|xÅn?bÆŒ'«:˜Ù­ºƒÚLMÁHFeQEF¥¡ô·Ú:sp ‘á—+ÖûKµŠàÇÒî¿Êu¿à¬@>t çÄÙ©fÿX&ì™~‰LY²¶Œþ¹<Ç{teÁw`Þ`hý#ß²Vn™Tí¨§öʤ+:E3­Ek¦Hû^Š=B£k;ÖºÀ'¼-ÛT<Ô÷ÛÂÉ:AX úsr°0:‚p¡Œg©¥ØßÀ² £¡²œ_$^ɪ*©¸,ŸÔcœê¬-Ûé9¾ƒš#örnà8 )ÆMˆÆÊÀµ\¬™UÍŽÁ¹ë‰ƒe¨lº†âÂn@ eaŸ;óxM–©¹*‹‡à`ÒýFoÄb¦s4Àiøì0±ƒïêùüv±¹rÖ×%a©ßoþ¦ù¡ÿæ:'œ8מ°l/½i[”Ò(ö¯Œ[ñÅÿÙµ›— 'j)Öм5b‡Ü Ø^åaÊ&å!üÂ~Ÿ³!ME®Nšln@+9ž‡žá©€÷ 4‰/]F¢Œ%ÿ‹,5`¹R3éý‰)µ¢‹!.%jtË“s¯‹Ù؈/MrÂÖ »Ð#µD ¼âv/ã®…uAÐ1Î$%j…¥)ŠC t¡Bñ¢xŽ·Ã\y‹7éö Æ{y ‰T3o6"è¶P—|Å0;Dw\§q°%ŸSI¢¤E“*T‡?r9¼¬É&!z´ŒšÈ…¨´-@(c<^@ãI“’ÉÂÓBARr¨¡¥ €§vq¯y"ãû‘Í£L/ÑbS¶H °,äJ‘xD ñ3ßWj&˜Š³Ãíu_ëü²Ã}“Lƒªr'R¥ϓ9¬@–^éFž |ÞšlŒÝì Þ1ühk´Ëò ®€5ŽV?pÎÔ?Õ;€íƒýð¦ˆa¼KÓc&S{™´¨ ~„Îë¯h@̧¯/މù¨ÚÜ 2ªè‰RØ(M¯ìŠPà„(Š[ÙO­Ï÷Ü)v½ˆò؃_dÕÕ™°žÇ—1äá²§B ÜÂÁ•n®•Ñã­O…9Á>ëÀ»Ä3®`‡´¹& rìÝžp‡¾?’á¬Äž ÀjmhìUÀê…|Ÿ%ö†¹DQþ˜†št˜`ó‰Èõì3DDZèã*=êM“œv«#E8 $Ÿ`ìùï‰ìZ?·ÙdmŠàqž‡-%PNDL+xk3Ã:)›JtÚ›4õÆ?cðupÖ»!?È)•UáfRt^çq΀ð³…ÈS¯%ðÌG¨™ï©žSWeàÇH/%ô‰Ó¯‹–+\ h§ì%Þ[~¢Z³1}ñâoôšRê2FFïy«ê½ëVJñ½ºHhÓQÜ[ZÞR¶J=®µƒÕ¡ô™ž]á«Þï¤ Èµ"8ï³A®sŽôN‘x§(âÕ øÒÎÐÈ DV ª{¬%Œk ƒÿ€¼¥íÛ§Y`ÉÞ‘@Ȉ àHd‡àÐK^ µh(NÅ,žõ¼+\eÍxÅýð•ÀRûu§GíÐÂÄØpâ¥ÛkH ¯Á­2ߡܚ¢^x"l{n@×âbÛ[5iR¾V ·Ãj¥‚þVŠO¡ŒK"(%l)z4!]¯O›¿þü¥—j¯Nùš[[‡î$¨£\}¥@[ɳ´õ‡¥Ç\íkÅœB‘ëSéå$󄄘Q2»4õ³MâDäUÏŸ âk‰Q”Xk9L%½¨4„Šz^cææ" ã!y°úMƒ_7ºhá`rþ'·’ÄW' ø0ž tÌ„"ràà8R ýPVû¤87ÔbÏ*ú|–pb°îHæX‚¨ñZP§Ô:¢Á„zHpú…\0eÜk÷ý5“óÔóQÆÝåþö'ç·{‘ñG b}@¿ãFð¸«r¶b`w(’XWš¦@¡)ôœ *§s¸¬K“%ˤ É ·§•W!ÔJKwDZ2Yã~éH„3N"?A !‹ ®„fñþË+–o4PRb ÷ôj›Ù ‘b‰à wíùãz…\ôè=H ã*±}"×ÓmUù¥|ù€ü°ÃGZ¢öÌ9­6dOóš¢Û‹š¨¾Äy–lpNðA4 ¶`ãBu ÈO˜AR§U§æµy2NŽd˜}&mê•..X-Òkm¶V³áMIG”Ûg¾sÚÓ#SpÇLn2B ð1?Ò¤¾éL´IKw6O×Ï(-BH|zïm€¤( mfN(½ô2zøíX" ƒíw¥Ï±nÈñÑÿŸ{òsl2¨ë²ò%]ÈuF@‘rBM|,üÝa§±Î Q'ºà‘„½5Fœ¤ºÿy¨n¶QË´Eþ¸Ý&àã Ê«9§sÔD´/®,Á4У'׿øÒ.u_‚ð:[´Ùy;DÄ®O¸^aS’ø”b#ð07a†Úý&ÆçÂ/È]Ãp\¸‡¹Ði˜%“\u•̓u³5^Ž­¼ÃPV]ššOo+ŽÉ‚£ OÂK#¿©¹p}º{À·E2þÖ<ø'MHÙ Ž€n1©Š%€¸4® ø4D.¶›l˜?Å «ž+ç®FQ1ÚeZ^¯‚Œž åÒ¶,DÖFzÁ=’ÝĹX!ߌäüÃ2‡¤³¼ÎÅÇ8ç´¯¸G$í!Óäý”e$<° €ƒÿgýƒ@†lsib¤ðA ƒCÿ¼d$³¼t¥™Ò£P8%ˆ3œÏÈct1Í}:Á#õœbiêßc”Òõ{qÊ: 8"EmÚá¯-™]{hxÉwîÏ¢c„8Ž9ÒW^œ1±÷JÑá­i$dðëVÆöØ1õ­ÉýãäØ^=EL ÿØqyÆ7L2xèí«°ÌgÆ7Zò<Ã.Y¾qPØÆooßzêÿfgÍW+°^®^´vjØ×ª]wtê×°†æ­–·«V¨e=.S_¤ és¯é6a`­ºÚòêrõ% ]bû,>n?xx6èØ}[XT7ÞfÑ]iüWÚËþ}ÜŒ&’ŸßJÿñ=²Yf·†W"°f;XÓØËBó/Uxç·A‡‘ §4²Ïüü§^„<)J Âúß|Ù;˜Úý¬9Šmý?PóÛ´.Ü@((ň›¹MñY‚£•4ž„\ÖÔ(Ž„!|°HCý¯·ÛÀÃãÞÔ»,ƒQÈûk·w‡÷ÓÙ#HšOrbÑ_ipâŠ@ r1ETòJó “óôA dYS|{\gDgd_õšÛi2íí˜æ˜Æ0è›bMžÇl1÷—®Ì_9C/šäIë uƒ3ª’Ýø#™î#ò}²™€ Hcƒ¸ú Ø@ã1eæ"I„‡°Ò$€(Á)$¦A<~’èðÁŒåCdR&q2#à0´_šB@9 þ`m$„X¥¨^3idØÏ¿QƒC…1úñY–â«~|¼üL½œ×@ƒÖ=TFŠh½Y  Â]ZÈx¼Ø}ÀT=Z¨~®~¼\ýø0 G?'?¦~ýìþ*'÷Oð—‘ÿ ¬ ˆéšm‚Ÿ³+3ž«Û¨fd Ä:Ìd¦ÇɃ@Eö"%‘Ò߀ÈQbf„'ÁBòú#€æT°h[ü_¨SÊ+]ë&‰g·ŒÙ¬žopõ$èûÒꪵÓ>LŸek+Òé/lJš¯tçƒ/Öc÷w·xe0Ðå°‡ã Ü:'jPt,t§Hn¾¬ÙÇ%ò2±eën/v~‡}|]½<ýü$„M@µñBÚÎÐâ ÿÞt½}œ—yð®¾=¿ßŸÉŸï{Ÿú·ßÃÊÇûM²ãÑþ=äþßÒíåê æŸ“Ÿ£§—‹ë¦e«–žÄ ý"¼_×"Í:i×pš¤ òl£®v“¼„Û$Êöa`‡0F¹Æ’¦à0ðð¶ ¢X=¼^tJj~÷Rr ôƒ½® CY ä@Þ¢Óì²<î^6ó½Uÿ¾Ó’zåN/4{Ùä ›*$èˆ+j¾S|˜/¤ƒZu _ë1û}õP Û!J'eÎ"¢åñ¡ÒÏDÛ±’—¤„b&m ¬uiAÅ„:1Ó@ŽAÖvæòHö–4xògüzTe°jªT½CähÁƒÉB÷Säy¥gÉŸ¥DZxIpgrçûrÛ'ÍÃçi–®‘¯¾&œ •Á2ƒJÌUÝÂáxG‡vÁ ,‰ ÃרTº&5]7F Ý0%4ö¶ð©TÀ×4æ{€GÀÊ?””~ØnZž¶œ§þÒÝk¼‹• ¬MG¾À¨²Çû•¹Öb•DC§Ô,6ij‡³…Óòd4¨n<äìIõs¯fηÉý“étΈ{œþ~rðn‰2åòé4±nÉöHÐ ÁüÑày¡t/<ùbh^ŠFÒç È¸®vìû¥ìÌb¡Ñ¶êÄ”¦YT»ðo¼ÂªÓŽ”8 œé‚nõ2ž3ŒÏpªO€-ùdO›ÛpPK`æˆrr½* ‡7WÓKga@݆yªD;ä03Ùõ&³nì­¡µS ‹£”Kg*ÝÖ§ø9WqGÃÔ€}ÎSÃeƒ ýØ6›‚kÄt¸[^¥T h)TçטÁtƒ1†ch"špª×¸@¾ÌOMí¤“J/·˜ÁÛ¯„ÈÚ4¼ºZ*&”y˜k•Óšê·Ë! Õ»3 eê+¹ðô`§ë˜ö1º12nB=ý%£P©[2ë€þ/‡£PsÌjëçàSUÐìUVÎðí#M~ÈôòÙQPe Åª„ܱÖÌ;f8;(Ôó±ûìñUÜž#sR;@’N£Ôöá_ñí±YUP›Á;×+Z ¡UõBá+k5è2ì35c¶W1`…·®H¹ôa%x8³èfëCkNã2̸˜­×ÅvÎÃuiŒ)É¿ç¨*#,2Û ¢ÅÁƒå¥¿]>m/pºužèëTEª×ÈZÖš8ÕÊÑ5©Šj®ö´g滬Ý–Í–° yìo…ua'ÅB¼Ü›n¸Û9q0Of!P«Ôþ¬¶k±i빊œÀ‰T£Îp-Õ%øHà*7zÛ·Y±7vú‰Ø$´6­¢SJ AÜAÙõŸ#idè‰ÏFF¾¶•ºeº¶Vmž5„j97tæ¶Þi@LÖÉ\©·¶‡s•颦 •"IòÈZ AâUÙÝ$ÐA;0v“)…׃(+´Ç{% máç7qÉcëÈkU:ª")¹ BDÓWVK`löaáíb€ðRt/O,(…¬g*»î“)d0PÐÀÔ „»jÖ5Š•GG€ßÜ:©íPÂmjÅ›JµÀÿ^@HØX›‚"&ÉðëˆöV¿l¤™¡dY,L¦k[kEþ©O‚Åþ[jž’eõaè|1×Ë'¯’)ÊWóí/k®ÎnçæÐÒ ‘£ù`´I‘tM4…j"„ l ÞÄIÖì!3c2² •‚womÍãᬵډn…†XŒTÞß{ e0:H›—*Vy|„Q9ÅñøñòrqòºèããíâM’/l¨Š§ß=´AÍwæõXþ{Óï©Ìû}ºyW:0Šw$€11GG_#«%Z<É.¶~;>ÿ|öúãõýÿûèùÛ&Ÿ÷ÇåógöñÍû›ÖbÌk5¤"¨MxòΙ3bû'5á*jG0`í“Z=¿ÅZÅ2ä ±¿?Õ³d…Ô`Øz³zG|£ªÿHaâR4%<ÙcO×üûèÌ©T§A€1¹:Èòçì‰wõT"sNó·`7¯€*(¢Vcå…r7ÙM84g2|5‰[~Ë¥Šµú5ÁM¾2»ÔÄØ°Ø1è²g¾çèêHÂ?ЄU­ÅÞF.öÞà­šŽF¿"zo&å÷3u]É»ö´T9µE`Vì'T8ðz.\S}|Ñ;2å7-ÖÃTWÈþEÓª…õš’G„Ñ#Ëój.Ž2 &cls^Ñ™"<ìO$ÑUƒ4÷6,5 Doƒ¡M¸jÒé‹è~Ö¶¥ÓÖUWµc RúÍJPõ«$tUû­Tš€*Œþë1É;Önû30¦çv°°¥‘èw#]%+3‹nDÖk½º˜53wµ‘tǽf‹³>ö–Q;×»%¼¨P¸Üó&i‚óp… 7çØF‘دœ-©ÛK5'©@¯XÓážm? ­Ùˆbˆ[ÜKÏ̉´}½ÒU¨MÝ¥tµírÖ:ŽÇ-l‘‘+LM†¦Å"rÒ*º1ãã„©ž†ûVË{_h¢œ‚kB¡ìÁûÌÒOd±¬sm­:Z„ÕªZsµ‡«Oûb5C6«^9C(¢‚u§Í]$ýÈ9ŸxæÒ£óMvã qÙjl,±´i­Vpú}.1þ%è2/Ûh¥}œÅ*å¦ÞÍ»ÎÿÎî&_ý¾Ñj•¶=Œ´oK °©ó„ÎêäH‰¾g´1¯Ú¢¾Ão[-6ºî÷_€­Þ´»MþH ìDD¶žìïv­Ó«|B;Ä®ص‹ÌT‘¼èr ™½r·6 ƒ…ÚéŠ`>h[ì­PÕ¸”màc69šÙ¾Û‘‹\ 1µ²Ž Ñ/%©+«œ?Ïè8½VŸ9ØiÿyÊΫ“aæÄ¢ ÀSY¬ M ¥x•<7ñ„é"Zð¹±ç™G™§€!‚ÑϞ˰&è0ã¥&¦Á§-ÐÃ,HÑ98{ÍH¤h‘š«€ ê#³d1y/èfVrùšEŽÿ¹….éIöû6Y2,’j¾ qu5zá+WÌãÁù׬äk´sPC+Áælv@Ô/Õ9¦zá%;%…(M8.(l\ZX€Zý:–ê,ƒ»A±†}©K¯`Sû‚Bûîþl‡òAéö£ûY}ª–Ys¸ê¢¨‚™g°™edõ5Gê(³ b›*]¡UdS'ƒ±b© © ¼>‚-V»°(BZU¬ò(½ûàZ]žš½ÍèÎEÙñ1˜®W(BMÎL¾fxÏ;·á2û7ÁÏ0?šSâf-_û[¿2µöà{û °*m€sÉ2U:±‰¿ªúVtIJÈ‚†àXIc;™²÷&Ç>M mÑ8=Û¢O-P–Úð¸­Öïo±(FÄÂE.s;Üi>ø-v‰Š+5úÜ%@Âîpfû'¶n(ÕmFyWྙ¢ŠàxQ.yè(u¸»’$¬Be0íÿî߇~j¤®gs¯ÐHN7îgøB½pxL¡/º^œQuíGSôE9ì*6ÿ¡û +ù·3Ew]¯mÚi‘ßáQ›£k …÷ „íå†÷„øñcžÖËÄÒÁ÷qc¨L£Ø)͜ޖãJó¸²ì U­î«h”¹ Sƒ[ΚS¬1“eìÞO¬¾¾§?l<Œ~h>äˆ2JÇûH˜ò™C’iŒ€.¯FN•&‰"ñF$ iáMØ Aó4!릟­h¢—ÔZ­×î¶-ØÚ QcõAV\mò û –^î.E{3‰óÝaHãïÄÄJx¯V¤Ÿp”÷5Ú_§ÂìÎ¡ŠšÁ#~ТÅ<ËE¾»…¡mñ›7˜øŸûìÜÉ×~jg‡Éß µ³X gãïçˆô&ÀÛMHñlgà¥*•<Ødÿ£r‚}ÅÔáP+7ƒˆ}6sø*MÙ!Û;þ}#m‰Œ/–pEœýTYáJ #æß§¬lùá³r·Ê­Íâ-î»n{u¯NÐÞÉé¬b¬©[µâ³4€v•kÂ+w{u¥hzŒä¦Ë´u†¥(…‰Œ¥˜£çÇãs­©ÃüX7òÌUc=R%œAçX‰.ãƒô9=Ô‡=®L7^<®—4Ißà†ê2W½³Z$Yfø¦Æ¬¸\Ñ'h)vsVš ‡Þc±–sÙ’~ïâòEsÄÅÂÜÇ~qÏ!‡¥?†×úÿF$¶³ŽwQÌyÿË×ÿégµµpÔµøÌÍX–bÄÑ¥Ól8ÛãDþeIæp€$š¬E‹¤¶âhõÕ"•/_ÚͶ°,óÞÞ.‚Z@ƒ¯k½RUQ¼BBU(òN©áxl’W=«ö<´(ô(‚þ½åßæáiº) ˜ÏãäòùÒý}ØŸõçLþ[|˜ÿ½_“§Eü\|á~†é+.‹G•!d[êFÉf®81§¯<1ç¯@5'¬„È)^ÃDç"6r&±¤%5­ÓÇ–w4±QKNS´ÇDÎ2ÚMFžvXÈFÌB>ä Ï.±R²‘‡º”¬g"¹ 8‰ñ,”…ò’äH6ÊÞ£– ñ zg3gx;f¸ý²ÿh맨ÄÊ):ú™³©À«Q RϵmXŠ]êzdjñýRø< SK«ëk}žt¿éßÑùy—>¬Þ†~Õ»0ÃÒë_*ýyÖ]ÙŽ{{‹Î¦õHìµ>ì‰â"¥ï“sy@¯“±Œß¯sS‰^c¤^£¦&zÔg“Â;‡Þs¥*Ñ9–å(ú\7½³ŒO”ª‡<+úá ¿b¸g#¼r¨ËA{Q'ñÍ¢=“¨ÏAxGQ‡\3ë~Ë9ŸÔ4˜w8É­›w2Q_;ëpÒ_kÖñ^?éx*­«ø+ùHmù@ÝsÈCùŸ?X>\$¬¯t<`Þ>Õ4°ú½Ð ó_sæqò§ž…þ𨟆òΧ¢cdÒ“Lè ¼µÓ;}-ù žcGÓ#qÒ4Äâ.œyñ)á@:¬ËÁÑóäÅE„u(|°eÿËœ¯Èsß”+8»ç 5ìÈÃÆS)Ç7„Û.$gnÏŠ› ®Ëš]ˆœ+ò·­3ì>°³±›~àN¬GÉ7Š[°´£Êí€P¯\¿ìín .²Z~á—¯ï7ëyµš­\³>­w מí:e¶oõ©? Ç=œÍ’ÏéBÝ\}-Œ›l|Bð¡:N¡™Lq5 Âr1ê[v¤²Îe¨`õ¶ù7D»v5ÈAQ,™Uy+æÎlÁ[l+K…(AdNZ2CÊPOát+! WòGAÞ¥äâ|(!´àöDä#+"bVd‡QÓML¤öl«°<‡pÞߨ±Ï›?–^ˆÊÚJ*o}­Î»æªÂó,JêbÅ1K¶?vNyî@ÔÆ­¨RX–;¦_·ÝÑXUÏŒÝ23aźæ;¯xjçøµÈh¢µ/‚¨¦¨ 2{Ę¥K#3ï³Ö‚*–]vÒÜDP«s"t±Ö”Ѧé‡qB"’$Æ CQZæëö) 5©Ä2Òj³’3QÕ ÇÛ|²~óc—IZyŽˆ…Ù.ÛoÇQ3Ž$¹å– gÜ-ÿˆÍ –…¡¼å'Лˆ°Þ~±1Ö‡ÍJ`xV§¾F8:³cã률£c(c"_OÞDz\#ƒªµViì;Pæ¼aÄ"‚öÎè¡%Xj8×€^ÝÍBù[Yò½ü°"gÚ´h㥙JÈA¤d<˜gÍi_Òä5$—?y‹r=,ó$pÓ•°óâÑeÝzöEï꺎ôVÅj„ÆLåóôvÿãj«o«wQåñcÿ÷gãOÓìÛÿ³¾s•ñrµþ@r·nÿfÿ~3nû^K¿Ó£† _š:ÏØÞî£í}ñx2E8PµÙ Â| ÅèúdÞ¯à2Ûà³b‰D~.¾a%Ëó䘴|‘ùÏd¹ßC °‘kwÒ[ºaZ| u½6b>ÕN «0Á±$S~äÀayåCD.hÆ<‡ÿ£¥uÀLqLÛ –ùѱ‹…}E&Vî{ÂÒ7¨©jÇ-[vö‰xÖí<À‘…½êÑ÷·q¸â&S0öÙ¦¹u³&Š1©ëé__*PùÉç”ÁÀäY­qy¹pœêÌŒy®ß…åÇn¢›M¾ŒlÏm®¦ŠXEg üÕ²ÌrÓwº/zsz¦Þ‘ ‹Ü$¦e €EáV;Iv¬î+Z·PS§9î C-ÄÈì9íC¦ÎŠKJ yPv HãEe>;\”™v\üÈCÞG?_ŸE<ÿ#±£tú)±} H™Bó?ÐSíNo9—ðÆ‹ ë¸hƒòŠä·?,š]3Ò‹ûÊ0F[oh&r;MyÉMÜB±C–ÀûÄÛÒQ{)A];!”bJ¸óQ3¯³ PYáò Èè´ìKn¸áMâ>ëlà Ð>üTágøy>K^0‘„ï½_æ"åMúp…$uY&ZÇÖQª¬£NÓ5\€¹p¥X €«†1Ìw]„1ô?6CfÙjÍDÊâ E yÐ8ʽ*böÝb\- ò]GÝxÓ ›®n›®fWJCŸíH}êÚ;í/è"6ÁÀÎ` ãÈ’¢}èˆÉâë—€[" I`´\<3°2“ÈÓDù3¨’ ÷î:r•ß%3É÷1Óž(©¤L`„*h Œ8þš)~_P:Òãà×Êjðÿ>¾y¦F¡–™Á q‚ÐHàÁsêÃPóÅi4^€â±&ã¢VM";[ÄNd•86ÓÏ¥šÜYEó¨–jöp3kÒdëÀ¾‹ E¡‹Qÿ wò¡Å|Us~ü›¨Ϫt#ÕSÚlÅåÛagÒåd–Êõ‡¦qúÍâ8Åñ LŒÄ¸àPþáó™„ú(HZ/܈‰);ºPŽd¡àùŸƒk#"Î Û*%=2o°‹E~g«¤vÌ™é5e¹”>RÌ{² ѕ똬Ìgl¦6®¶©¤í퇪ü®ƒ/xhŒl¯4DQ D©ª6ioé…×GÌȉ0“'Ï>`FÒps¥-b@âŽã:]©!”àDc£4ƒtèvqî›ïl¤¦ÙD¢`©aY|öë„0x²€(Ñ\¬ŠAŠ£z|8.÷Ñ6¯ƒÏqSfI™»aI|˲N]Ù¾amöc.v*Ц:zd[lªµƒ×EU²7÷&³ÎBÆœ&ŠN(M,ÕãmêÛGf®fÔоCr/2ÿ Ò°¶ò:r¨Ð\㟂pl¸D}QܦÈgíGì‡ðŽkäºÜ¦É¿ëŸÙ—n„Ü“àà>g~åMAJpJ˜Äü¢eÖ1mRŸØl÷(½”,(K ¸C°àx:ò;ÑBd–¯ê'˜ƒ’wÞ$)· ˜!~&˜ónm®´B㉵à4¾‡ÉL©›`tšÑ8Ýe`€•yDËnòƒlÎ0Â?c déý.é=hÈ衆§°[+’æO?<»¡êð€Æäöæ÷— JîŽ841ô>n R¾‚÷ÅØÛ0˜â´Ÿ¨t­ðÿþL‚yÞ´t×ô!J†Ž©¡ÛÖ8QHP¸®–v[óhÀ"n…;Ã^»ýCï§xÈ” ì\6Ò''y69kÎ0Å ÖUnðãF}¸ï,i?I_8Gíþ§å&>ºHÑM~ ÎÏm8Ó;ﱤ˜÷¬~ràÍST‚$cÊ5"–Ø ˆ‚zœÜ0Éh’ê6v˜²´˜¡¡ø5°M:ãûµ›«Ø¢·ÀÙg›Í,¿þjÑd{é C ð]ÞBžäEH.èAîîQ žê‹Un‰o°ã(X²]8_ž/ÆÛŲ(²Äšr&rÿöPb2nbøí 66™lHCæà›¶àÅ„.Q·ï²O ,ªÄÍiE¦s—rYbEXžwPÈÔ F+˜¿õFó?‹B©Õ;¼ÓCÂÅ/á®ÓZŒºe–s¤GJë»~NöÊÄkHå®Ë•b±gÅÆíkr‚…ì8 .l_FÙÐéª Mw:ÎfC» ¡l:˜Š§€F$B…Å*çÃVâ•wÖg³ËÅê´M2¸-»ÃúÔÅ£"Ã_ï¦O5›½õÓí¼¾vÞÞÂ)»0,óÇ/¥ÛÖjMÖ•¥s+ü…@ï´³EÎÁÉeýù]Ôº_U9êWÊ;ýúu‡wc½’OÎ瀒›ôîôÛ’ö-ˆu®Ž\¸â€ÿ$¦j,Eýƒ0gˆpE'{t!µÇÕ­ І†_¹e³ù*pÑ:P¡q`㧷ܾS“ÃmH\Ì·pÓ°;®|,ÔY#è’DZ‰èàS«îJ„Ú´IŽÊ.Æy:JcÛOœ¹²z7ì¸Îb 0tš%kÎ öªbo>ï¯Á!ßUtFz­º)Bm1ׯ£a0zª×òÞ!TÂ*œW v/Á3W¡×H&2Ñrqß¾ÔÏjÉmó†cŠñ¸Ìœ+S2î±^ꨭÀžÑôÓº<¹9›D²w bpaÈÁãîejªêÎûíej¬*ÛÁQØþ¶Fw_ž­fïÓ`VßTp^¿1.Ëìg+5{L*5»š}®¶iƒ&Š×-)æ¶öÁºD`P©l“$ Aàp%ÈfŒð¾T'”-Ó+P˜t/{‚ßóåuó­Àœ@œ^H”AÌÓdõkºÍá8QÜO”âÜi”#FiFv"óõLÕ¥“]ôu¾ª‰ÞÜ™™©fÜTùy%‘t'Ooæ‰ûò —”¿üS6¤íT¨O¨ÞahóÆó*]Ð^ -sÌ/¥q8 VøÎv:J‚¤XÁr‹†„#8Yߤ…§Id°¸ÑiþÓÂ'„'ù‰7µ;¬&›ªÑ^Ì|‚¯Ã&UÎ)e)þ[/¶²U­íÚoÏ‹I§ÿE$Ð]‹··šÓJ¹e¸·P)®Óá>\Ñ{> ¹ÿ^÷`4ÇøÁ·¦W;„öOÄ2jC'ó~434·(̆ö]ôV‰ÜÌ´Žé>BÎám˜æ7Bb¬ uäSjoÎ~ª6õˆÿ=;Çw==àüpMÞð„0;xxÒ]nŠL —£¶è¨w!0Lé Œh®\Ù$›a…î϶̳ áZžmïOµÄ²ˆ¨gæûc 'h€3øMiˆÅZDrÜã$iy†*©7·'œz’úd)nÕ~êÔ/AmÑ£¤»HÖŸ!°fD0½ƒIùäPj÷_H¯4ÎåÈcÁáñq)á'/Œô ]øñ<qÁ2ˆÓ÷ئÈèô$!g»tˆ•€Ä>bªæ]Ê͇½†%]Î!Û»}iȸ˜)±·ùŠÜ/ð|ô)$‡–†AF6ñÉ ¥~!á¹36e+¦ì òülUGEl#iÉ–ºÎIuyDf‘±}u §‡K±±¦†{Ö1IKÂ=q©š+áÝeœïê;êV©#Á…µ„×C§3¬ìÌÙÖÌÇ4ÖÀàA¶Ò‹¬ Ï29F­‰«1á¯Äˆ!¹[SÏØl¦†ß³‘g€AëŒî¸ØülàŪêwJ8•4ÒRve\x4½ˆ©šÂë¬f‘ qSñ¢KŽÆt%CšÇp0û̺)s9¾‘)9=¹2æÅ Ü·Xš}uµ—f1¸ q²­  (1à¯}vúH£Pz_²SL“¶EìУ¶M‘¬b/Qø ¦—5)¹+ßš¨ÿ;Ïì¥y3¬n7ZM³×~´· ù11mN¼0ÖON½Q¬qM”®¾³N4s®ø[9Ǫ´Q«éh´dܹøáÁp^¢¶DÅôóŠ?ú(~$ n€F8!È%5²¿ø¹+'ŠNDãgc ™²¤Üg½Áí3ýŸÓa€¸.yŒé0îÌÍHÓùs;ùÉp:¿2²}tP}ÞW ñèšà“Ö^ìÖ.t‘@RÄûoàûBdqiG²!_EmßUÑ͘ẓ–OtùPÄ÷„Õó9¾N£%aŸ‚—’œ »B@jÛõºE (¦R0An¢#“¸éØ\â+±øÉ°‹žx8º1×ó?šªF좞A‹Rï^tl^Äk¸Ý…—Z·3 =LN;0þY6¾Eš<ê• g"%cß®©Ógéƒþüó²iNÏã©c2ÞÕÎ+òC­›+´¯úþþ¯ÏÅÕ ù»:(ùЯää˜1KÎ)O m«ðq)òª›Qш{Ü ‘g°¸-V\+ôøCömž3b·úÛ}´C![jùÝz• ¥1=ùHt¸Ôà ‡¤d¹ñ=ßk ¶rè:Éí åHúöªkBÃ$o8î:VÞM™OÌÑçõäÕ7l<Þ«Èø‹eªŒF§7u§ª ̉²OÝ_6& “¹eøŸüg¬Ù§¯_>ÜÏ9p½9ÈEqj=)´Ï<¯ 12äHøÓà6,¹ƒA¯W_[£ÛO®ûZ]Ÿ³ïÕçîÛÒûGRÛ2drBU7¬òJí›ÐI²+ýŠ ­Î?ŽEáŠÕc•à謋 ­é¯fðM º_}™hP¬2×WÕ%± ûØ­¸¶ø-V²GB+³ QXñâÔ×ukÀ9Zñ«æýÁmý ˆ‘ëX^yî¥ýWkéêÍíñgݼⲕ íº>êÕ³kÑϸ Z‘Ͷh †Zô…݉Á6DÓ€ûân•THµ)¡×(, Þè úñŸ¯3Õµåõ²·èBÿ“؆æm»V„¤ž´˜£Ê´VQ,˽þ•Þ‚Ûø—”w‡¿(FlF=Ñ¢EãyȶŸíðI,ðJÜ~DIo)þ¾ Á\¡pýTͱ(Å›Œü‰ˆ¸¸i§º Ç9g“ùdF>Úù©L*­åhÌQ¯éÁ$¨ïÍqn85‹]!=½£¯‘Òº [þˆP{Âdn ¶W[œ|&^Ò/y,&°AhPð1àé™øÌ¡ôŸõIÙA¬íúl5Þ¯(4.8Z.aôþÁÇŸóá¼Æî9~ýhS0* 0í10ÎJ·rÉøïlº—£ð‰’̉x›÷¶9;ÊîåEÜèKÞM Ì‹ŽúR}™ è‹õ2?ÔQÖÄ ‡VÅþÀC²(m »â\‚Ÿ5Åo°¨»âw’[nµëé?È(~ÑÌ(~ñtŠ_¤QýB÷+ÿèCò`¾?3øÂý{`ª¨Î¥BT)Ïßøþᣳñéµã3£ïSY¿¢·ÔÄ•?jyµ(–.ªåî‡3éQÑv vËg~ÃâuÀØ„n0 U@-Ð CÖWäçH¯â:߃Ú.³¯¡ôÙÄ_Ù”óJñ7uöùÔMïÃÅ‚vWæƒìäøóc 8ñn>»ò(’Õc²™g ^;âeÐ>]o÷Ÿ®¾Ï¹ÿ5¹ôg`Tñ µì),½£q8pýQ1get'&vBr ^¤Mgàÿi¼cÊ€†H@ÿoWîN.ÿk¼Ò°µ?ê@üÍÓgT…Ò°Ã!Õ¤q …Ë2swkŸ$”ëâ0QL³ÈFÏT‘¤Òswüû1“J³)—l‡^ÁGšÌñ<ý å é–¡.6C£ûÕµ„íéÍ‘övðp€H¯Ò±fÔ×ðpöz=Cû(]*]š"uŸ$“R«ëÐ¥ü±cHÓrÌ“ANˆÍKI;iJ"rF«“–³p’'M¾êKþS4ÑÁÊ&u®¤ãÐI¡Ñ¾ÿ4øÛß$é‘”#$ ¢ÀýÁG4¬¥Òhb•0-dÂ! ̤วǤ§…€ÄŒ¢IÆ Om€&' xPPžNÈ"ô´ofÃe¸cø¤©¡ ''¯^¥2”Û¹ø<€œ8ùªžü2VYoÜž ÂÎú«FÂ’Í­ L­+«0T+`iè<õ0C½è^ Òa€²Œ£èdP‹¶4åž«jÎÞ9§##ìÊð@Ô§';š`䋵÷ïêë•GggGWo?zCîè œ¾ªv?ÚzxAíó ˆ®Öß·?ªvÞ?§BîOÉz'íL<;n†<Ã5›¢6‘ʋ̜X ã¬_ÏÖáu¤¥|Ì’6Œ™'s¡QZO ä#ófßµóÓÛ •~">,‹ICqHËÁ#$½,k5½$Öò0Jù5å+‘gÀM“" ÍšW$bÞÏ×#¨¯¡1Bë¡8O|ÏJWoTRú—óò4(S}âl3… ™ô„Y -8ÆýçÅÛé¹Ú'“{ €HåNC»Æœö ]ö\c0†Zõíâ|up×ð{¿ðˆÄ‚ª´Î Koæp`Lã´ 8ÈB–Ubxa_ñ@àÈUž™Ôœ²”#íÒ‹lלŒZ-›Ô¬—N¯o’;Aµ‘„,º—²–J¡ìn&SE¥­†FÉJŽžJAsûóäìÇòmw8_]Üç-ñ»òg\ïq‚•k®¢¥R],on/?ßuß_$ª³d7ÐÞu8ëyDýK=o“Å2§ <O¹æ%²B»öA#Ó°kºíÌ£òoem¡…]}°½÷— cýeÚÚË8¦Â%ôî ˜šôúÄk¦·•D\q…GßjŽÆ` IFué³l£ÚÑÛûã¥ðkgŸ‡ùŒÍ¶/ÆúTöz´f†´—ͪ5‡èXft—òåå™ÝÔ*³~QÀÆ‹#`¦˜Âš^þ™[!kKj:¸ œ·^°­dbWûýܳuûóÕ„ê’Z|°¸í KD/ãó¯0ÛhmI/3¤ŠüÁ/²89Të0çJ¤èĘÎÅyÛQR"ñï~¤]çôÁ×%GTV×fÕµŠzc%ÏlhFÂWÚ£¾u‰‘¨0¹E.ÝŒ/ŠAñ2ã¦l¡”Ã$mÆÎ©R(ÅVå×’òñ&ï?åaWü]}¶h0/î±!4®[ÆÊAãAÁl¦„ÅáÄç9ÐP!Ÿš\MK«§n,}¿ƒïïûùŸ,ƒ8 g¬:Žƒi 4*[ޏüº<›#¦rؽœIмL÷ pUÙ }RÖOØœŽñˆåÓÄ-E7çKË̘Å@&Ønw„ÜQAÿÖLüØ5¡ëÄ©m»<åµrÇä:/+Dïî›ýDßï¿*!㬰˜Í§¨¢ƒøCˆ«_ ÎzÑì±K ¦‚ÝD£¸Ðw—¦méþ˜}ûþÁTÓ*˜øbiUÔNf»°#‹ƒíK#Ü‹q,ðfgýy6ÊÍÖ‚þzÄÄÁÙµ¾P9 `e#‡&Šèlê FC B ^æ¤í¸zjLÚ·]žÏÂËøäN5UÌü÷ –[]_‚—>Q¢WÙjàç0)×jnÓ8…›¦¢þõ3‰xós(»«Ïó[ä´}Gª©òö‰pìzúÚÆ8=òûïT¤yìÈ3ÚHnÞp‘–¶2]\zÈ,"ÎÐ_Ú<‚üÉŠJ†ü Fw¸•ú½>ìŽS1õ¢\P£!æ5»K¶ì«,¶î9I–mÛ.‘½}çÞžôm¥¶* 6\!Ú*ð¨w,@ 3í™AÂÿeÿ+ Ьë¢Ã¶mÛ¶mÛ¶mÛ¶mÛ¶íoØöØÿÌ>gí5Wrræ¾è¤û²ÓIõSõV÷æ(§8Öà›¨mö6iÛ‰ü²×³òÆ[ÒBT U¦ž¡,' |#®r|Õ èéûg%ÞÓŒÖW¥ikÏò÷GGtIPRMnÔ6K,´z´LnX¿ûEw5œW×<Ûßí¿š¿ðÑ©-[D–½Ó×9@uƒ{ïÛ\ÀZ’¶¿Z¢ÝZ+*„ÑïB=üZ¦¤´Ä6B!n ÊžGÞƒÚ/R?õ•s0ü²òc.| ü©‹B`ï –5¿H©ãOêCë–úç‚ô^p ¢IaIUƒòb-û)^â§r;ž¶ñ7¥„ü¦å«fWùÖK…,±*y»U«Oæÿðw'Ÿ”åBöb¥Œ¥`³ r…&ugØÐθÿÆåzj™(ý$ÔØóÖÐ g_—+} WDãÁ»,#…NŒcó"ÊRÿÍ:èúó¹—@iZãQeòUì'ý2¢¨4ûT ©gïËUt³öÅýßUçQéó¶(€lÜÿ©êü…ñ¬æ®Ž8êmæîêíåxR©„ (œ1 îvw!ÃPHÎdG”’A BÓLqæÞ®æìœM™7i´¨ªœ¢¨ê”‚ÎŒŽ¢'LG¡|ÇþE梺ZCËå çî²ïÜÅ“¯o—êÇúÐõþŸ×Gzà„?Õ³{ôï 1•ЧZ‘öíbkTm*cM”]kŠq˜ëb-ú¡M”]rh[„ù¨…œËYHèYTÄÙ¨…  úy'ÕIÎù" ëC$|è{ƒ7à±M»™DéùþsójSEzE±÷/ÉÚ]}ñF„~‘ôåËvo¶EæË·ÀÞksÂW?wÚ$ø­wÙdõ~Ъ¡PÔŽXVŸ5´æàu¾†Í)“¿ÍC$m³Žï¥ÈE,߬ÎÝm „yù—ú©èÆÁTD¶a·YgÅgM¤C5ºhÇjÖ-ºšHÇD*ºˆÄ.Ý£KõЮ]m L»“²Ýp ÷=¨/£„]IkÓ¡ õ‰y=µB%”îðe-¼¬²î9˜ºGc›¶ê»²nI±ëÂæÛOŸŒ ·DrÞX’Ì Ü –ª¿#}O—§¼®íîv¿W(õz½O-+´ª•‚SZ¦õq£`虨d o|zŒø CÕz´ÑE¬/ïiñá1„m$½¹Òº¡3–²<ìjc/Ö‡äy)2ožlëþ4™Ò¼¢©ÄòÄÈ~¤©EH­ÔX/ZZ>ô *w27·a‰ w\Òdð¼6;WÓþÝ{–IRi58r™$¹*rT©œTbkþÅëû@h%½¡] jµdÿ´Àn4¿^q´YlšPÌŽ}ã°­Û»iÈ‘ØVdJrÉáYì²Ìô‚$2y¼ñít5BùOßÌF/¿tÇ`èWL5XwÞLÓSƒ;´h¯½¦ñ¶€nV/Ã¾Š‚l¥‰s|ÕQC”ÀŽmT 1 2ЊЀÖJƒ¯ú >‘¤i¸ ,]jëëÕQYÒêríÖÉI|!䙊¿$¯ ÒR‰’Ì}&GIl@rˆo÷5¡€îŠ˜iœ¹ó ¾ZõFV¸í~‘ÑÚ¤¿ùÒúR&E&§BI²÷W"G½@‡_éuõAÓÛâÓñz™ú0°8_¨â:N+v¬'?L»å¸ÛãK⇔Ã嵈ä¸:0q2”áf ±%`¿œâA¬Jþðnþ2—kÐ8ꉫ–3èükéë‚Tb®CŠmi¤ =ŠFT’ÞÔ† *?³Wãû‚ØŸ{L“N…)’8N‚lRW&~%Å…wXÞþ\ÓÊó]éæÄz`F†ÝIœTÆ?ÄqÅÓ+ Y(+ŒÄ*œìq«`ì`Eº ý¹qýUQë=­¦ª×ž—¤^+zmiíf<ó‹V$¤°ŒH(™“,qØâ†[Ì á/xhÀ%WŠÁÐΙËÌgA÷<Qœû@œ}Xœ ›EþPaîlÓÙn$#‰º9° J)r¬1-w›GC, €6îÔDWP³’ ‚ ÒNFá`Ñ,k¯Át [Ó·;ˆ põ«€:A¦b8s5ñIÒ4€„¶V!Û^„&ïË "$>?Å}óà–pLE—|9à'øj¡®; ã°Ðe€ïáû«ûrûøÖµÍ®omÅ, «]_ǽØp£©°á9’¥%Nc"œ-ÝeÇ©¯6œSd7ÎÉ4›.·"€DÛóp9-ì¬]À[¤iv‹ÀÌ„ ‡ïS›©àÊÂÁ;.BX•¥.nµ2 0-.%êSè-¯ôGùW”Öçt¨Û¶lx€”Gr¶5>\‘Â{¥^)Ä0ðlP=÷ºjé3$b§>߱ŇX„N+VœÃ/x®i]±Z"r cö67.w±Ì]éMhÍȰ&8ÄU¬"8!Îy+Ʃ͘×ÛÉ›rRAÒV8âY0IS´É˜NÚ ZüÒäÆéOP×ç{&8K’(= 2ê%)è±ov2ü6-X¤¥‚òú‹Ð˜Œ“iï»™=lŠ:¹J[ X!&´¤ƒ2=?΄Fèµ]¡‰§Ÿ0Œ»1®™v¹VÔ)W3ÑkêÔWk]¸ªµj #æE^Ó tŒ^Ö=ÄÈè: ú6üï°ÅmFù€ÓÄw×á`»!?avE#zú¦„7›² ‹tƒÄfHÐI/=Ê´åWÅüÆ‹Ìì¥IN|]”çO€+„ +ÄÖP§wO\Ì÷NL\©ÍÇÔÉe…“5[>ⱦ/DV% jɹ>ÇÙ`?óß‹Œ ¹ÍqLê3>ò¦aËÅKsŽe==â—‰DÌH€Ccé`+(Ó6˞˅³n ia¹¥™È#}\Î{ UÑc éÏ|~G±^—#~c%‰ÍIÍÄp-’*RúéÊ ÔŒÈ¶²SÔ*)QŽf¬R$·Žn´I.QNG@t·¨(ž;Ö‡\P9…áY“…K1“ƒ=Ðû4Gƒ3¤_ïLÿ€ßÀô„ÖÎí ÞÙôÆa [ÝõܸY–|a"Ó-üRWæ´Ó¯™ä¡f‚´Þ€&Ó/}’á¼JdæñwWTöp,:l28§‘‰¥˜ßŠnyõ¥ôLH— qáçµÎ;ˆÊàJGFÂòÚµ'4ÛWº&î\ìÃsG†ã hœÄÇÉwF(`*Ûœ ubtЇÉ7;·8ZžšÝ_=³XZÇÔ¬ûf˜¸£8s×7`¥Ëw VNVþZ {;]b?‘ $žB†T²–{9É3}DQ0fté+/j“²n>¼Q¨ïçD¯¿±š .\j4ñßnÒËÍ'Úe„ût„hTË/B•d‰Õd1Vå¶æ:1ð[cñdâóÅMhÑ!˜ØU>žû}•<~îè4x—AISæî¿Ü!ó‡?³Wù ßïòb­Í1ì,~ÿì?°ÄýòÁ7%ù}ÙÆú=ÂJžž y#Ì“rlÿîʯÇÑg(åßÊoøû†™ÑïÁŸlòéw¥» æÎ\°î¶+ðQù¦ãTsú7Û÷8_÷„{«Xöc%Ú“²äI/Vð[í3ïòûÝúu]üƒwfÇÉ:–NCF{W+C¾cPËý&ÆYpü9ÁOvY%eíRõ²0(ëñðŸgÇåNB¿4EÂ…"`Ò}uOÚõ‰¹ºE5Ð,‹òÊ…ñOû¨¢l[{~¨líOÂF»˽FcüGœƒ &»d0‹¡¥§¦oÈO¹ÆÛ—4¦l¤Gñ8ñgÓ=7Å.òžËÊ>—aì[× ü½zò=ðèSÏb—öîyú¦Ù¿èÖ¶@õŸtó£­µõ0Þ¸)”»÷Å¡G³)ÀÐÿÂ÷€QøEÑG忯A¢ò]‹à©9ÿ½C9X±_=¥Ž®š™þ*9×jNú_™Ú–…Ö ™à;DŠeäeUÛžZýÙQ(ˆœeB‹Êü¥³d[áï 4Ò0WMË{7$}%—»°ÛY„ŸòÓéTiËcóÊcy•©ñ ‡ߺ"ï £•ôzryÓjW?w›ÐnþýV½XʰÝF`“¨ÿ“¯1œLM, ÿ¿e³m6„ß[}G¾ =[NÊ0—sͤÂ1­îˆeTQDˆ—¦Í‰"1¥Æ^Ã?_w¯Vò9éÀOa‘Hwø|ïÇŒà5ÀœJ“Ú6v‡/‡‹ô÷‚ŸZ˜¨ZY ‘KCR¨íp÷½ÏŒlXàƒsˆrȇËŃú°‘ÃÖo\4(ÊroêBÕ(hF°M X$–)ˆ@u>24>­D”Fø†”7<ÚeŽGÒ€…ej -!{C²œlÄ3ý­“Ûƒ:]DÄWT¼Ø"˜=øyº1dè¾D9•>QÄ,`,¿¹R²erëHGÇÑ”1#H)`ŒËÎjŒû¥ijcÍx óiBÄ… K´å•¶Õ&âlöP Ý"0s aÈà~ü¹0ä¼Cï<=^ì¿. ùòôôòâêÃÑ1ô‡6<\øz°ãÄ^_Àrâúó‡ç=àuE¦©§ò$¤ÝˆÀ¯› ‚*œ„ÉôÂɺ4¨Ì!™]¢¸Éœ zÃ,"ô)t%³Mƒ;¬S³=î™Ý³ã­¹æ–ßQ­†9 ‘¼>¢I´Då- ¦pšâLmö›”×ÿ’çÃÇÅÅK#Æz‰&™6¨O+SýÿíìR“µ”¡ì@›¹^ÞCÿlÜš›ý3ê'/:x˜ÿºøK¶*ùpeŒ¯•Ö¶+4ò!„þÞ!ƪ܌A yR¤^±rÌwÈÐú„5’²»XìkâN¸5ýB0Ó£ïXAi8ž$‡º]‹Z˜æjÜôˆÇÍAû•R>Q_jqŠ!ZjöòH¡M­m¥vlÞI4þ´yS¢Áûøu"hjª1Ë€bi"½ûôÍúfY½úH,(ú)¸³a¼úÛ¹œpÅwÛʱ“”àžSúÊ7XQYá[FJ´tY¤°iRÈ®t¹vÔrèG ŸÑLÄHkÔ ×8´M/Æ­òyöÕ‹ò∋¢Nî8Ž™AæR›J[¡Â©sº!ïý ¾•ïCX¬ûEKe¥i5&Ó;ôÓYä%àM]့¡¨‘ê¡I£×dÐþo:'=êWhf˜Ö¬Wƒ¦á ”íƒÛÑÀ:_Xè8NzeþP0CgææÓdæDgéÝ»‚Á‹ :¿(OU/Çü’÷R(iÅK§Ó«@I»ÏAzé¶Î<õßì*EL•²õº÷ü¥kíLWOÎJjé°Y's‹žä¬>H®]{á$·ö¡* …zÆ{D­¡ôã…V«Ä•Ô\(RvIKÂʬÊ/>îŽÂ¦´.ÎXè(6ߥíõŽ/€±UH LÃ#_6æÌψ´‰ƒþg†žð}µ°¿ªÄ×N¡ýJÂèZçÞŽ ú˜Óy´EZ=ö{ܱÊ1ä 9pËï@lžZpQÜ J=NY`Ãy£Ômè«]Š[ýáñnÿk÷É•Ëçá‘åyÊç Ó 5w»TŸxöØñq$–x|= ]¼°¬kÖ·fgky÷NØ}ž"w”fÜ]¡ʬ‡Ži†í”s!’à[²ƒwƒ½-²5Ó󰦿sûôõÄ÷ eºgŠ÷ü~®ïsZbØ”¯agö¢R¸$¸SÅ¥ÞXÒ”çNàIטq5Å›]h™ö†‹Âm”ÕÂßä,éöní\ñ9w¬Hã·ñWܸ+n6¨'3.[¿;Ôß·Ù»Ò }«…G Óðѽ’^0°Tu¢Ê?¾Ž+ó„¬êP«=%ús1jû„4ƒ§Ã Ý—©lwQõk £(P»Ò =lUŽ3ÊÇXõ–܇Þö‚‰af¨l¦ö[Â\QC¿ê·I9ÐÖ2^/‘E•w×@¢'Hwò X Ÿ2˜¾6ö¾Vï²¥ŸGåîW Ùç/O‰­õÚ/ø–1LÝ´ì¹µ.îK©JýyÖM¸Ä\áÙÒGê¤n”¬¬Àú–õ:æq‚Ahi>™÷?ðÔä*£Å°¶¯Ø¶y¶ÊÛ5ÀíóáÝp¤9b®k"Iðýe¥ &{-Ãô.x"4h˜R6—7‘£XR&m•¢›‘­ËÄ)=Gè.›äîøw›-Ã7é­žCJˆ} ?vÍeŠºþ¢|›ÓäÀ<Ÿ&Ožì„Ýšyz.‡­Ý.ÔêûÊŒX•žU/<S+ãÇΕ¬x$§bÛŸÿ÷”¤ˆÈ€ï"ïü¯kÊxVóÏ~{ •_)ÅæÃûzÄ.›Ì¬­ ™IÚªqƲÝõêÔåj÷ÜmS‡t …ò>®±wcÁnà7ïóËòoú7à¶×'«é¸L †îÓ‰‹ïîÖwïMù¦úïã­—ÿMó¡jðMø=ªÌÐù4Í7¸3ì6{†Õµl'ëV{¦Õ¥n§ë–=c{á‡ótîâ—Çù̵r§ì–¾a‘›–Ñ]R9-²ihµ(ƒFÏô˜õ´®ñötÏLÉšFÏüöÔ®áAötOEÅã·wRL{fwI¹¯,²žùÍUÖGÙžù ‘­rÌ=Ã[\VÝüA—mÏì–M‡cߌð€iüwnc=æg²U“a¾CvÅL²7¼¿ ààÛã¦]æ\ :KgBæ(v/ò¤—Ƴ̎NèébðŒ)ŠªI%vsZŽ,‰N,ÎÒ N¹ÄÔïiaÞ¸2/ZÀуáùßm#QªÒ8¹_°Rú‚ƒÚ†[,[…²pG£1áÐF%1Õ†%Î`$[³¾@`SNùœ`p÷Dèe€zeóLNðÍ{TmÒí¡›Í 7(ˆ,¢¸ðÚý1)7Û›¶+NTì93–'|왨$ o¼À/Í4' ’ü·Þ 0(ú*œ68ë#qüQÁ64!·cÊÉøÍñ›[ïn¸.†ËOÿ &yÿAäõëò‡lWå඘‚;9œÝ1àœâ¶ØdE)æ®BLLv@Ú@…!N B¤¯#úVݹ¨¤úýµæÊ™ÛÐ?Å,\±É´¥ù’áï°p¼y&¯!Oœc3’¤&¿«ñ äõ¹"³W/N²žž£bÜ ‘?éæz†oË$´G àèn‹ê‰lÊdŽÛ£p<Iê6w»öÆ®ÝSTÌ`;HÛj©7³Ò6/¹Ìn\Š×kå©w†Y±¾”驘ôãßÞF¹'¹rÁI0–øMû3 ð€"p-Ю\Jwÿ5í£’ aº¶KÀLRðºÃ¾=”ƒçßü}NŠU:=r€vqË„ŽhˆG¡”Ï/ɘ†ÂiÕê/ç‚ôàÍÕ¹à‡Òï‹ 8ºðY<(’„”ëŠ1ÑüS—¡Å9„!%ÊJ'÷ˆa¬†äê(;1Qý÷ü¤ô5œ£XG7F£󎎰l)½P‘À"º×æJúh»8_‰²KqÌV$ë::îÝÿ&¸Ó!!X¹°bòPá¢XÁ”èJ ¾<–c,õK%³¥/ëÙºÆU3‰Årà/p”9‚”Á¤‡ÕSI¨_‡Å“Ù%Ið‹ èsj¾ËqéÚZšžû,7ã;Úôà JwÀLn ÂH2³ÔX.æ1ò$ã;¨¶Š¾PõO®ÇšVŠF6ô^¶Uý˜e¸£dhLpØ AþŒŒ­½APÛÍõŒÖÆá . `N¾áãE{]dž5Š´JsØFkVë_/À¾™sœìê:•¡I.•¡Jt*C=ê(Cî´rú «.˜vLÝSGáôAgL˜ºa7ŽäžÙ|­k4I$oÿ‘+ÅF_ølÏ«Åfå7¾ñŠ jÔÚm£Uhõ ‹·9;ºÔ¥sá èwÜÉqÁ{oÖzÍÿD%k/Ž<øÂª‡"P¦NòOh)fƉ«¢Té:ªŒ¸·C9Áw`hß›‰´¹U•Qlù$ž‰‰°Ì‹¤$ÞùDE S5OÛ'†SÆ4£ÓZ´¡øeñ³Øw´êÑå™$®“¶£ù[œ»ËŸâÙ_Ôѽ¹5¸9ô÷cý±SÞ8g»©Õ—=þ†¢ú;ü€Œè—âY..7”-gLÈ.xÆõB¼Q#0¿ãÎÓ×Y Î颴Ë\d…gjª(âõV–X`Ý.äDÅÝ7ÆÝ^ð)4©þ’ÏFcaÇ¿ãÈeø^Ò°¶)§JŠ‚TªÅœ"öͯ<¢ eär±Î»Ä|R*ìjD¡òºÜ4.Mxåêåƒ(èì¡RqâÛ\ÖµÙ¬FPÉWÏ.µ®8üF©.1*‘PÑCñ¢Kì‚*FÔþjªÄâ9†k_Ô§üwHšæa ä¥Û†ÙXMœHB¢zMþùÛ¦ÇÃ#’щßÑ¢Zí“Æ¨Íiž/“iÁ8éxSÚ~•Ψ@ùL˜óàÚ9TýP¯Ô£û})õW%ž½¬#Ķ—)†È“j¨äÖ!)²v)²TeQ{íìªDÉýˆÐö2X<ÕíͼÅ1ÇZ„¸6Û-]nû†"¶N£¨Œ¹z=šÒ… ‡‰B§ñ>/þ[çˆóK¤WÇ¿ËÈðórÍ ÎD•Ø¿×h<š?ÅÊ!hô¤o\‰D½‹ht:å<“ÊáV{M± irÿJô ÂÞLM¸˜o!UÇþd›ÿÉŽo½ð7²ûEïYjÛ¾?þÿ‡Ø¾L”Æ|ÛÓÁ®°Hÿÿ›³«³é++RàÛÇáÜ•btf¬wT4²Ñ™‹&‚Ñ*²ig®HTÅ©?÷ßÅ¡P¦a—»2û6>ܾï@߉Š×?ê”a¢K ûq`~ é=)HÒCS¥Faщ^A@è?$@Î%EFP¥ÅÀ(c åk_$ög, ÈùÝËK€Cò´Ÿ¸úƒBbg&¢î"ÂN²#ê$gÇ4d'”†9 uJ!ˉNLde—îŽA>Žp“ ÍÁ‚ZA=SòŒªr łƬ/epË“¡Ü løS—Œa¾ƒvÄYXŸLÄj}$F[¦a íiþfß´j ¢¥//$ð߬LtÉo½ DAÑq@ÁMÚè—6GÏ­‚à —8JÇÙÕ6Ҹߖ&SˆÈIÊœÒðåÕ CX ªÃPh9Å»Q‹eF¦)e“0xx–xÐT ?K}ß°|Ë„ü¢'_Þü¸©Ð=¬6†P!b! q ”2#SH±æàÈ¥®?x´Õycð«ã÷D†%’€ˆÑ¡Œ¿ÅR`¼ïpØ^ƒ¨çyOm\·n˜våZçŒg¾Œƒ­)´—µ0Ér¶,ã‚ܼÂ3;¿âD÷%/@4‹÷èH~½²Nõ¤ahXå³a¶®äÊ83ñüºŸ\%Zðò”pö  ò'ÿã#ðØqrHï'Üæ…®¸–Á…B¯‚dÊðE'ëÃGºÓ¸‰-WÖ­\ GMø™WóÔxºŽ.2–!àW­ÉšE† ?ÿ•:dqå¬(ÙWÄ®Z%W I¡²"ø×8lîÉPÿ´!Twî,b†dpÙJgb°9VðSÈ®5\Ü"Û:ñµÓ9dG—dm¸Y h@UF8òä}Ž_ú›ðö6®U´&m…!uÚE$Õ€à%]Åæ›X™L­BËýÎÖnÙ·6¬Ṯ\Îÿ:‘åäüé<:áiæŒ8-ç­@’/Ç—Ü93ePÚæ{®¯¾‚âíVÝ*c ´]ž×5›cGÍçv;Ö ÊgNqî©i¾Pí¤ pzwE_3'ª|®'{Ve[ˆ:µ¼¶f'—í&&ÜÑ /]Ó¯Šh§¼×šg^¹xQ@²%/Œ%ẹðºkWŒs=î›Ëvª¢-÷‚N8@Â…w9N!ª.µîM'à·Î!B ÖIVõ"è,i Š™gg3uì˜BÉ:=çv|ôÂ\k—ý†[ýõ†·…°—eíµßæY(ijzêQ?W²\+\SžžËA‹éÁr½#`1ãÒæ ú冀ïŠúSbŸ®sz¥ÓbûèjÇ”;F. bÁ±!B­¢zÏûã{ÕÃ…:J3£g0e¨QWôøî¨QŠ·ƒo‰d˜fSâÒÃA¶ÜwëV¾Î:†`ï!ȰçðŸÍQ«#E]OHñajeÉàÜh¨rm, O¦‰þˆ"î¶̲O}Ø>ôŽ ®™” ª‚Ú©ãAq¤ËVÁ$‰^¯9…_6=ü«œ%O”ø‰LZ¾zKÝ®an 3mx¼pß?Çßk© FÕe_­`V)J?„¡ãH\†š.$æ×˜ Ëà<{5,ˆ¶6ÁÁlõUí*“MUaÄØ$xóXÌ2†Õ.Õ/ÚÍÍs()À@%ìéXR?ga`7m¥=|PÛ¸û ;Èœî¬oV™Á¼™éN«s°2zý‰ÝÀmJ›7hï¨hÛÍYËlš F>6îñ9Ô_ïI‘òDJþ 逈À“ž¶É¾Å|7ó|»\¡§%ØÛýß…ó¢x:©Wwêž=uû×çÞò3.¯ͧ?ÊG˜`&8yƒÊ cØV’€;ó‡Ÿs?Wuñ¾¡¯—ïmäÜ]áÄaØ]ÝyϸÚiÂ>’ÕeÇÝj|ɼÿ/sdÎËë\CDþOì&þ?—¦ñ¬¦¶=Ž$jÝíEçg×ä˜äHX.G·!Z9¢"@c÷5)vãFÅ $ëÍE–c¼·—Ywµqö†¼&äÃ0ÄÈ0ß}€Û(ðÑÁ}‚Ç.ï~åíë{’lˆÙõFž“ª«ëêÇ\í§òûÌ ¿}Ï[v›„?Ô³g<ßÅÃùrm]„Ôs¹¨µV<-âLÄoÅìRÅIÍ9Æu¤|¨ÕH½–ó54çxsª^y-{•HÍéO Ìe¥ÕÚ>¿Fd‘` 9­U’Fi1¦Õíd\6ˆT½Ê2Kó¯ƒï¿W[ÞÄ'+~yÖ¤CÄÄ&G.P‘q &—¨€¸‡’‰\'(pJçò,!Â<Õ[Èú±\ËCYGs±+Ø„56%%‚"xÁ9íAÿ° r· NÆQÐ\è–BeÖ+Ødµú*¤‘®U¼¶^°®ªˆØ¤Â›jªŽ8üÏ)Ç ClE¢Ì\Þ¬¤ÂnL‰xOñl>˜Tä._ω °\°Œ1Y˜6[2¿Vv^~µé@|²nénþŽ«Þå”]»å[$É<×îífÞó7pCgr’@³œ'Ïóà{£~¤éã"EÖ­àøÔFcÉæbs`F~ÇPáã:ÈM÷<¬°AšÁêjÆ–R+v~-í} ß?sê™ÐZÁYÁö ÁõÄIµ±Ò8|‰… +–HR\™ÊõA H\Gáù͇Õ5E¹“ÏF|·Ìã·]‚4DìúmeÌá±IVµs *ò`'`ãhNû•rP«úD*¹xÍו Wðx_û…CFëÀåÒŸBy8Þ†A*5P1VLbp?hïlŠpcšZ¸+™„Wh7Ï!¹AøÑ¬¾>_¨ØÎ©=ŽI§¥U»@ÔUUÅcY~›1&¯ëöÒ6VŒ­à¤A 衼|† õ¥aÖ”’RÉ!SóÒ‰\è=èÍ q€ýO®Å¼/ü‚Öƒfç–Qó‚Ц0¥˜"Û/(F fp^Ëf픨;Yª+ÇžŒ§qï×z<<[„ZL{¹BÛÐκr½lügÝ/1ÏY×è¦þ’;êrGmúüÔ„Öˆø€_¯ÐÖn~ôH{Š%¡Æ? c{g@N¬ÒÜçsøý ˆL–ó¨VP©oºQs ŒÏ”òã߃„§c5Æ:h£¬xÏàzH À²ØèÙ?TYÒtÅQ§Øá[ _\Y¢,ìqXÖçÐrzÃ~LU£Uª´8¦ºq¦t™’>¶§Gå¿+"¿DÑ–CB‰øIéJi=)Í'¥I¤ô«4ž•¦“Ó*i=2å“Ñìùš‡³NáKœ·3Äõz90"ƒ;ÊY,¹”1%Ì5¡Ü~ÞÚ^{Hvÿ¸krÙn1¿È°ÐÎCŽ•øNȉàëè/O@uFüéí“Ͼ¥Ýtã©MÆÏU—&Ñ{ªÀ¾h¤?yŒS5Ù)ðBNâ¹ì´×òw¸*ìá’º& ®‚ç¹"a{Í|2qÛ£^)äk¾û‘“)„éÍžÒ»m)y/V—9JãFûb°;Q(o¦$ ¹®a<ê˜#>ëŽ8†ëæ,Ži1Arúv}RIôš„ÊaSù!<¹ÿ½-ð$°ÄVâ²CuF¨^pk¨Ê"¨^`é_LC~óÅ6FP$ýbõ3åboÜ©ª5€‹å9ÂF‰õz>Iè’Ë£öòÌ›2²$sXýš1ïS—Þ•þÅs~4jMYÕ^DÊQÁXAÁí6LÁ™ŸWѪ›¬gz6(¡×Ýÿ²ÅíîÓ$ÏàmSE, *±¹[½óFî÷<.$¦µׯ}ç§¡rJ¥ŽæÞ§–qP °ûß<¤Š¹I‚ù§~k²i>µQe‘º5Ú¬±`þÕ§Ô,Ô©]ú5Êbæÿ¸é_¥O4÷PÁMõÍÚ‡¢‹ü£|ëü–z3î¦è#!D?músdy‡Mq´ß¨<~UÖ«†ª}k=)­ZØýhÅ7«Â°*ÓÒc,›E ‡_‚L ]}Vq~òË7ÕcÖ+hqÆ®Fgú¨åLvxs ôÊ6Ý5^k{ù»1˜¼çŒž&Òrÿiwþö0W2<”øz ?õ÷µ)¸‰|NM‡¤FŠâωzù£¨Œ9ÈFö\=§˜`®.qÈzgLÓJû€×WxHs׫8@ùóªj¬8Î:Ô4YÎÙù}ùž½—i ÿ³1ÒøÐZÖéa‰@4ûµuŠ@ÜFö¤G5I1ÿåƒê$Íc_è¡l¢Tp~-n˜RõËvÊ« 4ÿ“01%ÜÙ=ÓAÒ=æ{V%-¶R­vfvVkÕðÍýìâ ùKó8Œ¿é«q×—Sk˜ßø½Ìž÷^™þ@/@à“¿ð¸?}Ò›Q–ÿb[[ÏïÕ3ì Í.J¦Èj=S¨ãì¼¾¸û±íãé@sr6::Âx²Õ5ðI‡f]xµ| rŒô;{ÐÖð¸¢þïEó&/(a¨ÿ‰,äìéìbjû__C•ªùÛ_²¡üäË;¦ÊA@´X·WÚÛµ,©ûÕPÞT•³› !óº™q—^_ø¾érCÊn÷\ÊF‹È\_9Ïóº9­pEN¼&jÃ’ãL Q¦lXà$d&ˆ0j‹Côþ‹KxŠZІ:Ž ÙH!æí(U"Ä‘ë!ì;H–;ÃÛˆÆQj§c9‰2‚(3F‚zf,É#D±Lc†â.kwËdŠ'¼F0ª…,‚Rð'³Înz[(W°OÌ«n”›“Í~×âŸUï\÷dÏ-_qkÄzë© Ó5ø/•_ûPןklnÔȇww¶ìxÒÛæ6XÛß,`:û¨.šãe¾ì‰á8µ‘ÔìMøK u7ø5ÐöËX78‡¹—Éü8ʱT2©&&ŒW°w˜gnþÙ†jØyÚû´2çytÔÊçz*GwYÂDܪûV˜…‘rÁÔæ~þª¬±§G#Šçií>ÛÚþOÜÚlRðÿˆ¡ÿ'¯þŸ@dܪ¦¿-‚‚·-É&×d³µ$¤¨© Z×ÕX"…Ø+©°QAýÐPÁ*ÇãHöŽä9¶­Ûõ%äñ¯ü>þý¿|Iß—foÕ$$$ÙÌÇãæÿq§cßÝìéçóýxNUÅÃõ—ß'û@Ê3ƒÏjƒcÕº¢]Ñ0n²ø°iìIfã¶vŽú…§s½ƒÝ1ôO±ÝdÐ\/Ñj=kn”Ž„’ѶinzÓ\/¿ô4€$¾ƒók—^Fv<2kR ¦{“uñ9Æh[ úçlS o ½—N?ê–Á´Éê[Îú n¹Aÿ[¬ÓÇzT¬µ3D@×!ôÏ)ݦ~åJœB ¿%AQâl”ˆ.^\ þúý¡×Áµ»·NtÞ‚%Ê£rÏ"•Lë2å©ãI‹"/læy]øË®h ŽjÅ}׋±Ýi[@l@ Ôù OÈÍ·ï<ò$R²‹ÌT½hiI"€%J[Í#õô£(Óñ³­äœ ›Î ?0(IÓj¦‰k^OÒ`’¨àQàájê„ßÙ9ìk’g3=—o’yXè3Ž\‰-a걬ОØIENìJ}Ì}4f§_ ¹ì·ÆÆ K•òǬèRyG aO¢b#üÁbQ‰³kpK†(–im-3ÌñÊ„FÁHG•Ò*£J{gÁÂ}ÔjI¡¿ì€9z[Ì'€eGWï·*Ú %kCÎPoÓk+ÚHÓ%z»|<’;Ñ 3y×ÙŠÐ ¹Qe£¦ì—}4':í“(…:Úo±£;§úJßî]XèÚ0QŸÀü-¸l’_3ðÀíyѬ¾¡›Xå6À\+HÁá]ÛÂY§²&Q Ã×AòV^gŸ‡Ë°²iK¨{îBéû®WrØŒŽU^%ú‘Ùí¹4\õä¶ ~zabi ÉÓ «3¾#ÂR€‰F >d\”Ò¨`éûÔoà0Ôdˆ˜Ìž—Ï]¸­<1œDàa4|$ ÜbPb_K€z²ì¶}ÚÅlx†ªƒ]£nãC’?r(j¬ˆ•ȽäXA ŽŽŒÈZ¯€¿8‹ Ü!ÀÓÓÁ¼fçª4X„•]§e`|ér9Á™£ÀÙ*^Dê9éä*ü^®§¿ãkÊÿ¾O»ŸÛåvBß…YšOaE¶´X}D °3e¾šíU†ÏYðxU`Äæ,ßàÏõvÑX‹¯øÔÌ3çt,æ¡Nû§ ôcÝ%kxkeýŽ( ÕJ“ân¬ÌÉãßõ{õ¬<Û‚{ÙM[ðM—|¶Sw\Ÿ?ÌIb9ËQŠØ9Z‰]¯6L¨I$,_HHÁ‹…×OW^eïóòpŽg;›Ö²Å—aR¨è ¾¾è*>¦°o,l³)–Ù:Ëlíãÿîcö_ÍšèBº6© ˜ÿ£fMW#W;×ÿ™ÿ{³æˆžî¦Ü6Í,šfRÑXWGD2ª(#D‹È¦‰ie…^ã_1åvÒ~—.»Á/BøÑƒ^7Ÿû£ž1üœ[ª Ϣàî`=MŠP¤ÆqØLIäÞ†ð ;M‰Î"„ž"¤—Ð! 1¡_*·í{¬Tè‘Ã{¡%HÓ´õOVQxŠL 4lgQEe„¦P ”eY #„hiÌ}‚ÐàóHeê{Å‘o¥P ‹ eÐ#fÝQ‚-.[~äÚ¦c¹2J¨|Ms_±Üˆê™ìßõ‘“Ç n›¸Ç1ÿ©ŒQE1eÌä$D¯Œ ¦t”·†?ô$Ëñ1Æ ¥UdÆRR¬“.°õ)@ø ÄO!8>Ü×0Û¥Çz¿oGg˜{¹ºzyúúO4!L݃ç^ÿ-<ã ”ýþ0ýÏô›BÔì‚|&ËoqòÌ€qðfÂRp£\eLáØè8'¦2F47g0f*¥2Y&Ôn´‚ôR™-kÅjOô^j°ìàveÞ‡‘Ü00aªÀüŸà$y ج—~¿³$òØ“ü9ä:šUG~Ć-"èm¿÷ˆó,Ò¿jHY¤§zNv}¬F”‹²d!LLþ"ÆIdØÒLçvûš ± „‘7³çÜ·¼îñÙ,…«ŽE†Ôð( s.ˆ¬(2F;EΣíëA(&¡ßokÂ8ì_ü÷'åpÄœÖK_}EÏðIIJ¨ sãªàKçKøŸ²á£¬¢Îöê%èxÒÕÎ&Ú N{DŠËd®t¡=:qí¼Ü³ÁtN{¯RR øÌ¢f­¶äí)ÏÇ(’¨¶*54efþtYKN]Õâ@IÖ5ã§á™®u=ÐzØ<Æ“Ÿ> IyhSs¹ÅД¡5³ ±±ã®¦[§y÷iÌÌ[¦„}21à×~¤’J$Yg’íûþ&ƶÂÇ“‘'*Fs«ª3FÁGD(™Rñdh$)”QkTÓ ÇÞJsuK2t óÙK¶Lñœ’L—é}Ž5½«³n³Ÿâ7ú ·ÜYr^^¹=4\«ú ]†t¤žt6ëyöì¿aÿ;?ZÔ:BÈýOÙÿÙgµvíqÔQû›ó÷ZqÉáµ—(‹Æ(oêrÁÖá$Ae+àÁb#·´T†=În½®w3ofv7ÈØ¤BŽBÕó âï#F¨ãS§€}Oþàž¸ÝÍÜ^‡Ñ`3“Zß¼n77½çß>r¹é~­û~?Ìë¿kHÔ÷öKݟݱt>YEÔ#vŸ·ÀAPºGC”Œ¤4qTFìB#Ak£öb6rôQœÄZ`ÓFÆ…x‚w³¦…Ö2´$+]Õ/e¤rQíd'4ËÖ”/sÌ‘/u¤rÑo}š†JS¼ unkèxöú/Z{­J³Êm¸ƒë>ç„ÿ¹®’ñì`¿[5øi'+Õ¢œÿÀµ<*c‘\ ´ Û N± Ø…mÐ z-L‘Œ‹’¦ù´CUÖŽtM­·jè(´6kÃj,DÄ:âo³ 2þ`¥¼R “dFel“ƘN‰mR¶«W¿›aZÃ|2µhY»“Û¸õö–uï_Ÿ{µø·Š©s!^dA#±[E<ïú$n.4¿àºí+~}œV£Ý í±þ;ÙS¸U•#è¢(*ST¬y*Aï(¡Ø>Å*‘ÎwJY/âêÀïÈy£ »Â³1£5~§ »"O®5L‡íRªHE—j5ªÕªžC=‹Ò$p¶†™8ˆ†18ÇHX¤ È-½á&8BÁHr4Û±jdî\Ö‘áîa WΫ7óŰl.àÍŒ¦?î;³¥«‰Dïç ‰p»äwÑîÛãJQÛºÙ‰&2×Ðv ^¬~±ÊÃ{ò¥ŸTC-V›òZóÔse¾RÕLRt¤5åФüÚ-I×ÛÖÙ·¨ÙõÃõ_[×c¯tʾ²G,|“Ér7í’ÎsŒt¤Ì¶›® ¯+É:‘¨ó£ìëÊ=GQpC¢ÚCÀ¬hq²ØÐ˜K99'QjZWÒu>xË9_ðéq3#t7…âî‘pCxÅ)µþöÙkª" tZ.d5áÐÚ©¥\üDy¤›ðO|^cG~òƒtž…ÿ¾HâŸq1%)ÀôßjÃn9²2¤½2ËC„{û#T ¡¢ñ°-q³‰õ̲úª(÷ë×>Ш'§u(µ¥ž¬¦ÑÞÃ|~ë¨Ýí4ó8Jò¾£}Œi¤l!;‡p¥íÕ6Ú¬mÍâýZy¤ý¯ò`ȦêQàqPŠÄQAô¦Ì6 mNêø Y"R®”Ï÷ËAl QÇ n[\âÿñœø ï_ô[HÕÌã¡€XŽê¨0ÚÛñÓã OÙáÀ“èè¨ÌØLu!È›‡ÿ£9"­úâ£Ù#÷6ñûYCìEÛH§—vb7°xӇ؞iç<Ú™ë—«àŸø°ðž^Ìžøƒ#Ô]W ÄÌcÏ]v #ÛvzüÂÆ--¿ïÍ÷9©vÐè~óØAZ*]M&}d&/¶¥8¤QxìnR6¬%ÓäÈ~îb2qsˆüÉ%þ (‘·÷.ä ÂÖiy%µy¶Œ³»e[?kδš!»Å©7¯Üõxç¶~åùI0¨Hâ ÿomv5<¬¡·-¸µ1•b5–Ôê$ :zLz GWŸÎG0ÇìácŠåø’á[ì²5 šj}4œÇlÍxö®ÂÝ<Óðfª×ÈœÙÃkÒ.Fí9yð(Œöœ<\u\9D&ûÑëÒO> 4`³;…»øÓBqx_ :>?n—»³Ù›tÑ:m[_çD𷪛‹T.Xc|ö‡>?6"ÚˆÈ Ò1ìáAA(L/3ÙT‹¸€ñ-ïóø×Ì|X71'Hûð“Šæ50Â:z¬ZÝjô¼o,êŒPCY†ûgab£5£¥üos¢WÄò#xtF–”J ùöR¬à¨ÜXá¡õ.NxTX¡AyíÝî±Yêq#ã{"£†IމGc8Ù_úþdFÔãñ D~MìÁ$w%~ Ò´ì@rœü[ìGcÛRÒùnd̺BîF:™Åg#ƒBˬ!|…¬Ø…±õN´ÌG$má iE¡ÝÄ„ äËÇÓÌöiþö¯ƒ1LÕ¹~~û8[‚Ьc0qžæs'W&He f+]+–e²$aeXÌšv–Sbx ŒE àÁ[«Ñ{ ÇM;›¨$ÇÉ/Õ±Š` K®Ï&„B?ðß rS-ÏBÐ׺‘‡Ú WÝÓ\ÜÝóùYn’næ _š^òÅùîýÝ¥›jþÔ°“컕Y¾/›v0ÃÈ>qøìcrÛÚYÅWd[ÇËxø·Áß²çÞs|$Ðâñå §ÕðÎ xç_ÉlÔàþÀÞ<혚å¹*–㺚¡làP`±Úܘ[,Ö0y›Ä´Ìr<­>ï_vÖqXõŸ8`¹YÐüÏË$¾ÞcãHïH”ge~ìÎ*¼ðžY˜x~ñA·Z‡ŽŽ–f-- kºÞ÷ÿöŒ·\Q«ÞÁ?À€úÿ7f98Ù»ØÛÛÐëë[ÚYºèëÿµL“í¶Dî>¢}-ÕÝœ(¢^K¸è¨XR¦-ˆikK@M’H+©>¿èLÑ%t!ŒÂ‡Ù‰éYÝœvÜÛ³³åß'S™÷XÁæ°Ê°}[{´Žò¨—ò¬`ºÈ•ª©4é’QýŸˆÆm›»)4tÄ¢ßôG ™€å¬‚؃É âüzUL€5Š™È·›—P Of×ud=ú42ŒŠÔŠÚ›¬õ® †`púé@¾<{ñÆ™mŒtºoç…úûæÛº{v|>&Šo'½9>y’+Æ!§ äKã„6«Ç[ãLÐdNpqÁ×n ›9‘Ò³||p†²›~%O×*¥-i–4;Ö[f¦R=xLÃïFå/üòOÃmÔ‹6ŒµOÜ7 ©6}…³øÅÓök”Ru„é×DýO4ˆW›“ÎaŒçö”FaÕ“¤Ûƒ­ k?„å¤q4œÈ_̽«GKÏÃ?Xßÿ„I½¿œmñà•ÏOÍï¸òÄ EO¼wH²­Aÿ;r7ž¶¤™L–a%$’æR‰Txþ¬ÿþnˆ#Q)§¥ñÞ¬áÀ£Æ t4ès@ÎÈ1ͬòå œ_=³”žÈ¤Ç0œ9 %ž ¸¥Q[´Y:è¦íýêÕø%›D3CÄRÈêw2¬e’rd%2ä¬Ä£¤¤aQ޾’Õl:Oùfœªä]½üwAäúž9x€C6ÚIžRI­¦r¢©I–(ÊBvã. ‰”G.ýÅŒÊθ(éµX€%`„?4‚%ED¡„Kúƒm•Zq!>¦o´Q#ï¯ÏÏ.t&oK¶í8ÏQŸ¯Ù‡Åƒg£Pˆf²»¤ŒSÎÉs{Ó”Añv¾c±CVIf¤9&¨»(”‡Þ¹"")3Ý –n8f(2E=“Å #ÕÁŒU(:½CPξû‰èáèãhºwžï‡›nÂ??.<„ö³“ó›‹éãÇmå÷×(œ¼ßìï«÷gÓtTa{4Ì#x 1 1.Á,]…*BYN¡ ¸bK–0GÒf¦E&éz‹´‡‰ ³'ŠñÉÀ^HÈbO»‰ÁŒ—9ÎM''7m&6oÚ62{tÊøI¹ú¿›1°‚43ÐRð1¤yK=^@ÀQeÄÙ‹1é3†qG™ñë'¡rCLôd ‰ÂG÷˜k\"÷·€ÔSK ?Re¤S?}%V9sf •Sùž¯°)™½ñ ŠR-+MëcI…ÄÐã"«œe" Ê„¾ og/Pý*·C¤† :‡éìa@> ÑÐó¶ C£p$j\r™_ AýrCÀ'YpÍ´6ä¡ §´RQ¨ä±,·Éƒ²€‹ð7qàEÎKÍ‚2¤©„ÔµKèE]xztÅÕ{ˆMSÿý€J†qü@Fã¬Êùž-yì ŽK¥Â•=SEtŸ<´g0Jïtcëf’3½|5Èd )\P+õ1êµEÇÇ_¹AHÚ¡ô†ZŠèŠSa•*†Qûœ]:(Ÿ“á ÷jVºóØ57D3NX²‹h ’d†¸à¿èÖn°bJ…ŸFqë›s©©!=x¹ðÁ‹ÃIãã—íaöÁÑ{ˆ½\üdà#H׿Nÿv^p[|R—¯þ„bx3iË [عpôÁêcv`0°lÂø²7Ï;3ÖCQТ?à,cµØ^pµJ°[‰¸1:rHdê©_$†± j†¢’‰Á‚tš“÷e)Wj¤Y®!ÿB“傲)pØ,Êaμ$Ùd·ìsH,—UF"…ÊØÄ/vÈ^2(€[I™rac€ciVÏú¡ŽHéEþޏ…ШËúmŒÂ3†WHP`\¹!EñQ[u%Àí âApxH˜?¦Ü“:9DÓ[AzX|áŒlß²,vSsäŒVGÄ}ªŸjU3D†vºW^óSÃCêëmà^¤“&‰â’{î\,Œ.>À.[9æË·îJA(ñãpE¡€)(…î#áSÃôŠ.Y:Ãka• ¸gd®åõÏ‹ Š gÖ*z×HwõûÅcriã>þ޵žÖ¥G=üèö¼=V†ü5rX?±ä~«Òóî—ÇŒ¥Ðý;_õáâÑçWÍ™Í÷w¼u"äTQGÝ„´ƒ7TëGEÒyów§·Éê à>Ü •¢ ;\e¸[â\‡I·d‚K†ÞñËè¢H(1ïñÓx~ŒÒ6Œ³¼°ë«.$@£¨¸ºnõÿžåvÏ•Ëf·w¥Â¸m2ùŸØ0KÔÑÈž¨êz´…~G­6²ùÙºªcƒ_ðÃÚë­Ì­éè–n¤áe&M°@ø't¡Ž¢+°b¤“iÒø¬8ÖÊ¢`bMä*w!¢ìšKu>+W'JØvÔŠQ¯ªT3Æ+½®)‘þ 7&G¦s©í¿Î®7` ]éo¡ØÕ È S”%ûªt襇ö}k•ºÿP+ G†±>( iu^'Ërýͦú¼ÃøØY•‹ëVÖ¢rCყßs40t֮ݸn¤¼¯víj5¿åø:Š D&WœÓâ§.Örr½ÇÍÄÝÃ4'ÕñïdUéY#Ž?ãíÞžG•hr‚ ±©ª«Åú8x9Ôž”iðÒÁ0w\˜…¬f¶¨ý¾°ÛRþͲ©øèàª1òŒÊk¢$cU‘5`öVà™‚ªS ­¹¸Ý2Þ¼ø°Ö«e3Ý_YÏ£o:‚ëÄw£y´9µr (Y$œKo@êŽ\Ðrå°]§?æn ïNÝ–û©¿©Íf–úÎzVs-ðä}¹Œ1D÷ СØÉ¼GQß脨óL°«}&ß»ÀÈ̲9ÂKd½¡:n~‘Æ"=c”Í<-B<qëì©> ³ÓxuäKaòôƒÕÔŠ1®iml67ínJº ÊJ)BÜÕN>tˆŸ¹¡;2‹"#ت­_©Ô†7ô“'Ê ÔFÔ¿¤½ã ZŽbàÜšˆÚš†›Ú±÷ý^IWÐL9Fó<ņ’{´èßÓlÍ"xOÍ8Õáðå£.eʉOÛDAƒ^@x‹òb:¹*W %ê™"5’¾"¤Ów>ßžÀ5¨á±9üÃô÷‡A“Pn$5ˆ:Èõš…®o¸9ÏÆoçûù:zðò`ywØö7fpÐÆf‘ìu¾¯ªA±¹ ØôôP}\<|h°÷>S·óäüuLq-Ò"ûPI[…Å—½D·",SP/«ü € Ü å W“Á5`R’©?h`óŠ˜åÄ¡*Ën+èÀŸÜÏKÀ-MžÐøw¢ký{+hqMOÈÀ¢/t eE+ðRý¸šLCËûñ5±J„ªHD²„1Ù„ „Þ²Æë‘âÈŠÁ^UU•2ln.û}¯:ÎçÌÓ=Ûz)Wx›”¸ßùtœÊ%¡äžŽÍ‚ÑA¼–O=šPá&-ƒ »U‘Z‡Ê~¦PÖ%Øãª¹ø¾x@nQòñ®Šª}ŠâYý įóˆ =x,EãRo tg©4ìßàF #ÞøL,¦Yæä£¬vœG£é;ìw“[©«3dÃ+³îªlÒü`†K‰m†¯Q_ÅèÚ£ k·ìã¬-+ÛöÂÛ{âªmº˜:E ÚÌ5žšw«T1 ÆJ_¾|¦½â… Q ÅM›jTFn X^ÒKQÀ}³ˆ¶‰<ðà…º”òb‚3µ¥øI”â„}•dþïÔ‡Zzrç¥v€xšö”¨M›£ÉaÖÌ@£îtÞàTÈÉì8 Ö±oš‡çDNü´MM¸µ à_W³]Z´NOiÌ=•k±d»ŒUèI3زÞ(éjÕDnŸ)E"ii¦âX£=Pµö¾W˜ºj¦˜Òf5­žh¤Û¤ÀvâŠY"\ë/·ÇEE=ŒÛYkìjõfÎi7õ«Õ÷fpk’ v%äðPa"hJÀÝê–çIÄm DÓ˜yxlzFÏ•Ž%ÝjÖV¢0¹V)ifÁALG„tƒ'€n@ؘ.ü¯i"ÝÆ[¶ ªÂCä,ed œÚnu؃óã>…ßÜŠ4¿ê_Œ::ƒu–¤Ù²ÖP L¹Ù‹âÔÆ€^#WE*Æ©ÊéJ½3áÈ-Ñ$ýò•<¨F¼ÁÐàlj«6#3ùøR=/ßKD™½-5çsº'…æ™ Rˬ”7£núB|—¯¥bÒˆwX}[¾wVŠ™$pq%u(½¢­s¨~H#“*†=­œmªYï–èÅ$œ,qJý¦'F^2ITO÷2c ¡Z|C”ÆÈaÆ'm€aŸÄ8=||Xp:HŸGڰ¶j²›¼e}.xŠçÜ£h'äi"±Ó˜—ѳXƒ¾óó/™‚DÊC@'ù¼ØkÜ@ïžbÖâž%æ8ús;:"ÖóÀöÍ‹77Þ6–1÷ȵ†T_ï7[æ’hŠîMœ¸¤¿}¢ì#ÑÕ]oJÚÆ n0H@Ç ’–|þ5ßï-b®\_yük_yò]þíM\~\ß_Þm¬{Þõµ]7Uqˆg£ô´¡*3÷îû <Yœ׃„ÚE÷( 4¹×(hë8“â=‡Á…ßÿï´Xbê¤!\!õÔ(ÃŒj…øÇßI„Cbê=,Œ€`O}Y8ºý¾}üŸÝ“Ï{ùŒó#ø¼ó6Eßÿv›Èwf\š<ÏzËus:Joþ·m%ĽHòà ¶—è1ð|MõZmãB ´êBÛâC[ˆi=2O1!­ˆœ?·,|OßÛ «W¦Þê69>-öQ”¼ei!wÌÅ.ikÞú>äF£šåP +ëG¯=×.,¾ìи¶üÅÜ횇Wêmìp-ï'ÅŽ÷#¯×g®ûÙù³ô³ÄN® J°Ö2Ú—–Ø >‡m÷óE1¹üë§öñrWëÛîír-¢ÌÆÚíž\ÿ—ÇÙEË{F[ÕH]ìmØ·R'Ãmj£{”<É%5µjQkÅOq[Þ‰ [;×`5Ĺ¡ÈÜÎÉû¥_ÉåO•#¦ñ¦u®È@<&ߥå^a}ÀmÔܪ 7 §:ÃÇR„¢zdïë¦kZP$ü«†Ê:¯TÝGRž-Áüö}ˆA‰N;ViËcïRGJT-‹þ­c|êã_ÇÄŸºgoço|öw¼S2köx‹tÎPÙ00$ò¥ñ½¤ Aƒð™XÕ`°bœ fR½4³Ô†'GÖ™‡Cþ?ë%$÷~šŽ’g'YIW,†_^×\yD£Î.“ÜsœPãýF›‹™‹ŽG CEN%šU„)%œ] æÙ.$ˆíŸ*ͼF˜^Iâ¸GÚL@ ÇLÃ>Îïn³q@h$ôè!‹Œã0îƒ"Ï0ÍÔ¨ Œu€ðŠ>Œj•Me5QIó€ý£±Jl–™ ÙŽB< ñx÷ýï pÝ96¦<ˆàb”£4&o >úxªºOÁ1 ’–™† ëÜqe>V©Áeyg”‰ìKï/®¸ºItp×_–^ã;2Xamœ© œ÷’À‹#7bˆSÆrQÁ¡l‰Ÿ6Nê2LVåµècTܘ‹¡qA=&ãï¦u&*r¹ZJ*î6Gœ ✾kÈIä¿æ±<ÿ¦P[&Mžâš©I-4vóšìÆ‹r×Å ÿáƒï¡ü«P?cÀp:nà…÷g¶4T§C¡cŸ›»ÈS'ué8´*Kãè ŽÞ”í°©‚×Í„}ó¨jŸJ òFnig™M‚*ÜãûÔè½Ýrà¿xFx"nKÑi'Àäë]*ÇÜ/U¿„æR5¹œJEQè°¦óÀbYN2Uhf‡õuaËê<âÜtœû”ÄDGô²&¢ÝLOé™FS(Í‘ÉØmÈãëöEù29wòcÀ¢ñØäþ™úXkè'Gœè™€¨êó¿^W"™)×k§Í¢oòƒñ½³¬¦‹ÓwÐß]>Û´øƒ5ûsßàùNšB·t´t $¶ é¢ë®›LxCf¬…q‚`»oI{KÓxÍò`ƒ(õæ(„voˆkû 6fý© ª5hÀè§¨ŠšOÉ–1…¯F¬Á,[¢ÅL¢¹lå’±®A¯„z*¶£‘MêŸH=žëÊ™òÆ3R‹ï8²õú:ý8 «æÚk®Ú*,>Ãd„›çAa×þyëðCj3N£òvIo5æ§éÐÕ6xí¨ˆ*;„$RAÈC¥RZJ¯ F\DûV3Šj Ñ,÷uë×)’õWþW³–±ž¹3ÎÎcìØ¾T '[ÇöÒFS“²ªú¨Îm2U¾‰íÒG½åí?t·jÔy¤cTÿ&©Ø:ý¬F-AæèB·l1{˜rcšÛ­ˆŸê™8G¯ ÄÆÀv€ÓØY–±3§Ÿüe\tŒX]è5±âíF÷Ø”Ï~M´! fcî~ú£Œ—Li¡w}Í·õ5E)BKÈÍmS•W£~_ýïäŠç¼6c k]–•Z-Úì‚Lÿ(”oI1ObÓ¨  õ¨?ñÒßUÕmVÐr[}îcKj3ôüË÷2ØÖ`‡ÌÀÁ&ŒVˆNëí=îk–†ØëêyŠS7ZE]ë_—×ïôb2–ôw"ÿT©Öº-»z,tƒêõºèÒvÍJ1•Ç\ý—ªÌÿ¼W-ì’pÖ×´lÔÓÔ韛WëûÑßÕ ÝvØ–®'¶Ôr¼üóyë[ÁBâÇX÷×VÝÌãMÁêÙàéΟÌH>'"·,ÕÝß/Œ£È'Ï'4Yán½æõ'ó«sF“—¦fY„þ­ERœ'Óé{Yëx&³¼úïPn½îóÐÝ'Ç$V4µ«þ»Eݶl,Gœc!•_Ë>p# gíñ%Ë¿‹BŒYD~y Ï„¶Ïo7ÈëRÒúšÃ¸ì¬½õKw[ Ïã¬Õm1 /ð)Ûä͘¡1`[±F\‹C<þ³&Àô@Óµ_Á-ú4P¢¾qUà ïQ®gCÒsôÞIJvÃzÿ"Ävc+?¢“I¿^lžÂØR=ésHè,âØRa¬&mRï9/ד*uåZB¬8 û¦ijº¤â×BÅÙÎccÿþ-ï.à€îÿ­Šh|ãëmƒ#é¥ï:ïð¾¦P"YTbÛB]'çåE£¾Å]'qsi':)kÕÉnÝeϱ+ ÇJïú¦pB—ÉíMιˆô'†„áþAˆtømÍíð»!™3ŽŸ>íD‡0‰tö嫺ºÌì#áݾ³­#‡¤­»ªª¯êû²úVUù·¶ýþ÷}Äþ(ô#¹0¯~NžÁêp‘O(%dRì.lûcUS¹L”}ஜ’T´CÖd;¤°¯dÔ¬kãaN·;É©K´=Ê ©x‰ö/ñ&Ñ5©h…zŠ\ä5D¦=RI+åK¤M>E>êZ23©ÔOa0Óîì™2‘—t*˜ˆ›Â™[ÄR"E>úÚ2ó¥”¹ˆ‡¦i¸ÈGÍd>ê1b".â‘3‘—|êLôµ‡Baºè;j¢h*mB«ÈGÑ„>ÊIÌ„¹ÈGÒ„>òÑ4±—†Ð-r©ºè;k¢ly·ÈE4E\ÔM¥Ì[äÂU±ªI]ôCšè‹T±¬i]ÄSÆd\ÄSÇd\ÔSÈ„ÜäBO1 åebnK¡V‡eb/ÖoŒƒå;{È6}{¨´p?©Ö½í!Ô¬bA%&Ò>Ú2a_^.ê­e®7Ù”¯rÇ0×›MŠ·x1×›NŠ·zÇ™ëm(Åía_‘s-ÅÛtÆFéb´÷(1t1XoØî³C¯PÔ®_J&ïs†öÕ!wé¥VÊÄ27½‹•_ëé‡á"´˜ì‰ûñYW\»(ëŸTíL/{{À 1UtôjTˆC<Õ‡U§Ú£Ð]®U] Ê©ýà[u_Ȫtü¸kÍi5霼âÜÛ¹*s½%ʺ©gXF‰8/ÄÏ"^n—¸.4÷Ê´÷(Áɯ(Â]¦»º£´Ê‘ˆrÒt¾Õ”×þ“â]¤>Ì×yðMju!{ú¼·à= @0¸  ¹¬þžÿ„‡OF5 4Þ?Øëù㦟¯§Å#âëŸpÄ*ôÓ“sơƓÕa#ÉSƒš·ï\­wMÊ\ÕéÀ³ŽUŸ9U*»:#—CÝÁBt{í"ëÜ,Háþޱ”¡ôM ö·¹¡£¦æŽ·‡B …'bkfäd|y;ÙgØ£Qì¦2»®—nvà¾VBQ¡Fx7N–þf7j2õ89%3¦üêV"ˆïSåGñ?Gh©ãáF,·ö²ßO¶<¼<€v™-mý²0Ì”=l5(4ØG¥”rY\›s“!rnÞ–-±ÿ¥Þ~Ù+ "d…¯±Œ–G°J°¼%ÔE3 C~åeû»_·:x“¿“Ž–Ê6P‡,Çi–¡ŒWä uº)]$Qχ°Ää€8½oŒÚuß÷‘â©{âØ½Ùª,y¼UkZ‘ëËøíÁfè¬& âûM"rùW¶YOXÎZ‚E«søìªqe°l]á4TJneØÑÏO€r «úé¦A»: 3‘» ^Ú9'v¯Îâü’w¯ÐRÅØɨ½õF•±l žF×)_&9kõçÎÓ œÔúcL-K™¨’9Z¤ßûí1¼I‡ž Fz#]ï”æC%ß+1Ž‚ã+›MCÐGN© Λà»YV@T“&° Iáóæ{õµŒÚч“zUZåú+ÖF5j™»Ö0u©#yn~Ùg-ZR©g˜-ÒÂX[}~`èuSyºáÍzÂÓæ/-ù»áOGEB~Š½Þ­`¶"4*{Pgõe&8‹$=`™{†œM N™Û…j«<¹ÇE^OýžyÔ.‘xtÓ>÷¡Ÿ°õ B±ó@oM¿š.›è˜£IÖkÎD÷›K‰¯ê*·¹ÓJÕ[  TZ•¶,CÕa€ëU<–¡ª›0üÉ °©Ù°ìN ÔHõ«ºA"=Ø ÄnT%¤<ê@Õ´kn²PW 5ä^÷—™â¥lúä†ïß§S…zUœ†au%N¦H’6ðPj¨­9“°Wsô 9ýF©ÀÀ׌Al°Œ›©÷œ©Á^v >V ©ªEnXºS©W{­ÕR¾ÏF¼9]ƺüú/µ­´øbw_hö§;;[*1nu¢ºF{@®uZ-Þ±UÙ ¼J©ºd:f;ÕZê9³°©dE"S*ÓÍhñÓÜŒnÐC×jñXÂæ¬Rui=á1 ö´Ó‰.Àõán—ü“žwÞ;òÐ+¥_¦4¬ÖÂ}üd"{Wœb÷4µ6êÃyÒ»'´Õ}œ+ÅVK­Æ@Mæˆ, ”Ɇq&óÇJ+¸Æî÷¶KŠj=ã%/ÁjDî±ûPªø^êõœ ; ÿZf~‰ÞšqjÀ‡ÏÉŸ9"ý ÚïbÅ…iÁ±˜)-ŸYŽ æ±~<¬î^TÕ˜÷kÁ2êB§ÔëÕ¦Dlœ"xJcsî:c:†­ôJèÁªTªoß ”*ÀÒr­ '…âP˜ÝÑ~ æ.€´`ÝÄÅ[Ý~æŠ;ßrJÿuþ=‹ü!‘ŒI“D½J´Ê8VÝÑj•°bc©‚ÿ'(î‚–Ïã²§étKO³ï½Þˆžv-­#©ÔnÀ³Ð$˜"K¶%î•üoŽ/0oÕ{¸†·FõªA#M9…§Þ¬ýÂÎ;j„ÙPåC¤^NQ1WÙ;³‰Z´$ î”Õ±ŬNe¶N·âbxC㠇ƒ­:_¡€Ô—S–u±_ô(hç –ó#¤–Vœg‡å-ùŸ›Ãºpf¬éáÓ7gž)ÛI/q êŠ»½on9¦d(Õq‹ ÍkÆ­ ð<Üla0Ùã ­¦`LÆž³Pg¬^lÞ¸þ1¡À‡ÒQƒáâÐ fíLºvî „Áœ¡*Þ2,sA»k\GâÈÃ=1ŒóÇ’–1Ðhãj@” k™1€P}‘ñ‹Ñd®†óä&=*õËMãÍ6{n63òÞç§Èmq×£Üé±-\6d"CXÅ-¦Ô…yü¢ñe»s»ÀBXÌ“^Äh|媟påOwÍ }ã(ŠFÀ`‡™j•ÖBpú ·V{­µŠô›o’/Oмìz€ŸŸ·OMó"ºc†Ø¯¾·@„Eô}òÊûEô=0tD Û°†Ñ4?NX)Z,?VÔu>0—L,¹žù´ßîa) ?}ëwz„ñÍ Çš¦µ· ]æTOÂoµ]¢fKTÕàžtÑSé*Ë•”l µyúÚ”AàÏXƒM0±³9Æì+¶ÇÀsšg¼k^Š"QüÛ:üp¯2ÍÈf2ìÎàÀXWÒIæ;šøà5 zŒ3ÎmÿqJµ]o ÈÌå½GÃë¼µýªl¥éVõûÉA¢uQû1G1~0üÂMJâw×î/F‚¿Àç5Z`¹h—èâ=ÁvR<£fŠ%“&oзA $Y™Œ` &“c—MÊ éÁ]À*^'MR—€Ðf8E‚‰x(ÍBt‹c Ýu‚Ð]ÚF«E‘9I’i­ÅÌ÷PØ©Å^uj·¨8!”¦‚d+8V/Ã{ƒjÚ¤Y)Q9Ë.Йã×k1ÄZrDIrUûv…«2 Õ×vÖ ’5/ã 9úž§lÒ¸ŒAÝþ ­iƒÖse¤ìCãËXd¬ù¾˜±Å³tÞìØÃÑ'þycd>ˆ[àl?þð­NwÙX]+~·Ñé¶ Ðf™[†Hº›ˆü6ø@{࿎L„ì¥íÿ$ &ì„k^á×#F»ª|t³àH×WÒ|»A»Ö<Á—Í•hœµ_=sÈ4K}ñ7³È¯)žXvØu©§zÏ–éV—{¹*s Ü_‡M#ý$<®÷ZStíÃ{úþ_ôoþ\¥¼é_ü°änvpŽÀ×»!Ñ ”0Loo^"Y¼:Ï<„Ì;ÒOÍðt›½É@¦ŸŠA\aï«æc{zìnà#§ [ú?]ã{µ³ÍFo(ÜÍCˆžpqÛº·ÙÜ!ˆïæûËù\Ïåø÷å{år?°Ê^nòC…Ðo˜„Ö ãF‘»‚c/znËFî;‰Ã‚vŽÑ]tá+;Å€þó·¶`>Œ` èRyhÝÏõñÛŽò÷(y§ð!~1=ìlËGd¾_#²cÀ³¤ü¬Ý×4£&ø+¶cà?Úš„üoƒ¯ô†1OÕø‘½†·£ iºÔ1x‚,ظÜÔqV?5v¸(¾˜rSd_§±!D/€¹z›}Ñegêöäa‹Äï]ýÉò‘½ˆ„€7q ›¢°×ÜŸíM¼Ð*³õrѽÎJì¯sÑ÷¬¯:ùèÝg¶½ÓÚ(ÙêYsÓŸýÙ†¿\ ¿SE0Ür6÷»é{ñù§ÁÚöéUÖԣǎ†ØrDïÁÙÏŽ™,RšhQ$6ÙÃQñãS ýrªø| el»fåcäÝ*“F Õ`¢Íh®Déû2¾ËCp#»3ãÉÞ™ßr³ÅA­Z~¸U¬¼jÂôcS£Moe¥U2£ÕÌ‘*ý¡R}PÐÆkA²¾ÎeÕWǼn— •_6/Òöô=OŽß—ëh}q(ÝF›oh×ÿÀßðw²µõæ}/Ÿ†Ž|TÂgŸ²úUM¨dzD¤]é2É«±/!arön` ]ΊõÈ–{4,}È+lXvêרâ=¬g¼.o4B¬nê–Z7DYqSõ|V(@êGEjL /Ç uan#ÈÎ0½i´´±9€-`:z0! -Ÿ6®îsÜD Ó«–Ø•¦C±¿› ßÐõ6lëAyhK°*oø¬-ö’œÿ½¡{D-á­DxߢK®ƒu°öè*KøŒáx…U˜¨©œM/Ø}}® aèЀšxÛ;žß{¯yÚöíQL—ÞÃìFù˜zB1xZQóÇok•»UÛt Y$]„­ŠõæHI“÷Љl똒|.ÒQ¶ûHtMÅã¸j¸ ;Ç‚ãw”[?±f•ï«MÃ)N|ŠXðˆ±Ÿ¶ÞÉv4¤ÙRp°w[<©ûÅeK_«ãô,–æ=ĤVy.XyˆåŸ-t²“gSQ÷ /üg©ø%ã( ‘¯ãac‰>(qMMñÖ_Œ\×/AÁúOkÑ©yšÄ :$§Z±îiFÕrœT»÷Øà=¬§,ì…Ø†\mw¬X ¿[ûÌÂ$R¼ö^=U-¢E„RMäéÇ«·‘~è °y#‹éµ‹Æ%È dsX’ÌúJ¶–Ìò9…ì‰OˆLGþÊb‹Ó§Üªò§.оë(D-™e+ÝŠ·}RûaH¼T3[ªsx2¯KÙ”ÇD›Ó§^0=o"Ý6éÝ7Øe1"Hø Á!ÛPB•ÄS§å”ÖwL¦•l1”ÒžÓiÄj’A„&ÎGM6µ=xÆýļÁããïŽÌ猆¡ðL£Ÿê/$a.C…·óN-2ì-ñ¶G~˜¹ÅƒH¾†zŒ`°q2Œ26ððúiâvEÿ>Æ…<å”ÄÌó~ö¾†úH_1)—„&ÉTvB¡&%XÂMH²…™¶°1%Ê\š’“o¸6G'á,z÷Íl)·a‰!j"b þ¥5ì3díbOa)ßÕWº¿J” ÷»\¢³E¦\< š|"Ü@Ý0\Xºe<Á—lü²nÐ=U÷Õ% O‰ µ£¬¯ä!6¹+4ë~ÉrL¼=ÀñoÉåoôÁU›û½HÛ å€TIÈ$$¡Â`Ðç%×Ä*O0æý4jê'CL ¸•ÏüÖÍà)MO ŒÏh¼K„+p*¾K¤ã•·àn¢9 ˆ%K„$Îpƒ¿†ÄŽY«D“ý¨¨ž. ÙÂ8í¬M'¹ŸÞžM*-&ssSu»‡ N#ç[pê_“ÖÈ[˜mPÔ‚Ë[mxÅU­îùs€C¦L‡÷G‰ÎàsÂÅY@²2g 4Ò ÿsl н|H"­e¥² UA›»€\¬Ù‚ÐÇÂ]|\¸]BöVÂfz§i ÒIJKõÜä4EÅCsðý!²ª¾®‡ƒe˜”‚•ößìR®ŸÔeuù{Ü„ûDúš¼`4¾‹åDïÇ1ˆ[É•³ S7PÙV ¸vŠ5¹lT9 ÖÐ6øóŒ}ªuRnÑFèΔ»ê«>žXhÕª”Kâp€ •;ݰMŽÔjU£ ßa» –ðñúÕbM˜ã€¬SríU$èõçÕžç•TŠsg7Ÿ‚àLÝkü+<}tÅʯIP¡$á5LÚñ2â„{=}Gr¿Õ ý6…ˆÒL»¿G9¢·Ë$ù'kã\*yYöAÿè-i–Ï$—ÝKòÑ8¸f3Q[O橃.ôÀ2zÆq¤bvƒi´}:oƒ3_?|cÛ?™!qý±ÊØÀwä=#™ïõÖz%3 ûp ÷[ËãË‘°H2pgotDËÖ^§×kµ ýäü–†~J+%8§<Ëõ·lU¿mÄ ,I´è†D^œ[€Ž½@UL‚Éù¡W›2í­oqÞ±;;­y“Bå1Þ= ºû¶ƒc?s¥û[™ÍaKbiîA¿Ì¤ñé:VŠŸÌ -@š&ãöí……ôi}2¾1XÞë¹ Xfë÷Rù¸ ×;-¥ìLCªK¹¶±E8ž Wi"’ÔUÔë—K¢ŽšXÇ\*QGP$=>K×8‰I“x‰ŠÂDÄLá:Â6æÑÚ uQ;>£J“…óRuæÒ†8å,î¹åTÄóOÈCcækµ@ büŽ—LůþÒû8úEÉrÒûÕmLÉæÞÎÌËÇɇÊxð’رyðzdD¼OûÞJÔßš«_-3§òÃá3Y¹Ó‘aïg€—9¸d’9T{™Ý^ç ê{ï%“ç-v›yùcÛÚ¾çpö¤ÜNVVg…PÜ}¢\¢¼×« 9äØì$ì`FÓÙšt~V£ÓKÃWIûÍ™îZCiêaCìŹ¢‹ç9T¿^&´DÑ—ao[tž˜.=˜>ìÂáB½rÍò¥‡¾|üLÞûâ©çm4Û¼sþ4zœg^<Ö¨9Û¼;Ù+í÷ìõAsQ$NËÝ` 2O•]¿gê÷œ÷^šzW¶%-6ý®Kª†'!t–…¥QÐêã`:ÂIé¸-è^óèåîŒÉim¼vWòCv´™Y-¼v”†š5i]nµ‡#•Fu}jÔ‡GçÌIÊ0¹,QâõÀá9ËBzæ, Ôrç•Ýí醭½•— Y»›3 {ò|ì¬Çº3s{ÝÙZÝ~_â&wÁäg-gµn¸/óbPY‘"É«&dbþeö«~¿«µ™~Ç¿ØÙŒ>Þ61d66^fVNN_çK7ÃGh[.sK5`TbÃl‰¾æÖ óÄØµ ÚQI8èÉ%“‡ѵäL~6pÆiLjØZJ Mñ¸˜YcÚõ_[OÇç¨ù| k‘l5›Îv¦0kKe4©»?ßôó?<að§÷œµFËÎ ‹ÏÅëaò°-Dõ’Ôd3zã¡sbnrØ+°Kï®aÂc)1½Ä<ìuÿT`aª ÉŸÊÚ‡Èkt[^§õó û[÷åqÕœªñOi´FAÑâiÊÕÜJ½²4¼c¦÷¦#ÓÞ£´ó­=N´;páåA9 ÖŸ8ná ݃>vÃSÚkÛF>¯êÉrz1¿¸<¿Àxü1e©­ÄúKw[»ç~'ái† ê×þ¡ãð÷ïñBž«®à8»Á¾ý¼|â7wÈý!–¡Õº.înñ”Ç1Œ¡Ñkg4õ3xˆÐ{Þë+v܆_Dô4w,¢t¼½ýbT#\êð4øã0ɱ¼eòµäoyYCýòýQæìƒ‘ghÄ¢ tM@I1£Wd{quíÃ_*¶SÖèm”© ˆþë0éÂeóhÙ>ßšÁx6 °þ%A*úä@Д‹<¶¯Øw¯‰f²PÈ”Ç|%·c«Ô»èJyd¡ý† DazûÅõU§]6W¨Ã(×ÇÄõä..2e®"èbŒ¿yÛ»î_X´¢Ún²°.þ+"ä]Fº×c‚ûetn¸[£ a•“Îk+ëºd@ $js†ñóùìŠ&Ñ÷m$@ÃTB{èƒÛÈðÛzzü<qŸ4½d07ÊžrYÖß{È”„ð÷ãéìãg¥2Nßœ{¿(eœè¿=½Tïο‚Õô,§úMø×Öž”@$oÄÈL© Z‰ZÚЂrø" ˆª¸Lo̘«N…çà†Å¥¾]gè)"AV»óÞ™š^Ó_£Z(AMŒ8¾-·Æ ÍkÝz•*­jU~“­i’´º¯îeôf¯u¦EËÿ@®‡ã™@#Îa»Ÿ2­1Õr¬h)¶´{ÓÏ«_îÑxè3³ò¾{&n&}wqoi´l¸8'>YJ¾ë¸ó……¤l˜ŽÓÂqej÷ ÔªøÞVx9lƒѰÔr¿Áâ@ñï e¬Â°ÿŒÏɉ–† ‚@ñ£ß_8i±­Óš¸jÂGŠQéµ>[¼¬%[ÏßAX­¡ˆLen4­]EЉôTÃ1g´Šñåïzç̉ê4_ûÃiñþàâý᥀m/ {QÐGKö'–èˆA‘j`E2´d&üN°`§üb.oçµ”íµœµKXŒ—Å/£{Àn›Ð¦’‡'¨&Ö¶W†­LÉ,~äž,»÷b"ðÑ·#0xQ/þÑBqçwAª=R- l­q²PÖŽaîeMŠ,&“•‡y’Á¼Ö7Y£.„‡¥Sün9˜ëñW¹Œt¼®&çé¶Qü–*üª†²±!üËøK>ð`"l†á?Í´yyx4ŠÆéÃ÷JPGC$¶éh{ ïGFklöÍM ›Oøzs¤Ø}–Ž`¼`ñš‘sFoud²R«q´ .Ëß¿ø£{ÊcT易Ïå Pl€›„& QßæˆŠí22×ð$s ’M@t§ž×I$a"vj‡HLû5ÄãiLƒ/’ƒñå*Å ÇÔ†£²¦¹C£ey‡âR­MÆÙ/ËR“M]OËr«©p”Nd¡ã†¬^3-¤{ƒøg˜ûÆ'òOèä^«Ü(ðZCùÊà`áÙ¸íßa`ß)<©ËÕû XÄ‚#BÚÒ@ˆ²Xr±~Yå±²Çòƒf§°Xý º a¢ð/}ÄFæ¹T Irm hžDsb|åz¸$6ZªOÑ\>œ:’ƒ*¡‘(V‘Qå›(ÊÒy¢JjÚ™Bp툱$eîs½hGe6*Ã.¬6Ok*|üÑ)aŠdV¢tKš~ûØ+¦Á—êŽU§Z9§Ç_auÐñmVÊVæƒ6cé+ÅU•æïDUÔ§[@w˨kÇ/ïC‘Önx´Räx(§òòº9ßqí&±MÏÛÓ€\iÚ[lGP©ª8u~õt ›“v^jóÊ‘q8²Ñ\I…Â^˜eÌ“U f#ÒÈ·¿â£¥Ëo³YÒ‡·_œyœYØÿÅÚ; lM‚×¶mÛÆwmÛ¶mÛ¶mÛ¶mÛ¶æõl:^¯þž˜eEœeEfežŒ,1jÂõmëÆMlI·ó>h¾xÏpYý¿UeÍjàjËõ?ì¸?âØ‡t€«0JƒžR.[òÙbß²MT÷¶ñ2$H¸_áÈ y|Þ]ÝÁ½ø5š^ô.s×Ãõ›ãŸ¨·þÝ™ˆI+²¸håBg—ÛÆãö§ëlõßRç >³…þÃ|Hkô 0ld¶JcIίýìëS§œ¾7ñù™\j)ã’Ÿ±=çëS¸Ú}¶ ÝÔÇæzÿµø(» #U çé l‘ˆöžr»O[^ÚÊ[ôGÒØ)µ5sÒmáœYµ+jCZô…½]ÀSÒßÉ¥¥1ksu«¯ÉMdíÅQDsÓ÷Sÿhó%§Éåpw3ˆ…NõÅRÖÇR`éXÎ*=mü> ^žL9i¨ÁÑ—–Yn¿™çZŠx8 AˆW’Ò ~„F"g>g(=ä`{S‡wÛ-C‹Fe5y€nZÑS°âéÔÚ x½ˆœ8 óã2Ž˜74:ŽºhsàÄ:ÂMAÀ['®‡ã5þáàõ…µÝ¿“á±+&+PÜ%²6yc[Yú”¾éÈO×ÞŽCçí Ò€Ø-‰í/„i1Eó}rû^ƒ(ÕæÅýfV Ës,3/³=;ŸÏ 7ýAÿwQ·zØ¥&8@Ãÿ bdnb`ílîdâèjaô¿RF+êWÿ«Žòï¶/Оo]T‡œ2´ð~ßÀòm‰»>ÛTD¢YxëÚX¸‚Ã6þÏGL^Ò÷¸Aä™0aºëhçg°3ŸÏ,p†ÄÑ—$;ß ‹Ôm<#I|Îüj t U10Qo)Ѻ²aþè?ü¡µéR¡uõÒÜd‡·{ûM—zÕÖ¡÷o–àÃ{oûEüΟ¶üzÓŽHA"Ãã»`G—NQ0oPZ¡nÁhbž3U¥ÏZŽ–̶„AëN@J@ÔÄ% ")}“Í€6Àþbe7mû«ó¢#¸m¶ñv<éYQ5dç›è±Y{¦ó.sQò¢ƒ }& ؃+dÇP½ü  lD¨VB7FµÏ Dåq  )q‰Lf¨ˆôwgQŸzêLrwÙùì$¼N 2o‰ÇR€ x¤EÒ.¬áRܘÝõ~²ùM:gŒn¿¢ö 0"Ä£ÙìüêÇh©gæ+¦Ù´ˆª ˜•óÃTLÜ`RññH¡Ö|äÞø&c´î³ýO>³¾­D*ŠÅ<í&ôDL–õÊ•ø¯ŽÛ¥” y_CìÎtŸ6ÎŽ¬xUuô? Ðt5+–¾á^EߪB;ÆKwÃi5¶2 NÿLÇŸÌ…KÎXA!ÍlPHqQw÷åéVmøÊ°"u‹DÛ8¤!ºK˜–…Ì2åDãEŠÕåáÇP £ïY¯RùBí 9 (ÍÚXzTšLÐ7»Á‡>×ñ6+ogáY±Há Ón¼Ù2´vRßXN@§¶sM¼2Êi¶?©’L:¯þDöÇE;f… áÁýO”e ‰õ'D]!2׺BÙwÞú¼­Ò&À§Íó5TJyh¥ÇÍi«l;Çîø”C¬â̆7…R•x)õ›í\ÊéEÌ'ïŸY0d““}ýžçìÕ-Í ÏóÏ96Èó-®§,.%p-•tþØ>Ï_Çêó›”õ¸?§—ÅJ ¸±;ï¿/ŸôÏs^rŽö2ÜCN8Ï~²ÿ%Ìg§0•!–Ÿ$ïÓnG0w8摌„f‡Ë VpÊ!'â­j û ÉÍN,Ñ8¶3_—ÁÒɶÝñ…+—}‚&pô#©ù·Ûÿ6ohësòUÞTz’@Š[„ôÁU^»íÓ’GeÀeûHÑduø3ñÇ^€Q Ë“Š-Eê’NOºG¨×µ¢™À©Wô"h†˜ZÉQʇ™· Qø´)Bg§›n€‡ª&-©§”•}T#tlfF®¯îj؃ ìÖÇRªu^Íã.…¦¸Žï.Z–F_L0¡É˜(U…C$-ìB^Œp!¨0¦ˆJ(tј_¾xþò­2åÎãXâÂÃÊÉ.Ç0ŽII©£×  ·iU>:`%¼ó5“¬wư­3£%•Rõô8²ðÔd'Ðå 6åš8˜¸«9ñûW,€ùÚ¬\ × æ†“ÝU¨W>o˜MfT,€"—Nx×jª`e«rV´æ` R“ýöл¦.™u¢7XL;êë28’4€ ñf«ÐÖJ-=›ÐM»O~_},ž¯ÌÔÚÛÔü‹VÌ· Õò“¿æÁÒ#%¹ÞõÑrñYÙGìŒp¯Ñf_ü­Ëè-zûÌðM p7Á§]Ú½Š/YŸÚ)©{9…gHë¼z¿Ûio9ú¦ŒjÙva«=\‘ê<>· ɯ†øŒŠWªóüg ÏŒ„f9™HAq¸e#.¬nY­yÞfy¤qÓT¥¶oßÄÛuq‹þðú®…‰•…É5ƒ‘ySÊ»4/»|~ à;öDsvã ý¼gæ¸I×´«å P̨p1:ó,Ù²¬D&ޤؔuÊݨ`…2@öÑÉÄ+Q&‹ëòöbHq>³¸S–ãú~j:¯ú” ðY¿ës,bŸ>Õ–ä"¥³,Nže9&$²ånžâê)l§d´ ²ÔÛ ºlƒ³ƒÏϲig:¢{qÏŽ¿>P¦bÊ\AE=]dÚS6—È0Ï|B+îšÂöÒš{Í*Qµî,Z@Šj τȡyf¹£M ™,×K·°•Ò?užU)-n °(CŠ™¨EI}đƩWZÞžk.B“e+b CMð¡²B£¢†ì •ô²%ʃ–©äDÜJ…WG”˜ ì;éP‘V³Ë]ÙgMÔ¿Äß¼Ðâtû¼r1=ZrŽÖñ´±3nÌzÁÚ·I¢(×ë³”9ͧˆ±h%SÇÔç3Ò#Í fXÅÃGF³XVÐ#‡ —ÊÎxÿV]åìJngs¬ÏŸWx7= 3Ъ‘žŽ2MF¯AÔjùðßQàÝrW¤HÆÿ±'Ž8OKÜÕ2Ø õq½:–b‘ í¤qP¦~$HùRÛ* ·?Q„hlz¯\µžùÐudg &+—Á¸´­hÊ…ïUëxôfÉ«›Çã8—Yâ¹"(*É ƒRä·÷‰yäøxª+|L™0ÇáküiÂÛøYÝý¦WÞ ã³DèB¡·0‰xóàÑÍ'5ö{µyÉ(ë°lúãÚPõÒ©Ž…#A^Ž÷Úd¤«ËoH{Šgޤ{§[ÒnÍ£i3ÆÜ¢ë@q0…Î:…îá½:'ÿb”òL£ª«øÞÿð^ÿ—€ ûÌïè­èËʘö¬ ÛÓ°¥àÝhy°=Âm ¾cÀ¤aµÄ6¤µ|˜7|/Í%–ÈKŠþAÒÙßçD²s¹…­* =f›òEÍ­ý}ÿ 9[UƒOüA‰”möØûòjÿû‹0A•ï~\9¾j!M„·þ oö°Š! %*€÷1²!Y¡N3‹Ž~ήG‰†^ùÍ–Ÿ*GH>%ÆùäëÒoëú:ÂÚÞ¬¢¸yª[¨öm§Ríó´eUbpÚ€¦Å]™t?9.ë×Ç?ö„oh;ï·¡žüéDåvyíøßÞL‰‡ý„8<Àÿÿ¬DÒÜÎÉÙÞÚÅÌÂö•HjÙ8mµ#þÖêP1·8'`nÉ0©é$«:‹ÕÐz‘pºUŒ Hˆ†iôª/˜<îuAû“(6¦p8­É¾  MŒ6¬û¼†Ì#;Þr:¤7%?ûMŠÎv| :¡ÇÇsD¯6°ëŸïß‹…|sD1”V ƒeßO;©Ç3¤L&Òa\ˆ-kÔK„u›9sÐ’˜OŠÍ¼ÊõàšK¬‘@(%’¨»Cfxð_' &»: š{±Øchx\Êg¨÷#î–¢”S΋ٳ*]@ RÍÉÝ6™U­Lj«SGŸÍy ¤ópcãÁ‹& ðAL‡iƒòI!!5ù™éÂHõè cðÀõ€RútHŸó˜ÞÓÀ*ìª\—è2Ü!,oDŽÂ´·ë&ŽîÚ¤ZNN–±VäÓaÅ›ú †&‘y,åÁãî?<ÙÞZJúÞÆý,8s¤ÇG1röbôàʆ¡·sþ,€?Μۄ1È£Jñ‰0N z–‹žÎ’E[M Ê#1ŽeyÀUS§›9ÏŽót9L"F¥éfeø»ÓÙ?ô”¤9Z‰WF5¶¿Ž Ú@vh—AÛ‚Ñçš;"*& ]íSʳýkg •YŸ:ûH‘RJõ”JÕûËô?˜¸Ó8툥K .Dý2F±Y ÒÔNœ‡?sB³+jVÇ s’2øÈÜÍØÏÃŒC7òbº‘튚Xîåéâ½Nú½¿¶äYŸý€£s,å›ûîŒB×]&\v±‘mÎu«Ó^æf0¤3 Ó5Àâ*˜£osÌ0¡ìôªýÇÐ^sœ`lM‹ñ²$2xȽ¾)äëDdk{’T’›w<Ô¦ØýpHùõܨ*«)Û òÔb7t²:à†Tt\þ{¡ÏÒ"öó$ æëÇ!×?ôo ÿñîÕãýHTˆ5|*ê!ùy\/V§Çš¥vTèëØÐ{7o³,¨b§çÔˆüßr2ÃóǾÔÛÓAš™bh±E,ö7(Ï æ×ÑÅ}±O &ì˜[àÃÇ‹é²+[šZ}°“퀚‹Sщ™„ÉøÆˆEN¾Åì`ÛþN¾›š}tÙ†9Ô?&ZúWøßN?{ˆ9û÷­u>>ìÓN±L¹¯)ã=Ày¨Å:%å÷ŠfÅ ¨wŽ+béÍ¡6²äQ}´mB^5\–Ì×0â÷R8H`fUÊ=8º9:¾ì"a¨‹D-Ìý¹Þ)"~múI\SI=Ä,lôp&ž—Fdît'!ÆŽ„þ[ܧ4£r–~çwM+°œ7±¡¤ã­sRØnLކÁR‡Ý?w_[½À®nÎ.÷¡¾õ¤ƒ>ùócpGw{åob¹qÎa¢hºˆ7ì«Ä³3;¬˜Ð#¥p¥;}“4_¶·@œ5̓›ºi‰­0Q;ú¶¢Œ¼ÍÙ@0À'ƒHäKòê¼<˜Ò±»ë®zð£Ð[ <)šï#&þ’RÉÄ]ƒ,xÔ߆/u>Œ`¢ÖP]Ë­šàæðþìÝxWq–Ö3WqœÏi´,³£Ó‰”‡•™H…t”èAÿød%øÁ°²ËëfúæUÒ lÀSeÿ·Ü©mÓ.ý A|Ñä•"¾k­òùâ·å'B%)â¹Ùh yíœ|ué:õ4Ù|K°ðe©œúg »M/FŒ’^”ê&&|`ö§T>vM¨è¦'ˆ¥ #j½N±è’ÙÁÓŽ0ÚĸÜD·üž—[ A˜U …>x;B6«I €7å*TJa^RòîD÷ÙDÛóTÛç~Z^àGiwña¡Á7”^òxE¡Cî«D5RòÃŽº5ÝýyŽýû—N ÕŽátXe˜AI7Y-™ª^¾vþuÄwjOœÚX{T“œöJA”è4ÄúW- õœ¸ôãJé‰d‹Ösû$«eØ/ϵjÔÂF •”‹•­ÒãиÌã”xr¤òBü<pƒíq¬ìå†v8 N)›ß*\3ÿªLH ©b÷PZ6Ò fÍ•*á¬4)mE°ôü B‰œpËÉ=“Ѩ앓Ê×ÀTû¥¦MÞü¥½sU’bªCâjÒ+¤KFínbàà =CÝÜ`©b´H†´ÝÉï%²‰#éÂʉЕ‡¤2Zš$[Å©“c40’;á(U±[ ¦ï|&”®ðáÚôuZµ„½®òò*#(†»Ô‘TÐNvØ,[@“p‹rlæäºg˜±Ì#ÖÖùI÷2!þÿ¨1Û^fi6×#Áf"϶œ?Ü5A3ÄjYR2wHÄèX/w–ó}ÄfÖØG|mRl6¶¨IF{këù¹j  ‰€Î:‹µb ôžqt‡â™Ça錳Uj Á|«2…Û +'+Qñ–iñµ…h“â¶ùUÓsí)í‘[`¯‘Ö{L{%: n.a6ñíÔkd3ôÃçzŒ£ïULÌK`SU7)0Fš7mª²çA›®oL.¬ÿÆz¼ŒEƒ©kdžÅ¢D I6K:ŪlF$-uÔaØdÚ­ H%/¼NÖd òy¦°Tâ'W,þ%ЈÚt80‰QØ:Ó×&óôß(¶®®ƒÝäTÑw “ÐË›˜ªƒ ”6*"Üiäƒ$5#,€õT£á)§#†@ êlÜ2Å*²„CµØOë,PºòÔ¹âA¯Œ˜1·®ÂÐè°:Uæ| tºTo„]¬šMiy(c1ëÌèö™“)XuY†(`3ÖÞÝmûÃYe¨yËÁ¢”¼IØ¡³æ'öغ ´_ªPÇÓ¸ªÛÙÉ=|öð2¸´¤7þ]TÍÔX²‡Ý¿˜#G>:#šòQh'©¼mØùÜôQôôÚé·𮼵o²®L„jöœ›¡ê5.C¦(”‹ßÍãÍ_ªÅÖ§Œâ…b K7+òPwð;¢0¨üëÆbñèÅ2ŽAD£úåE[\µr´è°ÞÀ]ð.gwt›Àä|Píê‚ù2}Ú™7«iÚ“¡-¶cÜ¿¼Ä_ }»3lÉŠ®ìj] ï¼^—ñžº ¹ µü¦•z µï·x¿2É\«P3Tuœ*ÎhjŒÙŠ1—N‹Œqƒ¦SßçÖÀ1ò³ (5(TKµJŠj ¥ÃŸµCX¸ žÐØ×¿þç3ÜAtˆ4ýÐ7!¿´ñÆâ‚MÂÝa`xò˜ÒN…Å …úòE6bs§Sž_®Ì\Gn :ìS ¬Ñ[QS>n*+V .ZMAÇJ‚ã{²Ÿ¦|"w¸ Æ?ÌÅØÅ=ÆU^5›•.øxã(”»MsŽ \%‰q‰jŒBõŒC¯c5¤üvR¥¤Ý§™e1‹Çá 5×ë]áóÔ –ðã}6Sl9•¾Æ»Ètêžée„ÉSHÓr—ʦìÊd¬ µ·_¿ä£• â+®óø‹#%¸n99·¼ßP^é÷Dä{X*tË3ùæR°Ì0ùµÛÛ }”ÆqÕ÷Õ4éõÁ™í¨˜©éÑ'Å™ª·£vBäwÕfRy&èË;Ç¡-œLÝވƙªdðSÒ[ÚÔŠfðXò-´´ÿÅ¢/ÓMø\ŸÂ&å¥(Ct¾@ÙjÇ O„dÞŸú¦uþÐ>½Gš‘û9z³Þâ›ÂVøa˜9˜Òº'<î$×UY*4…¯’­WŠ1øgÂætÖf/€¼ð/B¹Û²ä2<Ý:fýÏ ´tËS8~Ž+]ŸÀ$^(^ÂoГL ®ÿlëß°ZúA‰y䓹j3ëp|"b™Rò)²ïXïßC¾"K>ÑPI· *ößÊ; Á²9PÕÜ«ì'©¶hݶœc™¼é^*ur÷}ø$°:êo‡ÀA¹‹Q[·Àœ /ÆcôiÕ9mÉØWMØÓÇ¡FA:T«7jËéÀ™€©PÆ&| Å{‡NYô(£ŽÚúró)þd;ÝqJKo¸gŸ×»ç²a!ºæÚÊzË_cù ®)%åü?èz­¶‹Ý4ìûýÞ“ˆba\WøHËC)}zÝ6ù ¢…»Ê÷)4ª ,P„9Ê}X¾[±eÐH<“0„ ¥ªLЧ䴣©ù™ïønª= H²žï Û#FÁ}ÀÂfes!À—Ãûz™Üƒd›Ç¡Zf­ß:Ì^m*ò ³²ó³á(äyƒ Ž}ßÖèEVx㿦síÍ2`‚äF Kå|?y*½J½ÁUGoÑ šO;A%WXA'¼H?Ytšóò’Øñv`_,NÉÏà“`Ì|É'»6bðÑBD¼D°œØ,*ÞceDý*C<¿ÝA¡ZŒ¸e¼dk`µ.]Úú5¢s]DP¤°F7µÚH€HÊZð5³ J@È*€.ú\çñSäýO_Ç|ù¬;ôoö!V_Û¦ŒÐ5ЦV WŸyR¨³¬B¼ŽÓµï™¡ù±|2*Iw’öÑ9b6_×0>Kbç';(û£Þ@I׉òàË=“ñX󸼽·üÅ""~ß^øÏ£±h€–®BƒJºÇ% …#$E|¤L÷ÀÚ§û)Øá˜¢Æ•M¹0k[—ƒ3È‹.·ïµœFFeD‹…p}©ß~Í0÷\ZµœIñ”¤F󼬟†ÇþÇ^x?JYOða)mE ü ,Ú–WýP‘"ˆaŸÚr{9ùhÿÝ´e|¢–»Š<®+ÖØöQo…娫1!f¬\–¥ªu`Û"3é}_ b};¶éV×ÿ+^‰F7%8tþ§fºÍn˜&àHþ·æmkLRZ’s¢rsR¥õúÐØ pR3bx~¥åä˜/ŸkqÖ®bJ?¨hð‹Ü‚öYÚ³8ü÷Kñ ,%t˪Ôè%_ºµšÄÐŽI|p×| [b:µÂŸx§!Íe|H׆H›ƒºhÚï§î’â¶ÌRV%4ý!n…Ix«ãÕ›©³ÄÄ K©çjå<&?CIݤ¢N`ÀÙ”Jróoðÿ^ÔP°×üçhÄö?kdù/InÔ£5e‡#æYw‹ Çy|O¨ˆ"Á«õº)U'¢èj<¸ê¹†'EÝ;_Lâ…dgçÔ)Á¶ñdïOºAÜ "Pï“H ÇIúÏqœ°lÛqšòßoð¶ã )'çÞ]Uen.¦"Ú~¤aEeg®žÏÞ—2åÑÐßÃ]›Ÿ ò¥\¬/Ù^»ÄÁ‚žP° òR7aeèlV¡å¥·¬²S6»H:ú  .Ø$'"š°¼2¸ˆ¸r˜±n°ˆ>ÚŠ>â ¢7Á`by\äeyy\Äez5m¸½ö2¹°‹`*¸ˆ»Ä2¹ˆ‹bj˜0éešÈK# zmåR#Í u!q%s#Ñ †Bò §BU¥šÈOf¹n‘©BOæoè½ùgÖ 6úŠÖ`QíÒÊÖ`‘ÅÒnry/-Oæ£Ø`QÄ.â.ª‚š;ËD\;uåÓ`ä.2žÌ=òÁÈô]kùdèÃuˆizʽUt@ ÛN÷ˆ:Áùçó^<ˆzÅ¡’ƒgzxù3ÙðJ‹‘DfÌ©ù;B mìÖgW]=ºy6Ö7]ÙÔòøt5ÜðRÔTó6Q7ìÏNVOÆíH°iù“ò7Ä#•o²k©ùk«í¸¯NZ âè¢æ¨^W'G=™D|J¢f5?б]^¿†˜ŸMòÐüü,.ººÈßú¾ö/Æã 7-q´~öÌ)}õ¦®;[HroíPK¶xÜFç_ñlZ:ÜTû|JÅ£w¼Ýß‚âÐn}íß‚0¬ü:äãÐ- þù cÙîÄ•yR½džy^ZÍP< ñuöOÁˆ_€ÁòN^u>–ß: ó@çîAeÕ]*‚•4dkª]³I/¬Ã¾7ÖžÓ×Í ®W?\?Ôôʹ´!^/Z”Ù1H3žau¶ú`YLQ+f ½Bãy‹Ïø'ã߱Ġ¢ÚËI³Ãù" ªOy[â…ì×-ý“‡Ìð:¼›MìhS½3Ò_`bŽ+‚§·.yáÃñTH>=÷Ô[êwOéÛ!²%¦KªåŒgφº%¥‚Pkó»?ºV]ú<~ìQ2ã¯Ì´ar´CQú¯´#—쑰̽•ålmzNÿ‡âŽþíÊ´GÛÊA#.:ð.üçVR¦Ö¡*Iu$ë[þöh;Y–:«’ éÎl9Š1Dx¡8Ò¡Ôϧ~X!ê7Z“dMÍ©Ú[1ßÅXñ/[1›=(×ø»Ý¢¾Sp±^·1ÃKV‹ðs„/‹¶÷±B5þ×$ók ËÒr6ß4ëú¦ò.zÜL*Ï‘ÓižJõ1Þµl1ìWM-EÊ:[~ƒ9³6Ì•à$'‘Õžíú6ÀòEq"¾ ‹ÑÞàÀýhîaÏc¿%°9溄}‰e áZÜM- Ä&‚NêÝÔeÎ~˜r$`RÃxH#ÍF°U¦| £Qwd«â}I¿^ÂpâpnYÇý¨¤n÷—2¢øp¬“$@wð!U`õ+Ña@#á¿þ‘D*C‚LK”ïÐ GO*,¥I…#±ú¯óâòË}¤} Jxhí‡ ’Õ§î‡Üqd2\¸¨ó¿`&qâ‰hžß|¼>ƒpú†ë©C},Ñ/<4îSôRíÞ‘ï&¼>?:d$uu,Av…èqð¯¤^Ù¸q†gÞç›K·½BÙI¶jV^,¶Ñ椸£^*z+_èß9N`Êõ$˜ûÉ_ìÑ}×-<µÖ˜U›>àTè›ÜøÖàz~A g3è@„ǧzCnx¶Õ¶)ê«d-qCzhK‡õØ&×pï5'–/©¢Ä⌮ݵ ‡ˆ0¥ÙVÂ@¶ù  ˜QLÿˆƒklϰ„½BÀŽ:bî2 z€àÅ_õâÀþD'áa¬¥)‰5‹?é(~…¥"…ôLëklˆ-ŽG¶Ñ“XÛ 6¨ï$bÂ\–1È«: ¥S™J|öÚL'SÚ2a.—í_ª•:òØ-Ù—gH?Vf=ç… ìÆæ¤!kIÁŽhø!ù´ljBÁ&i!DÈ4‰ßX–¿LŸ>áW¥ó3¨.ÓËlŸõ± D4U›=–ïy(C×Å1ªÞµ5›‹’4©0š@«ì2Ù]žúÍÞ(s» ÀDN2’„%±¹‚'¿üx¡mG+_Ç.=s*=[µUNåÖ65¼|ë·ÈY¢QܹñÈVòõ—iŠ –Œƒ!¦ó“©¾Ëˆì•ð›èa¸Q-3V êº:žJk—ÍYøâÎp^E\pI .­ÆÁT/íb '‹Jg@( ߘŽZªS|ð¦,„@ƒ¡…¥_JŸ#Žq™'?šuÁÛ5Ó0NÄÃ, ÌDœÄñFŽ ö2ÄB+²UÝI,ÚòÔâÙŒà0¡CaWœÏGõ‰„{7¯éý2"p«dZçX|qœÉ#Sn¢¿,LÃ:¢ Sü××£SnC ëöºÖÅûòÆŸý#5ýÒêj~W6ê×ÉQ!Uâú¡ƒ€§è›âUóÞù(„«^ÈËèÌ»ì˜Ì ž’3f=„Q™~즶ïBµZ!ØÝ!XúÕùªTBHλ=ªy±ôµèn ãyš™>ТҘÃåÏ 9ÅNUÑ3žƒ#p.¼Hk °9Ê­ }È‹&†+wëréVRöcEÛx€S~ ¿ÕÞœ\†t@Ä·2-1ë!úÖ>áÇ”ùÍPÑFf8E0Ótä’G©³6¿»Á­1аcÌæÓLý[¾¼ßó"G¤"ÊìXeÑ X© ºAIbCb–̼tUÖ&c6a7Z©‹éG¤#~F&Ýi!þ‡"Ïùûô/>mÄ©ÝÐcS)É’–#-G½Þ5{MÛãÕšØ~\ÐX…\aƪۮû„Ñ ]%/± ['·ÚúöÀPX˯óhñÂjynÚ…'Zu9ß;ÿNN²ËÎUâ0 Ý[Z~¨ÌÜk2·X “›:<Í<©ö'¾Šúu.¡h›Ø'vKDê¸/©XSóà†OÁä~ºŽý¨¦m‚‰¿H‡€VÃâx|Ö‚ÚÚtœ©ÐP£¿"÷°?¯ø ÏŽ9–© ñqÀ0ýÌ Éܦ)ßi¾„áB¥Þ©·åñ,còhÜêó»uE‘Æ-¤ Â0ô$b‡É#?$L‡X] ‰%˶ÁÒ+¹Õêúô= B¢–ócÃG—^XÍUº'·ê¢~“Ðõ9 ÷ h:›kÚ×F<¢ÀeáÇ]Ùy{ U3VÒ¸ ®ª‹aøO6ëŠ½ææ¯Í:–qNžFª·M2Ú´¶Õ¶‘«MtëÙë“A¢&† “4Z*SµÅœ³2I¶ç.rŸMÖFR¢×»Õ³mLâTZLf—'Êé¾±W ·ûÂÙö¼!¤ýyáXW'*óN$ṡIõ†¶eDqæ—‰$èR†‹Y\è¹Úá$­°£S DóD"n±&S«å´ï™,["tú&߉ CeÎ-ÎÃ} J`WÀ ¦¥qmYõPq(x™ôd& b#ëHÔTUy kÂÓ'˜iY”‹ ¤’^J‹KÔÒõƒmj¨,îUU¨’Lï?¯Q!Ä:Q;r•Gæ  `Çê&Å0i× ˜á›d ÿØ·Z:o=mç/`:GåŽVÝŒcìàÛÓ,å@])‰iDMåj­3sùºã‚ØtE×sõÃ9ðÕ‰¶–ï?"Dñ?‰ˆlú"¡Ê4Þ—œÆ^8ÝùÍÎ@œ›ˆâÝ£$Á<ºE{ ‰C‰Œ¤AÈêïõ©#Hf£8N\÷ñK ?@Y'¥P´DJÊhKÿ^,d©{1±Y –«Ô‹DþJbÃ9Lr‰£=0 ’kw9@þÚtÂÏI3î„ÍGÁ™23Gcd¯@ć8xÓ!ûûÁøÇàot²OËÖݰÖDæ†çÂìZ‘©OzªÁÂïº jýe2íxM¿T†›XŸ0Í(¡¸ç#E(!FBæÕ§—íw˜yjœÁ»B ööe¤L½SÞmQ:êHúÜ`KQeÅÖüy¯õ,jˆY¨Tž‚4é®1$H”iêŠÓKÑÍ[ÌÛñ…=‹xlR9(Øm¦'ÅC ´T¢9 }dÂ%,Ÿ[fž:ðº§\WYÀ8ÒŽÙ#ÌÂÛÊLº¿µ7@-ƒ7«ê¢ûšà’€ †Ë‹=ÖO BQ1‹Ö^Ïý:8jžƒ´³Å‹„¶?ÂfÂ8¬ù(LLât ^c«í­·5Tç}8T Õî?A·("„2•†°o´y²ç1ŽÜá“j RÉEdÃF¦0Ú× Ö1Õi‘Y™s$©f1æWÍÊÁÞÜÓ¯<#¸‡OF²œå‰-=¿Úƒ'”ËÞæH²-åžäáý¯ {Ì~GƒñÌ<ï­ËgLª3 æO³*²Ìʱñaºóˆr“V‚Ä.Õvô¬³f—÷lhšµRv¯ÄJvCqÅ1>;õëZ,üçöÙ¾a2å׺·*>ÊÍì£ö $÷¥Ê燽Aκàe{KM|Ü™à©| ? ­_Ým²© ´^µ ¶gïý“ÀAnÒÞíutÅÔÖˆðÙw±†bEÒ¾5 Ž,,g\‰6®XxÑ:UMn.2›4|µ? ]Ž g9´¨W ünss,?œüDqK:I÷µúÀ™OœR†cŽ¿CÇÎgšò9×n›Ýèá´ú JË1M¤ÏJc²Fg²…×'ø¼qKî’NÛ# ›=†ÚeWÿª-ygmœ¿p4׉ý$Œüü@2¡Ô¯èù„óS¦&Òf›’–¡½ÑI&Žš!ƒRKfµeWyh6?{‹2ãQ ?.!‹â_Ñ×gMÃÞø^3åÛJr¢ð¼pIÚßň`úXžsú¹Hgóô©(Ì©²[|,|hª û‚í¾Æ=ã¾ø~>ͯ»s€Š'ÊjËñTÙÔØ{5‡¤®RÂØÄǾŸ=\Dú&ZÍ.ËõÌÏ0ªHôÚ¶ƺ#§{nu±[u4N´2ۀܬŠãÓ‘/ÀÏúúµ_Qì(o†¯»ë9Šþ)sÓŠ€•tGò[ã–¹•ŽÕ)™Ð•î RN/>êö–céiðnuæœID.‡(x;0›£Ä›‰ òô©jæ…Ñ×âÙ±ã„)Ì:\ÊÔá*&Ðs®£CŽ>¸í¸¦sPìÚ~:¯¶Ĭ™½naUæ¿b\ø.%Ñ8nÔkZ§s-ÖõÅ ß3­v“6ŸþjšÛÌ®±&“ØC¨;1âHH÷kÝ < –GTi"ð+’P–<Ö¦—í:»·°°rêbáåÑÎ&—F¥EñÓÊ „E¤û?ó¡?I©ßìAájþ9‘øÕœÔ™ D9 <ê,ß8õÑ;2ä=d´®é-áýñão<8u {Ê(ýüŠlª|Ó}~̸mA®<'|›fF]ú©”‡- Ù©³ÿIÐЊøJßÏbgÒ]þåá7móôux62LØ'Rˬåd±Y;„XK‘Àü‘"uÿÖ#>$«£ 1+'ô#OT„ÎèI¥—Ð¥ZiÌò§$þ8 “Z— Ó£K‘M³~Kó¯ÍjÈ×éAmx—ѽýýoãóÀXÑAe°º €ü`|ZØ;ý¿)¤+¥£qÄ¿Û>ÃI-Ê-l úë!È4÷õl™ ÓºZíXIq£yB©HÑÀ“Ö÷½%ÞÌ1zà/ñ¡ò®–ŒÜ‹Û‹#¼n¢"ïðí«¬ôæ8N· ý®ÐW "jW`=r¡9ândbD–΢Ø]w‘Yí¬Ïh.’\¬„÷ÐDó{B ¬ib0q½»6Ws(Ŧ§´Ð.@DÆÐÞo¼…Q.(¶a8ž¸MŠDÇX—C’gs¡×Ö_q¢ËßÇ‘ÎRßê­SàO²†+F\–˜ì†öÕo¯ÉnSŒÃ%Ɉ2,a¬ 4’~P¾nΚ$ŒMÒ¬à‘d,¯Vu™¾›ã[õp_ W¡-ñf‹à…u˜"‚b…kÅÖ$ ˪±¾v5<Ö»aOÈVòB£"F>þܬÑãwC®\šqD‹›¾Ÿü7xxç‡ KúѼ˜ ož‚޽-¿½›—®žÕ£#{Ûô³QCwm˜™© .»þÑ žžºž±ØÚì­ÝµŸ±>‚-ÔIŸ~¶ó‡û$Y³#¹£Î3ãOêÉ y±´§‘gœð/TáôAg6öØ€í:½lXtò%ÌXß%ólèt¡-´ÈÕÜõÅñƤ­Òè74.h€ÝâpLk’PËMMÅf#_ƓӴ¢Æ<;³$…è k®-Yc–éè šl…c2h#6ÌÜ '#°¨ø8Ø°Ú ÖýÁŽéf^t†ù^·n1²—ûŸvfs„÷ ‚ur£sžÊ;hk½B¸Ý!+¢xø}èÕa' é}äOpÄQHp,‚^ýk‚=ˆ•:&2öUÜØüX«±yÈ¢`}£8ƒÔ(UéOÞ·y^­é‰h@Èø Ãcþõ“ ¼¬|YJgFÿ4äué½ñýÐõ½"_v¯mê×Ûm Yšº®€ý“Ïäþ$O·]cXß¿Hë`ú Níå#°aß<Ûà3¶4öžè¥!dóÌ"µˆÔ±dÑ8ñ1¨;MR@)€UÓªÁ×Î@VÊHdjýW`<€èÂ>Üöý¢úPõŒš´…²Ú§£Þò.CEì÷Î>$] ©`B«ï¶È1Zr\|ç â>è+oxŠ ½©µK ¬Â9¬#Î)ÿÜ#?º…†ÑÓM£iø»ÿ’®‹Ù‹‰¡þ°ådKO?ì¹@ßfƒÄ›6¾'6p`É—&ÜYx¬àÄ7‡³èùueÑÒ(OPdyƒHýWMjS”^áä=á W–/Ç¿º&~vŽÙ»&À»&º&ìÜ’önÈúС>µÌS‚önÙ6¿p˜ákI^IÅ´Lúxíâ2ûM¥Ï<”›^? ߦû]yÀ­ía¿yÞ¨óß2B¢3;,¿9éOÖß9ðd%·wY¡‹ÊÐG­?-x¨(,¤ÑT$©ªw/í Ð6 ÕOçÎTÀ|Ör`ýjXKU—˽<©d X)¥¡­¹´ÏX¸YgsÓºþN£åón/왪Ój•®¼Ý¾swFF¾AwÜΘ¯qÕw«Ÿ˜‹€(¸6}Ý|ä/RÅ9«KºkŠŸõ¤.Ö#÷w+]®Ö.WÏ:ywâúT>VØÓ¡Iÿâ4‹K[Të†4{à¬^¸ªW®â¯^¶_t_€ç¿þVÞwWâtntºÐZ”Þ?˜)J¾F‰—ýêêy£š1ð„\¼lGØPoö/ÝʉGjAß8J#Fª,Z1_V±P¦Z á[/æATHs¹ú{·Õ±¿X˜Àè˜ÙǵÊ+2DzªïOOôŸvmª´>û“}ßeòž ¶zbsƒå‘Ú¸;to3Ý‚<‘ÞåóÖ‹sÔñŠb*Ôc…â,»í¡ ƒÒú­GÚ ËÛ&S-ÎTjÂE’•Å÷©Õ ‰ =-ýDþmº•H|ä¤MJ"Ûéjfˆáð&c«hÄ m*b?N)D”ZÒ×q‘ž^d X{HxÖ*Óòƒ\Ÿ™‘²Tà SÉ·á*íè}‰¦ÊäTse ÇʤoÃn]$©£2§ü#öwŸÐËÛ¼£ŒM;nÔéî?åÞ&~Ž…âÊ]\^x‡±Š°~@X…ƒå*žË+$€›õe¼”«ŽŠ Š9YµÌlè†Á]'æ_ШUÒ ÏU)ôMÂõ©!%U›ŽÕöÅÑäYï4uañpWÛ]nêÙšÞá·l«´ñp–=ÒqúÎä¬Áø!µÈ~´êÍ~8ÄaþwŠ$³øžÆÓq®jMKADŽ«d]ƒW(žRÒŠ.*)nàöÁ»WBu®i9è.@þÞ6Ÿ„ö:ÎzÌM<˜í{ ÉDƒ+‘:Ž=‹Å 2ŒF~†¤ º)a1î&,€þ-n´º7’ð5ÔšÜnvh€g…"Âaë{à ×{;ôçóþ7c¤$7w³ùa²€`™áÐ’MÊÕ•Aô¶ùe©oAÚï3Ë8âÎ--Æ€‘v÷sïÌxB0‡Å©¦3#Á˳(ó­Åßþ–rêcÝŠŒ޽H üê*LËsq“\ëãKAÞÅ¢†ø "‘hà»ïƼ¡Ýô¼ŒGG³ÖDЕ¤Ì$œq¼ik1½#5§ÁòU¢,÷VÑ™ƒg¯ºÆôÒ–U561ñ…æï²Cñ5ÄÞ3@–©ìLTž‚Z¯_h·ÝÚŒL’˜¸+¥ ƒÒ^š~ʹ¿ ›£–Uæ¸M¤èa#ýÇ47Z-”‘qn,i*„±Ne$ïôq0=¦lìhª:Ó®®kÉx|3P ¤Ogrà¶²íE¿ûÅjI–\ïieVoÛ„;çúô¤‚VvdFZµצî(2b¾YpÍ›(:“}Ùñ~ö ¼>ÆN'pøÁI+èÚS…_‡†Nš'‚ÝÀ2Idw{C3ºŸÉGʇFêå0ëÅ6k¨iä¢íA€äÛ°°¯ãh.Höܳ—õeæUö¾ª(In40,1è7Œ 5€µF²'f-èiUšHnß š*ã~f>Ònr£Úy€Ú§ 4H¬Pó÷¨Z¡lÇÒ\¸_xÜy¬3™^¦Ò–kdø}òÙ!£™ø¡Â@rhê•^Ì›z§‚™b4bú’>/)³’!]¼×â{@SØØ4šÓ¨´ø\À\d5ôŸöäx·]÷OçXV¢’ý¿{½äñ1)M‡mwG*àІHôV_W+ÈÒÝX™øßôk?œ+` _>€߉–ó’M\w]û¸•(2OÄnv“‹e©µ;¬Šv¯:Yì”´CïoBäðc2i˜o®‘U›úÛuùQ÷ß Ñ7º¢ Ê< §H˜%Ç‹8Ë|weXiÈ¿šµpgO %eÁ ?¶Eqæø\Ä ˆåIáˆ.„€ õö=l„äl“,tôISxo*Ë(Ø.Q«]!Ô¡vŽ–@¹eYPk^ƃã¿ål*gä «2Sþ¢ÐAü–XMÕÎÉHíÏîÊÑräw>ÎwVü„åD*/7E>1¥-׉´ÚeÁIÏØ•>ìåzàêÔ"B˜%u=g#F½Øqƒº· ^i•S©¨9ʼùõ{ÎR‡ #Šlóõ‹øÃãc€\”iÖ·çf»½S…=–»L’Z í3êHïMÉ›üT¸BuŠr€¬®fY_×ÿGÆw<Óê©^‘â¿Ì•`­º ±~`RsЇ™“¥®šþEêЉ­7b(z Ћu¥T‰Úªøí|ívOÁy~!eÏ1ø‚â ¬Æ+Ú[3¢'xÃ:v¬g^[ß`Y­#®·5Ç«áw–ÞÝEŠÞ¢Ê¸â ¡EE—òén„"k›2B")/Ä*{^ T;ü}õ6½%R I–ÅÞu4…L||b±ññ‘SSœe³k7Ïfk€;!4²Âb/y—*³cB¤{å…'² ÷øCƒO¹…•ýµ{W”O¨q†œXŽU¤)ëÈ’mÉå¤nRP÷ù¤&eQäÍ!-õpÖQ­­çÔEudxOŽnÝߥ ÿ¤öäx,É:¿z7Ü!r>hïé{–gZ?;w6e•qkB&Û,óhqniæF’>uÔ9RÏ¢·÷䣼ç’F¾¬? ìh‹µ[ŸÌ:o$cêuR%ñŒŒØ¿¿¡÷2T»éÞwÂS¹ÛÈk‰w=åçVfÊ|üçÄŸ¾ÙRehÆE0îÐDosBÆáLštG]:)§åŸú‹ K:ø¦†N~ ˜þ‘ 1dÉ ?]O ¾»³ûÜÏ9!OK8ä¸súrªOÖ`Ë &Ç›#5è)™ðƒÊœXRoJÏ¢½‡gÞ9ãx¦hëkú¥fn#ÃÞÝy‡Ž ÷¾Å{éÇgb;ËÀÙKy7© ¶ÕL*é-¸±(ÞAa—CÌÅçü–D\km¯|ÇŽ'ÔÜ/¯UZäkŸÀ爪§•Ú½8ŽHÓþ¢ðß|o–ä3œÊݦ|1‚“úØ•¬q½<ù‰ -]¬þ€2o=:PÖRà!v͟ǃû£Ÿ[:U à߀sØÐȸW\…&¼Výü„ÉþÖÍõ¼ÖL|´1WY“‰T=MNYC‰ݨ4·Œí1øVÀèÑzÃ.1Äñ‡`ïâ~C¨N%+M>‚WR|С¶ º f¡JÊ©#ܹuÝÏ.x«É†Yp®XFdIÓ¬jcÿ=Ø7—ÏBþéÃÔú®)–SU ±£®'÷ø#ñ>У¿‘Þ5Túꦤbmq¶hÑIí ‹ÁŽ#m;­$þfLÎ¥5á½ljàVóD}½$’!ÂwüŸyeNŸ)bdNVŠÿ¹ì5ºÑ’rÂVsí…õ‡¿'ä#YH¬Qð4×:u‡è•^`;zÀ €ÄCÔÇÊ7^*zfY€meßÞ‰ŽÊÎŒ|L§iœäJQ6AûÔ0Nª¬~¤6eŒ¢i”!ÝRvìuêÊ"ý—‘1ITüÙx÷µë4wÄ}ÖºøÇÇ÷xåÆ÷¥!#Ìüì-*wßA,ñ‡rÑÖMm(wð–l¯X˜æeQ 삹€#°€ä%´MA›m°0ÙSmÞƒ¸ÁBœ­°€ƒìe$±æpë)r­sà <˜0#ÑÀN™©dh'ÙŠ2Ûl®‹ê¥F)ç¤|Z¿ÞE'©/šÎ2€=ˆƒêÅ,´‡rھna°XŽ<˜ršÌ!•FÙð†ÖÖ7rÓ7vcçôµ‡|Úün´Ù@%yñ5ØMV­¡P&£'ö!EØ»í¥•Î<˜y¼Èž¸èìAlëÅLéƒþU5Ý#ÁÒòå+é$¬Ž+ˆ%3³E;–)U@#5Ä`ê½÷4D9i¡ºßò“é²çDK—’ÜÜ4Ÿ¶3J±Á"h4ä$´Wr€š‡zI»g¥ák®pÍ&0‘jÄÛö†{•pÞzF¡¥ûånpr{)øÌÞ¿©ššÊ?I[sœšôFá%>¥©§íÝàܹÞ,8T|¦G“÷»n¾ÑQæXŽß£ÎÙ2 ÈFÖÔÀÂcÑ}SŽlS~|€ º^»RM1æÑò«Ôezvç.éMm¾ðX6ôÀ6RÂAøX_~•oÑ`†kÓQ@Ó%9Áœ%Z„I£×¦Ýt²¡âˆd­Ø¸×Šëä°×d?аóú´ý·úܲ"rµ¯¿ð^z£¥m‰3ßÅZ·ñ¼-Rh{¾Âc¶PJ‰®9&½ LÄýè³f¬"ù›û6.Éß„ý¡Ø¡¼=Ο­£f‚ˆ~·»E«âF z£ À­6Ôoá ü¹Ò¡X«¡«£^è×¶2yí]wyò»BýSŮ׳Ù÷iûwÙò»‚ÿZ÷KQy«Ø'úÂË]÷B¾)æóYKóR¥A=·Â+áXöíÊ…ÿ ¨AL}†Y …™¦VùáM¡fÖf³fÛš•œ¡ó“.«o`Á†Õ1…£iÀ¬GËÇÅ?ez Ë¨çiÒ oFÿLw‡" ?õ…ަA¡ÒEÛhTÞO'_¾ÄÈ¿y+:ï¤ùë g8«§­p;0;J5(IgeþH ÇÛG~#pp`åú|ðw[Ÿ»äM¹g"ÄQ%Cý‘c·Ç”¶·Qñ6óU}„c@]<8›ÔòX?¢ðIÑ|¢mbÖR¥^xj;—ÙRËJ­Ü´ÝdZb6#rÊ[uÑÛš’k¥uÐâ ^âøÖ{M¦ÖÜÁðô§ùLqÓQÛÅ»æÒvÖyûª½¦£»ÖÆ'^Ù |ûó·ôž—+Z½Ã­õÆ•«ÕþÛmx+&;$1ZBe»³Å6¸YpB‹>ÉÛ/╾MãNDéŽà.]3ß— àiFBaKq( HGæ\]Æð&~©aob\Æ?íÌ=3˜‚½'ó™„ f$§“ µèèƒxãŠK#N~íï{ ŒhÎ)¯šâÔl=Eq8êáLð;‘ÑOH?ŒœÒ!Ó!>€¼BѰ±¸¨à;Èy»{@–,!ä\Hvà Á!64䯖K™à$,Ø/í‰ì¬âùt!ÿFx;zrº8[\•¾­èF/2>g¿Ý"Ósb¶Y~¹P¬´iÌnøW€Na[ú@z™wT ¸ðñýj/FáÞ\ø½-²Pþš½ëè\ƺo¸#¢8H.[#Ö§nÜnàÈÍ܃ÖvÑvÀH¸©#[»Ù„ãXº°ü²­†ads…1— £K7¦ ×£² ÑjãÛÑWÊ´ý"c€vùæß™Â³Y¨ÓóS›ðñ®)YWm4eJ.§'Úy†òâ$®‰6¦äC$Éš‘ѦóulÛæW,n|öchgÎnº@ܦôøø[Jã`дïòÒ¹£cè#sXšx.‚ؼ °Ž§éVA5àü.7ùøc€C .½¥üDéÂ9q݆eXõÒ .5;»‚­*TæáMÌ¡pß ÿb̰V^](ÁsX~([fdƒ… M-+-bº/1Ð31œŠu5›œ¦€Ëü=¤‘f‘ËDƒÃeî Ú¤é[pÑ·àš¢OÞ5Z‚{–>¹g>9g}RöÚ¤ìݤQÉNndìÚ•˜DÕÈb0Âê.jñ tå(uä´äƒµJ"Ìuïm³¦pâž æ*wo›iS¶ý‚v ¶Î%ù®Uš¸Ù8ä•M4Ç0ÎP£"r/f„ÍüœuÏÖ€—¼©ÐÁÜͰÁo³øPÀ·b}O};B×*ÔH¥!È¡ÉN|lZf ¨LàÂêI¼ž•6Ìp‚h´g·£G-µš–xª•Ÿ`À³Y…Ä-¡šðã>10ø—<šc &OåóÌ¡& xaÎXË…–³d: ÖtL£SÚú‚ÃúÍðÂùg„‰—úÙÛ~23*"–™UP"Õ ©q[Byùù$Þ5Ö[­÷ö'*üÆ« ü>q&mL+A L?Á&±€ˆÊXß|(FÚú”hH\ãK(?0/LZY\H ¯ËÊUlêW›¢,R^šudÑáÑ# ê.­;‹r’'c¤hž’«&wêDÝ¿¨EC’á ÍL漞¡JÈÁ…¦É‹8Æ:CHÍøð*×ÌDzÍq†UÕvgUØðP™¿%Ú‹¾Á§ë´VÒl6 ¼# ­±‡´ñ ÖqˆGpTFÍ‹nח¢ÀÕõ0³ Ói…ÀSqö Dz´Èñ=€ù¨Ý0‡‡ÐôBí{ƒS µÍ÷‰¨«á a†ëLPë@ƒËýXCžÈÔÞc‚ßóúö1ú”3íd’ÊÌö ¦µ"FR‘ÙÃÖÚ‰R¢†Ú,T^ÈÞWRšÔià>ë‡Ü9¡ž?à+—±"TƒLèË_!•X;˜>/a—Iø•Ùš4MNùtÝøÕ¹/óÕóü·»HXÜ-š——“cËpÚ“Õ•Ø´ZqónÞ(­B[VÑÕGd"äd4*-m†ªKð´CźÓU5Ïà!ßC—Àx7Ù÷ ¶µ;ƒßÃê´›…ϺÞLÈs.[IkªIf"„ŒºâÉúÙ%n]þ’cD51¤«Hx‚©ª† dû ª¢ÉÉÒe)¤ ÑçhRñÓ¢<¯L>|©=Þôz”ç[!–¾Ìz3W]š TÍs 1Æ~Òoî[Õ¸FÆmÑ÷÷LΘ«¥GTt½UË[½Ì%Ù#ØL‡4»ý^`Ï×Ñt’3‹"½þµ€ *în¤¦áéúðãÕG‹/ŒGP7Œ‰yÃ$nF X0SY7DÁÇЈGÍ›SR©˜Ü𣬤de¤¡…aŠÅ—GÌþ¡ûÁ0YÅ‘iD2Ï׿Àô!j€„@vÅâêJòä/ÉåÆb„QK€ºÀöå·Î ‚. ãm…Hrÿ(cÎ>‹Z‹ø‰×ç1Nœa Pjæâw8žÏ;ëKÊ!ŒŠÛŸ¡¹TCMÑPý”/¬E°ò#ŒÒ‚ ”YPQ?ð©ÕÍúV—ÆÙk‰Öƒy´¦ö¿*àÒ=ö VÒ¹IO õ(DS€,i˜À»µí0{”!‡IW®Î´ª ʳNœ¯eoWUŠÓ¾Æ\пn¼r‹DyzñúôýM£òí|vÈ^¼V×Ú[ïj«T¼·ï%ú=½sTÙëFß>¯šóåæD‹TsœË½bxc»Dj–±6‚ÿÙê¬A¥ä8F°(˜¸ÉWLëQЙkj\9r òoÛiS¡²UŽ8RZREE•X PŸà_> ê®æ`“nH vˆÎÝ´ÔseˆéŸ–¢‚D{X¤ý2ssº üÃ8lº! tóö€Ëß4p]1cË9ç¬êèn®= gX†‰}yÊwM@Šz8Ë <Í@f×sd\0†à,©Àš1UÕ4Åá¤hÏ óWviOâó ÌYÄL3Æ(¡Æõ'b¦#HÁü˜…Vm^Á™dH•6úQ‰ _ÿˆ @“ v_X ø÷R£hŠÀ탴Òƒ)ƒ³ÃD`§®.겜n’Ü“ ôIRÏ%î)DÐç¼Í§È¤×/›`„™²ñŠ*” ð¤¶ußMõÚP'Ñ¯ÏÆ ’K"ƒF«˜ß·Ý³¸õ)?ªS’¥ç©®‹UuçŒÿõÒ¯M\ãµ<7(­$%Á:Èë ͱLZ±·ðÖLœJ\š„\_$Teùþ#Ãú Æ+ ¶ a“AEÔTDà²Ô¹dpµÈk(Sºg”0AÕ¶¬Â½}0hÉô·$+¦gH?G¸]l*Á¶»Ùþƒ†ú.ÏDUq(LU‘*à¶d€ÃG ˜„IŽx|2Ú$¤»j%õÂ@“–ª:·²Sv!QqX=чVŽôúÍ7l 1ØÓäƒ[ô>÷ˆ¨’%l‚;¼ñ„4Po!ÔUG’^ôæ„ú°Â ðPƒåZ;Ðmvèçü "ì‘xô,ÞdþrÆ;S}Íy÷;@$G¶¦4£‹(cˆÇ1—Øó¥+U•ôä¹…·ÐdwÀÎËQŒˆ¯Dã¸~ðÉ—Ó˜ÑøÄË!¤Ë!ôy âmRìaLnÜ_ì¾I˜?cËbFŸF·Ö† Ð ;¤ã[-ö…–ÛŽM7þ‘kœNÊ+˨W§Èä}n}–t#ja'—ž´sáó ‡,fù­9ÿhI™\û®ùF%JÈ? ÍL5XÁ1$,)£< ðpê­SO"Â1ÈŸ`°ƒ¬÷B3ÝÚ¹îMÂÍRœOJJ߯¬&…SÍæ9þNÝ¥òŠP‹EA ÛÕÇ›20NTtúg ÛˆUݰŽô4默n.a² ñÓÊ…³ÈµXú¸êOŒö]×ÎÃgÙæ™¬ð|"q$ƒËXý³AIÒ™˜Y8Á%lnfQ„x©okͯ› àE6³ë¸›aM §~ª6(„LQç$é:üþ\)‚1BkU•… q­c$¥•˜LÒºó‚âóèÇGuÀš²ªF jºÓ)^Ç‘vù/Ÿòâᯞó>kú›7BR@ À¸šÍÝ6Ú±e{¥]ÄL{£§ˆ8QçÖhª‚ø\ÝÕ7 pIï 6ÑMÖ°èlù]é²¥¤IÖ´ŸÊIžé’Ä;ƒDÊéN#<؈}4?‚xÝø=ïjCèR˜Ûá7ÁP™1½FSÐý­³Õ ºN¾I|){w›-¸ãƒ Ô«K5çw¹÷h`ù¦yë±³ËbÐCõ÷›cñzÐ1ÇpÎü0‹5Á-»{tˆí¦Ãà1–y=ÿÃtÅb[JvEf¨0­¹ÂïéÊ"ÔG§ct0œcö+ƒáº„ìp ehòñÚ„YžgéܳÅÂ/ý36ÀP|åÑZ㥠éלkì « n'7$ö”ûÞ3Å,hbÛ0ºéV3­öœêâƒÓAHø,Euíª\ž–wŒÕïƒâ‡È|Gw/¶ºvI½ÍÇ  y¦‡¼K÷7»3}-çáæÓžÞFÙ^;©W ~˜C7ß#q`£«¾Å‘ få> i¾›q"Ñ3˜%WD¢·çXUÕ_D¿Þ}fýjâH91ºDqõâ>Dü‰É:~Þt `¹µÓ(y¢™XÊçj¥Ïo¡H=%MüøÒsÛ¹”ßìÿÛhÃmWXJ)OŠ8„øm6&ÎÆÎzÿ«âÐÄQÏÆÂÌÑÀÙÂÎVÏÅÙšÎÞ£FUÁ©ˆ Á÷­nÞ}–´CÚ‹]f >[¨všs=(ÇøqxœÀÈ@AbØÁÏGMœƒ¬aÉY“#@uÏ·ëË¢„¹Ý¤b?±õÝ5p0Ye†ù³fyßÚ Y÷dŠŠŽžŽnš\{YaÄ}KVC’Z‚Ù¼2JÊWföÆTÔ^.DØ©ý™®,6Ç1´p0–]>WY[€˜RÒJU>޲µJQw”XEÒ`j¹ÔÅH€'ؽÁ»B¹¹îÁÆÇ|X¸Ô»ä²êééÆõ*!µzLÄWCȃ–\ ÂWpð&±«2_10ßMZ¿¼ 7ë£Ø8„º”Îÿ8ð0û0óN•a¤Ò„ËL‹ˆ¢ÐXšj@5æ’´Ê(ŒK¿S4¶Ïßþ ŸÝ‘N4üÎ/o‡ö` Έ_3tÈ”QÚtÁ¥Æ ±wÇŸw0Ž¡pD¹ûSz_‚)G4ó"›–âQÝIH|•ÅÄ*ÕP[e7>[p6¯¼œ§¼‚"cy$Yœ^(90EºÅ2Þ.èqóz„Ìé”»ÅÂäþ$I¶ +Æ0¯'JΜóŸ£6ž#?‰fVH*ÙÂä³<iݽBM€øüœ­{.1% [Ñ€>­»Çd&ß´s¸Fj3¾húmþ9¡bë¦|©Bu<^hõsùÐâ ^á1at¸Ò있GãÖ(Ý[øSÄ÷–>HRǪ]xc#<Ã"¥ ¬]¦4 Ði uî£aü6˜ÔߦÞçO.­YÒF³H#ÇNæìêBøŽXs …BHÉr,(©Š|¢ÅýNOoÿÖ}jùüˆ7ýž#²lPOß×T Aʲà ¨— x]iÉ`¹„¿_Y=ËÈ[À©^ß‹¾èÉŠžÚ|eõ) ¶‘¡äzÀíTyŸßÇJ×÷‰Ù"y‘ÕöŠ„ÅÒF!©07I…äsé°c×PUÀ­„W:p`Žmô×Àñüñ¾å„§ŠÄ€ªÄh07Î@ßÌfý y¹¥p‹˜™b›öGTA+*?f¢sÇrdbîYƒåŒN™ 4‹‹\ÛÀ:3~½ÐzÛf$‚djÖŒkˆ±Ôo1÷|UÊE…‹Ü§JçÈŽè<4¬«í,½<‰Ñù6QˆfM³Í^<¥Y^'žŒŒ…Ó§Ìg”‹àø{–ìÉںǢ²}½€jj€a45r‡daDz³zaú¦æ'\×ûõ„lø/p{kNGd—z3¬r> ïaW—ž@ä§¶¼·ÐïÄçu íä_×O 5ÑŒÂì*=ót¶]p™ë½Ð*óĺء©"i5^ž¥±¹:Û(lLc7ˇÈYº¹ÔèÌ©àŸqRég(Ã-O_kKnŽyöåÞ/€Ë[ÞóÝ>Á•OÛÙÎûÊÁ*ûáËûÁõŽz®–u/[}ƒÏëY?¦ï¶¢øæÄY:|ñºoÝ}3o)/½ËâQð›p·Ö–èýCéﲔÚZL§Í=êñbåˆ4Ì/دÒN<Å@"aÿÿ@:£Õ[ìÿÜó5Ã1ÙÅb êepƒˆÕÐJ’y¢`‰*¢†š‰à¿}ç^­.³0—.8XK’â{(غA­{æ·ƒaŠ~½Žá«œ‡ëÛëo_f§~«t?F›TÎwo`ün¸o§SF¶éÓl'ëL³!ïJÊøÒñP‘Åãt5Ã_(ìZ¿X5L1›\“Sµõz¤)ÐÓƾÉP•Óµõz0ÍuƒNR¶m}8š=c8š¾¡l&l[ƲA_PöÖŒ&Ѥ®éŠEã0NÖÌk&³²·ù*_ào·¿À2¥1î &˜Ô£ôGi0M»çM.Ë…ËYcc v˜KÚ6ƒ÷Ú%Þ›ô²°Â»¨¤Vx¯6T ͺ­œ€ Ü6Ö\Çû„r€¦_ƒ¶"v7š–õÍK‘¸›y¥®|®ðê3ƒâoDoÓ‹ŒI½›½¿KÑ,ø(Û VA5–Ó¯bPðßÖwS£Ksy¬‰vƒp¼sG@÷ÈÏÉ ”%OÖšúiù”S8]ã¾T nÑ:†Õöe½U÷‰‹ ï™Ÿ°2¥ØóÃpA ºA–KNb¥G¢ŸJñ3E¯W«ð§^óžXÍÓ$–À —Ï^:Œ ð› Ý\<–cÔ„\–מL±œ¡cr†¦šîfS«džòc[ ù¾Á,n¼eœŽÚ%عØÓ{|_#¹oÕ”äI¥/O<‰Å-dþVKp>ü0™M Á¦®‰ ÑÒ{9ÃýYN7ž1¿ñßLi°èx"гçqTòÛøöYäs%QüòˆDÓ„Õà‡ñFpÏò¸/еŨOŒpÄÄ'²y³lÎæÙ ÒÅÏt¢§_Nĉ¾î§`I')¯¨ªûçݫ朡¸~;%³ídˆ*Î鈄ՠsÐ}3_Úɬl·–(.³ ë4)‰}Wœ O-jï‰áÈ0—(¬rˆPÎøšcÛÓ fº&8/Y£ðA«æ‡EÅì˜ë•˜¡*ÔóRÖ´ÊuJ>Ý("é‘äipŒ„uOâ?ðÐèaò¹ÒUjàz8´°ùK®§8U i Ãi 7Ëe !M5j>ºg˜¨Ã¢8 ô%œ…¢J_([ä#" ‘2äf"iˆáC>ÐôHJ"«÷—´aëZS†iXÐ]ßÓêöåéŠðû\D‹z‹éªÌS¤Íö¸5ùl¿4*òwoD?”RÈ]Á–ºƒ® °Ì†o}.ÌœJàÑ¥×ÝÈšˆtwÝ ¼QÐí¾÷ £jç§0Ø«j¬H’ج״h¯©ùžÿ+ùÄâUt°´²;qý Û›I{L'd8 qlЦ³û†ýïòŦìß t0€zdÊÿLÛ¹ššØºþŽKÔ·l·D|ÎÔ`ÎÑ!̹áöA•µlƒ9 ¸Ø V—7S»¤&,üK©W‚\26˜Íj¹Ÿ!½Q 5EÁBýÎ"‡jÄŸÈ1è<,³‘Ï@j[U ;˜î®¿•„~$+¥©Lƒ¥kk <*?€PÓQà FQëH€ÛÏ$‹¸¼ïgóÅ l)¿ø UOãëi‹ÿÕr BÀ&„sÞ—Å› CH7ª×³×#ššÌ$à zÓ”ÏÈóÙ)(Á«`ö¨Ž£—•eÍŒ¥V“¯<²Ð¡¼\ܼ[²<ˆÝ%Ôó>ÒÆt¬æ¿ëuÈŸ_¡©‡z ¾†ÓµVøÓ¢Še$ »#^8 4Á*á(  b“â9-A©ŽÀùØRé·«CÝ·5Ü Ò ù]Ã#x¿y|ÞÀá…>}Ø;²ã}0ä ­8;òöbË‘¿ß€…&ÝÝ›ðtàŒÉ`þ&SÆ‘€†$ÐS˜òÑ%Ð@\D‡!~¦Ð9µ×'[Õ*:ƒ3Õ"Pf`]Ð"¢E0´b=K•ñ`÷x ²Á6Õhl0½¿Çʱ‘âu¸½Â±ê¬¹”_WÓAqN¸<ç–YÝQF*2HO´| ›^ËÕØÒ¡ ‰{4œ+ƒƒqXcG-kËÛSj¥Á›áÏÍQ£Ò|¯)?S±ùªÀbzBg¸Gˆƒö/ÉûxßPEWüa´ä…µäŽ/G8Fç&׋uœ£X6"9—'D‹IN ‰mÏP̨òO¦•ð^jc3©:?ÜãªîcþÓˆZl«ŠÖÇ,F =¤5ãšyú!oÇòµ‚hFžnî‘ ÕõçÁÖ‰>;q‚²¬Ê 6·‰KŸ¨îPªæÈ3ÉÄÏ^t\ñÀÀçÉT~ˆo˱‹ Û6»L¹dƒÆ¨7©Ó¤¾ÝO õ\ëq°Ä ƒ¼CÇøGH{<`÷  ¯ƒÔ(¿nc‡ ´8•‚8ßýr~­½ÔG ›z‚ì¸áGc¾þ8ãÆ,ºÈñý)eó¼qšapn9H†²jÒ’-Š»:çæÌ‚Ò¼˜B/xbëßdœ[à‰[Æú#-íu„Ò©õõ>{}ñF9éréÂIîܬ}йs”à•œò¾©6Š8¹²(GÄ·%nÓ¤‹ ZB?@)‚‡„ 숷œ/C4Zä6~ר±Êö;v/ÒÈžÌ5רþ¾“ïY&ÿ½~IQF2‘)§µÏª×Ow°ÜÔÈ*¶–xº¤fsÅ…èÛ!U%/Y™‚³ÁZ½íˆQ÷F±ª@ƒÎ¤b ‘XWE^¢tx°ögáHB 栱ѲkÕ¨C3n©nÞ:–¥"{ec–+ÌÅ…¥¨þ,üºƒIÕ cøsoBîÊטk½—œÔ[µy7À¼åŠž\¼æˆê rÏJÝ,5‰Ò†FAˆXâÇ!µc—dMunïÑ]A~EËuì÷dA¶soؤRþab÷ðRLžŽ‘Rßt ÷hZÍTÊ—&¨ŸŽLFªØÌ«lû÷Û°èÝÒœÂDaÊY PËäº n2l ÛŠÄgü{êÔvuh­ra^W“å+#Úœ:±¬‰îÙìõ´šáZ‚=Ÿ£ÈÍ×´òg€“7—Þ£ƒÚ“÷æ jœ/¼'ÊÈiòÅÏÓ¨X¸è_™q5A­U8ð÷{ïdÊÈ0«ÙèÃÖ«£Šâ –£;™_×r¯[Æ5]âÛ@‘ïÔ¼.¶·Oɽ~9@Z# RËÞºÝ;ªÁ³ò=þÀ5s­èÿBÉÛ|”¥U7]6U=€ÞÃ⥹ñ?¨Ü:Mcœ*œ“M°.¸÷Ç´‚òJ¯’Žø¤ò„tjZòµ‘^ÑÅé²í ¼÷f 7÷ŽÕzö<>Ï—p“W¿bÿÌo¨uÆ3⺠v g¤8GÝ>OÓü£ÞæŽnûIlåWžÁ0adnÆW?_]80ÉW~“º1Bì9}5r›»Cÿ‡ºgDóÍqWü6Ý{⾆®ó¾R wýc¶á€®P¶b»öö~ç0»jJ…@õÃ.F-jzNGâˆv ¸Ü®‡¶l^]æE“(m¯¶¨."¸("ãdõ.ñÅÇFmö¨#XÜŽ$1rÌ’ÙOSeûsCÈq(zAð;´©Jû-Õ ÒUf¥^Ÿk~ÊŸfM¿m}|_;zwyû5¿1à ësÁýþ o2™} CŠ»zA˜¥¨ªŽ’¨¥W 5õ£ÉH .ìúí€`1kæÿÖƒ ˜q¢er@—Ð`´x*`Ø‚m0Z ·à‚»¶Cê‡àd‡nðüÕB„îW ÚÁuaƒùêà ñGÛ‡Áv¿=ßÓÙ0±‹ä*ð'7¸§bx¬NG%Õ-¿µ…Õz²êG`j4hê¼O¦ÆãÈV¯(ŸÀÁVÇquÂ;ÿè¿sœaµ¥Ù8£JR‰1qF¢•±Ø"Ò¸:CaíNÝ‹«8°¾ÎÀŸôêÎpŠP7ŽMîB>éøè›s}9œUæ _ÚÉ$ðGn‹Ö}iŠä…;ÿùnçñG'ncŒ×ûF_NZýò† ‰Ú26þâgK£f² í;àÊò7Þ•OÁºÏþðaÃÓ æK2È/[Çjñ!æ;÷œZêe÷pü |ž[µ&ã…EÉš/ .(æWºCÉвÇçäeÄ!öEqœrßÖøß¼K¼ðIÙçì|ï;ž>Þ¾ ™.šð¤]ÈÊ )Š~išÀr6\^ccŽ9Åÿ’ÙúÍ1h–.VÞ):5ÝA1ú i¬AêÉÿƒ´ DrYéë˹H®7Þ›A¯}Ñ€ªÓˆ V¨,¨ï73â¥H ȧ ‰29ÿD‹zfcZ`Û &‹E4ðæ°Žst+Mp«üÛV(KJVœãÆæ” Ú´` X¥oxi«ŸÉ{LaS¥-þìÔ]¨êÌo¾ˆwÁÍb·‡pEå{ÿ'KFdZÖŽôСès H…ÛaÀ%žî1? 6;­r©9ŠWÇ!(—D¸¹jRïƒJ.È_h~Jýƃ«ˆ›uÝ#`&±|ü­Ø­Ô™ãÀàô£~çam‹g–Q‚+šAwÖ›}˜Æ¦L„Ñ”]^-÷í /ÍÍ‹ÿ±é»´é…éß]ªÐåÄèìz_{<ÿÁ­ÈÒ_#‘ßA‹¨ª˜µ[›€=»ÖÁG?àµÔN›dûÄ$rÿF†1¬Pz/ƒC検^$xÉ99­@i¯œQ$ÙÉQFÖ§ô>—Ï)§ÿN&/‚a^¡iðÊ5§[–Ÿ[×'à3@”–îÏ»FñȪöqÉ©‡Û=C´·Jµfü¼JÕ“°Ô´ª»Å2…5¼‡±žg«8û°io$b×ÝB ÈØÖÔkûÄÅâriÏê«ûsÚfŸ%éoy‹ÆÅÙ DÀætäÔ²pA7½à¶àÀn }hôûI?ºeý‚©eïôD»ýƒa«ÕmzÕ»£åæVÃ53j±þñYñÁ&4¥à“ë_öf@Žsα9½v÷0ÈÇñn¼ïÈ]8+—?ç7ÿƒF¸=AÌð3ËÑ¡¤¸Ö€§ÑÐ_æ'RJ1fæì­“+ÄTBòü²k¨uNKãxKš@<¶ÀtãhY6_lJú9{©E¸‰5²ô)«INsøó/ÚœûåØ×Ó€Ld”¨ôÍÒkCϱœž'ÊJÙ’´(Ú˜å•\bÚS#w¾½ùÞsUƒ@›Ë»Æžœ”&‡å`SFh›ê%!2M ¢¦÷­Ïõ9ÿþÅ)ú’ÜYófpn®nž :×[~ßÂø¸ÉŠãå>RÀäZÍâ¿<*û `ˆïò4N n+ód㎆þ{!í7in›F§ˆ(wÄí…Ó¡3þf)ØWü:  ’·œ·pöëzèj²ùö@ñë ¤$OÁ’‹ ©FÐBBo–ç‚oZjµ |vb€>.{`cà ¡$FD+ÀXé³þ‚ÚK7ëùLˆ°•]ìuÿå"bgy¿[Œ-˜íÂYòæ•è"sŠ˜Ç&q{wÆbÌ6?X¦%xgBJÈ 9ÁŒŸ€rë„2yúæçÄ6á”Õ°.“T—ªFbPPaúNÙóÜ#D lˆƒøÍ¾~s_B•Ãq4š%—‘‡î’ÚÍ ÕÈØœX-QÓWAR9 ¸VÒ4ÃÄÃŽ©&IªÝgo+NQj]™ë;~õ;R•N-:rƒ[ÔÉ=Zµ"ÀExi›Yó"]! ¶I”G4¬Ž<ê÷×ð5wÁd¸˜zDlìÀÉ9[ÞSâßü‚e¬ÑRÈTóòë_-SF_ÊAµb%FËÌ-yóÕþ æCñ{@F ú²HáÏ?»’ûK¦á¹Šñ1r¿êù$åq‹c·à‹’ÁçF¤¶Á|bÉKÎÕ0³ð¬}†R|™?ƒ‡@ Éõï…ø¦ªJ±=ŒjÌ0‰ 73Ñß:hy©žd4ÆÀÕ&=ŧÀL_ÿ§?ž§F"~É#(òHk­ËôZ6]Ún®Ñ6™a®ÂÕ_öÿŠIò_­gÿФÿ3*w4qr6°·ø—·`Y9r@ ¿€AÈ*ÒÊWD%¹fKüêH‡*%Jü$9ÅœÉ ðŸNzÁ¨~¨hØç—>[f àX]Ñ}Rç§ CÜèÓÂŒL©9¦æŒX#ÏO4ØÑÞ<¿Ø½ð?•¡Ý¡‚OðÒŠ5¢‡93Œõ‡ŒBL$t: èdžWušO´ˆ0Ĉ€)›éHëOQrÀ•ÑìãB¢^Kê6H>;1AqøÏ¶,€âŸåk.|ǘ c!@){á\ZxðÔ…æÿâu¤r·Äÿ^Âëî5`"Z°¿6£\éÜ¥û5ÜP)ov]½@ jAãP^LÔÔJÄ&ZY[,,ßlk²¶dôD›>œÝ{ófû™nŽ=xܻ튖œ=ùsÉ«ZÜL¯KA\U"qNC]ks¼<ȹ¸¹|`Ðü|qPíL×´ÜÌ>AÜCР'Ë5i2ß«Z3f}Œ›“kó Zoǯâ8‰Iå·ô£[¨ÚåbkâÒ•?Í×(”(ƒSí!§Ýžª }Þxì§F‚¾ hȇ„üiÆÐ¸SöêÍpÿ½ÁW$„ Søì÷·´È6¡9>uI¶(ú>†dàÀß¹ «šµÊ Ð dTRØ?“˜£+k/†ºï×BŠ•@4 ®Ä(òµÄ,†iºU…a…Lš™f,ÆføÐ¶ø&Æ:‘-†}‘0¯¸‚\ø/,ký»Ä™!×#s‹ç߈Œ ‡‰×ÆÔA„ÀL4ñ_º7„ ë‚hÌ;ïNhqL˜/Gù5%RŽxWãÈÿ­MîQ> d&Ô6%¡RY+د¡ŠÝ³i´‰r‚Â÷"»Õ#œ7W˜gÇ‚}â “¦%2}¬"‘D§‰_%=Ö‹t5¤¢ö оA6huÞC SL㤌¹Uã×Tbk¢¸6rÃ*’-• 4!Mr¨cE!C ZfM¢ÃQÌ^ðýV¦~»â¦@EÝ’#9boJ­M{Àî-Ì¡B{»Æ1xs®âåwiªF_ƒ*4þ¡CZ ¼5:`œ_›à0×¶>¤¥ì¿Ì7ᔑrE.’ÔAþšf]ß_F$í v#oú¨çÞ‚7-äEc{U8<^B.µš*Ù]¢r‰®ÌÖ0›yBûZÖRס·V׆<Ù=›®íÓs«W.ýºé-‚jp%bDj#±¤<ØÈñ ®,QŽ]7†Än·#V„«ö éíÁ—h·¥ëMÐQ Ðdz9÷K (i¢X°Õ±/–Fy—‹ ¯@·}×,ߛ͕”„„༠HtL÷/M_|qỺ.ï-ùèµ qWÎôLy'/Mk=ÕB¸™éï–Îåû_ëZ²*ÊÅÔ¤4S³NÎB­¦ðÃÁÿ"µÍœ™<1ç’ŸKCƒñQc¢¶àÀn8󳿞cy+´_aeаN:¹‹¯ˆÎ¤q–1’&÷Ï•:GK6ÆBºXcÁøQ—DMfßûæ;ý5îØWG"·–ݱkÜ{w¹Øìèpµ­Ë¾Ú5¹{œ´ س­ØŒéö=:= ÝÞ~Þž#̹Zyìtº¢q8cÖõ%»%ýî¤Ó?DÀßê>ÚtàÒÏÉš’ZwÞÒr±í™kÃ=ÜÕÕóû^×ÚÑ~Á:?,ˆ‹=…Ds¬š[}=*áœjÛ·.hv/ùá }™B¼1•(¡±üJØï/vÝÖÚ:@àsÕ¯8«”Ý šrU¬·ÊÂ\ÓÝ4ê4Æ.ã¶IÙoœM7ÖõKÑÀ Â'6ôB»M†EŸ;dçDX§—qÛxÄÑûpó! ãóé ¦{t®óœý^Ô þœ]£*¨ÒŠ”;Zè“¶BÕDÈDÈ]Œs#á,Ö]Zn¶1lJ ÞÞÉ7½{MJ²_‰ê°t‘Ÿ•©â2¬íØöÔDZµÄÒ¡¡ i™8R*cS'­‘çÐõž)'yDèZñlì.€õYoãÍRÍþ=T*Z©Œ›¿)Ò+ªNôq£÷/¦ˆMV5Qî3|ÃÖMús;õ@±®P—4ñë«/&‹=×§·k›÷¡E Ùdžþ\;æÍÇÑljßHX_Ò>mŸ‚f§†*¾¤u‰üÅBUl/8øú6AÜßëP>ÐÔ+®OÑôò‹­’UoyåÖï:UÌ;˜°ÌØxÓ—@^ØN°¾Ðv\pßšáH$YW¤í®å/T‰ã°=wiºn[T;¡Ç„©ïÑ ùò‰¤ñý` œØgw{®8=LaV‘ð³KçRü²UÐE§aà>¦ÐÙuÕª^‰­……Òû•’D¿}(îgW[ŒbR» ðÕã…˜™šã¬±M·P¹ÓÃW~.,œ\U˦ Â6ªˆ…_»LÖ% V°øPÄ B½³{¸IÅÿ°%>K›J/Õl™)X^›jÜÆJê$³dÓèÙ Žž™Ê̼ã2s<ÊspšzÉáþýÙßûrÌQµ,á&•2iÁúó@—ì0n,L0¡ˆ0=PñL•Šb{òìƒhKûËó‡âÍS½@•|:ânÎ¥ê>@67õÿÕsÑh[ÊG¬Å¬/‘F£ÑzêÑv,"Q½*ŽtDvƒLÔ! Q‡xÿܘ)ó"E,3Gcîym]e–‡"R©²Ý¾Ã1g<"X{Œ½Ýž¼ûͳÓíµHÙ5ĵõ§µìkæ«o­gÃöûv Ÿ‹æcjðŒö}8ÏÃéÀ†!¯Ö#Ç w lËŸmˆiúŸyꇆêLCLöõYzžÙ Пoê¶OõæÛ§†‹¿ ÌøÔ4ÿpåÿÃØ; £ÛšmÛ¶móoÛ¶mÛ¶mÛ¶mÛ¶Ý=ûLܘˆ;O祢¢¢ž«òË•¹2ÈÚ(^%l}A *¾1r`›ïÑhãëtÔàô•úÜ?Žù->ÎòÂ) å¹>Ïà¶>0ÖüœÂ)Žâ¼<ÏäV”·´`,ÝK[pŒå™ÃIŽâ¼=4–ñ­80wqyš·&ߌÅ9»Ã õ9;4×øÖ”ÖâœòTW~–wf; ê314×à–ÔGChŽé 4gyÞiÚGEPoiþiÚGGÞÊ<¢C Jsuz:Gu6Gÿ¶:pÖâ\äTWaÞÖ'¢ª³¼ -ý+4Pwe>Mf¬. Û3o`ï’‚Ùg¾½u°Êq!¡Ý¾ƒþZØ‹Óü²!ÜÜÚÁÑ#+‰âíÅŽ¾®&Ë­ú«m¦å„»êP¸ÙÓê¤#ôw‘w©»§³vt|Ü!Ÿöº„!á‚qÈ}.*„àþwÒQ²e”ð|ªÜÔœCèÄQk7Ú'Ù [ôvAS–°–[ÊýІEæ5˜šŽÉp‡FG€] }~fI®L›˜©Zöt%iìef=¢;ó~PƒTÙ­ªŠm ;„¯lV|QÎÐ<nŸ„§8`g"Èmú'2B/S\9ÂÎ Z–ieøî_“Æ›·›ç(WŠ}9®§·Ë‚fvÞ©–ÖÒkÛ"yyÏ0ãMðŠ%†ôZ¡|°ì¹œN}…ÇÉ&ž•†î>äWXGÞGÅ×[o· Ÿ,Üú¡ºw×ÈË¡å¿Aþ$2Nˆ¾qйRN^ …‘ZÏß\eÐa¹}Ï.žÈÇJp%ïÎy>TòÄ<¸IžýÚ”²ØV§J74¨„‚{»¶Í`A»êeW}ÕK¢iòPçâ6|¡î¨Á ‹½2s?N/ãa;`f ñ,~*ÃYkÁ7k§©òCÕMº­®¦MJ¹Œ/WY)Ç1Þ º* µýÌê¢,‰OÊ™öTÓÈdQX`Ù{RùÝZº„—P VÙ±ª‡·´êl½iS¦¸ËPºâ€Uøÿ»•|{qX誀Å5ɱ¶J0»}f¹8 Rš¶¹$>jÓ¯KA¼6¬.Fn˧£³ž^\§Øßruªg™°ܾ)QôÉoÒ"® :€ÓÈŸé$Ʊë×â7Ï£3wVÿCÓYL%zhØõõKçeEø»ù"ƒk(+Ö’£HÍjN2X€ívÏ4 ¸Ìþ¾´PùwjI>E„&ˆÁ÷j‰Æ|]£ Û!é`‹ªæžý¼£V?„:¢©\u ñà·’”«MV*,ܧÄucĀެrÑÕòÊÜ2ôæ;®œo*Q$êgÊÀm‰ sùÆ\ÒíœZ*n=—Û®oŒÊ ‰7UÝ…g wú '¹Òö¯7“æ,Ô©ˆ*'-è‹ú.´µ/Tð ÝU‰UŒtõ°Æ‘wcØ`B~ ›j¡«©6‹”•{'Z;D·ºX•Zdû`—Ž£DEUJêT熭ÖM¾{v“Î^mÙ ™<U1{ óìz˜ ­®›‘¿‰ßéfrúûzûz;%#"ñÂn Ùkê6&"$´·žáJšvvú¾âcòô{¹%CÆë²²Ç#…ÛéÞ'5Ú¸u›¾“‘‘Ÿùȃ­…œ]'«FZÇ©N7ëp¶åãcddcr0e#bñYæbv“†ù±…·Y†®ñ²°††Ng“‚zWäëyê_¥IœMHéýÀDNÅY3Ošcù²@9ùºœÆ¤ÜA”´[ƒ¸pŒcj8.Søœ²h†@#T¢àR€ÄrK+Hº12P¡g%™>üQí»Uô8Ì VAƒ²¹zëÄ+ \ £z³?¨ë7&³lµ/† '+70g&ØíuOPª`Q+Õ½%`çñíuÈ}ä$`uæþ_ÿ'£ Ù®qoDLÁ·Ð¼M‚²Ž,vJÒ§Ô¡}Ú4‘±f£°y…ÌiuÑÁæ8æÆv0£q< [XˆìK¢âZqKaA“b=¥ì˜fCD‹&òÈ °1°{ã›XÉúj •9_¾IÐy—¶~¨­K3 nn¡KQ:Ò4y²ïèr¨\é߸é2ƒáøñ‹¾­w!ÿ~É8äè-y¬wE—ñW<‘͉aà‡úi¹þ>/p©/ œë´Ë)r§Týs'Ï›UêI×!¨ôãòÚÁÍ…€Ì¿EΜ@Is@·»ÌNjz|«S ¿µ°$_º'~"ßÐÐ55ÕWŸj|¬Ï¹Åã'뀢¶*²Ä*Â5³Ümx¹'â=Sò‘)wk!ÉžgVV{RH“Iä4Î͉aStUùÈj–Ìxc¿$€œÀ¶sëø`÷‡¯½Üñ»Y˜KÇÖT_®îÞÀ ÍJ m!Š–ÄÍJœ–—”p£U›Ÿßß5óRÂ)6ѧ^S'–<±S¹â×uuéΨéúù¯ñ#…LWŠ>ü9žüá2k%­>®HÎÌp¾ÿeÍ 8Ëž§‚ É¿ˆï릦[ÐbãøºÑAÛ+Ái87g >%;œaŠ_»!í?Ãê ²J9ÇÓŠ‘Oä4jQ2ÖÆ ó™k/Ü ØI×É‚6ŒAîó±¦nŽà3VÀãþƒfïöª.g×v‚³¾‘‘ ´.í:ne´ùÄ|â†QD2Ütâ+äªïDòb¼gOr=5dçžòytÒËú‚6@ùÔÐ{¦`'XLX¿e±Æe"ѹ$pJÀɦåQAVMÌ‘üT‰›—£ó”‰D|!¼‚+F;Y*Åg>ÞHƒÚòòù±ª‚ý6ðÀ2/‡K†þ'ûbZEþ=…’–ÛÅ mT>.ølóºÐ/VîXü„¬ìð Åž²BÇÆy÷ÌïeÙf¸giÜS®¬ÃÕ¸íH/5içrå8/¼gûÊGŒSÏ Áo (k{ÊÜ@ ¹©dN“Et€2!íM?uw_m´}ºûHWì᎟0j3¡N ¸Ÿñ«Ã_·}×à¤ï÷KÝ»B½íyz¼stçý¬Xô%-®Qp¥s¨”ñ S¹#*XEI:&Ñë„@›H¢ ÌÄÒ&¡C!墵ñí4~®ˆ8t½0ÓyŒ©h âQL#nWwø< æwN^Õàד§ùÓ±OÈMð:b£!£°|µïÿŸ®A¶¦!Ù ðB÷¿süü?ÕÈ-š6NG­ÿïÜáþììs÷×–ãm%îÈ)–Ò8ã¼, fœÈ`’Q”¤|ò¥¿]€b¤dç^è¾p"ô(WPÙqœÓ覂¡½¨ìÄ+ÚÜ {¥΋ìx©ÔžãKÚñ¯¦§;ØG©T3xáiE wÜ0–"¬…ìpVÒ!ˆµfï%ò<óÙ€ìî·YqxÈÎO•·ç»þƒTcäîødHE{(CÙT‡>pdâÙ‰ñ6$p‹ñ‰64«ŽX3ÃÀh}æhÖ„@ùpþ|A~RI± ’ÌÝ«ÿŒÑïvmÚ´èã=iôz¤PýÜx{qdN‹ÚêqóÈ’—þÐà*étÅãx ê(“ô¥ÔeUdrô)îØâD²k?AlZa¡´ Ó£xtQ™T)qÏ­‘ü;“ܘrÝ&à‡>ÜwÁñàºòJßÜŽ 8²`ÉÝŽ²¥Í޹:°öbÊßß`ÈûâóÆ’ªø ô©ÒCùÌ8'áÿš‹bÌœ&kÑd …›±D§9@x–T©+4Ê±È â&.x#;9“?í'g©‹³Ü—*æ6ïJgÄ$m¡7¤2¼ßjáÈ”b"<g{™í™€u°Ì)©þtwc_r#Ú¹¾]eƒ!Ž«ÂÂ#ÌAr °xäÀJë$ú#ó dÖ¯@¡º¸ùw#8 ò‚ÐìÔ?ö6Mo”Ú`€ S©C‰ªh"ðÎu{;I=q”6¥%A!G3Ìì³™†Ð`žpN}sõ<çý9+¨M ‹å® Õ­Ù½I6Ó€¡ÒÊÖïJͶ7g“é)T‘{‹Dp‚ü}Yõ«ü{›Kµ˜é†ED ï?cþÊr/Aʲ‹D&cš®SïO¤x`㽌ô¸Ræe4=/Z6’“ÊäK±”ÎK-z¾hOØr?ëo§o¨wk†FO_ýò`òI‡?Ú0çËôÁšù~¸¹ºfÍ•\×Båhgþy^øãèÅûH:˜ ‡G_öþôœË*ÞêÈ‘¹+M[²Ê*^.ÚêÀüÊ‹¹5íë?êóv }æþ~8† ÆÑzjš5wtácƒoÓî4IÅŒRl‚={ »¸žìÃÝ ƒ\ôi?Û˶ÓÀûú:ÍÇéÜ8õtçìÄ”¹ÍlÞ Åš®Kf8Û½˜8W$ê{Î ¶¹{þ§É^†h:úö¨R†LÆóÓœaËe۶Ũ¨ˆŒj€‰£ÜNá—†H\%F Þ}dÕ"¶{‡™í8A/à\"w êCÔ Šh A± Ç{¢+¤,Lom‘o Œ]ø¹·\ZÇ)Ò_ O>- Ђt0rÑV-^<ØJz&\bÊiVÕØjjµgNQOt‰u¬Â–Ä~޳'Fýá·d\‰þûò·¿RèÑdå¾ÈMz,\ wÝQ]ª.Ëá 2OÀ“ˆ0Þ ©z¼èŒ8‚¸žº±¤ Ì /yÓ¦l´þ¡Xvø-"ÙµàQ X‹ ?p9Œ »XK7­ÑÅKIZ·6YƒÜ3±g…kœØ¼‹¡Û\^¼ô\B?˜Ì?¬¡_Ð\ç(ã {šÊ×Ò‘§"ògRïE`ã›Á Ÿ ,’Ýßvd|×N`·*&”˺´M×…,…F¢û¾ƒfs_‰éc*$E–ÞS)~bþßÛš¨ŸÿtKƱ ÌNÁŠâsçŸ(}Eâ#µñ H¸xúQ 3|éû-¾Ùß•ç~¥ ç:ÑóùÅŸM¿lüÖ€€‰×‰Á¾øÒ‹ö﹜°ñ;t+æö (oM“P0¬³É5å+ìѳóÁ›än¶o@Œs1µ:¾Y!ôãØÇ¬¬,¯ž‚ ÿÔ.ç ÁÈ륊í_w­f›Ÿð »Jí((ªË{çƫٻ ýYoâ^oâ²'ý–R¶ ˆ›H¾¨sÕJØrâÃ.‹bû£xN¸¡…·L­Â-<Ú´E ’kà“s»¿6³º¤Ák\ø2—DËKª÷¡ +Ë©L|î¦f KoþÜa¸²é–:·2yubŒÁQ0>à=Øçô*&7UǰX³ÑkS3mæ枥ubgš¤@¦jk±Cú¶çžõÕÕ?}î 5»'òÉ%m¢åýK„²Ìç@ùÛd …¥pÌ•v–Ær.´”wÖ/ƒ©œÕ’òƒØ+YRš•´eDXQ œV1‹m¤2©UfU‡âLüx:èæA^Ùí›­i9­ |6O˜J«™8eåPzµä¾Ä¯d>³xa°2]å“õë¤Ôcÿµ³åÞ9äñ…>e‘YH˜Ò¦VRUbz\þ™Œèl0óT`ÙØpµ…ðö´¦ýG"cÄtò¨x2mÌÍQÁG̲ö_g5Ô_ÎÁéŠóÇ\’Å:nõØ*–aÅB I{K0šLÂçŸÌnŽNàÕã1cm«– OBuÑ¥²tTmv¸­Ò7^«o‡©óÒ]=qÇw‡âTÇ,!˵½°¸/—vaÇÏ”"Ùq÷”ÃKìnЂ=:‡§ôq‚³bpA´EK%à¬Бv%Cá*!ÞV°Ë&öLiÂÕ탚§©1²d-$òÍ¡Ò zgo%deKµ’K›WAÈÎꆕƑ¦ÀAY$X¨ÎQEÉjè\!:øZó»j¹Ê›©Ì±œ”U3£¹ÒŒbé£4à+· )¢ôþpX¼ÙÙ¹ßÓbŸ§Ó˜§“ÌÐ8Ž“¹´6Åí0êöGtáHáè¥uªöÜ0„ÔRpãS¨\¶Gê½~̓áí¶½·íÀrã%A? Ý~àIbæLô ãÚ6ã'[/¯ÅÍ€5dŒþòngÙò^°› úÉ©Jí@Ö¹ªzj1.¯ØÇÆhøGhP!ד6ÞÑš©õÕ7n®uÕ>Œ¬ÖÓR¿: Q"žù¨?å +š×¹ü¯,ÇÂRQ,ŒÔ,xö<Øy«ê wL©“SAÛ鯲.NO§! ²ìõ_¦óóÉ®Xq^j@t—-ðFzÝÛÙÎé+Żݴ!«µî÷N¦Å…-`½–¬µSŽÎ"vky¿î¾¦=tÒ05š¿;ètjîœCƒÓhmŠ™w‘§¢µð"G%kp¹î‹ƒ.)nAÜÒ-›g÷qEû±‰˜_S½•ZZÒ >¸6©#5[ÕêÕ÷¾Ñö]ÄÙå6ýêT <ˆæ"„z¬ÃË`¸‘'Fáí¯Í/gKGkNqÀ—™"ܰ4ëZcâ’–æ¸Zu=U1zçDVXj9›ó'±EV&Þ,¸Ts̪`ó;ZÁjZ bz¤Ë„í]ãè'÷–¯ê¤Ç|Qÿ3ÞJE”#…|ØÃ`±„šG‚“³¿·]¯»º½$BWpÉJb­™‘Ès"ñbSwQfâžM˜Â8Þ„pTe«êÎϬÌ(Fã*Žä@ÿ K#žRÁQ' ˜p§ˆ+é<±l¸+ÎHH]pøÆ—hT[§GâÆ`•¾+ùòõYŒ¢¦ÞÅ¢6ø@íè£Q*SyÚ†`W‡£KàëÐ&ÌÒÚSæÅM¦³àÛJ¥ÈòÎŽLlù䱨@ÒóFvTQbËlârE©c³ª}aÒ9ööà@Ðè vF2RcÁâlAºÀû†Q ¥š^¼¸½H~y¼cl¦¸î'[GÔ€ÿ·ZÅšj@vOþ¿ó?üÿZ5gœ¶VõüQ(“…§¿1ÁŒÂ~G“œµÇj " ³ýS^'òÅýÁ6$‹/¤³¥àyÉr,îfüá2´½,þüs½¬àoJÚ\Ýö¼*à¹Ã*ߟï{]÷zwönùžêõ~ŸGÏdâù1¿¿qøJÁö÷óÀlüE×ò *:á`uV˜çÇË=Ѱ4TðUá4=ÌU¦ÚPòäiEl1T!±Hm— x¡rÍ£Dç0Íã„Ê`b˜œcŽ œÉòsŠæ9¨D x!s(Í¥#ÎÍå„Î0Xb#Îpšã„Î@XdÃÏxšåþ¡°È ž±°ÈF­˜÷å0ÏsçBÏûâr±ÀØçÙw“‹QSE çp1«½äØî8ϰãI¹¢ðBÿ;[.Ùb/zÌ=•b—)LVGeô «¦’«z²XókqÖ‹„åꀒ‘Œ2Ç‹ZÉ]s5UÃ¥4ÛÌÝóò}ùÛkÙíỪ­›ÛÔ{þ—¦uã’¼kÍ|î ä×J½–]ò”ua;"v|QÅU‹ ™óÁ2ÕµñëÏ• g/˜É‡Ù3ª™M(U"™*L%d¡<‘©H*×*–bÀÄU‰2>[®H\4 ¡¦2ÿ)ÖÎ$I&’ÂØHU¤Q7!º_SÀÿƒÄL%zæÕ`ÖÖC½ímD Û¢qe¸e×É<*·8ê^u%Ú‹ÛÉa̼nŸC=ÊÌóëŒ@gK‘"£Y”A4'Tåhãx$j#Ü£R´!šÃÈjT¨ÇxHÎa»Û]ºckG¸W¿Û`ŸðÝ”¿æÝ?Ö«²Ön*Ü ¶¤¬UPN^EÛ†‰­Ã>,> ggùÈãRy­bmQ³€fÄ©ÒXÉ$ŒFáõä?Jt½-°¡! Âï7Hþä;¿½Ÿ¹RŠE(E{¯ƒ—yŽO}35M™…îotšbã/¤œ°¥Ë CL˜K…2Ñ¸ŽŽ¶1Úô›Ê‚FÚªTXmáS¸½@°Z ŠÖÚÀBï¼¾°$Š5a˜¢ƒ¹ÎõÏP„~ÃÑŠúÆXA[ÂM`k"0T=Z4Lé¤!'=óûKX/H®u6‰ÚÛ­/Gh‡h$‚‹€6Ç÷d‡¸aKàšmµ»Õª:DpÌž —nþ[h¬ßh/]‚,Ý–|];5?š,X+!&FW:i~Ò]–‰Ô™ÒW49xÍúþë˜O¾)böhªÍm5Tº_–KÁäÏc‘¤‹öLw‹Óü¾i>ê ¦6[~sæ›ÕÕåÚ‡7*£qÝÑ0'&tÌåæRMsYèaà°¨ku'äK¥Ší-òváU•v zæhÒwij½Q¢*#X´Âåz½Ù`‰àw\Ùéf¾¬çU; 0ïS·–~UD猓1$"G†ffåŠ:ê´»…¥­[›ÕêÁدHC§“ß­ì3_SË´ )Y[0L‹êP`†`—ZxšG‚&Ùþ:q©øþgHB#@nI]—ñfÕ¼¹C´!°ºðì<ù=¼K«±ø°F63Tr3ÙUrÎ\ #°´­$åAižé[øzÓxQq9 ê" ExgÈð f(ˆJ†ïɉ.ÿÂtÝüŽˆÎܼټá¨uÓܸÂg}nд°è‚k¸ò ‘Xò9bd num'^å$÷³Z gè!"c žü’ql‘2Þ¬Y!•TŠ’˜Ÿ†”ú!‚"%}kÄBÆ…2 ?ÛpÔúACfÄ“wàꆯXÔ¸VMý…úgÜ•¿7”tìã˜aWá5–¤fÝ?t•š!Qá°y<²|Wâ<g)" 'Iqf{êxð7±£½ÛÝìÁ72†'ÅÍËWÔžÁ›\ÿIŠ(Ùè¤ã¨ÞU»f†ž–n9õO›¯ÊUz YúY¹>Íö÷ʇM[n°Ê,à*ªï¡ ®€Úå3MáýHÕ²tohuп$Ò\®ABuËÐ •e|\¥Â,xÚU ùaf‰ŸX<•â6áaÆ "eÜ”ã;‰ÀŸ11™ïaŠà«É’ÐY6RÂËœþ•|Ã%ÃÖ?­X1jÜw dEþ)Å~¾pGg¨ÿ×|ŒZ"³üǵÑQÁß‘$ñc{ŽmwÊ0Þ7}ÆËäú©¿<Š9%ˆ|GÄ`›é>þÅp€Ž …넇{Þ«}¾›‰/Cù;{…8à-Ø8ÀäÓ¬¸öá^·xNZy"'ÜL¢&nÊV 1;¨Ð«YÄ ^jža0G¿¶¯H0]}Btc×Ü©ðx4[zsÑ»ðíÞüÔsqܹŒcå†/ñ' 4ÌQ[È+ô ÒG¤¤L«ë}Q¦¤^q&ÿ?V1Qô@ A _‚shˆq½g\¨¼AÔˆfû‰Xþ¬w¬ÜD]¥xC‘¯©×­á«zc)É›‚:quHeˆnÑqÄs$S+éËr¡¨6¨”%sÊeÜ\Š7UA 0 Ũ²1o|ÉybùКŠ÷€Ä7f$ÂúØús¦T÷ÖµÄÅãòÑQ·5ÑœÓïˆÁsyÙ‹¼ ÙB#'Ë{kpØØ è„Ôf6þý[KÄ"w6ñ&¨Ÿ0¯²žÜƒùæsÇS„Û2x?MVáM†Î÷ÜŲg¢µ:ô>©mä‚ç'zVi\œ^~§,¿íBuO+id<(T“°¹åg_ŒA|w*½„$&“ñöA; yrWâÌn¼ñøÀpùËEˆ«y;¨9>ÊŒó© Ô‰2þr‘ ÏŒF‹?¾C&’éÁ{ƒÃxO.æÛˆN>#}#|ñåz =êéV팂k“‹“Ÿ †²-‡yRªåðÕ ÃZñ›½3LÎŽöÞÀ*Wņüyïóæ. >žH÷v „˘ª ®€5!ƒZ®¦§ô!–{3¹\Μ,‡ºçel‡ ‹:üý+†(±ž6Õ >ŸÑsìç¹’»Â¡Þiúœ¶×lKñw¡Œ 9¤¹æv…U‹Bo ñÐEš®ÀyÚ45±ó¤ªr!ϳ'T°kÄ&' ¥ØaZ¡û£â>#Æ-)Qëj5ƒ¸;]PNt·ãÍB[zèèžð§|Ò>ßË< ù G rä{7"‡)‚6ÀO“€EïÒ®™ð§@¨ÄPÚ w§cËâöý”fÍ.5¥;ÞD·ø†uGLf [.]Ól¢øÊ´0žU§E_d H  ò4À¦äׇ‰±yK-OCýE|•¼ í"Î">X¼JÔ'£-8_~’ze->(üËJë>8R8%16ŠôV&ÏÆ^ĤçCÔÍ¿5áœuçðt&P*Ÿ•^Lûí™Ýúd/’_f-.f[F¦ª^¾ýÂ_±½„ŽZ½¦ï‚€"—r‰þuN‰,Sm ÔdŸZp{CÑÝY’³Oøùt¤<?FÑwún•O^˜9œš@[Ù_pf ª²º¸SòÁŒH?Á0"Ò½NùòÆ=Ññâ~XOa,(˜ÄÌæ1Nع²0ÉœB ¨‚½#»‚ÆäÅ`Þ´MøˆÙל—(®œÓ'Awz¯fÂDÍÜ‘z1TÅ€Ù‚énH-OlèÈÍ™²(6j\vò'·J9n.>ìäÎQÎænÆ=¥¨sõT16ª [Øå8èmþ/X{I:¢îØ ôÏgK%Ê—¤½Óe} –dxFˆ-¾CÌbÊ¢ ‰„AÓŽb¼ß¬g£›#iùå„…X/ïÁÑ¡ÃZà‹-0œ_èÁXqédƒ<<ÌâÇyàˆL–OcÅ“¦É“™?TÜüç¤1+5>ÍÏ`°Ñh…Ø^zô¥™Ò»vo–[tƒ}¯'Î…Q_‰+‘ìËЦ+’¶ ç\ä.d&IK GA›¿éPƒb¿¦Ë~ø%êGSæ’ÂȺürË<2›ê3K‡ñÊ \â>ý?í©b>Ь~CðÒݹIuû{>ˆG“ÄP_©ãtì¦qRP¼ï¹AŸØPYNË”§R V…³ z;¨ñ }êöPÈ|¢ò“ZÉü°eç±™h†Ml2ÿäÌ7ûëtÇ¿Ñÿoµ Éü9„ óð§–¹Y8šüÏ|< §-ñÄs·ŸH,.O W¥æc>WÕ¥k_ˈVT+)ÍW¹¸ÍñøÁt€r#œÝ½_¦¦[`Š-Þ.z€Ð0ï´à©ÇÈšIùbÒ¬Ò§`Ò·#c£#ï†ì26ÙuÍyV |ùo2ùã5˜yÆ~úƒµå`#F;L_ÛJKûç‹~2Ã/F/®3ÿ“%5r±¡¥©:i‘]ã§b„o£´ùå+p4ú€\oXåÙ#îTtš\TÈî*Çâ·Zñ*–¬Œ× m“ÄT•Ý:ªª&—ySd€NêYõ«ûÄíÖuú°4ò4Þþ‡«‰ó“,į—ûÃÃ{gg¦nc3ôëóëçwcgGW¨¾Ü¦¡¥½u¿Ül¼7u Ð’·)  p"¥ëQã@× 4À¥ldeº$.™±ùZëÖH3Ȭ“6‰A§u&J4‹ˆ ä~&9t ‘mùϘK¥;{[ê!!ëû!võÌZÀò즟ß]vnnM]åë;R†Ô÷Ü´G-Àû¿•‹"…½è±cw'A_#Ýz£oïv*%NÍÆ_³Íãèý8OL§,Qÿ’Œ±êNWHpÕ"1@b‹ÔŸÀÑ=Àü)‡û°Î>¬Áxú¿·Àì‰ôHßP}‡ ¹ËºJ1~Â'WÈ“Ì3‡Q#›Ë­S•`Å1gNˆ0>̵iÖT™"ª04£{t$¦ÛxBÎ$š££ÜZÝ¡ù;Ü™"ÎáßßWú€èíK…( ¾à ®ñ›uŽŽ=Y¹'J=â€YµåÀ-P”ESŸˆêõ׺=sᣈèkÐ%2ê"‰æ Ýõ—€¾é²ÎhììpÑÀ$¡Ô Ñùõw ê¬"²k§„tXÁ FfÛ}Ëó–,€ÚUóiV 5";¿šxrÅHÅ+?€rµƒð°ð:xø»0DÄ„Þï>&ª œ=ß=Í{9ûÜ«ôXªžÈè«N¼Y¥Täú‚ÉÏž…]%™¥ûKUº¥I3ô‡‰ÂGíõ­TIyÝ!; sQÎò‘Šp¤îF¨õ}uÕ+ØE$‡\³2Í÷zO£__YýSŸ©2È£Š©ðhjýCãT„‘±ÓÒôýÐ4£µ …ͱ%%;4xíkвžU`Ÿ>*A¾æóŸÛÝÕKH&ñ‚Øg*U¡d’AmÈHðßò)7Hrç>Ð9ßq§žå]èE°QtΠ=1 p’Ua áç8$™p¯Î;Pcà ŽïGîIÒÎgF@=¹»ÛA„:›ˆ{…ÉËææ ÙÑÉ­û5mÞëß.æÎ`@[žÝß-Ÿ™:1ûßï¡ï™èï Ù%xêl5¹ÑÍ”ÎÐÍβ P>xsAPÕJ —Ù°z±zñbé|·”åˆ.ïVggëÈчÎÝk¢àââè›Õ‰Sõ¤õÈ@›+[œ=Ý<ÃuC`cŽˆˆuv6=\ß©:qpcCÙâ|*àÉ0=ϯ?‰°r{[ ý§m—ˆæÐt ö–œG£tzyº1ðÄöyÚ¢tvýµXéCëæü㇊ŠNJ`½a”IÃŽß3‹ôć%y¤´úøxx=øºq6îß?‡~Ç~‰f)ÅIÂvpspsziBò÷¥ø·vöö~\á©øyùø¹9EŒËý[‰™<Ýÿá‚×3؇ÕXç5ßëeøKä^èøÅ„Ú%øøy<}sÂÜœLì!l1¿¾¬Yÿ·R÷ÞÜ\_GŸ1¶Ú¥Nºº$+\âÅÓ ñ*ê¹IïPû8*$›Z Šä0eÈ \M“MñÌ€ø.VxFÆÞR®+ý ñ‹dB[jLÉNUoQï]R¨Ûäe‰›³¤râãŠLĺ$*ï‹!”€ù€…ò™=|ÃÕ /‚‰ü›åBôÍGnD"°%ά„é׸¡?º,ýs+oš5Àg]áé‰%³X¸x­~]õ³¬¶]«p‡©Üg^Ó¾‚:ïlÁÅ0GίšîÂAO’e—‡òo ?'‡‡ßšoøj8c›;wÈnTfép•:â M0*òo/Ú‘ÐtÛ$Ç¡û!ŸÕaaȪ†Êœß=”Þ†cRñk!aWûRÀµÏ›0f RÐæÛÆâ{ €•Îo(ŠYùžþ‡Ew–~«1üÃl+ÚVè¡cQDg IkÞÖËÒ¥q°Ç£Œ&Ó8 ÛÆ*?g“þ°Šâ¢ËÙïD?/0¶÷<µâÂI§˜©ž- €€š íÜŃ.^®ÍÍä~9ÙDÀ/!àÀŸ“*ÄO(…¿6˜g“® ôQf¸ÜìñŸÜ„áÔ[“"]©œå2ª{6ÑR1Öžp“;ýâGPàeûJÖ ƒ0zÙ•8«*Som9=¾Žª# ®ó1lž¤§e5R;âšG†³&}<éâŒq!J E óPÃÇ"Í}pŽ1 /® ”×4†¹s‹ý œfL­JJÓEî SüàŒû…Ác܃ûŸöãDÏBC9kOÇhõ¼ ÀË0^‘'FOŽO Y‘oâM¯Ï œ‰‚³GƒùTÕézÝ CCÖi•‚mšèŠLB)»ñ¦ð[­}ˆQb¾G>##…h9VÅuÏo0xi#2†=$±6ýØí‰ÿ»æ«;HŒš•ÈSÙa#U¢û‚C…Рܰž6$±ÚϯÁ=OnZÄ^ N#dùEM&Ü¢J¡ûñߨÅxÛ ÀÚ½ƒ’þýÕ.ü×Õ¿~xppæÌÚFÆfG<úZò‡×XUñGJiøqîÒ…ÂJ{©c.ƒ#w>92š\U&ÚÄG°=U]‡¸ Á¯•쑵JC‡WýŽóaç Q w«O4xP­Íx ÞW–3mø×æ†P§þ¨äiôÉçÀCÒçÍFñ/\¡ ¦;Œ Å%e7…«Õä“¡ˆœòm¾Zâ {3bàHÒÛmS‘ŽÄÏ9kˆòžã]‹Ž|Ú¾æ¶L gŠé){¾?£×À%õ¢2îwOÕ20•¥2=oŸ ÿ˜b½]­óMí+lÛ³sÓ–v°Úƒ*¸ƒÛr–i}¯î,ÏTm{áÅ…g^> þ%C‚/Ø3ŠôTÀ:ôö!d8T¹'«x¦&C“™á=퇬Ë˰ rûÏÁ¼s»Æ„,Ôú-ON“ÂG6”L— A÷ôãEž¦‘v@l©,@ÍQuÝåéþš@[D§L¹X¼ðk©oM~Ö¦&!Ý‘ª9xGÑѼÞ1P¥A<'ò^!"aŸ7hmúËjMþßh½ÃÍý·U_¿»74©F>@RXxÕç·¹¾6ßß (0”îz´þž5Ë­šß™‰ã2x/|?oK>ˆ¢²!_ÒS­;ÒÂ=ðgÀj«s´:<ŒF¨æ?Šbâü(µ÷ß¡¦¸û«Š°³‹L®ÜÚ’¯‹O6þ𻲫±â•¹'CDµ­{èQdõ‘S![":@\#Å#ìÊrv|ÅkP1oV°³=rkßY¢^ö=À Ð_ÅP[%ÊS1Ï<ØQ7¯¿`›TŸðŸ0ãAzäp@œÆ•ìCmC¶˜%ÎÛ_ ¶]ÅÑ-Z…hÇW>‰TÁˆœ|£[S¾3;ÃÖ¾7Ô£­a3Ô  òàq$JÇã~̘Ôb¥§ŒUL:.9Ps¾h”,À¯7ð~¿‚rÞ‰Òò;ÀÞ‹XFMbà§uUA›—»˜pê$È@Aç‘ÑõZ_¶>7,Á‡ä­f[îN G%¯áFG$b3Öë/ÀÀ;CõPéèŸVRŽ¥r‰à«; ~˜¶õ8cSÁ-5;F7ø[À7y›¬¼tÄFŸ¨WúÊ+j“åþþXz-[‰ÉóÁ=Ä?¥X ó¬‚ËA§‡é¾•"Þû/%Ïð¦küVûq.MíløËÉ•æòŸÕ¢jÞtJo‰M Oõ&'>ÿˆ=onµ"µ½ZÛÚþžëêôäu*¾|í8ÕÓ…™-üOßRŲ6WYÙ<ÎÀ÷x¥ïì J#‰!öµÌ€¬ÜCS5ÿààÞÒ7ß–ÂìÖΫ€ïºârª_XÄ—bhµÌâ†T,zYiMsDša,på¾: ~ÇLzFÏç 9µÊÍE•<û.;fiŽæ lR2ûë¿?ïzĸ©š:¯¢ßBT]ùŠ;¡öá#ÑBªÌŽ^ů’De¬Ö»ŸóƒDáÒåQÁŽmf?Í_³·"åÙwo½vƾ€€ÐO¦-=°ø·À«sŒ»ìè¶2ë_‰(§ÜS›e‡™ƒèdãð[Í1#.2 •¤þ2ÕàçUƒ.;#ˆ‘t„‘Y*–DØì6ìÚ啕¦Üh;=ɽ曃Û7RXa ÷З#£™cþ-¶iŒÞ¢&Òé¯EÄ@ãs0è~íO˜C‡áï6›¿kõ"´¡U6×Î|´ù1–áÓ§I°]¤O;îH e™À)» xR|fà»]ÊOyc(ÃÔG‘ øŽ‘nPú^4÷~Ú¼Uܪ%-¨ÑÃ÷zúܯœìiòÂ1k!Ûëûw<ïg"˜Y$~^¢Vz† äo$bÜ*à<ßRÖ¸[ß$ö‰UP¤“ÀœL™Ä×êhòÖi4ÍX¥¯O9Õ(쀵ÂR™ÜŸGŸEzíFÇî—|Z˜ì‹e‡«K/šøa¥7ÁZ}×m~Û}áz'u/ú‚[Ø/gÑÙGk'6ª/øšKÊ+¾:#³ÕÔ æ°‘mEo«´è³Xëë=M!™KùØg™À{èÓåµ$y3dÍmÎ?ðJ/°4´ÏÆ Ö˺C»Ì¤ÌÞyÕÇFÀ‚Á)šk%Ýàè·8AјϸåÕ©(h°ú F­@PÌ䛌†Rw“¤¤pA¸_Uvmz™ lM*ÚãßS­ìÂJ…©ç°2˜}µ Æ‹:c{Üxúé‹C§Õk“¡Šn'oÿÁDS›©CCqLW®©©ËïãW9©F]›om.{¢ËëiÒ® C¯ä«ÞˆüabžÚC}F 5ÎnÔ>=£2þlŽÌ¸ÁÂD‚kƒ·õ&ûÐÏ.ZŒòg&ŒT8ÆÔCÃÙz]•Š_ñ»@m»çUžVÛ|˜íjñ>X£)ÿ0»»²öNògÏDƒ)Y!ó $ñÎȯùV%»{õõÙÉêKí°E·"…Q,Ü\sÆðt ¬ðaRž,ß}§Ìø)ÄU4ii<9iaüGDQFàÚÐ뾚– ·­-Qj±-c±½DCrWüIàÙŽ™ÞoJ€½#°0ÀÊÑ1àß ž‹)`ÓeËHÜ#j´ Ú‰x¯Sw— ò wÞû~5’½Éòø³o Û;¬I´5ú@žV“t7b_-Pfgz×i+CLD#°ÿ­ì–m›„¤ ·ÌýÖ­«RÈ aÝÜØSÖ¹›µnö2s|OVÿf)¢Õ¯³eÑ>ó›gùï£ËwyûLé ÞÈ«F¯í«ÿµˆÊ“üw0 ¡@"›àÝÖõfíöוZêIzD„èe¥ƒØ8‘ißì¤;¨ç¯á{(MÞY€},¢RÐ^cXy F©K?αIƒˆ²¯l÷GŠÖá÷#°·‡ënIìî}lÌËwvåþ àf i‰©·ºÂÚ*B+:•ßÇŠé;Ò¡­‚Ãà;GŒ_ûã÷ª*ÃDŸÄªžÊ±ŽÑZÐq ½E(éîËykD ‡pFL~X„+΀º¬A ­CŸ=á˜Øßù’¡ÎŒÙ ý—ñÞ°4[ ®Ö*¸sÝ+Ù¨áÜøîìa†åÜ×EáöpC‹<› c é_…ˆütó½ŠÀÔÕO¼EÄ„©° /^ÍOlЂ)!gx½:q]i¨<ª†&² il[ E•&û3 ]éš,á›Ëcµ9fS¥P]ˆB;‡Ì8eÞÈí4ýSup`îŽÃdŠ'ý„n—‹lu˜Ù»¯²ƒÞjíâ–SD"9Üï&yÎ2uf¸¾ªÿ.1äþ«~«zyçÇã´•#½wMê@IÅEþ.^@ºA'†l=`vK4kÞR"’€¼ß¸ÿŠ©˜ø„QíÊH Qp`V8ôιÙJ°eJCžÙAÒºÂÀ,ÁÜÓ(†+hE¶Ÿ9Ä2v÷–÷0VYn{o ò F Õ¼[ kÔŸcЉA9Ößl-¥%ɷЭñùpäõ/æxO˜Ð„hÈ»è<îñ€EKÚä¢KÚŽS“bõ÷Ÿa»3?¿ ƒö¦Ÿ£q@H…Q±Å&UIä{ˆñ²6% UM…PX ìò*²v«ëÂ4àµK}` ¿÷è…Ô©Ž£þ³Ñí~•BB6”n7ų±gRqSÌ’3‹ û~#Yû죻—¢·†–+¤@w ôYó5À¾|Ú Iz€»Ï·cö ³ý-N ˆêú|ñ¨sÈŸ¨Ùª!s½¹c%Ê,¸u™Æªð¢^Ž?iRÓ}¤sù‘áNÁòùÎÃ5òl7Ï÷{ieááàü ã}§+Ï×[òÎO± ³ÛõóÏxYEó ª'…Wè¸PÞ*Ü%*< Ót›ÒdHŸž.µÜ0¶öQ»Ü€øOádƒ|Ëಎ\t £T^1 œ¹œYΕóhX¡A¡¨$# cY2Êš­Zƒ|N$rD4 =nü_¾¿³³;_ª[E#™ÈcÞ)é÷Ç箯V[ÿKp‡#Ï€Á n¬èY´ì#bÜ•dµ"ΊîWÑŽ_×÷¸°ä$Á.PSÌc:¾ ºÎÕÃzýêJ]¤ô(ì#O,«.…+“†¼æ"á#…¢!'Š3O[ §@“†ÞCF@‰V°Èñ/—¥Ä±âAäÈæÊŽ2E³^ô:æKä bÕËï_1n½C’\³ ѾìnV6p ^x£h2‚«u·4˜(<¸ L êC®‚{È7ë^HL*D!‹\y˜x… ûɾ¤ÂÁù%ìä0o¡QS+Yë3$÷醆E+ñuY´:Hf3¥‹ÇN}Ö9¿ã4#ôÆJ>4¶¾×‘Hý ØQ© © Ä™i¹ÄؽÇ#Ãz•çéê{Dkl„•“!hDQmˆc“FµУã ձ͔±†ÑÆ ~ ;óYlÙ‡v’hFàP52•:¿s¯²æ8vÊÕDÇpò$dé]¸ÄšcUÎC ³tê·$H²-×^—nÇ¿sºÜ»ÈÃ7/Eˆ›™äò·±ß_—ßV Ck˜Ò¸åÐ5öù髸™{¨®r‡Ë®vw¡7}”É˼ÑÍ”`ë§Í^Ñ i> 3¨?XßèMЪ­ Ï…›—lÛ÷'­{ «í½RÖ–aŽ1»U‡Jœ™îýQ©àg3 :á›öJý‚ˆ]iÆ6“_vØèe®¾DNxÔ†U#áÞÆ[ûñ1€³£ifSE‰ FIÜHtn4÷-Ì›§giñK0² &³ æ´)ÿs)T@fS2ínx1ÖiÉÀž)CbRÎð‚ØŸ®àÑÞÉ×bé|$DB¬Õ±~®÷Z£Îeg0mmBD{öø˜Ëéèß„H¨7µ>º_uÖ˜!áXî^u㛣ÊöߺßOKJ…ÖÉVüëŽK—™ò¢ˆ+ Aá…Œzö¹gP0šÁ÷DOer+Eý O´4k2™V7þ{õ@~uk± ›ˆÇÀ'ØG;ë`ÁFœ¨œ»«R%„×džni‡ÙÖi.:V3(‡`-è-(£ïkCÍ[ž¶éf–×…W»È“Yª ÄuÄD³8Ú‹øƒȬxí¤›ñ¶JëŸE"9¾X¦n{¶Y¢§–õáåc4(Ä?­]“[ÿÉ1ÏųgÙ¡‚5¤jFiÿœ“PÚ¶úÀ(„Ppv1M}Bq¶a ¯½5©vÖÔ_ä5T‰c¢ µ˜ÇážðväÄÎ :>C…Ìhª$öýÔã­”O±]*,ËWYb; €)¾ìáÃêQ+Q<Âuâ$…é0™h,—££Ë#¾Éüš7ÖÍ8!/¿z Ë5;™|{ù“4ŒyxçžíÅ.@áê5g«hÍuŠeû–yùmÛŽÄôÔÇ`>vkÞÕŠŽ¢a­š[0’Çý}RIÐJµykçD:ªìG2½òh&£¡îõt£ÿà+[}a;àÍɽ½ZÆ.o°Óäx oäž4•Ô,Å|Ã’ïCugX6ö‚”ÌLßN®k÷Ñ—²ˆ×¶¹W£¡e¥ôN AÃw³åIvtËÎql¼ÎҌߙéˆ2{GÖùò†1a=»î-Øë>fABô¯D)!Kç,n„ò†¾‰êÞ&ëȶˆDS6/‚óÖF‹ §l®Ž¡}a/¯¶ÿwÕ•ÛwPÓDkX‹µIY, ¥ £ù ¶‡˜˜(-“Òɇø*¾^µ´Õãæ•Ô•'ÝÐØló÷ÖYwÕžÂÅÏ a¶S»á´ ðÞzÞ­ ~­¦ã‰žÑm¦&EÔâ]æ•·šJ[ÚCÍy„RÍJê¾”sšïgÈŒãQtTÉe¢J[¸T !7äÔzÃŽL‘ű¥ #z•,?öª/ߣܳ_µÚÜ)sû3Êk>ÆHÛ:%ÕA@¹ɵ噫ÂkŒët†ÿÃqïß*9³v)3'çÈ n]ÑA¤®Ü"‚åQݪŽH>ó´Pòã«Çê \V˜Yïµ¹‡“‹[=óî ~ÜƇpµ&ŠKÒH히žç¹¹1³k^×Ã%—ÐKàÃ30¯áŒ¢véórbþ¨š4¨‰€i âÊ”ÔQ¡Î?ø?4ÜoVO×ÓÃÆ1Mµ³¡¨¤¦£$bnÁE\žc+°»qd™-àU±ìñg䙪£›¨oÄ©«g|l‚%X¬WMHº®Y)n”×Õ;<[×ëEõ½oEåŸÏmM)•Âb‘áyÉplRÁ“ÛÅ훲OŠãG&‘—e)öÖLîŠ 4÷ôR0oj¥¥>wËØÀ¸G´6 úŸT &G(‹÷9î \Ϊ8ögWãÝì!±–@§³EÑì>˜bû,A‰.tdÒRBm=œÝWpQ cÿÚ4Æh¯´·i­.±Ó¶žÕï ;lS³p$´ã@ï²·ÐŒÕfE11ÒMÂè˼„ß@L…}\î¦æZâa;<™ñ)ƒÛ¹ |Â1eɆcè~Y½`µ~8Y“jâÉ,>øªÛC]и::¢–#jõBߎh‰Dz1zOd׊˜|ù+ÅuÞÉßXZ‘U®–ÍźW³ÂX‘/Áš*|£·ÇX:SζSÒšJþо•ffÈNa–‰¶»ƒ®¢éjsŒG{’âl+â¯O1|›O<˜§¼)H±½œP U[\0æÕõ%cTCn£mÿÞs¾ü¬N÷ÝBé‰Á08;öoOõÒþ-÷¦-ÃK¸t…“û²ƒI SÍÃáŽ@Lì|“ïÐ\²¸›®.£¥€¡ùV«ö²˜>ìÿ¢.ÇŸg`‰Í,ß´²9¬NXäÁ«X€¬P9ªpÕʾ5) ¾9œÿ61Vœ]Å6=™ô^5S¦Î’›îå–Ë+/èL“«dî/E’·€#wãzxëõήôöú´䜮šrEÞóJÊEaùšÇ—õQPèt®Ÿ¸+¹°]Å6@é'” s8Øéx¤ãÌq$ÚÁ’=œleeNâ=@VEÎhGào‡¤Xod5€È.'L$‹]´fn”¡íd qâ:všIÆ&—’æh(Z’а'[H€“4óK)a ŽÈ}WL]Ç‹EÔ E댠˜.H×'£`ê{xçÿð1<ÿ©MXÕÖŒU ;ÂF]FS ,"™H~V¨4Sì57OKBÌa䟯‘ )³oD(‹K»¯b~X§!ƒrq‡”'kÄ»Ý}FÀÛMÖPkàlßNØtž¢]gÍò—5aãiÙP&]Øú·%U[¡gYÅM•»B¢ ‘Æ‘ÅQÕ©Ð><6Ï϶äù®¾S¯à.Íô›‡Œßäà×€™.‡ÅšQå­úaÿlYµêÕ¤ùVo¶(o’ÛÓ· è”@Já½ÍC33Ʊ£ëOFÊ÷4”t}ñL1Ís'–÷óèÿ¬OŸÎ&…îð¿ï?àñ»µiÈGžý( îtR§}”^j¡ *ZÝŠX²3*Ç«·ø3…‘^C] "õ!%Py‡Jýî™hvsi¥H‡µŽáÑ‹Ìê‹ÆÆøu²âˆÎ©NEñ‘ÄCYÀÅÖ€C.ù—ÂÃ$ÅC©`"È@›{4Ð .ïç®4>,ß›ºÒ’wf’¡;§’EiFo¶Äíʤ&auáM.Y‰ŠRZOÕ–•h åU4ŸI§G\œg¾îèD-ê5ä¸GoŠ(D½…%xŸî½±D£/o¶ºãäi<*J¼ÍÖš«²[4üʇrqÙIÁ¬vdž'£¥o³¼‡! Z‚=°9EÕÚéb-•“Ùt¨œ²÷1²ÜE[}3bPŨtOâ´\·t±öôÝ7˜Ëçîæ\î‘Û[Vo0­ é'cÍFu­úHåÎ%3–|%eòX[gKð9=j‹¬NÆÝwDZ—”õŽÆ[™b´ ~¦»Fk_ó©†ÓWk?„§‡qÉ“õÒ^XöC€«ähž-üve’ï±K>ùŒ®¸®—´¬UƵÚë\3aIíÍÔÀ§`¶4ÞQQÖPt¨‘½HpÀ™Y¼“<ÖÁ8÷tBü&ÉÎnšÃæQ¨N_FÉ˼Ñog^„Â÷íH‚knI™H~ph ^»ØY;ÈEJ=„$ÃKM‡¤5_ YÓ%ïv€ ârvs€fÑg]? ÆxÍsBk>óm¶ÔÿÑ™^¨Æ^5ÂÊq­ZëpîÎ ­šDùhìÜ#yÁqLfbãhóª°mtÈ”7Òê} –•ßûF¹Æà—lÑŽêëÞ¾?²QeàìëàºË¡Ê` æ'œænˆR2.aìütú,ز™–o‘ù–°Ò-†b¥[ÌvÖ€ —Â^R笑!æsм)´í.îcæ«ê èÙÃ<âqóÂëžzÕ}ú›K/dqÒ¡‡ÒæƒÄ°$ùµ)d܇AEåPVÒ›‹T•|&ëžÌu€*|É'S X=–cÿü’Jû‹£yñtA:ŠÇjÝyÕFm«d³§ÕgõRJê|8upPµ0unèîëÊøiôrˆüz“»)]‚›óÓ“˜òD¢Ñ¨Yûäçׇ0Û¡šÁ´1§×pKæŒüJÛ‡¯Ið«K„f†C‹¢ÂUôÑÍÿK•[4ô¾÷%®ð¯p·kÎÛx^´âýõÎW8>>Ðæ¥…ADƒ(“èñͰþ÷ :¿ÔF‰;tøæQ-¹$ì¡—<Ê¿\8¿ÉéÒÁIXHNt/ª“w„;‡««/†ÞǺJ¾ª±§²bñ÷ÎŒ7o•Qóó«{eæÀ®²¿ôë@’tƒsT‚ªÚá22+âõ\óo¾Éj?oè*ž¯%ü£Â¦MQ{ðt÷åvÙ i¥æ™Á׸4p!f[zJ•:½ËØXpY 9{šß“ Sç, ¼#1áÄ6Õù·C–¯»°}½=¢T¦pY#1õ‚)€ÓfŒS.nhΚ£Ø; 3#Ûž_ã£ÝpƉäN£εrXƒ¨åyC8c,V¼¦#T-H ù~QOeBµÿŽüû^ÁÉDZ'sbIIìÃ;7ºªé0Ð~1u”hÞ£RÛÖfuðÅ€l Ýè›CП q0¸pôMœì¢ýr>x1?òŒÔL"Ÿl3^¿¾uöO  ~·|ØÅî×%òAUkE8Èrñ˜)¥ýX–DGE$•Ubñz €yf˜»:8üZ$¡.•ihã%¨4¸VyðÎvJ»×b¾ëBY—¤L~jâ|˾۵Së<§·döã(–ó/µK÷tz•+ó'xLé vïÐu‹Þ.—_W8Ü;TÂkPö¼l{*Ï@Rv×%9ùm½ŠÇì©=TRÖéì’WµvCì#ù —e?rµíöâže·‹Ë|àï°ï;.ôˆÏ™Ÿ-Ý$@:Ï%¯ ½|è<=gÓ…Ø‚–~Œ}‚è6O册¡ÒÜY´Eá“Ô^÷uÀí.°|ËÓãý’ÁRVûjqc†üP?*2úˆ=5)nÍ{9 ›iQDÓ…çkÝfßr2®—½ë¼'ì¬Vã<„”0–ÿ Kâ^È­ûáø×9æÔ’ïŽÒ`Ÿ½Ë¡á@`ÀýZ.C¹@ 𞄽«× ÍÜ û‰e ²É*3ã±¾.Àç'Ñ®Þ{ÐÒs–EÛsÊÓ§ÒuXŽø¦î,ÆØÐ„p§_¥Ü{¨î à b›Ãö`¦ñÎÌú﬑LI þ¹Ì¨æ`j"aß-—H¼¾4øÑ=ÅOå0rÌûIů€›’_Ë~áY€ù‰üeÚ³À¿+óSHQȘç#¬êî—ôW]ÀTÙëoµð€ËÀØù è6ÍñÅìo¢Ùw/@°0O‘©kUEE-´ðW=ºã¸¹«Ëkêìqo²t<–Z7¡UÎUÿ§þÑAC“òÆÎH•|z)ZSlàràO“Ôý(ž+c]-9G÷”Õ¦R~¯PrB 4´·ƒéößÔ‘I¢ñ±i­£G`z¡Gºà–Ì‚´¢•±ˆrÍ)½eVt§#àᛀ­3ø>Ë ® ¼ì¸žˆ,¦äd Þ?‰¬¢ì¾¸²RæJ8³nâ$ÔdÁ,(¡Ëv ÕŒg°kÉ:߬¸œTÝI5S©æŒS`ÀËr·V"ò0&O<4-«eÂåuw©½½Q/;¥ÞþÏݦ+8é]”K¾Üñà\ÅüÔ®jfáÐ5¡kGGõ*ç«I*‚ÐoëEôzÚd@["̌˫Yæž‘ —ðÑ u´gm4ë:}ùá½9"í7>€÷4žž{_øÔsÔÚ°N-+—ÏÉéËH¸þ ç܉_´¦‘;‡Ð&U`ÙáÖ0zµ„£`Xrm°Dãß× 9ÑÙdFÉ9AùH‰ó.bWöpæ¬fÿÀO݈~½týå¨ûÁá Ö/J\húD‡ _ÙqzŽ2Gº ˜,Íd[ŒÛ¶f2Ñ}ÂÈ%. á¶UA×½¬TŠKXØ:­—H’é=P¦÷=ש-Xnch>vw_Aa‘édÏQ^XµÂíð†S{ŒSĘ. ÊÉXVÀ\Æ‹žYI#Yìíò)æÏBöÿ@ziKDIq^E)håý%ÿîkŠ•7©„Z°VɯÓÅ"F_ ÷܉˜É¢5ìOÍ>Zm²õû1JxÁþÞ˜ ³åÏ–ÐnÝsã±}UèêêÑ¿O8”Ã3Éž"äáô†Æßßí;]2¾XÐ 霘 ô† ±Ìñ¨ætOŸúŸz# ŠáæÞõ%ØèðRn¥ ù?Ä;À®í;²£–ñUÑ1¾êgÙïMÍf¨[¤À¯ ¨À"ˆ¢/"‰ —ÐÐÃèåwç?ä•rœ-n9Åäìe8k©–9 s÷‹ƒéfCxýÊmý9s_ü²NÉü–IhÒYÑ5!"Ó.3%kc$kq¹ˆx¶÷ö¶Ür8|w6ÿ@ÿoÇÿÀË/vq€²µÿãÂÿi$1ÚévÇQsÅs¯úd@ —/—¹U$yG ±—D€Œœ'Б™ÊÌ4U4¯Ç×Wc…ŒàÆ“eÂ"Ç#wM|}àz0>âÐõëÊ yÑs½6v;úév0ü[›öÈðv«É_߯L_¿Ï¶½ª°vŠ)â<{½ ý Wj´Úlw¼f;Î|˜û˜™žŸø½ö‘QKþLÎïP?£ø¤Ì¸*K•'r¹+ÓÔR´rJ¢9ç*W³Ï–%uk`ÊÒÚ+ÙÊ/AU¨(ÄÖ0©·–'ë©`³ÌDÛ»¨lØI‹´™JU]Ëáè¦Ë7Û+ï”KråUW\+>ƒ©dÆ]dW\«>‰+ŸÃ+ïJx+{ÕR^+?‹*ŸË+ï”J|éë,¿T~U<¥WðÕJÆT~ U<ÿ­DU*+¿W: ¬ÀW. Vðæ[ÊU~*U>'Wò×HÊU}2W>Goc+·˜©ø(¦©üÄSá*¾ U:«ß¢*ßpªðJŠ{НÄV~&SÉT|2ªp•^Ž©\´WæÉ— Wú*¦ÈV~>U>wWæ)–(UþRõÈÙ*µ «üªxJÔ†©ÜUþ¨|ÖWæ©–,)rZâ+¼0W:‹Ö¶Umj¨ð—Iž{¢iúi¤èª¾¨)_ ªüwÑBÅO%EWåå©ÒYw…¾|T%O¡S¥¯\ýd‚QÅKf…¾b‘T%O¥Dƒª„ÓCô¨òYu>X[u€N¥³(3šõZµ¤Š*?¢½(3ÜNP&¸ì¿ê¼0M¹~•Ž Ì ö÷2,ê¼rwE ¹²¼2÷Ã`‹“Ü…Gù`eÁ½Ä‹EÉ-åKƒ¢¨\…}¤T^k‰]Å>¦vr¯èÜãÄUúqR\Õ>BÞ;Ç4yžü€“·¢ê"_GE`‘·¢Â@2°íOº ¯ò€CoOy ²wcq`RéîSën#”lÆ"ºµ… (}æbšõE$Ÿ±$¿µ”àï¹²¤jŸèß).WLÛ!úÔ&Ž2u ^á"Æ×II=â—‚—Ë6Ô¯3åÐ…*ê[ƒ5òkØt¢¦Yõ8îʵM¹V)È ÷³âX¥7]Ð-æˆð~€kI…WV…½”VffïâÂjrdÙ„¯ä@=,åÜ3V¾ïݤ Mý°’:æöž}0óºhmÔ0—¯cY´Yœ+wúРɥmÅzdZ¢‘e5Šžu#ʰd¹pÄ}‹.:ãZ…Ô»j¬Ø™%i¶zX¨ÀNÇrÇf9Óè 3Ãöž4«„>µ"ºžÈM\‘&É÷¨ˆS÷¸BB¡Ç#fBU¹\ùN ½«Wñe@=»hÂé¢Ãó«Ù¤gqÈëܲ/37£×uIû~fvnïðúÍÏîcBæ[|tÊzsœ; ,ú®ÈY·²)µzqzLpk0M—|ÄÔ[¦R;ü`þùôàz¹GÙ¥l ÒRïR£Ý!"ß_´ â™‘O°»âpSÒZÛV¹ÍÑó'˺ÂAvCÍäMu²øBÎsÃYèÚªÇó°àmÂYF®£<\µ¢¹[ÅÚKí@x³ÏìAA¿º¹à¸ž¨óø±úié‹ .ƒ`‰ò)Ø„o_“¯]õð½¯Ý);Ø’n5¯~ÿÐ׺ÛüÉžå‚çªsÊñˆ®5Ã%hšc5­Zh*ä×x±fzŠf,Ê(}Vç…µ›þpjV÷&²Þgó¹^çeéÖ²\U™ÝÏ‹.å¸]L2–)kÂMµ¸szžªhÎhŠ?ý GV¢ðüT9ªG[jo‘U×9Ý+oü¨ñ1˜dÜ*OÖ‚=4Ê,+¯©hñ¤Ý¬;hØTÏ´Ø®hÍŒá’!Ü7t‰ Ž6hMªaG½5´®a?²ïT,Ý<}-&d[”KÏr‡:»Ë^Ï®LÃ^‚î ÈÍãñ=<^@&åóë†r/7¤«t÷pŸ¸ªBû¡Åê¬ Œ$ †’Žbçà†¤ùô-Ò†÷É»šÉ×)’ª0_÷wmAÃéÂD8•^©ŒyÝ\hï9ïs¿Toov®þ– ªÕž ~ØìÙàh­Lx_×Áú®Xq±ƒ"ŽA[ꈋ`\ßr8ó[pÍMÕ­f¢SÎ6î—< V… ç¼à˜ú˜hÇΆ tÔ†’ íOÜ7ÆvĞ j韀p‡Ê¶ÇHR¸‡çÀ>òOÂU_ÊÙã“‘Þ§á˜$ïÛ7fçÌæ2 ÷ªo<2¦'z®4`ð;{èÀȇý]Õ ªl‘ã{Ë-â;)“ÉŒêrî\™>å‘;Õ~^OM  ÑoѰ±TÎ.21BJ˜®í6šw?ïŽ÷œç(ø",Ç iC]ÒÅïÿdØ¢ãóǤ9Ë Ï]$ ªŸ½‚0 ô¬“ Hý|å/ho.¯ÊdIZexÚ\~¾Ã«fÃU9V#Ø{ZhÂ’Ÿ…Ù’%sÔuP—ïd" ßW´×Ž+ –|—눃÷÷ YJ±Ò.uK¡’žB<Ÿd8o[0ìíÌad×ϳOZ<@2¿Á"¸&¡GÝæk‡÷ÄŠÃÈ,œÒ(wd§10¼_üÏ8ˆÂ³Æs ¾’ÖŽ|,ã n´è“mAh‚µOþ™hþÞÂÁFxzF9\ÖÜj¦„´çV­á3@½U-CcPìó;ÁÛ«`âIW>³îðõûö]ýå}E1ø´FJ´ ›>È€Î;8úƒ½`ǬÑm›’ã‘=‘ÙÆ”»5SŽÝvŽÑõÞÚ£|2ä{æ‡ Ã=ÈN¼ôra!÷æ’¤I åSgiu/w®°üå» ”ƒ/`› °·oò»Àžj¯\ÎCGµ Î )”uSƒks Î/KswÑ ¿TÇ<w×yºd*Mø„É{° &A”¼S\å Ø›oü ”ÿЉ,zÈ›/°3ìÆ§õÊA^(!5d«°ó(Æ“|XáËTÙBEMÍ ØÔìn9ÃÍà¹W)o肈«áßÕþò1[“¸AÑvxT9ìºxgyÝûjz‡Š|*¤õA5Ð JŒÐU<à‰ŒOð-ö¸)?ÄV<›ÇPMp{ W‘m€øVýïÁ†ÛÒ~gר5wº'³&·èšüµx9«9û±yÀ-:ŒsQw§zͼ¸ðÿE[yA—9cU_#6ìX«·Zýc¿Á½² 2f~ݺ9 ÏË ž° Þ)3R`£hR‹¸R@PÊȼÉ8ßÏO=ä—–î­}Q+S|1/öt I“%ãÇ»öŽg^?õ>²ObÑÝÑûjô‡ó]è"Pfµ/œ¨š]¯ŒFª+Š~TÀxŠ”—á•û„¿º¯²S9fK† ¶7Ù†l{¨zÆhЉ_E·z­šÕU+>@U[*ˆ›šÛ˜gt~4†SâI1`Õ“ ³ÆXí‘ø]äoúb M¨} ¹»²ï)¢tÃ5Œdx@9Ȱ#Œ¡ cBÁKÚèÈ|øåˆÉ<*5M£œáÍC)*4& –Á•)3TÐI=뵜ûs a fŸ3ø¯0“/q7‚kôó.?"çKº ·ðì®P •&[ctŠZ“1õ7´îÐ}Ú¥O4·r~PK•W=Æ~/÷Àb""MLëÐËyPÿ ̯pSL‘6¥¬‘Úa¹¸"W0¾à.-°ñ¸øÆr(ÌxË×$£èòüEWg¢x°ÂLº\ogŠ4¤ÖHqÄø2{Ó>,)kSfàâJàsv‘å-Ùîìø\´ä”'¸ ZnÃqWëì¤fKZy·—iõ·ª;¶§º¨°äþt¤ˆ5Íš‘ÀɈ#-T‰C(‡3ðÊp9œìóL÷|쨘ÍêÕ 0Z@€o¾Ë‡áb\šÖ1“ß_ž\ÒÏ,`Y`Ü¿ 5¢üe³ó5…<‡C°#9áäО¢ù“ÁnM¿=Wç%ãFT!Tå3€.Eá“‹;XŠ5Y“8XèT¶—31€Ìªna ³„šÛ­ŸµU»&R¥ë„:íµ¾ÉbkVFÂI'Ö:ý]ú‚¹á¤EwÞ÷NþŒ`¸Õi–!»F\‹a\'¿RÁaÀÙ æ«öàÍ|ì6gÍ™2ì™g‘…×B[è­(¶pG†yô)ÈS =gö¬2'4VëajMqìqö7aW·Å»æš‘fæM4d]=:ÌË“?SÕFTŒ -qe ½ySÜ‘B¹Ñx&äyÌ‹ÕåÝÑÄuW]©eæ£ïi\q­„FÈHÛhiÚ®Ø!Ù°Ä{¤FÜ)Fü ‘6ùáßÅ£j†žø1y”gÓ¡{¹æ¨_êZ.]=å^2u›´§ R4œ"è$ÉÀ‰%crSºsR~\ÑZ7*ìV3Ù=T‰L ^”ª lšRFåT¤IÌïä*‚–¸|amû_|g[ý.XÄÃE?—3€L~©,ÝV;·%ðÞ>÷¦™‡v—leÓù5;14bt6‹#Õf_ý$2Öê|¤^ù÷ßkÅ¡c¾¥ÝƒuŒš}J؇å¾›§¤éš/x .tòÜQbŒ˜jûkýoðiK,J6 Ø \ Û» ’ŽÿTjü8sˆþ¶¸íõŽ2vþ¬‰ÊØ9Úuùw®Ñ{jú‘œÍ©~œ8E}29Ì<|™¥©Ì õ“9ãT\—{ñ€n8K½¤|§‚ü›£eF)a :ªÓŠˆ&w%Ïð5NÕ‹'JÈ%Ô]i{ÎáÌ Q€qº¶;,b|ŠÍþz4¡ŒiŒ¿Á.ƒPï©Jƒ d~)´=)msQ˜X±Ô¢ZåV¸lÈJx ôÔN—[Ù-ÿŠ5-.·Ç÷{„‘nM®X ïa¾•û]\.·‘ަb/ÝcïÝ6z™DÙ)iðl' |ˆ&ùÚg—EýæÓO\@ê’’³å-^Rh¬K_°‘‹ËCÐçÊ`h¹Â>ó¡OÞâæny7XU£»ÝÂrŠšÝ4‚§A]AúÓ¯FQ3HTÒýæ^,Qü’§,R”z[¼¸ÐyÖÅÉIv§þ¤Q ”é»! ¼ïdËÖæ‘~ûCoµËÂô=(³ïL5 U¹©Ç:&Q®éúÀ‡¸m\T"$y¡¨'ÏѸa®† ëh­å2·°–î6m:µ™4ã; pà}azêint½><êþ OCäh\ízwÇ?GcF[4“õ(ý¸¨2WU6ƒGœFV¢«ÕkÑÍÐy Ó‡Úáìú€ÄÕ <¥Užü…ÑV([Ý“¡.f?ïyP©êÑ·i¼w ÏZ?íž;²™!Àù˜™lßÑíª ¶e§Ñ†oõ°®Èc8Ê톹®¹ŽÎ§þ½:2&ÄÒ*ëž>ºé]L‚¶ËC…ôûH˜MQÆ8`jÇ­ãá~ÈàZ “¿™‹•>ÃubR  ¿Ä†ÉGènÆ™ÎG–tûk¤+˜êÉú D.Ã/I·3=|Éê…©èÏ]ÒrèˆÙM 2"ò“÷=}Mhl"éÈ‘@‹Âõ}>„&q Mg$aÀ´;˜Æj8î|õ)HX4ÞWC¶è)EÙ± ³Ÿ$Ì`HnC{P‘zÂM-ô+ÏèÖ×s]ô/S0²³1¥O\â9ùØõ¿H\P°`Õë€!¾óSå?ÞÓ`ô»»{‹X½Uȉlf1îKpý÷Ï=‰ÐCÍ|ГN€·ÞëM"ãÏAydfÃÎ20c®Ó†°Í‘Ö9ýÈ•©dU‡åãž«º¤D“°$3`¬Ý èï_Öék Ð(ÃiàÖ@F.E'þ6šO Új,ã¬/^Ój5ÆCßýcÀ¦m«”5di„Ag]“´/A?b5Œœ^Î7Û \§{û]¸‡­\÷’.$†¤©—>ˆÁŠ/p0gk$Ê¢|'›oéiQJ—LÒFÔQ 4Êos_¾­!ÅÃ6¹â«2 „»¶qúöL ÇFwY•)¬…§²»7rpwp¿–âàXî„߃Ïzo½p”ø`÷>†æ‹®Z°Ÿ³ÂQ mª\D‚#5ÿaãzÕËëþÉ<é®7|`W±Ó/†‘¢'P¢Çt¯RX nD”âl¹×4`è¼”vIy‰~gáEôî†ýþFt´‡gÓ¹>´Qß4u‚÷Ž9yœóCv‚©D~n9l‚DŠüK%!±ƒˆß¶ò“ùþILÆøf‹Êìœ hEè~ŒC~ȨÿÃ>‘•œ3e‘ÿŒV™aðÒÜTzUmä.{gIh²âAY•ÍžÌ6|¬3߯³Fæ>êÂ@:^îšt·Žš“t•¤èe‡âØõùGv³ÙàXü <±ÎþF­}3*’ïLzò‘9³ÒsF†Ïº-£+ùãþI²Îï罓_ Ö§àèª3üä*˜0Én‘˜2H?lë ï@²+<|n`&_GOfK ­Ðé\Ðà·hÂÁƒ?Ýû$Ø(pD—×÷ñN[ê ž!­·FLŒQUŒQáí°(°"âmõî!eAçñqìóRv6päºÅåƒÏJÅû>“V:ÞwýÍ‹Ð@ùÇX xBpÍÙûfömi"2ƒ°ÇþuRs©`®c'‹A0Ñ>fg\ÕPNSFä­q“4“D÷s9…Pµk¹ ¼ÀC„ER5j ­.ˆÄ¾É¥½ã*ZKAa@¡اY´ô¸—`YœüýÏOr*n0‡úDsFV ¥õV™ºD¤C7¸£ˆK ªOYúÛŠÄ©ÇbØ_1Wzó_õ'ñÀÝš¸ÉE­žEyùK\⇬k 6nÔ/ãtûÍ‹Ü\ä'5“UÄšmìe—¨ÓL$¹îM>­þlè­¾í~€p¶û>MÁò‚›¬µc0÷tí2{ê?™Ë'ð ÇgÛªM«W°[v¯½5ΠˑъöíHÀÁnø§ó5ïÇŒÜú23'ð™cb–<ž,ÜÌ4¿[kÂÆ,[cÀ®3ŽBÿˆ¦LÇEm¾NKÙ›&.Q…«º›Ò²!ìˆ^Ê!¤t›Â²F²² …ŸöÀ Ô[/5p¦ûæ( i¨Ë´b-¾@‰DTаe–E˜K¤ŸF¡#oæàÊ‚€Ü¸G嫽@{À_¼máÇ?Èq¤¤ØÕìž ÛÜ]ôó/¹3@u¸A:ys³ ÚFØc‹vG²0̦0ûÀØC¼a #B4ØÏXUî8^²cáDŸŒfWç?‰A Ø=sç°‡Ev©QKG+áºÙݯçòÞ ÜM«É¼Ð•zNùÔ.˜lÍ÷©Õú)šê¶ó\©,CRJ›i#9@ô…D¹–¥ÇÅ ¤d‰¡•ê`•ê0ë[]V+×ÞF*1 ’o¨¬UF(p!‚+TuÌHªØ Õe˜bô•»~€u½¥ÑXiÀRâªÒ£¦ ­Nûâ-°ù@Wíëü‹=ÅPoVŒÈщî\ܱºõ¦=ݧ'ó껜 µ„žDè—04ì[QŽäÄè?êU~qXŸèùxuãš9˜nxä»j«W¼·ÁÌ·±ÄÖgsK\½k˜ŽoBó¬Þ¹7ß •r-¸Ë;‡†à.€R+Dgîž}ÿñ—|èˇ· ÏjßZ™o?þ·]yÁÜQÐâqΠaT^·Ö8ê(TгÐ_çLûü;xèiø1½LÈZ$8mI¢h4³6ÇЭŒ2GÚ<ž=µ:I&d—ÔÊ2ç4üéÇ€à€ð:œý[lá|{L?èy±Z«ÃW©×ÛðZ)šiœºß)Ö¢2êÙ:wìý¥~‚K—™ -zG?ã ÒË»sÖýň҂¿öNÌ!ÚžGëNù`çwu< Ï—ie…Ù¹wÂ}z. ödëùH¯Ûn})¯¥,Ú6¢s©y ÕãSãcãê8Lçž-;äÞQ;éèNœšjÞǦ‚þ8Zýl²Ó‚“e}ä\Þ¤VAÓŽìp )0F…˜†–Ö~ŸÏ§ßG–¸á m˜÷.Ðî&`bûý«-”>hÀ’”óß+F‚ßÚ9Éãl‹nõPÓ'M¾ÿ“jU@3ø.—Ÿ§Á'-PPÛŸÛ)Æ?ùÕC0vö[»Qø‰ÿÇúAÄ„I*ûœðnQl.+&: ÀÕP¿]ý>!ÿ¾æã]4bG¦=ÏûFûçÔß¡·Fà `Ík©ê»c¿6æÏ[Ñž(Ï7œÃmÞÍ'“ñîéæ, jhÇ \’¨y63˜Zž+‡Ñ|ù‹)Ø fШÌh~—Qü,ásùa3üG˜}#~¨Ã#F"ôá±Ã1úð²8cxQ#Ìá1ìá1ã‹ÄïúØÌ_Ht…–¸Ã# üá±F1ÞðD±,¯:Wpsgõ\j.3%x\zr˜Ìäe’ã}º§ûv1"\. %xžØ¤`vv  tB w Ô[–J$2šð…|S•Òtœïn2ÃSk4)YÇ`ÁNS±(Å*Ʊ8•h6Ïô© ,)}êêƒü3Si`»<@±RºÌ(¥Ì·&'Ç]Jêú.ò s;1½7ŽÐA‚‡™ƒ?—Ôã[qîÚÅæËíˆ>|®“MòJ:kÔóä,€"ñq±ê­EÙ§óþ×äMtÆ|õ€ÊVµØ4º}ñ1f•ög6Ð}c£»|³fµªah€ø¤®C¥xw.5 °qK\^ Ü⪉¦ú/vŒEá/Í ‰²Qd$XǹÔ$¡D§ñC:Ùý?íÒ$$lÿ ÖO#¸É#€×6€z,ÝR¿.÷,=s󄈺÷Õ’«Ö@×a¹m€«üÅxXX﬽ü)˜™ÂÅÁï,!@=§ ϱl.f„~„œ ‰Rû*¤"ÑôжÔa»·Ul(üÙoïæãA?€XêÖ.«¾/R5xª‰“ÛxfÉìYͳ7.næa¥%•âB„½!ZD/ô’Œð›kJ©ïü"Üõvd^Pøã&×c$¥M»Ì­ª5oc÷ë±~C¨ö‘½îÅ:«¦b°Ä¢»0þ'íIÂø ™0Î\ÐbUD$Á p¯®œ?>d)–\–dÓ²9-ÑeVoIš Ý]ÀžˆœVÌŒ“Ö«,®iÿlØîð¥¹kÚ›®).@§úó_:‚„ý¢W7už`u9›ò¿v}gÌire—2Z…Ì–%HðЃs9–ô&™aÞà%šDÀÀÁ BÛÊ*vXœ—LˆvoZ û V¾iH·xp.bÒ²`ælŸ(üÔ ú÷œ—¼iHj|#cΈRósÇÑuÿ*W1Ç•Oh•º_uªQáþ¥1¯9Ò0½L&!‘*Õhù£Bâ?ì†m½+·Ü=|ûÛËÝÏœ$*,¨;­3Xæf9 Àíd>°7b{Ì©ƒLwt½ AŽ]ùCö³“ŸB6n48LÜÀ¥7Ìfã[Q¤Ü\/4i©^A§‘ž®5ü¢3 …°ªvôBP·ô üQ •TvøI‘³¸N/ 2mÜè­‚W¹ìt‘(;&/[v‰YõYiãe‰ì(£ñ‡¸âèÈCâî¬Ù%¹le/LT£ÃR›Á€ÃkÐÞˆb‘ö ™º‘S"µá{4÷¨FŸ@ŸŒWÜ0…ÀR&«D†L¶®¦,'Ñ@âóLuy¹Y6e¿RLgß–Ø“ ]¨ä¿ ¸«;Œ7óÖL1/úך¡Pó6Úv×H¥êœ+ò²TÁŦފUšÞ%9ÖñRud‰æÛ †yòHaéþßõª£ÖF&›ôÉøªO‰Š½;–Ê’Cƒ/ËÎe”O™“«Ñ0D1¤DÃãó;f&k¡f„škÀ[uSƒwóõDrÆg¦Õ« óy09÷éõû ëµ+‹ôß?·ï+#Î’|å†Qd¬yŠn- CñæÚŠ¢årû+ÙÏ…×hÿ¢’Ë•ùŠ›=óîIwhZÁ°³øùöâ_MÖÞ]Í£»¼´×7²Ƕ„•˜­÷‘–=z²Ïq4Ä´òô}¸Â,à^ £ ãTsÓ‡úŒà~ÐèÚ12³‰iG°Y¿fbFåŸmŒÎ½2YÞ>iÔšmí0éûÍqÓñïÛ®J^=°>Rjp×jPêaõóôëת ìu‡pT­Ø %Û¯ØÿÈ^”³ÙŠøRFÃ/ØCk ÐiõÇTH­CºVÖé\ϳËûád«#©­™çÄ"Å ©ëµQIŸ4¼›6b;Ä-ÅYýåìt²LëtÓyèÀXN¹Øã6šbBÔ%"Œ½uWõ ëôXâÞ²eÿN•pË©_kI»5j·ú°òêÕ0Pcïç·>äAÇ-í¦ÝñŸÎÌöžétzngʤeqvÈq̾ÜT-Åz‘w§ î¸Ç  ³åæ?xÑû¥ÏÒ̰σ1W¬’î‹É¹+ö¤^Ÿ"…Q"IJ}“BùËVÈ/pô³DÒÜj–&Ì5·â€þÍ™('êùRF±¢Ó:¬¹™×dìZˆ!$0¤M¬cj ã²R è]Ö/“¢«ó­ý‘¢EÅJ`\[¦”—ó’ÞþÐiëÄZ¿¶ÚNh4缩\R`ÿíáúðØ–¾ñ+66V5Í¿&ØÊ܃ÉwyµŒ;ó`ÆSÉzò3îÖQdDù»EÀÁKÑõ•HücäË0¨&¢4îi_¯NXŸ)ê Yɦƒ€¹Šã0h|KŸrKy@jš¥Ë!ÈÆˆ‰ŽÕs•v$nÎ=$t–ìp¹³nÃÏÒ ê@0ÔÀšIÿÅ:ˆ†ÕÝ>³Ÿ\ÀZçð\ŽÐ2ľô‹^ ÃÕUD´È·,œvƒ®zq2™™ÍÉ{=-E7­›-y-IzØ\¹˜úý|”v‚¸È:=R;C7=Ó}2;UÄV—DÃÝ…’o=äŸùZÚýû^鈪ÈL¬Cÿþ4ÑMHwgR«]”*›ËËËÈ“ëá:OÎÂÊÝ rv °“þ½`~:Lm+ G(oYkŸ}‹·-/ˆ~¥QØ|ý,ÏruêÊ!ÏݺÀqÎu‘í,âAJ¬”Ú&}%ßT•ÉZz8M!wË9‚Á±^SOþrlœ”s-.j55uÊBêÿL1ãdZÇ«'Ü‘õÔϽUŸ4·Æ3sm ]¼A×¼š®Çˆ„ #Òi°|­48ðï— Š‡©¢U¼¤ÓZ§<ÊxndÙ<Ú"ÇÜR³–\:wŽ›[ËrGSêrßµÃiÜ9ê/ßÛh„åY `ß0¢7RT5êÎhß²ÃçéꊌƻJçÞ3’Vü0)zc (¼kUÿ…/¦÷-€zŒ+¨çZ€üŒ%ËË•µÃ £Y~ë=c¯Ö“”Æå¤I°*<Ü&i]5Þ6Òúáé!?g¥¡ŸŽB’¬¶òS¥*]3Ô¤#U>Ì~9‡ûA5¼åù–>Z²ÕÖ<•~=hЂÐÒbr.â!»)ñ»õÅe˜†I¹è؈bz‚ÖÊ Œ=ÚŸ‰M/[ü²ê’Ù Á4"_ ù,V1q_ô•ˆ—±­ž}c¡›ÊÊ,W·êT W ˜“fgfô©¡‹¡ø°ŽqsÔt©‚Íš}Í>¢—˜Rý໤±çÀú^—–ú_ë—Ÿ:ÖAÖ¤n"Söqvh,Y+0ˆ-†¿66ŒáeÐÕÂ(b=Úp<–"ÜÑw.–½{¶íßdªÃ'(f™¾ñUW8Ȥ=^ª×ù§Î]ßZ6[F±9÷éóp8^«ªÿÓ‰& å&M‰¾ª¾QÕnk¤ bs:á2—`­=DGu3z‘¯Ãâ’Y vAúɹ[뀗ºæí{ò:ž)zIñˆ¥´ ¢ÓÔŽ¨  x(ùÊPqBˆ ûn؃#2ïk ‘·ÎÞ?ÑB@áhH]¬‚jíÄýxÛ”£bH¸——a¹~væ{4݈ øaåç Ê¡Üú“®oƒ­̵CÆ9³ôÜ*Ò&ŸfŠnÅ+­µ#¶º×’ ¤¡ÎÏ¥a¸¸ÀM¶ž‘q9ŒWw3åÛkR½ýµÄ= =¢GÛ—CntžàuêºÅ¥\5RÜgJ èžq”[‰ƒù]"L…w+öŸTÕìá–{í·ð[ŠOÅËÞ¾N’Žó!-íÙ$«Ï©õÃÔ\£šAÔ {ÞÉßk Ù®–N–¶‰IŒsÓ¡ ¦uëO!¸ë,ä æZ[iÖË›»ƒ‚g,ë' ¨ì@14ÖÖFLØ©¸ü%æH#£‹ñh@{î&&zjör ·‡†ï­´V«²J ´:í+²Ã»lßÌñ:qùõSÍËà€–¢yÄ{Hl$øé—Š&ÕÛšÿ!yÝ\±Íp‡Ä0 H|W7N·@´;B5ÅŒ[Õ¦ýCßܶT Ô<|^8vw²»ÁÄ5]šy#­XÙ—œ^àR7lXtêI3,4AM`b²¤H’DÑVO11-Q¿ožFôùo™CÊ¿Æ3‚!ùbνºÍ²H ˆúüÂÈ XÙ&º0í42 ‰/ɹVD-,ݱûG’íž$+3–AóM45N^ ŧC«[ÃB"õcg’ÝǵÔZN¾áÕ°Á©_û±à"«7Þ¶±ªjÔÕ'DàÉXc»QâŸäs²–à—ž(ŠGT<À POx¯ ìÎÂ]øó^Øo^ wáó(s0^’êÙÈàJdÜymޏN{~‹îQ98¬¹¿LùH»n/ c´¡„Ão^Ö/Få}—tfÒÕ+Ù÷®H‡= L±ßÀñKÎxA€,} wW3êÚ;îAì?™|à-  ¬à2óýùÃÕô·Bª(4ðQƒñ¨-“`vêˆðMÀ8Ço€9B|ŸáYDèËÄÌ’úË £Ë "þÀØ€&+/†´©[-ïá¯Så«S5.Zqõ¦âgœr'ˆÅ þ”äù>Bâbs©hU4ž¼qR£2åÌ/€³Â©·H.UǤÔî™T:S½ï¶æš#Ðpà3£M†Â 5D(2~Æ(¸5 "ºTÕnsež‰qù˜¢Ï†UÚ¤ŠþZ4ˆ1Ã4ÄØÒ7›°cmް½WÂB ˜ ñ…+Zâ.¯V3•»Õv§ÎJˆm¦Êb‘¦Koœœã(6@3’šW±„ÁÂùmwª‚8FÆ4÷È_áYP–]f«?¼ÂX¿‚§½ ðßùÄðª„/y·Ggß…8³¦.ÈH5Þ ò­öÆ&.<ÁMk¥¤Ãaä‚Å:©^\"»™Á#d5–ÇVñwë_ܨxÉZß´n¤?\tr9g»%Ýtíœ#ↄm[²™l»,<»Ö_«Ë©¼¸`~6f}SÞ9Ó ¥G:øõBsm Ba.ç˜8n£úØNbÚÇSÃÙ¿¥HZ'Ä}ÇÅB›#ÁÆ?^²VQ@Ë¿†ŽÙÎ*9Ès«¾È«`[²H1§1¥ÎØf}cÒUU3 Ÿú°{%1 £W‰rBßm±hXËœ¸ö1[­e(#Eÿ®R‹º ƒÎÿ|‚2ñŽJÇpÄx­Âvd·»_\j4Mê†ûc·&”»]¯Q8Ó?bu>gÂ_4í~ÂòÿØtÜëJÉ­—Þß šyBÚj Úü²œ“íÚ*+’‰$D=N?œWÔd=—­Ù¬Õ9®),`ÊeœðØ$•y³0P½`ÃǘM¡¡Ð/‰p2’\ÄÜbª>›ÎìK·o7ÎVˆÍ,<'¹V2aŽã c•²Ûú‹/ÑeVÈa¶zÏ{š8¼x‘³ŸR÷ \®k'1á” â³?£í\3ÑIÇ·Ž/Ú²f¯]„öÌ?u®è4¸3üIU‰_¹¿ß$±ÉI冕ó`÷d`¨°îÿ&1]Åvd›™'…Éä„ɇz÷¼ÚLÎ=ù†‰oã“J¾g‰i„úIåÒk*¥W!È„õTs:XH4ÂYÌìØ'z×5“Y¶TkŽ]ø«ZƒLgx­pœ;|ë‰ÍÊì×êµn&Òæ1> 9o† œ¦<’n%SÖ~稌²ÿ½§qÌ9ª†Å«sÕANô à[ƒŽäH%Š8|ë}MÀµ:ÒA­ÿ™Gè±?#¼ÄÊ‹¬V8É>|½CK–¸Î¤Ü¸`ãXMMݬL1ý²Ü]¶ÈV•ÉøÓôŽßNÀU“1³I¼€´§|¿7KJ y&Râ+ÁØmX§Ä™ï+гž˜Bw‘P2~™F]Õt>ƒ³*(ïQßï ÀÞã Þ«Ïl";çôqÛ© RUÍŽN% }Æ ¼SáLoœ×#5Äh›e0}l Ü=‘9¹Íú7í0ô× ƒiý‘“¤J%Á8½‚7¯ ˆÞU„ªïJ‡sÞ ‚Ýô‰äëÌïÎ2È[ÇH±nb¯é_ýh¬aÓ—| ¸€k?b›6Ð9ÛH/Ç‹1@X¼PÞѾ+³i/-oW4º%Ïó øÁ+5 ƒ}#Ài±ÆýŠy édfôvÛO¼™Öê¼ µ¦ÿúaì› ì|á;¤ò³‹µ1¥3É“·‚Ñ~I±{wÚq¯6 1õucƒœ’8—<ý£Ÿ»¢)f©7 ³¹®|Wt/ÚªŸ>ÚSáÁUH7X¶NŸÑë¿p_Í[Â)™xÈ 6kÆÔ³>¾°¶ÌS${玖 Òâgk¹Ð¿*ƒ)ÀO )ÅÃÕ‡÷ßÕŠæ×¾jàùž:›¥NOo‡>ißöxÛΰ&J–ÕÛÜAÉãrª>zpÒZìb —*Ï™Q™$h¢wò8œ|kxƒÏ€¤zì\>7PÖ^éyª x#¹ì6ºÕÏbÍYÐ@Š?U¯pÔ Hry¦~xÔמ(å–{ä8ÇŸå< dâºÄ•mµ` üwe·ü6IÚ…£]PŠ5Ê`ÂæHÜRyk} TtÞ¿¾ ÕÙðìã6",6çWß9ºÂP×h;m¶@~dV3¬f[Þ8¦_´¸‰X'&\{Aà·U•P[]ö1uÛÏx5â÷⠽㻭ÜÜéóàrYî·'ýH¹ #‹¿ãy}p¸Í]:—¶~yy ƒmm¹á0·`KNÒÆ•»È‰í™¸ìBTFKžq%z¶"ͬëj*ëNfw8|Q¼Gïè¹ú^¨%+ô°Žx­LEŒ·º¨öHø±­ž*wÇU¾Z÷Þ¿Zì |….ï®Þ9û,ÈÁdØuüp½÷¾>2P}Nðþë&w ‹HJaS]}3ÿ™ õ®/å¸ÛŸz€ó›·h(S7“Fsˆ‚²åjÿaH0árEk»M,„=4á»øLd=˜yU<µ9?Ókk`íÊî=OtVˆÚ—ÒH±.n #&ÃÍœÅÖÍŽ%@˜Ib÷¦åãnÔ¨NÑ’*x¥hP%ÚÐ Oû5Ľí¯JãZñdÉõ>;>%”u`^ Nc“ϱ¼Ñ|¨¾öø3¶ÊBæ¾?Àš¶gHÎ?êš2(ßïò V)o}óçJ;…Žn©GË5|´>íŸßÝè÷‚2úAO‰Åìbü)å{Þü»|‰B7ãöóme>â ༦äRmÄ&ý¶ÞG2pu8]ËÐÝй ·Dã'¿¾”wcî6o:>‰ú·un;FM@=àȼJCëäŒî®æ™½ µFljÎLžÄŸ~wp߆K³= ò¾,´ò–dþ'‡… ~OhJéÚ×Q>¢ÿºÿž˜âÀeðt”âÌXTG}ºñãÐM?˜µs°½3£xí½}wÌÇ·ƒhFÝf¼­p¶1­k6î²µ•±~x“«âšÁˆÍ}Ám»ëaÿž;Éq½>¥$Ž%M«8‰ 'äé6G‚æÝ!—Ð'xã‹gDšA8¢6ìy¯5f²zÝYA3^“â sÚ»—ÞÖd׆Ò÷E¡mÙùoè÷mw×eÆ;ÏŠÑGg¥óØP¤ö8TeÚê]¾k2c øçaÞwŠí¡$!‡G¼úLœrâHuÔDmã ÓT°Ôºñ> þ”¯'N¦í*pê0^:v[âi|ã䉬6Ah´\ÝX·RÛ3ð|.à‘t}ó~4¾ïb`º›×AÿÔí÷»òtm5ÇB¹²rRO‡KÒÍÑfkßâÓ¤Zƒ£ãuðG+­~ñ5Š_ÿa°Qœ=+ñÈðÌ—†„Ò¤'‰$á–"g'a=Ÿ«¬ƒ–t:@âð å÷b/)GQŒÖvdIc „hà‡×ðfá{Kk”Ô½âHzlÃY³²¼¦ª´Ê\JtŽ\ôj×óÑÃu y1T9&í&pÑãHb¸º’ +{¬M hP‡3‘¸ÓªH›…! 3Wi—¥)sÇ6{7F» ±Ê#ya=Á—/žCƒ)P‹Êœ}Š+A~žÎÜ1,®ã²ëÒÌÒÑtñ=;{Õ ÊR @°óµ­½{Ž¥S·þY;#³›2|a˜ï¹kkùÐæž¦l:Õ5Ý{ÉO‡hu¿œ¥‰<¿ BΊ}ïÈ7š KX$×íðÕh´kʶÓ[ÌC²W ž>k4á9¸Ú=ôe öï˜C0r¢ZH¦Ïã«RV~îõ¸ãÍÌßì]’!寘/{åoîR¬ÿë‘­6êoÞ9¡©ÄFâqùd¹aD”¼×^§e8&[Ƶþª§p¬_©ìͽž˜ƒ):oIç%èt ÆÙ ŸóçiÆ•f_¾^å^ŽG·_ ªŒUœ‡òŸWãN÷ðÓå UléÝ.å®~ zGÅRÚÌ)ñšÕçÜ?õÚ<›n‡ßq– ÕÈŽ½”_YìTé‚ð 4.ϼwA~–Ú犡ƒ²úŸûû÷OoMœ+È ´8Ó ‹Âä.©É³rìèÿ˜¨yn¸#ˆ7šF¾ç½ÙD–-1#>SEŽ}>÷ôXMw4þ]œT8tîŠÑ9Þæµ îB¿Îä©â6¹þå±€œýõid½˜m_¤SlÛe܉?Í’Ù­z†Od¹^ÛTþÒ»ü¡³ünM˜Áp9%}/Òt™26M|:S¦¦ÏÁc“„ØTh"‚sI(o˜rxüˆýG5yÏ’öSC™ ˜zñ~µ6Â.–HŽõ?vZeÝøPÑî)†¦2cj(„ÎëÇ£´½òÅ ½q†öö͵Jµ^×OÊC€8Þáö¯ê9ÃàÂ,A^Á»Kr…PGƒw"žÈ%Ô`j÷cSM¯1tÆVó²Ùvrñý7ucéEÔP/%iGXŽ?kÙªf¸l¤Í‹÷:¸Ež1†³ C6hÔt}-%}¶A(³¾´ÛsÆte§~â9fðÇCA4,sÿ¨üìywî|}v*ÃîþyðeÓæ'õ /ü†€qê3ÛH€ãûšdï4^³ËЯ†;‹‘Ýà ÂÍh*²ù +ö„ÜŠ¹w®DïcŠ@¶åâÝ/ÿ4ÐRÚý‘x8Î|Noâ˜üt6äÉs¿…î[{'Ù0§xetÞ0Åã{žû#ºçõ:Vñ’#1'ì½ÛcØýÜ <¾Çfè°KxÉ£:nOk;ùs<$o›óМ¥O¬"œXe‹ÖZóW8µ¸3ÒXƒ‘§¦º(Ñîr1™ŒCçªXÚ©£ÂïÂs&´âŽmŽf ˆ½³›¸ˆæö‡-‘óÀ:M¨µ³uDc]°àzÄž„`EØä°ª8ï#]):[‰Ûü‹OL’ÐݘWœÝa© n±Ô›ï“X%ˆbÞOÙv®D\à,šñ3(^¢.;{üX÷ÉœÞ'qÛÐ$©Š,Ë¥/Mt>jï) ÐðŠê…ÿÍJçbj§Ôã^a|"õÚDäîÂS1ÓŠ[H Å^½=¾.LjÀÃe¥˜OÉð%Ií0=Ò6¶QÐ/üÓ?²O[„$9œ™Ôzb—SÙrôxLB}Ûð^lc”õÉz_°IÔ:ù±†r}åî*Ìe^ýø¸?OUIñÔúøk„^©Ð¡jµÛ£¨kÑÊŒîäãR!²Ø®gÛ"Ä+ ‰wWм‘wjeafIa ÉMbE`öt<¼$ß öâ‹kG‚)¿ >†Ñ ÞEñÐhñæŽPŽ’ ÊÁ–e|„EÛêÿñeš°BŠùÐ0¾çälâ¿Uùæ«©×ÓB’ƒýùìê¥ëÎnæC †€Tû&…K;ïwï€N3ìy77ÂÞób;`ù .KUXb³[÷¢-yz,0%Æ|cÞÊÓÚ˜ ý^—þ­5/Á‚ÕÃÌ<è1÷(©ÿzhª~,uøL–C­:¤‰ q~q`Ø"æ%1Öv–Ù«º? 62Av51Ù"ô©ÚäP¡q¥ +DJû_y‚jZ¼•!1’V>)^:ˆsÄvüÏþ–KUU,¸½e_-Žo·{bÊ”˜(ÃIš}ƒÜÝ;J.RªÆ¡Tæ“Ýz,á_-H_½i" Ü…JQG|WG‡î4f*=‹‹ÐÀbO.ö'™Æ€Â8D¸7×á?Lâ‚ÑM®N þµÓyow ¿‡rF{Qu÷*VwçänæŠwlØ¿Œ.f©DÅ{]¨#ñæd; H>CÎú/ QBmñ9Ʀ{ýdëOÅy˜8ÊéÄ¡V””u𫬸çƒ_€%98¸,ƒÊV®³&fRe¥ÅJJÙÿ8çà2izŒÞêô¬hiB×zc¿”re4¢òÝå•(ÂËnÿôH<†¿Gêq”Dâ\Òåvÿ8Ø])ZÂ×ݳ ½ÿ~¨¢ª`d¬ƒ(ô{¡Ðñï‹Μ†£‡[ëý˜¥ª]§ŠÁcö ³øÂÛôœÓ»ù¾øô÷M)[< ¶R4úž òdmn†Âçx‡™h離TàµðÆP¬¦¹Ÿà`¸ýÓÚþxo» ƒþÖ½¤ø¡»²˜G•î@­NTM:<ÉŸé=˜îT²"xe{€'˱f35¨O‹Ýëœ^÷iòðúüº2y$K>`ìߢؿUXëÙ9?G«ÙŽææÜí¦ÝŸC u)›þqu³^19¨!E miÚû…kæuÙß\^ã Í^tߦ|HÙ¿õ1ðLa^ðö˜6¾1Y# ´í“70áðÞ¯9¼+àø"…åàÝ×¥ ­’Dt¼ÙÉ¿³gôñîVÁ<Ög~·ð6 w æV°¹Òp¥Âa „á>1ÝFw9gÛž›-æ8¾q}]\7s^Ÿ6.Ýڪݽ›üº)®ÑºnçìüÔ„ä„ C?3©®k­«¹›¾éPbnÿ°¼°¼zÖ4h4ÿ5Àð5?QH¡:0Éa`¢q¤N€°¢…©l¤¯¢¤©C„®¬©®EˆmHˆFwF„¯¥g§¯¡Š…¥:3K0Ÿ\_3 S™://] ¯n]~c0°y¢Œ­¯¤…l%îx¯aüÀÿÜJ¶¸b–•8$À9ÞÿòfõÿkÿŽFæ®ÿUkóÇnSüÕíµ©YñZ‡3àù©°±¢àŸ"V!ÈÎKë…F,?[ë—QX½Êß›—wë˜>„ÆÍÜ?@/عqH95¢l:FÃ/¸”Ï@)Þ©ýñŽûÆxòd˜¢p  $éí&͹qó&áFÿc‰~ ´h¥¹06Áz0JLA’îïA“‰úÇ$íc Œ”½_ˆ% +2wi :# ¦Bø©;UÈ›Ÿh´‰ßýD…¼1q܇œâG’¼¨Ë­ÄnŽü»’8ó :€ö®ÒòžmˆØßMkw=­Õ²eFƒzÁoÑ¥TS™‹ ðáˆeâ"”^(ôŒäÞ¢§I´¤ŒÀ,Cb©áÁ‰Ó…Ú´ýÕS Rë>²ÜHd½äH'è‡tÝÆp¢¢§Pb(Š $&DÇç^ ÍD¬rÞñ`ÑmŽæ„¶”ísÅ$R´·$™£Eát&3«ÄZ¬ìž‚6(ây‰ð‘gÐòßxæBÑ(Xùü^µAòô|G†*Á"%aQò‡EBÎ$ú#„-ãˆÂš„<&xLÏñŽ$ “"Í-Š ái ÃèÒˆPÅÄèA”ìÜùÁÙá€ú7¦+‡šÇ¤¤iéEÌOá{”íê«Î&iâÑé[*2Ø0¢@d„€@÷]"í]DZ Ê é!@âÝ·”' `$ò§ùh ¹fúò?r–È´*$ ¦pÅ­ûÁ€‘À‰ Ö§€þP„K$ÝoÃ(»x´=D§ÐØ@A_ÈŒ½¹Ý^KWBáëSŒôk=è'ºW.DõÃÓÀnî.ßq ^œ½X9ÒäjŽt¡y¢ÆüpÍNpçK÷Nî—ºraK ,üÝ· ÷ÇGã=8²eEUÐÞŸ#D÷H0LL È[…OvY¿çÛò_dï{ÉHIiÁLòý»Dïï׆@D0ub²o H ¢½ðÏeÍGz%M÷ÝóËÐË›Å.î£=çX=j  …4ÌïIò„Ô„(ÇÄÐð‰ÃãÓµ’,¹ eÆÉãÚÉ ÌfðÃo‹0¹âŽSM ÞIéÃsã u·"w+Q»Yw; ï'Z›~.ϧvv:Aí^ òÓ1œÔ’c« QYû[Uwltû7åÞY•c£ØPhØÙÉ3Ù}|~»šï?œ½ßU;ù–ã3ßãA“Ùe‘Ȭ}?°ŽËª ÀÿĵóMk­¤Ðf:åÙmuÆ`ýé¼eu&œK<äÔWVÿà,–˜¥d´M¸TO°WhRpao¢zYæO©VÙŽjoá^CP…•ä–7c°ÖF¾mäÙà­EÊzÀI?½ØÈù)ËÑu!«9:¨ZsªõšÄYQ¸Œþ¹š¨ë<3Ä»l×9Ðÿ"?TÿC­ö¨Eÿd˜Bã.g²R±º]Ÿœœ3“ž3gHcšÕW üG›2ÿSù(í7tC±0‹¿vÊ·®ùÔüU¬„ HPB Äi'˜'ežëBdCé¢gOÄöר‘ï.ùvb³w¯âæ{âÙ×¶Àlô³ÎÀ² ^´¹-LN‡õŠ@oëGn*°‘\BçE†Å,æšØ€3ˆAš1•ÅÅ R¯jÐT±’>ÿõ2¥iØ"¤ I}¯G‰~¼›í»ÛÄÀc?AïJÑ9ŸEè¿ËéWH`‘b)Ä@*I™w[š¿÷E\ö0À?g©K1íCƽg—K©'Ž/ö¹hHŸ tm˜â7í7ë®;Èâ ^6ÞL'­·Ý.%+†?7tþõ–Wþ?æù…ù U'5ëùK7ŠV››ÐH˜!ήðj°Öõ² 8>/Àqiå?_N!yõ¤¿šÁj7_šc&W1ŠÆÛª¨ð¿ªÐÚ¿¤ØûÑ'Ìof?ÐÊ…}Í:Ûø~:]°Š[½µnqèeâ;DŠØwŸŽµ?dâK;dãcŽª;RÖ}{g¾†q'Rc»Ý«Ÿé ª1!»·±F7ÿèPvÿr‹×{ßpàQ ¯øþ;$¢­?Ž®ÃÜ“ýo€ÿÿ„,£Í;5Ô>¦/ƒ:¾Ãºá†¥yÙ¢pÛ%gJyÉòÅôu j¤.,-—ËÍœZ·Ú›7·U&7£.¼xea” H<ñÿ´”xþBA‚&øûiü~èºo@{ß³Ù¯,Aù´õÝ.·Ûm¯Û޳¯ì«i>ð¯ßï…¦;UáǤ¢9—Ïa;-Á÷g³ˆJ*‹=$ªÔ‹-ÃÖŸhõ‰t_Õjã»(KÙeGCRòêc]ë·ð݆Å)&ÆBM„Þ†E1&s¾{‹OÿøÖu<ß\®²áQONQO-â¬à‘xLºæQµÐý<öBäèô1__TÞv2Ösã C-AȇÕI‹ƒ<Æ"¡‹sÌäz&nÎÄØXh¿åèØ’"³¿M~ö¤XY¸¨"Ç{ºÏ :¤’Ë’Q:ªŠÀLëk• e•—ù!U„*zN]0¿Ü‚ÓËŧª’ÃÊ? ÞɬÆÚîÏzB£—ï‘÷‹YŽ–ãK‹>Õ’e\eçèg3QZå-ÎP3í~¸Èª?ouÜê> ½º¬zRÛº6VjRÍÕç /’•i*«<`ÏßWèt£l|àôU%o£",üù Y+¦Ý¤EÊå6Ÿ;ï2–ÅâÂé@-íIÍ…I¨Œà ]â¬ÃHG<ñÒΞ„©ç(1çÏn ¶*õ ¬ …Øë÷_Ë8š‡òqì–6‰]0xm ÐÞúYÙìÍ fßò8Ñ#™…bÕ';aDR@ Kä…wW¢êa˜ :•­_ò¢Âø+t­àöx°xÈ`#ˆ2;?üóŸ^.ª‡#DñÙqÐ˽‹Àg•O¿ix(rûFº8¬E‘Êé(£PaÉ+hn­Cùµ³†‹Í^G¯ÛYM»ÓÙ\^F>SYy½\RÈo’Fi¤Æå7ïÃùèhfEÑîÊ¢ <ÇMMo§hiíZ°ll&Ÿ¹¦¥ ‘ï01êªlú,êo8bv®÷IÛuã`$ůNÈsòM»0•¿Ër©•=‡¿UuîuÚ‰YYJ'o™ Ç ªÅ’êU!/ÿÊõ¸Ub)µ|4,qèUñ" ò¹ï¬ü¹aÞUrëEk¢Y aÜ@AÆIðÑBhœB§û¡ ã˜Äè'ç†-‘„X+¹zˆ>?9“Ä›§é©•YÇ*áåiõâ¾¼…UÌüoK•ñ¬%®+æ¥f E§³Œû«Ò¬%¸ñWI›†—®ˆ³L¸wÍE³Ç:ˆCWº­ ¸úmëýÝ‚‡<‡‹¤ÿ©ìLn?E¹L›±BŒá¤JY*²J+ %1§`c/§µì€Tq}t£]®œh´à?tì³æÆSìÒ*å@¼É›[×뙬z ˆ `¡1 Ðr˜©© ݤãIå1Å‹EûLH\¸¨u8ˆ…™CáŠSÈÎ;ŒõÈ,ðý¦& Ã}‡­>#sºSÅ•Äp`âK½]ÀG42úÄÄ.ó´Gb7ec¬ã„°dŒ+ñæ“ð­AÝÕ”;AÈ“¯z—¢IW“S€×0«kŽl|7 ˆÁ²6Ï$/j\Ê®—‡ÊUk9½ š›`ßh¹¼¾`)Õ1%ÄBf;9„6""‰ú÷›v•eë°Y‰<ø ²R‹¿nRŽ>& P1ÛxõvÓq;pÂ\vµSÜŽÍ “ì õ÷ù6c›ôµKåx®û“W’Û‹-E]À7w5ñÔGKqZŽ©N£â¥Ý€ùBßÚÐJïÜ£¤–¤šv±—¤¤šn»Y?LT6,ÿ!jÁ32±È‡ÕŒ™¢°b7@'¨®—WPb+¨K}Œ±r‡ÁÞ ? í˜Dæ²A(_È7<.ºèÒ˜žX eÍ(”`Šó´úß‚V‹®ýÓX†ÑÜuq=`H|ÀÙ=­éæçÚ+ZñÉ#;hvæjQ #@ýŠü¡gЮ׬d²²s:ïW…ùõ×3OãÏìóC%ó0½9¾“pk¹XpºÖ—«*(³®fs­ƒüÔÚÍ1a%kЗ6á˜ÉT8¬ÝùˆY1ßÈ•CÆŒBÌWЏ%¨,:ê„{p¬:ª¼·]Û/è•üºŠSÍ?V.ìb΋>äÁö/)°¨.x¤æW¸˜±ò ðck„xN/23ÑÅÌ6àÈ¡ºÇ9OXhêôôKjW-£Úé2÷ÉÑÝAŽÁ%ì:šÅô%±5§m½„„dçIŸoþ!KÅÓ±¯S®LÐ ®]YœªU0o†êŠ)ŠÄ)‘Í÷؉Vø±RÜÛx—ò]•Q()$@°6Þª©ífš70{œ×„L‚?ÁÊTÎiþñëÙ«c).œx\L`$RAt ‚á<ù´(̺›[*[^7¨ud:j¹¼Ù‚x«’J§^ýw(•­ÙŽŸW\®2Ìi5ôŠ”Qݘ-ûclPøØ•Ü´¿#&¿p £¡åÙD…òÙ[ɽÑ=@dâhnÉBƒ2i¨p¦>Ñm½!®+øª¢¼e‘NÜ<œ¢M ä…»ãʃϺ›éÑîMÝĆbcJmòP¢H[%÷mjLã«Xa—$™Ï—:¹†<†ç¢úd;IÔ#Iþ×úä U¸cÆV$ 6†Âz\ÜbèZ¶( É% –+ Œ…è -sËüTÝ(È äÜ?\>Ò0èZ¾5ÞâÏ$›ƒWË"@Ò)`0Q{&|9¿Œ{¼qðõÿËRQWà©ó¼Õ ä†ÏâHý”}¬´KâÍ!/³ç)•Ó¤œÛèr0§™wMâê™±Åäñ/æ¼=oCZFDñ7Oü/Œõ‡µ$˜Ù#àqé µ%õê;9Õˆ89ïâ ²ö’4%xKœNž‚yÉtµÖ^sJ”„)ç“•8„5Œ#@[`O`:¤ûBˆf~pó·‹Uäe+¨·Z•…˜/ÿœ›TÅÊ~?› Ç)îŽÖ9XÐi]€Náê€÷bCfÙG•6ÊH:úF$ 1[µzºç`fz·›©‡™k¨Žr…>U‚5ò8˜ãjôo‘1´Vv…g¦§Aêêà®,õ×8õo…yMúAñX‚ê´…Åž6ã©Ô#-ǬàŠ]J§ÑX¶€Î§,f|l ±¥Dïð@ÇìË»×ÅgçvÑ0FËO» ŸYrA&¾”O^qÔÉ ºvëZ[•Æ¿7•„åâž®¥·é›ËhÚ:˹NË#Å|°x¦[ÆÜ‘=4¢À«õ<‹†ö—hzìû?:ÏR¦±.ªß»²2ŠRŽ{G. Æ¿:Å)RÊÞüìv»8çnD5=~Û^ f+»Ižu;.R“7”ÿÚ’`VôIzSP"^íéÖþÂG’Lú‰Ñîd#›íçÔ¾—léYèÒb,œüËSKÚ>Jí÷:WíRîw2¹FI`c{5oÕel“Z?™Ÿ˜^‚tª.…’ )f”_–úúßõš[¶xsEÚï°_tœ¢ß æõ¢ÉŒ®…êÙ36[QX·àpÍö'ö|2‘·’ŸYKŽýÈìiyoùsüž¡D¥½]™¬ˆ 3Ï}€½§·k>@.F4ÞD¤=Ñ`)÷mGÒóí}²¤IÕ„8n r›‚ïºDÄàu—V¬®‘üúû†Pþ`òæ¶>`FW…O¥mDZ&£ë¢õ0æbâ~°ÿ§:ríWì€ ýƒÄúß°Ð=ìÿëñ?úhGÃÆn‹é÷VO€¢_CÐfdöuS×U‡àŒÝ¨Ë¡«"‘?ŸtS•J31òèê¥oæŸF")y×Ö Û'“ ;S¶;“±ì8]RF5JÇ»ÆÂãu„v5¦ZNÒ,$&ù…lðU ‘YÐAÔ;¢Q9r-$_0ïQ"(É ²° F‡xEŸÁˆtÜw$äññ4.6dµ÷¤£/\ 1Ãb.E¢µ „ªÁ0„×£F¥øxDÞ°Šâ-S#ìŠhÔDF{ÿ¡Ò,‘%Ô2ʹÒyM¥spfÎA›%ÏŽmíÊdg„zžh¨|®oz,ùÚ“>ʪïc¸„È—Ôú¥£˜9Ôç»3¤¦¨³’À¹&}ºäejÔG§Ø¼©%†=¤`…pû´òŒzˆký%`z”£¡Îô„³mBŸîtÙÞý»KçûÖõQà·§ƒ——COºì©±«ÔhÓÕÔ…!gWè.jGû¯°o®®Þ+°RÆk2Ô2~2ÌB45Ö fÕ¿zÉhAFßv(Ÿut=3ç™qNãÀ!WñIaIçR }¤‡rf,c'¥ÚÖ݆,6®–z÷'(µ$ (aF"!·{( ®|m" ʼnÝnZOiˆ\Üz¹ÓªÂ4q¶˦¼éÔ qïu‹²&$ i ".A}¥áh‚HäÛZÆð95“þ&p<ëŒUiî®e¤†ì:¼(J2Aú߄…FÝ —r’Ò ÒAñNÇ¡Bˆ¬vÛ½ g䤇›²¬¶°½‰Aì?G ÑáOÎwÑoõDß~oåèg!8Û˜‰“c´-½õXo\ß!2Øõ£ eqšý31€B·'D’õ‰G¦§§Ý%¤zÔ‡îø°É'ë‰B•8’Q—\|pD_ö3ÕRþ2ɺ&30ì—ä|íBPî<È”&™‘¢0ò>1ïô~Õ.¯WuúJ€*<íX÷^CŒ¹ÖaD3±-NSz¼b‰q@ i€üÕQÑ«®¡œçpв½À¼ß¥Jº¹iJ¬Ô1õÏl26 Ù¶693ƒÐgÜö"Mî߲ܹ#ìµw%›B ÅÜÝCQ$Š(Ä …úAóÍ•$³ò°™"³ÍþšS­¬ƒ,Ñuô î¢ª¨sö‡p ˜gþcŸ i¦]6Iß—êÔ섯Taºœ‹omb7©#CSª}œ:¬é'Œÿ¦Qdõ³àDC2L˸)e‘ÊÆÏKÅ”­~œP|C[~è„{(Ÿ!¥÷Y†¦åE™k5ÓÄñß²ˆz’ê3\72<ŠØƒ-íAÌvú<Â.0©CÆYÁ]h¹lÔ£P¹1€^{#6r»Jép;ojñÙ{xôĤžÔKä¤VtƒJº³4"YyI#Æù7z5ÅèâzO})CåßZ4{»z½Gðü¹)«[ñ°`WÆD¢Z…*NÇúÒNBw˜¢’B%”‚G‰cåmi0D“c§½E4þ`3Ú㡱ßsàÿÉ’iCÔ³®ºãô¦5•tÜ¿Òv¹{aÍÍbײ¼*jÎið¹†UÐ —âTÐÜÇÌ)•ºÌÉúx”üŠ|z¼Ò%5ŠîyÛFN*=fW/ê‰èÆ-óRÑ-ÜQ+ZpHþ\<^¤ñ̇±2÷;¾Ù¸¹#Ú–Œt‡G·’¼ æá‰?¸Ksó»Á݉WÖ¬ÑgüÆbtÂwˆõs‚˜êŒÀ]¨­ŸáŽ•øãpôºösî/#Òœ]lsa&ó.ÌQˆk\ £Z™€Ó¯zT1¥ŽSæ4Ç……t†·÷w= †€pÓ‹ÏSËW뎃l"_O'P†'$ ÝÕ ä·µÚq$©íš%º?]nš¹ù~úÿpt<ˆq æIåô½ì-ÎûzÃýá¥_[ZåüGøQí„›àäÃR‘<¢bæ±ç+õ{÷º[X@ó™Y_çbygm¾)¬†½88´|Z{†Ë£*ªõç Äo˜­Õ庀a,ˆ/r¡cyr»ÿø—~ÆŒ«+»§Œ#ø¯cüÎRC~®šIø•Ú‘ÏXòï'$kyÄF#3v¢Õ@k€ÉŒHzÄK>€åà¨Ñ„3€è3Î!¦NúL½±×¼ûFMA4}! ¦Ì Éç³ Îd=O¢Äý1ÅÐ-½[ «AÍ4W¨C’}Q’ƒŽ#8é¹6®¶xlOÎrÙÔGpðN-6ЋׅÂïÄ4õ” É)¦üÔÑ&Go.0Kf`Ñöj“XÐÆŸxÕkUQÝyaïrþÔÚºSåí8n8 7"°$g;ÖKRœ]±Rüì“®±Ùîôô ˜uÞ´µ8Lú$‚€jïÑ{8*uu¹;ôCØ;l? Æ PíìƒbÂx<^ß‹¿{€= æ­ëÑÉ8k æ©[‚E:ȃ^²ß~@‹ä@QSØš²³™ðq<7ÿsó ¡éMÙ Ò°êeÔÅ,è“4 ì­S¥RJ­çt {ÇTQ›;ª\8Æm.üß|r½ù¸ FáÄ qÐåUÅ]L­Ëbé U·.+8ÍIë`LÇ×׳YØ$ŠñÝñ”b™:{žë|Ô©Âe#4eýfÈø=1=Q¶ÍN{üüï^æ»fÛÍ98€ óÿõ›¸;›Ø:YØÙÚ;Ú™89ýУnc‡%ô;[WNŠÏí4j”TˆŒ-kØcŒ^£zm›#Ï´Ä?5ôlí&Gñ¸WW©§Vo‡œ3«çŸQY¦›wiÙ篕@Yq2Ø¿¯T¾^è<ˆ^úRøTkÒ¡bØÁˆPâ ó©Y©HiL„qÌ¡NiUËûH-°ƒð(øSIt59”o|ƆûCƒpÏx!( dóW‰ëgwÓ—„Z½e‘¿ˆ#2Îq?–œ& X¡Ö¡/2i@<=‰õH/+8 +dúHú«¹|âßaag"½ÑÕ½mÍ ÙtK¦HCçD^Ng'º6ô`ì@«•Ò&2Ëú«FŠƵDµ—<³0•mÃO©/5ò“p|' IiN‘„]Ê^h,ˆMzšbQLåưJoPíÿªT$í‹!-šgøÍ›ùëýæ Õ{~taÎÐåÕÞ‹ýÇž{;ú~ 3z·Ÿ¶ð>g/šyWBô· !`á\’e¿k¯O˜#Ôy\¡ŽrD™Šªå¨>§Š—¬]RêhéL'”¬]ÒÀS$iýz«³ÉüÝÉBµQ7+}+‰²È¡Mºã”l]ईÏé½d¨QÏýÍh8£U’h¥¿<ÎñÂPÄ*[ϳº^R«W¾Ñ=y¿éM¯,Yi•á°³Á'üUÛs³5ðܔЖBHéÁ„ºZ³’ gÚ6tñÓµqcψùÍœ½÷›¾36æ×{íªU£:Š^o08M¿Ñ¤82ÅØÔ33‡É[BabBô× ü+°m¥pÖºeóõ±®NC]IÔ‚˜…Ê„¦uGyAXNÚ;†§°¾&õ€Ž†‡Yã¤4Õíߎ8î!„J—~°j‰«¢OAå‹å>+i¬hØâI:&ÐÛ°‰œ¶ÀVI©íÔñ悊ï?ºâÛ½Ê"bThžë:û@ qT:vˆÊ“nK¢ÎEQ5_בÌ!¶ŠX¬Î¬°€Hbbb}´ˆ ØÊˆ&ƒ;Ú¸Fh>³i£Ê¼ím}ow®)_—“Ì8W”~ 8Ð^¹µ]ä]è4;uNiËMšÅ«Ýr­t¶>͹چqÛñüås~P"K!tÂ8h'e;Y2^˜ÃFÅ•>aü¼DW”…m–sÖyº¥óLø…9ƒ-}<× ~Ŧ­ðR.¸LÇjò¯”h‹ÎÇx„µ°×°W Ù`ò;šÙ®WÉO؇*ÆùÒ27ÛœeêÒòöC Äuxø:ÚÛ1Ä)xé½¾\Ob3Ê Âø¸é›XVIX‰T£Jµ3¶bx<\°ZfPÒo,7^ÈÍ™Ówñåµ¥{Œ·¾{€1ÔXĉ“³‚ãÑŸÿ4œW—~&î= G{üðYíÑ~2wøã5à *h[wásŸ&d¬8vRîTœFÅ…@-oûЀÿ¾mÐÜÀ í{ð8çÂ|ŒÌò ¬ÊC¼·êíÍdÛÞc¯ŒW¯• Õp˜ë ó¡—yŒf£4†wŒ‡4ú™Fxˤ#g†: “ùw5h± w.•×°iú/238ûiêæ,3ªÓ}9×s+¯KïÏ»÷óÑìáã$Í5p·6#Â5É€*óv8÷w‚O’ÿoø6ñ;SMJáÛ:,9³X29T{Âz0Ò­ÏJú3H?óKnÏ+–Èᣠ·‚k pUÚÛ)~Æü­g½áôfÍwFÖ)–MæãàG:"ëÇ#.0¿— nÖ.&*—Ëެšë;E=é=SXÜfï‚3߸„V2þÉÐBu¢âà>4=j)ÄöW,'• ¾,¬?yòDÒ¤ÁTG[Þ&TûãÂF4û?N©,W¢ydBQ›7ÜfäL¶Z>Ñä#`cb›é^QÈOGÑ`ò¾iúÃ_ 4ô?N¯Êkø‹ßìxN3Ï÷Œ¦ä<\NU'ÿío?vûúÊwÉim(ÓÙ„¢c†\÷ö®MzØ–ž+Žv×.à+Õù‘LÃç‹>Øí]†ÍÓ 1‡ëŽg·o«Ä˘öÛ’¦É@o&pYò”ÚûUñ4³Î¨ûì#¨ÚÖXFg$Ò) «z¬8÷‘ïãT¨ÖTƒŽCŸ#“ˆB–oL1Ünp8QaÚõýÚ•€O/¯8û¼_mìÎsÛ{šû7€oà®·ˆàî·ƒ{ZPOtÊ’Áë°¯Ñ+>\Ë-¡ìŸ•rìÛXðö:lÈ¡…ög½exÑ.ºM¡³m—Ë_ü‘ïä5tµ¿õ"|±ùé5‘zì'^ Ê½hÿ@?^ ³Ö0n`ïXœË5óã¢_"Ûðÿî>A"ð%A˜ÿçËÿOr0jQ¿±ÅCÍÑP$ÿu%-³m„ŒO3® …R‰, ((µ!Õ¨¯lÖOÛ&ìXõ51cM´i„¨,”X §ø,«[P¯ëΡŸÏõ2KC!å’lu5s;Ýíp:Ëe2SùZ³üÂ×ë{Øê¥ðÃùþ…éûtky§!h¦ ¨ÞŒe·o¿ /';ìd/r—À¡Žh'„ÃTÔ“–Al€žÃh¤$}o·ŽÊˆQž?„ÐÜV'}óÊ1 FøÅæëÊÁÊ<å¤å%å”NÊá–{ ³ÈoÖàúù ’>é­K—:³æLô^qø×<1D§|¾Tiur'¤ù£gÏñÛ€!y\Oûè6¬^&˜}Ÿ§õ<Ê~h£êGR\ëNŸpØ”ùŽaD&krÎ]„™GûÐ2P›il¢yxÑÈî,m›ãFh“yÓœÕ@ ‡$4pÕg„kƒÎ×Çl€’iv›/þÙl¿í}„÷Q°pK0óP¾M-÷%iµVlËÓ$Žy‚bŠB¶ÍçfÒàÞ_%ãæ‡r™1VD©]ôÈ.í]侺¤;àŬ¥ÓAùÈTóL¦ecÆ««ºU[F©&¡§`Ü›….D6Tù× é! Ýts‹æ wbËißrY&ÑU¸â{dèÕ3É3,h8¯¾×Á(’kÝõ%V1õ¤oÛîiõÏ M}'[ ©bä%Éß«y•êêyÖ6“âU^cç BhPS<¥}Ë£¤rŒ:†È6‘m|’zÁVLÒ$t’y¥4›†;“,SȤуrÆírõ€þøv×ëù—Çwú—BMjY l↱'4°áêbÑ“…\ïY¬ý9’ªÕS |†ìQgÇ×¾ÏëOLË’}û1½ã°íWžÔº#nɸŠßã’FÅ >;y×ÖtnzËDYǸSëÕeJgèœv7µ GT@»H^Ón\Åc‚êqf³RÛá³ÙØ0J4do)6°×÷&\BH:sPœ¡6Â!GWVÓpáC,˜f›i}”Æa¿9'áñM‘Ä~ÅøzMø·„?uÀÔx1› “±/rGÖbÈÐCî÷Â[jXu…i x ë•~å ʈ¿˜Dѵ²ÒPXéä·’­¿ òÇÿŒDƒlT)Ô¢þÂ×?íC¡|eTû†ñ\A …n@|º©w›‚z¦Wõ¬PRbñøÖ)¥ {otîoMÙ+½_bi³J7‰#úOÊ“Cˆ°LÑe3a'Öæ R]3înP¼ƒ³¸>n^þr^„Â~<’J§cI¤N²wg‡)¶¡ã|̉j Ô+ƒna—¡Z§(|ª†CÈIzþ“lÒgÏ´ùé<‰kë6Ê+é5 _6=dœ 9ß•fÃAD?«ÝüÔºµ´ Äú²dÄø$1EÅ$|§«^q~ë¿×1¦ô»ÃGe˜¡ã³õwñ¯îiP±¯cDЦù{Zµfu´·’G³¶fK"®RB™¦QDÆ%Š]ÏNæ£5Æ^è²E­{ÎCZ‚Å/¥×ng%]W,GÜ5Tí:¥&“¹¾ƒGjZTøô†e¶&pCç@B«g˜]`†@x`C)3¢tÓ“)pñBU"3 îî7‡FÂKö3¸-¢¿ªøxn¤Ÿø0„Ðx©ãh•…Ü_‰yÊ•‚ÝZáø0Á}E:‹‡iŽêÚÓ&c øîh wi¡qê×8¾ ‡Ô0nÛâÄLÚÁ/A„‡!c(6®)o˜¨úo$£H«‡Uî^+<ý(\j9ßOˆTÍzýhFˆ…iH(LDs$ìÌmŸþ“Y–ZX‚³7x—ã+…ÚÆñ;ª¡é°G!> Å#$”)‹¶ÊÀ±®hõQ†dµª\sîµëßÙ£r®¯ÝºN9×××]¿²¯ã¹Î87×™˜é]«mÃ{ŒUŸ¼$ˆ’[_p€Qߞdz^*ö?„ŸQ¡ÙbÜbÜEÿªÒª9¯1Û5]k'§V(D/ÝHŠÎåªåä/ZÛ ø{‘”÷®ü'±ê‘o$bš¡ä(DzeÖ7¾ ‚£ùÛeMX™¨µçÕƒš„HÞÒ™á>õeòàç¹\Øpáژϭ,¥õjåR!jIâR‡7— ¥Ê8ìdeØ(ã“ǶŸ)нPRÆLO$ /F©G7®2}l´–Y‚~z$%zÆVz7ûøVÄø¯ñ)9Ñ\±úÀ‡Ôâ"Qš]~§3c{ .ÅHÖÙ£‡ÎTÞ4I¢OM>"˜ ϱ‘öià•×§P eYóÜÝ‹¾“Ç5 GHÈ(­S{ËŽ²¢~îïuü]Nµ`g¦2J·å&*ø«x}ùãM`é‡ä¨^5õGëȘ`ØmBK”Z;õÇm²Z‰h/ q>'Él¥¤hÖsÉ™ú°è?uúõ«:Âù†â©²œŽßš€´ÌÞbˆþEp²Í1†C¶·U‡(]½²ÙÈ+uýT¼D—y%Å`!éf õ°I@!( ’.þxЏë=t˜ÇÕ­]$é–eÖ*Éܹ\«áêÐ0%ôº(œ4’05q® í8ÓûÏr²E¶ÛºPrU*knñ!´`„]è·îÇ-4Wõåÿ;W3ùhÃô|Þ-¶§€è5Â~Ç “(c™›q&Hæ2Q5½©}ü"¶‹•Ÿô´*ÙÊ­É m3û7Ttâ6û|:’ÀíS7Á£ܾéïî »Xq«]»—ºvÂâ!²›hµ²sÿüä>ÑÌ79ÙÞžá½$Û[Qz†‘†[Zrý¦kËDQ.üN}2ÝDðZÎz“ðìêr«Ó~^ìòK"D¤È×£`1á7kã1b±œº–fG·÷ ÿßý‘&ÊÛ—ÿPŽ=ÖÿF&Œ©…µÉÿ ›u»#Öÿ3Ô‹ßúŽsr Ð&ü]ñÌ’¹=ÂK £QdËšDQLÑê îç.S”k¢P}ÿ™0e(ì‰W(·Þ«a~²¼Ä$è-³‰‹0Š»º½¹&i²KáC6je³qê) è»Ä©Äˆ*™d”«añ>•ñ sƒÅ,ˆ[·ÚCâ¢ÉÏÁÆ?GGô¬: ÆFÎòÍ$8‡Rí.]958˜ÞúŸâì»ë€ÓT°1tÊIEøI`SL½Õæh0ÀCV˜¼6°Ì7Ç·f¨7µkæAS+k'×–5«‚ô ¨dݬâ˜ùì­(ÛÐÃÑ= H)ißd2öóŸ!s9ç^;¸ÚN±ô¬RæÕL€)êd× TÇ%SóÙ*Ž<’A5ÚÓàj´Š$8gŒ~­zãë~B ÞÖ5~ÐØ‚>}®èRyŸìÆÒþ}nlhœõc¿-ÙQžèÒ‚1LŸí†þ7ö— óoÅ{:TëÁ–vÑ7A1?Ó#ˆÍ븇Œ‰#PF6ݰ"5?]å!ªÕKˆäe}ö•4†s)à¿„µH‡2%Ñ{æ«…Ù ØžÏðŠu"p’Ćˆó/"»Ök—K”¯Z=¼Aã`d2 tG>nx('… æš&%ö›¢¯N‰¥n/•RïIV±7¨Ú;[“è3n´G¼!Öp:(b°v©qrå Ts Ïö×5„ÑT[ªp˜É‰WUË2¥—ÍNc}z(^·?¿y‹/¦\îhÓ‚ñû=½‡»ñÚµtPÚsLh‘DÞM2+gÈE¼wþîÛwÒ ¡¢1·’§ÚQî,¯c`ɾèR dÇÝÕ¶R ÁÇ1ÜÙ?Ð196pF q.]#ìmø²¤‰ãï9Þú˜#a€cå •q7¶ ïM*{‚tžm9 Ð0‡Kû0MØÊÄãáj´Å9FOIKqÎJè½A‹NX™íu$¿«V>ÀÒH.óÔ¯½üíôÞ×ìyu  ¨ýQ&Â4¨CT°J Ú=~e·žæ€ ñcy}Í·ÇŽ×'»¹”Hó¶~ó?øùëP ³9Š\™7¡jµÑ»¸êÐP w]þNŽ B5zÚ#p¬Ã½¦'«`?s¯¥q˜<Ÿ~EK–hŠÎL£òã2~¸‘:JÕégûî}{ÊÀÆ¢ÁÔ&Ãô«‚E1d*»ñp­þkliˆ`QûzFP7\-QÕÊÊã;I~ç»­ôÅöxƒûì`Ò„T¡múâUG·8HÓƒ€§léz˜Ò”¼$5¹÷„Ô¹7å¸ýÀˆ^~˦ø<ÕÙUÓsgsáçŽÒdp0¶ÊýwºùDap3fû#'Öv{¼U›²y>Jç©Wi˜$Æ6þ"º¡Tn ÃWˆ”.ÎÔühì0Ü}&cÌBú … ]ž¤K?»*fœç’,ˆm()Zni3[¡*ÙÅ2»¯ë{?›¬ÏÒ&ù»O¾eã2P:õêuîì]…Æ´ë }à±ÎœfxW#›À̯~ጙLöµùðâÅt ‹’yõí ýïÇ[ÃRÈån/¯І·oÔ@<ú¨S//îaB-©²’ej¥€”GM>¬+š“]`öŸ6Í»mÉl-åoX0ìNÓ߈Ѧå43Ϙpà³›Ódâtß|Œ’¢¥÷’æÄ÷Ž`èneñêÿvo¬W|œ©ËR¨ Ï*I.WZ=Õ,Ôñ_z¢qw ü:j³ˆpÒ¬8Õå–x~´Ã+ «ç£ËèÓðà dö9»ºØqù Œ×3aÛ9T°ç‚ÖÒIiâb …"aÐÚEžÔz ³Æ˜ïPÊ…æÅV¾Ý΋’A*ã%0iØiäiÂfêû|c‘R LèØ —T2k÷Æ*¶¹NY5ŸJ£Â‰ï¨2^>LvhêŠ9ÑíËðçè›LÅd9ÚN˜O{:Ë´#è´yìuhµ‹;þ)j¹T»Q‰Õ}  ‰[5€zû–ð?¿š˜êäÆáÁªì“x8rÒ C«ÎèÛ65r–r§2l÷ÃÔ‚ÍÞ‡+kîƒqÈEuãºÙÇËtﮟN>Èïß•Œh)Bânê”tìç.ªGŒ‘V’‡ú°yÛã„b æAãÑÓ”q}s€ƒ°·\éð}GÂÝåmŸÙ·#øÃ¦ûÇQ~6 Ä q;ªé/|ÂZ… ËžûÌÏ༬ó`ð†Ñò¶ÀÀ×÷ZJŸÙ+^{ Íf‹üÔ9éüä#óÚÁ Ö¡-7@n ðJƒ¡)<ñJ"ÌV¡ˆ‹…à#^³R¹€oœŸSrÔ Þ†Ïh> *4« 36ÇWŠ)ÙœöΙ35Š/Öž{–‹QŒêʃ„HÒ€Ëz]†×}FVSbr›•ÎN¹)yŽBT–‹Ï£Ü&D'ì¸W¸ßž®G[°¿ã!Ÿýy–ájg’åC †Ó©èŒÌ6‚¼HwȰêέ;…f..é$ÿ\ ¼m·¢ÇÉI„9‹êþ+a_µ-[·"lÇ£zþõ53Årª§‡¤.vPJ§¨Ý<¤ÚdSä{&ÍgšÃ»ö ¥+ÕôÝ«¿Z´ZU ƒÐ6ƒì˜¶Wô…U<“Ù lJ0Öä€ùZ®ÁeUùkLÂ[)©ï#¢>ÙMÊé×ëq›æ·­K…ÖBYŠéŽo©Òõ¢1)gSEÆ×G2<Öåk¼ã¡bNá-ö+²¨5u6Ê3˜G„Jf÷xñ4fªÀ0j0¬Ïcfäš¹•âÌz,œL >© ¯¾) ³þ RooµÀO0ü²˜?{™ø¾”vÎÐÅ%ôidói¹¸X4h~E2šè9áŒ-£ K£üpc…¹ƒ€‘#‰‘œ¼¤–Ì`ù$«Œ,”¯re¡CÝù÷¦Æ2¹­ÅŽœvk˶£‘ôv­zY‡ý,–‹R}‘Ü;-Z•t¯À˜ÓŸºÆŽ¶r™tÍ}š~Ο¨Ö†k\»€[a½—8’uŸ9 áÞH6éCm^óu‰. Ö¯~ÄØX°IuˆAfdIâÙB˜<®E)]KüèQ@òqË¿D,ù£›|“2Xž2 ïYL¦Ñb…9œ°÷ ¶ŠÞqÌsÅ€Ö'g¯XRH†.¥¾­|'ø‘ž!tM˜Jö,(l“±\ü†…‹Î- +âñ»Kšnš÷Æò¶Á~a$5ËþXaœ-š[ª½8`Ìž®hVE¥2K4;øÏb ¬ ¡¶Qæºr-<Ò¶¥˜+“À*žcì‘ QYJ…Pô =04xF}úxoò±à!î¾›¢Ö ú†9­ß…Ÿ<×ã¥TÉ"÷'ÃZ×AS%ao^Dƒá˜úËN Á!¯.<ä Õ-•l‚Eâ j T} Kßyˆ±@§ˆTqLHuf3’÷L°¿¢?Ðo[K\$“ÐuoÂá1­í­±Á‘¶r÷~ú%ZPŒ9›«ëÅdU(˜1bHÈ©²ç['ÆÂ½‚ Z[߬ڰ/oWe56)jol‹ž‰ÓgTÛ‰J1£3Ôþ¾—Hæ:â’Y‹­Õë`­±â™¸±¢JgÌ0%;6ËQj…â1x̨ÏÄna^‰#ü8óMm1`IŽ˜å<ˆ >ñb_1ñ˜âˆ„H:À\ü™Íý³MAJ¿Â5AS äö¼…Ž фԮèà›#Á·÷Và1¯ôNðÖãöeà?–ÐÄwÐŬ¸ú3ì[{{‚mÎÈ6üO$Q¯í&íLNÊMô–‘XãJÈ×pßXÏŸÕp £¬ª‚šQ8Ìô?[$ȉ×ä/WIÏž‡>‹²¨“²øÏ¿¥ñž$ú¹Tm_E£DN5|` x=Oœs›„+‹~Ìä(ÿž…Ö‚÷8Þ}12ãÎ*!y^ñ ø®ë'À ×^¨ÉQ:¹OX:Ütk†C¬,I}ÿ­š >œŒXL#MÏÝTE(°ƒ¸âüÁí!׌©Å0“Á0Hž6Z}ÌÎ8S46±¦ðìê¶ŽÒ-Z#“våH8(¬@F\R4("g¨ù"ßËÿÑí2ANñÏRCÁ´L´ÁRÍQ8¡L¿fŠA´wÒ@ï±ØÔ¤®Ccù¨l6ƒ'¬™ÎÛ¡¥q•ý°5@o”VÎr’_¹œþ‹z„½œDú†±ûƒ"Oÿª@ãióŸçÝç™ÚŠ´³o°t‡ªõšÙÐ2ÔßóHIo æi,›‘¶Fë,"4R§Ô…å¼ñJX%‚+Hˆ\ìÇ ð¹• uw¾!rÇ ;„\ÁÔH'ÒP&ÆZ!°¹Å÷èÅko»\­^±Õ ûͯ;sl&†i`Û¦Õ§åÞ :{Ê÷p¤ï7”±q¥XE‰a3¬ù•úÑáõ q¢ŠyÆÐæFc.ivë.úÅóļ£F§ˆ£kþÐ"ªºãChT|gü†Ö;à'u0¸‘420ËåÊ…rƒ¢§4Ñù=~X:©}¹eÎZH.™qŠŽ ÜäHÁ¸y/ÕØ Lb¡9@Û:=bŠ 9„ 'E7.gEÔËԞƮ$Æ ý­Ñ nÀ¿ÅÁ=÷z¯\ ÂÖn>â;B‹ Œ,£§¯wRóÂÛÏVOÆMDµ|•N= ÄKOŒÅÎÌI/ºÅ’ ¯9 ?o6ói=“gŒNG6nc޳Œ~'#ßomšq¼´­gïÝHQ0êïÿxY˜:`v;ÊØw˜lLÅ(ãOÕ¸„o—ŠŽw±=*( 'åõÇÉB:zÇÙRºc)É©läs&/g_3ÏZõO0æ0` ’Pl[$ˆz± ðéà3e‹Ä÷LB°F8T3N„­=×>€’óH`ÆÃ9°ã9ÕÅ¥H²É;!¥„Qèù¡Jz¢C*#+ÄÍ ¥¡³KM…—a”D6:¢X8àMpW…7™= 14”c5jàãÔk“ÊrüÓZn8-[ÎSû6AÕñ±&ÍGؘ9Ü.ÀܸpM [nY~̈ê'V·Hî/b39QÁÈ“_¹-ï‹3Ô+¨-#¾rŽÓެŒ­?©Ï?&ÊOìâMí©™ƒí2,f {h}ùŒß`ž"#™qsìI~"u–anü¶ ÇâDkl¡RÄ•Žp×;d‚Þv‰;¬¦@B¢!Þ&dˆË¢û½|ø±˜g~J›¹Zc‡š$d.ÖŠ ß¿1ø®ûÅ—|K'º—mí[öš¼ p¾´rx¼¦^Pšž[Bcå½=mó%õ«|ê +÷‰c­¹ `,¬õDŸi§ðp¿35ŠM²½"øKFY@"e¤òßV6•.\ue哈§QÁãjÉðÍ1íYÍT¨H_aUóç&S^as„Ç*ØWŒ’ŸxQñåX³€ÉÏε #ÚìwW…FÉÔ];Oü{û–¾Í60Ðcº(½ˆS%>{õs…›´; mjNC^4g‹®µå^.(Qå^ÕêÙ[sL:rLR¬‹e†ÑfÓHÒ~Ÿ xû?[’¾Éà²p®@¥X>Í;@ýFð^7{ êºTk¹Õ½ò+Ýîvô’VÀ‹,;}ÜÉÿ9¾$[ÅQ¦|¥<–u?oþHž,•)Êuþ]æ¶iÏ"¸d7&ú®p)Ë[NÚ¸ðJŽS²VÜ—çßAÆÝóÅ®ß;×â>Bz§]‡k0/ê9ͦ³™1¥:¼³ååpñ¨egŒŸ=†dxû2òäut«dþ©ú²ú1ò½ÐýwÏ6zçƒI@÷?ƒþÿUxX›¸[Z›è¹š8þ×êÿ =§­vD¿µ2þ”Ùønñæâ© +ËOo%9¥/2WZ§#)zP‰vE`ÿTܵ½ž.q ®œYçIð,´®(üWºçÈïrF¥ ɤ#ºãc\ô\]¦ð:°ºq‰Ô=©Þ]sƬi2¸ ²'iÅ&ˆ½‡`È‘Ôb)²ˆ]zÇì%ã¡Ê Ä Ðcc49ñp!.–è_œÛ¦@ë Ä ²Ãþ@ªÆÃÄ×´dÚñ$[§ÿʈ ŒÇ^Fä õªLvÌÿ+I×é]³Ã98•kȬÈÅ×ݾ~ëÆÐ29†©ƒW\JÐåþöúøùûÑ*‹_x2™(ч¶=¸fw93ˆ²F©“8ƒÄ™súæeG¢¶C=°µ(0TMó1KÐK_es WÈ? Š‘»—Ä,Mè1íhªi ñþÝÝõe- ^íøèáëmìLͬ,íø‡£ƒýïáúòú§×—hì¬/±/'ç&ˆáš6­œbQ´Uxšó EݤDÒ Rïä•tT‡©qÖ6¥7‘©ã ^)ÀSj?ñ‘fmKÙM©AàÎ2‹ˆoï*†!èNïÈÒ18Ar‚9·lr@ÒT X°À£R~MÚÒѪ&¬›èÃg2„WKØ—¶±!_‹õÔ°{&UdÌvŸƒä_ˆÌ_r#Üð*„ÐmëÞ¬!;Næß˜qV[õ5QúV R:M¡ÈÊçj™{³°–/ðЩ咆äóÖ?æ8¤ý/Å{Œ¸ŒIìZÒ”}à‘W0zŽ•Ö)DÅ£ ÛĈç\%w4?ìxDÀ$À_Ê&¡‡¬÷®~G6òIG(<è3¯¡çX††<zܹÑ~äê ˜Ôö­þ@}ëÑj›~ Ø-KbXk};£ EÑâÃåb‹Þ‚%}ã¾±6,1Ó0B0^phà s…6æý`C²¨T}g–çk,9Ø7» Ä^`fgÕÞIcÍÞåf° «‚üØq„ˆ‡>éÐ<ÎöfáÀ( Ì‘ y:#r†Q[A\ueEŒ²@Z™_@ðf wl›aEö¬ ŒÂóÀG¾àK×\î»8ö <†!³ ØcU°;ÝÔ‘ÔhÚö=êÁ‘%Uˆ#÷Ä«OOÒüË¡NYà Ù`×à­MüϧúôýLB0÷}Ò–(6j×áãùdÇT(çÝ¡€¼ì ä;%®Ïçz'HÓ,ò®ÚŒ!}¿õ:&"`=I©%´ý%BfÔ— Ð?È.ÄÔ Àçæ ýÊ`6 J5„Õ®’®£ž§¡ |:µùA±<ݬ±ˆßr §BR(,T¤K!Í V„Ã…øëkù¹O –½œ(æ/ü€œJ×(½`ªõƼN˜Áv~¼žã#Gððô=°†ãrZ•CšOÚÓ>%¢pòúP‡{L´ƒ43B†¥H™Aa£¥„¹"÷ɇÙS0ŒY½u¸FR~sÆ a4åÝJí¹Þ‚ׄ!·ö¾Ißä^"öŒˆK=D¥sáÐR¬tˆ [T†§ô CšIØ 8¯ZG†°Èì›îVÏåæ:E"zŸdTÿÀúæ›õË„‡˜ ré´$¢@à“ÆôÕ3DŒ6þ:ØxÑ[4MEt²tš ¯ß«Å,u ð9¦Euë…omÝH4F½€üà ¬Då /çic+›êúfˆëXI`5MH„×€d‹8{-¨Êé]Ö7TM”r÷•N‰;µW/£„3*³tø§+iœn*ød{ód¶ªêÆö•êÎbüCó› 'CÑåùiî+*GMËŠZ¢7ÕÛÚ6j„m:[þ¦¢·îÝ–l¹ÐEhM+Û+´ÿXM½O\™É“xÃxBCÍšL2ÉW­äÊù“ªQceçQrÐ-›†™õ èáD°Ü“Kåâé¤M…³Èò&gdè£<á¹í‚üðË;ª¡Je÷ñxI*<ƒ™_õG¸ 9™4tñprãæÁ“«ÀÙS±hÐJ-4©÷a¹ªF — k]#^‚¤oº˜¾‡V¿ Ìqˬ–õÜJc»Yª~ÄíÊo¡D‚ZÙKÔ±(ã‚|C…Ö§ˆ¶sŒò®8Òü>³C àíοÍÖoU°±“Eb¨d9Q 'P<ß.ÙœÍÏ­ïéqÔï¶Oê ýÒ¿¢¹‰AM¶8¦V?MƒBxœj•  y[;ãc¹ù&|C¦©4êÍÜùFÚø"ÔÀ4½YïãB£H»ªbˆé]}vàF.ÉÍXòŽ/S=w//Åi‚:†jÕ^ZÉÒÅ7¿Â¶²q ÅbaÖ\GÔØmŒ~iÁ™>ûÓ²ãp5hgç¥0:D/Ÿ9ås˜VâøI±ªö%Ézƒ²^[ K£…3-¯I5FˆÇ¤”$F2Cp{iÈR$UnÏò,g2 ý$Ä\ù¡ÿË÷|otüÃú0½ÀLùÔb“ª37û=Ø6.î?>¥Sܰdê 5šNñ'$'ä…Ø†ƒxœXÝŸ¥* žë·ïW'Nüwz-PeþɳҴp©]]JMÕF¾~Å­¬_~BöúöãQQÚm6d]a©­#§Ü%’s~Ú8LÇmÒ;ºb×!±w|x{½ÏgŸuø.¸ÿ(ª&žjõ¿ÙMÙ±–®ñ–Ù‘-ͪʧfïîÌ…_ë˜_Ž8¦}Y w—ÝøœÓÒÓØ÷x¸ŠÛ;@c5™ÛrOîòÁ˜¶5­ÐEϪ=Ë<³zˆÆù]ïZaªÆjªä›ëÅÏr°Ðg³mŠÄ¶x½1”ãFÕ”„ͼ#ûxôi¥¯¯È¢yÙ:×ÄͱåÄÔ8Åmæ9:¹³A¿™6]ªáyËÎ ™øZ|Û($ÀöÐø¡*ÍÿèOÿCŠá‹ìaeüšUU™°­a+FܼhoØS•÷6EtDßKr`áÓß§¤GéÑ| ÎT½¥«lìH)ËëÒü"˜¦KÏÜ1K¹E‹$Õ«á‡*×0*U¨‚ð9 <øœXªšDðòØ"ݵ¹GßoÌ77–Wåq=¯@\â63Ä”ýôาԅû’“ S³³ØÔ†û…ýï^ð,èºÛ > Ãÿ$3Ñœ±#CõÛaÂë2»Ññ³1¼ k[ÛÐ÷2Š»™!ÃfÍré!Ó/Ì­G$+°{[ç¹½Às[³¾^¥ˆå‡ ¼…ˆ>ä‡ÛÞ@&§…@à:„÷ÓUyi6{"DØv³¬¬¼:òù½\¹¢ù±*þ,ãÓý‘^. ÞCÈòÁ„ÆÿÃDl†]§ïÏo…L€Ä&{'…€1OÉ·ð× ˆìíP(E|ùÌÆýÅæ{[&@ƒd†Nr×ɽR¨G·ðÙȰ³;x3ÙôE;åï:,mÞ¡N8äüðVÚ,ÂÞ‡ý&\ÇÙ Œ!¼¯šoìç|…⪡YÇ ]…x_Û_6®`a›´‡ûv’ævdŸœQØÁ9:M· â“}‰¼'og„ü%Q› òË¥Š øÃPÃÿs¨•àI±¸/ëpü†Öî€ø©ÕúCR£ òË bÚŸ³?zWk7 ²s:¡|Wë`„–õù•H\á“öÿ?({‡ a˜(H°mÛ¶mÛ¶mÛ¶mÛ¶mÛ¶íþzÿ˜‰˜™ËÎߥêTY™/³^UÅæ§ß¸%¹)C®ÃmçRX€7>€@mÊeµá›QQTSº®\Áb ]NM÷ôþ.¨ÐG%½3k“¸­Ab:ÿ±Åë4ñú¤Ù…½.ùrßµ±§¼)VWçÉÄ•Éç@pGûzÛµº¿½äôZØ»Öå“VU»5¿Íähéj.<*É“TöbF¼ü{=WK·£9Ì2$ƒ„óùþµÍahñMzAðî­i¯1+!o“â)DñÐ4|?¢“)Cwmë]í]Õ›ÚÖ»©æ[ E‚ºC2/Ä/éðn؆Å÷@„zOÉ(á Ši—ˆ• iW5pmm›ªæ·Ý/äŠë&¬¶4VU{j¤ ¶Ÿ}úØõN[¶ßÊð½'ì|±µ·`\¢¹Õ$—)åeY.‰Ýœ´PŒµûzÑE×3>¿®( ‹ê¿mCÈÝžžÆä•]YÖ"¶¤¿w ŽA›IS·ßñãgOo&A•–ÝT w}eË"«%†µ ZíD4­AäÒX Þ25®O°ÿ‰:/U0"ônr´@ª¸ü" œJ„d0”tß#³á¦r$ŸwÄ\©`*×Lø" ZA=F$€Å(ibѰÔEDSm°ø’ÆSu Ëÿ™À´¾~F-qÛe$xöe@ƒ³På£k¸ë2ÄZ7 ¬!wüOåXôEõW(«TJË"å^Ùÿûf¢e”š.ã2DéÀ• ˆ…nž$Y Ÿ_’©¥_òÜJpŸ ª  Äë^ôÃͺŽÉ9 øÆº¬Æû÷D&¨H u|··´ç<ß5¿§×Ö–Ö ßÕùê¥}A0-¡À™ŽÙœÖÞ…ÕÃõ çø‡BëÉÆ 6k8Ìô`­õá½Zód?ê1c0È9ºÇÃÜúqM”`2òïgKÛŽëqqeuqK[•ÜLnã®SÇJAR_9{“µ²I¸e…"kUSIûÕÀ¹+NÔ7Ϭ&œA99Hb`äQó2&¿iddKZ²_èÕà_<΢¢¾Qœ…]<­¹–öUñ eLòKõŸxÞ }ùËðØ›6hÞE8ž ‹üÈ$‚3"( l µ6`‹~¾q Ž%‹€Q¬3¨5*ÖþTã´BÇêuÍSS©Ð<æ%úûƒÝŠ4AX%xxi?cÑc¼½‡¨Ñˆæ@BØASñõ SÝDa6Ij`ê?ýX1ËËw÷Á©…\•yP‹]MX‡e€é4ü·"»­ìã a'[2J>'J4×¹ÓD)¥¶åÒ-Á „N7¯FjÃÒº<ÄòzèH#fãvx£C˜kñ­«‡½ŽÌrºk‘ƒÌ2T”G\Íæ[d¬”­yÒÖ¹<¶$Ñ~Yi9†Õ8¢Z9„Þì³>{yÌõYÔ šÔù`A›Bûüu‰g‘áÑ‚eËŒv)÷ËŒJ9m&ušO^ØA‡LXx¡6íâ"Íø0úí †ÔS_ã:šôáMásà<9”I ™dc]º© 6fŠðüIÛS¦)­eÂXl °‘gœLNÛ{ò‡Îú³ïºíœ0ͳóX…ã+²ÉO°|ò#K”-¸îZ° ¤d;¹ËcΊ¨æuqºšh´Ö•QD¨¾’#lÜã)€ô•Ã:h°¹[vÄ‹QÔϯä¢]X#zb³QÍvåÍ@è7ã…èü¨>±Èw0ì×ëdk¹S=InoJ˜Q7nDà ßÍÆ‹ cþÛg×ì¤WI×lWvö"œ¹ó®–Öð×MSK˜=þ˜v€¶XK…b!kOÑÎò4cŠªLfâ–¢›Éü¾=ÖË”DAB<0nÒ‰5Zú¥w‹“È2œH@{±hnòñ[„’Uùš—ã!ˆè,‹Æ×½Oá÷Kb\æŠÁvÀóC€›Œ„ªñiá¹UŒ¼N2xZO“£éXa<‚YÁ‹O¸›È$¢žU[Á{rÝLNÞ»Ña2ðÐQŽE ™Õªí¸fZ¸UÃÈžâªñÆüʲ>s³Ê±ohþÒúÁñ\Æ*³˜Å1ý¦ÎK"¹LÒ?ŸA5Üb×ÿ4ó]Ó£Eê}ë\Ëå•ZtÕÇAòûV¾yÞ“F©è8[<¡´”³&t)ÆpfZž‘Péó™¾iîò——†;ѦÏ8ü4AHIÁÐ壈’x¼B;’iŠB 1øÓÐϳo*½ç“?ÑÇûÕ÷»Ç+ànÊOEªWö2»í¸—Œ—ñQuÒs[ã²–‰J“åó:?qsB§çÒ¼ÕÃPœ/K¢ŽVæMû¤Á6Ç[†)¤½@M}&; ` ­Ø^ryÓßø.v@µSzN¥F”$Öiuô "†Îj‰ûØñ§!œÚWPRlí·è©¼U±š·UŸñÀ:y¢×¥†6>‘$íeŒÿ+u¼Ÿ†€fåVèn¿íä›AÂÝG4âSh¦â˳Ä'ýƒòÍ3x—ô)Å–ióâ\ú¼kéä-4ŠŠ½1t¦½ü¸†¹UUÆ÷Ì8ÞÙ_B,2õåLB…¥)Ÿª¨j>ÑæªŸ—š^Ú¡_ïáœù0¤</±U9¶,dÏx&g×sÇl.fÑÈ Ð9{Ù$z¢„#ö7i'/z„ÈgLÅØ&°õÈ/]ÖÔ9 ²¹f%µÜ,×%î‰@š±lºÝ¾ Òu8ŽZj Zjzr¸Æßº_CCY2}  ÊxÙ›Ÿäq¸s• ¼þÙæ¼šò‡`-à´qµ¡×ÐÖÐàÛ‹¬yo¦>Ò4EyÂ&q.^<þÔ$¹†p+ºãå’Ãa |Üsל.½_ÿF2 cÈ $KÈ/O)rA©ö@R/Ô»¹Ò%Yzàí{á¨:K•^:Jpܳà$[þòÒÚ6 é³Ò';ì&d¸õ g£*¯{#8ÂûÔã1P·u ÅQžØ¦Ï¶HèKhO™™%ƒM˜rUiàÕ¥£g=á¦@Ñ…b¯,£Kã¨ÿe¦Õ5h„ÅFê$†/ö†ËoZ÷b¦¹ðñÕ:µ{¦‰]H_ÆQ¤…Ú¥Ø2\+à-_õ¢Œ5_º¦¾`öZnõÂßW4’ò®­ó—Í mÀ?zœ=”W&mÒ ôZ—Í[ã ˜ÿd±Å0ŽÂÚØzÃ}ž­Y.Ù 0xÑwY„ËrK£ÙŠäÙa™»¡Œhmß…®|nqåK[vÏäÍ#eéy×·ékWÖ.XOïY«xÑÇ£Y}ÞÜ`–g|w7÷‹:œpÆøÎC8˜Ä30È’Ö:"@t ]0Vº̲ä7k¬ "3»B3ÆÙ;L†ÁÈ~ðŽçœš9¶;ðòv{AÐ@ÆÚ @ÈuäC{F!Vq~cÿ9`Gú€ä¡“¾õ´-VDS³¦Ÿ‹éÖ¡_®„*ŽŒAés„UØÏ–Â3:Jã9(*Û|kh“±´¡Jî%q¬`Oš…ðç± ›µôû¹„·0æø8ôÝñsè8ÄSÍ^:1ÝëÊ17™‰ï¢<} õöB²ßÓ¿·»¿sÆDZ0O==,f±ƒ»E&2.ÝœçÝÐÙ,ìôÊ·7’f¡:äÚõZå[»_a5„§ŸþÙe S‡[á#TÈw!ÇñŒ#B#öÝbÇÓ?, Ï£aä0 9‚~6#nB I½GghÄj·.)ƒ$ÆbaS±‰ÀÐ?O,s×ÊÃÖ²µöX:Í®ÝaÛdòLV/6øM˜4ôWÁ‘Òˆ=j¤Û;úœIÃÈk,Ôwn%aò ˜ïÞÑ̼¤Ï' äP†ª>ó¾Fkâ ³Z¢_n™9Zúÿ.ýk*ÍïO€àj©•í<ç”ý'6&ÿ ¨.\FÀfoù {ôokIBrÎX©I“{…G-ú‹K#o]Ñ\n]-®:-r[ÑqŸ±uZ….”œO#n=wœ/ÊdB–Ò†[ÔΖõ(Û,í~*M˜€N_”9·jh‹I_h;ÐÀ®¹MË5êhË)bŽQ²f²+N´AÍþ<ä•em_ecéèß+5mXxxÖú@׬ԵJ÷7,ÞU9MÜñ¾¯müT•'ÙÞ˜ùͧ#@¯žAÄ#¡(WÐùÑ#ˆŒËÛCÍÜ/Œw9æ X@ìæW/wï{è7ô‹Ò‚T/ÆË ^ñúôPA:¿ß/–W¾¡ƒ‡‡›%iõÌû£‹ÍÞœÂVijØi)áÞ¦—È*=fz3~ýCÑsÝ?U4ÉÏÔZƒö‚å°Qˆ¨ Žù{½=B]¥ä|zˆJÉ¢´h•ùð²ä…ü…‰"Ì7šE-4$m×`hŠí%Ì.Ð$Áo°gt ¢Jfv^«HãGêIåDQÀ H.³oŽØò‡n e3®]Êæj•>$ôCM_°IiJOEy7â¹ïº"éïºynÁ±IëÌ6Ÿ‹9ã¡»ä Ça6ÊÆ‰ ï˜!m»;!/«2ê3²ÑCqDæ•´c¦#”HƬ]6Báw‚I1Í£ì2éôuÚBχžû½ÃËn÷àdâãÃÀf¶_åÚì&…öË~:Ýù…¹zÒ³¡ù̸QÚ§‹Ò™†™c/ÓƒBG„2ú 7û-Be œ½Ç§TÔlúÔÀÏ$ Væ]U‰rvW´¤¼RMëZö1°‚…QapÇy:kúµ^êò—:üs-1óä[¢‚í°jš…íÀ‡æŽ¸€¼ÎY¥onnRzçìÕ‚-A»ÝqÛ•ç(îEÇ Ü*€3©¸SA»O뼘œ¥G ª]0°óvÚÐB<ÛVVi¾×;{ÈÞbÄ ,¨Ìù q)ö¸9úRÈ3>¿ ¢FeÞz{â›óC­ÚJ¹ÍoBÊ.y°â¹…Ó÷, ¯)ÆKîwÉÌÙÇÔ./¥`2¾e—¾áÜëÐ}?ëñÁ/é¤.Â@”Ë3…ß}…'Ñ rô³ý3‰Ï,šÃ&žOnž«<©†¸9ùy?0•Xš¥˜êV@Óž+^µ’*ÓƒšðŸËAýË® ËãVͶBÞ?_<Œƒý AUÄ3!Ӷˬ%÷Žì¡G‰EM0éâ:?ïÍM–ZmÏø¦ËsmàF¡ ÄÈi݇t!×>Øoòá3ϳ¼mhq¼xeÞbêÓµrOÕ¸a—ùŽx$eò*²SJLù0ÛÍ,¬¹ø2¿xrÐXŒñ{ù|­¹¬§iz?LÒTå£,á›õÄWûʃ—´jÆÜ"ÚRË*qš¼aïÉ{áÝgÑçé™!p¢åO]²^qçlÍ­­ µ½Ðd¾ý~ê5ßV»;'¥Çs©bú§­N SÉ5ÖÓCaÚ™KfüЋ‰V´ýÏÍ##-…îM QÕr˜4¦´›ÁN]ešV€áf­ã'xeäœw¨î…ÃÄåÊ‘í™(ºiùx|¶IBÖéõKýlÖ^«­,•¶Ä:é8hlë®Æ¤R.l²\ªùïÑQ U~¬ÃÇõ¨8ÔQëáæÉvï~.¾ô¹}‰TTÔ/ìœï À<7oRöý»iÞNYteOú8ŽýP>vPLo(9}ÉûÞËûÑÖÅ^ðß݈J¯·4êòtѳª•BõlNõ½ƒ®Vè~ÎüºÀñ|‡?78‚ùŸBxµƒ—â—ÜþÒåÿ•Rþ¿møûÖ 8€Zšÿ”Ò¸WkÖGñ>s×Òt3©”#99&¤^Ÿ„3ŽßAfbXCŽ/mT"Y˜)ÆÌË]|+Wsþå XǃÓñh$˜nçp$”ýЀêëøMÝIÈXRÛêê6¯òkõUÕUVåÏóɈþŸÓ?rBe~Ôt¾Oó‘óF8cÝhÞ–ŒÐ–Y.¶kµá¨êeÓ],o னŽá,´h«å18Ìï‹CŒh´\“Å L,óÅŽ< öbW4Ù‹Ed3— Ñd›ZG¨ŽÈ?X~Xh¯"(Y~ôÌ’« ûlw._D÷“–öe;v‰"nÍ43# °žú\o1l¾ýáSþ°0æ¤/‹â ˆÉj æ&ãÂú¸` Õ«JMEŠp.WRª&>¹\ämk„äþ0³¡Püøw¦¼g‘»ƒ3Ç¥+vá7´ÝW6wâ¼hH‘²g;jòlwAŒvìm»twv0i滶ϺìËêŨÙ캀jšz;}×`Š {'v¹‹1Htn˶S»È»2Í‘fQ,øñ€VFS€öÄuS‹7Ú¶ù xÜl.µ `“T.T•1?ØG®g+–#= .ˆ -à@½ÈòBÛËÜÑÈMfˆNŽÀ1µ ‡(¤l—X¥è("«‡þMH­N‡jº&ŽôÑJÎ{ÞŽùaJ<¿ËC\^–`°eÙ|,Ø¿Ⱥ[G:4nG%ýÝ ÞÇ]þ¿W¤{VºP~[ó±`´ïJl©dé OÒPÒ$hÞÞF8tgߎHÐû`z¸± š®¯ÜÆ¡iCî:Æ=Ñ:Ì# @íT4Üó!¡dàÄüÍ@ n»Ú2~î†Òøô ÀØ®-’Zo¥e6,SçsÁ‚xK®ó^.ɼÃ;.€05¬c’¤gëšÀ8ÉiÔJìR¡‰ßø"vdÛV(íÐÜs»w°ì6àšP8â>Çþƒc1ŠÃ%FtR±-¸pƒýÍØOx•è¬É ’ÑÉ`Ázñ䈲¡´·±Ö²veŽKŠYý¸._N~Pîh,áí4¶|á[!ŒølôJúAGÄõ˜§[Ìk)§æµë¥N´§J &ÊmP.9B݃ÞrØ”°^äR,³—‡¿¢J4Gº6L‹õ“ÝÏ*ù4>€¾ŸþgÞ7•ºê~Ñ}\C-µ=´Œ¿¸bŽhlœw¦³\ml`‚7*.åUÜ-›†ìø\[Ýg”-ñøó´Åҹݗs¼~Çv{¹ÚT(òÅé–/¼”±=’ÄîšDõŸt]Eõ{>§±×ÝNpžÚX½n îîœXB̳] yïž6sFÏC ç­¬'ÄY¦‚ò2¥ÿÌòÈö‡¢¹AzŒu)P- ?³éôðpÁèðWP÷z<Ä+ ÝO ä(„Õ uÒ†X#k߉ØþH«ö:‘n1;}úþÞ ´‚ è¤ÄR4¤òŽ´md­-ÕØªaT1) ÝT·HµŽGò2RúÞM©,3ï÷‡ªÕQ5`_Ì®®I.œöËgVÞ»-Wî ( ã–Þy³bdã2u!*å¸cKà±ÁfØ{Ò<=%jþúmðÆt{¼ù€c•]k¾>Tzõ ÷µzÉòE§H§¹»qMs¼#aOvòáÊw™Îý4V!ÝaãÁ˜ÎÇD|nÃv°\qˆôQÙ  d áÄït%t£ýDÉÛT3K§$Ý„1!¬$ÎHuv¶ûª¸×+ l“ˆØ ÊÀN>qœÍ´Ò°hö†«ã’-Wk%?`mA[ ÊJ™ø­TœpÑ3¦VjŠ$çžH tÜ_Wºk"âõ¥ÁNæ½}7°çÒ{†QŸ×Õû#>«Ã<1õÛlÒôí‡Ú0'{b‰µ8˜j£µ¤I΢””9®LM#/Øâ}üc1óù‹ç™&"ŠA{[æÃ{¡E7®,š_1cÒIá^"gñrUÓ[Á¶¶¤âŽ~ tþ§àÍ Ë äù¦U†‡+ó¦¿IqŒ¹;kile0–× Á7Ñl|/\ì²9¨°âô›>$À¹F•(kN‹É„ÄE„‡Ì«¥šæ'`®,ìV~Ê1Šãž#•©ÇÁÖüèÔ×ÀÖã`ëA‚ « 瀧²Eú! ŠÏ—+KpÁÉÆ€Ð1†g»Ç)á 뫳t ŽÇ7^ãá È=Ó¥ –é3§ùNe¼[ÚÔeþòpÛÜb` ŠjÎ<7õP…)Ѐž–O,•×0?|cù’\Ó±7 ‚ßÐŽyg«ÃüÑ7& ?T;4ºgrE!¸™äUAÍ@íÅ2O°qçnïZ@v*Œ6숲hD ]èŠSé#øóf&&Þ¡£y æE»¥½ißú¤‰á½z¶±sÈiw„¿MQùôÒõ—_À;.Tží›P½;QÅ ÓìDÅÆ‚CªŠ‹3â†K`&\€H†Oî€Lf‘Gº°h’;É$#‹ÄY?$ºÊÊ!q ‚*n•ÀË_%Jò:E‰É]‘*‹ä-°$*gñÈž´™GðÔNj™³|ÂhVqÿ…–^¥]ßr¸Ë€ŠÒèm4n( sßrØ£[.m:wóH€¬%/ÑØ ;R##¤7N•¸˜U€´7*ÎÄzm Açʲwuhìx’ߺ±_œP¢ }d ÏÐÜ¡¦¾u…žYÏéoæIÕ…ˆnÑÁ34ì;‡P•b[R™°ª¬ËñwpË·Ç~áˆ*$Aºæ‚”µÛ"Ã4v@1³ñÀíÚmgÙ‘ï†ÓŸxsŽRY…ƒsºwúd3õ(‘ÛNxÅNˆ/¸gÿ’Œ 3#¦£Šw9\[ž<¼n6S-FÄtï·¯|RÁg,ÿ£a߅椡+[kæË¿=!á‹ë¦T½Þš5A×~d“‡ ]ªèd_OŹ›P^³¼¢Ì™NÂV‡ž®éýÈÈ;„}ϜӱŠcµ…õ²“ »J ³ mâ¦=‰H¦ Ô† ïÛ?ZÌê¨øâÄ–c~P^î™—Ú#¦‚ .9ÓbM&¯Q°YÀ¶$¼C)Ì}†¡‹‘Ç‹Œ£Q€HcJºÚ£Sl>!¢nÈTž~Oíðäµ7„ƒÔ£àÏ`ÀÔÒÀNøbÃÛ(äVSÞh^Ã-§*xÊãÃáK ó PÎV¶ÅQQ´,|E?S  ;D;E’ž&hZ ¥¹çi°Ÿ†~¶~dz58†Æó]Í”(Ìê±ï ×þ¡otýÛ/Òì6.ËÔÒdiœ×·õ^lIXMà û0æ`ñr"¨þuqóàAô̳fœtf$•žõÊ8öDƒ¹îÚö4˜1Šë+‰p0,óG6ð½äÕÉpˆ¬`þ°6Öä~8 ¤Û Åæ>ËñSgŒ ¯+5Ô&¡rnÈÎúfš˜Í«vã-…3>Ë2”ÒžzÛ¨2”X׃Éd\ª³·ˆt¿_Ëúï­·Ûݾ–fÒu‚»ezä#g¨#ÁpƒÊ?Ü=‡ ãÑó*†Õ1Æ´3ê)Âq tJüvYàÆ× ‡(­¥|Œ­ %nZüƒ¥‹ŸAw­õtÙ4(39¡Üw*è?|ïJš®Ê¦<Ÿp¯‰ªVëùî€ËÇ5$³Ïv†É‰ôh'3©cUñ#»ípá¾Ph,› _îËeÔpÝy‰Ìägú]yËõ´²é cñ.¯ôJØ1W5l®…LkÍt 5òb ¬¬Òlâ!» …ÐDUþu ª¢W hÞc¯×׸Žº¯Jr­5C8†t![¦ÞÊËÉ{—­‰¤¸…öÚ ÷Í3}ˆ‡J± Ö/bOeÈ2÷M&ðlS ¨q9‰‰öK;(åQã¿|ê‚V'Ÿä¢Ô¢#uÊÑój´Û£’ö%—ôõA²´«Äþ¬œ©Ó(¯¥Ü˜‹cd=ŽÚš3“æ7þLò Pj‘½ Èbòmm…çeÐk5bªÜZày:ˆ\-±zܸB¢ö–·Ü=U储›÷<££WÄ#ó}Bþ¹jôÊs#;*=>‡'8©mtIoÁ8ÅšÀO–¥|EœÂ›$PMÃc„O—gQÁ)YšÉÏô[/e²•ݦ×"¿=‡Ͳbÿ£Šbá Q24UUeÎ3^Y×Wõ moê §ñÂÇ<˜Ü6öUâS€ƒ*B.v™<Í圃µÍ!B¡Oø›°'~1øÊ$BÁ¸ÐgX~f–Ü24½ÀÔL„æ'ö`ÂV‘xµ»HP0PC£kžB€V›@ú›‹Ké#ƒÀƒ§\×–—§[qß8;\‹0ñ[¸v5Âû†°`Ÿk(4‡uÞêœ ‡ñÈJöd#ev™FQ0c9‰\‹Ýõ7i¼ ؘÈ7H†ƒÔÂÄ{œËÒÄoÄõ®âSÍ­OìGµ¶‚჋ÉÍÈ ‘·u–÷}X KĦ›Â¦cûFÀPUày?±IiÛÿIÒŒ>ã5‚yòŽ•­‰S´ð]Ř›M $Fió¡=ÉÕ¼…@ÔXN5G7ß5'ÿþßÍ ¦Xx»ÿÊéÿ‹¹*N¦Îÿó³‰SkçmޤýWj…§Ý¨,Š–«1lSyãå8º¸“ä'µ ñTdÅý¤„Äêqé7³=®+\Dæ~\ײ2r_ÁøÎæ~„±¸ûÇêq£=›$6hIÃka•yxŸC“‘{äX@-¦êÝßæä&Òø‘çã$‘DMN:bsF‚28ñÿØp¯4cx8Ãä‰/ŽšEé¡P€?Ú¦†©ÔÂòeqÀËÇÇááá¹NG®ü:øÜ@ \|Xù˜Éœ9fÂw<袉šþdsÒº|HX¼hº@¨t¿~\' èß÷Â"qíw‰'šÁ±˜Œ¸q¸¦Iè¤Á­ç€º+Ä·Š91ç·OÎ}xîãäG»/gƒ|°fÍŠ'lf&]ÄæÊ’ŸxrÚ¡™²|ÈîÇ“Ÿñ ¨2Ânƒ.Á‰pMˆ›ÍÇ @j|P¦59Ñj¦µÉ]f'¶úÍ Á§ ¡ÛåB†bx°:æÖ";ÙDVE/s¼™Û"tøÇ°ëÀêó-˜`(œ&îù'ÐÎÉ_Al’åÉLVÀ–À̳]ÃVŒ¤­|aí} ¿há©å•WÜÑWs‹Ú® ´´÷ƒõ÷¢"îÉtý{ÃB<¼€x"qÙ—ŒÛ¢!Ã+ÏÕ¬Q¿¶Ë…¾%~î¬c^†ñXž’ŒÒûÆM–7ºËëŒó?ñîÑžÎz‘k’0ÚûF⼇€NŽð$…Ø ïvA:ÚÑ'{wv¦_ÍŸ‹³ƒé²ðÅþ8s_'.–2hf&/‚‰Ó—S9õcÐÂ(³HmªYÙçìÌ ½x1åéd‡=æ&tØj×:»:µ2˜…kñ“ÖÜX¿lâÈÉ`ÂpbƒÕ¬íØtiêbp.&«kZýf8ðsCΆ>LeoÙŸwrâéÉ‹A^Ûv‡î£/½‰ýBRgb¤Ñà0ˆ’KCsû­&&/è ôí.ŸûcóþÜÿ>å¿rÿ ŸÔ„ëÒZ¿Ÿ»ó»°þ†ßõ»vI ½mòdç9GÙkOÉ@&^0ÛÑx7´½Nõ®oÁ€°¾ê˘óÇÏÏÇJ–šÃ›!§D1fŒÙ€ Q Ë–A;ÿ~M}lÄ>KýÍ;?#•|Y¾t.…G æe±'/Ž/~X½<§*&ÏŽ~žÕ†M}ÎŽŽ~ØÄp1ÿ·r/ÐÅé¤<¯PH¢¡¢Œ‹Ø3°X$FƒÆìÍ®“;_o& júräâŒéèÃå·YÆå7”§9³±ÊxÚâf¦#,n’懡_Fp+Ѽða…ûòçÑ×ÕÒËë™W_€'_ú€èµ(Iý9Áêóe–k¡¢RÎ$øëÆèúŒ¯âî×LÊ8̓£*¥÷ÎlsÆìÀ†zw»àE0xÄÀÅÌàsC“6w„VXîéïï–tG³zÜç‡áìÑ1í+ÒžýQ­µWßl\½°ÿyƒÄxÚZ÷èÑVOž¬à©,_‘§@YB³·Íóžg+·Òo²eãò³j¼€°ú‚·ÿµ?ï?Û‡ÖÚ`þ½% î·éˆ÷± «G¹#ï6ÕXvÊ)$‡vÖ¼Vì|çgF,ùà<º±töfD)¤Å”^hžšW" A¶‰ýv1œç”ê¼86z1‡˜¨_F^Ì‚Ê{iP,Ϻdcú“ ©„.ÿp¾ ¿ìŒ¼1‹ÛVl½¡†ù×øï+´¯ÃS2ŸÇî¡÷PJö‘—p]/‡§ëSת•&](‡9Ïiß=LÃÍÊÏé_@—‹|g^Ý,ÅË&j‹…î"tP¤ï´»ìÙfà½cÀšimPÄþ60»&·ÞwË>±m¨¸Ï3#Lðx=-ß?QLF m°ÃÄ-›¶àŠE¨ 4T aÓ!w|üi|’ÏÏ@G‡X.=5ð`|3QDL8êÁU4ñ©Åݼ¡K澪ÁOÊ9ÂLç¬óY°¹6ÞäÀ ?ö\J7+Æu#«!RR¢Å¸M$a5¤!Mt«=ºXÕkÈEF½® æœ$&ˆCžH_üϾ´oë¾ôq`Y•Õ•¡F󽵇Ry Ô+Dà9ób¯€›éÖLi<–½‹%N³-<8ïPvø)€â\§SZ€"pC Й4Ù™á‰dM­x9x¿¸;ï×v¿7Y”ˆR”ϾH8ÑF¤L ûœ,r¿—‹ÖAEQ«ƒÚ'ž e#s1é>'Ig³|xF0= p=B yV–œ{fù[D *@¢Ü‰¬É3ÖF‡î{ô^’Ÿ\ÜNwMÇÝ‘fîh†xá!d df Øßé‚[÷¬¿¾1~g¦®aœw6ym&§@€é…¿!îß °Š·agÑ2ƒÌåYÏ®;ƒ·ÖWßv²l¨¥ïA—T-‡ª/]Â¥@~=8Ë…â×U‰õ „'$•†Ëø§€ Û«Óq&•CÏ¡1r^?ËÁa0HR„fu•Í(¶&¶ƒV@ùmQ7 ^jת¨Äá ¾>‚gš YŠh=Øk5CRŸØÐäÃõ…¦£”ïŒìéØÔ:¯q©ÈŠx!Ðí±× œÜ;>^} „›LVÆ+ƒÄq-Çò°ÈÆ@Ê8އÀD˜µy³n­ÄWjj²^̃ÿÐ,}¤8&àQÇщe[y V·^‰››o›%h Û8Ï|<\R²ñk±< Î]*ÐÙ DôQ@îŒ ?ò8C2_È1Söi˜N7ï‚k"Ê#œ'O¾â&\'Ôž¦§Ò»G—y Nñâ×Cx³UA“)¤œpƒ]Y (Þ&LôR4=ÊÒ¶ã[¨NQä„Ö FðZ‹£¿j‘“޽1të4Œl¨ƒ\p$¢Nç¸Xº)’ž¡Üã•úhåÖ©eëkntKµFÅÍß:«LQ˜¶¿»¡>˜L^¤ yÎlh‰*4&eI × +Z­ŠÎ–’SJ'uµ4n³ÇTøÎÚëÍœmš”F'UR¡a9Wœ^}ÀZÛµ8E4$¿¦jãèF _ØÎÀwÜš®üÏᢇӮÅkˆÈO/˜Ž(¥òˆÇ ˜ˆ™÷SíøV»õú|÷èøß– s³#Bç?i£àÄa°h)çmÅ[SprwR;›|¢XtUÀÚúÍ¡"ŸÄ%uÚÎzhšbί.?Eº{öväGñ†¿.ñ$sþ¹=½¶£k \£ºE¯ÔÛÅ¢Ztª(L’ÉQ$Á-ñáäãþ84§]†§Ín`©ðSÚî3AØHòà¼t{z+s²†ÆÎsjéÒ "ïW]ÿp§z«eê—<è¼,¾®`:!'©,óæoóRZ|«i6t· Ó)ˆ‘Ú˜ÖÆNƒD½&j/ ppðËÌ£ëßoµ–H;«¯·î£¬Ã%š¬¿MÚÒxšjµt ›zþ"ü36\ðÃ!2öª$•m„€2åÝ?Þ„é¼”N1­g¾Q]úPÕì`­ TùÚ´xlw!‡ê^ÁÚpRìB,é›ÓƒÞ;àû×'ô¾õš¾B «.Gð§Ëƒ2 ­‰1Ëÿªÿ®ß²” ‡Ù­TÛ„AjR8ˆØ9¢J½bc»‡Ú¦iS¨ 6øtä7Š±Ñ‘^bO¶¡þs…T–ƒ,û 9¢ÔbÞŠ­±ÁÛç)ïxÝæ ‹Rªƒ êžKºùåvÄtš›y§ô^qÚÉÁ²§ b‹lJFî&€ÅyçØS09IûùuRúáž¡™âÂL!Ïï†7/½¿Ÿ”"}ÞYPÜNÿ°ü¦Ã®S5‹ð×’]nOòx?Œ àloRU'Õ½ö~s‘r0¤RKqÒ”û&P²}ƒï™g±K:*zÜA/^g ¨v»µ·I›ÑGà3áX9†Zz:˜MµÑ+inÆÒCbÒø¤Üúq‘š:ì-\!Uɉq©ðx¤ky<Ø#ktµóüN0[ø„Ä]d§ÄšÔ&Ä-þ:˜®Þ Å5vüZ‡•ƒÇÌB[YÂÍŠ®÷d;ï$Ûji‰õÕNk§ÑÀZšãsK15VçHMKu—DÁrDæÎôj¡O(lÒ8“Ì}! æ‚Ä*0çK›ÉS*[^lj®ý-‚Ä´ ‰b^k-õ~s|ÆàÆ’ú 8žÿa6 ããÙi\‚Ç¿p‹àôðÔ䊈Ý"13¢¼B|)Zœ‹ß¾ØÎé¹°Îx-ÀSÑ’KÀÜpA·ƒÛéëu¦+±ŠÐ(R¥žÊaJ×±¾Ò©¸gÊ9u0û''½•¢_$ÃU»+…¾ã)/\ ggwzïÁUÒ¶õ½Ëd’&kÂdEIÝ3ÒØØà0»÷$ÿ”œ.Ï~3NÔèÚ ó\×’onRLñ!8ë†7íôømÄAÃVpOä_]S€d~Ñlýô©âª½áꖺ뛴ZZÙu‚ùe©Ì˜Ä§làgéRñt…yÐuŸ—†¡Æ`œž´C ϼdy-séåïP—iä]·šFf²ñe†ø“ˆ|­¡àå‘Åå–«Ú>¦è¦?Ã;JíþX{‘iíIv¾Ã1ÊTzh»¦Vx•WUµvÃÑ(DNãÄoëȯ눰u„ü üû*\¿¼®Bîáéðæ[icE=Ktj±HåÝAý±Ú× L#½soììà¢2ðÕR‰ ¶/ëUC{ÛˆáçAÕ“2é©’\³Ø¹¿M)ÄÃ?¡X¥•RÑμ¹ü¼ÖsÁ?ÒÊåd,ÖOu7zÔÿ‰F•^°ÝŸG‹ý€°!Q¹cÐhÍ\Ç-û(8¸ß°å…rxá?I2È2&´1ü5ïÕ”B¯‘?¥oºT«º'Þ[L¤×*¼c†"²($é"ÝÅ£±ÍË<š¡“ÊÍç7e®.Ý™€ÏU€¡úW³ì³Ro7·:¸|)UØü¦V»O •Z’Õ£[Pʽ–þâ²7Éò•Ôô¸"¤¬ðXcÛˆX+Õ¹¹Ïj•¾´}µŸ2–Ï#·|i ‹V¬â<ŽÑ>s¡X^pRÉXØAüªï¤+à"ª?I´§°ž ÚxHÊC}ŸÅ‘vä‘Jþ©â½=ëóLœð±v%)i½Yð»Ë}²¤«XT:ŸQ™Z66û_üÿ==ccÌï €Íÿ1ÐðéY¯jëÛàŒ{æï~ ãy’J•Hü>k5Ùh¡Iþ6šj§ÙäO»²¹–¼Çù´õèíÑMÇ{—ïòFÆÄIPY&–ÁØfÅ*¶€bXD·Il³P<)´XÁûÊàC¿”ÒÛ"Èß¾›¹ßÇ¥,¡Ð‡œÀš››÷e~þóߟýW\îÿw3nŠøó²pnûå¡i¸˜OT%”RÔE$%¥¡.* {ͨúÚA1:ì%”‚dÈK)5Ù°GºÖ=ê%•¢d( ßþ¥d¿‘t–Æ×¸G/õ»Su°ÁC">rf¨ÈEÃ.j©@ᎼJlô\A‹š-\ÄBÂ.j©Ñ}ÅØÈI$El䬒"6zæ¨ÈE‡ò æ)¥ÑÒ³Ù(U@Öé—8-­£‡V2F­ãŸùÝCT£W§c›}Í#\œWÿtl­Ó7wÊJ·P-t=$½kø¹”=D„ÓGZØø¯­A<ÔWçé“iˆk„soWÖQ­ •è·Áƒf¤á>ÝP–­ð&ÃC¦Á© “•¹À=”5ʯ”Ó÷|ÊÖ0÷@JOƒ¼“‰¾FYGaͲ®‡¼Æ¼£ZúXJPcÞ©”"Be ¬¢¦yÇ#Ü\å#uIÍ_އŒŠ4þ².çLý|Ęèv··¾æ|ÔØ(èý½µà¶Ô÷Öxçß`r™Gbù‡Ã~Â3Ï ^ý3éÞ„¸Nÿ€«°ÆqOe|§c¶¬¡¬Ó1´1º$õŸ(ÿ'c’ÜayEÇé÷kÍõšÎïe›ºu‡©wÛÙF“k„–óp³Vc™Uœ½Îö);ȉo¬@J5êö㯗MÇj×¥Wþxð98/¼bãCŸ#y5ºÓ³ç±ÛY®gn¸¹YógQâüÎÜñ w~&3Ô¨ £õû`„RµºBŒÐ Œ‚co%FèŽ Ÿ²ÎJ³½—zÅm1c͸÷õÖÉ øÙ‡9Óqiôq½üN6ǽªš}Çš7»ßýO´1ò¹¾OÝ.ögãR~ñÝZ[ÛïÞÁ"t^#9~—‡‹£ùÓb)n½øš¾VólUÿY¾ÐZE“’¦mG™~a™rÌí7ìöó«Öj½9•*·þ³¹ìøÓWÅxiu½¬N/“ãåÓ5rzÚ¿4õ ø…'Ù Êë³-»RŸÀ‡ìÁ³ÊP՚ڨ̓ժœ@ š¯·ªÕHµÒ§.­+d76´¿c‰HW862M ” 5^ò¡1V”¶±mí\bëÞZ—ˆ·ÿ=j(c Y=|OY°®kHÿb «^£bm*ûàÊP.F›ñªu ïÙ£öshQ#C;Gö46ô³×"x6†iÂö0&]JÝŠäØX5] êt¬Ôj–NU¿b-Glícíz–º~^‹º:ùì<‹N¶ö¬jRǤ“­só™ ÛÔj¶Kd¶Bv…}³ƒzN“µ½;ÁŠ´1X«8¦ÂÒTW Ó¾AsäçD˃Ÿ%Òe’›hU0Aû €h_(îäBøPnšU FoéÈ&Ÿû[ÒÎ<gd³ºÍ>#Îô¡f;`×¶_·†ÛêÎÛà3uª)þq °!ÎATÊ¥Yïá6˜üרD¤½8ŠläŒäÈ×|Àkæ%{W(7‰…/w”†ãJ_â J˜óMíÕ=¶òóòfÐÊÎd€áDõeì‚BæÛà#rñ¦ç£:ƒhR{¶š’Ò\cÛ žhUÃì¥7yƒª>l›”\÷vð”ø ï–QHlGÂwr”L—5EUK—0"^“{y´èÓMjýÑBRN­cp膎.Sœ ÁðÕ¶+‡R?"e ùEOžÄyëbF&–JOéèrîŠ~QW5¡›ÊøÆ“¦*£¿)wœ+ÃQVõC¨F„ú•Ye+:§AÝË‚™ Õ&IšãSV§ìŽF-o‹n˜Ê 6›°Ða¼ËñòG-Š;÷úÆqqC–(óôHi¸Ú §ÑfÕ~ÐqFQ )ÂÒ˜òJ˜Öꨉ¸Ÿ:”xW8p·Í}@he›ü öÍ¿h½X)2'0Q8xó"Šœ¡Ý2YXµVF Ô(™§o–XŒZ`ƒ@*éw~RHB?Þ ‹OC/Í=£ '÷ƒ,õÍõÈåµ›'± “Ó¨S«¤–>-Œ Kx™v#:õÛH81`{G`e[¥,^Ù²_h™™8*e :>úR½Ú˜/n³VB`Ó_1‚ àÆß¿0ô+åŽpC¶wom·4×Ûà/ÞšNìóì²vG;FK‚8)؃«x€?Žæé$e9J_)!\Å£Ÿ58Ê]«žQ–D¥,¢‡ØÈ ‘ÅDöJ®¤ÀTÃ÷MóXá“Ô33uÈËwM Nx‰Øìµ­2¶ÎVÔ¥˜”¡µµ5‘íðËä]&Ákk-SѸj­ÓôÖÔ0å0Ð… "ð-=OS¢jåÎðð·m"6ê}vgš±¯ˆ&"ŽñúÌÀIwìSæiì%i Ö§Ó€!Œ‡pѱ¡¶üÏý¨…Qyi~ð*2¦_}dCãP—„6cu®`îBL‚)Åëp*”_9Ÿ{ #µ-7æ9âÔ·ÇHßcÛúlÈÇ7¹¤XØÕ,ÉâP¤ ˜`¸]b-Ñæj’Ý^[nµ€KÒŠdXÙqiÚ i·V³š¯c+@Ÿ =cLªÜD$ü/ÌaêW[°·K×éä <,Ž‘š ‹^ŽOOÓ0d_ƒ ¨š‡:=A-òõ ֻɋ‡4 +KÐ…Edf\¿lQî ;‚‰ÐÞß7‰ëš¿ ;$ý¸‹’18H„ýtoö¢ˆÑy“3‹·nÞèªYŸÈ4Èט—3+Þ´(ÎìÑN± ÌMR\KMÓ|‡™{…ÅGdÐR,{‹Y’(N— ÑØ,O›1ÉÁIÎÙ3I#é=H¡®gë¯ë¡ÎÞô ?z3<?ôTëpÜ”OìKŒˆ¥;øªEK¶è€×'óþSt€ÌéZ¦v[‚9¨æ  k\g&nRåKƒ‘C½A:ð;ÄÔJ¤ÒbÌ»åÖl3«·(sä±{ELÍÌsP±„П±8ýWqÓÀ*Ãg 3ë&Á"iGŽRîZ©!9TËR ‘ñM83+¼±m'ôÃ˱W¶®Æ¯§ 3‘Ä+’ãý"‰2×¼zâРõl÷"ÃÑæÍ„zðHÈâww>ñe]ht ’dOw§“-’yØ:žîž§«iøl¹Þf‹õ(1aœPyïYb%@-îl™€â½¢gäÔ”®þfÇø]á¬)3ÔÔG©¹Í3‰u»Åf;¸/mŠb…k̈°zúXí¶'¶µŸ¶…mê[ìhÏ^A™UIÑ–·!ÄÊÖ•Õ†bG›,ä2^ä °ëi!¹¢G‚ãŠu¼søëœˆ• ÷’éfÉWùªC^Ä¡fC©Æ6¾FÚ[›)Ìñ曕‡›cHV’öv¢pYìÊ>¬ ÿÊ)<]‡½½šu™«¡@‡j€Ñ¼doŒ¦±T—°”C$Ÿ¯©ÄwŠ€r‚O)G’´tb¾aŠÕJ0³ˆ/Ÿî5Fil/`pP!Ô‡}­NÈ/ç6s>Áµ†¾¤C‹œSâÆ¸0úúæQ5ïã¢QQë÷+¤ÒR\î[C‰9ácT"®¬E½ƒˆ?¡Û¨1 ü1'HäΨTb@ÛÆ—ÿP†—³…d(í8l þ5d"°cåùÝE¨·vA'¶ÜË¿šÎ\›dtYI™å0#²;…Ð5Ù­]ÞC;p‰DV:qHÐ3#6ª–šêE’ñ»‘¬_Tè?õ¬À¡¿ä#<”Â< u¯¬ÐÀ]RÎqmüö×äB­$ª ù‡Øè—"¯é&ã»GjÙž¿þCø&µLƒ³ /”JK3|&è?Ñ«æ.^¸ìn¬Ë->¤ 4Ë0ƒµÎÞQ­¯ú›xÊ:3¥¨X±ÈE¶&j óñÏòME£p”o~CR^€<ð0ÌzúO]Õ&ˆû»|ƒñ»ï*îl—“êÇ\äÙ"šBP¾¨h¬î]ž‹›?z@èÖ\ øþ$£´ÁÅ_6ÒøCŽy?’’[¸eñèns ´Ä@¾e±ÐAø¼C®;3¨m Ïœ¢OÀ½£FAÀ(ÿbòûÙ™à«[:ŸÔáòŽ `hêd“|Ðö.úDÔK¸PÐ Ô-6v5è—Q»Êöd+îs F%ñ÷ä4E&4¥Ø=Í‹Ö&¹T@hï¥<,Dzs«Á¦V!c™'ê¶éSàMDéÊWɦšõk¹¡µ7àÅñDÇ5ƒ טW£Þ#™kÎûÎÓs£bQ/ñ®ôÖ žzµjÌ<+S`¶YÒP{5–¦°gð´üOÊ‚õ…¾ðC¥Ÿ,HyÐÊ2øÎ”/e±Ì*4K2_`ý­Ôzhlœ\gïqNÜׂ±mu1ó^+\ýb®ì¿Êc!ë¾/ù 0탟ÕI”i=!Òçã{1°ÜqEŽ!Çoá £ô…§ )õ˜yƒ›Z×­‰"°?Î@˜œmÒ\È`ÈÛ7„ܬ籤1yó§ç±6·;‰8)mèð³êˆWÇ ÿpsdÍÏ™î±;{ÏŸ—AK&†ž%_«l×]K>âÒ‘2DŒC‹ˆ_i~“ÕE.š,íMY’]^ }öÊÌ¡Ÿ£¿TÖ2rçÁË}vâ Å.<0˜hç5‰<àØ™—ANzGm /‚E<£°œ–YxôgNžNÈmÉØ»+Ì0ªœ©úé"ºnî/êC2à*(î5bŒÇ‰ÜÏÍÈÓý4A.©¶²xnLèl¸ÌÒÌñìD}–8³î œ# ìSÕ ¥Ñéß 7¹bRØñnq+:‘ê*tZeãTàhbî IöÊ3J¸kKŒˆ[œBt ? nž^(/9”`^äl­ÿ^ªá(.ézH×%n‰Æ!ê [Åc·3’n>dcMIØ䃥¨â²•€Ãø+ܪ+`Ac9ñÎÊõZaÒÑóÿ-, !– ‰Sãqh¡çœìr{*!ãšç(æpìÇ’BálgÆ4¬!z_ô‡éþõAÔcµšüã¶«y]1¹Kˆ(ý âUÇò] Xehp¦½9óB2¦D8†÷;­¬–ëz+¬Y„l —2ɼYÆS‘,îä(¦¤˜¶Ýú9ô—4Ù•WÔïWçQ­Œó~ a¿ëý6r®Ѿó‡kB> O Ó˜ŠÔÕðárÒØäGÖíÏ#NÕð¶_ –¾ R… Nß +'Ô–ñаõ ÇaÓx»M¾ZiëO;åÌOŸç„.»·Ø„‚IOqy …†\ц¸Ã­00û|&ëD^XÂ:eºÝf#7ìƒ=š€÷bTâØú,¶ÕwíŽ,¿Ýt‚HB=ÜM˜Ûþ¨òØÊ/ûK*ØÕd®ü]—6Øk[ë²>¿yI^—Ï«%¡æ×g±“—?7{Ò´|û>×ç«ãÕËJ5¼»9%5Ú¦§®Ýyæ‹ÒM(DûUÙs|]»Íƒ¹F ±ZrÚ/ê`â­ÒBÖLPÌæ¹Q×ëôÈtç§pÓQËz•'÷I6šª7ÜWàÉñ~,-ZÉWockùt†Æt1åO…[ñÆùC*¦3¨¯CPœ6»ÃfÆÔÉBóÂÛ3R(Û@œñ@ï–‡âmŒ=ü»†Nj'²ƒîÿD:ßXCå–‚ßŰþíw8[[a>ÁØ•u0­ÉòÇÛw:ÈÞHÓf“óOIðâÙG¾¤ú9Ua½û½ €à|!ˆ$œ(íîO`D’ŸˆùÉ—oLþ˜ÛE½d<ʵ“Z~xŠ^ã2&˜³¹Ð1-ûh_ŠFŧM¸²Þ'[¯“è~Ó±ãh’©Zn¶uNéCŒ$LR¬ïr5‡àŠ‘6wV‚ÈgúàTV`úŒŠìõ––Ì Àö³'[dŠñè ¾eˆ®ås$ó>±sÅ}“ÆÞ¡JkÏmu .¥ú k¸Ó¡Þ_*Ál@+šG¨ŒáSâ`ƒVp>ÀKäV9ŽÚ¸£t`<Ƈx Õ"õ,Ê_@нÖo2À9Ù Ù…Šß2ð<Ó ÃÕåïi! ñÿû‹]O{d†, ì×¾aÁÛKàbªaæ’Akõ?xk-Ë„¼&Ó ¼Ç‰Ërxƒn¼2ƒ0£÷ ^ž¾Ø ê)Ÿ ¦ÜÜIš9ÔbQB»ídÂO[˜OY”6ôñ‡5é BûH‚°ñu¬¨œÌ‰³h/ÂbtçÌÎ ØvæúýYyóç§iŸ¡ùcn·'lDMÝüuOå»·!7¶Ø9ñÍ„MÆîÏ|erŒ”ì)¬èÏ\¥ŠÌlgM<ìƨÎíÍÏQ¢ì\¾æäì©CmŸÐ£ž¼ÌÙ¦ë1§ðœ˜µ ãgû§¸r‰Uc+ZfPï³ÍË6?Áßä´Ç•þðß|QÕ‹ßYø8"›i?y9À)ƒÂË’S¾$RNÙ®íjçöÇ äj‡›1sÙe¤ÏžÎ¨ãk¦»Â‡E±-vŒo8¹˜(Îs¨ö—ýÁïæ<9bL/ž}&q5‰ŒÉÅ\lLÌýùk‰a«ñaPm=rÖIšEéD«£–ô%I·¤Ê^>¬!õQ¿ ñŠ£‰juñÌ 9òÁå5„QübÕh_ù…tP7Ïþ¡GõŠ>T[»‰ÁœuÓô=‘½Þìij[~«‚JâÂW’¡~ª8î¡ùæsaÏlŽ'¥‹*:mìM¼’ „à1ÍÀº—&Q^epåq¥J•}*öUä®4¼2´S &2þËÜŠµôU¯÷õ®-eË’òåÿ°ÿäyµÀŽ14çÿ…‹ãlajcó?lœ[mm÷m6$¿÷ú*ø$¥Ì¤ÔfË㺷vǹ/›5IcöÍ<ñ2 !ÙD*@1Åîß{næÊó¥iåÕhR(Œ_>˜ï~"JhMTîïJ*No²§+^níGÜ]î®q<ôm_J5ëѲääúžÃøh'í”F$Q9àüÈó¤òš)e=¦Ö³lrk»~ÌFõž†ÙbH샀¼Ï*ö,Š›è(Û#ÞièQªÉÀJ¦¹ï{TdNé¨Y6dÇ<‡6d+Ùi "Ä„¹ 4ª°EƘý¡òRþ¸Q-&ŸÚÔ­¼¸ñî]º¨ÈÍ õcø”csôøyúú¹ø@ì'ÛÄðE+3jж›$Œ¿EgÑp‡|õÒyä ÙÉÅÈçÂ]!~L•OÕjÔ”h2ýÜ[h¢g §¥ I0¢P7Ž .è'é×cR©ÀYí‰×Àû%íÎEKÛR¯àåãš•úLþ=þÚB™íôûw»›ÑôMéÕ¿Ïst–²Fv{A¶Q.AáÄàÙ×>õ…`Úq5¬§S…¶~]¸eùÈæªE€j¼«æ¨þôÐD;1`hÙ ø!1Ì5+'Peìx½†AÌ0µÀì¡XN=Wð»,Ó<3Ë']€R ç}ÎÓëåëu¹œ@X‰Æ ôp—8k [hYtS…K¨ø(ÈSvJE¥x·‰¦y Ø›ÉTÔ˜$‘Ì ¶Y- »÷2@sDlLÙ#TDrdîÉì¹aŒ(+ðÉë_Èo‰…u".aaÂμä LUÍ̶TÜàq——D~²MA^Â$SÒHÕl•aÉkŒ?Å…W^l>wN„f̓'™O'ÌRT™zDÍØ¼#˜_¡(cŸMm^O¾ JÀ– ¬†8¾†¿ Ôj}øS÷²ùX¿"2ÁÚ|à± [;.xÃÔ…XVþ=Ÿ¤÷yÂDMGÿ|ë¡%ˆ ¾“’tK5åËû]º‚IÕ³Ç$Û±6ÀÄg1ÁÅÖò ?(ƒA]øÚÀÙfC“µàãáºZþ™<­Ïà«[Ã1æ`DR“2µÕ<…ŒVÅËg û¤ük¢r¬ûÀòŠ•vÑ ò/L’Ê‹m½ò­ŠäÀ˜0îñWͦ*ÛÏXÆíÏÛ{2/äææõë£Ó=†Ý ŠãÕqÕ{;c2®º›<{ÙÞ+ѯ†CqÊÛ^»äßø €[Î{v¤ŠÄÌÛ†v£T°m VØœQK飶3Ø£V2õk@ Þ‹0¡ÂfßpˆTj ›2nŸm§ïðÆŠ;ÈóLç`.&­K˜3VU® ùMãÜ<ëxýõõ+›5gd}ó·RÞè•Hã<š¯@ðÅÊ®f¿ÒôqýCän¯hÎŽÔFV-à…©ãw„ﬞlôj¯´³Sçû£×@û&Üa!>T[Ìx¸É_ 7~"@“z¥µÂÈV€Ëú¿bÈYW§%©Wža,rðŒ nÜsa÷.âN½1kö€‚ ‹P-r ®¸½)ZcÀìbå–F@•0C]L1®Ì8)›I[paen¶ApSº§vðYÞåËÙC'_hqüú¹?¦Lž õ²ßŠÆ—šÒfaìGÏärE°ƒÙ˜”Zz×h±~ÇÀKÏ‚eï Ü•00±<ó£àùnbõaëÍ=m¬¤™w³B‡Ø ×Û[7Šó6'F5…U˜ß 8<ã…-Ù©4ˆØ8á˜?œÍXL7#ÀçcÿϽ»É.ˆ¾_ /4€ætU;Þ¡®™õ•W笥Ï‚_D¹°ZýBD÷)Ծ͂^Ø,%gTŠM¤G$">§I±0¼†FÝî“È8Åh^«z B‘;@>b+xV7¬–®9^xU`^ƒœõ^c$0­qv,75«ã^ ×÷¤,…Nh8kM˜V¥ƒrðËKo*WøwÛÎÛ÷’–‚Wþ`)3Ž \³YsãµC¶æo3ZkÌÝ̘SbÓ|äyÖÄiyðš_kÂ× ¸äÖaà¾<ž9ùMi¸H …?KÅH\೫hxz n¾6UàY÷3äÀª1"ø`ÂmKˆOŠv\æ؉ëwüNáµù\;WëéUpÙ}‡Ÿƒ¼J¤…•…¥oë~æÛפ²eÅÃk‡\N«ðï_g>Ï]óßá]üäo`ß¶È:éËö°K*ÙWM*"þ›¹ ûÄó,Ǧ2oÙñÑ6eFuk® ÕÞ®*rãyKÛ÷×=4{_!6™«‰¨¦'mê 2Ä!ŽG/û-õ+\J,¾È2‘" ‰;N·]wÀ¨¦š%É0M"@ž~ònž[³²‡òÀþ—ßPë=¾‘­ 2!%'|5° ÇTJ’e§uWFÉ›+&o*ç¶ûNÙIeÅ+˜ªïÌ©tè¶L]#X‘bg ĦO8x™ÛÂÒQØ%¼\›Ra@ÑÕ9“Pll¡2µW»é+¤ø†íê)ƒ¿J ?¶ð¿Ù*è°ëÇçYµ[Ù)z°WDјI[í'R д¥o.–hÁþ.ÒK¢¸8oþÓ”O®£vœ§³¾ ³ÄoyìÕ þ‘»Âu+¦Wï"ZÊs;*ÕQL° ràtqlËã Z‡KW—Åè󬣙xC]ë̓Ý£W~’‡“u£ö¥I/†—{ÚÞvÓ²p ÑéÂ=¬kE…‡ ¯Y§9å­óqSÌeUL©í>÷ŒbaªŸoAM ÇX‚¢ ö¬k´% CÚvÞ×g‹Köî†÷ص»W’{çÃïØ,+–—®‰P¿)ÄAÜZ³îÍ~'íÃw[/óp/fr«¬GÜ+¨¡¼@÷;ðî×À?û÷À\¼!OÈÅ+‘]ö»KâZ‰ñNy¢ãEDÒ®#nÖ‰5KGN]Šepï‰òHúµxM×Ç”Ì*”Se¸¼'ü†w€ßá‹h.(giðyëJu­íŸiE)Ujý®†Þš´U©b„,‰À ÕÀ1­“nFl×cþ@mÁ³òêP!ZØMý—ሕîʉ«Î–J1ã~WNcã„¶Aœºã¶k÷©áõð‚AÞe#›A'z¹a½ rn.Õ`ðÚ4]f8Ë«ËE1Ó½}‰2ÞXX1Û(=q7IV3çLéÉkC~·evÞ÷¬]m:<¡òÁo‘MÊ=ûžäTƒ wÁçs“†™ 2Î÷H¡Éðx²Aä QÞ€}Zß·ŒÜ—J± ¬@B ðå=ˆö÷ ÚàTôÓp¼‰Á]¯†Ž)ŠÑï£1¹8 cæ¸A#`ÝùeÓ£á-ÚÒ褘zv…ÍD“œºÁéåÇŽZ$—AŒº!ùqÌîØÞ^zÕ²nòà$•ÆC­ãL6`Ÿã—°`Üü êÌØnhÀH{Ž4cõÙËi¿ +¶:F®˜!»ƒ°ð}=ˆn —MLþ`f±e"„KkP‡o„¥œDÒ‰Çtí®/šL˜àŸ¯zÆ÷ó¡üj ~ê)—;Ý*ÔGìµÃí úÔ à.“Ò}³£Þeõ@BJN*Ç0EÁwȈG1‡ì¡jlwYtG±D9¥‘Ê»QéXõªr;;+ëívv œ‡‘йÙىܜ%õP˜“½!7~Îȃâ=¥±ýÞæ?¾$í‹c–‡˜T†¨#~A™Y’?˜ãÑçêè±rråÉÉà7ª‰àÄo¸ö›. ûä6XƒüZágéôÛV„Ä2|ÔÅÚP_Dãcôcñx|^/'‹éñx±™Ä)N?Wƒ<€YÉ•s Tz³ò}YòeöÈõõž¼æãˆØ µ3}àÌ;ö>wx~ì áßwøì•*‡Ð;#/ý}û÷°¯°ÈUï­Ü…Ù‡ôʸèijޱëºñfÙ'gæbná§¶Ž&»ï%´ŠJ€n3¨­…3q~hÎ,ì´·Ù­­¯y¨“´A€ßÅé§ÌL FQó¿sù înÉ7]CWÞ,•]œ´Ymh`¿!w$æ˜÷rKD¿({Û>)>Ê=ÇÀ¹ç–„-FÎÏs]0´ÿ¤ßk^3ÃÄc¸ëW…»³Ke{ôøúùømï‘MNøä×L£ó d“ö´Æ*Ä÷Ñ){²;N‹Áðþ}ÉžHª•î[Ìrù™ÛÑ™\z‡«,GäêyØqî…Ø€´½;Î éË« ccº‹RÈ‚ò¬hÖ‡ëtœÖ¼=„¥k3*|‰kn¨ž5°—Û#ŽgÔ¿Êÿ˃¥(ù¥h@¡®HŒºÆ_Ý„%¸Â9ùûßó¦ïb‚Çöÿ•êìTÿ:Æ·Ú³Î8ã®÷y¿h H(”K)—ˆµÐVá}-FQ1Ü -"aL­73Fà34åæ`UJ@Ó"t’¥GѹQÕJ“´á:Îú S„œ§GÍÿÕ¤©U:ÉïÞ»¼ÜÐOâ|˜$a¼¼»õÝúýñûÞ»4¿ó+#ýÿž§¥q ý¬^¾á~«âP†Êû•^B=ÒK=l7áq´é"W›4j#ÀQëEú©ÖÆG‚ ƒT%o8„~+¦(k û‰‚ ITWn Çáã" ÂÖl½ð‡Ïï˜)ƒ(cÎâ£ËY>Xôdeeyh#ÁÇCøiã¡ÿ ÐÍBø-Ã3§Â9 Š>Aïh—®$¿yL/‚^Z<¸èðz<°ÿ2<¤¤ùhŽenc4 Ï×E"V›1.¥‡òåõÑÿAÒå ÿƒÖù³ [“Gjþ+ÎårÔ)1õ2¡’]’¤rñ4¯Å5¿$öŒ»hêÇ7ˆþ'Æpß^¢QuR#‘ùÈ­äC8Ø¢¥ «@I˜æOvÒF܆cƒ.û·AyË´®¢Ð¬‹¨šøÃŠU¯æ¹]²QUKõfi«C¢€r{„YÅ:S ¯µ€üMó SÑ£1ž²ÊÃÛwíºæ˜ÔµK¢ç®ÚÆ#q$WÒ÷­É¿°<Û‚.~—z–m‰Š«œ¯V_w&º.­ÖôJ µKQ–îÇï‡ôË.³KnØOn÷Ã!‘´ð\19SGrŸQpv*„ë”'·pŒ\ŸÚžB¶~Ø¢ôð9ì!`8îÎדŠ7˜7r}­‰âÖÃrܱ//­¢©T§QL[8$›¥wà_1ùˆ0Ó2ÖÁP6Ö¬å=$êÃZÜ>夙R9`¿\ôáT’»ï12×¥?>õQh·lÌ ýýï§ ¿ªe¾ØæøhLÛ¾eßÖOC3óo>)ÀºUk•G IüYëmµç ô—/jvÐ :éÃutj®$Çmn ““²Ú)+Où©ÆFÖh͆cCßE€^ ² V~Ò … á³É‚vÍóHCwó¥;Òo&DÓÐPDP(@Á@*ºz'S8g" bÆ"·@ýL£×ÆUô×îtž¬~É‘hÕ.\1£mÁ9µIÌß@ÔOÚ-³¼eªªÆ:u]šK*̱N¦¡h§¤±gÂÝÜßÃÒ{muÆéãâè\©{,²[ª—EùžÞÞ:Î`Å.§Æ»R!mÇ:—§¥4þqiR£u»>Ð%S]/Úõ"îPæ¤ü–í^£»Vµw×n°«u ví2ñÒ÷´†ÑvkRõÔÄô¯ gÔÄS|"mQOÆJQyý‚ñpI>º} 7Û$Ûm·½ƒúð˜À‰Û¬\è4iQèw@Ú{'ü=¹•e’¨´½·ê%-‡£…60ôʳÊÖlìzXF¤3¸ÓOµûŽ´+Ó#ã5ÙAï»Er¿©nÔܾ‘8·°:@Õ5;9LÚKÇ}LRÀ1Fm‡Á©{ï˜Cz§ãšî6Ó_y¾òÙî\>§ Yß©[sERØj”x vb+"o> 6ǃtpÀqeÿ$wA•î&˜Ì±üÚÍËš¡ÄþYN©xÖ MõŽ' /*…ve{3^ýÍõÌ—§¡} lPôL‹»ª†6LŒ –¹“‚«’íVÄ44 •˜·j´¬Am¢vóü! ×fV±˜-¼˜×ã8-%‘8’JUúh5ŽFpm ¬B%@xÂ»È ¨A$©ù@ Òá–aBK’äpQÖn¢‰kt!¼¿ˆqò!×–Ñé =ï@±‘uLÿ7g¿üÖ[åb—TÚ—ÞK>eÙØÈVˆ¤^²“—ŽÌ[ôJ/Dõ5áÒ±‘Š;®Tx€µ´òÛoT©–ùë#qµÂ;’$Rš¬ 5KT bôjMlarI€Zhy‹JÅÌêñ®ßǧý°ŒF´ñh\%(VŽ! ­²¬ÑcXµÂ}yÃÚb$£FWh·Ô3 4DIÖá~{… *¯ @„Õ-Ê×*FwÁY‰%,ïÞ Š@” lŠÕAÞx4å‹ ¶,7ãµ1/[Ì\e 0ký”nÜr~UG©ç‹ˆÝ¨¸ý+½ À6Lzg"Ž÷Ñõ]Ùi^;εÅKÈfUSCß,ýýÕ\l×T·¼É/jz¾Y4~>÷%LJ=ú öÜÌ"#ÀŒ#¥Ö‡t ‘àÁ©¨ÎŽyŦp«Ý¶•…Aa¨­˜¢åØÇúá¾@¾ûº93Äû1@P}OæYÓ¸öªØnæÇPù¤×}ÑHY›Ñ‚ÇVK0{D»Ýxh  …®yÖYCZ†ê|üPƒ7]bXœ°Z=•<'Ég43Qy®„Yö^•J~ÚxŽÑSçÈW)¯©zÜC«-vÚ{Aô sjR¼,©æóùC˜)…úçp7œ|´-D³„ЉÁ„‰Ïy¤x]Qƒ¥5H¿-ÄuB¼ö³cVB¡¼)+ÍËú¤)=úŠ=þ‰'!šemd8¯úú',Û®»Ö±ôV™¿ÞB|~þTÖì‘ïÛ”—oUëî3zó3ÜÛ« ½™þÞÆ+ùÌÇ+¨^\s ¥èþ=qËÉøÝ¨Ä²ÉŒjÎ×XÒ)Q¤ÂÀ·§¢˜¸“/]í˜aé#Ö·-oœ0¨Í¸F­xúrL dÔ¤¥§ØœâB*ñ:Îàªó¦^ðu©Ó`úüD\N%<üe†Ào´¥}#’5䢙‹žv«á€`‘’ ïpG{Ýfž‘[ÌÓ&Q‚©zÂÍ­žOtm [O)Ô?Õ}e^! ßzdŒâÐ厅¢–µj‘+zQ¸0În£:ïÖßÄ›v¼I(’. 챤)EéQ íFÂÚ®lç5,rë‰üb§;d‰¢0­ý3LøÑŒömL÷ßß^´SÕD-ceêⱌ´A´@ÆŒ4$𧌛€)à¿ µä û\W7^†üìb{cµÉùYË(†z×®†æù?SÑk¥ b䇷 Љ áD^¢¶pKóÅNN 8˜xqB†(`ó×$+æCbås¦›•Ï9ÙüK0²´¤ËYNBÊÏQ ûpåƒ%“ *6aEVÌ(1ŠôeTÕLÓ­².3ßlxû­9Kuɯ€¬ßŒátÕl.† rس“s¹PÆ^Oº7æùÁh±×¾rÙwM’áj<>"kå3ƒÃü”§è&õö ª¡Õÿ¤%¢äHÆþ…âzÛHä È‚“’Äc`ç–vÈŽAg‰gbÂA¯ü3Ý>nØ¢ÅÊah è¾ÜÃÅ"ÙNèM?Y”†LÄúʳåÏ3¾/ù¤ÊÃ,”méf8†+WEŠR}À¶ÝZЊ.(´ ˆÌ½³£Ž®!l{G,ž’eB q ê4Š:0Æ¥ü”J‹» !NÀØ1å v“D ôU9Å[1/N Ã6”·HÃpÔ¯>•u ×Ó# Ù§H½Ð§X(Î&z@²W§”ã°ù32{Ë­Uèˆ×îµÖB×;8²l³ž\ŒHÊ´‹ ÙP~Ÿiq=m¨‡ßµC÷yÜ7€Æ0+"1Ç ø°×¼«¢Iü¤^̇ôÄt/ãxÖM=7¯Îd8 Ãåtmœö‚a£v›všÂãäOòó/õ8‹0'}¬¡ÉSi®ºˆìû‰èqõˉ™Tƒ53QG™™À˜™ zÖôUb”úwõ6â¯oé·,ê][µÑDeûÍ$Šël³Ï¨)}>Ï'¢´'IÊ„S²ŽØr«’p·›÷*y[¾­Ì’>f‡œlò˜ÔQ,ä³Á²/ ÙÍi_s“§8Ú¦É|8ƒE[bÒåˆr… ¬Ò K&‚5™MKmÆ|„.|±^T9‚Þ$e\b}D@l³ò 3³H8S¥IÄE³Ò792zWBÏ,þümºl!M®›ÉªSgŽtóÆK“4S y€GWtz€sÿ‚¥•¯“{w—šMò <’­• 1 m•œ®-¸ŒÓ #÷ÌG×),¯r1•(b#$¡ÖBu@©•astõÆÖ®®0yËk)-]ä0@qU"%#n¢=/^;¦.,t>ü:Øe¹¢šê¢€OÊ ²U›D0=CHز Ò–ñaØô9Ï'ÿ÷W9¬·<&Ê6¶t…(% ÛÛŠfc¥ÎüÒÌu²ÀrÓWKr<0>õ3bÁ?õ‡Üȧ64óôx†Cä; ñ×n%’{€['8œÄnŠëàˆëöMûÒ‹;¾©m™-Ÿª¨z÷ÐɆdæÍS—/;ÊùÒtÇfë¹+Ké‘p~à 剟pã_ JdŽ$òqÜdiòmQH›¾‚c<†úƒ­~K`¢3{Üù g›’q œ#¸Vq¨ZãÚô‡ƒ!ÍáÎ: ˆ)|."+“˜ólðyxèÄâo¶x™ÉÑÄ¢Ô‡{THܰã2p‰ÑeO½>†÷ÚÃÆ#‘>§Œ¿ä}#[!"~O@øsëC3\Ís–TøÎ¥`ÁþàNü_?:̹Ç3Ü;"{ÞÄÕ‹ÑÇs´Ò…­ã·8%§?‹²pïUðœÐwû!¼^,‡íéúvcQ·}þìó‰阹œàÅÍt⮦ª`¦¦Î½¦éʶÚ0¢g6…?ÔL¥€çþpcæ¢8Ë4FïFŒá¥[ù/Crñ€ÀB `.žÔƦ“·¬Ñ$ÍôYåjŽhR|±{€“«ûçûPÛà38:ÈGÆ,oº`h» Š— ¿giA°«ð"±Š¦˜ÉäëÖ‚3€ æü›Àˆ‰¨Çûƒ…çC„hžr« !™myzbSl…ë~ŠŸRv‘.ĉù—Ù9 X|þD¬%©`¡5ˆgVI¹Ü;fÒú@•E³mÆrO“FIßûÃéj5˜®#–‹ýÁ÷úµ@|7NP·1óŸTP¹ƒUÔ$¤r…Lr &ÙΖ"§²E¿¼Ž÷&òQ,3.zìË›T¨åÎ% …Z©40Ï÷<0þyê Žh.{¾@ëÀõ0a÷À3“ÞÚC@ y#¹µ”ÈkÏÓŠfº«;&[Ëdâ<.óëä,]òÑyè®ó—Fb29§~à:ümøäÒRJ˰߀ÁqÝ/HäzMžÙVZì!’ÓÃ"}aòq¡ú¹~5A¼S;YÐk ÅŠ«¢ªZ˜Û ­ p­xsèØ­p QðÛB±ãn´CT}Åò›/˜{;Ãë¼G +˜Y^ÁŒ*«”“VêØ‚œ9‰°œ¤ñ\ÎpSã•.S™Iã¸:~9nrËýà…Ô°uv1AÔ`Óóê÷±E•N{)Àú¯±¢Þ§ѹzƒCgOÂc?—a8ìˆhWO¡¢¯LV]HlÄKéXÒ1):0çëÜ5„kà&þ¦ÎËø¦(ǧ!3Ælà 9Cl?Ç;èœ=Äùo$rNNìƒ.ôoÂMzû£èEO-±ì}Óíë@½,Ž@§®­ùéb`·ÒÒ3}èm- ô’¬üZV'ž¶ÿP”1:+%¾ÓÒ²^®;y”zýU€³sÉç®kŒ¯öËSçf;íûo·Ø¯*É;1@îÂñiö:÷{|~§J§Õwåú¼[÷n<ߥ‚ûÜw¨e¿Š èeÁ×ÏÏÁ7›¬„^Õ da²›˜ðÅ[>ùfô½·Z­Óë{=o±ìý3³þN¬º…Yèu-ß–^ÿ±+Û—ú~Ö¾•R¿¨ë’^'^CKeÜPÜž §ÓCôuQ9ºªâ3^wËæOˆ“˜:Ãæ~b$ÚÐä÷Ìþ3p[ÃùzJ,웞1üªÀ?#½+„#ý­6È듵 Št2VÌ>¨RìZ¯q4q<žC« îK‹ïÛèf yshZÆÈ}OkiˆÏII†óv±@ÐCÿÛ_Áú<¯‡ªä­é݆·•£sÅ|ÒDê.ˆ¿C—#äÕÔÝ6¡µ¥)Ýúíh.û…ÑWã÷S(^ã½fm=¿cq2ðå÷0È?]“Š›Äô¼  F<‡±7ƒêÄ~k 8ï ×ß ¾1àõ³©»Ðãh 8cê§ã%{îÿ |¡lŽ€ÔáÜ✔v–Õ!u:1{šàÅIÄÛ7\}‡;§OšÃ÷@ D.„8àÂ$Ü¥¸µßricT†Æßéuõõ· éØŠáê%‰š·”a+gócDŸ¨°VuÁÉH«M*š• ¹`d¤‡ªD¨Ê™@;£4‡Íƒf§Þ‹èpÔ"½Q±”½1&O…¬žÀZéTd¶¸­”oV•æ'aóxDlÜt¦6åx¼:ZAkR[ÍëF’ÇïhkÓSÝèëÎ:s:Ö±,R¢ßd;ÈUŒŸ÷²ZH Ó¦xõèŸ0ŽCiH~Lu¿/øŒ´µ33š¢Ö¥H43ÃÒ,k‹wi_J üõ –LVh °-aUh™ù.xÝÀ¨œ¦‡°(08¦€rX׆³…ÊLÉ2©–¶9Ú6ÀAej#¸ÖY½º~QÞdkY¹Ä6õ¾1 …`ƒ`‰‚ªCí®†‘“KÊEÜI«W‡TÐ0çŸè¤yM²‚ýÑ}QlÝUЊÊSðCÍ๹Ÿ‡ëQš|¢M}Ï @…ªÁõå,ÞÁ”ØGxóæéÛqv¢ŽdDüÈ^éx®6ŸbæãhAêb¨Dêúî/XlÔIm?¬¾ˆp%¤ý…$Å:½Û±Ë}£´B±( ‡éžD­ÏŠ ,ÉŒøosYMv>/žÌ\ )«Á°¼àwX'£|³—GdÍX^€Õ¸Ò=üA¼„Y+Q±Š1çÒR ¼½ÝNnf5\~Ž @oiM ˜ê>×¶žj3%ûèYo'œ¾ú1¶·¾äÂÚtÜi›àÖ3ì-\&H_d³5¶GBjŒ„S²gÎp¬ŠŸä¶ÜW†V³Ü*Ê&÷B“͸@Äʤ~î=÷"@œkæ?¬ƒÀH­]\`ɰPVUiNÊÉÆ58­|¥‹ô=O|çh,òûþòxc7üðÈ»d¦¸„Éæaão“Óq1jâ$éZçD‰¢`¦rê"(ͽú4¹‚Ç`¶rMIéØ,™Ã‘HH,…›©¶Í¸…ÛdÿŠ)®žx›éÍ0þáïÄm©j“qMf‰¾Îâfx:ÄP†óláåâA­b$lM¨ ŒØKk†œþ.~ÈÜ}ã%ÌyânRŒZ“tòq¾í ,&Z(sµ¨>[p"›QQÉcE¤ѵ0WT­ÚÅ·³4óïøù»ãÙ!›ŠXgr úGLäÉâ N!HWŒÏXËoa[Æ}x(¹œÌï®±iQJ¨Þj€Ì3³O;¸Kt÷í¾„±¹†¦ÏAµóª¦uÑ`<ïA–60|ctl ¹Â•"¼]³éxlÑÙAsbž“ã¡ÿ ÂùA|ØY…žgX êM&¨5÷<ï~ŒÅ0ž9qµ7Á›qãÄ£W–pO¸ iÓ«Ø£=WPFŒÝ>ð dÚ—ÇR{Ë—¬õà¾gøíf«5z?ø¼±5ŽUR R½2¥†à{168{8ϲ~6ïà¹i˜l…1P{Ж¯ÕÒ¸X{;Ô¯›qý¸ü:n¿qõ¼¶=}%lhL\ܹ֗– íÔ{âûèãâ Cø²¥‚TOYñ¯˜!ðÈIqUH!¼2¬Ø:0÷éÕDh™p—×Q4ÀÅ¢OmÅ…/·€j (%Q °n–§ ôø*9–K•õ.î;‹_WËt¨Ùùÿ¡äŸ‚… ¶v]pÚ¶mÛ¶mÛ¶mÛ¶mÛžó›¶m³×Žèè}ÖŽ>Ýÿ‰ŠºÈˆºÉ¬‘c¼ã©7 <ãá$KQ‰Fh¾£÷ÚæâU¤A‚ªnµ€Üc©«½°3B’„‡2ï ä¸!Jnš]å(s ÉI2`kz$ÜŽ#™¹ÕÑIÔÙϨÎéÐEOJEKz 'ŸD/ó-Þ‡)¸žÈ缞®û¦°]«ê½·36¤ƒ•VðFë85èŽpÔ® /“¡½¶T•x¡öêÛQ$êXªPóÚkÞØîÎ «ƒŒ:ÿˆ°]Žg¹‚Á&ï®åQ⽡ˆ•¼£loö~–$)á·¸{Ë+ã›%XÙVÉYÚ4Ÿjʵ¡ƒ"ÈÎÇzØü‡— Foߦ> •K¡ýnõʃ“JïÍç¶\ËìxÂ÷–Ò5½ðl©so|參KåA¹Gf‚¼¸©(63ÓJ6™‚½Ä¨ùÌ:¡.WäÔ·‚ ÙÇL'›ûrNjãLn½vf%hŒ$k»u!å\pÃH‚¦E TíÌ£Çîò,H{U#‘«šrl r­¸ÊˆQÓŒÄУSØM4[»Ê‡èïÄCÿ¸·_ÜÚ7#C ïÆÏÀuéYõ^²>¼œ|||’þ럧Ïa›œ?Žñú¦f/£çã:«^áXm«¬1•cž¨í]ïà¿°lk¹5Ä™¼æÀ‘_bÈòäá+Q+̶lÚ6j¸ëü¨«‘PtòÈÎèÑJ>º­nÅÓc蓉|ë /ö‰ ÿ¥›É¥@Û¡Îc-Õnlpd½IÙ™wmí`Õ½D\N¸ë ÜC°08&^°«¢¡¹Áîöc§Ur)µ…½M³5b÷–›5½ÜÀ ç^^\[÷÷;âÒy¹| tQXòÁ™d÷€|8?n>Ï?¯ÀWGõÑ=½³«¿V`27eàLÀpfIQàÚ4Ü//˜´‹§\Fõ£ŒæË&°M—åH¢&9ZªÞk&1ÃQÇÑGiXñBΣLk…cì·ºáݳ¨¯¢NÌÂO}wg• ödgu7Í80Ë•ø­æ¨fI…ôªÄ¿b—­¤W½£²ìfpi26ü°Sû wwì$þ]Fgå·”æ,uçL¤.ɤˆù´´Åº÷þ®îqZ„ÓÝH"¶ä;,KØ•°R! P‡³*(q‰,(+Eáõ¾p7¾+‰‚SŸp*s€/ëRŒñ!‹ÉBAbR|ÝÕû÷GeS©á7Bªübœ öÖú͞ɠÏ"/L…Øÿ(Kº©Ú¹Æc„ñ§4éÚ2¹3Óo¾,¾1ÂÐŒÆ~ ÁÝ|ÈØc–×ÊÛzGEuv$ ½3…ÙÇüDã*LàÀë=µŽg”Á*h/"¾åhT14UϽ:‹$–ì‰ –/&¬ªHž¨…¾oÀiÿ™åB–›A±D´hMŠ†ÇˆpÛiò ³Hó30å½™¶¼èÿ·ùDû*ºà—õpLæÿÒÅ÷jO;c‹{Ýáñ@(ˆH¢PJ5q‘&7‘F=ZYjÓK3YjK¦9…5•8H†g20ϱÐ,!×_\Se«$I9MúÂWnZÃI¦ÂWê(q§H·)q^û'ËUξçšLI¶ÅD4/./Çëûÿ|áíLýŒ¯ŸóÿuÊ[ ý5ï¿Á}“ùÓ‡Þï‹JG¼°Ðv±xJ×h“å#_q&ÏG¿ôL¢x GõZ<ùŽK5Ó°‰~VPr’¬ÂCÎ#é ­¡Ò<8JŸ2œ<¥ðõ©b”¡â¡îB â!ëE2‡ª—̈́܋Q: y„S0Sö@™–…°‹e&qËdZàÎPÄ*Ÿ¶ÜÇ)ÆÜÇ)ž¢à–ÏhÈ*˜‰ySCÃcåæ"ïJRå î²˜ê¡îRNå ïZNé ìbNõõ¨S0“÷4§d¢ã±X++£‡ºÇAé£ïÒ˜î"ìV§t’ô<¥tÒô@§h h¬žêÂøþ©Y7 ]²úÁ¯½Ãt Ù ÿG´A–¬·48àëeMò³}MÄ"ïݶj·¡ÿë²åWqãÛt+:$°QH-Hí®òû¸ùÀ9VADâ¯Í«#Ø‚ˆÞ¼D8æ^ßÄêåŠ/V"•(x¨zô†úBI!¡N°q Ns7†Fj]€²»sº»)½\j¥ö+vý©ÁþR¹d3Q·RìÆ U Uo`œ¡uqSÙxÖ }˜7Øò¼.ôYuh8M/è] Ó&EŒC°îNûðK4H6J7ЦÈÝ£m<-]ßaZ÷ݧ޲m½F(¸ŽÅÕî„Ý‚ååvÙò[r´îÛååoS/ ÂOÔ*xSÃÇ9·Bù´b‚Õ¯mñáïlZ°­B¶z$…W[kBkÐÝÛ+LtãCéq½<ì¡7š.^÷ë#zRÓ áŒ«2`A¼ 6€¸Õv¢'wsî¸hŸ"ê#L Õt-¯JÛ_ÎÉÐ ¦z掇%kÿJ¬w,‚øú~†‹cX„qØïA‚‹ª æEH åÏçä è’²§¬æE1xÂZ'µi©ÈK07¬Ê¼ÉzØA?Æm~ëBÛ;)g6Š/ñH6qg&o£AÌ/â!èô•â™3ñ¬ÂWB/)mI zªg5Ì‚0êÆ¢æß¾x'q†WÏã©©ãÓÐ+´ô¨sªôü C…ä¶2°‚ü‰FÈ.I‚:éà!ƒŸ¬>™™AêHUv)Ç'¸ˆ»ìO[Ptž3”‘Û_&™á´køh¤²<jú—B"8`•†V§âŸ3ýzúL@éÕ”¡K ÚÒ¿¨Êò "¸dR¤rÚeMõDœÃ>‰È¦$Ê^F]àŒwAlÓ]*ÊŒ×hà!E€ÜÑN³®ÑlD/?c¹Uy}:ºÂbà Oe9~Ÿ³Áé¹3ðäÑÔ T, Reƒ!à u#¿£¶&¯!¤C±šCPRMU%ÖÈžß´,Ö.‡ÅQµ†ŒàYBgU†ÕM(‡?ê·dK)JÃŽBИL'{™Så$³™¹J‹‰£µgÅ@!ª…šÀ É1džß¶åµµE?° •bõцïˆý+O°”³ÈddÔ Ùj0$"‚ÄÏ ™%Í‚oðfZ‰RG%_6•¼1bGDj{,f´#_ê±H÷}m„Æò›n°¨£œƒ³ý1g£•6tÔÃE£Å•\¼ZaÓ Õç¯ì—ÎNJ°&ÁNPÈäj÷qAb§–P‘2L´Ež9+ÙF‡]“Ö}'Ûàê ¥Ðt¥¸ ætÏÛ}ñ†AL £­§áó$Y¶ÂVLà"9|¬¡«µ>fÄÐW®ü.WOsU"eÇ„J/Á ã4_ðà„ ¡Q¹xêLVb±+œœÝï?À'[˜T €wÚV!WÀ"`6›dÍ2qœ•YvTm‡³ Tgîê=—; 9ßZdîü:«Ý=©ÃlÁrVñéÓÅ!¥ËWlÞÒ£aKÁ,.Ÿd…ÀTøwŸö.Ù5 eå.Ÿˆ8;½í¹ýÆbžw ï¥Å ]A€LjßÂb×^6° ‰„m¶mÙÉ}+6½Ö &äz ¼ožB§ô ½ DVÃAý:«`PËPƒë ‹ ÷\N¤úg>8CÆmQ‚¿¥/eŒm¸ÊQÛ(®¥@0ávþ“kr¼!©ã÷¹6Ö:)éƒJsG^r;áá½ ™Ù1Ð h ”JHaÄüŸÈS`Ò\°Â=³ÌFÇS$Öè8N½¨í/,?=Ä’?Ü0¡Âv¬/»AAå·G±ëÛ¨Q‘ƒü@Y‹[´zoIyc.÷*\ßzcó‡“ÿ¬x-;ˆ‘Üå,41îç~q ð^©2 xOeTO1°zöù0OšÑšî$fÑ.¼®—ÿEãü ë“ÿ+d§z?HX?[,™·í!8Çÿì‘¢ ÛñŠ“`²v¿¥ƒäs:ÿÂX"l˜éH¬Ž–ž§Ó¾Ñæ Š WÊU…ÈÍÄ*>3fÐL*èŘmN8èâÒgóž†ÂÌ/TÝ’uAŸ¾}f,Ö–ì•„·šÅxcR5X\®æp?§YÃeŒ%fȵWâbm‘AøÚ°Y.ŽüA3%ÉI?¥x¿!75œ€Ÿ|Vè%Åàñ;wY.io ™ùhT…ÜT •r ¦CÓîâùmW†ÿ²[´if¹Ø±ô(î<õä-¸nÐ=õ/‘‰;jµe‡ÃÍ]k¿pïþhDMàh{,@ÊÚ\°[ºœ›r%N÷.ŒU¤ðM1¦+sÊ<»rÊ×X‚*ýøÔ^Y†¨õˆ¹¼æ¤×R.îä¹Í×ÒÂÛ)«²[üômÄîµõòA£Ðop£¦éƒÅz óü®›öÞæÐ ;N¨DÊ$¸Šd*uCËÄVTiÜáMÄV§‹ sé ¦žX>°\y „žUr‹.rµ{À×!{¾)”ÚÿràÔúéÝÍ,zgxpk¿ï-÷kË †šÖr½ ú¹àù{Šž;Áè|©(bP(ôÖr¡SÍ•—þ·%ÿÛÐb˜ÎL9 –/Ÿ¯@ÈÛÚ ¸ì €S´è™ÍjU÷уR"‚*Õ@NæœÿžÛŽ„SÄ^0z‹ Kë3ÄüRÔJ|Ž‘õÖÀµ×—=h!Öý^³½ìÓàë(-±Û„ú¨†Ð”HÕ/ùU›l5´m#Rè;÷M–¸µfÔ±L—F@‘í3݆3Š Nú: ~C I¾hMD  ”M­„)¬…Æ?Ò m­¢ÌP—ƒ¥]!qØ[¢ˆV's¶çuâ•V÷‹ÌF&n¦Ü>8‘òyÓœ‡àþŸL¼ÅWuŸ^ßP•„츪ï£~Âl¿¡ÞÈÂ6DßýÐÄ>1ìôQÒBÀ*`DýíÆ|ý-âF¢‚5™iòpÂèmx¨á ‚ò#‚l,aª°m€Àš¤Ò„iU'ZèKÁ  2ÌÏ‘V`³œý¿b»ó‹ëÔ1= “*™PæáÍ3ܪ,’Yªø‡½F“4‹Šh5żDæ‚Îi'-9mSÍÆ1IîôÂ3 AObÇŠGÍÃõ¶<©V0«] )ÜðZÏLDïi‹ †† nPÜ ?/ÍëØ„õ˜€u”ÙŽéÙRv@µóÓâS@ü”ÄwùX…G9!UZ°¼r˜êYûE£`ˆí,PÌ S²ðžkj³Q\ñsâºõ?—Þ»F–o7*øåt¾1ã¸9ã'% J=Mg«æ¦WmMÀtÃwhì@½ÿ<ÍeÍ %í1ÃE/\G!Ý03uL~"š_ÛZF™ëÆ ©x^äŽÛOÇßKôi|éÑ“Œþ`ë[#6öÞÂFüœ>n3Ñ|Ó:%óÝÉІ°håFˆŒPÑ"‡§wL¬õT}ž·´\Q…Èãªz´Øg7LQÓ%I !& •4Œ5µ^7~œáú ÍŠæ›fE ÷µRHR#4;°ÃB=¸ôXêÔŸÉ7œö¼úª–ˆ­ˆ½ãÁâvP’á› Rß Èô/Æ7?ç÷xÉh¾^¿ Ý;Eˆâ&+[A”‹l•±ã.w~ ÊáG)í8¡xhýÛ E=ŸxôŒ¢§wi`à^Í>E­×ŠMo…/N/Ý&Éo¬éˆ˜ø}’Ž~‹ÌQ@C™ÿ ÁDîâ< ä'¼%Ú=³®_ú–¡ Õ"šM‘i5¢•¢IE2Üè? Û4—M@øb »‡; ¼8x¸®Š/W Züã8‰Rü±[\gœ­ý4¾ƒâ=Hþ³3”•ðAhÜšJ³p¤ÖŠÕÇq½Ït´`7rÕ{¯œÏWÎJ`=‘6ãY`ü<0®¹›æ^oˆb"¦ñ¨E÷²XMÛ½ƒÜº V T§á Û;í¹œÐw 2°Rø{B«íîEEŒ#—]py·Iž*ðoRÀPžðkÃt>éîç«%¸•ʬA•,»åœ„ó;dÜhÂþÑjã ~xaí½¨³ºHõ?L§•–b¢¼•ªŒë+Éï†È\ùÑø#Â~˜? ™ Ñ«ûimÜÐÖ£vÇzä)l4éµõÊ¥ræ¹ï8D©¨îªÃÑ4*˜ã"ttŠS…ĬLÅ ¥-\A ƒ“Í´mYIWMoò("ÕªŠIüY‰ÞÒ‡ýÂþ¼¶²ÒÎù{+0?øÊ5ù$–‡È¹TãW]•åvLæì»}džCc«×ÌÎ×(B…`øls16âe©U¶{W ¾ 2ϰa4è6_ýB¢€pìí7Ž´-TMç Ê0Á€?¯¶d¶PIå·D¦Áž‹úBìáÁ0(ûf‡·—‚úAoÕòêr–í%X “h@N«Q{û÷¼qæFmEàOTýÄŪp9I”Eë¯s÷ªxµœÖ/xµê½î/løô àö:¡UÁ:4:4jï0SÆ »-®²;¡BÿU(¶ ´çKê„gNöÐ6£d+[ŠÀú‚Òê%ÿG„û™eåïa{”{ìÌ€·èW3C…ºÙR&¦<æuK'ú¡˜ Î~ó<.ª#Ÿ¸I>2Jž¹ô²)……A¼»’s s2aò^°‡[ö¢§²ó|>Ž´~6ŠÐú`£c©qç¿þ€`‹‹`A2xãà— ±^_PÈV¾èóúÒR• 3“ì4ØÉC»´Á$¼ûˆÃ³é;Æ—Ó}@JK§¸q„~&ùð"çs€Ú/U,©Ôç}«2Gª€kéàzÌaÚX‚"Ïd.€¥ã¼NºÀÄX숗:œG' GeŠ"_ÉDæÔe£ÉÓS¾NT t:&ÈQGN¥q,\aï=wú5túþ§çg†î×W‹Ðu ò«‹Ê…SÃë§Îqš­x5|ê»]íI€òPO06¥ä {ŽñÃãy>gnU¸Ð@7‚xóîmªdæ4ïLQ“Ä^›ïúc\âM’µnµ| l€ †Öˆ¦ìBŸj7æH¡b篕ÎêVv4o@Í%»ždÊ:U$çn6ªÏºý `™çôo7ñ‡PÁkAð4øØm îë†Õ%Õ{Z¾ë#CIÞ:Þ½áQ8v%Oz‚cTÏÍÏOYSœ 8Ìs»…—Ö†»ý[h_CÃÖ»1©AÍtÁ!v½RÄÎB\­¨m"¸ûÓfnn†è9ÍÜ=ó§‹Ú¡_T2³ñ´\~Ä1uŒM˜_‰†Ñ]þàÀX]Œq·üõ^÷¸#Že.²~K·!¾YE;|òx¶Ñ¾êäz‘|Õht Ýìž’ Äî\3…æEUÃ+øzSC9ìX›n=pN§YØg*µN9°¢º±“0ùX ä.{š:Wõ§ý÷ÀrÇÅŒé²ÂYëû¼ïõÛ‘ÅòÀrmß ¤PØEñ¯î ½“n{\x97„VcÈ›§ÿж7”;•Ê5zæZkÕcÍ…p>»Y}Ä1ªñŠ’§³ ÆH2[?äd89ðؽîÖ}ÑÐ'~‡¼ûµÂÖ/@²”ªI) {~n 37 uxGÃè!†áìfvˆ;FŠJŸZ@_´3>2‹0.ŒA¬ ža#çcðÆàÀ²Ú'c™ûÍø'sÊ“¢wnÿÐÉy0 wŒv<»6âú­Gû£.$…Ä`þqRu¼ÚU¸ÔqªÒGŠï˜þ“GÐϸRåÇàN¥^ZUª{^x~ã?ØÿMÿö«îå „qZZˆþ¿Ð?CÓÿEÿôõ-í,]ôõéPEœ6f$½1ÂnŽK1ÍjœÃ8l• ×[dÒX¢òŽ~>åÀZ*èÃFÊÇã嵯>+«@íV×ò÷ÉO®ƒ¨E¡¯q± ÍÍ£7€n)ıæÐ&X…®eëú¥M!á'Okv(2ô¥žÞôW p[Ì®@º¬¥¬îSŠÑ  ;­}…ÇN!x%y[;3D¬\īت޺¨ô/&@xüZ#þz´^|ñ3!Ùœtj³šjn“fÙfÛðQ<¸œì±a1üÝG¸.ŽRÉÙÅKmdàãUb´Ù_e„Ú´†24í1þ•ÈŒËé…¾äË™ûQ.tÄ„zæ&ÍXçn(ñâ%¿íüâýðÉÁO²ïà±u?³q’t«¨ïÈåï‹'íÒ²RÙ“Ï÷ -a£›aR,åçHM¹Òi»– Ulã›TˆÛQƤc¢Ô+¤‡i%]°†´ ‹*{}9ÛúÁ*ÉÏïýóÀrÓ- ¼²ð-Òœ¬}j7¶™p#·µ%¥‚i›»ñ¨—ºÓ(òä³ç_»\&˜ÿüoâKÔå’=ÎÙ½uŠ‹½2i²ÊF‡Ùøûo’YXlt-$€+Þÿý\m -íþ3Ï\M[{"N¤ß<}E"-ÛbÒ\“’nÓQp6C·iLÌØ[9ÚeÛô²ónÃõÅß·ê¦ýSÚÎ#èØî*ºôòªRžHIZs0/•¨ábŠ*û…ì§!=¡^v©t°Ýí_V‚þôª”(LBhéw %=‹çÒDœÝQvDÁé~«VQHlÍÁ$Û‚0ªYÅdNð_Ú“R@EªtS:D' èæ†-Bƨ÷KщJC‰Ù„¾ýæ¸ nL_´±÷µµ«.3µ ª×[ˆ——£ý>þœçzÃéÇʉÎÕ‚wªÂûŠeØ¤É ”Ö™ ¬ 6uœ=LÉϺT†¢ ÷›6³¨â‹â‘&Xã›™Â@,èÔÿ² –‚ö¼’K‚™ú`òøcøx‡Ów=¶±ùà?‘1büX|0xàÅ„¿_ð¼†ëïßï qdC·îíK‰~%ÍŽ17l†<…3] .’ÊÈ”xÔë×s†uê:¨î9œZQ²r*{aJ(cbÁ|=*Öµì¯gÊŽÆhl0¾‘‡+Dˆr‡m}ž•ô œ”0ºü–²oŠ¥û|ÕhN”Óúl41Fäg(‚ŒaÒKiî«3—öêNøÄO×y{…ô0¯2‚Rò"jýú:2(;yFÇÕß­64ÓÖCá>Ÿ×ÊÅ["¥ëàÂǦGñ·IåšMZQFeÈ䌔˜FÓ¶ ¢rw<Ô0¥œûÿ#NÒt¬¼éäùÎÈ©Í3¥HceLñîžVé“Ú£5¢sÛôï?ŠýÓ´M¥Õ˜-íª˜ SIލcÆ›s¨áó:ß¼ýßs`¼åâüýp`ýß ÿz §ó¿Ž Ou{ß2xâÅóæþÖ“|¾µøÉå{ûcÇfSÔr2Æ><\t^b½-êLK•âΰxuäÑ2ì9ùœé%Ê«O˜R Ðçšç4uR1™yŽ©Ï0¥Åúô".ùT¶«¯ ›o<'ÿ±.ž½3zò¿sH~Ï?X¼‡àÓµ ¹×FGGÇÌ×€˜‡k‰­ô5Ï)`HT€= ÿÀm,åV°­<ïñ°æ5ÿ³Ú¸#¯’4+䘮 ót0 fTŒJõ9  Uãf’5%IPRnÄYUêµ!ÝW┄ L"YâÉÿ ½æ7VEEk쾯¡Úõ9KîÆ>fÅ­H‹®Q3_ h©ì¼…3 æ¥s—¿ 𧇮êñþÇ?ìWQÂfP(­ì),LÈÏÜ@WÊŘîÁû<~*Ej?&ÛµîiC}ógU#P ‹µctêE˜mEy ­Yjó¯ÊÊD<þÛ \¥ÔQß .õL㨂BžMRÞDWœ6šb©gJ‡vÖÁ†<|ŸñÀ}åÓïÉ|kÇ·ÿ§ÙÄ ‹`™ÎŒ¥éD$G°ù»ŽJ“q*¢¿Ÿ˜Ó°Ú7㑱%–ùºz)½EsÅ®‚Z»I MºÅå^È,%ÿXZO!?Õb¹6³Òñ?-N¦š $´LéàÄÐ?T™*J1ᨄWâ¢d9-JQ# Dm9Æf\™ &äR¹!Å„çD®×ë?Pþ¿¡š¯‡×ñAFƒÆPå6Jäâ†ÎÖ¢àD‹ôâ°Ü»Ü¯×h4o?q3 vˆ’Ó¸MY.±·éYúö;¸×})RiY€&Ûv£˜_RfȨ¸Mp˜-;2ª‹0R•suJÍÞ¯¼$šÌÄéX‰ãv+w8°6Úîp^¥ïJƒÜ—Ô¤KrüóŒMÑ'‰X¼9†Ô”Áö±Â… NúƒWó:/îfX:#4 4 óëÕ¸•ô±,â­|`„Ñ_<.â½eîgžöˆ;åœæœ`>„ËGA|”<¸t$nRW÷EËhc¥þÁÝ yCãˆ%#DÖ=‚îË86²Ú$²Q  ªîy†~õ4cy`<9ÜÝCÄC„öuG²ˆvâlÈÚ*÷‡²:Ü­1§–Ðb·}ò«.‘¤|Xð_'²Ú½Ûk¬¬þ—SÿÑçŠ6..R÷y­— ã- –iFåÏû¶¢ŠÝCMÃc®KÖñw•†T-òäÚÐÞðCu¸c®÷Ž‘ù´ b¹fïß^¶nÔhxØ Í;—Ë[Dm†uá"0V-±Œ†VNEW?È·Ð×–ÎŒ"T¯Dª_ºÈ{Ó–X 9‹ÂÞ³![:ý¸ebœ©wûc};»¬òÁÜ¥ÑHÙ 1ŽÕ6÷ þ•6íK®dv³&Žœ¥¡†|ñ…Š4D‹³«k\S`´1ÕÆàm³»……­·†ØÖ ßž‰ÞîN[)«v=%ã›Ù¤¸X-½ýFuûtì–Öƒ‹Y'¸°ªÜ¬+û´uœ‡žŽÓCNMqmêµ1ot¯Ûvõp9âíˆÍñRV˜;‹Œc¸ÍtoÈ^ŸY6–¡„$¨â½—¯­']¡‘#Ïaˆ7ÇPhjÜ@ŽTˆr¡'6 ÁôL>çxÐ Jvë¹èèæCürÎs*õï?÷ÄÞ¥S†ƒj³Ä®Û¹Ôå‹}b€[.ÿÖªº)-É<ò.*p¾m/v)Ì­'ö/ —Vž9BXYn¶¼@ŽÂËñ.Ô0UJ°]«”.‚ ^¦ÉfúŽ¢i*é}ºÂ3ÑìŸfEÇ©Ô& x´óÈ3‡ÒÄÙbRhx|³0ÂÁçÓm¨œx¸çˆY>‡v<Ñ™ëuª»×¼ðp¡'ĺÉô¥dù$*-äÐ(¸ºÛmê[ˆìa¤¹Õâdzœ½A ]]üªíÏmC$/#1}É£.Å£z#¡³‹´äîŠñHJ¹ÜGj缪Ÿ_<ÚŒ8ÙØgÿTw‚"³“î¢Ð7"æ°ôxð²²ù¢ ™îÂj*þ1‡Ñ;Þ m`K˜ e±™œ€p™XÜ„Šy@ÈX îD+79ÙâpN»^HAü ¦š·@?ϺhßÚ X*~Ñ>UBNŸ¬4c\úûê-qšÞîƒïáܽ¬_åbÂÅ™Cpš¹ßI]ZÃo† ÷õ’ÕižÈQR_ÕZ¯»;hP^¥EŸ‘Ù#à·<èUy ÈjgHDPŽB„ì[(€rOÛ$Ù“O5}üš‚Iì_>ÃÁÉeJµíó¥>¸[•ìo’¡Bû½1 ªÕ” 5ßž„+MÛdX+mÍ!Lñ–¤kHÚD¼Ô£m69òˆ¤Þ­„= ÎçœÖ½êÓfgÏÆž0s#ÚkÒ ¯7–µË“nrÙ:Ò¶¶ij|Kby,‰~cŒìg ‡O*Ów?$³Ž‚eëF¸¹‘ÞXDèO+‹lçKËI ̃‡•‘èº!xóÄ…ÖÊunQ—ÞÆÌ§pÒH$»Ca_ †ÿsÚd1ÖÈi<—Á^-ÖÊb)“,6˜ÁX#i¼QÅÜYžÐMÝ}é>°tŽð³@çþHä—Y¤[˜O%.ÍÝâœbtþ¨"ìǹƒõÁÝ)93Âí\°µ„^vam¯ª¯3Œ¶Åð&ÆÑôÊô¡bûŸ<ØNÈx˜ê¬ßæò㮽Qdð™³X².cd¨_¾$ÇÇFcˆæˆµÂØd¼æêNÅØ£• B),3½_Dÿ(ð…›‰å„ÞØO~=C½"•6Ç”§ÙC"lÊËÐð„kªƒgž[Æ ÊéTíøÂïÎω³¡îø}gAä$É0 ‘¡i¼šÃZ†(“e‡6;°I'ó +&j øh¨aÄÀè‚þ/³eH°NÊC`…‚MìP•DÞÁP¨ÔžM³Í©ÀÃDÙ£Ã$ôß-q¸f±õ×€‘x Û³£JÕ…9J ²£´â´ìŒØdžÑ׆ð¡d~?‘“EÌÐP_9<¤…K˜)ÑpÏÑW°$ÐkªâÈ ‰ð0M•½v‰öŠaë9õQuGY¬î)ëblšWfû3yï5£+žFGQìÓÒ!¯Nè³=è+ëÒ@ˆœÎv4?¶Ô!Ò+ójÐGü¬fp<»ZmÚÚS[0B#AùД+în ’¯`w}ÓÐiU Ñ¥ä¡eߺ68äz =­à„H”LÝ$ ý|³FÜ"‚¤KüÑyœu%x ÷j+Í×ýúËHäŠu—¥i·`<‘X›lò˜O«ð…ô•æà†šÃ¨&RTØ Jql³ÏNt²oô0O(–ôÎljÕXJ5ßyɆb¤4â2+õ:~²<’:n$PööhٺټE=¾'xC¤ß‰%AíÃ-p,¬ ÌðšO¶úÌù]ìºË¼™#÷gIÅsK\€PTš_‚®ÝJÉò„J̦váý±›W™¤æF6>‘˜>éë h$±>‚1œðš¼"K)Jˆá=Ÿ1¨NÌdp^$ä«,®¶¡À Ë©÷Í´nŽ$T¶Yë§1՟Ū Ä;ÝßÄmƒ‰½ºŸŽÂø Ñ»—/gGÒ ¼èÎ"UE2}Á©‹°P`M: þ“£³õõUí¦¾Ò099¶7ÄM8Ë"ðé!Öåï`ýªéòh–&µtòÄJ3T)eÌ™òiŽ'œõ\kò¦OÛäÚú÷|»ÂÚ‰nØ¡ŸÄ(#ݧä´ë éæby ‚„†t“$' „G ù·ÞH½ ‰Aù‰ÐF y:uë”H©åªdeé½´_Ñ1Ú1Ö73û ²Y°á`ÅaÓcôø›zº‰›©çÍ»JWóØþªÀý>U©|Zþ”üŽ’fcÆŸ9/·oäË%ÚðÎ w¢{——ûÍXŸY%þ?o|ÉìÜçpÙ¹zçw¹McçŸðê»L*ÿrl2šÑò;fïÝLIùćCo¯ÖÞ°"¬Ñ’»ìŠ3V°žê ƒŸÞu¸‘ÎWX)4–ZÒ-Ýý°wIþæ$Ö;jþ_P_Îbzëºé|Ç1"ký_ïzމì¡Òwþÿæ­ÛFGØC Pÿ?e¸³±›­ítxªÚ–Ý‚ï›:LJøD$·v…2Fh6‡Fq¤ñúš Á©#q"•äÒõ¸»„dÛ5ɺ}YÅA&³9Ý3äv³©ìðš’R‡qÏöC¡Ž»¹mT­‰dÕòßc{ÚGkBh‚ó`í.4¥:KP’¸J% 4RYËÇá :jÖ0y?|£Ã¥¼Ü¼Èz’S(KJˆ2Bs ÐNHËÂG=“˜‡„ᬋÕÎ ŸO5áˆr ™…þ¾öv°’¨YžÊÕaÕšvý_£‰/a²žfÕîλKf<]BïãsŠÄ§ü$IX×6e‚¼DÚ±5’¥ßòн®t¦<CVmdIc)©vÚ§@MîØ6›@'¸9GøÃv`ØÓöpLýÑèYø8¨îœÙð¤OÖð0L>ûÑè»a};CŽOpÿx26<…T¨ÝyYB’‰˜æìi B„¨¨*‹ #4#]^ IaBj*Îp`ÐTe²Lè3‘ȤÆ\K-Ø Å^{µ‰2ÃÎåJÚ`ýÙ) ŠE SÇóיņ “UJ§&俚·~šôPB°•k‹´»8åÙLJ3#«ñ¡IÐ7æD’‰XŠ 5~FxÐøØÆ›¥c|L(U«4ÁØ4\îܹô¯/€''ìí¸;±Aþ @4AŠàé¤&+bÿdó¸Ö|ú°æÑ#– ýù±åPÞs õ,‹¥H ß ‡Ô3Á(,3•®ê¸JKIWJΙÙöùÙr9òˆ²_ušþe…µq3†9„Ž(3Ç^{¼§Í Qs‡ìfÄjÊE>¦Œ<Ûz ˆÉÁËxurÚ©¿|G¹ØÞë*«~3íRË8uЦ3S@Ä}FJ÷8î9·phöÁÚI”›¶yiÚËÁ™Ь1hÃD“ a¼É .êJËúŒ†˜d/½¾"0",”[ YÔül32À’Šd”~5 r ÈÎò“Íø–.¯Ä1 ~3ó˜IHôdëºÕ˜‹û×û[“Œb¸(Aޱ a}“B-QA«äí´Q,KÆ_”Ìa0¢.ÌIôÄ@ÙÔëœÆ`7"93òIŸœ‹†—SÊê“å¿£wóŸ”=¾u[µü޶èg;ÊÙ›vGˆSóN†m;\W;gjC¶…Qha¯ ‹•¸ìä=a­ù¡[wnð›•DâìäÙÓzvàé›Ï¤Jj¨­:p¯Òõµç­|ÏfÒÕ£)”ɨoW àöÙ?üÑÔ Âšn‰†ÁÓeá×>ŽÈòO4¤¤ôàÝ(Gòµé÷­ð;·÷—ŒÆÅòí€*ƱæR·Å‚ ìgþlàc%“ëÖÿ(ç²1fdª§ Y)¾Ýˆ Á1¨Â‘~îäšÓAÇh4 ã§?e]{˜7ß44rzffbP=H³¸ªÝy8tóÛ|nëøü­hósv“v“*Vâ(<÷uPö<¹ÛÌït/#ˆ/çXû²§*ö¨ÑºC¦“ð÷Ð(ši›ØÙ‡8$¾-Œÿ(8ÃCmí\ôMSÐné~)Ö©ò)ssýÿOkÙirñ* ÐÿÿÓŸq­š¶ó–ÊnªÍ‰9!iK¨¥#8ÔmK$ôò…ÊT– ·°CrˆÍäWWÇîYå6Ì_8¹Þ,Ï#ð¾xÒV pÏzœ¼š÷Çëµû܇کþËèž÷£²< ~²ŸþýOjŒ¸¿ˆK™-%W“ v‘qŠ­²ËL“.b®ÔF]T!i˜s³úëŒó|Ð9C^ œÏxolyx2^zHáÛ/ ­Ž?Y‡)ºÈ]Æä®NEëÊ$òw÷É!­Ãܯ8R4ÿž^ÌÆ5Krkˆv|šg> ð`¾CâkŠüd.HFâ°KúbéfŸ’É#tç[¹•¬®CÇ¡ „>uÖ¯Kq1zê BÙ:ºÖ{0žTvEóÔ?ºÜ'<ˆÕÏb Ø$mRðS -‘ÁÔ(¿¼b³PÌͯ²{qÍÏÖ¶æv{wlÓp•†JPðà€™“d ³îÇêÒZeá¾…ÄóO=Üp…ÍøTäÉ”»ÙŒuLmJœêðqý!ŽÛ5ñ/—˜®Ìdw“ÓOT=EµœTÕb¤Á¦A'š¤:9ë'FCþÝZè%4])1Ê•FXèy&.âHM˜ÓЈ°|¢žé ! ÓÃHÊòñ–M‘@ øJ 7•Ì9ÚúÅ]½ü©gÁK‚§bW”û„²¾>Èv®ûÉ!Þ«©‹ÖÖeã•A U –_G2ë²Q.fG$÷Â4 Šê­KÈxãžÁ¬¯œE‡Xòw™óºíYÚoÀ¿áŸhÓ!zj€pO$“Ívñ–Œó‘±R›~G6é”é°QY•Ü\5Ï"éN€%¤=õ0X9‰ÖM³z*öù]ù¦Eo%Ð÷³©ØXÈÔ#ÎÎOàs ó$ã%îÅ×ÈŒ¼j¦]‚ 6߇:¿À\Íäªê-XîÓå¦ Åæ hÈd6j¢Á»I6Y€<ÍHÇR•ѽ­ÏÎÅn/¸ãÄ•?LyµÔ^p¾‹¬õ¹Žõb‚ zÖ»,$A›Ùö¶­‡Hlœ•sF¹ã}ÁÔøFÎPwd÷rlÏ 5~؆{sÀ}æúëæžf½Ó¯¿„ë‡_O3¬ Øÿº­à& =hšýfM á¥ww8,µO¬˜|èßUJNk0üþ·Áåyd à €þÿ>#;™:Û»:›šX:[ÿŸn—e„ÿåv)>²rXÂCJ– n€`‹R®Ê«Šåm_ïž¾Æÿö2ËU‘ µ¯þx½ñžo¸Sip,{Dø»Á4ïâÔ)~mµÄ6/ëzphïûqµ‡ÑžJ^Dî(jFòÒ% ¶G0ªŽÓ'“~¼ ï¶€µbè9µç²ð:_Øô8ÇᬨP\¶¤1×VêÀ˜`_fFO” É£C:=O]ò .¿Ìv•CëóþrlÅ #TJÞ˜p赚/оù_Þ—ö»‘{—Áös†ßÛHÒKGÓQù¸Ã&U` 0Uæ©™ì}wƒÇ’+Ò9°Q *RÎy—ëŸê&½mÉSkK%¬SG8I¾ðyǼcéE8ýæp`ž±/ß¼$[¸ <6M¯|œ$šÔeôˆeò÷´’7iJ©¼Áä“}}¥ Äé͸÷)Ó¹°œÑ **’ô:oUâ•Sø¦4ƒDn§s®Fõ()iÜKìí¿Â¢ÊÞŸN6p~±=ëû_²í°¨c­ŽØUv¡9*6ÜŠ†8Ûu1Ô4F*T71q§½g—gê*•ØxÂXCxÖϳ«gâKÚÇküÿ.Ô´èÕ =ÿÁÿ'4þŸ‡…±nü<Ô̘voÄþlp<ÂÖ4<‘7ÀC» Tû˜æ5×þº-¾ó ìd¼@ötXü$Çìz%Á†©†_”BF †•T\Š+A¸Iügd¢ÂõŒ¢œ®. ÔWu‡D5L‡îÒd(§Ðn1p±­ $iaí•|c3Ðͦo»ø#t¿è±"ö¨ó7öº¶ÕbMšQ¦…ô¯ù?Xý@è‚8áz-,1ŠŽzkÛNm_³çÿeƒ|˜(¡y/Ñ!@sÐýWÂÄÔÌÐÕÆå? ÑÊmí¾Å–¶÷BÍðlIËŠDËi»ìUu[·m•µÎ™Ä•™³R­@bÁH™?mEöµõ¦@ØwmÒÅ5Ù(ðö3ê"2§éÑ›­+%7ž|7!=§9´v1ªvÙØà¾ÂMDÏ“é*ÆÅBv—¤i²q/3Éo™3àâd'‰OŽá4ñK€£!t% ”–DS0$qõ‘²à-Fù dÔLðÈKÝB¹á}Wœ( ð d°µ´6¿?©z†H}rdž^¶¸19é×ÏN+W3y˜¬/##§G3w–^ýq*íí›EBRæo¢Sc?€·µ ”Ú×JNC¶*+cHc*qÇ÷ªD߃‰”²±E)àØCÛk›f54U œ÷’€ŽšdÌF:ø›E›VM8àÁ«ñ÷ä²Õ“W›Î<ýjÔG´.íøûòñè·£+¤š©ÛGØOW‡ ªˆå™/*Ñ`I˜Q Ü´­H…xÀ4¨€¤¨íÌ‹íu×d–”ý:nŒ°A˜ \/hJ&€¼à]o¿t+‰K™,}W)‚–°î".{°]Þµ3Ê@é09’ÓˆÙ['Zy(é;BÖO@üáGAF±§ ælŒ† ]!iþ‘2*c5_SÆ—~æ6°u0-‹¯»0©6$ÂHfÚ„]úÀŽX—`Ǭ0ºáÁžÜÄ žÓ`Ú‘í{¡åÛˆm¼ž€òÂi£yå Ù¼Ÿðöû«Œûòëcê°0Ûå¶è|i0‰Œ&Úös[ÚîÌq¦Ët )N®®·ðB¬çBÇ…ü(R‡õËÓ¯sWgNoλ÷¬ùåzôŸU+ x§ŽÎoO§FOÆ#©˜­ g>¯?'ûò×ʾ4û%ECÌž÷ãCL¿fw¦æ¼~_—Ö\šýÄÓè¥zóZv(ø¡F%hØ”ÑAO g£û¸¯÷Ë–ü_F~ýägœÝß•þé4'POjÿ`ãÉ÷÷eþ©f à9u¾Ö~ܲ0qH¤ömmtïб$4m¾8bv‡Z€k`=îø°;Ãd!ùH°*IÂÈŠèžé¶¯~]ž>7´À%P\jRsL¶àaˆY‚9ÈÊ$¬¸&  ¶öÃÉ](ܹŽ/ùÝžÿ×õó—ù^¶ÒΛ@Q¯Þü+m„xUå7Ѓå¶ùÖÛŠ¸9/Ì2œë¤ÙQ¨^ׯ3òe÷#¯ß=#{~¥çO ߨ¬ïÂg£H$Óv½ƒ¤%@4T‚Õ¡‹ñF¥»yi‹Fã ©*b€4bì ëß*¸D€ËÔED³ÃBËÜñ» ªü@ÃŽÈL0#i:MÉhÂY›ãøáÐi ‰œ&t50jhŒ  bå¾Ùwwÿ,È"Âò Ù†¶b*•\8Wݾ2ÉGi„‚Wáôç˜ áw± ],¼.’•Yèü2 ck„zÏåêºòšpe@dZ6€A$¦æÅœ<ó?Ñᬉ-¾@Eqd`¿ÇcC#NaNEßÔ!ÂJPYÜ*š}4udµBW%}ˆ{$U”qÊ ªy-‰sü1|X [ÆÐmœªÃ;U‹Š“R0+R”pU% bLÑÛÛp5rS3¬ 5›¤™¶B¸l= ¾´Q@Ÿð¶Q©"yp[.ÓÅ=ðŒNÝj›Ô Ç{EøK}‹LaUÛ_æ³hºK§¬‘¯d» ÛêáýÏØGÌ[¶¡”)K…DåÙMçûà~¥Ê–ã3ÌJ°3YÆLòYÙ¨æÒm/pÐ:"¦ˆÑÎÄ>{ºö¤Å¤a6S†4èð‘]AžcC¨›Ÿ3}¡¡&,H"€{² ÜBAqµ&M”,®ša©àl‰ö²H˳cÎç`ÒM¿"ÒxN˜Ç°,¹ëŸö&B¼j$zéMBðaÇBÅSõä°M”eh—!NÈF21RE/†Äa:UQë÷HÄÒÅ47Ø>YŒÜj4Hh†¬eÒàê”C×éPÌ—•MSƒ,› “¥há»@ýkÚÓâGh#{~>=l›³E ¢_)9cÙð¯›ÅB‰üAÚãÑ®épÂ寝×ÙöVz¾­T>™¤¨ÞH) ‡cDN´Hõ‹ IFâ4|™¹ˆ †”X¶4•#®´aÁâ 29Cþ?†G”Ò*ºWН»P<Ãt4)à8Âѹl‘Ô‚$X‘Ú j¹Î’¯n>?ê<æ™w÷YðºŒ`R•BäNíÄbâ J+ —Eèϰœ²ÉWDĉ¡¢KÆñôq­Ùéo Nwdã¨[Õ5®Ó­ƒµÐ“±¡íXïEëbørüV)ÿžR²aPÇÎýl¶E*s Uùÿ¾­h…q\ˆlrY¸%`vÖÚYìï:î¤qÌ€¶øBö„HX¬‰ËÈÌî>øûÉõ{qãûúý€:ïüväA9ÂhHÚjóad4.—æ+±ÎS‚P¦‰3[Òü;2«{´\ÎÔTPÏzö¯9ªˆxKG9:Ϧ5ª´)éþ!Du4þl¬ ;ö†)®@Éò’ðn(:9%\„.m‚<Þawo±ˆqn8n¨Õ'߆6[wëóP)£ V‰‘tª¡ ?¹cýABHQºýß9 ë€pÀÞSš‚¨HÏ=ÙøxVÅ” Ö86 ,ÞÑ9–›¤©Üâ-›Ûþ=m‰QœxíÐPX=Iaqµ™ý¿’Š*®ÃÏÉ«ÖBˆ< ОîÁ!g(°¤Óªæøì{J­¯i–·ÊMj”Õ"BUøe9^âäo‚x.¼VpúÈGDXøEÕÓèÙq¯Ï‡.`AÃÐf(ÄVùÊ“©žnýD;Ì"+oÑ—Š=Æ*Ù¥Óæ’C%Á¾XÕ:T7ú "ãD8q"â8 ;«ãáåøñÚÓ4w[k`økn%eBf¿.•‰`“}æˆ?šË÷#ÿ#Y®y8‹.ã¸$· ØD™¼óc ¿Íø½I~?äq5 mί”æŠù³Hݤí|/Èp5s:‹æÁ!ñ~Ðüv\ïwT¼£ÓlÇ–+,uq„ ÉMlþˆqƒ™W© — F™iF?À23Mº€5ŠênWÉm²}AàHñÅ4‘abüêÕÅÏ!zIüôÌêY£O}þd䓇ªÂHÝQmзþÜ{.o…YÚç †`™“²ì¢¯ÜÆ8ýR÷ åÍ…!`ÈMnŒ«vZËG ˜ãÖýÑåF%•tè+pÓÔ`S°Ì>ÎÑîX[°ú‰“ïÏÖ¶B|^Úø³¹EGXðœsæ¸ð-¡ðyG$»b‘¡«=Š,æü{ÅÒ’VÔsß{?[Á}Ôñ†÷ëá{c®¨{µ»u·ºêÝàŠ×Û@¹CkÓµº­TŸ49ÑÊçj¢ [U@,Ã&´·•+ïÿàܲAPÛj¼î€%°ë €+ôý‡ 'Ѻk"ö»]î½ÈE¥"½)üF³U„‘ìà)eµÚ8•>Ñ,èqÃÇ=rquãÿ€ˆÅ’yÊ×m¤L”  ‘HNXÃp0èX¤³5˜g lä+®¼ÚÂÑ[/x[™=bŒÙˆö¼5•×Ù=ú£½))¨[‘7ýNG»Ý-,inÙAsÜïîoz(ÿV|>Cž›ºÑåÂb«ÝïR:ÿѦI»tj¤|¨ÁD/ˆ}S½„Pô|@x í •ÖÞsÇ kj£æ§0&÷ŒRÂ{ó'Ø÷ûqŽXW¨’„±ó…î@˜(ŒS¾¥ªg¾åuÌæÜjú†µÙ§ÄrHèŠØú÷H%ª]«Å4Œ\±.ɸH Á·võý;D?È=ñfÂrt{¹ma:‡Šï¼P« =’gÜ¡ Ó1ì8t@¶>ôöó .>áŸh|]¼»¦ÜÐD`o_‡õ6û>G¿ÑœÐÜAà?Øoïä  ô÷^!ßë~表C„Ú,è±xMN]ðüN33Ä |Vö¸äÄø:©Ÿç©ã°ÉêŸ/Pê.e!*Z\*ý6¨®^ƒÜ@ã5ªŸµÜCS71ÿ¢ŒTÝÜÚ[M AQl«Ù’13 4âñ{ñù(r:>ãÞrÀ‰²PèG"‰_s5+iÛŸ R3H©ôñµb%ƒHgßÀ_ªNZjk†¬ù¢=³ïOSÅø¸¤vªškÞ=\XàqŸªÏþ•4k›D50¶äµõ=Bž9‡“a%ÜRÈ–iTTCš¤ÿ¼ŒŒÉA±JT–«Åµf·T¾WÐ)HØC+$†?æv%%ßAµê³(#Ëß’˜K¶ó:Þ$ÌLA+¶cBŸ%À,%ň·ë8àúã… #ûððß êËÿ¨÷?Cp†ÿ9šüßäŸUkÖ[ÜíP !@"0å0ª‰'‰–ÞºÀ„2©VÃZ©4•6ñ1ÎvSË‚ƒ™gF`8±%²„jŠ6•߈ºü¸!œ÷†<ßI®ó?¡êzï5Gj÷îå ÀâCD çÕ¥ßoç§oïjòÏ´æ;ÜýŽÇ]ä3¹|û™º–PçÓ>B "¶Æ6V¸ÝÙˆ¸|˳1qí&h›<ˆp‘**¶ˆa©Ñpq6i‚QDÄt‘‹MΡ†‘Œm £ˆ (¹Hu ûsd8ivÙè¨EÏKÉ].2–òi°Åb6"“µ™"ˆ¹ˆ•šÍìßWÄ\ÔjÎzFñb££\GKw{ˆ6Ú®çbÁpÂÁ5a߆¶uÍ\<’ï÷_qþ‰'Q’D˜@ýÀÓæe«@|œÌáήÔsƒnûmïó §¨ýUt†Ff·}¦u¿W‹ÕÒŸ¨³íÅä2ªõâçXÏŸÅK´VgÆmv]îz™LѺó,K²­~–Ýuÿ³ŒUŸëÛS©·õéfTd« · ä^R+HÈ4õéç!;FsÚmí÷l’)Ÿq‰<ð|ZÞ…˜©Yxyî’cýI¸vˆõIÑE pòç÷=n¢}”¯@ûÕç91Áš´ ‚R¦m«Ä5kÆÝd›9Ÿfo¿Ì4W.Ú¢öA#ò“š'·äf;šÖ¬5í^$1&kRr+ZOÃf"t`?æÖÃïP~×b]KÿÝÈ®P³çÑ©xþöw‰Ùô«'§o„~?‘ˆ^N%Òº?zÑ//O"Ü$RËØ¾W`UÀ ^,‘z¬á 1‚‹„«X‡ ‘Ð!ÔÉ(NèB‘? êflòÒ᥀IŒ‹ò[Ce;¡0͸Рlâ±s½¾¦¯ºæ\Úá.žd"žˆæD.… ‰s¥Å%éÀ²×0ÍÒL Ú»¡y}×ÛCìgìôîê*Prž÷ íáAµw-…ι¾Ýà –¹„ÕÍIÈý¿+¿sò—OUšÖPF+ŽÏgOÙ@wÐ,°Ár»Óów±ôM1)¨àÊÉiŸ×‘¸‹Š'ÐÚµI·ë :Hºøºm oGrµ@3’˜ ô$äg ÊÌq‰pg¯—®’_iî&¸€_ê¼›2/É’ãȃhù‰Q D}"FC þäA”‘ïˆÁ¨׈Á°†)!K¢ý4¨­4â…æŒ°æq(v’L°GžÇ¿õ²OBÞÌ÷—Nkv̵_»ðŽ…¨ˆÑ—7ðÉGGA É¿ñL¥Ñ$᨟²¥ˆ.–Qêý,A_ŽòäÝRÐ”Šƒi„‹4Q§ ^çú'"ìä¯ä„é³M)+–5áwBLTnSD»äsÐÀÕ1ƒ÷8ǘ$,#k!¦®¼ßAòLçþÏÔ ²”LpO"yQ6ÈLL2& Œfl¤É÷Ýõ"ŠLk…qÓH6:2VZ/éé瀚,&MîÆùë? ‰)§u¸á%zí~K©è`™þÁ2AFP7i㷋鮎8òS&<å`±²J+yù‹d´mm=埆ÄJFž%ºGªšëŸÔ„X)Òbt˜š[rm‹#1Íâœçàñxôfà͓«¨ÙæZ ¤'ÊW?ÎñjRûª4ÕÀHð?hоŠo¼ Àš¼ÐjQïÿޤûÉ›Ë AkÐÚòbʤøWêƹÅNýÑ×MN0™+?S*X%k%CòŽÆ°«¡¬^p@Å ïpŠ3šã•¿jÚ'õ ´å ¸÷Œ:‰”ßxzXØÀš•W\pqj7ÈOlä¦ÜÄDt@!#VþÞ®÷ }À4˜I.§rTýG#̺Ac3xdUé8_ÿˆqàJL~$é+ÎP+¡~4wòÕ–x‚Tìy}2< þ{…è4–ªÔMz†cep`š‹Œ–M{P !‡o%ß–\ÅQS65îD8V"8 8>J2bíó¤¸bô¸ÖªýŒb¢í"ã=•:ÈÉëaßP¸½²Ì)â4‡»Mä¬t¸Eÿ”év–Qî,¥Ï;f-ŸwüÝô\ä!sd²™~œ¾±ƒ±úç"Ñû»Š³ Ì÷Uàß=®m_  ·)#^„3Žó¶áßU>R>ÿÓ ”àG?\³ˆöt¤ Â(µß9-ÞR’Ù¥F~t88H±Dʱ¦%‚’]9è¢Ü¢ _oÿrªâÂüiÂëÏRì_`£ßÈþHãx²eëþ4(ëKËËòfäÊÏÿƇ¢ûÓ=S8ä’®K¼<©¶ELÁ4e™âFòÈÇÇ9 ÇèÒ?䈄FJ®ÄQo¥CtN õë –Tã:ªèÖÞ?jy‡AEàhÝUc¤äü´óëÜð!Ô væý‘ ‘ÖÃM— RLJ0•áŠ>ô yO“äp·£Ùà ӟfYÎÛ}DtÏÌf\sˆg2² J̸Édø¾*Aß#*’;t‡ _ (¤GÓ³(×Â!ñìó^³’ô³’ð˜X—ZèÿÛµÍ׺ %n —[Szò¾5mÜ »”+)j•ü¼¸?Û׫ð{¾¦Wã!p®Œg3ÒïÕ?tˆú=‘&ˆºÄ"P’¿xü%&ÉÀ$‘Ø# .ò2ž Ù¿!tA'kq{rj³1žO‘>]¦R§sÄz íeÍðræ# !k# 'ãF¿¦Š °nX¶S%¸æ¦ð)fçöË¥ÉÚœTiÂf¢²«Z%³”„Üéoáí—²,ÇdÊ5\‘o‘Ðgç¶UÇm“s|Ù®FÄ×¿Ô Ò7ךJ Éð¡3×-ï¼b‚œ! Ò/‘½.YŒèMŸ;W^X‚u[K“%/A²“¤ù;‰È¯ºž‡È|vA9±ªéá$|3hÓNš#MfYzJæ5™¿K0ükÙv–ˆ¶z|Êï¬8ÚLqж.w´¯Õ’Š‘ç€À(}%|>ƒ0vÞ Lvy†8 ½a·m²°eˆ %›)F3Äã¨We¤bpÝ{¹´•bà$Uñ‡å1°LN’Öu¥“uOr#—¤²[V׿öò#\’ii~'~KU3àÌn”;wÜ€ûq¶$‚—o@6Ͼ‘Û¯ÜÔ² ×΋Ú/çðö‡=œæjFsïiÁBÿ²{¨ÅVfØí]›¼ÜäFJøE¾ÖÍ%ñðN”€NKOAmá™rÐz>3_•Í 6ù2U¯r o"šÃVœ‚ T¥?ÉцQâVy¡iZ¼Ø]×¹~ˆlŒj:îó2/Š1¥¾$Ž3㥄º²n¢úbòŽgÌ yä?ô? 9CqúÑ¥À.­ÔItþ5,N˜ÆfN1*J`3›9‹aQd*ƒ,ò«ÜæˆÍ) ±ÌrˆÁ,c´ëu5ΓSŽ.¬pŸÃ˜.„}wŒ9U1ôE_~™›Ü¦´’]ÜîÜÝœ¶p¤áOË&ÎP²Ëhu¤žgÈH.ç„ã`Ýίøiß¿ª¤37NQ­ `ÊB;M !l+zoJXÕ¦<Ф'mì¼ãø+ºŽd¢•ªC(ù‰–Û〞v4rõˆí÷>¢t#ŽkªÓ ÷_ƒNœVçK¡Ç<°Ó’;¦õÒ£ƒc™=W¼‹Ï†°Ÿ†TF¦5Qàt}ºA¶è¼á¶EUrç\–§q7ÄÍ0=•4ÜTÍ!NèÖaÈÚ‰vÉ™W]®”wã»bï’ï(Èr~H þÁ ú¥ƒßñÁœƒ™®Ã¼‰ÀªfÛ'Qĵ¨«*^¤ÛT÷°ãV¡ÀÊóPZDr?฀MR¹€ÝùCÑK¨[eÞsµÛ©Šw˜Ì¥ é-ó¼ßºcD”Ä׸øÒ]"Ê5(¶b—çå•ÊÍys.SõD±:1 ä)ÜZ7Ž |lY0¯Ngö¦üSÊ>ۣƚØ?†D—Øçz$Ù†D<3D¦­ÇªñbÄäðGÎ!6â<ÄkÔ~YJ;ù*?EûÅëÑ®ëO[#ì4.¯J©"ˆàåî1À3ì‚_úð6÷]_ÿªz[ÐVò`òa4Dºë,RÊ÷8RM2„µßaFEÁµ§<ªª.d­ÈUxÕT ÖL(l7ªë)BÎÓRßiÇKãÃ9EÒªô– ùÑSG(´ÝõÜê'±õ÷e¸ï [Å<{?ÅN­U¦!©PcºúÝí¶¢vZ«JУҘ†!ïÀ×kNNwµ*%¦U<±Hà¸,úQ3ùz9êU>¹ÖG“LA)¸o0OEi V(ßÀñAí42G×eXY·¨/÷Ä/^(ΆºÎ¾,bCx°É¯^1Žœ7–ÝÝ[CIu(‰Æñ$´ð'•UŠuüe•ôK“,!tKR*§ÜãêJe‡GáBº×9+«FD=6 P²óõNÅNÌ£ßËúÀè+$Pãuð” Uº ášFjUÑbú#ái4蕼Å:ØÀ#ì¶²i8Éx÷§Tþ²è}–ºžä¡•ꎵ×÷¨.º¼’åð÷{€f:ØÎ?i·{0ßU3þj-Rc>xvÊ RZ§¨Å³u+±ÃîÍõ–ýúL¹4½à/Äoh.§DX»Ì_YENѬ@Ë Î(lDS9¬f”È<ÅSt•Y"~æq¥phMºË©' Ôw…«%¸É9ÝmùNÑTU]å Cd¨NU¥S§y‡?\nñt©±ŠöªƒªÝÓÇWS°¨YŃ<$na"¿y‚ZBÒŸ]Ä/}†±dŸ\Rú¼£Cº_™üß9¨yIGtÁc`Ç‹’ÄÒƒGÚÇ ’e[öû࡟®êÃU"ïûh°Ó¹ç‹“µJ¿œÀÛª£ê€§TB&bàúçRÁ›&j,óV¥£ÒkòIŽ„/C¶APPQFUî¤RçýÌ+LàV™Dk˜Ð–¹‹¸Èý%ÿÐc ° ¤*~Ë!~ƒ`aö) Ͳ:*žÝt&K»k+vâ {æ^eápÍñ²hÿ–Ï£3”g[g ª“€ä¢ò ñFL²W²˜ï¼ y2W7o¢Û1™djR›×Z#¶-n²ôЏù,eràŒÄPÄáÐö^µ¼Q‚¾L ° ÆÿÓ¹·Ñ3Ëøƒà®]¹òøãMjq'ýêñ?}sözwåÛ,ç[·Zãg=å.ìyç³ÇÚ·äñßÏ*”"ZïiÁlÇmæÄY"ÑÝWÕLòëÔ…iëj•ËzTºîi¢Ë®;F1x‘DY~¶¡31ÎÊÏb¯å댙SÌ=#A™ì+ÎГ\–RP·d{¬©ë·’v/^!PùÄß—â¾WNžŽªh^1ãzxxOºæ çÁÁÈ–›*·9žLÍfŸzß½8–`¤_î Ëô;[¸ ˆrÑ:éþÁ á{ ãsFö|m‰õ;fWìy;šØMmiL 2ø`+6 ‡ÒÛ‡+"¹ …—ܳ‰ŽòaŸdï÷AˆÚ£7y}º±W3ÕÒk¿SÅy¿ï_Žtd„IòïÝ CM m‚KIö©Ìÿ4Toüã‹nBÏ¥xPa…dVte÷¼EŠDô?a,Äo•í@Qí7§¥­sœhSÒoWKÃ÷®³,í{~éW“î& Ô¯Õêêc›ó–xäœú,±·œP© 3 ÍÜ›øÐÑìî½¶FøMÕ†`“\¬µÄ6Ôµ«Ž¿RQ;ëzIëm¯¬b}ú fÿ`Óñý+¢jzÄØ9Á;èsÜ–Q"u7Ÿƒ/|i8é½#F^è÷«Áú ÉàTuYqæ‡Áï«–Oô`.žî‡…"¦»ÚtW@p±]J5~÷³=²›Žk5Ã~-Y]òit ›¢€ßSo0ÿÇ¢a¥ýnL ݺ‹þ™¼1[vF2ó`œFµ'ŸÊ÷D­‡ð©(ü'ÜÂ%Å![ÅZEþ‚2=úýüß§†–IÙKì|Aÿç43Cc{'O:ÏX¥A¹-„Þ/j±IP‘KªB*U¸0CVŽTÜQlïëš÷önk™÷Ï8\pÒPßF±§×Ùì·2è5%[–ùU4:°3þý¸q-¬L-+ RÖf¨ò+Ã)c0d Ñ ¬k¬Aeš2ý­‹A`+a‡p%`.ÛjçÏd ¶¦ [ èRÁ¢}0¥<â}é˜) t„i¡“œHŒÂC·KI"Êã)&!æ!êª%_†-ˆºþÖV«õ;uÕ“&±j×Õùvåüϵd‚,âÁÙdïÄbÆ‚ùIÖÚ®Mår3j¥Ìf¨°4Ë’¼ÔÖ])çiÑÕàÍæ(K’´ìêªp³›•Ź/€¦hÈsÄwE𺞉x¯‘Æû³öæ/Nš†ç NþH´¡Œ/g4W`½{ˆþoƒm`’j ˜> Ý+òÄž6ÄäÍfˆªö ˜¡Œrž²^µ&Âäù ÐZ`(×AYÜ2BOÈÆãžý¶N°'?ÊvGúíM³ý·±ºlæžjýaĉc‡[¸9ˆ‹-´=éXNôèX´ÕÒchÊí³×0 ÀJÚ˜ ¼¹´@wö‹›¨pIŒDpû—Ù—Mƒék²Œ&¦vî8„X×c»<;Iù¸.в”ã)î}ØTõÅw½%"ïw¯zú‡Ô¶¯wQŽ?ˆ;,¦é· ‰¼´+Û¯’NŒN”šÅ@R8Ÿ×Öy-yñ¿Op]ì¨8kߣâÑýÇï/ÿÄ2Ó<|¯è—wNfyC{|ŒØ‚e°E̾ØU iZ¾Èããαձç¼ýÅüf_œ³¸ZP<ÁoÐÿ†ø‘!¯'€Àÿsˆÿ¿C׸UI6~™wÜvÙ®ÝbT€„Ž= Z‹IÈÇE½FCkRS`Š´¤Q»hsrÊÑÝ1X<òo> 1¹:=g&úÌóú¡ò‡qóã÷æ¶fsQÐ;HÍ$޲b$¼ŸÏ(±{à!XÛ_$q³pȤ¤Ë˜ÙäóE5±³íU?§Ñâ¤ÕkÛÊÃÕ²2z:2 0´Ù:õªW22ÏV¯D4¨š]!ŽU!žh2ÿØÌÜèC}£Ý¦fþ“6k¼Mgê:Õ÷fd÷“À]pà{P‚¦”³ oƒ‰°¢8/ˆ1ç”Â[äu5ÙØøŽ¤ST÷'¤zG×_ElôIÚgšõ/ÖSOS—L”J$¹£Õ?—ÔôÐåŽR´Ö:º†þèàKÒ¡ž9Rø36+š§¢¾GˆQ'̲|+Zœl:QC¡<(U´B»Ö!Ùj‡D³²•ô£'igàym¨d]ù]v£ÝÈÔÀ[¤=í-Œ ­[‚ï4›œT¨äØ-­Œ)›µ ÛðåÈhQ-é#Ã_±š_±ÆA±¶Å®ó?¾Ù/‰Ôž´Ã‡×ÎöI½')ŠÐÿNvGPRå8ÿ’“©©‘³Éÿ2ÜjÚ:o±!ýÖé7–òKtI´”°tÆÝnsŽºiÉ#wsκ.Á€F¢JÞ(4e]ø÷ë”Hôj>èr&3ŠÒ¼žl˜£KŸÂì†÷¡HɰÆüN„»&ñYŒ!ïpXç –J¬1å̲ÂxýÜQ¦ • 1áF@ôAûæQà ¤èå9c”Îä?•÷;T*à€ÙÁk‘ìKË}áWÒ2El`!*㨆@•ŽòòüC ÛæˆJ{ÇQ+ÌÃfîDBWýΪF@žUÈ*ô‰ê/Så4`cê:Õ*~zúguÂtTC}|ÍÓtù½3{òróÞ —M‹ŸA šôGMÆ¥ ,½ˆŸ/»0Q+ aÀ<é( ýèN*ŒKÖ ‡`FMÆi)TjÊ1é[3ÄÓIàÐ’³ˆa7׃k?†»òðpz0Þ ïèâÏßåÛщ7¼Ã«³»£·Ã³½½sXßÎÐõƒê×ÕÙ{ ^±ã/Cú/B ÑÌxÆ ^6ÄU¢t¾¼0Þ‘¤9f†cf4it 0eŠC%U&ô´‚êX‹½b§P'x·;T¶Öš‡Õã'Ø}@'Jˆ*¸wÓ+ ù,++„꓉Ȉù°$Š4BäH̘]ˆ[6°ËK˜wÁ D÷yk¡íè¿$Æz¦¨rMãÏÓø}2åøÂÞ˜JÔGb)'ŠXÝc>u¢d¾^¨µ#Miâg x̲{¦4‘š}_9MÔ+¿ ñ"ÑŠå;‚y—Hu/¦þ5¢å•Ç[…%î(£^%_m~BÁt]Vät„H:â'L©“²=ñîD{(ñ>S U¦Ø%ÕV%qaµâ‘<KS¦hTéÃw`o¡9úϧ¾øI*·bÂøpéM¡SÁ¶Nˆ™óëÁÒ€J£?Ù!ØíëUFc i’ü%„R;Ž2™‹¥r©ìú…ëd5¶únoþ ¤}#8!ÜÇ3NMuï«ã~xžüN zAe VÍ×cÇ„¤7p2Éö“TeHûæ¸ ÆeSäÁúLþÐnlVÖÏ´xk¿Èe¦Yosüå–ø€ ÆòŸÊ~KÌoDöÅláŸRÝÔÀªVƆ=n)ñºÊ8wÖwÚJžzÍ- ý˜%óñÁ¡¹1­Ë±h‚”Ú¨=T°ï"òßßKUohZ~8 ÏSp—VÇ3iü?X¶–ìª(öÄæ©gÛíÒP¸÷§Ðp"t{L´£‡Æš¨·$Ž‘ ™2˜U6§³›Øh` \ýíkDVk€~¯ É÷¨r;ð­I¼&' Ï÷ªÉÁOi„@§Å‰òû9ãêኅ–9Ém„@ÇŠ!çUÇ>æy?P%yeƒú€:öH©ç‘xsÍA{[3VXpl =3OewP*¦§ÆÇ*™ÄpÔs,¯Ý Þ:µ×Ÿel §¶EDÝžÞ-:+Q¥yR™óî{¸¬¼ßÁÞ%L“™äÑ¥›uƒ{{Ø>ÚÈjèÝÕs>i½…÷¨ó;{‹*½Š¬Ð•Sñy6—$ÇW]‰7×ÿÐŒµ¸ú"¿$úzl7÷'ªÖ)a7öÛÍ4 -díÅ*¬¢6O¯L#ä’Ú¦.ÙÑŠŠÉEhÒÞ?Ñ@ÇÈ•åzu.sWCèË”+âˆKG‹]Ãý¤Œíˆû™ÿáA=K@¤·áãºä+!Ñ·!+ËÆs¬÷ ä¯$ärÄ—ÀtÅĵš‘¸ÀˆÄ8’úòÊk£îþ#|=óˆºs­7ç/QeEým½ZO„6‹¨ð³xéÀü¢ö<œnýÚÚržþ$‚"lptH ïTˆÀšÅx}dq|š F~W¼Øô Ó¾Ï9àAì™6tá½ýý2Œ …V¯/ÄÆ½ŠÐ÷¾w›ì%oQ轫Œ=¦$-Ïç™[ëpvÉr¬ƒE2:‘·?çÏ‘éÝ?0}Ž…´8X\ØmXE“,§‰‚³‰ð9.=“4øå/ø ðAÅøû?D2Së¯+ÀÚÿ‘üÿ‘<Æ­úÎ[cˆýÄïâERi"Òq¹ïIe¹)mvE]y«És%&Ý’œ)A±ÜSJB}+¾¶ø\¾àý‚星+á~Q3$ô7Éú›;¼%&\Ñ”@C77·ÿ¹nì¯øÿ=öÿ~\žáIó£éôHÿ€ÚñùpBcZËçq^j¬ÙRG*ìY£3QÓ5¼ ‰ée¶N‹L6:D¾P×è⌣AW×èß`Å<ɬj„3÷ü¬ùm鉑~Åa }Ãó½aáཀvl($»ýëÎ&q4*[“ý™ŠýV û$-èÏ—ðôþ›Ë¹¤qrØÓèf‡TÜ·pçw PDÊg_˜Ûu¼@åáMÉèÝ£(ëä5ÌÿDÒ;§"³à$MZ9†-‡'Æ /B&'bmÒ”3=áKËøØT uK®•–¸Ž»k«ƒˆÜ±F^F¢ÔépmÌ©ƒ;”ˆV—Ä_ÃKË‚¢|¾ÀbÖ’}X´õ¶sVÆ„Øh£o7TýGT¹Sö¾ô5O˺ÞËçkw#†bê—êM‘]²>e'g«sˆE%†ŒA®ÿx!Úâá†íìvXÀ­Ë•‘Ðãîçëlrë(cI ÈôF“¶-Dtüô¯qOo†ŠGä9úúBȤ1{tO^ÌCCƒ Ô-åÒ6‚£¾ˆþ-$×0 *˜´§xÒ‡Å1!nH`ÙºkЄýq³Äg+Ø.<‘@J‰^H¸°ÆÐ ©žNÁ Ôy°ˆò爳‚.z•âÅctÏ\µÃ¿Îð:­_†­‘]7€áŽPÊSe¿X àý/ï9"‡u£1lŠü ›ŽøD9>rDªâoLÄ}‰È# Øèm)v;bª½ e¿QóÅ@W©NéåÖ¨ùZWW31K=•žf<ßX´œ(‘;¢§ô"ÝÃc¤lT„ïÑ&³@Ø€b)šôóIä ÂŒz&Å¢MÕ’˜5¿ï´¿÷Áwäç&‰•lã­éfA­#]¨§êhrÔà°÷.–:fÄ S^œi߉Å_¬mŒzFŠÉdi–‡w,Fd—Ç«¡Ov¥†¤^Œ¥ócÂ?éÅã ™'õ‰VÔ᛫åaŸ®&ëêLÉ8ªÇ.¡°It7ÿl€ ³œb­mTŒ#“¥KÙ™- n¯°®è˜9IOûA¼8ÞèË3Am0jtÉôÕÖ!^ €(ïÑ;Jï´*7ÓË•â)©™¯ô #§pþ’É/ÿ¥§sçÏ4Kç%†† ªÍ$ú¾×±îìª+³ß—š×°ÖÖf1\S¼(¿ÿEñD òö!¬æQÑ~ùômcꃛ@Ƀ:ÉhdWŘ‚À· ×Ö%Rk7Þ—¦åñ…sЊKÓ¸]2mó´ß&Ê*Ѐ«¹½iysï1þg•GU>ô¡+€zyÚZTU3ëÎ_Ç\¼…%î•P^[ÑÈU‰ (cÁ¹ý»:3bGïÌîb€LáŠÂ¬¬ Õí}MAU¦U¬QFQ:½CÉhÄ­'¢ž‡"À¾.Á&[z‘—¥&õ ÷»žÞdž¨ÐšG‰ •EÒoÛßĵ¡º«8› Q„À¾ D»±x±ü̱4ÐP àLŠ'ˆmÃ#hþÄ|M§“xïÐÄ»LâÈ«MV4:ê ‡Â6?Žm|ʰ|É÷—ß& °$hÈ6Çý”pA¹%âÚÕU­½£ÀN–5ކ…UÚûRq)v ^(æ!–Ü[€ ÿbPq#E†@m4˜·uœ L•3g†h;ÔË‘Äö¸Y*C_H<¾Ap9╉ ÃmÂ$–X¦E,þiùçÍ×R›ÆFq×]˜¢‰îE¹eR<Ô7Qñ(€O2¦Õ”gÂh1f×Ê¿à8ŒízáüNV‡ \QÍ‘82¾o!d j3T5,}ï‘Lÿ’Gб<ãéûÚFmìÊ®i·¯å_¤æ˜¨Þ1z¥–‹þÀ¯?Wû({nœæ¹¸Øe©RE?¬zÌZT¯q¢Ê…ÛᄦºIÖ³dš¡yäŠQïcËUHô£ÊùXÐG¤ª÷’ƒö“ÚþÕÂ°ÅØ 7i‰Uê,ÿœ2ÞÖ#M_B¼ÜrÚ°–y“V@Ù2wV, ˜ãp3ïWð‰o ß7¥’¦å`lÿŠÊö¼>ŒB† 1FVÙò(¨^BÒ\Q¥ä9²ÞIºÒžIeΑ¥F¼£±¿œfïØ%t ;ÀÕã+žº©@§45XÈ|£'w¼—™*ÝL¦nþoCð&‡HX7OZ—SXöwð…ŠUŠyå:¶ºšÅTÑZ}à^ÙÆ/÷£ÁMM—/ür5¿¶ŽO·7þ€±}ÿ —åeWa’¶X>܈š¡Wε_1¼oÀR9?]é´cyÈjͱ' 0(ç@åî à0w_Á¦‚b vH²U,hØÍîú:ƒ]¡Äxö<Ô£[!¹4 h)æ´V+Îr1FªžÕáôŸ¶ á0‚Èe|´äÃt÷—vþîøÞÈ­¼]­×ê˜vV…í —t½IAâ.Øî%ŸËõðÑm@ ÆšcÀkëw‹‘×d6Ì®^¨˜¦1mp¹¢ŒŽ¯¼.ÅÍ+™Ý©É‰|)8Dš8+W‡ªÊ.°h<)0#Õ»Ð&}ñ3µGñ+p°éÁpYHS0ÞWodšˆ¥ÅêÀ¶çY”FóèZâq¶¦¾XK1XH]l(L©ÍµÁNËè¾L¾7øéPKã¨Ù’'‘ï^Á?èX©å ñ•—ã|ÇRm,Ó÷ê§V\©om…õú{Ô€Xª½#¿P}ƒ ¥Aý@ôCYª¯t§S~QŸTÅeGJ\?ŒÃaþWœp¿õ…ŒT¨ÝO`xWŠ×Ê—÷{7îèAèoGPæ{(*1îhéÑí¢V½ïÜn ²eÎRYúÛà%³ÍSJË¡ÌrÿwH7•'™-aí‚}ì=rŽ‹ó€óéùÓq’¶Fã‡2#ÉË(«öó,ÖsaõÓqÜ0­± é@ïAì‘H—e’…Ú*; ƵŰTÇQ/þÿMÓ$l¼O@ˆ‘ÿç4ÍÞÁÔîÿMÓ4lí·Ø~óô•Öíl?L§¹L¦Ø#µ=1¯MDaÁhÙ5W$ªâ¶^úïâ’^×+“€‚$Žwn;Ïn3p†JïSV˜þ2(àЉ¤n ù99x8B”1ëS2faŸ¹vð¥Ë•F•‰MZ(_¡Ä|\Áƒœ†Q¿kî¯Ô1Ÿ,ª|Õ«6èWCï=J:á%‹S2ÏÇg²Ÿ^ä÷¥Š}Ï;q+’’m9có†¤È ¨#JEœYY@ˆŽîxñ°=œÃDñÂKœ½eÌFD‘¬0¤˜Sâ¨)¦¥ Ìíd|¶íèÓ¥Ó5Í)óÜ2 ._G­|v…Âk§lQœjü‘ V. 4b~=l¢ôÂl •³)ã¼Å’ú¥Ic"Ìd Òe[¡mS2ÈÊ€V7¹c:i!r~1ÂŒÁõÞÆ‡‘m{°w{zûzÝ+ü]™yð‡_ äêëçÂØ‡¯¨¿±þ.y¿Ùz¸¬Ä+î1üð©ñsQ@ˆß ›ÏPÁ2¹JŒo#<0ÔY¤×c†eüö©†N­YH•‹½ù´ G…0¤ñ(ÖŠØ¥ —₩fhaý<$7bu°âé ®ÜôŠH>ÉRÅ@à"ûcίŒ.Ñ™œ„9³ ]Ë}¿‚<ŒD*t{7?}éEyÏ ;Vgøû .')}€Óðñ¬°>¬Ê¢“k"”¿u¶Í ©9Ì[R{"ü‹¢ P·bVÙn<­+k©%T•ð’:Š|´ŒÔLa„ ®F ÍlnM….Ã¥¡äÓ–]i“^ЂܚaÃÔRÛu:6¿ŽqV¯Iïžê>¾H‹ ?x|™“µý>ïÔ±Ò½´ç¥¾%„yµHñpŠVš[ä, ’‚U2(Ã|ÙÈ—‰@ö‘áHõ:ú$ç²‘Ïæxõ-ÂLÄLõº…NbeçbÒá:ÁnÔñ rª´‰ô{½ÏêU™.ævI²Ÿæš^¸—Ù"f™Ek«‡š»öpeKZ¤ ¹øµ^^¯c>gîà?6 yÁj©u™Ó~‰X¶úŠ?éÒn5º„­Ó*øÑz3ÀëÂz`lj!ô‰F‰¨[¬È·ê‚uáÞ»aš…î­ápŒ;U`’_86’6¤Îœ™m3 ±WoÊ—šê² ûؘsLùxàÓš"9œÛ'~rC¢|Ñ«¢–Kуí}{ë×±ngI©›ÛÖt_†•}†KPÞ+2‡ª™à‰Á®š nBà3»\Ÿ»A‡ŒaÎ Ô[®¸(¥Ďgü3Õ€AZ)KÌÎI¾ÖݺH££ü%pz”u]®¡Lµ›1Ϲ{OÎsvUqö‰#¹Íàe’!h?×¾ÇÎ5BR0ç@n  Ü]»kŒ]V²(zšð6“âH'7 õÄ×YP2Çr©è©Ãö‚0’!…”H†ô€Ïã;;ˆ¤ú£•¢Ù¸#¿¢ÅØêØ„éŸØ`š-2ÇÌÞe(åyZu¶°lì‘758r%®æ¿ëðD(ì¤Ë±Y±‚œÄ y´K²‡±y¤Y“îbCià”]˜ÀE…ÆNIHpŠÛé©ç½D²²Œ¤ÖTuIźèä»ËèÚ»Š ÃÚMÍ’_ aE‰½/°Wøµvègq?âT¹ ØÐ£1Z#ýªibf¹F¦TÜB0÷ԙؙTìF LDS¤hç}„gæ³æMÿ@3 hA“åñ­Xåù€hqG|üøUÙ[;LÖ ¹/̦õ8‹ämÒ0DY‡ÈÉ›j¥CóÙ ‰Á’¢…µ¨}3¡üÅä‰ 4`& ŠäYS–HÖ¤U¬å¢#”rê€ÆÓO™Ö±‹{ÖZÑe ˜ ùßóKÁV *2‘.œ¨Ö´§pf¢E @›Ä®kŽ_á(Ã(ãY• ÕMDrƒ¿œ:Æó3­&„*OÙ5&v-z˜Áî"q&ÛöŒëa8­FHÜíi3x•²¹˜|mcæjPfu¸¶wØÄÌ xÝΡ}ƨõZó "0$Ǩçdó¸G®Î¨½~A6vÎV ÀhÓñSÎçu°ÔtlñßÌß Jy»‹ç € à›tưŸ—u”C²d‘ÄÀáZòQ]ðÈ–gÃû2Pò«ªC¨ VRù1VXeèáG'›*øØëân÷ñË„pðÍ-]†½e½la?Œ½¾Ã#Ýí÷é³ ÚQ]’øCü¦º¹c—è©,C|ÛÂla[õD ÁƒLM‡ Ñ»1MªKØÛW”°À Ø_b3YÈT½f“Âh§]µnåÈÉw„ž‘~p"E½¨0';B…Ôz-´Ýx{\J"1«™Ã Wײ½0?°"#¿r—Ü÷™sMHt³ð£Â{]2  NëÕÊ…‡…Ø6Ý=±²z¿™yŽ.>* eܵM|¤y£>*¦ßâvNÑ©nYú䯨¼Í¼ 6Ï„ûƒ· ”Ã>¹ ;Ïò!º¾¹u|…€ n‘6G´w·by9órÕ ×o@ &o‚?“¼Ã3¡>¯Ýÿ.¥`6'P•rzn/۾ôÈ1b9™zÝØîëRc˜Hw2[Úõ_ž³6¤e”Ì6¶ÐÄNS±sר|‚¶µK®n_ƒVIÓØîæ6¡ Íœ™Lh÷`tÇØmd&¥ý‡s4jf‘ñdšfákÕj’BÚ{ëÉ÷Ôj¨ÙÛ§)Ö×®HÝV]yÛ·Š=ÏïGg3úýŸ ]:A‡Ð8fí¨Pc«CµÒ+~¾´ÝÅUk2ÅÕi< _Í€Á|f5Ù4'˜XiLJx&µ?9ûçnÃé,©²§£Š±zÖÙ i˾Ï~&²ô 3Ô¶Ó…và•‡¯‡vG3o.˜T/ îÞ}3†þ~v-Âà™)«ÿ©ô2¶ó§]!8ÀêÿžrsüÇIkþ‡ýy cÞô!ãÏR ¥÷àïÙå(™ë'Ù Ç@2´%!:®n°{v¦iÅâŠØÏ Í ãyf»Ï@»QêTEŠÐi~)™C\SdíÚöàt`tû’(V4¨RÐgef}ÍãuɴɦªäR¬JÌö37’bLÑDI熈ۑaà$ $sR†dƒý2™ÚóQ/ó~d†´°rùÉkúiشШtÿÊ ›ùìq²Äùž(0H«•ÁÙ5”Ð%ä‚èâ"£|#ÝõøÄÃåŒÿ¤G±Uis2%Ń Ë…ËÁ§â3 |öYqèñè?©khheeU«T†³£’¦ÿe&¹-Uµ­5µ™×}3Ù˜ÿšQ0w~Öƒ©æó0xñS1µ•Ð0¹©&ôPÑ+å+W§Ÿ’“ ŽZ>ÈoÊÖ[^ÍŸ±†FI@КÑç€ÞßMm8³ b¼-uíÙòîüœÜÜœ¼ì<ßÀØÚ\ž~Î<Ûz:Åì‘Ú{ü€óöø2Ų|WÌóaRGs¤Ï°ß¡ÇSÚÅS¸S+"é9*4‚†…|¶O8D'AJNÓZ‡Ä㔢õhá±ã­º^èß?I’ K1²GÉê’l–6ƒš€U¨r¨'8•çfD?Ê+ÑSG_ú+`CÄjO+E^š}ëíÐÕŒK ³õ…×ü~iD›N4UŸZ÷z‡±¨2’x~®yu„ÍåE–"¹OÅPºLïA<½«Z•|müÐQ[/#?  bÚ"»`“ MÀÆ`ª>ù¹ÑY²vgÈ×| Ïƒ¦¸öÆÅÌ-YqúLGBüw‹õ"£0Äg­®×L@I¶ njeäw`̈¥Xà=É™À‰¡`©ÏZÄÈ"ò\cv&†U”mðbÅ.™`4ï^ØqJ&­%ð§Uép¶Ù‹_¯õka+–/©•Œ*ÂBîLqm{ö«‚Ný9}èAïþåY Á âd:4¥qxjŒcÍ“Û ã@è±GÝHñ´d‘¯—ïçãºû†É2Ô/ú ù*Ü7V,t Øà©X®×„Õ6À‰ *kÎIE’«.‰ì\Ùö‹-D0J³×ëjÂ&²„qÖáÏ>nVdð ¤ýÞzµÛpˆZ—j¢ëÔŠ”þ„K>H±S6ª‡å u,õð1\:·.(&@°”’»ÔãR’@ §µò¼\âø érì}‚ kâe]Â]Nµ›’²O&öõå”ìºYËaçP†­ûÃÈò´ž왥0’ý×ó·æÈ¾¤@…°(v[v }E¥KVðç^†ÓËsBBš$ŽºÕ —¯óç| Þm|Œz yU%Ûnùð»W5¦ãÕ¾\Š\÷vdKÉDg_^ÀVsú”ãþìí]6­Ö«iâÍ7o aÛ–¥FW¨PÞrˆ0–ÜPd½b8(èÓÞK-ç_6^@s$Üò¹Ž™å¼áí5~Ú¡ì7yÔpØÀ.ŒfSØõÌ÷£ÒüÈ^ÜðäÌd¯~ÁrÏ€eò5ÆÓ–5ö?²vDµ€rØ5—›U¬×Äë.lñhÜÓ€Ó¹ñ³Ö{xc<äM[ó眮ÄOV]Ä–úÃŒ¬L§€MgŸ?îkóõ=/¾¹óåž¼W E{$ˆ§ÁÄ´ºÌ°*’›(åÌIaЦ©q·¶§eT¡z^ªäxŸ^§³ìj_Zx ÿ6ǸǦþYüíæWžØ„ÏæÝ«¼}ºÃ˜¹à9ÿe`­…n¡<õÌΕ zëb Ŷøh?þâ4¢Ø÷5%‚úpùë…*ð!»BCÌi…œíÖ¼#DŸ뜎&gÃY™®2Ú–(?)BP·8 ÚNEË£øÁEìÇú–„kXÜ׿0a”±ÏLxeÓhºª¨ ÿ4HÑü”óMäôèæÓk V&ýÈ®uãaÖÞ_ve \A¼­BÊðèŸßä‘ð¢R[%W8K)¿aâŸÙ_0}Þº-îî cI?³ì™íš}¾K]–é~ÁÑLûõZ™8ë“.’µ?K”@ ~ûçr’(Ü\ (ìfÕwÎûùý캒[ÊôÆ:•LR;‰/}ÛŸ» ^H”ìŒoŒÄI:¹ïwvl]ûÚûÞø’Oû©‰¦ÜªöÂk«!ËdúF"=œ¨²?Öï¡ë¥þÜmº‰Ò ´˜ØÎ+ähÑŒüÏÅ#h O§+ÿíþ÷dìÿ7Úµ M9b5˜¡@"("‰Lª>B;ã'*ÅNžW+h»†ÈÒ¨`´Bk€ïE鸛²Òì#Æã³”m?‚g?¢ñ~$Ëwz¹¤%g¹y~þdê¾íÅlöýz|½ÿi{óŽŠýÁ¦!øúçú¸á£Ò»äŸùÏi£§ék‡êvô×ÒƒŽVÍkÑø¯©éÖÔ2ÔÃõp i‡cÑÛÖYt¤áë†Y@{Òl§ ´ê“ãW÷çÄö^ë’~Ý (3ÂÿÞ´€öf2ZàIq4mIµ¿+–e`ŽËò|´(V4ŠeÅòÂ&Ò§Nj‹½£1ep?àõ»_P>H!½’Ò"åÃ,Ãíe.DIñ¥!½Ù<Þð'7 (ð«Ü“édžrлŽsd5Hy“Â{ÅÛálVæ%5!,ºŒ_·I›Ì!,Y—ËLüöÍùÈ?5¼H³&M’ÕkŒ”ìÅ%Ž?ÛüŽkޱjLDýb§™i ‡í˜ËÅ&^Û¿Ý€ÿÀûn¦)°½Ó*% ˆ“–b9f’ê¤|ÁGî û|jgÌ^“m´…;„ðt\\‡VÈOâ½N“­2 Øò·MƒbC?fºP¯ÁQ߇åÚãýC[+æ6%:…3Œä0~ϵÝ÷D˜byqí{Òý66 wOêU¦ $LŒ $O-(;FHởxŽ&•ü72g¸a‘´Z*‰ ÎËôo¤4‚+…ä'ù‡™©i}³ÔÝõ§51ÀÓ 7¡`8ÅÞùó¡xIþ—S.'15ÊäýóР¶‘ž$¨¾¹un´×©¤MxG´"D®$\14RÑ5@pÌHœ IE¾ ™sðœ\ï Áa;¶"i¿w0‹Ä£•¾–ƒ0·#Ú¼½"˜*¨­DÐå´${§扱¼m¹J8&ÞͪæKLKß’^·Èª a•çê…±BüÃ=¢ã‘‚¢U2’¦F#ÌXzP‚.ïDƒÎ’¾®aæHjµü3X-ýëÍ5Åjß+A«wÄ3—~Wtm–þôhÆ[Áv¨»ÓùÝdWž¡*M—ìÀÛtTM#«ô‰S‹d½,˜M!xh®?Ý]ïLª¼ªI,fÏ_òžÇÌ;)" ›Ê)¨|#TãÃÇkËÃ16é7J(aÝ  Ý1Ñ õ)¦Û®Ü¯>= a×â2ï»…~Bëó²›é?_v ü2'ò×”ƒE½BFyYžùØ øøéû¡ã\þžÑv Fhɺú»Qz+VÞ%Ì9%”Ë:B²”dÃK9զѺ“’+×åIò,`vÔç¦tÜžU¼fû\­ØšÅÝÊ™Ç*Õ@ren9¡ ìZƉxàñ9Ú`ã.8cQ+ºÓŇzè} ¼epI'ŠCló}9Ü=IJ ZbN^€'G¬ë4•×/&¹7µHŽWŠ‘ZI8ù*“n×} _ OÉ¢²ø ?s'€êÝ5GM— íмëmïÓßLíA5[Çrñ‹„v•ÚaÏqÀZm0 ?ÏmË¢oÊkØŒÀ°”ѹÈÐ’™ùâÕ|àfÇ%Ø}iÔ@O'ÇÈé•5Ån¿ËK À²K£0fU·Ó‹“–'Ç»ì%Ú;‘@hàW>ãÎë2îã”5=òaƒµGÍ&ïÓ:ä…oN+&N¿âÿeZ˜*´ü'ô¡à+…Ÿˆ£Ln”&?ç‰/jch-§3ó³‘Qœ®;|å_C0Œ¥PýÅ“½Ž0W®Ý!;{KÔÙµH ·Á8Ëmײ,6±¯_^ÏÛ|qi°ÙM<í£ð`’äÆñ*>¿KAû¦üÔ\ßÑ}öUnÉeâ—J9$Ñ’FºX†ý¤5Â,Êûh1xÕ^^&º&b^ݺüÆ``óP[_I ÙJœñRÃø €ÿkþÈþ¯=vrq2ù!ü„‹áå]®ÿ±²þÓÈÿׇôDÿÿMûÚ›ÙUþ'ãÿ8vÆ?üßàˆûßønfðÿRøÿ ¯ÿµØýÇJøï/ü¯#ù¿_ÍÿÆ4ÈÇ£ð_©+ß#eã+a&[3–bWø¯Wf€3×à@\þ÷Aø?¾ÙY[›9ëYÛ™9ýÇß ;¬Äß[=qveËâ”›|žþ€ÈÊVGÕ¹T‰ ‚Œ¨Z’‡FM[‰Z’q¿;½×É@¼Nnr¾XsgG?ÎømNŽ\]Ù»rvf÷74s¼ñzeMS¼û”꡼e@7àD ¢çM %(ÀªÛèÄJ˜(ÐzœÐ«ÉÕÿP ­ÃŠ•­3†õ%éé€h¶Kð³²›ßNGm·oVD‡öëêìY;dÄG—¹<@Åô±Ñ"… #m*¾fK¾ˆß>ÄZ™ÃûK¿Agíõ€†£ãápûͦþ½§U‹ãCþòiBöúÄ«³0èÕU°_,½[`ÇCý¦Ž0$!§Fç([5ðßxrÄÕ»d …%×1†tX´˜ùÇR+(W"hÛ”m[Ñé4Q–Ú¾ç#­;¥‡Nöƒ®hoïnÏE/âÍÑÕ–Þ‘ýî¼ÿ^æãÒ2* Ë.Ø8Û‹R¶Ö·ªå¶—NºóÂa+}œž„Ž>´É<8nxOÑCÙfA ÀsýL$f帽 øñeÐá£ýd=ðÙýLf]ÀìEûeLßþl¼›.’¡Ý:}›ŠÄÆÆv.vµ*Üà/î¥#¸z‰ëÑ#Ñ+Fi §ü«ÇLŽ…D?w˜vcv¤ŒÄFîÝ.NŠ0Tž6n܉$NMM¶®05 ut*t!À¢Ç%Pºg¡Puü”ó2ŽTØ×À.@ÊÄ×-‡àJНaJËÍqZj¢ÖGÅcbŠ86b¾2O'F‘ëg^$bªGQ& \º—î(¨€$ÛtÊ·,ÑzhÑ``Ù>.¼P“gT„fvPéÆoZvD$ù3½Ðân4lEX?@MØ %ýHœØ¤ŽøzÍ¡ý¬R‹c F¹‰õ)¶+ÎHð:p¿#³´|làÂ`ñûe%äû×j©Ÿ-z 3 ÐZ°æ ¨¾@8Ž™sXÑm[á«Æt!~µ1ÉB~®mmþŽüª DV E!_ŠÉïHŸ'×ËíiŒ=&Óßé(?'ö]›Æ(WÔŒòŸ¾>îW'yŽ!<Þ¼««aoè3úûÏ)£1à`àšî:š¡ÜÆ:¿wÃöß÷Kt}Ö3Úl3ª]Ðîô´†Í™É@œ”uf¼z îÊ+‚þ%…'êWq:GlÔÁtk n"1‡½A€új]ÂK+1;`ñê]…Œ6hšþUl~›UÁ“›°¶’;Ù²¢¿= 80¯4áNV" Ì'üDsÈä%÷@¹ž>ÓA9,Ú”´«E¼ï¯ªcïŠœŠ¤fƒd”ï®áLº`ë­{ñ>‡²ÔŠ)ïèh. I,;ÊçI+Ü“Jï<é_µEw•Å ½'A@éì‡àVøKÃÁ°ÌMJýXj=ãñÂ7Â*X¡îä8¸ü°óðîáë^ŒÛ×ÈžAΚ­`’`JhËõz2Í´ÖYëÙ)²ädô›«•ïDµõ\@C4ÒA[QMV¨óU«* 9E—ŠÎó:ñYZÏ  ¦+ÜÏ &W=ÕR‹ŠH)÷LA“å[Uk)«úrYx] ¡L (N ŒBΑmoßÑ8¸­ÆÞbÚÓfà”¼i p“¹œœ{8§÷–£U°îò%BÿæÛF›SÐôA)) [m‹dºš8çËFÛw}òì³õëCJuI^‡PF5$SÁyâí ÿéü˜ÝA/]Íe̦¥QRù`Û@²ÈVÄ^©Ê¯“)>ý¨r¢›  ê¨ŠCÈúR@–ÇðÑ](‹güݨàeåý”²4!KÇr\I’Wvövå xÂ}9=–8e—ãÔU}ã‡9Û k@÷,´žÒÆÐ”ìsr¢BG-ßëŸYá?!jÃî­—ÍDC¾Õ›è‚Ø!¹à0LF¥…Ø]¨‘ì2;ñ³+3ð®< ËOä ÿfG“Á;Håœ^Ûñ®æµíµHwܶ;+ ¹ÑdÌÃú3‘‚c¬ìÇ>ö#Ø…­¢9锯®Ž2ìÒ›ê‚Õ€æÕ‘f-PðHH¼¥ÑÇÞÛJ¨¸ P>%G‡·ÙfÏb+Ež`s7ˆï0£ó»ÜÚðÖx»ùLŽƒ’ãùG¥GäaŒD²èº ]… *i¦&öU!¿ ÿ8ÕèÞŽ‹|•>ËÜŸöë$¬‹®j<òŽI_‘ ³îãUÇ’m}‡pí+gž:%Ég#F™Üw1Ë,{Ÿ@Þëèoƒú¢;öø¼\Ñ×™«–¡TwµˆXÚŽv.¼½älr¡\EŸÜ¢²êQ@Kl€Knžì6U̘Iïò÷²-"?Ò2²Ï‹h ­ê`­j5\Î×/8~G3úqÿÑp«².úâ2ÐéÉ8“³mûn`¿ÃÖK.ÐSŠP6k‰]®CIŒt÷.ê¹°!þ7\ãcÒù7‘¡ð° ÔƒŒEãø ˜/z8\iÊtáf²§û³Q›#ƒ\àÓEಯ«hgr vÿ§¨šWgñBlx«&eVÀí:¡Öt;™N$*¢Í‹ —I_|ô0‚9$ƒóåá|îϺèª|¤øíÎÝÕ¦ê3Ñ»Ã~AÑ}ÌÕUJ»>µˆ©$t-éøòóÌmŒü¦C¹þ±+—.a’°öÓëÁ¯“ŽCdý£3LS(/Ïfž«ó(x¨ë²°$1oY$€ò¥29Ô½öÊâþ*Qo]–U:×+3=½êèG*7º¿ø „Ê*Ç#Î}Q«['Jù"vø=ÝÝÜ$a€J’À{"UM4·ÀÕC»Ìs±¹zrçN´=È”æÔð2o!R¯š¦us¨¤˜UÕ˯ARôÓ¸Y!•d¨òvÀ¨YÙ”xï¸ÊN…BϬ,‘Ž j¯YRo–ÆôGvС.6©aÕzþFnºP8¦‚’Väh "±³K„™S ªÂÊ•-ôvêÎsQhÑ·—"E^½Wƒ <%Qb_}Kž_繯Zâ: ½ê“mŸRÛ{øž8øì¡Iië0±¬ ×Ïô–U2ÿZbCëSøÜ«Ys{½-Y5RZטÚrL…Xhf*lˆ-‘ÙálZ#`-9‡en_Øäå‰þ¢ h¬ S&ÓWk—4Û …Ifÿ¼†ÿÐþ« @kºî„hÆûïÿüÿ/²Z4¶±Æot´Åýê É/O[¨7žC’Ç"¥z…›…µ¥%5'Saµ6ü×I:öuãwˆ)‡“S”))Ô*%N˜ÁûŠ?¢¼ùËÌÐ@$”+! ˜™¸¶–––ÊìÈ¿ËtÞF°ú¬^Õöï`KñÐ…iÿ˜Èñ-Ÿ¿ÀX¢»í‡¦¿@-‘}0Gºõ ]36…–Ñ1’  ¨–Ëã)Cˆ–Ñ êEc0C\,—_šA‹cŒ¤ã¤£•Øb yh•è%YJŸÝÃÿ!Ë*iÆ…Q°KÀ°˜>· P@™U0T©@É*¹‡bš=»6!çr@Í)q‡ÔÉ¡@ÑXªwŠötKÚêp¢ãØ®j`¥{È>Û±`{ºØf“œÿ¥ý©žªGOnƒ<Œ†¦M ô_‚°éÎ_ç^û"¼W¬E±© øÎ;ëŒy]‹HÊ<ž¦EI¡ïܸK(}8K v\”R.%ms¯Kw—zuat¤mG$§U³iåúòÍÌtM½]—¬¬”hÍú<½æÔ¥e{EÁÛ^ µ?Ý({T‘‘¶oÜ'/I¢´ö_PòÍÇõw^€Ü*Ã>ÑϤ%Ëžöç¢ßú\y÷×ívtRÓù:¬ñÒ{˜…Uqw¼,N›™8xHÄl«µÆDXè 5ìÉØ¤¦Œû¨¤óPTƒ¬)êbYÆqtwnó™Ö\ßž°¬É­î‹í”=ÉñµŸjM˜Ø9>i°=·v ,öu/to/*ãôN84ÕÈšbf»±¾øÚéÿSzG™•ú®9ËþSqÚeí÷Žû"m^d³o¨Ô"%QOñO¯AAÉ Óï5[„SøvÚd(>ÉêdߢØGœ{:~"|‰l_{¥¿A¾èÓà 7™™W €ÙÓê‚+¾ÏІ×+—/š–ÜØ¯Xé@_ãðß {-õâ`íYMųQÿí9keĨ÷Ôôµd Â÷=ìPUÒ°L›„ç¹êÒÓ»àѬG ò'û7×”ÿÈ«+Q¯²¡ùT=¯´ÆÐwkçÜÒª¥»¬ ¾êÖ×»éõ›BQ±"Ñ|R¶7¬7FJ'Û•#›M>×3 ¿{Ń”.,C ýŠç%1IŒ2ûÏMøâ{Y¢«&5­´‘ðn¯­œOêÈÞ–›ýè®ßM+¹‘¬|“¹»+ÍØŸgv°Œ‰’ìTܪVöú<´r :8Œ†“Ý+Ie>wKÐrÕ‰>0™,)f€TkEÒ„HûµÔÐÂáÆt­˜Aý»¡úô¥ ~¯NÍø›7V—6²ðrt¦ÃB:ó”¨/J‘mò‰R‚ëx³C=ï=A•Üçž>…“°½Xvœˆº~¸‡:º~é”?§Ð¼shÁï¹²?€s%?' 'Ë>@ç6gQ2‡?ÃFy®3íЂ7{GÐ(ñÆ&l±[{…æ·ÛœÔ÷ú‹uÄ 6LÊxKg¾m'…²æ•6#?÷Õ•‰ÔÈâVôeð¨nÄ5[ˆM‘”Hè£îvkª]„œ{ª€`´#@ûI/ ³–èhUÆ“Wq¾¾Ñ ºÇØ4è>pŽ"¯ÒìõºOµ>/Ÿ9ËØ’$¶‡e:WÇþƒ!t€óhE÷;þ…g`´3Ó-Xm’3‹| ie¶2KŠÙO$<þ-—cýÎÎËlZ5^C¶÷Ø‘ǬBM¦òg^oùW%`×  UTó¶Ï,Òkp\:9ÆfZœ.‹¾‘záÞb-w¿QÏ\‘k„Wò»Ì=G6í_ƒ›võæCÌ^¿ÁÙ“-ž©µc?}—có¬¡z¾+"cHǪ́‰}°ì4Ü.Ø«&âÏ•þKfÐâ_‘J²O5÷ÊBŽóN)Úhjô°angIÙn!wòÓ2TÐ|/ˆ‚NHr¬”9þ­ÿz=ÄM2ƒm¶÷ŠÜuס€Ìvi)Î×BƒƒWΰ vmä¯Uï(Æ%–’CÙQG~­«ÞÚgÙŒi­_²öx2¼aŒù<»`îkG¶Ÿ‚=â@CNqzŽ1ô_Ó‘²×€­Œ‚QÀ%Ø•§”8‘”ãÚLo¬áÉWñªûÝd£©¼l—K§ì~X/Òbc‘tëË0¢”² 6:©ìÀ¡íâã3íÅÝçh½·è,xcnÞ˜~-€þGº5u²ð`µõ&œ½¨Óa‡hEª"…ŒÖ$ ÷ÆŒ¨%Ä õþ»n$h9 ÁЀ¤’ªjü®¾z¿½”»è§Š1WSöɾ†%”HæÅ‚´œ$¸µŽ¾f¸§Ð|ö.ýr…ZÙ_±M;ŽyÜ’,†GBbtø7›´Ìâl¼½ ‹ÈÍ¿å\Ñ(¦l¯h9ëöÊz!iÙ)ã²Â*´ëa[+a ˜Z Æ~ÌѺ1¯Š¼"\,‡ÌÉÄ ŠÎdôK¼4ÆWÈ+ø›8 1ÜÎd&{”7ËO>WÍ•°Žeâ9$o NÇŒmg?PdasÜÀ»ÛJŽÜ”X›–³‘¢@UUÕ DVƒ¨¨¨ÂEMåsÜXµ!Í(nÜ£eFÉ…Îñ&ÏOØ-ë–#ÙC÷³xz2fpܽ{1*JãÙ[_Öù g7Ktš$ÍšFO:z ÀŒ|î¥;„PÔåèè)ÌZZöÅäíÅyB¢ï»8⑯ÐÅÕ#Å-€})ŸsÝW¨væ5=F&.JÇ+¹(¥Ð„õwŠ'ºqÕRŸ€ƇEöÃ΢ìD]Íiƒ(ú;Š›ª6ˈ7Ìý-°}G›§ÔÏöÆ_0˜áâ]aÂP“¢ ‚Žv_QÞ˜n\Ϲž5µ‚+¢Ùbßq$mSÈs@L *5ä¾…G¬‰°ûÁÕPÖE%Ó” ½4ÙÉMŠë•2d0–¬çlåzÙD²o…©fR»Ñešæt•ÂfÛ ß& l7+þžœ5ó-vF,ÝíÊ@¯|"7™( a.|–`ϸþÑD‰ñùêVa4 …ãUP‘v–/öÇùmäTš^Ðn†»ˆ_U$¥Àá>¤¾•ºíÆÙ”²À¹´Ù,¢ C£MN¦eÚL¤3‘µVCÝ…£re3™åéA)´;'q÷tHð°V¶ýá|_<6{c2C™é¯aVõ×\!ÔÖ×(Mþˆf n„îzjþ-z2]°ÀI¼Ñ»fTš(TŸf±àRߢ€©9&]J“;ѤòsÏDÓ íx½6ÈÉc%ùÜ7 µÖã¶¢À¡ˆo]„“™Õ%f¯FzkýŽçNýÜáz?ôav`ËTtÀ—Êxüþ‰ûA,ìHÒã EÃa…óžÿtfwBÞ´8üÁücO%˜"àÀcúáËkoŽéuöƒ3· eLYþ𲄸þB 2ªI%¦8f)™¢PN]²—‡6VO‚äsÅÆåлv† 1:¥­“A!(?iEo̬¯„Q™ï–pŸ 'ð#ÉE ÜÄ‘ÃO nI‚—*Ö“dã %(R3š—¡Î«’g™8U«cÔ"¿+¥¶*cTF«E ­Ú³c±°~f¢0߸¨ÉÇøhÑöÇ6¸¥íAL>Ò¯¶ %…Õ®EŒn ]Hô(Âô¨²}Äìo\­uEžµöz¾3òýзœˆ…¬ÁéMMODÈlQPÖ|¾ÅG…ÑM%]€jð|#aè+Tîì_Ñi´Ìÿ™(vµ?…ÕÍùÂmþä˜LÖ³Z:a³8eÅTÉé·«!~“â9%x¦öåA‡H68žúý×ý\Šò™:,Ûÿ>“ÿÿ$}œM¬MlLœ=ôL\ÿÓõ?dÐ •ÛVkâÞ•}2Ù±jI™L×íg®‹ ÉK‘—ÍÉé+©®d°5+rÅãÁØÍ„ß^Æ@>ÄåAq×—ÁF®*R }úÌ~Ùµ #Õ#cžÚõ}žËÀ†EâÊÛõ}¦ÙJR™\nc‘÷‡<ñÊÖMLô©˜GÃtT¹äc4f.)Sn ·© êS  [6mѬš]èµ]7yÇ´èôð‡åþîc&M!xê J•~bâLR2“/ßÑT$ FÖI”I¦ðFtSWqgq<ÿ7¥Àñeà*b³Þ`[ÇF¬ˆ¡Éû¶òŠ£ŸÙÙí-m¸y£€®ö]q¡A‡“§#3o&_¤RZ›ÆäT"Ê÷'…~Å8*o“9T“tW*u'-{w•cöN‚²„+qxSWù>‰ìd¥x©ËŠ`j&Nõ†5Ày$#+{·!™ðëÒÁÊÄ ‚~7WÆ·÷¯ëà.Ž®.n®Œì¨Úõw"NNNž´ììÜþ•«û+Õ×Éñµû8{>TèÁÛÐ|’4B ·h¶ŽÃnf?7Ú?Ê4ïÔé$Œjª®Ú"«ÙÚ'-]ãzìÖãB &©t]ùEãeaX7@ƒ;Åüc4øØ„)»z6.=Á"F“®s,sH§©¨È…‘*ã•·=É|溕2Á¾:[(Üz®!Ä™wæLOFºGÐxÁÌÇŒôÌÀ¹"ΚCcU…Bã ä|ARÿCTÔâjJÊŒwbýN,ªû&î¬:K´ù´úê´õ챃l1Æ‚k¿º¡ ¡thý2ª—Ëw'°¾Tª±£¶{¼?oÖ¨89Ú1±¤§)½0kF^œ> 9Z?X¹9½°g^nÀÇ{’Ÿ×ÃÇo;²±#)K„ñú0þø²0£oˆ,Êâ wXu%{gäre`ìXN:ZÙø|ì' ŸÄ™§‚t»÷ËcglÝX½¸?^ã©rò€Ï bÞDS#ÙÆ-r™ÃÐ`ð¾%ù£ÂqZµîvÐÊžÊǓıç#Éý§ÍíFLv7“'â*wðÁeav‚Î t‚“‘Ï$ñ¥Þ}•‡ùÍùˆOêûí__D”Á‚O—™©Èào«ÜÂŒœÓµ”ñ>ffÝVDb¬v3WÀÌ*¬¨žÖ<ø¹X¹ñY¸½è¡¨¡ coÕ-<Ò‰´!í„X '‚=óLÏÇcÈ{Z…Dã+ù#aaŒÉw’¹Ãݶ_Þƒ¢ýúˆŒÂ~ÖÃÉ<6 ¥ÌDñ5ÖEÙnïO,€Ô"X0`ûeÜjR‚ÉÀ´Îù;LÔJ¹òcUÃlm{ƒnŸ:0sxŸÿb)WîôýQ7¼Ð[1¥ù7SQúýI½Švðrã\óâ^I½¼¾Šð^ÝÞï\ÅèþnNøÀÖÝÍÆ„¢9ooŽÃ=ÈÔÎæüÜ96Mð£Š_iÖ> A‘ýsûñÓ\Xõ}æfá,AûsK% '«¬ƒŠ@óQ‹î!R®©%ݵÜvEŠ‹]õ{–º¹Èy#A`›%ù¤ï Ã5¿±æ£ù#ƒ|XÛ+ÍúóNºe5@n¤‘W{Ão7¢ôB¯GοxX»w3R{RÑ$¢ÉñØueÎWr_$•> {}tÈžø«ßw.¢¢“5\æ­Ö-¹dšÚs¶áçÞ/».D˜D²Ygü K_ÕŽ\Þ ØÓî“3›Y§³¿F3´žù$wã׎ý{¢AWóO{¸x@ñ÷ÀSQ,=Æ1ÿ—WÁ—“4èD—Gâ¤Ä^í=ïÄ^Èv`+¼/[.„¹ö±Ø€6=,¤r®êDýŽ7Ò)¿JP^F #^êdoÊ*_ñîÿEus`’4ûÃ?tkVmF܇Èv0ù1À[ímW­ ¯ÉF-—4͆%/©Zl¶‚gð¶oº»·`æ/"JÆé׎#’c2hõLFdê¢_¡6 T°Ü çbÍHÇœ¦õ[Z*ÿ€.@Q™½Ä›úŠší)U±¨u ¼z›4ÞëèË­wülî±v_·«²p~a“†3³cÌúJca›ˆÐ–ðrfdá}Š‘Å3úÝÄN¶h‡ª"~z(ü¹[>ÅK±m^"‘C)»6XiüÌù—ųC#È¡rE#̱ë?¼O®Aˆì±o¼[sE áÞ ãn'T&7GÆâ}…¸b¸E€3¿ ]x×3|MlJé§YçºÝñá†~×7à]Ï­zhZè­g·ãæcMV/¬ü)%[2{à«;*ßœJû%ènœñ8²OV‘B wpts²³ºZ”¸«\G€ŸD´j£iÇqÇ=Ì ëV~PPÛãP.TD€Wù‚a"R<¹Xª¢á‡ëªb§È¯ˆ¤B ´*äч®={ÇAEQ̧ø„tW0Ôù &_þmjÍsº>$ü|[1‘8ë)8‘w֌˱êKM¨ŽÏie×*‰FSš—ŽŒŽ c±±Ì²ti ~²Žùï, aÁø>ëÞ(Ÿõ›«Á<^Ý„¤E( P£æ§f’,zåi¼˜íâüûƒ€ØÔ ‘ã³P;Éfõг…„¦‘jšø›Š¹±´>Éhýñ”þ¯Ö‡‚²ò§"'ÅÕäƤ;²bÛ-û¦°¬€X,€AÑZ+ü>Ì.ZDo— WuÐÎoå&3µI­ƒ.w–Vtœø\NŠq7¿l¼áþkãja.e±qͩŠ»EÈ]—õa„^Ô˜/Ü&wìI¦­þ°©®ÎåÁ@cýôŽÈV»Ä "kª-Lš ±à#›+R¸‘Ó¸ÌÞ~Z>6ØX;ŸL|Ç,ÝKB,Í ©¼Nþ6(+|„r¨/‚6¼î[ÒÅÆE‘±£`‘:mk~ Y=+ÝŸš™ÒiçvA„ýÞÙèâ /Üá¾ÄV‹ÀÈÕ鬭õ€,çl™Óe2qqêJjíåf%Ƽ~ƼJú¯rpfMõ™ž¹¤Uæ2á'Xp§ðêM ‘‰ÇÕ(SY¦ÄÚ¹h ‚Mà‰Ë;;9Ž(ó~áZ:´¬Èðß4öÓ\Ñõ+ß׫þÓhš«7 Lï €C}ˆ£µ†¿ÜysÉjnÙfé}QúŸÊÀ¢m[Šß©§&qÊÃ)¼×µQûb‰U«R-¦”øËä8Ä^…/ì`2ε ùCªú·TSM2ƒÞ—á·Ä™n`+À^yc¼âˆ0©úŸ9E ’Lþúu†Ûèn%Pˆ@×3\ÕžÀ­£%´4âxž¡ÅéÝ¢ý Ð*·2B+Æ_ pªf̰QüJ”¢ýÊç‰T˜2Iæ™Å+lµÕ‰ÅŽØD¢=Ò©êeC;Ã0¡*h4wèÄ¢ã!†Ñ²µ«N;4 Â~ÔŸ£6+â~ÓTûÑŽB¯ŒBK6?0z|ÿD œ\îú’a•9îeêPž~™Û‹¢òÈ‹º<]D.>ðp¸Rñz‘èÖ±UÚì~©&N;ØâA•Ñ£Ð}XÙÆ—GÁ6W14ªq´1´vi½wl Óçs¬õÛ\°]?m¿–w:{ÙgLGôU²ëé<Ú3¥¦™˜´cç”ô¹c^ýQUÖ$u>’bÓȼ:¼Z˜n\01Vb»4oó8Zƒ(Ú”çå686ÓÉtÿEZgØ iÒW\0xžVÏ–'c/ÙQ€gĵŠB§Ì ‡¢offÞÁ81åV©"ë¬X”ÂSýhÝa¸ŽIæë×°x=]ÉÀafCžâ1b½¬°T} ¨Q×b-/;ž ÒnqþMáx”ZŠåKùñûèV\rÉÖã¶±ù0ѺFè¶,*üÆÍ=¦Y@³âÅLjÏ'@¼Ó4Àmä§ÃynœÊLOi$PSe…ž¼æoØïÛâå#Þ|¤G±•W}<áÕTèÉ»û;^¡dˆ±¿YeT+Uвr2†X dŠ5¤ÊÓg ¾»1^ïà5é+!Ê€P™ƒe3‹\hUˉèêžûƒq̲çÑ;“*Ú¼¬U=1u@vêFª"„÷Wµþ˜r¨cUr·¥@v²Zc$b!Áì|’Šjlfpe†@ùkPR°U€Y>dm!!é ô€Ž¡`lf/¤”íèeKOõrèw©JÆ€8øÁò‘È oÕ‹ )=ÇZ†"ž]&väg™™$yŒ•T™ÎÐñ#uGÍ«Î.#‹É#¯8„«÷í“ï,³UFÁ+›…$Kä&›Ìàì×ÎÊiz”œç”J£K|ºUšwHí‡ðj†,b½££°ÑW—{M’+=ë¾ÔeþUZ†E>O| ÁRSà'TÑ›«‡Ô°Ë˜%³8¾ó\t­Š~áyMŒ ¶òs&ˆ û8/.zÊŽ¢w¯`j5&ÈÞ$»ª v qÈ+äUaj<ëâE®´•Ú¡=,Á²ZŒ5O"g¹DöÓ!A³hD€¥ë¢±€14¡ôŽæ!’ #Rsíœa/ÕsmÎXØ›uÍþg¾ÿ3žâqã­Wˆõ£YÉeÃÔ-UWîÜb‘•Î&¶å» /'ÄÂ(¸’uNœ—7x .c™Ÿo­JÛì…X'IT¨h: ÞÃ€à’¤Õq™×4-šßú‰}Ç"kïNêÕR ‘À@òò!·Õ“ôÕ`ùv>V%“;VMRS¾32Ö€•žS¯=t·V W°¶$=´öU¡k Ek¤¦N¶1Ø»â²\󟵂 ᡾ Ë”môX¡(07(Hg2}´É¼S:£Y2B…¯ýÍ÷94_ahò7uõ°¦ðÝÅ+m?÷ ÛÒ!¹ à†*ª¼áÓDœUQâŠLeT~m®°‘ëçÀõA-@c9>%ª$bÇŠ-Éä3,&ÒWG7rJ8&ȃon»HL{~/еfOv˜?G B,Ž2¦jô?ñhÜÿgDÌ3oö#!®‡ö*±zêCöÕ*‰¢¸¬Z@ÚE}fIgAÅèˆ,©HWãÌbiECM@áq2¯›Ó(wÙ!újKáüB3žÂla=›nýµg•NÜMQ"«QÁNñùOrnƒš¼ þ~¬DŒ ׋ɖ(ûÍÕ^Pu˜¥#ïx3ª º´Š:º©¢©]%ÉØQ’8?Éÿd<-b•bùœUÎÐÅ^{™Ø –V«Rµ‚NyÔXTv yʼêBƒÕb#–²$ñ.©ðÉZc·f/vHK«â𶇅“ÖÕUíA¼Ku¢Xuò¢pä¸&$Š}?Jîð8ý–Œ+Ø_N}Ÿ4q†.½GźQÕÝ®ù«œ«oï® Í;Ôª*1 To(FÇl4_¤äë« ÜiòùwïXãG%œÑeîŽú]êlÏ>ÎAݪ:|#tL ÓHˆÃ¨¡|ìÄ€m$¢:HoaJTØ6Í?ozkX(Ò‰E>ú¢ö/czëýû"¼–$¯ž¸êé·õZŠj­Jd%ú-O›ç=Ò±<©&ÿ§G–÷û6Dâˆ'FƒŒ)¨GÉÙ#‚ÍÓRÚ*|µnw4Øn/‡ù$ö^×Û‚BÌ®:Ä0}Ì«–4é5„ȺÀ‘Ÿëâþ3¥ü}v}—çÀïr6œ?'è”÷Kס}PÁ=±ò¤yºbK\%…ykóSð¼R2ËP–1m›¼æÞ‰¿Uvˆ–þàh gí~ûǰ•ÿ›õþW)(éÉ ,ÈbïôaÄ=¤ß'ƒzÙ%g\ñõà°— 'IÏh HÍ„y¾M-±û´H—©ím\à[~£Rw™Á}Ÿ/€6쒣ⱨQ´ªÂÝËÖeðêÄ¡÷†‚“w+Ÿmm\FEä7¦Ù ìÅ MÕæ2Gô,Æž‹‚õÐÜ@2‹Îrr†ƒ[ìÖ€QQïàJ[†º5 ”JÑÙgBeĬæÔ`qu #ÖÆ±Rˆ^Ê„Öyi%!M›,^Õ$m†Ê(~ã ô}wjG9UNüÙUå D¶r¤&TÃýŒFrÜEÙ„rö¼žËy ¤t*|™ä0nÝÖQŠ!Á*•ÀZŠ[¬Gæ¼j+ô,]²ø#·æ[hRvv†§Xàn¾Ë†ücyú:Å/-_’øŽ×Çá’c?Äw'kÚÔ2Ný_RykyHø×Ôô‰¤V ÂÃæeȹµðè6 ìéS2¬¢/Î .g–ºy*0 â“ÃÏqã~›„nÚµjp×”+FÙÛ¸5Ý:t¯Å­§ùß:­IþIAqV‰Ëö±æ¢ˆ{ ŠI°uú›Ë ¨ÙjÑ¿pî€KšÔ]F;x‚.Ï’™“ü˜zÿ'|•pîEt[õs~2†‘z}QÓ<³ë„sÁ,f$¾¥b>9R ~zFö¢ÖAË9ìd¤ê\eØ$1¯‰vus½Niðdz–…Jõ3®ÆY0–•Žt'λw‹ö*Zg™U^/¬zÃVÎ{ÆÏ3Q».!92Öõ“YM«ŸG\+ô–ŠOóýßɇåvNîÞæÎŸ c(!kÚ›oÌ$zÇn›F ”6]ó,’õžŽ¡#Xâ“傊ÿ›±Ø~ò¦óÁ™¤æô^w„Šˆ™ÁST['¿oÎ|¼ôîéi£ÜÆz;ø3Ø|w¾hÁR@=a¢$hº>>ólà4L…Ñ ÎáбTÜeî`=ýhq"KõNñ8XL¨j$/… £3 ãhVq²Š'·­;É‘Û&a%ó·-Ñ"±­ŠMo`êbP{ÛÀìåE ´dhÛ¡·ëy]¬ˆ¯3õcB¾Ž8#Q½cÖ9 j`jºÈ½w}uÒ¶µuêˆk-ëÌ2Vq!Áæfº­rO+U+€ ¦Tóš~¢Ö—é(å}dÏj~†;UÓ$#> ñ>R—°òÕ gm¡°í@!qʳ©î‘J棫[ç…ï€ ƒF[âR{O›Ä×$%52¡RîéacÃ(ÁHE»®µnŠAåôQô<í”Wè McÌB‘_"ÚÜJK‹¯ö¹ŒÓŸV¡rªZHë%£”CŸª½äjöÉz ³×ÙÎñ>óÕ"å^¤0[‡â n³ à´•ˆÝ~de²Áš¤3ë€à66°rZAÐ+¾»x-¥µíÖ*€r0‡âë q3úš‚ {ÂRCù©X$Ÿ=ŸI¯KÐÙç$:÷~ñB(¯C¯rà8Y\XzŸ`ëiç£ÿ¹IndkëåqþPM'…æNe[áU 'ÅçZ þD5âÒëŽAÜŒU ÛŠ‹ç¶:¤Þg«`ø Ȏ˦é‹Êí-×[ Ü„dŽ\/ÛDå7G)Û$˜E»©!™ óÖÏçÅ›dIùK‰à ZZßH›».1ªè^Ë3üuöìñÙÔÃÃ¥P›G›õCŸ¢ôÉE¯…T‡SGòPÛS ºÎ+Å€^èrŒž ÊÖ‰Û)LâI­—3VvýÕ3´›€Àá(í5ÇWD>5ßO,¹Oukì¶®=ñ€p|ùÙ×ǦhL—ÔiÙê\Ck¹Í{”UV}MÚ!´¡)³—_I Yû+ò.6FÃ%Ï;d—Å5ù”=Ęf*‹(ÖËÉXÆ'²Ø\N,Æ€ÌRd#»#Á\šfÌ0ò­›$&H”W>¥2FEÅA~&°N¾÷2Œ»å*Ær¥nø©àÆh{>¹o[3§¢Žy±‹ÍÔ´ ÇëóôG·¹K´×Z†v‰szù®k¥?k7úÇSøh´t(~“'˜åqèK`D/k¦ôÎÁ£s¿†\U­›ÙªàΓ®Ë¥î2Œée^m;’/4òpû­Š·Æ»}DÇþêF¤mg¾fd¬âÛt*îЪJÙu©îË@>æZ:…¥ït´ló¶-Nk%ÁˆS:,­nFff‘tZ§‹åf»·µÝ†Š’Sen§çÁa 4n/·ÇÃåqÉÔÑœv§ûÉÚd™‚‡¡!u饪|›®^·mÌ õ´^»çÉ!If¹—íyΕùVÌ­À®üRÔrÄNe^+î+ƒˆæ¨ykj•@³(‘],·T—f X]˜¶òö%HÓÕp"–! ™yïTÌxž<„•ôrû•æK@Š‹ôo*žˆ°qYÍtdH­ª¦ UiÃ-&ùLÏ©­¦\Ÿç®ççaSZÍ$&ƒcÈ;gÛ¹8nê÷3nêÇêI¬¤ÛìþíŒê¾†±ûlެåͽƶÌr1kŠˆm£Ÿ|írä˜þ…sÏ;‹Fmx†î|½º¯)c¨UíA=¯¶íªƒ%lʈ ›žgLäÑ:Ùùv(~½Ü!¤©;wäö¬9A½ßa¨1ŽØÐ<ˆâ€ùÞ@¼]¬µk˜¶L{_~Ÿ]³S_Þ¿+q$€;¨ ö=ºð[q ìçp7Ón,1ã•gH;Ô1Õõ’U8ˆ>.—·Jrx¨<˜ä ÷tÛç07û_ˆP©ÓOúë¸ Ã#¸hF ý”è~Õ Äƒ8(‹Êy&c–„–ÀG£IM ¼3(ƒàFµñPB-P2²”AUdKÜEšñz…êQ²DÍuXêé6K-üÅoaI/ã žFqEu_Ÿ¢“6o’oËoæâ} ‹xá9rX!¶|Ç1¥Ç:D„80tdêȸ˜ž—Ûš‡—r?jv¹,T~ðÀX@V&ì,饊ÞlU?9ñ4¬j@™ÛCož° ¬-íËz“j"ÎØŽ7TV“Hç  å;½Mì`Ò¡)iÁthF©¾-›Q/ð¸-Œˆ8§FûÅ'°I,eqÎpbå_¥ýÆ`w*Ré/»SëÅ/Â:¯–”l± °9>QÉC^…:F–Þú‚A‰Pß,CBs×w/o/@Œ›ö¦•£Ü½„ßÓ40^>1¹9!XVv£1Æ`B‰á+á/J'W‹gÊÌß-²j3Ó>÷iâ•â5A‘·Zˆõïwƒ"Ó J‡­¸Š>×c-­øìY.ÿoL~¯4÷ž–J\‘°=ÒtzÖ­5çÌ“éVÉi®Q7ÞU5$Æ2¦$Òãಚò,Ït4øÖ¬b!6±e‚*ý«„!ÔiÞ…¸ÔÎ â®Oañ®ZçÈlŸÉõèÂŒz_&!P}+Nò«qX¶\ÏÇ%Æ·ßT?•×ú ¶xUˆÒ+yõ•$‡VwF›F|bVð&`³„Á“nõëläüQZülhš$ULžD§¤!»=y‡”gÄæºþ ô‡•/]veGœÔâ9CA‹ÛµÆ+ oá¹°îÐL[G×LîŽåÔA’œ#Ù) ãyM‡‡Wb‚î†uª ôó½_ØÏ«0½?”"1Aox3pÂEyxRik½î û&äÜ×Õˆo! £7µì@¬x lÓxès²pü~œŒu±Úðè¡%ÙòãƇý™=™xòN£h†º›é-·HT/8ô’~}¥Ê´žøŠJiu™Oe¦âª‘ °Õé:¢žN.»1›\b÷†W5KG昬jwžÉÍÊè²à‹¢œ3bèBAɰĩâœt§¦Ÿ£Šá¼ÐÑœUÒjÕCý²-ª–Þ;(_ùñpvOgA¸ ´ô=Œ–\3ÍÁ™ËÁ½Õ·~/å¦Þ„ïJ›Öl hÃX?Ì8ŽÞZ^Sä!Ôò ¶¹˜2ÖÇ"»Ä++$Ò~&a¬‹2Ö}³WcnQh˜Y"=²úA×ZïÀkѲp@óȈ­êÀrcb|«H6ÅÊe™`Ju"šKGMdŸ\“˪è"Ñb^ÓmX7Ãx?{5¬·ð³åü p¹ Ô.ÎfS¸C|ß3ï2±¥ O,–ÇÆabÜüêdqËú¸OÕ™K²e´p.w7ÁÜp¹¼¨¾Ìh®e€í-ѱà2Þ®“Õl ”\ǤzxÙRI‹™ÇkÓrñ,M1°ƒ·êÞyÌj¶5¡Íâår؃%g’\ÍyÇבçÈ‘é4DV´@ÿ¸ÞîP*Ò ¤4)Ò'Ý«©N@•W³h¶×„Ž;œkØÎµ˜öiO ·Q†›ˆÄm;€h:n™:ǽ€Më|ª‡•ሟ&Ÿ§esÍ5~ŸÅÆ*V"èâó¤Ûê×a‰§à+-šØ%䙼w¶cà$9C*iQZc ¸%:Ï÷(‡]dÊÐm\êifrU¼‚ÆZ\Í;`-n‘—h‰­\¯#‰ öª€©'°>Kâ1yšœ#2Aç·ÀF±G^.ÊÁÜ´+|aNB¯ NYܾjÓÍlÍùÀŠ…ß)­rÖoOLXÖŽXlä nŽûAÄÖÛ¸½‰³Áølõ±xN°Ò‰rçð)!p¡5;²–{n¥` iÂEmûˆÝµ­Sž…qáî°ú u®Ý'œà|[ X.ðQ¢y{w÷ë:+ïÑ[X<Ç`˜Í :&yÉâ0êÇfÝgMÄSÁâ(JjS6„Ý0‚9và^3ëul÷L•ËúYL”$S=ÙƒPC‹@c틉ªèò9ƒÜ@K+S®[hÒÞ0rz2ûpåUèÙš¯ÐBMs½ƒ®¦|Ò¥]¡K»4…}®Ä_1³v Ʋ]_Ë÷ @Ä(œGzJ)´Œÿí]¡^ + p:vßAzîjv§f(ÌKRŸz²Q,(<¶q™ªTöQ‹Ä­7=œŽ^æ4žé fɈéט¶OTZû·* ùe!CoM?æ¿ó&(Êì¬/‰’ym¿Î](êú(qŠË@q®ÕÚãâ³ù‡É;ª.Ðq&d¿5ƒHÈ9Ež!­¸¿@-1ƒµˆy”“ƪ.ˆ[_Ö¹obÝ´&s|?ò¯¾>ÿZþ×ÿ¢ƒTCy}líVüYAQþ£GþŠA Xú¬J[ÖÒë©ÎÃåm$ƒY mÝÚÐIÃL !u0£È;µÜªâ—‚p )éh‰ã Ýë˜"qG€e+2³ãìʤÒ~€äküýW÷\©I¼¾yvxè»陿Ï[$Ч~§®Šòs”?®þ¥ÆÖÎúS§ÐðÄE¢Ìc0²í0Z­I¼°KzV?0ù8‰ľ4ðC]#v%(ÃkøxiÁ]k »ÙL‹ nÇSØSc•‹PÚ6ÝáºØÝöšnbvDr½|„,ÕIXdèM}“m<ììŸ~t52wßâý±Œ0\ñЊ}x@¦ŽÃLd5 •PÖy‹Lõéøp\d<±·–¥‘nÅo"9Ó:»ðM'Äg€»¢I’ƒ«Òõë{ëÐI.žÊ`¾Ä!~Ѻ9¬¤“}RïÑL½é±7dEM»°Mõ†bƒèÍ<æ¨føöÄv¼·Údš¸,?Ñ)`ѲKrCw Ã8ç%ºE nS¢ÑÀshRx2²?™ºçðbߥ¡s4.¨–3ÅÚ„'ú͹)Ø\•ù§>´CòmR9>…½ÈƼ›¼7>­I””ă~뻾"w2 o22Þ,*<ÙÑÆaBñ»xë{x;…84}% {œŽdœÈ Û1ëAó*U©ŠÅN [ ž¢Ò\ëІª‘N:Çdø®9h8‡ÿâ¨Û‘„Ýâ@;…>É}•n°ÿLk&}o–¯:‰‰e—À±˜j}ah'v#8‚A¹ÿÙQ³Ñ׿tò ã ÐrÝa€C&Š+ÉQÿ€Êúw|×ÀqÙú€’ ¿úÕ Ûª‹-ê ÿC¢–ˆhL C× B}ÜÂ<K¸ ªŠ§~ø›¡N]ÈwKl³¸Â.lå ÷«—ñ7ãðEÇ[ΙTâk¸>3)Z§q­GgÌØ4v…s¶*Û·qÊ‘suÙ|ꈆ[Öˆá}IKëæËn{Iê<Ùc….C?Ý©†žÏÈW…Xøp¦á_DUIdI~o°‰Å¶Øæ.S8Ìè(ˉê‡näëÙ`b¯•%û³‹5F9¨O&l ä‡y “Èc¥¯`I $´7(Âê ñ“…9Œz#ïbm”Dîð(GÆkÌÖ<Æg¥…6{,KÝU*×Õ žG‰€Q¯ ãˆÄàÙ.‘÷ YÙb~ñ\bŒ8€©z˜gåù”¯•šØÏb<úf  á h\%ëoÈ T$zÉß·/htÅjd!-x¹5à*2ñÉÛ™ù>„\ë>*ß9YͨåñO\¡ušòÝû!áø4†1IVMfñÎá®n;©fcò¬øÑ6>1ˆ©“Ò,Ð[Ž€–RyYL0†Š;Þcº›z%^a›]äÔZ‚´BŠšeƒ€W(ŒÜÝ‘¡g–óÂt;=“ç™0'XóíÌ-s fþ·N‚5Ñ»ô¬ŽÇP¹h#Có^Ÿ-kf×g£ÓÏ×Éʆֽ±zÍl÷æá(‡Íî¬÷C—pª ½“Öz¨t˾¹”ŸY3÷!B%òÈ úldÃæ5 •žÝ©SÂ#á—ö‡Pþ5Ä>&B@‹rÁpú\Èý¯=Â!ê–9ëV-þ5ÛšíÓ²sJÏ4Åv ÑE=—vÜêÃ-¥ ¸)¤£%¬XŒê½jSÿ™kî¸í91HëP¿_®0`QÀjìÝnïxü MK”¢TK*ù $éìÏKåKXü&@¤ÉôVjþÖ7ÌÊ{Ð`m‚\Ñ•fùà°–ÑI`·¯YòÅB§þ‰Cn”]z*‹®ÿRa=â°ÖQ29_Þ‚¢è%!²Ý‹Ò=…Òz§ ÁÊÊZ?¢ì™k’Ë.ï0eCé´jµ9Q»)`5AÕÄk«ÌGÍ=΋í&ý#oP›M…“ØMµÒÞ€ÐqlŽiù¥IjhfËzS ÷Á–õ÷µ¸Lt¤NÿØöG HU¡[4@6¦4²ÞïÇlÂÜÐy-—À:Æ÷ðeq½y(€aî\ Ó˜ÙÎÎj+«ÓÓJoõ)ðfÂÒ{e³')è—ñ³êu>b7ªô†îKÍ"O¶{n$wbô™‚™ê¡P{NU…8EÑôo sÐSÄh2Úæ!bµJçõ°xå*@âgâ—”M¢:†:gÈVºH>%8ªÔ˜F›KA»x7~H8l‡¤pþÔÜÉJÝuK%Io¢hîsK%ÉЗ¦îß”`»ÆË~†èƒ€9¡|}4i뽺$|% ä£vaH‚h¡ÒhØ,oï_ ò$}´¬¿ü0`Mj ‰Q(µêÓ£,й1Ì>KN0'6øë-¥ì€„—à1&Š”Oá ”³ªI'F5ì„l}å´îYÏÕzŸþ:EÍ´æ”ðŸS¯”#Y¿3 n‚’–ä÷aqm²`cÓàDGí÷ã W-¹ù¬߬tïk£6¶AôƔР'Ÿ–~ü=#¤Æ¥7¯ø€©\•¨NÚ8«¥…„¥4躙*¤9Ÿó%gü·1 ŒH“”v8cÁŠUÙ£›¦Àÿ‘= 5Nb쬥T2÷–PŒÏÄgwNæ=+ß¡7 ȉ• šNRÿÇ,ŒiÞb†ê4Jž 9müE(Þ¹ET¡…ÏÜ÷!Ô$ßg‰õL Ê8&òé&,θ<tŹ!Eé"JŽpÿÇ…3ÎÆ‚*t%i¦+k´šmÒ©k8í‰LypAbùÛÂe\6‘ÿ <{•+™Öx—£ 0uP]ðÉ[œ¨éöy_­+…ïQ†ÿò ñ´–€ÚW{ô 3¼p›vÑÜäýç^*^‚™Í_¬ÅòP°•T)w4»8êr8¦4§Çh™yj+ XÇGy¸dè…létÃÄhf‰§íóÃÐàƒßóþ÷ Þ©“Ø–‡°Eâó÷¸¬Ë7uØ<:²øw¥ÁÚºQEÍ&!¬4=)«³“V€Ý8Kôúl<ÎsÒl:’"),]ªo NóS¾QîÅùC6£lôÆH„¶è.yê’H^òáºEO®Ôì+cjŠ5BÐt(&vËt(¦²\Xª€üÏìP;IRWY˜¿ “Ú‰{+ »û°MLÿ}zojü4˜g½0á.4…ãRrhôn¿>fAÇÚ¡64FuÉW‹\jvw–ÞÁL_p”^ÛñÓRvEÏ…J+§%{Æôyö—£@ÅYSfíÙ)C$Ey‘‘VmINRqúöÌA-ÞžgÅP*• (‡Á¥mAÙœ »!^ÏÙ»ûöÜ>ÝS~Jýu4Q>·˜¼"{¥éÊsaÉì&SþÁCá\SýázÊd÷IÝœãÅÓz¿Ø<ŠXyÜÅ>} €Au•˜ø=žò­™7¿ÃÈó ‡ý]ÈòŽJ â(ªöBÍi`²@. Â>ái5Õ`.á‰U9F÷Ÿ_XW¨²øÂ¼˜‘:§ÓþÌpÆ—Ú@ãŽo–AJŒXQ›LÊQ¼R=%ò “‹›rOgýl.&E&J†sÁÀ`·åÕÞŠ‚¬ì­áÌðG¦íD cLÆôÎxq·ËZ R€éè’­~vlDß{1WÉêùZ† pR Ø€lX°ÙuÝÁýûw¨¼¯ÂñØ5RB­…n}ãa˜oO¢µJ¬Môð¦¡`p’¥%1j¬Ë_€áðÞruêÁIø’]g'«”Œ N'„’Ä‚ÇÚÓõÞ¦^å7kË›Z¨,Ï4F0.ÛèLØâ’\¦¡§aµ5sªp§Ñl×HCT(¯ëSƒXµ$O^ãF÷x±Z±Ÿö?¥:0,[ú‚— c'££Ðü=’Œ»+䓯ólý_X¦"6´µqÍ”ZŠkÕôˆY㦘æ]õ׊tŸBš)ØR›æC‡õùjð•ù2—ŇÜââ¶i®üo~ʺ’\Ý×*Ø$©LëYÊùA/)ÄITëD«0übÇ]E;¹Ü¶D5œ%5†$ÂÎs©ƒ³Ž‡ŠÔÆÉšÿr "è”>°‘ ÈMÁ]DQ £ þÂ]Ëý†eÖøƒšp+&ºéåõާU༵¬ÉEýžXv•Ä&0­R£z«òLá`‚²W0V²&uˆiwG6t À׺’"4Üi[cà œˆ(nÀK·¸þ½BLYðî1ˆNª½pÛå*-‹h…¥:ªãæ2&¬;whÿ„*Öh´Ä1Ña‰Ó¢ÄÑÖð„g#  ª– y™»dwêünE~éN™ü޳/ZÅ«šaXè*dZ?É´APBg¦»ÎTñÖ¤›€ˆ¾˜!¼pj¤Íå_T¸xíí!ß iúMWæQµñ$K°jçüwm‹p LZsj¿9´'@è“:¢}~¯Q$fJ«vˆ»«×{ <LJŹ˜çKQ0ÒÒ_ ò†qB†×m/}L{l]Û‡£ÉLYr™ £†ú’íW9=‰í®ê—VáÆË9]>-"¦]Þ4£]q (ì„Æ75• ž*¨W« ` ,ƒ Ž’½€’çvüìÝÅìa†2âÀ)å¤bÌÙõ*iT`™§Ÿ~ýéžåmD`ÍÖ[gØî©;‚_'6¥ 6òìQt[Kk¥ì?.}¸óSþ|’·¿ÔcÆõ¹Ïñm, ¯6!‹ Nˆ‚Ðþ©oÄL'ç1Rñ¡<5•â†QÏÉÑ4±wWÊ”ÄPæ¤ûþ ^½ HÇIˆ‹£1Å/±Òñäp™ß¨m˜Ûo~HI }r7°d_©³DvgM_¥oÓò0$ʪüÛ)Ö²=ŒééwÍë k‰Ûñß6 y†ƒÖîqÈn'oÆžYWkâ>>bjÄMÞ’¨ O®S„ª±Ö³A¸7~H—8•?º"ÎÌ‘*×*Ø$nVÆð™ìi(R?aqeÐ$•QÑ+œtŒªT‰ˆÞÞyùªAÿh üÄU»ÈptYÕiXÒ¢³u7Ñ©Ÿý]7ÿ‡ÞþafAÒ¤{®T-pgŒ³?.‘HkË ŠþlØ}Ü÷ûjAgDÀ] e$wûɱª€q©m8EüSO}2œŒ³BÏ.+lò µsìnçèêDy‰Œñ ï!ü0ðgÖ­ßP®èRFöÑëæ¡k•˜QÀPÂYŽ]XZM»G pš8úUxöŒÐ3ÇVÞ§;*¸Txðµ—§#µ©D§Ö*žòš8Q|ÆhH2Çî{ÏóM#·Ú“Jœ$Ipä¾Ù¬eûAÑ)Ë)Æ¡Ò4¬cÅœ9+Ƹ—ÊÈÙÃG¿ù¤ÑvÂÐ1ÄŽ~!%ÓÃþçÕsì«%¹Bé‡ÏïF­"ˆÄf$¼oMèÏ.þÀƒ½x¬Ö{Þ[H©±)•]Q©4öÕ i™éÁ‡XdSé/£)p{TZ¤ªåËB]Ó¹‡¾Ò§àu&“l w*~]t ɸòð9âÏŒF-šç¨qò>ž/GÆo>_Áš2.mhHöKñBÁÅláD‹@<³;÷–HÔ“®fº°oœ&kêQ¹(ö¡*Hµ'%ÌÐñƒù£Ó±óÓz°~1¸ž×W¢Ú%éñ¨ã8"Ø+6Š’4‹ª(O>[§ÿ ç.!AºEôá‹âÚiÄ~ä?¶ÒÆ õ.™^ÁïsGF¬Ê÷{J2Ûs¥ˆÁÊ÷pFB9ëH˜5 {¥™OiÃ/øµêÅœámQðÌ Ý¸x¸Àöw>Túä“›Îãû} ’´“šdtž$‚ÏÏA“½}t¡’Š{%‡2n}½½²E“«>ºïÐÝÂX¢yêÑào|ÁÕùgL³g—¦÷á ˜O½dWüç_8¤ß•€Uð³DŠã4ò=Þ€IEt÷‹÷_Ïro#¶=Cà`ü÷ &¶®ÿã6L+§­ö‡._PÙ[ðJt²¥]gï"Y–<¢4ÓÌdà¤ø`&ÐzóŽ ¿;ZEbJG²Gp…ãÞú;Ï`íGu š´Ð输Ȅ8íc9ãnÃÁgYO2cÂ&³5+¡ÝœÞ)PoBRm(è÷„1ióÔxmbiÄ&Sãì÷™H2¡G*%Ìþ‰®ûdˆù·¸Ï =ÿÁ¦é‰PÞ°,ÀHÚI$rr S0ÃŽI%Ù‘é 4lVS£ÿ †Íäw± æ‘÷™Þ[ª/eCóZ66²Ø¶nX‹U×å+W²–/ÙðÌ’.V ?1AZÉÝM4ÿü&9Šõø‘2{zÂ+–:K:— s3!ìPÚ¶«ÃB^ZK¡SB®‰>|m/'«ˆƒ›Õùw–ãÿ<N£ú<¾=¶¢ÅÈ5mE]ui=ZpçÅÕÙš-vä&M¼¬|\»ºú€öí&=¬˜=Æm¬|ÄÓË£ÔÈe~ˆÇZÿçíê2þ%Î@Ñ#ÞFìiê'Î#ã’¼Œ$ûìÄF–C,˜JAîò¡XW°žò+»Û2IYfM­¶÷OTäkÉݰrz€ÆðÁI“þ™QTQT4G঑z¦5è2üùHa…ÿÊ·uØAü6Äõ %ÊIßpæ•kUñyºªtí*ŽK¡95Æ0@Ô¦…ß¹4‡ û¹ƒÇqÄ$’ˆR3îÝ!XØsæ£àë…’S;ú_Šís‚öµÑKS™Á¸Ü“µªF Éž«Ý½ã×;,^öogŠ¥%ˆŒ2Be¶žu ò…*Yü¨ãºúŒŒÜîî7ð|€Ãà—ª¶Î-½9p'v¼cû|sfÑøoŒU™©§‘'ÙA[°þŒÍZv¹;ÒEÙŽ¶¶.° …W†ª‹Ð¡’L¡?ÿàùàzƒÒŽQ|5x?¡z7a~çñŸ¯‡V^¼91èJƒȈ½À ¦1H¡¦¹‹äìþœ?_½ŠÙrÏÓî-’~å'ož;'|n^hÇþ²uvuDU…ξ6Æ(.ÏæÊzté ¤®P¥¹Q af»ö¯¥GŽD“U³š`˰˜XóG¨L[y¦Æ7r¸ŽQD´íÿ‰ügáé(žMöuö¾À6.ä ¾, ®8Èýï#ydpÿÕ<¡¿+ü˜ˆÆ5Û_Q‹ˆNÜŽÌìÂQu±2˜«{ÞÉ™ºÞó22––dœl…øòØÏàtA9/?vq§sƒ&É( ø³Q|ì²ôµ>Ó °8#6H€­!!QÞÑ… ;Ù–Ú{?é| 2¾O…à ðX"9°„aµ׈³—˜ÂÉÍ ̘™°Œ«–öUW9w¥ŠáHâ½küBåc‘¶2øFîÛ¿Q† Ìž¥~@[Ÿ®‰îhC5Ð&a3AÐSĽð2Ö®+(pZ®Èò¨=Å–"ˆ÷â.Ñì8ÕˆÍdªÅH®»*G…ÒÚjð~+?aè7_è+,§tJñ¥Ì\I|ˆ'ûJm0NaîE)Y#AЭø>9µàt˜K!AЦ—=ÕüZ¡LH˲‹å¯cšÅ£ØâóŒ,8ÐÉ n×¹=T³|QîßÖàp¿ãÅ3\ #dHLÝX‡VŒoŸü1&Ýû+? MŠéŽÙ|•Y#3%v9Sä•gµ; èÏSd¿Z¬™b?ÊÚ}ÓIJ¯@•o6K‰Wµ4EEà¯×¤›Š?Põ¤­éYå)›î"Óã×…_ªz²£ò‹3yEÊäÐùV;xÓîÕ±‰òñ¸„Ò–ŸŽ=LáÙô„.àEþºŸ*†HÌ**ÐÄâ9%^¯Ã𥼦³’/‰vJ¬ »ÇÛ×Ïã'©=¡‡@õÞȼ*’A%PuEÀù2©´þ±E)®¾U‘Î?Þéx¥&ɹ‘’s¹½¡ó–‘ä¿CE>b‘ ^³&Î [%ø ×a@× 2Nõ½)À\´4XñX‘©”F”˜<7ê á7æ•kô­ÜøŒ¦ô›b¡¶‚œÀ„¶hküÞú@~j x£¤™:vò…Ú×0žŠ>ÐGìó€OJQZò`yîhƒ{sB:Oî7㊌Sõ̺TãäãµÜµ!Û÷²]übФ]É·$ÒJ#̇ñ€ç¼„^«¸¡QP3Ô! Š¿‘…Èå±$~ƒÅ7Ã0:k1ÈÅâ:‰n‘nc‚1Üô¦$i®à ÷ô̬7ÃÜ4ÑHÙUÉÐIœ¨HSت¸ì¬aRfþþTõ²`+àà"á<¶ô4NÀ#”A8fãéëqŽy©¶Ô•n;6ê…2ÃM ;a>êÄ :ÊÀ$³Ö^ó’⣠µn¢. ˜ nÉìò|˜vg1œZ:ª 6?kȦçs #/;Þê^ÚmVo?ʼ–Ñû.ø1¯o„—.ªbºhD<Ö“?Sã¦A:¥ÚôLaëœfçQêOZ½ó{?þåQÏ ˆýEÃb˜„Gô±²¹cp±!EüŒNÑúš~ó ]wÐÆ=E¼úºâ6µç¡2Öš_ˆGn\wðiVüþ÷~©Ù@=Êã…Ä5ê096öÊSg’+å›!¤‹T¶«f…œÞ À)nºœ)ÉÚ ;„?j_Zæ˜ošó®:ÎC+Á6R(rTq^ÔŠ©{CÉõ¾€†"3,ë’fÊ· ›iù»ö"t§/)fgd\ †Tá]‹@=B.«ÁÌX]làu:>ƒ³r£ä(R¡½ öIÇó›mXRžuù’o–Ó Ïö¯òŽƒÕÝÐÿ¼h‘``εýSÍêÑnF¼9yÛ"Й—ö(€¿a½,qíæI’KŽvzyu;zzj´ÕO¬âžMQ[Ù§8Y©P„t%ÜíÉÔÚ!Ž4§zKªßrOÐ5@Za7kAýÿm «Ú߈^%\€ÍDX¹ñ®ºÞŸ±¦”…guI®tž±²Å0y}17£¤î£ ÅšfùXZÒêuÓÚñƒ;ȈX Ñ:Gÿ:A#å—ÀTº-ýå~áô[]Ž@.s«L‚r™:+Ày~œ¦~ùZS‰Å9:8ΉÍš¯Ð1ðƒÕ¯ë{©lAjC.™û¾¥R(_¯¬tP¼,‘M(A °×Þl CÍ‹Müç™{W2EœaXãÅëÔÁ¼«³«–×ê+ÚÄ\#ŸÇœªÁgûvS‡?ù±“gˆ—=Ý;¡áLÆ ÈOçª #¥†µ’¶¼süFmu­k °Ê¸(lDuÃd7™Øíq‡ÂŒÊÿYcîôþQüÁö¶Ø×Jøñ\ë½>×L[†u±Ë2ùÊ0ç[×CmÉf+Ž·^L_êÉðŸ|²R»¢ßéOŸ…Û²ªÕJ7z¤qf.­±6x}¸´ÏèŸÓ£àmglØ&Tà‚ò5F5Á¼l¾EߺXn 5¸VÙ°Š¿UnøK/™ö È[jñÝÏкÛÜ6´7¸«¥+ì4ƾ†î>ò®âòâ/Wm$GÓ¯'S²ÈÓB~ÏÙ±²âðëàíF½ž ðÆÜåò,q\á«Çoî Þy¤ØN·s0R3ª÷ù–ñ”èfÁÐ roݱˆí­XgQ›t5›f…&ŸÒ}çû¯¹Ï›7Î9pÿ1)0ÿï–ÊF#š3NØb(~LMº%)/R¿‰7×  kì¤Ä(56!ê¬D#xX\Cîe-˜®¦ÕBÛ†>›‹=Ê~+ÏÀ5Ašeév"Ü,oÆeT ¯r¯(ñŠMeöPsÄÔc>‚ €‘t°ñöTwh¼mJxE¸§ñ¼§g-˜DE8”Dq¡¶OU§w±<7²¨”£ª,Œ @6›±Ð x¯·ñH·k÷2—™I#䥰)šÆôÇu "¯Š¯[ˆô&)b0„¨Ã ±.3”ˆ2Ígb±VB¬eÿ÷sØ.„Í'g…œn·$º3þžäñ\{'@Mß9^jó`«¾Ã˜ˆ KØÊ=³±Ææð¸­n5¢…c=̉×]Êû¤Ý†ñÕŽ³f"™3‘›ãmp›ÞWÚ¡Æ, êÀ¥f„ì‘=]“ ì=(ûŒÔñ/—,Pî™w™A¶FÇ¡ó[Ã!’Ó£˜zðÑXBãpÀ¼|kh'ž4êòº$4\ñ›è+‹¨½â\y´}6™°2cf6LÜôkÛ¦åľÁ©„W„Z[vSðv Ï]Y¶l·Gþ=‡hVAÊç‹ä« a¯˜e2C09º‘!†*ô”Ïgqüׯy¡&Ëÿ5^TÚÞU;õ êT¬Ÿw6ïÒì´í£ãx‹›ÄÏr*ßïÌJ3þ;’vEèRÑñ>2ÓY¢šX‰Q+ ‘”Á•ßÒS\;W¾Óç  çY©æør…-ú õºòj¸è³²¼®‹9­0I ,2sò*1uŒKŽü-^RsPlo„¡aC’¶·n¥‚¢±³Púd·õ,Ù…Â;ËßJ~ƒœŠ•Â0¿Ôç' ¤fnוœ0웼h~F(fGyîq0>Æ ÂäСf®ë 1[H)>fØS•€ùl-€£‘²¦!Ï€Ó"ô‰Ýwr2ø*<êÆ%Ž­¸ÅÄ™'ÛÌ¿¡åX~;˜³ Ú%ü4W)?öXc@Ê]Èx'z7h±•dâ­ÑÝòöÍãß iš/`t¢ýÏ'¦}å .ªF6@´Ó! ú¬ ½Ø¹{ #XÒõNïq~à½5Ú&9‘F-ÄÇÇ`7V­ËÏäÖóõ~땬În&žI?¡Ä!ð^žVH9âÑA—p»äÔdü›ƒ@â¾ÊvU$ª®Ë#ØáÁoƒŠJxsØ)$w /KØÖ°‚èºÐB#éшªÇ›…lc3QÈ¡Ú ¿mÑ·9.Ž{ «¬ gWߢ®˜m6d0j³fªÍñÜ‹_ÄëýˆÇç8öVƽµH(bÞ…¡K5¶vÊ1Á•JN5WîÕÌìvKÆâè¶r“Ã’[©Clph㙾ýìS`ð ´,èûW”³{Œ›‰žˆkå_º Ziä¢xYxÞ…uèõõ™'—WÑ¢¨žõÃSÂàëõdQ~ Џ\|Un³ZÏr/™°qÛ|b¤±Ð{ŠwHÈ~”ŸV3/VO4Ó)Ì€'™´pˆoW@_\;Ùv5¿[¾˜}¥£¾#…ø0ð3Dü5gæøÀï¬S®˜‹¹f…1šP΀íxšãxÞ]È·Á<ê N‘s‹GŽcü~£pFî~£=‡Aähê(É¥8ê]XáÁ žkjá{ßec‰Ê‚Ô‹œq C:…x—8Öêõ·C-ñNÚÓ¿5ÇÞ@Ñàvd6“§H¹Y¡?2—ûnXÒÿƒ²wŒ† ÚÅnÛ¶mÛ¶mÛ¶ñܶmÛ¶mÛ¶Õ÷;MÚ´ÉIÚI&Ùý³Hvp%;suL#• YËŒÅ(ðB 4°I…$§èWŽ´j*X"?¸'à&äI±[8r,À¡±S ‹€ ƒ ÓÊ-Ën½tJ(£ƒáÔæ˜šÎ²—âI=¦Ï!„?¹ž?º=j;xB’.ãÌð‚µ'¿EÀïÿ⬽Ffÿì•sµíP¯ñ¹à°Õ°+CkY‡Cn"(ˆÉþÙ k7ãƒIÿ jü¨´Äý¥=8›ª‡Ëp× '* ”âr¤9S@—~aр˚ýsNÒ Õ«r` ÂvóÝmÈ:LÕ­x­ÞÙÅ |å¼ùļ>½•‘½7ÊQw¼©6Û,+(å[åK³¸¢ë1+úèÅ}!Qï/õj­e ªp SaƒaüþeZŽsð‚®KÕ6vÀ•¦F§L½vcw¢”jE ¥¹/ªuôW¡`u £ #˜#E]®\xDr ³À‰ä&Éyq‘GýÅ|a^(Gizz ú—ùC•¯¯qQ‘eÐ/Ö»)7¬I—ï@„°&i7È/lŠ ¦úqíu/H ~ƒz§‡´L-ÌKO¾d•„9»‡ž£èMj'ÞNV“Ži»6ÕKO¶½{¹\VØå3R1;û¤8Í‘ï£5zÊÀvq*—æªcûÔ@¬4¶˜#Ô6r’;Ä—h²m™åHÂQ9©ÄÀE¨Øåq†ÏÍFWé Ñã©â›ÝUÑbh4&£JD"0²¼Ò4åvB‰¦MEÕCC¸Ñº> «l ÷fÌÎêÅp%®+÷È2|¸dVŽq¦¡NÞÎ^|™¶zþv¢˜Ø\Ä/¨ÇG§H;Ksá¥#ÿÉÔsá ŸÈD“: "€ªñEc†õÍ:VÛÐtîD«Þ°nÛt­S•„LHÏ-D~¢‰ÙiíROlå&u­ÿS.aS¾NOMS¤ðQË£j™®)[ü>`&;îɺ¼€6íœÄá‚Þ)]ýs€ZóÁ>®<|.]Š2vþâø‚ËÃcW¬|²ÎLn¯Ô-…e]­ÓB¾£€»ª êÔé=Ì]Y ¥#6c]ÇbùoI1à‰„Ayj#R÷IÌ£aïÓÛÛ܆MÓ¨(OwhfèT0“Ëߦ•AlÂ8ºÄýÞáÉÌ Ê)[º8¬ADX®“o0|>.ªsQÚt7«ÒZ¹§çfõgïîÁoét–´3XÒŸ­±ã<“›€û_»J¨T"‡Uâ˜CcÞ.ôQû œW1CóÒKþˆó‡$ÅÔ5DSH ¼×…¿6¿6*m O•¢žUqTá«ðŽ>¢ÃúhÒj0k­¨Rö!‚Æ5žÖϸ:i|yCà_Ü:í£*"ÓVW\…'m›yù ze¶¼0¡–w¢à¹˜‘²[¬RÃäŒWïê~7lg´0ò3; |Ù¬¨vœ_?–8Þß]³X(ë,Õ<¥Î‘¹Jëä(¡1ký4>4 K2Æ.sYýÎEõ¬@ºZj4¾”‹JªŸÖBEfâÏTUöuFûs„¡Ê¶MkQþl±3B š¹ÂÄ~8 ±ÂQ¡2õÌNw⺸ÖèuE†_V-÷ûyŸ€·³ÉQyœÛƒFÊ·mW­³0pZä`¤b_Ì•£ú–&r_a"ˈC_ +„ÃH`¡&‹8t o RdŸF£yû­ ’T³üÝk¶Š¢Ûûð j"Z%§ÿKG~­*›çóÅÓÊD.•¯±&”×ÊÇëó¹ã€êüôY·[ ­cv‹Z®Çg³€¿ ¥+Ôœ¦âŠÓÆë¸²|^)jï`SÔ\†lÒ¢”Ú¾º9ñX.7µµU¨Ïíãñq˜ú†¥fò–‚“5h”:½žs¥s“´*ÝÐÜ̬nEçšyZZÕì!’ödUSZ­ŸÇʉöÓmI(|¥´œ'ú—·¶†©xøW¤)Üþ®êž@‡Ä–Ô…«i°ÿ ¤ñ`±€ÍßHohѨ`•E ”rI…z™XiìÁßÃÐÿ ÃÿdšÿÃgØRHÂZÇÁÌOßKÂJÎÊÞR\®ôj&ÿ_„šƒFñ ÿ/jC®„ 1€@Âÿí , íLlLþ³¡¯µûÑhþûTH1T*YƒsQú[-¢±î£x&w#8‘ $&’€ÖÈÕ϶ê_`²YvVÏ”qœD~WgWçÿ^]a’‘Žº,«mQ?ÍÝäɘÅýVŒ:v]áÕƒ ºû[šS_%ºjN‹¨ªRÆ‘Qvš,_Dê I]Ñ+_Ü+ÚÆ;º=ØBW·]¾Ç3×F¬ÍüQ×O@H™æLSYyõ¦ÅÆ1@¡E¡%ãTÑ÷‡~³¢¢Ø‘Uo:ª&çlÑXaO™G<ˆÀ÷° J›-X 3E_€¾rNrƒàÁY¯-z—¾ïËû{ºÉòFqå]ofCÐŒú˜øWÑRÔšKz¥NJS·e!×ßWc¹ay›¼bkÀot“öù wt%×äí%În êë[Ýfa7¶È¿¦±êYÀÉb‹ÍÎvÝV°F‘;ƒi/@u5±žž¨óóBtß6wGFŒ»8º>þ®ÎèÝ»œ¼»=¼ºÂ¦'‹¿ëçýóÇók?\1ï2~Q~œ<˜BЙc|tÖW|.M¬'ÈÃk¢¹8êÕ³„¸(KÆ0Ç÷©kº¸ÛÊðn:CŽ¢º3Ñठ72Sµé¨ÔòGð„×YÌÍÌóeoy“û™4 n.m3´’-}¡ ØsÕ¶ë§Öô³Ã·êNÔ½¿U€‡;Ôg2ë*­_»á6¬|uçi!«Û4[Ƥ¾ü½Ç¯¤êíÓ'P¥¼ï×’š&G¿¬þàšÜC®,C¦ÉTRvgªþ“–ɬˆ¿ºŠ.j;;€h _­…på’õðï°UwÔT|÷¢¬ïk1–º3Â#â›4èÌyÅK<Ö ¦ÿ8'·XŠ%ÍÖ(ãÕ3ým GÔÀæ°ÛèE.„ÕÞY²Îî‘8·ÊšCÞr~pöVVœ=ènSEð £¾ìïgz_K‘ Á Fµ}“£°.jië³bºuôÖTcPÈÙÉÑß‹»¿\'HJÀë[Í7€Öí¸èÍöp:¹ñV8S‘Mwïpïbî¯0iQºû¹‚ÌEY)Gƒ-ží\¼<ðprâ^éçfý/oMIp«ÀWŒ9«!clòs½ýïÔ“t}H¹­Ö:´´÷å÷VUµcª2^žš­à© „ÜtÙΞ›M–Jä¥ F ¦(éÊQØÈ rO©h<ü‘~Ù´Qà VÓbV‡¢Nj^¯½ ô›ºWû¢Ú`tïäÖïfõf¦øÑê¥KŒzžzqÏîV5¾ÔVzØh/.v',nôsòŽêÜ‹ž ×ýÁFN>0ý§üÇÓªßìg_c—¢¦z¦'&Þìý‘ö–’zþx¸Ùópnòò4ååæfÙÙïÞäàcÅÏ–->Ýî\ü°`‹¼žüGÖþ¬¯N§~K©ˆ//GóÈõGÿ¾œü¿úÖ¤«”~Åz£/wÕàCö­óý<ŠØ3‰Ð4–0v¾9sNümƒäbwûÄ¿¼Ñ‹~ż¸¶»¸zeÄùõJÓÄɉÀàÕÓ aŠøí¹vopñq%ÊÿSû¸÷xæš'‘t@B‹úÜá¦õ Mvá}wRš¡÷çG…w»75 Õ”}y!–íÎÿnLl!Nc¸úx(£Õ xýkÞ+¢b»¾?÷¹Ãcîå_˜”Tˆ?·ÍÖËñ'˜…[ nz+3÷¿ø8üþÿñAŸF=èO±"óhٚݲ ̦MåII¿/©›OAýüÀGø³º`ŒØ@={Fqdz‹~ä –H'á.}h©܈´\—OQ_Ááà™D ›«[z¦^öŽJ²@:>´ñÆÖ\è«Ä‹dCªEcoè €Y¿€ ˆúm ?æžèyˆÏ 5%ÊI‰9Á”% #M"mÓ¯õ­8fæåø¼¦èå’é÷†\Ô™³ö‡ö÷¹á÷3FáÚçG0½2å”]€‚3óøâfa¤uÜ$™„Á /†ŽÎ`zñpþôëpAŸöîÖpÖ>ÍÝý½—°Ïy3É≡žç`“‡‡çqSãµD²Ðúã2LÁ§À¡"äŠ \‘Ÿœk3ÝTþ x±(î'½›C÷r·OÌСRÿèè¨Gj`÷]*²1pDÕVßÉQkŸt4† 62B,<ÄÇ|“ƒ¢ý‡úk¶› ”þz“'Þ'pîcÝ5§ x™=¦ß àžÊ\לÚY¹Ç˜zVDI›+-ßaþ믗+ŸÔžN ÝÆìH˜»‹ÆøEHØÔoãõ¸2º÷Þ€ÊMN?E,]àn´$qQúÀ ò?jÀCôñ•‹Êè·wX¥\¬§#”߀¿=]ùìåœJí«Zß„¿ξ™ºôyиTÎìÍ'<¥ =lÚÆLÜ£=à© ¼¬ó$ÒZsJŠáË¥•­ài‚²çûïëâ&ý»ƒyJ‘¡ž‰âÓâÝàâ¬^^þ.ž-~n¹78¸Ù@ðŒlW%K]_o·Xš(ÜxlìúˆyN!<Ò£î¾(Mž²íÛB4€ KÞ§²ÛKaÆ‘§ „!%‘µ-öŒBç/ëʈ¼L ªÉ$•–ŒRĹÁ|ý;)ˆ±f_­c‡ÓwŽ›Ú«Î)q_½|Ý{¿fg-;ᯩÄû€id×¾¾«Åøä:ŽÁ½0…NlCâ@éô‚ç¤<¶Èé|ɪ¾UÌß뉬ŸÝØA¶°1¥À¨NIÛ1r %€Õ @c°ñbÏS³uŠ£$ñHóôuþ).WmC0 ‘dòšWåû‡×<ŠåŽzý®Z)\n¬‡Â^§ EÀÀ¯$Ëa»${9õêú Q¿Ÿïìô4±G?.$ýü‡²ªjc¨ñ®›4ö˜cØcsU|øþó$"†µ°$Û9Ð[eKuïÌа‰¹¦ÖŠ_£]üÎ’„0™î®çœ\c^ Ș)Caò° )Ï͙ѭ6)ÃeѶjˤüeA¼WfÄ•›ŸÃuÊÐÀG‹ C Ÿ\¼Ö¥«°¿P´¨ŠarÂ…P׌j˜"å;WUXï ¢¼l1å"N?W;üvYY-šË%ŽñK,àCÊjkKtt¹Í¥_ÁgL8¶ÿ Æ5ø§:Á(MÁ¸UUgsÿÉËEuZŠ¢ãCs< ¢gÑåÙ@°ÝÃÛÔ”´{a2E_QÏ“.©ØËеõ7ª±ò¬,2¯åªMYŒ@ªEÞɰÖ"Є‰x®òì´,ñí^‡h„ 5Ao‡ÒCw좞$¨ÚתAî×¹~,ÚÎfmeIðPDê’ök6êMë’ÀÛöÖ9jqùú.‰ìª8Ûâ² A©Íc¨!èµ§?}üç`më‡ÑR1E•Ò6`Ãõ9wº{Ï—úÑKTç·*±š ½ÝD/Œ’~ŽOHnw£Fbyéo­Z±"Ž!ðœõ6¯–N ©Q„»„ZïLtæYÉ¥šÇ#ˆK'@Á"Ê*j.¦É Rˆ'«>!ÃvÕ4² œÓ»´Ñ*i´ÝÊT³Ø}žµNÂ×&Ü;Ck|}ðy!£ž™‘´r昺Ûp»í**ý%Ø'>/ «ú2QÛô[vÔ¸ ª.E˜^É ´¯zÌb×Z&fÕaœø:»‹:uVFtƒ xëðÊ\òZ¼Ìi·ü¢jƒª¸md*w± ¨lر„¹`è»=ïÊñ(óO§4µiÌ8´áWÀûkχÉé(¡B”#b @g°Ê“}DZ¦hæ&kN€2+Ú–,ÅVHmvÝn¤k`[f+„]>¦Ê¡Õ øŠ¶DlO—󑃅b5!WuWHÞi?´U“å ™ 5¬Û(" ½o1W¼FÁh¨ÐaÑÜÞ~º˜éœÓ;¯pèã’€þbbè: ¤ûî^XçÖ­Ú4“Š 0¯jžo (‹ú[ék½½ªê꜉ùéšÉ>ɬ…Hâ+ÁðßÎýQ¿løo>Ta]á®ËŒö:uѼJœó…KqãáãœÖã@PÛf{›j¸­„C~î´*¡gPÑ"Ló¾†éd ‘ºÁÑÀLÿ.Éÿ ¿”ƒ@Ðc­,FÝàù·³MIè }OÈÖüƒ ÛŸBv”ª0@—!÷´CÔ€á_8u¹fÝóÖF®]Ñ„ vHîç¾t\X&Õ綸æ|«² isÆްùXø¾uqüí÷ U€7dC(="a­ËKPÃlö²=Ê Ìl…57êd¯¶é˜E ]Н\A©âöbáùk³rZÙͼÕ¥³Tìë(¤CÜ3ù7³ZÕƒí Ó: ‰|$£*‹•TQ¶¡GdœÏA}zŽÀŽ˜ÓŠå/0³ÎÏ|bËQãxºD\¦”xÛoüC‘ž/W‰­[6€üM³ˆ{ãäÊŒïÿBgYAÇ5‡×bG_à»ó ™‚Äû;XÃæÌÀcdõÀ3â1çB)’M’Ñ]T É·­³’gŒ„hR‡-l8ï*Q^Œ¥A·l±!næ©ñþíŸq[ëÊWá´¥Víô]ð4l#YZòŸì·dÞ.-‡ˆŸÄÀýʤ,»±µÉ@cXße•'7‚>Ò`~‚AM»kE´È÷hÁè›JxS;¤ ‘˜å#! tyQ+ä”Òµ~æOPˆd¤Yþ²2úGÅYr¸Hþ£Q"ÕŽÃ ¨)ó†à~Ñ[ ëö¢­¤TûBæîƒÿ%K%IᮺkÅÓzß1žø_IöÒLAþpºZZÕpØVò~æå­¾Ú½Àm#£ƒÅJPà§Kßc}©câ>¹Pa! ,·hS®Uùv-¤‘ R/Ü#¼YÙ+Ÿ<ÐËbƒ—ˆÕÏAư¥/ÆÝÅþÓK,P7 êù¹`lM½Iƒ%¡-¡æ ’^Xjm–Ê#˜XòˆØá¢Ù†öN(Y:sfs˜¯ïqކ_'4o†F9TA›º>ÿ¹U¡þU ÏÆBû‚Ý:éoQÚgL6暢®~-£}_+ï³_ÓØ¬†ê z3,܃†A jAÔ7uí¿R”œþg¿¢|ÊŒ}k¡M¸f³,å¶ÓkQ}A4*-–aÉ,5£_)wÑ)‘•–èáNNælZN¶¬Îuu0@ó‹mV !fËJíêúv¯>¯¨9ÏÜêjø,©Åí°©öUÎ' Þ5‡:òP° 8g³§ÄþŠçk1µ—Áx”uIJ×@œ-IŸ#£qF¯‘†gùQ>®ÒôˇJ‹Sh›¼Klèm*ÅxñÎ0££“H“.©}&ÅKì(xX‚´¬<‚!J †DÙIüH'¿HÐ¥<òÌêkÌ“i½zà‰Œã~B&úv Ÿ=2 ¨S˜?¡ôç®*…}›j»¥ð§±Ðù7 E˜¸Ì¥™ëÛ¨‰Gõ6†•3ä¨uç˜6su‚”ày£=ø3êuéTÓ#x|J~I×¹…}kÅGÏyè•js!UWçè0óL»PÛ7zé¸\ò!8øÔÀþ"£Ÿl¶›¿~Ϧ!ës´¸b~£û Þb ¥œ¢7D<ý¿Ôu:°>I+˜Ãÿî!z‘3ا`=#O8œ°LžÛiÚà ¥$Áéc=ÍfsB£å¶),pó°ŽW}ìo ±œ@ÜÒoòû1¢ i¥_Hh˜ÈÊ]hD\Æaå˜"Ótü@¶Î˜q Ðþ¶êÄ(»¶œápƒ?dY®mŒìÎÊâ‹u~Ôˆ(ÄümO]Ž}w™¥„ݰ¤¯KarZÇ$X¶^Ž|Á±ôÃ}à·Ä𰓤#DÑ© Þ^’ÒÚVÆ%æB˜ðnó®›Nåê_‡ŸÌÃJú?´×yÿ+D¾+(=e ç ý)©Ç2šèÉÆo#D 4rE{'r˜R1Éo|ù.·´:ý™¦Ã~ÉÑVÈ漢ÆR­aòÍWºNöÏô¸4ÛÙ9C£5ˆ]ß:[×nüc&Q2ÛÒ {¯´o‡e¾è¤xkÚH·æ-D@ÎnêW¡!–ˆ»ÞZ?õÇ…„ç™E_ø ˆ8`açÂÑöï^Â~¢É* 6 §ébîÛ¡;$q²_5ö# zNŒi>½?]Ð;N-+ƒ#`Èp «*%#Ž!;–8ùk­ß!Ìu¢ïZæ2Þ9;”QU¢µøoÔ£ù{9*Ddp¦ÓLÉt—ßµVµ»ƒ,IJÚ3ŠoéŽuÔ.Œ‚°Õ¥Ëµà4 o«]RPáí> =ÆÞB7™ 4 ßF‘ç{x9G®[È^¶aé+îϪÖí—DÇrVÅ$¬‹H¹Ð@›«+)§Ip@À‚‰qQ0èMr Äm„OvÈb”‹´ŒcD€ä¡!Ygáu‹)ša 3÷)lvËHa‚$7Zýá+ªw‘\‘Å©ÌHC<:aÙ¬ÊÄ%öH«!º=KÅ Â0T”žª×œYÿ™ìÀ¢®^ª,ͬQÑÅ-U_KT>žÈ$lmÕ¥»6óó}ZôãÈ­0Ëh²ÓZXÃål{O ‚¿n–uO4¬*jd žvZc“+2åNÍCV±¼½œW‡K9§¡{TÌ™™t ›b­2¶2T˜=ëP9¶ïC[Ô¥ïÑÈ£,5É–dVKqhlüe.Ý(gú›Æ¢žÙ+Ì3Î7´¾ê•ÛfÛ¤>aþüŸ `š]0ÐÛ§M>± îÖhá­á6ÃOºo÷¬)Ë?™lsy2;!Ä`OÅ‹’dºíŸÍš>­–ŸOYÎsé›t7mI~§íňÅS—‰ÚkÔ‰ø"¸L)ef˜H®SþäÛ øÔ"VƒŠÁÜ\×p4~xø"·%#ŸH€“ê|Ì£pQ2Öî+ûà +2{ª: “ š놜6™pIÍWQoJ€×¨åJ$ôVÂ5°4«Íî±Ó¬±ø»4]ôbc…Þ“íM÷Ùc„“¬ÅÛÖdÞ»ëo¯Ãc\4Ó;1÷T^yWàêhí´VžÖ¢êjÙæLWh<Šh¶€[wü:º˜;s¶lD'"`ÞZ»ÛÖ7–áïZ©³í<À¶¦ó¿ *Sf¯$v’iÓw¼ kÕ¡ï7¥y¹Z"ñ Cc½Ö[Uäq]›­¶,1Ĭâ6ÛªF[lLQŒšò¶6áä,ø.»W‡WÜ rXf‚ˆ|ÍÓÆeŒs°þ)±:UÉq²‹± U–Q–“ã#u‘î\&Îè- 9À3T"à«,榹†p¼sìag†'î3_µ² ¹fd™U€ŽÌèEä­¬e•(ü_Ó¨éâ|cÌ’1nëa³êІËõØOLi=ž„Ò ¯º‘WnuT9DîSfª¥PŽà[^~h3Ûƒ¬3,¥eh›UÜ:?—ÑEnh÷(èÙï—9@dõÕ .âØ2r¨+cn:€b‰V–¶ØöÝ©ƒ9ó7´ÔYÅB_¨oŠ ¸µ¸]äÞ"MC BJÍ[Ê2&8 ‚‘,žKdá9ÇMAšæ‰M oá¢ó¾òÆŸhÝ6 Õñ» ø½óÀi'Ñmmؾ Ï…ôD~PD+ÚÎt‡Ù+?0ŽËW C3¹À¸÷{Z^˜°q_’9ÎH”¾º ƒ‘§ZF(Ìø2ý›“6‚?"X7×6w.ÞFoÃÅ:M¢F”2昘LÀÖÒ$t²‡m¯©9‹ïôW‰”Ó0¥ébi$Ò‚cÖjS„®–ø%têï7ýú™¬2úÃ!„P˜àÁ*º›&í!§ˆ¾…Éêî—ØçÐlxôø˜³7$[/.š²_ïÇÜ[h›ýŒÞÙmAnœ1âÕ 'ia·›)‹ñòýÿp}`sÈ­³RyBøžPÄ é2;éU;‹‰í¡óœ,ÜbEžw·3¡r\ë±2ß*£õ³T ôw¤D÷D÷ §Ý¼X=OôŽuº{m¨œøÎ),€†XTûíÎÞXÿB?¹Äué»`Ág"ƒ~Qií+©~ºìj‚"îÐå—''ð§Kž‹ScÇ ëvð'lb‚&‚á1Ó¡œé†Y¾Ù f3Êï>Æ}õhÆê|Û®{TáÒj$'ï —®c”’2e =4@~Vë{m5àŸT23#iüx ¾êÅ97¼¤Ô7—bÌš«ÒHI ˜(Ö`ÑŸMþXö* ,÷ÉBÅ Ôá¶ ˜ÏZÎÁù'éøÆAWã=[­§Í"5÷pGµ`±nºüºDüáñv:ýMAý5iõIeó³iYík1C1 ©ÏᨾZég¼knT—ž¶ÁÀ€ñÌfº]‹X=™½ÎÍAŸ q¶äv'kîU_¶ºïšåvÝúYxG{Í þ¼€ßïŽ#3“ &ol“Z]“Î?Cùdž£)9¸7" yÙŠcdÜóNA¡ÛÅèSïïåóèGCÖWÅœÙ'‚Ónóø ß5“ÔÓ‘hÍ,æ| 6Õ’×¢å~¹À¯–ø±Ós~Ln äì”muh£¼Â€FéKù2zćwí´¯-µqœ (ì¸ÑD5(û¼´Ö…›ô(Öê`HöZkEö ˜JW·Vl)¹‘ ·ú) ò™îÑ”tâO±Ð¡X Âpo|–VÑŸjèÉÒô}ú¾F&nÑÑýA£'·g*DöìL7®[^5ü¼Í@u埗-©•°ó>q©G¿ªðlLû¨ Ô&ã¡#`ªOó1¬wãxn«2š· -Àéyµ¥¤û¨³£ám¢†åOÎHC„kœd|`&|Üw’×'ºƒ¼ %ÑK·%$Ñ43 wãf²®d˨³¨6—;ïMéÜØ–ÍC^ŒTE[â1=)‡µPU*Û^®ÆXw­tøÞ ý±:Ðøß5ûŸA5‚¸òØìYòØñüoP”É„-¿“dRŸD‘±ïX½„ÌÄ1õIvË:ÁÎ(eù‹a û ‰ ž!wóÐk”l1Ähk¤ÉP ¶ß9§¥ød#tQ®SZYÝï7I8áãÛˆø^]‘¬a}èx¥ŠÜ osÉSy5`ß<ÕÚÚ-^k.;ç5¥˜á7KfL7yv0ÐcÌÂP·¬Í?ý47iÐØlØrNGãÈè/çõŠ8+ÑÈ ‚j>OŸ'׺æ’Ðó°…·táJ×:¨±>.çt¹AÝÂâþ¶v…HýžÍÒ°ž%¦Õ¤ßìž_š–Š “wÉ!•s7€;ß$©\iEFÀ¼ž(ĈH¯—¸ð“6Dõà:+Yò|eO9°Ëì‚Å” À@ z¬OI¦ j·Œ¬hð¦ù0oñ®_Üè¾Ã˾€î ç˜ÍEAÀDsž®+%ê…ŒH(ý§Ú€ü\-é.C ­€VP[Ížéaž?H»<³åI†XVå;·“‡ˆm©úïKfµ¹9öêj8W_³Z°åà¥}ÃC-Jïà¬r– ©L‚¾Í)ÔâAlN’,ÒBËrêÐ\ñlé8ñµ¹ Éh"F#jÃÎU0ò]¬—‹P1R`fÌÜ~7z¼›PÖfù:4c\‚l35½Üñ‘µTRTAK÷³ÇÎôØfŠSË+ëg×b‰y–éÏß1»Ï¹m~^`®mf†áHÏÞ³d™½…ºvò$AqÙQ“ÒÀ8§>F{(— Dz1©Þ3ç)TÞâÆ„ùá¦ã‘"†7çQ÷º¹8o­“ll˜dÂ:qú‹îþÇ«Ÿ¾h75Ã7wÎ…@w½“R÷™Äd( †ßÈöÒs7Q¹ký£~zU+Ac‹ï½ÉºOmÓ¹÷âM)ks‹Ír =…Dïç&Î#Wvµˆ¢a’q=oîTÔû;<"¶šöö¼¶ú¼™tD…9Kô0zgêjÇÇKÌ Nª¶›,dòuØë©Fš"p z¬â($C c°zõímŠgBõÆëæÞ¶tR](ämíP2nb‹\å¹Ek:ÕôÂhò[qi䘆‘]ßÓ²íê±V¦Ý7BM¦É–dY¨çαÿ›Õj3L!^ óMH HT™…;oo ¸×/ïÉÓ|(’êp0JQïpŒâ?—Ï™Û Öv3\L†YãsÊ $¿ÖK¿Nù'pOû[ìz&^ÏûdWûš×C x¨¯2€šK¯ÚkäüËþÊ)7·š&VÕ ³/Ú‹q£‰AØ–J_ÇèT1¿é‘„ä3ЛÜ`çÂR.rÞ°ù? Ú0²ÿœHUÝfýºïócVnHÈ<2:㲸³2í•c.Ý•Pe2í¡ƒæF Z¯ðŧŽgwbwò”W*H…ÆêWµuÚÑŠ/*~è)»X}lò’óh| ‚“ u!ÉÇÝ‹Î2cË› .4¤çêôîÈHº]Hy•tO¶|-;ïoaà’éGœQCdñû¯µ\©öI5uH—Fxõô-· ¨Á¸ÃAÝÈaüz®š´ «=q™;}†éHõüÉå‹\c&Ë]9÷E¸C„Çèá•§ö™'8\®8*Ók´ënÇŒ`»dÚM;3uJ•æ“Y1Rb©ÊS?• "ËÙ[Ø!)äÁVΚâ¨.’éR70@ý}û'ì‰86ZÇAk3ÓHDJŠSPªQ‰Þ!èú57"·Îçü§‰´Ù­×ñÆh¥\z¤kºªa¼±+8ŸâŠpÏì?Ø:Åï÷ôíÝÄ&c  jÖ¹bå}HèÜ6ɹbîT ­»TÔŠ€$ØkÞxªPd©Ð‡8 YÑ2uò½˜Ù¥¦¡çý럋‡]A/¾N~,]Cü šmú²%Ì…6ó8òžƒã¢Æ±YhQm'˘BAi´ŽwHBª‚™ÔÏ1ÿÅe¬Ÿëe2¾Q.”2ÿAÈîÀLº¯bÌ%ǵx $9Ì2#¼‹צ'?O&~‹Ha>M7œZ"úpE¸u~%Öé;ßÍÒRÚS„Ú_ñº–âõðq™Ç´9­òï² ÀQסÉ1¥üM6 ÇI.p!ˆ¸r™·6†èÂKwR³DzÊ|žÅ^[%MÀ‹7ÝZìÖw®’gð-æë›®{2owÞI™1ËL—«@´øX†ŠA c*8Ò,ž„›Ã¢af|a¹ÊÙ %vÑHQBER3‡Õ ú ‘X œ´“>®]4f5¨ÐšBø{sU’ÓÇJ;i0® Ë”Ÿ{.¹rt‹Eøut³1Œú µyÈÃIùá4ÌsRŒ²ds—¼êoCŠPÒÀÇ¥=ý~¾>g‰ê ßÞº¯†óÕ|(%m(?ˆ–K[V·e'/«ì-׌™™¦%ľá¶ÓËÙ…®Z*Û î›çf7¢­ ™4¸ÎäX®À­d"¼K /¨qêäK8)ȈAÚúàµWÏV°)èÈÛ‘òK°W=7¶Q †ì×`ÃMv÷…*à÷A;Á÷רýÌe¡¦°*Ãé¡ÎãzÙƒCbGz‘åöQ ^>'(rAÎoÉñ¸E›Ðì­ŠEKÎË‚©+ýN9ÍeÇ>èà@Ç>MO›ˆA¬ÿrÔOÐ.yÈ\½Ñ¼dPBŠÆZÜß¾IÛË^“ÿäqw½½“åjfN£ª¦|)`z®)¦;Ga•9oe.ÕÛ]ÕSÆÄž³‹ß)Ú—&¸Go'ý{ІJ800EY“Yó•À3Ë1¶ôÃäÓyê‘àU¬SÁóÅ=;\]ÂP–³¤Ïyýè_P£‡äX, }Ô…3Š¿ÏÖöµÿ°ß.d=*ÂpÞYÈn玼쿥o¡¤À–Š©˜LïR«®y…Ñ™ÅN„ÕOpvò`$ƒ …­P'»|æ…1 ë|[h’ ýë—å>í åŸVì«ûƒç¥]ÀC ®®tÕГ+µ23@j(jï>3?VãÔ­¢0>®.¦NdP]T®(F°çÈ“w»M¸¡üÇ;í¨kdx ´#Ñd®ðAw?'RI%¢ aUÁàþê{‚–,¸ñG¨5ÑŒ|U à<‘¬Ò« ¿ïf2f _GXã¤Z õÔˆVé-˶­·¯ƒg­j«¾i„¾e¤âå×@w­âÑšÑÔ‚Õ”‡òø™Yïo7ïm¨îAhôEÁËT±‰@ù\‚Ѧ9œÿ|3¶±T¨*³T¨òjK|:ƒT*[ ”/&]Ç Ý‘ÛVûíÅ·»ÉóâÖ,Uè.£Þmè\U¹Þµ‚P 3Xòyõƒ¡H\Á.ú-ÌAû˜¦.×~äXx¡•{±{T¸ìýv»Pz0ßzEìÛŸŒV(Ö+†Wt5Ž™”=¡È™pÑ6~"!7hA6Jä l¦³E“Ÿ¸Å5’guXTÙC ʺ9PÚ‘ªég\”gX bRq˜­sÜvM+¸2¥¶ÛêuÔë>J ·°õ•ÐeX»6k›.¤”]ް•Ç‹óR Þêoç¨\tÎFTm/>¥§¿?…RQ7¿zÉHO_þùõõ怼 ¼ÉàjYbÀ:ñ¹Ñ$ä󺃢֥'î‡905;<ÃÌh mx #þûA[MŸºÜq<Ó•‡!ÇE€ÿ˜65l ö~ïGÌà}·5cxyòWßvÛTäŸ#™H·…õ”{ŒÒöÒ½0M ÞÜNñ“HÐ/<*¢›«†£ÁCˆå…<¸ é sÇò»QЩë˜AÔ€Q¿kz?H¨ª‰IåwqÓÌø›úž Úª¶L›Ý57=X¹,h!¥þæQî42v ©Z™YLc?Žæœ¨£èèÌàîŽàš9bŒ#SíØh|L6è0àòx!œC×u9³™ÞïDZ©’ã>'©öˆÇ|ð¹šâ*è¤rí¨#Íë‹:ª×å!ùÅû$€CŸ&Ë—UÛÈy£ã @ ! žH¼Þ]­Ý5G\TFÂ`v{R—‰ÂAN.úÍkÀtS.Ò â(‚• ¾'9qò‹zˆÃŒêÑ6sUkåùQ`óžÎÅry 7sBµ¨ô ƒ& £ÖMnúaD7½Óxİ«õnÓ’úŸŸ!QÿvÜÀÏ7óöXŒî¸á8€ctàQ"ÈÆP¡u0¼F´ªïò'z¡Pˆp±èªSMÖ7@Èišò¡×¶ À€ðH;«<ªÓµ]2ôêû§ÔúóÅ!çþÉáµ¹Êeì860.`ì©lD—®KÄK Ûåf²å‘ïJ#ÃVYZ—cH(<V/1®xÎêb™•Ëiš6‹3Œ, äÊ 𤾴 Ç–xÆüóµ= o×§yÜzô©¤øÇµjú€”[pÐî–JD´n"6W_©Ì’ÿ3f/ÖœŽdÛ•aûZÛbÿ­ß|ý†*iDȲHOž }¹óÃ‚Š +H†ÃÖ2EÈéÄuêÿ—/z׸Átæ;)ù›à?fAª‹™Ø]¦åJ€ië¡‚ÏÓ \˜gbÉg¤ÃLZ³êæ‘Àã„ê-Aj­7J˜dYz vÔÃdÖÕƒòŸ‹Ì¥wâüB½†M²b?¿ðåû °‰ç’“‰ÝX Þ{첉®(¸cžÖ´ÖùI5ÞòñÑG3Žöê§„¹£|‹Ôa°¨.|kŃÁÏgýÜAä;tû§f™lxÞï3r8U ã»hzÌ6ƃòð·!×zð4³¦/r'—IQ A9w1ä|â×`š³¨!6r¯0î¦Ó Ð:ˆb(kç&`:².кšˆ.-ùŒ‹3Hî•ËŽENPýx¾ÞErL» éäux«5=þ€ùå8ä±Ì2ų¹±ña†Ó>ˆ=æ©”,>)ÑW.PÙv(QþåW?-5ãDo KÐÝ5­kÜø† × I«hr³Ë ¤nê3O¤œ.”ü%i½ò1&ëTä:NI'Z OÖi æÌ×û[ùš<Ÿó$âC2HêPa>iµÁUƺ]JKº¨¨Z4¤¡©9S"ÀÔ¾›Y8o³ö·|àÞk3Rù0.çgZfkrb„¨kèy¬r#—§pKÆî\¸­”AM }]Ú–Â=Xù®ûÅC­å8˜…‚‘X óµÞ@êåZÿîQl¸Þ Ùu<ðàpN° ÉÞÊäö}}V©Mc€½*€ÐÊ}œ~¿úh‘õ Ù¨ –œÈ"$Xð20Š&7(±š—¯—µƒþ 䋯»}Ñÿ#Xº>—¬–Ì%ÿß”rôGQŸ‘Î$óßÅxϾ9Ò*8î0c¸-.¬ÁÙëDçýÁ¿X[6H иÐÓÃÈN¨EÕ¢Z^#‰ZŒ,à A‹ÂŽ<¨Éãôµô”<^R!k°%²R²õŲ¹Œ’â€Ñ€~…C3¯Á`;¿``ÍH#…`µƒä/î)E“Áæ"Ä ‰½À“w9«ÈAÄûˆ“ôDIe7 tC¬3n«H噣üʰÐì¹TEƱŒááŠò3#wvMcúÈÀ•òLûΡÏϨåO”LŸao¡f{$‹ÉXLŒ;Vu öÎntÚÌ÷ô{† «Êœðâ—ßÉ~}î>é~þn®|H]~Ü\ü\Ü›}€RßLNAº¤#ÉLf@b·Ë‘!!é-p”¡¡Y2bóNß.ãÚé0}Úv‹ÿ:‰€ „œú¼ÃaD”ÕD¼M¸°{yÕçE?ßεRÙãE[†ÿ…&Œ¼ô>Þ±dËÓ`ûj&Øøˆ?BLŠ—±F¢‚°™µcîfmìVEð^RoÕæTY¹Ì¡ps‰KÉ¿ú-?-£‹ûã–ˆ—]¸óë~fÐLÉÚò÷3ØíÒà~¨ièLòÖaŸ§kaòü>C´õ%$):¼ëÿ¾SÛ@ÚÊI·Ô@ÐÝ&ž“|aà¢?ªüfž§Õy.˜³~YÒ|uÇ‘*ÁNOàT§O¥€cëI^lñåß!Dw?7² Ø}d@ˆÉø#­Ï¤õ$h l3˜hÆw¡å´Äð)ºxMdg⛇™" ö,_žÜ•ýù·¦¢r_‰.äU7g§£}Ø >*ü#N€qŸéõú$âÅÔÕ¹îÁ,+ZbOÛû Ú«òµõœêŵ,ÝSé-hr,ÏݵöÎË©MÚÜ•kĬM~q=kó¥¤8ºÑugoðUZLØI×k5DÜü|Þýý,þ^Bw;ÖŸ@ä[¢¼0=T@Aâr bÁ¼PiìV /´>½Ó”¬5²õÌL+ÜX"f/礩Æ=TŠúøzrj[pF¾ºÜÑæ';ª:/&è5¹æ%5´Ž„T´ó¥nÓØwª¸5­«Ý¨]ÿæô'°h.õ˜žï,´‰ÂÌ¥¶r…–S›?…ì3¯Ò÷™÷kìß ¿Ë$Áð"=e9¢Ú‡áM[(•Ε9¨1Ý…‰Y")U\'†AÂ}iõ#bõÂ'²Ï¢‰87HüvH4L"ØíLPg+øÈ”w`¸)jî±i)©Úêýly],ŽV=U:IlÇY¦|{oßQ¡ç‘yĺ´—V¤,eéÁÉÅÏÉurAPGwÝ€×of vƒt!R•X~ÜÍ$f&_“Õ=݈ÃÈœ§»w^}|½OÃ5W†™)}8xÆ–â?܈›X…e #?’LKÜÔ×ÊÖ¦Ê;Xl0.ÅL\±={ê0KM晇EßZÀ–ER(È&–ôV™p=$³¹^#¾RAQ…®m’œÑÇT:ñFÌ9$¨Œ£I5~E1 .=J”„¢·wRy´fdü>PŒü]c-NRùð|€Á…èZ;`‡p¤üÊì‹®Åþ*‹8ÍG³¤2‹^Ñ"”úv)H—¨ÖBº~ødÞòÀ£ ñÇùÎcxªƒ·öøžA¹›ÉŽå](„EcãhUz«¢?î&À…Í‚!.ܬŸË…=tW:“úž !£k-$Rçï<ªc º]ÿ—¹õ{K}ypDzÄ?F4Ò7ÃøIšP®É²†ˆ/H-e¶VR>“îÏH®ƒZhÎVåZ-âXH7»Õä^omË•ˆ™¾yûHu9Í܇Ìçz]jçÄl¼‡KÈ})ÕqÇAh›u…ÐS'Hî…Læz©ÍÖþæ0lQXëÏ·ƒºæ/…dì!•á<>¡2­µ•nU³æ˜x´‰flaunn¿Ç¸ÿð¡™g•|p¸R¦çÕ“ôY> ï0¹ýB‚0ÙbGF~Ø][t®“0a¦R¤y:­{óV:ýë×§‚“ª"+öÕxÒ$Ï…9 ïû‚êQ§ÍhH¼ÚwÆ&곪–æÓÁÕÄ•›Éá7òfô„_ËŠ8Ia>HHæpœ2¼WÁä&ß5I›fT瘨‹ÍX™c[ÓAß™Ÿç0ç ¥Ã㙬™1†çJøÀ¼ùÓx[3ký°l™ŸCAýW#î“Þ;¶M"ë—ïæ£oFÛæ}„ˆïdûv®–§ªªÿ™ã3É vp"È2°'#¤þÊB/`DŠ`ðU;U Ýdo b/8?j“_´§y±³^Žì0MáÂ’‚#“JWi¢æ?x†tºòòÔl Ã]D³˜`\ãgs-z¦p~qŒgu—®ëŠ»‘hÈÙ«‘2™ìIznŸ)Y¬©öO šð(óÇlQÔ„ôPzáLѤãš\Á]ÜÍ}f&”·¸®—D–¾rßgb;ÄäMØbY.&˜ Ù˜ ŠÖ©x_)íHëbŠû‘âèxº†K(:eÄÉÅŒ Æa2ïøäØãçpM‡†¤PÓ˜ñȳ{ËFÁobâZŒû3±ËþñN5¯Ã&óªäÛ ´ê΢ß(7çü;K Ç|¦ ÌUg#¯ÂS󫕆”„žË0I*Ù뉷 u6àsô@Ž JN]¬µà×/»åƒÙă«Â¶†³Œ.ÊD³H*ØM./ þW›ðjÕ—sV¸‚GåÜ-Ý4–P•‚¿Ót++žg7ºM ¢²“;à¶ÍXDŠ­{rÿü(IÑŸ{Z†ÛïÒüÊ*vÙbñ€t“âÎǃˆk^Ï&GøÞ•Ú뇋0=1Xx‡t`˜‡1„mF+ Ôä5¾ÜV@:NDœCú÷dníìvƒ 5]ïd¼Óï}em(V}rjÐ1\‹x´ª¬~Úø"{öΗg~çC&ØËéŒws™ž°ÒÒÒWúp^•oœ`ïS‹_ó£-•Ýo)}ŸF²VÁ’ĮРkŽæ ^|å’®Ò“TM—D¬NØàZ*é^y?B°–úȳú&…XÜÐø˜9>¢pã_Žñr‡éÚ»=+»ŒµÚ]wÓç\®ØŸn ÐEžë1ÓCËQ ï™~ÍÀ‹y ò›;ÞvŸI/*…-ð¼?„R7 1—ßh6WbàížÅzƒ%ˆAª¬Gµðbåѱ²¾'$rP±gƒ†œŠš L¤¿hrF¼¥¬é¦n­ðãT)³yèFm=Õ|+.u^³æ.,ØEðfÿßÇ42ÿœy¾-{f用çmÞ„jFcöɸÉÁ9?ý¬\‘$«Þ![ø˜jÃW\ñc¤²º’—â}´Æß•‚kø§l[?ÃuÝgà9Qlhà<[j‰zÊBOÓyb°Y'+A4dTiƒ½áÿÔÃ;ÈQpÅ~áj'X¾g÷œƒR+«À߉ùqë×6‰öZ ý{l½0뉵hÚJa÷­­º†í¿qgÙ¹Á´Å¸.7G¿ÛˆZ?£%—ñ‰ºš÷ÿ·¯~;†4³-å¢zç&;HÏÒ™‰ø…Àšö­#öedøãP#€žÆ˜ªdÁö0jVE³{mÈõ‘Ç^òFžè]uä(+TGRF K`• – T0N,'ØgÈt¥°+y«gêMÍIArF±ï·¥á)E®HŽÂMo;R#äQF5Ùa!EðÁ™e-œ…sÇÔ4‘–¿QàÚYë g&$´ÏGOGrñ7ò5ê²n±çÄ©½§ºP䀸šYdRSz»•0“={Ÿ=þêl¯e2;Õê­Uô5íƒHƪÄ<^ø\l+Ê<|ÙtRXƒÏü u`7£ ï¥A±BdªÕ|[â݇5”;|q±yÊ»zr¿<6p¥vÊ[§˜½Ê<Ø?šÙÖéÉ›ÝHSP÷kRB5ÍÍ¿ë3”‹9îk‹‰N•6Ù3ᯠߌ38Ëò›ŒûæÇÔc;à®gñ»‹2\ìÝ%ÞÿÕ8}Uðcd[UëàéσÊ+ÔÊH:ëc¨ihÜIäç|k6ª»DdžýÛ1¸âÁ0zgôPpÖWàpÒÏ0]R>]ÉZë7qˆ5^¿ÞHÀø1‘ØäÂKü-ÓÊ_­‚›ÇB4ªÝÝo ¥§¨õÓ6;wˆûƪC¬ªßþ)W›µðÈþ¥ÝÒx¾ÕD$âQù£Æ{Ü|ƒµ—‡ <.Ì<˜©Ë§!NhEjÆN‹àzžƒÝ¬,`õòî\øAW]{Á> 5–*rSÇ+Ýa\%wȱ^ˆ‰Vá÷\d?3ÀPçáåÞcÝÝѽ8õTµôÓ3µýv.ŽïܰiZR^nm®¹ä!‡âèúÕæC£s©ªd-v½È³øíí‚q/78O3KQ^`lÔàÝ«º]VÝÜ > ôî]†íµoýÃN]°å`ùÃ~c—-ˆ»€_ÏÔ9Œ¶àÌ ÎÉéoBú±ƒ2µ,-¿<“4k3Å’ËBŠ& ©²”lW|ÄBó"q½x}?þíô´ÒåÙ¤ùl7´$²ôŽÈ|Œ”×ÑqàØ ËJP­…uš¦×¸ >[üÕ]¶¦×° >kS\æ¥!³÷º >sS\Æå‘Ù«£2.cq©×ð4ÞBW×Ì"régq†ï¢J\æFQ©×ô4óŠ"3·UåØÌµ£²ïä4Ÿi¥|ÖF™ì &óW4߆qê®yå{ÓûŠùÌuÓf&»4¹Ì °Ù+pégv†ï¬ ¿…¿E%ù̵¤òO|ÆOüîšuj.û |ÖÆ§²Mϸ„6{Uþù—ñó¯¢×ÂLŸe…‘™Û¬’>c#Mæ%9ƒ×´òÈÌm[…|ú:„‡rÊy3ÙÔp6]C®5‹÷³ÉüÕùlúP¹Ïfx‘Œ_Ñ$LÕT³É¹´{(Ù4Ç-󑮉€Áe—×a“„]¾Ôk#çö$ˆò¿Ý£Ji¦ÞQK»×¦ÅJi¬9L'¿d?6X¹cþfô˜N‚?¿X•S]³zÒd?â3|w’ñYª3ÐèÓÝ£;Œ§¯šìÓßa›ÞÉâÔ§ƒº¯h•ýÆwÞêûê,&ãöÇ®Õ3‹x(3 suXOÆ<˜Ì'㞊m&‰y"d? äOe? ×.’z—@•ù.µâ>V­Ky&éÈôXNŽNq7ÒÖd?(ݯ–~WI•ýF[zçÚÔd?~j_4÷´˜Ok_€{[ì&ç2Þ)´ùϵÓœKÅä jË¿á?ìHˆËjÊŽëÚ"KBÛW©ØÔSÓ®bÔtèfÇJV¥ˆíÐÒ¨fÔ{w)FÛ´k4+Ö2kvç“»^šw95+GgV[»`qQëjѬ¦$Úà&–3Ä|M?TzØô55,“µ^F%þYoÙ¢OÅ|:m[¨«³°ne6géqʰ`½ŒªT-GŠ;YLM¨Ð¡e= Öè>RëQѲ›’¬L&úÈð¨×¥dÕðuÌÿcó&cav³³ê£·–¢ýPÛ ¿-.P¢W?Õ ö8³FNZRyhZ®¸€^¸"æõxb¨m—Ī۫¤]‘6…‹Ãß§·\š…•-»4ýXå½pã‡ó5SÿÜ,9iôiÖ;OƒFÇ~Qó­í-¦íR¦MzY½ø”ªé–$çúéÎo‚÷j\´a刕jâÐ׌Œjáð¢ín‚v‡+þ7¶µ¥V·u®cëÓ©LI3ùÛòÛôf­ËYÃïìô¸(yw;¹TúÖÚHãZ8·»Ž¦ttºþ¹ãv¥Œœ¿ßaq}4±ÎjŽÉËÉŒ¥;çÝJÚ»MíYt9\ï8\~^QävAgŽªëÊy2®ÓùÇÆ’Ë‹ª»¾ÎáIhe”nÓý°Ú@-giäClŸš‡ó1>Á.~ò'žË±8Žg d°‡ócaú±0IÃó„5RòHe.œ*ë¿ï=7ÙJ ù½Š‘˜fÕÈžfJ¯f7z}½@¦çË ¨Ù&å&ìÌ'Žžõ¢ZµK/ƒ^$”g1 ¨r†ê#ièâÒ|?Ú¬ÃKšÄäW‚Ôyó©eY¡d§ÝȆwLj³`^¿}ÂÑo rÌ$ôˆžÒö™™À¼° ‘KцEúAÔÏÝÛ”µ·°Ï9*·nlíljGgÇ&î0[A…Ô†•]iÔ E_±ŽVdœ>¯ˆ{tŸœÒ”d^*Æðy<ï#§®N%}r¤>ô*fÚ©n‡ßõ³qñ†‡Ž›ÆGU]“½{]r„VwQÍ"ž¦^T$@|M<z ÿVÔÿ]áÕBå.¿l׫f¥"´á‡&Ìu…mîÈh|8¿ É‘`xiŽNÆô‘œô} f¬G›f5l(é÷-ég7Þêf6£IiN'+#{fy›´Ú–§…¾ÍÎúºØqXñt41e^UF-Ï¿^•F6'7ÙÊå^{ôßÃçèée”øur¸þ<5³ƒ¹ÃÒ¨„ʺ§aíüp,õR9Ý‚=Vud¼Ô˜:²´útËèï*Æàò`äbø9X}i|t´0¼¬ÒTજF§×Ù./-;Å.c&*PnwòOþècÒ6J°:U+»èA ù cDèÔªÿ@ä& ĦÃXùæÇòÚ£a¹ó”¡ Ug-Päûsd“Ú¡÷:|)Æke'ö5CŠØmݯZm{ÞæÆ~läf¹Ó_àïçÈVs¿ÞÿÙ†®¾ÈgŒrû Šl5}€ W~ÿÓËëk&“%ԌʕtˆºÌÓìz: ³ /ýe¾Ú|ZkTfBL‘…Is`Måªn„ö}CGµÐ_¹¬F£bf­ƒ(ˆ«¤ãÝÊò¶3(k¸íÌC´ÞØ(æÝ­¡¥§M(÷É¿¦J›+!Ÿ¿8sˆ”lÓeªog@gTÎ(-Kbú[+ ¢k×KÞ0ej¤ÖQ ¥•£Æ]æ]5°B‘¯Yyô¢‚ =óŽ8§RD_aPQƒ^G—ÖãõÎäÀ§d…€fJ'N}‚y2€Z|cTŸ¢„À‘7¼*@Í–‡6{äci!íEçRýÃß%z#]_S‹j2î°Ñ&½&ÇæšÏ¯æ^qĺ„LÏÙ÷ŒcÏ÷¢c¶£ ¸gôbT'Ï"ÏZ,ãdš×œè˜]\y‰v,%Y«5Šn<®l šé¥ÆÚ±€,IÛã+ãå\Õ‡/ 9g+ÇÝ6›íâß4|m#y-¼çmNîÅFÕò³®õÅw¾÷î·Ü^¸ë§ ]í[:è_š¹œ¹¢]}3Ù{ „ ß^ È’hºh¸ô£Ø"\TÌÑ£§ ÓîQû ­öJØéîÌm#ºëc;ËŽoh–T§©ˆ{1>]gY« Ó€-}ÖL{û¨~ŠA@/6Ì$¾œ5o~Y¹K“% È&ÑÔ~fSþUwûºi™jkº3m õ…­&QKQÃë©CÖ¾bÊľ}¬òñ4™$%jKÃZ&µúËšã> ì;¨ ²µ3¿Ý"D@†5’î à·*ˆª¡dÑš£»}go!»CúCQž_Ó.›ukú+bÍÅçDh¡ÿΖ4œ,Æ5äã@ü÷FÍ1Â1qÞèa#Iz7¬61C£ÓÇ eÿ –ºøjy@Åœ–žÖêÑ' º3äfx¦ú:&×Ó×1o#û=ñ˜è0ÜQœ¯¬ãÏwÇ‹´H½ÑŒZÏ›Q¾ûmˆçŽu1V} 50D`\¢€h„o-—ÜABšŸk`ÂÍ}8•®}Tï5 X úd»‰{{ÅÖl`?ÁÓ²-«F9E* æôõkžÏïD²‰._Jôä¥Û²[ˆ…ÉÍ=H¤Æ±ú‘½ýêw3˜Nh_ñÙ)íûÊ28JQZýH¨Bâ?YIØ$AžÌüÁû¤íV>Î~‚‚¿°·ñô¬@J+£^IwÐrôsÝéE Š¢Ã¦~SË<¦ETC¡¸ÇºÙ•@p#É:iߣѾ ãCY¾ã¼ÈönoîÆY›TDšèJ@˜éb@"&UÂËnµõƒŠ´8¥V-)Ä׳™Hs殌ëÉÐ"ÿù¦Ý) ¾ÙJ¾ûü:ÿ «ÒÇGMeͺHá݆ ëY\E‡sx;½¹Å…ú:îÖÏÍÿ^5þsÄ÷×Þ_×*Ðj4FpTµø!½Eì?]ZTnÇ2©DïJ"OÙ=¨ÅëCYoõª#$€½CdÉ7qæMòBMð9*ìøZ×;R÷àfÿ¡ò”y-½¶»‚w/©5 €9¾Äó˜Õ=á¯Ë“mqèz•\º¤#®ê[7‰2anœk2\óMK»»@ÓA €óåeóag7KÅ~zŒ™Ãúàv>*Lèc®â^?ƒãv¹)wÄõsÖ÷ÜN|&ó¨FKŸJ7_L:5ýxE;î{JÝpp‚|com1§z2èÿƒšÆ¥œŽ:±…‡àVëSš m”Ðw®ñ2VMoSÃmk²ß*ë‘|óu²§¿Ë¨n;<•ÌE‹E °¶ãcþ\>¿T5,;­Eµ‹P ?ÒÔzì ™B˜r+¾ Íê•é¢-eGŠ£‹Æ£ÅéÓ´ÂѯÔáµ[›S?Åz1óíeå ãe¹üx;=ç¼.k0{Á}yv¼ù·‹Ï“ Ó±WA£jYT*Ðê£NøY“‚o5¾½nßQxuǯ÷Öæ‹+]<ö†v~ÌíX”ZŠ‹c…f ºœTªjìYéÔì£kº{ðh2u˸´ƒ6×fú’õ,wáµëTœ¥)ÙlH1õT.ÊÉ«›–,˜qfL ¶WfZøÇIЬun…f7ò÷áou’·ZGHe÷5-Y(@|Ť@+¶ÃŽ]C†Ú½6w aìüÀôOª€½mÑb¼ZòU.,¸÷NrcFœÝ?$Ï0#B¼œ>6Õ±–-œÉQè˜5—fÕô Émž2ÞÁ”5êqâ;D Ù¼ÝÔ°ÂO5Ë,Û«e9š°§Þ³ÂÚîÞ˜ZFSsÚšºf)oL%)ñ“~Wú%4Tc ¦ DkS±hÓj¸Á*5œë,[‰œ2ÕÚLØ}Q`uNwÙ”òSþ '?æxA-çla8™£¨³=ÞBÿÙð9¯ô/`¾ÁÆC#6üX¿ç 4ª2æÃ,÷ñ+°ÿâC¹rOÄ2 x¶Ñ`Wöéê¹û¸á’¶j.v*;÷½Ô¼,T+ƒ{ßE²´ÞÇR“¯¢-W„Ý—.oðÛá}´õb›©gu/…îøèŸG¨]VYS#¥Å‡tNî©?S*bŠj&e·"9)Õ{P ËvTAáÀ ³ë@7Nha‚òˆPÙø¬Öqf¨ys^õ‹vÇMrÒ£ç )wýåŒÓAê6ý{3LŽBA¯ Z²ëÇwG»Ýã¶ã=:m¾Ö×"‹fãƒ@z%ÐGJUJ‰ÔQ'jåêóuúa£ïñl©ñ $ňQŸ:ÓKõ¶¤Ê/÷-°Ì7ïÅaŒ ¥Œ‡­ø¹†àýUª²k uR(ˆªì—O_Z ›ûÕ Œ@o…Y8Ù/´"’UOiªDVù"G¯ª*x›´˜+Ôä¿Xý 3x(7åƒÊ\Tm¦É-*ìMžñ<ÞXjÚ lpJÑÄ•løë@–r‘Ræ°•èüŒ(»ƒy—+xJA ñQêÚ+‰ÿÛjc …ù0eÅè*눔FìéË¤ÓØÎ'Õ Ss+Žñ]ËžážÍ^iv¿þ!®¯NR¯Ç:n¤’Øž•zèŸçÖÖ]±â47ÜÔ~È?0òaVŽ‹ e1OtP÷ö€zD×%¼C>9/Oav^TŠ­¢øjV¹Ï Ẉ6aq[q`›ë¢)Aº\¿Ýîÿܬî·ä|/r, Ǩ>RöõiŽðí¬¡GZ¾/ü=±v Ù­~¡‚ *àJ$äŽ$Ø$B¶KGŽÁq¬Þ c˜¦‚©í4—‰H›Ê0…4©fY»ªÒèU3C£¨¡!}䥌À+¿éß æ9à)Å•Á‰ö&S ¹Û›Y7Ë¡ mHE`³RUsäˆÎ>!ÕÝo69jÈ%ÒŠ³ù¦‚ú3<“ÿÞ•X,çÆ¦àP$q.²] TÂá6ëyÚp °òX°Ò ô³ZÏ…¢š…ã"¡j‡©Ì‘)U*eP'åÀªÇ×(ïŠíJGÙ)Ä?¨¥DGí¹0©¶3u+g“¼³£ÕmÖ‚–ÁÞÔ|ÀXÓbܲ2ݬ6­xÐÃ*jhhRÒ¤Þ«$ØM$y´Òÿ î᠄䮣²…Ú¤xh#Öb+izæH‡Œb‚`Ëå#"â,¼|,ï´{ŠUè•b“£ìqž–îqNÖ s`ô'º÷`Ø\Î ù@ i-v§g^¡G i5¼ià£Ð¤É³'€ø~kÚ"§ _+šK=¨iìY-úÑ¥“µ+>˜¯8¿AõodVt7Ó?õnMÚIŸQ¢‡$ò¥Êi]upÔÌÓŠÆOîÑ͹ Æâ2…ãœR°/››ÅgJ8Ò)§zòhlÊOAc[Ø,øqZ‹PâM+ëXçm-‡æBøÀù8ò1œ˜¸ðÕÒ¨ˆ Wí{QKÍŸ‰Ö™73‹„G/_L¤U¶á¿Ð¯åóù`rì½¹§nßþßkW´}˜d+‘î0‘W²åiÝy¢}ž%ó.ˆÕl²ÎR ;n®#Z`ž¬ŠšŒêiv)|¨ —lÈå!!5±m.§—¦ø„¶n© ]Õ°–-ÿÍzÄ]i‰êxï‚:!´·àöV8…ÐçÄPè³eS%’þóc¨lõdzæ j—¯BÓÖøÁ8„©,>}•Á¤|TMŒ_hûlÙÛÒÞOáPFèÌÐ&ÓÈg|ªl©e켄QfåPe´G¨K¿½ÏT‹4dÑ@Ry’K'pC}äíäv‘Z!uÿà" gÜï ÿŠÏñN½x„>üŒa1ø‡üÂcZÀâ‡(§NÃ|t«ÙÚÒý‚•XÔ¬â@änÌ‚zæñO‡€'„ô6!A½Ö±mÒ^w„©ékÞ×p§ ïovyFOž¹,o›{O“Ôv”\½Î¯ÇVöM:Wr;ä~ëdxŸ„¯T"´§æ\—…&œ8ÊVjÀ“a1†€Ç•&>$sGz÷žòo7îƒÜ"}ë3Ç;¤xVùpqÙfÑ@: €x8®åDAS·ì YTÚHUŒo±0óÒÆÈÆædE\tC—ŸEX*l(o­§Õ­N xL]ÀÁmPåñª¶ªtGy¬oÄšgÈ£í˜6} þšºf<:Dæ¶Ù`1RŸ7Œü£ÃÙoŸ ›ÜP{ÀnJú·TòŠ{Jù×±ÍB1P“‰·SÉÄã&8ÐÇáèâ¡u -±ú˜ 8ËÛ„œñŽ_Ñ•€˜›“ÀÿËAdÄ _P«êÿgή;šTC8™ O ¬±ª Ž ƒµéœdTÜ Q-IS=y"ƒ ÀZÂM›²fмj-z `)†ÈÔ,hla%KoᜀΗŒ懰b)eýl{ H-yXËÅdÓ绞¾eŽÕ¡béúfçpkSè  ‡_`·“›âåt4C­ J\ Á?yñyÁ>޶ðüËØ¬êøjüÎêø~¿~ Þ³]5={Î.®}µ†¢§Ç+Ä![ÃŽðA»‘æ%ˉ-|psH+8å÷¢pÞÁ/ ßìºP€M³ïÆ×Weµ9êý™É"þDmKËŠ;0¯‹ñ2Ö P‘Cr2_#q«Ç­zDŠd°¹Ÿ+¾%9±å‹¿ô|‚ËÛ®’µW}ñä ’K„ŽAÖMòÛ5w|‚›'é;í{eÛ|Xé׸•n Wj¿<¬2ßWÀü‘Þ~‰å B]J£%S®á“¿Äý !>->B/]Ö…Yžcܹüä>Γ.‚ òCº‰w/(:ÂØÉm匿å +J8üZš‰»Š¿³$¾Œ§?ÿIQÓ9±‰§½QÊÆ¸HŒü9¯šQ³Æõë|‡Êïs|–È9Ú†A}‹?ùüÍWG0rç yv‘Ðr7ý<ñò¯×¿õd‰ªRøîJå,«yNüS•75•‡RÑZ#Ü1du™º&rþE{ÀCõ^- 00Þok{wɽÀ’L™œIš&£h‚øU±(>í]ÿFû®¦Oqumø†é¯rš­R—Àô(^t'§G-Äâ±W˼ôڥÇúÒÉ@ï­†¼M@ê‡â@ù´-IVqÚoCÃòSê|§’‡ æÓ\²Y? pg¸t·þrŠ/ô˱–î%·Fmw~!J,)ê¥Y¶¬) öƒãü”lŒŸw†¬j9ÏÚ‡”LÖ+å!Ëmó'Ú™_´G8Qö¬ösªÅ­£¶½aÛ˜~é_öG5yídl™<·žey¿wÜ›5ãóQÔ“ÐÎW" ‚XˆE˜Ë· ÈàTwX öæxiõbìÿ¾B~r~ˆ±gzTŸå'|×ĹʒàÂ)"%Åú/8ÖX;1UîF4vM‰¢óT«8òê©Z›E5S ”pg’“rhµ«jjQ8Kë‡èU ²kÏš$Û@0…-ÄÞrþ(´hêÏ ”“ñ„ZUÈ/ª º‚´gW7ÿnÞánœ•+>ÛG§l¶Û¿oÄ'̳SIYתõ•"î¢ÓG€âµqð9z]/vXÆg>o'2¤Ù‰ÿ-p˜R57œëøVÒÀSßµ«ÑÏ ¬®KhzoCŽH-ŽËšÔû»J„p禫U:E`¶wÒX… *0®pz&DŸÏU/k¥.B*Ñbú7ü—Õ¦™¦Y JöટÏÊ8mQkË™úIu¾«à&ãêy2x(ýÞÀ„9ù ÀPÈ祭Ô™ àvJ=u/¿ööW™0›½ÄYÀ*ïTÛ=ÑÔÇ_ï(p^oñË ’°øˆ×¿ØËÏëŸG_Ë ÅÄ-éN\Ê!Áé¶ÚpO±¾ièÙOO—ôŽÎÙœS[7Ȭ ]@GyQ×,ñ”LŸ$³LAzö¾s^úÝ:¾ÅœMOËÜpÞƒGÅRmÑPÈä)#ý¦Ø÷³ñøi"¤pͳêAzCQÉÚÃéYÚ­›„V1²Йz.ósç«”Py×,Ó$CÕ´+–$ŲÒ^íÚÙ“Ã[KN& ™âÁCo´ì Ó°MiáÒΣeìÅØ-c_)ÚûèlD¡±jxžu€-˜šRžO8 O-²ˆÝ›ý£þð}óÞª¨teSÞšòu< »“Y  Ç:\.ÜVR9ñuÄc~1^×SOgè‰À(\óiŠ9Ü¥WŸ$!‰igq·=ºÜ© (-ßK jiÏÔ˜ìPj:CóÇ»ý¢«ì—3¬üüq ¸-úSD* ²‡: Dí4%uÓIàX@”²0ø¡tÔIŒà¸é/h ]±¥Ømé¢$ü<5*I¬ÃIò·Y0‹Âú%¬€›k¦b8Áƒ6'­TLŸ½W¡'GèÉ5!$šr~Z˜çz§äîrá&X¾fì”v>çø^æå(0 v(¦ 6·ÍV%wûžJ|ËŸD÷KIgÒùÔ“a ¡ñ›“Ñ3`çÑ ÆÝ޽‚-tÀ†"dó_”‡Žhwçnþq³`Ë·ØSÏçׇÁ4NnH×rHugQÆzï‹^éV$FJþªHù²åòDb$οj«‘fϽd®Lšó3p*j#ß-ºzÆÓ(þ4v ¯|dîÏÀá¦Å¬¿=D;Üü/Þp2øµ“ýe ¾Þ{_nTr n»ÍSÕ¯yQ‚´¬íì¿=<*wwïéó]ÐáïŸ@wûÌë—âã'»n_£L·ÀeTdh©Ç§H‚޼+J3>zéÇ»‡³ÆòH8%€cG]P( “êc† Þ”…Üò ëùÉ{Ãoçx†øÔɈ’AXíQ¸óOvÅUŽo·äôdõ8ƒïmTÝù¨zyôÈ„»{’û:à'|Á=õfaßG1¸4ò¨¤°½Q>²;rç"›m<¦Ï^M|Z"a—äP.”B ‹L,VÇCµ˜µÄ}Ê-&°*¦…T°w¾-[csY¦ É¡É H6®Z5k|^Ó2f­;ÒšŽØî¾äI°ÔŠoYTÝoÛT-4‚~:éòD ‹ëÔç2ÐÕÊôæ3Á¿â4¥ä¦KÈaöl#4¨pÈ>ÁÞÜ.PEu/˜ùxyýLNãŒ|µqãAE9‚•dri‡[(A°"Ä yÑÓ”¡µ¯hÐ&£I¡*E$ú|°^P²Š =VSÊh$´ƒê«DÓ¾ÔÐ+ãø·–´GòÕD?)`óRoéÀ‚Ò×;s$ǵ,MB£Z³´Ïö²NqÑAŒÐ³Š‚”ݺ5Þ9§8é4_=ÃÒ™^ŽI,>bÊçç:GC+ 5ƒ,\ÌÀé礪eI)Sjí"/‰õ¤¨&V¼/Èž+?Tnpào¼~Œƒ¨Ínha‚JÜôØN’3jÊ­óŨ¿÷´­àcmk/piZüvžÁÁN7„AIPùï“ †XöžÁ¤~™òµÎìä°‡(‰°-äh’ÇF7ýúÅÊW ÅìíÇ žÙ~b«=êMqÈ[çb«ëÊúpm«A¸XªšÆlx ÷”Ô¡œÒ¬ØÇáUR|ä§ìKeìB:U#ÁÑÒïW;à ¾ˆ6ÆÞ_œä3y7ŒAÅ!¸Ê²»¾ü ÇçÉ» k°ÚÂéÉÇêR³T¹òc!£SIÔzFiÉjséÞ1•/aÁnÉ'¢Ú G¢‡~›M^×Π¯¬ÇÍKÄÞÛ»*ðã¨?ª¥OÙŠéWtbÊë¾æÀaê*€Ø%÷( ?ŸÃBxçŸ÷”À‚òVÍËmòHöóKíÌNmä7´‘Èø#§Ý¬—rëšÆ%¼3 “ÇàÂ0!àñ]4hóQr}È+Žõgá°¶2'[\Û™÷U䦻~Kêb­iÊÍŒZ1Ä×V.Þa ³c»IÉUõáæõ[$Ò¸r¢“\>sÙð¸;åWÇÉÏÕ}|¨‚w ùv:å«ÏM¿Ôzq<"ÉÆ?t7í ¨é@pÉ5äöi©u~§-z¦`˜a¥[]d"?‘i‘÷Œ29êZßzÍt §CÔ€¦-µ“+!…¡ÚX¾ªS ƒk Ü_có$@Å_]¾oÕªŽ>^YßIvºG1¸vZRhëäÓ±dԸѺ0Ëîf{õSÝ=V9Ò¶TÛòjP‚À‘S sL”s¸lñ†3 –8ì\€Ãø¬î$„}ºg‚dl¬Ë²í©ø¢Ù: ó™´ ’Q;„f3ݘ4t}TböÞØ 2¼”½V}u0ô«qÎË/.²²þ7•› " õê¶­æÒ‡Ûb™×d°0{HýfjˆLæN¾[*¶ïE©á’K '2*@Ïñ=ør…wç‰Â›jøÈ^0æ ª-±›,­ÞÁ“Ù(i ÈíШ½¬YÀźà  º8íÀšP_lÀá²p@€êÀA0y/Ù(ë.ãíP‚J„â»ê!ÍAróQ¢ôbÕñ<ø2ÿù™½ŽN^œF¶ƒš%,ɉ³ïxU÷dxÓ ùv¶TH¼¸!û&I—™ •è róéÇÞŸG]_Ϭž}Ëâ5ì‰Z¥ÒdA…t†ëJÝ"m«jÍ+”^ÙÃ'œÜÁc1Á6'Ð-~7ÓCáÓ“`! “+~J ¥ƒûºl¿ 9w\¨(TˆXE?C%­1\æ²X’±¢²»;ÉH=%¾f!‹ÆÒ¢(E I_r³»]8üõôÈÉgh4«š¼üµ/äèhÒv´GñÇ26/„yÓëµL¿34q <øYÓ,\ù\DLîà¬/„ó‘;úgcÓwçshSwgvoÒszûî¡5Aø½Ù bò1Xþ¼Í¨xþæâøvJí®ºÛ/ѰÂdêˆ`Ï4ÉŠ…é/íòjk•š+rÈM^XBö·`Kß¿/Sÿ ÛžãKÊã?œËM’TvC\`~ð¹ýøVZuo‰yPb8èZöAÿ¦É>º%Ý*ºêó=‰}üêÖ@™ò=ÄG+½>BúÍ¥;ˆf\K@ÇŒ\À:íL?¤õºá ÝÍã£ù;³ÍÚn%—aðR¬ˆî;îøv¹H¸°[ŽTør”/ßû˜—xp}èð ¥èåÛ&$C À°^ø…Œë«•*þv¿puw°àíGÚ5MõV‘s]µ<~b{-†ý¶ÓÓôƒÍeý¥ðu¾ý<ÞX'…\{œËÍ \Ú¤¨¯Ù®f “à À(Bä5ÛRýšÐ%%‹”Ä4ª@?I“¦°•H×Ë0 >XΪfõ\`žþ8+ñÕ­ÙòhcW$° 9ÉU·Èr ãæ¿]MËìqÊ»-‰ÊÑÄn]ÑšF~¦ÀJezP·'-]¢Ö3©ëoÿż xt:„eSÆ^UKÿÞ.žÇ<»ÀVʤØZé¼ÊZVÚ`ËJõO ñàq% ¼bmšŽÿŒ Ÿä¹q;´{¥¼ùpF#²†k ?1TÂÄ¥‘gBÁŒ°¡F]Q”ŸV×Çù<¹P³x{ÃÅÄ‚*¼¹—=Äü(?žIU°O5öU™°òͦ(¹{£kHp$@ƒ>oñnAùµù\žÖ£>qÈåÛCèÕ$ B’Ôÿ˜V 0-^ÏF^M„ÐeÂuDú’‹É@²èúÙÔª}™»†uX'ÐÓˆéZ›ÒîÙ{†on¼Çõ‹Õæó:©>±¶7Ïή+ s*uÿòî7vokç.;Y‡MFç5ïÂþ—S°9™Ùê‹>ý ti`³s[FQ Zç/Q­ÿšÄS VÍŒ`Y,µ“Ë.3õŸ I†­ ùžŸ—î ~Œ—b‘Bæ³1Ï÷gÃЊqøÔR¸>Ÿçû÷Är ´Á±­Šs|g£Œa\/õ:9ººöšËRy·Éóù³è”ÍÛq»B F”OÓšŽ'ú…:®ø¿»¾‰›$ÄÃÈaþÎ!øŒç ,ÈRzYTù<^öŽÃXq‘‹\?pƒÁó:‡wBc`ôJŽaUѸýý¥Lºµ4Õ`ÆóUgæ)å^”®¶n%ÝÕ{Q‹R©‹—¯<ïìõ«à@WÔ6ç)Ò¿š"Qh8Ï–"Á¬’žž‡ùWÄ¢M-“‡ìè/ôšuc‚i OPÓkèWÓ¯Æ-`Q”à Ï`¼ã'¥¸ðÖkïpÈêP[i’·YwCctb}û@2Ʀr?:®›˜›ó€^ÑK=IµBͼ•ÕÀO!ó^•#”®ÊBÅ2‚NÓ¶£†{á6T ¤.‚Æù˜Ë>lüéþ È06sü9wwÞ$2¿¡2 ˼ó){ì÷½>ò~â¿@³Ô¶õIGÁ£2sÌ/¡f;õ3ÞÇ™ÏDg‰z.ÌG¡†|ÑxiðÄw†Ï!”¶LÑ|ÈoÁׇí;ÝI¬M[ºNQ€iÛFó¤më†ÌÚ€£áDpѺtà£áŠD 1]ŠØ 1Cq… ª·¬w&Ó×z¹wG?_ Ì m7æG€lx(,x";(Ù<…”œ!”žó«²´éï„(’ ìxb9ÉïˆMïÜ>!ö<דÅ<§ÿéídqt;¥¸NF2ùÿ¨"h¶ÿT±‰~™OrZìeSQ-Ϥãªcyв_$¹üÖ&EÓ\>”Ü®ùUát/>«¢ÃcuM“7Ћ‚Ã# 6¼é+§ßšóm߸¡Oü“3RÚ!|Æ£dȉ§ & ‡÷eýŒb8;Âê¯åiå¿ .ýcpV‹d0QÙ§÷ JqžlQß´S'°ÛF¾VoÈ˲W†Ì’É…>±2ßSÞÖè™ßáË>”ÃÇÐB2…\øH}¥ŽWAË6òW˜*ê³íî„Ö™?/¯[&ÏÍkž £÷,¢Â][£k?†cQd'¢¼ŸD°+ÞtýÒ.‚þ,ÈÛíý¥„Øg±_Ç1‹K¦ìCÕÑ|‡ºÁ†u‹Ö[-¥¢UÓÔmµLý¾IÛ.mµèÊ›)~¥D§‘8\à«):aÑ %ŸBeà(>q‰~¨^^=¾|ÉèxiØxôÉW dãTãŠf¹žqÁ ^O½Ì° ÀS•ÑhÎ֞Єa ã… ½8k6Néï\íb:A‚±¿©Ä: _…ÖÅrkë\L§ç—× °¥_îÈ¢‘Ïô¸Åá öeûVðê÷2Ä_Cð!6—òÄO²\A…j–¿>š l”¨¢_݉6بw1Úo„¦2 5]jµˆºpL³¤l0\D¨n ¾ ÛØ·pŸš ðàC¼€J»weßf’(hâit3TFL¾“„¦+ÀBÊpIÕÜNäœØ0¾3—À«uæÍõ]¢Y¼§+ùøè¯ U™e8¬•¿uÇÅÕLdÿÉPt[ž•üòÃ<7„>‹ÙÈlÚj0‘¤½DÝ-ú±þÔíÀJnÅr £äåÛ?*u‘Š Pn¶ÛöCx'ÉdKÈð¿)ÀÍG×PÙ‹yëeAev1sà*” =ñZ‰<ÑÆÞþcý‹³Í“´ÑÜG)gÁû¬ˆº—â‹Xã ¬&þRÅÀ' ÆØ#^u}ŽpœÖÜ_Á’½­×›'1•ص¼sÈm||¸Åþ_ÁA?¾—2u)ðºmâÃÅ|/¶çBýôȧ&À ª¯jæu£Q+_VäIÛAkŸ9ï{ÎæpîŸ"'av{°GÁ§®ò0^èņ;éõ"À/¹Ma[Ä–jŒWO›P-< eĺ&.㻵 Œ«>c–×ûî¿Ò¦Zù³›- Ýq"TßÀfivV2¿šá6?ô·)i»oºßY~î6ÄØ!’NLmƒfÌ3ÚÑßkÃDˆŸÎžíâ­mœ©i»§KëÓ5ú[óþ¾I÷<0H]çˆ ”ŒsÔ¶,¬¥œ-ñó»¼ÏPžŸÆVq™¯^ÃÆUNoHùPʖƩĦ=Ò7óš<Çφ ãݧÝQˆ?‹Ñf2r `½aË2=æÛZK"ã ÈIFž‰,‰+¢”fÚ*L´ð“š6†N½q"ë'§-õùªÍ/‹zµðóÏî¨pBGúþ£Mßš·n3‹èƒdQD:BQÇJ|UawŠ(÷Œ˜ FŽ`ª),sŸBb>¬óÝþ V¹ÝÍ,)ß“í™ÅÚñaI9sôŸˆ7ŸSòˆ¬Ër˜8ƒNè2"ËÞÉ‚§óNvUdì0A¨ænÅ<*€jê>õYÆÄ2gº‚ïØºÊmla\‚˜G¸™Çs{a¾Ši6n·í;óMbýOœ€3ÇÛü{”/‘WƒùNŸ1…ù´`8ïôM+E8ŸŽ'0ê×Y|>Aÿ(à†sY£çYl)’ÅGo‘”ÑÀÖ%–l*Üw›³6°(a¨ª×Î~SÂÛüŒ«[¡›¹]§Ó"É‘ÖàÚf¸Õ592¬Gß²Šžc?æM“ qÔð9 ›ÅîTõÚ˜Öé]¹/çÄWN‡/= æú ‹ý‚ËÝà ~î-67gq“öûO-n³›%²§uŒ€ ÜøcÈjö°ön>F\Tš3ŸòÏ”¤4ˆ¼åLÕO¼ó8¼Ö3WŸöÒÝgñµ+GÏž33ņsK½[Æ¢˜y³´ ,ž?f‡é¶A*ß3Ã× ±9×cö74·{b£¢q`•lÓЉK6È*û) «!=žÉÀ§5¢efíÆ6­¼ÅégpJL> ³D7ZPÊ3”ÎGšiZY°5zêнjãv4ª¶4Û~lTòQR&4Ø…0ÚúOkÑ©6ó{Xš`ú÷17Â&“¿ë'Žã¤/?Õ°‚*–nˆéä…4r&HÇxÜÆöH£æ *YE¤ÇÉM€É8ÞBÊ UvÕN¬9 9€êæèV®dsßèûÝd²õÈJ/K' š„ö¨áq™°…ÒTŽDÿeï$ ¬á­mÛ¶mÛ¶mÛ¶mÛüÖ¶mÛönþä&9U§*ÉÜ n§æ}Ÿî®š®$þ‘{®ßžmõr,-ƒ’ÀP¥ªÆY¹EÜM´§”2ú¸ìïùV-‰{%›¸¢Lùè~׆ÞXÆŸã`&@ç‹aqõ"F×ûèa?W,+¹ÔÿéÝ hhé¤òt?–á¶ £ƒdãº;U½ç™o5Ͱ…%ö$ÃpÏ“¶E©x¼›¹±dã0ùžŒž†¼ØÛ6}1–4ž­ÚÀÚ;ØÝ¾±©gïßÃ<ó/ „ÈxS~9Q½›ðçá K¾{\œ íö í- r.ÆÜS(£’g¢hü –ùtaϹ !ºÄÜ+ÐL}M,HÓ7ÓbŠï3¾œËÞ—¼e*ðû¦pÉ^ñ¯ÄÅù‹­«ü¿k+>Y•^;fîÅ5èÌ7à„HÁ¾+ËŽup‚(…»MæLÍøGÎÄ‚©.ÊžY‹Ë€?ÜÀZÊýt·Hë]7¥ È>ÿT\{î´¬ŠÝ\éâ(í.¾5¥ìþþ)à.ÓN•’Õ Ág¾Í¾ˆ*¬`J=éI“>¿M"kX‰«ìiÞs¾Ô¯yWmypÞË`Ù_ÇŸrjg9˜Ìf?1çb@T]þ°j-ŸÓ®ó”R»_Æù”šbi0$s41)J!Û;âqÇ|Ã:Ä#ˆݢsž44ÊŽÌÑ„k1¼|¢–ÿÂm;·nµ4^ÉIÄ™¼“ág.oPWéøê¾×ãíº%ŸCMy!ôéïÕ•U8ݶ¢îk»ß²Ž»àr¼,ÑzIÛÙѱX »>n©ŸÂWYRHLYÆ?VZ¸bù~P¦ BöÎ~ƒæ _˜ ñy½ Òò{«+`í¯Í6R£ýž¹y¥Çü&Y¹d>40îw(Θ?Ê]— $ûPv“‡‚ Ôǯ¥©× Nƒû’P–”êîi`R«ˆ;Ž¬Ã˜3§–&ߥcÃæHw˜È¿ ‡Û·GT`²Àíœl ™sÏ Yðž³àn×j ’@ïÄÅ&4>×" áô þ'$ µ™£ñ»íÕT*åJ±Ú¨g»j¿j©NK§-§¥Å+y„;ˆù~ Ê>ùH”©O¢S;…4;»8DIÛc\¶'ÿíÌš±ºAæÆ©hÛ~Sôp¥'ξ8gÒ“ÇB¬£>ÿ§±zsWŽ=‹÷ø½‡ûÜ–.¥öÒÛùC5;ZŸãtÁòª,ëÕOxÒ<‹Xþ¾ÍA5 ó8í”ü ®k¼Bž°$hU¡yU éÓŸ&*ž‡0?Î`ÿºs›Åm æ‘x‹\˜»=³ZêQ~bÛP4äåN`ï mV¼Ô@-0e²Sl6à‘‡×’ndǺ½c[_4Ú¸ÚÓ½åëðÜzgC×EBhi˼MfRÌû!}! ‘ ÓbùÕtªU¨2Oã‰:l¦ŒSÌ{¤gBù°[,]HìBDK—ìˆQHœ;G°\ÉVŠ—¼©@C÷¹·°¼'çÉ•ó«D³Åî´pH÷)Cõú±ùºt7 ¬»äeÀNNŸëoID¥Ç¡‹ÓÇÀìy;¸îÁ˜•7+'S™«ž ÑSEvdMýôDôø3øXM|à?Ø‘2|N4ô¤†{c&f;l²cù¥ ³YÓÏÑ…5å<’ºš³úˆn²¿Ü·èW2Y4ÉžHw< ð®Pñ¹Ò#È·i5Bv®ÿ5äW"Ž4À„þ,á͆ƒÓÅ2vpÿþ•Ô²ˆ7çW4ÄWÆÊÂú¼CEb×w^…A;Ví´xi‰ ?¤(¯´À7àI0•NñÄkmæ ­ÿتz!g”j´Dùï‡òRƒwv\' è8 ì@ü.ºðƒ|qvy4Tp]¬½ “ý¶¨ÚØê8 }™kgÓŽ8ºÖ>/Ò;Â6¼Ö"†/CR™˜([jBjJÓFš®ýpßÐ *¥òa¡×$@èáæW-Ú"“çSjÞ¶7>´kdhUÈ¢”¹•’¿«}°oôúö*¤¼mb4t,ÉüªÜ\—Ÿ{ÔMͳºËÉÛ%¯c¹ÕÆ-ÐŽ{{šíêQu€MWLš›MÏRxHv˜Þ~U¤D÷ã¢ßè(n÷ž"íï÷™Ø*ÛȈX½ñtñ™r2€%•ñˆo¶W유dþFyéømiq-N7+JY­ØDomVªA¾{>"{F•JÑÅód¡mNꑸ¤,…އ8¾±‚RJÕ‘–Õ‰_]x»Î*ÅzÅKÓrü*ÐÔ6»–‡ÎƒO•Ç[3¬îÛBÇjZWÙw×TÞ¿w61ò<µŸêêžÎÜly«o6Ø}DJ7ù S|/bH)a€Ù•sY $äÄg‰.tqd’éÞþHÁ&–àÉ›¦M„4–Ù° …U „¹°ÿÞSþ‚kÎ f\S¦'þ_. ¦”»²§_ï¨FkÐtÀã-æOa˜Xy[Ë'Ñö9Ÿ:tÄ({T«×sq^ë'­«Ý®Ò?wÒgéxÓ80xsסsöðÝ?C³2axïì(8!­ ëÒé¯vf7·mæ;·uBäx³ïË4¼q|‚SÙoYiÎZ5 <h´4òû!ª ^ŨÈ^:dèé^FÏZäNå5ÅîóöÌ•}¡µ#6W_&@/Mžò7Èܶ•ž'Á›H•/H•°]¼ù»Þ,zV¨ª3Žš©Áû©Aܧ»&ãîaûfæû›ns0¨6q/&gxÅ­9‚« ¢ä²Y¼$Ãç Eá¸.eI‘[_pÓ3¢Ô¬Ó·pN¦à?-s%9ÑD…ùoYæ”:Ê‘Ê>¸?Ô8~t ·£Š´ö´ßh[³^žÏ ÷ªÁ«Â¾¨»9gÄÕ ›$­|¦s$§¼ØB Î ÜPK +Æ©/hM5W ãÖ.ØB Á=îܳÁ©á”ÿ2Ø Ý·rPˆ V¸èAÓÊDâ½#0%FMËÕqE0w$Ï °¸i-:d@²‡Äž©ðïé¶qøN#Ñþ8Ë]Þì·­(~½ˆ¬)›V‘|¾}±É>·÷I3ÈÓy–•S[ó¤°O/Z¥ªú0ɨ¾–uC২º±} Ö*¡ïtŰ:d܃ܚŒ×QX®[Ýc»ˆ!ûÃñ§Õ†Æ€ò'±ÔS4„M4 îVl)ãî§*o鮾hr¡«ˆì˜î´XÖgxÖf9¥H=»oC¹ßIw7}³-ç{؛ؤ%›Ùÿ>\ž±C:¾hËëÞÀrªÛݹ¶¹”ßæ÷k%¸Á_»Êan°•_‰ÏvÌ‘*CkÊÝá¶Y5øZ(«hØ•½6Íåüž]~ÃÜM¦sªƒ–ã¦vÙ¸‚—…/²ß´ ­õ‡£Ícˆž¬yÔ žÞDUÄ«¥A—ªvS˜’}Ï^rÍ&Î]VRp5âÝgÈvלóÚ–ú±nj»uoµ]“7ïôø§h¬’ã#&S}%ñP4Ž,6<éÛäÞÔ£½YÙkÍáìfQ¨—{»é·¯›7úYü¿•~&ÙeE‰•ç8Ð:»šáÖÎíJ=T.µþÐT¥bë „ÒRíÒ2$úA}ߺ֖¨˜ûqà¸,ùÐ /AÔ&¬Ý¨ˆᢺÐÂ2ñoMší•™sXs¶H.`èLY¤‰£Dšb…Útê'TÌUˆ¦ƒŠV ‹òå šÚMv6´ãTq1v²2þµÕ!•±ì«ØÁÆòÄ*EHÿz¿¦¸ë¸ÔùŸB»!S½É‡¦yÙ-½ú51hæGÓµKji EJàd߯¨ºkò«ÏXIÊ#±HËý̦ðümÛ¶C«G@,®(Záâ%Š›P-Í)³–iM:Ö,6:d˜€,~Åei]Ôo î­ã-û˜áw¡}ŒL{ë3³sð3YªRªÖ4³P}½*‹R*@P²/3ØÐÖlp¿¼ÚÔ£­Oäk˜¢…e«t!ü§p%é*• zÉÑ:…Zärþø‡_üQ( ‡Š(ÍÏ@¦^—ÒŠª8ÕÆ÷ݱ4tfË@2tÌCu©xðBºJ®¿*Ý?–Rhµ—ãA˜)åJc½¸DhÅX©/9&upàìåvZ\¬Ò&t6àòˆ{ªúuß3«…!HÝ,Dêõ¯øà?!ÝâþFޑ膟dú âØ'Æ} ŸFóÇÃLlýûÆ~(¶ 1ÇêàtoJÞ‚¾‚{¦y/ÀâßÈo8­DH7–—kÇ<þ,÷Ø8Œ§Bån;5ëH4²ëa؈b…]óäž{ "èGÉk#jiØh˜sþ~^i®p›l-óñ'k4“Ê­ äköO`³‘Æpåàyï9õ¬]%Ìîã¿×cßäE-q.$}ÒYPÚ¾ÒùÞù‚Î̆’?|ù÷z:zg€šÇ½’wxÅ“f„WÆ,çö ÆÞ]nW¼)›k§×Á9­‰_Y A¯ÑK Áá1Џ´Jjìì´~®ë²¶X°›¶èSQŸ;ئ €zñäYˆœú¥\L¢Ð¹Û_ûmÍY”§m»¼¶jLl»…ëÂW||–Ø’î!Þloø¹*ýe„xî‹úÍ!‹p”ðåÓí­Õd´iÁ‰¾wôTͬŽnܳ{OPdbâ«$³‰@œrºz=௾SO”Ÿ…€0áí"‚|åÐíd&²Ÿå\œš¥ pY¸Üæó'¿Eq&® á·ëFƒž™;—F´Äü¬ð6/«ï*þqÕ¿ÙÛŠ\º§3Qñr{Ýâü“•¡62:\6ö8“¤:0öÍ*è›JšŠ›ÁN)U¾IÕ™äj{~kûRâ)ë)pV­ŒI}ÄÄBCÕÄ'×î<9ìýs9£Žêaö$)):_º¾ï0Uz{3Þ“Þ„aÇLï”×AŒÅÁ8 ¦ ˆÊ!Hˆéô¶cqKk»:CvD0"O)fùAUõVÂu35ì¢Ö"¹­çó›(L+|[ÁõcÛçYžõ2îúž­¯ ý8lTC´³¨v)ØZ¾‡M‘g¯‡“FçòhzýÑ-žjä´×„s“I¼—o@Ù@iŽ~ðP³b¿k'®f}ÇËëk‘‡k“‡Ò5Åx4z4€l¤{á8 /Ü[·é8 ŸkW/;’J Y).½’/~~FVV7³";£Çéç)Zé…´Z6õºÍPVE—ÞÜ?x ±F ò¥6™ó±„¥êŽ+‹Ý •"¯Ú`ÈôÉi­ÆV¾Èì ZÌuû 'Êä'ÎñÍÏ«y£üÕ#ê?gœ³Áp'ï ±“Þ[Ôù®&L/ b)svÍPÀm`ÿ(á@‡_ÅêÃ+¯÷qÜqËInÛA|Pz;ãVí ›P=0À™þ‹iZP;Ý åhY*ñy!p¼ˆ¹üÜœn³vÍ‘”·?)NôvÚSÛìF(ÌÅkàL¾‚ôŠ=g‡ÍÔÎÕˆÝ{Ûh%¬~'qÉŠºÓí3XÕ̯†ÎÆ›]o¤bÊ_<7eiÎ혫Þ6ÈÅbÇ‚%N’ëͳƒurür[l6tS}˜VÓuÔu îÉáJ¼ÅÈ!éxj2ϨÅâãœtÝýiM¶òàv…<›››+ƒycV–Z/8Ô/Šõ ìçfU£À‘ +õ8H”dYëýkˆÁå°úY «êУ*=š­ÙöŠæ PˆKªêü‚«ó¤Äb@F®œŒ­Ÿ"Ú òFžª76ñäÓ?|“véÝ›Ïí(„P=l‰*HylTíŒî3Àk¸ïÙí¶˜ß¬X«>MÓPC0Ù %ë$ñ(¸Ž ê6ŸÎƒ^Ù |äåʃl‡§Nºu;1’+ˆÏH× ¡3úg{£M)ð jô i_\vÝ‹SCÜÎ*YufZI>Ïuˆ€|„U]ª•t® ÖuHÏç}€¿Å—‡ž£1ƒÉ½™L–JþŽ\C¯üg,¤7õ—EÒ·¥û½6¨)Ê^¨Ú¡¼VN@…MÀBöà(½Ñ¼†aÚfš‡¾’~ÈøÓ°Ee¢ƒK¤-«1±v[Þä•„@Çú*ßKß%’–׿"Öù`ú´,ª´ÙQNUƒãKò¸Žd3ñ$Ÿ6Ú:•=ÅÉó7ö*™úµkéH¼Ï'ó„”—•¿“Év\ßJ„;TJÕŠýÜBÕÖ¥mKi¬›‹­˜¬¯êÈ5Ï<8óöI ý ÀvüÈn! ®¤Õ‘GáUñ5ü}^Ç!‘oqü¬˜ƒûÍ¥Æ^òTÙTl4ë$Æ3b’f ?º…±’Ž‹UÞÕO‡­D(·;¡„WÌñ«¹§!`Ž–ïjÑ¥hrÆóËÝ?Q—”66ƒï©ðIkW¦Vu¶˜Öß ÉÁ7ì© ´àšk' }MõØþ†Çw1ÛkS± X?¥ndÝï »0I>Éú㡬\[4|ÇZxnQ|âhø'ûpÊ”GÛ¿‰ÍñŸ #øà‹#ß‹ØèPv«F¶“ß^}û¯oЋx ñ {tçŸ/rò]¬™sä•A»†¾Ž4—v¸®£ÿKêGvˆM®™Â­…$âÈm£½gþíÿ ‚È|ø¥<µ 9‰Xð[‰Sy…« Çu ù¥‡ÏlÑhÁR¯¼dWº<*o¼%E $q‰›k©W\—¸Â€=Ájøe½;^MTÛø°ê‘’•ê†r0:¢¿^0åÒþ¡ãîÚêE§§½o×2ÙÞ]2ìR­©kݪ„X¹v¿èÉÄ¥Íã¾|ÉšþªT™-¼­QÀv©W^_o‰kdñ9&Ù¿ù,ød;ª J•ˆ÷(ZÕä½å JUo®Ú©a Ÿßø“¾žßG ´àòGñ}]âÿŽå|v¨éÕ©[Õžw€ jÃVbþD÷3ÚßÜ~/DÛ¶$5w€x_0‘ÿäÏ|–¦b3'…å´-!†ÙŽb¼BDü›UT+Mé‹–ýõp÷Ö'`eTè›;©½¬"ç#^à=ø> ÐX’žñívÔ³Úß)ó26t²….ýþH-%•ÍDम‡Þ¦©·b¡c‚Æ”_°eA6iílh­À¯£`-ðÊ@0mþ 2äÀQäõÐgÀ¹ž8ÙžMn¾Aõ„½ÅýbΜ..|ìºàÿ!w÷_GÏ­!CtT’ïkyê«—åN”wï‡IZÃ,§K0ÀœŮݕ]^²E¢E™eŒ”¸ìàÙ:{$ñ!KiÞêúZ¢˜áĤ0ã±m;ã0í°P¢Ã½Џ^&®œÆUd±sº«˜¼sAüs› (  oÇvëøvQ“]Õ.GÌåkp=¶k›é8' ³ªúÀàußNŸxmÆ@Pz äþ‰ïòÌïßT«Á›Oxu0ž ÎbŒÜ«zƒj õF)hL“¬ÚtBà R\êšì£Á²\[Ê+YþȪL_àOn¬kù‚¢P?­žq®Tå]ø^ °þŽÒ?ʇ¸Æ*„]Ϻ½–- úóÐwºÔá¦úzôå_=AŽÄmxqýÔ¨{fúNd´ÝϦ٧L÷i%ÿ*SîÝŒ”Vo?²ÍÜÚ›/\~ߊ_¯ò>n=¿×“¾, E/ƒ 1Gˆž@âj‘øáhh¸ÚXfhãÜ U5Ý:6 b4 cÐTº0\’‹Ì#•ÈÁ‹CÓ- PÙ«-ëI‡¤0LvÑ­ìŸ\)ÌYÒSþ¡)™Ní}˜Ñ’‰]}nM½\9ãW ½úq¶°®Y&Pí`«Ê°Pw‡}ŽØIR–dçBa>&ÄøÊ{êÁ·¸ÖÄ‘ Yí4‘ÓȬHÌQfÜÌ÷~~sÊ6Õ}aðBò¦ ØIé¡ß¶zvVØ}jÖU£ @IgŠÐš "ñèy¶›áfŠ@z™¹¤p¡Ú)£då>HTм€¼SÞ+¢þ¬_±½4ÀÎP›lUÛoQl^•±¡M#ô×cÐ'5v¶ê7e–oÑÈ#´‹@£NeÆàd©Ž_—æÃàÖ]¦„|ß=KeÃÄò4„{̇ãP]ÄOúîï"š Ÿ S08»³Ý˜©ª™€Èe©š _E*Œ<k’Ê­£‡T–å‘êWÇßúŒ­SEjsõ¶@¬B_øû4X='´2‘ôŸÂ.V¶ç»UYxÜníÐò¢UÅIîm3‡Ì“gaGÍ&DZ:¯›EØþu3j ½À㥀þ÷Âòn^²µwùGª…nɰN)À/s¶ëW8”¹˜t@R0e|Çt×C{æ]7ŠH›ÝòÙmí~{6㬆¹ônÓ-^=•Úå@ðÙíØ ³Wwi¾$o•©ý™KQÚå#åžsÕ ¦|^<Y©rXFÀjWú©B›ÛqGŠòcÍ+ª¿Rc_–f¦Zت4Š\’Dþy¡ÝÕæ ôëÿÏç\5¿€Å ̤´6Ó WSZ}œDÓ /üFH®O½¨ /«’÷¼=)Å´‡M³b†Ó_®8¡~£ƒúÎѰBLöQÇaubæÖ¡–ùÀwC‡ÂáEýšÅ%s•|“!# Æ®J1»4`w׫½•n9µ¡[Íâó8ôÜHÒìŒú_£(jú¿ó«Ã$–ZÓ ÓéË5ÍÑrÖ‹ÿæÆ,Ò?W*[äâº,~«#v•Ò­a‹Dç=+ùM´{ˆGJŸ~·ôRµ ö•ͻ﮹­"&a™ 2…Oy[¯ñ9ª´\D ÒƒÙËÌ v[Mmõ¬J”#³ähõ2º4)WÎÔý·÷’ê0r9ž>ƒê!Ã47H&ذ2™½qM÷SÁxyoÞaÑRIÜy/RHäŸZÖ¹ýùóé5gKlC*~Ø죵‚À@ª"†Ó¯Íß}eµF›Ù7Cû·†-}w5„7©»Ë½ '·ý1η¤e e{ÖSš×f@¡|‡\) sàÞÇY#ó?Ï]=môïØÝðÌ%Jmñ@²»eœ>>¸ûzé âÖ/TŽ“ Àãö¥mBý‹V\|òö“%“ÿjêU¢`Ëtµæ!å’Xº,sR‰Tò…Ì’Uó{nÖÉÆiØdpð>/!±¾Ÿ_µYp“­¡›7å\b¼®ª"AØð72دN×»¶Uziµ²›Ûªô¬¿ášÇ–®\6ý3þÍ÷Al—­ÛËXÛï¦騑ƒEž†nÕVû¢“lûÅÏ«‚c” ÒÓ§žqÁ&iÛq íÔîÚSAÒ½A·OæÅj›ÎÎ^jwTÇq· ºNh×AuSÐ{Blg¢„¹Ë†¡q-/Þ$¶zÍà4©:ÛÞèšl+ÄÅî‡agÖ‰6œŸö8~ÉŠ#™~D‹jZwœ|ûc0úõÙÙ¤ÂˬÎJê—iÚ~º|?1ÚÀï~m%Å<Ê%ËçOÕyû.ÿ|âUôÉŸä¸Î•®îݳ¾Áº×1ë¤u‰þ®ú~ÃHˆ›ßcfµz‰_3g_£:w=ÿº¬OØ=žÿøÅâ(]Pë_Ž_ýùÇàH<ëû×Ú`™ücÃdù©¸ã‚ùˆÖ?Ìaßr«Qy=Ѩ +ë]žëÐk§ÿZë_~Øìé=Ýßé·l¼ÚI@H%Ÿ '£”áЬ}»’=­ß»’¾ UTwÄ_ê™(V2½ò!×¥å–g»] 74îÝx0´­¼ÒgÁÅ<°d¿@Þí:i’ÑfýSŒV¶- ì¿ÖžæÚà‹ñ‘r ·Ë´2­W& Û§·ZY×Ãí «HXÁmGfHØ@7iÖÓ¸ú˜ ¸ªÎT\µ|Ñp·û‡…âúe}³ÜÔ¸bR‡/ùè§I`>{ùåíXæ6iÝ¡ÖòÙÞà“qd÷¬Ï}m”Wqo;Á[Øn&ÔÖf}¿—ì2bóM&P5DiM÷®EÝ,Á- À¬pàp(]Lݱ6#|k«p]älËY ,·ç õeÝÚ!ø ðs|.à¿¡k›b}Ù©ŸÓí‚×i³$÷Ä–{SÖÍÅé’õ\a˜Š™eæ±±ò¨£®2þB_ä7¼r»Òàoúvñgz€¾bösìxpFâ¦lÍvZ-UñOü¼—Õñà7±o×gÞŒù“îc9;þ¤¹›ûÉ×ñœòüËË™÷×çãίðïÇ÷oø#òîÇÍGà÷n íGÁÝÐǯ_{yí{ ý_ÀsÒŒ9ׯ 6Ç2{ ä8ä,æ¾Á/©që¿ \3ÌF~é0“´º‡fÍ„¶ ºóu3çyê97aw¶-à— šä‡¹W!k¶.ØÔ}oTÝÃ>þd`» «°#åŒ}ø£{ 8üƒ‘y°®ÒÚ˜‘1Ÿf¤=©g-«M‰‚/RoÀð.W¬XN—J\›Ÿ,#½¾- h®?‘9®§µ%濸lŸ‡_ÇAÒ~ÉôÚñjè@ä:P¹Žƒ¨ýè dÛy³Ãxèám”8Ç—ijzIÜŽ`µæ®¹ÃUò@º%W›È2UmêƒÊÍòÛÃ;Ö,¡V—# å’*nZ›(ÀŸŸ%_W†ÚûäÆ:¼½;˜HƒFƒÛ숃ÅÌùæWq¶ÄÂ]Ó„ü&˜ETÄö²ü¾5ðÍÑȱY/M7šRY\Óêü½·ñ>ºŸßãt³†6>Þ“tÛeý?ÿOi Öd«Eÿ‹)¶cúŸßítÙÉoSìGzé¾¾{ÒÁfYÂÿvOõ’}}.ô¾SymŒ[\j…—šJÆåØ8GÍzn^¼¢ó,Íÿ½š¯>¾býÑû©·Åºœõcæ¤2RåxÁJ†å=¨…n<¯‰lýL“?îx–3îoùXûÃ2µTéÇ >$¶ 3q©“¦þ*mnÂ>ræû>ÒÍ4C±ÐŸXuôö2ûúõ˜(¶ÐþtçTüD¿?Û¦I­):Ã#ŠüaƒåSÇw×Id€tw¤mHš7|N«?#½0‚oröÜâ¶aggF'çýâ]dü™>™Î_9°kmçsÃw˜ÈìN´5¥Äþ'ÉÏV±³Rûq[µ‚§vF\;Pkí} 4)z¬·ø}e: ‡ØQš˜â ;¿;„Ö榜vMÐiHAx²†Žz;¤Õ~쀹’/mgÙ÷^Ôd΃ó¶{wÌo r úÃñÇL‘U–Ç`»“ïÄjTe~·òŽb¾¯oGg}ØÌwDÝŒXCÁŒ+®.Ô6+ܸ˜¢ò=lPèòb@ö+qüo_é—ÐÐf}Ö¸½_¶Ôµ“JÎ(÷!Ìt\Ù-bS…]üÌ7Ÿœ^o_ïÅ„$ —Qªf£;½>…Šúšjèê(¢µì,߉]üA2¸ý^½üï «ÂîþB̬ƒ…tƒ$Ìȃ;v¡ÝU™+L¤.‹‘Êyåñc=i)‰=“vbÊ—n"ªuîš'¨rd¬µcw~ô<§WÎ/¥ `Rà´ê=dF‡úhîåWà ®×º¦”½º‚/ à u)]à;í àˆevGna¤s…Þ\5ªCD‰lmOÉ•zDÿ¾eRFz5ç¢qìô•dL;œfVªg‘¥Ô²'u÷v|s{Î¥@÷&ÝÈz\JíVïR;aAc%ÑÀYJ‡%9‚ß›æÁLÔ³  Èéu¡ õz.ãwBzç%Öö‘3¦“ £ˆ0ªÑ…ð³$†Œìo‘ùá(4;‡1 é æ$BL”~;ô.¸tâI%y&Mµ‘~²¦{ÔwJVâ…Òú«§)EŒn6W`I£žH»G­ ‡ðÃ'Œ}÷‚Õnüu áI¦A²“9‰+Ï¿â¬Ú¥:Ù’k]¥œCR‰ªe'ä%äØ|áöq²ååLH`)Ò¦ZòUÑ8}ƒ‡*´œTLw~…ÜÕÜÏŠ‘wH|z7<¯Ä†˜ìzñ¶ Áš%H ßlØ/XV%;ð£c Vá˜5’\JÅÐÁL+ŠÓލ‰([ú~$Ô© dZUñúœ,#9Içqb_Ž?›†äEwª¶£{žY:n°¥°Á½ß„&7öÌ ø‡ø|WAË•CÈYJkìUOte~>‚ "ñ©ƒØð02*̽vHð•L fÿmǤíÕüð5‘ Ã?}æñreÒ `aÞüªÁ:*ŒæÁÿOgü©bL¯}1Õ f–`ª;ÝÜaeh0A b0£{MÞþ)¼C„OàOÔ™Ê# ìY0Nθrï®úr%)1'4…é%X>à¬ªÜ @(D¿_xï†ÒPy¼]•údÜNµÿˆ¿Âþœ“ÒˆT¿óBãÃa*]ÄÆ-´j—OiæZÍEŒÜß#Ø¢…‰í@ÜÄ |éX…«Äº gD÷}QâsíTV‰ñÓ’á<õů2ÞDǸA8AxWÇ¥øl(¿Ð5Æà»€œµúQTUƒ3*ϺççÌ ?«ü‹pÂþÜ…¢NfWÒñh"žÉÊrÄU½´ýÀ¬.K|nºGy©1†ëž%Õ“`çáÅ;Ãè¾ó}ršª'4 ¬Jé<26& Jõ×%:XÞU…γúÐ ´°ˆFNäFÊŸuqàòr7“ZôOAÖQõ…axÒ…J#C·Ž‹”V‰ñŸ¯–Ö’Îü”àx#9l°U™6phùΓk”.¬%½d'>½s”£|;h‚í U:Ù¼2Xp.çTt¦ÈFÖ©5!XgŽŸä"ýGÑÊH*²Î@U² ï6ä¸íæ ]r¼½ºX€·jÁz+Œõ†óËÇáT¶ÎòÊÙ½}J@õ\}ß³®@A-}nÔÐçþÐdCðd‚¨ÿɽÉòuö¶ÆäñCÃÊ+÷añ~OØá,EPg½`] MˆÖlr?ÞU)èiÅ*}äï”à…òñäWoâÊ#'+Ò}î3@:é —DJÿƒ‡Ûª†ð_¡öâÇwé4‹C¬ù÷ê÷Û—–¬’pµé}DN®±!?¸äÙå¯f(Ê`4ûÃÉhé\CŠÜO…Ô|ýÈõÇ‘ñQ¢–öÍXˆÎÁ>|<ÿ¯$ðJ~Mð>…c!hM¯aÃü@´ 9å:z T”±€^âTt´ÝìÁ?Í|óÕƒ““pÚSÚ¶¬y²hbAé½`;Ò»gèhy¿+¡ÁC÷ç;¬ú$•YÄ¥¦aµÿXìO!kµ°“Ûéï„&£‹¨ó*•¹IxÐÑ9Ž~†w‹Lsù€7íbop—u$ìZ²o‰‰07Ý|\Ù°“B³5³¢>?K_œeØÖ‡åõ3jT"jꀘ'Âf¼E'‘¸WèþÒ¥ö匒xkþá゜'4’XBðóëäh¥öºëƒ;W™ª>Ž.m(Ötÿ|ÿž¾¥âNT[ýÊ€·è/¿?MS ÿõ•ÇÎg¤ñGG”9’1?]ç[¹ÂÔ¯SÃ@Õ8›œ[¸óH2©ôHæÈMº0UPÊEG°oèfÿwE|ÖÖAÖ<„ׯ¯p)ɵ»7x¡1[™—Îý\é ¾õ1aà}K7èÍÍüÈ ˜k·e=ùÈ]Q =Àª]aõ{Á¿(íËòÇÄ (}Åñ/dkÛÎE¡Ý•yŠ›ÌÚL1ç…ƒ>1éÁ³U×zÿ̘t±-üDjPäÌ¿$WшÊí Ä`¶¼4E¥Ûí#TÙùsb)ñl^ R£Å°jU!ÎaÃ×|¹müŠV™4g'ʶñg·¬AÌèiLHKƒÂ*"@€ŸR6t¬ -»ÊûSoïÖ·b •*WMÁ´A/-1où¹<ó½EAËê<– õÚï™ ? «íO%8þˆPz”¡CÛÆº– †öû‚¢É¢hDÝÏal(þ…çDèËgÛ¦$wГ~ÖX­¤Vlþ6«l YnRCÅÃE¬Ê‹z§•ÕM…Ñß´¸š™Y5¢fv®*Û6dw¸lO~Ï4t`¶Hš~ÚáĬP·*Ÿ\Ï0PE†‡¡ÖI"%{úXð3ÿ“f‰•_!Ju«ØšN [môkÒH€£ÆÅB6OF*”UÆëÚ¯)Á5§îÂëI^O!ù¼i²«È6È,øìÙ6­¸ 6”½eåf¹Ô¯‹™¯iì¦é"^ÈoŠ¢ÂÍ €´¨Ê[Y{±C4¯Á•ƪ-™½=šÞ¦rqJrV4TúÖ6T¨öùKô‡"².~&mœUÜr£D*Fnž\PÁ`ÔpH—Z0Z&ýyG—õu&­ZþÚ. (Þé·£µVTÛP7 ±õŠöøQûêM×ô¾öwùê%Ù¶zŠyV7C6kÒ.žt:žê…jq_iGð¦?rÕB(ö¿f/nB[~ovÍZû³ä‡Ø&|\½“›+Î/r9jè6;ÊÁd¿«!aq®W5¹-ÅZëøkpWìVÆåÇb^8 Ž#GXÅ—ÈÊt³Ž†~Ú6%–mëT¨Ó’ÅFq‰¼åÍÁêÊÔ;­~ Õkú÷¶`Ö„|vÁð¨2#WÚf›së«Ë´šÐ?›ž[Û0 AD kUAí–Y¶Ö‘~¼–xÑk¡:¸yp B±(®çÊ8Ïý7±DÎU[U¸-Ì1 /5ˆmâWE©ÜZ™z•e•놔 ÛAdWˆ_x<&a¥Ô½2fYÌTæE>‡ÁÚ]5ÿØ@½3ð{mÔaŠÞ^ _‚eeщنÚá}šR¹hÓHøïÄ@䦡 3R#¤ð„Ó+ÈÁ[‚­nÔLÔ´P—&UnW¢ §ô¤7NŽÝ/­½ÌU=ËÏÓR"Ún%b·<@EsëYæ[žpúL/–Ü|¶…V}KC+¬ûœ‡jtEZÿø·1ÕA|jÐ<”§µçFº¿ ]L €E;_¯xÁÇ*Š-ÇTNÏ$Ê+|¥œÕ6”ØN¦x Êo0–ªÕNÔü€V7Á©®0i ‹áÖXÐfê"(d»a ¸‚ ½ÒVKF¼Åå6´ä×ôN¡?§Ÿ©vR*Â6zvýÿgÃ!‚V5±$k¼ÿ·èĸGKÖGRÍOõ‡ØµëÅx"é¦qLù¨qŒÉXúhÇÐmfÊ‚&–´ÁT9i­¶ŒÂ½Tk¥êö²r²­Ó2]`È-ˆ b2Ûáh0u_À€€a D°[ñ~wjúÒJsCm3‘»üQUí·ÛçΛ?3?Fåïa¼oàGПþ§aŸš(ÀëÕFp‡iŽmÙöI'@ Q‹´"¦t:…°s’A™j¾J¨gR»Hy¾N° m!VÀ岸J/ÄfóH_ýI¤¼ðÒB^ÈiÓ¼ÀO¬‡Z˜ôƲB¾úAļÐîòLÅ£èiÞô²¾Â ´z¾â%ºJ/ô\ÕB?è“T•tÁzÞ­à¶Â ¶h­Ðó2]Õ¤ˆ½ÀôR]Å¥¨zÞùÒ^è k¹žòC„y¾îåJ?|.1pÀaÍ“S0Å7À~e/È`QaÎdAóÒE¬r@S˜7]ќþ¢Þd™‚ãr$æ“ ¤>ˆclYóÍvisÚºãïàoß¿š™„€¼¡Ã—<Ò{gËÑœØæI4Ò‹=¯GzM”dSˆŒûR5º”$2™õBšñ?¥ÖxÜ,/"î܇vGO«pë£nØb¸G$!/Â&_,¾ »¬¥OõÚ¶¤YeG>Ÿò#iZL$Ú¸Ç.â÷R– —ófŠA‰åÒCó#a_yda‘sYjÀ-Ñ#†rdº±ü)Êf²N\»³B‰NýêîÞrŒS{ÆBËöûwq 2$ÇœÛ8c‚vKS[ç] ý–æÑ¿¯›±§ã´¦v[ÿ îCÄ7(6/ÃZ9a«4Ch5¥Ý6ºWûzæBÒÁžEÎü¬Ù'qÖè±]lp+‰PæÄÅÝs“½¥ï¿æý–˹`3û‘ÿŠ4 Òi“aýél»rdã..gåg‘Ô^)Ö)§»Å#ÜÄ¥“8ÚSBˆW¶¾ÐÅW² ‡Ä’‚¡Qƒ *âEÔ5Š”ü ‰¦ þÇìãh4„!>*‡>æt\æ`rÈÞ&²Ï’E=+–Ù²T¬ÉHÿ³AåÞF„Ë/e\ÞH‰¥]ùd£l€éš16†oç)\R)‘Û0ôÃÌ_WúÏŸäcQ’ýH…#WƒK>äÓ#­Ë^ëQ´ÙÔ¾Š6íúptc¾IÉäpÒqæËàFÃj£:éµ#›P+͸2A¥„×$@Ú›0¼ñ¾Á}è©#H…I¤–P´8\@dñîJžª(Ö8<Ýå¾úgæLuézÊlÅÚj…6ÚÄu.è&»Ázàð¨º!üªÝSq¿iÜ4rŠé}àÓ ]kè™ ö›Á8Ék‘Š˜0 dn,? ¤Ý„cY ŒåÈ$ÜsSas’~PŽêGN˜A;ÿSQ¾Ã %p ›|ß@ñ¡Åã¬ó ÖëœU§4 £ ðj=%^íÊ´éî…cÚ¦~ƒxûU¦MgîêÝ6h@¿  ¬'¢JXö0ììa¶Ó&`¨QDòDö2œC.µVJ0¯¸Q„ªda¿Ëñ ïÍ/ÿ—bü^]œX˜ÉoÌPNðQö‚?R¡pËo=ñ]¸Á£iȾ¸cE’ëÊg¸ 1Ð+¦Ëoè½R`€WãÛ†ØîèÐ|å­ÙýÃ.è0«i#ÛÔ¹›(_á´C!8@Rr(kÆ=*ÊZå¾.¿4è5¤5ÂôÝÑð€(¿Þ‚%ÂÓÔŒ[R©8cb”†°òៗ·¼Qo ûç“*L—Ј*Õît‡Ð?!íGæÌ:Mu2MõñMÔGÍ›ÆpLj¬î$õä9pVPÔ´§âèdï0ÇÅ’ž—£ä’.p’H U¦è 0[èÍp4fÀµ€UQuÛ:4™Ö~U66 mTB_˜®ÐmgE~ò½baLr›€ç ëÔñ(£‚@¯05(☤8;m\¸øU³0MÈêšÊk±2!XÄXð (¬är¦ä:8–ܿ뛞7™¥9»›ž1Ù3þ¯É<¶aÝ4ŠûœBóôçœdLš¼øT14”PV(ÝÂ6z]6MhžSÊ.©³I³·ukcy†\ßÎßcWšváêLUL Ø¹ì±êŠ7Ø€‚™æÑó¹^#YMæÁR^J_Ò?ÎËðQc[ߟ‚¶×!b¡ö^¾åaÝpzfGŠòg"U³öØ4¿”Zbè‘0ðdkÏ:\VúòÄüõhŸå‘ô pI"yÚn¦™d¼0•ÃR°>*ùí ©0‚§9âi©ÁÁªCVIØ)øZJ¶🞠–ì>ÏåN8Ñá´Z*Ò©Mê#˜ÆŠR%0·©mþÖÖ}ã]Ã)+)FqÀeœ¨={ „v‹®=}wüº]= }-—Õ;nÙÔ•ÒÖg B¶‘‚ƒ7¶¤YØÓêˆ[o°üïåáÕå\5žWÜŸòõ”…0vsáñQ²·¨¼ÝÂOùâi˜B¶…Ê«)Ù4‰ÆØÇb‹ ܲ](ŒS¢GJ'ÉÚNÕõˆBÔpÜ·s¦©»µä¼¨å´6좩Nx"þ§ä8ébŠ~{íÊA1ŒÊPSõ’G@ž¤îwIiw{ŽÎ¢ô«—Ρ!HæíQrÉa¼¹Ž~ƒ]ŸpÔJƒÔZÈïXMâwvCݱÚ5„aëUÍS5¿qŠuÿ€¤œÂt±WŽ€’vP´Ãò—¨Óä¢KM2m¢˜’*ô)®&¡&©+„oOÿV&Äp5E#™&‰²ýÄÖ·š?ˆçẉ*¥X,56`ØÄr[F+…y1ÒÓ.7±AÄ`±d-b¤ðOsõ}i"EÙ×á5oÃý†çà»P>Á)é(W.'mÃAcùºZß2v*°ga6\H“HîRø¦ˆk¤.YÎsð%mB÷G°«T'…nýD@§x>f'׿tÑD{þbT›Jã–ÚczÔ°Ü·M½^`Ÿ/™­QÃ÷=ýC‚þ2óˆdC·@ ³Ø3— ý„“vÖè³dVÙ!ãÕ7š=¤~dšâé•{M“åþ™V¢â ‰)øø vÛM«vìJv2B¬!”§!.â5 ·Ó|‹ð!ö⌿5« ­þ)Ô‹8èñ+ì2k¼8Å)™L"Hr³â‘…‡¢åÿ’v¯*Vr:eÃÐ,ÜÑTPêIçÊÎ.Ìr]½SÁfãò; .w–)µs¯Qè®9æ)ªü©pȃ¨­$‡8ºÁ‡ö{¢MÊæ=êãês¡ÌI“îï–¼Œ';tŸ|Y_íwÁßÕÔxMIC»¯¥÷€KßNþˆÞP¡²C9„T› Â5ò1¯PcqP%š_§Àð7äT‹XuNžÆÒ¹ñ¯½¥a‰yWkàäTuý,þË«õ^¢ˆ=,H¯biŸx¡÷ÙÕ§„†öüÒÛÝYƒSK»9DÜŽ5NÕÕÚr'tn-fR­é}ߘ›fˆÉÀõç©ï»Ú KˆÎÑP§IÚðW#Ö<‘Ÿ²G¸—ñåøj“Ñ[æÃÞ«5l#´¡v OíÜ#6w{E'øÑôTV_F)TGÑN.!^Wîw4¾ŠÁ´‹úé1ôjWíÒÝi·ÎÚ©ãtéŠÂHR?‡Þ&:¡H¥î1ÈqþqæfãÓJ…æÜñ¬]â«CøBH6÷£JBi]; 9xÁJÁuß+Ëí(‘•ÃJÒ»PÇî‰ø§b04/³ xÖ·ˆð䑃å`2WaõZê4Ã4aü¬NβQ)+g¶a)µ8*éÈwPN”ž¸m œi“|ÁjÌ¿H6„ƒI©Ö‘]€|KU¤–§K|`€îÀGAZÕàmõ”ˆ_*Ôe)oD’P6k~>‡VI䬤ŽVjô í ·ÿºØ&â0Cÿä:O ÷f~a|ˆPž÷Áa®)kÕÅSŒ¶rN Éľ¹ ³ÎÈmWU>øSi ÐÞ'üã“ XÐÖb»ÎÚe»EüŠ’vô^óKõžÝ$¶°*wBa*MxjÈÖŽoE•EnYîûIçÈUÏc(»ž#JHܽ·ˆŸýñsD1ù*wê=ƒ¹ã¸ô®`=å7Ðð™±ËÏң칤ͱüü ‹Æ=6ؤß2ñ\ÕbÍ4UocÖjÙÊ,]Ceƒ8ßj©zÎV›ÒAQáæ£.’<¿Oµä%{¤Ì˶ar¼`‰cZÌ1*æ˜C{hi!ëð8tl;Á’.[,¥ lQýH¦®¨ߪÓ.“&}¡Û. º!ë Õ”f'â=fù«êk`¾<ú“3‰J‰šÂîì]  MŠ;Y ó  )K¯úþ§j ×JÕ,°Ìy[—¯¨í?¾WvøøG_‘?…u_ÎT‘ß2÷ÛWð£^±ÎOhAîòÞ|V¸Cj1—°Í&AGÐIaÁ ¾Q²]WëÜÅ5¡MFXŽSœ³(*’ŠWERûìUG» [|(‰ÓM´Ø·ÞÁÈͰûÙÔÕÒ0þ¦÷‘á8M²7{0tîæœ`lni¾%“êÆ¤ðaœº}#wk‘@šî<é(îàaLWPA¸:_Wù/ဴgVò$äô ÃçÞy, ÇÞ /ÇÞ^b¹9—\Éáà/«qÓ+oå@¡Që€a–¹³&Ù"fÕ!á*Èo©˜+8B]ôÌOΈÐgŸðè¶/˜›Tm ÎðÕcGÎÍØ%ê’¤$ðóg€pz¶Œ=G¶>-èôXòÞVâ©–:½Ù“°¯ AŸA§åI} Ü?7ê.»rïËä_&²>L¨ofë§¥_a‘0¥t'Œš¼ð&!ËòAoLE t¸”_ÆšØN¦‘ˆ`õ¬ý ¨pªó3èlæ ,#‰Ž ‰X>¡¨–hƒsŸ¸ÎçaÈ»èÏo;"}ìæÑjË!bS÷O©xUÚ?ÒŠf¢49ôzÿñ/£9¬t›|ô¹Þ…¡Ö=VÎx³È¤ƒ¤'"oÒ\r˜8òvkÈçæc›yÍD© âNÖQ솓._/|7½àççÃ#Çuꥥ+–öŸcùƒD®Ð,ƒü;þ…3l h¯¼ãã1ÌuÜÉ|¡ÍŒœ ÕN¥¯÷ä=üëCòZôг®ó|ƒ²ã† èÔfñœ{¥’mIÎq6ß|Z·óÜŽDW"‚žÄ8$æÞ¨cÇmß9O4±Äd»ÐËÞ¤¸¡\(g3ér-ü^Ÿ½â¸UòŠÙ˜nÙrvÿ2 †j+ÉÊÛÃßÁ½ër’Ž"C2³g½]1ºñ½Î ÊãËu£<œÏ s°ÊY¾´U˜§À’Ñ«y´øŠŠY̰ ^¼pc"l7úÐÇÔRø¥<øõ'3:¥–íó¥©[™}Ÿ}6ÿ^©žîéŠËšJ ‰u„Ù q߇]]bj©¸F@)æ1ùt›Êk­Ãà­?íÜgV9Ø6…ìFŠ/bjû`7I¨Ôü÷PåO2vÌšæþD~"©›Æ¢RJ(â'™õ†ýFî‚ä|zö?q 1Ë:oÏkÞßUÔìNGf(äÌŸÈÕIÜe\ûD÷ôø¤|ð[»Ðêëô¾šä•,í¦ª©kêûÞ}ËÈDäÈtxŠ(I5¾Ø}—q׉9Z’%Ô%‡ˆÍæùùÝÂ{KÄ]ý…wÎ:>ÍÀÌTlá¨0ü꺇ÍQÞªt¦§ZN)÷u˜û÷Õž6¥YÓ#ø¶sRõ¼“Åæ€…¿ì>›çŒQŽò¸ìñcÏÀÞŸ¯¼¿ïª©‡÷ 0K\:Â*TšAF|sþè.ÿÞþÊj„J9xu³ñ²ý¬?s/¬ÿŸ ´ÿÏjû¹yæ3(À2 ÅÿžÏr0u²´7±4Ö·ÿoeèbioGçà™ª®ë¼Å†à³Vß1 _h”™¶‡¿Í¥Ì®¦$«uÂdgC±òÍR ürѵpuàÁÅhØ~ì›èX+ùàOU˜pù¥ÉÇØ>C+Ieä<ò0+1å^‚8Ò-¯ÖLªŒC9/ù5iÙjžÃL€N]áÍ[ºÐ"ݧ+/¾ÄÇÀcÖ'¿;w>ô²³óâÁ†%gé$^O~”½`Âyt «Ï¿ˆ_ü ›ÏQ2¸v?¦¤ùK@YÃÓ˜%˹G”0™•@çI£„H\o¤×ÈYÞé³_%À¥â” m])H(Eæd˜ƒýì§T¨|W—&»t3I“Kwc••±ˆ™l,â& G%ü_ ‘S·ãJ®ÊDþj_êAu‰Ø …<ˆÆ‰àk@væ\— ³4¹ÿá%ÚÃ+—ÃXá<•š1PÕæNp÷wÚx¡ëVcª×íqðe[ßÚº¶‹%T9Áð#* n¶MëÜXo vT2Í‚ßíÁ&\MÛ`1Àò“º©6у+o6$Ý.¤*:_œ#<bdf £žj¢2RGk¥>ž†.†Γµ/¥­ŒËvÑMœ%¤ß?£%ùãhÓÔÔeÇSƒÖ7 ´€"D8–›OЍmH?éX (RkKq^ŸïÛ‚\ʼnµ/ÜÜÛ±tV—íþ$ßO7ˆÀ¬©£ëÍPPŠ&R„D <ÑÜx÷ø¸^ñÁ NQû#w.Š!Ö]?QlB¾fé׎ S~*¼C¿ ýñ¦@Ð:tž6Ò ùžlÜ$Å“ ¡«?Ä.gGO‡ê€uA'†EúŒ4ØÑ@ÿÊ c톲~j¾­¯ÃNÄhµ²¿Â1þœpVmA€®åÿî8Ô³Âs ¯cµ‘Óßd¸~e¯Zç†Pì³÷Á½/-2Ð| þÓçÚm’ ¾_rØñh+~Uo;¹„Ó3 #˜‘þôz•““;WÞÙÀIë<0Çž¾é_tM®e7•^Y8³‹0çd¤VÙ³5¼{q³î,»Z!Äü¢¼3ÀÒ|ZO0oñt…hÓ˜jÛ{ør}¨k¸†oÒ_)×ýÅ•}fXýªnSlÛnÜbŠ2B ÷iiTn}ÝÖܹqË¾Âæ¸ÔnÝ*n°<­ ÚP¬9”œÑ¯p{j{*Ïr3Ã…“ÿ½,_Åï›[^‰q€ÚçdóÈã0óKÿQÝRÄ[.›3ÏIWTÅH\lß`UÿŒÄ1¿ŽÆç [ß^Lk½Ýâ“}Ö£ÃÀÅ›cd+òßßeËÆ|Eè·C-¬íÄõˆûÇ›À il{#\Èãò xo€¶¥YïNˆê“dÚa›žw¥©¹igßüsêFæâÅF ,~/ß;¶&¾Ü"†•&ê÷æÔš åݯàv¦2¾•×ϰbRK$·ªê4„ø¯âzµSxÅ8Èá:¼=µ,Ýåù÷Ûƒc}tòaÊ;‚{Ÿ¼*ÝÅ‘€ÆŸqµ±ÜÖ¦:G£§hàÕíÐĉq@äøþ44,&zÿÞ¶ÿ„—òÿ‡ð·ªiÛ#«#ä5÷rφ‚@õàRQÅlrÔªB ‚†BˆÖâ´B¹J·gï{ß¼Ôm8Ã0[©¾Ãþûžçï뫜ÒßL_{Š©½ZÌÚšÍN³3—߯9ý¬œhó{'ø‚~ï öµ%¾¿##¹8é ˆ3Ø Z TäA`¡Ð V€ä"ÅELÎÄ’z@ÞíÉ…ÆÝ±NQ°½£÷¹)X[àŒöäI-:"NˆdçžöJ­Ãwï4Jé /eî%dzK ˜ˆ¹Ò(€b H¢b½h§Âæ2¨«0Us©ÞOM/ ¬ Ô7Tzô‘6¼e©"ýBIê¶#[‰å-2ÝN¶×$Ù•8¾(H/¹Ã¯>’‰}0Å Vºt‰‘vT˜Ð{¥Y%&§‘~Õ¥Q¹Œi}ÔrÕ·¹ãÛèŸ!OX饛~v,8Ç®‰3o[DQñ°’ýh‘ÙˆŸ ¤È…‰öŠÜ%éZ‘(N(³GU”kWB¢vïSDÌ+àn ì6'hdúß%@ÏW;€0ôçŒ6ö£ìK(”€‘z :…†-X±×þËcK÷DòGœQ¡”^>›Fbô–]Ü4 ny<_›;( vìž_Ùt”IOäVâ XðP&W¸ €Fº{Ô-¢ ÀÐ5ý»t ÛöPü^®i pµÀyn:ƒˆ¸áP2Ë\Uèsd:Ë|Ifö ²ïkê¢áU]&égZØ’€#<·Ç}d«Eðõïð|_a=ÛL×®ZÛrÊ—Ei¶“5«>³‹eîàôÔÐ>²<$€Š9Ò![8F¨6R®‰ÎºA^äIø#Èä›4² u œˆ¿¿Fa•ÅÝ*,Å€›l‘¼þŠ.·(:£ €'ø9w§(Òâ"ñ΀¤Fƒ±Êd‰AJXÏG›£hø”?Å 6L #Geͨëÿ0„LZ³9§Šp<Ö+OœÓáõƒM ‡¡Õ®'Ü9®˜8ËŸÓªMûM#K&‘3AXØvýøŽÎÜÞÞNá/ÑìÀ)Œ1®†,CtQzH´Ë9e =’|¬”²Š•]o†U‘ x!oTc{ˆÙ:jØ”vî·ŸcÎV ¡«k¤,z¥BTH`Å&ÙïÚñžýµíêYê=ÛÞksw»c‚‚Ìö±Í–ª,<Š’ð¦j ñÍÊOZ†Žêøƒm4Òrñ£°‹2¡8rþ5÷Q{jË%¸¿KáffÓÝ’¡Bsúãšh_ÿl¿)Ù óÞ´'²µlQRÜÓ]~¹bL§­Y%¾¶`'ŒÏ-Â7 „“É<ª¦3eTµ¾¦>ÁWA“bbl…ª#±.ÀÖ)ŸžµÀ©BÜUþˆ9YzÉô…#Ý™ÞQ1aW¬¤œÝy–¾,ù-vÍpÔÇP°ȧñÐ)¿–¥l¤ðÕúÇ0tËD/bÞ”Sª9kìoÖcƒ:;å R"ÇG#ô˜ãd­PŠI‰}}í-+Eú3–è¹Wé9ÉPà¯[ËŸŒ¦ß‡ `s†µÄ5|ú)¦|fcÿh…'A?Ü-Ö­ð/«ºL­F¸tTKŽ9„‰ɱ¿ ÆŽŒkÅãúòÛC`¥¦¶Aú¢Ë‡z¿åÁ6ïëÅôÊmöþ„·±o ½ 1|/×*ci_VÉ<6=½\IdãÔÌR¯.Ž×Œ àcöCiS@ý,<÷èµ+gn&ûÛ’ä¼íµ¥Ùb'·[;¿‘­†ZMø^yˆîŒtsä—¤„¬‡ÿ‡ü¿v :ûÖÖ_$¤ÇÛîVÊ'$£_zÏúu%ÄgMSÑ#_D‰™"}*ÔµZ­²vª•‰ß@9™ŸljWØIÈn*ZœØ†Û$¥ó½ïªövx¿;—í(8ð†š¿µúáëT +¬þ¢§Ô²ˆk5Kƒúüÿ6·n ¥æÚžÉKp€aL¢ÿÝœLmí]L Mÿ“ÿZ íäÃT¿3õœyQÚe†¼¼e;GÔ'‚S*H[³ Ó R„ÂùQì¤×"šà]®`ÚÀ®â]lö÷)ìÃ0ÕI€»Ú+v‰ ᣒE‚älj¨Ì]ôz-Æx~¦;kûj¾%èa>Ýîø@?_< ·¤‰ñ>`'{±]ÛBþP¦Á> hÀOÿ0–„.Ü:¸Èæòé0æáoH}¨®Œ»®Ó!Œö›Õ¼b:ÞøBi£¶Ê²†é“’s\ô Ó$+D”cîºë)Q2" y\cÝ“¼Å𯳌üÇñÙÌë|3é1ÝÍè Œ²·+Öß¼ZöóÌž]¯6 Næ7¿f7ó¾í™56Κ/ÇV½u8™~cŒw`¾4ö`X˜ß;߆Äz¡÷{7„^Ü÷Ÿ¼`ƆNíZÜ7;Þ?•¥ÍyŽ(QÈ —òaäÏPÔ§ý ùòæ™°_µ7‹—V‚Òá׹ΣÍ<ò%‰7oè/ô5Mþ޶£i4w–?á»bÞ¾\_ ¾ÎÙò—IG;sŽ·Þµ«kuž”¬®é'†ëmÞO¯þ5%iy°âl¼‚Gg⸢VØ‹r2ÒÉg °Â-QRü>ùx»þâ:j¢B]÷Œ{î°0!,ÃBí³zrTrÌ8,Pv%ÔxX¨ú{DΩ£ &§æ{·žyü¸¬EkÂ?%±nA"¢øÊqH$öԻη•“=s#3§¾ýI')or,L* ó—½ó[\Lh‘Gˆ¥î}r’CÒ©ƒâ3 „ý[JZ©Oå’éïm°,u ÿBp¦HÍÌà{lÂ#q™î`ôÇDUi¸´r•þõß2Ù;ÓcKxh82˜¶')™¡±˜c>ΘýŽ›MOuÇ)e\ϧµè/ˆ’„íoJ¢ …RW{»¶ÄàïôÛ¯õ ÙßùßiK 2ñ–|s;j†?b> Õ_°ÈIúkHFå^Ž7Ÿ—$2Í%Ny,Ö«Q +|9ŠIl“°¦$ÇÓô£ÝÀèF dÜ‘CœTÀP¤»5ázÌ :D{9 ú*¥™…Ïþuþ®\KIåªP–Ť— Föý¹ª°Ö-9b•å’¬ð¢k3\ R¢®èRV¿§E´9­÷Q÷T ÙŽ/vÖøÙi1ê%vE-H¦Mp6#ºÊ“Xëˆb+¶¡="Á й§Øç%jE BêoËÖÒø<‚ºõ›Ž¡^@j„ËᥩÞpé/ jat2òÂ\9TR^ÈÉ'ò¨–oÑ›+a–ê²ìÈæ…J¸^“µ’¼ÔãFŸƒEˆq–b"ƒ‡æ9‚©™X1¿í`t…«üv{´/¬ŸªN÷Ú<îé™>¦B&Â@‹O5¨žaˆ ¹£S`µBè´Âõ™üè.\ÆËCŸëC|\#ׯŸz vZjL¦&Y­ç/è·Þ·(‹Ù}i+[K %¬eÇU0Ûär?EQ é²99Ù”Eš·#Œ"7Ì 2!ü¨ ÈòûsL‹ D ”É£È6¢­®è“[ ”RΛf•Ûe^ú»—ÍЊŒk¦ôN ‘„CLrç[Z0¹ ²ÁFý¿<1SÄÊúŒ³&²Í´Ê¬œõfñ€öÏùŒ &%)ï+QW’WTdpO=Q#œÝ¾yIm%-9\©{C2úWÅvcmœ>ºñ^ú°Ø?©öu¸_Õÿ=¶ÝoÝ.À2¯L¡ÜÊù/ýÞ¦Xu£¨/ù :>Ni‘ž{Â*[ò Ã&É^ã 2CfÿðF¨T˜wåú:E.^sÐßp…Ζ"Cgáµ3m/IçR/Áx}¥‡’~.Œ›,ù­±U^{‰OÕÎè2[ Zjëž+²s öœå4Å/9X‘½½®Î/€LÄÐ#ÍÓÇÓþ²Éº Í÷A•zD%³¤W$¾º¦¶ŽNMþV½)ù°mé“HTe·äpá%ë°³ùë_5áí~~Òú…ßèc§§ò÷ØnßÔ‚7ÝÁõ ¬'ª… {Xrmh‹LóýÄ9s²Ý N|øY˜Ô±3D¸íwe­k¥ò­)·´ËÙì+%YÒ¤«Rl1Þ@ê¸ÍK5ú’Y¹ór`„Ë“CíúÆÿÿ Ë@µ¡1ˆÿ?‘‚q«†´3Ž$â쥧…Ø]º)¶wO36Ú*õFÃhãCØ^=hÚ±E¬­'š¸½õ?Ò·Šrïm]cé‘}Ø}pŽAýýÁ\"ŽÌ¾þ@:‚¾šÛÄcto„¤*º¹ª©©'骪ïcë÷ùÏ»g¶“~Pß>ñù*”Dñ(¡"µ‚º¸UÚËM%vŠvZ:h•ª$©’)Êe³­a|¨–•JÕT´ Pá•ÜBµˆ—qJÖËÏ4Jb;ŽV±ÙoH¡È˹%«%žôÊ¢;–V²Ùo„PæÜÒµš—y«vµ Öqöy›ý4 x(±«à âÛ˜ÐùŠ…óÂA ”f)Ú*RÆékô'/’ZÔTªLêia ñ¨³ :H;иÅò’¢DÿظH½?²¤BS¡žI¡*˜‚䯂àPb8¡¸ÑŸˆ™ p ¤LLÈÚ¥;ƒ´Y(ì_}(f…Éu]A.æ¸Åñ%êFëÇÿDزš¹UÜ%îk‡dKî¡ësÜûyo=¸¯ ¶œ¸Ý<+óKÂf”ñ&ï¿€Ìxˆ™#ÇE‰«ªæî—WÌÍp¸OÝu®ÿ Êlð‹N˜+µW(¿Ž|çm·ê&ÔŽèÈ`Ð?]&°¨ÝWåù‰!& >D]GßJù‰[í•)-c’ÅuòBÿšY»¿¡Ý0£ŒFî õ/% äæͲŒŒl—ª(ŠF­‚‚9\Iá?ï¬[~ñrò…ó®?·Ö®ÃZWž2ÔÛŽJÎdT›]«"çëTû:zö½Ž°¿±*|ÅhïËsPÝB’-K~Þ$œ;…ÎôP5ŽíËw Ò½»?ób{`\#Hظ,(tà‚íOV®Ô¢-ï‹#Ð Ö{yT2aÑ×\Yue WÈm·õ´µ‹ú>¥¬·5R`[7"c{ÊÏõÆ5¯;a=¸lÁ*ûo‘;Y¿%‡¤òùL®aÔgÛá21Z-Æ×u7 y Éj<Ó¬Í2ÖL=¶8Ñ™ÈC½~‰ÆÙ›½ÞÌI*åå#ÎûD RèÐå0a€\ýbûˆÝŽÝwaÚÊ?²¼IA=?¿¨@8Û*ƒŸÓ}§˜‚a^ÀÂ`htÓ ]Gçh|®O§!¬}÷-IÛÑœtÈس†¯s¿´Z˜wÓAòÁ0C!ßÁ¡ð¢¡€cuOfäÅuøv¸k,Ïu\e‰b:6²·U§|‹YÒÉóTµ$¤(#:ÁÒÔš¦¨œÒ ,-ÍîÅùy|«7,ÞÚ òÊh‰Šø˜§¼v0‘å¨&¥9böÒ!¦ §|'¾v”Šl‘¸â¹0¡ï;Aåë¡Hˆ¾ïL¦¨¨¬ÖFxµ•‘ )SŰäö= ¶Èó›å"õ”-ÿ"HŽi:”#š¨m`q”™\¸úáÝ~+ í?ønÖÏÛžR˜ž÷æÎ)qf¶M–¿¿$RJ%pqýÚ¶ÎÎ §7]#‘ª×ÚšbÞEHŸZôIqËOê*šFÅÉüÝ_èƒ9ç·îò±wå†Á]}ólW+M:Ï™*xx­D_ö‡R_ˆÒÝá–ĬGh!àN’D‡õ'r¦2w”Hƒ>c°s‹p:uõFÒÉØÐã}‘í[Lo3æ÷²| MUëîÃxŽ. zG_šd¸lxg«6Ù¡'r„•Áƒäû†âœv9vÃ*–œò¼ê›ÿ€*„˜ˆysÉv·ž-lÆØ–Èxô+èÌVBsÂÓQÎ$àHˆ¬Z#1ÓÐk“óå<ç˳§©gÞýfݰ¬ÞÑn¼á³ålU¿:õPb=»rGåoáŸ1sr€q‚~$^“úÎ]tZ~òÈñ$ ôŽoÜS/?rK¼‚zô÷2-Sw{“Ù†•'ð ïØ°o4’Ý:ŸD2ÅXñ‡g/[¿€5Ëü[qézuS´Žâ5laöÄkªZ«ôXÏ8†ÕWYÎ>š/kºŽf)’\yZÃdî òë×5=W¨¦µQÚqÖŽÖÒüõ9il%µ¹² ìqÞ;üœÎŒnbýÖ̬œ˜òéa1rNïÐÑf¬ÅëŽÈ?î“TÃ^—ê¼ù'SÌBš¿Á8ÐnÁÈéÒKä^7BbV§àæñlD˜@Ž$X›SX9@Ó«‘úb:n,§;°¸;r1kÊH' Ò%‡„ò˜&:RL•˜ÎŠÖc^°‚qòõr(ì‡È}Æñ_³?ÿÐ+I9~ÊüÒçN›f¤ùO:L·¿2 G,:»+sÆ–ç;(¾u·?N= ðWât‡MAæÀ÷ ™ã®-ö¢—;SÅ‹y;˜û­£øõçý©å6Ÿ÷÷5Ú÷©k’0®»ÏC91…G©¨-;¿ Wº49j$6BBÃŽA#ÁRYoþG‹ÞÕnKæô¼ý>ìØnK£‚AÛO¡Æ9)_Z*6üˆÿLÌãœ^Å·ú—Ìõ¾$wÈÍâ8I¸G„”^a ú˜ J¥ÚH˜¿¸-zE7LcÀè·ßŽ\ö!Äí ²ô§7^gv{ß¶¹OûÿÉ9M\p¦5©`e*:ëd›Hqž~¬3ŒŽ…J²v”P ºï‚èŠÑB' 8r ] ü ÷²e"/K#`"¾…ʉM‚ÙzçÆy‹ì~¡Â¶-&K!ɸiàsÅuþ:ük\;c.7†þìôä"õ'ܱ… $o$Ç!{e{/8ZÃÑ2tNºõ™¬V)¯¹L4r5;ЕéNÓž~ÄQeã¡ÀÜ ¡ÿ]ä‰&xæÐJÉ.p»wD »ëÖܽæNlôÎÐ Ér;ðéØXÛ¶‹»µGdV/9ºº²G VøG±@óètâz;ó¸a:VHhMq[ú$"ýêJnÉïx9{fô±§,gÝn-ý–ÍîDVî)®gÓf×v Rƒu÷üÝ–@¸ß{§ÿeÔï_1TŸó*Ëþ)ÆÙ{îQîÀ~»í/úÿ¬!ÃmÏþǦè¸Ôÿ;›:›Ú™è»˜Ú˜Úšº8y꛺ýwþ¥³4“·ÚýúôxE°¨Ÿs¹o†Z×*m¦Œemt½’ŽN#¥!y“ ïMJ“ÿvuH£’rå>Î% »Þ_·àïY¿(H[§a²Ô¨ ðE4ýM›iSN/?/³zW#ofÔÙëí‚ìKªJÅÆá¾ (XKj蔦¯f¨ò® >˜Lû9rz¢ìÖ3Àb¾ËŽë-¬Õî©K [ÁXY¦Dà`zÉ0É¤ÒÆ‹&Aé‘ ¼ ÝÜ0íI„û¼Œ9âN9zûšpŽè˜¼ÜFUÜéõoß¾"´Í‰+ÛèÙú¾<˜õëNù‹†Ü&Ë™Àk3ƒè[0.Oû=BI|‚õN}æJŒ|ªú0%oSжå0OlæP RU,Š‡ËØÄ'¬º‡2†3èôšlFЪ½yµiÕ”[<ýþî-gøæððͧ^Œzàã¯g†oŒz<4õïÖm>¨¿,}¸M*M÷­KÇý]”°KsjâpÆKœ%Ô"dH0Ô;k?ÐÖÕø‚©qÔ©BÊf–Ïõ¢Ôœ!Œ 䃣l^ñt7Y¾> ÜÐÀü=›7„Cƒ]f튳Ñ9|“'R°ŠN훕û•uz|ýTxaåóêê˜H¡G!§9 3‘Sˆ‡e¥?:·íu±0˜bÎ,±¶Ð•eÞÃ7rüŠ–ð~Ðg4Ž£ì.P+Ìe&'Ç«‚\êRº¯ÅÕ°©¤ù»Ä d6ø­+sNEMáÐÉ£&˜ÿ16´IúÄkÎ ÒfÍ©««SšØ@“­*”5â@1\î­>ÌÊb¨KaeŽóÔÅŽ“»««CÍ!S/¹2Ï™Iôáç„F“rŠcèe0^—é;°`müÕGâc˜½#‹e„*v*£„(çÕÀ†Ml²è,‰°~f"DzQA“[q#Œa®it¨mÖI¨‚ð#Ãÿ~1£wÒúÕ¸6}…¶…]'V¸®ýF¬ÝáŸ5·ÿ'i޳·ç~øáÑ4¿_êÝz>9Ê‘{í© þJã°øÖ$^^g’ÇþŸ‘Ѧ<¦½:õ‡/Âß^m~Ox°ße_V{#ùæÑ­ºÚ§¹W!qh¥ Ž1øO6“a𾱇§ÔYüXˆ£YìG4=É®y£UtŽ›Z+óÌ—}´¦ê7â&Âï€o=«nSºžXÍ ¥é-¬*È®‘:¶SŽÀfð ^j ˆ›¨L¸7¹A@™³7Lì¥ dàFF,È5Rµ’ø-E¾ú5ÖËýZ›Å”@´ŒÂèhrÃò¸ Šni\ž[\v dî°‰jý‰¤ÕMŠÐ$¬Ûb¨¡–ž ¶Hѧٳ2–6Þèpc#P¿ªÑ{ˆ·´, ¦€â1ò±µÚîÀÇI`C£’µ(›³•ï)ÙFâŤÛÃ/ !Bq·'îÌ×¾) C¯!,óQNŦp`þýA¤­ãv J<-òöÈg$—‚#¾w L¯áeH5c¹2ÚÛÈF¹úŽÐ8Û†?ÙË衬{©HDÛjŠ“­$&×2qâÔØ3ü 7Ÿâ,—kp²˜à'T.ÓÎ0?¡Ýs⨡Z0C¿¼Âh_3ï9϶–åj³‘KÐ…Ù—RH8/陼þànu0e¤äêÙpá£=|’¢GŠôÈQÐÇ;€±¬x?g§îƒù­zOÃUÌ—X'… U†ƒŠ!Ç*Îp’É ¦´¡"ª±B?¹¤éÈœ› \¹ÆV)D_¹nv BT,ªÅÈ$D¯:=dÀÇ(­ SZA ô ²8>Èí™%÷ 8kO„¹¯ ­2';Š1Þ‰ãH”;¦ÙÍî(Ú.ÍÎ “ŽèÙ¡ÌÇ\É7D,uk*rgrÙx!|ܪ—X¢D©ö–ËÕÿ¸†*À¢FÖ@]y-ñS|ßö2·ë¾7\vÃâ³)ª÷¢§‹ ŸcøRÕ”6ý®R´=dž•Á áµN¶^{þ‰$š6«ª Ç–0å⎮6£õ«­"žáI¡ˆæIÏ’B}Ç(ãpÉÂéWÑù^$ 9}F³Íœ¤<À(¾ˆnÙÅÿèhëÎag+GÃD5—Â^êí”p‹Ë¹Çjc®dÿÕJîpd.F}·þÄ·t­§GGˆ‰_KþuHbËw¥ð†sºi‚À¥ÃÁ ûÉíÿ J$úþ1s}E°ëðPŽrò‰2à’Çïtަ0tå,UÉBz¶3ùŒÿûÞŠsÝ9¸Ëꨉ¹ %GÈÂË…¨ª`öŽùewÏ•å“0s©#+iÊôÔ=×mfYˇܲX,¸|p` ½)æ"Дõ6®H¿‚(4š€Ç½3ªJŒú´¤^Ï)•ó’RµŸµ´®ñÄ%íZ¬°ãº}sr”whh}îó&E楳' ¢òº[ÈéøÎ÷ ¸T%ÃÈâ—Zïy56í^L€1óÑ¥9³°qfÆê `p´Þ*µ\Þum½cAã¶K‰&øáç1Çš<îûŸYÎ2#ñ}þÿHò€æÿI·jh;o¢î‚Rê‘DD/&Å:3q©,Jæ®Î$Ú¶ª•§•3ØråÈ)BhH86JK»”^Èä^ë?%åϯu»~.ö-ø/àÏç9£PnÜb›Z·Fƒ¾:ͺ÷>ݤý6w¿ß½õý5å#}ûÁö]‚Sø¼ÏiŽkôU8¯žyîùR×§kô‰¸F‡š¢Ñ6ü­Ó56 Q]©kªÿdyj™æŠF×ôYlBÓè)¿`Óh™ý˜¢±jöY~RÑè*F¿hÏ×5Ùržeè7€5¤qñyެ³ÿ¨êL½Õð¼Ó_·pª¸‚F1ll%[n ºáB­½á~jÕ U·lÍÐÚðbM,¯"eÓÏ ?h”ÑÉó­NXüÔ²È.²aI¦bPç^‚V|ÛÑŒ%Yi/HÈOŸ¥œß–/¾àw4âOºp¯2ávä=7àû; ¾ÍowX̬3Ú€¾øn<úËDGüÔÁ˜’¸r¦VÙH<Í2¡³,D¦‡Ìj€bûJ†zŒÜr@üÄêò+üC{x??*Îßs³óo#|{eÛN?RYTv‡Û]wfG™>;÷2¦\Á”}îbXÏ[žÅÖwxÉÚ°t4Ùb¥¼¢sû†š`¢ ŒàOÛpD)ðyo*¬°˜uy¹‚À$J–†H´Ÿpȇ!œŠ@¨aayVÏè“HE{JwBa­ê×,›ô²!+§®L´‚`*ÇÏ…\•KG¬½‰tнQOÜ–æ;éC-œB ú8&Îõèà†@5œ ãY¾ Ø$á2åÄ}Ôj‰Dz4˜d"¡2û-|¤lyAÄE*Á…ߊñÚ:.„ØwÒFÃb£ùß œ sÜâžë€ðg„n1Ëpì0hàëÚ'gŸ -–KÄà’¬â;}±°n9qŸGl7zbbM}EcÀˆ6e*)„Zž¹VÖš#ˆ¹zæ÷½«yÀ®A·ü°¶€ææ!O!„ô=Ãa°Z‘¹)÷Û0×eÙ6AgÓGHyT®ÐzA»T5 Æð 38aÈ‚ÿP€÷w™;µ~w·bឺ$xìsÊÀØË÷Ø=ùd’`†L-õçMç÷Êü z€+þžEˆ’ÌA¦Í=!Q*˜ mˆ$¬¶ÎÅ¿ÀЬA óè!`ê©®å<d-Æã;›o²ß%;L.½œÐÓ„”±Ä\Í•¹Jˆ5’;¶u@3¾¾¸‚ZtÎü°J¤²'ldp[pÒ+{qöð,ø>O;¡äW*nöš ÃÏáŠÏÓð\Êá •÷¼ó„=oqy:G(.‰bÑmœ#D_ÝpHì˜gJ¨j4¿ßÊæÜô+I'rÂ%{3¥C2¶`êtI KœÇ´ ¿)Å}5ïÌ]À@(ê Äe‡Îú§:Ò8lKÃÇ/ŠuL+åùüØ ~u縬ZÚ 5i*îJAÿøêç^'ÎvÊ¡x/l³‹zl3çhÓý×ꯩ@¾?•^è9‘r|CßøÍÃuo ûi/Mp‘ N®ú囆ŸB1Ï7 Ä™s› Ö•q&‹4¢48¢2‰Z 4Zy¯Z½«C¹6õ¢Ô£T¿°‡ö­]˾u欽®Þ¸>½#“~l^v»‰É®؟Úþê.óša†÷à¯Gƒ ^×û33€ARÌÂ1œ@r!ú¶kÿA0Nÿ’*ë'‡×ÊéÀð*¸“n¡=i +÷GÓÝo!ègzyòø?SšW3Ø aTy8ÿ;ˆº:˜º˜þGž;¾ÖÎ[Iûî?Q\ŸÁ–Qi]gfî7¦c•¶§u4”Þ_

‡%¯°V7ýÙ$Ù=î1û‘Ø$lÛ>©’}_˜¤« ‚‰1šùó t¦Û.âŽ_šdÛ³yæFIw¼M=\<@‚N~eä~û8¹¹fvçÍÈëéæÎù߈%³Dy^òTOµi™ØOqWV5a5Ãsë =#¯V±šwƒF®ot×DÀ€¨vüç]×yU6“¢™DZ%馨ÙMvnÙn¹ÔTÝÁÔè.'B,—!$jd_9CmIÛ‘ºÊÞÄuÑ3-ÈIOçÚSÇ×ï?>&~[¡ßÍ9:8úº¹Ó‹ÕðOœz%ÚÆ³ìY^·/×V~tQø—fzäÑmU ôA³Ð}Ù²m8ÈŽ/Çç$Æ»%¬ &Ót~©óIocn|íõÓ³ÝÔõ§g†Q5ÊP.=¬JÙÎ6zòvïÿÈŽ‚º!)È<=óE1 ÷ÍGr!Ñü¤,z(X7È‚QµxïMqDæl‹Æ?·#Ø  ´ï–ˆôAΞ ÚØ5nó±D¤:mnZsúeýriaèŒxE¾@W§` Ãu#¤8Ö4¯IÄæ0?LøøóÉíÍÊÉm&Á0¡²!/€GóØ+”å«à^¦+ m¬¢›´ 0îâçÌìåèêQ0ÂÝp ,|yúˆf¤[Á ·ŽÇÙƒƒƒÕïÌÊë“ÝÉÇÊÕï’60kêóãå žþ&~ÝHnóñü9¼Mwàwò€¥æ1 ÞðçÁŸ¿Ÿ—}ò¸Ð&²6.Â9‰žá~|?3k}‚“TšC®`zͯ½€k’áºýî_¿ßYñêåP<ÓÇøsóºÿÊþ)X˜&l»·mÛ¶mÛ¶mÛ¶mÛ¶íýlÛ¶=ïü===ßDOL÷AEdEdd­¼jedÝ‘«x9óô»¸ùžßößN<ÝP±: wH¿Ù¹¹9T—’„#À ~Nߟß×ÏêVI=¼¼HX~$•ŒïÓ&ЬÁß_åµÄÌþ*îâží\!¬\=;0p|¸884ͨŽÒÜ)&•ç ­õLëñP¾½Éoû£d ¹›}zz`Ö"<ž®N^¿ÇV¿ÔNß—åÉÆûéÉ×8-C„6kËøqT|áï$v’L5ͳRÃð¿)Þ»ÿÖoˆ«#[ý#ÓV[ Ïex[A+ß•ÝT[3}áÅ ’ o\À•€[·)¹ƒkw·UŽ®}œniìg“¦‘>,öÉl£GcW{RÀ»¹¡X‘±ü0”í°'*áÚ%-þin›44Õ 6Öٓ iì2‰Xýý¸r`É p’ÎEò¶QlšÄÙ‚D×I àÑb)ü)<˜_HŽ 2ä3Ô„yXÌŒà¨\yEc˰bÀü{£fé3`?A’]µÝŠ)aÃr›fNŒ»¥‡†«åàÃwq‡†Š"I{Ì þ2R.“í%{ÖÜ0ÁŠ~Å` üºÊnÚ ¨…U³‚š¢Õ\]‡¼“2î† dl+õ;®F°x_8 ø· ±_.%ÈÆ«hÑ:<SÑ 7w ^ah¿%›|ÇfA2ì›…|+jX”^ƒã#‚"ãC3”3Eq%üpåôŠ&rýÅèVòá«E‹ì·|žû¬þ/…é¨k°0 æEAÚÎSfá£-8óß3â/ÇgP¼äi‚Ìà™Ý>E614CþÚ¨›d7•;.NÈBÆ×‚DZ-Tί‚|Àðž/ 0*X@*+œ—c?NØfBC+"4K2Õë¿•XÕ-e÷¤KV-3‚oZ-@c]ó ÙÄÇ(…S„qŽÆš2ŽÞK3‡ŠÄð}3ôTž.JÓV´ôÚÒBQ[-›@’¯£tÖ¿Ë2ÊÛP§ïÑ@Ð[Hî•G莥ÅRFqYHæÃW-iIåmûùkôã%rêšJ{‹iÿºeí Ì’‘Åaã†åh­š+ßy]6àYÆV {ò|çÐlƒõ$ê EÉÙ2¾ÕNGòñpß•Àþ]¸,ú áãš/oÃÌBBóžÞ¶l6²uR¸k2Âñ¿ÖïÒ—Gí}o¹6Ól™EŸê˧ÈîWï¸ôQÒø¾'«w(†¤‘ͼ»jš7p½$©|Áræ-éÞQ6ì(k¤{qÆd2ŽÂÅëšÁvGNº¬5â´{pÒytdë½ tHƒÑ«m4é:H&LÝÓ‰»‘põº|v,m1ïqE—ÃvË?…½¿Ù‹t§UñA\H‚ÐxW°µù<šÃy4¯Øöm Ù&@®7Õ" zN ¢xÂÖ?B`œÿ\Z±Ád‰¼& x‘D 0¯ÿ Ý^àH(¹ð'{Ï'î¹µ_ÃÁj¼X[ïo"úå"Ax!›v†ÞKÜC2óV„Ñ—@áxŒ´˜ø å\bÈ·`¤J?åÔw,Ì„øbþxL¼A+1vô%§Íp:v%ðyßî†ô=ì­˜IË¿¢’¶‡×ù ÇÇÀ<‚ýRW0õEØÂšMæ]# ªÙM2X±cƒ—Áà°ª‡0² 9‹ûÊCïì.ó>é‡u·õ¸zµciu[qXvbñíJNbºGöÌ…0’]ü†fß²=÷…*†fÇŒuø$½G þϺ¯ƒN1§ ?M$ƒ?JyŽ™èÒÙO‘¾ý:þ¦ÿKâ¹6Wä‚åóðÇ‚ýÌ´DAWBBˆ\ÿó~C(O·ýÕï鬦ޮÒúïÒVk‡lh…=dC£Ô ~1‹‚Ö ÉtßÈgð7dv´ ª¤3¿Eí«k]n;VH«]èßoæ•ÒÌß»åpÆ2¶ïè?Úæ09àa|(ä“ÊŒÐr8šèjH{Œó‡¿üÒÛ¤Óþл£[Á iˆéh²móéë.ÝhßÚaW²\ÜŒ°&ÊiܪÝÝrXO[ŽÄYÁò9(&ÆÎ]ù¡[jùÐ^È´jÑOG师ƒ´¨wkÓ;ˆ@vMbc`)Ó& ];æ©Ù@éófm3ƒÅG¨¢°œØ`âeÓÉ'3œ…MÀ.΢8(0í£M¾"%{:8eñt› Y2HñZ"1šE}%„ ÚæµdÅq‹R*ü´~#¯ÜñDÝ·šêÚUècƒi—’¼òƒŠFý Z|Ì/|©g¹iV äpªðü| Û).Â%»þ1˜R$†2ªÄ§‰þ`‡Ç$ØßÛÚn~€ð„¸ÂôÓWH2e>ÜŽ³ý“Ý‚ÛÔÀ±îð“l?µ5}2çÖzçõÑU1¼t€ Ì¿´«÷.n]SŠZBÚˆÕq­ªŒÖV£ ý=×»Å(€åjºKN—#ÿ†±SKÎZ<×M…‰ÙX²H\ˆ%‡ä³Žó°æeg°ÄIÊýbôäÆþA¯Š„ÈîHôMídDõ#o[¿¯PyÝ{fq}€.lÏÑfhõç„â¸A%¦°ªÁ«j·dš!îÊÍÓ›×°‰B[”ãôMP“õ¾wšØ.}¶ÝzÜ#b¥tÐ_…DØöÕÅtP Û´Ážò“ År‡q0‚x;ŽšÛp¨¶ÑfÓèÞWÄáÁ÷çÝcS©„`Ø&ÀïFK*ˆ•ÒRSN•Ը݋Ü0dX´Î"r½q;îF²i¤¸†ù$ð%µaàL=3Ýs`Q¡JZÇ€šÚ Xéš^4ѰÁCž‚‰!œÑzúiçbx;¨;—ž` ªàõ¤OJà›6yF4„ƒ.Ì­J‹ á4‚dÞ•YñŸ^pLëfGöþÜÑj¤VX°°s¸‘q®X™ŽøÓžžËˆc~~³xîZ­ÊX1ð¢d%¸,Å Éö8†>“âÂòr¼ðyžú°ä[q<®|è’]À­tÒS3Ô+SGŒç.B¬åéæ=Ù¼†ãÝ ‚7¬Èê©ka*z gÇòÈÇXnƒÁÍI@pÚŸø¶ÚXeh³ 7vuk¨ÛÅŽ¹»oËS_¯È¶u„–ÁÂk–¤".žr~“øä¯3¹S߈ax?‹BÇÓP7¼SÇ,1k‹Áðë·Åæ·|+ºy@l!컋gžïgísèx™$µkH5¦Ž+`{, Ç #̸kZ$rš¸ƒ‰*…âèœb!|%_0º ùA®ˆjD.Ž{¦t_‘ŠÝAºµ]tðûWÞ”ª ˆ@+Ÿ;V²ïfL"¾ÊBÏp|»[Í#¥Ät³ìí­}éûªõ2sýt pé>>×õÌ…½;~#a3Šç'… è4ö6¼i,¾GΨÇN¸´”¶ 83zd„ƒóþyüž`ƒ-îmg6  E|DB°`‰5KX€M8S!wh¤¼²FâO…Ng¡ì–úÝ‚çÃñ"®3œWx¸ó Ô Gîÿ2ÌÁUŸšS²8–Èì{Œ*(rÃŽžÓÙã%á5g„4&ð³ ©-„´»JkP„¤ÈÚû| —i ‘­Xærþp;‡"½‘‰‹õkð³Ë‚Iöº´€š;‹ÜohL<p%—튱-­LWlƒ?=µèyo8æ—’f+œÙÌ‚Œ<Ñ!nT¥È é”Ò’wtÏÁã›”?Eëbîò¨™÷àµÒ‡"wÙóÎ"6è}«ö7ðAwÀüÚO(0z4øbáÉíˆA)ÚK/¿q’E»tËçÉÀs”%è*eí¾Y.o‰S“à”ª ¦î#ùXgoÆ¿‹îGAÓGÛ'T CZgà”ÎDólç®Íg9]Öúm!¥+Pû9-a¼ ‰ízr'-%ï­Êqœ¤¸‰3±$GªHÉö¦U¦RNÊ¿ Ùtc±¦5]W‰t[€)B½z…#_dsvEª¡¬ôO_™Ä|ºû”ðûÚnM¤mÝdàG >¦JY{^9TÃöòßwŒI¤ ÏIÚXdæ) Æ:g_‹:—¤Úz¨£:Þ%s•œj^¡6¿Ä+ˆe%´Ï¢Jê:õds6>‡íÔí7Vjɨ’wÄ•®ÚôD  „C2,þߺ*hIW `*ÌžIšª{æ®>¹™UA„fÐ#]ƒK,w‡fZžŒlpµ“ú òt¨™AáAöÒLø¢¬=g€lo·¥:$´ðxwûå7¶2¼òw¼wЖi 䯒ô¤)Á'£.n2$ù¶žq*ð]'ól­' šrÀ‚h´¡eÂ÷b£lêº=rÀÛ ëk÷ß±qAÙi°[¹zE’Û¹¦}b³©¾~‚Ä)Dlq%•7Ù°øÒA¬¤Ko…–)‰ª÷š'ΚÕg…DìF’çÆ*Tƒ`‚vâe<Щ8Ïm¸þ£Q¦oÔ$¶Ïü”ߺleô¯E,ø\Ì®‡Š od”É9ŸÌŒˆoÌݺ³¬ÒrvG:/7š8bÄ GÌpmÛY¨fW@ajɤ8×pÀƒl¦ÎFÓQªM ä š& -H±4¬%!=9ÌÃr²B¥Åù‘)ÿÑ8ìrìŠúÖ)\þI¬XjnÔy·Ãá­îÝüCÒûzáY©ä÷‰dï5ð§ãTï»»~ÜAkÍ#@‰c6«[¤% óåDÀ² F­¤éøq£-¿=. Df¸ŽΆv^'ŸæRÇiÞò¸ñ‡ÿ·j½wûf¤æ"ǽSq£tØïêâ|qT0?àÐfÊÙ ÒýuÁêø9¿¯Çô®ä?=Ç ûOðK*U€¤¾S”Î}ƒR0^·‡æm½¹þÓR¤8@E¿Ü5‰óÜ‚úòÈ·q9W{÷¿ŠÎ7>A3‡–U}êåfú˾á!cDÓh)Ë,,u•b¨j£kò küéþ%c¦´Ïú!{>c»vüTžo“~úÊòˆ÷a6µ„ñE9Õù³ƒ>Èâyœàk¹Ãþ”YêúŽ'™3îUåøÍ‚®øËßeBánS­ ¢Œ¯ps†¾Ùw&R5²ÇÀQaõìPaD¦±²™O0G/{”Ö ¤vÖ~9q>µ#OJþSáNxA/ð×Å[Nw|ÿ¢ñOÞP]?.|ë1ÓŠæ¢Ù¶Œã‚Øe{Q­Ê™Ø·y‰ÌCú,¨“%S¥%Ô1DæaNl[»%AšªììÂ9…Ð*À¢ªfüHkÝ?à¦Û“AÝñ2<ŠÝKör¸kRRÅD ¹F—¼·hDïWÏSÞHC¶aÝI!™sûK#úyx<܉ÈrƬLMFwî³»óÀñ¡âˆú.dl-´“9O²Wt˜~«ö’Bçn¾®1«%>ˆ^ ïy¯‘]û>¤æ¦B÷CWÜÓàAÕצ :B5$ÇUÇ’xï˜æ4£Œìøò§Þd.ß°¾~¦º¸¹ OøÊQ,},é Ïb^%©XãóÄá–͉{Yï³n(³àSÄ—5ÀNèí*˜¶Îèÿf¯#«uLC¹âQx[–PA (“}¡À¼œŽ& ²]°g=@uñÒ{›bõ±K:ËÖ3)°û(_cGÃf yìI¶Þ亂-êÿÚK•?Äû$xbÌ=•$܆dô©8ˈ9•ÿ8=1è šaÆ^Êêð°tô1á«>È"44ÚûPûd~z1åçÏïô¨ïO]m²O®jÚ«eÝ|5od'OW{¸[yŠ•yé‰îêã !+áÅ0pŒlD)3{DeÙý³ÂØÎ¿z–ÃmzÃ\áAXÕX˜›$Vš¤z¼Ž‹‹QÓÕ}Ø™å]6Œ±¸ƒƒ5-ö¶8et8ߊ6w…¶Ê~˜™lácGK¹wßkÀpÜ_âÁ¦£k’µgv Áh§Ûº#íÁG–9-ÀÏmî#ÆW‹+C¥ÈYLôH@š+–ŸÂ¾dÞãÌ ôü0™‹hõ¸Ê­V‡nݘS>¤¤Ü[=\-€ña´^Z=|˜c¶<¸£< 9[uõ`ØnJ.¸·Ê˜yTåE<ÝòkKgµ«Ô•¤…]ëùaÈAw`öè%$ì8d´„¾žhÑõ"ˆ;)nZa³Å™Ò¶ËŒ˜BVvÿTͯàÌ]TëÏ+Ÿ\4´cƒÒ‘@<_̸}Ü™©>„Œ.K¦©^S'Æ··¸€Q´#/ÚYð’ àè =šE¤üª«É†–²ïM€¤éè0ÁÜÌnÆúÜ‘¥öѧyÉq(ÈËusH¨'ï<È$èzYí4Ø·±–s˜Ö¥e& ÅQ…9{¯…4ÎûÅæhÃäw>¯ ×ÚcI™or¨pC HÁ3Þ© õÌ)C½±VM®_8.F1Òãn?rñÜN”41ïCÅR~@w‚)ÞÉÇïáý™ÏoÆ0¤EïKoÑxÓdoù8ÈÁïj^[T™‹½‹1¤±ƒi› S'¸ìmû¬bøèS| ±IÛ»65I8»R ¯c«ü¢¨ÆÍ›¦äÙ¶/sHE¢EªÉ½nVBV~62)Ó°°íJƒ£®°Ãµ'Í›ˆíµæW÷‚òr,Î Ýe ûò†þ¦q”‡tjÓ¡³ìh4˜ÄÆS_+”P²t íÓTŒ¾ô…'„;2óN]énª@\_.[R¿«(QÜQy)³tËpŸ-“ó}:.5'¥Ð0JuZ2«p0°t¬ ‚­ ßÅ\4Ê „Û¥¿8ý,äî– åÍ—¾ºêq8œ|\.¬Àž2_o¿³53¤Ü—­|t©´ƒàØ ?݇m¢ÌD²-d5#ï”ìžØK†Vüç¡3Pê›ôØ,~xŽ8NkaÞ%•*V {o¬çS9©•62€EëRYL‘Ï·˜TþÜ&µø·À:°ê¡'uÈ0@­EÈQc+¶NÛמN‰‡”v/Þß'Þymt1ë­K‹²ûöê”é«4\o¾D;‰â:<·rÂT8“ƒÍ>õÍÏb]ß“g ¡°T¼›ÞFzŽ·búÅ¥»Ïë9\à E€¢¸­ˆf$ÿ¾¡žHD§•ì¹n…œÒÝ(*½´Œ˜¤ íµG|û©‚1,£^C¡Ê~®®ì¹ta¬0Ù O8î2CÖ /Œežjø»˜Î"k<ëˆõ–˜Ñÿ½Lñ†“Éy“p£¹g<Ë«‰ÄŽç(u–hµŸßëv.Šëdó!R6P{µWBeûdœ Ðñž‚5÷X‡D§‚ØØ{„Œ8ª|â{q¼TH“Þ™EˆUù©Ê•Åÿñ‡ñ‘h½%XzžÒqAÂgøªüÂXÂZiýTÄÑÚéÖýfk ÉÏ{Œïz{ÎáyØëüÍ'Éë±ÿi'ÛŽY¬‘¾ üïJ“¢'ÏBÏiÔ¬_,B‘÷#ÎPL|¯ümÞòi’Í÷ÇÈ7dçEUúÕÚØtS£ÕK>~Bœý²è&]~QIÖ+·"”£ìKõqrP•Ί^;‰¸W~ªÄ÷[lèÍé“\?ŽœL ˆÅaAÈs”߮ՓÜáªz\Ä’ · ~Ý4êóÍ ˆ ¼«æÉÀ_Æn—_ ½â9Øm|tꜘ# ÇõÀãi¡ù©µv ÀOØ/àp¿¿üúçÁŸÄ>μÿ”Ük¢Ð#9J³Ë2#Ò“˜,†H%šÑ[‹—TÚÕQxaƒ‰5lSOÃÕ& Åà-±4O{PǸ}êM1rÜ´{çáUvRßðŽbÊ+Âg'AkaÖfëùÉuÝ{Mo}-d; ¸1c``Þ¯zz1~dÈ‹'*lêÁ¡‘=nþ ŒÄ·Àñùý„Þ|á€â´ëoxC©ï¼¸—{½×„iüy³Çê},¢¦P•¬€=â‡4!Ö5еd×…¢Õ{¦Nª¤¶C-9 û9'¯ñ-öÞÖùþÀÿ,¤Ò–žÖ*æ8ÏùÿyˆúÿûÿUãV_OM¯½êËÓEeQE"×6;©bãº\iææaDÊHn¤RJ%aÙù©ÞÙ:¢Ú¾ª´‰Ü&n¶°> mkêF#ÝͯfìsD6$q¶8$¯ü‘Gq l64×5¸ô6‘¸Ä7&O?}w½ê+$¥˜ ÿ$.+VªÝ}î¾üöú¤~Lý÷îpý˜IKÿ®Šüíúï¥(󹪪Tvñ¸©î2ŒÕOts”>R8åª}6qÜ+}Ò9oÕ>윷‹yO;ÔO¼gÅN˜O;äO¾fàR÷ ãš©òYǨöæÇëªnUZs©äû©ø ptTÃTLÓ§î˜æažRw¨ú·t0¨½ULë§îXÑÅgGSñ™Ñß2£¿Týøõe*/U¹+ÁUo¦ð“ÐËU_ªNTøÈÏt«¼DV⪾9ïÔVŒ«ÜtNâ«7!Sù ­ÈUKªr•>”«Øê”êÄUå«71Sù‹­ä+¿•ZÕUç«69SùÉÏ0Uyë­ÐU~ƒªxkå§WøT}ÅVòT?ÌO|«øÊ­æ«¿ŸœòTWä+6¡©þ‚rÞª/£«^“¦úK¬è«¿§«^£¦ú‹¬ÐU‹ªz«ë«6­©þ²ªzëë«7ÿëErÕkØT¡2Õ½²¯úήr“<¡¯Ü,™’—êåVu•Z¡«þ6Uí.zT?¾ ¨ÒSmµLÍO{TýI©ê-}´ŸüH¨ÒSn½¢ò×]1Sñ[±«ø†Uí®|d/¡SõÕ^áI};öÑP5Wµ•;9;Ï){K{WùÅ=¹Süð©âCJBÙÔ<{:'‘ƒS¨Ìfùb£tìóÜÑrÙ«F»Ç¤r±Ð(²÷3Â'ó3kËûû$güAxî8oB5Å`8à¾ëë…A4#,ÝÀH×!)cÊÀ3Ì‘1Á„F8×A0cƒ !dpnÂr‡ @HàÞ˜Ž è@àÅTÁ÷Ëw›ãåݳ~‚(ÜœÁÁV#¤â²“Ã~yG¿·G«xŠƒŠ|IÞø»Ü`áLã„ÀŽB ä„0Šï("Dtq؉Y㞣˜|¼frùJ»×ï `a ÂÜPC„2IÅ““¬$/Èi%«)TTN³ŽP68à0,Ž ÅiÇk:“Á|3tpnԚΠöpnF®"1ØøN¼—î°ÙãîÙÁˆ<¬Ó66e3)=à‡àÛl—c\†š‡°XŸw ¸4þ§Z7à+ukÓ/¤}ÂNÑþ­iH‰VHÈ@µ2?„OïKLµè†\ß9êÑ7^–šxªêèNÁÇp¬Õ(!„Ò‘ =*ôjÜïöوͥ'Ó@Õ Knñ]σONÿHgï"ó)*usÕ¥bÝrXv=5ëKÁXÉCZ(ž’Aàƒ/Ûj…ïKê!àî&¸4ÙL¶aüp[òã¤2¼c´W.õK3´¨ ªÏíÄZáuÕp^t㪦wPönTkÛ¢¥²ü»ßÕڽέ6WC½¯\Yå}a;’“‰OpSq©¦ª¤ø5þaôÚæ¢Ë©bî—/Stì‚.éÝ©½¬É€êV ŽÈ¹îGTææ<4ø)IÕH/îxøÍ2ö:ãù?Ÿ¶¢Ð)Ñ^m_¯°ëñ.¥žse-¯f¸Æ%¦½$¨fw×YÉr[ÜWô½Q=È,µŸ‹`Õ{÷Ã×—RîLÒe+Z[:(ñø"ä €‡H. «pÅš<ëL6Ö¹:J+±(p¯xÑkõçî×âȽV¢òóé^)/*º¦§ð¡«ÇW¶êÙ _6ämO9úÑŸ^y«rqÞ¾Vªµ_qÀ1Úvé¸ÿ»iÁ,Õw²Ûý" ÿ»²î¥ò™uÔÚqU¹š¦sôˆ)bIs Ï+hÅ]zK¦â½í_…]•·.uÂòáçv“X—W\¤ï+|¯³»ÒÜYõö?^­@YWW4ÄBùisÆÿh+Þ²ÈípG¦ÜIf‰“ðyÉŠ;‘$ª|Bù‡ŒÔ3׉2ûåóSÍRAé/Íy¥éñé(ØúK"4µ*¿9¦…æj÷€&›îò‚©ê™ÖÉkÝÿ1÷"}fƒÜßÙ¹'³ó•ŒB–‘»8xÍRyÏ¿(¯/÷ÇÞ®ç<Â’Ží'Së]+F‹N ÎÙ€3#»mÔy{qr"ù´ƒe‰ÒF»:wú=ìÕªø\‰wªöe“™)†Òc4?wR§ Ê9˜ÐéйX÷ÙJ€Ž*‰úô Ô#²kN5 í·’qßRKãÛÅ*[«O¥æ¬Tµ³­Õ•š½Bn–kl–›½Õj9ôÊùÏà´ïÛOù#]Y{YúR‚F€Ç#ìb'[:¯¸Ò£N¦È SûJ öy"WUXÑð¶,z™^„ö¶ß_¦kZùcé8QíóÀ¶á1ûf"'ÉŰ;ä{[ öáÛº B\šó‚cjyů²é¿©KŸ¯Öø/˜0›t»è+WVó@,;Ɖ\±äV‰¥xÇ@%\SV/de¯ÙøŸ×Q£N«Û– Š*e@~Ò·ßæ«›t>7ÖGŠføf—Ò1:´HˆMûoV JPÙsV£™wç턼¸Žy岈·¨AC +³òú<ßÜÆ9£â—‚õGèÏ×5 –ªË4E(“¿ƒÆ%PVà`5a:ÚôIÈNU‰ããV‡PZ&±ú”µª캄ÛóÞÒ–r¢ óžˆ×ÐÑÑ:õàT¡õš4â1M…0m¨A)+ñÑP«új–©ˆZR¢Bð°$t„©*±éo‰u=†wx×–à.wTb.ÛNEŸNQ–ò ûBD©4½.._Û&W«ÃÍí•¡gØ{n䵎®¬“‡ä‡ºƒ1š-ž˜8ðÖõ錇œªâ‹]fŽñåôhÕŸ*pÑËjœ–KĵÚõ.mK„œ§Ã q!º>2+¯€!k¯ÀÜÖ–;j–|Pp–â €¶áMtKÓ´¼#¥.ëx'׋ˆÞ.ÿðp›ÅÇg'öAî‘ÂnJžG!$&+m1 (»sÍ`rõ€\þ)aA`¿§XÜá­H;k »AÜׯ¹Èïb‚Äψ@Ëv¼EÏù¸ªæ’ÿ- >0sÔ-SðUC ×}ó°ßp®|2ˆBI´±N¼yŠ‚g„ÁnDÓMFÓe¡û­„†wÞC ˆtüÙáºçÍé'áº]Üñ€˜ú9ÀÖK«ëø/Òö]ZÚ-ˆò:Ôî¿öL‰¬Á{\à8üF—yzË<]z³˜"•I«´…&:ýÂuŸÈìj­ EØB~5º¶ÊN…º:t4K8¡wÚlíRθ!¡ì×"9Ò-²,FÖ襵rCafKUµ"ÝÃq‘›ºÖ½zÖáTÙ%,%ˆk?¤èw¬!hØkÜá†_\Æ%›äÃï?ŠÞÚ@žÞ<îJ͈¦­Âê÷AjÃò2²l?®'ãC-C7;#»©KÑÐÉ‹HözYÙ¦Õ¡ZÈÉÅP-é2Æ.|ÚÔ{¨-º S;1kæÄ~KÈâ´3dànùêh×m©«™]Üo f˜› {„#À™ß’0Ïš0¼ímE~ úÄ0$Ô‹ч»}²CAƒq3›ÍvåkÕûG*¿]5±Ð«tòu\)ª /ÄÛÖ‡§:ÍŠ}ƒm©¥°-Ä£Coøàa[äDC Àn:Á èX KíHS¶V;$ø¦†mh€û´}}QÓq^Ý2%6N=äzž<5Ù‘˜­N©ŠnÐ<×»³…W|¡ØsUEgcu7¦E-½i› hÒÙHÍ3»)âQ»Ô›%úÞú¥å?è•od•ÕT'°nðœ>}ªå\W·F¶Îè–~?óh.³xîsI혿gno%àݤ¹ô)Ža·ri/K¯„ç%ôç!à9ôáO«´—)ËgQiëg%ºÖŸÛ|]д.!·ñV´È^Æù[òY׺Ø%Õ ªÝç?¤5žßÃD´ýKð–(AÞµ)ºñr×cŠ1§ô ä aÍK³Xæðx¢¯BCqßu|µ&žŠðËj¤7Æ{‘«M뜑ífíú´eÿvðV>£Ô¸Õ0ôÖÒKp6bçÈü‰M@?‚&î|oQ_õŠ–™o’s…4hŒpŒpŒ;åïƒIz! RÀÛfÏÐU–àÚ™ú Þa©™+Õd\6éÔ3ú˜ŒãÉéõFo {hMÓÛÏZ­eº£!7Ù6s‹Ëû³4 >  ¸$­§¤8ÅwßgèÀSX¯бd¯˜X†¯³Äñ»h ›µË>Gu%“?=Åj¹3ª¤Ò­pÏ‚íb„GôÈÞßÝ;.wש,¢©D[\Óñ*›éT›=€ÁBö iͼÕmÄac?!D¬]W3\Ø7·“óB›Û²¼Šýì µhvj›$V<”S«Zv#:ˆŒÛ×À¶=2õôõyq©…Ø+4®9al€÷Ë‚@XÕY[lÃÕîÙ£5–Ù™ôv×vÅ8ÆqÙ†4c†h  Ű¼òÔ ÍjEBW‚[žÝ°a`/!—†i ©ñ¶gò†m‰…$NáC Ž«È¥AÖÅk¨Žp¥É¨Šæp®Ý«óù&>+£%ä,fµ#öCø-õˆO™WÆkÈcÕ× ù1¾Ã$ÔïÌuyI@· çÑ9ÄÙã0ú¾Lõ%‚tk¤T‚‰WqM4»ëZÒÆÐ4ÓÜt´¦Ù œÓv’l~ç0È"©l`ö¥AäŠO@Î"9P`¾@!¼eKÁ­#yFiÛm‰[›ã`‰$Ÿ1oÜoKš[¦%q›Îækûƒ6¾*]Ͱ3(†5¾‰‡×‡yÊà›§Çd™»u÷S3æðЄ¼ÄtËçðlú³­§I¬¯˜ó>R¬~¨1X[M.–Yžô‹8eE’M‹>ÒœV¢P´e˜2 ˜1Í" Ix‡Û¶[ð !Ð.s*¬— ض—¢¦Ü3v9µ†ÀðÏÉÊ’d]d‘•Ç:¬Bð÷B–®#éJx[N(f,cÏêðx&C‘"ȬfaŽ0§Ãbˆ²¬•¾#Î3µÉ–IâèHY뤷¡ì² djÍ}j¬÷Ô¬Le‘o‡‰ÞàE… »žìg|µX€Æ^¸ [†)«QsÛ;þÈ®|¾Æ®=¯3î"àÜnFŽ0nwª ê@®_‡~Œ®„Z{ßiS8¹ÈË–…|O¸dèn¼–`¤TWÄ(Û´CîõѦ¶WcIy×¾ Âû¦OÆÁgZ¼øm9R&Pªíøœ„zÿbÈ_)‹}ÜxL]†q§BÖœJ* ªjÀ#?p™…vÝíÈí?‰xêií"Ù_`º–~æÝ×ICäÄ9…ø{ˆ¼“{.èS ‚~ÖG£JLÛ¹5Ò“Ýý û¢VòUifXê5Oa®v¬Núšä08èqV>91r,3û@üÁŒÍG†}øÓhyéB¢Ê,Í1(ÝE‘8ØÐ|S®×ø°•“ íþ‘*¼¡²Æ‹ƒ»Zç|xd‡ÑØ_Â+„'H覽ÿ“;±[î÷d¸8„K?ë7IRB|ÁÏ=Çó[)E‹l³l ó;çš Âhë×ùPî|æDßïóécÇ'¸¯SÛ¥½®b~U¢¥e[dð]šŠÄfUÁÓ3aÏ(62b탽h%ŠÁ# Ò š’ƒ‰I Y HÖiJÆ™RаEïIÙÐ é|EP¶ü¥YšªâÆ4A‡¢Çhs‘š:oJ÷à¼ÜXŒ´ð8ã†t~~ñKÒWÔ%–3,âÀX³]“aDY· ’+»Û u©ùQb¶”N)„lDD”ÝÃ<6eÕÒ~je<÷^“Õ¿VÌY òa¼€1í¡ŒœÌnV׫ÝÁãk[åp²Ënî)”V ºZ8ÅqùØН©ùå-U­Yz‚)©f )¡L)ãÝnÖÂöF爺ÅV‡/´¾˜ºÓÓñ•[~jÍÎq­ Iª_ý`d$ ΞÜ.ŽÉÌRÝú¢DÀl /"–´UÝV­­_VþµXËâNM­„++“-÷xþ}£Zˈ|0§¾bxZW™Jò›×æ> ŒÔ{-áÌ…øDÓZßH´‰:~/ȲHèX‹¾#ÔU ÀË!5©sÿ¥UI€yü6¸MÕ[F‰ÉVèâÌ@.E¨¸ÖÓW{ÊŸtì©ÌÅõ‘"z‚탦˜ÄST?P¦7BhÎãɶFPKرV–´²Òܩֈ&PÒˆ žAF`K¥"8³¢ÝZ ë­i³\_öOâòÄ m@¡ÒÏ÷oã%g—a|ZÁÆÁÆÅXìƒÈo_ÍÄ0'b½‹ÍÑúT‰ÈÈÚå×ÞÊÍn(›Z'¿ƒ!5´†eËõœWa«–üùáü…£™}¿tçÇ:…T²k×aœÀùGëE GB9,¯p‰³€ËÓ|„F+†­ö%ó|ÇcãÑÈä ù¤!¡Á4ãÃ{l7*86 ²ïøªE÷"ǺaCO«-.Sƒ·È»Ólx3‰FFRåexÇĽy8êf)µ¥í„×…;ɸ9n¾Ùv㋹ôÁ6PrbýäG• ‘ë]óXØ>¼ÙwkP¯ÍÚïá¥:8ψÈ·Øæ£Â,'WéT€mK«©B<ðûG™îxÛÁ–$o “|áI¯ /ñö\F³gð‰^«&ø åÞèꄇ¥¡×¡µP¿­¶¿%·æýÒ²½&Ù±]Úý“2 ùRϬœô3velì¿Ëôc¼/Ãdv¢£àÞl&Ú÷4cTò9¦äiM¯b¹d´fv@·yøþŸ"ïñ²6o•6¬¸³x[N·†bn¶ÓÝøì²2>IÍGd•²Ëή€ÌÈûñu›`¿ë†åóÜøÙI¦Éǹ€ÍKÞšŸùæÛc°€°GÄfÒwŽ)RcbþÈiãSrÝ~Ù½€úa©š×w}×v7KÙ$#²½8äÎÿ E>!R¼ ±wŠãnçñl§ì;á²ZÒk³-¯&pXÓ~½Ågì¼ýcÜQÊe“Ïî|ãZŠjp{o"˜ à¿b~ðR¢à°ü¢ª¾rºdÿN+K›¯ß¡ÿê¤($ñ¶ÚF=!p#PÉ’ÁJpBphü~›OÌâF™f`n>w¼ŽÙÃÛÊEúÙ6ãfOpðÅ5sb'ØCÄGhJ}1ŸË¢GÓBÀE#74lZ*)M_¢€ Ë”#ŽçN¢úꉙ¬16‹G™éfݬ!A+ù¾xŽAÏQv‡ã)Œ“‹‰#ƒlÎGö^¸‡?ñÙǒÂD¬Cª<þ‚í£8Ó…±´O°„¸x5D@ºÎÇ!œÝŒ¿ ™ŧqbæb´þAÅÚq_-&œ u0Re¶]¨!uOhXŠÓ‡u´ÕbUƒ(³Y&ÈæLñª;­¸nóÖð¢ñÎt‡ÿÆO”)?sKœÏÙqq#U·©‰õÕ¨QÚï\%m ¬ÔÚ.¸»¡æó>Á§^ø¹×ïIš| ð»}‹F%Ë 3S‹oE¢÷V=ŠÃ´¥«àD´Ã·ÿrJkÇ…±@²~#om$ýIËÿ(-÷^ ;âÌRHºa¤Äœ´·ÖäÅ“Ž<ëåGþž^Í#uK3wOS›óŽ2"¥ÚÙÖ¤ž!ôf0É̃Y@XÊxÆ£]ån{·pܵ,>¬wƒC‚Ã^àL£xPN/DÆ ¯â åÝ­‡»ˆ2}¯nŽÝ°ž~óðþ¼1ìv{öò¹Ô3¼ÑÀñ"†È‘B¦ù•™—ÕñR,þLÅ„Û^ÄÈV«:7öks ¶-ªîÆ«]Üv”KV†KVVÞD%)DBkµV§hÌÛçßàR_â rÇÄsš]mÌšáÍ–<‡Ç•’›ÀŠ,\­éĺ«R {©U%‡p5œiºßésW‹ L¡QPš"Õ7lV‡‘Çô ?Q´¦ß«ôbO+Ã0 ˆY®d¥[ÙÔeСʋ¦ZÆhòžÀm˜±w5š£`$,›ÊìzfµÇ¾­//¼öA(„S÷x#p¹a¡¯ø%²û½^åæuHdzöû×yÙ¨€±"Öã‰h‚Ô}F‚{ChØÇ*Ûj—±éòФZ-û]?“h@dc;¸ ¤ì¿ ãßà5f«½­÷j!N³ñ»,ˆ…·!ÇÂãv<ÁèçæÖÏ#ÞýR…üL2hlÔ7å«eÓºE˥ш7,#=ØŽ{ 7Dÿô÷1Íê¢/Í3¡ƒ¾è -„Bž" ˜m_Eji†Ëæ¥e ó$®ÇŽzXfà|qå™_5Œo§Æ(1\ÛiÙƒø}¡‰kP y<Ô1gE³Ækë½ÞÝX¿ÇÅV´l|# vlÒ«iÅζ=Ì–=³Â•7º¼YÅÛ ¯+øö)b;û÷˜UB7ØcÎáÖt•ýÓcÌî¼Q¬Îÿ—Ù±ÊÁ‡É ÌEQž.l¨›mWN Á/ømžñ1ÏwÈß‹£!°rعƒÓÕ-½F£YÙò$ËÙÑs~¹w£8&¦±¡Y…¼‡‚Ò&—ü•˜ÌhMÏPo!Õ' }Åv«fF÷ÆY†”w[p\3söÓûÃc•à5/wÌûQtåñÆ) hüìœ†Š¡Õn]Í•>rèõEVÏ}u.˜«¾˜¬ðÓ©W«¹–šzíÇ ´—g+Î/?ï—yÍŽl²‹øVÑ=ÝyŒSðÙ€sìjȆ鋿sÓ¾â‡Rú&KkœUʻ¥«CñAŸbi;üøUoD"lj¤R'T@ñUY¼vØ¥-yA«Òy¬–P’´}Œ;aÕÃ|1‹<)«:Z&ù×xÊÅQFŽ< ÷P³›`žÚ<©CÐÁÑ|_l*FƒƒŒ$º­÷­‰,ÏÄ\ܼô«Ñ"¶="dXè=’‰Þ(œ‰¤ÿïÛ«Cbm/;Çëi5u[µ™ô!é˜ Evî•ÊÞ¸¬œ\;ÖŒùÞMÚ8-^%û˜©—hÒFhã–GÐI¶nšÏ2}Hc¶0Îæsw¥Òþv¶2²“;W²‘HÀب‚xW´{`Á:ÎàH§GÝÕöòó_ΛÝ`_¹ƒóTv#åb>x GËÃdŸKëBJŸ—þÊGù=0Ü<· >&Ôøð;l1­KÁbª‘$Ä`G!`SvÒØÊµ=ë—]&ûzoºD^0u1ªãµ‹éVÚ#¿ºÃ+q3 ý  ù ï~ WñmĬeðžÌ%„JX’ DØ–uÚŠ®æW=Æ•Üî‡ãØ~Žz¼ÕЫfÀ%­Ñ>µ*¥VúÓËŽš{þ°XŽwpŽ™Ã ÁjÎ*Æí† (£=ܦ¹_Ö€kÖÛÏ„¬ÇÀg ¥ 9WáÒóÛO¶þ‹ª ’”̭ʸ›N2^*—¬\èÓ¦Ah¼Á¬TËÛ”#!þµZS!©wÇ–Å{¾²yƒäqí6¶\“c¯6ë\UbÒíæ£àŸgÜ"µõå0« ñÙ P·¦‘+ˆÙVÎ,½Û í ®£Þ-êÜÅìGЈócž‹Œ25Ìûkh3Id»%I#*át®ÍVÒŸ¤×'CÏ¡[ª÷À ½Öî<®ù²°Oßø K6—¨;\¦‹ÀCŒæ«AvcŸÿB#¡å.KNœ°ÅóíöŠº¡ò£¦©ø{Û‹2µ:H‡ð´:I‡¯Jù&âýú¸õeµÑ~²H‹3Û¾øùsvxCSÂx•8·•›õçõXHHX*5m˜v†¡™Ð³Mâ¢RdFÕÖlÎd÷´äÝÓA L‚A¶ÛkÏØ—?‡”½Ú¸RSøæý¬øÊ‰¯ ål›îëÇ…8ÚÊ )2¥Ó„ß L @ÈñVžB(·i;Hv³Î÷Ž\ç˜Ñœ˜4ƳUñ·©¬ý¾ÿlÔR ákmßÙ"ý:šï.O·mÊŸòó36%²OæÔ#Ú¨Ýî©C†(.…B5¢/óQÄÝxJœŠlþ¦…‡ÍŠê•ï’é]!niý!¸w)6¨ùÇ©‹ˆóédöwj¶Ò)yÙÙ!,åzìàâ=N×là/ÖW=³usD R2"Pf%éDˆ0™Hg¸óÖV<±‡ºèK¸¦À?ØõèÑ…‰îÌkÓ’yŒÀN0øý6²vsŽ7ÌÒ`¦î\@væ¤WèÌÞÌ™…ßëƒ2êä9œ‰&îs}ù2oÞ†à¸n¦Ê@—Sƒ6séµð&z31æ/ð¸Ëâ’gJ¹°ø8 &ÿõoÕKàß>ù„Õ-ÅHÃýf¾$¼|W} #ŸÅȇåÈ‘wM¾Y’ÇãrW/Ÿœ[•AòIwm¿U“é;öG÷,›aª•[ñ’©7³qA ÒçvyR›¦½ ²+W!m©þÖ<¶8º µ“¸›úFÜAÓ/jð¦¶ëœ© >6H§š;ù •3ðÍ>:ÅGK僵V€ —q ª¦Ï)¬³Žøì·;`…Ã. ×Èê¾­s‹£ ›5©,ŠÀ £€z†­pMÜ0Mc¡Òàÿ•M’¾Êãî F d±«¦'Ý¢ð©Q^Ÿ^_EšÆºb‰{݃šÉÔŸ±cA´€yV.ûAá²[˜½Ë•²µó†NjP¦†>1q®18‚Ò)F¹.gs“½äÀx‡±¹–Âõ˜±Ü9ËÓÏûÝöʶ%ÇL6 Ⱥ_¶ñp( óº‹óS)ØPƒ;%uÛhrLx¼ä/ªÌéØç µ^Ç«ñ¸MMjëõU¤r®ˆÜãë0³hàÈã-ÎnU¦U5§{ç}ü¸l\Ãk Ôò¤_MºÆýåÈfš£áó£gØI8l,H,nÏkv¾š5€zÖþK”‰?,Ô"лÔÊFÖ´=ÂaÚF Icw_ž‰ñ˜ †­HãbÊè*Ù+¶ªìýGO G 7àa¯(Þµâë¢ù.,ñ\Þ’ ×n ï`¡’çú+ŠÉSij7ËM\CÜ:˜ÈyÀ‹DWÌn¶{­ä-N L¥4 cþï¶.jAþ„Ës¦¿ŽÁ ³ƒ\® ¾FÓàò‰õnÚÜóÞ5®ìgd æÃ}ÿõ6ÏhA°äkúÕÝÅø ¦ÓDO Xñ™xÛEìÿwÆØt]5\Çüx[þj×÷¼ÇìÝd'¯yåTþßÌ‹ûý-‰3ŠÜœ¸:Ú®aÇÂÆ§ßÏ‘™ç“ÊNòñ°Ø85åàß·äxìæò¤ê¬/ EmÜBÄÑu!ÆN àÿź78®qâ7ø~dò!nÕã0m—éÚo!¬õÞ"¹¶}·ÉàÈ–D5ƒ Šßƒß=H™e¸gäÄh¾ÔoÚö³ÑÄkc…¯úݘ[)!_o¿/pœÂu½í ÍMXc)W€•±ÊáËj[ž‚²âÍþÚ¿á¶ÊYá°Kîd|ך«þ›Û8²fœ »P¤Üv¢ÏST_–æ­ö‡©r¯Õ‘D~æÜ˜^s,‡Ó§G\à«£v ÚNÁ7åú é›L p3)š±>Rªi mhb²}¢=Ûz:5`xå8a€r,]/û‰CFÅTtIjœl`Ì›pɃ¹ ™8xæ±¼µ=ÌPã·‹Béò·¬ ¬Í ù¸˜8™_Ö¹JÙ†¨¾D¶#a,–óû·zÚG´M1AŒ  ;æ{Ú‚‹ÞsnÌsX+ïúZz¦«@òâýÎg„F)ÁvýÑ;¶_/åK· ]øV•„èj)®?o¤GWŒ4¤â–_—ø)uÍžnšÃÙ>»··Öã¹Ýs{}ëÈ~Ï; —®G€-A;›ƒ¬à®8¶w¬H>²ÀíÐc‹kƇk5ºÚF‹:fª>ÜÝ»¥±ƒYÀ¿Ž6ØzGFFx|Ù&–¹ŽIü+ê‰!Vg$°ˆV½ß/Øê­a1Ž»;èíˆÖd³ ¼ÖøÄ0^"™f§¤_VÊž÷ŠJ»×N˜W„âÝxÑ8wº ±<ß±¯! -6 †ÍŠðÀp­;:ýµ;ëo»þn?pÜ(‡´eDèriÅ‘7ë8!eèÉ>u´ž,µ$•–Dç7q‘{:±Í6¨êìZ–á/ Ë&ˆ}83Æz„±4(¤•ÁrD€vùaoz¡á× Ö£¿ŸyÅênDmtzai†Ol{‚ U&È]Q²È®òÂ&¸ñS¯§¸þ²Nç¤Ë=Ý®\5?6l£ýxæ1 ˆnl°ä;º9Ͳ}´­Ä»ºv¨Íoæ®Rwñ –ªû Œ:ä®m*"â’"sƒ9 -^ž¶ÛñÍõ&0w«CíÛ¶Ãu!(ìUR4l•jdSd£Q´;wŸ¤Èq¾[1Þyxãj|…³È²^Ît¶¯&Îv=~pQ-‰Êl!‡{¹†ýIóGh}“dôÍ®¤FcmsœúÉEz³n·è~aŠë ’EéKæ}¿<³«pyµð–m]ÇÃ]ñõXƒÈX ‚‚ü_Òps¬4XŠþ3ˆêø+c¬q58Ãè^â௯7+kµ®ØD8Çèf_¥V?eÑÀ T¹õýfÜ/}Ĺ—Ò&n‡—3šØÞã†z~õ~r Þ('zeÍL–ër¯¶A{°TZBÕÂ8`Îê#§A ZؼnÜ(έF¨øÏÄ\åCM¬F{§F/8‡'꞉ÐY¾Øúx@O½“°±Ë¬©­_ó íñn)KMã·ù¬FA{ÜÙß1åt˜üVÂeœ=b±T`òŽGŒÄ£âX'Ðñ†ÅtÑè×ñY@8ó…k=æÆ€¤ &Ž[ ó¦7pŒãv óØfŒøÝßz.,ØûXí±è,8wå¸zß¡æ;£{H" “潘1ßšp£›óîÏêDÞ‚QÃÍ!Åö©Ùºš¹[›ÿV½ÈoK*mï“~pCO¯¦Ùö/ÕY‚÷ bӠز¯4œ8Á–ÐuÈ—ÑÖaGš †…¶—dÎâ¥VS—,{ÙÕ ŸÛ¼#©n(r–ªS–·™Öƒê M®t{,%fp´¹ÛL%"VÐJmMq2סêÐ¥u4ôšíŠÙ1Œ¶j¯ƒu‰“çÓ²g¤rèÙéƒ}0µHsF€žº>h…o“‚ú¶ 25üzlšp­É U[ølµY¶.mk"𵏆V¼“úÝ™N¥z+üRÔú¶-ÏýÅ‚†cN,ÝŸÊ7yè°ÃHV&Eûy:wýC©¾œh@•A»9¤ÁNØÈ9³êT€ÆÇ!å‘ëõÓKýqÌ¼ØØ× ü2­ ³ç–(uQ@“NÆnh»zo®Õ·sC¬P¡øqëI?0xº½‘Ö“ Ícð @B™ºLZS{ß ñ¤î¬·âðA±-oòÉ··³oÄ\|Âü˜}3wiØ7Fž‡ÕÁ¿»p‹•ïÍoÉý\Uø¤Ç-á=æùùVA)¿è¿O–cómC ùç÷OLú}è¼Káøº|ZZ4n"äxº0`æ<¿i˜ûHªfÅù,lVwݹðË=D PÐÿ`¬øÿΑúÍÜ£QÑ$ߊƒ"ÌÎð~vÓš ë{:ɸ~2OK&E>“"þŠñfM Œ„! éØ|ŒuŸÎÌð_JcGaÓ~"6tlœß¾ø‹4ˆO0~çîÆc’~{(åcÁépr?0J‘í ã3ÀeµW @#Yd³ àö­Ç>•ãPÝ^“¸°»×ïÌù{i.„ŽâÉÝý,?ñËôùÙÐþwô>2îòj¥ÏˆiÜ•ãëiw•i°YN˜ùãðÙJ8]wó&·HÛ+b?õÁƒE´Óî ì¼pýKÖÁ¹ “÷¿·G[ûÇæ÷(nY~rõÏÕ{ýcŒÞÑJØÃ—“ß?î8FA˜,iT*Š{*%”rzšwdԑȧéÖ8aYú”3\ñ>ÆwŽ¢áý_’ͶGÿ>2~·å&WŠ%dÆÁì~-h~åÅéø®6s¥.l 2]Èz϶.·.íÏÝïþ€ÿêÞýª{¹Õ9´Î8@7 ¼ÿ“ê^Cz}}K;K}}:OÓ$Eûl”н'긤tYYÉÍðJ­|‹,K \BÛqp:R#ï6Ì“½;!¥ÊÆoŸïÜ`ôu nŽºì€«—X?çƒvÕ„Àæ±Ê±y´fP¼‚›J‘ri'X¶ª„ÍÃÌtκbÖ¾˜ O–?/j¸à àÈsžHPo•m¤óZì¬PÎÍ´T:Þp³®S¤ÈÑ«!œ ¤V¤ÞTÝEpT²ýk¡†=õê¾;bÖA±±•êW’çêp“fÙ§›Ryñ<éÝáÅü¸\^5(W„)µ¸*7’f±Ý]G]zF·UjñC¥&ø8xÝw•Uî\“_Š„®”1ž¹ÄcçíQÏ$úÑ#Ò¿™¼WH÷nŽò-œ ¸lš^ùWÉ´©[ðÃgÔþ¤g?Ȥªc,¯f»ct <ë§kòO…Lä„v‡ÑÐã%×r+!Ûߤe$ð´íÄŸwæÞýÔÃ¥ÇÄág¬ï¿Â¢ÊÞJ¶øqB–eæw7Æ<13ÑcmÑ+hŹ_ÀÿY§^{aÿß]Òþÿß7ilü;3mzX€ž=#p 02J#@H¨LrX˜$LU(¢di&å§$eæ©'g¦gjªÙ“égåÕåg¤j©H¥ÁÂÈËZ/ÒO ÈÏÇÇLßL§W_Ð\±GÆÎ_ÊB¾g"Í8¡ð?©[̘҆ùºóÿ¨£ú?韛©ƒ“½›¥³¥½Ýÿg¿]“í—Bï~Q‰Ï€¬–ð€,ÐF¥T Ml¯{—ÌÜ3u×ýy›å”}Õž¾O½ç¹ƒS‰'Ûžš{üÀ÷LDÇ2ÇžjJi²sk¸k ×X‘Þá[‹×<™Âñ%âN’iTFο?¹cʃ1±á{…Ûà°*î5 _XUN RQº„0JîÊXÍ“ChñæÑÌî@«A¯hó³¤q!øÃËŸvûôi %F@²1¬Ð/I\¢»mZç¢_°Áòø:× r‡özIâºð5N+Gò(S1"ÒÌwAvKÒ»r…ÏmB®•¤Ï$o½ÿŠjú]r>”\!aµÔFî)i›Œæðà7n§OýÀá/ܲO»mtÌ «eý²ošdçÊçNãïY§íÏtPë‹Kæ˜þE¸ž×ø.d '4kLŠ©åH·÷Z j=…”pÂi”»˜sUoˆN?/Û:ü…I•½¶•oâøàû²ë·«X¯k q‚®M>F¥* Ÿv’«(½‰ëÕú‰*“>‘ o– ¦²³„„ñ´œOG;n4Ö E5¡H¥’ú„ä?ø‹<ÔŸÃÿO~RV¦þ»#ø êÿ[Lë&4B1„-s "‚8::¥jÛ #‚#²³ƒï€@®¬jUôÕ‘_ZdE`$Å#<þ–ÄËy÷ìwûj)¨™ìÌÁ´ÁdH (¤H&³Z óQ2‰éž#ÇÀU¸(ž¢kolIz¸tÆÍUÓÄZ õ¬•º­®©[¶À÷$|‹Æ?"<>¾ª…’*r…gŒ¢¯$5Ô×*ÕÛ ¾o9ºäÊ90¡mb<¸‹£(}o#F¬nLDk[$®]çTo^xÒg‹»$£ÿCâÖ#¡„Ùì‹Wôß ûÿ?$†NÆÿ Gª²®ý2Šï|}G ¼u+ÍM˜@…|!6£Ò?7ä‚)Y;%é8[sÍrü+ ø}rr>¾³mïøîûLs1MUŽÿ°ƒ°éOÄC¤Rè.Aô1©zXzu8º°3{¸}cÓ¦GRí}Øy­™Â4N,êf›Ðîd„¾¢Fðh ^øs›x®Ÿö‘5VX%Î Z$Ðv‡À€xL+p¡2*ZŽdL¡áªÔ$ÇþRéqˆYˆú‹jŽÀ-•Óµ)N^,˜¬²H k6¥2œØ8»¤W®n>âá›TmÊ-†÷æÓ0)ݺ@ËÇäFÒN”@Ó€T°‹Ê»vÆmGͰ}ïU†à‘$ç+ýÕ[Íf·ÛŠSa€ÖhÉóf 3\ôù$M ÷Éï£c78÷¬ò§c/°dÊò U&y2JßÞpŽÞpyI/T ¥j 0üÖ‡ßI’7Û‘í7›°÷B¶°š±tžâ.Íf¤Bx ’T`,ÙÑZü2RoÎF=ǵ²AŠ˜ª¸:K´“ŠX¯ý]úh5hpíÄêòâøcÑÈo€¿~¸ˆÃ_TA2 Wn¶KR÷ïj~+×I4§vgš¨"~ ó =eY»ãáƒe7Oö+û«,I»{¥>y¤›É¯Á½ ¢êWj¤%ö‚óvæÃôP5øí°¦Ù‹z¿ÉKa®ãî"âö±dyo\)»,’ü=€]6 ‹ÀÇHóröÚD½-QLéâ¹äHü6ÝV‚Ï&g[“øîò~×…ÚËÜÅ/nƒ‘2­žÃæÏ?äªÂ¨˜žÛ ûìÿsâ{£s·HüGù.ùÿeÊ[•dí‘pïì”° P¢¤à Nv «8DÞ& +4iç6%q=ù[^Û@K˯8ôߎH`Kq*Mç72Ûïf×ÿ:W¾|ßœá¦޼"§{D‰˜˜Á˜áÍøJúœ½ €)¸Ü Î…6AÉ|p3}€9€ß3ì^)® ’>C1¼:ZÛz]•q)‹«yéCÓb{=y'hµºcw“ñõä8Êgz‰¦Þ%RÅI½DØøHˆßžrh‘%ά¥º¤S˜2ˆmã]ˆ<à Š€Â Cµ 7µØ"—`eÓ*•6hœ Ù *É Æ~;TµFŒ cb[¨¶’{“Í¿6~—5äÊÿ6Ù–“‚´QT—·ØÚvw¹ÒJ'6¨Z’KM¯RÝÀÉ&.nt„§$4‚•ØÁöaCqžÊVNU:¢o÷â×ÓʘUÃ×0ü¾ê˜ã5ѶMºi`ÔìILBgš¶Á‰’jÅŠ¯)¥÷’½øg*&Þ!üTN³ý$œ¯‹«äGC(ꡊªŠ©Ø#çªçMÐÚéÔoÛ2àýŸ.âÓ8ðÔ¶AJ:éºúÔITS??‰ZêoëYö ·ýìýÏÈ•Bø9ýÆsÿócÃÿ¿`4¶15tú_ÿWð*Úo2`øÌ׿ƒÐ˜rUfe|&,ÊÅ® ŠžWI%'Hö‘äÅÜlÈY¾^½qвXú9>}ÜúÜ«çašªüªb‡xÓ]©GH§ÐƒûfPî`è©ÁÑ'Óßg2iažv$ÑÞç™SО*LãÀv@®ÒTß7 ±³¤í;`Æîöþ(Ù¿'b`©§±¤o):@ƒ¢C¼gº šXUƒš3 ¯;ª·Ž_Kyì*Wć˜…¬=«æ¸Î}Wä,//Z´o‰Ö bS*lj€·™¸qËÛï%î»M£XÐb|k: «±Ã2Èz1µ¦Ý`” j9O]í`ÞvÐßgÞdMvº†Ù[½‘Õs=ú@td„èå%½î+çYJàÈO^Ú=t•{tÆœU³…Ž"™EÈ6I¡9~Â<|cö‘YÕ/#õü/vûNêlƒyÙQ[/h»×ixXꬬ\܇…' 2DŽ …T؃¤Ù_$ß-êóÇÔ“%ŠR|Cï(ŸXc+µI‹7 ‰÷ÐùW]Ç烇*n6Øf‹pZé·¿ <­ ü'fóK¹—(BîyþðÉEOŸ‘ý)ä¯A3 \¸™gZæïu†÷¿×6«ˆ>ôœ'w¼x(¢ª±äÑÎO¾&ãÆÅqqêÞ¾‚ˆkÿ ``r(¯¬p៥/©uUÝ?Öõ,(¦÷ôÄ^g ×mðUbK|¼˜Í„%0›R«ÄáøM.¯G,Ã:kkf/C°Š5m³ˆÙ gÉAûÿ™­k;(ÇGÿ •ä8ÓþßÄÙ¸5IÖþBظœ›6Õ €gJ¸ ºŠ£„U`ˆ×D^DBP6u=¹ŽÅ¢“–nù/ºÀÌŒ_ TöFýwb&ÏLþ—7;?¾?Á ‡þÑõ¼¢̀̀ÌM;0$[ˆÈÄ%£‰1‚)€HjS>t@üœ¨|¢SÿCg ÝpJ{×M}{¬w¹Ž¥ëÙݯŸ²‡ŠH¹¹Zõm°ãðJ¿!¶CàSʼ"‹YŠ"“$F A.ƒ`Sž9JK<:øÄÆ+I‘zŸL)Cï]Vi`QK­•ˆõÅ^±ÜXÚ$i/ž¶ŸÛË! ¿gÞù‹â^˜ÜSe–ÕY²éImq A¾nûšzóI§ãØÙ±ýYsxµ#·ïö‘Áwh‰\ajh€ºÝfùöz͉/‘Ø'ßbggîæÞ çaŸªZü®f„`×a‹GP§Ð]€íbPó²õhuq`äñwÀ§.N5Œ£Þ²÷ˆ[2Œ¥YÐ&É:‘ÞÎ ¹G%åÐ>÷0÷þwÑOÖjU`f0%hœHÑš â3£È½ä¤j=˜9ƒ¨¶M«I+)‚ò¦ Õê³sŸÕ7%ŒÓ kú«ÉU.Ù©Kcºüw»³¸Zîè½ÞªÅ»ðôe9húHçÞ(R!Œ@´>aß:4Hx¤;„  3â2ˆ}Pà @à„¡\E†[Z¬ÍAd²®•ºYNÐ™Ž ë àµlL| ›]çM¼»|~]É·<‘+&Hæú[bìÙK£®hÓ¨óâ§©±—ô¨{%IxÈs¢A¤Ñ?Ñ4f@r >Ïû áðÌ·&åhr 9 O“Ñû‰û-ÐÍI”@Ã6×…P#þøÅ;{²`w`ömŸNž:&æºU¶´_⛿—ìUp©1׬1¿Ô:ÂúB£§HMES­­W‡TÕÖªS"4£¸ÖÅZå°öYD¥½'Ó) Ž*2TËZ=Ü(N)¬¥täiðL}¼«ºº(M•ÕJ1})»Ÿ_*M㽞l#]ë(† ]ä¦j,®¼»(©6°v•9½A/ý‰ ÏðšŸjê5_„€Nð’ÅO¬E=TQU1ÎØ¹•ò÷§äÛô|²cÅþÿ*šÏáq:ðÌk!ç˜ò¤§ø³§¿¾O“^h·þ`ÿgøŒÝqA(“þ_BÒÄÔÌÐÕÆå?&{µlã7;Ð~ùú‡ÒýÊXd´+µ\ïTŒÌ-ÀmÃ+ÁܪUï( "éålÁú7ÿüæszº+.I^Žžn¹é'X¡KŒª$ïú+KŽ™.x/f)ÖÈéåô mH²gY=_?Ë>B"Í‚°‡æiJt#JxŠ’Î9I{I ÎX®.ö*Uäv=ÌKBA`×V,þ` °“Lu,’¨ d)£) ßeY sBŒ2Ghö I­ËḬ̂ÀGðá„d\þ.ß³Ñ.%H…õw®N~Ù¹÷êy®";­ÆÒ^úáé<‡ÿR¬Ã|ÄŽ«P]¼¥H"¸6 }‡ ‚tij"&0KÊC»Ófšù¬V4»PˆÈXN’UÒQRÁ™Ð!tÉaõ"bÆ`§ïZ0\Œ­ ¾_ž¶3~lv÷ÞÀŽÿFlo'Egœðt`m‰/aÿ úáÂÐs‡U`yåKãg˜TˆaΉ¥¤ad@M'HYL”Ñ€t¾ÓÀš¥ÌŒDIÁ&„L‘*[È€^‹ 5“.a«¨RÌ[Ü­VÉ&Òcó²ÛÝß’Æ‘±C"Tfô¼Îé@2—5kÈ4™ö,<]£>Ÿç¡d?Ìí±QáãðŸÏz5ƒ×5£àüÞSB¢ öÃ'”eN Ãã¤ðæË”jÜbŠáœOy}*Ís±mÁ}…¢F|)Ay:T(„Uʨ«|µìŒ zw7Ãùóи½àÀ ›lìWïÞ?:ÿ­öwtþ>vòß~ ïºîý-aǵ'''GÈ j 3ç•tµ‚üw¼~!Ãð³‰F{Ê) 0¡îÍÝN„;?®"íË+åJ)(Xö\”6iw¡›Ä@ÿzb†¿bÈÄü7¶úÂõ …,çY£Œ=åQ( Ï:Q¬a8\­P$Ãî×;Ùð!E¶ ²A@ ÷w´²6)Œ²çP¾r˜5î)ÃI°N/xÆøg\ìοC5ÒP`ZŒÿ”èEü-#'óYþÉÅW˜à d¾þ¤=´—Q q*Ÿ ~Э~H{W³4°¯6˜oò/;Ëõí NÒÊý¬¹Ëý~rÛ‹»ñƒe=Èw/å;‰Ã™âË9qbêG‚²*¡O˜[æžvà:yi X­Y>Mu=_nçNŒîz^kÝ0IRà˜VÚº \Wùd:%ÍOŽ|{K› åšÿµloYÜ[2à¸ÓÁÊîq´Ë ßh½ª¹#ÆúR‚8Yi@cršþìç0Ž‹ î×À£IH,+VJÏ90|?ÓI7-²f‹ãY¦ø¿[ïsÙ‚T 4û EaFcÒmMxÃŽw=ˆNkŒ‰²@…=P9µ¹§œ„òxóU§e7…G¯¤<¢b”[™Ìs [] vÔ¹Q’¾2úÁoR <ݦ;¦­•!![:BƒJÿ— ”«*Nî%³’Ú‰aàÒPHZн5¦õÂ(f_W×ö F4VÉ6† ù3£H?BB!œûè%‰Þ‰†˜ /xš`]©‹vü{ä8ö¸MùÁ;{í6»ÝEð’PÒEõÿ¶ó¡K@wpP%y¼Xºˆ Ä`Š¢‘´UÇŽDç©Påé|}Ôr{™ü‡•&<è¡@–¿4<[7I#SÑ"í¡ñQš‚ºn)"Ç`êƒÅqôèèäRy·‹J«knÃøß‹6çývœ¾_®w®Ú9¹9ðÐÇt³ ,ªw=±Q¤ùŽ™·üÞx¦Dp™wê ÆÊÆ' "ÌЕMײAÎç^0lÑŽ²D$&šEå,ÊmÑ‘¾ «(·¾™º¾ûßÝW°u«†ëBPHÓò‚v´Ž ‹%‹¡Q Ù³\ux£Erðjš4ÈÙÑkŽ%¹8ˆmP-.Bòƒ[ ø,¨Wvꮄ\iW/†7ç÷×awU,{†×†ÂùžßÆn÷Çàï-Âõ%YžŒ±*ŽšN@ñdEŽ/¡-À×ÀdÐI=u …]±×ŸwΔŒ‘ÄRËΨL³ÝT‚ø±¯ðIK®>Vÿòc¼Ñ;3Éxªeï\®JQù¸M«•§}Xû·žÒ!A¦Óº'r`3y[ô`¤AëlWDt©É¯qw(ãÆQ–Líý¡Æ¬¢_ ưª†v¹<14Ob¶Vz$öã¸ÛO Ýk!Ðzlé™–!¬•å:óÆÕ4dda"à iÌÛXŒ1O3µjB’Ü%”/ʡԮN+Ïò¶\5èñþ3‹±æiýar¶LâãL(QviÀªžª36-”mszë5>Î…H¡iÍ;†­G³¾¨•µÔä›u¥Ù{´4™uë·ê)>y0«é Éo™–33«ä«Ì,ÙÓô  f„‡D/m0}™3´Ì…X!^— G€èÛ9ã[²­ÆKXøÓœé<'qW$Üpár#æ?¹ö–òE»É+æß ηx»´úÉ@\Yúà0H1c {ÞÆ¹8´Ò·NY¢¤ +޼¥©sg%þ¶1˜³£dc¦RN<0¬’< #|ò¯Õ‡Þ{X ‚¾ñZš=u“ì(~P·¹j”dÔ®€ž*`=¾f³û+Q{W”©î´l›=ñ8€j¤Ð²rRUK€D½ÎšU§‰+¢1""Ôaf§Àðœç-ɆÕ_"¥P>4#¾äO@u?s «>*W¬èq¸Êo¤^3{¿ì5ïrU»wÒš‚±"üb°’Ž˜ÕýÊùþÿÚ¨»oùKƒp£ü¿´yûˆªq¯Ö´=ö8fþIŠ$/­¨I\·d¶ˆ›í’è‰@R[)ÿ‚£šØ …õm‚Že§ô,Ü‹D©ŽIA»Þ§ýó*á9©@ ž—5=è¦ô¢ð­óŸ>{³4i)'úBYGžúÜܾ½7œÍ¿¬ºŸƒ«µ> ¦Ÿù°»øýì_à(€_4ƶÌÚ6M×lÛÍ2=“lQeúÆÙ¤ÊôW­º†Ù¨‹J%ùX[ù¦YÉKš‹(×òS–æÛf‹0WóÓ+tM–ñ.ôõSкF‹<×ÿZAVéšÏHWtM³¢ò æ {Ñ4 ~‘­4h*g!òìôÍf^äͻԵ/¥”ïúïím&‚ªrã¢Xd—a‡‰&K2Eª„H…]Ö -ÏI-ôÃ/º ¾¼¼;Ø0½P£?'­©ñêºwsäðø¾=Ÿ+ÚÚ~—¼Z~ß+ºÖ‡,ßhûÞàšº>q¶Ðì^ ­ÊF‡ªdÐ×qÞém•€œ«é áÖ„NÌ#—JˆùãZ^S•ƒ= v¤M!O‡zúSTÿBúï­Bþ”ýSpݶ%ЦmÛ¶mÛ¶mÛ¶õ¥mÛ¶Í/mÛ™ç_UqoժػÎ:óa¼Œˆ³£u´Þéä•ûfÒ©»lž‡(,l@ ©.1^áíp»k_o¸aÿÚ±Ñ)Óó`L‰ÍÙƒàD4 xü}øL¬~…‹ô ú ô:os|fêÒª·"-<œÜ8. q‚¨â%Ç¡àpØ¢ ™3*µ `RW,‰,¦8±üˆ"ö2*&²fvÓ1`tÙ,}ÖÂÛZ¶$,à…Æ,3 %«CžYEéiÐ6’KUã·SA¿V9tiàÊÈz(?&ÜÆ¤1ƒ­és R7 „/Ç•š«:Üd¹¡¬’Òÿ2h†MÕ£R•Y§«×gJâw¶ÉæõÁq<uË`2t§ßؿݟåšÜDÒÂgyéÚ~„0Ë]“ ò‘Eíuèxöþ,e?“Tjs=rÜȪ™|7”Ø}=$88`)õgïg!F@‡É² ib-—ªNQ7*‰™D¤F.Gm²R·¦²©^n9Ö.šLY Ë÷¦¬œ®7È¢jŠ™Û×gFr mZæŸ â¬¤™?}’¦É(›E£±`… zE‚úK)ᛟ² ¹„º)³f›Q‚ÁàÑ%s bX;(%œ!œzL´ %ÚÕº°*|‘޲£Ë/Õ!bñQ,­$ÿtȳõäƒÕjÌ_xñì ¿Š7%­p-`B‚^¸›œ›ÅffƒÌÃ?ÌJ­% Èó§µÔPl ;fy‘ S­œÌB‚¸x‹íå Ö²%Íèx´±ëÅàú¯uœÒùÓËŽ¼ÈÞU¸â‘#xë­½(O9w™ï öÆ€8™\JóÐú½²V~­ fÒìí³ÂÓN´ŒöP€8ÞìãttJgsÔÂFBÒÈB7&¼n&$5’n™m¡¥¤d)€®[{>+^yòâ/ö½¬HÆÍr††Yš)áÆuÌU¥â¸Ä73ì–¨›Ž‰D©› ËÑ_€‘;Ö#â-¡óm"â¤ÞÜSµÆµÎ‘ÞAv§ÁE©GrÒYí]Ho8„é›'eº‹søÇ!þ>ß«·2 Ü€6`hPw.ItNze²‡P-òrjGáÌ„qݯ-<›1RCä݈¬ô»y)‘µtN+ž3Ø„N2’öÝÁÁ‰aϺPé>>¶GÅìë¿c-ŽÞÕÁŒ²yJ€5F3Yøz G×ΠòT³h‰»9ˆ¹TÝ9§£L ž¾ß| ü©³pü –0¢^Y_¦Â&Dd¶}¨bÙÍØl"kCnRx¢Ì‘ÚNrM”²Üx4W-E&ÖžÀ^ñá½— á’O~<¸vFÁ¦r›Y?e+šµ=ƒønÌwË®9û·zê'˜­J™AQ)BYóKƒÈn0È=žªËò| Qœí•ä&%ÇŠ¯[åS,¯œãdÁ€¹—mAc).>yÕõ¥êaã^D:Ì¥ëÈjJéO”^Ý5¢‡6•¤RuØÄë¨Gˆ÷ÁavGwó$’Ö„Œ?TšqöõœFž>®¢z¯66ÍYrr–äVäG±B9Ey‰HQx‘á¦T Hç’-èá®\PÎd¾¬–yòá–ÁàòfzÔß¼ Ú:Xüéò”Õá ë2ŸW€Ûò£# “ˆSsp©æ±"$fÄi?dçæqP»Ùõ<Ù–·¼ÌÐcû¨FtÉR„h–0÷òž!ÈÁ»–®; YâÑR§¯Ü%°0\kùu=-Àneû\…eÕ÷d&Ö±çINÖ²ŒK}dstäÝ<Ç›*ôo¢_jm+ß%ƒjíwhŽ]PÌæ+pYš\šCå°RVŸ –Ä$eÁÛ; ©’›®U©ðO²•W,)͇m™ƒ¶y%êœ÷­ó8>Ï۸ϜJw(9d óÞZ †7¡á:l¾`‰G6Ÿ!¤¨â¼åµW:ÿ•c:4¨1Dbé·»BÉ|‹:2I dk˜ብê¾õÙ³76PVÁ¡òݧ¼ëRT‡½¯Aj]ʺ8UR±Xµ qeE'\G¥Ü-!ÙCÚˆÔS*¬‚ÈiÅ]éóžÊ¸Î¥ç¦ÂÌ(dÐÝ‚éyËß³’Õgã¦Ì`¢hçêá“iyÏuâÊJqŽñð)…«Î&„8Ë19^LÌÿ¾chMë²orô¬‰ÖRºS¥zAŸüÊÞ‹¦Kî×iù Ôvhe¸é5â5ãÌW‡î’°M‰½ðŽmŒéé” ‰€- ®0`s±xЯ XÜ&¹'€ –Û+tGÖ¼ p'š’oð†¤ ŸÎr5Œ\½C~Õ>gG©e4Fàš¬2A¯93¯mØÕg€-à–Q‹ô+A¶ŸšÒÁ_‰2&SÂG.i©/J4 Qu¯G‘Ô=Ü+Ü>’ê²Ü¹ÐÏÈ·ñšKo"A{T/Ù,«{ûÞqºðñþí„ËåD¯rœŽYÜ^ž06!+·É–Î]ÉW¦/ ›§Ú–þ‚ «â#¥¹”aØŽ¯ïßÐ5㢂U~ˆWkx¼ÁìÌ×培žð«nîØg1ÓâKåKj["ª°@ü[±‰ !›$Ö§s}®þ†ÓÇHÁ–WY3IIa¯µÖïÑmYÏÄSMuÿ¦£ áÖ”?UD—u§@7Hz›iµÇÿiÈcåD™$,U\ž¬„ˆîŽ)°ä]Ê[{¢ÏÂg çaçß7ʾ,Ñ¡2ÐU’-,˜Ìã?—Ñ[`¡,%uze€6¯|ŒÕälÜ õÝYw.–G¯t¬oY³B9^] ¯.®Ùú­¼pwQ9™Z±HuÜH)©óØ +F’ÂM4ÂBHP4ºÑͧ†OØýåù$2޳RçÜ7Ú­Á=š}c7¹øœšÅ6fbaß+vµ±[z“álV#Z~6õÙoZ˜ÖÒÑ43Äö B=  È™Ô$Ö%úƒmå±…½ùððpÌ'2Ï ô‚.ÓÓ]–ÕÏ݇cûÖ©†O¬°2'¡l£Ž­üô–©Ö!$èǘ¨³²)5ª° Dù4µO”°X J,”%òÂòæó$·D7,oÕæQ¢¡rºìsæÕ3A͹ñý;4¹’èÓ!ãÕ¦¢NŠ'u°¢Û­'71v÷â4“ˤ…[ñs}ÉD2_p‹UÜ;8Qä›ZßiBG¾J£$~ø|®>—îï|ÁT2úöÆíÆGÇãRxÜk«†V'ªÓ¾Ë×òBDùZ#$6®tI“ò©Ø¦ä‡¢pZü§ .g²^™ûF0€Ó%ƒ¶Œ›PwçYp]ar0.Þs6î¬÷Sþ$^«È¢ëæäŠ[ÂÖÝ ¶1%ì!Yø’)\ìd)v٣܇ìØ-Y½54yJ7´îÓ¢M †½Ê^?P°VŒžÛ¹I}o@,Z§Þ܉õÄ7P4-M:UG4”UÙñ‹ü4 ªÕ#“‘ð¼°)ÍLËî€iÈÖäg¾àzkw¸lí9ÎK«ï†3gô"·ÎWzø<Üøš›B ´±ä*Q¯__KU‡³YŽÚYáH,Ð*8œ^wýw£©Æf÷ªp9¹›pÌt]ÆàÕ"_g$*B•²26~}ÄP’B“ü&÷9«ÚöO]Ó®~cBŒÂ~šÓù´~·ôjR–WºRb/Å€P0N ¹ãÕDtŽƒºD÷ ¸H'Åùsâªê|£Ícæ µ`ÎÔ:&¶Y||¬á<&…¸zÕ©»Ìš´JÑ7™¡ß¥•Ž‹Ô ‘g¥6n@âãŠH+GÈ9DCvÉË vÅ2=Ù.zs‘”˜«Ò¿2øcÕKBëjµØ§scփ͹ºË%vnt¹ŸùR§ôÓðYÝá^«3~ûn³2ßÀ°£î³yf~mŠø_Ù‡›˜M[|ÝZ(²•J¬ÀO&ÖØÛ¨tq¨À)-o ß­gàÖJ›Êi?6Ý3™ñº5ºÙïøÿ^†á[¯]ßXûÊ0f†Æ.öNžÿ*‚«,Ømþ«®^½G˜HQIDEÕ! RBùç KžÉýzƒ#Ô×ëÝJ˜²ü:ËÍÍÎÍ­Ú+ãU· f·™Ãí±W¤5E•…IªìZÉp@‹hB$j»WBS߸ ®’µçpØZ`˜Úøƒ& Ë™6r#(”!Vt€e”G¸c^@G˜”Ír  ¡e¿üÊJ&:M¢ƒ%f!ö‘*{ß> ºnb#»nÕÚº^5íÐP¯T¥¦ èd}ørd°IÚë6¥CZÆBù™m/TLç6 <‡ÜHR!³VÔ¦,ÎJT»Us©\m¢U– A\»75“›Þ¬*ÊŒ4AGV¸º †òyâ·@Kæÿd̜ۛø#×s?Æ)äÏ„÷ÆìÏTºÄëqhþv+ÐU;°¦¢ùM’¤Íؘ Ad¤ ±HÆsB8Cª†©MX}M%x(e5Ô¢[Jæ‰Ø@â3$› öíHÁîn¯µ^º¿6[ýˆáÓѺà YŒ|Ö~m-ÄÕôg;dúN¤‹ÞÔF¢ILLÉj[<ò\¸2êœnÐŶnGB~¼ÔÛÆia{œBš"T95©²;ò{EÉøEJêPi”M©»Ïm¼éÜ„ñ/,ö{ß‘vüzèÝë8"hÝ”Jçö“÷ƒ¯_eÝÎÒ~² ƒl\…Îþv‹øóÄ'Y…â¹ P.y†c0Ã4’SÆ.–Ù°‘×&7/Û´Äy­÷;dÒñŒK£@SÄ~d¤³|õÁc*tGævXœŽÇT½#õoì—´u6éÄDM¸ëJ—'ü¾ö“—Bvµ ü”lép¦îêÅ©”r¼„òh«—:nï3\IÕlÎVoO¥»|)5«Ž–@Å0_†E÷ü/¿2ŠççÙhÇQ£ê‹0ÞiÔ·¶O_b|6ú~•ÿ‹6¾»íçÁ›Jâ#ú¿ç€dì`S½þÑ’iÿ(ô¿´Ä¸EyFnKág•_Rˆ„@®Üʾ—’•ê@œ DÐ ü5ËÍ{ÃDÕî‰ðC†÷Y÷àzv“°aŒkãþˇÏŸ™ÍŸ–ÍÿÇã“TÀUóÂGü·V¤ü(“8ÆxPÄf  aëÎf˜Õƒ•Q ÁuÖ#v±• }ŸUº lAh;lîšk˜è€ÔCºd'@Cl;pMp;t ô­VO¸=Àž¬5D6&;͊.œR¶5£Ør–Þ#MÜò ãˆt~ÿü,Ù› ïÆŸ'IÚùó`7'ÖÔü†‹†…®0Ét‡•ô¥iï#a×ì\W¤Dþ‹…‡ú[ âÆ<˜‚òF_¨™jæmãŒtªåžÇlºqŽ>Zm±3©½¥«ã±ÉYÀ˜6%ã¢á/Ƹa ¸ËJ2é$ÉPÊ~bbaia 6ö^qAÁâr[qµñyñ\BÚKe¢¼jV#È"Ý=«¯³ÕÅ„°äç1µif¨o[jgËÖÖö꬀bꉧõè5¶l­²z«ôFR³Õü '6 ÅJ‰ÝÈ)6‚XÇô¢ãÁ˜Ž½€én*è˜Jm•D¬ó™ð<¿ÚAc„Èz‰Š¼ýí»û(÷–/_æÓwèhËZÞÈ‹ý·¾ðï$¼Iï“þµªÕàñ[ÝÝ['r,c™ið†™ëªÙeµ‹fÅZlA“”ýR­ ɃÞ3iG4Š¿ìòÏ–½Ä!‡ln³-]MªôWû ¹œ$t„mŽöÃzîB%&ü{ª>Dņ«¶|ó% ‹¹Ï7þÉ™àí! Èn‘<)ù#›FÙGM&óÆÿaçÁÝÃdb þ£^'W#W;׸FMÖþ ¡çS}z2¼¥¡òª_£}\Pv±v^…Ψl F K„B‰”’û¦î6ÆÄüÚ#¹5sþènæÏ»ÑÇ]ôtµcéÂïŽFvÅðõrߺÝË¡’Š6µZ)øÓæWðýôÑÕQ(úX;`{“°MC¸ØÙ… zÄ-Òw¡7Øš"ŽøQ†½ƒ6T˜>òË|$íùQáÂâ`!*ŸñúV“ÒÊT.áu ýVK7DAŸmÁŒtZŠÞRýCœˆZ&¼’×g–M¶D‹ ±ÑZ±Jh—¢?G\9ºþ¢![ÄîZÎ÷ubZT® ŠÈy24‘%HØ hÓDמC>ìj^ê„õf؆‰ NçÝ©S£ø‘-æâ¬ø` :BÂs™ wÂ<Òf„†ÛäðK]œÛcmmŽ\òµæ =¡S–^©ÓÉ–R}~!ßüäçªJŠX>å´äII¦ÃÙ¬ñ)dÄÇW2&„ÌTÒõXB뙬›³&±lIqõK‰½8 (4%w;rÐP[.vÜ9ß\/§VÁv{—j(Ç)9¢Go)Åbž ¶ýÈÖ³¢r(×3;ò™¸¬ºÿùYáw0"§h¹¯PÖ•ÉÆ-NÕ5šÒfrB,“Ct+e5ðÃR°ûñx]Gä³ZC¯^™HÅG; ½œ¼œºXrZ4RPѡˋôhÀá•r ö‘eyw+Mét7c§úþ[x{ûXH>w:LLè‡"ñõC+qV‚HØ­vÈ yÆÙÏ„l)íÆ¢Ãi$œ•&ÒE <˜<¶·ãÛˆŸ6Ê‡Š£çG¶»;òM·Îv˜êðDeâ^PÊÏÈä’ŸÊØïP²¼t(5ˆÒ—ax„.Ý íÑšáÑï¼ïPÞ)üÔøî'zà^~"mZ,†Šp†äÏ9¿‚s¥‰?”(!ä\øpÝ'¦(Âø• W$_èôKϘ·ƒïŽM~c_/w—ãs{ðý3ù|kËôª´ùó^V¸ZåãIMH}óÏ›:íÓ¤ŽÆçæ–ÌüsÙcðAƒå(&Œ±é~"÷ƒ¦0ƒF7bˆaɤ!× ìøëãYìg´mÊuÜšzcn=ÝÔ1áõ·uÔ5oZÜD³$æÌrQ:Ô˜^?˜Ä3F§6õ2Ñû`†PÄúqÁH†àÌÇP±ª[-²·úC‡Žb:” ÔâÕ¬p»H‡ o ’ñÙA oÝš–Ç ¿ÆîB~ÆÏXD„ÝŽa°y÷ñ?[)ša˜±Ä—ùBllü–<4âBÄÕçY˜ÌÞT{54»¿Àÿ–iVЖÜÐôßúÄÿ’€gúÿ#e¾£ýORfP²dðÿlÿÍ­ŒenwrKßpÄ}¼Ìt² ¥_µÊÝõ¹õ4¯wò3¨Äüsü þ&GÚµGµè=&6¾1aÃÌMµÆ#`?N+t![Q7eÇŸ<¹O¦9ø‡"COl©-oÙÐg`Òp%He™h¯B„^MÊý‰S›è‰d¦ýÄ(©|äpdðz¤|³üïe…W01±ƒâ¯cÃÙ'#±ÍA»^³¹¡Æ¶i•«^õ.åÝÛD-#ˆÁ× ¢õò7À(µ¼å‹ÔFNp¦+ÝËŒPk®ðU5{Ì…S>̧œ^êsºsŠ…îè -ué'm£Qí¿™—ÿ%!³¶Œþ_„LÚ2µRk’OÇëP¬‡ÿ¹TÀT¡ƒYáNCI´ä¤ÑÕÜ*d“—X;ï]‚ðg:ßÜÕò\‚éoع‰½w-m¼`Ö=Vw_”þÐhE³CM gÅ:îÀ× †v㎤º¶d·$O—y”tòdÊG#ô2…üo˜|¬Wëx2ÿ¿›ßºpó–N?ÿ­ùýoè—± Pƒ aÉnˆ¢O°84%.Ž®ó² l `‹ÅÅͽ­ðùè‰A?Û¾8Û÷º_Ù²KÙ:RO² 9ÂmЃÄ*Æ.£Ñ9Ih ’ 2~åÅæ±Mî+TÖë±Ø%Ü$Çê?gÙË“.¼uÅ÷Åû&<äêz^—aÆo¹_…;ÀQr–‚ÐZò|q®}iJšªOs2Ørâ̦2˜Ç˜Ä3{䛫j.Z7'‚¸_öÿŽ{Å1_ÿ‰ø÷1þƒ·ÛØ»šüëùÿ<~FcËnK ÁwMÆ%)3 ‡bûBÐõJ¬Ü< ™˜ÈH®¤†2iGWJ ø×Û< =‰t*ÚExêj£#»CüªèΤú@ i€¿İð)– á|Uð2 gtB)jB¥éÐîVWô‹%M’¦ÍÑ„><(¯$>露^p°‡Åžë Z"ôàjýœéHõWêÊ(bšã¤™ÏD¡F“  Ä"P°7<ÒŒÈp·GZH8^}è¼²³ ÜÁðžuÜ"Î1r7t¥SŒyÚB–UN6.-t8¶iކ\Ü Œ®.Œü¸yxyýÆ'” %Ùþ­Š‹k·  /Ÿ­p¯Aƒ‰àîyÆ“®D(Ë9ÃÃF´Š>íÇ «9êfÁŒ*  z|+FÃþ¸Q¬Ø"8£]8;~-ßnýÖÀóóÞƒ™›‘úÚÕÓÅÃ÷™‡k¤û©Ã{ê7c·Ï®a@í ñ_b1?åxì^òœ+h‡²é^¤€‡…:‹ä;È:InpÌ[¬SÌk-så‹eEY˜½þ’[j~”æÜy¯ØºôŒQK§RmÙH ³E æ\Æë<í”>ê+)¹Z¸ˆ•\¶[‚@Ç`…㇯K 92 xhS•ø°2­Ð1‚íyT+êš·­Š2ÄÑéEn½I¤÷dCðÙjßuïõ³»M`ç_”ÚÀòŒ¼™…€´ÏÊ`NŒìD 0©“kókžÉb Û¹øO’°lIÌ)¸Ëõhaç.d* VÕBòuÊÜ£þÑÀ‘Ú*úÓ›÷»ƒéž-{ox‚¬t€Ç=š,ö[ŽëÉjg¬ Ø&~‚&áõB/ØèÃeôÄy>‹;)’Çõ’ÓZ:ˆ¾+OÂ1¾’6aŸ¿üžŸ|ßÅB¥p/&ö`g[PÃy>˸§û?y3`ZT¢jëÚsƒbr‘[v©æ,“ñ” Í:µv‘»Èõ,ÞîvçÌÅŒ¦©;µ‡ “IˈŠ[jâ¢ÑáHí}Ó^›âw܉$o³ ›ØH aŠU/<+¹U,cgOSJÁÝ…†kˆVV:e‰ ù,ɤª#¥™5Ö\ û³AEú D]˜Ü×`‰Aýt÷»™nÑ䲋H:t/kE7ÄUCÕˆâk××^Y³£‘"­ù÷®Ç)YK–¯pinÞÚ€œv ð[Ä*CSѧLÕ~3ž—Zj±Ó0Û7 ÊxkF¥PÉ^áÖGe§†ÜcÊ×·;à+­8,£-û­‡I]K!{'¤+vmÜØ:ƒ lRÏö&P€ü²Pwý”S[¥7[¨WO„{f•Ö¼”¦—A€[·&ŒÙÞÉadE¸½U¹+~6çg‘£d•ÆF‡òd‹`”ù?ì²gt¨D/%Ep³‘oï]ª%¿ÿvÙ¡_C"¥Ó\¾R¹Ž4V?|¤%×ÍO¡®íg§Ãf>î! ä|jfÀÖ”Þ¥«ŒÜ_W""¿fj,†NšãžÖiˆ°Å’“•3ºùÚÆ×´ÔÀ·=Ö#Y'8ã©Ôi÷’c“üÅ©§;„ÏÒ&õ1p§Ö³²O¹R.¡­Å\TÈ`¯ô_^öüô\j‰œ tj‡Ýû3Òi"¦&­N:’S:ѬËÍ?a鳓ålù}Oõ®& {¬4Ã&7ÑVÕZ`2ŒN/'¼ëóHª³hž²±Ô^ÍÏhßgøò‚7Ù\ÃiÓô—Ç2“G†ñ’.HU#zx\ãé2Šû*á9ë[IêwY`ï’í¶PÕ–{¦‰Âºâì:±ý-FjÏn·¨Ë^Ìð‰‘cH^ö1öuœMÙ@"ñv Џ@1ðñš”XݵûÝoº’Èn‚C CÈî°y“%OUOø‹.Φ ‹_l ‰ÊO8M&§ÙP¥ê FµSm!v¬nݱ¬„MøåÇPm{1ŠíìõîH¶QYÖOLy£?$9N“GÀ‡¢_zÑDqÝEà_›?-÷7%ÈÓô>î@ÂʸB÷)cÞxö…:½2ÄZ¯bUŸ¥}Gç»W†d)x5>ý— ÚžV6ct\‰ð2Äø7§]I[]æk®=oôÝ;»zÎs¢‹ô³Ý-äʲÙ©Ÿ®ä¹e ˜9¯KXjª…Õ—·ª©17mljˆrÒl1ik?(Þ"¼>_NŸû³Ð«hkÅiþˆ!"¿P²UŸ9èXg~@ÒÎ^í4Í|¯ë<áÆ­éÞtàÞXîÈšý"ÚœÛý š.Ôªj.¬F%=n×¾,Î\éÿ;r ó‘å¾û'hGùo³ÿµ÷4nÉvÆC ð¾ƒàID1•zØ<¦P²OÔ›2!4VåTj”ɄҩœAâ&@ßNw7{”>úMŽã|K‘–{§çüHpy½¢ÌjôI 0hg^ZÚb^ú–¯ü͹ý:ݵÿ6…x/‹ê“¿/[ ×ìÏ÷AcžCÅPyëD¤‚©$xèÅŠ¹d°ëÏW”`ï( V[XL´ÔE¸€\ߎ{+¾wÝ ÿë;ÍÒNáqÁ‚š‹é¹Dó9¯+}\g€ mˆ`–g,Þçúm21± ]B^,w®üÑMÇgÖÚOXï‘Pû[t~v¿01¯<éÏžÞÁ¨otœ2u¥kíêÀßóÄáFñ#*~âÔµq/¨Œ=2‚šüMÃCüW •5I¶¡çfî‘83¶Ù$JÌ/t+ìèžiøîÞ9õNü7E7§Û£!ðÙàÏ?NþþKßÖiRv®¯Ž]lÈîÌ”«Éè1çåål"¸‹“Ù@äƒe¬Ð–ÎŒ§Îûè2%½À]PGYµÊOedœ¯ NUPrPo†ÉæòrµìÒSåz;Ë3.yz…n±Ü5©ÖTèYgÒƒÑ)TF£iºEj(‚ ë"òçäÖ[O ‰ÜŠxËϫ֧B3”ÆÛ,):¿Š¥”̹¨j¾«*RíE…¹ÍC; C!f~)oop®k;™/[üší;©to—±F ‘$h{ºÒ¸ž’A>?.ؤ¶4‡ƒJvµ&®Þ€»;½?§ãÜ”cQE b»ªàŠÝíº;Ýòõñ5®`_ÇqiRaÕ¼8j_gº”]Å“Åd»”ÂÐ2!}ZÞ™l‰×ƾƅTËoyÛ| §Ù¢ŸV-nš-È@ʹx†„ËXð Y„É„no©4Þ(Z ÄJÃ}ªÑïï×GÞ0G*ñcM\†ó%°83_€õ†}+r S “'AŽEÜ<ª2èKèJಃ5,Or ƒ å/¬9­I¬gR1@™a“ú' çW΢1.L„Óüs‚šYšøK2›)ÑlƒBté%1KžL–²]¶¦·›¿»õ? _“–ÇÜ<*$Ë_ô¡ù4<³DÝ%¯uÉ$T«£<«\®öì!óîìKˆÙD„u*h¢´×;ÃøûBî'Æ + *X .|(1rëï³ÓŒš¤Æ3™´K]9ΡtTMÆC‚ÚæzÃ-‹0¶§‹X¿Á³ÿwDœÆhed0[2ÙRÄY×7}>†é ׿qGXB6 €-«œ‹d5;A¼³Rü>]Â,P#H^†·Ó•ê³²ÜÔoí˜uË0y) ‘®’—‡Â"!sÈ®˜ a´ˆù,¬Nç>R(`êG Òôâõu™˜›‹ùƒªIQ[ZÉ?‹Óp©ýl¸R꺰y¹ÆdKÉÑÂä{c~'3ÔS€Ô(b…›ë4à]óP>¿MGË;^ú€üe;í wDó@ Kk!¯–e¨ïbãêf7eWO1Îg IÎ3€oEÔé¼W£r½õ^#o%Ц„¬]e¿•GíUÅ—ÆD šS©džîÆOö´K’`'=¶°È•{s᪄¯Ëñ¡j:ºáTŽÓeư}r4ê0½‘˜J)-óºDÉ*&eÌmÿÀe £â5¸‹Ò‰+éßuæáoÁÝ+³k¶;¥ÞïÒ4V1.nÉdµ‹›ÄÖ`&Å,Èþ–!êã(Ï´MYoIâ¶G»k ¤o¶ŒØ¡KJ2f…Ñ}ÀD‘á¦ÞµÎ+R9Ëåh~)“¡LsðÜSj\£ŸR’óÔ±˜q€Œß@V^¼†ÓÃUÛžfE¼·¬@~÷r"›¼â#Ï—pŽm_(¤eþL‘¦‰bãÓ¯£1„ý¤R]kmh%Sk¿ Þ¢Ž?—OÍãœËr Í'ÞfÝ3 ~(ÀÜv’AEKŽ)Þ¬;ÑĸϰÊ >ïJ(crO±DÜrZõT$Áqºã–ÉêœðB9Å4h*¤xFq !ýhù¾qîÑ[¬£xm~);áü eø^ns‘ú M"êž­& Ëc~&îE©Kyê‚ÛÕùq~ú#þ± *êû€|Î'àŽc¹ß`¿Ð`¿ßðK´\Á^ž(ªy¤Y{òÎëìáp¶w_c·p¯‡·¨Ö]†¨1F]Õ6N)¥g´­m^Ö™(L,s Ä4‡@]*Øš½€ÔIö‘zÈÄŠyUÒÎc®90e”I·÷à¯$ú‰ɉd¾T7ü¬,žG¦¤ÿç|Ž|âȧr¿eÂî“c—®ñ0[c¤“9Y(Q»MW`RXx €ÅnÙº Ȭ†ùRµm§œÓ »uUÃ1•ŸìȘJ·2ÀEªrT)YÎk¦’î-gKHac¨æº¬`Ê…Þ EïÚ úâøÈŠ¡ ·†7fãæþTÑ·3•ö[¬¹³ç1òÅ‘jæ2µÕçèÁV{´,Û›ºï_¯¿˜noƒÜÀЇåsätú;춯O¢gf‚u%~¯FŒ\0ªüáÀÛo ÛµÞ坿ãPáÿÙ{ ÉQ”w\«¡QMcN¥~oûjû$ò{ºñ÷®ñŒ¼1@Ëœ½ 1Á¬ªa½mh=b§¸ò7y΂.î bÕøp](7haøµUO´äl S_¸î3%Kv×”è™O[$››•3ZU†¦Mø>ÑëÈaªCÝÁíâqxÁà¬ïšE,˜” ð9PA”‹Y9`÷@Ø«\9úôg06{Á})à³A|ÐÍ"ºÀ›\9JEž{ŧbNÇ€0Ÿ!Gð1>êÄÇK2ÌŦ†|c] "AbŸ©eLNŠÙîIÖ¤´ˆÏãît&<³¯g󘿊íÂÅ]xÄÔ ^L Õðd«gÜì­2ew}geß9fqùÛ@.œ=`lŘ òIøÜEÈœ‡¼þ|bõå’㜰E>&.LÑ,äjŸ§ÒA‰ïP|»x¬AR…”ѽ³ÂÈzsøŸ×È<‡z±Œ8ð½”$/ Îcã¾_¬B¢o8—LMÖGºÍê“9|rqFØœ£—˜ È»ã?ؤ‡K‘£•ˆ2@õŒ‡È˜øëyí3[{(Ú-© ÷ÊvÏ,?àX9áyË·Û#eïêЪ8!ŠÃ)žf…•­xe”CàlËc™¦UÝ‹´‹{šM;x¾Ê¹œ¾h”süÔаÌV°ùuGŒ?˜ž œdP‹†Vú0iá¼Û3?*ºÝ?•£!³›GËõ-#é. öå¶ãÙ€aI?3¾fÆ=,ľ¾Ip6Ëš¦²i›ÂוžõÇôÄ…q¢™«i–Û ZLÿžŽiŒh0º²®€4eßÖ} ÁM€ÞtýÖ¼SÙ—x$YI4ÂTˆ’š§"Ý%à/Þï“B…ÜAMÄja%Q ͈ùê ¡Â¿ÓA¾ÿT‚ˆ~s™Cþ%‰(ˆ†½ +Ǜ룋ÚÝ)/ÔÒ¬&7ºôûeàõUÛh¿†VÒÜPfý£ý…jmïîFUê7é§gл1‹ËU;eÛMÕ4àõhOdšLa”øaÏÐíxDÑðØÁZô*MY™µË¹S½Ì‡Èäë?ûëâ3÷B‚°³×ØŽõÔê½õ øï.açä™Ì šæ¿†ö_Q«v°¶ì¶ÆŸuõ ðdLMµàê»j QUmI©Y§6®º(œH…uf磴Ÿv½guÆ÷(¨×Û eg³9_·ÎP;h'O©v®9=™{~ßFU¢hÃC+i*#ÊDù¼X½Dÿ–1Î`ÊJä7ùÈOtǽ©‘‰k1ÛQÞ#ö+Kÿ}£Øï(Ÿ ô"óœ¡È—ÀÜ(Å$}’–‘–[Ì)˜^¤ˆ§ä,õ¡S”¸oR´ñ™GìO¿³µLŒ€ºo:íîîèä‹$‰€²rO•p{öóìÒ¨AO¨_ œ£0..ì[³­¶Ë•Tâul¤j:“ˆŠéŠÄ¢OøkœúÀ²+ 1y…¡ŠROBy¨ÚYá)®‘ŵ·à(Êybݨã}tûøìÄ ˆÃO‹ïßþû$á—ÅÏ3­?5’µÍŸ°õ¹²óàèÙdo?áóÌü>æäÊNˆ":6OoúœH•HFÑÄZ‚ ÔJ‘Ê"×f ãsÜŵ©ØHk‘ Ì]\¼BhZ.vD@* O‰¾Úÿ!*ä¯dããsâm¶ëÜ5á )@C¼6ð„€ã™Ëþ¡*_QŸÆ`ÒŒd/.à®è¨TÿBIAÜ»¦/ØYÎ…e[u ¡ü%µœôó7?úGWEúñº>ó úJáèŽ|7)ñëÈBíÊ­Ÿ`ª ½ß:׌ÆÀ;Ëô-â3‹ÞÜV´mžû¹éÀÍ`·õäN/: šR.o/:,.‰…žÌÀáöÚ¤Dò6/Á?v>Ý-uïwŸp¦1EAkPŸ°Kµ:ÎÒ¥·¼²0kÏ]oX´´wÒøf-/@ýþ‚ä%òb™{Røýä¨T)# ó¡\Æ;¥f`ôrɆ•ãêñYèSžJ¡ræÑ=zŸ=Ë_£Ý= æ º<™EÞSˆ35j°^m‡X kÇO‹ÿ¯?ÄíIüãÝõÃwÐt„;ô¨"Î žgÑÖëkÞÍà±Õ;†_6?ž@Ã}Кú–>$ zDË»NàÏz¶7r5Mªo¾Ò{‹§ûýŸ±c, Õ9|ÿ…z¡äè sÄj1@þq‹=ô?»]÷±õ‘!v’™Ð¾bM5^ñø_•±†fëü7A.ÍEÜ·ÂΪ§/œçj$t1¨›Ô”«ƒ/°_ãˆÅï­w=aü1µL ² |e¦öÀöÛt£OBÂÂF½Àå…€†¢EGZu ¡>Ë݇+ÎéãYc„@óÕ ÖõphU|ôñÙLSO :´ fÊ›àÈÊCްýÙ4!k7„¸iÛ)e&©§CÞt¶_ø—;ú¡©â/b’ydŽIí·Çέéä*!€JP_|êž©ªé,TiÈØºœDÁÚEVU<ȇ/÷q?< ‹ƒü-*`À ƒ8&SÏ…j””ÿÅIÔã‰ÜD—]“ÎäчO'ÑTÌÉ€úöAÕãA£†Øç°çÜ…X?Ovs¿ìÚ¬Zt¿ñàH¹j£R™¥elßB’n"ŸA«ß‘F–"î=F÷È5…ßØÊƒöDÛlÀ¢9YC|IfŽNyt$EFeg¨¡“}½AP$0YBš¹'i‡؉mÛ*ÿ²ñ!ÙóI NYPB\¢“Œ›™WCNL1Ü~£– K]òÏi—¼]A¤‚ÑVŠ¥©´*Ô=D*±›jÏðEúR·h¨ó*G2çˆ#(±pÙXùbM EªÀÛùË3îüv ¨"TBžèê³ÎÁÓ`DyØüé `ÜH}‚?—¶.‘ZQH.W8ìz=¤£‹4âÐdÙÄ»Âoˆ¾9U`”›£Ù!ˈ«jàÐBaPÆ Ë}¦(ÁÕt¾‹±ñƒ:7Ðxj}9ån2Ÿ³ҟK[+¿4®‰ãcéò˜,» ½T€hÙrc?–i áv,­M|c* €p¯eÛ`nƒåɆ +\"oÊÄ%ƒò&çϼ„£D¨d'l׌š€%ê²-ÎGÏë4º“'ùb¤%ù2ÉÇòpçRp·êW9€|ÐÈQP‘î„(F˜Ö5õʨèO²ÏŠ!TJ²ªB ¾ Á©¨‰~í¢%»5ð<™lnùÔHz8hããàsÊr!}þév$AcÜÛ„Îh–b›˜h”kÈÆƒ¿Ð’­´ÃË‘™„?ëh‹TÅ9¸’çÁ7ŽËÌ„œQ n¶XœW®£;žh(•ëÇ IúùzŸ-®Çz®-ïTåÌõ¬°«[nFoˆÑ檚?„@R‘g:DG˜Ý$;¡¬Eª½j§"è,ѹ&tÝ×Dx„3³ìPU…{{.1¸™ºÎºÉ°ëÆög3É1Ü´cW㼤vУõv\¶±xG+¹Â<§‡´!®øŠŸ.¸SÚs2ŠÝ‡a’Í'wU%œýF4lL‹ÌVÝâó Š^y¼ÇñU§H…>Fpª,6hKzlÑLŒù2ØÛHV ÈMÛô/Oy¼E«ÙÓOCÅ.¥ø0ZÌÞ*ܹ(‡SC“Í IEâ¯Ð!§¡'îl$rÊ'™!L>H<ëk#—2¶^Ä„g^GSø5ïiö/`]ºªŸ 9X v2øLÀc- ûP®»fAêBUŒœVžÃŽL>­7Í—¹Éƒ!Ÿô$ÃO@Š5Ëô-téú!˺ÏP6<(16äÌÿ†²^&XV n›·üÅ qb;•¢@6ÚuìS°pm‹HS’ßÍÚRþº]&*ä&40`þ‹U,ÙüæÇÕR¨dgq}ÄÆÿx·pj~=¶ÓG¹êg^/NGºç]$rš"z^y¿©O‰1{~÷ø;¬Ï)‰P=•Œõ)Ç€G4jî¤ÌrúìcüЦú]M°ÛÅii´©†3ýיЌÊ;³ÅàGoІˆ»2õLŸHVï`–úÑ·Äô’M|é Ī|ÞŽ!¸çe*ì¯Ã*ºž·#9èø¬^þCVà0Et•ÊvBÆ÷ÐvãoŸ‰thf{Õ;ðá3H'G³=ßF¸ÜLwª®ÝïÏdôŸQû¦žMµØLÌ&GM÷+Üc×Ó‹eo„ñ×µAÆ[KA7BÚêÔuª[kÔ§_‚y~²™ßÿc×€œý%1À"ÝÐ(ö¿ €hÑšqÞs³7àß_ƒ‘T$£Õ #Þ*Du̘ñÒÅ&—KK´¨Óa3 n<Î1A‡†ó’ˆaèǰP0REZ Ÿ¡L­Ù¬´)i ÿÂå$Q­P! ' ÏvšbÂB4FMÔ^5¦6â/*£rëw-ea•ÑSÐYØeava[ÁîLÄ™(c'™×ÚÓcuDmô‚ták’Ÿ]äYFýå)'"§ÑSÔYOaީ蓵‘‹ò“Ýâã?wPi½Y ­ß¹ê#›7ki õÈ"6‡úÃXhˆûPg"ðŸíŒ‚½²§Ü©vNû¦¢‘58 ’,uÜýžíþ%“.×ìéM¢Â‘ÜQ*“έpAvR¦í|Ü›0Ƥn’wã[ßY nñ YAµJKäŸ5Ù‹¤€bÞ? X"I;S¢3b—MV¡½ K}J–h›Vž þ–W)Ï{V:9s¢HM7áì™·½¦jñ=®âY¢O‹ž= ªeÇæâ¾½¾á}zqãÛ^±µv9—Äó0Ä¡¢ŸæÁüO„iÕ=`ê<¥ÎË\ïéñEÝÚö‡þîjó1nBÀZ­ »úÌZ) ¨èƒOü"× Å¸bºÜÀ·³Z%ä†Xš„¼·°‡âöñ³<=[bðÕúZg‰wÀ›vo×܃£]2DôþD ÃúÚu `Þ¦Ön×ìvh¢:B%©ÜWE¤‰¦·ð«´_­ŸdÇÜÙAúØ£÷cÈ”ÕÂÌ+òHShí¨ „ý¤Iì‘e˜%PM¦Ó:£+ðè¨ÅH˜°¨‡D{$íOæ‰ º&˜Va‡:ü:_]ºí=îy>iåáÙ~ù¥'mû9¸nèÓ•räNæWq÷ŒÚ·´ñØ4•!«n ͸þnÙÂÖZW}¡µ±‰ö°[  wíÞëÛ5œ#g§y‰ÆÊeÄÊÝ0˜¨E’1£ÂPdYnTˆ²ãG gõ5ŒÔÈPŒu²kõÉ("‹+qbµ gp6CäuhÅYG,ky0Ì”<­Ø·’4jl¡ØtˆíÒ½ÄüÃþ T_º:Bùx8ÝØñ V¯OC>C‰ÖÐ? ±¼Âzå`=æLô->å¥FnFc„窛ý0J޼y Ìy—Ä‘ã+’ÞÓ Þ£sô¼“¯ù…iŒfFߥ¹rÂÀç28M(ÐÚ~h?tŸ¡Õµ}úù®Hõ½7|N^äH²âr«xÇsºšíèêbXØ$ãjd}øvØð´4­éLÃ0›‚¿ „sqbvWgû¤ŸM!·;Å'ÿ÷Õ˜%W ÄkâäÊŽ‰oÀfâÞ@ztl§¬H‚6{ ‚`c-% ×Ì:5¼Õœ>Ç\ íõS ¬¹s†ÿ'Ö|ÛF~¢ÛÉ;Í2>ÙznÀ¥L »Õ”>ÏŸùNxód ‘trè8ÕÙ÷ï1Ýzì-?xõô_A7X^Ž)8x“)Þá÷†ô$°©O•­¸Ó’Ç_)c‹®¡ JcÈx}âº+ª2Þ$ŸñU‡=€5S ˆmÐd†Y¾H!âJù¤¸ÎÂ+ â”f¤ˆ™„éÅÑtázi¸žâ’¥ÝcâEZÇì˜&£öÌ™xᾚ;Q/çë#’⽌xi ñˆð¡Ø§;¼2ÎçbYÖÙj^óJxTNÞBíu¦ÏÁðI¼ý¶{.Å3·Ú¨É.f½º}À¸&ÅÁYHê»ÂÌóIQcÝ<0튗x~h|Ûî¢Wq¾W®±vØmyÖ®E»L[¬£Õ¶ôš¹V‰x•»xÍõ›²ž~ãxÍi·¬Wa/¼W´Z8èÊQýŽj5ï&Ü÷Äšwߢÿ`vI\«†]1þ·U`¶ÿ7ªáû¿Pä¢2è·2ê³²é'0±-ï®Âd²²aLd¼ðLIÜ™PšR¨t"ë¼îIgC2”õHïÕ{ckADi4”¶Ö¸ZOTZŠkS‘›-æED‘ÊQ e° “P:‰M¶#½$>Í7<5Ð.‡¸ÜUä:”ËîxY ½æZU:‡ò öiLâÉ~â_1"ëb!+ÕÑ.ro–4µ$ïp¬5žnwÛP¶jk‡Ó¬‘iÁژͿWþÄ}3÷æhë”èÈ ÎN‘ÄØ¨6j†x4ÍÙ$ƒ¬Î„©¿³¾f ¾Ì0‹ zÍÌ܈¯‰Ú»…×€!ù&÷c#-ã9\&wË_žRO¡µô,E)”,ÎOÛ?å,aÌg€0õ:¥vX8ó@CLÊU±#¤SgÊM¢@½÷$ð¼ Qs¯Z%÷³Bâ€{TÝß{q^¤Äüs }Á H3ã¼0JÜ¡tl™Í‰3Ée8A•Œ G’yWQ@š„^ø\¹xçœPU×¢bn=u.žBâ„t¬ …ÄÂK=1§KLjƛ|Ö-ÁÁ¾AÖ”æ6ÅF×G¥€£X8k˜/Ä¥fB<—9ªûå—[ÚÌߤl”ŽnÕ…7±.šÑ¯†4¨‘0[¡ê~åN=åäO…„‹#Ó *ÏpÖ)Á¥›àèï;YU>ýdAÂŒ~ý„ä3M-r†ž¡!9M¯Àç0£UÐ7¢Î{ øD=eTÒס¹  ûÊaL–“úaÙ)?òh‘Hà´ÈyÑl»ÔÒäFx…“Ä|Æäoø…KÁ¶1ÄVísYãT'ª+cŸùRÅÝdúR¥œÁ~îOMÀbžê 91ûd¨RÏË„pQ®5á°;†–©óI<ÄŽ^µH·R#**n^†¬ïi)w EÉÉ[TAñÌ*&…ÏI5RgšÑFÞ\q²þ”Ï€’¢ËvV¡ýDç‹ÏÛ \–€xþi¤dÇ€˜yíèÖ¤)VåX0J'²E¼úY\}'| žLâ»Tž«Í\©¶jø7¸Ø­!@QñA¤ô »j§F:è—èI¾æed”¬äâ2«LÃóüwÌS ÷k™g+"¨8«V#Kм¡Þ¦%ª·åôÙVíÃêÖ—ËÓþ«]ÔfµÃìZ&›ÝÉ Å]4çL…ïWVãîÀ ‘H<¦5D稚¢Âéz ©`Þà:Pâ‰PD•½3[ùJz$Rà*¾ø<ä+¸?aSèÕf“äÅÆl(]š«°ªyL½PXØ B‚˜Ø…™' Þå9Y¬•Û~Aº9Ósþ!g„³ÉO |°pf¿zïªQr&ª0½,’èÅSùqÂx=™¶N€îÚµEe>G<„¼m{}$}b¡_-/HëD¥yXë•QùµèÚõkI)­\º¯WÏü\§•à|Õ3Î,š€äáßvÒÅ%È8L#ö‹ÞíÔ%ì³%($ÖYÇKÏ6[80Ö1×›w#…y”ä óŽ=—¼ 7ý=½hÓ%%UôÍ“‚¤÷Á‚¿%åÁj”ùõRêm‰Œ)'tbú£úü¼˜Ô"M0¹Œ˜æ;ºb’Ó‰”¯Èxx&ÒK¼EM̰Ž`,!ÁšßsÞ£wF÷ ‚ÙÊ\9Ô :3Ôñè’ÖÝnûÐs4Úögu MÒ‘ê4}ª|KFÃÆÙXH¯IÅyʹƒ\ŒqIÈßLüùÃ|̉I–t•,š¤šs‚Ž3ô’œHDOúùE qªJa¢µÕÚ¸QÔ>º¬? šÏñÐÅÓe:-‚¶Ëtóv ™£<7Ìf·w¼ŸÃËÈ$tåXË@×…oÈÐKCláöÅ@®°æÉî•C$ÀS:$LàVY?Õ¢`òæêy‹)“ÆÝûL`û0ðÁo²y*š!¯¬å˜¢¤‰KaüƸCû›40ûÁß}÷7´†â¯Ÿ…_äÀ[“[ ò®ÎUˆê?U^œYWð~zTEnlfñòó¥ gÐEãÕlœ#=Ð9e=IJ‘Á»½þVúò—ÙofäaóO<íøº]ÿ˜üîÃ÷xsû·Ï2A~y ëLØÕsÅ&Lit.N@ªD+§1¨%Ÿ†ÊÉÏ’5ÓùXVP>4—yô˜$’¿Ÿ—,·ŒŠœù Þ»ªe¤…VO6ZÔ$&U'vÒÜiІ¯œQ ™"¨ -ûÿ¡“ߥq9ý ¿k€? šE·LvSðrê|€x8m–­kš=à竈¥hzuá„ù¦ÕÀŽýš­ê”'÷n~ÂÅmùýÁÄñQ&b1AõˆqwÝÈÑÊ6ø™0DÖf°.4À`9ç|•Kß©;ÎdÐìáÉóÉLÔÌ5Ã,‡ÿÑÏç÷戃åTT]·Å)ã\“kqÆpô¢£r¸#¬xDÊ%̉L–L᪶ö­69kJvˆ9u¯FÐg;ô©ÔLöû<“¨‡ÙÍKë.« Ça²ôÉ‹Ìú N.ç8\5€Æúx.Òh­ô¡¥ää7å<ÂùÉú¹Ö®ƒ6Ø´eðŽÔÔ­kØGûd®ÁÇ¿¿¿ßÌTv·êŒØTÿ¹ –Ì€ü2чŸh>VÀ”÷kÉ'L KtxA¶­+,¢T–ãˆ2™›š¨Í® EÛJ3…“eø%8Kyâ3„#d¨¤Ö)[¥jïÛµcUC×¼éúñ«V´c1Óª¦m]ó¬V´b¡u¹ºMæ6ÓºûT,ßý±ÎB¢™>Wm³ªò:×XÑ”ð¨ºáÇ„Eê˜ÞW¨÷Ô$g®çåçÿ­[ûÑûóeÇa1ÛyœÉœµõô~`ÝgUÌù±ªº ß˜+X[®ëïáôý¾óæq­¶í4|ö•ô#ÿ™TSRKQ/4óU‚¢ÏêTò²úØ…©½v,ï1Jh]ïõl “áÃ⩘ÙsæK£<Ê ÞíNwÆþÒEŠÕõEˆÕ+ ]œä[Âo2˜INäÎïq7Á{ûó÷;ü¯87<þMggu|ˆ{iW{×íµßÉÁ#›®'FÑ'k„ìø­EÑ}×ø³MûÄã÷.vêkT÷ªë'—n4?6]-»õ/ü¿—Ãuu£yZHÀþƒrøÿš2Q¢¼`·ù¯xê:Àù¥­J¬Œ÷ýªEŠåÍ•,”L‘È4 ÜM9¥W—î× `¸*ÉièS÷œ§¹Þ>;!‰ãmI%Ö°k=UôwãD”),xã:…™{rK–ƒ!¢)ÔXÂÆT‚u”0ÎHûJyíw2%B:Þ¼ØE|V.½…ƒj(õ„Ú{BÂR  Só¯'Xá61ˆxŠ™z[L-áHu-!–AAo8î„p­a3m«\·î]¦š^ÃYÏ!2]š *;‘üXòÁ÷¤X­)¤såJçsA’6®- IEyÕó¦bRHT”¹–†_iI–£Êè2f¤‰(ªF¥Í&UBø¡-Îá¼ø@ tƒŒ0ýðXw¶oÑXô?†­:ýS|ørè?8bMç9}wæÊÛ;CP»ßç9Ð(EƒLO¼é£üY&dbX¦¤âaH ™Š°BD×Ñå¡“”$F°Sq¶³'a•O˜P7£I±K¯YÊûþ<Øx=R±Êk÷´;x®3L!3µ?x¡G¸Ìg'`× -ÆÜàF;_ïïØƒŒ…’  d¬H¯g Ñ&G²”<0¿ªÁ›Í´?ø É…Ç»·yâÍì†âÃ}M©/ݤ'G¢:QëÉ +™x©_HF±›Yñ lÌ–¢›F`š{oÝ݉A£ac9Œ«Htdެ;Ö#«„¿Ç!ÔR‹mÅ¥CpnÌ(V͇øDŸŸõb 1{œïèíøl:Ö¨‰s¼a¬ýZ‰¦ánl—mœç¡·N~Ö:j {¯1Ô#âš6òÛºSáIË)Ï72êgehŸ¶Äcq †¢Ñ~ À ÓWïœ?½–[™ðŽ"/ØÑ¦z:úOn{iÔ¯l “®„–™®D‘Vøä)Ö¥ÊCü€ý{ŽUðóIáÑ·ùr¬ÿ>`ÅnI!·—[2™­R„ÔO¥„]?KGa°²5B‚Tn/Úñªå²þE쥳5{rÉñ@×`]s ¶ÄÁÄ4þÄÞÜ­×^ýwáÖï·õ•0À óÁŸò÷@+JÁ£?©_ X]6®°ÄWZ YY Zz‘­Ý“™ÀðR`´M{ƒ¬VÂ{ï…à:x \H6mž-Íge`g8¹‹v‹œÿ8kS¤r0%螣´£¦¨‡päÞäe–ó±4Ü^nødÓÏÿ`šÌ\™Q£KoF^^U/h¤2 ÃïÏct±„ÇËæœ™µ5†'Åâ¡)ÇÒ½y”A XHXOñÅ =,pÏ(|4a˜sué|à[á9nøg¹`C ìóBHdùY= Â6Hó”Þ•¤©‰&Žx8â\x•&µ›}ß»ŸLÓ'ek(°ÁF€yâz¢”Lè«wT™—D<°¬[’èö1—ņ˜°F Ñ©ÞI™ ¢Ú:˘UTâ&jjŒªçÖ¢g_AlO E\‰F $ôô6‘¤šÞ>÷\nœ%Nü÷%SÙdUkœúÏ[ëÇðÀcÕÂ…¢·9Ïmwáb+fùÚÂ’W±8W±SzΉÁ#*rTA XËo >Ü+ϱáÄ´I@TÛDϸ‰øR¤œà¿SÄk :RZšþ‘LâÿZ*Llÿ¥Ñv[ "·?Q¡¾–ˆênNã¼qÑP²ÇƒNHG(óËRD‰WNªšßt¦èº VáCìÄt‡_¥vv«kmwKNª4Àq(ØææÑ@7±bØõ!Ó$úa….RÓÖLCÄ‹ œÚ%Wï¡NÒ¾Ñ1tás°¸Âøò52R-1 7]{Y'­B˜q%‡ÞöãªEjDnMU{V 9ø`›~»µ`‡Ê!ÙUê6›ªošfØè[ó@<6 ´a!ü_ÃH.AŽCÅ«ŠZ­Äó­„2¢»1ÙZõ„®lB”²úMâxû(/ùsLüh:SB=µÔ¦÷­µLF”xr‡^w†s¿ùfà7ZvðØº¾è¸MºtÔv fòöÅÓviK¨8Áäs‰ú²4ˆÓš€îaŽgŠØR’¦V{*­^Á'¤pî$r ïÍÁ¨GJXsöÏÙB^ ’*z¾;ZúÄìJ­¼Q݇f£ÿů®á¬wÍÿy ©iø–ŒkÍöHUب(VÊÂe $Ãz'õó·®ÍúþwcdNÌ\ÿÏéöŸä?ûíÿ;-\ÁÑ}ƨ1å€`‘Üá°ÂÁ›‹!ÿ'+Üq!ïÓîæ¦ë«#µ‡í¬ F'0œ5$dpœhÙÈeb &GøFWCȵ=½fÏ}¿XõG‰Ì†`˜ØË46ºh~t:‹í\–¥àq‰ò›¤±ø×•E­Q+‹p~îÝm;µ›&c1ë[P“^#ˆi6–9ÐySÞ,ÁwkÂbÅ0µòìߑǴ7j4Àö?¶—ðÿòbãÿc5T¦¶ýšß\Ý^%M[kR³¨¢ [¿3-Ye’K” 6D”ÈVöx¡”’cèïg®°åDz¦® žÖÏëÔX‘ø®4hºOéø†?•à„¶šÊîøF6ÚÅÞ±ÓìT‹Ác(Ñ\\œ>çÑ©NŠIÔ½r¯)%-ÉŒ„ª‰ BKþQµÌ"hMžÌ¸½Šù\!1‘ >èa^ÀP–ºIfµäj (šLæeÁ®’Õ TÒƒifS±¥&c˼M2„Æ—æw*€+Щϟƴý]¡Š‡£;‡-ÑH5¶#¿ l!Ï·—ƒ¯çã™KJŠêx '*RÄ'ë ¯62IÈâ \‹LrêAPÉ“ AæÅÝTƒ(ÔGHÁ¼$Á@éÑb¤«Z¥Ü&«ë~³t€íTŽ.BtvÀ÷×™‘Ÿîöïã‹!Úw7__7Oïû0ÝäǪ©÷gÇ÷Ž²Ã¦ß>Öü”­6|ä+œd®Aœ¸‚@´žˆÇ>|à¸È’Óe¸G©F1GA'œ~ÃÎ3éÁ™{S²à¢­2Ǿß]Múýq†qqŠ= 8À7«°O%Ï|ðòp ’ˆâ1 „†¥s0žðL†E‹RbÒ\%åЭ#¦ž £ÀÅ®¼!°fÂáw‡ê–rpÔ€qŽ^E¤9‚Q–dÆa9¹æÃYþ¼HãŸíâßàOÒ¬K £»„6ó– }°$zd~FzIÑy ~ƒW[àŸ‘cæ$r˜*;âHeÍ‘•{ØÌiˆ[Ó9,úcHR ¢Æ.´1'-ù`$û'™_^ׇ"ö¬âBË…Áƒ.ãÊ6d` ¤·”’5à$ŽéêbÙ•[erŸ™Ñú¾¹<™ýâù[^›†ªH掑1lZM@dÍ¡‡)­]Öõõk ¥w^š|Gtï•DrGîKØL_¸-Ô/±0“¥­_É=‚ŽN•¾j]F•AFÃ3—ˆ–C]qÎVßHœ¤¤íß/`xÌâŸÕ%½•ŸëG‡{Ü+Œ®âXŒ€WÖSi—0 Oá¥è ›øìf×Z²q”ÚŠTgÜ;„€Ë'h_óH닯§’"nžò=)K‚s‹Êè »Šh·à¸zÉi’îñ¹ˆN”d­Ê¡=ÅK¼a¦‚柵Vº?† ªx”|I¬JV“š00«œ³ëñ"Ùð¡0|ÕR5‰ ï-3š[)a¸zEõ‘¨ûÈ=§@íTT¶8¶VZƒ•ø“íä_ÝÎu\gâªzå¢Hä)†Õä­Æ‡!0@22¼{™Ë¿á ‚4hQÙ bÝ6„åÚç8·¶,qD™9zòß¾—ØUkIÏqþåÜe¯5ý üÕÔñ×(‚šbÖ‚ÁUåÏE¤Cu –…lÁ4h“΢¶âïÛðýÂ[qÙŠ«KˆNÕdÅò/Ö+D%•¢ÿ’Îb‘”º@ò+pcóñçs5>Œƒ~tOLOw§­ñö‚!ýû5ɳ4{ø)¢ŠD2…úyPHFǹ ”¸Gîcjàœ`Å;’’DgµKS¦LÅ FƒŒ¾aÑ!ú\©ƒ¦§çÓׯãÙVUúý:^pÚ/e[ü¤ˆàµ1hAtÙþéÕù©Å×÷çãD<þþë!þ¡eòÆf ¶Aæiœ „÷×ÁY{¢üOtó¡ªÈ—jØá>’ KHÚ$sY °.~$ÆA9â±£§œëx30î#HH¼)€p=úäšL¤ùÉ@s¡>UÛ@구èåàbIXå- ‘vÈÃÒÀ0]¼ü qHå›fÑD™} T¦ø¯v¤ã$™¾ÍÈ^nàÅ_™Pßd@ïY°ó#|_yì[®­Ý®š¹0©µ”T8™J3ŠK -<¦3yAóù5ô†|¢ÁYaŠÐ¶üaUâMó³úrxk/ï`ŒÁ?¡çDŒÓáÇ0V–šT‘âV^ý4p tB˜&ó~»ù^¬àᎱ¿X¨;, ºígX+ópGãgkD‰µ”M!†¼Û‚fuå)èV¢â¾þbNuðÝŒNOiC-ZÁûrKTØ¥ÚMǯàfcÀ4—]öÒôÛÉzÐNÔ|85U¤{âR¥Ö4ŽèÃöˆ?ü‘µ¸íÚQÑKàR;‹Æqù ¾4wÉuJYK ¥ö¹ñ/RaÎ$¯$ # E‹¥‡éûžÜá{b@{ȪOx&.Ô€•*³ÃŒ»KXAdVãSÌö=˜?ž]®êÙ‰™?ʼ64ûj ÁÒÂHÒ¬!ÓCvK¢Qÿª¥¢Ú€s×]í¤¬í:êÈ\†sÞ88gCÀ¦Öeô*¸¡ƒ¿÷ÇMÍwGK|¿’p;Uõ8Q–ß¿<˜òcŠår­Þ‚ÊWlüÒ´ „tøm×JûJfýªq¸C+o³ÌèÎyž#}Skú 2åɦj6Q—«97wJÞѱë6¿J¹(;/ ct´Õµê~°œãÕw¥+ þ¢•{ç™Éwá¢SžÓÙðñõöÖB\šás¨÷'º •­û–h~ÛÈ{©{¸Ñø³/÷•Î<Œ…Õwí.d2ªˆj߯¤mi÷¬—8 ޳{Vï2½×¸±X¥˜3ž‘ãÞ¸W'ë‘ÜÛsö&};í¹’Pƒ}*Ñ6¬¥mþ…¯äm5Îä/·˜ïµ¯ŠLð­8ÿÝTe¸ºÑÔ zšÍ+zk{õËÛĦ®ÿëçÔ wv”Ðdö•î›,“6` ôÜrie¡K¸“Vdj" ø¢ŠÍ¶mkå°Ð½beµM±û`»è»Å=ùCõº­BÍ1îi|ÑÝiáô†Oìúãl7|ý0;íè<£Gv„êòöõÇ]#§¬™ËÆä´ÿ±«¬©]eõçcº kó¯–Úé³vÿŽRá·ñ¬÷`£K,ß}ÊCP4·LÆ¡{ôÞÕÖb;ØÐ$×ÎÙ–-#}û’8oÉÓb3žYјÓÑr9±5„¢Q, ciáç‹a§¥|ö†±Ý+Ô†j•²¿ÿ‚Ékûnl·ÈW²În¦› å[Ô¶¢öKô‡=µÍïÿ1j‚ž8QÀ‡ €è?åÆ-šÿcÄD.¨@¤RŠÄ…á†ÞT)Œ&åÝH"îX~äÒ5€^³ 6BA:ˆ[A¨¨VqÕÁ[Š4çq‚öúo€ \Ŷ³D\Çi’ö¢þœ1:A¦T †ÆÍìÓÓôÓÓáeÿÏ¡3ÿ5ÄwD_Û9¹¿|Þ»ˆ|„ìçM„ìX7ú°Ù@7úˆÙBwúðMÊ:môL£e…ÃTózMÔ ;šˆ½Ø“ùÈIµÃW!„™hú[Ú°ºÐ—NDžÍ¬¡NŸhįè/Û\OiÓ“E¯ûoÎm q:É9‘o¶È¹xNÕŽL ßî€ÙX¼ÀY‰ñ}:›Ëç Jé¨SúÈ ¿eª²ÆôIÌÝÔõTßtd>Ù—¥Õ}&º_êzÿýiÿ}´ §9ÅÜ£ùŒÂO2-¤ ~Zæ^6¡âQ›þÑËcéwҽçé>ýýê‚ÍŠz½2χ!y¤é¶,6wx0å;Rßnš¼âšÿ ¶ù“pš¯d͛՛d;> SNÙÖîþ[”…OY>+xŸ{C oNÃÔ˜¼5]Ä2¢8hwòúkˆ„²%à y#ªÕ÷"b"hΰéW&R:´¸‡ˆà’\­*=òn,Je=š‘Ò; /_lP°à¼$ðîuÝðâÈäGçeí$B‚µrT8ÎÓ Ëˆ‘·ˆ~Iß-¾K ”ð"–ïù¾^_6y­! ¹kŒŒ´ÊSç9 iŠ%’wV z»þØôW‚ý‚±5ÆË×3­Bíó …Ç<‡Çwœûi˳·wÏ÷;d{K¬´~óš#Ô¥¿uðK–†‡FhÅi`…H°Zöã,hY0¤ô;°FAšJ„®8~€ðà_ÚÁÖ8ÜꜫÄYÁ©¿}Ú Ìš´‰ ëO*€â$¯ 5á³³¹N4‚B«”ن̞ØÅOiœ ˜i£ŽòÌÖQà©OÂgö±‰¡‰Þ£ ¨Ù3 ¥Ùe´?²rG#k1..&ä`j„‹Leâ1Õ»Ð'šw.;ÎåöüÞ†âuûpŸÇIíe¯"Á%×å€ÑíFU[÷á6sx¹èÚ°Éó›®2J±É¾Ç®I4šfå}T½ë»è6’3)1Å1l…õ]q ¹£–ŸƒY$$ŽƒÓÄzŽÓÔÙñÆ o:åõøfck¼@’ž¤u¹£©Rnæ%Ãä?wæòDÑAᕈŠjÆW~ûí‹ä·&xî.G³ëñ)>¯@ò!¿åÒ {‰5=¥ÔrÓT¡Ôç£ÛÚ(¡ 0\Qèj›]^º9îŸ`:Î9/vá”à[McZ Ŧcgvå–HeQÄŠBºÑ¸µìÇ=ëÊUã\`Àõ°¼´¼;5 $q/í¿‹ïfŽô¯oò©wÆÜ.°n¤§±¯låW;¦Í¯x]u¬÷| ¿¨YÍg5]&åf*ROÎnƒš„ø»Ssœü¬ä*œgê”qÜ…çòû?‘ML~'qq>ËÒÔpÿúë“[ýô÷½\&“×ýåâ«åïðõ)*(Ú76„3Mð@Y¢~.—åÑ‹T"嚟•“›×ÀŒ¬‹D–¦~ËG†O²(|°1Âæ —¡‘ššDHòT¦È\‡8wq‘ ¡9"*—¸éÝ>àµeˆÆùŒÕ!áY1Þv­«™´ÆéË¢AŽˆ€ 8žšcàÔlèð³1V'DïŠ9¨·¸‰Ðy Œ2 }É&à€‡BXa£KšÁITGðÐE?æöÜý ³hˆ$év´¤CCó¨SÌL÷~Ë1™ÝÒIÜ»å!¢—Iº´±ñDbèhOÁHÔüä5ÄrÕ€™èí€tÏ€ ³DvI}~ ³®lˆå ¢F,©¹\!˜ŒÊ…P®Ç\mzJµa7a8mæGó²6B©Ø×ï`_ÞEºÅ\’ºbhP:ÍmGGÁ»œ”³›u7( éf-9_ÂÑ|H:@ve<ïtÙû¼þÔÈø1 jÒø‰˜¬?éÉPºø–Š>d–œWmAoy¸¤Q}2 ÜÔRêÇP8æÔàÀìMÎÛk\5A®ˆCzA öQ"óƒ ¦\v…’®åJQ»=®=Ì}t÷œ"Cä† ýˆ“â 5JàÛÌ‘z~7ÍÃÔ}7¥pš¡ñD»Fsý)©}rzÞ*œq¸ÊàŠÒÏuä!qè“&Æm™™Õëk4Ìz®ÕÚÙ "ÏóŒGH޽ޡÎɶÐîD{´,ÞÖ }ä‘|ŽPä…_휼B:¬ Ä9fÖyF“LGËžâϧdøÙÍwÍT«æe0àqè°îœä¶Ç·?$a4y^­UƳüÒßíé}°Ú GÃ^äýùkˆ?å…ò&¿!ùˆê-õ[fÛ-CÓËaнjï6ö]:ÈÖ+å¦ÑYSÃÆ¬&ÊœZôžb9IÓ™ä¹a‰6…Zõºé>XH@îVÐXZ²zj+e4N¦‰RŸæ¾À"„[ꈦ„]˜½fŒªÚjŠ¡iÔ•ÜiblÖMæ"yS}deçÙù¤y'«arƒ1õ‚Q…Ì?úgø@ÚZÿ¨*rø`ì+(‡ üƒÚ¦¹’CcJ ‘1aÖMýi'åR7…¨ŠlcñÑ×lœ6g²Ñ˜À`¤‚â²j{m4TÛ…6LZ]³š¸sO‚š2²()¡ïÔóª)Þ2 ÅêèI™°„:×ÄŸn®—&n’‚(àû ójjº×h]6ªimëOƒ‡!⟬dî/Ç\þ$zØjÒ®|(Vâ<Î’oÖE?в¯dŽÌö*„âÞ-ÙƒÞ.òšºƒ| e€~ÜoXŸçv‡ÜIŽï/îCàÚB9f:.¤§¤îÑȦL˙ҹÐK4~ªRp®ÍÇPª7µÿÓA4ŽÓ þöw¨ ‡N€14>„XäRQô+âÓh' ˆ™£˜ì’£µ\j:,;¥,’ Ïü¸gB Ès÷6dfÒš$Ô²Lyñ³Ñû3CØ.œVUcù^fÔ‚áºÇ4,F•ÏÓ¼¢Q“Uèƒ!”ûÇFÿðb5Síqí§#’;Ћß]ê&iÂ.‰¿×’¯P®Ó°è–ŽÅáXëU€³ûz‘…a’€VjGçi ñàÛ± Ç Vmä’ÎKçG×è;€5X)“é1mr^1JôVÚ„ö<öðõ–‘ˆªǸVÚ›í¥YfôJAÙˆ…iø‡øyD¥ÕD|Cž¡H†âŠ>‚uG54J¤ܱ £„ÕNB§Ñ$ÞPí_bqØû:ƒÒœ%Qÿ}E\=T° .£!Le “:תõg0ùè„Ú Öw G MŒÍŸ*Cpý~¼¿ÞíövšR`~$r'E—š™Í”:e°É6Œ5w™)vþœYg÷`ÚœMTôÍ4ý€7íi£ìyÄ‚Ûl4¼ž°ˆ ¯Æ£6y¾Ò¨_$N0¹‰æWB¾$P8H‰4éýB¦ýÏ:¸3ÙMK_¯˜tæ[Ü*5°«ã0Èñ†DíýYÐô¹Š†¢ƒÆf¼ÉYEH>™í @áJR¯Œ;afÎ9I¥B™Å §½zëÏx¤R]¨ì×ýéSžº¤$ûrFF<'“ˆuàõÌíóä %Vòm£ØK4y(-y[Ië•F[§3‚û X›j0’U¼½²•ÝÂÇ×òàŽ™k¸ú«q .'Éo'vŽo:ªw`]8õ•§Žñ4d ©R矶p¥®Ù#:¦Øs77—ǧboFÏ,9'·¶ÒXvyÝTNës·U°.·¨<·˜ b(šétv™oFT߉ôéÊ-"XO þ VO»}£OÇcpåFÂz.ß0;š õÖ×­âŽÄïÔäÉíÕÙ.‹3œ¼Þ¿!Ä{“ÑTÀF« Â¿ ¾Êš:˜Ÿš^ÕÌ`®…u7¦ÅrþP±$ –dÑx'Ú~HÙCìA²šý¤Üsæ:©âéKyqÑÞñÈaeí1Rñ­ÓÈ ”L¨nGßë’_Ç-Ú:Åp7Ì­ ùÈ€ºÖ†œ À£Ô—3](0…ðš*ee”ïíÕóÒ馿W6é0ôYu|ø´4*TÌÎo] Y¨äÔþ-¾íurmˆUνÖë^ñQÅp˜»6àbûÖ"¿I^­=Ÿ²)³2|>йAlw!²n{•W`¶Sý[6‹yýÊ}ëòdÒã©l9©¥£™í¼ìª¸M÷ƒý¿BÐ/¥¹tf.j`Zðÿ{ú¿jò±*‚ñ›=§ê>“8@Xã0¼O ¨©aV…¦ùRùªòã$rwT M­Êa5»¯M92Í;3 A»¯_™ÙП%ò÷g¾hyšT Hb¡Ñ’,ÈÚÝwú“JR¨³‡˜©ñ)¦­’Ä»˜‚äU¨B†þÜáíµIÿÜá2e‹£r¢ca‰&QŽkJö7¯ösVJ‹yHí? Ø-%…HÜ—” xË"È"èjûÅÖ<â4×âßÜÄKT¯+ŒÔº¸¶ÅœD«6bp7“ªB‘|w"ÕÜÈ X-''ñVÒL †€(IDYkaôˆO°Ž@Ș‰Q6šÈXN¼ÄöBT)‡çÎb ïïÍ!E áÿd*2 ÅÍ-œXöìôŸjâÌ©ý±Hìç {÷zư-|†Úåë·yý‰Q‹'ÎÇ@µÖ·²|*’03*Μ )x Ë&P]J9¦Õï¶,@Я8²·¢¢.6¤JI'”ž *µÅVÙU±XŠÖµâË}XÝ»®É s%P$éî-N—VÜ@Àš‘Bõ³µÎ ]®GZ¨•éi…²ËçK¹g¶–3™ÉHŽŠÃÄW'²–ºÊ'ÆT¦ }êNKúZ’ˆ*šÞ<âNS /¡€b úÇÈÉ­Ó÷3UI3úùäò4=ð›ÕfóŠMéÝòÙ¤OíX½tc¸öùh CæVB1*Ѫ°³Œµ¹.n1ªœ@µd˜ü¡ùˆ0ÛuØqɨñå÷Ôs¦(ÿ‹ißüé¹'ø¹ž¥û7åxv˜çú¼/¬Õ¯L½5 íìÓOÆ•¸£F® Wï¸Ï¢Ò–:–cœD«-Fu˜{Úé­`š'ØÚ“!GÈ ©_í@°?ÈšÊz‚¡4DêæÎHGá ‡+ê˜èýw~giÌ“¡9ýçoš›æ4Âß¾ZdI†ff[ž“Dº¼ªG˜ºà¼ITÇÖØ‡Ý¿¼Ã¾ä…ÓŽ‚­'/*ï™n·Oï•ÚßM=EîÏ›¤°˜S£½º*iuâ o mâ¶-–7·_Þ€Ú¤¶‰×~üçâ˜=­§ÌiÄ_ Ïö¼L¬>Lȃþß» þ·ö—å)¹-”žÖdH@ T…’Þn¢?d%VŠè¾¨„*nçƒØk‘ä£ ­æ1ù¡ªÏø¦ßÑ~Þ·ëŠÔn-¼êî³Ûc™ÌƇ§>c'cŒ)ñÆÈáoadýòÀpÖ{ù :àvýŽ qU{jª!€Ý` µš"*k„v[ívÈŠ0Õ€yÁí vUúO õÜ 2Öé…¶ÁÚ`±¬¬…æ¬q·I/Î]²ÆÉ?BåÅ…å[çaUêái³` =àb{šúZ.ÉÍ5ÅÉëcÍÌç<íñ=ìáÁíÛ[vÓ¡ýûá}·6¸1ÄT (šBÌþ>ôgÒ–¦¡-p„ËýæNrâGN›§Šˆ…R%Õî.èPéÊ» TM®™ûÙ`“„YÞ©ƒ“Šž:€$†ª©j•/Z1÷VHùU¤“ª¸è¬Àª T‹ÄÒG‹Y4–ª¥,PRN˜ä’¦ñ­*6àSãâ654“Yš-°ÕDÏ"¨M)"E}–ð¬H=8WåI¤å˜%¤ 1Ť„0Ÿq/ÒàØ¿î´’„®®‰^нáBÊÞKÜCVëêϪÑn¦´X³‹äqË@Ç)ß\ñ )QÜzi~X]O%W±^V· qh÷i]"ЋÖvkº¥r!Ûþ¹¨"ãr¢Wd’´uu¶»ÑRÜþs”í‰Ïìº&UµRà/‹y6Cï1­(“¾‡—×·~Ç–û¬ñò5ç9Šr^.z»|k*xõJö£Qº!úú$ä ›ùñ:y½(j¥hmùôwEÖWÕú‚vfùÖ"çø¼¤¤¶#¦ý,íÈFªñœá˜Åÿù6n‰C.Ù®pB‘à…Çt»‹7x²âhh+¸GBv+Šo4B¥\Y¡2Z‡Á•ÁŠ•ÞÛ麤¢þWó€kÕ³LpvÃ:I.áÄÖWÓÚššã®cÓþßÝRÚ.@>þÿEœ]Mÿµ§HÓÖ~‹éçFŸñ” Þ·%µØ}‹û¨Ü#ÅÚ&‹æÀ$zäï É$Q$WRååÈÏ[NVÒµN|sØŒyn¦¡Ü^Ñ]ê2 Ã7¯2Žh’oàyy©û"w u¦]ŠF-L7oß›¹í{ªJ…Ê4DæzhÛàiä  ‰d?jj¸åä*½?æÐœŽ¥œ}3Æbß½²ÿýr}"æ° p$K‚eUH˜‚Îz^˜PoÌ’Dýði=š™ieŠGùAºP¨Œ:³NÞAY}/œ†&/µñâTSÓ³×5#ôj²•'Ú1ôóÆõÕʼn³`¿ÚedûjþÛ/SÁüò)l=ÊýÛ¦¢˜ ¬ 6U\¦¤ÇšF"͘Ÿ6óˆ¢4A³â's\“”‹…³/`g4é|Þ¬!ÛíÐëÒ» ŸnžY<ÿ6?ï`ÙùÐÐÓ£sø'x{g_W.ïìÍíÝxÆßo þÙ›Ú±þÕvØß$ªÁxÐŽDð£&ÎS¸…“—I_D(ük©4Lm'É9R ¸b¡%C«ôü4‰áÁu‹yÙ"žËžðm[,¿ÍŒTØ`w{ ¬ebâö»ÙݽI¼ äÉ~¦¤¹c 6ÌÐÒpG„©áè#§HmFØ÷­ÀãLÄÄ—§(qÄž$ç‚5èÔ˜.UfŠ~ÿ쟀vâmû¼éS¢mè†Ë¯û}¿ûÇ™!ùòõ¸8.ñ–¦5©™©ÁìëºSq˜x‡–¾Š}ŒÕ¿âé=x#nýÂ1¶(´@¤ ¿ 8º@Ù‚Ý'I/e뾿÷ñÑÎÔbò}»yÇI/(Œ£©2©aµ šz$“ߌêª2Œq=RéÒWÂkÀÄosŸe#îÀ p-ä«ÒðÁï×°:>æ‡$BPÌD›³JNÈ£‹+0ùâÓR6Xľýùg°V€3…‹! M(È14¾ñ¹B;„ÔÂSÉ“àçÆß“+@ðTG«n«Òz(âŸx{HÙ¡£Š>÷ËoHŒzjb.¡jST¢ .¤‹wi ªúaM©… †·‡Õ`;)‹ž·;«&µö2ù™µ¾ Ya©}mí*z?|“‰ti bM`eŒÆ•yÏ4:%tΠdÈ"³÷Ú$ˆ«àÎ%>èY Ñ@^þ¤¥F[Ü€QÙiWp*ÿLêFªp.ØF7™€ õÊ‹& G‡Œ ‰$ŒÜ›}>Bú HSM01³é%øpš’0«ª¤iÐ !CMÃi yFa÷[–ŒÆ+ånè’YUáûÚ0ñÒ-2ͲVP, Ñ¢¡´DÌL#¨ÆnŽ.½wE·=kà¼ÖÓ0s©÷a¯wÑE¸Z¥#³*Y9ŠP î¨´‚ÜU¨p·D¯ w¨4Àj›Ø‹Øz~Q3žréE¶‹ž!üÕU§d7à5'‚Õ(±]3r;_4”hD"f'ºˆ[äžç_ù6¶’sÇéJgèwE¾[¾÷#NA•?.ÔÔãF™I Ëþ¡Ú`üýÐ^+Œ"v\#o<ö«¦\‡(‡QN)kxeðOÞI <A—Òf½¡²fÌ5œ¯¥pýaÄ`]-\ R2ù ëcguYPÒ5Æ«îØ ø@ãºouæ÷ø9zv„ó\±Š×%[g\£²2›Ù)¡íËý=Q¬Ë†û3y±Ò¦ñÝv³‚ ` ÈV6Pÿ¶}±­U2L­_a¸¡¦ø…ß…Š¥Üà}¥ íy@Ìü%áWv-4'?ÿ>ó-OÊ …F94ƒ§êÓD%NÐÓ~¦€€kÈ+2VÒƒ[¶±J´¢ó*žÓï²U¾÷4VJØÊYñ÷¢¦¥z5q –ñÄ¡£²ü GçT³¦á&9nŠ7FÜÐ^»±adú0x¸©õöÔw0_m­;IÂ:¶ ÀšÒ »¹ú‘hðܰ##+h©LÐ!îã…¥Ô@k«¬‰æÜ>êô@Sb9Ϧ±fØuoJîMãÂF\;¨â‡Æxòg°®žÁVUWú¹±è9õÓ&Ú*02´4#¹—®Åu”‚¨µ(h~d¼•@ƒ£•D$™¶–IT0èP‘NÕNp–WÚ +üVD†Urž0^±÷z¬Dõ8CáoH3ÈÒXé'6§†úòl lKO8Å`pûüŒ¸;5¹t«¿üžÄH Œ5ðò1¾ä»˜â)BmU G‰åŠFê–QÓÈQ36nhõ‚År%Z¨˜{Õ~þwE½}@0L(cd#O-OPºº[[žÍS%xKVA{¨Àœ‹e[™tzÐúmGM\°YÍER¸8gYn§•6Ñ«‡¸ûéT¼]ƒ ‰fPbÁÐnñ­ÛÍûʲ¹ŠÉ§•p ƒdí«³“µ3ÏËœ ßœOž5®™¢Ê%‡Ó:š’ó-c«_®'0øµŸ'Ô>ä¶ÒÄlïkQÁtVæmt€Gœ2ãÔb@fÚ dš1V :¨TìRYEO!h8Ê–-þæÇ*m훊%E´?>ýØqåo7 ÎÞGZ°ïA¡-†ÓV;kÀ›‹çýÒî_¾¶þJÎg2°Õ>f©MknÙ²‰Ÿ±¢y~ëŒÒK[oDÖfð~˜sp$WpFOñÛg/ø¿¨Ë—ÜÏŠˆš\žô莀¯ßœ@Gƒêîè ™&Úåí^PóàHDÉm,MÎ1¶æÉ¿2 Ö«åî`.³¦ß|v‘ì(Ž'Z»ØqÞØ¢r,¯>¡)ÑÛ¡ÈîÔxKÞ»ÝQëý>àÑÍî¶lIεù©«‹þ¬NJý-ë³wrOúEì-=Å4ŸN_ÕïkåIáz¸,'–µJ‹(°–è"Ý­W˜¼0œ[“‰ýU_XVw߯ßò­:œS%q—úòv?'YUv]°¼pSö዆Às`NÛnvú™Vu=/In»ZSxùÙ1±6»•ÿ÷8aû;H¥ëlÄ… @ðÿŽŒkÔ5ÿU¾Ñ €üË.ªõ"†H*†@‘h"íTbíTEJo¢ªáH(=  ãoa¦F¤ûÿ°v&àP½ïÿ{Ù÷}ÉNY²/…¬!ÙwÂ`05 3cÏ^ˆì;i±²²„’¥(²&{T(D¥þ§o¿ß·3|F¿ëßuéê‘ó:ïsß÷sß÷sžãL,Ê,ûgb^âÂ÷«¦Ûܸýõ&šH œWÎõl®ooHĄ̗¨Øþ\ïwòC×/(&sêìT>×T¾¥Íóê‹ðc}a)~ƒ|ÑñÄ!´áÂòéø±¹g¦ Ô /GœÿjKBn%ŸÞ¾pøšÉóÊS¦Sw!o¾¯9ä®\Ñ¿A2™°õåâpÛ­ŒVôʈë#´úÛ3)´ kµ_0?aŸK‡† 2¿<êNw]Û„Ñ÷kLGcÕªC/íX|3Ð’¦WMz¼@¿N;ÉyÁˆ}±/Œ¢7Èüõí1Y;:?%ÉGSÝS]á+¢Ö‡ìØ BNQ“‹h‹ÄìðÚ²tŠ@)Ÿ%CUðÂÉ 9^_{¤ 9´L“lyS­Ÿ/|ê}¬s²ª^«þcirêå£äª=-¤EÈo«ö´æ“:rK9È=%Þ¢ûAŽ>Âx$#b†´($ÜÒyžŸ¹g²¯ºf•%z.»¿ê1éçòÜ…1¼ÊÔÀ&å2ºwrµ†Ä7áx¢Ûx˜Éá‡õYÍ« JÒuª'ß4")Ru­\¶dL÷›tÆ"Jbœ/M“²=²q$ëdÏà‰Ïyð¹³FíÈáJ ‹€êì0³àà»~vÎ4èke^É霶‰k6>U¤Je}¥÷æƒÚ_Ps9¬ó.^k¿pÙH«hÖWöúñQçúôËäé­Š‡-¾W£c囫%è¿7 ŒË±Âß°Ï*9Ÿèhøó^GmG—™3g5O¢qdžš—¤âóÎ:“U ÒOêß=š†&d0awF¯BáFQ²×$ŸîÒký1Q·i&«Üõ­|0mYq|A+’ºëõ)“Q®G‘Õ«†ã}c÷d'# ŒDð603ä„«´x'KÏÌ9¼tÌÐ~¦á¨}|ylY¤^+f‚tý^KðÓ’»-O‘峤7îÃ(jQä+§ªRüï5™Q?šV|—úæäQ“&sxžîªQ†gïpÆúݨºÆFæê”5±m^W†Ô¿ÌiÜp$oùæ ÿúo<ëäçq/ΖÓsÃÓ…ãÅr£ý>(ç[}?<™9Zß¼’Îû®~§9æk7:séÓ¤îÜúÄfåXо qÚBèàšéjŸ­ÁxÕîµ9º…>%’„C]§ýç4 B–ëß$$¿è‹í20J*ÒÐôú²XK§߬j·CïŒ;ݺþAig|MÍô•טñ›¾øÊ¨¾¡M¢¶ÓÚㆌœ ÈAtéÇÅ,¡ÕOÚG¯”ðe=iü||\ýÊ”ðR¿ úMîæ·úsd^]#/¾6IX—3jɘ޾©ÜÍœI^AüRY`©þ(µ»ÿÕÛ·L5ì8®§ß¨“„.Þ§÷¢Î^¶í(`Ë–ÄÜ'ñrz³(cKòÀé¼.¡ÇùÅáO9¥DÆÑcG2»Œ/ô+X6}s¿ZP–=x} NüUâG9±§ZcÎû ï·§k#š¹¾H¤¿?—¼Æ+7ÃÀ¿BdO,´Äz?-ìäÕkJâhþ7sÛŽ[ÚØºÓŸýHÎ83¿õ1úNý6´òÝé:Æ>“ />ºÑãùÔ±ÕË4¸6À<.£H”ÊýS•qÉe)ó|Ju¡/)W³§R›Œúë+ŠNùŠˆœóm^ox™ò¯ïh ç¾ôÎ¥(.Ù©k×ôu|Ã¥ã[e¶H×?ÜÑÆäS¦²ü%ˆJ®Þ·R±þbÔÎû1•Mò‰.Ô^ŽüÞ ·ù…Œ6×¥o4à íl4—Ò‰½a3æç6”¢èÛfû}º†R+,¨0˪³ ‹P_~¶lô1z¯¿¼vLY×µEÓ½]z#è„P_mœªºê1í+› ²…H õª3±‚–›ƒ7žªÈE4Ž¥tfB§;Î0œ©ŽŽ­ /HžyËŸÒfËT;q½™‹­!Oú(¥ ßµ>"—¶šÙ”Γ,êèÖ´bV÷äaW¦ÕÙ¤‚ãfüŒL¤ÄKœUNNè"o}€§V{¥& z¹ÀIj׋e%§<cÜ„ n—½W.͉ŠîPÚbÒ89æô9¹¿˜¿èe{¿JW¦tA»œ:á!åÏ¥g²EÇGÊß+¸¯ŤVœçŠ3€w¨T;×-R;ªŒ…9ô<Ͼ"˜¢¿n.Û¦`ØÍØô8t½Ð¼"­w./m°:¿fää# ŒKæçE&µ7ÖØ2à²RFsÌæ¯›o£‘ai')½¥%™ˆw»”)Ò¹ÝÈ…ZÎÌèÔ–þ†üNÛunÆÐoôeåçé¤Ñ§]_l¾¬È:×MÑ% 1gj½9ûͺËzèÛ„¸ë&ÉÍ/on6݇›••ȸ¦|°²…]°cøéÓa]=0Ññ}L ¶lšÓhÈ˵պóñgѹîCua¶d=ZúÇ`³â½.µ·È‚ñË›[鸚&o¾ÝìLwyË|5³jaNÉŸH¶“[“fé8ÒÝô@û¶cï̱¼±äQF×ÂWÓ¹n°¯ÎËpÍ>ùyIÁíºYr`]˦ҷØ8s5½tûá€áƒø Ú©.²uGáDÜ·tιzØŠ©F&ä9ºIH•ëðv"ìÅt&e…Í?ÿ˜ÓCž<¶Á–8ÚS'Ô,ÀÝÍýó¥ëû¥•Ùý/ è} ›´;]ï¨ º¿ ËexýÙe‰§÷pPж÷:‹caƒù±háÛŸ}RI“R'ýn-|ô/…epVÚ.¢ˆsR1e§{g<½Åú)¯°20‡Ÿ^î|C^†<[Þ!è¤%#ý÷{Z÷N¾’,ÜЊ;´ÓÕØåpöš‚TóFÁÖä²Ç%ÿd³o›=Œñ;^^üzÂŵ2~ñA‚qíïMŸ\½UàÀFí÷ñD¢÷ §Ó\ÊⲡƤ-}5ýzì~Ni–Ób‘Ž7R¸]Ú¥9ß’¢]t8Z÷2Íc’t³s9Ÿ]€Mýå£ÈŽBñt+©(ËÒ0 £ní¢'WyaY­û1›{ËûPDçc/š§3õ¥*ÓŒ'Ñže?l|ëžÖ9†YÓCsë\˲ì6:!üׄÔßo½åZ¦»ÁTr”ýÞ™/šï*Fm¤Z·÷åÔ–§1ˆ4ê«d>‚. 3w*ÈÜ›JL©×'Ìß³‡ø£ä3"?>êqTœJWr 9mvÃæè#v²Ñ>ÇVsMTŒDNìO e7ǯ[ TûasÞCáïsó™PD;ÍׂDšƒã¹ÒŽ=¤n`þÁΘB"sçt0™ÉJpÀÀ„Ëì¶¿íÑ`e†‘qúÚ&2ú a »Û¦Á7ô(%-$ÏÞÞQÞ2úØ~f¼WêèqšÑg'ïèˆÀÓï Áó´ ÿRH53]šÖ#OY»ò4Èï“ÍC÷ït'œÃ„¿¤Ç“MéUÏÎiR~ÛT2U±½ ûäŽî[ÏCo$;—\ØÆ’zO™]sk$à&йC¦’KÖI6zþzïÒ5©¸ÕI9¶Ñ±•qš·eyk-8Ü´@À‚ Al‹wcöô„Í»+—ZÐF,|ÛŸ,F )Y‚Oxä¸6Û]hO½¥äîÛuAR̹7¸¹Ô9Í/kb@v*µþÓ‘7ã·3]›;,F>ë2•Ù¸~LΘº/¯Õ6Ø5'ò*èDñĺ…@žDÄf«9æˆY-ï‚Í­7C¨ÍÓ´¶l_Ü>"–i‘gGs_fe½ä+pQ­¯Õm*‰Ûa»t.,6w˜ÀCvøî)‰u¥û/ÊÞ]ä¤QK>L'Ã,ÏdrY=•Mz½Dœ—ö±qõ@]Mâ)§øºµ ›§7¯ðj¹|¸fÄŨÐçøäÑЧc”/™æÚ¡rÉFÄߣÅí"¦ìL%É’Óª?—$¿~­R,Ñ¿müõAî41꣣Os½n㑉ë?Aïrt5r¨{¹Tÿ¾ÿç3âË-z‘lšôyÞï_W~~UÏ¿ãHsVBº–¿ÈÚTU³àÆ!7J6†ÃÇî•59¹~بÈȈcKÎn‹ãýyÈ⑘g(ÛOÂé˜Ð/“_Ç'™‘‰—«FNv­4ùsÑ-÷ë¾K˳Û.i¯â"k^iµ¦¢â ¦`”Ê,£’\u5¹+:Tµ®Øõ•¬,ϵ¼ñ6}tÕëØLc>Ý+i–®D =CXBËI¥.Æ®‘.‡ßbÿ–ZIשŒ-÷pÇó$úÙÒ¯ßzË:<êóêZDBãªà «?næhÍ(ÐWOüò¢éÙꔚX/_XTdþýÞëG¤Jú[¿Ð%xrúêÆá•­ñäö°n‡ ëʧ‡ÝŽ;슄gW.7ƨáÿØ"Óc~-5"ñŸÊs~¤Å‘‡F}S­ASW(y?NSò<<ž‡V5pØ™)‘iò;-­¥ž@ðKÄSáPÉ5ˆ„TÞà¼ê’¸¨·0&ÅTßT¤2˜úä,ïŒmî𣶾y—n·}7óIvHÊMÔDº+”öØ ŠEsjˆðÛ9ü >aýüñKÇ ÆôËÞk,zzÛ$·*öUžL ¹à)~¶¯áÑUÈZïLxgŽÙ¥Òü?–D|£<Ãî‚GáÊ-ßÚŇFÖxL¨c5/þp¶Ap2±|bÈíìq’gÖ£e;.,'•dqÈmþÓ+ÿ´Ž~—ùç5Åꞯє¿Ók®0¹$^\}iåÉi«œ‰j•è¤çu…–#ʶäÑc§È»r§6ñ˜$£ #›èÆšM½â$ºÕ5phûCŸ]yU3lhi_ÑHÜ}Ÿ A~Ò»kRÅ“·¢³’¹>øˆßiÕšS©lÅ4B5E¸ î´xþ1/ýÂk¾Ó}|³>³Û ÷.dtË ø(^¡>i,Û T{“cÛcþxãÓ¸ÏÉõ÷Öžj°>K#ÜŒ6ûÔ9‹û”oLrò²™>6’[äç‰ȨoÌŒ‰~°feÿ|jÇ?bæÔ‡~Ö ¢ç÷T(n„•O0?­›7ãã}cÂοs*àƒ'¾W‹æãv 9 ·U3ª »"hñ¥Y‡ðw )¿nÎéµò%*§†ô Ú o³Vs§½}gÚÔ•R[›“¨Èù¸'õÛÚëŠáÂzÊøWRö·8ˆ>I_VUT>#÷q¤¦iÂæî³yÂôxÊy9ý)Ûð'œþ9aÃ3ÕÖ¬’, oH$ñ {äЬ¨;WIñùû¶q®Iž[§ìŠo¨òRЯûÑ3´Ûýªûô³Š7üâ (%ŽzÚõ¸å­ÎpÂ}„­^Àh÷E59ƒ;„s•†ÇV³Ÿ‘ú—ÈöE×çROr_vÐç›(†¦<ˆiÎ!,xƒP¿\¢•Éhð¦yf²ab¶É.IòRQ¾€qoâðcºb2ZQ„ ÏŽÊ3|9~' :ò„×Õ¼ü˜‹¹f5ÂVÒ<ÑÅ–Èb.™dÝ©XWbT3à{–_©³ÄJFœEf+_O¦Ð¼‡RÀ xsrãXá^rÉ<1½ç¬_¥KŠZÓ¯#—ô|…3m_0ÚQyöÝ¡SŠ*ä¸íÕæv;×úqÃøÓ©—.’Évø’¿ã¿ÙžßcœèLMdsÓnŠu`¤f³}uÛ%U›m2ÿ¶qhÇ`aF! "©TßÄÕ·ÉI>v¥lfMúx+G÷HVÒRÇHaÖËZŽ¢§×>¢¼²rãáýž :õí23Ò€×{üßVø£V„º?OßÐ(Vh}™V*n¾Ä®÷<6Ÿ$¥R@BÁQç–Â'Sëá°÷j¾è{ÏäKxÎÐÌúÅ“Ž}.ž?iPÿyÜAIá&•\'acd&ûRž…§“ÿ¼ÝôC·P¿‚àz>®ô³LÖ¡”›•œKôùÓ¢nXÊÉö0%Ò2ô™¡G,ÃÔ„YdÖϾ­÷.Þ¨«îM\Fˆ/ß9Þ|<ŸòXˆFsRD¹¤|±åK[ý‡ÆÈZÁ³¦DÏY%JÐ1ÜuQêõ¢ÆTý&R]¾ øÝ¶²!ŠÈÅ–W½®Õoï'ƽ›jýÌ›AñÓ?ô¹ ‰“f|óÖÖ‹†¦¤Ü—Aï¿Ò~®EÜÌóÞ23[¶7<ƒ‡OOðçý?ûƒ•Êñ |„ òçÏͰ_ÛÛ»Cáÿy=Ã?üUQ·JÄ¢²€QðE¾ëÀ!¼]:”•àæ~æðE³ëÈ,@„æéÓ¢:úZâ†g~ÿ|ø(«›ý'|‹~×áï þq¸‰™±º¦‰Æ&$K? ÎáØE >ô‚3Ìæá ópò·:… è½Péƒ/8Ù€ 🠥ÿOéi€ùÀ¸äì¶GÎ?pOQ4Ôö õ]Ï8àXoÀxl»ï?„Ýèüã…`É÷U:À(øbßJ!ýWÐ.ÿºËÀ¿NjRM tŸ€@˜w‘Na“þó÷zÚýe§2@ ¥~Û÷…} –˜!ož/À6u„ˆÀ.Ìš6Æ éîŽôWý58@:Bè=¤}Ê{ÍWØ|Šà÷kÆþ0‹¥Êt;r¨±!€úàÿîQÿ@Se÷…þ‹?t,ú Q*ä÷ÃpxÎòáíøë‰¢‚Ê^DDˆàZ‰]@f…}ÿØ£½==‘( ÌÙÞÅ?¸‡Þ3 ¹³~ޑ܅G(ÿðXò·l£<È€T•'ö{Óï?_c_¾“+ éí¹‡Ú†»Umª€!ÈÄ~ßIûC{gˆ›†%ÎÆä™€c—; ŃzÂ÷Ð'ÙôJ‰ZIý~Ï -ê@@,‰>O زB ZžˆÈ.âùHD'¤‡ ÜÕÅ Q{hõ’GÆm!¶ƒ@Dw‘'óÿŽŒ%:=ÄÅĪnÊß5ÿÍý!`î0ÌžKV´¼¦_p) c»¸½Oÿ†‹%øv©gÊ¥=€ß8wÛû÷VØC¤IŠ0› L?ø‡5?Ž‹…•GÏÑ6¬Ë9´‚œG1«ûœ¡(Ä  N{¥øzž¨€ÉA Σ+ëebYPŠÅ ^ R?E°+¶÷‡º9í5Ë9Ç]?EÑB ¢` &‘áá`a%øâkEw‚~%xBð”´àÜC¡(4ŠÙ+eæW}ÕÔjPéùwŒˆe=Á<ªIàR—‚ÁW\È»?ÒgïÈÐ_`LQ 8å6«ã„aE tûÙ²#$ˆ5+8Gš!÷§ù9Á<1p¤Çò´ïDêB/IìZŸ±¬GB%øÝÁ¾`HؾDï}*âwÄø<@›¢טìhÜ´]âR²µ¯Òå_Óš‹ ¬øû”°/î¡\ N°½ú }‰[@DŸ"ÇßÅë#b9ÙµÂ8ËÈYNªà4S—¼/tuB"0§½ë‹hu,à‘V)pš1(:(Ëš¦eÆŽ€%Qüþ å?Д¦A%’܆ÆìeÙY|ÁžÀÌ>Mîµ4ÛþžŽeåàgêSÚE§„ÿ†w…íe_¾r…K¨ ]epœ†¿ÀMÚD.VÐF 䋃-[ñv_œ' …†£1ö.pÌŠ@Ø£¼{Çl¹løc@ì a°e)VÿžŽeÙíU—h<`)xÖ\}Ž@ð÷Ãÿº÷¼‡ÔõÞï¥yÀ ¥ÎÅçq±°„ °Èùóô5-x(H‘Þ('˜7øö ëâ¡Ü^@ñ9Âøû¥³ ›Š†bIˆŒ,•1¸Ó,WߗІ{¸"`¤à+Œ uÞCïä:™¥-@¦"wšùZGÆüt§ÃÀ¾¦›â´Î¾èÿ¶‚ûUÐÄaúïõ5Xï¢N꜊5ÅJhÐó(@(ùQðŒ0ÝëókìYH}õɺŒ…Áüà¥k¼ÓpXmü^Íÿ¼5v÷Usaöå!ÑÞ8âßVÄn™Ð»Àh^ °ùüËõ¨s^Þ'd&O-Z_\\(Âî±×ÔJz÷í¡ >ާ¤€C±¤>fdéq¨^X£X0N*ÊÉm¡ŒTm-‡BHN ®Ø#aDbÉ䮽™P$ª@Y0S Óî ß«—OôVëܺ€M°—Ø3ÊÄJo)y˜Ÿç¨Àë@»»¸ NõŸÿÝC-¿IåRO^ôà8sÿ¯ÀX’9flU…‘í0ö'‰‚!÷ªªÏË{;ÈQƒ£Úr`(–Ô²Ò‚[@´b­;pQaŽpè^É á˜p³¸þY*°ÔÛO Åʨêjøi'!ÇE0µ¼7õ?ñ¬õ•–¹y 1B4N„1d¦b‰í¤ªZù xk„,VÏöÏï΂Ś> ÎB¶œ,öªóÁ©XAà‘9Ùp‰ÀBÁb=/àÄ¢`0Gô^ÕòÍ:  V5:€Å*^>8Klø,ͬÅad’œ_ãïàºBá{u}&ßy`‘qYœ …*ˆÄ’™Z•Å3@'˜iüŽDûíU°®Ý’}tp?-5x²Â;ÊÄš¤TtW Ƚ‡ÁÎ?׃ êEUp¯Ëm¹äP! œÇêªÞõœŠ%¶.׿I8°^—e[Õò%.¬zï8ÝbWåÒ'تœãeb ¥{ªy„H2`«FÏã‚"=a{KÕÏœ ¬«تã§b‰Õ’Ìk!Ú•kL`±xä„Àú¢öʬgkó»fq€ÅÓœŠ%–Õ„â¡#0 ž2`­²¸paQ0g7è^Zùná#€ Å ¶Àÿ¡ØE ¨.í u™œª®‹ã¢þÏ/F…fôؾ\Í ž]²DbÉt­ UÿÕ^’‚/þ‹ N¦?sß+Vk)÷‡@$ÈÀîÕ88ËýLjç)@^eÁrÿ¼6.ìŸ=ÃZ¶Ü-ðäܯǶ@‹þ¡»¤rÔ‘ýsßxwç*h¹/Õ…Ä ÿºÊz°Ãwé"0ʃ€;׫¿cY—:Á ì^aàÕ{»5n²+аßïN:çÊ&>äÛ…>ìû—h,Ù',£–¬²Å ¾Ÿ…ÈÂÍvƒA74 åwÚKùd‘í- :üÙÁ÷³è ÿžŽ%þ–¿çSm T,«€m~¨üx$ã‰ðv…ïՈߒ{R dÁ6ÏjüK4VÖð2k+«úÐz9ðDL~› wwÞk‘cªôŒ—QÀ*›±S†bMÄÁ”¢¤X M©Huuý3nª; ýµ#fÿˇ0”½;Üýµ=aÿkþïqis­§C¨í:ÝäÖÿ‡ÓaPuö& ÌÜ£gÚË?pŸéãóðÙ«+r9L*Rð½œB¢ƒc±NÃoµ ÀtºÇæVãæ¢`@Øs?ž Ä†ÞÈìàt;Áø7\,ÁÃVº-@èlˆ#=’7xŸ ‘I… Ìj©±"]âÀP,©´në—%ê8 –zÿnª/|ÏM²Ðð¥orÚÈa°ÔÔ¡ÿú0ÎnjÀÃ}©¿®ýo¥ñZ ¾?0ʆ€ç‚~ÓÁ©X[QYªÇ´1»ˆµF lÆýuî³—]_ê| äˆU°Ð]†b…@Ã!þ @‡ØÆ ¶@âKT'”¿'fŸx%,¾îPLQvð¬uø ,–Ü-뺋@–+:é]Üsïppa~˜Ç¯Ü@¬9ÁÐè}”>L­’Ä ø–Ì®3¤­ýß΀~¼¼œÂ“ìÉ ¯8NáGÀöžÃÀl) ÄØ“õĦb‰½:±-¥ IJ¸Y’ Æ…EÀüàŽ˜ýþ}D¯}çÑ\|àfé1ËßÓ±ÄkX 'Ö)|à$¿%€ïÃø"Qö1öJM8þ) 7Š‚{ê3Æ’,ÇGå “Ï€ƒã• ò¯¢´^ÄU'•„MWLNÅ[zjÜé×Cw `¬,Ú †@ì£ö‹–ê ZȳÇvð_`±äÖ“Ue„R@ ÎÂ`ÛÞKÂÁÅ“{µ&l˱u@3õClŸ‚ƒS±*^¾:ûýû@Å«ÄzüSƒu†Âp<~Êóý¤½‡€ŸÖq«;Ëš.‘q±å@fðf﬛Öï üõú^7*øCñtzñ¼³®Ô†‹…%lÜq†­ hàŸ‚a{÷…¡|ÜÝ÷P–•ªŸ` ´çoÉÀOf´à„a9õ;ÝsÂnÀ© €SÅwÑÌ^ïKûßýyg8ú¿yX”©Ô°Qáa­kGÿ޵‚c ìš{²àÕ¡ÏØÁèûo«| âši†$ÇÁfY}ÿ·l¬pä—»‰ìDþä@ðý·X®\ÚŒžÌ]O^Cø·l,á3¤Úºw€>¡ˆ,|ø€ð}·[$k~xSóã3#Xø(Õß²±„k#¼æ~½ò•,\†õ`ðýojKJ*´^–¯øT`á.Gþ–%œóyh/ß0‚…«ü¿ÊÎ5&Š+Šãƒ.¸¨ *Š[d*Q´­&E@«ÄÈC-UJ\̺°Ëºw–±•ÚÐÄ?ˆ‚‚ж°±Zø|ü@D!šUãhë#„T­ÄhmhzF©›=÷ÎÌðé&ì/ÿÜ{î9çÞ9gæCv8ý‚;rÂ`Ï/Àvúaá‘ •²ô*„~J[òòí¼”+Inp8r9Z¯Â‹(ž¤@÷#‘c‰(P¸‚–RéÜu¼µFBß“{<ßà •”ê×Å’T³AJaíÓ©Í<Œör8,•c‘Ŭ‹ëƒÂ =™ùÄ2*l´øPŸÃ›i×ggÿ¼|’ï¦Yx‘}“Xx„@mÄñÞÆ@Ž+²—¶_JßHéßVHÑÄv[ûúŠCÞÕλoUžR6!¼ò¹«/Á…âÖ¦ÛT8ýìþ³üšp*ÒÀÜ0Ü79ªý ë…ߌý­{FùMóÿÉS/Õ#¡½>xo;Œj8œV·+Á³÷uQtù½UWåŠ×ŠŸ› VcÜ(2G^í®Ü§ã¸á7ØÌÿÅ€#äEÚ“Çg‚™‡-Àí…üz5Ž×B/\nN]U1…ã²c×ࣗA²†ÌÕ‹ HÞšˆ×@.•e3Ù-¼Ñ’©‡8ð¢0-ms×% ¶LĦ“ºJ^YÔ·uÿÁ벯‰nk‡ð•o2d ÷ ™?/WÖÁ†ó˜]OI ˜Mßþº^á“~È*0Ïd5²øcñ¿à‡|ÒÏ>¨LH~Ò¸g'ìÃ#çqçK•¼Õf¤?,Ó­¾¶¡Òò¡ÃØÂ›®K“¿½¼ ”M2ø¨¨¦¿t9!Ì©ó®êaCc°óRûRaF“ÍÎX÷%%òFåÛÝ0 öÀ†3ÓO —Ø2š³;´µ 6Î?{_8Y,RÚYÓ$tŒüªÂYÔ4V&!4Éoäq @‡TØÂk§ËB%k&ï ø?~óš¨ÂÏ*s5ÊÈ„è-é ¾艽çÑ@y´XÕdVar֚ѩ@×dZv*!6øÊó%àCRÂ0vfˆ,VüÔ<ÜôÆk?¤xKtÌS€%ä.»Ûp÷7˜…›^Xn]´,Wü¬¼ÆêS¹°=*,·k©,!w\AùεpþÞMf—-—åŠy\Ym«Šê}5žíJv*áÆ²~:·ï3ω{ FÏ9ܱy¡” HÌfOá´ú48Ó÷ÏÅ?'w‰ß ÄŸ×váâ*pDŽŠ¿•æâuÔŠóÄ×ÊÅûè ˆ¿ªãÆžŠ”Kâ×л`igdaÄÒâw»ºhëZÅi¢ ¿2Ï…KlgÀ½ùtô PKdRebK+ª¶ à "¤bin/WALinuxAgent-9.9.9.9-py2.7.eggUTê$@`ux PKdR7t‚¸m¤· HandlerManifest.jsonUTê$@`ux PKdRpôŠ¡ÓÉ ¤ ¸ manifest.xmlUTê$@`ux PK%º WALinuxAgent-2.9.1.1/tests/data/ga/WALinuxAgent-9.9.9.9-no_manifest.zip000066400000000000000000023351671446033677600250600ustar00rootroot00000000000000PK šEUbin/UT o:>c;>cux èèPKcRebK+ª¶ à "bin/WALinuxAgent-9.9.9.9-py2.7.eggUT ê$@`o:>cux èèœûc°¨1³.ŠÛ¶mÛ¶mÛ¶mÛö¶mÛ¶=Ï\ëܺµ¾]gŸÚ÷þèÔÛQuÒy;Îi`€ýª{¹ß9?ÞZ@€* è¹úú¶†–vúútž¦I‚öÙ¡w_QäŸItnNU§"¸ê¢²Ž¦9"œuñ ][;¥?³FêR*pýLö{>Oë”i£pqWÆý;‡¯È~'1ìá æ±Ê±re(ñ:‰¯ÍâåoÇq!ÓURxé“0sÇÝãŽét¨ ÉÒ–N¾ôpÁóU€› .yÃA¼pÛç*‰°±“Í­†¥ÔBs­%F^ ]"µ"ö¦®ÕÁPÄÝq„ësOž{o•X†ÊG—žÎh{uPO.ƒâµó\„w‡þÄåÆtbÔÜ«X‘§Ä^â¨ÜHX ˆìî:èÎ!0_¥;n4Áǹëz ¯òseþ­8TÂJ®Ñ“»'¥~r"ãÿûºõanÓ4eÛKÕ°kÐA‡÷hÛKgò‚ªùuçùÀ,©²&·*êxŒA|clFºÀÌ ì)ŠßžÔá²S™¥½wÜ,]gŠï_†ÞÝÓβí„!üë_á"cï7st†wÄøîìnl+zŒ½]­Hd®ð±Äi¥áNnY'̵[Xbì#™ô´¦æ÷¼âúˆ®ðÿÕ*( ‰Ò{]xö?núÁü§V¦§L г§@e®†Gˆ@#000010‹00•0130²010‹°J3ûåƒ1ÇiA‘"UKE+93=KQ#/*=I½ CPI€Í4;"3=±ºD#d¨f¦%ä¥&h耱 µ ÑìÍŠò³òêðñáÑðÿ¨+hªØ$gç+e%[‘ƒ,Â:Aò#üç ØqYˆþ-Í‹„ø/WT\œVRNLž^AúÿþPŸ‘b€»ó…êë×Ú“Wq0¢–èw{ÝlnIV=Œ‰/»‘*%Mùñÿêf¢…²{%1Ò´õK^9— ·’æóUïyHª}úôéÝðv‚’‡Ì‘þöªh2w3c6ú3+hcªøL"Ìu ÛUô!;€¢ž¨š( Ém$~ ‚ǃ£Ðl‘i#ÜHêS˜É¿Ýg(5ÛºŒßÒ‚Õ€ Cã¯q ‡îÅÔÉ2Ù-6õWáe”uZa©IÐ…Œo E¸ü™6äøŒÝ} «uO”€?›.3ödùTK1åºä\®5´XÑ¥yµ¹ "ý#íÐ2–¸ü#Ta).²&àÄaÇø+È»üú¯ÕÜ.ÔØ@*ô)þNè&–…þ#0€Ë¿,”ÿ9¡ÊòªJ¢Êt..©éÒñ[lcwŸº4è¼ ãŽ vA²J&ÚÏóÇdiD |_çåw I-­qëSF3¿ò>Óx_¯_ßSåék}X¦y~½<¿?7¥'ÈÆ¼êa fí|žŽ¿_O_áLÛ°.;r™«·¿_žo'Ÿåø _Æöøý½=mIQ 1 Åt2ÕNg-ê—€x™¾öÎÈcÈmæ2˜¨d²õ¬örlö´›ðÑ?jn­å –‘˜™~ øà‚]I¬ÁA"éEήM ‘öƒ7MBXk/‹Ž:e×Áçç…Ô¡F0!5û!ã '»)*€ƒœWÿö›’=s_µC`5†l¶@›µ§AضæÖkÔúÄò`®Ác?¹ Ìp@-IXx.§¤š#ÜQéÑSÙsxL0¯;Šîdf*­êSá,:‡Çß>Ã`é·pF·añtAs%ˆë ü=§ž½ú“¡úõ="ÌB¨ÔpkSŸÁoUdbÈ=ź%Uí R7´ÌM©øøb8ß ¼ «g±"ME„— %ïRš P&µb83˜TjeV_Ž6]æ’t!ÔYЪ©¸ÈÒ;‰œÁ”Áe+O±í ­#ÑÂ@Ix×P÷·*^«´nÙAÖ†ûJÂüj·ŠL~· è±¹Ž8«D N[v7¦ä6#¡‡ã…·ð¨ÃƨäÑžuŠ÷^Ärü@$püð„)q(ÒGgZóeŸÆ6R23㘫—tÎs(Ž:»è6¡ôÆÐxOyú/9eºÃ±˜UL¨'àÃûb¯"ŽIaL³¯â({ny*Ò௠ôÍT1v«Îõ´¢:uD%ªfÝ#„ZöùÏ1Ûäðj2Ðñž<ïÇfSùÛ誥˜Ø~|‘ºX‘Q¡’´ÛÒ^­¿‚™ÿi±×ïïŸzù¥dÏ߃!»³3GU”ìKWe~¿U–¢k#•GÄÌr›Å¹Œ2Ìk4†,»Áç¼ ¿d#ê˜?i5xðžž15,¼â‹:Z^D&¡ª ª§ájØÞ¿5Léé5li9 ߊRìN€oüYß¹`ø1¿&6_òbxCvì¶%¯^Hê­9`w±±5¶Ïõý“(wÁòøàw°Ì±Óh™u÷•ú\B{=4ð¦£s_¼ƒQƒ¹ Η>¿ÿ‹ÑM[cúg"þÙ\Üÿi#LLLíLLíŒ=õm,í¬ÿËXý/­3–ð°ÿq˜ÿí¶v±wз1u3µù¯fÑñs²ÓÓÑñgR5T´R3ÓÔµ2ò3ÔÿkwÿS˜ÿؼ,h ÍLÿC€ÿrg´þ8øÇþ«îúÏù/A ½\LÿÉëêahnjçB¯¯oigéòÿqoì¶DîþFúZ"ª»9QD½–pÑQ±¤MY&ÓÖ–€š$‘VR}|Ò™¢K(èB…±Ó5°º9m»¶egË¿O¤2ﲂÍb•aû¶tkæQ/æYÁt’…Ï#*USiÒ%£ú%>Ù6tQhèˆE¿é2ËY±“7ćyõ¨˜k3‘o5-¢ÏϬéÈ 5xôjd+©µ5Zë] ÂàôÑ|zöà1*2ÛétÝÎ õõεvuoû"ìÿ/¾ðæøà­O®ƒœ„’/Ú¨Fl‰3A“9ÆÅ_½jhRäDJÏòuðÁÌnüY<]ª”¶¤YDÒìè_kžžLõà1 ¿9”¿ðË? ·Q/Z7Ö>vß ¤vÚðÎâ?OÛ«PJÕ¦ÿoïFƒx¥¡)éVÀxV`WiV=Iº-Ø ²ö¯°œ4ކù냹×@õHérø_Ö÷_aRgïOg[±NNFÉ*Úøe¨û´ÓÕ Pb3‘é®'KVêÖ qÍ×U × ß›-bÙ<¾¾ßç9¸Š oSlèö|yŽÊ}h{H¯Žéývz£òØDY<<ñEA2½wb8É›Ž'¯ÑzhƒÀ}…ΫûŠ-¯D`~WÃ>-$솙Û7³Eanm>5Haì9iÅo Í¿ÃßÎ3›ÏÄ®5xBgŽÍ¯ˆÍ ªì“ª,Ò– ¼ 7?vo[¸È(^Ã_M-//Ç–{’P¯–B#$˜‘§3v´Q´”0gØ¿”BúÅߑ߯dß6)ˆ·ÖJ l>TÂ9"¬VØ=<÷å°aZÑ›ì1ò´Dó`ÿ$£¬Fˆ[£I àÊIÈ ÆÌ–…y}¼a±ÊÙsÚ'Ç7·‘>àÂ…1'>„™-³±aâ×Ofüþ;ª¿Á/~ôDwT@e0„rNè«Bü¦`Î<Ë ˆ–ˆ@ÛqEŠJçl>±ÁÔy& 1eÈ(¿ê•ä7ônÍOl-‘Ý(á­\Øš¢kׄš9¯wdÁH»®}5c]ÀÃst®||R ƒˆ‡&ÙlÖÚ3Î\¯¼5J*"9z8 Ïl«Ë4B¼S:‚|®ÃWú×6G„®¢Àf#-~ƒê <šiì¤-Î[}öbò> A ㈠ê@¾Ý@nòþŽ¹ä ¯—€–—GGdŠô±b$y¾Ψ0Mˆ’c>ù˜¡ÐÙ†ãœH+ÕVlÕõ¯§|xÓúSæ·¡ c3=ø“~~›ÔÓ4|d:O“èTrpíè¾çž}H Ä{0<ø\oºŸ¨Üƒ`«´{Ó˜ ᛢSù$ß}7¼A 69D™AÄErÓ2†NÜ٫ǽåzêzòƒøgÛ ˜y“uŸ~!5þÌéR$iß²]íkù?ÙRÝ*–±t²“ [®ËÆØÄþ`Þ øSáÎ$ÞÛ¤3GM™ù{˜všÑX³ëqèÉCh2¸faôTÑ‘:Ç ÆÃB"6Ç~ÅpNàO/kñ–ðŽ«œ$Ïí»{ ÀO!9d?yqøw?@qÁÐDIþ@Š㙀òf|¢•¬Ç¬4'¹CóÇô}UÙ‚K˜a¬Äk²ZEtžÈ³ÁÈ|Æa°Eö#©Äl2¦üí¦‘=Q¶W¥Erˆ'‡OhbšRÑÔÞŸ,MDáBýÛÈãž(6óý€/RH€=/:ßÑ4•¾G‚ÑÔ/TÚÇá¡ ò¼EÊç~~‰0æ¼GG ;›sZ“h‚N•–Òs©-ßä¹þìÏÂüÖÌGƒÓú‚-ˆ@ )»bòzcrí—1Åñ Wöæ ‡^ä÷ßÔ ÀE ¡‡û]ù"‡Ø× .$M-.b=ûùÈRç+j¿™kœT–?ifz)æçSyݼqÈvÛ@ºúÑ â÷wÉ £.bÀïõsíX~*ÉsãÎ6À ³­ úæÚÛÌžœïu …•µ ‰M­…ñmµ:é&¢ái bâèŒ8·{Óß O¨&[Zš[ùÐ)´RÓ$‘î%DÌ/‰³¬UÛJæÝû|‚8$˜‹A,¾[¯8~ýo‰Gõ;¢uÅÊ‹RÔΡÁ Y]šSÝQ!Á±h“ó<¾~@ÂÊ ¨õŽX ôùED`ɿҚœÿä@J¸¾ŒsNà rlŸuV·}-«•1¥ëäQn‰Žw À.× ¶#5*£5Ïzô¯ñH·¾TÂ/ßøx0~±äÈ÷¹iÇ6Ÿ(ïi-¼íYD•$°ù*§Áäåoòò'úEºüa¡Nƒäšg€ŠV‚Þè§KФ¶Þ”?|7¨žl2h“9ÚʾóðÃ2 •Øœh–är ³FUF¹·Ì]nZug=¤ÓÇ»ë‹üùZ’×E³Ê{Ž%‰b*.>wT2Z©5ëO&‘šó.ÆÂê°Ë3jÓ4ì¼"KPDð'`„*æÄG—rðœÐ/¿ÞÚº³­ ©Yr'B—àDéU½ã˜ôµ… ̦{$Xgê‹ñÆ ^Žk¢£a‹^ehä@-0À ªAvÅÝÿ`0Í™V\MØý,Ô`à>îŒÉÊLN4ÆñåÊwUä¼­’ð[`éí×çxÖ©$rŸ¢e¸Þ¤â…ªF½^ =#Σlʯk_õ;g,­·–ЬƒòlÉ_YÍ*Ï(TeVó=@=ûö» G{Ç9<  ÏƬUð£rÃBö„ë"KÕ“£´ÂF¥wŠw@¨lÞ?¹zIÛá4“Y\U ÆqââQ±ÌËJ¥~±Q¼=7>n [ê#æ·Á\N… ë,Êß8+¾¸·áËô»¹F8£É\–ôŒhᆽ~¶áàe£ac‹¥Ž$¼Ñ(hSÛ£­Ï¹ß/§‡u7O~°QŠ÷ÃêV‘ Ÿåv *°†D½“Иs—¡U¿\V> ¡ÿ_B  <,˜ ]¦Õê1ÜÆ€i4E9ìoÊ ™÷€vØŒmv+ " ˃~v˜¦ô¨YÔl™ÖœXÎz‚ä­Ìì]ΑQŸ\ðj™ó*F†ƒžïÖ¬,Ñç¥ÊÖŠ9{퓺0ÚúkV]×acCÓµ@¬¾k¡Ž¶w^üRí6û2òow?{ü'ïož8,«?¼BŽR(óL¤Z~¨¼ß¹´¸ –)Ü(ÞnJO!Z•ýU¿JóG E­l¥U¾!—Y”â?0IÒº¨¼·À:a‘ÓÏ©®B*áð`Ý£E!èEhR>cš*z7ÅQÂøûu­ÑîqðÌÐÌèßèiAÍ cÙ‚×7“>—Ey˜ÀóŠl¥ØÐéX ¬ÍótO˜Œ9¸«‰­Ф Klz·Á}•û²„¿DõûRoº ”rJ,0X6š@°Àù¦@ªD¿ÃRËûcIpŒ€Ç š‘cîç6µÊ#Û sžw¥#Þ/†t~¡,ļO;† È#V9ˆx§Â4ŽxG¥CU\ ?É’DggðÙSÞNHÌ¡Ñ&‰Üܤ•ú(œÖ#q¯E„Üw{¢›- ÈHH·ÚQÁ±kh‡!xš q` ÿ12ú>˜‘ßâèKÖc«&^xäÛ³hƒ”í(Q¶¥D<Ï\-fWžÁ¤ÃÌ~zâ7o¶â~wN'ŽCØY.ˆ$¦¢B–ä@ß&S„bÈ"/ißoOTEΈ^OÔ£ûZ‚šPs²û¤ ”ª2QÁÚ² ÑìBs–²O¬…å(nŸF(n¯²]¸+¦“z8?h+»bµZYvûAª?@¶SZ–qîY&f¾ó@kBËä‡Íë¦?Åu×Éõ[Ý•GÔCs—)­]ÎV–Âf¶ …Î`%OýYYÓR”\m—œÓ¨Âª .ïŽBXG¡ÀkDbà§{iÍ®QÖÊ`›‘ Ð}V”¤.kP¹Ù0fSUiÔ´Dö#JÊfç–ÕåQ!%§¾’àâ•à(V‡|(¨%™#*.²mf „•§/\)É})æµ4 2¹*-è¾·í G•×ü§^eJص Û(õ$¥RtW(«{ÉRuç/ß”2Ù–±ê»d[%¨'óžÔÐ_ì²í^è#TÏÆQi˜®öa†hpìqœšø~I‹’”9”9h:}Ù7x+¬Â®WìêR9®ÝyÙ³,­ª©—ªXkËnëà ª?¶M- ¶ §ÚªˆHc¥’Ô¨XÚY±Yªt¯]~·ƒa©ÛBfÒü3Õ{’³÷ÜŒpP£Ås_ÉRkNÖÁÓ,0:=»Î=r"yÑyþrͶÖt—/FÊn«Ú÷Xi· Š]ÕÖž"™4Q¢Ò½‘ÂPÊ*}icÉW4â+ŠýŠïd4ÒNÑðð¥ÕcBÆVT¶ž"¬yÅ͹,ÓB³wŸYñüÅ?’M´óoýÀÝ‘æ@JÙÊ&TüEèÃÊZÏv¶ÝŽŸ( þûNAê¾›éô}cÈ ƒa£Î§ú ¨ãu±ŠR¾AF'¼&]RJ°ÉoȧÏpC·ØÚ»"œãdDÖÇô%q÷ðý–6·GW; I§“Œòþ#é|/§xÒ»`§ñx¤]n´ôí.ëZ_ˆ,t™Ý§Ó‹ϯ¯«ï½:ÓÆïÞçî¦qy[”%áS:Œ ]¿‰jçî:.Ns@ÿ LÐ|÷X1ݲ¶™·O¤.’ŒKÓÓá)â±ùwg—)‰`pewTa}…ú~Oík›l»ƒÀ1œî“¿e"§´æå<ª“RS´CHl¶ «Žr÷¬â/êçë­÷ ö'å(a©Â 8=†z·0IC‰HÇa¹‚Js³€žtjÛƒs¼o›êcQ ùR$ÝÕØe®À&tÏrç8™)Uaso“²¡Š‘}dQ{ïê¸RØ«.ÚžÞ&' 1Ẫ|Ö!,O’ã!pX öwPé‹lÅ[ʘ€Ó-[Rl>l¼zÿªáßì1½opÜx^ËÒx^ÅCÏkEʇ2©p<•÷QcùoQ0:88ÄúeÿÀ›Å¶^|ÝŸi¯ó˜]¼Ü_Üìd=xÝÁ™ØÙózTÐöÆ"ùÚÚ9_ª¶†ŠM%Fô»”Š»mÆQ²UêyÂ0=OðØðnJþ\DLÌkðaõC:`Fn#ì^"̧ùåV—1¯"†ºx¯“4œô“Ç~­yé³´ö&öx÷nwó¦4k©Ü´>Þf:I“œFêômúPtkúÞ£…‡Ñ©yÍ#y¼O_7›-Eí1™ñ5´Üâ_Àÿ Ç yö夡 3ÿß‘½ÿ] Ã¸VKÛG]‹—ÝR¬¸ëZ£Õ¸¾a,=‰üêìOS)7ÔäøJÒ‘‰Íw–$sÖEžÌÛyRLš)rU»`”©&¨4*Šý”Úƒª*м Êò=¼ö0ýý 0èמÿ¬å‹²}•Bç¥ó¬ÿíïïwºsÒ/gÎgW_çýL•Â/éŬaľ=,!ád Ö dDa¤¢ðß QNÊew©ånÚIËï\Ä“˜<„Ào^ŠÉànêKNE|¤Bݤ³$CE¼URb&ªÉb$¤XÆôGÎìf ™ˆÓ ù¨Ó ù‰Å‹„™ä"¥Sp‘#ÐÒÉr?–2ѧfŠü´¿59ˆ3ME>j§ÎEEyÈ'ÎEžz)sÑe”“ez'ÐEžŠ)ôQ§¤Š½$Yé¢-zI&é#OQ{‰²ÔEÿVY ‰t|b!ÅZyê¦üQæ£ßÆ6Ê• )ò!?ÉÄ\(-V´DŒv.£˜ƒ2ÇA=IeÀÿažÄoǸ’ÜCµ½â,dC*òùÖ 3`Dê€׎AHòVwï‚E§©ñÀ˰îµïY?pV;¤Â¦ÅZÏâ{·•ÆSOR„â"ä a N^©ÿQÞžEûʼ}xy…Áß…Ë!ñêÖ–úG Kב޾öfþm_m·„b“!X[þó–0쓞Ux)1ÊŽ=wkkØ7Þ6Üy“ Rç8ç¾U7wªØ0Ñ&x(áΣpèRÕ.–·[!DÓô4iøë7´šÔ‘%Ô+$–<}Í–4Ó¡Äàn²xþyÍÏ÷´ZâÐèR¿Ð·ÓÝð)gé×ÉËíË î¯MøòC6߹ݴè“Ã}‰‡<›Ï-V”݇œ°­4è!ÆÝ§lY*h"E±ì6‚É»…pi£zr!„hÛ¤Ö‘ ,3Ä™z‡¤v3BâàÁLäL£LôDÃZ/ ñÌç½K3Z†‹_‡%Vì–o×d¸©šªJEQÒãÍÒ¬µ V|¢ÃlÁò"1¨öbi×í4½ëX9iÜKZæñ'¾PËiâïf›.l–Î-:¥ê­Ø~µ›8uwK°n{AS+oàÝYÿr—ž—M±r]À+æ%ûÎX5¿?Rfôl_±¡q•KTuÔq0M4\Vv¡Š3{­7ªYu¶Ìù›nÉ ñÀ9®_T¾Á?†ñÏFgð´n? Ô4ìÍËFêÇZ™ñµqm•RYXÔÍËÞÝJz‡[Í"xòÌ’ný>-A¨»ñô F}@×½BZå«fk8Y³2Àà:Ûß—¶Ÿ¯ÖM·££6'År['J`¹šræZÞ«(-Ý¡trÊI’f1?¿+ÓT¼Q˜<<÷8_ÊÂdÆP9qñ,¸ø˜pûþ‡òÈÓ×–tsN†aÙg€8AûÁ ÞEl(ŸÆKU`nä.ðK™1×d"épý GÙR0 ½Tƒ%å"¼´Ý5?³ß$þð\—š ²Qò[JSýíÚš u™/_‰»%»oÈ&‹,T33ŽZ{'EˆulÇø~‰âŒL7¸Yñ…É!‡Ìp2_ý!+>HzŸRXÊ<êÀ®2ïC*òM³ÅäÛèÏåÏDØfx½kä.Ë=‹(ÞTBÌE¾k*ú(:)Ÿ7ÝbïqfëýôRç÷A°¤¤”SÁ÷˜¡‹ü5-†QàÒfŸ‰ ñ¼ÙE£:7'#_õ1Fý´îaèJL÷vXZsÔ"ÞǦx:Ò=°+4`=N PDÔ¬ø‡ö@f$”&u€ï„ÇÚ”ŽAÿX× ûGëÑÖ¨bµÄ§±AwgµvîºÕÞxËFHˆÌ4nV…dš<¥¿¶ "C¥TChmoö!_6U¼sãBLnõâƒÙ/Œ Ú­Ø®ËôiÝ4oÕßÚ(Y7mºAÞ3ÜÚ{!Ø'+̺H<î¼É`]Ã>©¿vô9K;—ºÚÒ0ÙÊ ŽnÓ(0hHŠÎå/o€Éš)hÁJß¶n%d4ô´t7ö÷Ð¥ÝÒ‡'?+:âh³ÕSצ]q.#½s—3U{Ó¦{ëjæ’'c§ûÌ™ºBÇNj[¾ÉEW* ³|EClú%Wjt±îãZ¹ÏÀ·ÆE6·\g[|°Á÷h%{™[â´]…˜¾Ç´ÆŠR}«®[Øw3˜ÇB™ŽÚ‡Û ÙvzÇ+z¸Z섪^9«Y6< jÛæÑvaª.Ÿ(qÍ3 ˆïÌ©@{npø;ŒtîÙ© å©Ð¤¡õrlm ‰ól¹e»¼rHD¤žT°]$ÈŒö°·°âaxk¾M4—Ñv‹·ÎGñöYÐnkë–…â\ ;N³ŒÞS™ÀlâÊW1• Ú–/Ù Ö{ܲ9`_iåmפ¿¨`O[gHùÀ`NN-T\s¨d.¾uÃxêî†WÖ$Êç1Ë•Éa:8 r˜ëÈÈaGòœ<2|Ëb!R~dª™²óqHª ‰¡–‡êk]¡!SDzp*Èm•FƒqÞü[.bTˆM'LWí[MQ[PëôЫ± ÃÚîöÍÝÍý¯= Ñ ^¿,<ÎÎþi¿¸Æ"†*Ž”¥+ž•{IÒ°°‘åÝjâ¦ш_«ÌQ-o«}‘ðö)-bjãTD›ì)zÇ"ÎVK£(ó%ƒ—ž1t(l¶&alHØÈ:1ÂlšjPÕ¥'ž Éz%k4@~»±žšá6þ¦å –j¤n×RÞÄ ޶«“ÂÆXï൭]z[¦xp콸Aöcn‡–š“âÒϤ‚šT55…˜q¦ ™³™m–$%ã ãÞÉ´‡´ï®ãGІS^F0 åâD ˜ƒ@Œ§Ç¥à$© à:¨ŠÎA ‘òÿ Œ…—&×>`]2Ééðú¤ òYã“VÅâ®Aºê 6è9G’Æ~ß‹µR1#v´Dújý÷ˆ`ï&êÅ›’ÊüRuŽßË=þ¢&ˆ<^|K/þ¦’н"Üä?Ž”•J`Pƒ>cêpw§3®]{vý 2ó{ŸÄl”¼K@îuÝûâk™:û¸sªÁ¶¦Þ¯PYAQ;[Þ›ú Ã7@K7ŒiÖ Q4#~”3wÔè/#dÈwö†¸nŒ^ÎH”U,Žº‰aÍ,ìr´>‚v!7Urâ!7 ªPä¸ï/ðFsFõ8¬ãg}º:­®wó…ä[ggEoV¾j‘‘×g¬Æ¾s™& T á‚5ŸÌB6,<01Ó± ©;ãËl×*ñÁºÃ˜ wá²é 9(Ëô.Ów"åŠ=òA¡ÅéCr}¤•«…Jb- ZO³:Gð ©lˆ‰¶]mÈ»aAóiÕ×Ìô©U¡Åñè jÝ7F;“Õš;¨J¢§±ŸãVË<ØZs /ëÝÑ¡#*t£Ac¿pŒÒÌÉèîÔè¡„1Cò¹b‡¢÷ÀÈ SgÝV@$‡Œ3ïÝö'sBòÔ¨–N]™«öë ~.z̦J(I p`|íÀ¤Ñë@Ùà;âé vªáƒ°/؇Êf7­ ]ƒ¶p: u NÑ^šÆôJÃMQÆ»üõ ñ#ÙŸÖ±HüèЗ¹Ò` ËIˆÉ»Íà^ª2›1bQbÙ|¢,´:×çš6¥®+:…½:²¼(’aHT—M]8b–#“âr œ©¿ÜIÏ$‘)\ŵ}O¤b@çŽk ôlÉÃð…Ç_Ú’Kuœk#¸cžˆ|ÙÕlºƒ-³«N³Ö#á͵O8A1ñôü Ý0G-\<¹È-å5Õ·Õ3ÖÈ­ûýžùèk,;âIß ¦ñ|@ô¾Î„¿‹“ºH¡áy×r‘-îx°„É„ì[“$Fy̰©©KþÎwYô~Ý´.Œû†eKªóíÑ‘rè«Ðõ‹´“{¤&—0Ÿ5])ú tÂ<7LiXÊËgÅ>:£r 5ëä5JÈjŒÎùÛ%ýoÖo AR–<©»l;÷Èö7£c›Ñ! 6¿%)yV*ó5 Üsã#AÓ£:¨xçøŠe&—Å\'$ÔýCNwb†2Í6ËA0£ô}嵊Ua·„P×ê!òÕrÎ>W\§ö¶1‹¶º*`Ü¢8SÁæ8:Ÿ™à„z0¢>㌅/Y@PöCÒ e’¾ÈSȨÉ4Àß«¨·È‡9ꨔ".S©y y†Þ|1O:Yñí ¿ ñ¡O[aU§s¢i"ž¢•ݼ:¼ÃK|õýéRÎ΃TIØA¤áÓJÃkøË±ö™¾Èºnx ¦ 2¬¡OxY–Yb1ø…’lY– ‘µL )¹¯±N¹]¥½³ÎäÆÛÏÎ[Ð:í£NåF;ù5Yçz‡ËÒ ÑÎuÿÙsÓ¥®Î[ѺOæ:îrá—]O]ß¼êßÈjÙÜù6¬øž+atŒ00äæþ{"} ÐôXA¥9®Ϙ›ðT6ÍÊIj¶È }ipÀŸý2Èñô‚%Èhè5™E5íÔ…_Qû­='ÙòÖÞ[¢vc©VrçN­—-¸ì¿®[ß5˜ ¦`õvç(ÓUVëùn©vçTã¯ÒATÚü'Ø5c CŒIû&üùÁ ¹tUS’óÕÎ"ÇhÒLt“†T€±|Ó© ê¹ÅߺVÖQs‰:K’z:±G{1GÊ àt"™öªÀ cf d| ò#“©Ýj•6@¦nY5‡TÖv½ÜnUlŠ`ðøDŸö„#tÞ7ví¾²ô 'Œ)vîÔ¬7úóS>.iá+"·ÀVñ>Æ/¤Âx,ò{ŽWìèïÜ¢?4„ÌÂë 8ƒI”¶+w΀¯Üƈ‹(»éD¡> ªGgd!mä‚þU^ÙýQ"A&ooI.bOs~ ÛsQvhÀ›øtÔãÕM[Rªí\S‘vp¼‡QOú±õu1ëe¡Í ˆ£¦Éï’I³s˜ õHäÕvü<*èq"k®žÉ|_’IbêÂgËó¶­äãòe¬â-º·8'G•°KøODbœVH¿)ÉSö˜L$ý'+1Nðÿ»$ãdï¿?&0Nü±)J™Qˆ™‘‡Ÿ†ÁÃ-³<Ë$q 1ãl˜d‹öjäFÆýˆ ¥›œÄ\nòIˆðn–æ\jòýeÆ“øª±c¿á稖þ´-16 )¥=_ØÒˆ’öUPuVž?R·s‡{G6mæ“õU@õ¸ÐG÷ oÜç'Ï’,•’äî¶…§xÒì8.þÛ!—ÏÍ4ÇïósþÅâ' r°¾ ¯² ÄY£¸v9àÑéšF×ÉU ŒÝ¯”P÷C«ÔFhÖõÓ‚¬ÎþjYx妵FY(¥¥ç@{N€Ê:"* ~ñ-ü¨´ž—z*Ñ µúl#rõR¥VüÛ GÑQZsºVÁöy)­¨Tù¥³²Ó¹ªæ>.Y©ÍùµÈƒ†üÍ^iš× mµö²_¨°ˆQàßaBÆhsŒŒ¹ÕnžKPKÜ6žÿ´‡Y´ÈLntòx‡¹XdƒýŒ-gëBäƒâÙ]ÄÞ?é ÍÕO’ò$uªýS ö ÞÂï0Â_]Šߦ’ Ê«›Ü½ÄÔæ©_!!”»CëiÉ*ËýÞYÙ»L»oÚ“‹ö·ÎÖ7ÊH>Í5 ÏI›Q‹N{2æä†1e…:Øüy"]õÀ‰‡ùS÷{WŠ¹Ë däÍøþ9äàB³ÍãýŨ­b¶S;E½n‡h/Áô.YŒŠÿ1¬A;¦O{8rÎ"aw,Z¥nÿföp¨Ë QÁíláðK·j=“ Ð`.¯šð¨ñ\£ý>iÀMìÆø¨f™`L'±±ÒÑ¡?Qâ9l^äò«u´¤/‹°Y$¦~ fÝ àfÊ£,ëâB€†ê„qw`Z†¢îµr ~,U*O$i<]ï!À,–c"Äp˜Ì´Òž½€•Èâ•û FÄÂâ6œž“‡ yöïöžœaz%ÿN3PÌ÷róCøƒæ›YM×/Z‡=ŒØ3»3ÖNNö‡%}Žñ¸ÀÏ`^S9ÙöwÂÙ¼ù1i“.ql“/Ž›W!v½ÃKxm¬ú|×KqnŽ,=šd­ÅÚ¶Äé‹0 _Üg"‹áKâþ\k/ûÝp03=î»î •—½ñ6ðr„¾,)>ýØ;ö,a‚uà¾ôì­# Çètÿá‰+þîXºûŒL$ÐgEõ‹R…ùêR¶×/a¶ÍèzŸ¯-ó,ŒÛÏ;º|pÚ/ý½*ôÖ“a‘³#¿Êã7æÏ½S~ÞQ<ƒŽ{Ï¢eÈ£'¿‚NõR™”ðugø•3 ½ K0,eœN§œèض¯8XßÔŸoúlÉ}EgzFC™úbÿ$¿Ç=%/O§Ã㳿ØîC‰>|ö_øÿ‰Z[u%zsh ûæÛÛÚÚÛÑ þ#ncodhãLçàé«4õ_`ÉîOj´ÇAÛ™ÂpÐ*.Kj–è)ËeEëÈP‘FÔ‘ï:JÎúypxJ8sópæÐ?Aئ+‚Õ¬mB­s );&\™ƒZň¦'ûN®\ÁR&ÈS`úµµh RA±P…Š0ÿÜ»0…ßYÐ 0é LPtzHȯ Ť¯)B@ƒ+ï3‹ßHìW-´$» ªN ! :AÌ@ü±tç¼â~*v’Ó§M×]¡‰ˆM¥Lw…ožÚrñmš°C#áÐéHfÕ‚óoT¨þ‹˜AÆ›sRÒh†K\Ã"a.çÙ“‹(7î·Û‡I’&¤a.²Û™¨IîºüÄpm„TD̽/œTº‘Ãq»ô}v³ƒw›¼C·q½” ÓÊ­aϾvÁ·cgñ†˜4ì#ɾæ,Ð5¹ßĦlj²+FÓmŽy(Ñ/øT#F¤¬ßh£©PEÎ’´ [†¸Ê_Fê Ú°\„HW2𳡂³½õÒªùîæ¢1­þÃfÃhûçh¥zÂdÐZŒH„…ʼ%·ò ÜÉÝ‘õ!MTñýÖ—Ï‹V+Ÿ÷ÏNÑSZ›%tï‰:.)]VVr3¼R+ß"ËÒW…ÐvEœÎÔÈ»ódïNH©rÅ£¡ÆÛç+7}ƒ›£.;àêeÖÏù †]5!°i´²_l­ ¯à¦R¤\Ú –°µ*á„Gó03³®˜µ7&è¯'˯5\ð…ƒ p3ä9O$¨·Ê6Òy-v?V(çfZ*o¸Yç© RäÈÕÆ NR ROªî‹"8*ÙþƒµPýžzuï1ë€ØèJõ+Ésu¸I“ìÓM)€¼xž‹ôîÐB~\®/Œ‚š”+Â¤Ú \•I“Ø¿U£gt[¥?Tj„ƒ×}WYåÎ5ù¡HèLå™MGPP:Æÿ½ÌÿÍè;»:8Ø;¹˜šè›™ºü«øo3ê{ö‡b~g08é ŠånñªQ•,¼*ÐÖ;žUTWGc ±»º®PNüï®?È#œ:xRÀc662íÍzqýùŸHv {µ¨ß^ˆuH˜ Qi#XeHns\äö™áÒÚChôŠZÄ0Ji ¤Ãé)H2‡À?a:7û©Òjvé ÓMê ¹#A3EœHCx„ú?”AyÂ6JÍêO¬lQälœZ·)ÄVc®•;¡³ÝúSkø­K..ܺZ2­É\±NŸŸŽ·3;gåAÖm:¢ˆS@~²+w¯JCÈ ØC˜Œ´%@êS´”#G»PuT¨ ‚ß*çß!IÖL‡Kë2£ÛÁ dœ˜FÂy ºUGÕ ªáDe.¢Œ†JíÊH<¤‘`ÕÃ`=•Z+w‰è³CþÊÕÂÂúù•»Ñcx^ãjºq C”e—3P ‚2¥Bæ4ÂNÚ9=ÁƒU±ŸŸÿh¦™ÖÉåØÂÜ“†m,/Dû°e™¢‰"œþJ1Bµz>¦èÕœ"žäÝMÑ(}ñD ÷jœ²ÅÎw¨‡hq‹?D.F( L¢Âo uqÊ32^~‡«×R=%Š=±|¿N“7!6¼-ùçYìÆqt…ï1§PÅ+šÚ|0üÊ–åP ¥Ug]д¯¤ðéOWߟ‚‹‚ì•L¾u!×Ã3¦vÁ»ÿ5Ä×ZV­ÖïÂÒ«—0J ý9—m½»ÒUÁ÷õìTi/Z9\SÆ:«‡K9údE…lP½¡jæ Üì'H Be/ÄÙÔ%à¸-ñìOg=<ŠÃš@Y¾Çv¡‰\B´Ìùøqq¹P0ÔŠOÞqPÏðwD£‹E˜QÝé<Ô³â˧å1—ù͆óðojÀ,ݱÜf¿x¹¶*¦{¯™Ÿx£N§ÿMy¶~Ó:~ÇcMÜÒU½\áuîþ«ý|yîáxÅ“ØÓíÌzý‡Jò½‹–ZKÙÚW„î …ï-Ê(O n†É›a±ž´žF»Ÿb#¹Ž²¶IŒïÚ+¿÷c!ƒÑyó m~¬K{nÉüž]n5ïeâ;Ö&¬ÅQÿT.ž õµɶNì§ÇlZ…§ä¸Ý·‘‰¹I{÷"+H¯w’.ï³(ýeC…×üU}ñt7{ !^½„tWá«­2à¢\™•0X„µxäy‘Q§:‰H¬ã:~ïðjql—‘`ÖhãbŸ±I¯ýGѲX)oMû#‹‹ïæ8Ñ-Ÿ"d®¶y1„YˆàL=RpÍB¹ûA{QA†J~~©oEˆ„Ru¾)ö×:¤¡)2W¬›Ž’™¬]z“!9táCfÊ8÷Ÿù¦5làS%ÍÔìã)ŒÁ40PÞ°Öez–8wõŠÀIµ—zûHnX*uÎú¹ƒ±ßü$Þ‰V´Ì[yRk—±¢š4J8ÜûPÓ‚« ÄÌw¼7îÚg_4¡† —v‘J¥+OA§.Œìc[Y'ý>¨»IŸ´ŠÏÀËZ‹Kì3—ïZퟤEúìª&„¸ÕýLÈOcjæ|†¸Š‡bæoP.8îÿG"=Ò‘wüϦž 0ýÿcSg4œ¶Dø&Z‘¸!-Ķ€¡Ž¦›% 2äȇ\°°P›íÄÜ gðUgã4Ïgm/ð×ö5=wƒÐ Ü哌×`1 èäš{¹sïÜÞ>íÜKó[øëvàº7ÖB‹ 6ÁD,°9œCÄ* t¾&ˆ²H=(©â´ èæ:åéÕ™ÚÍ[P„ôÈîÊ­FdëÝ‹!öê ƒØÿiÁú[G_r˜-là$].Ôfh•ñX?mÏ;ÇLuàž‹»©Väª_W÷iú> ]Ø¥ êhà0& {žVaXåzl¼.ùå8Æ¥„,¤Ëk<ÈœHT½Ln>MiSQœŠ¢˜Õ ˆD£_åß-«dšÞœrõ˜£3]1˜oob¾=žÅô 6+|±ÕÉú´ V6çh+’•y¶ÚÀ%ˆ{f0‘ê_`lëÄEõlLþfó\[ü‡ ®q©p=â0]ò`ú¬°1Œ7¢ ™i³ƒ!S"âG{´Åš )ë)õ¡‹‹¼è邆I }”çsAÈ|°Q($tq®Ü$×l¡|Cr1G*ñ_T.àÊÅœ~-õBoBTlÓ˜ê¨ÅHjLÏù?±Ê~Þð=¦'~`Ù¢—Œ›é£Ù'±3à­pºG·|­¹§¯Cae3âÜÜð¹i=¬•Á«ÛX ™¦LÅ&CØp»n -œÑtE“;Œ€~…Wˆš‘,mÓT&ZS%Íw›µ¹î#T1ÃT6ÕõŽxjœÐ|4®ŽëI1ÐÅÅœhâöÝȇ6¹ÁçjàñÑÁ/x¤kj›è³ Áv½¼Ry@GJqdÅ8 øZ4ºý`.ô¬ñî8Õæ íË*!¿€€p _¿VçH`°vpLôùáVùUç°?SÒxäPR~9+¸(R:ùÛ3´m-;ÍmÛÞ¯fW‰Æì®‰G›ü-×`@ünpy±bE­]¼5……×Ð×, äL Ÿ®¼¢% Éž¡TmÙ³tCI¸Œ§e]î"[üfš£5Œ¶‡¬.É|î\”S{±5í=ðR%¼eI¥Xjéõ~ü²Ü\X(M/&ç±è+Ë–N"q|Z’ïa•K¢:2®Ê{Êiþ´Yù ‘>ñHêñü<~ýUoJԈמxÄAA}hb5Š °ÅòòFd2ÓÞ—"liàü@4H)ç^¿F‰µÜò? ùêÀúd3}™±' 8 ®ñëû#¶wuvêØzÆ’oy¾&•AÞLí:9Z$¾Ö©’¯ñ7p¥GÖ™Oô3Ê:¹…ùýceÎÖë(¨táðùpñðúPg¢]!°Z§Võìè?ýðwp;h€|:‚ÿýžalîdïêðo‹¸Ñ²MÞCú™ý“±/ȸ„Ûe¤ôioÍEß@~¦m_i6Z2Ó-­ô÷n—9IÌc몺«sKгÿùùÙ›Ÿw˜)îø­[6Œ?Izï(1‚ ÍÇ‘ê›Â†×‚—àO|hP­$R&¹Óظ¨ ï{¾æE²>º€'ð¢hP«¦Õ›\8¡·!‰$Fý=èÆ; ñõ¬q‰ŸD·>8,¯˜o‡ò\½‡µ†{+èF¯­ —P)Ư›ÒhqSÓÖ–v8•an„ý,ŬžìXò†Ó#/DMk7$0ÈÁõÛðYÁåLà<ÆñÃ9¬æ¼6’£P[Î@VŒÂ®ö­4ÿö+äæuš˜ÎjoÂvŸ¤É€F¥KA“,Ài¡¥>̈́˙—†\ºïþÕˋә¨_ Yõ…‰ä)4SÞœ_^]Ù¦·™ýÄ^ySþgH`å>†ºs ~ÈóBŒM,›6“‚²e È4ÿ‘wz»ìyZ¢ísàÂ2!ã‘·~3hD]`ß[i˜¢ê¤âª¶Eäã;LßðŽ p"Äø<&­G3x(à ;ëaRˆËÜ*S$ðªUf>Æ™aŠ_¤ÈdLMÇ!ü8LM´dÛê­9àó ü Cž= Bn}n=<Ìu‘2tnrY¦¡t7î0,rí*©Ãc¾ECá›q[±:—¯ÓÊC…ÉVþ¾ôXt’Sþ·‚Ä´Û× §Kw­Ú4ÅpÃ.ݨ™5{ç_Ø 2v+C+}9º³è¶Ûw#VOƵMýFÞ„–•Ç÷—ìA,{u kÎ(}úÓ£ 'Nýö?äÑŒYž¬=9ÑöÖxž ©}æ+>®“ –ÛìÒÐHuD’ÀE¥„ö ÂÉzz_Å’wVãjôèO+—+Çg×Ç+qâJ`û¼¾vÕ#™Ð»ÇuM¯u!ù  NaÈÂ9.žào;†wº‹ö¹Û£\‡ÑdM°ŽºüÏ¥Wn Ðl¾'+|­×£*·0”?-[7×O(lBüë*KÔÊÌCÇ,` É8ëmÞ(ÊrnÅ/›ã4‘]´¦¡ Iç#ºÃü•?¸Ns{Ã^œŠÓ⢜pF@á¡W-i§ÞGåÒŸ K¾Ïöé^Îîÿ\µÜU^›‰¿8ŸòFã«9#‘u;ø¹8ª¿ÄHPàÍÚ'â»1§½¦!¹· ± ‹¿oiL HŽ×D{7Xì‹•«>ÂfÓ8«WÙÅÇâj•o+J¾R!ÇwDËAê’ò/y`hj?" qj_•¨ 1Ž»–r·+eV]6ŽÐ—·¬ÄÔ±º&ZnŒoXYäèpû9êV¬MÇî;ü䀒\UsU^ 8—‰¸#N%Åõ`«ÈÁéêHl[hPÐ׃¡§c›€éO³Hì/wôUÜ¥ôW9äàùŽPIMG‰ì#)a>HÙè*¬µ¬‚N' R釟ftýÆL\ÏSž**îåJ¢H¹xƒÊm8f¬ìcmÙLl‡²`DÙ´ L(÷ë»#Ïë¡4`$”e­ˆ{I®aÃæà„»lÍ-èjŒ´7úÒÀƒP#<Ô|ÁNIi«èIº^B« j¾yD4(Œf)h4#­o¢SÞž ¸–;›NóeÒ¯„ëɨRHÍα"¹’5qî"V¾ä%‡Ón–¢[NÌðË]œÓó(“²Ggk‚¶Í–²Àæß}ñH‰è&tòzC´ÐšÈùÓØQx|ÓŒEß}·zrQV$Às(Çw3NÍvúßH|C%r©‚:,Ææ^WîÄë—‘ÔB4·\;9dÄÀ°€éð¼öÃ(¨s)^y®Mñ& üu\4pPödØÆRT“ûä4°àDvI€žôÐPÞáà˜OÔ¸ó“i**pI×!ƒtp^­ÃÝܪ›•ñkªÌì˱ÕÚ§JÈ™{Çq1YíKvýˆ +¢`ß‹*‚˜ÚÙ ‹ª–e¥Ã’ùŠ\V†:“w×é¢Y¬ w’OÌ ›'׃ÝÃ.ÖÞx’¡jO…ˆÅÏKÀ¦ÉĆXÎE3yne´¨ø~î›æÎË~¦íÖ)ä€êkô©ÖÅÃýÅ~Ð>º}¹]yUºZœÖõ¦[¥k.‘µ“,«žZ×a,à, ½µ°…ïVËáa¬ ¨oºS™þ½.e¯M¸pŠ{jsœØ¾Œïc½¡‡nå¸Gª”cƒŽ?ŠGõ¹"ŒFÇ…*Ë$eD xuHÑ>–Ì6F²,ö-¡\5cV#\Gua!ÁñĽ%%mÕt”Éûuxù PˆIé(‡ÏãùF²y0~>ï×ûd‘ŸR:¢ŸÇpÙ{:y˜$ð.eÞ·Šzknïázõ f&ë*ò眈¦o Ûqzª†1VUÇXx˜fJ‹®%õ_$æFô,oiÎ2œE䛋QÞ.)$€B»ê^þl»lÀ’_ج,„Q ÙçSH’F%cަ…ÎT€òWn“R¼‡2 øNbˆ "œž9E#"œ*åîʹÛbôbÚ¯rÏ*µ-=¯“È ²_ù.”e!êd“þ¿,ã1Í@mchâ/ >›)ùg@ OÞÈ¢Lã,D¿:·Jé¨åsÉBÆ87G"Y‘ÊàÂPÌíÕ!*üý,ª»Üz4ñ7:¹Ñ]æ*$ïxg >*7rí` áj¿ s•Ws§î¶âá<â^åê~ê ѸM€#ŒK i€CoV3y[,*CáMZ9÷µ¤¯6½Y1—ÊÕo‘ó)0úô„X&Óö¿Êc©%¶ê§×˜”©rYÕÙIÇÎÈ/Ô%ú|ˆçCk4 ϻǟfÎ'‹–žJzÞ^k¸¯Ù¦uõÓkUûÉG¾fèƒë˜ wU“£ÀèÊ€2`"ÙA­fÔëÄæ§êÚ*óñÒ'Ç%¶5”œaKo |n¢ŸÖ Ü õÛâAh?è%æø a? {|·ÕcŠAg}R*èkŒ áÉæå;¹½}¥>ÔPC)½¼äê oö.Òôå¯Ðäß‹ØdrßÒ¤ß_-ÀO *bÞžÞ­ÅUÅ 4•Ûl媊ïòþÝUå=t¼¥ PÜ,aŽ;ß6¼Ñê–¯Ô_ý^6·$Œ{ê‚écXtщçzŠ8@³*Ú#ÊÜ}\ìá4´—Güá”®Å/‚-žn£ˆ±i™¾+0ê7^ZKmLä+­ð3]­DïTôÉ)€&ûÏŠÈQ{å}r6êZä´ù—hÂ1ãú£ê—S àÁP¬¾ >¬]'¹‡Ÿó”½Àµâ¢ïÈV׎„*¶hϤåé»o×Ü8W¢hïôÿ3øÔñ§qPúßA‚ðÿà a<£%팣ŽÚ»ó” –FÃoJMÏEiœcKUB*Âä@š¹:OZh)«ëŲÄ{qÛÀ*VÄ!]µo®J ¥VPDÕ…ò°j³® ­àÐëüùú<ž”Œb %i·×ë»÷ƽ÷÷ö‚·‰ùe }ïÃP¹„ßôã í;ZŸ-äþ|—°3l!$Ð[ÙE¦Á0ŠE4—.*–¨ºHi§q¢šKt!aG²±òVºHq‡²±ôvªÈYÄ¥|d´¹3!s—Š-n‘W5uAOÔCy›¨Ûò`&BM‘§$yW‘0>š–Ššå°ÁMâ.c–7ï$¥‘b³<Ä]ˆ§2mWR÷\EÍBΣ<4çh»ƒæ-æ~ö|»IóVSÆ»–Kþêä7Û¯ªÙŒ(³Hzß­4ýÙ·£ˆ©¡.·~%)±“_,½”Zqýnäe§ìšÔ£‚\´óùˆ‹m5)yô{ø®¢[²Óæ/ndÎ;Êζ3·ßüž«6“„UCÌí‡ì/ÝÉc¶NáY#ªX° »!¿„É0üÜvUÊùiÝÖÅ” [5}é S€È À|J?¨-&€!Ô«ÖÊò“nš®‹F¿Š}z´×u\ð‰#N^Α‹ðŸ½Zwm}½–;éèØT)’¥*§4ÅŒ°ôwm—ëOÜ»Á²³1ÉXôÕûîÛîŸMÐ +]‡»7bN<è™ú-Ø`è´ÕÞUìuñŠÐ³”Qu¶I1'vß…øÙ].‡Wð LQHê@4ãëgéÕX ›I8âêwSŸ &lÃC7š‰U/™¡£Æ©Û[ ²°¥õ¸A’ ‰øjõ†N¶GÛM&ñtì×K%_[Û7»å›Ò©!Kíº§,65ìè5\-Ms_Û)ÞLAúè6gÎíUï–…v=ÿlê8ÿ\Æ“ô…e“ÎÙ÷Ø|£æ©ñªÞ«\þ7;øC·Ü!‰LÉVвݠ»Ó¶}áh‰{1‚ï@Éq&sáÍ2H&ò •zMô~+ÂÈ\Ë7˜qÒ HàoŠœ±¾Ò©”Ôè¯arínzÚ1gä5«°Ü8›ÁÖZÚÍíËZÛ0A¤Ë”½óŽÄ-ÝzèU(ÝZ‡è« T’-íò3²-èUØÎËGpV¼‰|PhÍѨÆ%#)µé• Ù8Só)¾hî ‰º&ÇA"R|µYG:KfdÔbË’Wµñ;ÐZmôÍnJ[d=›)«x:¾úÝ¿ ¥àw•‘ÍϺì•ëûVfŠŸ•ŒÀ9‘+ޏg°ÍL†,3(rÄK@¶Êsà KýýjËýФ1žÝÝØ7óðY<÷ Ê¡ðT¸W,ý‚p:rGç'3¾O˜Œ‰æ!›l(N5ð2‰™íªMQþéì(Ø×Tw“0µ,IëRBй+e¯„;E¬-p²Àš«¼ü¯ÀVã²5tɯGûö+ÁÃñN¾ehÁeN¡É‚ñ…vM<»(Ÿ :R^ßÄ^Ý<—„¯#aä†\'‰Ò¸UÃîç!»„zȼ©#Ódæ²dL%îø.–`èb­ÑûâF§‚ˆ[¨+Ãzˆllø( E‹mA{D…ŽæR"øC mm ­Òû[nš×© ’þ`˜C½5ð0ˆÍ•¥’vwƒ‡ iŽ`…QšFÊp¾9o,óúx‰Ý\ન3N>óúÌ{­Ia½ÿ(Å9b%Iˆ:l_½CŠ¡=Ý´ô'w½P@›¦ÐĺMxÎÍ-‡e¹Ž«~s$w×'XWoÔ«¯©pÙFŒÂü*êk’òŽ &ÞUø6}æ˜òám˜×aO´ïIrT ®rKÁm1èee¬°$¾ Ñ[÷¢)›ä°å1™ÅhÏrH¯ýÁ@íä+Æ¡/®é8‚wÁÌ‘ÖR^“£® ºLåMê¹K˜Ùò›õlô—n•_•5DèiަÔí¯±zó‡ÿÝÎÛ¿"Év å6äSØ="l B쌕svîSžñ½Í-ñ@E¯¹Ÿ%³ëF¦^¯3ŽÂä³l`:Š‚ÖÛ†žÜU’VŸê(X•x[8#9ÅôIñ¤½[[ÀNKbÛà‹ÏQwðDM×Ó>È‹ùšÊžåe‡ò¸ºd¿rAèìôøntvuÝQK釯lgÌž²#Ÿõ.Xsô´Ý>, ;j'1»›¤mþ‹3$Xf“CD‘Ë}ÊRzF þË_âxø{W8“ÄwlÏ7}þ¿Ö u懲€2gÕ©Ä#JÔÖ!lNTZéºìAË~¯w"1Ù<K䞀ÜßýNÍбZÚä•î•„§, gU˜Á¼:"’Ï0íò%Ç•/X֥аrb>dvsÆ ¤ÖŒÎ2h3.û9“H‹¿VyÓâ€(kHcZ̤J9÷üìsBblT¹¦o…ëçŸ3go–?=ýl’†·°üWLJƒ14KœùYVy%.Hmp ˆÉç\>¸ÀDàÔÁíÉ47c>R2·â|8EGPhêÉÒÖÓ1¿fõÇ­•g_9 jµÞN8“@íð™Ès þ³muFÏØõý< "9ˆ_ß„_ýçZB8#R~Ÿ+ùËø? ¼¼¶'¿DJ §‘ÅUõùdúgL˜Pu98/­d ú¾~Þû´¥>µ:©¨/8docÇ{æo(HìQn"T"å„ ç•IÅ”y'‘8zã}\¥=Šw÷™ R¤”WåDEòhT{C›É%gßç¶½kû™/PÏù$è^i$ff—‚kÌs o Š #< ómVlÌ@¿oÉ&T_Z:otvœ.F;C¿›«pd©\r‚qb>u‹&fóH¹^° ÷åÈ»Ï@ ‘ß²·. ÅâÄÍKÉ$û‡ò`éÈð%©jq‹Ñò·lÈX•T"OBE¶îKœÐíRJMù÷$V3ÉN£í+æña=«÷Y)‚¹Ô’5¼“sbáŽÒEž™Îüjm‰^e%[²J°";Ã9ÃROÇF»¦êâÚ'§ùÍÕëD¢Û„‰Lj±¾“Éy_zƒ1‰ó‡NªÛqޝ6Šy²9H¶üIp¯3[Í®¦¿]“OÙù¬!ø¸’Êñª1¸)ž¿;^é'h£‚%›ä ðÊu.;:·öp™ëµÔAwÆäCWá‘AH.B«p(âMÓf. )¢ÐcÕeWá5"$gà~K¦öý&Ÿ†ï9`œÆ™•’?7d%–vÚ 6ìûÞ`áÄΑ«¥‰âyýL›øà¡Á,eNkœo¾ŸÑj¡4[™µU>>ÛìAß"Bž5QUžSlŸû? YšÅž^×G“ë/+…Qí‘W† ‚c÷}ë¨qxKCU2N!òû°óÏÖ+…˜öŽÁêÇióc\›iòAðX# 9Uâ£Y>P^%úý$-@¡´!TI0éa̰‘æzó0Í·ZR 9rÑŠ“¼‡¥ZxÝh½•€\8ýJÕaª^ÿNÆ8¹\krá$¶²F…ÌoÊ<ì§ ©¼7‹‘ì {¹ñjWNFŠž­¯-I½><WS‘Ü|KÂ{Ä«­5y&àŠÈõvwç«8ãr”–$L佪)g.·¡²£_Dç0k'G>š\ì;9b´3âyÌXõsšälÅ#òü†q ëÌ´–¡×ió¦?ëŸ.G'З#w¨q!ƒÉZÒ?µ6@õe¾…±{ð·äFt’0"ïžÃXÎFúZMnÀ …¸om©çoïæs™ÔázN‹¢”CŽô;»Ó[|1ż#€裞=¾r“Ê™C(<\Gâ@D E‰¬šGf ÖœÖC ÍkÖ7F2Ä«ÏÃâ—G(«:*ËÜ—÷E¥6ÙŸˆ.*­“’æàá &5_N ÛCš»è¡ÜßJ?LQè §Ô¹oÆÇ'£Yþ’Ýå!:¾}Ò§ÚÑ'zD4>^³^1IRt\Vü•Zå X‚Âó7e¯íXµþóØô¼²Î »)ñ:^Z} ÓéñPº]¯®†ýz…°îϨŒ¥µxyæ"‘<´^¦wæÛdùÓñ”6Çgh¶|º>™Æb¹ioÍm2Õd­‚㺣»*Ó<˜Ž[ïýQ[ŒªReUÜ9q¿^Ì’ÈeàÙ(| ŽY‰vÓu.ÃÕhƒø䆈Œ‚Ú(Ë…; $Ÿ1v^òL!ýÇG{暉2«j=Wï(4Ÿ޹xZ´¬ZÈ…FÃû¡cÎRú¨ÿX"YX„m4dÜWüÐ9FâïÃX´” üÄm땟ÝäâGäÍÒ˜{ÌÄt ³™D Zð !Â0¨Ì⣘*|4Ñè¹L:þ@©ùR¦ù>£m#Ç;Ì!Fùò.«À_ºV¸ž?e-ï«ÿáÏçí>VˇQtJ¹pYŽÆz9=ÍŠ~Ho½¿~;DõÑïhl×qÇz¹z9ñêÚ7ÐD²kµü€"ø±m?µÒ¦Û«x«%oàÅ‘ù,û¨y•%H5Þ{·a”%~^£É–G?•rFn íÀvöþŸà—ÿºÈÔV^¶@ÀáøAþßñCK:Ïm[ç­vÄŸ:ý@ªÂL…ç)]û:)×Ú+uy\ncît³$p2ÂLô(Èù(ºÿ»[ ’ô‰×fg}9æHZ$ š×“ ]ƒïÙ<9”1ÉÖŒF¥rþýäd·œ?l¢íª×¿ûà¯T‘Q:§Î ôQªz©Çç0<ÒD‰’ˆMLÑ¢˵âÔxðO£æüÑBƒn×Ìèà13Õ o86I´ÃL;ŒJQˆmòJ‚"ç5(ÓLúRÉx¢A´É/ÓÒE€fÆíQÂpFÿìomãµ…=8W/[u¶µéý–›ólX—~èpwóòqòse ³œÌ¦AUjúê’Z¿œ {@½G~ÀÞg:±NC¡‡¼S‰ûª‹ ”ñ cÒ~HåH¬’°“ZARÁ½ÞÀ8<ÄÄÆ×mXªfüü\X¹H…gÙÍÓû=:œaõövñôtñóõr¡£Ûxù½tµíüþš„›ÛgüËÃëÀ Xd:|¦øœ ¥8LÑDM¨3í<“ I‘IŽxM­âC¸NÔ‡, ¶eH­Hƒ9™Ò¯£²h’IѶ±§"°Øµl”|ý"¶“„éó cÞ “DÉ ñš+Ö¯¥G‘6uÉŽõWv7JòÊþd¦jœx]ñEd-¨V~’)R¼Ø3‘ó·…F$ËäÄ|]ª ¨¾„”Á¬ì¯a”.q°…âñIU–C7UüÅð©©RJÕúr“ÑÙÚÊÓŽÈ£ž¬êâ”Cê0!—xûá¥Û™Ï ¯d“fÓ‡ä˶9y>j€/eT7Ǭ1a|í]ýÍû³ÑÙæ“ÒÀ“ÕÒµ×™ƒÊŸ±£Û{@޹Z e¥ïždö0icì÷æ"èÕ0¤%{! äBÒ€0äÓR­ÔIs|1¸5rsò?ruô{¡ÿÒº8?.H}ÜojnNô'9”Aî`ϰàôÓŽÀÁÜ.eÅ—˜¿._K'#ÙkêSKMÉ÷—;+/Õ÷‡‡Y3MäìNötoÍ7œf6뾞ŽnnNž Å»ñŪœ¯”iž ˜þ8±bŠ“€8uluSïË›®]£‡²’G~z ‚þ¾ÔLd/Ë’Šf6³‚0µ‰ M 2"¡…Ô¢\oJtfgžd0²_ãH1S7 €hG=ypÈ=YÆC TŽOÙú"%0}’ %8×lñÖh1ÒaõQªÃu-iô5Ls‚±Ãº¸Iޤ…k ïÞøWƒûK±‹Vk¤J›êÛÙ ƒãgœÈž¿§—”h6] RÒyR–=-±Jþb _6`bHx*›U¹@xmù€Jÿ@üÆ3»Ý[„èË&–h{ë¼ #YÈ#`èR‘ÌhØ[y3øu*`®Gw}ˆËë´´0ÑiÆúÛb=CLfrób–áòÄQßæÐêãÆ6ãõ ˜¼à„P?5|§` ¯If):?K@TÙlo‰¾`Zã6ÒƒëŽM¤npÜ"‹t²ŽåÝ, G m¦Rn+Aùµ°òq¾Àtž®ŸR0”èì‰sq³G}²TÐ’pHj£ÕÒ"Ys’ ÚÑIÖ Ð{&m¾SUÿSò¾ ã« …ß¹õ]²L‚/o£é{裒µZ@Ãð $ešSO<7+ Iž!ϦÊçd5„õMÕ âW7X ¹'e¦£ŽæI;#zÆJ{ø€áý͈¿OÛ VõäcRPŸo —uxd=¼.Lïm/×…KÈBÅˉgðRzÆS”»«ünAi=‚4ÊRgT-¸“‘ÝHç)'N¼#¾¶:üº"•sß3.[JÒeÉÒz­<õé1œgæhj´Ì…WÑ"¬¶›´%«\,33ïx]xeâðã‰^ÚYk#áõ¦-î¯Î9wHC¢²È[«Ú€À, gŠ’74]€øÃæÙ-âN”{õówAÓôÛïpµP—*!¿íg«ù²!Í¥Ý D„'`*`xà£ÀõDÚìŽ7ÀÿN_]õý¶¿\fV{~Ê“z݇ˆ&Xð?p°(~«ã“ÀK/ ¤[Áñ9‡ó#¸ÁÃØ`tòJX.9ÞÎ['NÒê$ªP©>ŸÞAÀêSæNʪq³6÷²]H@¤¸è}-ìõ®~r™ú¤ê/ÌŸú– ×ÎîÜÓšÐÄ‹H/æ@bè&° ¶*à’O+í{„…V©4;´‘Y›ÍHçê9c‘Æ/Ù Ud4ÁDeEVT×ü@À+Öƒ½šWs\/³ㆱ„¾ ùãfV£»—œù‘ž ´:è«] mØØÒçÆÑÎÖDÎ6Xä’‡nd­à$CÝns‹‹ƒ÷UÏŽ‚ ÷Z®å«Œ ¹ææ-}“¹'G+á÷lƒ­(fPÕ0 ÎFwœaÜc-‘2Sv%,4õèX÷Lmpþ¾Rذ°Ê&‹TžÇ^ûÀìq kt2ä°Ø˜§ÄfvïÄÝ V9û'—w¬ÚÓÞп?>œ¡?æŽÍ¸ 쌰¥vPPf7n,Mw~š.NíÎ×;scù¼œ^ÖÏÆO½"t3Н¥5L/¨Šò•—*$VËZ T°'åeZϰ"ä\¡ ¦ÜÝmQ×f+Ž×ÑêSÁ».ÄDnK¢ŒMo3tWç´Q™¸~¼yê‡v¯ xªMö;w'è¢0¼Pµ»ãWØqÝ£àá¡K¸‹îùji±Ì„ n‘ ?›šmGFˆnËUŲ[<¨À>± à;%!<Øtõ@ WÕl‚±–èñþGŠ:¹¡ÿ‰Ì4tŒ<ñG1uîàvñÌÐt®•ḛ́° –¾ÒÉæ.%0—0X¼ƒ6ž­¢F¸.'ÀméyËÌðjá"‘ÑolÞL ^¤U»YÏðLDB]ì¯ÔüßxBV4QÃü·àz‡‰7®xCk#°×ãU3´9s¢¥\æp®ßþ€iÇjç¡by¦“ÇÕíU­Îùòð¨3—Zã#vu˜JjL ^ØËç– ™»÷Vaˆ±; œÊL5Pßï:É´s¼Å^`v¹2çÒ×Ü&}\ø\Î15|'E†8E8w«ï 4¦"€Æ† XIÐ?KN‰&öòš½³zV+žÊ« î‹€!¯µ}t©ø>wÊôÚ¿¾g›k´ñü5L•™+‚èÝeŸN!Ó+`û² ¬ [Xæöð?Í¿ÿÎÏ17Êk ©Â;òd^c÷ÈÔU˜/l²·i/n%t-Z¸,¥«qgojÄ´3ÍuMqæ}?4J'Ó.ç¬îf(;swM]˜ZÃýµ)ý³M˜ (R›è"žÕ\UÜÍR×ÕjX™ŸÓ³Ì§ ÏÙK0Möê£êçCV€Åé [`®õ1¾fŸz²ÅîÒo%¾qp†tq°Ä²ÊåxcÍXölçaÝ2ªŸýâv€}­†Tw¶n‡6ùh–Nµ§5æÄJ¦_Í­â–ïÍå5úqú}zæ<öY!.ê"]¡Î]Ü:j|¥ø ÃFÛÅ=]CUv.®Þ›§W®†¹z Jð«ÍÔNˆk¦œi‡áÊ,üg<àA(¡I hä뺬ßP5{XG”£=°Ì²yñªÂ֠î;ê‚e–W,Á¬±8aÙ”£CŽ+++é ]ò¶õü7¿3©8}'‘upUó,ÃJ´ŒÝÆ}¬ÕmC˜ŸšÄÞ5Ùáæ²<¶¯áĶÚÿº T¹åà!ÔòeØØÉN©{ƒµ‡»EÀÌM±ì€\‡ÎÛƒ¸á(>Û °Ñ]¨´wF/j"GÔ5i4%à!ºøÚ*ÅØzî @­o¨¨#ÌuFóŠëÉN„ Å5´ ݬšÓâ¤Í‘õE}Ú#ñˆDФž”ô¯êÙÊ.ŸíèfìÆÙzH–ÐÈ*Z¦ô„ºÌ~Å‚hp:Eài+„ûLyYÒ1Ù”µ‘ªkæT¢™»!·mµü:- h·•«vÀa4p’ö.ñƒŽÐ0CÄ˵Î^Ë0ÜúÓÕ)KÒÚz _Ónd ÿ] ÕäÓˆ2…­(³QIˆÚ°ÄfÞÙ »ŒÇ2ôÌsºJÂÃfHï"„.ù ¯Y‹WQa[û­[fÐt DW?³Ý̱¼XmLÌA…i°§0¤¥:éOŒû ›7¹¢óûí)<Öá8‚£ùr-@Ï*6Ú/á}ÁV•¸á¾µûàeŠ—“Œj+î¿#Ð)Q{]¹)²çö4Ug´g½w~Ñ]™~%×Íáõ]¶d­é~$b‰]ÛLL[~6Ó2hêïÆY`g¹¼Àa JEa`W®»â"¡8F5Æß¼ÓºLu3ŠÌÎÞ>œ‘/H‹DžX´BˆÀâ#m¹¢ö¶‘¶z”Boo4›¦,å׳àÿ¿¼ÒaêYáEûÇj2üŸ*Œo´fœqÆ\ý™¡¯@ƒˆ %q”Ú"C%&…%c,R2‰­ ý– §ÜÌ8y£á-×»®ƒ©ÈÓ©;NŠ'Ë¿@+¥à8ËæHÑ÷œ ÇŽ{Övš€b’¨“ ØÊ½½:õûüðûÞ:¥÷db¾¯²ñ¿‡Á¿•ýñø154YÍ¡±Ò<[ ³ÏX¤N=!°„O=1°†¾l„UÂä@±Pë+ô‚,@¸Šç1T_#F œ°ÈU|1ŠÏeŒ…V¦d…Ê¡b©\Ê As“ç9 s--dªñP#Ì œ¡°ÊU|AŠ4ÏsÂF-Ï»bÕU®(DÌ œÑ°Ì á°Î ˜Ác9_ì`ŠÔ Ÿ°Xô”_zàrŸæ9â_-8d ±ÛÙ *‚*„ 7AåáðXé†ÝÛ]¹ì}‘ƒ0ì®*»_hæ¥lÑ:pÈhÖNR"’ba«>(ÐØ»z²±-¯ªÊ(­>2.TEŠˆ]ä{¡Ãý´ëQˆî_ÛD­'Z‡6£sÓÒǼÝÛòrøz,É$äƒú¢àbo²=ÓrÔkH¨{xNEå-÷„wÊaß²gâ˜Í”mÂoâR¡Ñ·³[5#Ñ‘X~{ÑǸ*òBíÞÞÂïZ÷¾Ý­¯k<÷ž—ikv¢9sIÖtœ¹y¹Ïñ†ÓÌ[°`kLµâ6¬!ÎÙ‹4mH鉬@©™ÙqF‚Â3Ü.ŠÛd–/XT9UÚ0rjÚ¹ ™»»óîéŒÆ5¯“/³U_"¨Û*ãžZM¨Léà2ýë!\Ö5¤$$d–Ìcýú ¿2Wܹ¾ù/ÇÁ†/ô«œ¥a?æÜ!Â'[ÍÈCÊF=+ØÅ§&Úíý¤0Jò+‚o\³ÍFKAü!³L³ lñù¼È¡,öikyÛnt“žDXP¥“$ä¶d±p¾§¢<…¶€þÞ+å;¥”[ˆÕ•‡m3MBÉ€Ô‘Œ¶dƒÜa?šØZ[Kkï6q‡ ›"k§DDÝâ}r÷\ñj†j£œ&òjý‰¶8ûþ˜ÆµnÀçákíœ9@G<öbk fpKË»7ç~`h—à¶øè‘a“%òd@P\÷˜<\{³4^ãGR íT8K1ú˜ä©‡à>ETn´~oÿ¹4¯?u {æCo2Tæ³’>o$µ»Û …Óù’k¡WTÁí=ž®sow£>ñ·³æïXež¬í³ØFžµø~P"·VKsRó‘鶘Üpiáò*’qèˆÇÄJšÙyzO-²8Aᘧ´t{f“Ú –:¶[*{Ê0V[ãLgÈepW¡Ì³“Ø7Z» Uº`%êÎøS¨UO³ Ò~z Ó‹¬“²8çTíIZKJôe„æ}àO¡îâéaÿjõׇmÒlÀ6Y^‚‚|òw¬òá_cì¼nhjkŠì| @Ó[kTJ×Çå$ÌÔU*1£$ººH én{¬-IŒ'`OÇ“ÜNÅŒ†·v`ìBtI¢´XôKä!ЧFOÛú$0<¡¢ÈÅíá#¢~?aȉÄ|Ch'¤¿]…'„µsøŒä<ûx;¾¯@ïî Ÿ¤OV¤F®\ÂxضÂZóvó§õ…ý^Ò˜QäÌþÖú_@nþbÙîWO¼6K2Áˆü^(8Fþ 3òO žû¯u­cëù0ÈG[G_1«–@¦^[øaµL%{ dWì÷ÇÐÚÚ½Ÿ¾¢cDKІ\Ë\Tf¸T7TȬBO¸‹È[ZÞj˜³©XÅ¥I£¢ÏO¨[†‹Éqn ‹’Zr­”9ýÂhÜ-Õ/Õ/îu4óþw½¼w×%’dÂ\ð·ÛDÜž‹´~‘·ƒNŸÑÖì·IÛj»s” ™éf&fÚ–‹Œ&<{¨æâØêd‘#“‰D;p 0á MG°Ãµ|â!g Íqè³zîv"L9Kýó„†2.χ%`z§™Ž¥‹‰Ù’ôP13è]ª~¢Þräb’xÀ]VôB:°Šîj œ‘ #*×ó¯ ¯É-©|°¨ »eŠÝß ýÀ 3]4ñ§øµw`Jƒ|˜÷Z(¿h/ Î ˆ¢–„ïÌHØxüå‘ÉqE~ÓsëÛÐIíÏÊÈÇa\"þfq¥; ¼1Vf!×#ÚÏ ƒÀýÀr$ŽÕÊ ðJ-•{TRŠ“síÈ,âT ñ…ÏZýqíY¾‚1ëJëSìF«Fÿ”«¸ M 9³ÇÃk½ÝžÈ“‘;µ…<«:*Åî‹ í.C 0TÈADŠñ “ü;LÇÉöVòð ˜lrnGÞ^Ãc¼šZè I⿪kÛŽH^i5ˆÒ(F±ù‡jr±óf±Êl‚ÃïvçÿaPêù9»Sz:YœºRê-ûöó­*JîÀˆ¿ý'ºŽØaÁl?"ÞD3†¶ë6DZg˜¢Ó”c0x\Žñ‚ñƤgÌjùš‘êòÑ÷UÞ @ýßå Éb{j[Ñd§Š­’½!³ ÕB;$¯Ï¤¿À,‘€^Ñ/ÿÔ#Ù,‹F›y5‡ÕVVó zF£é•å°Ð‘æìÓ†×Wê­^Ú»%/Á'›¢ ÿTK4³™øöNY0ÆÅ^Qи]ÚAqËV4c”—­ 6d“×8·Ö íC*]×(Pš°Bš`ï;!J¾¢ÝW%ÉF0–oJ͵†K wt¯ ƒèê9Pj‡x;Žš2”t‰#¥¤¾Ñ‡"NED,aYí¦X§6ãâõù<Š’’Q“áÃ*éP.iCÐMH9R ÖÚñO®×èwÅuˆàrkb—ƒƒÐ<°¬Ebuä  ®î¬m‚fΖ pª+$»”œÚ…ŠZ|“RoM¥¤UQ~Iˆ"¸m±ÉÎ,Û„OÁ…ÑÑÁ®ì-hUþ›(óàšL,·7L fŒ/V, ¼FñW63C<£ä‹ŠÄã ‘˜ýb€6ˆm.¡c ƒÑUéwox¬1­iòLR.?øO™Ûmƒ¨¬ÇTñ&=ö¢¾-„ÐçE¨Dcªp”fÚ¿|}{e$°·=èÈ<%)›û-,u ºƒNH )‘ ¤òÔ`í ¤n‰°B£yн¡¢HüiéVþ÷ûÀQ»uô|z*Ê./‰2soMÿŽq‘‘¤®Å÷7Ö<"Wâ´ã¦~⎃r«Ãá)¶†K0£N¾@Ó¿°g ¤ÓcwÐ!|(‘W0ù'SrÕe4ZLgY=ùI&'y¬PÝiŸqÊ2ZÉT›þA ý¾ ,5Ï/.ùüÓ\6Œ$þ®¦Ø™V²ÅG³ƒšv+-Ö)­Õéš ,Œò·©¤kyo¡ !=bl9’{±¯ô>áyÿîõé»¶­€¬¾`ivR_ëYü‚1íºÂǺÍdX“·›ud3¹˜*^å-'-p}n º0Æ.k(‚¡Ý¢yÜÙäÔòÜÙ¡ˆ’Ð%êÃ9Ðéì²Ò¦®ðþ~]‹]ùx”””uÒR­Ì1˜Ž>þÄ fŠJù%öcÁâ }uŒ²^4Ëq¡M_ÒâÇÑáëâÁ åJ,ùjoÅJû÷öë«Egã*ýtÇ»à`‚Ö*o¦ÅErNçÑØ. H¨üt<:°w’³»Ö- ÞÁ+—¨« â°³+>ù±1õ–#…‚iÒv8î"Á_x ‘'<²È©¯xyëTWK"®Ý?×›)q™ °@ˆßìFU+ Kª{’’ô©¦qÚövþŒF:OÞºòòÕYn(ãl€|hSà\žªdè;JI[ §zsõwŽz³ÑIÄ–*H.|fò¥áÏ&®hr¿üpcGà9ÿ›¯ª¿Î=ׯó~¶‘óË=Ï×ßgæ¡§†FÃ>âÉa®gxhJ~ãi-¯ò縙…E‡ÊT´.ÿÊäqüZÃ|‡6¾`ÌÅò²Ï´0ˈ³—ʘ0O¶ÏV$À”V©ÔÍãc.UÖ¸L—Ëü«éʪCuF¦VjùÙ!¬CžÜ½¯³_ÚQìˆVµ¼›jQwÎ9 X‡ž´na †|å}Š’hqÖP9,¢^˜J:$— ¥`ÐsŒj§×y~Î\÷Zô2EÁ±G'ÛAþ·§!»CòŽK¹ËxhÅ¿‘Û¤æŠÞ|V2³€f…³sŽBCÔ³Cÿþ„’R ŽÆôQh7½³ØÊjõv+°%õ]>‹á^k˜Ò,r³‚æ•n’­kœÌ ížI€gÖMýõõ©+(Y$«R6pk!zE’LÊØ˜Ò‚æWøË=ûÍ8µÊë/-–·½\nœô.Ï(TîC-½ëêúñç'‚Vnµc¯Üh°Ðhx}W¾SHôæ² €ü$ê;¹f"¸÷úæ`[˜âI#…Îh_½¹˜Í˜}”œÈòOٌ۳Ín›E|ív;?|Ê摺ð½+0ûԾӇ̧õöŸÞFÅ¡fësÊ]œƒä&ü¼úÎúÉOrE6 ¹z c.¸¡5õ×1JüÕ¼‚h™oG¾ð iO¾XH¹óÕoH<«IƒÉõ?BþINÌ(Š¿³1LupUô°aýrÿ  æõ›Í ¸E¯nå3oZÚ¡…7hòCù[k¾“ ^ä…>YvrŠ2ÊJÁóU‚ƒ3k¤ÒÓ뫹Û=qNaàŸT`Ÿ‚üÐx¾ó ìo‘@•‡³öI²ï,}ÉØö/`žÅgNR[Þ­q¾q∵#a2|1 Oú4õ!ò{¤€+F¿-½ìúÄÒÛ#ʯ'NØ›6jíd‚¼¼è)·Šëd gý«u@Å«Ñïžîn½^üŠkœÈi[g÷ð"äj&礧 y¹ÛY˜³ËWìï6Å…<Á°R$­t±8Ã÷âèï±î ÏÀxöQöOAÃðP³ úØxÛ¶mÛ¶mÛ¶mÛ¶mÛ¶­ùþssj滋=S•«Ü¤RÉêÕ½ÒIp(RŽOßho·ñ¶q‡#qûÄ@=Ò^ð”l±ÕBï¼2ô)…$Âêt¯¡Ó<*m‹òÉà*®¼Õ‡Ç|X¯¡B‘‚vøJçÊß‘(}%GÔ”¿£¡ßÎôo·æ»ä*n¤,‹ r¡ç,ÞNq‰;?ŠÇeøÃæ/U“Æ>/D >ï®ø 0hu9ßïž#Ùæ<‡p…ìg2¨^¸lsZÆ®)ºŒ‰ÔÛÆj(¼V9Ãr-e4̸Õ(&4›+K,º´+ ñrê«^-°Ï’yLy¼âùÇ Å^ä^ªnýÔÿü¤ú;–0µø›±ý*Ðîp84æà¾ËÑ œ,ö!²;MýG5’ñìx ×0FûMV[õ ¨·ÿÉ[ž,ÇjË΋zó <- O¥ýsÖ‹’ï ÂÕ˜@ïÿ÷‰1;Ú„f"û]^À‹EžM¨1Y^vÄîL²õ(pì² |÷©’-òønÝLN|v4rÄE]ˆI'šåð^ý™h+BÅÒ[œÕ鬔9—·-jk)§Âz‰²ÕˆÃjz%°¼q$wTþaG…ýÚù нt@1Âì•?„ßÏ¥‰[=«r&*§jV•rù‚™åûÉŽý5YÒõ¡ w5\H˜¸´dtÙÙoAë­£2LÞëx·Ù‘w±ñrÉr¼íûÀÐòfÌå »MÞ‹u›æ%ïSý@­Ôê‰n ÍÙ­¢»U#,ŽŒý>™ls®9ƒã÷³T¹Mµ ¹/™ ±Ã*.©ÛËÂê¹¼½¿¢û~6 ­8TNî”^ìöðóXéñ{»Mìb¼ßœðÄšIZìM1B”)y½ÌýtÉÔX\?Rýµ’Ó¸|úå²é‘tÁÚ1¼x¿ÀûAû_ P®rØ”˜¢öÔÿ§º¢‘­©…™‹£³ãÿ<¦Æcå´Íš¸÷BU´‰˜2-Ÿhd‡øaj×’˜]¾šŠÍ“85Á‹:2 ÍqŒOBUQÓÙ¹÷võš¦íŒˆxlÿ]™åã ‚#Ú7¿ãÛ0†4ºDž5r1ƒKõqŒåÝúŽc<ÆÝÛA³~”³c·>Å>É”0íž@×ÄM}H½ã«7Œ š×À˜óÒ–}C7ñ«`7z-– m>‹ûÅ5=FôbÙüƒÏmL¢ O=¥ ‡o8pê0ˆá‡ö`2p˜,²a˜1l€éàŠ‡ÖÆ tž‡~Ý Qàq“voeÛjd qtÖJžêµµõööæ+>þ©áÛ”3jø5ò0°0ñ±ò¯”6Íb0DŸã¯%Ú)ýò:Í_Y8UÂ}®1;„#ÌM$ô}÷̇©xÃ'ÝÉŸ1J$alÍÄAÊ܉  c09æunãá#üÇçïû݆ޗ³÷ý÷ù1ð§ÙϵÕÏÑÁÛyVvn}àbâàaäáòÛsöUÎ×ô—› ®8Ðà½Ë$ö8‘!1a.Þô,[ bš)"^ý‘ 0™XwÔ!È\n’ì2G¢òõ² „õ}*m=+‡¶ A|½¶ïéd,?õt*øLXiOØ£gº â´¾a® ÚÊ)wØâî^ oÝ0^å‰\êLh“dcvîKósA&7Ejç@_w>ˆaëïöü˜}ÃÏY»‡ÝÓÃÙáõ)½ç¸Þ?}|²„C{åþî>`]oÔÈõª7}b ZÌÌÄ3ùlãßú<ïtÿyÃpˆï.´0”žvÒÕ¹à(g‡–22Êî Ü]د¡Hþ®5vwçva²í|è8í±s1#;7— vÈ\œúóÞN†¼^‹¢ÜÖ©7ntsP·–l~2=—òÐ@avX³°±}ÍŠÀ‰ozÇIº¯nÎÓwì=™t)˜”ó!<ì+<œ,ÝÀx¶·ÒFŽúÒ]FüÎGÓ´õû$¾#Ÿv®îõR1îQ±/y¡6Þ¥¢ ‰»¯ç«ãß!Ò\ø}æo#é¯ÜËPÔÝœ,­.6Ú˜yb/çßïóAó†O÷Ð:Ñk: 2zL F^@µ>¬È¿²§iàV½`ZNZ=€³¨šXç4Ôüªy„tƪ:?FŠ–dÒ2ÿÊ4o :-Ù-Ï)5×ctÎØ÷%(A,“ûŠy¢gu¢™}Ž n^_IKâ™Û±óNK­PUB*$u=Úƒézèw3²þO GQbgŒý`á‰ìÚ¤÷)5;|ŽFAÜÈÊÒžZšõ¼’öS¡å«Få+>™ƒ¡é+Žòï'[µáp'oY“÷° ‚pÂðƒT'g™îW£‚‚^qN¶U³Rj¡2ãáËÐ|ÿ[eßÁ¿‘;åúÞ'â+´â3i&U¤ZÑ.ÄÁ[N-kB_DÄ™ àüäÀL`BžR”šc 6ÑXð@ÓøÝ¿4H(ËíŽâß7Þ„¿GšÛ ¶j+L9Å7¾…ª¥¶´ÆÃéëé®yéç1¬Áíè;i6P¼©Uƒ ÆÂÛïóÜa¬,dœ[ÀPÔ5A2«\ËíÝó¼±q‘iÎõ*-ÅÂi/Ÿ¡ÈÀˆeÛØ²,b ±Q7Ïqmé(d(„9GA™‚R”Pµµ ð$'Êú,§é2 Ï¼™î~ „´÷£tF©=EÒðþyEWãý¿ ÒöÁ¹s-ï\o%¯êt»¤F·çMó»SÔPû{Õâþ4‚ú*€p‹ž…ãçæèéê\ïç{lj/é\rÍ"~&îëòFrug;Q€U÷^WzPœL7¾8ŒñèNƸà\§M#8k(í®´jc$¢gær³6šÞœ%DYÅÞ)½7xÊŽÓ’ÉÑQµÒ·U ×Pf%ù0ø«ùø°ò „O)ïšFn5_Mñ¼Ýw¦‘1;=Åþ/$ÒxWx±#±‘wæÄ"t:û~™ Õ:J¦ÌºŒçcöEž³Fé4e|ŽtA†u‡Ñª5°M³å7Ô8ˆ¿ok#£Û—-¯ï#ñôY³S n­4\//÷¥$®¥eÉ*¿Í#¼Ñ'§Âæ‚>!S,l¯Ž}DŠ‚]Îk¨š:ÕD .Èz\ó½uqYÄpÚ¢²¦øyÖ £­Õìš1¦ÈzÔ Á½ JÒ9jÓ‹ŠùÛ›°„u\óÕZ “µiAsÖš`¨ÅÓJ¬¢›fÑôô+ÔëyÄwWŸ»ÖQVíšMú=Ÿ0¯ê¶.¿ÍëÌþžl~gK»§fTáÏxÀÔ³˜¤€G?ì˜Å¢Œ¦”¥yIÞ7¾hUy/dÜÊt±dÉF«‡ý\DéßUÝ,A7,ø:v2o–øM@2œNÒf^ÞøèsèBû£¹F ®˜­Õ:]$3ðƒRÝ%òœ˜»Ô7Êø¹m[Ã9ój~®HMtwlm ¾·E­>‹e 8Ø‹î¬vCQÜI'v°¹}r*8AÂ….D W MlzmlöÕœEàm”&$No^¶M÷دÑ_ÕäYÔΙ†€ ™Ã¬.!QÁìFn” Çñë]½öTÌ _Û@¢k6k×*|Þó”.bÿ X‡Òdàø6 0?»™Ip´+8W-F¼”± ¼ä$ÒÌ ásÆ®M&$Hí[®Jô€´Ãƪîåúsõ ¬hýR"ni}æ »ÚÖHÿÏÊÉž‹hð±§2á Ã2$nò±< ªn?ÆZ±*Ü!H¾WÈm=Hª@wKkÂç|Œ§Ñ¯‚ÿi‡–éóÅÙ=o?©ƒ¬zš|ŠZz!ÚiŠCôrOOÓÁS»”3ßHÎ`~<]›:ÙÒ%zEþ­ÕŠ^ül<ÉüYÁkÇYÛnq¼? t‡WÿÖçèMº/ˆþD ó¶aBùè²ÐT‰÷6"–P®UìB/§gÜÔ´.tn]€ç· ¨ëÉ”—» Œ'¥Q]H7¬ã/öŸKŸD•Ðy£§Ðº‹ò¢¶çe7£ŠˆŠn€ UI‘ {“ºép9|ÑVh_)=mÚN¥ö~Lå¤öß¾ "ª¯ÊAñ˜ü Ô·Nx‡/½©„kßI“Ïüƒò­Dˆ,;ÑÛ:}M6“fçäo 6‹foO²˜c;Ï(iåbù„éãSßß7Û<#Þü´t„3ë70f&Šì­+æ±yò…[OÙÔ_¹­\Ÿï'Í­H¯NÙ–¥õO¤Œä‡ôþЗ<í¯°æ. ÿ~¾ŸÇ Œ:ÄȳÇx„ççŠñÇýúßæ›5P4C@I¼I¬É’ªX€Žœ±€«ö€qO$ìÑÛ¯E’õí´påxËDzñÚÞÀŸµ”Þ´¯5³1j h_»Uáp¤Ñ¤ˆÒ‡Œ°£©Ã84€cåÊã%Ì Ðd)’¦6öo©ÚO´Nh¥~ÿ~ÀŒrÆZ—º?'4PDQEŠ0?]ÉŸJ ŒÜs»þ¾©IŠå¢‰£, N9—•éQ:»K•díà5k÷Ñä k!1µŸ,û oyÀz”¼?-Ö ”³(P¥'Ô=ÇèР£hJÝTöÞˆ…æEÍN}·#¼  ¨¥©£5¢áKhtÑb 2ãÊœƒ½5´lßs¿‰šÌ4È^mÕc….„Óò¤K«æ®yšS6á_†údi‰Öh=¤(ºµV¢sh°f ¦¬ÿ€á<oëƒ`Õuǰf‡9V:·Õ 5Úë}³ÛßN†'asU«ƒ+Ö³ þwÎ’° £‹òK-6ON=êÄöm»¨†q1t2w³’å­ ¼Œ¦¸;Ø» l¹Šêô¹I“‚ ×V4P‡˜ÝÓ¦x3×F<‰ÎýÝÜ‘sªS¾nìušGÅ…yÕ^®T”3ÃQ¼Žì˜¥«€³XgZ°tŒ71åÊn^æf«¶¯r#¿ý»²ð^¤>nÃ{*×òóø½ÌïªîmV˜{º¨jFU|<|ádDæ<'p'5o© †ûÍõ2¹«˜Ëz@'¦·>üâLd`¨žrH*ŸµŠåËjFúq£á%^ˆ"MBEoqæÐ/Sò—1NaL%æÜ¹Êä]‹QmÊDÕ½–åg¡åT($5¥Ý?çÇ$” NÏ‘‚j{¥µÂR„ðú›ŽÜ·sòx4%½#ôê¸ÒSƒúf8È¿òS V&wK+—ÐR¶‚NAgëè4Ù³ ¡¾Ž×ÄÉQ’`8xÿð>w®('¹¸æqSïÎ$ŸŸg &ÐÐ}fÊwnÊ~÷ä´ºUø›rŒyµÑ´) [j1v® LVÎx.Ys"tÑïTfųR'm¢"°’KWù2¯U³ÆRu‡´®³±µ¢ÉQôºÕªEÝ›B¹èfM‘†«_WU˜JÛûJê5Xþô´ÛöÝUöV¨%0á¬GчІܨ#³ëÎÁËsânWGcèN{~3[æNCT~‚Â{ëc&Qà6ÙãÒ¥Øtõê.‹ZÀ+Ì’cT…° ·X)3ˆ='ñ˯ȳ3îÎ࿎Q¤¦¸e ÂgÐY•üÈÿÀxÉé-H³F®f2göaº—6J呾¶òÐ*”A1ŸqÝ\¿iØK‘-ÈøŽ'åó»Oé“Ó«]£– øÍcxR26¥Z´)מB^uÚ§ÕWâGÆ“\j&V³T¢†ˆø'óí¯,[»^P¹× Oá‚`2Lj*™ÇFá«ZÙm¼à*¹Ä¤6§WxÍ]¨¸êÍúìsÂ3ó*¦1dV‰eÛý™“pÈ‚aŸò¿-Úq´,F¾PgÃY©º €sÆjÃÿ¹)O_ÙêQ?Ën¹YšÏ­¹Ó½<‚uƒæ¾]?ÛŠèý&Oéd^ãTrª°ABM®šÄšl»V`V‰iÉ6ìÍ[³^wžç>ã…€)ÛˉUíµn¹ÑëÏᬳGAÏIÜnîéb\ Ä]tp¬«µ¶@‰LÝž÷ ôƒ¡Nì[.›Ô»gû¤AeÊf‰CWtºÛ?S›ñë'â®®d0?ÿPÛÉû–,™¦#2¶¼µ³ñÁäIÙµÞn |,þ©~ “bNO'(9Sù/[·%£ÛU´/?ß87àÔÀRÔ@Ðb;סàäl,mqõ@+V•ïÐÝ(f&3Y*¥CVw”¦G9|; $@êO»¡‚ø$}¬{’ø>3‘h» åæ’ ˆ©}u-xåƒm’`)jÄ&ÔÞU~¸~Oô…1~J{{΂Ð\‚*ÑXµ')È"·¯ÕÓ&’ô ßÌÇv¶¨ÌLÛ¹¾ÿ|HÕÓÉÉJ—\m_)õ<`'Db‚ŠÓ$UÞÃ9·ÜÛükhCp¯’1˜Ž“ÝyœÝ®ì¯FRÛÌ#ÔNµD¶ô‡' û…<(³|(‘êÔ»+°ï xÐyø2Åä?öQÒ”¸ÑŽGqçP?”ÃÎH‹›¬Ù‡¬ËS\ß¹«©[¾~4ûªýq()¸ÐÐ00–Ÿb$þ^á6>l:½îD+º¦Û@ŠH—sZsÑEšˆX*h5ÿØdPÕ8Ñè †˜¿FûyîUÞtù\~8ÄãAªDU´påLürœrQÇJy) ÉFegk3"‰<3|`„˜«ºJ“±møê¬c|Y ,¸Æ-+å¦ÒÇ{\¼œ@ÅRÖ‘B­ ß}>˜\+ZNë%¼Uòàx†˜‚‘s ˜SCLwÕê—Ùˆƒ1£¯L;tºŠË¨l$¹K4A2uS¡–ƒ6¥¬“£f.WSXE´å踣&CWy ©»’ÌSÓØyäÿÄ‹¯I]¿/;O‘-ëþÞ³¹×›&ø¯ÎùiË«1¨xÆ&t«¥â ÐEû«p̃IÚ,¤1&ʪÒʤJ””SºB“¾?ÌtJï™ãI´ýqå¡ ?%°ä¬/ý8A­Æ?S–ê(OtÞŠKjŽfÖmžô‘­ !„òùÏ|•y‹zS®Lô iE‚¬UöE¡9}á]©µ#fy€Š¢ueH¦çD“ÖŠ\¥Õœ à ,P¥Ÿ¿'CÔï–é2©›3(2i©ÇïÞŽWãÃ;ø<®=øü>®7's!pÒT’_vö¯3cãøÙf‘‡šœæú±tÓE–€5¯·“¥Ü?ðd»Kªç¥XA`ï³+$KýµgÊ) ¢1ÌúŠŸ3,ï/E¶‹+íÇ:둘íë&Eµ\©y Õù=YPƸ7„pK ™b°K¡þ“œQQ„Jê€êÿömI"™bÁǵ»mkÄ òN¹[õ)´>m¿¶—þàIе¤ÄŠ_ ˆº¹j¡ÝRT*?Rë ï…¡ h g³X G½B ëýTN-X¡ZM ëüZ¯¶O áöZè®"„ezŽèç¦|ÔÀʜɅ´‹Û¤ê ÷ÄHÚÛ}|™Û}~úÚHôÙ‡º¨Pw·ååûÁoUqw„’‹ö¨ÍTwE¾ÖWå#k¶ŒœÍÚJƒ<· )L¥9h+ŒÓ„ZeU'tÆ¿ n «mý}û…ü Üœ‡Z±€uôQPўğés:𻎪°Â\. ´µüšùf©„1ß7‚\¼x,®@“o„µû7Ëó/j²·êIAÕÇÕ îêç\nBø½Cn©CJ±Á¬e+Ì.nWP‰Æ)\¯Xß®n 1Ô'Iïe/÷åŒõª6¦î?ä«Úݪ¿üpïÉ{ B“Ú·ƒzÍçδ–œ„Y» âÀ®cZ<òí%Íá2Íé²Ðr9›ˆx<;ñÏ4 ®Ja=.ª~=ÿÅ^›Ls1é»Lg'%@gã9ªñYν]Ü¥Ùã ;àæ¿ÿÕËîÀnóNठ@óÿñÌɨÇ[Ç{#e·æÖ4i‡XD™Â♉d&Zd&\ÆB”©•¥ª¡ƒ˜…”:!“%™Âbª¦–pCÙEE…&Áˆ¢pÈ8@Ú_¦ûÊÚ·›¿²šX71m„PÜŸ›˜ŸÕÑ,¨%) ²ž·~öc¶£­²SB2 D[Zu§óï-ûî{Û‰ê¯lŠÏç)k¯†â‡N®o¯xïí2á&¤¦fÂS¿­Â‰uuÄW7æZó\Íh¯ ïný!tE…’k«1HÕúK7úã€É6«#C#Ø‘9ô´NÆÕùº¼Ë¡ŸÚ¿¿®'ÊÖ{ÑvDåBà†Ü«ÿfû{w¥Ç±|°\s7á‘áý¸óš²EBMêG¤äÕ‘²uÙTöætZJÃ÷\H»}8Z¬r5 Õð^Ì*Š£A¢ZIç,,>\>se]â½·ºÛ>;üK©4¯Œ-;H‚#M=ý4N 1E.ÕQ0CŠY.mìÆs(ËƵŽùRÀËËñË ¯O;ïqm­,޽%ò"“°Ö¶5°“5¿w—t×&$¥G?íßrÊTÖkF³¬ÆøUÒôÕ÷ãÏóUœvq9ž¹ø Z%>cØ’šÀ—¨HDZÈÖ|ø<(—áSZ¿4ÇA{猘v"›³?ýe§’•0¯l°Ÿ£.c3ÎL¬ËôÞƒÆü'{VÂø9Y%oBy¾À[_³¨niDMsÅ?Ñ€‚o®!-|4ðQbˆœ² )Jº2¤‰°0b’YÈ$ë—!ls…¼ñŠÿ•–Ô0v³¼ÎNšÕÜË•JÀ± e=“þ²¯Q·áj8åŲèìÐ{ÂÍW»KûTõ¿¨YŸ»z[»Æâ0m[½·gtEÒõ·@ÜÖ#8…õ6æ‡LL¢‰ëDŸ/º WÃQ½R(½L™vh[­Ú¹G‡ÅeÓl!s¥£u"‘F½åŽá˸Îθ^«™6ö³=Åbó8A›NÖš?½OŸfIbl|Œ’Á3#Ï„!ñ½f‰3ñ½fŠû™_–·˜Ä°+H98EGf؆š¡Oã1Œ½?Š÷àž:u@¦ÄÌñW Íb˜¥·dŒ~:‚|rOÄ›>¢8ÉSͪpúÚô'vY(/1³cr†#ß×<%5³}L—Ý~NÏ*ì¸P™þ,·2’Œ¥ÃíÇ$Y>õš¦Í,ò‡ÐÒª‰ ìO‘x º<# róX¯ýÒQæëwWôußÒ´èþ£-ñ~]«É™G€óõ…Ä.d;æ¨c…vOUáßÅEáðß›Ñ;ãíöI0Òñ‹¨W=£âµ³Wñƒ3Œpϧºcušd1NTø¡GûÀZäìÁxÊÁˆ’ý7&¿Ëƒ ’9ÌfÅñR¬ÂwdIÍíŒU~B¹_9fÿ?yº¢0Æ;ÝåÑ"årú¥Ã7±¦”}@üùP\,8'èçåRxqm¥íæ-j“«òÀ’'·'ä Úü>Ö_'î“#U¢§²®*7Iù-3µIô×C[ñ¸y#@q Eíâ¢K稗Ü9· 7ÐúOgÊAb9L\zĽ¿6xp°¸G¸®JxÌ,"ƒ›ïÕ*ƒ›ŒÑ#÷ƒ€ÿL—þ¸Aq™D£YÑ;1Ò%ƒ{3>+¿ gzo7Œnm$³˜ü²XgCpœ-ñ´EÇØJ·;á9§&[Ù_¿<“‰cnkÿ'ªÝÿT#r0ðâМȀ˕ÝΙ¸þû>ò E¬ 3¤uužî,za`>;·å½;¡VkëÞJn;ÿ„õûÅr¦_0ùYžø³KYÂ5æÜY–@*RX©›Hž¹Çsb;77=R¶ˆJÈGšmPlïQ’”vAß 3žiÅ\·qhAæ¼–-…Ž0é!e£|:wÁ´¦‚™‚lïw%nGisJrZ¶d—oÇa¥xåï¸q}ƒëå—HzÔÍ0OÊã6¤JeŠÍJ¾õC†ø;Ÿ I€@G ¡¢ª{iÝXWØ¥ðÉ¥«>”̪‰½Ï6va;Ùq´²ý¯‡JË̉ìüV:Ö%²QÔBô<Q¢院¥ : ÝÐŽg5x hôF%Ÿ° uìžáË<÷ÅÌV@hû `âÝõ l¡–‰ò…Ф ~^4„â)“mé*ò·®$ ²–\/áêŽ"*~©Ð_¾He‹ÆÁª+€Ò Ϧ‘DzBëJ…ë?wF›‚ÒÿN—DïÈó ò«x××XpÅGÛE·’´’ÀÇ4ÍS븹ìO¾&þßž‰Kr6ÊïðæÇ´ô2Cnvâæë²-‡TÆcS ç\¾¶]l¡ƒb09[e´n?*F¤02D†=væ?ꎲ¼ý¿ÉÉ¥<õ ‹O¶Uƒ%á[pIg´þÝÀv5t5K" Pæpi²,„«é<;±ýâ Ë"ÀŠIöcƒçH߯}5\ѸÝ^é9—îZrtzc/×íF…<›ýœ+û0g:wÄæ\ðY.r׆¢õ¥V¼ÛõЏ Hù+/„÷KT3Üz]iËÖNŠxЇ¤i“e„€‰MO¢ÈYE ϯ˕‰°-+«¸Ýnä…GzO­NBóö3^c¤eaË'‘3= } ù3Œe¸­ŒCÚMËe#Ö1¡<¤ñàZú¡9¿îúóéiº™’võ@­0¦È¸FÙ ŸêÆñçl„ÃhiudÀåøí"êéyÙ9Sãø­r\5Ѝ¿‡ÄF÷Ñí>Tô' ½NRlþ¸9WE\ÒfûpYdt/fê gS`”—2"§ ”Ð2š0@Oš×¥CNFG?߰̑ʆ´v9 £5J µiòr§¢5…)eÉW,@KGÎ|Ãws§´ó^`ªáu‰€«# êF­AtÂ#Ûò€Ö§SÍŠ4Iôiû†iû€#…«AnH4rÍ„L¬ÄÊÓ¬,ÝÁuÑÛ BÌ›½áëÆ'¯ äõJmóDúßBø›Ã~ÒYç{ÂPC ý\w‚^ËÄ ü¿‹ütÿb ’Tœõ)‘gånë¸<gi¨,Êà7–JøÔÞ.ÜBª‡ÕÁpÛQT×ðÚ¥Ná©ì•b>(f~>ïlIÍ´˜=.‡Eº‘76 @ÛûAšŠñgÀ·!ª 骒Ô(ŠB”Q7¶-M»ÃâŠáhUÀ^pÌ*JÖá~*Óú·AšØ¯Ä†4c–†Œ\§ðœ^Û©Å WË¥ŠßA@ÁŠ*Ç^iÔ²²æ§Âg´jØM6‘èòsÙX…C2`Ä(2"³)ËL­àËÓÌÐÙl¯HŽ~³r²÷X:“ÿþ(2sÿý’ÝÎ"éÅiüñÆpoEÏÁ_¾FK>3ìˆbÀãÒÒ;Á{!3yÈÊîÒ$²c$™hó¢fÑðÓßZÖü“>|Œ½du’šúrÝZm»-ß¾/?ÜÖ#¼ŒËB_·ßÔãHÔô-¨Ÿ¸jÂ%;Yš½1FTG†uÖï2ŒÐ´±¿æU#]ì½ëI"¤Ák¤ Úð¹ —¢5®â6îc-†XMTW?1¥W«.`º8Š…‹ ão"$­ùÓ@Ø£PFM—ƒÉ*ZÕø4Y|ÄV û§Í[+=P\.@F%}«÷U€Œ9­ÉõðÃaÀ|ˆv¬ Ñ#V¬w ÷A5=~Q¯øj*sÿ «Uþ-ŸC…2ûm‹dÎçÒ¤ò¨VV[å¤T¤&ôˆµ”nìdO¼Ocñ…ϪÇG/J7,!%®ƒð)*!¦AoÕ6@§®Gj…ï.Lål>¸ZÆ  Ì÷È®¤Ï¼G¨ qî3*ˆ˜¡æ¾+¾ô=‘D:³¬ù¾ÚªÙ låJÜœ`½¾‰[rW4Â¥AwiÕxþ«Æ1mn_{;óc³Šfš„.2ÏÉj÷.nFBožOÈ­87Ïê kGüEŸ `S1ˆŽÞR`´úØd2= »Ò¦IfcÝà©PýŽòê¾kœ°!Tl‡Ê€æD”Ò$ª`L›|òáÛCcÌ€¦g׫GÙL`Yýóœ©2Òl‹úE r'vèJå|HôÎFþI˜H ’£HŠ»«Gj­ÐÖ¯LL‰Ö¼r.•”¬Õ蔼¼_P9eRáVÖØÖ#!ÉË¢ŒêŸ—N![øA«ö|ïèȉÜì Þ«c¢~p]•ɤèÕB|zi_…í:´PýwRÝÃXshUž«ÐeRn›—ö9º¢ë×yÙõS8÷ I‘%Òâìw&ƒ’§’uÖü5@¨¤ÿäBž¥+Q#¦–Ûɱ´ÔÜáhæ”qcYé 4)õÌÅ£ãÈõ¢2Yjo¬7†¦q`%ÆÛ¹”ƒK!¾Ü/YŠˆ8’—óâþïœ1 ‡öRÕ«K¬bF…ÿ4CJ¤5‹„v·le@eY3’’Ñc©-%Õ=[Céz$nȦ9G§vmX!ŽN#êl¿ÅLݸ‘Q€Dàçà·=4"ÙQôÍì6ÔJÖi‹÷~g¦÷=:×ûþÄœv6žúB—ù=™Îò?kVé­+s0£ŽAGñè£sýS×ûçâU‰¢w5€°'Jå>—k-¥<|¦¢ñ-¡à¬«p¦}¾v#­SNRµÞPúþ땤Åu§Ú«IàÝJAOjAA\L­»(ÎHRƒ­9ì šÐSì­9ö¥÷õùò£ÄúRÙ¸PVfu9_JZ§R{h&‰,›È~B›Rf£³(A„Iáš#!4rC˘ç"'2ضOÍæY[¬ÈR3^?(ù˜zözäj½¦q±×ÀƒÃÓ(“øyÓ¹8V-ÆëøQRÞ%Q%|ŸœwcIdI2o\¨±ø‹ýá&AeËÓC¤Nz±!ú’Q×âÑ—$«€$úOIÏðÝÕåÚ•…º+qCo '„· °ØX˜òWÓºmœ¹qM±´MðO£g%öCj0#§ç“ÔWûêy×ɦñÉ4ܺîµy‚6 OøóÄ}œô ÷N½_a¥L|¼0GoÊÎäùžô^/4­©”ªHtÃØà–¢!%³›<m€½rç趇npë¤>ÏÅ«oîGc7<õõ—X© ì³Üd·_–êîêÓE¢–ûaÃGÿ"àÁ‡“ÈpûL? <;ÀVQOgÈ9înIÑ£õ¶By#™rÈ›L+CÇ]½ô•¤–µäòÞ爖 £Ogx¡LãE’U¬+‰(gPa9ñÌô¤ö£ÅëË£i‰4B† ¬ŽHÒ+þ;j!>"€ñö ¾¼G̸Nòëz0ªÖgbÁÕÁcí®ÌŒÍ•¿P˜ºÆ/cÁ¹ù>ÐÇûÜŸt±çÄ*GAqTòÖ…†#@—ÛðÕRÛïtŠ4¾&)t·,qrƒêCŠò*Ó™—ù2CÖ̦ZÚØ˜K¯ðw–|böèâ+Ú‘!µg ­âÔ‘ÿúdh¿÷¼â €_:µÌÑh•b‘ ìâ­Ù…5ìÇâûù’Q,£Y)¹­¼N¿ÚÉP³û•t3%E¤fbç¤Ä+V–!Á1Ï(˜÷ íö,ËõGøÊsíR þܤúÝwQJ8#wÀ8a¯ Y¹>;–K÷• ø^#üL=ܨ̊“Úž µ£ÆCu©Å¾îl¨×_ºŸ¿G«±-üÞAQHðÈ7z™jw"ÑS¸­¸ÀWú[. ±‡Ú’:Aî–†Vïç¿)–lþ™¶É•o æc] ž*ƒÏ5T1ìä’«qI§pì|©q&§¯a_ËkJ¹ƒ§ÈZKmžä)8U9çÔˆ5ÑÏt±²7Ñ_¾¯ˆ5—2ÁI‹ŸiºžÍ ãyrÏ`û4 l’ðxrë\ ¸æ["VÂ÷Å/wóPdb:7›Iö£æ:?¯!þD£Â’IÐjž(“Íf1­JcP âNŠ‹êõ~'캆{ͼÕóò%Þíré=¾4 ¬yTöKº7Èâ×·ykŽâRÊjwy¾Ð|Ê|a2êÝ>ËN$ ¼"B Ï6Å ¦ð˜o˜ב?NS9,'Í¿Ïׂq¹©T±®{¡¬òð1n^à$Ì9¼DÀǩΌiôʾ‘wu»§e«fÀ»|¶Òc{Ø9°O'OÈ™‚íÛ!Îøq'Œ¶röÃ"'•sù/ÇO cÙ×°“"ºy.ɹg*áp±å<”Ø<5oõåæKà‰+ŠYUPö•)cþL–Ë/ÿÙ_’ ¿ã)£F¢>D’1íM¾ “Лì–}I.pº* AÜCFÁ—|, £P¥`ÇJ PÙ ÚùYïÓé|áû#úÓÆ7˜Ì¶½0æ—©w  ÿ‡ΖådZ£ñ³bÖ¬á-ÑBä8ÖŒSÃüÿ)ìqë;³&%ð9Øû‡”é{moi; ¸ôHHpÌ$§š6b¦–Ÿ£æÖ\`K"“*{'Ë»!Ø=¼¿¯Œ½KY‰ïió¨3Í®4 × hk±àý§Kû<2+̇ã*Gb8…·ÕB'3O}ê’Ÿ»3® æ‡0(ÛÂì=ÕÏ‚«Hºe|Ðqc¼Ó?<ò·`UÜ]rUõcŒ¶‚ÛdµƒºBsŠ&*ó®5Ok9Fß:DÂÆ¬²‡ô!ã®$öÌtìaú‹5å‘ô·äO&†Hm•B{¿j¼Ô Câ…·ò¹bcBJ‹W}‰Jr~ZaCXÅ*˜hÆi M AâäùÙ ŒÝþ6ºgò¦à = æ‚c²X+qõS’ˆ»pìI¦iØ:Œp¿ngv$@Í&åT°8€H]Dz¶ñ2à Qa™?©bd`ÌYaùdçobþŽåË pö¼¢ËL‚~bïu¢#ލ(•p†­Ë³Å)œËïçÅÙûIëèëÁcÝò¤`wì£îMùsá3u (àkêö±8òc"m~8¾Ub}£´]º>ÃpaÏ‘”Ÿ 9(žr~ UQНŸË¯\öˆ†?eØÎŠûN>~“40çHÉO}ªTœV²!|‡GtAœ¥ õTÛü ?Åb!(W·K‘"° ))ûP ãÿ˜ÄW_%“ýÒ#|AX',â‚5'ƒ5~#CÂ@ÓŠBºT5¥Åßö&\(ðò> |jrw†X µÂªW‚4‹h8Ê–Ô‘OÛuû ¯þÄcň’ƒRöŽpºù¦½Ä[‡b5ñ!¶r²‘‘rëñUdnDµh²Fa-&‹3ždí¥¸š¤ñ£à”Û#‚xò­>¡aÀ‘™XÃ^h**q,"(›¨ V„à *º·+(h¿è™ºku%”ðUæ°¿NkKBÖ3ùºó#ÌÚ^lsŽ/ËiŸ-G°æz,“9˜H‚s803—ɾ®‘ŒD© ª\vâÚZ/q˜n/R$ÖD³ØŽ™ç '/ÏÍ*ÉÑÎ)~T“Ò²8Þ&*ä‘ 6ü›Ï"ÄwLüòèsƒïüº”Â0ä3+”¶,?­°%nÔQ'°˜ˆ¸é ðUS–æpÌu 6JHŠp²9:·˜W““ÍTÜe_ š±+.¿}*oàiœÜË`6BcԲ㨠ôhÙ,cÎÀœÎj kÅ9—É{‚Pè…ù΢·‡:ɰjÔ•>š"z]HÈtb¯ô-¬óË`¬âtž×”Ñ öOÂwC»žšÔë%©¿›a‰¤y9×À{ÇV<õÛù¡ˆJÙ'À“Lm²‡¥K^ÊUZòî?UK-ËûHô³0ðP!|dÓräàí¬3•„ê=î ¬Ü×JÙ—‡ÆK ”{q}Š}%à^îJ޵åt Æ2ޱ„3,DýÄH€.däí%Í*WŸß•7|œá†_ýMý9^~z¬Øjõj´ÉZùv?wÛÐPÕP÷4/¶'œU<ôÙÅ{µƒ ×³-”4MWߪ¯ñ.άا5ò2„¢B°¯Îæ§ì‹aÊÜöú?t¤<rn“d»Ø.¾¡aÎóp¸.nnm…?@Ña° =%O†KÒêa¼)·àC”R_:ÒdT x_.—† UË(xè« ž¸÷™Q‘¹%äšsÊà——t¿õ&Üǹú ë¼Cþ1s),Ïh(²fSD¼Ÿ£Ï-(¿ wÄx+ÍP ø|hÍ>>Þth¸yüÎbs5æª`ÙðY%53Á㞥ö¶¸{©NCÈ]'ÿå.ƪ3ÇÂù¥YbÍbð©ÙÎwêÏ’aŒž©Þô4}RŽþÉ’ééÇäÑp.LJ ø ÀÕ/ퟣÞÁ<½1<±!Ê1¯Ðu ,>¹¢CˆGÎ35xm P«5œ­5zX®ðçᢑ’2 ÌâHè¨ÝDœa½l‚i·`J$•w…3ÃÖÉÖ"árdÒÆ¥”Ï" T7Ò`G2ÀPº :F³4áøeSî™xš”EÙÆ"›eû ¶äHÛ—ŒíO’Œ¦`’#ŸÔDþ•2öÞƒ[žÝ¬3?Ú¦ñ%´LíÊg×Î9Å ®þ3 ú|G0^=Ž’¢’óãŽ7#|×v îY«êÏ'@’î[«Ó"ÕüÇžDlŠ!¢³Š;«W3By{/·¸y”oÍóêîó«$/€\ë¬)%)ߎŸí”5Õƒ%²|nÄ~…c|uTå‘3—A[![ê”I–»1CpiÉ8=LJa{‡àߌÌc¿fœÁVŽñ%9™/'zÙ׸Šð“‡dgÖ¼yí™·å%½åŒà'ä©#tFá쬥ú9= ˜á[“á³ ©^ 냄ï-~ò½¿ÍÎÏïò\‡²¯EîY$xv.uª^8ÈÏ,\?bØìƦM9'»ˆó•“C~º/ ¹øQ@6½ûGŽ‹Ð¯ÝùnH¬@¹6Œ‡A´U®£t¼†Ó2¦ŸËD¿Px¯—©Œ*Ìèî$5²Á/¾ëoˆÆ¸5ð¯dóVöœÊÓ. 5 rúŸ¹dûiºäˆ©R\Ðl{GÍ<¸B—UÀxvêñ¾n Õ×ý¢‘C˜wÉŒÄ ?kü~ÞQiá«‘Q¼¾áíãÄY¸‰uÛ2øe9vì¶[õÅ72Ý­_8t¬6ç\ZÞþãÃo¼pÞ¯Œöï>„ óY\ykb86þÉQà‡ÒjÄ øê°FÑMûö\>Ã~ÇúqÎ%§maÀŽòæ"T¸rø‘Wt: ’Úwb ñp ü˜ëÙ¼%HÆܵ+Ýãa†Í~ †„6 í£Êô&{oø¤-yCð¼½Þ©^eènŠ#Œ…kÆ ëæÅ3¿¨Tö‰çÂéŽ=x'Rµ¢ÎÙç|pþBÑïâyþ¦`Q¾Â¹$¹,ϰÂÎÜÞ\ß¼˜YÓÓËܸÜí|ý×GÜYþNHR|‡ô3pI'ï† ”ƺ¤/ú˜ô“fLK‡ýò€é§¿”áFölZV N3Ì\–oì¤]ÔM§N…(S˜v@|£þWÃá—Šöïg< ®i””̱B½yÔ¯à·==kߎ¼¾-lÊO•½¦ê1Ry6$é®,—‹YÉdo¬z}»^O%¿SÅì/Á›*ºàa±jR˜b륧0áXKuZôPÅsu“!½òeãxhƒÃñ÷¾ûsÎï_Lœv"<‹h£ÁØÁášn O_ÑB^óc¤êûUM†Ý!>m¿Gè*í};-W»þÝÖLÚÞÀÉÝá#‘ö/ -¶ú€mןú³¹BÔОÄ÷†û ˜ãÐj½KªohÍõœ°ÞuÑžáé%PMäøÿzm;ÝÏTÉ`€òÿd¡qr6±6±1qvô µ÷hQÓ±ÛbEöy«kwÂ5¬ËM|6@çFO¿êÌj˜¥µnQ@£ÑÑÒ6Q ª`·]øë’”<~ìDÇNJë{;º?£Ü²èˆ¥áéëýùM,P­¡Fu.m®W,‰‰íI%( %š\ gÙH¯È‹ø‡w"¶ÛÀJ_v6ÀÒ’-ФmÙê7É•²$zM®’ê8Ä.ˆ©9q'8K=‘,Ø2H±oAes½“Â~(䬩 hø³§êÂ…¸ ìJÔÊz¥«cÓ¶Åï^°xUULϬV'tàÎú éÿ“Úf18êçXá°r+ Æ] dBØZÌÑbZ­qroÛǪ̀ã6>bMÅ|”,ŽYuÂØæ5P90ë,U¼|=gÎlµõkÍ|µýVô—Ôæä¹—'ûuôaÎ^4‘ –ô0µàÍŒq´EuÇ7ŸWô0Ç Êæf†‡_M$FkÊtÊ@¥ÀˆæH"´M¥sf8Ó¬ÚV­¤ƸŠe¡Í»/ÂN’€eáÃ’>$Êë¹Dhº{m}=µffË&Èžlxe‹¯´‰h•SyË“¢Š…¨ç8!ýÄÕ/iã_¹¾ß¿È©pÅrå”*оJ9W D÷Ñä¥Ú1âÝB&‰#šâ|ßµesš}Z³Û²ühgÓµe/²jÍúÆ‹¦uÏä¸U—gÉ­­¤‘eß\ï8¤îK‚8"«üŽ¡ôVQZǰdÉöýi7 Êü¦.âKzˆ‚Ñ*g»I|†˜‹G÷/[ð˜-שƒ÷¤ñT†‰0ƒwá‚Ã,Ä'aZõAÁIid+ÆC/ÁÊcªy k­À–4±€J«ºÜI)Û4—Tí\1ѽüyË£¼„V³»Œ~ TÄ µÈW^ÛËu„#@z0Aϰ+¤­ªñ:´¬ghþãsÝ &=VÞº¤‚²~×xp4N>¯jL=nÉÒëKßh! }R²%“‰ªÙˆ±o-AlÃ[¯®…KÇtŠ¥;Î5‰ñE$„ø*{g«ŠÓ5 r½%×¢í;þL‚Ê& ¨Ïƒú\ÄÚ–ŠÜgd Êoá(´Uº¡t GèåÙšc³ÆüÂØÎÞ9d{%$Ä“§²äi~6¦ñé´ÓòpÓX€õl}¸ÒZÁäÁ»kFÿ3Þ^ïK„o§VÄR±áà.zë‚p¥{ È¥Y¥\ÃO,À”RˆO%cµð~bYûï/edì¶l(zhß#9¨· Þ{[‰-½­ÙÇèxw)¼Ëgº$ë¾ $„€î!ð4žº«â.z_*Tgå³Ò“@¹ŸÌ ñEœ 6{‰Ó±ÜÛ?Γä.ßn'„`àä<奠ûƒHǹ¸$+ɬýò¾GœJžŒw½|•Ñ&¡¤Œ¿¢×Îd`ºïó0Ý€ÿœ{® Î¥5ˆ¼I‡,.g÷ÖζÖ.ÖÓ¹Èúê èË&%iÈmè" ä¹Y( o¦¬hbjaé·ÌÁÀ^_¨™yý§×ÇÊÆÝcñDiŒ$$¿0»óùåtËÄÿ‰mP5çܼáˆÄÚÆy]ÌD˜jDñÔUÎheL ×ä1¼ û‹ã4(ÓLËí ïžõg½ÈJ¯$kú¶²Ö¥îUé5qáèµ;rªÇËÖü‡ÕݰTÿŸ°Ú¨EuÇqKùß.Ñ”ë%{ÃV)Ì<*³t Üt2r©R&&9=\ ´ÅÎEy¤óõøBN‰D–vÊ—î=r2ø~bÚ~bÛ> ¾ÉFF¼½Ùgç×ï—¥Ëo§óSôˆÊÀéî)î/(@ÐE“À¾,ì  %¾x-¾zˆKš „å¿Uy±(GÀÐwX_0wÀ{}ûÈÖ«,壿 µ9ä÷rKvsâ$mÞ¡Mb-,Ö‰2gìFe’J+½%AO90ÆdÊ,ÌŸ(ˆ ` Wþ „¦HŠA9÷0¨üŒ„a¬¬êË’ #’¬ËR#–´MêT#¨Q/ŠÖA+lhê(a¡iR¦@O˜44õq¥@ë0]s©oÒž„‘ËØÖ)ì·­5­‰êüž­B_᯻Ò™Çи‹uëÓßýZ;ÏßÊ.@|“®|Û?Ï™>7—•Ø1ª9f„«â5è£ •S½r*‡³Ær=²X’.Ôm2Fp¥Öዌ‡X3˜Q›Æ$˸9Ÿ9BùRœ—$æA¤¢ ÝóøüsF1„bÂìZ ptuAùe”t«á7Ïu+ÝüIìDô…c°‘̉,¿ÞÀÀ7·>ØŸ¤?­00½ÏÝËòZ‹è\ŒŒÞ$BB#ÜGµGã¯Ñîæõ*‹LPSeXÅg]½Žˆ!Q߉x1‘mÑgx8ÏÓ"ŒuéŸ •Ùã)¨b P3"½9ý~¤6n‰‘¶³BñŠ+²ë;´¼­ÐÙHb6!ɇÔ=wÒ¤ŒÔäOëq¡ÔNòN†}šéÞ/<ŠüIò6RšBÙ6©ì'¿Oý½Ý@†oˆÐ˜Û¾Ï`Øô_@£(ë£åêYŸrªúFÀ=^xMõs9¿Žé R•ÔñËÄI¡|Ü’µy˜¾ÃI€™Þéš¹¶¢wX¤p cí)߄ϧëè“e$§ê”,¹’FS 9¨P„€o0+†ÇÁøÙ%kƒIOþ~Ó¢üoø}c.7í¤ñ»Ã!a|Î5\WmwÕ<Œ«‡Ô÷Ñ䪡Xà€§ñ¼/;Üô¦ÚnѶ‰ûÓ.Gcà§“ÝS:!-r9<Û!ò $n|¼·În:Ù­åÞ”‘ï\WÕþyk_L$¥ÒˆÂbYn^W;»=Ýõ‰¢ÕоѰƒ¾—Äkg·r:W?3Ÿq¨~þ¨Á±‡Üeí­ÍŠ<«à_P¢ä9 ëFÕmÕäÆg.­ø HÏm7û:î\Š‚?X͘©—K…„0 •™êÍt‘¥îN®‹é¨áTM@ ÐÁ± QŸ¦VË)ö%B¸XÞÄðÏÞ­žÒÄ»§¾ÂŽ>ýI+G‘…4¹¤Ví9›Âi6Le­8£º¨ñêf Q:[¼i#üi±as”Õw VM6Û¨ gT{"ÿ´Ì6 w¨'@3ˆ:¥‡ºž\¸ràÓêä—,}ejJŽì:Z–9W8ÍÉÐÌßý©óÙâÌÏ1ÛÝ=ÉÄâñ\ÏZ BqºÄåvå1®¸ƒÎ ÉÞÁH@߆I=‹bDºt>þ·‡•J«ìSÎ`¦™pÿPßÎÖôX9¦ŽÓeëcŽ^Á7@M]Rsg|rT/u`ÈïíÄ}|- 5ùqõâ%X-Òˆ¿Ciµ-]€0€ÊÀÉH3þ"{y¿bAÿU‘Æÿ!!ç ÆcÓ~’µvx8À‚pñ˜1²ë-%nßÚ°UÁd!íÅ%ŠúÊÕ»ëêñ}_¢”×  \¿¸uÉ{X†EY&ùï­cάØÐ¡ºöé‰é·Óæç2K¨UÚLbBh•8*±@–õ¯Âxêoz3æl¥õÜÒI´é‘hÔÝçÆ3'åHÏq8}J:ζ¹Û÷t"ü)ƒžïd»¬‘¤ý€‘ÏGh,*q}ûW‘²~ ù«!8ƒ/¿æÑù¾Ìk÷b”$ÑìŸÔ‘¨™íüÚf=E eÿêb‡:ú5öqÆ Îæó}_e²ÃsÀ0”óÝ6÷é7ÊóZ»>eŽ-†å„ž`å³{´Ã²^¯O¤O7 Xž“)¨‰Ë"±Ú«•ëh¬×Ï‹vùïu§×\¾L°l¯Ê´™Nf~MÏ“¸‡¹Ž#r5jA…ÕN™ÕÁôËõÐdØTOθ4\+‚œŸÉL.Ó´2dTÙ|"´mm\¼óêºî›lüà¹çÉ^x2SŸ4{}Ry?2ç1÷Ñ2¬jŸò\¯!ˆx ;ŠTBPÞçÏÖ»'ÔË+}*{nå8Æ¥]qàÑ÷ –׌rÞóúé~Ÿ¨ï„|œõ§€­µ6ΩjЬ„èxØiù¤Ar½%ËÑõxŒÍ½+4È•ãSOif?¦A¦P è Pè<žìqTFq?M–ÆÓÃÞ}ELÆ&ß|W¸¼±¿€1l·uE8•¬Ù¦‰B†îh‰k­2Ç¾ÕÆ‘-ùXɱáÍ´ÄYˆ“xA©=“FáWY ¤Q¿7£íçÒƒ–‡Q#?Ïëž²ÒÍÏ#ôŒU>#g™F©FSšÀ¤W¤Sè B¯CàFZ†ÒÁ5 e•²ßMñw“羿 V)¥TµZpS­æò:ÀKqˆ6_`ol¾Vñ½a¾ëÿ Ñyð×H®i=|z:ÿ9Ÿ0 ¥òž³à©L•QGuàþë*íÛÚQ×° e(’ȵmóþºÍŠŠòA0~i|V•·«×"¤æ¥ÿ©¯Þ²†ÒhoÊ2‰‡+x—#GŠÂ¦¢GÃþç;úï’i¼àüq+ µ·1©Ï@Æ‚úN£Qíô±Ð@‰<èsÈâüë@5Þ «}ÄV ˆ9°nÏFÅl§­¾–ºx:‹jCm.•ÚѦœϯ¯þðØ"£†E)NGUÙúGãolQÒDMt¦PM:ÕQIÁ‚-|µÛ«´eó•­œÅV©ŠÉP£h£_¡H#”WHDÔ!¾ È Kz\ ²„¿÷o?4eäÖ2%XÃR£Oz»°>µNè¶ 6î,YÂrÀ`Hü0Ü;¾U¡'ùœÏÓ¦kìÜävãÂ$îãωÌ`RNˆ¥§ååízÅ®¦ÄÞÞ%Õ4²B´_.ÉRü¥‹-µ}ßI†3#’4¼‚Ì7°ÏŸ#äk BÖ§ä(OpùEÁŸÅM¶àW†©íq&?˜SÆ£Ò‘ %LæIoZs¹µNa§¥K÷aÅz”=®Nh«alsJ7EÐßÛœ³ +Uö _?]°£ã^ÏùÃùUrǪCÊ!?jï!MÅ)速 êÂÝeÆÆ`]Øû9¤þ˜¾vÒ€’0ÂÔ½EÑÐNk¨#sV8ó´‰Q õSjüñ¾$ì>M§a/ê|Å㲫xKx¥"„Ž€‘úù*ÜU¬b%³¶f•9¢\èS^ã–Sü†JÅM®Dóá³Nrí†e±ÜnGÔg‹TQÞ¦›rug4‘çý½ëL2²y& ^Ë•‹ŽÒšO½Ep»Jíª¶D³ŽÇP#-Z÷ŸJcsikÙ¡CÃa…’eMÙ '$)ç5vH5o»ÉS#]C=çfà•tÛf[y¶nElRíÖk'ËB*ì+¸y;Š˾j6M¯ù}ªœõ!ïHç;OêhþW¡UQ‚ Lpù—|“ ®+3Á„Ôc […ˆº¸Eò‡c’åÚöÝ[D Ïs÷ßX8ˆìýváµõ;á¿:»¢–.³ZKâg­œ\¼ó1e£_$ e¦@é _Y=â¾T¸­’Ÿ±ñZ$öh8}I@Å&ý‹ÓŒhÙ£½ñ“:Ø^à©´maãŠnÔö•©'f–7КÊöáÁ%øPDßÚØæÝ ‰Çø®È* 9K·96áa–¥%92š [KZ¤Æª™ìOÀ,7›wí‚ï÷ñ3« óC-s‚uåMÿ¨Oί,Ô§¦÷ÇFÄBBkš®ÊÏ¢µÞµ¨>ƒtM¾Â§ø%=÷•Δa·¢œq½tëéô5y}©â ïèpSy=»oaf÷ºÆ5ѰPv€´ÖˆæÞAœr”ÿ•#¸*A.ßϤO»§ƒSo»Ÿ1xÅÜS4ÆÊNÞbFËÇÈ5ÔR§2ë  ‰y ³q‹Â= 8VkÀŸ×cñÔõ©S!;ò‡ø\ó¾Fj`8T›ØŽô{ •פûüÓ‰¯)Å•7T÷2µ‘¼j+çhÝ.àl( âbçy0k£˜uE^q}«`µ³.uà*N/j>ëc5·$\M…ª8˜²m{>!wYÖ…T/JÄ5 ù/aÁkl*Öó°¸GI/\ YeæÊN¹7Ûƒð¼k÷ê¼cǤý³c(àcŽ•iØùDþ¢14t¡œqD´­Ð¢ m ËãO/é¡ö¡#(Bøø^Øìðœx•±ÖQ”j3Òò8³tÞ»eoÍèÓšþ~ûÇNÕÆ]gaúãÁáeKGquܹ±Bc‹Ü;Î6´åÀñÃ"L©&îe­dÿÖ<›š›·)gý±Ì91³ö”bú%ØcÍ)ñò¢¦ C‘’Àl€Xø]v¹ȧ¾h<‹…‹¼°ì5F:X0Ú–tõ÷¿ýi¥ÔCȧ€P  €÷”HF=Ú2ÖØb®µ¹Š„"Í¿<‰Ô­ÕÖ4S­ä‰‰í©äa‰Öµ=7§Ê ÖfžË;B ¨¯š»Öˆw¦IÖ‰2ÿ@8ˆùá¼ ‰ÕAþýÿœÜÿôqÇ Áy„Íxé¹ÌñùqF‹Vvv–^úÔéªôTÖæϹ¾þ~A¯ñMDœë%þÖ.éÙwJAP #‡„ÆC`N†;ŽJŠð’”*î_%Ûw ÷jI»,¹¤¥o’·ÉoÀìl‘ö"VüÓ}X2¯T+×%h‹¢«¸Çä W΋ç9„ëáÜ,(âŹY^ Å¹)5/‹ÿù¼äðiÓ»YYø¯{mgygugeAgÝ'g~áépr™öà‘æXÂA$ñ‘Y„Â#´°I0·ÈœÂSbü ¹‡ú4’QNÑYa\\âƒR$-¸[zÁJP·àÜ…`oþm·ø¼2Þî¢Ê ¹‡æ˜ ¹‡êØ%±@x&>žÜF}!R4¤»¢H‹³³¦ˆ!²—àØëóNÄ-{d¯È\pü‚1dúÒ<‘ÒîÆïFÐÏ~Jæå‘ Ÿ—€`=Îp pŸb?¨„äáÞ`?. Àú¼Ôæiÿ¨Ÿõrßbÿ,À©ßBÒQ0(–úèûƒçGÔ°q.°îÚÚ5@d¸D€þ°F¤à‚ø¼-@h_Í_ª¿ˆHÈÆÃòeßdŸª?¨îäveŸ-@- ÈëfοÐî›õÞÍðè]g¿¬- è—öv_ì÷âž(èñ^f_¬ôåÞfÿ8ðà«¿è÷>ààò^g_ü×eß«Ÿ(àvÿ-Àiä—fŸú>.€,à¿Ayã‚7öQ <ï<+g©¦‹uæR6iÇ^QðW2h>\ÏÝCž£*uÏΞí¼ûóïByß'SQ÷$~af*Ù£ëb?{ϸ(8xn(§N³|%1ji½ßPq*¾ãgKvô)ö ù!avIÝÌŽäÀ(pç0œøgóµæñgõ³è›€w´ÇUJ3Ký¤aƒæŸT<hôtšw\ .PNÁð¶â†Ž™ÞgÃA$ë×µq—`èª! š©‘éyû=‘ÑÏã­ÖN¬~îcø;˜€§!dÓ°Õàd“£¯%¥ÐRwÛ u:¸ËMÍÀoä¤z)A°¦=>ºÕЃ½GNì¹|ö³‘4W1héNØ/•æÿ¤" ¨9+*zé…ÂKøÎž=¦“Ùð"W‚Õ%Â?@¨Ò?¿™ªu$5ÏQƒ?pIeŸ—‚Õe"é)TãSJgA£Ä0h³-àx¸ó MjJíwDÖD•“J•:Ã]"RhsÐÿ GCˆ­Ò•yöhT1Qdþð¯zª_ö™“š¡Ï [>JUzÑi×6íûcæ±Ý1¶àÂà®&eKdt`­‰ô /:ß]\ã«`·àƒÆdÙÑ߇ð…ιD’˜Ò½!¯˜8 :ðIŒŸÎ+Pzå!0ˆõéµÌ Êc?YÓP¯ƒ\œ±ºb€GŒà}:c|ª¤D[Y›¯´íµêúuU&‹ßBpò ³u$’eø0–À²4P€+ÔÁOq{Û‚¾B3¤lÍ”ŽKܯÞ3¹;µO÷;Ë Û*`ÛuL¹óŒŠsã÷‘ZTbA¹õ±>÷?G ô§  7ÿ©³ÅÃE huí…þ$²†gµb•sÖd€³³£W™[]ªµ%Ž˜fgyE]G1»ŠÀgõÕª;I_BÀPÉãÆ hú=ó}ÕÛk—‹ y†ä!„Ô³* Ç!íÆ ñ–cÒN"FúÒ·HÓ¢¥äò«ÏƒoSØWÜT«²h•§Ø$¨Šç_¡¤ÇÊ*ŽËùƒo›/Ó¬{˜hÄÏ5>¦K^8bÑ–¤5Q#õ:váíÇôk=U'9SÛº5r-$]_\I¼XqÇWÚÌ֕߀°a³ahÜ‹´:4¾}Ô’÷p‹Pã¡1§] «4¿³æ‚ž/º«³ËÅôâØ|WЗãahKR†–…k'ðæ;ðA_Áц›«ö %¹ ¬'‰Ï½ëþRÖh¨‹pe õî£ûÕ"È´Îrl|MŒ¬ìÂûH” ÁÃÉ”lW%œ<'©©Ž¯w ªF£åß9À¦Wåø;ejÇá¾p8Œ>=¹šG¶S,ô½53<® éy¥t“§&Mï4„Píí 2íP=çÛ^xJ˜”Ã¥ÍìÃað0O’ÅðqDzÁ¤öX›Ž—œ½ ßgË¢ÓÔ[¹.=¬m¹å°T!ml";ßÊ׫•mÓIr)—Noõ6•{/K_ 9Q¶É§L³ì=˜MŸÈyk¦"CÃp·‘¸¨Ð KI1ë€qÛ‰¹Líƒ i3ÏŒ*@5M üøSr)å(ó‹„6lÛX>¾-?ß¼<½pˆ%W…Ç… ¹þã~õÆÀ¶›Nz*ƤMÎÀkêR(^ I¡ Yш]1ëBàȧžf! ÏG¨äD3uŠúø67@ $ÕÕ˜«@ S߯¿Zj±lŒ»!œö‹ñ* ÉîÓF®Ä¡ ɵíQo!ˆ+ÂÜ+b­BeÓÇÂí=ñ*1,!YÇÕnþXˈÍ9õ÷Å1ãdÜ4¹Âœn:‘lœðO>¬Æ‹Ž“ ±,R@¥…”|” iüÒq6îº&bb›¨ÑÉCs —oM€É€Wƒî àc¿Aë!ÜÑöθ~ÌíÍ·‹<¸О/ºKò±GàV†›õð@y¾|Þ þ}t;µ¾à Õ=wµ¦&wSã“ð4ŽÅÀxË«–Ý„»@nmFh9?VfEcO’Õõ ¹ÁÝ\æ5/×4»ŒGâ«Çši[íöø ~s(Yø5ŒÛ %îÅ ™lnÎ>ÄgΜ‹ƒ©\x¬Ü¼·_òzkHЄ¢cA (ÕL#­Ô|¬º~Z—áºkŸ‚Ku³ Ñʽ1šd-ðKš=»[‚2ÓánÃÑ}¡ïš¤MŽ\`kÿ¬ M7ã/;lUô°K¨‚,G¶ ux´³è…/Ô&[µXØ ,ìåcL£‚ë‚K†:X&žËkà›z-o‚é`Ž]¸ Sã`o°GL v™„¾bõIž窤ÇÚ°!¿Y¤* %=Z ¾ò®¼Ýµÿt¡‚²­M´­ÍêõTV Ùfœnó”^34`Ñ9Rô%©ÿÔd^ð"%Â+;06?ãÔc"€ã Ù8ß,Ê9$0“Q6“!´*ä½Ââ´ªä´Bk,½l: l0‹TÁRª¯¾j˜8ØZ퉘hð:Òx¼ŒUÛÉbµSÙ úëD{%³‹ÉTa;øðãÁãBÔ]ëœr‡îÁ ÚȨqHõ•ÖBï†Û¶Èö;‰‰5Þf¾.¬é×9ùë¿áˆmHÒ}ÅkqoËí©|ðßHèJq¢¿aΨ!ºòfÕüxÜ.ûŽV@[Ħ÷`ú(]W<ã][j.ö$qÂóu-R€N³læÂ#R¡ó´–lø±‰¡­ôÊ BºQ÷UнY¦<‘«Li¬ŠPä´Ä¬kÑC ~±lWÅàÛüéÎãß––C2é•ɱÞ^ª02XM¨P TÖH°ÒçÅZ‰åŽ‹Sœ¹–¨ž(Üàr.”~ÉW-öâhäF¥©„K1á˜6Ü{ËTgƵàå¦ìY'‡ò§drȳUÇT#P‹DýºÜXß÷N—Åz¤IˆÕ:ºàúÀ®|p´É‚ñù×"Âú’E˜c÷[þ·€ÉYêÒó÷Zý(æÅžc†ùáÔ¹ûçîúÑÀ}QüëÔ)yÿ>'3¥ÇUΦ§k ³zªð­„ºPÿ£G“X±ËnÙÚTìqm‡V„Ӡñ”êüý§Z73õufpý:e¹5Ø®F-Ó%K_VÌ"vå諜¯ãµ›ÇìA8;Ö}†<ÝàúÀùïÁ2ùÄOÓºœôËM†íss[ß‹ÈÐQ!‘I P]ä?éLÓX¹a¡mÅ×8“|¨’…º¢æ‘yßG³hÙ)°Ùž°ijØEdh )êVÿWEH\è'* QFýÈÞS§¹ÛαÆ#¸ÅV²AMÃ68ÇVó·IQz  Iv–ŽQ†.,EšÁ¬ywãòu:óºÆe̯`£JK×Ö"n¾£rÝÓš1õ.õoz/Ëd¢¡HÄ´%޾2ÏÖÙüÖXt¹Å­Üo§(õ‘)UjMbN¥áÝK ¹o£(¿sLÄÆ´‘ÎPÑ¡’4.ˆ†ÑÒÛråݼJ‡¯2nëìÜÓšõ(Ó¡(¾bÒ\®{–ŒiïH¿Q'ø£—š¿$#×OÔÐç6èú@ƒŽ¦ü$sª¦op¦p³Fc-‹w'Xù£c®_³øTÉ›½²ÛO{¤¡/ó()ªJ;Wk%˜ä5rhHÅðÀê9¥ðæ]µ][Ùf‹º\UxÁ õF¾í‘£èS{SµÜX)Åx|OOæ”Kÿ0“‡ŽÇÿÏQéÑ*Z¤0óØsnªEN…Wu UqŒP‘R_B\”8ÅC×Â!zç½ÂÌÕDήŸVs«OUŒQAæÑšhB»&ùÓzƒ"›{ìI¥²+b šO.ÕÂ(WbªYg)£MšèÇŽqç&¡åÕ¥¤WwÊ6U7ËlžE¬¯›le¼´¦Õ¯õÁZ•ìõïðF¥–G÷`Û¶ÜÖ>·¸ë§‡Î„KJEK <(eÏ"aqówÜtSú^šh€ƒªÆ_JؽPÄkû]^Ÿluç¡ÙÎéä›9ªkÇQ?¥üw5Ãæ“Úˆ åÁiR=4Óà$•ÃöÙ”,K®Ð• µè“Ï2a34LŒKÀÚ/}ÍæD¾£x’o£òvÕ~¿©™R ²îD ›“Ä¢—r“Woú°ÂR*o3ʼnÆÑÒ7¸y·Ìòª(¬ró¥µ² Í'ù¼â§ëh Ôý¢Xï°Š’GücKN ùöi‚‘°ì;0‘îQ嘰8'£Ùq㤌‚_á„n^Æ&ûÈ©íl17-Å•6)·xƒòg½-J`óÅ4Ûg ®‚Õ׌þ™Gc’"bÑôÜ‹©VÓ±¼Õ™å®Œpå[Å*gl:Ü=.Ÿ G8n_µ¿îf~½õ}•ë³îË9P²Û­ï\ „Zý³¤ ¯eÜ4'-™sà\Itá£ÆßøÝ!ªY¶V¦êÑW… "wËÿ@={ÏÆaX÷Î&/Ô-hóZ ×FmŒ×þòèM ûnÕÇ¢ièíÅCÄ.ä{@•E÷„‡êI1CÆl˪”zÏ¥ªR-A8Cj‹;¬´&d5FÕ÷“ŒæQó(„íÉñK«Òû¼ü(Åj.ÿ“Ï7tç`tMŠ«ƒK[„TÓæÛ"ЦiµË&‡ÝÄ£2pÓ®”ñ„~ª7ÊÑ>6~çÔô{º§ÁÔsðô÷ÿ¯Æ¹v‹+ê v<³ÔBþßW㌠œ Œìl Œœií=rTìEÿùž©Eg‡1q^Æ+óT({ì‹W*† .ž;sŒÀ˜jy­íè„Í“ÿý˜®‚Yã'hâÆ?Ìz?ͰíQ›Ý‡EÇ´z^äÿÙüUD™›—“ïXÕ¸‚ãÜCϨÆÖv¥ó­ ožhAµ<c5q ÑÌl_óò¥70!oC:Ò”rµÖ¿N§ÌÚgEý™ÕAüêbÑYèµVS±žµàñÚ‚óyƒö˜ýg¿Îì7ÿFîÛ&†x¸_ïn?¾fIÝèÓ£ëÂÞ†&Ò} ~ÐÞ}Ø{o••Áòœ†¾#Š12e°@R;‰¢OG—Ó$¡ð‰?!c ]ñÅ’$¦†š…íÈ+¦‹ŸlëÇdDt{ºY¡Û°ÚYߟš •âe/}‹ÇWÏa@¶LØ X„“æš#%©ê£ÊAm°·1oœ«—°Œš-TèöÄe]ÿ1x["Ty 9ÔŽ>!×áv¡íäi ükæ…V$’¥ͤ6â£0¾¦ž^¨A@!Õ<ÛR¿¸,°g,iP*3”&Ï@«°/™ìX Îå#öЇÓ6‚ž4)…T‰Q¬Rqì Å ¢$Z~ê£w½ÑÚÉ^z˜¨GÀ ”¾¨‚ÒÞG4eª(ΚŒºh^4`RÚºÌFÙl˶˜³ñ|¢.(‹b6Y»7^÷‹ÿYÛÉöC‡0UwŽ«jpŸÿ«êYÓŠÕq¨Ç ^ÓÇï®ua:Pôq5@Ÿ%Oò¬»@Ú̳aúÑÇ|U´Ž÷‹²ãpÇeÔ¬÷©t»ô–}pÊô]ûUUÿ(¤dÂKŠ>v±UF(»™;Çù…8#ÛDyæ¯æÄ®uýyê%ý3 àѹ÷¿ø!†yþ=8ÿAÙÿkè1jQqÄùw«qs9öç8I Š¯² JU UŽ :ŒŒ6)RåHß>léF­ª{3ÙU$[}ÚÀ¶?.t—í¥ëîÿë©Ê*©E-¥ÄÔÃóçýãRGåG7ûó1¶s'Dɇòë3ØgØ/4¬'¹À¿ÐKÒ"!ôõ¢³éFQÿz.¯p¼('l&¼w­Fñ(¾6¬ÉÓ¶JQiа—_N›Ä‰É˜!¶¼³Š‡þ7”—z˜¡q|PÅͤ‘N’wæ‘z—ÊjNêžXB /苉Š|É Ÿ °ºgs 1%@ΕÌ~.;6=a/Rœ¶zåMž{?3"±|+ê`КȎ³‡ãw_þtn õæy³?B¾fëÉÉrCÓØŒÃP[?ÜÂÂýxį’‘’Vï71 7âwƒÍçk‹­ '4œñÐ €$‚ÔS<ùy£þj´~.@& 5ÅÃÉñÜCÕa?á=Zºäaæê HC>9¶¦q³xQ—æ¤ýp=;$Iq~ÝhGc"½ªóèeŒ ²ëƘ1ø&&B‘¤‚`ŠÃéT¾ŽÄVælD¤ÄpÖ49‚*·EqêÁÝÃg3*âD&åâP¦•T`‹Êg\Dléêòô<МaZêñ¹Û|2à Ë+Îà“~ÔnxD( T\€Ê7œxd j‚U ‹ ïbз–wMâ¥:íºƒæA¿.üÎ!ñ¨©6Uiða÷ö'³Zš€L_2móË…!¦(O¡¤ab¸ònFý‰»9ö:ý9[`RZ[,¸M†2œîéìˆãL`´ªXÒóŒÛQ**æ”ü'(I‘jÖàÊò°_»W¦¼ Tš ‹´Kö(ˤfH¤_„‹å*µ ä%Øê…ñZòC€ºo®9û‹HK_ƒ{ˆî’¹|\ÞÉ­\~ÔwÍÑí¡Ö|‘šj†}¹Í̦!’%23iõ¢Ô=ðT·TÄì•ËV²%&ãKYÁ×¥iÎÇk½˜þm“á]ÈÎ€Ú ÿ¢z!¾uG½ ‚M†wõt GVè#Ý #•© d±Ð£‘ÒDôÌõ8¨ fK?.u“€#ü#EôÏúj£ï†Ô›#µQo3Øê·Gz²ÔEtgó ðo›LVá`À2ɬL$%®  M;´Ýh×òľ,ˆhZn½jûÝk¢F%ßÕüÆj‚ÒopÊke¡xî8[´€\§òÔ’dáiýóáµí.TŲºÍ\+g[ýý_­žLFîœÿVOcs#ûÿ±zrý8m±6ö}Õõ'‘¡U*K¤êˆEp•­Oª, Ï&e2Ö† %ħº›hÜlÿÜØ¹&HPHÔÉÕà$ôÓ¾~ûÃo„„·6ô©—V¯]ä6À÷bE O+¡‡!pUÐ   ½îºy¤ÛÇÕÅQë8²Fc£ý*ѼRÅ!¥ÏzÞÀ5Ù Á²dÖ©VoLn4Ê[çØóYß‚g?¥‰Q< Fª ¿Ëž;¶>,‘ÐÅ7Ž˜…N+‚›rÑó–uðÆqP Dç™dŠ͘.¹†¯·†£‰d§c÷àéE×–¥ñÓxCçSŸþi~1Ôsn1’yj›)ƒÍ†F¤sD¥©SÆïÊ\—ù¶›3$oŒFpè?|Çülp²§(€s_!ñkËŽcQ`Éa]ߨs涌ݼŸž]ɱ]»²gÀÔÙŽQtåÖG]UM=̼Båî6¬˜½N½_6ް‡C¥È)D?'ðF&x‘7Qœ:@JÞ~:íöŽljØ·þ‹ã† ÀA}p?!+ŸÚ¯ûN®&¨`\!½)^G|uÈ¢€‘ÕÃgîŠÕWD¬·íz:ÜGr˜ÌfwmÝo´‹°ÇsÏä)ùDû×Wg„û~ð »Äº>)|ÃOÊ›Dÿq£µR;±¸8/޲8U‚s¨~ Ž4Áùc: ¤Õ»Œ”È A¼à×ÑÙÛÓh7–´àÌüáÂÜ0†×(5™C-õkìu¥²<Ù÷¢yÝÜèñrÈîÀ šj1×J!ׯ¸´.BcH—U7À†{ÑWïqðfì›~¨Ìw¹{6žo¼ü ¬ ¯N½jþEMrΉ÷OV4wƒ‡Ç*·­«GíæöBvïõüÒô1#§élgVãí}çþúc` `ìŒØžÙˆnÀWkùÉ'¬¸9¯ À|Z×ÊT¯=‹²\´HàNaÙ“Ö¯÷}<˜9X÷$‡C¹ß—ØßêTìýv ^‚kŒ_/ž}ëUˆ¯Å†ê`@›&Ï{šúD› !=‘Îq¶Vx|f $éV¸½“™½]m'>†šz&wdðA›Làú.ŸbF ÷{Ü•ÂÚìˆC)YŽO)?«@1]“ ¯ãßÁ¶^¾5`L\¿2ÚùOâ%PÌSx)·ïpyÊ”7\¤¡ÔÍ%½8 ~1H3dÃL~¦ât‘C#¦¡| ò42˜³<"T<þ ,;•Õèé±"ä¦&iu 0)@Á2œG'ÂÃïÄΠ6T—%m;þ;æ:Ì#àzâÈïzGîW>õL禹V†’å³mjºô‰·}åY2Dc™lÖ¯—í¤Pèt+ÞlÈ ?çB‹âVL)°#${\µmYÖŠL·È¯¿„*ÆV¾1îNZZÃe7‚8­ŠÓÀÒî£z'M³ŠËW²ë„’t úU¢’§K1‘2¨P$Ëtæ`:ÒðöC«4í!?4j彃â´Z8IÓ¯à›:êÀŸEwi‹³ôä¤ò÷§C§k ¶Ç6ÿæ«ÿ>~ué‚’7ŠÍ «Y»*G|zd(??XRÝ‹ØÙ^ k·G…J“m“¼¦Ô®ÄÔ¤·a%ƈ…ÕÉ€íPJ[®$#>å°éñd©´/% Ó¥"ô ?Dpàä¶¼J¤l|eä„x&ʺ£o9ª¥k1¬‘¬Ïçz#sá$¶Ð’ÌÁdz¯ÂÝû¾‚J@h~<\Íhм§¢°OšMEªaòš×¾]7ë«Cõo5E t÷b¡$(‹3ì:ÞN`;ç÷²Uì3-Œv¦ý}ÖtÓ–¿ÀSòqí)ñÄ”šÕÍ:E† œ'ËC·RèجX.g|û’JC[UÙæàºa¢¹/À•Sý@а‹âÝ 1#FgUvËÌ3ÖØtZÕ(7Ò ]q“v$ÅÈ6G¡ £qy òX{@ŸƒšujK5bZ%ø¯+ ðåx7©dî#ÌÞm½%’øuÉUEsŠnq¥·×’sm YÕa Ñ„\á ~) ßæˆ¿™ÏvÈÆ(ÿ;5ei°7œÎ@’p™ðó•D"Wïm¡™ÄÔÜDC¶_¢¹Á`iO×8³™Í1òUþþ$2ÓZ™ÂÒMüßReîr詤۸¸¡¯€+ÞAC@x¦° ˆf ;õ³‹‹’ä”#![®"aR(pg.ÁzŽ=qâ }nÔÕÖ°MÇ”çê*Kó!SÂÕ#A˜gw0}‹õÌÿJGùçSg÷ø©ÂnðÅmɯxÿÁa‡†\t/òï‘ x¼P´äÑzºä»þ¦™­ßAJm0TÊz÷Í4À|IÖv»i;©º2¸H®àÇPÂ.ÖN(Ô>-K†=ÈŠ-èäcy¢h4«u¯ÔË´Äbá1á˜ß¨†0ìà-¾Ð)™stúa©-H™Ö;vHURL(-òªV¸§ç¹Ás(ÐŒ'?] «{t5X2i~-óŸ{4 ¿jÃ&‰Z šFkšÕiÍOË %¹· àäHa“<¿ŽLÉQ~e’ù<Ž3â¿WÂ’ü¦ÈÏã:5¨OZè+h‹á-­N›<ª Â;ê$ºëº&k÷̯áatk ûsõÝ\Ž;ºwc»ww×GÇŸ¥;7ÜW=–\ÏÊX6'µ NФ§NäÙåjI.}ÅŽü“…ó7kTaÇÚ$WÜ*»;f¬¤c:9ד*i²—ü•¡¯ (”*%І” ãàýhý7MhÕbrÞÐøm÷%šP¢,F:A°D¯Óe5¥‡Íkó;i_¦3ß5Æ#ÔHcЯFI?1?à¾ÏH\¬€ŸýÔR^žÀθoëRI¤3Ò}õhgÕY|¡ØW“í¦zš$žY`÷Ylg)(UxëOvgÌ#Åà “c‘5?uñ«t«tß ×Å7·vêüÆÅ)v§$sh¯|g^ËV™‹d•Æ'î… ew&d¡CŸ Ö}&L‡ñÕHô}†ÕùÕõ>úù}fe} “`õßR-à嬹¯$¿‹`½r‰=J8w¼=ñoè ÛEfkTÔß*ßËΞ6¸™ßm¬¯ÞÌ#é-­Õø[Lïâ–¬Ðÿcÿ+ l ƒÛ¶mÛ¶mÛ¶ñlÛ¶mÛ¶mÛ¶æ=É$óݛ̙ùÕéêþÑéN¥ªV¯ªÇJG» ᢠ²ØS Ž>·Á>ŸdTÒB<…M]Y‚Ö¯X¹ªUYÆ‰È 7 ÚÝã~½¤"À÷ —Æ‹i–`òÀÈG ügoRU©LTÚÔmª¢tѼA«.îç±Øjç¤ÚiËô&ÌY™H|‰R!2xFr'á\NŸšfhýƒG%)Û7Ó€¯d›ðÉ̯œG^Ç\¸L9×°PÞQ¸pÕGõš´Ví?¿¢6Ý„+$~!Í„H^!TG áF¹|¢?9g(¹ÓEIášÅíÐMÐÜ+¡Ë^,1<­Cb5>e–{{C_IsbåIWGf7¢D–àœìa;Î41Ä\Þãok â98£Õ”Î-šzÐaö§:óà½à駯Ûbbì3Hý§C·NO²ÔªÈ@›œT]ýhÚXLåDy!E÷/2¥méBL)J¥ƒxL‰sM“Šõ&WɤÇLÌîs_aœ}Š|‚GÚgàýC¤ý‹í|•V°¤Öàž|µDÁx)çDÇQh|„! +uÞú=¦$?äþúnå²4)V¶†Þ·žö—Ãmšfú4†ß ØÞʯëkƒ}Fõ -Ôà´}g;lOcÞ—£OOªì±ñ“DAV$¿´pÿè¤$ÊøÔ@eSŽm´OYf>3IUY¨ØŒeÿ¸3”‡p8F%y&—uTª1Ò_i®¡ ’¤ÜLMsæáÛÞ¤mãЭªpîÛÉ4û¾Éòò™"4µ® î|tÕiÜ!å]¥/} …PP¾ŽJ…¹q[ô¯xVF3¨ëͿȫÙÍú1]2–L,>A3×ìËa+< ¸äaS6CÖ߯\ðÕòŸ«3˜~±ºÇ·k\¬¯?c~J]=YÑßKæ®(>ïß´/ÕÑ×Fo7{‹øh/)«‹-èWiÝ`_YúWXJS•ßhþ×TܺU\Vx¢Ö. ¨M–€ñÑy­ßß&À¿ÇÀ ÙsAR\'õ`=§µ®:¸yHwwÚ/:´ޱ@€@8.yó¹°Êîm½Êwºòdq©L.bÿ¹E— -)¾(Whñø-ÞxåK·šŒë€ìûþ 8tWàc#øâã&n÷e1¾4¤Çd'­¼Òž¨jó‘;¹¯R¦xPí‚þ6¡â»| ²õÿ,`1ÐA,¸åÕyøÙA÷“ß[]WÅQ½£jo˜üëk >4ŽÞ` .ù²1 nÞwW'|X–ãçÆ>$®d ú8Ïùûš#!¥Í’?à«~9™Kq&÷ª]¦Öa·»\G¸åƶA’ NgÛÄìºjùííacÑF÷þ·hˆi€A©Pµ^¬CÔ±@P#Ávr& ¶ÄO$}ÍÍAé¾°Œ>B5 Ó&Ŭî8Ó†¿)敌±`s‰«Mry°¯Ñ¨o¢Oügèó¯Wl™ÏMñfŽ«õg|C„C¿º]i–t-G/2\C)«É{ƒ4;öüÉI ÕNÌYJŒ%òjokyuÀž+€½KºΊ=[‚Jw<®¡oÍ«”Ö}lþwóç–1bÓ…bšŽª/ h (6Ñ­Àá%´05´áÕ´ ÈÒ‚° Â(IóNÁ ®N·l;—Ý¢0š6JÈ8Œ›Ìù‚Þ.®A/MZ«âœWî¤rZL8ßðçw$4)ãÏìÞ¶Ÿ°§;t§~­%cGò­"‹zÒè-ùÏ¿üÿ'F·mþ†Dû¿’Õÿß q–¶ýÿÕos¹Í9º%ŠM$ÕîFg‘àd%[¯_œHk(ZQµ“[ÅÙLÉÄì;9mK½—w†¢xu@®âßäñœ=wúA!€0 EwŸ  gȲҎÿ•Uz™QxöR$–TÞV*§9ÍV´þ)~•©ïþ(j—û¡ýôHû]4Ë÷…žC5ä¬Á6ØžwR¶Êê¶ ·–\>4S䦻Eä¥j£õD®×BÛJ»Tª#o±nÐ…ß&Ÿ¯Œ¸é¶Fn!ôX6ðÒzÉ6ì ô`6ð{5y«=Ÿ¿fŸÏDÝx.p“·Î-`d^tßO¾Y@Ö…(^/RUy<– äÙé#øÆèë£ü¢¨ë!ùæP§ïíOšÈ;ŸDÞgéxj¾3ëtDÚ¶¤š]è þ•øeI»pU)5ô®ÝÚ“Hf‹³Rq žŽ^@w=nð âÛñmoº´Ýrc×Àž%ÁŽõüYTD§¡R½¯á?Ê(°ï¦[ôd°–¯ÄzûÐCnçÑHxÖ²HoÓ¸ ÏÕw .¿m~ÝÊ­;Ã÷PXÍ(†Êþ•^}ø:ï¶ÿÎÆ°ïi{“›ó¤zÁ£xjþ³£ÕëÛ¼NÞ¼×;v24ýô¡r3\ëk= ³l]„-X¸¿ë¾éõyÒš¿ÐyâÃ'äáLÖ-êy><Ï”'QÿÛˆæâhÞáå£&°y<đɫC»=î³¶pŽiZ&õèœ}n1Ww³>×6¸L°wž…öeÈŸ¥ài –#s¾ÿ°»‘ɨ(}\œ&_ddy)A‰ìë<ÊU£ú£q´Bª\fwf­ÌŽøqäãPŸ…@,™žèTa¼lÔO¦Ä¯9Ú†«Ð¨ÉaDeÆáÄü<Ó ÿS ‚±·¡Hd/º‰wñq. p< ÕÖ—â?Œ â’YñDþûNÉý±ÖåÓVB¬Åt^©Ö3ÓÖ4&T[N±ÂzÛÏ™DÀ9ÜâxÄa¦h XpÃDØn;º¼ö’q0Éf+þQL[›z2+Û¢ÊâkÆ ´¿çèkòN'a®î’TæMÓø€ª{ôòÑæ«í}FwŽŠÝØYq9òšÝÌ(Ôkš°›MI÷(tOh=±Ÿ9ò¬Ë(3˜[E\±.;›½ÿJ½ŠÙ ?ò¯¶³²8hñé1š¬üLœ!½Nû¸è‹Ú+u9p]æ¦p˜Û°Ï!&ሞÜ:·Lè¹Üüq,J‡žÍZú:œÎI'cŠÜúýCæX ޵Æa%uóšÜX•?€9 ßÝBeö¿ñüãaR 4Ä–4ê‹ëè1ÑgÒÊu ó4“Bf”ül,¸:Ê$±æ?Ð~¯‹œÆôDµ2 ·¢~*%ðòk TA‡ˆ Ä•4CŽ©ºÜüfçFoú{züu"NUR‚@‚+fø´™q­äð¸žÃ[¸É™‘:鷂ʱbqh-xÓ?¨]°žœõý‚9á&’ÎýÆÑ×a»]šýŸ¡äµ~m—%‘dóÞFÝŸ¸Iúóléþ8ÏrÓ (¢Œ`™9Õée&©¤oì üh5;0„“ ã¶UùÞaFî ¯E;¤üKMØ$öI¶u0²Ñ:úR{o* N¤Y5’Øf¾HQ.YJPœêa&®dÛû›`âNÐYHäX–Çó"‹ÚV’^æR š? *ÛÃÿ"ûÛ!°æ~lD…´ô9šf\BgRÄZ˜K^ÞHëuCd«,™Ó 1ý…²ÅÂ4*e˜xع›¦ýpìÒ;{d%u@sK~Œ´I`¡ò“{‰g‘š¹èÞñƒÐ( ª§±ŸN§FM¢V£g2¾Ä™1ø¶ &¢äÕŽ(ÌL5Å,8ô2üÛú+Õ²Q@2L*á¡/@6#}ƒÄÇüÄÿ‹¿Î"¤‡ˆÜ— ž›ïƒD.7L› ÐÑZ)ãð®!÷hmé÷žé,¹ýÉÑcÅTï¡Ð‰_’<?^;wÞ¬Ž(*×–Ú{ä½ ˆp¾qH^%‘\þ%¼â&4yc‚š¹÷–lÌL%gj›ÑHïsê¾)™¯á“à]^9…—em÷£˜0Ÿf8ñnGH\À º˜ðH‰ú¹~VÅód3›•tÃÒ"âcÎ0 ï=oÇ) ´@LXÊ‚°F w,^ 4E¼§V–A.J"]‹}D³â©ÎÏPÚ¡êN蟇¹¦³£õ£«Pïd›ªÖeIÊI%õød1{Úg–T¬ñsóIGµEJè17z¢VÿˆtÎ9{HÏìµ,äóÌáÏ"‰³LÍu)>ÐôŸw#ÙôÇ$ÄVË28©¾‚mOvÙ—Ö Ì>–¹Bn z œÓ)=ª¥¢9ëf¤¸E™OZÆmp)Õò¤^yÚbNÍX̬E€rJ+ùOJü)•ïFw… õ5¶[FgЛ þL¦LL¤°tè[9©ÊÖ²HÖºDê܈—:à8áŸ÷°°p~P‘—een?‡ã|]“}]ëC¤¤'8™ÌâšZC4Ž5ô`ãlf­M˜/ƒÞtFÍ^”MöPcù›üÔƒ”_èáÜ…j‹DâlyW±bLo-;v”–¶Ëš{/§2RKH?ÄôA8ôçÛR´>r<.[ÙîéÿFìzÚ×ñëCda¤6×yì·Ü°6’ADö:î²}ó—š4ŸVb‹,n/álÖg*‡·"—1ïBÇâĪc|¢´¬$3NSAÍ^‘0g\›íì|{³üRûpÇP*¿7èÐä–y%ÅÛ-M[§Ö¶4¨i´ðh¾ñÓäOÈ”,—3(ø§»“\ï÷c¤æ€3ŒbJýÄq8‹öIôžÛû…Ê"Î1˜‰"5²{š–QnvçósTôí·ª£ßv{+™V+¢î‘j*Æà©Õ—&eb<’²JÍé¾+ðaª/·ï s(éDg)¹vc3!¤…*©ÜáGÖ(_µ‰<¦¼êÌ9*$4ãÞÚ3û¨žkL¼2qïî2BÈø´Š7¸,O¼ªIr‡ò‡·¯õLs­÷ˆ÷gÙep¬„fÀº’\òÑáœ#(ˆ‰¦è|¸ñ™©Ðç›W¥Ó}Ûc‘û°N=£àŸ%ÎÐ „éÖ¯Ý%q5Œ% N±K2©3OʰÚ|ˆú¾$7-ñÒï”qð;¹ Am—‘çÞ‡˜ òÁFF? cú¼óAjz|ýÁdÖ4rŠóõ‰p1Ù/G‹F~Ÿž—•Nb¥Ñ#þæ%l2üq Ó9&0‚t” º&„}"@­ÿE'Ô)åy,âŽû*Ý"½$Ejùö«îÍWà$ƹºþº´³— |˜u=óšm•¶€p„•û¿Í"£HDrmQôÛ™á s¥Nñ u1ëŸåÚbBBÂ66ü¡Ocb–î5G;rÑ®¦eÃ?g °QÐÒAïã;žœc0A÷¯õ-‚URÀª@ê„d~•‹®\‰÷Ü«d4Räuƒ…çW—{ÄJeØZ …·~zó–ò\Biz üDÛª„xÿtuÞá6v`9åžM½"üt0Å¿†Ši•ujz%b »±À;I{O¢ý{d=’åí…ËÈÅé+Þcíu\Ú&(o~×åz5gŸèot%M!*Ú:¤ñøLP< f3LòÓçÈ~ÄéÒWPNtÜvÞr4qÓ6“äL Ÿ!ÜO«yP·×/ µŒÔ>}Èú—~“€J^Äk9¼Çiõ±vE0THу-aö¶øÔ2þyüÂèEc ð=èWˆŒ"é«ÛÏ>ŽÂcâÈ}{ú…‹Ÿ W4[óS¿'¾éfóSeõS†fg|P”eÜÁíQ³”ìüwOŒò¾+äÖ[ã?½Ûè“w:jô%cÛñ“(Ð*’P:@*P¡s¸¬ñ‹¹¨Srm‚q9JúÙe sÚh¥2|p³ mGîG‡Q‘¤ûþ?üçôž_‡Äì©<@Ãnî‹ö!1Å„EJ¦§2ªÎ„ÍŠ*æ 5/€7F/‰­z¦=é´ë¬ÊÍgm›‰J$~{¿âsÂ÷!½#GðÌ0ŒL:‹üG]ç-wæ”[ïWÑäg˜ßµ ·öÐû¤– ÞŽ|X|üÃÆ±áÅ×·ÅÔ‹61„º­–´ÔãsôÉaFâ>}x¯|ð÷Q8q²„^c`©7Eωþ•×ò¶ý•…‡i؆·û5¯ë·§éݰÿ[$>;4¿·†?§,É­r§?àj×éžf}õ¶ªõl~t´:ºùÙ9ÄgOý“©¸¡´ÂG‰$¥ÇÿèŽÿxnÐä…kÝî†NáqÐV~à°84ÇösIÀ šëÙÖ­t ÍRd§]‚v1?t!+û»ÛhÕ” 'ªÚJö£wàâÎ|xNRߥ˜4Và©W„O"Zß°ŽÊ/ªp¨ô‡(úŸà¶×k–ÑÕy&žB"°Þ©MÇ+Ð}Q¿ ) š=ï’6H±Jb~y0/uSîëH RöqTú|Œ6É]ÑFd™òQÓʲŸø•öqéqøUwW6ö­J¦—s¡òÂד9jÌ_!jÚÏtųxT »hWK—ÁI0Ú†%o­”Wqžu¡"ßÙ¨Ã8Š • ¥!šþWU]éT^ßÉ7oM7Sô©à(Ï7$ÔúB„Ä«q¶¨Š_šm”•]’›°¾^¯·"¹ƒœ(Í(*í¦^¢Ç’Áo«‚åòY/Q’Òœ1‹§ÌMNÁòu >Ö§Ô‡6l“–&ô¨)²»W”Yż@U·A5rJã´3”ŒÄ¤ý[d‘=ÒarÊ?¬û£]Mò‡{ÈÅ<Œ±8éõgÿCg”^uL€?J-êÄÕÈÏb ’<†#¦¯+qÏ%€ €_Ôæ“·Gï!õ¾^l®j¦[»_­¦} ·6'DãŒCi¼§îC‡õJ­XU÷\°›hæül|Ì 1d8M®÷sŸÀjÕ—©2Ð{+^úgÓ29‡s˜¿štš+ƒƒyª}º=¶*iUgUwøÿ k¿ò^®"¾¼ÌÀ €äÿ;†eêädïäìbèbJçàY¢¤-Ä€â{Ww¾E gBöi0,A´DjÂÍËaZ%Ô•Šaü}‡vÚNÿ?t­0ç^mxÊpò„ÂÈ”¦ô+/(iîB̨„Ç TWó°¼ûUw]Â×,Æb³å.fê§F Ë)wqD^þe+\$s¿§Ž $Îîö@w(0QÅÃxþþ-‹§°RƦ»ÕòBÊ2G2ˆF™%ÊÕ Å§Nrb_„òM\ ¶eÊE·64PµÏÑ;?:±Ò¨'Þ;ZPrж TÞ´ª©nã,ÚKØ%<µ³T*Ó#]†±\Î÷Õˆ`T—ÔŒA$PënÒde,dÔìH ªcOIÍŒ~ÿùþíÛ^á*¸ÏŽ¿Š‰ She “¿y#xC]Áe‹ÑTIü¥¹)ݳã†É‡FÞ0Fïe¥7ìG £¿o¶^Sö²„Å9Vy¡ñÝÔ=†åjTøó(‰±Ú¿Â$à.çÂÙŽâº@²Dôþ_4ÙüÆOQ)`XÒÿ?ŸÏ¸EEÖ~K !GWªñdÓ5@Ì[*JÇ¥"%(MKÃödÕŸúCx lŠ4%•šÅ2Gù1î xÓSØgYÒ»ÉÔ™ák Ãó–üÓÚþìüý^UhJxŽ‘…šò:ùDÝU—BIN±’2æ·ZvÊT8uáÆ4h‘¬ƒL÷Á,jÙ~±VªÍbyãfcïûÊâ(ÕGgÜ }sÝÜõ !ŒVïD³¢vòõA yŒÛæs7ÍwX} 0Є~ƒChBxÒ›Žˆ 8ÅÒ1S¨LÔÚµHª·CÖlY0W„>èð0¿©CÑEØ$„€ s_ÄA È2½ Wxävá,y˜]¢& ö’jøÃuj–нɫ™I5næ„(«lgŠ¥jð ûÂóçåÊ>oÃ\\—=ÃåˆmwÂŽÇQôœÌ47Mt- *W¯q¶f&’F#êXz@U»MµpŸ–[È? ê4ƒìTTVÑú8„v{þ9¦Uű¦c|9øÌrr6øv?v£¾]"5boO`GþëRëX ZšÚðE|PÌ'Üuo„8Á=:¡IZuÊ¥Š»ok\„e‹ì/>°T1ÜbSCjÄ<¯Ou /—Ír•äG(ÚA™†[›Z&Pn1oøi2 ?ï¹8žëš|Â¥ƒZÖ÷ŒäZh`,O–l¶ë:xW—jc~„¬ªU·Íœ†~ÞWºi ç bIVyq.SÙ/T“ÍÖðL÷ç6æóM71Ì™ölpOAl­0ÝÑ=É‹6Ž™!¥GÀã÷éb‹Þ“EBxðoæý-W\´ó@HŽ2ª9‘þ¼òÙç27LV^•ÂóR7* ÂÕÜÓJáMYdm~?bSÕ»Z¼¬‡jh“h6§…©­ffÊ…q é5†“Ùå6æëuì×`ÛØ{‘?V=¯_Àôiƒj³…¾.Þ~z ÆþîS!BÊ®bG­kÅ®Ù=U?”‚PŸà>r‰°,èþ=h)oNÌøkjÿ»l©€÷"nVV_„†¾Œ2ñCÒ•ÞêR¿ð ¼ãOôàÿ$}’çaÂïâ\úþ¯ÿ¦nÿMþÓÃÞç­ÖÄÿa}RÞ*Q1Ëe–Æ`ŸåOEœÚPO\\I;ád“ô*Çæ_·[þ½mÁûƒôÇ gte,5ÃAÒ¸¡GÖkÐôƒñkê•ØäFoÅù!Žä‘Qe˜Å'Ð%-O ’ìzÔ?à„ôF§À SÁ™P5v xKÏ…ù¹Èéw'U9< o é‚œŸWËÝ©÷;¤Tv!9&F¤$ú©a‰)Ïõê˜E‡äѤ*žô¢¤²‘3aESø®Q$>M[Àï§Ý³µ§!½g™o8-MMI›åäðFb(ðraÝÂËѤGf$¨H?ö©B©òö!í"½?ô8ï3=Ò‰&BÙ§JO!¿1Á61ä¾€N^/J“@ ¥”|PQCN9€»¶ èèÑpy3â¸Wõ<8lƒãôÁ’Û‹™#?lí^\ìN˜p`D-T¬ãçàÀ-@vãFö$íøÁÉÃ} –ûGwË$ø0‰0N˜²¥6÷³†.+}°<è=؜̧“¢3ÅpQ.‰–×3¢Ö£¤RècZUªO^ä4®'{x§ÌHÂæþø—ŒX@ P!ÅåÜü$§K/ÂlJúÂ1?½ÒƒF–æ!›æ¦&¤A?Á­Ê_ROžV<ísËC sê¤zõò¼}Ëc/±h†”µÎoœöì_Æ©R1èù€% gcuH½“çBÉC~¿aÍSdÌðLËË´ƒzÀëë‰Ot S%å´¶2¼a_;uï9[fû0b噊Ð['ȨccÃP+Ì´ì0ÀûÍÈdLo›fú¯š¨ÌcJÝY/Zêl†s˜ÚȈ96=KùÈDH’Rv¿(Ù/Ô‘¼jÜ,3ÂÀ@×ÑóÙì§?x‘ªŽÊg„þ*2)ƒãü›¨| .ܯ‘)ûš š5± ª®ÁXåQÆÁb, Ï/2Yƒ ™ä[&ÐôªH´‹÷éG×CyÆ7½1倫ñ*s‰±û»~vq`r܃•c€{y îY»o£?Ç3¹ó˜Gz8vQñ˜ºxûYvXàû2çî‹oIe¯§yH‡Dhd…ØÓÅíãØZÇèn6~3ðàZÏ‹–óCï*¶*âÂÆ¦;é;ZK—™i&6²'tï³ù[AŸpz›2~w,¢ŠßýyÑ D‹ìÒ÷ý®jbÄÄOZÚc­l++r ¨àæHžËíóûãßóêtŸZ]úÉkmÓýæßñjíù¹v¸;3:ãÿSôƒ2¦Fþƒ{s{]‚¥þ³xî”ú»¿wøzx2ÓXú©) êª9óöæ·ýù~j2—ÏÅÑãå½VQÐ8÷ÛÍOøiëˆ~qÁõdÎ4‰V6Hê7qaŒðôaDg¹<`X¿‰À8• I:á”™ôJ‚ñEeh$Uáº-ô”æäË|æœEAÐ!>˜dâp#'f7ß8,%^æÑ\\”ä`nIÞB ë…ùóþsŸÕ˜ Óùb”òõöö6¶&8vð`ÍñŸ€‚"Èš-£Ê8 uåòà}(Fjq…äíȲsûû} è*ö§çO?ìéó€ñÛº¶µq‚:2Çi'à?1‹È‚wÛÙ…ƒU¨ì’ÅÁÆÆö=@¼ßÚ]À£d™î­®×ñš›„N–+‘R« ‡zd-¡ì"QG”)@Ä ›—€Ð•l"×b…Iö]C` –1L)FÚfÈ%CƒéM}Œ›ßTü2€ªBjTê¤KIxn2:UÑ—®Î)ÈiìP \JŽ);‰ãûr®ß$I«$µöHŸJœ^Iís¼ Èöª¢jˆ†r‘½(f’ªÅËs¡šJKüP,#O{ yše¤ÕKö˜4Ÿ]àõ[‹äZ™nÉÇå‹&°($’àíÒg„Ì”nd¬ÎøxZ‹@1ì$™¤Ä‚‚Ù“NסÔçwžÅ¾ÝøçGçsi¶Z•ÃÏ¥ÎÔ“OF“_=ŒÃ6£'I¿Äð]ø„”Ò£Öxl¿¢Cåfç°œ¤} ©èƺÑÉCÓ 5B·Ëáì§¡Z Z‰/ËUÝÞMñ-œj½aQ™°¯ë~aZ.E.ICÍ9Iñ–Öe‡¡`ÉL†Ý›x¤;nW'0¸Ýø"×þÙì{ Ýã0,š¢·U5EäѦ¦ŽÄƬšh  c'’²ÒÅN˜•ÉJ褮æAÉ íšYL½¡#O÷œ;ï3üpúù”!­r°C ã¾°s¼ŸÉ5ÇX°emý"Âù~üM ydP`o§¬Ü®Š©eäöd4ÔÙ*Ö*¹åð“¢d´^;ÿÂXX î²XêjœrÝé­ö¥]?~ÇqîüÜH&MER%õHÝXsó#IaÙ¦pÐY¦ÝlY á×Ì%—ÊÔÚÿiļs]Ðæ¨E?ßK×ÀG{\Ž{´qn‚‰ûÅd8’9º#˜þþK%ªÏY8L¢­û† (4BéÝc-¢<žšù̵Ñ~Ô¨Š£òfТáf»éó`K Ø­OÛ¶\§ƒ.b ÒC%`D†ê“÷´Ì» 9p`hêN0ྡྷD{æ¯Õ‘Xäÿ(›GW—Ñ™­éöåÆmš/—'ý2+8âÞTE:óËsdXz›¢lÔ—ð”趬"zdUç« ¸ë¹Y3ÌÎG_(ƳF¡x"¥Õë5OÔØˆ`YËiš"ØÆs)[GQÜ'ÂïžöÛɨѪ…ÃáÀáL^º€žƒ<~ÃP‡®¡hÒƒ›iR0À¸×¬ ý>< cÀ *B87Ø@͆¾1KÝd.Ï ™Q¿;e½!&‹1›DbE7Á.ÌjkàT)1­¿çÄà 6¸¦}åµ"ZNðàpÛi¿¨„…9˜¿ u¾vwùès3{ûl 4SxŽ´f SGù£´‚Ø ZÒ+;@”U÷êE_˜ß&9ØT.Ó!|Îd ‰1é“(Ž8™Š9½Û,Ix^{A’²™F@±P‘ εÓîŒ>$¾ëÚB×^? §ÓS6/º$Éoízî’þyÅ?Hz“—ë-ÅçMÕžÃQ²Çĵøt1p`éÂ$^`º®ÒÌךD‰ž(ì{àscèÂõau0ô¦8f2´§8pŸ :Aå ò7΂ÚÌLã^@P‡3õgF ­ƒG¼ƒy[9†œ/3*N’ö„:4Ž¥á1xÁò¬® ZÁ™qĪ-‰ÍÓnžïs¯O^Çåµ0¤qiu±0ë^AD…˜¦´?Û!Cáo&ˆ"oŸjj锲„Îù³>«"}­² î˜+_×u‰%`Ì–G¦˜¬sˆ}› “[Ã燼˜GnôêŒP™–u(_¬ ÈC?´otç/‘Î'µOÊ©D$!+ÂÜ.Ü€e&~ü 4==;Í8«~îá…h>?ÙDÛ‚vÂdJqKÎm¦Õ™½ŒÂÎ@ñ)$Nàbnx-JF¼æË¬(b°™Ñüâ6cÅN*ab¹jm;ðB!%ÜÆ ]Ùœ„­Á)s$¨ø€ádyáÁw#GÔ,<¢ô¢QU…SÞõqHªQ_°øìˆ\ÕÏaý *…+TD}}‡^@þ¨K #‹Xï@)1™m…ÊM¯ç6ý²#!¯Ì“¤ ¬Òð±îÒ ¢n•¶j¢sƒ)J a­ê¡§'Df°7uk¿5×ê7½I-®&!Ÿµ‘¶T 9ÍÚhjæñŸÍôÈÜÆ‰qܱ軧hÑ üÇ*\æ°‘az ×¢ƒ…§Æ!r¤sê-ÀêæuIY0ÔÐy §2"¬h¡pý£ãO®Ów¯i,×–;”óÀ´,¬19Ã= C$*unL×Bí5ñ«h2UG é«˜o}¶/VC¾|‰SíMSódqü!¶Ö*’²X!Ç\{•¢AÆ‚W™¯%y’<|ÒÙþÇÅÁ‹›³(wz±sî´t/ŸÌë¥]lág=fr ö…l}U=£ºJOV¸ƒc<“th5Ñ®"ž V4«Å Š×Ò˜Ÿxâê#0øÊw‚s4.Nê4›¥ƒz“°ÚÉQí=”J³‹`м±ƒº`ûæÊj¹ÃÐã¬zùÕ"æhï,Ù¡‹%×DTf&<ÀåA \Z’î”ÆRP×_’t\Yùî 0®[ž1-°IišŒ##’ Ú,ö'%_Jö¯’éáäA?S ÷{…ò Ì0䟥|Ùéh¦i½´ûÔ÷ vô73 “iéÌ¡ªº÷Æ%Kp=ƒÿßzEÅÕ²öX8+µ¢KØJÉ*«ŒqÚÍKYî …Wá"Ãøs–xꩉp¢Å/8W˜¾Ù§'Šñ^d«LCl×¹0™Ö’®pøo‹OÛêø °¡æeZ†ôá\ÖÒ3º­à¼(ù5•ÃȃàöK”†\ÎUàÍÊÍ‹ÍÓQÍ~ïy¯©æåÄ€T×EqÆÕñ¿¯S½¢µ¶Ï•Š•½r;>z¶µN6ïöÁ5w"Ù¦nþQEcÓ¤ËMZúWNÃÍöÐ!õtÿ.ªàÄxtxªrøKø¡±cÌZPƒ_+6dtÐ"ñi<Š ‰©¿p…QS÷ 0v´»TmÕ ^! ¥ ÂÆ3…†óH¶ðÃ@«´ÆµZ¨ ¥s{UÃ¥Þ©W&žQ45z‰aåæ—ØxèøÔ76•<ñžwº° i¬9²eÅóvˆ\4­ˆyGÈj'W‚!™Ì( ÖÈ_XÖt‰ î5o褸ÎS;ø[¨PzS[ºJRgFègœ$k=mÑ( {ƒH…An¦hí¦[lI¥ ~§)é`Âi›ë×Ùa˜•…*6—TÔµYÎs¡Ä½„F©ûCž2V,Þ¸ÇÀÿ”΄SJ©îÔ@´oXaýUL~âà` *Ä¢(gk¸ r°™•Îæ‚%0‘&~ W¿&ú™VI× ÃB¡”»Ê£ñè&ŸŸçüJoJ‚(9EKI—^J ÝóF+yÖ4YgÑ·>ê4b•þö³5•UªØÂõ™Tò2*oC¡9ÕnÞ[è çèÁhµýJ•£orÇH‹ùüÌ*c¸2ô µµ¤!›áy“µr)KXÁ¦Cã%k e\n _¶~MƒL7mcyTܨg)\Ã^Ká•uU à‘°í£CÛØóðÓ¦úž^çÕª¡ïìÖÄ¡œ`(•G–™¼}k ¿¼<€ÀHji&z"NßGÙѦösE¸ërL ‹)Ù'™‘¡<ÂüA¡!U+ô+)gÔcÚc…ƒ~íYU×wøØÖ\)ÕP塃ó3ŸcGæ§/÷–LºßP£ëg™F‘"õŒê#zÚáÜÄŒ¾¶LÕ‰›•ؤäÍXš³ÍÐY¨ÊñžSVè]¢³¹LØ5 Í#Ðb[¿Ð·)%üú^ä‹#?¥bßx ‰8+Á»†(Ó¢Vì÷&tÜ/LICä$hº÷[“ýa­ª˜o#–ñf±¶VꞎZœÀ=ņEWº_Å.íV¯>Hõ”×+u–´†BšeäÓjAþiqVÂq'Y0z‘ƒ2ŠI^g'"_tsçÓfaÉŽ«BÒ]*µf–«æs#@¾ÊýÓ~õAÕêîÇ! ŸÊZu‡o¦±æì¨ºe!<>âM¿ìÃ;¥¤iØ'ÈVó‰. "?Â"{ëÁhkBlÁËokõ ¬v3±z= -Ë\º`®³öÊçļ¥á3”ñȘéÕè_twçþðôî"»½uHÖ‡€ïë GÉ¡$©-#¸¡Ë CÜýpr—HåyUµ³RfTmÀ‚dY‹á|t.w(êr8!›(*{ùTÃêÑW¤ÛC¦w]êØûŒM·ÛÛN“­A.€Á›ÂËÍ–íß¼îöãæÀ… ýÂ!¨zÌ”ÚK¬\Ÿãä»Û‡ä2Ü­Xèù¨Kš"[g ZÊOÞÒ:ѶemþÉ4ÑSüVÀ©j´Ã¾:i¤q¯*Ù½³áõûý6®AÑb×1î4›ÆÁ–w®ç±@•Tv›:£¤âÒÍ¿ñ)¢UIêÎRôßq¿rÒÀÄe鮽\£‚[…ôï¾}/ÛB†¿`áý¶!Õ@žËc~ :çØ0Ú¨ÿ}Âç¨÷‹íx9-2§?N@a¸rðWmkš,Õîs¡ZÌ-E&‚m®.~}úÛ+?ˆ0\š½—K©ÂAo·ävú 9¸:¨t\«öÑ^•Js3=¦Uõí§ŠW<}éîÌK›•Pm¦ÿV¨hƒ¼þù&–ȲxŽÚùÖÃ#—£»iâ=£W¥3Ÿv¾aÛ*Bo)g½b‹»A[YÒU¹š¼!ï94‹‡³Åã(S~úP #DajÒr–ŸéÒH$ý>KzZ *æg“€ •JѰ¯e›Ôî¸' °O‰—¦óÞë̵NÎKfrš¾‚:8–‡- ÀJ6ê¡ZÝR§îÿlµ+÷ó¾ÚRœ·ÛqŠ7Ž[ýöŒp2Þ!XÔ‰·…\²ï².¥_Õ1U†MëËÀ~ÈœJÄqË+¬§ü­@Kþ³-?ÆV’h»÷Yh$ªLÐjìv2^2¼(*xy_§ Z‹V|¹ÊŸwåa1t®ê½qÍLž§W=]¢0zAö9®ù¦ÙÆ$ÄšwÕœÿ± ÄǨÎÙåS¢ ãÇÒ¦†644aàP¨Ž}ôèTtRžÎ«„V—ŠØ-²RDÎŽÅ~Í]x­Xäô³¼v)róÊÖï1oqx_õR/ç¼§íufˆb¦nÎ0ð*+ 8Œ5)/F^7v'™@2 'K€š»·³ÂÄ/ð<ÃeH}C‹;H›•èª/!òǰ,¼8bøm£O|Ô7Øtàôïå ÿÒD½rÛ‘“KäM Ÿ#Ô£rè¬ ôëß.ùëL†ðRÅfîëè`ï†ĺîɳÝ'D*Üâ —TaV]| Vp˜8¬1÷¿O©¾³8¯ŠôØ>€†eÍ~'öžÆo=6ªZLãI{Ê ‡˜ò B'"¬—¾0A’,E,Ì›ð¡bh™cÙ¼û D0Y(j;-ÜÇߢVú Œkbüçéeb±X†að™=XdÊ!Ø[L@õ •Ø ö$ж³e`EëlÁV6Ϭک§íÕüq˜„ÓØq :ß?iä~1êX§Š[k•Zþmf_hãp{Zë 6ë7[+Òðw+r¶MÈf€ÄÐÙ:1&½¾&Ö%æ$rÇï'¿å°Of¦{°};ø=îy&õåNMæ½õêèÑ6&¹uLÕH¹}adƒï^Ãb‰‰˜`Ó  ™E˜`¯D¹ ¯¼#`¤¶zdäQt·£éƸ¥Cë‹g£:Vz ð]³C»{4Јuâ¾ÞW³EðÚI~¾ ³caøÉ‚†Ž¿júSFÃÅYJ˜Ø.Xc¤ñ™¬Òó´v!/+Júâ’ô/g'K'Z=XЬTZÄÅÌCá·I¹³¨Hl]%ÆKk˸7÷ ÏõíÊT6”k_f¬µË ½°®tk¯››lkëBZ— fŒ«fIc ©g§ÄfS@6ƒêNóˆ€\ϸ¢ŸÇaêÙbƒ»$Æ¢aíoLj4ò…âH$É58[ùIó2¢S(g„PýF6cDó+ÀÌTÑ©í!¡tè¥g,«Ã±ØEImÑ”>=i_ÒkuÜžÒH_§"P`Ó›{êó„â\k°A·›À¥m¼p Su¥¨mŽùÿ¸CÏ=Óžñg\§(wr…¯öìeŸi‘d%v§ìiN"Êø¢×T Î‰¦ÙI¿ó¡Ì:ü-íí^ùRe€ôXö ýÍ÷´ë>Êôf¥i|\•œ­K´qËß:z±hµákn]_>lŠòDªD«q þ]S [Κ}PŽƒbÉ]"c8¿^ð—fc[ñXìsRŠ)æÂI £ƒƒîŒ&vÚÞV•\¨€ÊµÙî^•î  ×7 ¿|öÄñ*>è¯Së6)¹®Ñ³zHÔúÈÀ#ð|öWÿšˆjk9¤Äª$œOé=÷6tÑš,ɹòÞ,¦ŒœRu/NÝÀÖœ¦ÿ‡X-’c9œ«Z1‡sóìØíƒì¦ Á$•ÝŸm°ý}‚ÄšÍ'îÃ}„†‚á@†£¢#ÄAìnÌÒï/Š?{FèožÑ6ÕtulÔ×D±Dæ˜O-©œ²yOÌxߣæ_¦z‘fµ=QGÄtÝ’27 p¹ìch•§Ú Ð–8*èÐ#§Jå´ˆÿÀõ>ûâvhß(¦hûzliθÛí¬dhštä^œ¦Ì†Xµzàô$Ôõ8°EÕöéNÑ–‡2=y³²´æå*Ra4‹÷t<Ïè\†ë±ùÇbÌWht£\&A,Þ Yw©·¯Ûf‹^k‹ûÒk}_Ýô¾HN´í*Šóï–Ðìý3‹iõ|bFv“ÛÆXi[g„´ÐÖC¬°GbéD’sî¾*ŒÉ"ŠéЉX®޽³×ÞWÓ*õ›´µÑY-]×2³ùk\I’nkeÕ}òò¾Ëê)~›9¿n¾nX±=ô¦jræpf+ŽžµR´ßa}n{J‰1¹ÂP±°æE6…óÐáG;¥uK*Rû ¤žQçÔ±][Í»C=l¡k¿ëìôÍ ÍÜk ÷3=×(Ïôº?™MO‰O³ók»Gi©ÚÃëë5qÖ§W?wŒ*yeзƬc§ŒÜÃzªÄqR†k]((°µ…âÅGlÄýÉ`jèî]m¡Ewßôœ—=xQ…¥ûzO‹`ñ‹•ñ›Jæ«K‰ªðfÊ„q;7¦G?Å®{_àè2Ã%{’ìh¯6q?n¢'¿¤¿cJSSÁùRQ·§Wp±eÎj™7¿*U² Mëjy'¸©²ËÝd^-¬ÌùÙÒ±Ç8úwg\ˆ8ü—;аœZsãÒ¼*KT#˽ _¤ÍŽ•-ÿ²ÿå:4˜5oXˆYµÇî`'¬ðäQTQ6\äFŽÒp(¶ïº§L«Tî“!Xe Õ°z9^÷é̋ߢ)-žF×k³ä¿¿çò'g×›‚Ëï9·ˆTá6ºXbùጛó±+ýK™Õ›mýõÅ Z/~€|;ù*ˆ sÕ4-_ÑYþôY×ôÇh+cÛ# ‡x/^ú´j–®Ë?S!˜½¦›\iÛOŸ\?”\»Ÿ•õ:Ô:"°fµ¼¯yÅe‚?áæôú…¶ÖÎ5µN¦Çv½Án‹ 2Tã¥pæ3!¬u“ŒökÊHÌq‡¢*VzÏ®á9ŒŸð¶Ø)TŸP¶ËÕöȱ½íáÿJôF+ФØt øÿM 0îññ¶ÑQ÷Úë`" #½¼¤Õ—öU€Wj¤YJ]¥Ä™Ô‘.~ML¼Ñô+êH;&=;ñ¾“(”µÝ—],' Ì_G±t jŒä[4Rn–P|Fm¦ˆ'ÂfÈìe÷CÝä‡jyýòÙã<›UH^ÅŽ^c‰»å=Û~úÞÝs‡kúsö¸‡Ïõþ:ª\üC=³ýŒñ#®J–ÿã®,U™àã®p‘ÀJO!YÅÏ«c¹‰7'úí—!ŽŒÊ˜×  ð:Ù´2z}ˆÕU:>[qÀ³G:ÌvúûûR-ímU–o§˜üöl–©ˆïÓYL<ëÆ¼Å73»é{;ÄÁ+wÙS¬J¿ \ MöôÖ¡k®¡h®)®±éޏ §\L'þw†‡þEmKxÌõ{â%¾©’«ßÂZ¿ÈÝ;ŠHî~8þ§ó(Ó$øcîÓL…ôÓøéòŽ>3¬3ûò0Ü ëþX`!Oλ@!€*°@XÀ{ ìB0â…|Ä2Î'£Wà<¥—²%ŠqxÐhNXHlôÇBÌ@ˆ-ÑKz6l@$ò xU%V3;—*›’ÀYUõ$wL 6œ<æ)kÁ©È xþ# £X8:sXäÄeT½ÃÆ š#ªÝF¦~C#éø ‚[˜s…Å„?M û§3!Aéºì&fk¾RT÷eÖk½ cHDǼpàYï;Tt6"Îg?ÅkˆÉgÓêÅÙD­ò"ÕŒùÞ—_ý<µtu•½›¢[öÆPÖêl–øC•©À2kêYHüÜÊ9ꣷb‡³©±9 ’—ÑqF _Ys‚øê´ILI× 6\á ?=–H~¦ÂŒ…Õdö› 9gMDJÐ`@\†gŠ!öåùÈm˜ì*¿Û–N>Uc¶~ß«Dàö—WXw’B&½ÿ[C|YTaól|¨"ÞëwúÐò†ý<ß^ì$rÅ®úœ4é™N¨Û™Ï6€ÃþjP‚÷`{ÓzPþ†ÉÆÐ>2m.7¾’°i«VuCõý] 0o*ÉÅ´ª¶³yY-ž-ay:0ŒQ…W°gIá31!^ƒ‚¿žOåü:Ž¿šum50Üz»N–~`Ö„–Ê(½*¬g»™Œõ~m~‘Â^+Õcã‡GØÄ’ggз)Ľ!$M #øüR–k2k¹!m+ûnœ®gCX{yLû9a—hWÁfA` í¦‘ùíȹëÆq‡ŒújG<÷/=æ~ë%Õf½L‚O¸1¦Ø» 'L¢wÃx áƒÛÂRþ)Ý90Õˆ*ä 0),1.Á  ‡•Û HUi*=e(Wú.Ä;/_ÊfB{I‰måÓßç³ä%*xM†PœCÈׯv^üNË +ôžAË|S?‘hpxî_1Üyºp¾ý ŽRÏŸÞÀužƒÍî_Œ@Cr㦼@§Sã ”Èérè#”¢º e÷sv"°ù0Ì1š×oÜ’.¡ ë#Y¸‘>:–leMV’…cêž5#†ô,å˜cb4%LfacõÁ`J0å±~y,‚ØÇyvr?¤½¿ÛÌfeTƒV1¢aÎGYòé‹;UKëùMëø6Þ²5«`vô&üÕà`Þ/’C¨ ®X¿æRÌíÊy’ë+×Ps?qc»O¥°Ë§'`—mIr¿œów&c¦ï Ð1†Y¯­™J3„µJÄ#ö’Îò˜(!-ר¨'Ý'Cã¢]“Æ'ÁÎIŒ+aø¦YKÆdͰÿ—S .[âÅ”c_÷Ž-›5Œôs·”‚¼h غ‚ÈJ‡ìò1[G&»A|¨{ ¨ºLÇÂ3ðéo4ýÛ<»1øî~·Áõ[{õ‘”4O¶_Uóo¹º¸6^áò÷c-œKá4?Ðä÷Š~}•Œ|+,ºTÛÚ©9÷c“—_«é–³—ß¿]üDR|ðÇÛ¢£KC¼.±rQ"¶ ‘-Ï¡<¾É[ƒŠ&ÿd‡WÏÁ) £wHú/Hzð{Û·»ÀXO0Ài Cœys­1–¼u~©×Л’$AãåëNpïºù^Ý/l’âöEË„ñï¾p®<ê¡;ßM£kÿÕ·zâ{[BuE…ê9Z^/óAË{Å ¥ëŸï4ÿgûæÀÈ¢‰[ßÍAûƒ.‚.º©»/„š Š¿ò<Á´ß®_Ëu'jؽ툠‹)»yÿ„Å v̳/º Š ¯}¶ps«ø>½!5êÕJÄÑWn/™L»žFÌû´{¤Ž«¯’1Á‘°‰ì.Xì­­3®m£ì÷7§€_ÈÂqݘLWv´íæ/Iר.“®úŒÂ=V êˆfÍ‚]Oÿ3 ð³¯+Cל0´.ÃÃσK+ Û5¤ã´ÄpØ&Ìö hý×xÊn—Í3 ò®V*òêux|w¹Û.wàvÔPÕiPû;Ý ¾IØ6óIå¦_î¶°H EùÆOT´g;þjÍ›Ýè¾6ÌäÃgU3Z5žøöb—g•ÿ®:ç:]{.zß;£WÏJwÿ-ì°10\,9Së|-ç|/§ÕñY´Z´nƒY~¦[P´ÞFX_Ê=Ô1‰-Œê¸Ö,ÝucËÔÈvÒDtlön€åVÝÊŠºÉ–¨¡ÌS±ÔvZƒ´Ð/Jàëì±8âñ2Œ‹h­—/˲£0i¯éù9+j@A(Ǭbat~Ï.Þ`;Ÿ}Ù£¥¥™×¢OI·u#‡<Ì*É„ˆhi°6‹»/[«)§b¦º>ý c¾˜Ùô%MÁ,دnœÁ˜oÌDÃʈuâå-:U…T îˆsM"ÒMݪµ.à GKÑ(5”èWÓ³§Õ1ïÁaWÉ^ôjbÞ©Ï,b¦uaH5à8ÖÍ0ÜKG…»ö3Š…Ûìræ¾%¼òÓŠâÂevìÛyÃú–Ù®0¬óïðâ¯KÝX³ÂÌýPaÌ¿^ˆUݲ·^ÚV½¦j6DÍqgâÔù«Y§p€‰Z=×"¨SÓý#`&à;d<š‹×¥að"`WBՠŧÒlùekî×-©AHÜgµŒ î¤%)-Ho(”@ŽŸoõhÐÙýJS¶´Qt»ÏÅUèË!e-ôFe·Û‹þÕJÛOrµh[‘-ͼ¬éq⛳”=íãvª¶€©7YxS ³øæ”T{b ïáôI_ãóÉ\뎸9gWjBÌà>9ñ—×BÈê¡Â0ô–ä (äã¿·8S®üð^>ó„ûÆ9dƒ¿¢©Ç?&¼aÆΧ¸Œæ 5ÝhY)÷ÝØ 1‘ÅtdûªÉÎq¸ŽôxP lmÅóïGž Eœã²9êRÇeU}û™¯üj)plkÞœn×Þ´ ß®±‘Á&p§Æ“ã/¸F`ûƒ4 „—§à¤]3•A8@×LÓ‡ÄJºrùx=<œœŽ QrŒ5¾è6£]/9/¦3‚ºqûûk¢2rGÿdªàY¡0¹Û ì+ŠázOzq³Ä?÷8;•õî×¾î¾Ûì@1\‚¯Ò ßw› |>Y.¼l:NK`Ãa‰¡¥½RºÓ§Æ5×OUF4E ‚˜= Êßݶüö]vд?-}kš :äNÿ·ÐËlºeS⤋=’—Ucè¾<;³eö¡)p¥ç€›Ða±Á jZÙª@Ù>¼Z-ëÿ?Ø»”âšÁŒ’=¨A¨í Ç9–ã<ÛqíøùÖ^üllYMÊ‘˜L‰˜Ž÷àENCNfl†÷¢¹‡!s‰á²=|lN¿ôdzƒ&= Öb•â'b3"©ð†Àt급8ÔhaD2Î{Æó'×€òŸ–ëQ@ˆjQt]2=§uV.%%“›„c W,i1+ud:”Õjçl³ÁŽîûĸÞElN»[²ïC€ažwe»íå@¢‘ë¼þÉ]tsô>¾×õ½·6Ê x2¹ƒOø,XO8Øõ˜-Ÿ½0‰ÆëŽæX·‚ç¶F?g´x¯!ë¾&»ûnåsQZú·ò%/…_Ý•^õ–—Ûíæ¤q×G»ê\'‡úGÍõ«¹äªæý¶—ü°‘JË™)Ù´Yí¹§ÆÅT=R-™& ×ö4¯?ÐÝ4UàB4Â’¨l¿p¸<êæãi¾¼‘`ýðƒÄ[Ðöó‰ËO„^Ô:`îÍ,6Eš¿Ž£PEcÊöx½ª(+ê23µ{Խ摅²¿S´×\ø!cXì¥Ä°`ÄÀ0ž‹{5²÷IZ]•^ËžûL Ö63åCÅÕmÆ6I&´ÕYñ® ŽŸ+‘íŽâ Xˆ î*dþÅÉÛï"ÁòúÍïчˆ<Ü›³‰;vºúzÎF_…sˆP’È5îýù[° ±d[Ke‚C >A)¡ù’ð…µ?yÞ×82+žBÀÅqùŸÈÉØ2a,¢Î.ã·X(²ø-“ɪ^h cý¬Ö=41ƒ¾·¹îêXËYøþ³`ŠÖ¥Ø×Äé= @Y'…:+”½¨ Þz™X8ŸlñH(Ë«‡ÏÚ‚¶F¦vúϸ®å`ÚÞºbZ× ˜ÏÈM÷TBÔµ‘ÑSÝT˜MQþX3¹×õ·£Çñßï>8M~±ðá% Õ–aÃÙŸ¥0Ì^/ÑjQEñn$!ÿvŸ7Ø™yURÝHYWg;~ÈEñ®q‚ã¹õîU²ë¼ìëb~Ï'Ù25ßY+‰£‰‡žÖéÙr-…  Dö‰N¡ñt‚ P©×Ã댵h]7 =Ý’)eÍb(‚fiŠÊ¹BoG½‡w ŘÄþÉ$ó̈’£ vKHo£&3 п½èŒ3êTÛW쫸BØYüZ³>ÚÁ“©‘7õÈ(ŽxÚ†Rž_Ibï/õ¸5’Û?À™h¶µótŒúkç ÷tÎŒò¶²l2\û·w q‘¾Ûs6Ô±µ~FßeLçXÎ"™ø`#^ þ¡YM]¦Cž#“ÇÕþQ™jµ5G¬ëdض×.y=ï½!“Þä"fï!“UƒG{%좖æ-ÅÃím71åîß.c 1\B,·ñÊø,¹¶å~KÖk±'_ñl6·x+šƒ =n”A ùZœeªŽ{?ò˜8Žj^€)TlXLƒºŒÛ¼D¢„aFöQ‚‘0Èn4‡åÁÍØ¶ŒzqørÙî~›Âm3ó_¶<ö-3öÿVòÏ¿p1Žú÷É“ÒP½»Çðah9ø†3'œ€!<6A¸f»«]ˆ1ÃÌÚ7ݦßÝtc«8ÑÇ2êl£4»î¢Ø]ÊÅ˪÷ñ Z ™ƒdfƒ÷?® ÎëìaÀ8À~µÎ€Pù*n”œ#¥Ìõs$5_“üœv7’aƱƒ®ŽœÕ¥:íÓX¬ ¢;JÖW— ˜® £²¯d,Í•‡¤®5Œ—\)ÑM£xf©Wa2Dàza›€i^ŠÄIdamb =BÅChÄ7I0¬â»XK¯h^³1U§@Â.혶\&0t8æÛý‹}›‹ol)pýÈeÌ‚Œ bhçëŠmM)íÎ&q[ÃØ‘W¸,"o~ßÑxäác ŠfOÅè˜Ñ¹ÉÊhíT‚;m)f%\2ñ;×çøúc,’:ž²¡é‰`à躕ˆ|O$3™FÃ7Dè³Ó~2%+S3þRá\ýt}J4tVc½[ yÄ—°# ðÜÒî–47Øùx~=#cU·uN¹è•±9³q@nÄUC£ õ _ šeÁcM¶æ¨’Ÿ†fhlPèîHð²€utq\O!Úé˜k!ܱ£Ä\t¡÷@3x ÎÜïw8þQB˜ÝÖu7#Ö v¬­ØþRA‰bÇ”[|Ã-ÇÑVÆÅúì[gð]D…d4ãÑ®øÃÑÅýXµg› ãwè·j馬}[दµ; 褚­a|Pµ¨^· VýØ ú:訵´ƒÔ{*øîa˜Öè¡¶§.ÓApî*¤Ú xÔY2݆‰tb_þWâù#(Ó¹¦ºŽÞ¦­M)i=è[;§‚ý×Rse“–èÑš ƒFÂô+õ¥0©7¿ôc¡úÂËÞž»ž"¼AðCr¨M¸×Ãaš÷t‹>ôw<… TSå‡ÌÌAb¹G ÖYè=HdÌA\‡¿Â>èØ[ŠÆáÖ&¹JÆéÖ&yJÆô[ îùŠÑ¥Ø”pçõ¾FíX”pB²¡œùY ¸FÅI  ëÎÑ£`¥£©–šhËeòÃ{Î\6>M…ƒ&c•¶%¬"3µH"ªƒb×jJÈ™*éXͶçñ†@jmR15¬†fGå6?¡þÂÿ°Ôv áxùTm[äsÜåW$C"[år6Z(;”¤µ"µYuJî\™ÃåaH‘vÝ ³|ùž­neC7ùÜWH§ì{íXøæn2lØŸïÚ±¹6Q¸ÍÞñ¨[´mÑO—Îv*äxWqó;.q"Æá]ع­-b¬Ž*»RŠØ\=7¿'Õܲks9а­JÜV‹¦‹:&PÐn^^æèx¿Ç}†~®OÂdD€ÑÍH¯?M]ˆR{ñnˆ §Kv™ è¹%.ê cš¼âxÖƒ{b¤c6–‡h¡ðnI€Ì,%3Ò!f÷_ªAü¥w9øtÜgzÔ¸½X¿¦ÅtÀ:# æà³a­¤(¶Ògεі v9eŽíMØú³ùš´H[ͲF|‚õI_“«‹æ Te´0o@Î24‚Ã{+A![µ*M‰l-a_“EXo蜕‚¼ K£iFÛ±O±•ÍÉÑÐÎÒZ÷²Ð"¦lã÷Ø&L×RS£Ìù¢çädrxŸpö¼bêô³»^Ã’¨Kà*âf}1œ5ÇÍ“8#†­QÎy+Û ª ´PU /¹š*X5ZC\ÈÂFX§{]X 2æ¢E¦ÍWs ÚU*È>©‡Ť¼üÌú»Džx2ÏàNstö™G2š %™u »çŒÕ@¶"Ó}MCKygØ’r4Ù*MãÈ~:“Y)Dâ46¥ŸÄ»8âgyæ? ÜÒS¯W?1sqîɈ•Žþ¸ûðB©Šõ›ª1Ù‰Z JÈe”“•Hç”-9W6B³Pé,÷, æ"AwÛ…üj¹Ò×#qÜ…’SAv׬¸tðÉ›Kiè,†q$æÀÍhø‰‘ôŸ(™‰2/Ž'wü{Sša*ÒãÔZ’(·®Œ˜!‡‘SÕ‚ëÅLJvåà >Cµè[ ¾‚8(Jnê º8'e±|F‡ýÜNfÙ Ç›÷½ÿA£°=`_e×¥[o„å+¦Ôí[n†§ÑÔ;i 4ÆœâQÙJÓIÓ< =wiУ­vGç—ÄaÕªΈ:oAŸ<1‡þÚÕ»öa~´F«G—:.2žÁ¼DL³#&Qê÷ŽPUǺ3¢·Û¶ñùŽçÑ C\ÇHÍ|O2î1‹. ò™¤ï¦µEnÝ<“¿<‚À87^a? Æq’Gl-s¦rfð^ãLDs!ãÜ$ä.ýaLô ®ÃÀ±ˆw¼ýÑóœcX£Z“ÿK@Õç;t¢Ìõ •ÓZEåÑ ¾ò¤€4JI•ñÝ¥ÌÆŸˆ‰†A‰E—Co ÙŽw¶1˜ÆäÔÀÀäbs:Ú)† · Ü6T¤{óÚF‹âêúàxz×%f#zmV¡sj$Eà÷É®[Myâ ï$Íÿ$hÐýÝ9Ý¿6þh€'›eL—|¡ÔKò— ïxð)±îV³÷»²…ä‹’_½ÛW—µLÖÿ|©Çbu VÞÚ’l™fßhˆH²ð­ŽiíзnQˆë+„{($ åKÃtÑ £ÌïI4¯~ 5ÕÈ·ŸI Ù_°ö1 ¬#0N]8+Ë™rÃO½\‡!”‡"ö@ï”°z3‰Ý8ëëÄáÈ&ìs#}MOi' nè¹2ª‡Ö²Cß·Vt’£ª»i…aR,…îªÈjëJ+XKØôv¼óÍ­ÆXŒê'ó÷]1ì ekÜã{ãs›íëâÌ[w\DN2Â0ÈBQ 'Î6Äð§êvÍÔ¾’ï#8% Wyá³Ø\z½þqöÁ¬Ç¸oá÷õMÛíT±b>uxc’æ/vÏþ®j‡P«p-Ï+fQÓÑ"B3J(HI«˜5î”_ëP.YÊ!Ae§Þ3Ky™6žq¹ÜRproÍg½Úéû5ªt­T¼j•¡lƒ8é9DR°íôEÙ…n.Ô•T½™å{È[m`Üõ–ŸoÊ«6®s¹X—Ÿ¯Þ‘ºÞ…ï”sÞמëƒ)l¶#ÆÙušÔíœ_´èÞµY#°l4 ¿ÝSc›Àºý<ÁþF@öwW-<ÏŸ<@›†»ÑÓ©–[¶â(QkÄó#2–C†,òYçÜ?–Pç—Ã7¯n…“uÙZ7·‘ùÖ†”#¢þkO‚Í{ÊÞžÓBìQ)é¥D²B_ê/ Õ…ô`­ÛãÙty‘ܹ(…,õ-Ì>”+:­Gg°5k¹(éÂüɦϴœ3a¶ìë7ÛzâÂé£)Ç”/[yõv Ôøôjq 0ôî©T¼ùCN²÷±OK1½; ¸kãCþ¢ðäÕ!vÕz"5½ƒ~¯ø‚AæâGXUÓÿiÔŽvoSölHÏ3ÈÛ9ÊùŒ»å# ²mÅ[d¿©OSBŸ8\pÃwoñ/mm¾ÿ[qB?àßÈ–t3YMJÇ%¤©~d{þO@ÙŽâ‚æh¬{^"¢!$§ÔŠK±]ûå­¯íï0&@p]T›×çðá¢ÔCå•d0Š~C£VUu‡lüGY ÊxÎ 7 ’.Í2Sbíë»èíεKÚ>†u%ÛpC$¹Ò¶õh®¥eñÊ9'IA—~’ap>DŸáÌ{^á¶W›®×Ä9ŒØxº3ßõ’@1è_t‚‡³ ì#×jÿY¼už./ß-Ïä³;ŽÈáÑ.È@x~O ÞìýÍv£Ý#A#û@«ªß€?’]Ç™wxÃw鎰4,™2—AGgß X÷LwdîZë®P=i¿%28xrW-1m;hÊ”.U/=s ŠÆëÖý§Heµì¯å¨'ìçå‹ËéO%ëœFPÏ/@¦«éJüìŒëÉâ£ÙMg=aWìÏ0£‚QñwƒL—ÜϯÛh®uè)ëo°F—í*_—üǤ ­xPsßÖ¡”€ñ5ÛÔ÷,\“ÌšÅÇlc,5!~—Q–ú2÷áj·àéÝ #Å£?æC]%1øáÁo£çŒKŸÙŒ5Y »Þ椑mcʉTgŒ¸ï#êZ/H}Óûs½ü”^~áOKIt¶M›) K1Ѹ­oÐiL Žtçâ\jNÎ+x«}P­ºÑ6 …¡ä¦KÉßr›|rëO :Ð8j ’õ” 6ÇÆ7¤®Ø´Q4k½¹§-É[ü¿±ç½©šÐçàø§­ï 0ÊŸÞøì¤ÏÓÄ^WP}è:µf.‹սь ÍÒÜgzH<È5WÕöƒ"»æ±F}cÊÂÝÑ´;IÔÆ½{òž>%[«ôS쨈‰š³ÂC©»H×åÃŽõo¥&0Êÿ iÍhîðÄSr=œ| ‘‚?'V¸þ‚3Þe·ÉÕåc$Å^Ã` [b¶2Žù¢Ê`Ý»$é‹8šŸø‹ 9&Ƀ“Îòtf¤² Mëµ.!¡ÑqàÆ°8½âø;p]МË{¶k_“é:.“?”Íœ¼IðŸO¶ˆ?^ç“äµòôÑÈAy´àÎù¨Ì3ò¼¨OãQäp' ¨¢…?¦œ¡õÝL'¾ú¿XÉ82»ëhõw9Çâàˆ¦]!&²—6R‡½G®FÞDŸÁ Q6~œ:±GD?ž(¾Åßá³îó°>¨ÁÒ²ÜÙ€ugÎ>|H”Çq=v’aNFéQ™„; ëëÛ”8xU0ü<¿Á­šý³ŠÁßÐÅÀÄFÜÂr~†ˆÁG³„ðWFp—†þŸÕÿ§Á=Ël58€ñÿBËõ06up±´·£sð¬ÑÜQÆfGð;«›OGO÷`±]ryÜ_à ˜—)½ñÞÙTãV— cÌø¾ÛI¹]Âwð÷$Ì~=j«ÚY¹5Ëʉ .KmÁG‚½ŽŠŸàµŽܬ¹©±]U|Љzn,ZíBò¿Ð°´ IŒ½‚À‚Ð$žŸ–óo1Æ“!àÞ0ö `“˜Í ³ ƒÀºP4þO!Ô!³9“&‡"BcD@`nÃi‘A­°Ä§ÖeÖ#”“ , Ãí+ \9•œ‹ãê3bí1dªcöðq¦ÍšÈÜ ²¤âà‰ä-àzŸ¹Æ~ø€ÜhÖ˜€ŒÓCÿ –³8!ßa £ß£)gÞ5€s5E=̵4Ǽ£ÿ±a)HQD¯’5 š›žÏŸ2vFI¯ZÀkØÏàoíÀ›þ»Ñõáêë5ÒßJK»ùU½ó蟥×<ëÞ»Ñ}™ýz_ƒ´~ëk˜ëñuì„Zé÷©†äD9ƒ˜0ö‡òOxô— Àˆ¸I< Ž ˆYÁµˆ8­ÇxVÁè” ºJFЍ¨§ÃšnT”=둾c¢;u;Üocž™5&aB´f…ß#8Q+ZX¿”(>QLÖãp×Ö^€A/Ó¢îqÎV¼îÝ+ô&eúüI(ÌÅ òG>´85¹Å'Œ^w (®:ˆ °h+³—•€üÖ—”]‚Ï­‡§±üþ9º?뻿2/ŠàËrê2>cäxÝ9u ø@ &·`?C%¦OªÍ8ÌAOœZT|ço<³À=|C•íj•¼×rƒª3˜/QPÂIŒ|ã’Š^ÿª7Ðq8\ƒT8D$U…HK üNì*õÈ| WYq¨~:ǺK­¦(Ð6•¾õ”ô©g.±¿Z'áµ4ƒé;"*”Ù-†5åh‰>϶J&‹;a;&Q±ÆiÂ0LtF‹Î¡QA,# ¬È´™¬ý×ÍÕÛ–~(¡{¦%æÛë?È ¢Z$Dv ªvmøzoØ×b"‡ºQ4z-±Hâ™^öl½!Ecñk:]©¬9F•T¡v¸ÄVbe1‚r•+Es’–eASqÛp„QêôÈâ vÀyRç4¦Gx®x¦ë•‘FÞ¡ÅCk£étìŠc°v_ø5Ê‚¾0]©˜ÌAhh¡h¬>cRìñ.•cH5ÚcFÏX[ѧ“WÁ›–‹vÕ­t6â)y¢æ^h§7Šéhufv)‰*Tcå)FÊå_Ë ¿òÁü/ŸÆ,Á<öÒ­è<-™)ŸòÜ?︀eaÐ&! &¢ÖKs•>`Îpȇ¯½•¨Uaž1%îÓƒo’ŸÓf8©Q+Õ`\µ¼ÌæX1é‹ÎƘQÀí¸·‘|ÙÙó•S~5LÔÛƒ:`#ÝIú¤€|§M5Œ­n$Ž’LÉ,<Õ*ί¿|”5íùß ãuÄ»G@]ÐBꌎXm÷ªèó1nœH¸i&™Ì<žÝÑ„ô8fF~˜`gEjîäýüè`•†/ñí£§¹¼Ë99ÎŒåí¶É<ÚåÕKƒ„˜ˆë<è›w’ôKË|*vL‹/"·+ã:]èÄ  Ç*µGä ^Ì ut-‹3ÔS›%§›7 aµ!w!ä oZg„#/¶÷×¶O+Çã¢LT@ݺ­I)ÂF–µÞÆöòY±Õ‚èÊ~Z½[aƒß¤t‡¦‹QzàŠ3ÝR¤‹éÈJ'‡®ß¾¶9+|µýž3e©æBÇLí)c;ìˆ×xoVANŽ_hÇXD­ö{²cÌÍ7Lôñ8eŸ×æ÷æ$¯@AdxZ_/Sçš¶ÚxþpÕ!Æà™~a®2ë0\08`M¿âœ(C7‹ž›è¢t.ਲ਼¸òüuyÖÂ:(27‡—4¥*l #„%¯µc ãÌlº]‘;ðª‘Jç ŽY;̾\b”²Ü—–*Bh)­ÐþIDó,>’r h3BOÊñ1òÁ×S&t^ 9†òWM´u|»4b™~ü%ñ¥CÔÛáª_G t+Ápàzc¾-.Þév®Ùˆ€Z¢Ãh †¡buô?=¹*B]o’ïÌ©—Ñ \RE÷Û¯c.—œ k_vKó«sÚì5—æü ¢m´tVø{ VÝñö¨ªoSAS¾ªÄÄunºÓÚՈƾŸ´iMË¢;^]„ðYÕžDv¨jT¯kL3Í뺌õÜáð X»\^^ ¡‚é_o¼âß ½Ã×=u澩ô¸’­zƯ»ªŒ¿ñtG*™¼jûÁlu`3 Úzþ‰ª‚k|ŽHj¢ü•äe*œó¼ògKwï€g~ÑGñŠlm`4ˆIŸÑ¼O0)R†Q©Ö—ÿ+ïE¢,T  ‰þ//üÿ0°Æ#Z²8⨷º5Å)&’­¤ãü¶ÜŽÇÒn‡P„¦JŽ$à“Ö졃”RÑ¥ÞêÈ3rö¥Þ&5ÆÊ ’¨àt8އƒÈèp:ñÐ:íNû‚þ‡äÛ=¹»˜?ŠÍ›+Z¶7Y¹k?Ÿ_Ÿ/W·êß.Õ_qs>cÄòˆ¾{úž-ÄLjÉppÜÁ¨ãâÁÿ$ÆÛðP!¨‘ôæSÐ ±SR!°Y ©q lÖ%¨Õ´Èl‚ïScµ. żƥ °Ù!©qÉl%àµ-Cê‰K[ŠÔ¸„Ö“ä´/( Ç¢ŸÛSbÝ%äµ.Í¢Ÿ“Sb‘Ý2ïiyKƒsX•¬J’‚s˜±fµ/ é|5¤õ뉃{,.õš‹‚{LŠ›Ô¸D×I‰}6¤Ê ¿¼7¤âÝF–sš‚g°/T©y×Yå­.¨Õ¸„׫ŠÙ¼q3œq"'‰ê¾Ó–a&Ùyw•z!LEU÷fÀàW”P"ÆáÆÅvGÓä…ÜbÉô9Á¿¿Æ ЬlÑ.(õûÁ(¾»o¤äzúS#¹\iOÊÝåø_#) Ü÷¾Á\|ãNâËæÝß»r¹Ž‡€<ÁZ;ùd ±×T¼+C&–v©YóõƒËL …q<È/6œx`Nºðu|^m?ZÌßÁô©Ü‡âÅ%çvûÁ›‚îR Ê£Ê/¨ ÷¶hkC÷ãRRæmSPiuÑÃÈ î£ÄUqªšñ?­LEÛª¬¥ð–Q}­'C}‡ÆÔÌ`u%²W«ðë?TO¥YÑß »=MŸ^ÖÔôºøæ°c<ß$qiW„9ï"Ì”ƒŸÐz;Ôº"sø‚6둇3År(Í=Ѷª.kU\Î2G7Êû˜jªUÒ´Ë”u¯1­¸6ë3œtC­¡‰ª¿bxŽ"CËÜ t„:…R]ê°}œc–—° ”›oÁ³?C…:Ã…ÅDE²ìüÇZ±M55$4Óh’‚ö¯J«¼Äoö-1c­QËáðû5Œ¸é‹ZÅ8ƒìQjb ë {èO÷³Yèœ_4D®¾†œÙÂÌÓËY:±kñÛ«ïð|{FïV~Ã^gm³b¡ÛˆÛŽ+>B¼‡úÍUI[úëÚ*°[+ÖæmÏ+«8€mL·Eà_°æ±@Qn~œà’il©ð¤ÊóbHÉw Ìÿ¤'Œ"ËõzkÉLpÉ ÀÒeoG"‹ïÍ7ÓkÏþà}»•63EŸâÿi‰\H%˜ÐjJŒÍ\‚® G“Ã?­«‘ÃAì“2ws. ‡/ü¶9!àO ýÃ'BP·òê7ð1T¯Fn—Öl¿;ÝW³FŒÓÑàOÙ îW†R_ ¯â€"õ¹®b½ t†«ã9@l*Ä¡ hfN…zÊ‹‚’—þŒSJENEN2f½†‚4Ø’«¼é½<.÷YV“£dÚFOÜ÷ÿG+žX¤™ÈcÃxßêÊJÖÊPœÝöIä'VKoøÈ?˨a½{ Žô—…Üð Aá)¡D®CWyQ½u舑v³ðX2gÌ)íªœ‡d&RWäoÑ—…‚óÉ-º™Ç f˶:ÊrØzÐSïmýBáÈÙ)ŽpS}¢–pSí^h‰¯:øU ¹ q†â£A€ˆåYÔ]¸|ªý°-{yy—"-æ§Ü_jÙ_Ñ¢‘{¤‰tkPÇ~†TÊ7‘]ÒG¼Â‡.Þͬªþñ']º¤ó\qÍìôBon:ïÅݬ°‡‡ù.ÛnŸy“r Þ³YÜlwÓÇL+eyÌ–xZ’ŸÓRóˆN¸$À®»-[Ôí‡gHÓÆÖÒx+g}WÀR·35<Ÿ­³;7û†ÕÚoCµ°§o¼M[/|³¦öÕ«ÜÖ7áê¯+ý†U¯ï–^Ïâ®ÎÉðˆwêµþ ¬^³§F=$+Çú­ÖЭv-ý§µ/½ §oÂ0•·¸vЇÈ\ô×á÷ .*˜· Vd½'D¶^pQÓ/vNwÕQކÏuéj­VotO=\.¹9‚IÖðɲ3XXeÀ_oŠ î­1{#’Z 8ZÖN—ëéÌ[6lÑ{šo³ÌØ­ù㇔¡^>(d¶fË9uæ£fÛÂÌÂÒ¨žh¦úX_²Éa#ðtüM’®XÁ:Á‡ŒT >‡Ysx÷˜ÊJ¹6m[N@PW2CШ]PþÎ]µ#WêÖ©-CdŠ>ïöCi0òÇuwUöJ³~٦╗Ž4Z•hd*å ›'ñ9ÓŸî=º‡l¿É×Ü<ñàÁõ‰ >×Ùâ_A›ôDg#@^ð`D`U¡Åà\'ÈîÑÛ¼ÁÇkJé2Ã-%Y.IfIÓå®0)ósôø^ýÕ·ê Õ’Þ§EtŠÏsݼÓCŠó¤ºcÈ9äD[N7º žóø‡À‡[®5µêÞ1ÞZÎpÂEËöÄá5ö†+.::ô2ñ²°—2&^&ÝÕ<Ã/É^̉ñçõçù?Àÿ³¸:8<ù·8@êÿšAmæêòŸüšThhÛc·!ù®ÕaŒÃ§"xZO‘OpŠ¡€»AóJ:S~Ç¿C,ßJuµ‘¬5*<÷¹‡ïº4ä¨Õ—èÒéðé×BÙâ t88ÛªñbÞÞŒª¥uö2­_á€ß ˜‰íÂ@«`VºK4êT‚rÀêMÁ@e–g£pN‹xµ¡‘_¨ÔAËœ.Ú hi‚ÊJ8ñ‰ ?]Lr›—$imä2gEáÍ“_îçYùS°5gwí#2N9:¹~Ö-5áñkp´?ò#CÒ^õÒ:;{¹}6®Ý½yß5ˆЅ€:Ï8K׌ °\9ŸLxV(Õé &Ί®ÝLpv1vˆ[¾ªTvüºyA'Af»Ëhq©“XÒmxhåÙíàQÛËéçäÄCbÅ„eb÷.›(A7•58üÑT',’KUß‹T&¥ÌNfÍÞ›œ™ƒß[?ƒ‰ë.ÒsÒsy”x"ÉhäϰÁ™“Že‰©·/š9Ó;ââá ÀæËpÔÈ—“®¾jÄG‚‘„HžrI!kWƒzFÖh”aNìAg4¡!&ŠñwñÔÇ=°HbEš¥Kgpvͱq!3ŤªjEz??×g¦©…c‰7’³ˆã,³žz*ajT.¹!™õ>(zp_Bô>L|£Ñ[¸¦‘zm‘Ú®,Àì`UàEɦ4ÉjMKepWØœE.BD+DõH.(Þ –pÍ1. €~.˜¨ôn”ÈÀ—Ùß{G˜ææ½›¨WÒxêM¾1ðëèå\Vzt6vO‰kG®ž^&üt޲ )×ñ÷çÿeu*{Õ´yúeÿnÈ·ë"ÚÕÛ~8Hè´÷í$«Ñà—ÞÄõ=1Í•á$%Гk!0óOÁÑWþÜîD‹l6‹MàÕ7Ëö{Q.‘’s¾ƒ) :L³{éqÒãÍ&¥åæÂÞx¾´_’×u@t¸Ž åiñFzÔŒ»H[zÕ,ԇΧî b ý0øá1ÛÒKçôDk{}õl¤¾Ôæ¸àö·”Ü÷ÞMÛLc¼^N“Õ&÷¡°¹¤×§ÚI8f©œâWX÷||£v›”ÈhE}ÃÏu±HÇI ‹íME2m-k4§„e÷Ú”ózZÔ— ´”ôNÓ"ÿ3°ÿÉ›/á·LÔÅfgM/ª_Ç×s‰®#«­>U3ƒ×‚#üwzÍ‘GÕž˜Ýßvt„ü·E×)¨Žåš/Sªsa(â.]!Á»<;ã3ïÃyðxøh¢öŸÓ¬x?aTßbs}»×á›KŽÕôÛ*ٓꢰ¯ˆä Ry—·à^œs $f¶zxµaäZ!ƒ°¹XlDÜžëœ%YàZ1¾V¾¢J¯E ²€›Òíº˜‹FÈÁ¹*†âÛhNc„7 ãŒÄË5Åz“¢CÏ¢œæ Fmx÷ýOÊ-r±òÒ”×gƒÉd$°‹j³·ÀûË+æÖI~Þ]¿Ö…é™sxñ0ì´+'#lÍêÌTz ¸Â‚ÛíªQðo \ÚoWc©”"šîD’´sJìø¼Z·¢"Ÿâ % ½'d–OæÒ) L‹S‘Õ†¦)Nÿ;ƒòÂ]X’çAZ*çô‹­„H…JF‚è  œVæï/mM¿×·÷÷îùi…^ý®WG¶×ïn®×œ¾—üyäO˜ßmŸŠn@W_ìÿÅÙ?D ,]¢à±mÛ¶mÛ6¾cÛ¶mÛ¶mÛ¶­wë ºûÖZõ¯Z=Êå("wÄŽÅv{käm°¡à&³AG>jLkG„äOÞöÀ^>g,úd` }ÕÜ ˜ !cëAŽi UtÆ#^-ÙI6w$¿þ‚¡¸·xïÇù$Œ_«º9™Žcì¹Ù4‘šèËø¹­Î›!;9ÊE£‹o–V:~ê%(yãùø57mД2.$7ÉÊNR<¬9N!2?ˆþ+ÚÙÊ9«ÝO…¡Ne@õ)ý^B_ù;^£^ ´ˆ!UA²Ä>¹×ü«oø¶Ügsèš>ÓgXÐø}Á³©ÓÊžL¶ƒH9D5e )FÆ_å‰Xž@åcÈð‡è×+Š>ß!KIÔ7ï6ëí§â >&!ŒFÌ.ž“gØÜûA3õ\Ä«5w~—¡éS–§&-ùf†!,ÄGÌÁc Äüð>ª9ÇNúx|ü «¤–h^T•g»__³švd÷wgnyª‘ ÿÍò|[oŸ¢ÿ›÷áÿ/°Ù¸FMß[!¯§)gÝ!³Ø6§”ö™T§€Kâµp^HI‹½ÄQyúÄöFÇÑo»§‡ P\"=7þ3ô"]ïÒý0ÇnÜ?£ƒ{Ìò€öaÇÌÎÍÕü•¹÷éncS_ÝÖûDèW}ô ¬w¢¨H\`÷ ÔYt=ß Å¹“ìz=ô<Õ€WË?‘fHV<!ÁßK©ß”8•ÙFÁͪÔÀ «Sõ—FP!Ȭ“™À·³‰@^Ð-ölaZ¸`B^ø«F`ÞÿÆyÎdO!;¤r‡,8j^ñy²LóCàxrNÖ"—’åì‹%îŽ8m=÷É$®[l<ÊD¦×•½n@syÇÆë݆ñY‡%øƒÇ¹Þ‡rï@aþ wP'ìÑ º[ó¶`—òVÌ.–›>}.‡Ú9\Î ÷ì/ÈÏÍÛ±ÔÎhIH<0w”Lu`^1“EÈÉ·˜ÕC‡sAÀ,y½šRžê¢æ½vÖDòôI¥4Ñ«ê@áî‹å` É/òðˆM_„[±ȧU|•¸‰?v·¥×㦟õq¿rÿ ‘ö7ë'OØòZƒ‡A›Çd1@L  ÓLÈÑ nÀ5à ’¤ÌÊÜ’ #CAÞõ¿“Ñ L3¾$À6ÌŠkã5 GRzKUO¯Pt‹#EB¤ºH] TôÖ¥C¼ê®À L´LÏÈɉyaÒ¡…g'Nuî›bçʃ/ö+«B @Kv ÈÊmÊ­†¿‡SeÑ¥ù™†¼áÐwfò—oÊÞ«ç§ôD$·Äƒ_“Ÿ+ÓC›]ÑŠ6ИÊ3YŠW\­dȰÚÞ*W>‘™£•¦¥ Þ´¹ÊY~B€+b‡`¹ùRÔÅ|þé}Áëy0A.ßLN™|ÅÄ›ós*Yô1=)1˜Òöh$ÝòœíÚUÇ´˜Q,W&Öcé,BÛÃ3e ¾9ôq¥Rÿ„ìÜ™7“xô, uòŸÃá¸OFùhSíhÖ k6eþûTö‰è醵×]ª·Uì)G ×^9D,0¦Å)[œP‡æmyÖØ6FÈÌ?¯NT¥{ž FYFzETTźl4—£ÂòˆR „&£Q{Ê> EiÑUoñðŒÁqåcÒ°qpxNËÔF„±àÁåM:$Cƒ¸pÒi£¢¸’ÖçÔf2Ú&dT®mr|ÉaQŒÐ†4°FõQB‘þ-\k2e × ‰ŸÔLѳ¨y›l úq­•O¢bé«3d‡ãi[οL‘~ñ +ß‚‘éÎHÒqŠÄjLñòp°ïà…Ø‡QJ³¸^ˆc“A¸Ì»ë©¦!þüŸÙŠ ËÆÿ"†Üí‡Ežw…±Å”óÛqçd¯_‚k ³Ä:Èý ß«ëŠnòö†äªÄÂ>1±Í³rÃä„s7bVb‹Q:@|"›‹Jª~, j j¶Üj „ä;¹ØbEÝävd„;æúûgte<Æâ˜| YÿF˜ÔöÆl’ª,öÒ¨Û¸Ýn¶ð_¯WäÕì\ÕêöüLƒVCß.۱=ëÈÇ4>_¥èÜýŒß6ƒ„Žý*bÅvŠ)_>š¥œøÑOÊ{–ŒŒò·;‘Å!e$цö± OxÄ¥ÓI’y`2ð ‘nÑÞÀ êYuEú%±,ÈŽëqf9««[úR¬bX.'3Ø&!xõ·Œ}ʳF[FÖ#JüD´uY2‘ß™ãÓÊõ+Û» © "—þ“wÏ1Æ¥2xP¸§]Há; L ·UÄe$͸’ 7eyÖ Ð'¹Ÿ­‹G馤°éøÚû§;±&]“wò`4q—„ƉäY;2ŸÕº¼·ìI–j »l ùíE¼ÐJ¥]³!Ž´¾épg}“Ø…‰¶V‹c. c{ú,n'Ƴğ0¬ôCšÅ0nм~úüà Mµõ7­ûj§ÙC\9 ‹?2L^f•ÌFòB9½ùÄd²·Å^ÀE€E×Dz¿±Ž–P^äÐ0eN.SdwëÈz¢æ„ËP¹iYASÈ{â¸ç×ÍÖo@Tb™ÂÏ?È­‹WÀB ìœh9ÛLÜŠBg{>î<Þ„9XÖÐ`e±·A+REíéüL¤v$ÂÁ4f@{ 4S<ÙëÐ?ªöŸ¹ÜKB´®ù1A«DÄÉ!:ªï ù,Êì)HZDΓ)>Y¼$&Ã;x†­rõZõ½Ê¿*àFéÓü%N3#Λ.ºžMFZ°BÜ=±gQ$ºÝ†C>1µóZßT‡Šmܰ_ FStÔ „ „{œG?®ÅÙ‹# Þf9&R¥ÕÄ-6;Mõx;Óò 8ió"”ÖøE,räÓí1kêSY¬ÛbUªÉÕG¢PS¨Ói¦Ý±iy|‚ÉÛiÙŽ*Û`øX¿ÅŒF§ÌÒˆy˜ã3ãlªµ$s%Qj(!⋞U„lÐ{®µ×kû;u¡¦CDÚ[ö®£“ j6µÒB º …B‘-“!à ¿rôb¿À`‚óxµ^_…•í=u¡ŽmçÀ/°Vzp}CäÏbümº}/[?*b­Ù/vLÓm˜XÖÍåÝnGøH‘Â2ÑQœFÕáãóîŠ+uŸEÐrâahË— #TüÍê}‘Ê®ÄT.qâ ð¤uœøŽâ à?üñ¥œ‘ã}~ÀÿßÉÙ2.¼€ý[&ÿËG’çl—FÇ’šË%ÊEýÙN¼QÁƒ) éy­÷èÌŽrHÄZÙµêªR‰ïAxÐꆠšl‘¨T +ÕZZŠ0ÿ>Nßf&áüv𾿿“6=%«zå úipûÝüíðÉUœ pžœlã¿BDKö*¤Ð-à©Ã\K™t®\,zŒz€ï¾ùFà Ú•` (N’èÎçÉyçä?ó»ø6€8c½h^ælÉÍF¡TQ=°,þ%(^(@WŠV¹lïÊðŸ’è ç­<¹rÑóÀXýý·ÂʼV)KÀX€ìÿl:ÖvæÆvÖÖ¦ÆÎvŽÿ1žî§,‰Ô¾«ºó¡3(W&/m·Þ•UNÅã.s3s&J´ˆÔa€P°­Éåß»·Ý+[‹ü„”žJUR±ÀÛ·•»®,âWBM”Žñ4Rî?-Ôl]RÍ—áYujqñ"b×;7ÑF­´ë™‹,eãwQ=ë'"ª$ž¢õ‰@óÀ±êI#½zÇ™6­êßÃ&|B/äl&tpŽË„£võÔ-îÔÒ*p„ƒ)’“h~Ú÷VÉQ´³,2ÅÖýkFʵ`1@H€á9(>zb ÜwÚo­Ì# Ρ¹Nš6srò¸¸8A‘a0.ÉGÖžt9²:ƒÌGFé„êý>uIÀêY7šM Þ( 8•Òâ)h´(¸¹Iœ šD¦ 5>á ^1Pp ¢I¢ uSiL®ý €ÃH¦Óì¹uçÓzüYó¥ xßÌݾ¾÷9_®¾.î¾Þ<9³ëÓXÝm¾œ]y»Úƒ5Rî¾ßŒŸ»çÏš9Žp¨ùû2›€gàÈÈ„1Š&Ì3Z Ô ë"M2è}jmÕ…È&Õßþ(˜uCm=k¤€(JÿnŒüâ 2JIl‘¼ ÿb7ò²õ›Z$¬ïÒY3vm ¥W.Hbq½dë”S§Þujj¿S!?äoíché=Ê$ص½>2m•ü…ÆqvšX©Xø Ïr¿©2+<¿µ¸û±V$+}ß©6bN !lÉ¢æx€e‡$sû‰TÏËÈ4ÃÌé!Jïϼ}±Þ¶ó*+ólîš)ÉPΠÿÜ8úuŽ-'ÑMõ‚³(¤Su’…äÿì}y{²§o‡®²ºàÏÔ}ŽVV ÔÎÙN-0øfÑò맨Lã¬àþaŒµ?Çà¬Òœn¼€ð’kï;¤$âî²ó/$BÄ'ð›5Çæc·_ޫƧ”‹SÀäEãÛ>JÈ0Ð`Ä NìÝ ¶ Ue¹l]àb»•\yÝïòugþÀ(Vòôz”ᵤ‰EC|̬¬þžÊ NˆÖÇLºÜkçšØb‡Íá(_Âtô}¹û8øò{`ëÉÕ‘1SGã¡ù+£©¶äDŽCD½ù¸³gøtwæ!ß¿~ÿþÆ÷W6£~¨UGeÁ·˜ƒäìévçý¾ÃÆÛ'Õ‘P~ß#:‹ÆÄwFË;ǘø|öèÝ=Rä´í¸ ,‘¹t “ÉæJÙ#ŸÐõo'®´MûäA;%]R>ZGýÞ0˜YÕ‡¾où”Á>ÜSÆ%¨È2UŠÜg7 ‘n ÝâˆP§Í°U™KsdŸz?¹;%>]ðÞe“Q툑òŒÆö» 1ˆˆì+‘D7Ž?‚:yÔCy? Þsiø¼ˆå ¾Hü™/é“õ_‡]ðžï¼•]s¿~>Ïä=ƒÚ÷÷ÀóG 1ôõN¶‘õýÂOYwC.M€vgåÏT–#ûWµ„¢Ù¸crF£Y‚  àéX\¹=}ØÝpcKYx"HtfÿÂâq9ZL§ÑÀ$ÑPÍ£òè~ÆCï{-!lSàë‹Uì¤UÀÃA 9Aø³ú¹Šýà V¤­›K4µÂ:ò/æì¬'€ ™< q¼½D4ÁØß¤%îoi  ‚ûs g?Ïþ!‹z+ŽþŸ!Ô´æ0w n™Aw¾½ÇWE@´©r$ºJî´+¤Ï>Ÿ'~=Ÿ"5§û“˜”q‚ˆn¾è8j€Ò%%w¥Pp–Áè¸âVÓ€_«˜<©úsîþy~.ÙðgGeQX¼üò´ûs—–Ù’Oèð†¥=`Ò ,ŽbYuôîKsÛ)¨礙k‘É_\r╯7f?ç)ÕÇÇ8q? ÙÓJ<€'‹…hõtø~ª:š©:zÿ}ÿÙxk:Ú §§q6ô…Èp’Ô!Fø'a®O8^SÐj2K  ^iº:} l¯>^SÊÜÓÛí!ÖVEƒbØ$p]IÂh Éeëx»ïêÉý xIȳ>ØT[äˆSöë«nØÑÁ¡,€™b +¢‚Aë®x2¿‘‡R©a Ù P›@®Yƒ`i(½Î½A’É –X>%(+Îç1L#7Îkÿ·kÿîÖ5«‘—/æ$öÆ2ÎÌfÕ!P¾3xÈüŠœl†3¼™³ïÇÞ"šäÐZЦh,»‡á¹·øhã@ÜÂgœ¯'^pu á2½JêÂ-ªvýûôa{K̪¿½ƒ ÃKaŒúÌ?OÏFв÷Ÿ“y îƒG,bxɬK!ýv.lˆ£òèP£ò'Å|t^4П®ËIa»¶‹/Y¬-“ ‹±‰žÛ/A€ËÓ®€¿Óœ†}L?˜°$æi7{N-ï¶’(‹õt±µÛqNÿ§Y`‡x€Ü0f’0º`®¨Öö¦‹ ?Õ‹86&©\앤b• ]›²1O‹å¿ŠÓ.ì=9ÕlAÿPt£‚azpE>?¥ÿmµºfw[Õ4­é™mOŸü»'šœ=ÕLú>F}añô·²;´Š+©ðœ‘D} K*º Jïñ$µ‚ 9$$ *Ækóg˜vŸ =RCc]v5ër!Uì~À0ëø]X@ôB/ÅŽÏrNáÉÚÈ2àB°3˜Piý‚¹¿ãåâ?|cÂÒ¸ ña 7+ÔÃ!ÇÔ®ÜfŸŽ_ø#CÂkÖÃ? ©‚áT‹h¡ü‡=s~‹)¥Ú(ëXÈL'-…Š/N‹Òb?æhJ{ÄÕ»ä4LÙÒ}lïEo÷Ùû ñ-ù!ß®°]8&Ly”eÓA¬¾?%ˆ8Ÿk´åC*bçt!¹°Jb9‹x›bp…V¾(ÏüÀóºf«.á·ÙÒã” n͈]-`ÿZåDãëR/¾ßrÌ;Aµ‹šÿˆOÄ2O,Êb9%î{Ìø´¸b‰\1³¹DŒd>'Ty3 <ê‡fZ­Ã BEz¹6 ª’öÖ½ÝÆºKÁèêk’®¼GQÝÇ •z÷ºÖE˜Û„5qa|óUä1e*Hbº¤Ûr´Glk!v‘bƒG36§!ñˆ3â['“þx ŒtRf²ÀÒ-ÿéz£ëTEþæ%;n>›Ô›j =‚ëèQSfƒ¬¸€’Cnäd;}Eo­lQbh|ø •°S¾Žßmõ à%/Ûàõ ÖY N^?gÎ'e¾õcáƒ~Í#+\TEaa½N ÁœáŒ9ùëÛ|+v“óŒ?É]”7®L\[ºm;Õ¹Q7t.Wð 2Íyjµcß!8&7a‡t)U±ªQC\(•Z!@¢&&«²T™¨ -ÔÃT• R-ñË W7‰c¯cQI·Â¢‚NKTmý»GÖ߯jäö$åV£˜ùRcƒ[oR¶‚æ9sÆÍt °9”]ûm¼ÝëOév—~ͧ1÷î"E1QIÝî`ÿÅ8ç‰&·`ðåwÀ}C÷wúŠðŸø¹ÛƒÅüeð„4AM“oŒ2oR©õÎá&ãÇ}  !Q@ Û%ôNRAÛɸÃ8‚è’ÔŽP+]]ƒ[M•‡vží2—DxJƒ˜©k©í ouéÚÄ¿„â¢ÏTÕ,Noù+D1áÇ@÷[¤7¬+…¨‘ï Ö÷º™8 jK—N‡›Ù ë“Û¾ºÎ ¡´QàÅð`ÿaÄCtu'â¾ç²&=ÏX‘òÓʵڋ»Dp¹w0!ü`z.6!:S|Ñê€öÜã`É)ÅŒ·Œþuht}¥âÕ8÷ó¬-ÞÍBÿàW8Û÷´mXÿe (ü®ph+A’.êelÓ8í ‹ÙvfMCÅÕ^}Š)£Ÿ;׆`å­¸i¿®Ì Ò$y‘EÑaJŽ“w’Ö6÷3íT³æ)i¨Íô+AN}ÄÒÕ+Q²c·3…IO3˪™cq÷@oR´äì?<Ʊ<}«Yg>‘ªi^r8-â°ß¶Æoí VãÛA[Ç+/.­ø˜ÀÐGÂbØ,k˜êº7àv ¿·sG,ÇlT,`Í+6,¤TKzh¢/å27’»øï” ¦c(ýõ‰ÿ ‡ØR'ìêkt‰õ5éY_­H º[îP{Ýbœj^vhÅ0Í1(èÕèá(NÑû´.É/‚Æõ>SàŸfý˜nm«im i¾e¿šÄØèòØ…‡AÜH«ò£YÛžîøÏØâ®Û¼åzI‡µô15•x Y|U½¢߀´Œ²Šb¼2LðçYÃ_^·ð`ÙãâÇhòŠ—y4µ¥,³‰ÁøÕÎ&2Ï¥õ$ì¤Ë B\þž¢~Z—#Yâ‡*”ãÆ]p÷¢’M¯áu•ôNE ï +‘¿—óGPÃòŒ %êÎÀyŽKCÚ¦+唲CªØñ¸Ò)ç;æDq²HôVÚ;Mö_ ªFÐÖ#M‡?õŒ•ÊÒ¤l„ê¿ý^:E†’p‹pQ'5Cþ«ÄXÀ*(Ê @Fµ#QÒžš_;EhíAÞI14Ð*ƒ/ æ uí­©Ù1áZÝ” q1ß«=çº=Ê!líºÁ#vÂäñÔžY20y“¬LÍ`Ÿ˜@‹÷¥ì×ÀfØ WçH:Îmàîœüñ},x!”³óÑ7 }Êé_Të”(° \9—Ôƒäqü:Î_&'«2u—À±.Ï÷LÑÛSâì¤9+É¥´å»Þ$ýn"{XªÍ zïê*#-*„QUé+@W9!šÂçNß”s E¦dµ²ÃzTBÜÕ{Lô¤‘ûûvmS4Ù2º2È#ƒ úŠ‚ƒZ~ôà uaâIåÊ9g²˜ã|ò6õ xJ0“Iäü×[¨— ã+ÙÚîr×ͪ‹8VD2ú2}aóîêèbÃõ/ CQÖ°íX¬3—iØ~>”Á7—_ÃaÿH$U1ùeRÌÜõí_R„å¯"µU?j¶· ó¼ÿ'Mp¶Ô“ky6§› ß }î”EË=`Ý+÷ˆ|5ËõÈl—zÈõk~É"JÄšÁŸTó¢³6qxA®þ£çøåLùãÌ4†Ç™ ¯ÈÕór Éì6TÏ9xB®çlØ3Ã$BãîA(³B hï8ú‚<Ñdz ¤ËÅZXã_N±¶TPÎs=•›K kØsÓÊ–[ .n£ WŠËÑæ×Q3§‘W¿áóé´µ,en ÝÚ%yûåÏ»ÿ*ËçÿÖ˜A[€ˆ 0À @þÍË·hi[ãhjñ©ú4tí¸rÇÒO«%Egìêdgè6s[¹š“_­c½ªpgÉ܉¥¸ª»tmî¬äT^Ï®$@¼2JP#©C¦fíBæ ðÜŠ®ðìîÀHðÜ·ïùÍVÐmaæŽ:èðÕ)Ÿ}ŸõýýqfüaÒÿíõ~.÷A~þøûq*C¤ýb‹ÐZbš·Rþ+µ5¡-ÚÒTœºt•[vÜf%§ïôPœ‚þ‰>¿-/‰´\†BÊDÍ)<ØL?O©!é<*d£Û;1rž*TKýé>—‰¼ÅTä&îT.d#…©#ÑCj4a±<ˆq2s/µ˜‹|;/ á&ÒCº=SêwÄ5Ýÿ(êÞ〢aº¼•å{G”¸1ù19)öbòrkW>4%å¡ÖùýÓ"ØN²\è…hÖÇ*é%Ù†.tÕlO¹‰¼‡Yh ~#Û]­…J;w’e‰Ø:¡dòÕñ‚æ©Q£Kòç/ð»B Ê| àÝö¼ñ]«JÇj÷]À;‚|:L™¸G‚Ç^-^®•eèÎ?0½EsqKì2îô í"ÝÉ9¼m¿Í=¹ ¾mtíID#ïQº×´@Ùwš§f™4Ÿç€o<‚=˜µ? ¬¯B½ç§+5·Ày¹Cï‰Æ8°'ÎF." 6J ÀäÙÒ¤@ïä¼=]`Ç:@s(œtìÓ Pi´æö=fω]ºéÈ…­‡0(pm±Ë?»y¹ ¸r–4Ä!ý©–RôNÏ ÎìFôl¦4ïÂó˜ OYàYI-s†DöÈ!‹RÃ*ƒŽÄG¤hà½ç¾½±I#q-8êÑöºóÙøx”ß{ÕªÙzAš7¦˜ú¸«Ûºm[Úìœß3Ï:äðÝ‹m Œô1D‰Æ,²F‰“0)®øSî—p™Ž­e")êë•MBïŠxतßüñìF>s7gÈ×Þ{€³ pº}¹µ´æ˜ûÞ(½5Ö2 ™¦ìP4à,dÿíXví8¦äë!ƒǶÉQIu•ϔډÅ_ôå÷Î ¾êÞ;Ûb4Ží;<­y+Îõ×Ëcùö8O —Ó›C†¯0T*X–`äõ¬ILx=†zE,´PNµÿsüI¯‰)à’<ÂX³¡HñÌëgá¶Â~Ë.‰ii9)©Ýêèú7¶`·¿švkM»ç¢¦¶–Zåθ}vÖ©‰=ùaß>äÐ!|MWæ¶.u7oªÖÇ-\ ’í.‡–Q]e”(‚ ¼Å9’„¾Ø‹#Q@0Û­E>Oùˆc,¬S_ýï6É JÆÐÒõÈ"÷wVýR8 Ȫwñ!×·ü­Se¡´Ar„ðKHVàþ#ÎnÆÍn¦Kû ŸnvO?»n•Š´Æ ž½¯É/äý—0£)}ô¬¼ïÁÀêi Õ˜^¿ÚF5ƒzö4§~¡öyЖ7Ö’‚ší`øpëÒ™«g³§ô”Íäá> ‘ì-Õ†íÉem:n²Áj[¾§³·‘p8§´©H ͤm¢)70^€¢/’\ƒÃZl£,,¨¬Õ¢iâ“t1‡ÐW1|(úñ,L³¡¯˜ä¢ '3I#þÀHÈ_Ü)Œ„‹ó¤¦×xpç39ŒøÐð6ôóMôØ'6qÿXs˜)=$âÜ•aÒ,œ®_=!¥8:&ü²(  ñ·cœjƒ‡Æ‘{î†Qô‚ó" ‹!ôüÕÅ /È{*ý`\ÜÓ–@é0ntXÒâYtÍ'B­y™ß ADm'Òµ[n ˆ?|,€eý6¬ òMš±Hþ•–ÆËÐd„—z÷ Õ ÉVKOaŒÜŠUBŒ`õ›+R·1ë9,Ûöͬ‘Dm’1ìÙR¦n‚7¦¿áƒ¶O’£ƒœdõ•¯Ï;ð\Ó÷wørMXI<ñ>Ä$C4ô‚½æ2v 埆Q¯ïÖ½ Œñ{áh^Q«YÍÒ„ã>Ý]œ­ÌŒ«ṳrøH¸Ûž˜Ú!˜Ñ‰½BS¾Û屢Á”,‹ÉÛ\’]ÝþE6޵SXk"Æu¾¸nl^l —Ë7ääìê(|Q‡ãß/^)ø¬Æa´*`ž¬—ïš Û_‰¡5ÃÜÕu®Rjæ»´dôUÌN¹rœ«PÝ^»‰§©qü¶™—³Ñ e»Óþí¸†Ðj„Ç6SAˆ,;¤œ„2ø]?6•Ç›)o\Mý­8Éw?‘¾Vtæ®ÞÐÕXríb¼<~ÝWw‚ ¿nòÞn³=^phñò~VDµßWýâ3ðs¯ó;G'y2„7q̸Q_š- [3Rô¸BÃÌÊŒ)XÍ2÷±Úh²,Ъѯšhñz ÍÕƒdq;ÃÚ5½aK¶“üøŒ^ÈPÖÛC{Õæ¿|X¡ui­ZÔT¶Ä4w̓B4$H½ 1ÚTÍ ñ5Åj.0 ©|~-¨Ä‚‡íâˆ!6&¸¨´r²ÔöRèÂ$Æ]¦ÒqÎÔ˜©çé R`~Sba~Ð@“žNLKPC`N)Â×Zá½[¾ÞIÄV¸^ §›hW*¦bO.:<<iZŽƒÎ¿üÎciL1©Ú&¹ì'3½ª¥-<°M®†¦¹(å΄÷&²a#¢$òH 3#gYXäÖÙ.?Îmâûfrd;·ïôÕg“Z“¹l¨YËd”å’›Ÿ–5ŒéêÑ$.³T°¤×…C…Œ$!ŽT ¢Ø>»nêWñ~\%š¤¸ÉøÖq7ØÓ¦5Bþ˜Æ{ŠçP¤_ì Û™$Ìû “6ªV·8è×ÁÝe¬‰ËºÀgåõOp=ã²²·û c¯ñrýÝ¡®ðB7϶w@Î7·GM‡óÁeaAiœ»Kš‹\ÄÄNW 1L×Ý©J (p‰ká1Z‹¼77ìž?>=?{r*³4I“9ŒHD$F]Iå:Ó§¤ßáA8Ê.uñ/ ºÜu"ÝÖqްù Þ¤SÀNa2u—€ew•· (È.±W†8¼oG¸_6ƒ±ä¼ a@ê}vyÒÿÁ¤Ð5sÑß_‹ÓkëÍ)êª8]çóAàãhë%¯yCÇž#’êƒO^ëë÷9rLo(˜.¨aÕHñ¯X=ÜŸ=ôNV%¶Úyy5(¶™>×ÂðF @ÂÝD¡îQ¡éêümªÔBk"nî}¦•Ûpóÿý?6áÇJ+ž4’ÏøßäРÎi” 1dJ~“[Æ´ë:ˬñu\váº-¯SI7ÖŠtDéH&üTž–ž¤z¸“>e¥<«Åy`Þ°Ô´:ˆãñoÎU‘yRGS§ít¿Z)3qŒGjK~æÕ€qS·Ü˦Èü8ÿ÷ÑJ··‡Êø˧ƒnÙFZÜeI§,$u¥œÇ„#uÔI#¤©”?A<¤‰ /«›—ìŠæž3Q1^ßF¯ïQÜo©km ]E‰2Ü—Ä×öù()ÃbRe•»“u¦Þ-ßV= ·å&Äc·V •o”Ý.%8Pg×í/í³+©ý^|Õ*SÙÔ÷ÓØÎr‚áîè•D¹&ƒ úÆ–lýI`?éáÞ¥ïgÑ*§ Í×&°©9iFÀ…6B`R°sá=~`qw|¿ˆT4àšåmÔVXœ àœÍ’Û ïxðãUB6”ÝÄ—0û¤kÆ®M@öµ+ßhJÏ¡ùýÞSÇŠ1œãÂû®”À¿Ð£õáRÒ[°¾ìƒw¿{1¸^ì“w¶õÉ©îÃíX` ~” !ÊÛÏœ¢O™2µµ4/`¸ìîÜb „ÕUO ª˜ª˜B¶ý˜jd¡±Ä­y¸f=HTEî‹·­YýëwõÛ¶û‡ ËLçEÃÀø|œË»ÈT¶Æ’Ù2Sdrç^LüÂÚ¹œñàùó<±úns—9–ä^R¸„$Õ?Sýøt÷ ¦ÄæÌ¦jÑMb»4ŠxÌ–ã{ÒJzs ´uèøcqB(sSÊ@m$®ODßTeŒ:Œ£ÊbQ—U<Ù&†*sÊû,^¥`h)Ïa~儯\ÖSÅJÂ×µ £ÄDïñ‰Zá¿KYÕTG…¼€ ÎÐפ©=&-V¦ãZ&寰Ó ít(ÄùœæÅð`¯¬!ÄÂP…¦¤ŠwrxÁó«>ó´tÍzöËsæ}0ë^ˆº}n?åŠðŠß­\/õ'š[(Á_lø÷8c|}„Ò³.}C?Ú°³]Eì ÞX´Ðn“N SD²C óæDJ«zº¡ZEZ¬ÉüîÝx`åNdKÐ<¯/‹/C"[«ˆvq l‡šZzÓ)[HâŽÙÑ*,j¨˜=ªêVSÇ·u¶FØ+¦Ë@ÅPŽá;±¿é G×'åe© 8ûâÂ<š9y|œz½œYܯ©)Ö‰òÏpO!èu"˜N©.´V½´>"­&Ñ&¥ÑاåŠlf1Ç(xÛ…c¿Ï ¦©.@HË”H*I -Dñ^XZœ@o~ì}ÌLº‹©cð:…dÙ·mAé·LIA6Øf,Ö=rS~­Ë€8TyîèwqØ’3Æ2ØGfÂwük!ÕùéFw¸˜ojH‰ºbÀŽŃàÖ™LG\‘,V?¼<ógDeÛž‰œöhF% Ií|¸îÀs¿j˜?(ÎrKç@ˆÄlVç&UÔ¦Ær‹PPIÃè˜Ä¨YŽ«³d­#âl^ü§Q @}¥ñ<iR'XöPGÙÆ™§Ã868„"ýIè&Yî3a]ÖCˆh¨ aC8îý—fÁˆÝeÄxOŽ$• gµX•¥“WÊhKêúòô‘a—Aˆ)ßÎ)>¯\gkµºMl”ŠÕaÖ^ñå­Lš†Wt[Ê))š,™Æ– ÛÜ—²ÛW 3¶;Í:h‰šÞQœµ!d/³B«ï¥¨ôóõ­EVCm6ž;éàÉ…Î_„ŽXEŒG„¾ÃEíL<Ñ6ÊYSµøÅ×_áÍÓï‹¿|õ³p¤KØo”µj”E½Æô>íŸ5Íš­;c¿<¾ä c8?߀¥éCEèI#¸ÅõÎõ`c3ÃXeÊ}*™à!FѾWÎÓ÷!€dpŠ.»6˜—®aãÑØ‘þP&qô ×±P·œž³™¦y%¾ƒÙ8¥G’ºÖ‚çŽR\áêà±8¤¤¤ý¸”>ðqüƒGò OÖFA éû ì'D¥åL "ÏÔ±¢òÞ4D—@ðKµk<3oÈY9â Ý4N&ã&Hì¿]„6­„zßA UbBŒÂ«Û˪P–<*`Ãâ½tYP¨bÓ%'ЖÐDÞ@OÈ&r{œâ Ó Â%%ácñÁ ç í –ÞŸ}Ö0}T>]“ÍS›Hż”˘½c–Å…#eü$I̤›æ¢^XIÒ[%hxÆÔ¦¥ìlƒà3¥nÓû]¼Qõ›¸i}þÓœnBls™Ó8¥´ç–äŒaÙw.ÉœCc«‹Ïˆ»šÊîGäâ攓¤Á”îîqaO8<ßé…ÛD– ¾Âñ·8(ª®£QúÇÀŽD§ô ¶6áâé  }Åu~cŒÌPš8øà:Wõ'aÈZJ[knžs÷N>ÁcÀöÜÎî¿PÂ2GŠìÑñrjà°ñˆS7egÉ·ãZŸºáðìÄÍo´CR|ïÏÀ½_͇à¾ÍOË‚ÁÍÛoŒì@V&xjì\¾ˆ®¼´x§4ÿðzSë-|g õÁN·¤˜Lûq °ékÍ+xxlAzÎ*‘ÎhÖ­AE¿æš«ˆ´H6Yûâ¾ø®kõŽnßsáaÔl\–:ººCc¢¬Ùpêcì}Î%nøWWàRþ$Ä){±ttû_QúG[¸½tË«ÅNÙóúµÒü&W‚‹ùní#™RŒø7¼F8k³oä;–%ºŽ’ŽJ¸SÙku8÷šð‚]ø>G sMò—b8ñsbù;Æ!&© ºƒ1õgSËkÓºÃH<˜‰™Ì¼í'8ìt¥“auN%Ö˜ÊÆñÓ¢ jB´Ô9õ3 ÞŒÍN Ñ ÅW×q55†U§ 5y8Ü)%#Ç|…±È{;ÉMãD’޽ܫ¡RÆ/JihHM‰ja%õ;ÖIv¸É2ÌIbsC–»ˆI~½çô ¼a±$w§³Jcý"1Ízu(L&5?1)å“ǦNOO„ htè¢?ŒàÕ{Fú0zÝM _Š =ïÖbDÐÓ)ÚÞ§D$ óìÿ(ÉU™B6ñ…½ eýKgÂøÞ¹zŸ7†²€Ÿðª¼¯šÂJ"PúˆzWiè9ðüÇæH¥fLaE[\Ïêjjò7~‘ „Ô7Y{û¬ÿzæßHßS§\±?9|=·M~3QÎßóÀŸãÙŽçHFjЦQa>Í£1¿•äxS> ÃfÓ' niÍáÿ[¢­R­Äh@ ÀðÇ¢êÛÚZš™:9ÿ/‘ìŒÚŒÓ!Jß“ºAXêÛL3oópVü°2wº³¹<&!ÿUë±Ü×ÚÛ„k8`RéÎ$ŒAßÛÒ×ï¼ÿ¶SiqjaR]ÓQ¡;QFð¬'°#éSΡƒÑøÉ»ãv ¾dŠ´(lZ!Ð,p!I„ b@Æq2éý4f¿€ep’´›Lo“¿ïХߵÔúD ‰³x0Kñ¼?Q;¢|@?rÐ6D’f®#F|bV±Ò„ƒâpšX°v¤Eä;Î^![ìü&ó—dô„ßCw#®æ•ª—%ìòît_ælPE˜º8ž”2¨ªNAÕnçæÐ9VŒ¥d1‹š?qMOÉah”»ˆõ´T%@ÉY á„Xmr9wÂö?( |&|âûà€ü6fâûªêŠôÎ¥IKú˜Ì3i Qúhé{Dwòî"Lú@ìâïÃCìW(ÀrŸLiÁ9f‹FØLid“8ëwÆü$= 0ê%ÇM šloX¶Ð WRÊuPV âs™¬)l«*(Ë‘¬dzÿâ•Eã t–òµÃGØèÊ›þɲ¬÷̦O}KÂüˆyþò1ˆÀ~”œhi9À2e$F¸+¹ßR:)ajCgþ;:jé»~y%·«€ âõ">*¤Ów bU›ª?azìg°;§øg±p‹6ð¯#¯U"bYl¾:†Š²eçˆiYñê–ÅìÈx=iëÂû…C{Û#†_/= 6.³ìá.] :‰¥:ÙDx¶n2us|zAæaÛÒŒ ôdÓ>÷ÍèÓ„Á WЮÝ9¯ñ9|5ø\ÑxeóGù†“ëd…‡G?û^3Eh ÜXûšæ°Ö•D€ñlº‚ªImOy€xþ4VJrå$Cµ/> 'ÛJ£;ù;åLÖ¾aP;Äï‰ákµ±ÍY;VB[5‡ýû­§¾SÝqʦÐzÅ3xB³m·EîšÝ»úhÓ~U ‹^mÄSã§ÓÚ}r\ÅögÇÎZ°4ùß‘ƒ¡€hr€oÊEh:ÚHX>ÎYîÛÒºaP´í6®Ý V1+aêrͯÐ&ù’MV%”áÖŠ›N?ýª{s©–QX5Õ,÷Ž ]k¥._gÒFû'ÖÜ¢çm¿¯EveÄÕ¹*íÌþ¼]r{˜zâÜPQº˜Æ©ìÒZa¸ÔoìôÀ³¬Xïë`ðÉ+8:>ÂÁzͤtÞ!Õøïg-ç–À¹j7Ô©-Ü\¿šÓ]Ωß[¹úŽÉ6*VP~ܶήa/gÁMäuúó"ÆÎÖZÝu z›ÿAÎSOÛ !Ü×Ä€¶q¨¿Û–¥WpmXÙVËñ¯8õµÚb3×¶å9.œÂ2ªvmq¿ 5V—·c“´+CÈ]ÛÁFS;•0¿èöu¶áÛîü×s^².võËšŽ]8=‘&)îÝv,ýŠë/öW}ŽÈ¹íÄ¡ÿÿø¯ŒgÔvl·ØPÔ´ò±aPnˆÝ%耈‘¢ h]a¦ÐÈ͸…svžG‹$¤e€ü®ÛáëÁ×_H?¼ð¤"kÆSk:4Ýør‡œÜ}ŸŸïÎzâü|ž¸!/û€Ý}ƒÇºƒCÀ¼4àßÀ}¦có<'ÊÈû%Jåààá¿úþ¡:e¼Îµ› SÍd\ï"‚Ð6ˆ •üþXð|Zú²y®]Ù¸êd1Þ¼ýV› *—5œH½Îu”» ¨VË#4À Í€ …²ÖOUm¼<;3Y d0©vOhf3Y±QmåˆcT5Û¼Ÿ?qôäÆÕѵônâ‹8™-²Y¬‚ý¶(^[«‹çO„À*€7!šWO ³ÐÓLÖ^Œ‡9õ¡aO_@Ú¼/tNˆBW ³°£i›òG'Þè–'¼^šŠËMR?;šÑâ˜Ò5:š€ý[æè*-|¬7 $•Oo e\Òº CK4 ‚±’d‚ Xµšÿ@w±ŸÉÓ÷ŒõÚ>Ë-²ŽhÊ ?ï“ÿž„®IÝÞ£*Š–Û';µ3fú›÷@ÖB¢6U¬Ôå¡X ‡qåL¢Cj+øôLš®MÖ# £]‚$›Þ¦Ž¦Î!¹ÐU"¬±Ó ¡¦ëˆY}g³£Nd¤¶•T[yA[›¦h°.Ü”ÔÏJ=iNe7CÛY†z1«ešµR.£\&ÁoÜ\ù2 'ß÷gÃ5‚—¥6¿kSrƒ|ˆÁö¬ðà÷95qô yo°Ÿ˜ä¿ß¶l‰a7aVzµ—ª+dꙦ«+¶ÖüÙF%‡S¤¤¶,hCz4úè´vŒ[¾îúê¼ 5dÀ€´<‚.= *… Eã^ß<±±Ðư‰¨»ò _¥Ë6‹ãͼ}ÌÏ*|ĈcëgƒQf+·*ü‰;] ,ÜÞö˜—¶È7ð¡HÓ¡†ŒY_ÕsyG9y¿B˜eªÆÜéB%5!ÅUdè¿áÂq†ÿøcàùûØ*°êvÕýÁdú• «ìv†êãøæ¸;pb®§7ÈÈ–?Úxñö|±uäjËZÿ°;zhø Ü¡3’¾nϧ aCÒé̺?íj{j«C”.`â@L¶¸{ܨ,¦G( ¦ûùf»&£Z}µš÷©¥€yÁßuGþµŸóý€ùïf ßHÿ¸®íÿ8å?îknú¿Êµ7Zv[ˆ;~ gåÝ\ÿkG´_yúé´Œ+t×åºZë,2_¨¤3%V^®>ûîb‹MQÊ ñØò²œôÝyvøˆÒ»e¿O¤âŸ–šòÆ©÷d‰ð<€¸9¤Êï‹ê]–¹ˆØ{§„fÑH`ÑiF$øKé¼Ïsé<†àŽ·ùHÑš]Ô»žØÖÅ(5—h!‰•fφŸ€ÅÖÑÓFß Š\pÞYœ17ÞH7,° v-Ã-è(”óã NP/ P• ¼Õ„†Ñås­Ù°³¤ o(4ù Qý™»q¢Kƒ4à:`G9%×û”øUÏ´…ŒãÇJœâ™ò±‘HƒéÁ|ˆ„<Ø#è¿í7H-š`e1òi¼éür¡†42œó¦E…oå'ì7ÉúžË†,ÑváÌéÇÚ˜[ù3žÅ› ¤F‘9ñta͇ ´ÖŠù=Åqt] •±_Iå|Å™A7­'Ó ýÑ)ܧuöžtç©uJ'Æ™>§– ¥WùÆ&Ž v€ün /`»+“ikØÍŽŽ_Ôª‹±yü¢‹$Q€á³91®§ù|`–o\»\i%©¾ý§Ð$pHïií<$›É ©Òï×¢Ïc«wHßý€Q 4H£éþÉ^”J 1&ÝiG†ÿæ’ÑÜ=CS`Aa[ㆴ¦A¹çÁÊjiÕ d ç8­ø‰{r«­ÚzÜUyé¿ÚÕR“TQÈúŸògÂý]ô“È•z•çžIÔÃì«j‘>ì.-EßzºóbÈÍVœ»²¥ß–ðy¥*RWþˆä¨U€¡|CYL Qgþê,tXDV ʯˆ“îöá$dè¬+6–íøÈu8>8{?.ýÇ è<à­`|PK“¶6Hv›HC®ȸ0ƒ0!Üh C ‘^$>Ïßw&>ݹ}=¾/Ýÿá{ÝáSÎBŸ"Ž=ësÄŽhî»HŒ\Ç"à€sxþ¢oÌoî#àö‹Tq2|Ü/\+cÌÐza¡¨ S i¨^æp"¨%àÓz¯iU‚ô ß×íI±(‘+è»Fæ»' J¸L QçºX;’D/´(ªE«QG9ц a ™üùàÐm¤" …AS‘Pcr´KE&Z¨ûPð®û„¯Š;n8Ü$ä{Œ7[½Da«N‚¬—ëçkê8¤Ó|*)òn–þ•I"¥·h¢®¦Ð×ä.”Û½ð’ªâ5Û0FðŽŒ®¡Òz•^®ÊIú*@Ϩd§¬Jž-oƒh¶»ÇË­wš&žÅ ®åŠg¾î<ªÍöh$‰H7¤JÈìÈ,íI‚çˆxà µø›rç²0S¸Oû*€_;2;<Þ†ÇBÊ[°.Äìá€} —!;’dæX¼’ cqŠï¢ÇÁ …æÛRäBo'Õ–à-õM—~ÝÍåãçfGómm0›ƒY¬‚Ø‘†>GbÏI®zNúšŠÈc@ݲ‘âúƒlÅðXWÃf ~™sì+ .s‚›Àj¶O@C"Î 1I`:.dÖ‘i¤TB†³$Ð>Ò8ÏaƼH¨Ÿ·ÅÔ*ÁÿÊ.CÊ1[¬ÄñCNØùõª- 9¿jÆ7§%Ú0³³yL$eJ ÝÎð)ˆ¿fB˜7hdËï>Vk)„>R£ —ºj§Îi½UÜŠžyÿþ©@‘ Ó`õ(ëÜ8PMpµ´â¥Òše[aû>Ì4š DV^¾ ×LlÔòÍ<Ó…#ï}Ç1ħ)Ôa¬<5Ž÷Âã›OVy?Uª‹¯$ÿ‰Ó¼þ;òåL©H ?X$è?dAM/'õwP C•]Zòå)›Àã§›x3Ðs³éGÉÀ _1؆-?›Ð\¯Æ(Ç„Z2-ó}kà_ŸºMý_wdž­—Ø‘Rv†¿ghO¸³Nq¼C8B°#e„Ð= €¼Fu©Ý—€Ã#êqdÊ‚íu`i|±À ·!Œ JaÑ;­—yVú¨Añ¢7Êœo)ø0a¡TMp ¬ªåEY¶a»%gy!åƒ}Z¬‚¥°´6G^žqq¡ôà³¾x /è&Æw [7¥Ý@)+¸ƒÖA$,N#ùiÕ8 ·™Ž$U€ë5’R7XLÚË B¾$q两¢ø¾llUT¥i6Qý•“£¾÷ï j^ S­Ì«G°ÃKw›L*¢LRWSf¿=À¾"‹ó(1ÇåW]€˜džº°ò¸zºÞÎ!)W¦"/N¬sÕ1L‰Î«TËè¤ZK«[M=³x{][²;¾ÂüÎÚS~ÿšjÓr5£uñL…Þ¡\¦uLª°MÒÌJÇ›Vx´¸ ù›Ødƒ^ ­Oîêý+—~uWL[%ãÄ:Ô¬£kƒÆyÀaÑøPÅë“ ¹k Ž›Fˈ©E7†ì§’†ð­•pð™ª_ç@ÊÓUL@G‰1Î71<~ ÆÉáõ{]‹•õ5Ì''Ž‚êÔ8pœ&”_¼bÑ›™,Ó³4®:q8‚8ãÅ®ÍÎzˆª`Ææn˜Ù_1`ó›¢ñB3=ŽìºêÜ—¿¬ŸÉîA.UNÊôèºÛV}Yášâ†¤Ö˜ƒïH¢L2. fEÌÏÛà›ï®-ŽÅp Èsˆn÷èÁTV䑜T½C éPRòänËÎ<’û©Ù×éȮ罧0KŽä@Íx4q˜Ê÷Ñ$ÖÔ¬$Æ…ô¦3Í ‡ð=»JR=7E鈣C]›Õ?.‚Ç´loñŽmòí€Úa;‹-Ñ›×L;‹­ðjšµJ·—9dÒ×]ù B}5R“/v:Èb@Ê ª~dä?›ÓË ÿù’[Z(¸Œ!&H8Ê—öEƒ ;¤Ék@¹7îÖ/¦™°;‰d‰ÒÖç‰1t†”½ñÃåW¨L•„˜;8“:.Å:0·e–»™á:9€ØÍÑät[ÿe» h”ð:ÿ%Qt» 7·>}?¿Z=p÷ÐDdðjÀú¤·Ñ þücä𩦉åÛU[;;Yÿ"`Ë‹ÉýÒê¹-~ø¾hZIwQ~×ȶã4Ù¦˜Z¯QÚ»­ovéfq¦¶ÕäwsóßèÅ7RMêSøÃÛÂK ,¿Xêÿóq›®~K[]Ùïôóbª¾1(„"ìfa³M055¹¥û4ËždÓ#8’a{>ào^Áìà© [UÞXDE»êºQ>jéêR‡Ð´¶Z>¤†¬fÛ½tåz\CøôtoJzwäÉŒ5ý’‰r é[7Ù-é£p«J)ñ ;¶îY„…+ASn#Ý O6¸íàJyLBÝ5Ê–ˆœ¸Jܼ‚HÕM^¯[>“Æ¢¢0N¾œŒi§í0¿(áYQàßà øWð‰¹d€kr)Ÿ³4aïmŠ­•,ãœO:é:uKå—~‹`œ\SvËì·~k«œLK^Çå‹q+'mÑeš«i^‰äÎÛ1š$x(P¥C2ÂPªFL Ï…P«a¦è‹£×øi˜‡0›^¾po|È,r8ðÙ^?¦z1Qü\\Oì”(-OZ‚<˜«RR–´¨<˜«Tbž¸(i˜§Zr–¸¸4ˆ)në#µN_n8G¾büSHåCIYâ;^ã„Ðä#è-VÈKU®ç§¸f®‡ôJ:«ƒðj:«ƒòŠ:‹1è›ÁœÌ;“15è¬ì“ÂLíʼ4Ø;š¡üCe^ õÅ:;íœðÀ•äû±8Mà8(ÕdÛL¼ }û·8w S "2FÑP3±Š|¢(„¹õ@d%6´DAºâŠÀ᫲ ƒ3•:¢)µiˆb?Úê*œF±aÝ4[%¼Á@ »f'.w®'p!¥(в’C²÷&xÛ§x1Z  ™¢šiõ}9½ƒP3êäOÏ4ÔOË4 aò€Îš”q$@ÇDÜOÃTÔOÀDî¯l2ê¯`2 (i"72<ÊéDJìÏ2±4H3#øN9¾™?‘£øRÍpøjY™éËÀ½“žÓ:µÔvZf7'ŸvZjuµ"3ÍWz æ™QVL7ŒLH•ÜHбÏ#õ #¥P­ÞZ€”ìÙLó€S3kÀæ.W£6õ`CáÑ©–Õ;ôø–>xNbª-·ÏfÓ¦eÉah±’Ý TvÊ—;QŽ [g†Faläi,5èF¹aÑ•ëã3ä:d1[ôáËóõne§¾ë{Õ©\§ßÖ»‡CW2ûsîÙ²aÁòs•رJwT¯äàÃ^©¢'œF ¥Ç-¿Gõ¾Ðy GÀx§ýœ»:ú)AèEÇ"þ%{ yIh/D^I1áŠ{%ã¬');>òþa«Õ"’ÚIÔôWH{?X“eç²Ðïjç/Ç‹v•JÖ&zåy–P9‹ˆmN¤kmZÿgcI½‘¼. ƒP`××1`tê´¿C:¨åsâ„à8ä.FkÞ"4öKÅ5ÇÔ‡¬…š¡3ý"»¼ ¤:ÌÒPÑCxªzÔñŸxù’HÃÄcþs2r޵€4»HWͶ`‡Û÷Ÿ’Ó¿k­ƒRΊbÃ\`æ´iˆ8±ãÀj÷YòXuËV÷M,Ã%+0XêC¸š|¼Ì²ÏøkÈ迆¦W1 ƒ"$ o‰ˆkÒ´Üàf°‰s§sd^è‡l¤Ð«µL$ȹSLó¯øOùWf˜®yà¾l‹<Ž1fçØÐ‚ÛÐÃô¯)d½½ ?Îi„О }ÛûJd™}‹uÎ…¸¸/y" ŠÜôÉÕþû.m–=}jTnì+fýb<Û·ô'¦Dj(«©*i+à2¼aŸª½eáï7´é•“•,`s€KÇŠ=Y§Ž°øÍ¦F‡Êƒã Œ¡¸ˆ~üÅ:²Õ'á=4øc\±Æ‡–ÜôuĤõ6ö2V^ùÈì‹GÊY0þ7Õx…°¸³ë ±ŽüaH“"Þ K=)Cß6¸GM|à;îU¾$Ã=†ˆ¸gh j#»Œ©—U;"v†ÚùV—<Š- ˆ¡ÃüVƒüÐYÄ+þ•åD‹~"ÊÌ`Ú ËW“B«,•>Þ]XèQðeÃzz<„‘rDža™ÿâB[¼,̶0«­âê 4“ÛvWn)S@¾þòßð×û ÷Òö~!Iklbq\>»Ä•†¼jzì™§ E´mÚr´:/Ë{,1&W(>d®Øjº ÁưQ‰-³µÝjç¬F‡oJçóµ2ôè½,¸Ç~-jðÎ×G×÷ rÅm>¯) !ÝêôŲߴ)º± I6‰Ñ+Y“ø,YîëVîØÃôa:?œAUôF¾|,? a´v¸I+v‰e‰5/ÉpÀÆhO¼d‡¼oÔ{oQpãÁb€vU:϶Ÿ€À¹·xyX³jå ƒ”‚mYß"<ñlúRª]ôÈŽY½ àÔ±ñuѯeÎL­)­¼{0E©5N°—šoÌN˜r£»5/^ØùÉçìæv{rµçµ×ÙNÌ”lÈM< q:($ÖïQ€ÛœÍ?2Þq‚m0áÝn3'A¬søãÌèÏýe*áå™·ænNø’üäH^…„/QŠªŠã«N=Y€¯[ˆƒaŠvƒä“=(˜è0ƲWïà¼Í¯±£ø>X' K HÁŒN+w _¯¸£¹²á-ÅI0>_­Áå…ú™%û0ÞÌ…7·™ d À¶Oºÿ`Öó»l?šq›`,à…ý²uÀUj7—-i:`4—;±?¸Ò ²™ÓˆO <,Z©mE¹v½:$*mçU“RœèÌírï5U%ùí*°”jƒÉ-Ã}{÷¶©Ä8g׋&'âXM 0^YMÓŸid.‚Wñ€]ò^Ì‹¿Ö¯ø{z¶,Èë" ‹¬®-£p9Ö™6w³U4mů¹Ž›]…ªîÀñRoÃd§š«â¢×lÝGxßÅðyéÜe=ÿ€ö¶¨[ÄÝÕQ0ˆ«i¸èžŽ?T™˜¼d(O¦†»jÇn4¤…Û"î-‡œ¹ª¼dqo‹b€²¶wÈR\³ÚÊ€JÏ6ú•y]§M’]|’*«ƒÇ©Fí†~Ït_æ]8Ê<ÛõRÉÏÉ*6˜+{¶zè.lA\%BŸø§Á¼¯E®_è=X ”òfX«-¿Ÿuü–_3 ãåW©k˜æ¹› q×ÈZ {ÊqWÅü3~î(Î[‘·òðÑÉâá+ÍIäk)X§ê(Ø±ŽŸ¨IKåŽN¦y¾ó¥3Q®@jCÒQ´N¥’ÖL­7׿BQ†K`¨²_h,”u*˜ƒ­ãšjEÞô%êü‰’Z˜¥ÐÌNh$¨ù`FŒ2HhÿcY(Ä](Íê–Ò´œx¸°u¡¯jžð=ùT­³9º.Öj¹Ã`Ïô]¿Æ—ÜS=0—çëE/F¼·Wº¶|­~Ù[Á3õÔ½¼Žs–ä&5ݬãB,äàžp•8Ý=ïÚ®Žq+Ÿô؆ÚýÊŸ™ÌÖéN¦ñKK`"Èã'âVp(ÙˆÕº)?|_ËAæW]·&:! ñg:‰oiy¿çÎÒ&¡Bb»ÉOþÓ@ƒ¢ˆ÷?báÀš‘gØÊ]ãž(v™]:P:Çá´~’¥ô.BðÑgBÀéRT]ú^‹ÝW3Ÿ§‰6ìªRÁpL‘*²lãå{*Ú©V£:][9µü2Š!“ƒ¿° CŸç‰™~"J”–ØŸ7êCøR[&Ô%––àhL4êÓïE˜÷µ‰í€ñynÐáγÖû½ùõöWJ É< ‘‚òvHFÑ!Q\œSÉGÉ€ Lˆ¿üXNގ͵Ÿ§‰ zåŠBðgà´+ÕO¿iÓ`ðèC˜€Ë¼.feOçy<Ï@=íú!ƒ§G¶þ·#^Þ‘)p»°ò;™GÝê¤Å7UõT—åC:yÌÐId®Ïåeq•€'Œ3êðÎÑrÕ¤hCcÝvÓ›b¯Çc$0ZÇ=ž~ æ‰ûŽ–ÔßßJ¤|Ý‹çÔ6òH€÷"Çî ekjÔVè—þ}ÿÛ#^:žXX¯‹0P9Ï“{G݆yÜ6É8‰[aM¢%š´Jì4°ÏkéQ³Žl-¡0ñò»æü ¦Sï>*ñÇW“CˆzÑ%ò]‰]Uã²U8ÛF&'÷t» üú6‡ÞØm¨YçÂ`•ŠRµHE´ä_Y=’É PüûÀIŒjô÷†¢Ä¨AÙ( {1™eð"ܰé5É$rG= î3ù¶îwG¼ ¸wrÒ½žm,¥KàÍ’×gºç î›C-îX'òwPÛ-ŽÒ˜ßàŽröæu’c0-‹G{ÈPçw)¤Z§yódÂ{ûHL ªÖ²a®VÑòóïÄÇ !üÛn¡K•˜ŒûÍÉ3ÏHæR~È„ldtð~nÕ?}óïÈ •W r·ÊÎ.˜^ûÒˆ7ÊŸÔVË ñ7¹ÍÿJS§ƒç°ÝÀOÿÛP:-ä“)Ñ’¤ÞyºÊÀ’cæPvú#æ/.üú´ù½ÈœÅ[C[º¡{Ëñ•^ì:D¸pÊcˆÚ0«e¬ä$…è*˜}UMÚ*YOmô(ïuÍï’vº!ý Í–:Ûð+¾áÀ 0EÔ!ÿ¶¦'$L=¶¶ÏÞÕÉ¿«‡¶ŸÓØ<EØm†ø4.¹ TŒ ¿'·`Ç+M"׆z·“Ú“\L Œú“AWZÁQdš#ÆÞŽ;@¿L×áÂ]ø=Iï’p»¦ûd.¬ãôì߰톪bª4u»{³ÛoM÷þÕïΖúÆaQcIEËß“ÕMܦa£ˆ¬'udߨ)9rSé)é#‡G/¹ £+ÝÕq$¶’,lm9ÖC ê°ÅããmÙÒã"kõDIy°DC(—À~]Á9ŠŒ¢qy>–äoß\_¦ïƒÿüÄC÷ލ·Úœ6ÃÙJb®Ž>ÓW_&d~Ü©ø]ùˆ•É3**"¬õ|bGòŠÿ¹‚ÿµ­ÀLÓ° ‚áÙ›::Y:9ë›Y:šºZ[ë;ºXÿ¿ÓóN´lœ¶Ø~kõmKU sXnvJÊf+®ÿeŽcNÈ•&™id“  LqÅý}ÝÐ ëå×âÇ2Gˆ"iмžl ßKAzQ¦]©LŸfòrû|²¿jW¨8Vˆv&[æ‡È`ÉìS#>Š<„›J+—že¿¤ Bø]gÁyM)ƒm0ß%·Ø ,õþkõ²Êh­7‹X¦QÆ‘€)%:Þ ñ®ÈР@ŸÒ‘ß:ç¤5Žp8d—Ú=gð…tSvÃfaŒ^Ó¹:‰JJZùw:\M¾´!i V™¤>”´`2j9é‚äT¢1ù—ÒêÙd6€ô„½Ï§&SJ“^‚³kñ]«T ÊtżA%Á©¼XÑ>á,ªÁéVoîN-¡ÑÄo8$™ô³CŽ›ÿ`ëþ2uò‚굆÷UTu`0H7v}rrníÎÚ3°ÃyçkÜ'nŽ;*¨RÓ§]:ÎÏ@©Í#”!ËQÇÀJØr\‰ðš2Ýh0º!³ûÈpSO*.TÀ„Z˜¢ALf¥•õ(áiLÌ6²[ý’=½fçÀÖ¥3H€ÚT¶{ÑÆ€äžTa¦tÒ·¬Åе¤B§qGsÒ ÑȽO¾€—>D±áÐ=ãÞiWFÈB¸ÌKÀ³Ö··±Qªã™^í×ÂþåÖñ›ú}påÛoÙöÖžÓw‡œr'ÏÙÉŠ˜ÕsÒºÙR";†~ï}(£‡U.øÉápi‰Ž~Œ_®ô£ñlèàúg%ƒ¿/&´úïäŠ9ý©7#K‚]GdÎ*ð ô霙ùPY¡€õÃWúÁ}Óp4‘ &¼;sº-ý Û$œc=6b à]32AdxƱºÒ‰¦BL`>zD2¼›]éß êrX¦`y0'‚-ç0K‹X'ç:²rè<¸¥™ L(ZÍŽmZE7*Y%!Å1O?ŒÑ´¦kdëÚÀ‡h«Ú>N¾éW[ÕÌ:»¾)C^+øv’’Ê=ƒÀzÿ~‘ò;䱜¸ëÚ¿&‹èqžCwÝz— €ÒSn ô ~ˆZÝ&o›uÞ@Z©Gñ†‚w‡ 7‚ºìi>GytÛÀ.&;;#L_ ÿf5Øî®¶‡Ú³àNµ.÷¨Ô£5¾ˆ±6šÏzr¸§éxà߃€X´Û%§ñæÍÒÉ‘Hét}½û p_n¯àÈËÑUOE¯h8z-gX+¦Nä/ê@Ao³J`?BãégžcûdV,Ì,J–é%”Zl˜ÔÅ¡<Ó,% áÎBE¥¨sÕf’ï"ƒô’É÷ioŠ‘ ÍÁÛaÔÌ%¼x"[oeø›`Ú¿ă aË T `ÍÊ×ìê¶™¤éÁ­8ý\¦³CòçGt¡¶ &H\ŽøQ@p˜1ÌÈV ¢$Â'² ¢°û§”*0÷Å^¼œœegäò¾LkQï&@Ú èš_èùŒ¾…îë¤`ÞŒ´ï1ñSUaMÙ¬àMèïEƒU Úæ*? 4‡>p3ÀwÝÝ7åúÀW%êPdñd¦¾=j6­iF *—‚ç Ñ©öSm€?@ýAƳº‹Åwxýs1*Ý! q¨,‘zõ_eð3Ûw}†{K•MKÑÝ¢Ò͸(õ^\ì˜2¶Æ&×(³8AHKH̱Œa~ 2¦U˜~ûÀ_âÜÞ_AÒÆ.ãýõ…P‰DËÉE™7“Dòë2\þ\° |ñn2 ݉šÑþ9“)aG*h³ põŸz7SÇØ°¶“ÊQ£¦©«E­×°ðÀÕ“ R ¡:Pû€‹V@qLþG¶¬fvK°’„ã ^CŒ“µ:ž) ´@Êö&Gïh´Ëÿì6ĦU%L *r€ˆSš=y'÷ÑŽ#±/ó“)ä  Jûi ‘yP&7oB­”ÚÇ[Û•zü³¡í,YçØö—í@#2ÀÔr9ü6¼;w¹ŠV\—øíåuÓä#ï9Vv,°é°‘3ÀÛºÇ{l9ޱREH“ )¤…€`é÷»Ç%²ìq³è†Õ§ÙÎ™ŽÆÈ4ˆP^ñ¨#éËï=º[sbîÄ<®c21?—³æŽ¼Ë¢¯nèA'¹™ö„Ê_†7&—˜L”dÁiæ—3nâ5;S ³f—ªXœêŸó‡n7€Í\`WM²Ý­Ijãý%YâÄ£¹ Ÿû €ˆ¬g_ Ç—éýƒ#tn]–ffú³i¡ÁàšFêa³†ØVØ8oY[&åÈ41KJ1Q ]áSŽgÅ4[Ì‘³ êquá€ve^7£@ K®ï²À…q]¨TÖb­\áÐPñù˜WR¬.d{ÍvªvÌÖáÆôÊ3h/e¯+’w1Z5õ&ÇnšÂö’"bMJN¹€N=.:°ßí%ÐpÕŠåÚ{b W˜! O¾# Kr¸ß,Xš²ð„©èh–$Âl @Ôgîáì†Ek`ÙgQ88’Ancü¬úȯ‡è¶ZˆHèxòãÇÉ“ÛÞQ"šSèûü)¹Sü~~¿*ŸP‡&öc†uGá¤ÿµøÇOqÝM¿ÅZ(â’¿ò¦Ù|¼Qm¬§!µœõU|9²x™r7ô€ÃÏé< :«Ó `ùÑÁv)u!¡oås@ð±B8Å[¼k|›vUж8ìø±#öcǾÕvm%´f|‚‹o}µ&-òœ>|Ö­óêéï½>Tnü©XÎiz­OЉnï¸à8D½nT||8‘[ö_A&¿ ˜Ù´î‡ ÖAÜ“‘;ÑsMU€KGîŸ#mÕ ÙêšÔ¨¦h>)¢¢êaŸÆ­Ý&V2=¿AÑkËë ÏÐS“ž”ðq g{VÝ`«¢Ûv¼L§jïÌ/kïñº÷‘SÑ·çòÏÇP¶÷î)Û/èÁ¯Ri/ÿµsY_unY5}W¾!U |z ô4”äXôy›Ó¿…U¨Ó²n°AÕÕ A:ÍDÎ6Û´º——LͪƓюáLˆìXþ6?x¦XH“ý†&÷Š’m¹²¬D1}^ž*8ã/'€Lðƒòì–©*†/RIìCð‹G cCÚX«³…Èi¬\ÉÈ6êhšVÑ4°ãH%£xìp¡4[ò®,ƒ[4Ó¬fº°}äÎp-Ä.½*+—;€\%¹ã¢ ÏAÜqMç.Ó1)+ŠÿBdÛÛ{Ogà&¹`Nɬ€¥ãjà(I|þ£i ýeܪhM­ýºyÆê†ÁóŸK¢Yù:4לN“}Ôwo~ÖL±^ªÈ$m5gÊ—Æ!lÙÑéîiÀTPçX¦æÁBªúYªZ—M/˜Žuh£dk7ŒÃÎàÇäNôÜÇhèŽë±Ù]·‡¯¥éŠžoû½7v²•—÷‹…‡å‡øýø«Ž·ž¶Ž^X,¹×tŠyw/tôÜ–.k¤Ç¾mŒ¶5æ³3P[ÏÞZP½v¡Ázèy‘‘®Ü(;ƒB¼—QLÏžÀíXt6’8ª9žŽy{ïCý©€x†Ms¤ÛÝËQ¥R¢ß‘ÏÎEõ¼w ú²9 pC"B™½èöM·NäH,ç]¥Wæ³ óYU÷ô-Pmî͸HαG°—û~ÕX¯¿ßŽv|­(J‚šå°á˜ïž'âþ ö 8N[IYdð"ä°ó±Ä3é,X¸ ­‡lõ0*ËÑïžRŠrbú©¥Ñ¥0²»ÿ¾ ×®!pØñcHm=a‘û©v&­qš—€vj; ãn(¶A⃂ÝaçƒÐØl€cᾂ<@7# Oú©·JƒZ»úvSŠ4W‡\Ñ¿–^ ^LÚÙ'Ï…š÷êiÙ\yCؘÆ7ÜdÚ°9hëàì:'ñ@”¶`°î³1ÁÈ8êË­F†níÕù#aGâ”ëºSñ3ó«0ˆêï@øÛ©_\và¯ä© jlìM|ÜÏm*›È šçùJ CzÜÛ”t[ªRVZÀWÅ_¥L_„»:ÝûŒö< vôuHç  m9³ñotjVCoZS‹eq>mMÛ®"4Lzºxï½ùðÞ³ä7çÍ=cÞ›PµÛÚHN(yM¢U¼¶†€âéD+CkÙQ–#Ä¥˜Ÿ6Yžkíº˜j„A¾Á²;Hç ß<É#ûƒ­™H²€[ÜŠ›jÕY²"€á~À`KÓ‚.YCPËQ¨†}(V¹sïô´Üʧø¸º>Ì÷<ö|ÿv¤œÓpŠõ<¥—ô*…ÛÁÒX >µU,Ü(}C d½Ë`ºFÓýóx¾eŠ/c8­”ÐTãn5Áÿ!yj³2KÐ PËȬbÓ?zk ÌaðaÖ·œB0ÁΣƒÐÎtß‚“€¡SÄôäùãVÇWW•#ƒÀ¢ŠN™éŠ*› ªv­Íœ©{" Ó:¡Q® e±n¹¥Ö@ ƒ½¥Z_)¶têôXíõö™DO,èhlj‚› H ÷L6q•¥ ÎSh_ì£Íà •$,l ÖUG’ïoÿ,Ÿ»Usùè“ ÉFÁ» ñEž<Šmr@{æ2_R° ›ƒÁ¯-¬¡ôÌ|¦©˜ìƒ;Êc4ÖËS.óúN$b!ë 37 +½ÙGåaÒ(,'-$Ž0M:Ň_à‡#JÞ<üpæ‹Q.è ˆ@’’™ìÙ‘öòsh –b¤XTè\‚Ì¢­#9Ãá(¥ñh‡ØâbO8U¤M ¤I;‰:Ìàˆˆ¬3ÞÔ˜/|V{ÐhÀ¸Ò[G*£Â’Mµ|¥ A«·²Æ‚nF‡ŒšôPœTî~)Q•`­v¬wÇN6ŠèrЫŻf(²H]&HžDg¨,p$%aÆÄœ÷–f§È9€c)EbZ ˜æGÂ(ŽðÊ¢¢<úO |XxΗ¦˜Sç˜W#lWg›$­åÅ×sc,<,Ù~áqð¡ê±¼b¨çóƒZ± yàŠï–jÛâXä„«Š\¶Î«%£!*¬²#kÄpEHˆ¯n;P_¹çÓÔ-ì¼lœš4Ù†3Õ48oºÃ®˜g35VôDß? gãšâ€ÂúãNáXj²K’û°lžßwH[˜›6ae¾È’¯÷s„vá8«ml@rò[´OºVBÉ^ Þ-ÓÙJPçp-‹7¤ã W‹ÈæïqXÄk>Ã<‚I}Å'ÿBÈR,Q’§†…†˜ctm2¶×߬’>·E¬ äOKW(OZå95Ä|‡&ܹ)o*±-ÂÄoÇĈ jtÀ1¡5#oK÷óbDdEÄ„Î&ö;«ðÛøCºR8÷#y‹¡5•æÃ£Xë_ :þ öÜ;Ä0ÖR4šA2“0-;Ô ©è«âÞ"”¬•ЫÈpÕ¥÷ê¯ )#B”´\ên,‡q£„Mg¤›íÁéÂÅ©{… ^„âB¾X©„Q\Þ½VCsµ)DÉé@R"€A8æ.\L‡ææHtOît†li#¯´^BhpA]Ö#@Ù€Ô×êD6- ‡8‹ö“læ£`©êð²b¬l‘Õ¥›Ò›HcužïM°‘kú g^ìÃìß„aÞ8Á¦©ŒbÒÉÁ“ ¥Àž|¾ÕÕ¶®÷ŒÐÿ 2º—G’ð·¹íKòÅ<äOWqÔüwO™¸BÚêÊÀzfsÞ H­v}v»""Èïw§ÿC69–×™GŠŠ;¸á×ï±@¡zõïƒ_z<Üd÷‘Á…«¨»œ AÅFowœƒ,§Õ7Äf)/è‘‘uÄ/)öf :ŒLOŸœ°{$V¦cyõoø0šÚM`4Di|ì51ô>Úš8ˆ¬¼–ëaÙûÈë(TKGÌm£xPÏ´ò÷}nn`\l|ÜV,”xJ©;“ØI,³:”F´SóyŸžéAbĤ³ˆbA@ÿ2@ì³)ˆõÖÄØjgYÖ ˆ·Çñf¿ 1j¿½ €;T½³jþ&_НSHìöÝ}Ø¿›iOñ¯z ð‰wÜcZ ¥õf¤<èo¹­ÌÝͳÇߌœ·°,íefxËè”…CÌ8¨Ö¼ÞÞZý?‡¬ñÁ- ßê©K÷"©Fú–»Sgü¾Ww‰jU èNiKRj”rûB •"ÍiÐsŸ«™¹‘ÁÄñχ´Æ-Oäõ$¤ÂÛÐy’a¾R D/`ýž'J “0Dêð®ý‰'ÌåÑ1á×=Í=Ç‹¨#)ù@]à,yK^ŸÌbß}ö„ Ћævo/Ï0·ÎYÉ&›ˆ°;“­vœ½U;zÜ"ÿ0åè`–Žc|qgT´Ó ï*ÍîМ8MwcÉÆà$´¬ÿ E”±ƒ“„õXJÕQ­BPZNÖĉSEv‚4ªòGE„’/“þ4úãÑå;'/cfž)¥ÑðºÃZ¶)kY$‹ÿ½Wƒ½9¼í湄{-!¼eimÒ…‘þN>w+ }òä,ÞBsµl”Y>qI”ø‹Ä/2n¼®{{€ŽÄùX­Vã.|roÜošhQzfð0’_C¶Ìø÷†~Ü›…¢ȪàýŸ)GÃÿÕÊ£ë´Å‘x÷‹ÊQªTÈÆD둸B'—3&g«“ÄAÉ1­–mªHDb,›À˜x”³ñ×»"¡ÐȼÑå¥Á§ÁÉòù× OÓ¤=û×JÇáE¼ø¢îÚ}lÙñfq‡È=ñØ%DS'íÊ‘Ëcn7Ò=nÒJq ˜`.ªIS`7œ]–8f3ÚmpÂj>æÁÒwº’„ø‚ŽÚ4d0í³!¾{Ï8¨ÛjVu[¢9±š A¸!D’ýPç”›&Sl[ó("r‘ݤ­¨·Ä„%@ð[-hÒĸ€=wÐÂ+ÉÔ];Å”‡‡³½» †Hˆm2òH |2Õ”¤~–V÷ h˜ùÈ(Ûž\½ÿׄÏÕe¬÷>]„i!ÒFC­ÑLÃÁCÍ<üÖŠP"‘FÊÓ§`D8%4½Z2R Å¿p©tЫߨ$÷ãîíáÊ „‘7Ë÷ý|{þzñ~xøþ=´Q%ýz˜ÓÍÍÏûeÿz‹óë5;÷7íËÇÍËWhú¾Ö-ð (`}Ìøxè ZŠÈ™’¬{áˆKË©m†­W,7åÑ€„B ؃‘ObD*µ¶A •º±õÄcwGŽ“ ¤¶ 9¥¦Ù ÖJ4ä.ÝÍáã%c‘ý3®¤·k6K#ð ðPM¡IVeׯôSƒB­®…¬r á 0¢iJé] &šÇJ¶áiÒoðb3vïB‰„™ßyÈž­æÓ·&â1“Tœ©ù×ÔFѸòAú§µÈÓ#…‡šæÀ†dÉc$ ç(è³¹UÄ™àÜóTÞzî³+,Œsïè‹ Nê`ÁÀd‹ö@uɧ9uH0qªh$¿ýÕ>ušÔ­sF8 kÜ Èz¿Ä±M°j[Š€L5D)¨Âg0L“´ý¶ ŒYn™ÆòloÖ×ë”H™ˆÌB €¼à£K*Á« æ4Èž }Ïù(ƒå „±ë‘ê€DY}ß“d3|œa‹Îèß h†ß e ”P¢¤Y¸å1¨î39º&—îVŠïŪ՞ÈjÉn‘ÔS.Ð#ògl¸Gø#?ˆôqj‰1âȱšy¤ÐÉñ|ýszÁgdl3t!ØÕ¥žŠæ^Zà~[_GH'Ûé–$“|ß#¡+úô¥øÉ–ù†¬%ø¡Æ…½¿—7-Ã%9Oä'šöýúr·ÈŸ ˆG>—›ó{?tµ–¤Üú§4‡W–d¾d^¶„ï›ÞëË›“ —“-> Ïœð™WCž7¢RO~}%ê÷&DŒV^ ‚y'NsPa€»~p?5ÿŒD¤kÒûÆ$`Üáï_°ŠóÃöËèñ¼u|§@]]ؘT“Cœ•Po´MFBB{:QQ Œ‘yÏ)f.2ÅRäÁñœ,V)¡þ®†Ý0J ï¦ø '„xÙ<°L²Ý mœûÄ’ZÍJOß®ú¯ AÆÜŽÀæ@RRTÔ Z MPøryQudí5Ýìw† Þî]—bd/>iŽ,lµH6¹ÚãåÞþû™ÿ²L !Ü…¢É…‘\Çš¢ÜÈÕÞ|bå”@€À#•ôÀ]A>Ga…‘°=Íž4ãͼx ‘J!rØ?yn`±è0Öü d?=zY’À`à0¼.~ʽAŽŒŒüA xšˆñá¡ËÎ,ɯZBÊ·b]³ïMÃVò›úx8@0°Þ Á¶µÛ‘ù©AO8Œˆ%=>à•SbØŸñ)4}(á:)ërËŠäV¸Áv‡¼çÀ-½U9(P¼Áîr£ž9dyš·b¡ÿ–“.ô‚öê°ðr ‡O°Y”Rù;ÃíÅ…ç54oÌö¬Ö ̘}mÛgØ;wdûŽHsI<èbkh~>é ÑRp/DF“›Ëh×Ã[êpM´f­Oj*„¥êeöü÷Æ'71+m›…ꊡ퓊¼ÀjÃ¦Ž±Å®gé߯´‘)‡þÃ#WÔ•‘Iø¿ÊnéòH;Hû5»ÆëPG!xÃóÄpSº ¾þ‰UÓ^:É4Ö½ùN8=ñ~\dµŸDÁàÁæéãðîìÇŽz1ƒóÕÑþئzhÕ Ùù@âµõM„寸Wj .ÔWWØLQpb¨á™òtaW}Ñ.N Oá|ÔÓÕChFj¼÷\£ˆØÀI5€õ>܆TZ”©STõùÒf[‹Q:‚’ÇdÞT`„V­ž€èfãësú€AHè2·–ççÇÐêŸ1Çðq+ðÃW›%„.\üì×…J–/tÖB]+Þ|¥­‚$šHaê™ÅŽ„š…¥zlÜfËo Ö®èb°0pÔa.‚”<U\!2,öiãøŒFNå/Ü Òjæ!-mÂ0{x²·%YlY¶q—EE ?€ D󝝸ã G|nf­W Ðû®Ï ¥©B£Uނ̪€»™t0t«ûgŸ / è˜Ã@ý'WHC`ú—Ð|f¾ðE;8S)‰ [#œ4)1Ü˰٫8Kƒ#\’·‚â ‰Xâ6”ªh Löþ[«·º˜¬Óœ"¥©$ÒJnªc‰ÙÝÄPºŽÈŽ9wøÇé¾'ð€Â’Ì}":týj¾n¾Õ ½e°ú’r{JÈ8Ûz~!` 0Ñ>¥"#0˜&d®CrPLhâ‘iÿ±ý6®à;?£jŒ‚øiF:‡7¾˜3Á9¢ rN§™û’yæê[ëó† ?í$ cQÈù5V™`i€ÀÏžá¾’O ¸úx‹–~ó˜ìTj×£–Ø‚| áÉà5p—¸h… Õ¥˜h´[‘Œ~ìy?V”:ôPcûÕ*…~€VRžrùJh‘EÝåáõ½i×:ILi&ÕŠ¨ŠŸ½î‘àvØ”’å)/zò¶¨¤ÝàÚ Q§¥›`ñ¿ÒêVâÂÎÚo›íæ³ùÔ-´hÍÅköýœ º#ù¤ÔcþŸXjÜkcs—T¼ä²N¼¥~Ôý™l%j5)Œae˜rž2» Û4$dË.§õ ­FýUWH;8÷œ™²D ó•äõ•sªjèÊ*<æª÷ ½=ý¶×ð{ó0ÅKM—ÁŠRýݬ¯7™•–h4QTÆ fœú8¤ÄcA,Rç©]#/‚ÜÅœæ[q\Îp¥ƒª†ÝÆÀè=A@¦›\çáø@ðD±‘ïÍ7*x/Ò¼wµ‡¤}ŸlÏà5ˆ²ìŸ·®(5ò±´ºm×ã ÷E×Ó8oæûôÔ^åAõ†à%œûVòvŒCk»¢…ͱ•íÀ&¬…–Ï$Yµc¦™íSOFž3¨œœæ)AÌŠ"ÔêÑŸ¾£ˆÍ1É¢c—ž:¡{1˯c;psEìʬùú¦8–}#©»E¦„­šä%þÒQ×fûŒ:ÁöS?ZKªâ´JNdö¨þòÓÞ"¹Ï*FÓd§¼ã‹ÇÂ5!é™å~«>Ñ„’w4AÏWTÖǪC~©æ+(M0Ó%é1µoÍp;#Ûå_Ë1 =LœPƒší©Ñ ¨ /YÖ˜¢á¡åÄð|¹¯Ž¸|¹#1¨/ª¬{Üqó®íð;Ÿn&¬O¸É‘ȰÈÜfW“c’Â/K•:'í£U-¶Vð¾^ï×]Ö‡Z_§fwMMøCJõÁ´Z§{½7ukUÏôê¦è¸î9/¯;^wÃ{q¨)¹Kó !ÙW=¡û‘æ֊3$¸þ°øÁ€xÝVÊ(ˆ—wfFÀ™`Sš(Lr Ë }Ø¥µj ˜aû<¬¾ã¯Ãö—tâ1)ò–â®9;ƒ…¸ì¬˜õkË þ”gø{½|¼]íÙ½ù̺Ý, 4gk•Ñùu¾+AÖ÷¢3DªpC6`Àö9›KC´.žì0)¥ÿœZ»3äõ°åL…d¡ Îí Yè×ÔU"Üë‘E5ÊP&Ê5í×Ûz¸Ä_’cœ_—µ»!ÉÛk _M°bmíh›gd-¯ó¤Co@bÅÆÛÆÈÁë”áFpmÿó¦–¯@x=Ù½ì^‹™gñ}ôKôr¡$¿UýŸFæAç’ˈ–EÄ›ªQµ¬šÕ7I·öKƒc1¾{v—d%¾üØöYÞ¬AU¥ÝZµ…´jiÖøc(‚!ä©ñZdО^À‹‰œ\I£ÖþàO B÷ü6£¹q*°Šî@(œŒx~âý_Óù ܤ¬Œ¸ºkÍûŠ ê<çÉkOÖ9©*!²%v‘ÅEÄX«ãµì›8CÐ0ƃ·rœ­Àwk] ] ‘ྠW<+¹‡O hq”I«Õ¡ï–Aê7Å™ÏæèTÃóxÕ[*éaͶ÷íB>Új„å# d©÷ŠvÏðÛ6žqéFÕÅ̈Á½¹ËõHë¯ò È"6eÉÊä3³¾Óþ“XzCŒu}¿›pîâå†`³àÞð4óÈÞn"Å+‘—Ž60*jIåªl¸­k Kœñu«*à&5Ÿ|b‡›š38厳¹Q½·Ôp[Ë«âÇÔgAÞ+?K„I·fäI•uúk¸à¨ää¿+7+´`POÀ•|µcjkÖ-.N@†QÌ‘ÖÿL^ã³_l¨Qù!±ïÈ·ãtâ¨Jb­ìÐ^ §&,ðbvy[“þs ºRΖ‘4Ý7:ß…îÑÀ Üœ›…6î<o ³£¹3 "¾EŒö4ùó‘ŠX¿5}íãBÞpf¹×8ËÃRžAycUÑŽ9^?¤‘;ök?`®Qy\ˆüÕ/ä¾þL¸Ò¹kœÅ\LÁfùvPc|9V©»Ödyå uóHæm}ÏRâ©` T…†´§¦Á¼V ;v ôk£8¢‹““ý‡ýDÔ_ÌÖµ§iŽÇ<ƒóìçAcêD8¶J|:M–ñ5îÉ•Mt.‘`µ\}aæ·…–R•úÅ>âF©»Q¢kš?.´ºT“¼ H˜O–+½™€,V[÷¹¾y «­œ±¯:sÁr òàœ#Ö0dÌ ~ÀÕC5$E¹´MH¨ÏH¦X§”ñÀcž2¦†dœ²ø‡š7·È&µ/^XšÉ诛9µýåœ7½6S²4˽€[qݲûŽyòD÷5CûºNÂOÕŸ$ºI“-ùYÏXY>öCU÷¶v- UÎ WlºÑ0^jJ6±ûYW'Ó CÍù‚[lŒÂ ›UȰ©D+ðù<¶ì–ÏxŸ5âJ[áˆW÷p‘½ßR+Œ\“/š/eX„¥T±±˜ý‚´`Û®€-nšloTh]½…ÏËQÊÆîö–‡¯þ†œdÙ€öȬßngú'CdêÂälXW…õÙ'Ó“y— ­Ea)cÜ¡†µ¨åLÝæOj™rì@yöª/b¿]ža,‡‘-¾"}³)ueɃÎ!#˜Öè¼pèÈ@(]Yãk¸z ¢YžCÔ=Ÿ«Mbrý“оßj=Ô‡¦æN°ð'u”·©[ìº ]Ÿ«B³ÂBBš6í±F¼±ƒ{[`®yP3TLwyL\ÒP~7'2u/…g‚†ÃüEÞ¿§¾»|„ÄÖóؽm"ê¼k$òäÕnx*äÐ…£×@™ï”ø$=Ü\¶’'›%. s¸[¨ØÊ}$j4ø¢S—Ý3I¨R©t5{•z Üx,Ç$ÛzæFÑ٭ٌѺ~vdȄδÌV±5£šü^Ü"Z3Ñ@#Äh‡¯ÌÜ´ ´i‹ôzòØ\M øáB+wÒýcÚ§3~fî±sKm«Tû‚‰>üùgºé‘罪·ù3Ïà0$´Ê9¯aû¾óäãj'×Hˆ¥š¿Ë‘¼«oŽïR‚4ëÀ[ÕbeP±+r-ÝX¥K €Ï(鵎ÇYlttïÝRªà¸Š@Š Ë8ô/N¼µhÜÛHB&2ÄÓNîlŸsÍ ù…Í>r Ù>Զϲ¢„tæÀ–ÖßV|¢ñðVkeçXf§Ìf–ŒyYÛ¿^³nÛ7ÅÞèËϘ6d'¬Îá*5ž1ÿ“Ç™Z|¹þÏDãž¶êí;ŸuR_eVË(B©'üEWi¬%°É‡ª™“úVÒwlÍÞ5ŒoæÈnÎEPDÜFÅ”¹m*kñœ¤³þ2ïMäyõžBCË è¹¾üðb¦£’Ù÷áõ¹}`pvÑr]ëĹmŒ¯¯# šq+ë ñKH'Ò…G»bW̨Wk¨HÎãæþÆë+į\M½¨¨W¹¤…¯]ò¡Gä(FÒˆ^Î zO{dh_îW./>©ÏéÛfª€åXàßÜéÆ\T•\¸“²=È•»žíØzo ÎN#ËâwÝ»R÷ „æAX,™„ŸWAïMéÿ³ÿËÄS€Š¢ûŸ³u’³V6\s?<`'ï€ï\ïõðÿÜOö<ó]•‡ à( €ÿÿ·xl4¢­e³æÁËÜ¥(¥XL‘h=†I™ ³±šèˆ»åp3iLnI#й™Z4¾Kãz?µs2MCϸ¦°H PYÜÖ,“ƒfïC¡™ÏïÏÛ ú¸Ð ÐC ¢„âs.„©û{Çu6=MWÑ)€¹ÌÆ|Æ{öý÷¦Vâ—Uî{¶uÆ»¢|Ú—øÍMÏ7Á7E*Ùª‹Ú^yR¾Ûã¶b³¶›¶|“ØC_ÕBØ¢ªÐFÝjœNJ|ùN‹i"¡RS±eÔj¿&[¾…LCS±ÊØÂXa+WCéâ%iƒ£ÖGMNQ¥/ŠÚB„,W¥¯î£ªê!JLÅ]‘ZrF!yKõ±U¶¬UiÃ’Êš9„f6ö°ˆ/õBã:Õc[Ô±«‰,‡×#;o5á³ù0èj$Qùcõ¯Ö}*g½ë—°·t+ÇDzN¹è/ñÍ´!æ¢\»3†òJ ,#õĂÜFrI4stA‘‰PÎÉ`3Ë¢fž|©•Ž ËãÁå-ѽvÅ‚MÓG'³v6+¯Ê=A 'C¯‚5w]GŒ£¢ê§Eup¨w»‹}ûµ/ï&´Òœ¼$Ê"äeqUå‹´EMQb§•ê¬&T¸+JÕ¤EMuâ̺jR¿Ê KÑKˆêÃfË’ÄŠÔŒŒ¥q'‰Y²ÊÜ=m̹­¸¶ÇWóFM¾YmàOÑós»vó›mÏ4ü’1Æ×KyÏsûަÙFì6ýyã¹iÌx-qìÄSiVjvAKÛfØÀ^…íŸæƒn×C"ùWvè ËAÆÓ=0ÇPy zÃEŽ.õýfÎòQþ5ÊüVüïExsï®ì¢h¦ãh7uùתªoÔæÏ‘±‘´ä?„±—,-9mÈ@véÄcdð0,óC‹È«’‚5 /W,zëŒå+š·Õ­ óf[X=LÔâ¥?ZeŽ”U(k5^›È°ó²ÆÖmWUùC @¡Þ)Ž¡¯zšøÁN϶x¦+»QW#NH£êòú·|[åAQ-_AróQ•ø?ƒREõ+§-ÓEµ¹¦ƒÊw.žr-°c;qx*W™õèt•Zý{Tɲw5c*P6!w°-eOý¼¸"O²»ŽvùÃç¾OÉ€|›v4 ¦EÚ%e»\©û»7U5ø °»%pû|5šSClg‘¶£dáue° §Õ!ŒF pì¾Y¾iþÛ¶aj9µ¼ÔØöl]4_>M)þ)¼" #Õø·×¶ßÈ×óX wé:NôèBÀ7Çtç#Úêtúm[µ‰Ü/'†1ƒü'ÉZ—cžÐŸ>ŒàÞW´ÄÐ8´é(±Ñ­H°ß*w»ãY±:#ªˆ¬Æh8îAD×s‚ýWmŠ«@š),½ýøLê—XÛß·¡=Ãbú+ù,‚ÑÍî¬]íÙ®ÒƒkNP"Ñ_]y¶kÈÊ„ 1¡Z±jtË£ò‘@µÝq³4äÚ™)S;–~$ñ tô;{ªíM]_‡ƒ†#–˜PÒ˜ïá’æ¬"ú5ÎâÈÊš{ËãŸÆî›0tïíÖkØ?õV2N_Ú Ž›™©Ú!AxM’N9éJTܤ=qÜÂcMž„¤?»/?#õåK,oÛûK}.%¯HQùâ„Ðô‘šø|GÕî¸áñz_?—Dß’&_"#·$[mk`Ú¯©¹Ú¡–Ñl}Õw$ÖâK'\:º—/ª•¬¨|º‹iXÖdº¥×.Jvt%U¸Ð"Úxæ·£bT‘L·e+6B¡(@ʨ=×A„KÅH˜åÍVƒíN5ÐZarèÂ=WPWY½; bY¶Å{Žrc ¬§ÚHKH« ªV¤ŠRS'z\qÍêöâ`u×-BuêƒO‰î«æ,ØAÂìx¢„HoŽïÔÎZäAĤ+|M¤ùQÌñ r‰†Ð_Ûlw]¡‹ò†qRÞP(kó7•àHM*ÄS¸*õ£ùyÛC}Y:B˜ æyhÆB•°©ãskq^­á}tZ‡T‚q^X6 0 “’ð&êͯYÇe¸=¤#?AÁ2!Ò¡S2ÈD.ˆ^÷Ï“ç{4Åw]“kÃ(+¯èšÐ{btn@&6²Žf‘h m$=áõÄñdgÅk¸‹¨žét ÒŠÓ͉Q€VP¾ñT–µÜÁ#ž<ë3Ó¸C<<Þ°n»×oîŒ.È W°âL¬†ñB®Óž3,¦zT­ÚD ç*ÚÂg=yŠ•þ$–SXåÕßïÚše¿ÖÁS}G4Ï[ÿK¶‹õh ±UﴶЙ6isœMƒ-ÚPÏšµÐ°ÈnÇfš¹S,<â°OdžóÞ¹¡¥XÈÿñ™³`Y¶‘_;Ù*>… ¿Kz–eU8í`0{F „(í Þ§§U‹éH)`íyp^ù¹Ûy±¢ØÃ¶BöPwQw)åÔ'âfK=—ý`ºÐS<âD½¯È­~dÌq¾ÄÝ÷Ïâ¤×ãÑFááAÉø¢"ü¥@ð3¢oS¾íàªëÂ{ºÖ¿L”2™ºU‰<®­5w–·ébW­·c³”WèðõÐ ú׫ŠuÚŽ)d†ÎÓsè:©ìnE±unv–4ªIá)øœJ×üݸsk¤1¨§»0þ§?mÿŸÿXaŽÁÛ#NÉv9ƒÉ>1e^6ûÛ…–DQ‚`ºfˆ¾ŽÐm$P]‚]0sÌ,{ᙾ‹LŠè–Ç)ÊÙI¼¬ô).Ø@ü`µ"Xž^»Sد=íÂqæDXÀA™ÓfÜ=ë±2tÿᔳ3«¨RQò}Ãq?å]ñÒ¶Z¡¬a-ðÛÍÄø“å@ýà0Ë“Œv?à5øRnÚGçžþc` Jæ:ÈF2.ú±Aœ¥Í}k ’NZä]Û"Ë3íºQCiè×.~"O 4HmPñ}Ž…#†Z¹n/æ%¤†½wD[ÎŒa“ƨ[cKèÀžbÅ•›ƒbX"g/2nI˹iuƒ„i«¹ì¨^ŠZ† \?8¡×….“rç5’k»±ã*y …®¼ù¦Å%œ³ì›¢5£ rÚöÌrÜ:è$Ûª ñù›÷Ý–7Y^ÆŠ„Ïkƒß‹ü#¢ëÿ#·÷¨€wóÑ×ðC`_:uÃÏåfääàäädá<€çô‘e–ÅcO«ÍCúæL ç€Y¸P*;céá.»8ä€ý‹}x—V›c|W²g¶0k+Û¿íŸq@&à×ßà swR )S¶ŸþL7}g·À—{r˜äÍ=8rørO[z}àåÍ­{{z(öñö+»>êzg7ÂÇî‚êOé&DwŒÃ'Ói($QÀ#AI}ÉB1ýW«ý‚5ÊCM¯™=XÓ±RãÊ0—@” úÐHPûZMâ_÷k iÄQ0ÝaZ•@âd˜"vc.Qž#æÌ ¤‘¹Ê;sºã¸áTÝ.é½SP@ùFŠFñ5¢C yÙö'ƒäÇí'«Ý{[ñ ñ±Ð”Ñ£¯Y÷b8É3| ¢lS¼Áü ¡†œ¸3¨bÖâ¬ÅüQ¾ë3¯%ñ$b–jˆp¦Ù4çê´y…ÉÈûý#W:œ)'"°‹œˆrùsû¡ÎïZ[¢n5qó€=ÚÒ1Fvÿ—în¢‡=tzîþô»·5UöJ‡ƳÐI,‡öQå³¶7ÒŸEë]NëãHîšÐÚË=X¬‹âçOSƒg²C.üçùcHO”ú¯1ÞèRój<Å©’$,½ÂHNvª32ÖTÏi7›qÈòVDn aú<4Ñ8­³+Ö)Œ“ç*šô‡ô}nì<ÇÕš'ÃôÜ ´¦ÊÊ`Áö7›‡Eš¸cÇéY—Çä2ÏiQ¢Eô€ÐªÿÇA»Eï7›8µ|»:G¿~yà…F¹*«c Õù0j[ƒ¾ªø$Ý+ãlê~ßh–évæë‚¢!Ùzã4-g¦n+Ò(ƒ‘j.Yá˜RŒÔo®'ï 6ïô¦Î¬ÁruG­œ:ƒYPG ÿá`ŒK$w*g¦Y!K•’~³s”÷à€¿óXz¯+¡%³êÊþÜÒçfÂzþ$°7ÕYS+#µü´'1ÜMŒZ¡kaQw´áðÞžÛ}\þ¹*Ý¡XBÅ- jÃj&2¯#‰›Ó‹Üýù0Gs¯ÎJóp¡â§\íáR"iúK„Ü[Zn™ŽiÞ2<[Fwꪈմ^OÄë—Q¡$ilÔ¯MokÀ=G•@” ¶zàŸ6äÒtï Õ„>¸iÕŒ OÜ(ƒ`ö8Е㡸2HŒm”Ü‚$®B€pÔNUv#þÑì±ÐºÓˆà”ÅgSÓ$ˆðQy•t´9¸Ûá)ÕÊÖ¾î{úßà u½üU*â†!éís'ä cç¿Vl‡¬ÔWbɨ I§¯iÖKÇ#€€¶ê%ä­×£k}Ò´¾ó4/PTêå¢, .S<Ø/Sàîù6¡ÔU-ÑÄ´¶9tz°°[{Y÷혚©Ïµ#±ðÇ:ñ?¿~þ9ä¹öðõYv“Q’]Z×Ñb‚_›“;fE_Õßt^=·Éý)ÌäNpŸ>=…U?²~má;.iØy©²ŸÐÅA¾ß ßOÄËP\J¥I¢»8J¼öX«ýÌú£dÇ©+ˆ§0©ß]êá™ÀS?øGïèS4RŸ‚/øÅgFaMÑZê±âØzÄå"[rM 5V¨iç?B¨ CÍEm÷íJ4’OQ¯¶¸CŒ}17>þ0\SGÞ˜TNÁRÌ´ÒúšG8DZŒ€N"§ã_?¸4—bɳ©E?Añ˜ ¼É4jÇÁŒ@_¢e®÷àRán´:¾Óù:V„¼5,èo-N¼?Þ•³¤Ž> .ÒC¼5B“Ã{iÏ««è!¿‡w?…;œ°?ráU‡wK²ÂÄìH ‡çïž/LɈâ¿w[éÞ»bÔè~b ‚o˜‚yYY{jYu“ujˆ^j—Gu'Eʸ90-דR*!€þÐ|ÂÛ_¿ÑáY1¯CàŽ/¼Ÿ—2ð/»Ýð'+aÞ+æƒ/3³/ð+uŒJJ:êõ}傟,~³fA¨Îšò¥a1žÜ…·¼ÍХ旂·)rôÔŵ~˜è$*-ø¨7©ÍaÊ_ ¥;_å5?<×±6ª:¥—àI˜bzIªÉ#¬&÷Å…(ž[–ÃÓcË'nNZÍ?úµŒE½Æà °®¾©t‹*e%w-koü—¦Ñ€Ð6èhlG0µPÕò4Ž.X‘Rçx’ºK§øë/Ñé/öçÞ<‹û©õg¾*7ÝÁÓ(ÃÍ!Q½Ç×kÙäÿŒ¶%x¡ kVã†ÔëCMQÝä¦ä³Ô²µL7-é(ÆÉ›ë^ËÏLtÜOFg¨×~FÌÊ-Y_6pNœïÌÚ‡tíÞØÒkVÍKyÎËzbH‘sLš ?76:.G—; —´á„èŽHç‹Ìø>F™ëø<¦:”®º´§ÁG{N.µy 1ãr³£§¾Rà“‡‚‰š(ZÖlD*âÄþbi‘·Z;.×^+|uÃ<•@€ª°u„ )=àÐ%9‰AÅçV †èšÜX‰üw.Žƒ³Ä?YÛ¦26 l 7d*¤¼EK¥:ûÍÊS=/dºÌÆ s¨¯Ÿ7ÅÎ÷^¸ÿ<ÆÔÙ&kÚ“f“¥H% ŽªûG|€(ÞŽƒ¿³ÎCæ™hY ƒ­(2 eÇú´iµ‰WaàçU¨Ÿê—µ½~ðqÅad(¦…¶¸á8Ô¶Œ ¼3õ"DíròݱÁÔvMŒˆÍ¨¨>Døƒí™L§ï38X„Û/ŒJÉùéEû&Ülþ –ôÖN… 0Ìå‡:EÑVõÛ”B|¼n\Ö*løÑ- ké Ñ(`z¿°¬= ný¢Lz˜@’Åã*Z$ôC> Õ¢@iE™W¡ÒèêÉÙce©S€yl[öj&)2 ã.«b›øèÚ™àÔOp^2Á0«Oâp»OX¸>Î=™\%Gß .@¾Àc7Ò’ÂÞò&ã ÜšŽÇÿÚŒ‡l%.i´[ 7ò¸¬Gˆ–âAcSAHÆ1TufbÞ‹ÛÏ<½õ»V†©v{›`Ÿ)Uôàªz—è@š¢0—ÃäRQ¡ÕK•™´+ †AZ]™aìÒ=c¥>C}%1Ío“‚S§ÞAíðge‰•&¯è?„RclAIDäŸr›È«G‡rl!-ùáÁù‘ 8áY ¤.Â$ää™Vù'Y —‰Ö (–ޅϪ>AIЗ2Tê\`üˆ€g|º¨¡ÕÁ·[]—¥¿çŒ!0Y!cq6»I˜/YÂU™†_÷¼{My8u]Ï?,¬ 柈±xôiB)`"f3\-â;(YjÂ3]CÉG(;ʆ¥Û N¯Yf@çr\yÚ¨l²Üeª¹˜uññ¾# ãÒ=©„”mP®¿Yä ¸ÁøØ(*\6.ížC† ÞU«æH¥ÄØÅk@.º»¤¹Íe*‚Nx‡é+³lLêÒǽ¤G€‡ìxI¢y‹ùŽÃ‡>¦¿€Š°6*Gév¨úIR9àX–û –«ÀØXy«(”3F.ཡ÷S1òŒ·Ì-vµ N΃³Ú‚åâÍ Àh­Ò"Ü* ÄñÂA­Œ%A;äüæ»Ud/Á³ÂÒäøí¦˜9|u«enkÈÇó£C&`°cŒs$ë •`±ãý%ÙT´©M›¡Òd§ÍB°sÓD9ñ£ ç´Ô)d­ÓÈìf„M˜»N§¸/µ[´k“à!2„cÖÞçüÓx¢`æT™eBw›ObÓgáh+õs”s M É&AöÓ‰h"×»Mmº&ÌYòÞú웥ñ¤þÚŽãâ'ÈÓË@GEGH~äÇÍÙÁ^Ýön¸)Jd°='×ßåV°ï¾Ke~R´U"õœ`íN:"87|þ=joöºf‘C_Ëãa UÊæA 2îIbjf²ñ³wT¢Ù•qˆñ›­M{?à/Z=ÔĈ¹U§¦v1äpÂ%.U6Ïö„wú ªD66<2Üêä;èCÔ©1 ¥_l¦dÑ›X?>šp>F•z/ä, |'‚ÓCï@mœ5zO©‚g‰¸óŒ.B)~Æ_”äøÎå§[¸Å¹¼Êí¢’Áœì„ vÄlæ{ØONÞnk ˆ1‰6Þ•3írE sÎ6–o%òL{Ÿ»|ab‚»â7·g^¡Üu˜å¨ž5ÎRjê2^·ê¤}³xO¾ÐÚcyÿ«d€g>Cù€Ý»ì¯c;€±(*¢?Ñšw×€WŸ8é k¥_œëN0_Ô°ÕÞôüFhys·Ñ-Ê©5OeRTJÃ6ˆ¢PÀZK”½q€‡geáó3{P°ûýN€]Ïå«óIUBÓi!ÚÐZ„³3Ó—Üó¦žçTh÷ .»¯Zü£ÇÍ:²[,*!Iø`\cfYÚ ÿ_ZW‚ËÙ+ x?1êÿ1•DÈW©J;ŠjØ•òüN% µ÷Ýh2ó˜*i•>ߣª%wL[Ävpð£ ÒP‘”ß[Ô ®¹ë„„Ìç6ºSz…s²Ò8zLä†Úðã0ÞÔሯ.¾k^­[|£žJŸ~ÖâT³i¥þú¢ŸÂe­ (ÙáäÌBòyŽ)g”­ØTNé4.8JX®ÿæ9äï?æxJž=ržÌ Œï<‚í^µÿ¼be(]˜gß"ÔÏn¬0ÝÓe{ž*±~DR  ÉxGò®¿ÊE¾Ï+Ø |œÀ»“üüÛ>Ò;S*·3n‹7wøß®t'ª/™ce…±Rõ¦!PPOÝŒU)@7ï£4K½N §œâ›~ñÅMÑï?n/,®&>ÿñš_.®jP$g1q@\P £QP‹T~áG^j‚V~EpÊ8Ž‚i]¬¤”0KýýºA€Æ£°Æ¢¼ ¬›ßy®9¥ÌÈ–3sæîE¸Ý ä|¨Q–ʽÂê÷5¾þI¯x!^¤Ùw›‚•ÇšdÙcD(rˆ‰—ë\üÝ,?‘,C1ײ6È*Fa¬«NîUúžÕĸw‘ŽQ¨¤‚½IŠ$ ¯_ÍɆ:Vø¦™[àáp4 ªäÑòêä™'X ™|ÑJ­æ7Ôƒ<ák;ÕûÔMÑ¥KùX û‰æÇð­ÐC6ÖGüMM¶#=\ÀñsÖõÛ\ðQNECÈ’!p"æzÆÓõZœe;õÍÊ^ÕÞUøâóï­Õon±ÎÕHq¼Üjè¬' ¯= Žl“fåÛ§Ïrâf®ø©<áØ°ÛeÀk¾ìuz­ãšúIŠÀ'~CfÿóbSŒ\pã¸>7JýhèP…ë\œÃÛ]˜KÕªü5K™€H‡]ZáÙ$nÂïÜL:cèMˆDÿJœ.4Éî4U'ÄœÁýÆÆÂA;5qtÊöÛÚû‘¥O" êÜ &]„ 8ÉзŽÅ0+Z1¤QÌZ“G柴7þè+Ž~ÊÑôƒú;§xq+þãX?±¬[iþÎ2F[)~P£^í ˜ ô™áüë¹=Ò‚Àß«=ùàäíæóµÔs'¸ÙËc2Pe{rËØñÆ$£c¸¿ëˆ£Ÿ¿0,z‘;J"ƒjñ¨ÔA±8þ×;©àTü‘Ê„8/Ù‘è¢ç0ÝÃÅ'(J‹‡ÏÃ÷¹~¾”ô&“›‹“[-›J'—›{ÒÆ×WWmÈ_è/Ûï\ðaåéË9Z~xiVJÇtó‰wó¢E(\Š~»£›_œú„Q,ðÇêì}žÃišså>Ë%ܧ0tÊõM¿ôþþEÁ !Ëe‘¶* úØÔ€OĪîÏã€Õv›¼Yl`~Pl‘¸€³¸&Îùr‹"­ûëõ&[’Á>&›/Pæãáj—·³˜ýdL¼îX²_¾æ¤6®ý2ˆNtH1z´*p@—!g$,~ä%O üþ®÷[ L_O'& OC;?«×r“7?Z†;¬-@E™©„FSÌoç“I¡ÏøÎ-)(j˜7˜Y‘â„í0?\µ ’G¸r‚ì­¹|âO¬þW“,)Ûµóâyƒ¼z`¸Ë,°Ñƒ“?v{ˆ’Å^¬˜ïW&­_þòðnK!ÔFçO:âiÕm5€•†;Ï¡…,° Ù_Œd[YcŒzÎpÚ P5<×Îà¸u¿ÕÛÛüàÈâÅÖ‘105à̉1'*„N¦ÄžM/Ú´`-'gÀà :'v'/ÚYÐ1ÿŽH²dQ0›!uæ:0üi=àM­[j,a0±§„hÄ/ÈXA¯DÔ PLc8©´ ;ɸ‘ej‘dߣ@­FßÂ&„’Høœ³;¤ ]àPà?¥jEèr,De>~øŒÆ8<9€.Ï…ýá(1Â+A^…§zó¼U€àÝ·K¤â®KQâNî5ÂLò~?bÿ9é$æçî.§äVgZ©N-¢sÿ™¿b©ï‹_Áfza,úÉ­«¿íTçýÒo«#G~–Ë…=]Û]V4uànÏúœ+[ª„yÙÒ±ïûftç¤vø#|#ýý}yèÈ‚w«v_0°(hâø‚ Ιí z6úÙ«ÜÊ(ÏG¢žªDåAåíIÌÈ…–÷'âc’'(…Ì* x)ªmÝ:Ò– U£×}*†rEË@¨ÓŸHEÏDÖ­,åõiË ;ÅLÙø,w1ôµlŽ òé3$GÈ|žBŸ¼®¬›kŽ+gÄàH¡ô¦†5©µ[_­C`ûemF#:½G{]·jnXÙ Œ¼ö]cûC‘ä|ÅB­ Î1ãÞï7më£Ð‡kÞ׋'+–µð¡uýc‘Îã€Qx¥[IäO$ é‚ý½þ·}Ëjw°àðû)·?ñ7tttD\¬Bé’¹3a;ïY?û/R²Œñí“­);/eg1$×·ñÖ6­+Äh+vX ‚ÆkÊSÔ­ÈüàÊå¯Ö€8Û§m~ÍÈû²SÖªl×ñ?ßúÀD¯jÅwã·wqLZd”¤ðÍR“¼­xËÕþS4R¬©ã'Þ•v8eâ0§›Öú³ Ú­È }mšGŠ"7B½­”åˆr룱œœ»{H'"nZ5„Lò|î¼çX¡‰é Cטz°ð9 ï}Üôz›ü9åH²å¥ƒ-%xxnž&šÂ¾iG…Š#ÁM´óúÅñÒÝçÉ¿Š‹zû `F>DX9wü,+£\¶±£Oå=˜eÓÎÓ¦ßOïÝ’Åkã­3Óe6úâÆk‚ž§/øô4!ˆ©fAjí<7¦¢Àìz¦^;ªÛk)ÖmkânkòòÆ@Jð;â%ïê ±½’j¶éã0ŸîìÑ6þ=ÄðŸëdáýíˆÝÎè/h‡,‡¦%ßÏåË#faà‰ôE¦*ós!i¤!sHà~ûœ¸€mýÊË|2pmO„”Z7 ¾ÄøðF¿OnÍšº¦þ‹ÜuäôF!¢fõ lª2Ö׿ˆ–ƒß@3ª÷Âkè CÉÐÁ–E!óîtmY—u¹VÿÍ|¤îodѽMëQo³Ç¶Œ•ÈIë'Ø,0Åm‹$¸«Œ‘?-›º.öt°tæjŒ ìæ&£ïzæ¢X ¨Ê¯d\G¦…ù£JÐyÊݺìE'‹¯ÉGjñA!p‚vnL‡ª‘ƒtȇŽT‡ôCÓˆed=ð¦ÿMòë¼¢í°íÿ¾1©-րЀ €âÿ>yõ¨Û8m"êúÓ7šh$’nÙl²÷´ç®'’²Jz2)˜t. 'j‹Ë"›rR€àHŒ»ìÇ6¤ãc¹pæ\ð]•ýý†û»ºÁHôg£‹°ÌuíºÝy¸¾Õ>Þ¬Lü}Íô}Æÿ©Ü!ú®ø5@Þç"òO}·6´˜‰ü@ £†WÍWfú¢b »˜¯Èô-€ §å»L<_!脺#i&éKøQ#H÷7€ÖPBدª@Ð Yae3 šÏ2€>b5ÓBÔîÆòÚÍ0À:ÆŠ™M.x@˜Fu¬M.\!LT?ž{sx]oÚh‹aýì¹ æuÆÊ$»r21ÿHX¯r1-ÌÐH=5P±ù”<í°ÍÈeÍžÝÞžšÝŽqÍeë<í#±Ý0$úàŠzBquaß.½ð±9èVU˜oÒKýŒÔáwk+o.5t²±Ž  )ÊÄÅ•:T›ÊÑŠúÔæ«lÎyH{àjÜRTœæ Ô•Å% ùÛÓ4Õ‡KaübŒV*pÞe°(‹ëØ7$NÜÉ3¢&^Âö0qÚÌuÃųSŽÒ}•a‹Áà£Þ¢‹QïÙ\õd_ E$6-’6¶ãš::Êz-˜Ÿi¤¼ZÉj7£þ¯ºó¥Óe‹ê@•r¿Uò”aA <Çêý._2B ¬_†€Da¦Sûß» p2;å#_Sü4þéÖœ J9ëA™Ÿlò=%–‘gy[g›ÝJÅ+QÀW:4ª/iܤÙyDH>¦_q'#Ãah‘&žåÂýÿ.êIÅ >fôàÏôXÑÒ 4|´0m_jyg„ÇùHx-l‹”ÒÏBÚÃæ¡]ÈÝÅ6¸5> 6zòSê®–Ž‡\wWc3UjZê*~u7=^xJJŠÉƒþ{f‹ô¥oju›fþÖ+¸feˆ££øÖA8×°²©YOI­Ÿˆ¹«+ÛöNÏKíÛÃiÒéë§ê’O÷‘uÍʬƒ¼—B9+!‘4ËÅ@ÑNµ1r÷E© ÜkDßpI¢øpÐÇ¿j.JNkjRSœ ¤Ï†å{Ûù¢‰ï{d+ºv!¤ªlÓÒç”N\nÏg+”’'Ûœ®”B.‰BMh®?&¯3©×TŽÀ#6™mÓ@N»ùä‹É׌°æ¦Ùï‘ö(b›YÉ=9¨½£{£Úl£Ëø¼Ü ¯Çµ_‹i’Å‚T¿â>X™ÃüÈ'û²ðfWr]ÂH·ð&=GNr¦ oµGK’½@ƒÈ}EvÔËö O í<¥X‚ú˜«ùñ¥zôŠc£«EäY[3ÌIàg8gd3yûÃC^fŠP– NâÉC*8è(Á8Dˆa޲ì¾ÅÒ®„SìÝû÷’d~‡Ú}ùý²)¿e$ ÍA˜Õ™·(*I3Õµ~vªØdJÊ+JÕµ˜ªk³f-¬ƒ—›UUñiz3N1¥D ‘DzÇBô*êóÑü'6#îHzž z»k:Ãvå·Ñ@fÍ@©Ÿjöd0Ö´á:¡9H ™ÎáÅ:Çû»Ÿ·ä·£³«©Qëˆ1%+]BXY’ò–±`ã ¾‡KÔ¾$) wÈ$\A" *ã„“Ûî5Ç`¯;Ãö»tñ±ÑìyðÁte $ ³«Æ“Èt¨ÒýNÛã\t½ºñrH²ÈJÃQØ«ÖÑ`¬4ÙZË–¶I˜jéûyÝüÄ© ãÐÔóåÜï Y 4ó–ï¤]îןáÔÌŠ«Ê¦ê©-Ö’×ßÞ³[®“C"ÚlÔÖðyÀÆ1—Ä-ºÎ&Dºâ¡³ã T³Û*6?t¤ã ‰ˆq”dí;F«;Èìfµ{¾ŠÔrVmÒÇ ¥ÂƒþFJœ‡ãèê" ÃaÊpÃ>ÜWàVÉ -W™Úèãå31*ajÁDMQë8•g )Pê[äП«ôý›“&3­JÑïSèèŽS|ùågÔ.]ÑÏR)¥{ä$´§DÙ_Þhêï•7«Ì[>ыۼÞc,NgcÿP³³æ2DÝ<&q@'²,óB[p9!8ûòCeég¹ž¿×Ù½[½˜bbÄ:ý|fÜ ¼_ |3Â]´ |Ìmßx‹å+Ç7)½V'ùR0£¹¿+¶«w½¯Nïþàÿ§ç¦] ­ ûÏwÓü?Ë' [3kg;[{GgsGãÿ´‹ò„íBÏ5\ xÍÛb3ļP¼.‹mYY!‚¹¥1c©DJq5˜È_ÏÈnb(úTS÷¿'ÓñÑsØöU‚ªÍ€uçMö÷ÂN€]ì I;c¦ƒD è7¨äóæÏP²ÏÛ…­»ém§°sé2¥öØÃ\« `Ù8Z÷ )SDÃûùB ÙâáÎgqÀ¤IÎRö¿@>¿"ÅŠç5µì“$ýª†€ÊÇ • èvcÓ?ºµ ?IÔOfˆ5¤2ZàùÝÛ8]^•H›øý²³ÖÊÙ”YBè…r"Á6µÔ3³{—àí¢Âà]4BFÉÛ¾8 ‘&3•Íù†v³1ÔO§ë?ÆÒ?Þ-7óÆ60Õ÷çîí ªeÄ,µ¬¥À×à.X‰7SØ gÑÝOz¨qç{T§¹¥÷ÙýÔW{+odß…{ý›uE;ÄlÙ*ônW0ñ¼æÊI,'®¢¶Â׫J$b-M3 ·@GÈ5þ*pxºeÕ:â÷ŸØMK÷Ë|Ã(Qv¿¾m<ï¡UŒH™ùnÔFYÞWψЗMÃâ˜r±æuF¡šI3óf@ãîjƒa4dîÒŽÜ6q†°J°§œžÜ9‹vïlëb<ÉÚOz<š{­9z>ížn/?Ü’ÏÌl[*\È–«<]ªäHsE&Ô8e®o•8AñŽÙ Wöë½wd½D¼FÛ€23 æ(yJMì³HÎBòÜ77`Þ±LÛ±oõÇHŽu§€’’{Jäµ!®î°n2’"+D×b°Ëëü¸y)õ–¿deò~R‘ú#{ƒ`ÿùÈ|¯Ì$È‚²ƒØ)ÑrÉ Aƒè(¨ýö· b¯:’ÄŸ¾¨¬]Uªâ¤@h±®CçBžæt®º÷cüÿ©ÔwŸ¡Õµÿƒ<<(íÿKȵ(ÉØ-ñ#ä´ —Å„S± VÆs€œ:  †UùÇ€‚ƒ7î¬kEðÝ©Û(°§0ÓË~£ûLW'ù/C§­ï.Ü^_O{qÍÝÖÉ~Å}™Vç‘ųL{1!ú)úK÷ óŸö"räë°£#tÜ£îcí/*³l“Ý‘6+€{lÀ¢Ey%s|ÈÈí`²wS ‚ºš#©šT8yÿ£n‰¼çvÌ>v¨lß;o|í[-Ï-›L§µ8Â,òå"½IÕ—Ö “1Ò‡gh™fúUâ+Š&4{v(‡¯€²<¢þ3uð«Œ~½Úʘý‘/©ˆó'H—Ž¥$§¯8Ø›;ß Öà[˜Öi=ƒsîk¯w¼ÑF£¥ ÷—áãYƒ›Öª/L‚órÅ?û£¹åCØø/Õ.W‹Žo1+¾F÷Ø×N&Óg¤)žR-)‚Ec`ÚV;ÓY?Ù&b+{”Çð˜V–ÐoU|Ò<¥4ÎI/œ¤–Œ¾Ê§4Ðu·×+ÅÒ•:iUÍÉ4Žv“0×$²`É ïÈ<^>œ{<›—¹—󪕴ê¢&¡„$˜ˆð²dÉ"à4ç`ÐÔÑ5ÎYl©‚Ù8PÀ Ë=g(ëjJ2QR‘N¦Ö$+ "ù€aßwø{¬kûÜÕ4©†(æî»í p5·W0w·¹äÉžqÿ%üíªþ’ØO nh ÏopôOºf-9*Ü­Ž»vð¸ÀàVÖiYÿzuk^jjéÖ ý'ЯQÊSNh#G 1¤Ön\þ'3Û=–(üÑu¸ÿ£¡t6±6±1qvô0qýoõ?ˆ¶¨ÙÄaýû_åÐtG|O…s]Ç*ÖŸ|œŽÂ® %$•i&ÿ,“ž†ûç5·Ñ×[OH…âGЭ'w]¼¹õ&3{z¥šhMó?-Ë`®Œ4¹y=#[.³fŒòÃüO£ƒÇ‘©=¢wþ A ƒ’IÜ{ÙÒtlmÁ z… ‚gèÛúÄç}\K!ûviÝ~àNeŸW. ör¤ÖC€ŽFy,ÔÅ~ISˆ÷dCÓøJ›‰„2/‹³Òò‰ñ‘ð!šêæï¦Uàƒb£}1-#WQQ?¾ygÖ…ÏŸDOüNÿ°`Æ–èë·Ùææ1æß+’3upøQŽå¨ÐŽMÈ^óYÂ[D5F~o[š™ôÞü9¶ cŠM(cV6Ñ4RÜPœ`:…áv~QD—è¾òbKy|Y¾œ]Áî¾ââ~¬Y²Á’^ÌÙ2£-^Á>àí¹°ôÞ"¨˜‚äBLÿ&$&X9²ˆZqÔ)ïâ)R¨3µ?:Q¬‚[6 f)q(äíJ†XŽw›©k¦Àž<ä#Œî–L÷ôÿÑÁúúÌv("Œß3yW=²%ÎIÔ í¸P{‘(ô<4™&GE¹LØÂéñ&›kX'‚®jH“ŽT¦mHde¦ïÒ½0!-5xcy¬È Ïe å8¸•(&×½Oûå‘:Ÿ›Ão–¬JfëÞžDIÚ^uXnÊ—2qÁYiOˆv4ú°7î>Mp£ra ·¼&]r4P¤…Ö} iS $e©\¦Ü5WAk©8‰óq»{¹ïÓô¡|#wÎ’:ãg¹Y½ðe"êõë@ÉÄbïØ#·p*F]$1A•å¬C4]ÄÛ}¼ÜXÓ´@BÉ»ž¢âòØNÔMœJ=V!ñp‰ /ªü!dŠuöNЇâåB!l+ ±Ï4^ã§crK™–¢f+ò6§ªPsþºä¤¼šH°Ö,Õ¦¢W÷ÂzÀðgx¾Z3)êÆÊ#ʸk[ybñûIýfÚ6p¨y(KÃW“8\~Çqê÷-·óÁÏQí4ùgnh3vOмž;f‡:2´•Öø¡}º8{vÖ¤ïÈ9TÕ0Y²Ù ûÎþ—‘jâ'àIO«ú·ýOÖ˜o å ±ë3î}ï]/—6¬›¸½šºM'x/=O¿â _XÌ`Q"÷| Ô¥Å4’UZ´4îÀ¬AR¿2G±m²¦)§æÚQ`³7ijR†×.4†åº•iwAñî«#ö黨.ÔÊÌ?Ýâ`»*vÞ[ù >‘˜[Äû˜qig™ê…Y¡$oïî?#Šƒ€Ï\óØŸ³Ã»ÿ} }ÂÊwû´ì‹@ùÿ‚–ZÔ¬œ¶”t6b‘¹$ŽŠIåñ|ä5¥ $´$)ëd…°CGa×a ù)cªFSµ/8Ir^•¾¼Þ ½ûîõç&žGކwt.jo9¼\ï\î..õürfŠ~õúÚÞ³¾FÏâDl cEZEr€6@÷iå»Äs6HñÐ%âbE|#r‘x?'è’øD;ivoqÉý¸Ÿdw!©3Ú2½¨å[‡¹R¼ªŽåÌv:Ü¿1-W­º÷ñ«Ðß'¥:n°Ü8 d®=G—cÙoA"*´Ü›'M-\J.XRWÕ¦³¸µc9s(¶—mvÈxÕ7– ,<ßœÇf:—ëV„Ø2ù†P-\Iy¦cyÀ,eû2ü=²ýý+ûåI½ Êo,ø[ã«x€ó]a!n4­Œçø¤^ÏÆy*5øà ­°ø0¬óƒVJhqÉóP¬ ‚äǬ.вl·ì1ÉÝÛ·^ˆô¦æ|Ëà}ã õFÃÛ2D2G¥^[Ö˜¹5Ï8À¤®L‰c¹sH²Çí•ÂlÍI vË ]?#5ýêÑÈFayrøÈµAØÆÜç¯mÈuqœöYç e u¯/VØ/]LT#z´®¼±§t¯Úi}þÁi ¨ûÑ9úÎ;‹Ä.E|ëf…A1¯`HcšâˆªBöCÁS+Ë'Y‡ˆT`b@g Ð-Cbt«Ì4U,ÅK¨®[¿¶öµß4¡o ¬oõ5ûu:ýlÚ¶ í„õ¨G¥1›. SÒeJGaeèœãQÜÿ€ êê+Ñ¥Ô¨ýeµå‡æÊN#ÖYª ìïDhND ûðEf…@@‰òH@…kºvݺÎÃMÀGÈ¢‚ƒ^ú8Üï_ Œ¹L ^1ø—Ë'AE©Ù u;Y_ç‹«! Q ;ÉÁ3¶7Ú9‡{"LÓ?ŒþÙ"NS»_¤iý™dï4k®%²žRÃí´žë±ƒ;i$HN¸ºd¯*ÞÜ—o<É<¾sê1à&lõ<ûöpf03˜ì†p€?áM{`Œ ÷ð-û¿Ó#4²%ÇûjΕ˜qM)Àows3šéf`ÈòS^LÕ.ÔPð",~>œˆ®öÁXêOÙ¼9÷pKï3 ð˜+î¶Ap8„S²tÙ´7lan~Ç%*ÆÃ/,Þ,™X¦£pß ~-ÕÑQqîGá9DjC´ÉlXb_ìÂ.D…ÔòT¥ ïì*@ï›;??ÿ "€qó ¦‹ £JU;7¢4uŸŒægBã¡”wÆ\{ÅØÚÙVB‘CÌ…©SA!ÐÿDò‡?йdÊFNè(;J  ƒ6üéwhEh'pÇ^lÇ’ÑG±]¾Ü'…"êk§×b&Ý =ù¨fªƒKÖHœcž4î84„Y‰Šò6óT­‚ž[©¤DÈ3Î-þE:2¤Ya8e‹¾”,‡ áu#XÚaß„öήFa! XÂ.g™¹!JÈíF¾Ÿ²GG߉By…xÙ(•ô›ÕjÖm0Uì1-‰Nr¢$§Eâx²½Ÿ‚á¡óÏùBûÑÌ_jÖ£2}ñ »L4&§wÂŒÞV"²¥çY!‹mâ‰o7Ã¥=—Û¸Ö›]È)K]®#á­Ùk§ÊEŠtlŸWù\€áƒHµ¡ÜARX¨ŒG}úg˵¤¦S™5#æàìÊn;ë¤S©á´zíTîOòJªœí‹‰*8XÀ¦ë yæˆÄ‹yiSkį•ÝóÜŽ ÷ ;¤ztΈ-Õµ%“®Å Å2Éë–C¼±ÑÙ>£™"Y7ƒ…àJ2/ˆe>p†_©ûFÕJ‘èk(Ûã.š¤ì/.×ì£÷dL9™ä‘x©¡•˜ÓvÝ WÊ×`{®øÊk·ùD~Võ´ô”ò1Ù÷?ˬDt:uüO‘ÃPþ?Ó¿ëÿ:ÏÄÎö?ÞŸÁ²rÛ}åê3Tý%çoM2•˪õ‚khZC¯OZ}¸„°ð×€Ž©Ä °ûûÚå‚' °>³þ*8©?í=í=ƒ?N.ËŠžAOìæàüÕn`8 œ#¡1¦œšp_d7lj—B[ž?–f&BOv^"†oZ$kcAÝu+ŽˆÕpÔÜ›`Z–J„†"I'`FSÄÊ”DBz4Áj"€¤Ô`î·†A„ÿ¶¿¯€àxGùÍL9'Hé×ÏèåãÖ®[ÓÍ~qgã‹fÍgï¾Î{;ÒQ¥ø'!B€ùÃj†iíÒI(õ<€RÄz€ yŠÂ:¡°ë2J"ujÈ©Ž J…ˆÌd´¤­šlRÄ“ȯ# ˜}›þ<÷1û6f“ÿPÁ÷ÁÞ/·³PΕþìeÄ„“°t°_Íw~¿ÆqçKÆ÷ nf#( ÒÊ[Š9 /ȤL#of$I 4ÝŽ&^ÌbÌŒ6Ø›è7M™ñ„:ƒ´'aÑj CÉß Q>l=QüÖÐG)gaW¼B6þ"J—ŒýýÄj˜dÅOD6¶»`åoíoÏ ý»lÚ[â@ Ð_ò£e- ˆÿjÆb*ñ 1H¶¬§ñ›ˆ 20‡Îï4axcô©>‚£1š6Gš$Ô|ŬÁ£Ï<¥‰UÞaAx~EñÀù’¨£ÊÇIÉl6âÏI…Ê2Ú%eMh‚åÆânœ˜^Pxïe5UAåiŽØ#£ÞÀcJ”HÚC.»t„h®7ÐÓ7 vÊ ÚŠ©{".ª´dˆËVª¬h¢ÒK‘q;›Fc·‘yauàßE5ßš\ßÅ:éÝ–2à›«Ñ èñ‚ ƒ?bF_±¼¹ [ü^©ZEýh¾ð½p¯ëýÄDþ­"«åÿ×?£zâKO§¬¤x8ËŸZ¾½Hïþ ­†Z†>gï‡Þ±vuÜ:7å¨wØqIZ½ùBÞpŠˆˆ’½c“b?šÓ%‰¤P†ð³\)4_#Ï´¥Ü£G›I›_L½“œîô­u·e>À&-è³8ßî´óåy±‡Ûcõ'åÅP”—û©bëdö/|H®Ò„=,mÏ~qg$€Se°;ÞV °ušèßR±ÝxÁpÜff²Í ¦¥K֛﨑ø©%UàªÔM¤H§Ö?c)þþkv†)¶ù>O¶ý°æð™ÚøAcݘ֙ծ\sª0'Wt•·ÐÌ–G»²®Ì1–£CLßÎIÌvôrÚƒí¤ùkK°Ù¾ë¢®D>›¼'79lAšÀhѰ*¨·7}G“ï‡nÍÉ•7HËÒ?‹JèÞâ°§ìA=öbNeäÍC7º£ÃCî`".^NNÍwv¢ãb>¿[ ½ °iqÎâ@ÿ‘òÞ—À .XÁÊXÚ¾SíÊ˺(ŒÍÕõLJe÷5›¥¤w¨Ô&Çû%Š˜Ñ‹ûËJ.ö§.üÑžaY²ì[e~.x›`¾»Wz/³‘Oq©¢IJÄÇy¥£HÈ\„ƒ}òév¤‹SÈmÒ8y4ô±Y©í¦cŠ,{‚@2.’p+,±‘ïeêïÇLè1y~8"ñ¸^ú/£\˜­\´‘X÷ô#ng ‰[Qo!¹„3ôÅ ;ì&Älv›¯v B;ÂðC CKÓ«L7lMTž7ýÆô>¹…FA0ÆsÅ2zjn€î8â8’@Ã:#qS¦ò¶íÁ¢ºœhƒrG”!õ­Í¥Ä%<jüÄ-×ã¢2ëåqì‚™iXANxÍìZ´3ë¢AŸŠŽ*ŽbÔöh³£øl2–BË)9F’á*H.™ºbk–²øE phȸÒ!k»ïNNEãùðQ@ÉËSÍè-j¹“á@XN}xL†ÿà¥X3; ­˜-3{¾¹ê­€‹©LÕíàœwÆ4ó ¯ÝÓC#ç}ô;Ã5 Øtå<û°Ãé×u|÷¶%‹÷¨šâÚâ‹ ­,Ì%¹$ƒ, ŠAÝžæ¼F'B¥Ù&ÜϦ[Þ˜‹@H¬¿4j 6ÂÏ„÷Ó^’ÓIŠÄùÅ9È”‚Íüðâ ®·]ÒUI¾…¾¾Í}€y¼è>A(]µ  TW3–p3.Þ½F¥ùJ £Î=8À¬ á”D’A¾OÞ€‡9êÍ ¦/é']Ö'Þk[_D¦oXˆ«í&³±ðâ4ñ‘´µ¡^¦C±ÍK`¢3ÒÑrí8biAö’¼†Z?yI|Á“œÈbÚxáðD‰’¡3ñêžlÓhâZ‰ùÒmfì2`—8k7ÿ¤(9&…E"HÞ°P-‹Ku–ÊSnp'AÈ#‹BC´ óà.ÒføËÑæ9HËc‡ï\wo8þ±¬‚Z…l>V­ŽÊî©$ ƒ²‹2I¾¥Ãïì}Vá÷žBô0++¶o|¥¯´¬JFE<Ò“ÒqœÃcŸ~W-hî%hܱ +˜äueá‘ ŒœµŠð°}R<ÂÚÔ¡ñ9z='+'nEâ:ŵGÇÕ‡ÿ³ðñó?¶ô´~ªêf£X°Â2ëy´¤ðçixO8dx¸»Dˆèé@v=î;"/¯LIê­ëTÕ¦6¸J"#%c,]೬;iù¯Ízý»ä&ËÒ~Ô  deE³¤…»b‘^§Ïý“wöºŽï‘ž. ¿ÚQw¥|ôòðs,Šªú [Êê€VÐé"jä_ZÙøÑ’¤Å©vjê©ÉùÚ,:é ³Ùe ‘åÇn•P¼¯˜=5̵JžÝ91ì #(²µ£«×¤±w˸§;Tf6禴9ºÂ¡ H 9±úµÍÕ#ð÷TjFˆ³þÅýÀ'HÖ~BšWØ}B2?0Ðgö»w°õS•úV L·X·Ï)bÆgn’§,Ÿ±Pî¶ð¥­"ƒCÛ=°¾÷OŒ€ Á*ûe,iaþ´ˆÇÔ£©5f}ñÑ¡Á*²|ÀO {O[w ;xþeŽô2¾“‡+UM,é¦ÒoåçÍ^8È0uà3ȳþ·~i¡îVHÎÉ“«`%åFå¶^®zaÇrY¯ÓoÿþÙC–‡ `Þ0Ü-çÈë1.é°˜Æ\óòLÎZŠKKµ›O!I{”Å"K}ÍÛ[é…ŠOèêäÆœÔ)“x­±ò\ë¡u^‹|O|Åìaµû×He¨f oJÜJäänxkò¼vv2j¾Ý¤r[AI%Šž3¬¢i¸8„OMH÷¶j°æúäáÊ1¯QÇ{U{jLã\+’ÈMg…ÓÊ"½ò)w¸OAçT$¾–%Ý軪'Ù¼n7e-LŠ"ÚÏ!®wå‘凜kvQl•ý6šbŠˆ\îFeÖ®¦ã馛z.æcé‰1õ“‡Í›b?å¢âNG'mw~Nç+g…àô蘮™âôë­·ë ñ?0Ë@“°~]Ç߀&–—»:¢ÈŒª‹]éY®½Î¯â²ú‘°g¿v'ó~ô]v!Úh½ïY`C¾j·o[zÓºÐ)?î YÏ_[z­»ˆ­üÊ÷:½æðU‡EyYK§™?€ÿY¯t“…S@ð% ú¿1¬F-š<öØc¨»Àüñ‰D¤’É4WÂÔDÂÈùÆbìxBDC¥üü L4˜ûhû a\ç·ü‘hê”Ì”ÜÈ]™Ïd2Æ£Ì d"‘ÓÅ’,tŠí¾»^]^¤ׂD#»[;m~w>wgnd?oÐw¿íÇr8¼wƒäí8FÓ|1Ñ*E£•™•cꬥ—ª1Už70 9$Ud¨&éÄÖä#•”D£éI¦7 ª¸&X¼cÌ£›˜ª3õ¯ìU±À¬•ÈÓX†|ä)Å“Œ¦°J&‘Äʦª‘Tc˜s Ô3xCXrò>É—ÌÓXEpÿ=U0Ii:€Ó4©¿²4i¼Â<Í9êªkèúÀ|Íx}MÓTªþbb*#ÅÙÈ9V}ÇÙ$ Ãk¦mÄåW6¦ö· ÃQ hœSl° Á§¿J©Ú3^­¡š£6tZA9õîðŠæñ 5Í9cZ­ÌÜ¢VñŠ´ŒVáŠfÉ1zdM>W)Õ(hDWŽã*³?¾–þ”3©bšS4y5½Xñv¤æpëeg×x©5Í)÷l­WÄú¯á$ì]בèJ2ö‹ÿÜ„¥<I¡Šû;_ËåôpÓ±ÁF’V€?Ë_22”iÔµÆ:–©s:ϘTªYà9F±j»K©Êf’5ò(jS"ßÝ¢[kƒ# Ù‹Í® -y ›¼mÅhÄCÁÚ-ù²ºƒÂ+ ÷"odýr~-ĶÎtÀÚFT3$€gëä3çÜ©ý–ù|¿µØÑeö•»uj aä³ <^qfo¾áµ½rö6œ0}»´r¥ýï…NêÍ›à0£6}Fe.ö|{Ñæßs.žÈRr“wË®¸~—€y'¬m~$¶3k»=DØwøï±jäÉ̦éjmêÈÎBÇ£=Ò¥ ‡— R;$ê—ürW«íʃå“óTGØÚnuÏÇ'’]Kê´\/yåF`“¦¥ûMë„ìǬ{ˆ7??M’PT&0(-?¡áÄÓ&â=¢:z/²Q—Äé•ûÜ!ây“ý*: àãâ_þ„Ô?öì{GsO§+#ж­V°ÌÊÌå]hNá1‘[¡ZOBífÅÿ:{ã¤/òŸcïXÝüýi.]êgJ2oôýÏ›J•¬O –•nuªñ7ÚñhÂÂ<[g±Mÿí/ Tƒj¦½¬=Öìµw†#EÑZ~•âP¹)òal_h²ìíCp¬ñÓsÁ´/u ›®ÙpžóÂwõbŸ;¥==s‰E¡ÈJLzcˆ3„Öóû0mxÿ*nzJJ:ã–™#o2–? 9~rŽã`œ×¥è‡IZ‡Ø)GŒaðŒÊÇhº€˜}S»b˜ñ2ô x½ÿ[•ªOùò3 ·×wóbašðl3XîyçHÕ¯¤%â4ùÀ]Å6åMÛL´Öðž DÜ5þ¡Æ2 )à¡…=¢·¬Ü¢pù£­¿d{¶Å¼tUnªîŒwØ[EËžs О ºùSdЩ—á·åñ[{ºAŽ Ã)­wzM¦¯ÚàVòîqfVëìQþ´ƒÝy G¸lm‹4ýJh\·] F袗°åXxà/PÀxFÒ§ýW3"^¯‚ŽàÚqxÖfå œ¿Ü¬#èÌ6ŒÜcAýajЕ'î§5·Û¤Û“¦6R@zŸ´ÈÓìëmÇç†?Na©Þ·¥¾é@šyˆ"¸ß¹ÓÈÅU˵øm°üM mð„–í´ôð:²ÔëMe–bCïEGŽ l)<Ÿw¼Ù w.¹) Ý >Œ'[Ô Ó‚¶­OE Ÿ´L{ÎI¨ÑxŠ-Ù²\Ä-ŸYÕA{æ(”p*øz2nÀš j*œ‰Se>‡ŽHFB´…Ë<|å‹­AŸ¸¹HÖ±žÌVjR_JÚ‚ù.m•¤‰EñóØ ³L`þ ”P,Ž%®„çÊ È ľp$þ÷h0ËfÌ~ $ÊâJ{oÁÉ]8GÀæÿXdÉ+È9ý¬Ÿ.Aä+ \„”QNˆøËo†ynŒD˜( h ¸ªÒèu?‹­\µ01°áȼO²xê®#E©åŸöÓ*›FëµhvJ‡ž©&Vœj‘EòêaK (²QÔ{®í"Ž3Pe³ ›‚Ím?y­m²¸¥êYšZþÞ»éÀmX:>økˆu¾‰®”$ËM;æT'ìK¶L\|þ3gs°k›oQb1üO±Þæs[s“¹f”æh«Ä¦n&MS‡k€Í §à¥Yf ï¡ ñ]N¯·GH¦%(´,m•Ú-Êð&˜ÆIùRãZÀa~5øžkLÐTœëf•r‘ø©°| § ©@óÁ È ám\¦´5ÓY)üZ!4–&µž_»(%¨çŒWDÿâøÙ*—þ·„ôWÁ²_¨2ò±´ùi3¢<©EÛMßžŒ1„ †Ægß‚†Œ—üUÓª/?ÉEY9*_;xR…ÎÉ€t@=ó?`¡MAù+˜L彇#{£`ôƒg›œÐù‡>ì_¥?«ý›WEW1sø¤{Ó¹ÿôÞjºÇ8sá†qÆ"}3‘ŽÉæ.ä¸Gè ÞÖ1fkëQÛ²ZëÈ>.抷ä‘RJ¡bÙ|´Áñ£¤O-,®[iGÝ–gpØÚUb>rŸˆ[/·ZŒ’çŠÇ&äX¨ó¹ËÎlª¸ÛŠ-²Ë2Er¢¿|©…P¹‘·Ë?v_cd _¸š'´g)* ]s¡±_ŠÑQ¨'–ÚÿN¥/@®]$ÔKn¤ŽùÌnÿá‰m÷W·µT¹ z<é$¶Ó%3¤£#©n½ðº?J#ÇÇ•£DiòªuB ßÉ~|çÚ?ulXQôçQ‹Ä4‰ÕÇV ¿…Pû2…‡)Vw+8>0o–³¾G*€Ái#Omð –‹ˆ4ÇBág†ècWÔ½Z yDð†ð|»ŸÌ$ VIº»‚¹'òW‚`ë°ëP1HGÚpt÷Ð NÚA sGÉi©ÕJš…kvúY­ô(Qm×¥LýM‹U\‡&i5‚šˆs'§ýÔiP†Ý‹Á‡o¯2BÖ'¼ÊNŸmeÖ_'G" œ –N ’B °„äë-Íy ÀÖd CÔÉðì ´ †˜ÎŽAŸÐ#‹¨(ÄjØ ™ç˜€#%‘„[Ú>£>vØ:ø–´nÎvÊþQxçÕû@è:ü!…(ö}}Ÿ„ž :]€‘1H“eæ®yõp3›­Pfà»y¿ˆPÐ#ƒ–ÀR.0û× ×·â÷üÑàv!eh_Š9ù¯,ô\ méÁ;>tÞant„3?Ò?\÷9¬äŸ¨d (=”ɾëô(5*3,„¥˜=¼³¬4´YKVëÖ<=‚>Xn(ÏàI­Wòÿ!c\èq)ÚùbEêßò­kãäzXI…ÿæ÷˜ª7Bê#¢¢ÁLp!Þï-aš›)#ßøP3°°Õõ+“!}¸¤¹ôUL÷O–Ãטs€KJÔ; ’míò_pDoN7?_‘61ˆ^lš™8–éìøpÖÇeI&5»µºÌ‚ší}J0dù :"¯€[2§“—-ª“©°G;×ïÝÿ4:’|aõé?aÔîªk ‰8W/I¡À”X4ð‰aþ4Ú¨€m¹Eº ª±e¸žK¢qixG £F9@Gf^5kÖ)Vc99ât£# öêiaïæùp½F0:h“ÙÉZ2ÔÙSdMÏ“*¸ç*Ò”ŒöÇITC½Ë‚ìœJ[”Hü§…iJJFè—™Þ³¤eŠC…–¥©~ýþ‰ýÊ{Y¤$‹ú@€& ÿã*;'g kz== [ g==:{“D»,~„»_Tâ¯E*:WÇòÊSa\5“la+ }’õY¸‘ã5·þÚäríyËÝÞŸÝ-þÑ€ŠJPÛÕµ¼}ð“ë@jðk\lB3³¨  [J1ìzÿY´qVÁk™Ú¾)cȸq{䚃Št=ѧ·¶K†v8Mf þ–|RV÷Éèøí–^B#§àÜâÜØŽtaËï{1ÆJ¶Ê·N*½«þqnß–ð¿nÍü H6GíšÌ&†êÛ¤¶™V|÷ÍG}l@ ?·a¬vÿ‡É¤¬"E¶Rð1}*Ñœl/2B-ZƒëêÑ—Šò¥Fäv“ŸbezÐÍtFxfñÆNïHf­ÊqeÇ÷6oöþeî}qKB3úma3t=Rpt++jáKfï¹ÅnRü5’qIg z„ÕˆQ M0°ü³üF²¤'™Sª9Toà–8÷ã´!LA‡¨—‹òRL=¡÷~åæ–v~µ´ýs—˜ÞÝXnC³U,ö¸5uÊÔ-úºœ£¡Ön‹E5žO5M Ð9òÈ)dècCéC ü!WÜ? ÿ³e›iP÷ßÝËÕÿ›oo¤Ó5È€šäšõ%ˆ–$$N.¾iƒ,D`SÜÁeõàÙBç¹íƒ³ýðsá”kå[3ц” DNƒ•…$&.4‚T‰ähJ‰w ¥°M „»±Š»AáZè=,˵5ÚËÒ ªûUú@›÷û^S=à6d1mÜ.ŒZ£T¨Èð'¾úÖö!jn4œ§)mÉX’ÜÚB@ÚÏ ´Rû@ÅÙ£}˸ÿ¿ ¶pÔqpápû?¶øÿ¾¶µ½…íÿê ÎQ°=bû_ý5¥¯l«Ôä4‘(훨™-è Kú (@Ä1ʼn¤¶ ~wò‰Ä\#…ü.Â3GYcb²ûÈ&ª\:îê?ZÁ\Š/ÐՒʾ,˜2ûšY=%ö®¿7ˆßšZE5÷Å*ÐÝE0)C8–)E z„ÕÑãTÜÓ4ûfͱöˆ94£?ËÌ×.°\ à%<Ëp8($ˆ÷¤BG(Æžf$)„,ÇU®¹$†‘ôÈvG3™b|ý–? ¾‘kÿ ù[#ÓЂ|Ñ$D§ƒÌ #üݳ¢¡&ÛéUV …)^˜5GnüºˆÑÙ–º³5HnÆÛ,ìÿl©,wºî¬î=îªØÀðwC,×1À8 Ï6¸DcÕ³[ñ„!ŠaáÝo$:ÇU(LÞa\ê‚ ^n­B6AÅ“-/ ôNJ±mÃäù&‹õ…Ôão÷®AûpbçÓA“Q>Xr8’AåøÿÚ3o²qìµHÞ×>ú'?“#S•)޶‚ i º4­FŠDãíøâR¯¨ž­7Ÿ‚›_þMk¯sÏ8=µÄ;ÂK|…Íô*1æ‰âüäÜ7ÙŽ|ès^™ômªÊrb"(7çÛÕ˜c‘¼È_ iU­"èx)}m<»%"í6¡¡}Vlœ:ùC¦óˆ-é…JZO¯m„=E РϪXŒŽe0’ð—ù =ÞäJ?îLH©z¿»Íˆ UeŸôÄ`,ýè>-ú[6Ïü¿ñýOÞIºøêRù/}!þ½ÿ{µ¨LÄ.ñ#Ô´"›1Á\"êB²A²#ôw`Äuå\Ì7»ed!{ã’ÿ†ÀûW:2a²9F(Y¨¥Uéõh^ûmõ}¾Lu¨QNóòþÚÍñ > éŒâÁôtŒšÉÊ`°i\@ê/@? ´ `ßcî#÷O@ñü;g[¨úÍóUÛ_¼sÐ3‘0ÙN€.‹ÑšÁe^é¨Då#ì’¿-’/ò $u?@†Ódáóþ¸Çtg&eóhZÈߣ@`~{ÿ! Ió ß)êg"üà}`u0€ïÄÓ|»1H.p´hÏ!!NéÒÕ<ÖøG ‡ÒPSZ;ŠâPíts¸Ž;‚Ý7Á;Û¬ðÀâ åÒÿÔ¨h‹O7j^õh¼D¡‘$Ú¥šQ-Õ\¯/ËFmcÜNÙâÂSôty3º…½vtµ&ÈŠ2ñU©\}<Ôœ.ÉMc‚‰¼›ÕªtœÐ¤ŽnÓ •®´\N‰ÅÍ™„8¢Q§é¤ü‡žÉ%ÞbdÔ‘"{æ]Ì-úéE´¦ _:ªÝ¨ŽA,M²B€ùÌA¯D5­P|Ì’•-6¹B^0ã8T¸çµùÁȸ0°›Ž·6mô&a;h8(%_Ì«r ·jùÔBLÕïÀäbdÚ³Œs dÊv­’\ÒHG™3+–Ï•SK+¹FÙHCâ¢b8Ý5»¿FŸ»è+³F*µ®geĉÝ ç ÅXPŠ£¡©* T =ÒX‚*òßußøDìÎ2mçûÛÈ]ï×í® {€“ ›j55üœ\ÞÔhÛæÅâR(ë&}3V6Â}Î~9Ú|9í;”%FYž¯Œ_¡ HØÙ% 0ÁE[â‚Ýpýì²5°¤—9Rˆ½ø´ØÀ’‡®,™ÍV)M¤L‹Žœ1¸s’[ÓÓ”‡ëG2ÈX¼éGý{ bª¤Ž?æâÐF¤²äj`kÿ*šÅÜU~[Ë t žÐ}µ ZD ¬Vƒåw€†–‹ò¯ÿµ!T¢XMÀ^Ù 6òXØÆãÌ¡'$~©šÚÒ‡£kÆÀ!Ô,úSÔ†ckúÒ%«ôþÁµ/#Iì.&ös’8¢õ0‘{*!… ù8æq!Jö¥âUh âê'ZM¡c~MtØ…¸®âòà˜­=|”d‘—ZŽ Üïú;Uë@9!Z(¿ì`Óó3$!8‰¬üõÝh!JeGÑñd™ók‹ªÅQˆsE•üä=zzÐŽ¡¼¦>ô2ÿ"Ôª8¼Oj”mÇx­Ð·~?)qTiªÚÁÀ½¾×³#Bg(Á\ï\œÉ<þZü¿!ö¯uY·óžx¶Å¾sÂÊyÐìE6öp5:÷ +’¢HYÖÏm›ì±Îñ¬~ôРtHÛôºP/IšÏQÅ+i,³Šµ$Þæ?ßdJÏ´ºàO^fÀÄö&h4:z*ÛÖŸy|=ëpBÔÕ3(Ÿª›™„ðʲV…¡bê ¯ P!½Ý¥ÜbñÑþª,œ¯™â¡6ËÁt¹dÆ á õ3§šóÌBgÕü+5ZdÞß¾c#IÏÃ0ÍÞ„í*7s; á^ºôP&/d[ ²ØE{z[ ?Üù‘¨(Ðt+ù™¦jïNbÆå±/0«%Ü©„l>6¬¼‡vêGL¿Gõ®d=^ª3¾²?2Ê Ö§‚›ÇyÒ¢;H‚$üŒ<íß¾‹¡eÑ¡v6—:>_És­´c«Çþ«9¯kí*&›…9˜ž’Aﳂ9œç»Õ>wsêQ»Î4 ãЛls¢ƒ¶ßÿ­õ ~°€ìÿ6TFÔ¶·„nŒï6»²vª!#‰¤ ÐU¬T¨äQÒì)ñÒ*Üûé_£mlŒ'J0‚§”A½Ñ価ñvj¢N† ¹×õµj0-Xœf/le¶~žÊ¿ÆîìþàáŸ8°BøûKü¥Àét0:¼ÖƒD Ei&Aw+N”üÉöâ6Ÿ·ë÷9 ûîÐ×›ÅÌ{W—ânÚ†î¬Q¶|ºEØ2”ÊF÷H ÞacÀ0 5QÎð8òu23WÛ¯”wÐаVé¢_ùqUúиE¾qåmzW@[Æb€å¿¶ÙAVÀàn Êmq‚8G!Ç þ 3 ^QxˆCß‘ï¡Ä–ÕÓ>ÄPBi†@¯ 'Ò½+˜'6ã‘H`¤ ¶çmàë"Òœ‘bj 0/jšå IÖØã OâTŒ¯ b¿>ŠânÁÜîÞ=Ùž†ò¥ŸVìK‘¯äGˆd¾—Ë»ñ ¡Á¦ÆÇ»EŸ< _#ÊÍq]k…”§]š}m™øbG{=¬«uË»Û\ò_Éd™—Ø(Ô`m°F°PYØ¢ô17?²š‰@ÎJ„_©‰Ž(6H—8â íE=Wù5EV±»"ñkQk^eõñ”.3ùgȧ¨Þw+#±ŠÔ ®Q1D2 %5£} C}ÒFL<¯+#bææ2x¨uÜrÍ¿fð(ºÄ,XýÎiŽÆ¬Ãr3¯ØÉŒSéL<=žd0°{¤>ŸK Ó ï\ƒºÕܨÕ²ŽdHØ×¢Ú;;Dy–²&žô%6èà#¢ÔšŒ -èX “-²DØ!aÝ1D]¿–»º%x3{Û.®(›¡¤×¤_±Æ£3ÀEÍøD‘}z Ý~M´wÝÉè–NœrŠÁáõ:ÄðnÒ/Tàúœ5Žõ&ž‚vN÷þŸ›g›È–¸þ†ŸE“(mÛ» ‹õoX5ÞØê?~ô  kX ³M—X´‚Wû@äwo´ƒui(“£üÚqQeóâÐà ôH~úž¸ÀÞ±Žâþ1Ô^Q+F¨ b0GÚSh<ÖÖG+s²’³î^µÐñozq¬Ú?Óa&æÏ´ðÚØ%Ó{$p¢²IE#@Ù ªÓ`%/¿.ÙrI$Qöh®ô`Õ8-œÝÔ”ËQâ×óÞ8š%Tâ‚j³¡Y aËØWe–I.º’^0J(IUfzluŠ#ÓRt(÷ žbaVrŸ\—Û'ÙÕ|˜T¹ÐMH;Øîò/¡áÞaôá¶åvâò¢ïñ*Ô·Ís3ßä§»ò'eTŠZ4…:G'Ú9ùn¸éqKØäGò‰&º3õã¢æ'ÈA­ù¬VÞe²;üýϼOØZ׈àÅöC&†föÿ±É‰––vGƒïZ]ÿ"Œ¸!+Ž4@4ÅíI_YàtLòµKë£ã;S(ÑBZ½^ƒ“Šu‹v- žœìúIÛµäzÌ¢[èÌ# §»ë}ôrvB#“\ˆ©+éºÆòûØ“¡ú¦xJÓ‘ù4{þ™ß%ì Jk¥±U¯IGc5 ¸ø²47Ia½ô)ÂQ·È–xW¡ 7¢~ÐÏP’¼Ó”KØ(7mæFÛ0ƒ·ê*ÚRQÑŒ)²Ã!«¢§Gr‡ÃéùØÆGUU–ZX?ýŒ{šÙ FéHŸT…AÙíh}Ò=CƬRæC Ú}–Êø­¨õÓI8fK=v-¨¼DIƒæ¬ÔV¢Š>\ë6Àâ!ÇUÎNý†L\wWÆN´ÿèׯ^ÝÞNã›™Ý^®ÎάîNýUÛõ÷祡+WWo`™”«ë ükÛÆÊH8d< 1TÌ7pR …"ÊF -Ú$£1TÂxˆ´¾ÞGã±Ô@ê¥î9NëTÁÚb>l}PŠtmÉIêîµõ$æ¸9ÛЮõMdõþ±UöL-4†1[ĨÞÐ8°¾¬ÅTûª76š„mÓ}¾ô»ªñ6ÐÐÕ= Ðë^´ämpMn5]ÒåÄd ;»ºy‹é÷™b”ôé ð5CâúšæÉBaó}.+GDMä™õô@ÀÊÖà&<Åá5ÌYC_¿+Ù´'¶é yNÅ Òð¬Ó¯‘¤…‰)Q’F¨žƒí‹=ÿSÞf”Þ3WßäË Wú»¤U ‡¦6‰äH`Z, Ýv±ó~“hâF­óïÝóÒáã‡`€wÉ8® B@Ú¸¯ &8¼eZÓ`v³(LY,Áè¡úéâèC¸5cvaw$mµUY8eÙÝØ}o ä;#éûN¾¶>h™(j‰AÐgèPS˜Á'å¢:©™Z…^sáÎÕËîÈ£/Nì;+º>@Ú›¶éø¬ë'Í€ÆSˆvjfvn Ä@â“y§‚5±úm‚ ÿþªA' jI${5˜tv‹rõü¥.Q/Á&–M êUøŒú Qš?±…ÌäâxÈ2A!ÿKrýí´Æƒ×˜EpGTŸ^}‹àE?t1{4g›8ö«"‘ªG)(´¢K@ñˆN¥>Fhõ©)µ¡[«V§:ãSQ¿£HkHEG«Šr”g%(¿Â쩤‰R¿¼7õj!À@“„¿¼wBWf)ìÒvWè=ŽêSñEc~>ÕSäænœê‰#è¶/—©•=²¿0[zôØ5‡#XÈøL½…­E¿o3Š1%Q†´(Œ)ß)yÒÚ? 1?LÖ-  †Ô Å( «Ð $ñShÖóœ:aG1Õ|H e]´«BÙu‹êº'»r–Osº'â.6j×ñ·+a¦°ú^h Ïvu¡Öç´"åmüÒôë:¤ @Tj8U>‰æ‚ÝpÒª.usÏ fâMk€¨ñ Jl9·çÅM­ž„:N®9mhß5Ï(¦ „‘ýwõ¬m#hO)á­*ð¤Ö›ùÌ£ô@ž3-J¨vqáóT®3MBvU ÝgFaæýÆpN×ù¥zãœÀ^N¬AŽb¼edÝbüú눃j\‚÷HèÍ+xÇzjMUø¥À9' >Y¶þO›åÈô9Ë]BÞ€G11#¢ç^ãÙØÏMxMãÚHü‘€ÎkÍ•¨1*"Ò„·ntæö¬|!?L¿1/DÓCí¤X ¬³š0y†€mßàŽᴩ!¿Ž¼þko„rJÇgQì”O|©iÊÆzÓSŠëì:¤"Úã9Æ„ÕqLJ«ýð¹Ðïµhú^<ï P1Û¯¼l+pJª“êÆæqêîÖe$ÿAõ†UÊ…óµ ÈYÑtØš¬¹ ¿ BFGŒ UP*©ÆÎPÓî«%u¨$u(¢3Íêƒí0>E㬕3k!‰! Ë¢ñ»(ž@jGç*p¶Õs·a¨…ª©“ õo¤,D¨˜ãîF²{² Z\__Y±i”grÜ÷Ë…¤½Îyæ&%©Vð:tS­¤ûº8 " †_D[ù»D B´Å”P3«b¾.ÛeÏÝôÐ:aYhþó(á‘ÓP‡Ö“þò<öëñðÿÊÞ)X K»¶mÛ¶mÛ¶mÛ¶íïÚ¶mÛ¶oþMURµ[©dó<ó2§«ûÌéÓ“{wV!ëÑRÅ3Ï»&Œ-¥ÌV­p ý`JÞ`h5ЉZ*L¤IˆÞ}HžªÅ¸ÀW [ÇŒ/³:u*òÙçZß $;Pr€ Î6:JäºZ#hýÔVt†ž xfzÖ× bvºJ:^R ª ¾†™ÐûInø¼<<"[7­hÚyƒ è8ÖÉ¡Kì«0>–b6zn9ÓUã‚aô6"ÉP:oFg¥«õhe•û^Qi[Ôõ™>á܃fÓÒõð@³Ck5L#Q˜5õ)aÁËãû5V'} k |4 ¤šªSžkjžÈðX‹¡5Ô©Ñlpˆ.¾tœö]ïWÙV23”lJ¿+!­þ‰O8Øô˜sº… rš†²AH°ö¯Q—!ëÇîò.!Ý’êC¹‡!ˆ˜°±y›@á0‹^Cû’A'È< ÎÊË[ít¬Ô*dÉ´…QF‘ÙìTÓÌ&)è>³z³~9=€g`ÚÈ 5£B’èi)rÐýzº¹œÆ/ó)\‡÷~С"º‡ýõGŒYjªÐ ÏÇž£`EúÉhIf* @#^ø¹ÐÏ4™ŠÞI ­QTÜY°FîY°ò® ýŸð–H D+¤*Ö3‰Uùš—Gˆ à!Õ<¬,*ùT7ÞE·Z»Èd f2JN‘ëy©ÇYdTètÞýÇ€#Ï/n L>EÞáY£!ñæ.ÐÚá1ó3ÛDžÿZïå$×\a¿Û„ûT9ŸÅkî®Ñú±K6í;åñMLH£Ž” 劗Pþ>B{Í7 YÁün&÷b©œ¯ VÒ÷ªøSNÒ9ÁëøVw&{ xFðNiFú³ÍÁ¼ñ׸ô²O,#’tõóØ”G¤|†fƒvˆlÊ+0×2’kŽÙº „ÿ¥‚­"õÐÚÐ,-!ÏÌÙäD~»0ÝÌAaƒ ‘ø)˾‰ÐÞw$º°lÈ;8í=]ˆ8ØÂ$0,ÂÛ^‚¨Ã&W“ö$áy§‚:€Byh Ó­ÇTY§vªRzW*7˜ÝZa{ }ÖÚa¬s Ëæ+)Êò†CÓ ¿]yÖQµ¯ð33`¸ÍK!îÍЮ5QK¬CtKŸ®ï° fŸd€0'î­¹EÝ´–eb¤§¶çÜÅ» ÷ô†Gþkˆ-}Dð!ƒ²#Xd‰“òº“ƒ'þ~Ö96©_9ÕI%Ü&uS˜ÁÞM™A›N>§ò‘7,Yš ëª*’¦Ç¸+¿( ͈`'Éyã ã® ¥PÛd+PƒŸ[‡Eýy²MU¸iÒ^,¡g¡ÂÏÍX4r‡·Ij°çÍŽûç †¿µÞÞ¬P¾¦ÐO &L¡~8WÅòâܳîn½0ݜӹ ËÔûí¥U¡ê3¶d7ÿ$ÝÆ%ÿ‚rO£–+¶-7W^‡À¶ 5?¯WÉÐ?öôGmºÖp¿ÀšxõÃ~@ªy=²)qãàL¨ÃžÙFòñSM˜_œ[n$g>©û=‡fízgºÿ&{Æd`ƒ-¡ž×ÿ}; &ý(¿N楺iP Ï[=iÊ[Vì]op­h¹Ê–垪,'ù%_°6f—~Í«ËzZ …w¨r¹ÄÇö÷e»Õ™’`×nŽ”ùMÌyùÚp2Ù#°¿e%›Ž%`îR‘Pâ¡qqyâÞòí»rEÙÞÒoß¼fQ BâÂ4®zÎSÉ'ôƒàÐ~ ˆw»òرê˜${ïâ„ʳ@<¤¹/©\æ—ï{iL_ý"üøþ,¾o‘îbåmÈïª6yåU0"Ȥ‹ƒÉ¤d¨ž,NlÒà¬ÿ°Ÿ™ï{=Ž‹€¯èôpÂNe0¾„üËÌ%,sA©º#›t$O@MàžVkr„˜ÿÇSŒh$Y(ð,²n‰áfÞï&» 4*Ý C6¨-ÃÒKŠÂ´û¼h 5¦M•‡LƒÈ`;ÿ8l–²5š-àÚë˜2ŒÎ ]ÄDÑ œ|šö[ gnx’æfÎ7û¼P×Z)®”´€_^ ¹ùöávÝRÂÙÎÀÐbH™ Ÿ•+•áE½å˜LT ¯ù-—²ó#ƒ‘yl§Åò ¬G1[$8jr‘˜úïy ï«YĪ¥C¯ µº ”ð圀¦>58P¬t§•©è %ÌB˜ª‡Níiáû‡3y†ø?uù¬Ù×MvïmP@ J‡j•NÇKúØ(’@ÕÛ=þ(Hc¸{ÛE—ó°O‘…Èr%[Þ&7RFÿâ\»ª´û2oibežXåÔ[ô=ÀôØYõÚŽy= ¼H¤28YIwí½Zf5Rw‚HyܶSxƼSûòkVÿ„+§™G'^pzåèË ÿ!ÏB½§.¹h7eÝ)™Ó‚¬\£Y\#Ù†byõù %5éµ]„öïASE9ŸÉzü]³‡Hj6îŽóbt«¦Z˜ÖYÆqOl6\¹ë{œÞ\nZgsŸÙ=á/üÄ8*.wu…TÀ½ Úºqž‰Ià.Iø˜Ø¬ú2¢À%ù™8ñ;N¤÷-Ýa=!0Fg7ÏšeWÉ èj¼Vl³ßª<´hȤÞ×â_zK_1Dr ®Íÿä5ûõ€ïy%çUùÎ{ˆÞû×'áu¨N“#ôÈ] ѽo:¼ùzaí3.¤Ì—+hæÃ]!ôúI90q_T‹·*}Ú³Còæ~lÇ×R½ ÇßûAÑÔ¬*¥‚3âIG'ØŸFcËê±·éû14øG¨,n]'Ý´B(~3fJ­[>`ºé4•šs.³Ôg6*èTÒz}ÆàÌ1º¾oGÙÌëe¢¼ñ=°îq(Sô ”Ïú&ÜÉï©é¬Õ)xŽ]}ĉSšòßïw$}gÞöøWÄXsýrƒ#Á#Î-`Êmœ‚!þA×2BÞwèõ¿dâc‰ÆÒvˆË¥«í{èHB+ŽÁÅÂi) /êöåV°®øOöbkŸ±¬VúaÚßNZ·äNG”Gñ6 éY€Ðyˆ¢{¹(³;ýÁ|/m˜òty¹v-ÑÙûïg_=õ ówózÿx™Ûïv÷ôöÄíévW2zÁ9€mÏëîEüÂ}Ò^g¿ýqÞ6Ù(T>z:/ÅÚ¬.Tþ±Rõ‚û~ÙÍõ½@šä³ÛþM`Ž[†ø‡Ä](€Ø Ú@v2àJ¦®Ç˜`V% ­#bóû¼•O<Œ?leµpÜsSăqÀ ªÆð†ÍŒ ¦3v_‡¡‰Í°aˆÌ7‡ãÕTœ%Ü)ýÕ-®˜YvYt>Ë®¤`ji»›£ëJÝìÎ¯ä³ }ß ]fhKõÜ ówïÁ濉æK%lT¤b`Æyfæ1åÌ)8œ—ÉѱLÇ?^;¢üþ8=rÐ'Šû36T^¦ò®3õ=ȸý奕˜ŒÎáX12ìWr§§&ľ«ü:»š˜5?>"f#d¨E uZ‰jçc §è‚­i a2j»)€]Q*(Þˆ}zØé=—íºKøž•îÛ4Ýv4{«ªâ¿få5ï©+òÞ·ï“ñ3ž‚ý:yoÛeX¾õ~í!Þ¹£Oëh‡Š%ô`T|ùwogOvìÍv«—Üòîïºý×’ú8ê·k Dèîú‚ô.È<yÀûmNl•dj{Ý£Ëmh Ò€ßìîzáù'ƒúsðV Ì4@©|zÑ >)ÀÓX=ÞÒv߸¿Š3#íìÒ7Úç÷«hÿNù76ç—c¡CžÃß÷ºa?€÷û?¶Ä'º þ ¼rþo&Aþ¯®—ñ‰¶¬ó¶˜—LŸHR¥½eO+!gWQ÷±BÒÚl›i­ŒÜjiÀ¢Uz:X`f†ANv•”hÊ aøw€ÊIN_¢²“ÔI’‚t\ÇIR÷ÜÖNÿtB£3B²Jh‘ÀÒÏë×Ï /òŸŸÙmþïçz!¿òó_ú³~ÏâÂ?b$ÂÖjoqî×^ÂVìW?!ùõ/É^Ë»òá×”ž|´­¥W?1›¹{_a’rw5›9(óû޹ë<´®'#ïÝ^Î!ûÙ¨Іž~zr‘FŠÎ³áW΃}O%’3Ñ· õ^‚œ³Jr²ÑvòGÐp?˜ébzü@S˜CUhF×aû"JBú4F-êajB¿._Ø€Ö4³óÿŹS$F9ó ¤cO†ûèé=L¥‹·ò‹G+MDòüSÏ´«tóóF:mÁL”Of¼WÉíÝó‹3)dø‡oh݆AÈp{²~åm™ ‚m™@¤m„·òw³Q…z_)ffê"efò"Í^NÚ"Î^B;Ý“½¥Cm¤MÄ…lf"®©øáP¼V ¢O^ò^×þ¾sž>ø0Qô÷QDÝãôt—H{ºès¦=ÄÔÞªîÚRkmˆQްCVМ"}q¤+l£6@@9íé‘RÚ‚þªë‰í£1e—‡ŽXè½µé¼a€Ìæ´~ù^Šþ=Ê#­H#U–dZx58¿×Ä¿yuÓ®,ÝÓ¤=0ÛL´jÃÕ³ «2)¤ÙK¹í8ï™Ýþ¡Íj€u òæwyM¬ß$:pÂqÃÄŒˆ˜zÞ‡E@MÈëTâ#¡k9! :¢óÔ£n›ˆ!°Ö5„fn¯0q~ÉÈ*Ën`ÄÑhчn¼x2Ô¦jßOt¸ãÎXå9È…3AöZ÷.Cz§èéD‘<—๠\‘Ù™ŽÓ3Á¤vôÖC·LpÑœR;òó“ªmŽ•%ÕÓ×Òl©‰„O] #¬* T­ØÕ´Œ•Ùm½¼›±(€1ËìS uXÂ.Y÷!X`­œÓ…‰",•bmÇ:(ö«—Æn Émà¬%È×Ò.”Ôжß+×¶ìTË@ôjÍO¨uhI-Í’Rix–‘7âuQLXé&£u¨u þðqÚ%*#Ñ0n*éI!2à¬,´f³ÕM_útÑÌò[¹4Ò7xü”/zH‘xŒ“ $ÂGÁˆb½¡eî)¼hêÛ‹ªãM%Sy[ñ‘-– 7ëù<Ô[¸ƒ¯C[vé2:Y].g¯¶o¾oÁ[uD¥’Ä®Õ@ˆÂGZ¯lË}ÄC¼hÂbƦ¡Ýוæ 9J'&RV ª¸î¤ªäW€ ¢¨‚€„S$Z–®Ðo·cÕ‰Ûú²Ò‹QÂm¹ÿ¹ñ Dj3¼[†° ÈóúúƒiθÕõÜ·çñå·û{¾—áBÖLš>-.éÝzõíêV+!ȾŠpâçót1Ç•Ë3¡•t£ArÕn*W6§•‰‰W«ˆ8d·(@˜àÚcvˆ@1 ±’õ«N$©þjðµ_ÔÍ€™³ÓSÆ ’<9Ü~7õnÐ\(×WCê0˜Ù¯æ; ©3ú…Îx…9Ûí$Ú€áRQŸøzÚ‘$Ó -b(Z•œVŸþ¼Bg1TCâÀ(šqOåÆ9˜zúÕò½Z…üÑŒ ´º#À'$˜tÉèXÏ€ÕŒÆgpPi/^øãªaSè¢k(Eé¹˜Æ TEN¶GG•`£ßµ¯ãgQUtuiY.VaŠLú·e„ç ŒgL>¸ŽÇSN ? s®˜†ØïË Ãñ„5†  òŽŠníwŒE`Ê?äžß›9D¿‚|Ï~ÚÏ”4‚ˆaß?ûyªæÛˆðIñò'¹„/iâô²A%Oþo–ñYýƒŸŒek»Vžœf¦œ:ËÛ1¥…ã.žË ù½sü‘Rèë¹ól©™ÙWO.1ŒV/ Ó5²íÒØk;‰ëGd9ÜŽ\ô;qF„ò…Ðmeãl[= &M.VÂ6eV*PEPÀŒ+G“¿ä¬fšZ¡‡éÎtËv5n:ä6á›VYngn2䑤ÒhvÜ"’ˆ>s‹j ÁsQè à[k“*Èá­^©›«xPò•€ÚÕWRñý¾cÛ1ß¶=dø.±KC}þX0`ú: Û‚ãKPIM§ÒæÑmƒô€ÁÍub¾G5ä“ôX—aÊìK%|x<úëSÍX8ãþ¬Š|(IA¯=(ˆ"mL†¥&v!²ÒPAJ“qLjcÿ~O¡%¦ã»^ÈØÈ®^n§ƒq2÷qTm~|Õ‘‰„u-¥¹ú0Bmç 7³sŸ'¬«¤,„Ëô’¥¬LÆ|>õ<3ÃOæÃβDO:@j4À$º‘l5TT SJSÐ[qñq˜wo'@b!‚gÅÕÐ’@qqî34ÇV)m³.+x À,[4q0ˆ¶ª¾Ÿ?=i°_ºåú6ÅXÌÀ\u¿Bìj+)bB„)LŽe§…iÎ’ÅÊÄ “Þ}¸NÂ|‘xÝ1D8·!fÒc¶ÓŽ^ÊF–šN$ÒºÆA”M ð5(gt ô–ñ_ÕIŒ¥ùE=ŶàÎÅú´Ò„hyƒC4Ú(¤'¯ÐT›´ Õ?÷¯îÝ=ˆW¯Íšµ?IQë™$Z*œ>¬°?Tmµ„$З$‚^ÈG°¸Iÿú,Î/°ÌK]ð%ª0/v92Ÿæ˜ã{ǯ§¨lfÂŒp‚^‹Ž*¨øÎæÉ½&]"]Öä\tmDöLë‹ìLÄ·X";1çóÇÂ>smŸÑ§$À%W¤ž^ Åk¹LQŠ ‡ûf»‘\bC'#÷üz)R me„°óÎy`o”G’ ¼ôŽ­©$œzêÙmyšÏÌÜòõÎv£¡ïþ“£¦ÔXòcĉþE8vGòq:LëX"æŠl®ÍÁXþx/~‡âö[jÃ]£þQês3ð!–ýà i¾™º 諎 Q)+œ­¸î¨ä O6œ¨x Êùš–e¾Cd[â~=n +Mñl7,•Ÿk1²d(èTï`æpòŸº8îëÓw- ¥ko>ªé£ÒÏÔ§6-½ZðNù2Ñ3].Ì|6ã4r¶?<BpG³M2l{N{N~—ÓŒˆÑ~Z].SÞmýxT€ÍøHÙBåå<ÆÝšK:Í»e†Ø‡t;¨:Û»”ñ>Ÿ­Ã Ç‚„pÁ9U„òæ>nÙ_¸C%u²ùà„d‹îJŽÏç&žÃÝËŒ¹­¨•Ïq•Z{FÜKÆ5^¨$ÌCì+#†õP¹º4–×Ñ8³PœYµõl4œŽ‚‡K¥]¾@×&‰“™f¹lðk˜# رÏi©˜blRœq’þœ4]ìÍÜNÈôÉØ`ÕP?¡Ò`ÄÄØNÕ.¿£ÆÈ5“D:²LD’ÐUe]‰EަúÜ6p0Ø©‹˜%³{k;[²îÑîdƒ7;8²o4¸Ü²g[å<Ã,S¾>¸†eåL°O‚ð¸'ãl6¶¤qÊÓÆA N°ކôOɸ•¥Kð. pò ’äL?~,²TF(OzøPÍË6#’~¼taßï»™®¶û?³e®rh ñì »˜9ª#\÷0=1ù8eÁjâLpASÀ)ìk©_; ˜AŽ£9»;ˆ­ÜN[ŸúvÖk\º¥çáÞþõƒûàSλ`9ŸïêzCÏ"þÔ6þ¿ƒ,«#’ÏñèXíù„ìÚ¤GK«~qWu§©3ÐIÖ¶òæÇVmiƒ½Á?Ö6GU Sî ZC)"˜aD-Ц)´Ù¶ :nM¾CÀÂìwVIQ¯”þïpø,½´}ûÏÅã“õ۶üãê”ì´Ñvw¬eè %VKÌ+‡ðcKPõ546^Œ*ÏÛ¼™ Íb³ìNïÞ†Q"ŒþØ6 —r‘…²/èBö´¬Ãö"Ú16 µþžªÝ9 ØÒ(ðä ?u„z:Åsií­†3»Ù¨á >ËlÉÏl(Z1ô|(s”Þ‘d7¿(”|…Y!ÓCFMt—ÿjÿ%{v ŽeO}’J…\(Éž…1Ñq†ÎL3L6ÕY¢ˆ ê TîÿTE®@G=zÖˆª.l¶Zx¼e´E]í8| í ôúæÊ$ü­ õ:Ù=É=Ái!ÃÄ·t‚Ja*§î…ÁšbP(£>üöv}¼_Dä†i櫳 ¢-)a­¾E7zßf]èutS€IòcPC-ÇfI³»Œ…|ñ Š®kŒØù¹ÌøÚôPÔos›pÚ¤ÈÃZ¨;ÙhϬ¼s¬1ò–[¥@‰%¤Ÿ·n¼p;®ö'Ñl‹—)V]QìŽë(} ·©ûýž’é||N —Eêïô•ÊaÊê·„vr!µTp8ÿŒìš Og”(7 ¥ß/~ uê]€‰®/,@׿ërÏMæíê×I‡`² –%ù‚^éL® ö¬ú¿#×ÿ4•]ž÷’]ç‡îüæä»ñ•Ü¿÷P4Œ½o×¢!9UG…ñⱢ߫¿º$xeŸò2­tðA±òÔü›ô“s¦²­y˜hGÌÚ3~Ü™É2ñö+ˬ=Ò¨\ÿ@™îüË—׈ÿR=jâ÷Ã"Ÿª¨Ô·AEtê&¦ ×BmùØò8'HèB£ä2ú\¢”ôð'„~‹@^`Ät`ç¸w\5p˜óEÁÞi¼*3uŸNªá Øî„ß‚}Å*÷¯:bÿwÄy*Uï°-¼‡E«™ë¦ÜØŸsXëŠÄ»DÚ6Õ†Œ|b5µ,8ßûø?ãT%UP!ì«¢óú)¸«Fîs-1Û·2`O¡[§\À¯a æÄ´Ê m8Iîú6©R]y»¼åÆÑ0Óz;&ÇxJ.ã^ËE"2b¥$–ôU3t/AÛÑW6©F gÜtÐÈ­£š¶ƒ˜LÜzͺ#Oqoc[¸=­Bãj®ëmÜTÈÒü¶DE‰@mE¿°NìeärÈázð¤”ØÅ\ññìÛ©ÄíX"›ÏD=í6&±°°9œ)[ËêSòHR*®T™™,YZ³Òóoø5ê|-(OþëQ š¥WÚr‰Jþ³FáÊu¼yGðPB™+†+ÀŠ?¸ÜZ÷ ‹ ŽlѲ/QtCGü”…‚NŸ¤ïÇKÑ4…kþô™ 6»IÊè)†âðî5æú0ÑcØ^îñŠ™ç å—·P…¥ð£ŽÓ¿£+Àü• ¯yÈ¢Ø@aAºÎ¥<Â9]ŠÉrHôCx~¥ÿ+Òsà„?kH§ÁoÐÈNûŸƒÞôî+ô—¿Ôµm‘ÒwØßä½ 5EýÞDP§S¯U/ëÒµ¼7ÀæjU¶5•2ߤp÷LëœN~;àÖúHÖC,ødfÆêÕ¼_ðìtî)©¤þ°¶xÎŽÑ$ØvfŠg^:¨ÆUï×ÉìbµpIÓœ%„Á©`ž› åêàçÀÂ/tò8 ëN¾ K¥Ì°ª ¢¯€{Rè]7UÙº!>$ȉ3 ضžC€Kä !aÔa¯5²cà‡š<|n:^H‡ÿsMü‰2U%·}•éÀ~»3¿™Ø69-bkúv·fˆJ†,ìæ¸: mXÃè è?¨ž<Ò5÷—Gòãܹ·éÑýº1:Ô2– /‹&Ýî⸙ª£7é{Ó(^‚öC³W=RC9bî|[·IÄ‘ÔFÚlîÚ5ݶÄs= ªÉ>”\4N={ŽÇóÌÎÆï;É»W;fÿu•ï çØÓòpùö~aü·ñ‰¼³1šéÊ›2ÂW->xºÀ}|azûT)cj™x¯›ê2&^IaN”±Å4"³Ó÷‹Jg<öAµh ½-îš)Ñg€ßÃTJ¥„üjÆø0!M³Ÿ<Åõ4˜1pÎK¤/&S–V)¯c8ž"@~zîÇ«8ìÀt>”fýœ>ö$¸ŠÌCTÄ<ÑÎ* ]Ùu]Vž{1JTÚÝkÙ)çüÈ}5ÍžÓ–35K´3÷ÍÙ–û„üli< I.£Ä㤱½TÈgp‘@¼‰ZÖ[ÎIYgë¸Ù7ñyóCiáRSv`oóœÔ^/èÕ ëר&=q’„ŸØy³nz-f.Š™6YÕ “lÖ…|â|ÆZ ¬x‰à9ÖY+W 7¸¾3^\ b¹ÕS Ù°Éý؈‹’t¹|g±PÊèç¶b¼@†úZ½·íyÑúuËî>ƒI;+ÔñÆáÆÔD·&€h( æVÓVœ¼9ÈÈ´È'šO@Y@$wceÝt¹¼Š¦ wë{t€FŽESwÙmßK“×­!×ÌmªEû-ôª¼”Ëd‹[Ûu‰/‚*' .€ÿGIÁåê¢2Ü@$Gn_Æ%ké: êhË!³‰È|÷¬Ù.L^ M¿¼ AZ¶Í_%ßû½V=1èékç\ æ.ÕÒ¹7² wÒÓPªy%Þ£•¬®TZ·åݹsPXðŠ6,Áê¨K•ÏB²&}.ï›KÏ^áu&'âß ä¼×Ðòs}2êiÞè8»qüï'ãîJ ¦w.nû­KSEÉãþ㊵S@Ø| 8½ÿî\ß ‚:/zÊ»ý;°^Ö;‡ÿÍ/pÊW‡ÍA0÷¤áË;D: z;^ýЉAè7ûî_Ánù”‹±›ÑëÍfçê_Ýì,ަ‡½›á/ÖO~¾oþî =cËVû÷,Ôžwt/òéÁ{ó(_ø:£OV~])ŽÿÓ(Ø®ì{gÌÇÕy€ÌJMß5@â÷«a·ëOVæJºJ;©ê÷ù 1+s¶ø@ðÔB¥ zÖÔ֞Ϩ›OgáéŽ1–ø×&£1Hüìe‚9e0ñÔGˆKíI|³­Ïtù8aš§ù±¾q£à«kdRdôlæh3ž;ò蟛DÑcâÐ… þÿì Û˜:ýŸ§ÿåšP·ý/×Äï­¾£2Ÿˆîe”Í=‚æa³w΍6D³KëÆx‰”Rj៯x”Ÿ¢ÍØÍ%|"Æ.ïÛ6ô]Eò(¤Î„O廋پéÙ5ú{0¹”'cs¡SeÆ‹œÄ}9K™B jæ é,’×{.Úw¨T2Gw ‚V`¿=èŒ>¤!%tF)„eÁ¼‚ÎPˆ9D»{?(‰SžyN$Há±Ùé´GBÁ®7'[L æÄYs–Í s\ýÏLÌD×›f¢³ qeÙM ÒòÍÖ¯ª=ÃeÁ ÜIí¸‚µÂ{é~”.}àÉ0¹¡±QfR ›ª„·˜VoéaX´%´Í´TY‚‚R]ìáÑàrP‹‘8Ÿ=´B{FÀ¾È"@Ÿ~Üg1ša÷Õ^^oO¸{º»{zð—Ÿ=Èw;\Ý×ÛÏA€»§¼ýgðAhÔô>‘zâ1[LÆfF,‚5Y#Q‰³Ì{ùqzšMCª¾"¤¸“Šs-ë‚4$鑳ë¹n9zO• Ö¯NV&uðÓ™v'äÌlѬs’ g%Œ¬ÐÓÑ ‹­‚M ý®ëìܬT|Ô±©—ŠØÔPÓ‘“§€³J ¥(©Êt'›dçìôýǃDLæè“ºøˆ%áZï·.É‘ (ŒÈ™=(ï0HØQÖÄEJU`ÈÏÖÂýÍXæb¢-8zìëO6Ê L“Mi‡žOrÕ@¨¦eݼ£¢“„¼U5v)‡¡¨Y?v_]ÚЂöÔ9åOü&Ú¥*unž’+–ä蹃¾éør­j‹¶!l÷†-‹Í1˜ŽòXá¨ö¾gÁîªb…¯f¡EíÒvĒرÄh+†Qv ‡ùrý~³®ÅÊ*³•·í]“]ú´ä¿¼º¹+‡ü 0(쥅[Õ@Ô›½æ† œJßkñ2 £že°3K ¯8 t’Ó8©ö“_ñ¶¹‰.ZÔ›Ö”r°)Ìi<*éTá0Õ†U„}ZÊÓF¤¥šÁünc'f ‹ôKZح/»jØ{Ñ-¡âa‹’Õ•Ø”&¨jïmÁƒä·júà[¬j#5ìs3ïKžèH£1] ‹²inæÿªŒrnÿýf‡v‰ô Ô³·—ç¨U°—Hcúe5ümCð*EÖx[ÖÙá6<åÂ\w ä­’‰ûuØò}麻»¤ñ9£üÀÒ†ß6m¬d›ï4©ŸåÊ­ýÉ»¢Ÿ‹àH‘ ,##Z~b0\ˆ;vfwv~„~ñh¬ËcÿÉçoŠk…µê›Š2YøyÑAÆN†àNÎæ_Ú¬¦šóW9ø_ ®‹#Šä“¢Œª9;_)[#RÈ©"ÚêÖ î‡=ÔdÑ»à-hôÖs%íܰ…j—Õôÿzjžúÿ÷oFjž¿ú†º‡îgŸõš3ë‰Põ?ÚóÂýbIo%m6è^ËIb3§ŽïË:š3,7én ¿¿ÜǨ¦ «$²ÈÞN•,e *5RZÛ³{uÑØÅ%@.IëL ¼8DG µ$6Û1)².´³®ª›•R¡u?š‘¨ÚáØxX9¹{£N ¯dŠÝL3”xGa%ý‰P¾ID/sú&™?œ0ØÁ9î!„ªÙöVfà—¬ 0²í` Š/HµÍZ)í°³F¥¿ª¿’*£L¤‚¼'-©¿µÒQñ¨‹ *x¹`dÈÒ/„*G*…~_tÙ’5}4žOy‹Ÿ¨ôǬTßXnKK¿¦dÅqLåõ¦zuí¡¡w¶Æa§ÿw®þÆnu¿Ñ}ÕÃlEóÚÞâ&3%Þ°ƒÝêuˆ¹áßãšž¦É˜ºX‹J®+Gq=ЙS.22mõ=FÀûc×QÙ‹ûD®¨O¯àðßd ä7zK6¾ÆÃùˆ nD}̰Wž¶£¾Ãïé×¾Øî¿,$õp°r³üwÚ.|ó°%©?×ÎgBÈ ã1gdmu-$Ðly„H,»Àà¯SojLK×÷»ÆÐy‹”U4ÊKûå )~/ øÕfÉ;˜Á³û„ ¼ðì %ç¢\.ñ[L‡¬ 'Rm xÒ ³^)àp8âh Tž¿ÁHx1@]xìãótÁØ#òî6å\IHµ VP)€¾¯"\”ƒB¨\æ åZZ_ù…ØÏCVžæ\å|0ˆ~è+˜`8z/$½O—þFòB]2br9· ZÎ ŽLCºžH¯ñ‰ŒãÍ"^÷êuÙï1Ïô#"w¸A«çÁåè »ºí®f»³eÒŸÒQKZ‹ØÀ}qá01\ø'+ʺ¦4‰õòŸåÞ?#º§›ˆ9ècÚÞd…æ†(~rÂtÅØxãm/M~|žÞÍñÛèÀ9KÅié£ÍÏÑÍOfµõu% oÂw˜jõNì}Ÿ¤H†Ø¦¼&oþÀ«aAèžÙeibÆ´õÌb±f)çÓF #±iwgeÏ_Ï×ö”£íúRºã„è¹–- #(ÿ¦" ¤è"ª(£Í TWFr:ã+Ͻòú>ùE·à· (õΆ=qfÝÞ1Ó®¤G•ùaäC·ë­c®lÛNMçͯ+GÛÇÞ‰M*Šãkh’C`*+•m\Í»QðÒ–C®ž¤Z±ë-Ó&µ&þ§xUrPÑKÇIxÀ%§Ý)Þ,;çþF v÷’C€ç¤á­9,ÕE{³ØZ³¡S!P ´;ÏJcd$Q”nÃA'3 ý7Mþê-uɿljOÃm;㜥ïwÓÁ|:x¥ø§ä§_`ð7’ݪ=þÄ$ï(òÃëÅTw˜©ï¶œ?úá2 …–ä•=åW„³ ›Τõ˜¹Æœö9óÒøú ¤1ߪwÍ㹜 æßÿaÖÆ=^Ô„þߌ¯Û;™Ú;ÿ—ðPS´?ä@ð;S9V‘î‚/W–—kÑEÝ¡S Ý­ÒRMÌ–‰øyÓ”[¶mõ÷› ™¢{“ ñÀμ瓇yVá ‚®TË¢_†¶›Ÿ÷•ïQRšY [[b–&½ÀI´/ŽWÈ€!Lh´l Ò`„ø³Ì üÝBÈÑ /Wx°ãUíýí©1äÔ DH‹6Êžp¨O¤‚Ö qJu$#AÚV ¯Ì 9>â{#ħõ_†mÑê’H`3^Ã7ÉZÕ³!CÍ4À'àÊô¡ ¬ƒ±‰‘?W¡ž‰ÜñÿJ ”`-ýl*‹Æ‹4•JÀZW"¾Ñ¼xOËÒ¦KvÈJ•!öŒìhÈâGk%]ÍÂ=ÙIБpç Ÿè†ö9KþðñÉ£©3w‡~çéíÁ•GO®æœ¶;0bëÉ¡!k¢¬åë÷ð‚÷—+KHŠÜ^Œûì‹É5sF Q ‰Œ ç)ên4)õ£¥ÀU¨p¶Û6Ä*BON9Ûþ´bø’ÕëÙ’bf§Îþdz?Ém Ë$J˜Ø§áð‡KLÞ b¹"µ/d<*ò ÄSí@4ô4J6Jë7c‹–+2õWÀ‡4’ÑHÔ¬(ÖQ›ÚøžÝpçR• ©r®ù½l;Ë£&t2m3\gÏ m|C!Qñ¿ œì ‚ýõ€.íO†!¦Lš_ñ­ÀH}‡ðÑÍT¯ü³ÓÁçL²x(L·š¤\ Ãv­[@Ë*[ökqm“”µP#+™¨ó͇ÙNX :Y¿aNû Ñá]rnš×ƒŸ >=¨©5d©z°U´RÂǘh™zV0fã´[oQŠX’¨¥ökÛ±>ë…fc#)o-Жúqé7bnÒðK§Ð˜&ÎVU¨=JÞ”ÜÆHá ­x7_38Z;’¬e”×+®DqTæ˜YX®Nr¼ÂÏF¾BHÞ|àKWÂN#Ùe†ýÛ ¹èÌÔïrfeòâÚ4WÓwYñP†,ê^sìØÁë-êØFѨºÔ€þnWú‰UM²~OÓ ­Áº—e´Üµ1‹ÍGmi›Wÿ|ƒH·‘§úÛ»·ûö@å_vÝÕ]òÛ#·í"K-ýâU¼ýBî!Ø•¡û½äaúÚÊ âªÃŽÒ›Õy ¯ƒ€Äm‹òn¤;„Y6?»sîuÍŸÒ š6¢lDEýüެ¾4…®õ–ß®|øŸ+Íçkg¸PAØþ7Û\þo2Q×vÞAÙM5!uÊ‘6¦ƒe ¡Mg.c–1ä ¤d.•Ù‰ÁÁ»D/Ô¸Òˆ£ñl¾*+œPþ÷?pïGþ Å8ËI5é€Ì4,ƒkmáó^ظãN_IüXsz™ã·ûÙ6Þ ¹âÓy·zwDc\¥5™ Ãβ±h¨ ‹¦²1iª›ñ0`2lâ3Ñcr›]_ºÚ]³TÐg÷Ù„…+Ò 3Sþ‘,ÉsñS2,©bx$ ½E‹ iïé"€³ô^39iG&ç‹UÇØ˜u ;ùV7¤—]uÕ0XRI…&¡i0H Á‚ÆA2pL AE£I¬Ÿ ‚´ž¾aQPˆ7ºüÆÝˆoRî7-èé‡ã)è5'’@¡‰ ¥Ì©FÛ˜7g“ÂUkBÌØ ‚ªâ9¢q˸ ò™,v XU³ÿ™a›‘9 /"#^3)„Qå68‘3\o>éšo%c²ÌR#«5ÞãGi°zMj§ßëàðGýÀêptôò)“Œ©Fáò¸Ëþ GzózØØèAwŸ¥ÀxÍ»å÷Y­EÂB–%r¥ B+|èf€K=«d/Îö„¿ O¼íŪÀ’1áx¨Zìºâ³ëË—‘…ÌŒø –™ ð#B-Bzhb5r@1þ!Áý¨ ü-ó.ø$Ih]”ËÂÆèY×µ€#3Di† R¨2CM>Ûšëë¾Úùé&…ƒ×þ¥OV™§ ´}IZ€˜N‡,zVJ?}E.h8ñ®H_ëHöUÿ(;¸75N¥Ê&@½zj‰H‰ëpùQJ—eœìä…}lf7àn\b» ,ÈXâ/ΑÅ~€í‡I«34s%hII4O\:¥¹]¯Lø˜/qh¨>ûÁ‘’Ï“°'•zë ö9P¸ŽæéôgÝn¥•;÷CõJs‘¹ZtE€žsœb­Þ¤(`ŸEÞ¤WNQ‹k¬S„›`u( žå¼SÐj>A£ãL”Šñ¢ÚLš”_I»ßjP“ôBQ¨zÏ“PΕW‚A!d ¾ q»r>‹‹'<7]¿MÍÐÆ´&6÷+žÆŠ_â¬L¾|…¹SÑ™Õß–µ¹7)‘XŒpD|eüçªT°„¢áb…TÝ_‚ÿÒ×=|Ú@6ÁÊ´¹«L¯—ä¼'`Íåuìs1øëßö!eH~½k·'Ft]Ca§€Hnˆ¹¸õQ¾÷/êÔ;ÖμzSwÜ¡qƽ¾ÞUËtÿAÚUP±¨JÕ¬ÊxW!0Aà™î¶fèaðC2Ûâ÷âEÓèÃ8RÇìïñÍÕ È?ø~Gšu›ÅŒÆ9žh„”ҩ0‚§þLŒ§ÞúìHD^eÝ à$«Ý2æ’Àÿ2ì~ªÓ¡VK“;:tÚ rÂ{bÓõlëIÂ|Ä:Ÿtv×j& Ž·¡(/Qà!(N­l Kæ8Gñˆu ΋èÄ ²ê–ÊUD’HW­že>©®—oDj÷ÀN¹Õ^7Дžµ&opØ#ŒñD‡gzCcÃ7x"dË7xºˆ±×s® +å)TÇÞr¸¬N3æ.3®ËŽ ýûÜؤëÉ5(ýŒz-[ÝÙi䤵µa"•S ”ZJI½,öW/"dþ™½&Òâl³Â­¼±À÷“%G¬{`ÿÈú€ÿﯦº¸ êCðLL, ÿ+3¢EmÁn‹ Å÷M]‡¾E][.Šƒ ##»$£aÏÈ\0D«3m]‚Ä]Rô!ìç‹Î”³™Äªs>’/ÏÛÃþ¢x1Ǻ‰,Ó n7ÿ$ßDfó2õ)+$YË5ã]q´‚†L¡ R'ŒèÔFÿW &Üý€Ñ…—À",hwnº~•ÅVŠ›‚–)´¹>‰hwÌG´N†.4 ‘ꢱSF‰iä,Lˆ>×à?Rù |….¤ôKþ4‡®º J”–m9î vŽ_n–¾üi º®\§§t_Z²{ù+Ä·A£qRG¥RDjÜXλU’\uüN“"“4q5Q'í¶¿¦–Kï‰/à *è‡ì `7þ,Ù3ŸÓŽ,Zß| ºS_7¶uZ³-¹Öèd3•S³™»MÞß&ýO]rüè„÷®Ã‰P¼nžjÚ5ë± z¦ÍþøAnk~¬óUdžèWõ·oÜ‚yú‰ödhH]ÖÕ㮕¿—ŸÚL´Ó#N^›Ÿ]Ætr[PùÛè‰#[ß[4W.Zî”îƒ {fï©õ=ð°Æ}õâ9ª’ ª$‰.ªí ^à©ô¬þ+#| ßìÅ Ùƒà{^ñ””!4µ½zøÂ%HÇÄM³„;îî¨(¤Ž¤Å™£Dɸ«.:CÛ•á½(=Xˆœ#©r1$„s²JÃé·ü:mB`© \‡þ\8gGL‚å“À)º'—êª9Ò*_ŸÒ د[“u/ú¤t¯¹MåœJ¶-^ó6õ–ê!®â8šâq µÞÛ^Lð~ÐGç<E7Ť!ÒéØïHL*æ4Ç<Ážƒß4:ÚÇ‹ÍÙÖ$dªÓ>Ì=è}RŽ·öo™ü2Ü!ØkÉ;)C~[§$k‡íïʸ‰ø+l'†} Ÿr üÁ¢íõçmÇlË¿á·1ú8›“á ágx¡©kªWX /è sÿîÈ?(ÁŨ©<úò?s£úN>Hêý¿“(£ø’ÿH¹cøÿ ‰ú¿+ý?$ÊÖ~K ¦Œd+r©Äñl§î% –j®•h=ÀŽ¢Óè‹ôضÿY4¹´‰V<-¦ËYïõ±òËõ«ù¯¿>1EÎM/«Ht[“Mxá7?œ'Éñwcóýë_¼d¡ç»g¾j‘v€G©] 9æØ['pžñ3”j±ú@5V˜Þ̹õ~-¦/n·kñÚ@5¶/0Y›A#änYƒMà ›LëÀÛhãm@›qL×è:È¿ 1ì$n†`{øšqÀpa¥@~@= ïëaІ¹U-Ëéê—-k Éoú^¸­qèsZ”¹† òOÑ?q\òÎ4AœúÏ9 ÒqF«£3̆Od(@ÐðvpD‹†Ã„ðW8ê•An¢ÚI>M¬KëbË«oÜm¿®8éðãÕëÍzÑÝí =ÄDXälÍÍBïü¡es‰ð*^)¡s¡ÎgÜл1Ë_}õûSpNôO4*d–H ¢IÊBûT‰0[ i!^qÆŒ1Éel‘†Å¹³òÜ¥9øïà1^F¬— ý3¥zVŒH•·A®™E½²Óq=z#®®&æô}žTKäú,5Èf£6çähQ*V –¿l?¯ŠŒoæÒ–eÉê¥L“àY–èáôŠ™’s#²E‰Cé©&¸ÎTPEòä J\‰%«JØÁ‡Dú’ãPÖĺQ"ÉqCY2NùÌaû H³ÑZL!»4TÙè;ŸçYu!¼±B‰ìu™åìE—xãñêf:%®ìþ+%³v½‚-©ÀÛE«öž¹x̜ܶ±ƒŽÄ¹?H‹ä@%ž9B_P¥)Òqér µÛcµ ªgI²x>íörÉ07ÅǺ—6>'ÎÄ' ŒbU0IÆ™¸Ð¾æ¥¤ö§M[™£EÍ×èê&¶<¥¬ùgÞ’7“§4}ñî8ãzHð^ 9‰FÛ½_õÙ6JTô;.8bA—ž,SFÓ]…Óe~¿!üÐÏ-®mëy:Óî´¼žý;eGê,%,P6Ø>Á®8‘¾·Ht¤É]’AŽöÉë„‚”û»œàa](‰ÏŒµŠMW©ÉúÙŽPY€Þ6¼L=^"ï(t±ƒ”e 0t3à­™ð9F|ý¯ò^?oûŸ¯ò–ʤÈ;«iO¹PSr¥¼:xÈX¢ãQ@#€ ߆¥šCxŒ†¬rñeªmRLsø‡"õáäFЕCx½áоÚdõ¾’;á¥d¡……(&V©'a#¸õ9úè­~ÞÏ⻥Ñt¥‘q†dån{áî -ï;î¢^Ð93Ж÷^  oÀHwD+IC Ãß.׉ëìåÄòþ¸çÃM ³DqE(µ43ç>|¥¨‘X4Ü+¶4<¶›:Ém™‡üJ÷\Ü‘ÂËäjjM÷xZÀ”ÁMÛ'<Ó±C +ño\b&Ï7ÖÏÆ·ØèÝ×§znÉ6,HIø³´5h«òz?ÜR®•g þÍüÙí÷é§àÿ´ r&8õõú ÑʳÔOÇffXù§Šçcÿu£(ÖüãF«„fvy'¾vÔ}T´(Øj§ÖêZÆÔ‡6úÞðÿ{ž³ð¢P:€ÑùÿÍ ]m\þƒƒ7¾¶î[í çîP*·z’‘"Ò×›®ùc 'Ãå‘7ÖÜ]7Gí<(’š6$J'Ñú;~æ` $æKÞ^·Vk‰ 0f`ÁÞÖÙ¸âSgíù¥6óWøõòØâ’,²¢Ý1Fä°EÖØP~0:ü2>ñ¦…gc4¹×í÷ñK†ü_Ž@#ý— hh걯ñœ“냞f†)4€" õ– o™ÇŸS) „ÎÅ#Nx…WMc¹ŠÌ@ ¿T"ÉË‹p)úƒÿ*µâ8) cò5³èĹ¹ãããӄǵ}ú˜Ï@“sãÍÇŽž®Ñʨò‡:òIh^ÿA]\§sH÷¸ûny)Ìf}¯‡ºX„|ð`¿à„³x¹àº $ ’J±HÜæÍ[¦ 5γruÙ׻Нš®àúßO®O‡Fΰ_§VV>f6Þ³tuŽ¢Ÿ’›²›¢ß]'ãfÿ&}Üü ù ²òÓá%À$Äö„³t¿zÃúY!H¥÷`Ë¡ î˜ÏvÍß‚ñÃçq[ŸDö ÈòÀNˆ$"±“¬ÊiŠvQɵéò—0~­kfèÒ(2›zW¼aIÛ:(ä6†%¿ ¿O8+óÁ õ_8bŽã+±Ëœ?ŽüðdßDÙäƒMq:æítøâ>l9ݕΈþ~Î'þ9îª1M-„ç‹ÜªóÏø—:ž¢õå9‡\³Ä¡A$Kn»|œOŸy’çr¸åEëÿMùã…l ÏsËм‰¢ ò¤pBù¯¡äŠ¿½ øPL÷ÕRÆýHÑ䘼ÃVOÜa«Ê˜ýúÐYŸò³Ue}ÊWÖPöÙ;z­,)€Fø;þ`RÅqD©«å˜Qò¤·0a4À•w劽s¾$§®pöÚ}wC@sŒ½ ®<ºÜòƒ_ƒÆ(?èHŒÂ„ú}bŒè«ŠÔ% 1Äæs_çôñA+óçqÕù_ë…ãzîRNó˜tWÛô*ºG'¦î„¶1hr¶´Ù|]>=´øz…ùÛºÛa°“sgžñ_¯GœÜto\Ñ5˜KBÝwx¡c= ZŠ4ˆšbåÒªý°¸}/Ó ^¦D~ºkkÇŠ_ çƒΚ«HÔEò;ãYÈÔC9ŸÁóÇK-õÁ‡w¯£Eñ$$kØÀpšçbYÂå¼s`7qBX`(¥<ú¡ê0˜V¡4E§ùºy:?ÃA¾>½_b 7ûzº83÷r?Žð-úC&ÿ[yߺÙëõЯéR¯Õþ|sÝòzËn°mõ ”€ºÌÒÿÒÓü~8ÄùõÞ kШÞh [-سù0ŽN»ñÌù¹5‡úsU:ælcacÿñf7’õÚùuF2ÏŸ*ÐÆÏi››U­‹­„Ï݃ãsøf•E‚Àd=³® 0Nj ¦4)à:n^ºk­[,ycDÐÀ™"n€#T·ÚHÌ2j§XFÖEdµÁjÖ:t±8)ZS™Eß´S8;)`Er1BZ¸/zá¢ñYoiî_ܤžå•|ÊÞÇfx96oÌJ'l¡” æsÊ Õ6î:eÞH %î »3ghÎ=ÿÀæãa¹Ÿ‚ ø·äø¼± Lw~û ežäûá¤æ`Ø6MaËÎår9£`p@KÈ1"ÈMÒŠùŒƒÒ©b£G÷£HÌ_¾¬Ë‡`ü½ÒŒÚ=®Ä¥oWù8œb ÓLÎös M@ O“ Ð@K.qÒõ³;@‚±@ ê LÃhLÑ“Be…°k˯&#AöˆE®`ïöJè~–wõ÷qNšå>–l¹.Ä(àOåçþùO/ÏÖù0ÿ|ÜxÄŒ|Ç)¹'’rÃAA­ÑІ&䜯~Þ} óÚeáɺìá>åÚîða:‡‚×.ººv~tï Úîº~z&J uÆgÖíÌ’[Qeo»”¾[‚$l»@³,6éj¨¾-6xZ Á¥˜s-ù¹œä²½zBèQ Î=I¦Û¾s Âq¸@ʸÝN.¦“•ú€r²yÞdÜÎi|(çð*ç´û)5úÁnåÍÓÝû´: ¤aFµ8XW}Ú´VãÌi¡Øjlç¶ ¡¶b„ƒÖí80$N#¤0ÚÒÖxg,O©õm[$àžáa2èÁ.R"wúAO«¬DΓ¯´yx—´ÀÁoÜŽ+ùá“ÔBnÙ·Êe€0„ΓB¨€doJn+<òÅ„ ÊÂÊLüeÝeÎ[h+qÄðŽéïg¹õµYƒ£ulD™óA/Á`œn0 Nut}yŸÓ¤dÔŽRÊò³Ü#zñ €ÀÇad $­.¶hÉlÈ鮣ßámë埬.··äª,m˜ùz7´uŒÃdD+–ÿÅx ®üœq ŸÏ+ïÌNM0gø9h­3¶$'Kгïg*@”ïæˆO„(ÇR’ J6ðÂhéÊ)à Þ.,o‰dc ¯‚e€æt”¥ã¼2v3@ÀµTy7RˆHÒ#SóG/¢G$B¢qT"ìËY•«ŒÚûOŽ ùÌuÝ…MOfl´Ã=hÞ¨¤DÖ†MuFÝhèïßÇrïá² ?Ë™îát p÷ç øò°Ÿb‘*s–!ÉäðÁgì¨Ø[{b“hÊFÆv…B7t dÊ{=KÚ³»àÅp>äì€wìf)ÃÚËÇ]#¤µ Vn#i™Sц㺀tÆ‚ÙÔ׺ ²]„ÂôÉÒšdë0 ÝJטQÛNM€]¨V­Òy¶nAŒ/ ƒ<$<’=Qº½EyÁä*Ó-艵1Q=ÿªwfUQå¶¥ª•J&0w¯*õ&Öf@f]u{Ï &F2‡ƒkdKo×\C§­7?Ç`9–P1}¡ae7 ¦zrócÿOë—Ò[( +7þ/0M¯ŽÜ÷–ï©. AóXZ¯2÷½ÖäV:€ù2I¬‘áßÕõuHø×¹b?¦Xï@ÖÞ“e±Bˆ/õX¶d–s9¢XfTi"-AÛ• Ázû¡ä ¢jì±.ÜÓ‡culk E6?P‘e´ª0k/S”ØB;Mí\Æk÷Ô0ÐÆ&¯®Qf¾Ú¤q?Ú(ªˡ&òŒç‡2R‡b]±Bã;.’Jb¼9,€Ä!–z !Å*{<Œ"uG!¹Ñ°Ã ÖøöÂöЋu1eP{Þp)]cY&s9MI2*‰òæÏ2±¬”¬§Æhfð˵GkCR¨,ÝŠ*Üç‚4Òâ ÇNë àa«¼4tÒ cn×5|xM8´äVwp¡C Õ*Ó£%‘w3[!°î¡±„;tŸ!N°Ð`T*%–Ÿ.CÀ£hOcËùŸ§k(DÔ­‚(E=!!ñq ¶¬Ò߬S!“_΢ „ʆxPááR!¤—x¨b^PLIßAsñ–LcU#‹c ™c WðÑ0økyÓWë›D’RÛòóFM¬4ô›O…Ê€ F_‘@#Mø¢ X*z“#NÊFP¿cú>–ŒD³|ÿéÉŽ2BûåPð­à-x!¨-Y줎"‚¦D¼0D”Ô ‡e°«ÂÁD´)gïÜÜ¢"›•󵂼JÃ$ãå›—V®Ïùou¢>½?Ô+Ä—¤ ›SЩÃ8Yq<úÀÐ`#K!—õ¾¶JÃq-`¼ÓwÛEM†<Þ˜ë€Æ)µµT»¬qúSXÍæÂZºaJC©â0ÿYoűu}ŠŠш÷Œ„J|`×ý9`ÞX0š‰ ãnS€k›Àš`×Ö¢)dìšÔ1Â_͵=¶n["üCnÑaû«['AÑÖ¤c9ZÀeý°É[¸Ôu„¶iDb/ê°š ‰{˜óEÃdâ3Ç °ˆË;$ËúL>38âqxøOÞ`ê¼ÿ Ž*&˜© °t/y ;TSÊ‹¨ùH”¶V³†HR1S¢„vlkáR#Ü]¯3Œÿ´:©ÁÇ@ª¥‚ÂþHñ(§%¢£Ü …)(|h­ÕlLARPíU»€T@É(K9ºêU–Í1ÉSQ”ï|àþ/7ÉÃ"6ðw:VªT ò´F„.u|jh¬Ï§WJºy­M*.O­HhÖÍ$TŽqÑÓ1ò*}}!ÇéY€µ§(`5ƒ7¢2(°ðønkˆD—6K¸›Q‰ ÿ¸|Ý3ºþÔ€º¤s¯ÎÆPÖ6æL: Õ,®rr)¬­—"[ÏãÃ1Ž6nG7Öçûú´J8íû’ÙzºøÚZ_N̽¦«Û…ÞZ”›&¨'o7°Õó>ópwój%'¬ü;î*e+ ¸4aV× _ d™!ÖE’7 øñ¬€˜šÀsÑA}yFªIëÉ-q%¿Ö[–GO§­bí¦h±>>?A½]ÌÕ’1€bØé´IyX-mÏè ²d-¹ÍDù Bñ»ç1¼Ç‹K[ðù³ÕLÅAB%P=¶!ÓA,¬ÚKoÑ¿áñ3—µ¤²2»1Å”S«Îš:*aR•¯mBuæ÷ÚÖÊËIAº`Á[Ô¬­ÚTøqîÅqÿ<(Ì~’E§úBèðŠÅ¾êÀ’xÄ·‚JŠþ1h<¬Tðoß½ˆ=[.ˈ„²Nþ¨d[Tci(Ì¡S†0õvà239‰Î IAŒ¼gM²0‰>5EÄ¥ûôI·Ž²aóÓ:r¾.eðr8 tI1dˆw)cv€E7&5‚"Îo‚—ÖÇ©ÅC ×ðÛ´^2®üÄâ©DåÝdnèÿœ•"«ÈÒ>t›ÅzÌb‘ÃÑ:|¨[gÊÆ ¾Ÿ—5Ù0jŽ>ôXn«Ú½ ¥¬~àŸ[¦[ÅŸËÄéÕ›œ{á‚iõó_‹Ú{ñ:n>ÖWAŸ ”ØðD‰OÐ…3Q ·•¬n­ÕX ~”µ¾³VhA:ß\–9^¬CSi¥XCðœé†Â{t"B;¯YкfȱclP†3Ì‘$á§ÑœÃ:,±È«/L^ bÒcƒüEÅAM®^m"!AI]”ÆØ´2˜Ç’ø˜}p`átEŽh‹…FØ%[ÿ)mÖ®¾qüÖ_ú”w?ϰ…%µWix ®¤*¿êIȶ™Å¼ˆnJ±Ç²TX„Aº5s¡·¯èÖâÞOüξ`ž‡B… å)P|6þ Èåõ[…þšÉÑ¥O9ð\Ó¬1ÐÞ뤟ÂìþY)ìéý-¡tà y󛽩ϷT2(Ô4óÕ÷-{މ PBe9™­¾¤WÇ:?@Õpä¿©¢uØD(Áq®°fGÉ:"+àóä <ž0 >Á`S“ã;R˶s„oj†”–ÙŠUÿaÿÙÕbbßÁ°ˆÂe™¨Ü.)/ªSzÿU;'É.ïK>'}b_×b­·™ýtº9åS/RQVÿ)ek×â©ýaøœœ.ü×Ä‚ä£pËQ!«G”e¬ípbaÇpTïdÛæñOÄ=û½B}÷HÂÌÉ[Ƭ+A2t‘|“p4‘ÍÁaÒt„sÞœŸ¬+)ºx8PlÊPúÆŠ·I[êß›j´Ï.+Ú6º+7Ì{H±_Š¡Š"V{³%”P²û«iœ¹DÊ~j>M1ÃCü2gÞ/8<ß /H—¢ ·’¤e—Ê´[}nD-«ÈšÉƹ‹J®ÞG¶Õ:΀x<Ÿ&@Pˆ®ŒÅå‚Så9ó‘_õ0“½óäÆ:ÇAÍqía5ã\Íb…ðy†Eºá©Öp4rí‘…ÐC"Tx„Oº¹©‘fì"[áÛþOnlò/pVn¸ì´S±Yµ“1‹¶¤O4gÀ/ë“ÝÎ-ÿÝţxÝØŸï½ù}•n;ƒ7 Ú\¬î·˜4Ån“³ËÔÎhoÄ>B”­Þ°?ÔvW”)CµÎã0MF±Rj–‚rÂe1BˆÇûâ“r¸Zal6¦›ñ0"’ž ˜Q¼G\Pž§…e å2,Fýã†ËÉR.¯ä…vÿ)‰§þŽ­b%wm;.u˜µ%=&²¤‡moC‘«Ì•tž„C’aè(%¶£S€&‡<[î×6{OŽiK7=GÚØj†NCÓÒ&ÓYDl¤/üÛózx`Ó•ç?‘m dIù…}¥d!AÊ ˜òn¥€Çr#´•l ™¶Ùq–}È\§î6¨qÜÒô¥ã¸EîÊÖ'  ³«ìÔ©À -ÏÓ’ñgXÔH¹ÝûÍõ¢K½ÞűÆÊl 8(R-JRV±;á¦Ê 2#}ZU¡ÙåQaìØä¸Ñ’òQ-©?6mÎtÎSÒ•v>¾n[Ê;Ò9¿ çgÓÝfd’…­Ÿ½3ÃË’XôÐÚsëºF5 …&œjþfRL so‡àÔl¯ï‰õ£ä6 ãb~HP¬ðqL÷çpÆáÒ04OWûì"oudBÉU™ú':ƒ5ötJã¤Â£*»ò $O¿4«m*S ÄÛï=Øñ`Šx±qó bŠš’Àç@=DÙ®_?¥¥£~>·Ž0+z”¿¬‰Rš®ÎþORŸ¦†™A$çè)Œ2¯²þö345Q4\ʲ|$OÇb<«” Íõp %¨Ñ¶·¸/¬µE˜aªÕöÙ?‡ÁYE [Í|¢×‹N ê•’8bj%Ržõ?H{‘S£ù’Ãu¾ºM¶d:S*CúÿQÙŠï•<æaЫû,€vÒ`~ö‚ìa› )gÚ/Û“éÓ9un܃Y³b$Cçæ_y‚T«’¹¦<)Íe!À´KW£--Ë5ãG… ®ÏOÏc<k\ÕBój„µ·(J+ÚzÍù¤ ¤p¶ÇÀ) ç|ÿ1-1AÝ]ŒÆ¶Î¾[Öz(&ÁQ z¬@7°ÝбPv‘v4õͼlêÝÄs|!Ò_Y³5Tñï']ð>ðcGZùÆ®ÞÎLáa¼¢jQxÓwkÏÊ8)@5-YØî?À ‚T mÞ¸Ê-Á>ªÛò¥ètï3$9Îh k';??H¾]&\‹…šQg´hØÝÔÈMêPµÕxµÀÎ8¦*³lªc±fÌö¤–9{ŠhÅr.œè_ó½¦Ë4*ŽÜÙ× ²7ðò;öèÖ"ékVF´^RRËŒbg†AŒ z\©EIÙ9ÚM×akQZ†ÅÖŽ0¢4‘ã¡ÒÒ&”à?«¹?¢÷J;Öѳß-Ÿ=„wQüª&â^>'¢Y&M‚úNÀÜQ±¸¥îò»'úÎO{wu±ÊÏ·_­Y9¸Š>âß"¿ûÇg«ö{ýøxyx:øëãT™7ƒ;Í4c¯ÜÌ`îQ͹¾nJ†Ü܃ûòvÛOÂjòoB¯a ~ÏN›¯òNÍù`ÊýKÖvÍëÀf¸‹°–Ài‰ó%ÞŽ¦X­k=Ü…ƒÊ6V.{Œ†b¥„‹©\î)|wŸ^ö5¸Q¾¢™üÔ;þ6ÂÈm‹6ÌݽZžqþ”uD#8œäoù¶¹â’&Œ ’ΉͲ!:á¾+¹ ýiBº÷•QöÑ©&qg « XohÚDºóbÉMbUë,hÙýAmTí«U@DòppÔÃt”ÐúŽÌ÷ÆÞûQÀæBeøØêãùŽ«ÂBà çøª´¹¤Ïæ×ô:ÃÆ,~˜ŒŒ )»ô.N(„h]„ú@¿¯ ¬žíŸ%äs¡¶ÒçW 3‹Éà™O›†‡øœˆâT£UE….ºñŠ“ áþ¹™Œ"çØ‘jjó{¦‰²Œ¼ + ÝJÖÁ¬Ü0 m@mæÕÀ˜™8åá8Úïà QMs¢â2Âìqº HmŠÐ¶w}e?tÕ!Äb‹È_£Âa÷éBWbtzt0—ʸÒÝ’ ëŠuò£éQ”o¨ª{«ÉŒæŠ{4Úw®ÆŒù ,ÿ!ŽëóS—Šö»Š+Û‡Ißo¦Jà霢¸ža³Tw¶0ö¹ÔSÛ@)½g'r Ü°óÅ|u Cmв™?S‡ûynóÙEøˆ39¥Ö¨ÂÔœÞco¶ûÖ{×VÑ¢îÀ `LnYâÒ¢ÔÎsEWGUd+„Á‰ŒLçŽÀöCñQ•Á‘fœó™½¨%Í‘ SC!Jü¡ÐÛ‘Åœýi Äa&'þ7º¦QìÊ^>'åMwFÏÖY,ZìÃoüº8±ϵޭ\~z¡úöÄ6›ï=N7ƒþ¶E­Æœÿ¢)©x–_!lé‹:ªÝxT7M#aÁÏ궦 OøŒɳÉ=Õ8‹m³ôvy‘ËTõø³)EU8—»p¼>Uàê~J})ÈZûì½åQ’‡Ôó ùÈJ|Õ[|*Ô]Klh¹ó_e ÿHyØ"t÷æx-ÊŸH¼ìIî±u MÅ‚*µSc$­þô÷ÙmÁÃ(µÒÏB%ÒI^>itr`%4˜é¼ë@ÁH¨úÝ,bYµaþigt~’q˜QyÙpÅ"åd’ûI‚ºBÇ|Im£µu¬Œ€Ú4—Þ ›yGu¶%ü-~K>‡C683êKD®‹Ä0¶;˜dÎ’‚ÉŒÅÉOÅkÑ5¦Lá^7Ed«õ°Ðácwœ ?{E «¥Ž„’j˜“]š™Äú)ÔÝ»P=*/«>Iz\¨å}ÄTŸId°n¡ ò§âðÔöOåj‘v(žshN˜Ä¡“öÈ2MŒ¢2†¥âLG<9ç}†Fµf–ë ›Õ¾ºÌ¥HìJ¨rq½ÑpXÇøõ9+¥\þr2š¯'Fp>>î1d,Ž|iPÆÇ¤­£iZÒR é a|YNqRÚ ½Â)†~åLˆ/lñXÂWy‚áðGè? #,7Cì{ROØ6(¶ÚGÅØ‡ØÍn9¢ÇŸðPdšÒØ!!þTv×—D½B`ü–#EÎõK[µê¨)ˆàjQ5ü.%n='¡X Ò0š)މ0Õbÿ©Óžö´DÇ-J_À‚¡wÈþ€¯†ùëbͧ|oò ꜖Rì„k`ñD0´§z9 )Ði«’P7…DŒÈþŒ#” 1Úpj©Ü/-cê«+›ŒI†Ùò\£á+TI):;9›wñv£}8?¨°óåzˆîNL|¼Ÿß£|˜úGµã±yï˜vÔC„Ê–•0¹ôN1èÀÿÆ18ýœÎý†˜”‹ 1Å_Úk5Qùj.Gh]s‰DšUõªS>–”ìpÇ2KcªC€"D˲èswÃ/åI…—ayÈKQidQ {ÑÛ]¥z´ˆ‹¶:#?”YŒJo0%ˆ QŒPvŽO¤ ’$K"Û –W)ug 4¶4ÔM`óRûiÏS½Fe‘ßËn‘¼ ÎDÕ‚ysWJÆ¥£¸kTHÝìTo$¿+nûÍ5¶±­B©·—PÚ–ß ƒ6¦a£$L:Ù8F`4?«AJ!‘ú:J2÷Ü4 Ãòý5o w´ËiN OU ;]–>œa©ïXõ“ÿSÊ–`!&y'¼µ÷%H½3·k?jn–U–æj•ÖnØsŸ]Jd›e¼¥w`K®Ô;Ê,yE[í†cؘªË.€Sx£¿.ûÙ’Ü“CW&‚h¨ä㨕yÝÿhEACjÅz¶­Ûš©Ã?få#U´fÐnÞv÷ö×¥ÚzÖoÔÜTÞ^U‡`à+ÒÛ„/_…äøñzún+`)+ ¹GÂâ·>¾×'mýý—ùÒ&“¸£à=}JÁ9HÀ÷†-Ђû‡žçÛg¶7ÿnìRul¨†ÀJÁ'eN{îÆáÐsœ½WnHÑK¥ÃmïcDêoÂñø+ÁzÛÓ–o"XÑö(Tn÷»0ñ’ï ¬N/‰w“nÇà™"Ë"Fp`ÞŽ;±‰ú*!`ÁI.@¶¹G›°Ê(cÚ¤ÆWÆKðqŸ‘-&4a—Zë9®Öþ!qÍ™ )Ò2q€ú³‚tZÆ~€Ç&Y¬9¥Z*»,«Á‡j¨Lˆ$²›:R8 fx°ëªa§5TŸgx«XMd®Ñ`?àŠ“Q½Ÿ{ÅP*úù:z¯k `hJPÓñÔª`ü<^G(}º‚^ŒŒÄnîÛ_VWË‹/ø8¿FîþÞ¯åâ£qó{\‡ÃãóÅúµÓI9Ó s‹ѽÒ>[¶'Û­²÷‘üó8 Ÿ°µ|_l:—s÷ZæíñÞÁë9>‹·ê´´µNþªî6“vÇ×|\:½®Ëw„ÔP€‡ÖxžYf³?LÈ.=i­„£l Ä8œ²à´Iß(בÁ„æÑSc&_<ÐÜ'™@ëõQ¬x&h¡r<†39P¬Þ²7õ*§-nô”­EwYÛÆ5ftš?OêBÊP‚¥xgÂW®¡¯.Vì¡“qùTýYÆÃ¨e3w6g¢5Žž!]=¯7üjÝêCÆ–ˆÅ·vÂÆ&8,BŒGäw%ü*H{/-eÝ¡„Žy¨&ÑögjN-¥ƒˆàZe2îÍ{î¦áò¶¾åñ\®±Ux;ëøh„G‘!*C jþç§Ñ)Ÿç4½Lø»lò¨•¶‚KI·¹1P¬‚ÛŒ#æ‚£¥`ŽHŠGí­‘å[¥ ¨u|oÝŠoÈ:I(t£—·GÍìoaBE"9:i…UùÎI¢‹>>FýŒßÉ“½—[é¶‹óÖ‚Ÿã¹ÿ0Ð?2€%†Ÿ¯Ên߬÷¬ðß…Ibzi'l¥ƒšçÇ?¼Ôªv±Þ¶€?Á‰z=–€ÝQ‘c÷‹!¤Š¾¾ÍQT°=ÀÑ[÷Æc†fä «òàѱœKQÚÙ5ë Ï–¯ TDÂñØÉOÊ|¯K+ÝëD z*}ü:^»iõÄÇ­úãLú}L¿À“ecË&}Û•\–©T&Ùñ[aäß‘™EÓ(id; V^½¹¢¿°_z_B²¨—ª¨I‘1v‹+ñÚ´“žõ,D;Þ^esE#¿p\ªÚ© £I<3ÚÕÍNy°kõY³©¾‘hxÎì4Ô—#˜¨CdÑÉD·IE¿½«°zÀô”øïîGv?É\šï4-88îàL8öje #9àŸËzo<s¹Ø¨(·ØÛâ9e}Lh5³ ©V< òDãW Ïû.AÙfê[1Ãö/¦{`[yÕõ_4Úº„üoÈ Éˆ ƒÚ j`‡ëíhÙfu¶v€V”@Ŧ0 _)®Ä¼ô@Il½M(¶EžÍ©Ë•‰Í—­-°ïØ2Ls[SážKîBj$–µo¨¿J³$†’7›ÅX‹FåÊtõj½r¸§qê_^ÍjE€ù¸éð|äîW n,R>Lø‡§{v­¾o—}÷oéǨi·FŽW+þ-~I®Ïÿ¼É~ZW,­/X@?ƒÚr€"¦ã|,2a>gõoVû”ó. Dƒ´dÚ*[¬c¢éÉæ–ªíüúU»–i^Ÿ¸gRl’rF¿>êJ(EG0yÉ™ö.Ó0U¨[`#Oœ\I½’¢tz¡„°9S0¬/Ûm|Æ4âë ­¿3èœý¬ÕY#9X‰f¸ºBÝûp_ÏA*Ó€’ÝVÇi€¢´öI7¬Iqp4žiÓ*Ó@+5±’ÓI’ì@”é½` ‹'\p:êÌZuTçRï³›Ôñ"0î*±ñÓňÕH*8JBô˜úÒ%ð‰š¼:£7ôÔÍß’Šßi5{W_Ž`©`Â‹é© ÏD´È“;;i”¢Sc^“'õ®-,ó¤#WË"á™W¤àÁ}c™å4s+í¯•€=§Æv4' Œ˜*5SÎå ;¯ s_Ç铿[dcÓ”_*p…÷Œç\’•Ÿ¥I=ºË‘Ù!m¶ å(VHêóÎ#ÞU'H™Ø©9 °¹'œ™3Åo×ý?᫨K2=dƒ—ç¸å D؆pðß5 ŠíiòÉU™\ƒŽJ¸–Çÿh V¹7—¯ï¥ÒžáìýÅû”™Òo;5]à gNBq¶Á ™ÍK§(Gø¶lïf£F" ˜nžªü9R~ª“vM(+WÓ¯.:ôR9#Zi³zxêØýƒ~KS­šÌê§0ãS¿Å‰nå=mâÈû ÔbàãÊDQsQRDxrøÀd®b¸Áa“á4©ýhÄÅÅèP‹èUzU¡€âe(:BÑã©H oHáWXî¹QÑç }ÎøîáÞ¸\W~ ÊÕåѧ,¹3i=l½jPE<2‹ÛQê„ÿV®¬+vt•7qKDη†ÿ”3€¾£Qx»ƒ.ƒ¥ØAcÉsF£vPL®Öº\Å©ñy•°œ¾-À /uóÅÊíªÇ5²ÎjËšÀXé´7§¥¥Ì˧¥,µywº°½ßåHŠÎȰ©'uI¬.ÝÈ‹#>€cðîQ`6ÕÞÃ8[ñÍ ùÅ’5"æú«¹ëXÒD/ O¡œn}3¡Þ®IgãpFÞJSCñVˆnõ¢sJ!x£ìí™2J F\~ní†ýôº§†WDµšJuЩÒSìŒ:z;Ú¹¼ø[åGùóqão¦Š³áÁÒ<5dKJ6V׎‡E|€æhVZ¶…Àpw™ÜÙ-0b²¿b°l'Dòú~Õ:ºÑøçùßH j:£9Ö’ýUåÄÁµÞüÖÓ»b~,ïAú¿î¥a»tEŒÜµ,“¬á'X’Í÷±ÅÄ¡î¥Àª-q7‰_ñvëu¤Œ ÿjUò(PI›µ®ÞS©œêûð®UŽ~7Ú)mvd> HJ+( "GF°D +­%Ü@ÎäóS~×*êMÙ%ŸóQê„àŠ§fÓ³²c(l¦?.Òç“çT:´¦äèZ›D}÷“ˆTjŒÄ¼.rìá§%°D›ô¦£Ò"ì:'¦Ç”=3ÿYeùÕs•á­ti^óî_Àü °Ÿ¤XgåfU¸Í–PÃ2ÆÐ€Á‰|sžA¢ßYßî£ ¹©"¤ ãT+ìÐÄ-ô¢ÈLìšR&òˆrS!y[’ÁkÙ+Ù§Ø6‘ܶ\ä¤JäI.ë¸C®ÆŠôë$éàëç'Vb‰?uàkSô=é’K‹|w$ åÿëÏõDz`N…x?:s†náçaÐDô®‡ý{Ñ@'vh¦žz)ÜN¥âQKø8J®Ž–o)µ°Ö0†Œi-ò‘d ûÖk|y€~ò§xâ™ka¥ž7¡J;­Ø˜m¸È¶!xIž¢Í‘8©]–*ÞÞÝwµ’®‹wØ2[ÀA&ï—¸"¡žZ”JÑÉ Øÿô`è6±_.ydƒíÕ5ò  ôr¿2<(/ˆ×êêË’{4.K¿7k¡«›ð_ITom ëósï›ÀÕ«ŠŽŽ‰‚6XÕžvݺÆP±Ôüêù%oó"÷2œxðŽ®„Ú„ ß/âè[£òÎuu(}Õ79T‚w÷â›ÛÝ„¹ú'ŒBÚm½¼ð|ã+Ï/a¢ü˜héúùÙbиhø[;ºÈžAÍóY ]ÀbºM-¡ÊŠé2£e]RÛKÜ;ÈÆåÃχ/ñãᖔβgºÊ£*C uÃŽº&^€[´Ø¡÷t’«K¿É=Éåžs‡ ×ê&Þ)“44\Ö"cKM›¦W)%{SYcÚŽÜRKÒÖ!v’Í é,øIj0òƒ`ðëµ ‰vXÏb+CŽ2h[£ ›aSÅÑ]r-žºZ¹™à—€*ÿ²E©ž7Aƒ(ê—î0ýP@ÌKí&#Õ¬©?ïÀgÏ/POû ½)ð-s±=Öß²lUñqu»Çê]f™ÒÁ¼½¸ãépˆKÑ•\mzT÷ƒD!kƒS?€;Aw#ívd¨„‰1çžhŽêra€ï¸U!d°=\¯« µ0è¬$Îå~`íº†¥üs _;ñ«pUläyØN“ëŸE ™ VÉ܆¾‚ÒæIì3³ï:Ì+•¢ï¾¬ZŸtó6¤þì1EcDõðëy–ÊR  ¥`ªÍX#¬?éøóÚt†Í[ÕóãàßšÊ!Öîm=Ûˆ^Q°ý<å9d¸Á¼•™ìÓ\rôà¸æ-Sçվŋà”ËCÉòÅÀäz8é…°Ë4]°Réó9u=Mï9ÃË£áÃ<èöày‡§Þã¿S¯€²æä¢ûu7ÂcèMÃ7|߆Ñô£jùñkjqñ £Æ_lŠîç£gNrÊéŠÌ”´9ÌdÓ,‘|¼˜»[[ (R÷Á5W=îÌȡڜ·Q-+,Ÿ}àQ¸Ï.oû½Æ¶!<ï-oîõ-ÓŸžƒÝÛì¾`~po_ ­›;ÜîP/©M–ª=ã!½¾77×Ï/˜ª¯¯ë™ììö.ð{ï{7ô«›ÛWð²K_¶køW/oœk¡‡Ÿ„ª;x¯vŽó¾R¯.y•¹«³³û8 &V yÇ_Q3”J óÌ@+1®Ç£ÉX㈿ʬRM–íšÙ@¬jU ú<Æog‹XzÝÜçˆl t,C4 «¨VGЊ€ënæ:ãÙz«•ñ¸©`«¥äŸ½´­eÔ¨®{HX¬ö¹œQ(Isº b:Á²†x—V}•u~½¸^‘Òõž––:Ówô‰etƒU IóçPžpuUÆ‹—:\Gv9è²"µ‰š} ¹€V|ȈÛaÐk„º},7(í©5AdIÃ’c^Æ¥ÔÅÐ"?x#4m;…Jy 2êñíS¹QI·R»¾^Qß ·Ä3ù=êúY¯ÄMòn¯àÃ]äyA·†ã†Úsoš\Vð^Ÿ'—¿˜¬Òõ:!ÆwpŒäLÓ~–µÚkȪ[Mjq•u,#Nð(œj0XÞ|0 `[E~`ÓÛZú7•½>•½ZgMzB üc©SW”q*-ÈÂÃN Ç=/U³x³u£R®0ù õû$F`%~]‹Žžró¿õé½öé~…)ŸK÷7ŠfNö±wÏÿN6 øþ?5W²õáû{ú¡?ó¸+¤G.eä²Í¨nñŸþA@«ïÙE·]•ËÓ(]þø‡½ÅÌ ’ Ä×Là ¶Œ[î{¶Zš ú),TF$Ųëéêäàâäëì½ð:ž=×. ÿáµuwx€ßh›Bw‹.—ŠéY¨cÞ¼‰ÝÞYÈöÍ’ÿüë+”ï*ˆZXA3n›¥ïWW7?ªŽëO» vo4[§ÚÁŠ ^0±?¾9(xî‰íU sÜßY ¬¡¹';èxšF–ÄSTÁ 6ÅÐõ‹Ä¸ï ŸŸ„ £w_„‹ ÿð1G\¿˜£[‡[Ú.$pÈh£@÷þÜO³c1ï \ùÛ³ŽÈ)âêúO©ðùrqñ7Ðóíéøõ"®Öÿ+ßÙX©Úãÿ¸úfq,Åp=WÛ…1@ ½èU€Ý¬¦O©LuŸZŸÙ¿ÛŽç®JÕ5~ö#sˆêŸY ù6ÌÛÎA­‚g¬’J«çɈP«æ4p>Z\c³·–SË6­P¸ †/)áŒ\×Ô¨#ºò‘“ß`D6›Î=¤î‚â¼›„Sfë0uî$¡h+Ù.yÉ;CGçN$c„—þ7@c¡ØÈÇRÛ¨–½ož-›+ÊàO¶‰ý躊> o‰ñ-¤úÌî’ÕÑçÜÄ®õyìޤ/B:ij0WŽ¢С@q2͢ﭵ”‘ð-ê<¿è0w‰ßݼ;k’þéb/¿ˆé–éúÇ׎‚Ô%pFQôlÝ ‚ÃÉ4ÒÅTô{7jþÂqë?$pÛ´PÑ·„oQün9ä«ñ€ó,V H©¶ÌÌG¹ÆknîO¡Už:gû£­@¨ùÌ?d¿ž] ȬRÀfêˆp ½¹o¥zS%rô’Ê÷"=2àâÅ`EhbwHâÁe-Íã"묋. ÔÏ'_AžwÞä‹]”Ì zpÊmÅMõböÀ¾‹¶ „äO-Á2[wüþ àì(c)1 쟫íS*ƒÀÆ'Tã˜(Þè3ƒ " Ð[u7ˆÁá\N8Áä½Ub{ãO"yÕ?nÞ &ûÖk5%Ò4Å2 rþ_sñB·^%}¤ƒÂC#A¨ÏÆ—çß>¡§—1xR?zÅ1óXsЇ!ÝÒìÆv Úì® K"§(Á¥W¢Ž³ð‰ ñ´5[†^†xÞ¶ ‹AqöÑÆo¦¨yÿá&s=V8$NMK}BC(2&@rN•"«8y{–ßPÏðǺ‘TæsâEªYUwÏ`' ÝJ;ÒäúËzèºØ…%Ão‚.·H9_}îó³3ñoÏö;Ñ5y•VBòLŒû‹×躛ڿÿ‘6¹"¦¦æ­z@ùÿÃÜl<Óí‚£é…çVÝõ@"p?sÀc‡„ÃóÀIrÚŒ´ã&™°EΜqðÌÖ %,àÞT â8Ñ¥cÝ¢Öíd2Á•æzÚ’V›šjLÙŠ2¢F2B®„4áDrfCŸš#Òê‡ÊùØ:#™ò÷õë]]è`¶6Ö£D¡²ãµÓùæßg÷ÿéýwmïSerÝWóÝKØ·oyÜg®›ÿÆË|×ñ“9×þS“9×ù““9×ñǼwðÊ=ãS¾uÇô¤k×·5íÞõƒÇ¼wôê=ãóokÏøô­ý‡yïìõkƧokÏúô͉?ؼ6ãðhÃùkú›Ï}ǯɩ¿‡å \wа‡í)\w ±‡•:>ócLçˆqí½=Ó›•ï+ŸåI¾‘ÑZ-ŸÉZ>û£üå…³é3Fói¦Û~5õiÎÊ:\Ów¶†×þ4íH®é3H“Ûî4þÊ1]Ów¡&—á‰|z³ ŸÅš|æ[’w-¶·&—é©üXc´zf¤Ûd¯‰É$ˆ÷´pz©2>» þ‹˜ìœ=?=‡u@ï6‡mÀ}6Ãß}6Ë€ß}6sÀ}TÆ!ûLÖ!‚]Æ!ƒ›>{?"³ï|×#žÃdÐQÙg„Éâ_Ö~F¦ÿŒî=‡»;|i>>ó²Ù/,òx„lÆ>%³ÿ“ŽúöÈܽe2Yc6S`̆*2ù ûLÀÞÓy¿°ñn®?42÷{P¿õ‡üñÞtyW¬ÝMhìfðáeUëuâLìÆVìã¥J^-¼Ù|F²,³Õ2¶·i¹Æ?tv¨X+CÃbD1/õx›ºev+Øí YàÆŽN¬ºttµ¡¶!/•:5°@¦X½ãê1UËev¬ðc2žŠÕ;9u,Cß ¨(W‚Æù9AÕ'^·'ÙÕ·¶¾Ú®¶µëç9,1uièbÙ‚þÍqÔâA ésðk÷‡ˆjXC©õŒÝ(jƒ^ еñ÷Yç$·ÐØ¥¶o¹»nù=Ÿõ:?ÞøÝøÝüNgËûÞß_‹‰Êô"¹N§Óãëã̤1ê£JáÔpòfKŸŒM™¨DA¹2uêŒY'"w“ekéÿ#ç, ték¼×¶mÛ¶mÛ¶mÛ¶mÛ¶m[ó­«ù³J¥+YösN¥ëø-EýF€8ëÅ~k½˜²£á?bMĹöiZ« ÁÉ~ÆPn¥M¾ EêÕ²D2®'è-6<¨M¥ÄÏQ­:[½Àxj× ô$LÓçîž'âxýá—c³-$[£fž¬è;Ä;`$±­…­Ñé’k£cÁ¤¥ŠFåÙ® µ¢šÄc¹ ·0jÆdÁ ´«[ײŠZb'zËÚ}«I×.q³ÁÕ“ªW±å»ìax ½ÑøðØJí1¤€ ¤zy ô¶é‹YoÂ4=H¢+|»¶Ûq:°šŽ³Û™N›®rOO¸Hš²#xxkAŠLóŒ(RT³l[=lsÓ³ìÙ³B‰v(‚˜ÛGWÁ,CÍ8z™>H°!F¡rµ½Ü{Zâ7¢b9ÚâÃÞb‡‡m般¿*@\®hrƒm¤2ŒßÙ­óõ)-í¼_ -ø}»vÀù(ò9/ Ô×sØΦrò˜‚~2}ô08²Bl)¨[®NÅÀØt¡ì‡R]½qðøzñþj=OT=ò¿M´¹}ë¾MŒá½L 67Ü0Jí|Þìlõñøïö‰~:½ÖdµÙ%ðBÃùqV«õ½=,Ç&”£H¹ ‰ƒí,ë³/öíÁŒ¾‡A‡ö÷@‡†“Q&‡ã>†‰ã:QÜCÀxCÈœâE½ÿ(YÅ÷¤Zz¶‹2ÊWªOIæÑP³Š*¬³ŠZ{žÞDX-% 1)9yNý ó9œL©ÈjSÊŽ`¨±gíFš\©G@=üµ#¢=b[»àÉ‚Ïp¿Þ{/í-PoJÚ7Õ¿cãþ;*mƒà« ÷rW7¨Ç½Ò?Ë‹GšÞ@ Y¿gø¾Á|JŠÃt2Á©9Â9Üáˆ}‹ø{ŽÏÅ=ãN?aÛ9ÿ3þ±£±î{^¨Ï9%§ª‰ÇɉïÛ“…éÅëv~ü9'yCÃ#ô•õ9}F ^ä" |ò–¯dùIPZXjÄ«N ªW†ôsWç¯ÒÖx,‚A°Z ÍŠé6VÆ•ª!ÇL©›0¯Ñ…É, HËn³ó³3|¿¼8rãur°º7:ºƒ3Ò"ãs²¥Z±?½NÕþüèG‘ä Ÿ´ÆŸQI¬íež½s¿_”¸Îæüt^~1)ÀˆÙÕR5~t¤ˆ%2§‰BC›íñ~vx—v¸¾_aB ‹2“< ¥Ë†¢Íí…OÍÖS7×—$s9V ‹ ‘„>7qè_„ ¯²„}¡6¤ÎGÍLwÁÃp[ªæB&U3öÛ-"Ü {˜lX‚â›Åõçrýx^¿8sòÜqz‚™ç0îŸå-s½=KñÚIÏ~ôˆ¯£UE™¹‡W–Wà-p5-©Úô{À›4 ý÷Њwd×9Æ ;ƒt#Çÿ1<›4ÏÝð„ì,{Í Ó‚Ä5Çÿ4 wòßFð 9éèÛ‘÷®óúynÎ-ýMûèéz··_K—yÀ÷,R÷x6ÒØ/ÞLîVÛ;‘-÷ð—"×s„îá“€×#Ñô›¸il› Œ}°«šë±' ’C1#’ïÆådo]ô»– •ba/~³Ié² È-˜>ä‹2Z',2ËPX[i û6aFvoª“eU¨ Z¾%8ÒL’©EU)¬û¥c'W â¤b(Ì^¡ .S«MƪH´Lø§ˆà4wÈSÔ†0c-Æ/©Ñ´†”:A…8Štª¾6„½¬ç¥_{Ùn;qÞºÂsÀº~oZ1ç%’ùLHP‘>)ð9J+¥2^öÿÚ4™q°Õ}. "I½S¨ î.©ÿöÚráÇǰ}š7ËÛ¶éœúßô övÇõü@UìÂ铲Žrß)裴 ä±ýR ÌÚ˜L†ZOK¢òW[|ï%Áâ—ÌI²knå2])ÕW?ÏןeLp™Jɳ›©zΘ‚Ó!2¤ƒýtñ”þÒR§;¶/ÂMæ1zÝé0±©u˜:ÆôœÚÖUüÅ>´vF¯¡ÓgéˆÞËÒù™ðÄwq ïa1ÏG‘žÓ•=´G?fø€—Q¢šñÞ\0Áez÷ìŽÅIØžøyµ9hzf<‰ðä¥3ª¤œIðˆ¥˜|ûíªJྉ°–tp`&DÙ%…ïz’š»óm;„ÁI´£‚9îáߘ೧~?:…çÜ©çûÊ‚Jùë4c×Ö‰Š÷^άs§…‰Tª—‡ÍÎk$Áåk|§ÈIïPng·»XQò ɸ7hêQ±Š“mZj¼—4g—åÉך?%ÂÏbêñµÀŽ@º²Ã—8 CW&rË6ÊìîV«á§=±T­””ÔÛÊÔ¿HærBûÆúúŽ«{ÙÀ ŠÚ„Œ VXÉ´¬ÿëfô ™ó_i °ÐÅyëæ½‹˜ösf%ÃɰݸKnõ±”½”.º¨›4= oAœæ!6Û}œÖ6ì甪÷U“Å¿ËHÁ¶o5é47C©s;Ù=ãVz˜æ±`ÃÁ´[DqkÛZN ³ñ}62÷­|#§ößUú#Á·$Úá8·ã‹• „5ð‘!i[5îëDßzŒ\°HRïþÉÇ~O±R#´£TCÖ6Ñ{ 5y²ƒ8ñh—|4Z翘í ±†p±Óg2¨~h¡÷yZ>±Rj i+{lZb3Åv=.¸CL¯ü®âºkw=š$+5Ûuv{œ‚œô¶þ• ã³ZPü­´(‚¡óSÍ}~‹fÃòß7¨ÛÛJ$‡ßåÕpŒ€… é››4ýYè[·bò(“Fu€g-J‘"tz{ï …Ï`Âí' Ó¨1Àkj•€|ËgÁWŠØˆè_î!šóó=³‡ £õ4¯öƒø«…pH CëÞºz©'&Äúá·T¿¯bqï@–é°MùÏ•’Í“ËÇ‚ÄS2×|®pÒiÑÔ¥`ý:2<¢ Öº¡HMÐ#î•=óá«õÛ+°Ñ©Ý¢©RYÎúîÿØg`ÊAJ'(®F¥”Ѓ†áG}ĵé‹I¿;TI.LJvKäµÎ³Œ7Îò­ýʹs§Ç_°šôÑëÊúZ8\‹/UÒa‘)2ã+vK%[E)ö¤5f[%’ϵº®«ºË:·êÒï>>Ùnè)Ò'­¡\é075†=ŠÐŽÌ-¸)rŸØl»×džŽ ¡þ6Ñ(¼ h¬ZfƒR~·Â'Bn'òµ£ ÐÀý§LŸºãfœla&–!}‡X IšÉ2ž¾Mİ3Oøcœl²ìà~~ÓS~œº˜¯&q&°ŸbÙES6Ê’ G:ÞxÑW¢²MJÆeÆšY™*8²«¢G eO騚ˆ˜ ‚˜Ä™²29ê+–‡g®Yªš¦ñö]Çýäk²Íßc>CuÁ¶òô]*Ôk£ß³®ˆ]ï]¸OÌ´Þƒ8q8@ŒàUóqž…>s"Æî¾ßܱâWéâ¼Ò Må<=}ç­f²t1ÙØF£ÉOqååë%ÉDÆ•^ñÔ©¯ÁPw5®¨˧ Ãò­Áè¤&QfqÖãeåîñ'½åûv€gþþ‘$Ɖ/¤ âÓ;)î®ÎµýÔ®@½ã#Äuì|e+”&Q­ÀÚ-û†‹1hÛ¸`y))ûÏØ¾„ÈøÌÑÄé•ܤÚÇÛ]­ƒeq¢£nõÎí–Kì-ölÍ@J\+ÉwâÞò~Š'x‡!ç5Ž¿0âø›Ú°P‹ìUÓ¬Dí›Èr#v“ *u¬ß™Ö¨Ö°1¾GÂäçK2nQ§ ’kZ&¡ñ;g@ò—ö©k—§IÕ°£(pôm.2ïB}@+ßjbÀ¾—Ø´åT ³­ö<¨ý€Ëï uÉëª;(Z§£J«`†|xS5…`¥ærÁˆË#d!xmÀPb&"bÇp8ËQ‘´£$gçó¿%ÚŸÕ<µ64êm¿³²4 5Å#.=X¯rá/êö­É°öʉ‹ô¹ÉkÆM.ÔQ-Á‘Œ#<,-ïjÜŽýºe*ƒñ7„^OS*@rWùšá|»KÿÂñÐYܬ·°¤YŽ8”q”ú6I ®Lá=**Ú.—¯ …¸”y|.% ŘôA°(׬ )‰½§†ûˆ÷Íþó2F. -? d#úUáÀ¨E[³Ô§voä–•Ha­ ‰1”·Fö±ú‡}“â—TFV«ÃÁÇÊÍêròx¿>¼~r •v|ÿ}2QuVJ:§Çáöaèô>bÙ„ø>b¾54£“ÑÍéáù>yÒPqÐ0e„„÷Z¯5u"ªûuÁÉ9ñBNCU:EÅúu„Ê‘æ½F6Èh²j )7N‚—4ÊH¡n(JT¢8\víñõúl\•ê2!Eµâ:'Ú¾›ï»>¢iÀ¡­ö– ætS`_â¤ïv¸“=Ñšk®0kMð²éÔÏ'Ü$'lj¸„ðë\ñ}¬ý:\^É@3˜D©)ÐÀ{·¡ºyj3±U\ûß ÜIDÝ®Ê ÑÕ`þBïÓõõT*R§™ä€xùŸ)š¾Ž°‹Rµ½Žþˆ–I(^S«Š§þ«¨s¤ò`´‰U(a,Š39‚æËTJazy.lï™–éÓ¯¿ÒhBÌÁ[ymËWQâ9¾^aÌ«œï5¦`/â-¤>'”³TD0Æ<)¸—z¶ÌM’Rø³èìfç>[½· VïA É3«Ã°Þ~ÏÐùZ|¹+ß…>kDjãdûÒ6íUƒ ¨‘ôÀî1ö‡(éEIVò¨€Ó78@y´¦A#½²¹/ÐÔZ:XÀªPn ¿Ü‘4P­±Y´ž±È·P¸Œ%kFÑ;Îwíz((0B6ŸOЮ­)îý³Êªä¤Þyíï~>$µ Æ&æ»Å©BJà=(þhÄÓe&.e‘MµÈ3.ö[`ßÀ…å~¯7Û•þ%p1´N˜žAUÿßãR0Ô¼¤&\%BÅat` ½pzÖVR[Ý6%d/§Mä0*™„5ÁÚòa¿…jV}whDT2z¨)0Ë#êÁçóñr V’RÕÙ^>[ĉ5Öê‚vZß©û ‘ÿYìYrI-ð®ÑÆÐn“`šQqd I{5ëk;á<ŸÆ_û÷ðªÖ‡Î¬‘dNÏ+¾øà$Õ ®7ávþäœà\Ö ãƒ5xè«ôˆ”`p¦Úý‡;M´ÿ—È3–eÛ_ƒã¿Éå7@CÓT£oŒpÃy ½†Ýüuzs˜ñçäòñ3ÿ.¸ŒS{é˜`£SJp£•†ŠJ"dŠò¼œeœ>pÃ<Ý®‰5ÈÂ5¿À/BªI7˜XгžwKsí(ßÂ<ê0ech_¬ß £Ïð‡›M« FŽ‹¹ív..6ŽXÖÝOÜ›š¯Ü(/…!çè‡Õ§B:µ–¬Óµ¹ôä[›.ÕøÝã»ÃkJÖqíüOùXûÂKصÛI‚ Áõ1 Ê‹~›ôªJ7r4 '; j5 Aa›Õ&²í!É)Á†G–£cLèÍ Ï„K—½ƒñÄÎ{M Ù’JóÙÞó°F¬Š9…©$_~ëerþ-aÈQG.¼‹‚}£—uÇêÐ6 i švuÏÂ*:{Ié»KÄ0>‚ÜLàŒ?dæP·ÉyÀ×<¤K×€ UúL¼²•Nj݈‚„Y1wÒ ˆs†JÕlå,”ö@üÙÕø³Hª÷о`ƒŒ¡Bû³é0XÍŠ(ò`r7…Û' Ð5D£%“úÕjßíç(9Ž6tc¸W¦’@zŠÞ Y¾–- µºb;ÁÙ˜|L®Ï¿EuD XþQF¨âet¢aB¨8ÔQ‘5Ä ?6ô’ Ü…ò³ÌxŽèIþÕ’‚:°¶­? Ár[æS†¨™|—ºjxë¶-p§ü†ÆÉÿÐ ÓÏþ<­±PÚ/‰Œa"¿‘‡T¥zúP!*bÖ{ªíO»GÒWÐv;|$A¡fEórbN.5×%3¨×ÖØu„7--7[™B‡GÜ쬧ê <ÿÃ=·ñ¤!']Lq×Àêâ;aW<ŠÙ`2vÚÕ8g¸Òm‹×øæù{—„ÛÓÛ ±^}œ‰d¡Àë*ô :uwÖ™ øpG¦¢ b-\<ª/œð(=•ÕKì#áSÀCýÈZŒÁ Ë÷Œ±ïøðëØ[3`Lº¥ZL1kÕØÄcœJGš©…¬ˆ.X ,“2Ò#ÖžîØ™ôF GY8Ê›«26íŽÿ‰ìæªÖFÆN!YŠ\lTñÄœœn Öľ/ØþÔ F»¿2)–ï­!q›Óž:q]aÕ3O¾¿(oVŠ6™ô¹ùÇ냒‚?ºôs‘_§ûH«×}‡x©øIÆ¦Ìæ¢¥äS jœ“YÖ/! ÕŒŠÌ"™qý5ÙBK½€ýSïKGŽé¬ï/GìOªÆÝJ¨ZYmEp²'îYÚ9ñ;ÁHw–þ\¹ÎÓJm™Þ”#ŸÈk¼tTaTeh-Z»˜&g B>™äl z0ÈÒ‘ð,%qÏf^øF-‰áau'þtèmaR€8\;jôMÄé¸îk’ž¢°ÀÞÔµ¼Â©Tû&•(ë= #rW•ȼ œ¸:eÙ‘ÔõÚŽøÉwá˜g,õá¿j{¾$*× `ôU±®ó,Ègßô€pô?K‘>úâÞ+Ó[8o³km3Û¹iÿG¬>³5#Ä3ðà×Ï ˆ's%è15êФóÓ4/ÊW3ì Dwmê¨×¤ýX¼~0èä'•û-<©iV÷·N' L‹ ЋºSJ+˜ô ždéJ°fö["ÆZæ|}½¾ÖªpûT›J)©—}M}tuhÁø¼¯)|+~f Š&åÀUºPÙýy¢Ÿ)ÿqÆqVx&JÁ¤ ÿµ9*e«l[røXÿž;ˆÔ8¥ã¤ÔÑ4}l’¬ƒ¼Sþ¤t³Òû­mð\õñVýE[…?úžë‚ `TÿeþK´¨²Ç@2»9ZãÉJ¨1í€FÆ(™ÌC¸÷–ÅéáÕÜ9r™xÕ¤ûB´’¥Œ*LÑíÍN)dF¯0,Ú£¥/N¡°}£e:OùJV“zÛà2íê#$¬¸\7©ýµ¢>JCûÙ¨°²½ë5'€.­T­f ;áoäQ§MKMÈ@$dí5vâM½CổŒ…‘òcâ|âϺš‰äD85¯ëø—‘Õ›B\­Ãx´½Ç!çî“ÑK KñJ—¡¼,ìC‘é^~ ª¥ £ÚÈÊb ZjÈ:ÞñgƒñwxYÍ©hvø)@–XÀ5¡'<7A‘‹¢Í—éy¡âÝä–z¤ð0„yõ…e;üô‚^”ñšÑPgó`¦9Æí¬ãEqäL¸†ýüÉÔðŒXõ©EÔ›°HÃ~» ’{.3¨¼?½~_#Æ7¾Y¿OÒhÈùγp€‡†Ó ié-tD€~ÐÖBtsCÅkù*›œb›W‹OÓdÝâhɨãÁUvà‰Ñ ƒVÎ}Bú.+C„9~Á)Á(]ÝQa<[£šö¬õ}MΙp¬ê ý%¬ûF܃ҳEÅ­û’¾ù˜× pw• œ}ø#Zi…ë3X×€¼€ÎkGbPkŸ”±â…wÚ7¤—„idÒkU#¤‘UÁ‰Ê¬g¢Òx³~¯¸ÂÈ Ihäaß– ø±ù²xØ Â‹Ã ‘b=ô‘¸ø—ÀºÀÇÁO—\Ô€ÂÈð6µJ€YP—«¢b pÀ@’)…¨ %œNŠq OãêG³§³Ÿ½ºRœÒ]aŒ`ÀácAš‚2U,¦åDtj •<Ïæ•Êåvž¾ríéñQ—hPPPª6ç‡ü^'¤ñç=¹"J|·ÌÑ I;×õÍ4ÿë–eµ;}ÐsQÆÆ©µ\F%Ôœòˆ;0߇P;9ÏŒÜû\§°RltiÔý) Z©ÚP;¢©rÕn8uq Fœl¦€>‰—è ¬¦bÙt½w} ùá¹@¦ÑgdÔò}B]Â/*¼8Åò_@9]ZæÔ)WbQ?£ }eO('iæd¿gñ¾ìÎ:(¬¥ù|ïkÀ Êñ’’×y9ñè×ac[STŸj ´u2â¿zË] †¾:<ûši9UµcäÔýónbè·iejüÒ&לøì øK¦&‘kùD>À~ëf¼¡…E&]zß@N„,—¨Nî$¢'E5ýžw†:‰´–wìÚ;ªX0öåîÇU¬·>8Æ0Ëù¶µï8ôfšÌ¨óÆ%2˜‹ÿýIÎËä‘ös÷Èß~€¨x{žL‘â3©,L·^¿œw(R½7©‹Ó0lFÜTG¹…»ˆÄØânŠrF‡‡•PT¯t÷2Y×óŸ“|öšGiÀUf÷“H&\ÌR÷Q¤Uíí"¢ÊØ$Én™o÷ŠM‹Ä5Oô/Ör¥ÈÁÎuÃÀUœ@˜*|ð3“½i•\ÛÎÑßÙþèw?!]gÝy3§ge RÚkBÝüI=䌒Ïd¢ï(ŽE4Ø\à3ÀôÑÔÞèI³* J½%”¨Â®X”ÐI¹çÅ`È’ÐZTÌ7È9Â"þX3±W{T?}û1ç7òöB1ÌÏq…€—h ¸o3å7К÷éu*9y —4’”|Üœ4ÞïÀE˜aöçPz ¶tÕå2üNŽøPƒ£«t„æ÷Ϩe¶ÕH×½—¨Ç²îô–QˆÅÌiILS·éêFäjCA¢±wM»î>™Ó>ùÙ»0·Ü‹è‹»r€ÜmŒ\Ò?y*½µlôD „æ$ºùioÉ\Ð`ïÖ&‹¸cªÝ9ö’ö¦¹¥î½ ]@7¸H~·þ­4gO¸n_î-ºšð’ÉHÍm’±0mMÙâdCóú¬Œ`ÃaTcÕ­™MsFÔð'ø>ÛŽörêW,Ž*Xʲû`-EümM» Xáä%-J:l·üÔ?"#‘[R»y¹c n¾?ëü»¶™”¥h²C9³Ãó^RC<ûŠ5Ä=¦H@û§Ö&ÔWà9žÀ–nñµ^ÇÞ2ßš¾À \¸‹Qtö¼áç^ïÁ´U…­ê/+ÝæŸÚ—/}ýY^м,¢• {î w÷½Ç@³v|gi §÷XŸC/ă(=ÇRn€HRôêŒ7J6gõM{(€ÜÉøêÂEŒçï4úÇb·ª°ÅÕ—ž}p+P ÑáTj„¯RôItf®­¹ßù@{)ß1W¨`O~ÖÚ9«ÑÈÍ­Ûýü`„º âí$fx/]ðžPþ•úŒHR¹Q‘Œ‰Ý÷¯EÒn|§•ô™‘–H„Íšð4ú0{EÏ` Îõ•©¶É÷p•­=[Ô¦ú¾3FiçôEŒ¯kÑ.þ 4r‰B!!qoŸ“Îcôϯ”[)AN–Ðò"ßrcO-Pg³nCCkG%Hµ-¨É”èÈÛùÈo¿… ¬NŒ¶^]ζ¾t¼qö¼\=¹ÒqnÊî¯ ÿÀ»{ºÔç&šòMqGúIqš¸s ³#–c£´ÜQâc@¢ÍÄôÆÛ¦èm™püxpËölÑ<Ë|‰^·!¾|$aF¸¶-Æ"Ç~™~<·’eXH:¨kè¹/äæ¶»}¦ðj?fòOáàÀË÷õÚÏáêÞ5‡µ$$¯l–ð|²LKZãY¿úÕ;3¸+TÕË¡’(QŠFíF¥[ã"×g³iܽØxÊ‚qÀ@×WÅ“®þ0j3ºŸ>„é7¢e™¨y¥±¼´©‰ K[KÓÿÉb3¯†Þ* \ÈGyûY1÷üÅÆ³#â÷7!5:ÁwN'äà( ëÖ°A?¬•–ø\v*ðåûó[Sl†|ÜWÇÔƒíÙʼn?vSpd•¿Ø†Q@áÒ ðV{\Jþ|¾„Ï ïe)o(šéê߉Î8ƒes(õ~_³ª¾âÎ'.È… £Z¥>â·‰»þãª=øpoYã¥=îpr€j}K-€¢!@°OTúÆè>´Æ|_x`è^8úÇp²kq~ë[K57Ù_ßu$7Çä¶CÆgxoŠ y ec›&xìã¦;‘ÏÔWb3Že³±Ó–ÏT XàúcµÀ¥‚JJh…%å›?õа ÝëÕóDÌÈ]Ýk‘$Èá_PÔ å¶UÉ‚–U+–6êWi;‚¢d*HÆ:Ù† el©håPC 8Û„RÚ)”ÓÓ•#è_ó5­(BE0ꃉ‘Ð)gøÆ*Þ˜ÄFF†½Ïãá“y°UQ½°=zºÂ7¢#}ô¬Ýò¶ï,NÈÇá‡U¥19¦>qç)ï +‘~·×aÛ·Õ» ‹šÃþCÞ·ÕÛj¯ è5/ƒåú_ä„ÔÔ'¢ê‰µaž¦Šõšã3'nÈ÷«­ò°éjêнåÝå´†{ü« ³>µ¥c»Ì^—P¯ŠHUç8Ȳ2h•öâNw,جãiðaÚ÷é¦Ñdnš³Ñ5`y¯ìK¶u3‰ž@ŽKv#lÁ3¦b÷Ú`GZÓàóäªvI¢TþËWùª*[Ò×°íÃ}¸ä”Z=¦?sÃàšO 8In¬å ËQ§ª4ý+×û‰X¿ ÷[ÈÑñHº*u¼:4M~(µmöŠÈ“–nX5o×R¶ëÓÈžRó‡wì‰Uh´ñZÅøI:pª]já¦ü€ø½zt$#’|(v þ:ÓeB 1ÝìÀFüãÜÁ—@3QĽ¹M LÔþ¥"НV÷‰N1­¡*L9-HÔ ~­žH– ®ViÂÜ›x¬ö4™ÚÁ¨A˜V ¥†4K„¶"˜òM2È¡ úA`Ìÿ zŸR9¼lè™bäñ4˜êd¢9ªhÛA’øê{Ý0]½}Àpý†"8·ÁªŒG$@á6Sæä÷ !Åtxsšo©¢®>uoÙms[W&&s?×%1e{WãΞ_‚î£\ gñ ú{7äÔ“6ÙmºÜ¿w>»ÖÃGÓì2áBÖÍšÞ§Áç¾81ðóëqr€hQkö"¬ä]^`Ís&îú®“#±æµ]ÔÓ´êrä’™©äîì÷Øe‘%&ŠØüLV“+ÂÙ)cÿ\ O®å Zl=ÞÏÝ`¨q ÍVP Íç-Ã#“ˆz¸$“E“_É ão/n…!‘™ÀÁ\ ¡‰Þk‘©:EÄù˜b|Ñe…7K$­»ËËQ¢“HYGK^ÅC•P„§¢‘XÕ÷aì*ˆ{Ë| P¿¬ã¨ÙŸl¬4ÐÆ—ƒ6áé&ïŽ2rT'¸à‰£UÌR”­Ø•ò’‰"ö2bCÒ¶9(øng/*R@Zñ<á?ýü½„Ã… °"¿»R.ƒ"3Û˜tÉ’N3ô¬šK×”MÕ\7èfȘn2/"°^ì±o“b‚<÷¬YK¿HöM‘ÎÎ+.a¥fQW½µªe+$è®'“}ªõÙ‡zéž`¼ýhñQ¨öíéJzD9ZÄÞ›®ã3”È YÉŠDºäè=!LY>-#×8¯Øp-¡†Dƈ* ýUùv<üc¼å?^±n$]K±‚@’ØC"¹ÔíDšj-¦¢äüWYaÎÒˆ„¢å"sS×ÕvÂy9¢:¾ôŠ÷3£t%BkÑŒ³?Ýhd¬I¬…ªÊ/Zjy¼7c8³r¤]¨fs?íæus÷ÚÙ¤ ‘¿|EÒwx,óOšxï{ '7»“Óák³fðxùšæÐþwue0 ©­ô”t×FìºÊ¼µõ„JãÈ2G°øLr~T¿ûý‹ñ¡L€Yù‚îÅæŸe–­„îËrÑÅÑúXPhaO=\Wò" ´Öu²+ZH=B>ÜígÍúŒIcŠ (Ö¦KÍ?¥/kKopñEuo^Ðs o€bQ}G^íjvëø„²Ýÿ¼²Ê…>UÄÔÈ€‡LoäÔÇúÖ+Õ®\_©V(§É[9ûøQ‘¾2A#ºYœÐ`žË† æÓ+G¡N¹ä J÷¬› ¥ÃKyw¯MG–’š¶Ê¬§!Z0¢¿5 Í{ó+î²_@æî¿áuÙ_Ú¬TŒ]Eï”ÿõb°ŽÌËÿ»*Éô«ûwAÖ >¯Ž7JõKzÒ¶-£|¡®¤¤À#±Ò©s±Ù¹}yPÑ£aú³µRa&á&0¾S{ÌMþóp¨·{ÿ9>~ƒ`c+¦¿sÚBR{"Šüø•@YUá'”È(‚¨6gb ÿÍs¾ƒIÿolFì×M§~© H"Xdt•¬ Ï‚›oíª‘Gu8ÌL¤Ü}Oä7ù!tU²ÕCµG;õ0rÏ_C&p‰h9ˆ‘Ðú*z(Ķ(£õw£áI6@´&€U”,!µ!3œ´sèŽÊÈÉ-Òÿòåö€GmÕâ_eÉðÛú¿Êÿ„ÅkÈQG³ØÑÃÑ¿Ú8½¨þ5Ù¡6} ÔŒµ»ÖòåQ NÀ(Li5ušsç–ˆeªƒÖQ‰+½·e›*™$7ß¹ ô=PÏï=Ã÷S8æ»Ç“žíÆS8é5?(Œ¨ñÀ¬Ó:÷U÷ž1Kª½æ·ó•7ßtÍïmîf¸®¿>A×aÐ:cÖÝS¤Ï‚êRÏÑõo€OàJÄ…W¸è­V¾4|„˜óÀ28 wz þ&ñr䢅;QÁÙž \—öÝïn³0k::c ¢è.MÖ•Z‚u³ÏËÿÞöºÁØ Ã´Ã̹áºÿWû\b·»™ÄÝú;Dº>FB [€°{ñ£|[¿o½ínß6*ÔÅòJÞz;2?õ×áu‡YmZ`כՀû½y Ù÷•HôÙ“Ýü¡fAi/ẻ\I-VB9^Ë4òy‘«oP"B!%Â\K4gî?Z²rXÈÅZ 6qSŒÜ•.È>üÚƒš™?Õ^/¥é6]û%±5[?O¡e[Dº'AÚ2RØ6¶‡ÃÓËêÉ}×Çâûýùþ°ûV¤ôéÿV”¦_$`ÜŠ†FZ·ëqqüÎæôþOÄíËm‡SÉÄ$Cð8ê;V’ü©_÷S-ÂJìé%ÓF²A–`¦0/ä¢KÕ9C¡VlÒ¡tË‚M(õHzÅb4d*qäüQñÁ¿‰HÙ\Nê=J ¾6Ž–ÜÉŒˆ†BNþ½Ô—O¯|Ž£uÊÃ9gmÚÞÃJ…2ÝÃÏ€¬ª4À¼p6þ¯Ú¬fF,Süh#BHHê_äôÔ²ö$aŒg x4y²±íJéH²_­¿¿µŸ‹½ù¨‰šžT’o_ G vVfzNa¤!–ívwK  ÓÄÕ2[Ù“R÷Ãe VÛ!øz|/Adûùn[iÜ+(?7hÇxlg‹»žôu·^›©Ì²³â[“g êÍZÄ\Ð#ͦ¼üÞV €”” ¿¶Y l†/tRé/(:Õ*ÊùÒ µ_°'¦§w|”ö«ù¤éÏΩçè8ë¹Ñ#ñ%ßÄ¡^·Û:W„£^Æ/1 }êBoC|²ÀH'Œ®Fl´FŸ±óG(›öˆ¨Í}+±R]àûì.—Ñ)H“Ùå™aÛܦŠÕÊÍbJ»Ž¡×2KHÏ(ÀŠùÍüþÐÉ%pVºØÑ„Ÿ7.;Nd*ùU’z#)gÕS¥‡gpƒàbŒYëy‘xI|â·d%"‚REw[­N–cmA°yWäúÓ+yÿ›°IS«­™"p¿™ÂáªXwY-màû4­Á²::clßжÞè¶[R=º–fõAª&ÿFìÕJxvÜàfü ú¥)3å*»f¥ÐØBŽKj’r &âóô±ëNÍWq±‚pnªnöôH…å° )šó VFë¦Ãø>|, Bø³½­»vIágõškÁNHu²•ôoZ”ŒëC —C2‰êÍ6{F~Ï寸è¥tÊACíÒò¡\ƒeñZÙóUxñ ÔT™?gŸ/ñK•ÅÉÒCu+© S å…\óT{€àoÂÓÄJ› ¶ÎxÉb®Ôj„%FO„÷swÇä¯û³îÈy‹*ì:«»@ûçøÀ—§­ý\§r¶ôŽUôNí?/£­Lü½N¨k{j¹Rúc¬Ëó6|$[üV÷§¢Î Êë^! –J–›"—ç£=]î~^Ô«˜TYÝ¿×Kc¿m‰«÷Þ›•¾ ‡?ç)®È¦=Ýîýþ†’$;?/ÄM]íAÎú—ÕØÞ¯›ˆÄ™É[Ÿ¿­ª*Z;x€ÈÎÈÖ´e:Ë¢ܘڑ@çœgnò£YÁë ÿu$5ý$N qÅPÂ]RßÀô±3¿I†Ï¯“BLg¶Ú]M{¡ñ Ó Ô¢.äà4fÃDÈ?ª™"kÝâŽôº3Í÷Ϊš„:g 0¹]~œA‹„ñ+»3¥®À Ÿ'ãöÏ °<É/ ¤Øâ–•IÀ#sU#ÿ’Qøh&¬8®¼ÔÒxN¨þqÔ$ àìUóÁ›«BG ²_»3f'tü6+šâEñ+‘­vÖË©±µûéš9ãŽsŸèe·~-ó0ô&ã&3‡€Õû¤3e´•íæê”«¿/»œÞ£Þ³VŸ2Pnƒâh9ö"L­Ñ‡Š•×R§@¢1PI´jE¢ÕÞÀ¨ùóß ¥:Nðæè4W›ÅdmµG¯„k}«´ 9OÚWb^_ÙRò×R‘hcͺýë6E’3ˆP5}E(êv¤ÄŽ$G–ʱY^r¥¨–Ó‚®/YšÊ“4fTÕ£  M£I_§ˆ$‰' ¼îwAgwÙŸêȪÿ5Þó7zN^ÇK£ ºÎ:w'È¿—ýž‚$7ÙÆl$3ÈzúK|©j Ô<®¶›ã€å å¦Ô‡ßmÐÕ ›Ç¼¯é‹š3]3ËD«¾Óü‡Cr¸6Íò¬VôŒ¤+Ám)ê„Üð“‘3¢(uÇAÚà6"¡~´Õ›v¡ê'¡\ˆHŠdDÍ!y<… زª[9`ecèX“žÊý¦?o:%Ù¤]߬ÝXçj"‚¡[ùkÅöŒnwÎ(yRhBƒÏdCUÂ1_wt¯W‹Ë6¥kÊÇ;B_'­òÁ ­X•ƒœO5†ÇWý{ÏpÒêé”Å2ÌhöE¯ÉP ÷Gñ¢O†¿µys×Ö†%Òy¥«@~)H SÔ…ÄäF:Òvƒn¤çißèÆŸkÉ|k¿…ÌÒ¡²ÊåŠD¯[Á´©¥§† ÂLE럿Bòôê!j(—Ü¡¹ßºàIÀ¸5ùl0¬ùŽái0¹;I´Ý¼îI6ØfŒ££¢¥k0º)ô;Î0ž Sé”VáÉo`ˆC™UlÅž÷}·Ÿá¦^lÑnfócIéùÒ)ɱΠ‚_¥#±^öbœåÈÇ}ñ™PWµ’*pP&ç”ÏÄŽa-\QQFпÍ7‘üüÅÜ­ˆ×_v/¨™Šl DÜšjU±1¹Ao#j¥S  DƒŸCèÈý†‰ÆmÚkV¶6ó¼–?JÍ¿`x™ëllØ 5¯<õðŒèb7žÍر¡g=Êgma<4ó*›"›+fÔ¼6ÔÜoɳ©¡šrŽ1’Ï÷Vs]öM(Õ2²6 «’7_m+Ó@Z££1kevg¨ÀÏ]ÿ¤£ÕÄF«qÃKJUöÌŒhÀVU4gý«l°nФ£K¡yEfù2{<Ì“¦þ‹Ó“À Šg¯¡ ažâ¬y‰¯z 5ý±©m74|nϦæC_Ë®ã“ÐËpˆ‚5ÓÉöU xr%x5j‡M®ˆr ö›ªÈ o…wwQ¨’ÊÈ›;Ä»šx€6Œõ1V|/ˆùz'Ӫ槡;„¥;Þ…µFÅXX„j±Ky¼ÍåkÀZ?;iÕ©ÅÓ¤¯ËÁ\ ¬Óa÷ê|yÕ´aèàÒÔ]êÚØv$À e½¹$­ã)› ¾p©rGB«ñ¬}!¡n¾ãÍ"o WW¨¥®eÙ—k"J°]ÈpFsŽ^é þPÎzQ|/P]#áÑW+fœ–÷¯]‡¯SÝ3äžúøi71@1Â/lÍîK!û¡–œõñ¹ŒžÅ^kæ»°‡W‰–@¥ŠjñÍŠáØbô0 œðœfiUc“^*fñž ÕY¶ÉˆdTȉöØêzÛfÍ~¿"AJ9ÕTë§sÌ•W“äG™ÈÍV¤Ez))GÑA×nòqÈ^•*År÷lùÝ­)ÍÏ…0~!8¾Ùé`ºíëqí°@k°ô°©D3[hlp„7xçmZá* fZ üE3" ÝÐb{D²“E!-ÛÍíeTiçp¸¢9‰…´ˆ¡jµÔD8ýÝdRlÉ®£Ç”Y¤n1˜Œ?<ë…÷Û«--â­Îyª¦ÂZit¯’æñÕ]?¥³^ñÖ t÷9¶Àÿ¸7I™ÿ΢‰å…˜ضÿ[×Òú: ×ùÆÂäf'â+ó—U•†cˆ€¶Â¨q_?ÌéRê+¼rQÏòÚi²êÔºù‚›&¾—pŠ×T=;q™ÒùŠØ k„Ê bá}zuò7‚ ˜Èdûøâ˜Wtô›å8WAh\öî±…kÏ»äYa³ Iã*÷Ö 9yøGmfóXÚÇä4-„±IÖ}Ö`K,ÄK’©G»u‡ƒ6sWî¶þä¦Ñm%d¡5hˆo¡Šv¿¹ŒŽfÁ3GÚ~ÝtJ5XBr)K=®ãÊbZkÎø@©§>±ìQ.zªýS²63ËíÅ©IG‘ƒ€îºXCFY±òf=ÁwOM^Êô4+¿Ï‚ŠŽŠêÿŠgØùcŽæÉBü`¶Ü"þ 2É÷Zíq5S‘ÑË"#Wzü!li0“J‰üàï¹»hásöëjÑŽ…Ÿà@¹ÿ³³H¢£Öx{%¶˜Ó°eÁìK‰jv(X𠈫«X¹TKVÓa±U=й;…€D'𯄠ڰåKK|öÏÈã §¦·+H+ëÈÒf´ô3¸ÝñBEBئËåtͰ'eÌ&h½$'µ ŒÁ¬3XoµÒ ™,ÕG\Ÿ¥T·<<¡Uy%Oª¸¢7GÓԺ،”Õ°‚´F§-9JîZ'ýý##‡y"R¦ÕÕ/zð3œ/ YJæ[&axàÈ\9«ÄÄ¥|›nUìjfÒ\©™b;bž¬ö´¹Üï²Zµ:5`#Ivø<} N?®@¶&‹EçĺEžš¡ã¯$–}Ù¢Ÿ+ƒ|ÚÔÌ÷0½ÆYòð„›Q_93fä Š²ýHJ¡÷c0]Õ±—¾4â߇ÝOÂdlTŒáÖõß禠„rø%殳-Ö‹¹"/Ž_©oΙ3b>çõê8àâ¬.ŠèrÆÆ…GÌ›èÖÝl£K+tf1Ãë8õ¢]lZ%yÐÒ±| °±µ2ÖÈÊ f»+Áïûôl}Ø.ó…´W›„4³DŸîH§ó™aD‹©”p3·`TÝÃË dHið‚»%¥Û0 í˜Õ2'a!ì7JZ$a¡½ß’%þQx r[Ô=-†Ô­ í¿¢m NY@ÍujZop® Iè._’\kÔP$JA.Þ4Ø2穃®…%"Ì_>žª‡Ð—Îyìk™ìI=µÁøõ°5$È`°añµr­o]È( ¦^ÿ(ïÞUƒDëjWùc8e¶Iœ®}%¿@9𠑤šÂº, #;þ)JKx‘ÕÈ+êËMÝyÃoâ6šd¬%ŠkúÔÏ‘ûaõGêj,p-Õ²TT“%ç%TcE‚Œù"V|*2($Ê.Œ4“ÌGWŠA…Mb|Kd¥–¬¼7P`Ÿ¿Ã §3:Ç9L®=NŸ‹ë§ßŒ'£³9oë@º¬¨³¹÷²¨3:ÝD¾Ç%Ï? ¿ë猥´Ñã\&4ïŽ›ç ¨ß)º<ŸÁ©3Úɇ ©3û½'³Ù[×/ ¿ëé§²¹/³±Ùܤ)ºlœ£´Ù\ÞŸF>××i’]7Ù\3»½ *Öƒ âêÐÖoemécàú¢ÌØíÔòÁç±á5K2b-¢T'IYÍ|†g²Prü¢Ê|;ô$M˜)|ŠUlžµëÃpMö]TñXE{åzÿ[=ðîFÿç\ÙN 8¨»ÛÉæuFÈ.u, üâR#•ïý¶Šô—+WÍb]™Ë¬­åÅ´þE›1Á'!£#ÊÜïóùýÓž´èï<¥i ’µ^ 'ͪ†BÄKXæ5TAÕïn®ºn–;6*“Ëvõ­%=Ú]¸ûõ¨ôAY&òøÔ*Y8Q%ž,‘ÍÔñ RqçO])ÐTjˆç—©ì§†~Ò¡I·{wÿtžv_oM–r‰žþ¯Gg›2l¸Jó¢z©ÐB®Y½¬QÅʺTšNCÿ`¼(•½0Áæûõ[´!­Õ¢ÔnwóhËõp¼À*$o²®™{*4Ô¬-îÙ×à—Ë¢a1ÍnmÄw"2€ Šiwb¡°§€J›0¨âurf ‡7TŸyy!bY§½’ñE¥”&a #­© „Y]3KáHŸ•êÎ/® PÖÕa>4¤‡Ã[,’~†¡‰Zdà¹È1âδ[6º" ±¼q0³ >U0¿oàψ®"–§ÊËl¶Ž q…§ˆZ2;)øØ ||Ò9§#ŒýR5>~,{ª/~‹€üsÈ™W^Ò`α²ÀZú¨Ñ=EF¡¢ÝT²a¨ž-71Ù"Ï2jÐÒLI)!­ÕÚ 4)q%¨t²Š¿yeú %Ïúw²ùÓð<bC…0æ_…z¸Ü¯ð½Û§£¦MžouÞƒv˜ªë%Õ²?˜Í—Ñ WÒ"k_—‘ç2ÀÚ¬Ò=VË!:ÈÉè ‡/ÇÛ ]¶¶!¹$gúáïhæõÞ¨‘×ô†,PŠ„—‰oéUõÖµ­=—r:µQ¾Œ^üì–DUÑíY‡°Y¡c’&ÑHºAµµG­Ýáðó9¼šXr€Ct ³TŽ@ÒœSâCµ’¼Ï£9¢¤qõq÷Ôßת`›oñkß B%1¾çʬáEpêÄÃÐÙ¸ªÄ}x¼ìÊ]CÀŒ ®cË8pÒCªò(¹V¼aeÙG ÷»keÎÒ£Q î…ÜóÕ]ÆHA²cA†š‰'S<\$»Ì³’X.%•2úi'µêA“ ×þzº&Å‹vׇ¢Ž†5‚­ílZÝDXìöCÉkõ8{•ÿ;‘fº¤çYGõ»(áM¸¶K¿¨ ¸i²›°×aw¼ä[¥%áNÑ< ÿhbP © Qá³!yÿÉx4ˆ"âe3×ó¾¢þ5—¯&Š0ñÃ%öø%c™£š$Ú(k! 2•¹h#ê§·FaSJäV¢Û(ØË„º‚V-ËCúF)ð·{<šÝÃpn•Û¡O¾½˜É<«p‹_ ™rúœÚ% -¡3˜JèfÑ>úƒ9ÓñE#Ó͵c[ÇÁ5ª–2‰)W¹ïÞð_#rvK',=2¯]µ¼š&’ÜŽFô·ÛDR/ªœƒêšÞŒ'Y0á4t-»:M<-Ê?b§@Ú%òã ¢atŒ‚–“ÄÆ˜o EkùE;ƒ Kß9C`&3_ج·zÈzŠ$ZCà9mÜÐËÏ𙼡S¾±PUëÎF_ ¬¼ÁÓ¾‘Sµúk_à¬?Œ¬ÿªzOaÑîèïÔ¬=ëÏA&»´ŠÓ-Žp†O ¬rOܬê 1W8¨5ýŸ«šËÉ%Ò ÎŒ Öò9tf XëßĬÑ:Zëß̬~XY}A3súOMëßjëߨ¬,¬~hY}a3sFO{úOo ¾Ø‹£}‘3uFOqÚæÞ¿yúOtõÁ´Fˆ,Ì`hX.Múî9ëφMhY?ëØS탢/–2ï뢿ÌK—O„¬hõÍâ½…´£lõo:4óŸxXÆ^ƒX„Òõ?:ÀßÏ»a?õµnüRþ&¯#¾¹É¯ÕkK4‚Žªæ‚³¿ÎˆM\\žõ'cÁ‰í‚ÆèÄœM|ÿ'ÁòÀXq®ñî`€üÿ›`ij`älçèAkïQ£ncGÈÿ;[‡nà ïl l‘B<\g] ƒW® Õ\¶o"¤{åÕ\Y=Ú>OIšE>Ÿ?Љ&_}Ž€«¸NßÓhËHÄ›ùžœÏ$/WÔL~äo.µî­ª9Ævv–53J–ZMwüÈ-pùí`¿‡öÃDl­RÊ](PìG*}¨l¦…Ïð9­!j ,¤!i¤‰Þ‹5Á|4Åšžn^ln2›œjjN¦ÎÁ•í„á@ˆÝ­ ÔÔ¥K±i~t¤ÿødÎ×óÐΖ ý =jÏ‡É M´_ò6å¨6Æ*ÒY@×Àü@ÌdPì„Ôä¯Ö/X¬,-ÔªO0E‡ÔX\¬W~ÌŽ^¡Íd‚ªDÐᱞi‘SÉI±'j—*3P§ÕÓhaÿ2j>D.™嶋߸ .ÞЪ Î•üG2…ELS*F€<ãQNX³rh3ÜÞËÐi*aÛÁ~d,3‡Î§åh€X*:[ž,ÈŠ™!ñ¨FHQIJÇ.Wë#ÝÔ²ïKœ*• ù)ž§hÞïE6`Ìí‰ûšûy£ÝŽÇó¼Ã†÷y°V¿'‡‡GCèr’Dnš}Êꥊ ª-P‰>z¶8M£‘·Óp‚ïÊ<ŽhQ–`t;“%öT ”øO…_Þ±zúºÍˆÁ‚*&«°°%ŠMÈ)+*QسÌBdìˆmQzÆiÜ ù.¼‘8úøˆï ‰i<Ø“Â>©›±ˆÂ©_‰’¾Vò7l ×- B (»6Ⱥ˜Vˆ7– „j"9¼!E‘Ÿ+©VÔ¤Ò ®T€<ëÌv-€´)øŠ\®&yH©Â«LïôñF‰p© pL°mGH÷fb-ãJ -U™ÊÄõ¾‚ÆüpÃzdë±·V2Ͱ–}§—#?á¦g«GÇ G¼p¼AïºsUfèR?`ÄIððïc{™+Ð,±"aãP[nš&a~™«°:u:Wâ=ÎÞ&ò:e¦Â?Î Á·õÄ~€=âþ]žôaI‰Â©MÜÝ’šXáXH¿#EÙ =ÕNŒï>ú#0‚ ÔAX°ºö]”XÝ·b±Ò=$˜­…$Ã35!½ ¬¸–™Ä)jxcÐ)GL©ä†:'†ÁÏ´}Ž :\PŽG šìý57=ÒN[¢ibŠtªb¾¶ÚÍËŽg+ÅÊdâÅ/sz5›&ßÒÖn€id¥‹¢‘ìmczæöa4ù^0ý[ìŽ6£óJ‹L 5´NoáÛòsçý,Ÿ%x‚ZJêáº3›P"¥¥Øfè93Âa·ê ñ×Nel×ãÙºrÚú¤Fš3ÕŒÂl¯ÆÌ¸çî·H(}ÍVøÓìγ >ÑUcóàHæI¼\§ÒhKå]â»z¢ö’ãûP U‡iÿù@v-RQ©ÛɈ£ÀÊ_y{Kë(¼²+WÙY’5‰prýª¨âvË ½Þ%£Òã<åArê^Sçá00SÜfæ'D|z”|æã@<ÎÝdíŸo%íú…øƒpx7ËÇ+rÑ÷4eÈ“^BS}×2Å­u‚ºÜ¡KXãøÿ& +{®å€ÔÀþOÿ?ŒjT¥œ¶Dmù“Å%–¿l6m¦Xà)&47f­p¶'UZ•AE76+6A„‡rÕÓFe³'8Ž3¿ÃŽñŠ)ö§õÖ'óÄO¥w€¡ ¨f)ˆ®?J»Êþ[XÀ}Ñ1ß~/³~[ñ>`/Nb|Zàøó|àìþöþûûë ¼rR÷ÎðëøÐx@ `ª` €ª€ ÐÖèÃõ5Júðïäá”õ•p€xÀ °ÖÛÑSýdüÒ@YÌGÿî)à—úKgþSƒÐáúM¦Ü@Þ°ÃÜõðPý¨Ðœÿ¼†ëI˜jø@ 1Ö1âù`¡ë™L¹a½A‡5ôÍfÜÜa†9ôϦÝß=àÑüuè(ö«ðiºÌG—úéÚ¡¯1æê‚Ú®ÿ…Wõ×b:£¾³À¯è€7€YÏä;õcágögâ¯Ô­´"­“†cõÛú¯µ‚ʪ‡ÖÐÁO¡‘èÿùëöèo¹c×|òÖ»ýT\1«¬+$Žßѧ( Þ­JAÄY:þ\ ÙÝé¿ýŒqY6òõ¶¹ùùùÜáÔ½ØÛµ‘¿éÿ-%FÙüZÞ=Ù"‡yÕø 8©ÂËlÉ}BÐú±]r_£ŽØwxq?/‰)l{펉ÁË-nG¡ àŠúv¯u^MÁ›-;zgƒ`¾ÅD/l'ìU¶“Â-Òwouê;LÀ}Óž? óÂé­ù]—y]\5vÕ¬E^ë²EïüM}Õ· A— É}Oëý‹lÍC]X[õµ"Cª2Љ‘6'okõ[R©ÓŸÀÊ"+¨Îl•§zíÚA##i“lƒ^¦2Q9ÆUMÀxÙ/ùå¸_ I¾¯,VBªß‚Û S¸.xbÕ¯dæ÷AX<0;jå5Ž}øËP¦›Äya'ëà\e!tKÒ:%da/ÝÙ|ÒÚßÒš©_²øÌ{^Œ—ïC­Ü ?,u¨ñ8nSIì5wÖ›GxÎé }0Ï74’Ÿ5º¯’œOy΀º€=Já­÷/|°†X”ÊÇÔUd©XŠudЭÞ®EÇ` —#iùóÇÄo1*âP¢ ÷…¬tVlNdâ`ä­Âã‡É]Ƙɰœ>™vŸ•ŽOh<ä´¿¶ ¢¼µ™U01j7tãa’ƒÑ2ÏÌuÁ'Î(rš¸¯¸;Wž([3ÁÃѶL(íãÄ%1ν úˆ5–M˜†3Ie®aè´X´ßÞ‡¶<_9¦kQï¹°‹••[¼íÚÃÿ³m÷+,X뺭„,b°`¡÷„Ûj–œïÓa1)ˆqPÛÛÁîž?«Íl˜7 ìu]©¹ñ9ªä€Ò!8÷R›Ñ:§àë°\ƒøÜP‰="q÷üx•~Z÷ì¨IÚ¾;¬U{ÏøÞ‰6¾Ð½¹V¾ME­ÄC‰5:jjäCuƺ ÓfÒ@e£e½eÈFãZÕÒÔFãÊ5ƺ†0div©=“܈يó8]V*ºË÷ä!¬®%GšH¿‘T0ÈEëá~_¼ÅŠGòÒ+¬æ–_Ö´iݪ½¡’îº$Ú¢š~YY¢½Y§"¨QÊjËÌj1ã¿™Õ{¹éL^ÁfAX2ËU*'h‹½,"Ånøic« íÅ-Íý`ëƈ‹&Œ{ä€îRH‹quN+.\rá<ÚXþëPI<$ ÿÆ]]Q<ŠÄWž©%œ&øuL¥“c%&¡ùåüÓ$j£'êP†š~ú v@žJùÆ’¦*Hû?e8ãÍÇÏ .þ ¬<†ôh]€²©þzxLCàù2G 1d¯:4̧K´Ñßò´ƒ-ÌÒÕ iFh —å’x½º›‚x™t<×™“söüqÌÓk}ØBhWƒ˜…ø,>{Îkso{½ÅÆK†’ùZ5ÇñµšÓÄsõ¹éZúÊnc £1E£ø¯²)ÜѶTè/jìø÷2Ãù’]IƒHì͘Èê·óøéìZwKièº"ad~[M¸›«¼áÑ4f›{—Ž®8¬m¦¨]¯Q¬ÇéÄÕ7ýs~4QT”"¨»S&¢t&~»¥1»,èÅvóÙ}Êtt‹•g·ã±V&Y‹5mwêit«9C”Ãmä”ÀäX$«îÇZ—7w-¾[ª¼ïËø)VH'|W¨ì¤“4é¶Ùi7£yï(èI¶ˆy¿MóãÿJùwíW´ÍÜíÆøqÿ#ähbbèdüãn¸-þC\êÞ •çþL|vü¾Á„*ûÁ2y£qÓhªça±1   "„> <òÚ«ßÞÛ'@g˜Ëº×ò*!BLÑ×K×'ŽÑ]L“{jRПzIr7Lá GúùžGëS4öè)ÌiTv4ØOÉ»A6©Ãå©vC pœ(ÆË壅ä#( xÄMù÷"Iß"sС±1œ8”úaÒÊÿú0U£3q¤Sã$¹?##” äyŽXR6Âá "˜í±B#ì#¨•(@àð°wN—üfÈ#$÷̲ãðIòAËø“ñö]ÝšÕ–Éø¦ç]X·þª7'×%à`õ¤ò"5ÅÏ›J Î‚÷É,EÆXsŒ?vâ¼3†1ù4ôü $:uP¶…*Ù¤é6׉õÔx˜L¯¾åý¡XµÌ.™<9ÔÄ<îw{pùˆ,ô“­¹.÷ÉÅ$UnmÐøL7~üm¥mxæUááf>É{£š¼)«%ü.BšÒ._¹ak¾¹*\eæR—öjòÛ›2ôÑv;ÊûÎ,uLšLˆGX/±/_—?iì"ó®.Ã,<ÊV“Êiã+Õïx€u ŸÖ‰9‰pJ¸‰™Ù”Î|¥‘ü—Þ¬®¾ Œ±YwSÐL&I,’°À-ûMt(Ó;ÿpJ Å&&ü9‹ú(m¥óäâý€+#ö9 [F¸f#YGF¼Ût¹î»œ??Ñu9/2»œZòÑóôʪ–â¼D_¿£¬S­-GDÝæ¿Z PZþºëºÆf¼Ñ\¬W&r³e ÌYÂÙÇqEQ‹´q1”س|QÆwå*9§º<ÏþkóUJÉ$*p‘b - Ÿ×Æ ‹uŒç«nŸÈêD E`›áêøÃú»´n$ MÈÂP"Uæ³æ=åp0›~£Ê@7Ê%)µ„à+ÝA£X#)``%>‚û•Q>å+H¤‹²ôÄYýM <= RJ*¥…W›þ]$ÿÊÏ¥ÈkhPÙÖkpî…+®¾Ã߃Ë]ÒÁ¶dd!tñ-òé¯%«á‹S•À‚ÛsÑê ³»Rƒ·W‡ê8wüÁèu =Qé¶'îõK—ãiQ(£°0±¶—[iá+æ‚ê†aÚXùñÛ  ?÷eâÅiÕIÀ91Ég-[~ùf Ü¥®£ÒTÁúU{`Ü7„BÈ ²J\Tø ð«Ç†” *µ@’²–ÆÁEnl åBíÂÖN˜öGÇ¡cé ¼'Ô¹_¯al[iPƒ õ#VÀµiˆ½¦gî˜ØÀšïÆŽ«mõ¢N•È•'hØÙï•ó@¢%æÒˆÕ )µXà6Ò°˜|Î4áF6È§Ï cŽ?L×xKb~ýô×ÊK;Ô4Mr"pƺܑ?eÒÀ¦:¬æ?„+RŽÇÍ_Ì Éü~kœÀÊlÄvìñ'\¿¶Fñ˜† š1jGv<²×æ¬-‘U‹oÍ ¨C hÃ÷§ÚOmˆæÔ¦! `3-OÄpìš’0›2 G%ø’eÀ‚‚è0,Ñèþ-:Âr¤ªò®æ–-Âþ$œKy«ÃŠŽW ¨]Ëÿ°r+Jå‚ÕGA:¢>ªëïDtt’ß#‚f0­—NJ¾™Ç }ÝÉéI˜§ÓÍCÉÿÎ[¸•ú£ C ¬Ð ˆùÚËõ6ñÃ>ÌSP¾‹Ó O‹oÝáÆ²ŒÑÆN}¾1…F,ÐçîÆ…ûfðaØÝ»œ‰b³ã›œ+¢&¶jïR¬~¬åû¬àw)FÝŠ´Lɳ0/ïÅözË´Æ„}ÅT¸‰s™²va÷´€Ûç¡ UC³ËÄu (;45Nbï,‰çØôF›VÊì´ž†‚­M/"ðg£Íàˆ„ˆÜî$ëV1YPˆ½s˜—X§BpdvÙ»€®f߮Б¿ñj\Èãé¯ë)ÒgåÜíØˆ ~¾ýœ[ ––››£š™VêäÛ;xŒréÓ”Eñ»æÜ]ÛUÛ_$ÇêE?¯ë¢|ëÐôƒivÅÚW½ßñéƒVi@ {K×pk^ƒp«‹¼{x:àõûÐvs{¯±‡ê7Âåž@ ž¤Ùv½X•LÕÆuš3Í£¨l<`z+Iû4"-–ú -0k†A‹â™¤‡›Ö¢á´8Æ·å¶ŸØL1ŒÑºß<gºÀ«f àå§š!Ów²ÞADYô¢‚F}i—N†·)V/V%—C¶+1ñ\77«B?>öBÈüaÒ³æÇÈî—/J”Ž„#O2æ«YskgK;b'Ðt䵬Já<6{Ô^£í†îˆÐèÄ(™‚öYf}K?Ʋ¢ÊMs7bŠ1wSW ¥“·ÖðÿꨢDPE Ö¤­]þŠ=Î_ž(%WÚÿçÀÝ¥î/»”l /xdtWäfzû‘pÎ0ÌôèEwéÀž.¹ïÎíš?ëøxöïûÞZ©” †×ž²¾ÜHÕ&-­_.}{nCþe±ÇJ¯°pÿÊôA{5?®®ÖyùTº9ñ$ä^¨¸TÇêw÷Tò3¿EÕ±Øù—²WCDÔŰV˜­R^9(0zQ/³9HK€úï£êc&~u9æ´™ç|5ó<Á`,‘r]ªgÒÄ´ƒÓKLî¶(“Ê FŒuÉ“}y¨ –¨Ìê«ÒS}…ÿ#iž9€í> ƈY)ÿ²¤`ËÒ'c¬*rtE2r¥KõL·g#Y’J~MR B;EG攄.’± @Óõ£O‡ ä|j`eýÐYè3û&YO“;¦=Wå÷M¼¾ÇÓ´ þÜÿkuW5Eš½@5[uNyšã¯V¨68šZÔ¸é$öê‹ýïüJ§ë÷$]±8“ýD®¯…OGh“‚ò)’h<¨i蹇v| °eJ»hqÙbá0Vû“WØÛ_^AÈ*y¥LúF7Hë8ÈVæa÷ÕöÒ“#–cþ–@_·˜C×Pcü9ˆ!ÿÈ¥õ$X@ˆt•ò> š—]#ïØ°ë=Ú„ôAƒúÕ¯in^ã±U{Sx—Àý±3ãPw/ÿ½ÔæÇÄÒå$KexOf+1”Z6qLA«›Ýµ”LìLÇ0ÞüS¤l‰d ' Ñ# SJ|“”r—WУq…e_¤^µ‚%lI­l(r ßùă×3su<¹n“©_U €Ó¡ó{/_I‘Œn4è§Ë³Pƹ`M¹ˆ§=+"êû›É‚¯Àfñd•ÀÄ„¡tÄÝ; ÷ýÄ?Nüd…ÈY B¥Xš ZÏ<à–tÌœx8½Ý=µfEt¦b=ØdâäA¹…úX9„ŠgiÞ(ª¿¾¦uyD6)õ¤ ÄÐS>¿¬Kèì%%Ö\Y=T)+èÜðl;w§Rx½0i%¿}ÌßeòùɳÌrL 7¾U%Žˆ2„L¾}\È©3Äq…LíŠ s€œÎ8w§íò!¸“ˆG<,jÆÆÆïq˜éÔµí úÌÓ2òx¹Ò’†òÙ:¤$a~ßbúDFJºÜµY£ÁΣó}ú½ÕZ{×hµ´[)bÐzRD2G'‚½S§Tk¤mñÑ[ÌøÄVîE¶O(sâa·6ÑGS¥¡\-&Ó Ë}d4ë¨áú ³^ j«%ÎEÊÐ2uF'ŸŒ¢dv_…Laœo³Í[›Éªñ±»"j"¤*Ë¢uDÞêHL–™Ägúûù{½ãÝ¿îÎ>1ÿõæeàca,hÏ$@¡¦?ëÍììñÊöæªjOvWN$à ßr¬ ª¥‡4¡sØ…±Wä”Å¿®2¿ ìe†M@w…?8^”*ݪÑUóCr/½$Y׬—Ó•cVIœŒtòµ71Š—9˜¨ý­A±À Q :"<ãb‹tìWœ¿Êé;´ÂA+¼,lnðÌÁEÆ, MšÆRÉ.]r°êüQ æ‰'Ú½2ŒüнÇa~»Ø:ÔU§G¿ ·ú«û›u­ÂËJˆWÞB!ƒ°µö’ à93É”Nü4£ª¿ÒÕ¾+®¼ßøiE%üײc›"ç¥Kei’ï(6åm÷4qÎmM<º?õ£² ƒ%˜Íz±;V7CÁ÷'Ù÷pÂ/÷hŽö÷ú`êÛ#‚ïëý ‰Š{=Ñ5¹š÷ÒBžÜƒàìSL&õe®S?³7Èø2Oya;νâE&Æ8“×!ÅG$CE44J̶dùaY¯fÑŠŽ÷D²ì39 öÂLó›â,Š–÷ #ùxìáÞË÷â ê¿£öýÁNʬ–…“¤…†F7ZZˆÃÒ+kX"GX°øŒˆËJ)©Î‘MDi·MÄ^ÞÐ{„’^ué™%P„wùÚ!.SM—Õ…'h§¹¦¹팣 è (ÿÁÙIþ°˜¡ùÑëj X(ý—hX,Å™+7KYP [åÌý»å|3Ûè­Ó»edpSërÅè49´¿(Ûwm£Æ^D EáÍ‚‘ÓAÅ8—–:ü/@€®Aô¯*Ó¥žN©å‰ $è)K¹+&wƒ½³ËCƧåtÉ*S¨h£ð¯ûÑôƒ”I¥/K0bÄm¢µºŒV³ch¶ ä&¿ÐYÁ#“K¬öÝõî)±ëKvg¢kþöc ­²i–Ò©Öç–@Boz§ã^gv“³a¦ Ìd–U®âÃp8C*«Xíí‘þ ˜÷3ÒÈ5„;ÔùwL’3\2ÛKÚ)a‹vmÂà.,”ªžõø¦Axì›çA­@e ùáW’XN_I ø+ÚöNi ‹55þä[aÑœóleØÔçFiõó³ËöZï E™ø|¥†Hfj~9Ò¸0öòZº¨ ¸rKèyÖøÍÙJˆBŸQ™$š¨ /ðDfõÎtíñø–Î~”1«I´ù#OÖŒ:H€âh‘oÕ–…:™QËÉ–©§móY¶ð Ó¥öPðßTyÚ~ €šXÄJqjí]sôº…(“W© Æ6B¤ã÷ÉNM.QVklOgoû‡×m—a°0é!g–\¡Gµ°yÝ^ Šì ßøá|3BòR…›õUØ«óYCAü¦49e¤Œgl‘ˆcÔ´OSxu’qìæV‹lasü?S5SbeWuè-Õ/µØ¸™1Ê)SrŒ}]ÜÖzà ŒcKH„E,+ùåjPÒAP è1ê¼$Ù˜°t²J—'5žÄ…›„ ðëí¡?øÒäñ ìä(¶ÐYøo+S‰.Pu¯:ÀŒøØ¨aÍÆÇšHÒ$Ü;‘Q¾þò‹˜Ã‹BüvŒ\C¥ì+‚²a<Ûmºò®³¡Žž`ë1Ü#¾T©™aT†[Ú3Éž ˜èûŽ{´ÛÖs!úú±Y?ôAÚ%îÿø {o#Y$àUÞ µtT)M)ˆ|ýeðÕLMTâqjÁ¶Ò•Z¿ÂGú®çsôäbgDwÀ,°h?ÍÄÄŽ÷o6¾€ðwoø)/ºù88¿Nî÷¯»­Šß´„âÃ< "ž~^XE.òKg ±YžƒIЂ«“&Õü›‚¨±‰\É4kŠ©^gÓ7ÐÊÁtâ×6ÿrkSŒéÖ Ï °ƒfj¤‡¢×Ü-²AÖ¶ö‰ÔÌÑü7œêFåÿO¹B\¨.× ¥iž½*}êı|®¡i”ðÌ“I¼¶Q~d}™F¦î(:²¦Ý›È‘Ôf_èí þWTKUí¦:Ö 5Éò€ÚVþ}y`ˆ¹0šAþk:a~´êI€Ð†PÕތʒÅÅfèCÌE©Íó8Ñ› A6dE3gRÒSÃBqûeü|‡Ÿ]H?(çÄÁ3Ëã*2!žX2,Jl¶¼?oÛm65â¹(Pw´T¤Äùö^0ƒg>º0m\§¼–|˜)Ñšÿ®? r`rblÀ¾Þ YAtgY+~ÕŽ†—bCq¼Ù¯Éž8’ËÅÜ‹w'V—ïåUÏ}­gGÇmùëÂsîêæûàöþä©ggCw’b ÷_Y[ßß’χºö©¹á;D`¾À v,7eK",· leÀ­+n'b\çj™mÝ™§fÁ¥˜az‡›ô5£¼oí)K\³uűŽÀÙ,lý¥Î.@œ€ÃüKÈÔB$}[ñz̘¸º/­9b(oÍm²Šûˆ‚‰¿¹ÊéÙ×O¯W D$o¬77Sk54áÞN' ÝÎV›šŠØJ…œŒº.à‰ük‡*¶@h6WÀ D¦Ø,5j6ÏDxµó/y,#ßü¬âÙëx›¦ÅKÍ©ÖJdÊî¸ë8ó˜ãx3ý³*ÿûÚãù)Ï£Ö™[Dùݱþd– Ïzh"°Åüä ²5}vRµ>[ÊòÈY¯BúžáÍQŸø94í“ÅÞppŠWb,d%¾:<­§¿ÏEiAPÀCa;ñ’]Ž ÿLoç½ãÄ@QÁTÑí¦Æ!Ðíñ@Ž¡•{^dkÿâ“Bãvörøð<^= ê‡3>„ß©åT©Òˆ2Εž]¨Á<£‰}š8©b쳩åèø„^±ÎÄÀ–‰J§ÇÍlÀî…¸àyGæžGÎ9ìËvçJZºõÙ¡ºæ•-û °ÿô¯ÿú85óÇ mءXC0€sjP0ˆ»B0ˆ{J¯»–YDçÛ&º.ú¶—^»r0À; ó ñÉÆ“…øÒõEG{…ôm—cƒòÒî–ÙáÉç;ãÅ} ìÍ›â&¾?00à‡+­Ç; KâÞ{ñ¯¨¸À;zNâüAPÐÌ}G—q•gX®Ö¨ÎL?ŸƒçRf%beèE0yÐ(ÃCÇJÃ(—ŠFH–mÉ‚!#/¨)³=i¼êPè˜ö€R¥q.ôJÀVl³î:0UÝî®õIaAeO$¦:Lmý^‹ÆP •©‹¡dåáê1³àpZÛ¦ ´N&ßL*ù Ñ–?HóÀýT>@æÚt™ô,Ìûæ+:Øí8ï¸NÁ ö  Ñ¢ Ä,@ dsAt„kËØ~£ŠÍ½fÃd?8þ¯BXKcºÜO·ývÙ’»K¼#‰Éè<ãbR ÏÄü©|’y¸óV:zM/K ÍïofÝ 22DßU˜>„c]_£!1•„—èÏ$Ÿž€ÎdqîɆÎÕ+˜-u²pQ«ñ."WjQRÛ‘‘›#÷8!˜`(ó@ÁB~ )½ëb@™O ¢hÌzq,Sµä­gD5³,d?/v*4î?Jþqú\k±ÂI³´QjG6c¿dcÏ•jC'’^?Š*¤Vò€ërÜ×ù­vòéÈ£ %ÇèEZÜ:3ÎFå%Æ*ö#IËW¬ïY.a=ñ–MNá9Én3õ[–ÈÓ…›‹‘ËB~A—£„Ëþ£o¹Æœ°Î!¶BWî_R{ÓŽ=0•£œ!‹¸‘ˆì((HíòMmxTXyÉ“9â2‰#Ñh>ƒåñ¹¯Ÿiôí Ñêt:‚k °AR|ƒþ0C"¥°¡u“ѳ¢‡¨Ša×»ŠÃx:øã‚P8°Êö|ót=Ѝlγ]{KæÅG‚×0>ÎRLä´QŸ††™*£ËÖ¿RY¥ó.s·Vy1(DMÆ‘Š¯60ä(k6j%½hÌ%Á éC©1gŒÓ<ƒ¥× èýÉFeÆ¥Cºó4Þ¶êx^ƒJ¢aÅ8Ì1´½}šŢvé:°íÙþ°}ëМRLJTOÉdÓ~ìp¬ò…n@ä{CþÚ{}ôê”ÒÌIqvTôX&‹Î;Sâ(PéD Ó¡C4|õQ%à ™€Ë>#¹Qý#e%ÈU[ĬX3åÏô¼¬Ÿrl¦ˆ …N1J퇒ó’bós¦óÂÆ1(µiýõˆÿLç1®¿!5a÷Iš-:Dí±çGŒ•t n}t^¹3q•Ïåó¯Ò%5d†¢ÁЩMìÓ­ƒÄð-‹iIü ÒÂwGS+òõÁx.¯;=X»Y÷"9´ææ²3ŽÙ°Xß =›Tkæ)ÙJ¾ì»ô„H ÈLýáowÒû‰"‹éÏÑŸ©UóÅÄõ pÓQ*ZF´¥%›‹ñSœÃ:öØgŽLEAÐRF9b9LcÑèøR‘~E¾h3M½s£ÞŸúM5XS²¹WÿUñ" ì3[ªŠX&Ì€­’Á»9޺禿VQk«•]ÜLf¿@QpîXvتèŠùpkÀ㾑DêD;]\F0®Lõé^ÏEÖZ™Y®«°AxÜ*LÙß)A#!aúNÀ[†ôR-?~óÑÌíMEºA?Ȫ*{œÜ{HNà—Lj¢ë:MnÞçÂßÍ㜠³@áJtÛS¬¹Ó,aú€ š$;F®™Æ' Xá*‚[XÀÔà²y‰Ü{éóWûÖ4-Dò¥ü­qúáÏâIŸý‰ŸÁ~]|ë ÀpÄœ(o a?â“P¶ž)|]^9Á#Ô€H¹z¦× €©Wœt”`µ‘Æ`œšò0Jxí^Ü¡tþg†Øº™Íüè]½,^F_ ½@õËÔèv¸…ºUœ‘í-_J”‘X€ã‚¥bèGž«LÑA‹ÓCH„‰ôIf¢W 2‡Ã«'d¨˜uâ”òÀa3”búë¤d&#¨2©”I`V¶œ¹!,÷×dL¾ º\, ù±C[%S²Ïa”."Ež8¯ä=¬r"óÿR?0³DÊ~ú¬0µS-Ÿ­C©yj¦®;ÿ”Ö•­ÉNqÃâ:¾:•oÆÛr;Ña·'ãöèÍÓãRV&|žj:M:.äif—Ä%ß /¯Aj9Ж)Õõøµ»Î3¡¢mŽy-ÑeM„vÜ‹vŽ{ ñ@êŠdšV¹%˜K²D1cúi"R‡ŠneãÒB= ŒÓÅpÓÈÆFÆá«kC‰VŸ¯ ÇlnêŸé_„Ÿ¬OiÑH†EÓ“™ig8_'íy±&3ôjÙ[sy¥ÜŒ®rÄp +ëÒêÄ ÕÄ“óÖ )pfnn–FNVëYv”íLNYæZž×oèlî5B‹Ó¯ ØÐEGV¦%î¥uçzÐMö¬Oþë2Ù哲ôÒôsp'‹—.Éo²PN†O#»LEûu’óOGfõdöaØF¡ÇÈ›í’d"*ù|µ¡œ¸ˆ0©bt?™óåt,÷RÇŠ³Ö<þ=‘¦³°øÛÀ1qŸ¢ç³Àq—:IJ@î"ñÿbì‚…–¥ÁcÛ¶mÛ¶mÛ¶mÛ¶mß±m[ó^ü11sg3wÓ‹^wwefgVe”ñʺˆ8«W–u ãe£.û B±»JÓ×蔓¦›i—ujhi^œ›Ñ:ÐÍê9œÀÏK}=ÅäU¾æ‡JÛ^àÞpéq8;võÖ†~\<ÉËÉë«ÝäI¯åÙþà-Ø{Ðû<Ìíx,ó4èrî‚þvf?Ìéq4ì@çéjt|¹ÍónäºÍÏ·ÜŒÁ΃ÒìœÍxÆ/,GŽºýCJ#Š}úeRV5­]ô)žÚ%h-TuV¯ c•Ks$›3luT©ŒVçËCœò Dõ_ßyvŠFáU‚/UT¦ë2€3”ß …Ðz»~¾Q­%.ÈBcCv¿·T½ÐiGWuyÇ5–Çã¥ë?¡<àþéº^nÏc²kIO»q¾0LDQ›”Ç;l ëä&i#æm!®2ÐÀxmX÷ƒz©ïã#vWö-Dd”C=—â¥$lfŽI)·ÿIc 9$§u¹*w;X(A?kï“ß.YBEî³-qÑúÆ)úI”“„¾$áMø$_K=F*k1,ý²­ýµç¢­(òPÑ"F‹·Õ-&=N°âêjðzPb%×\+S­œMšm´¡ˆÕá&°E£/©PzƒMu£DU~}²*h›Æj5‘›Ö“o]ñýI(Ù[tŒjZ¼Èl³Ñs­ªª$´ ªz†MkPKÏ#LGíº§Í^p¼$T T oI[2œˆR=»¿è¶Ø U>ø°ÜÙ5X卵I' k‰ ½Q¢|bÕ¦—.Þ\òßÒ/i†b8!Gú™cC¯ÐÞÊQÚ²CäMk?m` òn€æÉN7/%w)Âá®<1ÎlÅÏçi$ ã/è‘›8ÉÁ«\ª^RD©oh­½ºVVYVû×É"°Ž7u6 !rêZ;j+å·AqäôÒ¦&Œ—¬~+sI3®O»uÅzô›+ñz™j%/n/߬i<ÃaƒaìÂk3¤”sÁ>‡o*vAk¿Þ9Na&1Þä‰ñMi:qÎJcx$AÖvülýöì“ì÷IJõ8Œ~Þùp¸[Žsç¾.Åä'.Վ׃¯o–mű2y‘Ò„#¯çéð¹}ME9—f‹ ¯@mwß9=­Ž†nȘ†ur…‚lû£ÔBH¬élÝb{(}5ç¦S\«\çkå65™ ]>GÑ<(¨s™¤©ÙTd¸z¬,4Ü$5TsÜ‘(OÛ×B¿•èºÙ´Îúwþ\ªž1Ÿ£sì?O#¥Mˆ ÿ™Ñ,“¸Û#eå«á¨{ªÆ’’¥ªkÕ§™81âÂÎY{1_¨¿áRR)¢j'2íêYØÓÁÒOŒ* £¶-® XD{ª²»¥Îù)ÕÞO¨²DCF6Ö§ñÑYˆˆLA‰Z³~B2)¿pÔ EúT£‰ðº»x[vŸ8¼ÀhS·Ä’éJeÄÓÓäÑ*²Íéôssñaû:|X¸lÜcp‘:1' åO¾¥ùåôyû¹ôF7´JQntÂÊŠräŸÝ…¹£ƒ0xevP1I¬5Mu‡›ÂX»+èpÜ8Ú[qýñõå´ôõP… Ù=IX?Q£u:«÷úç¹MrŠC|¾³Í?4K1S;éÚTï  D˜Ú­3 C#ÌúÁšcå¯÷u*±¯¿Ozë‹û‘G@‡ºx÷õiŠp]–b^ë=>¼°&wëùÞ°¦+ü~ò™™,Ĭ5ŸB…Ñ*Â,"±«v=8‘¹³¨vù³”B¯+“ž6£ˆ¨ÔÎÖË’Ö¹0¬ˆ>r–ÉŽŒb³¢sÀ~•~Q”þµ´:O䯉ÇDf/ Ûì~†™‘Ñn AÑaŠ„F`ŒáxÔ´J¨8¢µqþ­êr‰ù8ÅJ†¹”a”$ùdü{£I$…ûJ‡}þqIÚóåªÃS—À>|²‡LÒ\ø¯\Ê$¶‚X¬ØÔ¢¢’÷=štžì„®,³ ÓÌÁ#ì]ÆÄ$)ojEWŽÊë˜÷gäfr™2t.=aN>áòš¼•;5°ÆÛ‘f’ƒî¸-×饀Æj•ËÍj€@3(-¤ÛjnÐ+|¨›ì«™d!nêBècò¢×ð7wã‹î? 7MËõÔX!›SÑ{îv¦CÑÉ"DnRf½Ã›œÅrf¹‰QLйsÔAí)ù2&7\­—òÜ ä'ç“8#oZ ‡P‰÷NÁM‡fòYÍQ×N=xx£8-ßh‚4NgÄœ™‘ ;í.•Ì7ƒ¨œÚ™Ôºg_¬}Óq¦”gœ`Ó\•“¡þTô5+éU(qcšúþ‘œxõºz™ýŽªc’=üo‡R9ž/«¥Óô4zQbƒ0æó~V3q¿x( ÉýÁ36IÑ‹N º3.~ª Ý‚ €†ÝK†¦¬cÊ Zð˜éUÇ =+LŽÞ ùˆ¨PwtâIwhï»óo€û1InÌCS¹<[mŸ[ £6Û˜ŽÓö85Î÷Iªz‹À1jS—ÌÍÜ#;'¾‹.¹rþ‡|vü¤”«[;ÞßDýËAðñË„v1lîúµ[7 jß|ÈŽk1™ÅÒû̦Pœ~„Ôgñ1&àML“ih€ë’ƒ‹n¡ìæçY¥W8ºg]k¢]y%WHºò…3ÏuÇ— 7ä¥MdÆXiø6iƒZ@R²Ç{ÿ´ÀQ@F«¤û ÐÜšõìS˧mk'JÌÎ'0Ìʸ°[”6^*&7m£sUpЉ· ¢Jàž£;l^;é€B:RW½7)ˆÅeq±)›pÖ†/xòÜ÷ìqÈ!Y§`ŠøÍP7|ý†A©æãz… º@A7€„[ p½B¯ •È€¶=CÑÕêÐKðëØó@øB .p·wGoxe`ÊΘI.\Ù3 L™ˆ{ÈPàTîk¼—޾@Õ}xì pž˜Cëà jÔià!ì 7ö‘f¢Jľ, ƒùNZ8>Ó“MüºY¸qÆ'»$#ƒã:ÚB·ÐмêBà¿üX˜Ý­å.\‡M*úÔ²amö”E³¡jî"ëÑŠóËïÍ#ül(Gå€Cí3(+> Õ6‡T Üî& Þ•CeþQ˜ŒCô‹qû:z“0‹üÇ)€… £®¤¢VÐÍLc‰3‹j±ÆìP‘¥aÀ‘x -J}œ¹æÅ¾Š¹ŒTJ!¥s¢Ñ}ßµ‰‰iµ*éÁB'íY¼ ”Ù{"=AH—†ó”þ̰Jˆ×‘ÿò0³šF"m1ˈ`y®æ Ò¬Sµ’qs¢ð]»ªÛàïý扂lrÜÛ¶%…X+µÑ–S2ÊZÁ€£©Ö!ôqBDfÈðCZb`•NǤPrwmk2"8æž6Qjsg™œî¬Ê ŸW]«§~aqP$àSw Â!\±âÓ䵇ô2•Ýñþ{Ù;èÐýWcAŠÎWØ®e²ìMèD5€¬è#†t~g³ÍÝ“mS¬ŽiqkÁ ÛHŒI.N…ç¯èЊ¶~·I=| 2yҸŨ¹ÐÀö%rO¯!Nr4Mdo,Àéð Dò^kÖ‘ÊÚþ[HÒ°E€I ÍhÛ¸E•®v\ÇÉ„K'P›ÄÔnJËœv”Lµ-à ä;ŸèÍ{Nj'amž¤SCÑDu¥DÖ1r©k—ùÎãá›;ÕÉ0Û² I~Ô¶¬ÑnRÎê]’¥DyKÊý s XýpÙeûCb2V½f+3ɉ†¥–[q p‚xì}ZJ{šÊúþçIš)Å3¯nÅø¤Ù»—ðòØö°)H—ëÍb舨f_7G¯Ó÷û-õqùý(uQK·€±;me%†ƒ;-%´…þ¸„IA¢ùf¬±%.ø|/%[e­¤üÝt®dQ'‘– :¹°¡ÜtÑ"ÅW/èö1›Uˇp±ðUÀIXŠÊqVHhÑ ”Rû.¸¯B´»x¯dλl¸’hÅkoEPRTÝÚ÷Ϩf];º"Ò5‹ôõOZžŠ-k¡&`–Š-ÆWbýpœ[©ä¯ó2½¯b/ø©¸] 6½¯ä£ûySÉ~iµ˜Õé:+µO¿möé•£ASް Œ´tôìÑü£gtäÑðÇð‚Z(´öŠ Éó_ |ì–6¿v¾²Â³œM‡Y7.}ó‡L ÉÍ‚%ó‚7ù£X`K6á’kœqrÍ.1â{1j ñiXøžt5I4 8ªõº–ûÀzÉMûÊv•´N=ì¥ÚõŠ˜Éþï6Z‹Ã¿¸2òÈ?Ù¥e¿xï8Ò`×a•QÕ¥7xcÿ$Ž~ª¥l:)õ.Ñê7 ¶fp ûq•ì=h˜‚pÝo6£Ö ¨|Éa#«yºu ‘¸OúO?cUª2eqî˜ÿ|&]6‚˜nŠ3 [7dâ¯Ò('Ä%=öp?Óðä,mBÆMu…o÷—ìÞWÒ6Œµ¢¦f´ë-Lí—¦úXèžÙ É~M®`›ó2` påo,Â9«W¨ªŠ©Ÿ€ÃžÞ Ã^'1Ç‘¦m{ÀÏXÇÉÛ Û•$2IŠ×,ÂÚ¢3òªëùǶֽ<º˜z(©¸i.¤çF“§Û£Á&õM'ÿÚYÂhÎÕÓj'Ù¼´`}¦ë4JRk#ð§÷Ìn²#[k%ѸÐþQ›û‚ü’¥áœ5¥\§›ò-IÙjUhë%kYƳ†Gyš½Ÿ”~ "i¶ï ¼5$² >|x>=>MÓÏ.ÛÊäðÓõ©ø9Zú}$«ÓÑ×ËÜCOÚÚ'Ð+¥ ÈSþ¤‰!u^0n`¾©Íœ’·I¥eí$’“¼œ»vfYQzùÑê¡!¼1™ð<“Âåµ-#Ã@ÿvÜÚ‰c%·¯QÃúòÜ‘EMïî<ÄG®ôõÆÆlz‡«Ï°Çy›“¾ _ÖÌÓ7”WägAǘÀ›d Èsÿ8ÓH[£wœ‹7ºúÉ<éqZ}ßÝ1Þ”k8 ïà“ˆ #æ÷“‚c"ßã—…uš÷?ç:0³Œlz¼Ûöx½…]2ÍwÑ Uœ¼ É ]#h½(`3L·à\fÄÖã±aŸ4'Ží-?ÄTÄ–þ哃nÅŸ°=ŸX‡+²Ã$PYˆ„ÇOÃ>.Á´Pií³˜€TƒÉË®L*Šâ Mü°é|o*Tv‰ï3Þk褋|¥m¼ÏÄÖ°å94@¿Ñ_$S5y͉57&®Ž··Ø*œNlT¯38úÁJ»žsÍÕ.{ÜL±d\ ŸÂ,÷ÙÎz¾Â¸…:óŒ4üæm±A"ßçËÙ 1f1²^iˆaYEÛ©<ÜÜâTíDˆÇ=ø.KØvñÆL‹nÂÍ|¤„Ù>}I@¹¦Lê"h0‘žkɃҔ_‚À¶s“ ¶ñW«o_¶}''Vk…ž^¬#jéªt3zp™Sܧlõ£¯j'ï „D;ÐîÝã!sIƒÚB˜üm†y†Vj¸†×6 ­Î¼Ô÷¢þËíê4¶µÍ*ß+aE”ý"nî.ßb@ËÒ;W›-Yò[È–Ññ)X¸]>î°+hx^,‰ã0ð†)»2šxšEªà-DÌ,P£rÖ?'V—žz&Ý‹KeÑŠÅ?Jls¼A˜/<+iÔ”ã‰ÐçÙõt´ÆÈ'Þˆ JÞÖü SVÃ…qEÈ™aãðLsÆ'àm—¸·O$“¿ ‰’ÐìÁæ # ñUgÆ S¿°g}O’ý%ÅÀï/*ÊVYqOùàî3¯ÐýdIä˜]àÓù=ɘž)ØZïimWÜ!b~ÿ©Ë]øç†2j ©Ã–;@­VãµØ¥õ¦^ÞX^žT9.I0­ž†.ËW=«RªÔ¼·”±1* C1³¡rC'|Ø‘ õo—øôw2bþƒ£sFX†Ý›µV9üɤÞÌÈ%ÜÁ2ätClæÚôzÿ«Ze†­ÀÚÅ©A¬ª†;U¶Ý´út×Í/A‰Éa ï÷Ô¼4Üœ½ ¦.&Y*uÛï Ë8¦–/Ô¾ÁùÚ߆Ë \¾¨‡8eƒ Æq2O%S yžE|s»ÛŸ_ÿlÓ72Ÿ™SØPÖ•4—[]ß_“¢]A¯ÓãP-ì7ÙSbÝ.LãèîÜI¸Òiï‚"]ó³[¢g1<Ö›ëðL.nÞ ‰~µf7ø5ŸÈôMCS=Í£¯³çý0Õ:Z]OG+âÉw¾¦* ¿™¬ t’Ö²L;u|ƒÔ-"i3¥ Î,ƒY×Ò.Üú„Gü[Þ©Ãv§Š¤üÔ2gšsOnxh형¹ƒÊ=íÙq8±ì­á”¶•Î"s ´Ä·Ñ5Æy•1;vå~]`«¯Ë³Í¬I7=2ˆóuš7S5+ mo},bytè žV)Ê’p[3T–KoÕœ^Ù»»ž<Å»Q*ñu‰Æ<Ù‘‡­1#ƒ§²”Ó9ÈJ¾Sxíú3KÞ±Ëgæ"£S^$¾QZ⺠¹æ³09'~Ò€š·ü"@ÕK«YØBšZ×(ÞGo†{І*Øó)ó+u¨…ßèÙoLû”C²¦Øe§ž[jºœyéĪ!Ý Å<ŸŽÚ™ÈÚK¢wà Óubò¢\Õ‰¶~TS愺)VOM c¬±_-‘÷.§7ù ƒ‡ï\í6{çfUl{—÷Æ7t«Æä͈º–f}MÁL¥§˜ýÔ¤T·oaƒ¡g†Ä—¸¸öV¡Ê®¿nËß`çÆYrm_p8É7ö‰opמ'¨ä~¬—nï³ÅñHmÄ|.¦Œ koaÑãtÞ‰Çy-ø;çXÞ"á&‰Ì/V9Á[>3‘Zä¢ÚÒÙi+nãÞrÃe“;×mgÊLµlÃåù&[gxC¤¹°,Úð¼}8ª3¸÷¶ù€5ƒ¾B}£ÓšøX<¶‚–³Ûor$,²4r¼Õ#}³c¯Î©©g§ÎÛY¬Þ„¹gÝa0µ+ÒMŸî¡…›üEdžô>6)‚9äîR_uÀbÊXÝÎþÁ«ëö/F»ýá+y‡á´àż×óv_èV{(Ü;õö/X{ÿðÍãŸÝ_öˆÃ…xÇOG¶ó ¹ãdü9C>åsÌ1ToèõD¢Ð„~ayBù™?ng¹CÙ‚ø|çaö7Üi¥ ÿZÅJå2rÃÑô,U1¢ î0åÔ cFFþ »±8Þhø6ƒ×õÏf„"­|…Ïýþác‡VeC&8ºó¡ƒÆß4¹k+ OÉŸU²I1§q[õ X‰ÖÒ9ÇԎɘCŒÿ¸ ‰õëÿ»Æÿ¦ƒƒŽÕ!vqHÿ=%憖†tž­˜¶Î[mÝþ U×q*´‹IkÙg±õ‘;Ñ9'^uñà‰ Y@1µ§{_<”AH¤Üšåðd€|wwú8=gM”}0œx±bsÌiÛÉù¤èa2'Û< £°ÒL„Îû2ø=%í<†BÍ))D¦Q1‰ÍAd÷}0þK:PN8±½—Êõò I¤šK¨ˆK J̃¬™O9ˆ¯ CtDé¢s•žûdÍ6TDˆ ï¥D„æa@, ï@jÎøq Æ cúf2Ž^.Ï÷÷ŠÔ6$«‡îŸç7'£«ÏF@Ú)ðDæZÈ£¿ÚN Usj!hòBæ¦rø*!t(ÁöbE=ðQØthÀEÔÀ©)ÖÅï5,´Æ«Ks|ÑPKÜÙIÓ–ñß_6^œ¸±ñéìöuüÛ ñæw{ùº;:z2“/ÞN~¾/—§ŸPjõ“«ôÇåÓÕêÀC*a¼hP'à„œY‰¥͔ƴ€§K%Ði÷œòôA‰¡ß#)®ƒ+IÖ.e`JIO¤ì+ë¹´¥DÖ½os6Yß=L™°ÁÉÂ#dk 91Å‹„xJ˜LÅY¹ß õ-2·™šõ1TÉ/S÷¤ÚÅ‹{Ò£NtSâ šBf” <#9 ²÷ž êNÐ  Øc–¢÷^Ÿƒ÷ßB”•¯DsÒÀ_¬jDpB0Ù©OœÁî n!4,Dô@P§(¹±‹ÌFÎt\9©<ážBø½ ˜,NöÞ¢É] ¨DûÓÛ+D}NÒå{z>#ÿ‹ÔBײ8í4Žš¸\dL£euµÒçEW0§Ÿ°œ{=Ъ ãËÆjöàáà’r„ð¾%8]§Âi•Ÿåáá¸:%pœeq³Nž~-ÓÏøââþ>—ˆöåÓp …±”ð‹Y ûÄuR‰äœê˜¥ý’L¡f¬•¢0™‹_´Ò:n_þagTáLÇÀÖ:ðl’îO¾{Ćð«•×ÂJÇBg[ðÉLAvÞàÎvp4=¾€•°¯¼ÚùÏÓ Ò¦­„P¼î ñ’ßnê'ªþS„×îÃbÉÞ®Hzi—°Õ*úE)~«Ý©ReљDžÏîõÑvϽ§I•0™r;"²Uò¸9ôh¤ÃþÊ€ç–0î„ÐDÅ~‰Ÿ—tx±û4óbÆÂí‡YX°õõ!g°c®Œ-v·€{•P™æ)ú3«©HœÃ¤:êÃO0 ðQH K(Ô¥Î*SE9‚Å2?˜r LEiÄŸ¼žŠvQ"²H ‚¡2Z‡tD âqï½göCóÀL15yf3ï¦EI«Õb”''OˆbawœQ6MæFwÆc-PܲÁ\Ą̊§#FTÉî03æÛ’ÁN£¥†»<¨$®‡jñA°0'„Ør¬«¯„¯%ä|µž^(Èïo/VP„»Ù¹23\÷–-DØ2Í\űÑ ËOš?¢ï­uØxñúéBQ[¨Z{Û‘eÿgJUÈbç%%Q>vÌ:ë(¥bé¦}ã¢$|§i˜ÔÄ£¦”ÕÚ_å´+ë Íå‘öôúuõÕ¾vÆï‹ÿœO·  0ïOÀÂS°m=׫Vo(*‡ŒV’Š eÕ4†šÇ)ò vGUì¸ÜßÓéçŽø¼EDˆ’AÄ1uœÔ™À²q3½ÌMïÀáÅà*”‚Ӛݷ‡··u²QÀÿ6¿0Ô¶­uì„j;úf£ªÐ‡«»úk§n¯t/R]YD×À[¹èDFÞ,t Â^ÉÙ*EñÛoøJ.Ǫ-gÑË~ò˜¬ÐÅÈÍÆäVÿ²2(ª˜…!΄½ù2Ô¬Éü!›UH?BBeq˜Å4hØÏ­®¥0Ú³å‚LN[€§˜•d¨8ä &DLˆfÊ[.¸N­·ðUvˆ¡"”Ð9Íè½;ÒXNç&PqÇz]*3Ý3¢Ðo[„EàÂ…¿K—aìm`\4P…§o>UoÄ|$b]zÊ=þé‚Üv  ¹ö‚*×W`E´z3 ­dž˜ÄK9,ZzGheüí©D ‹£†X\-Ï6¤ìŠøZL…9¼Â¥Šð.7Â3¨1D‚x‚„ûj>äG ‰é.ç¥ÑJð…p鿢ÎÙìÏÝg]}*¦)óp¡ë/0Fž§S„ÅcÕIåÏv¼&`k¶—‚àAU*µ¨º¾÷ 5® æ ~]Q0é‹ñ}…ª®À–àgМ›Ñ v~Q-U€GQVèß¼Ãáe¿~)<ö•òãÚ¢=¿£ÂÎh侨×H¿x} nOd芠6€çåw_¹‘„îEêÓàîÖl×Ú}-ÒI8G˜i¹)?6Š5=j„”bÆ÷í½pg·¬Ð8úÚbמ¿X‚»ßþrø!ò„Dˆ›úg_^ÇâkW\ò4;æg¹,ÿr@6yûp7Á='‡v¦Iü ”¦à†s’â{)aúv(¤åir¿Õå)Ì("×¢ ¹“wåÍǃJÍqXú¸öˆONNì^ Ìâ&paç‰{+vðR‰{êÎåjÙì{é¶¹3Ô|Þ§±€iùô™¦ã¾µbõ¤2ï–Zòxwz…ü2öë'ª½òú±À ¶eTWxÚé±LCy˜™Ä"[Áæ=/¡ áîCXrÇáÞ ¬<×R+×ÅooŽô~œgìãcðº†`¹øW2â6ÔøS ¶Ë"uCáß 7–Ý  àE,£µ%ß¡\ç.UL)diƒÆQ!ZJtëÞК~X€?33$±yi×{GçZ4xv%ÔÈé_#A_ÙwGÛ+8LàÂQÈóú¥htD‡¡nù!xþÒúß”aæèÌLÂ0‡h(€¶ÒÞÕ|ZÏèxž'v¶s·Y û/ö/©­SÛt3¼æÕ‚IsåIªKÄBÐ6©÷ ( ð±Â‚«ö:ÈêÚ[ªªÌÊ´.í¬¸'‘>âJZ‰½Áâwü³›ËØZÓ†ˆw´žð"Éç˸¬lt`ø¢ìüº}ç\ÐÝOaBÉÈþ[df<«µc£ŽÚ߬¿Ûº¶É´)S )£ºe))œ¶ð >:Teȵ›¹“ûr=cvjg„\OelÏC8\!E@‚€dA^"%á{ꌾ;õ‘Ëíîv3b©´Ôöv›Ëm>7ÃyæÞwËñ—ÿ°(ÏÇ÷%¨[ÂïùÙ#–¬Œ¥óÉ,B B›]dvî)·â䕇ÞÚ“‡.ÒM±IªHý %B\¤ŒÖÚÄàƒ.(ê"&j«â2&b+ãRU¤†šë¨ˆ­Ž2bä)µIòDa åR&j«¥ˆ"Ê@ÌÕ\Ä–­ëTÏA¹é\ÄVŽoªGO±¤§³].r“â =”›d%r ÙH=oU.ØÞ§B†ûž¿4¿™$¨Ì)6\ûr'ÅïÁWìQ Ó+2æ”B†½µ´uΊ¥Gí‚Ám¸œ¼P‰/5jœ†­7|ªÔ¨4ŠU7¾¥IjöRvÄe”ïØxô&_RV† “—€(÷ /wè¥Àn&Œä âO 7]9Óå0̓‰jJn<„{"©Ú㉾âPm4oPœs8ìxÊLº Y–‰F—›e”…™'ÿä£ÖÁ,ã ÔÑÌÃI¹ÌC¨œ{<Ùãpæ!ä–u6Yä€î!tHóxÜÒð/].ùÞ•bVBÕ —*3íbÛd$ÇâÌ@*À8—µ§ D©MjéÊ«‘5HXc½íg!½­ЙLoA#hÈS¢à16¿£»."uÞ[{Z›^·^_ë%kVõL¼wýHµü"Ô©Ï•K±‹”·i×*¦si§ÍàÐy?X½8ŽžGD2úÞ~ðýúÌ ×OÏæ)ÐÖñè5_´Ÿ M_²Vd«UR ¸<t‘JÒèlTñ‰ lpEÜž½8Š€Jv1Ñ.²X?IL.Nêœ1SPvØYDÍ K¿2KAìKØ;ß 19=›4ÑovŸ¨Ä%6ê #AD,wôao.Îg“ªÚ}h²'ˆ@žÆÎýðª×>9­Ú¬SQß%dŒëÁïè/¨[<Õ&Þwœn’¯ÉÍé§öÕ^ÏûP4ݶ†9E+â(®øc«ä44F°/Çm(›ÍqÝÖ×NÇ™¤Ž·.m“û¬‘üëa^'ž³L ö ÊøZû¥žú§¢…iC¢ß¸24O}7È––Á`Á®ãG6Ç#ûû¾T>±Ð0|.7Y·8œÚ„ÙF¡uƒÜ3Šô3·õqìò˜dj,f±‘J¬d–üI ZŒe1ääe‡µ4˜îXÁ#kŽž´ \”ÞuGݹ†drO´Ö‹3Ô,{ýÇZFʪTš­ÒÈ›·Ž“Žju¹P¸J4ª}õÕ8Ü3]¡>„~oî{þÒ@íð܉ iMܘ kÉî\ƒðnFx´’ƒúþÞqé“È Ÿ'ç>DN4/3ý!¬壓“ŒÓs.¢sÑw#¡Üð•´Gë?ÙÞUפã¼lžQCé,³eGn?‹ÇL ·tm¹%¦‡$úÅû°#pPÌÌåÌcZ²MÔ§ß'_1FgE ¾bÂÛðØ{š^ÚÉ_”<ÛÙñ±šrGÔ[ Ù&S[…«š¶ª2u#‚§>.‰MÀL3™äRôÁý”¶çÄ1³‘ë}öA77»€ÌdU(9à”%ØÄyZûÄAhÜ.=ƒqEæküÈCó½N×íy*þ^øé‘™ä˜$£?»±ÈY£¦¶—®l]þ01øE`~«(‘¤B‡0•I¨œ”aQ МºâÕPZ€q,¢–ÔÚ$¸óÉm§’“\‘Ž€R]Ò¾¯‘7 VÍ (⟦/Ò&Ãn˜§JîòÏ•›5n ®t:‡ðñ"]Êh7Š9‡ŸÂn°¢ìC.B}ÆaðEU˜äqÛ7Фötረí§LR Îÿóv;ìw±}¶T\…J"eÀ¢ö"ú(x‘`ŸXx2;y Òw&U(uc‡‚ÜRÏœErJ³DàÚU@4 'ØŠ|ÛlÂ#É¡3Ó©HÁÕWÌ{ÜÌÔEÕqæøÔÍ>ŒøpûK•HfE»WÉÑÀŦ3ôD8‡ñ‰ßü=”Xž/§%â¾rmèéç1¢ÁÌ­«ïÉ!¨SðÐ&3¾hêSý”L“©£áqañÛ8ùOŽ=JKç€Is-%@@ËÊoV¶Ú\ÐÔH®HX¿›È»y;)\>œX›Ð•fÕG´Œ¸wŒªRŠù¥`tæÍE"ϱ%'W¨óªoWO³ü­°Ó¦m×$âÀéd0œ„%{¤ÓSÚ¥Ó7&äÊ‚…Ø!üóòr>Ñ»–žCj·—NÖÌÞ'AÎ7¥Æ\eÌpn…R > ½ X B-Ïhí«Kb{­YkQºª,”ÙçZ2l-sÖŽtzæÈÂôÀw)/«…½áP¦tOKª±=Ài”Àøì¶Ê™WR-ΪÆ0·0´È+¾ã¤²§²@y²ä¢¼LQB?=õÉÊ^ž;Xb&û5S5M+¤DI¤Ã'Ü›“ƒÄ §?6¿BMüÇãž[àþ¬ÊÁ?¬(Ty¨yæn‘$̰Þ0G¦ÌÚYãÆ…ëáüë7á$¸µ6š‹ë†”P(bY_œWud›"åvrfJà.2TG:;Û®G­Ý猕:iTîkl¹qHIZ^ðòÓg¿w„e3 z3 j]•5‰é ]bfƱfûê¢èÑŠ¾‹ÊYOªÖÜ©‰ÆÝ@ö7cLuŽ:oO8‚¶ÁMi¤u ×áÓ|8 ¨›øˆŸl«û,«lòô¢±›æï]QTÁÎÞb.‰›vÂôÙÒè.° DuŸ+ @×£Á4Œ«ò#’ìË+,L;¼®Æµûö_¦’<¡ª¬·ïQ†GU´13õ@,­WO}Yçæ¬n…0ý ¥—QÐFxãž½U/œùv0¨K-|wi(¥»*ÙîâM¼qD— U”+Ù­Ç$ ²;àÙÅÁ£»ž¡}^Ó ‚:/Uk^‚à46öIôý}3éY1®ÉN6¢ºÍwyͶeLÈ¡B©›BX(¡f/¢]•žûºÈ…{]õù'Ÿ\{±ñŒ£Mw¦PÔòºgNÐ9¶æÎ¥ßKÈbÌR–nÔR$zÀ¼\!Ù_º¸nKèx½«Àñ¯ñá!-ã~Öb9Íüñÿn×? ç_ÎR‘º¿VTNŽþ’”á=küýô0óÑ.{ÔÏ*>EsyÑÆ^ùÉ'ÿ¥õøŽ´¶ÚV»°Ý™,þÍyÙµ£ 9LÝMÕ@ ‹ÂxLzY]3ißE·”·MLmYC0¸0¸ŒöQ¼Ãxcø”cøµÉã‹oB0c > !*I™„R£‚jBQ›‰†Óe$JGJ;YÇͽ²Xð×Û<-Ž]¡×ã"×»ïÃl~_-DÊ ã[›ŽˆºAãq¦eNËœ!åC.òdj`úw”üP¸Ò©2‘©9ˆè8óEs55\pnµÞ^Çc]Œds™:Ä…¤÷´•:ÓO’‘fVÌ´wàÖGUǪé\”ÇLÔȪUñƒ3—w¿ÚýmyqØï½x³³›¡ëEüèÀ….¬}½9¼§{æìèkR½w£ÆŠ<Ì^Öðfyææ·*SðåÒ÷ïeT´úe{TôOõêû»‡uüöcy#ÿˆËÒ1æŸÌD–W]FÌŠ¸]˿Նþ:kZu"S«p'ÇÔrD1k³Q™ Óx8ZeÅÚhúÅ´&Z§C6Çý -ŠÌR>Ats’ÁB-°évžN¢F#'Z  F=â8Ä[Kg~/îž+:ŒÅŒS«ØsÑ¿uÏ63¢B¨Í뫱æà¦Ö!#0ýKZúîm¾KOðàQü!  “!Aí'’[¾½âtþþ&¨çeаM>†Ø¡*=ãÊ妡~ˬ!†#Å#ûIÙ÷Í{(¨$r—2(rÉæf+õÅEyÇî$¹ú¨I‡Åj•ê~¹êÆêÅ[)«äÆ”- ~økθåœD¡QbqtDÖßÖv׫'lûèœ<“–F ëÁ¬ø’Ø\ÅÁH#Lš(RD[T"\i·=_tºD„§Œì G¦ûƒcc蛺ó`>ˆÅIT¸ß³Žÿ;Ÿús}PË7i´Ž¿›Ü*ݾ®6ÞÖX*‰ ,ëÑ7Èí¹b¥›áÆ_áa0™1øÚ4‹Ã©É­¹ñùŸùMÄ 7»ÇR’®ùt|ÝðNœµ•e,qâÆƒ2_8s½ý¥y#‡ZX6OråÎÇo»k÷âŠz4H…êü%Þq®¢SÙ͉—i¦1i‚4)l ûõ´ÊV­­æ¶¸«ý7*ÍþVZ©x¹síã’Ró%µïH”ÉÕIj œµ.òÄ-ãf*(*ÊøBô:¼`ë»ÕÖiçÂ5—܆]à(/ §5#kòËòŒj¢ðXOs<ºEúÅÊ]‡‡‹T6Ü’çV©Æ»RO=—OûÈ_Ÿ®ó’ ­yÉL‡D"™Ü·k5Q…ä×ÈÛ_£ç„›`Yq¯ EÎÈ­wívÌ%S~çñüÿ§€˜PÊÖ_€„@þ_—ã^õ;lQ>cnÇë‘Ê–R­‡#©È\© QR¹Ä@Óå#i3w›úf~3ó6I gÕÛŽý(ô}D˜:(½ðg³ër+A Œq¼yœùGÃoªÃ3àï‰|‡ûûY€óSg°a ˜èZ³¤¤´™A¼®'-€˜ ô€Å.AÐ!*—j:!wÄ!Òs:A`»Ùk Á˜(uÐ[*©3•Âñìð;@<`ŸEÝâ§×KÓ–™?dý\®áJªƒ©ÔË"+Ž‘yIàÖîöúoÚ‚ã¸Ö­Í½.ëví×›×ò-³5¹îÖÞÿÙjkwÓ²‚$ýúzÿÊÕv¿³ÓÎõ l¡] ˆS•вX|.Dî°ÔÈP~X2ÆW°Å ‹S”YµIä!pÇp‘F¡HšW ±`’,Ä\ðÍ¢‰Ù¹S ¾’DíåÕ±®×dÖÂ.·‹–ë“;Ê$æ©ø|W,xsárݬ:µ°çBŠTÓiµåLµ•¢]¨NÖ÷J(ù#x®÷ÜÿPbâ—ŠgÛë3Üó¨¦üü){ÒÍ‚ uý$R’e‡æ°K ú÷Li»)¦OŠBgàù¬q œ[Û,‹ÍµYjF•l¶Ïr©Bô<¶‡|õëͽªº²Œ>…‡oYŽ‚7&EEqX vÕIšÞ7ñÞ9“¼â4é7B±üƒ”ƒ¶o«Ä2‚‚ {­÷ ™¢W‹XI# Ù£“×§oVO5\L$‹—-Îè< Uú™ˆÖ1èÛIñˆŽSåó½ô§p/â,þ€{ôbã×ì8ÿM°¯DÁQ•£pÞ—L¯ƒzì¥U¹Ou Ç/ñµÿÎÁò3d1ætdfìœûÅ[ÖwLõ†ÃŸpõŽ çq%Ä»™P·¶2é$i¬hkÂ$é°³>€C±—ºæøp2<»\ÿB9$¬¾M™ìþ>|U~µ­h±/ñ¡Èß.€¸îQ®GZƒLp""è×™ÌÒ‰*w|¡ß¯u(1{Dñå›xNítL"ö/:ažpƒá•8Àð"›¥mw®fMÅTµÙ:=’UUÖÓ¶»°¼…¹#èLqžŒ’äÈà‡º‰µmèRF¸ƒA–ÂKH°…(õ*à§YZ¾âW–¶‰–KÚ i”[wtò ¹Pòî#Rå[À`¿<<%oæïÛó#*NïqÀ}4Y ©¸[¹üÊŒ69W4™;È:Ã5ü0¥ FÝL÷_tÅYoÌTŠ1§bzS;®zL^ô¬ ÷#èf=šP`ô'¿ÓsaŽ3x8›‘ŽýZŸv:“^ÿ ˆ’Úß\&¥Uäy1TâºW9uÔÚ_étú\GÌ«ì—M¯ˆ\Žë¿.™&ÍOIM‰ârïÏÅ”òчÐIÞÞ o«ŠÌ‰n2(t'?Žpa¤Ä”ÀZ£-ð1õY$–÷E ÖQñ'åœ*^àç¾(/>?)¢y Ö‡ÐúIb‰-ìâ-Ïù÷uÏ!™(-=_ÿ¹f†•œPžÔëáI.Wv õXij6‰r.åa&Òi‰ûò^ï½[cC½…Ðfƒ˜ƒO¿ê±¨ ÈígÕ@©ÏÊ9ơψGp¢C[™XWxt‚D„þ·aÞ„~ùæSÚ(ˆÇllò¼)é^Jw4•A×;DBì6íß­,¹@xNW1Yv‡¯Â½çÞSŠÈÞU·¬ÏÝ’©¶J>ã‡q$ÜO%¢¯ºäßãñlŽº¤žôBq\¼ÍƒÈ ðb Ë.2¶´ž8š¹££ü» ë• –È–ZWy޶³Nÿ]ðd4!¸UÑ a³2áÒéJzÈ­´>Ò–ªˆqæ·vúŠ7¼ñÌ4«`Òš66{Ë$»;el+a¦ÊØ6V3?ϸԦ=‘/[¸Ì\Ž$etË剴‘ÜÀÊ”Ww ~N°S*ÙóÍÑ5cSᄀ©¹Q¯¦åNf[YD6Ü#<ÆîVWp[ϸ]¢C§FÞhˆáuß]<šôË'ª{îÑ]ºÖÈfÿ䮜d°HwyªQÔ¥RµJu(³ú¹"k×&œš®!Ka‹[íz!ÄŒi+Øó~@0 ±“_ã˜jMÁPýÕèE  w&+×ò{gß7îgþr³–aíÈÏ»¾áO51çž(fƒ!™î°zä÷—vö”p­RuMÑÓþ€þ“ %óVÔ‹ÌBþ7Ãl ,íLþ×Ñ”ªk¿Å†v÷B —‚ízÒ‚.¼H8ÐC2ÓúŠÀMeD`q!q(–¸I¨9ŠýºçNQÛëÝæÑ×@ûpâ÷DxIÃ,½u+©Ú~v6¯7€šìÚöÜ#PKz‘Ê:‰ ­_; D™0°­vúÍ\7³/N2îä‘—;LµäH™ë7 ç]èÍðOØamJ°’h÷dÝKÌi«EK2™ØejÍ7c‘Ä8Ú½8tÇsÒjò3­~JWbx\þÆ÷ˉﮋ.™×Ƭ2ëJðü´>=}­zöEì¶}+vð ^±¼k+Ø&“<»l¥ Xš€ZHï‰íÑ­¤‡iÆh–Kr‚.Ô)o…î½ØÈ·ä(T‡Î’@ׯ Sò`kÔ¬3³q|ëäµûåm—ÎέúæÑ ø »C§¾.mëÄkT½Þ Ñ |^]Y€5|°;ˆ_¸JCŒÆh–-B 7@'º“JuÎ3“jª° ?¦˜V©XǦ̸'H·Å*å®|Ð7OåŸúEv0þkßíø~5¹#{6'ì|ÓyŒTìÄP¡Ù¶£Å󭱬™î½”9h“þSÃñU‡Ž½'£d’ŽôÜS,‰iBXw2ñì³)Ò×<AÓ4£cÅôƒßwÇ€¦\OG¬b²ï&S¯ú“Ð+óžrxaôºÒ@ ºÏ‘‘å#çÝ¡ò>T¯ƒ÷Y#¥H„Ÿã¿ÌQ­hµO/§ásà’™`â¼…Ÿm®ß2xÑËj ·©ïffh±êENÅÏ/ÆÊþ4‹mÏ|‹íaãPÁ=ÍÏ“æ¢1&H(±?§'QŸÛû '­ä·‹…[W®à÷«?˜ >ÜØèçrL mH’&—)«ðeo¼f<áTÞAK“`tjëw#;¥ó±j9ª½?_˜3Œûé}ç:4¸ `~‚øºECuüqçÝ6axyTZ®ß¯0Í|ŒcþØ1&8rñŸïx–#ä–Ä>öÙ1"¹½g@ºÛ•ÊÆ05Þ ¨†ÃvÄ¥ÈÉ0à“D›Áoy—o >@=ׯ@ûÕ½ÜàÄ{ß©½6Ÿ{‚ÉZÊ#J¥ð퉙ònç:¸ QíÉüÿÄŸeì) VÿM»ªÿç.Ϫk'o± ¤]_“nÌÚuR(Ø,AÊ`3øp/(yÏPÜôA'áÒÆå6š´<¶üP~cå½Ópœ­Ë5I£yh¸³EÊ›hþûè»;??üÛ¸ù~øº¹+¯ù|wðJ÷E Â7‡4vž‹ã<š2ÎjŽ`>i.Ée|ÝiW‰Ž×^ØþÍ Oº ò,)nØ^;²Ï•¬8­f`¯œ›Î*FÓÉ/œÞ°Ë`|†÷µÆG, X(Ò®ïÛ©2À±'¡¹fpÕøk[WÎÇmÏÜ,Ç-s«h_¡C}Í´R.œV ‡—¹QÔÛ+Н¤Cµcp%& ÆÈ%’×£|åòÇŽoÖ(—ãQ‰àÆ¿3…–Œ•ÌE¬Ñ‘s{m“kÕÜx]¯8¾tù-μê^o6“îHªD§mSµ.®¦'«ŽÃ2ÿ!Z>óåèJgºz³È¤½vÑ×x«&ûvË‘· ÅS_ù•®<úröæG»ÑfÕ–R­˜6/|v/ *jôÈMú)M:åZñøg¨¬´Ý£øÔñ"óî܃Ï2’=¤S—»ôóÄ2:º97O}¯¨ µ/RQ B/ /Ú¥Ž-¢a÷ü….ùå+Ör ‘*šN¿Ö¢™ÐñV¼®Å.ƒjªçþvÅûý´>Šû`•,éу•wU[ñ¥j˜jÂl÷Z¦‰uUƒW Váïª3¤‰'Û•i^ ’Çpbr)d^Öe*µÿŽüž5Pjge‰ŠgZ‰6oˆJ:`H¨'ì-ý…µ½e±+ä|¦“S÷ ÖDŒôE•a»k‡;Ô~ÓþCÜï{À]Äx¶ æ\øe¸ñÓpíW1ÕÜ!Pq\Ði KõíˆþÃeÃûgÿŸ¥¿=O{"€ í¿QAíœÿOo¼V mû#64¿·ú}*è 1\ÎÌp#ÆPü%p9Ds.Ц@é”ÙX+Iª¸­9ÿ½»s#é4ÞL[ó—‰ãÎü;¼O‡!:×>F§N§ëþõmËäTKŽÁWpLHSÑ(Tèám©†èñwõ»˜ ú!JToôœFBD—4öO@$¶.Ì=†R\êšKM"åÁk§ñ£Ä¹G'Tȶ4;ÕªÊP„kZ!zpVp߸úÙñjØ2ó_?&Íú5@úXØM¹¥¨9¾m=½ùÓµr†“ %CZæCþP×FCžp^Dý©”úL¶úؼe“»M òU§™¹í,l@ÊÆ–¹ Ñ®A«šày[ hêSÝ™›mc~½û3ã‰w—¯ûåqð׋Ãß_£Wo^s÷·ëG;7×/oöá5ØîŒŸ©_Oîn,æ@ÁXYØ xbÒPÅ”æN\`$]LgÄI±{iZè>›JýE‡ð“!“%%&Ú` *æ‹ju/9ÁY€ÔM޲›×Ô?5ŒD)Õ¡.'À¤lKÔ¯Nþö/ïà¦VÀƒÝˆZÏ?*±n$ËM;~°ÈÅ Æ1¨¸c­ÁobºØÅÓè˜Í¤k@46»ûû£ï*$& ˜¢bÉæ7ÛõBŒ^|ˆn‰R¹D6SæZ‘VŒèÐB•¥Ðƒ…(@Öj|mÛá4‚$Š Úa‹Ò4ÉNL›V€ô3ò¸¼ä)4kÃÔ»õ¨þ‚¾Œ ò¦ÚPã”B©c$ÆA³>Gê;k§'Žæ (úÙ !êîÖae÷°­¤ý°¤»‹ó ‡P¡ . Ûð~‡L%Fðë$j­ÉØ8…9ÿ'1]ëîÁL7˜Û+t–iã ÁŸ÷•ˆ&Œè]%(”l”2v †ÿxdÉÓM¯í,¹ÑV¬IOàÃðÒ›”Gû¼Ï\G0ÆdÐéèqFìq3„BÕ´…-p¡ž`.w•·×Êóçúøý×%h ™ò¼´Fñ4\ØŸK{{Ha‡]¦|ú忇 ~··,ß/ÓÇßÊÏä–"`nÀg'¸Nåð´‘…‰Nÿ~7Ì‘Eœ›–þiÒcºKFøŽó„Ä×ç fbsÌÿúýn¼}N L}×à9ÊL†F…3í Bsf®û]-¸7ˆò®Oh„¨&†ÎŽ5»¶Xƒ£)aHÈK²˜Íšws"¸KÌ)Ö ÉÃ_ø„&¦Â{ýÞ/Þ.Ñ•d­C5ýfÜÜÌI_‘M«Ð{Hm­ÁhýäÊÿÜ $Ü÷…â¨|^a3ßµ¼“=Ú|†RüÛäIGBNê¥gºíp,l­NFvï UÔ…‡7Mž»TRB©Í&°õV¦Öú¤Ø)¡©kTqe/E>JÓÓÅMª˜bŽçâ£DF˜íq¶_Óz³i±wù¬Èœ÷™Ê58û}7h6üÂó…NågÏ+8`‘-{ôz^«ñ F~VYV$P- ß½3Ï—ŽÎZÏ£ã€=êÀ(Ã![aBÍØ4Â~ÖÆåßUM­x—ÖìVC¬…•¹ ‡e:Žò‘ .g)UÎ"´À›`ÊPõk8òä¦sä È% y Xírá|üŠÕù#ÐÇEÙ´:÷%¹ I|sÖ(1¤õ›¿NÓõÍ4@7U#U2 `êU Ðc¬k⬋oåjg™#Œ:¥Ï¹¬ÖV%òîÊŠ!Ü5KîV[¾c|zX ­TX~j¦ýë"½¼0p—ú—úÛÄy²+w·ÜˆÅñ pbh–®|–,ë.Äiw’ê Ï\¹¹¼>i··ä9KL®ÁeÝHy6´Ð!Öñkƒi->íy½Èï•Ô*Ý(©‚žŽޝŽýg9ƒWi¢üwq! ÒLäÊ$ÉŠå­Û¿¡|X¹Þj TìL#R¡ê haÈ!ÿRÆøhô@ÃæK’2-NuSeB¯ÊöÈŽ^À+‘éÜŽud¤nídƒÕŒž‡Xƒ‚“Oæ¾Qôzm‚™qºÙ<·-µ'Ø/' nðu3ÀpÑeÚ üÊ«ÑNˆQ»Ó¶w“_–»Ô³Ö:Oïz™§®èws›.%›*„²ÿZ—ãþÑ&w毛9×/†;\Z¥óZº €f¢ó[<: ÍŽl‘ÏÂdã•Ë]ÑH—–Æú´Z¼´¸ýÉüÄa“| ”§h¡œ¿%!å¿ÒVŸ;eäz¸*—ŒoHU‹mÔ~RþÞ±!½l™íÁ“Qd‹4XÈŸ?Yrš¹jâ‹ ˜8ôOßšÎÛü²}§æè2Ç®‚½ÍàÄŠ[ÚÖÛ‡'*,Ü^UÎCŠpwÿ_=ȘXÿ3}õ57„¤0MLìØ…X¢èüІYZK„òn¼€®\Gç§}6§´¹xK-é ÃÛâÇKoý °´æP/¯íH½¿˜69 „í­8Èø{~l` è‰SÌ;¿õý’†wÔ AR(h¼ÿF´þ¿«¸ñ¨¦¶óÖê.(…R åÕ©µ](wíhæJI®äŽLy²¥‘èòóPWÁK2H ‚@#´tÓh¦Üa8ßs!üÌŸð¯dþ§þçRûkN3€„”šMû ‰ZwtoŽó+÷^žnÖŸŽõ÷Ñ;ߛƯùГ±Ç1zÂûý Iþ)'6¶MSŸæŠJ…Yhlu>-•µè ÕU½RéàW6‹Fÿ Gš,šžA2Ù{¡ŽaÑdXÎõ5¦±>Ó«¥?ìøEkؽýûU‡É*¨„߆wEÐ}1K¬²n'ؤÕHx¯îO\–«·i§J=Ð|xÅn?bÆŒ'«:˜Ù­ºƒÚLMÁHFeQEF¥¡ô·Ú:sp ‘á—+ÖûKµŠàÇÒî¿Êu¿à¬@>t çÄÙ©fÿX&ì™~‰LY²¶Œþ¹<Ç{teÁw`Þ`hý#ß²Vn™Tí¨§öʤ+:E3­Ek¦Hû^Š=B£k;ÖºÀ'¼-ÛT<Ô÷ÛÂÉ:AX úsr°0:‚p¡Œg©¥ØßÀ² £¡²œ_$^ɪ*©¸,ŸÔcœê¬-Ûé9¾ƒš#örnà8 )ÆMˆÆÊÀµ\¬™UÍŽÁ¹ë‰ƒe¨lº†âÂn@ eaŸ;óxM–©¹*‹‡à`ÒýFoÄb¦s4Àiøì0±ƒïêùüv±¹rÖ×%a©ßoþ¦ù¡ÿæ:'œ8מ°l/½i[”Ò(ö¯Œ[ñÅÿÙµ›— 'j)Öм5b‡Ü Ø^åaÊ&å!üÂ~Ÿ³!ME®Nšln@+9ž‡žá©€÷ 4‰/]F¢Œ%ÿ‹,5`¹R3éý‰)µ¢‹!.%jtË“s¯‹Ù؈/MrÂÖ »Ð#µD ¼âv/ã®…uAÐ1Î$%j…¥)ŠC t¡Bñ¢xŽ·Ã\y‹7éö Æ{y ‰T3o6"è¶P—|Å0;Dw\§q°%ŸSI¢¤E“*T‡?r9¼¬É&!z´ŒšÈ…¨´-@(c<^@ãI“’ÉÂÓBARr¨¡¥ €§vq¯y"ãû‘Í£L/ÑbS¶H °,äJ‘xD ñ3ßWj&˜Š³Ãíu_ëü²Ã}“Lƒªr'R¥ϓ9¬@–^éFž |ÞšlŒÝì Þ1ühk´Ëò ®€5ŽV?pÎÔ?Õ;€íƒýð¦ˆa¼KÓc&S{™´¨ ~„Îë¯h@̧¯/މù¨ÚÜ 2ªè‰RØ(M¯ìŠPà„(Š[ÙO­Ï÷Ü)v½ˆò؃_dÕÕ™°žÇ—1äá²§B ÜÂÁ•n®•Ñã­O…9Á>ëÀ»Ä3®`‡´¹& rìÝžp‡¾?’á¬Äž ÀjmhìUÀê…|Ÿ%ö†¹DQþ˜†št˜`ó‰Èõì3DDZèã*=êM“œv«#E8 $Ÿ`ìùï‰ìZ?·ÙdmŠàqž‡-%PNDL+xk3Ã:)›JtÚ›4õÆ?cðupÖ»!?È)•UáfRt^çq΀ð³…ÈS¯%ðÌG¨™ï©žSWeàÇH/%ô‰Ó¯‹–+\ h§ì%Þ[~¢Z³1}ñâoôšRê2FFïy«ê½ëVJñ½ºHhÓQÜ[ZÞR¶J=®µƒÕ¡ô™ž]á«Þï¤ Èµ"8ï³A®sŽôN‘x§(âÕ øÒÎÐÈ DV ª{¬%Œk ƒÿ€¼¥íÛ§Y`ÉÞ‘@Ȉ àHd‡àÐK^ µh(NÅ,žõ¼+\eÍxÅýð•ÀRûu§GíÐÂÄØpâ¥ÛkH ¯Á­2ߡܚ¢^x"l{n@×âbÛ[5iR¾V ·Ãj¥‚þVŠO¡ŒK"(%l)z4!]¯O›¿þü¥—j¯Nùš[[‡î$¨£\}¥@[ɳ´õ‡¥Ç\íkÅœB‘ëSéå$󄄘Q2»4õ³MâDäUÏŸ âk‰Q”Xk9L%½¨4„Šz^cææ" ã!y°úMƒ_7ºhá`rþ'·’ÄW' ø0ž tÌ„"ràà8R ýPVû¤87ÔbÏ*ú|–pb°îHæX‚¨ñZP§Ô:¢Á„zHpú…\0eÜk÷ý5“óÔóQÆÝåþö'ç·{‘ñG b}@¿ãFð¸«r¶b`w(’XWš¦@¡)ôœ *§s¸¬K“%ˤ É ·§•W!ÔJKwDZ2Yã~éH„3N"?A !‹ ®„fñþË+–o4PRb ÷ôj›Ù ‘b‰à wíùãz…\ôè=H ã*±}"×ÓmUù¥|ù€ü°ÃGZ¢öÌ9­6dOóš¢Û‹š¨¾Äy–lpNðA4 ¶`ãBu ÈO˜AR§U§æµy2NŽd˜}&mê•..X-Òkm¶V³áMIG”Ûg¾sÚÓ#SpÇLn2B ð1?Ò¤¾éL´IKw6O×Ï(-BH|zïm€¤( mfN(½ô2zøíX" ƒíw¥Ï±nÈñÑÿŸ{òsl2¨ë²ò%]ÈuF@‘rBM|,üÝa§±Î Q'ºà‘„½5Fœ¤ºÿy¨n¶QË´Eþ¸Ý&àã Ê«9§sÔD´/®,Á4У'׿øÒ.u_‚ð:[´Ùy;DÄ®O¸^aS’ø”b#ð07a†Úý&ÆçÂ/È]Ãp\¸‡¹Ði˜%“\u•̓u³5^Ž­¼ÃPV]ššOo+ŽÉ‚£ OÂK#¿©¹p}º{À·E2þÖ<ø'MHÙ Ž€n1©Š%€¸4® ø4D.¶›l˜?Å «ž+ç®FQ1ÚeZ^¯‚Œž åÒ¶,DÖFzÁ=’ÝĹX!ߌäüÃ2‡¤³¼ÎÅÇ8ç´¯¸G$í!Óäý”e$<° €ƒÿgýƒ@†lsib¤ðA ƒCÿ¼d$³¼t¥™Ò£P8%ˆ3œÏÈct1Í}:Á#õœbiêßc”Òõ{qÊ: 8"EmÚá¯-™]{hxÉwîÏ¢c„8Ž9ÒW^œ1±÷JÑá­i$dðëVÆöØ1õ­ÉýãäØ^=EL ÿØqyÆ7L2xèí«°ÌgÆ7Zò<Ã.Y¾qPØÆooßzêÿfgÍW+°^®^´vjØ×ª]wtê×°†æ­–·«V¨e=.S_¤ és¯é6a`­ºÚòêrõ% ]bû,>n?xx6èØ}[XT7ÞfÑ]iüWÚËþ}ÜŒ&’ŸßJÿñ=²Yf·†W"°f;XÓØËBó/Uxç·A‡‘ §4²Ïüü§^„<)J Âúß|Ù;˜Úý¬9Šmý?PóÛ´.Ü@((ň›¹MñY‚£•4ž„\ÖÔ(Ž„!|°HCý¯·ÛÀÃãÞÔ»,ƒQÈûk·w‡÷ÓÙ#HšOrbÑ_ipâŠ@ r1ETòJó “óôA dYS|{\gDgd_õšÛi2íí˜æ˜Æ0è›bMžÇl1÷—®Ì_9C/šäIë uƒ3ª’Ýø#™î#ò}²™€ Hcƒ¸ú Ø@ã1eæ"I„‡°Ò$€(Á)$¦A<~’èðÁŒåCdR&q2#à0´_šB@9 þ`m$„X¥¨^3idØÏ¿QƒC…1úñY–â«~|¼üL½œ×@ƒÖ=TFŠh½Y  Â]ZÈx¼Ø}ÀT=Z¨~®~¼\ýø0 G?'?¦~ýìþ*'÷Oð—‘ÿ ¬ ˆéšm‚Ÿ³+3ž«Û¨fd Ä:Ìd¦ÇɃ@Eö"%‘Ò߀ÈQbf„'ÁBòú#€æT°h[ü_¨SÊ+]ë&‰g·ŒÙ¬žopõ$èûÒꪵÓ>LŸek+Òé/lJš¯tçƒ/Öc÷w·xe0Ðå°‡ã Ü:'jPt,t§Hn¾¬ÙÇ%ò2±eën/v~‡}|]½<ýü$„M@µñBÚÎÐâ ÿÞt½}œ—yð®¾=¿ßŸÉŸï{Ÿú·ßÃÊÇûM²ãÑþ=äþßÒíåê æŸ“Ÿ£§—‹ë¦e«–žÄ ý"¼_×"Í:i×pš¤ òl£®v“¼„Û$Êöa`‡0F¹Æ’¦à0ðð¶ ¢X=¼^tJj~÷Rr ôƒ½® CY ä@Þ¢Óì²<î^6ó½Uÿ¾Ó’zåN/4{Ùä ›*$èˆ+j¾S|˜/¤ƒZu _ë1û}õP Û!J'eÎ"¢åñ¡ÒÏDÛ±’—¤„b&m ¬uiAÅ„:1Ó@ŽAÖvæòHö–4xògüzTe°jªT½CähÁƒÉB÷Säy¥gÉŸ¥DZxIpgrçûrÛ'ÍÃçi–®‘¯¾&œ •Á2ƒJÌUÝÂáxG‡vÁ ,‰ ÃרTº&5]7F Ý0%4ö¶ð©TÀ×4æ{€GÀÊ?””~ØnZž¶œ§þÒÝk¼‹• ¬MG¾À¨²Çû•¹Öb•DC§Ô,6ij‡³…Óòd4¨n<äìIõs¯fηÉý“étΈ{œþ~rðn‰2åòé4±nÉöHÐ ÁüÑày¡t/<ùbh^ŠFÒç È¸®vìû¥ìÌb¡Ñ¶êÄ”¦YT»ðo¼ÂªÓŽ”8 œé‚nõ2ž3ŒÏpªO€-ùdO›ÛpPK`æˆrr½* ‡7WÓKga@݆yªD;ä03Ùõ&³nì­¡µS ‹£”Kg*ÝÖ§ø9WqGÃÔ€}ÎSÃeƒ ýØ6›‚kÄt¸[^¥T h)TçטÁtƒ1†ch"špª×¸@¾ÌOMí¤“J/·˜ÁÛ¯„ÈÚ4¼ºZ*&”y˜k•Óšê·Ë! Õ»3 eê+¹ðô`§ë˜ö1º12nB=ý%£P©[2ë€þ/‡£PsÌjëçàSUÐìUVÎðí#M~ÈôòÙQPe Åª„ܱÖÌ;f8;(Ôó±ûìñUÜž#sR;@’N£Ôöá_ñí±YUP›Á;×+Z ¡UõBá+k5è2ì35c¶W1`…·®H¹ôa%x8³èfëCkNã2̸˜­×ÅvÎÃuiŒ)É¿ç¨*#,2Û ¢ÅÁƒå¥¿]>m/pºužèëTEª×ÈZÖš8ÕÊÑ5©Šj®ö´g滬Ý–Í–° yìo…ua'ÅB¼Ü›n¸Û9q0Of!P«Ôþ¬¶k±i빊œÀ‰T£Îp-Õ%øHà*7zÛ·Y±7vú‰Ø$´6­¢SJ AÜAÙõŸ#idè‰ÏFF¾¶•ºeº¶Vmž5„j97tæ¶Þi@LÖÉ\©·¶‡s•颦 •"IòÈZ AâUÙÝ$ÐA;0v“)…׃(+´Ç{% máç7qÉcëÈkU:ª")¹ BDÓWVK`löaáíb€ðRt/O,(…¬g*»î“)d0PÐÀÔ „»jÖ5Š•GG€ßÜ:©íPÂmjÅ›JµÀÿ^@HØX›‚"&ÉðëˆöV¿l¤™¡dY,L¦k[kEþ©O‚Åþ[jž’eõaè|1×Ë'¯’)ÊWóí/k®ÎnçæÐÒ ‘£ù`´I‘tM4…j"„ l ÞÄIÖì!3c2² •‚womÍãᬵډn…†XŒTÞß{ e0:H›—*Vy|„Q9ÅñøñòrqòºèããíâM’/l¨Š§ß=´AÍwæõXþ{Óï©Ìû}ºyW:0Šw$€11GG_#«%Z<É.¶~;>ÿ|öúãõýÿûèùÛ&Ÿ÷ÇåógöñÍû›ÖbÌk5¤"¨MxòΙ3bû'5á*jG0`í“Z=¿ÅZÅ2ä ±¿?Õ³d…Ô`Øz³zG|£ªÿHaâR4%<ÙcO×üûèÌ©T§A€1¹:Èòçì‰wõT"sNó·`7¯€*(¢Vcå…r7ÙM84g2|5‰[~Ë¥Šµú5ÁM¾2»ÔÄØ°Ø1è²g¾çèêHÂ?ЄU­ÅÞF.öÞà­šŽF¿"zo&å÷3u]É»ö´T9µE`Vì'T8ðz.\S}|Ñ;2å7-ÖÃTWÈþEÓª…õš’G„Ñ#Ëój.Ž2 &cls^Ñ™"<ìO$ÑUƒ4÷6,5 Doƒ¡M¸jÒé‹è~Ö¶¥ÓÖUWµc RúÍJPõ«$tUû­Tš€*Œþë1É;Önû30¦çv°°¥‘èw#]%+3‹nDÖk½º˜53wµ‘tǽf‹³>ö–Q;×»%¼¨P¸Üó&i‚óp… 7çØF‘دœ-©ÛK5'©@¯XÓážm? ­Ùˆbˆ[ÜKÏ̉´}½ÒU¨MÝ¥tµírÖ:ŽÇ-l‘‘+LM†¦Å"rÒ*º1ãã„©ž†ûVË{_h¢œ‚kB¡ìÁûÌÒOd±¬sm­:Z„ÕªZsµ‡«Oûb5C6«^9C(¢‚u§Í]$ýÈ9ŸxæÒ£óMvã qÙjl,±´i­Vpú}.1þ%è2/Ûh¥}œÅ*å¦ÞÍ»ÎÿÎî&_ý¾Ñj•¶=Œ´oK °©ó„ÎêäH‰¾g´1¯Ú¢¾Ão[-6ºî÷_€­Þ´»MþH ìDD¶žìïv­Ó«|B;Ä®ص‹ÌT‘¼èr ™½r·6 ƒ…ÚéŠ`>h[ì­PÕ¸”màc69šÙ¾Û‘‹\ 1µ²Ž Ñ/%©+«œ?Ïè8½VŸ9ØiÿyÊΫ“aæÄ¢ ÀSY¬ M ¥x•<7ñ„é"Zð¹±ç™G™§€!‚ÑϞ˰&è0ã¥&¦Á§-ÐÃ,HÑ98{ÍH¤h‘š«€ ê#³d1y/èfVrùšEŽÿ¹….éIöû6Y2,’j¾ qu5zá+WÌãÁù׬äk´sPC+Áælv@Ô/Õ9¦zá%;%…(M8.(l\ZX€Zý:–ê,ƒ»A±†}©K¯`Sû‚Bûîþl‡òAéö£ûY}ª–Ys¸ê¢¨‚™g°™edõ5Gê(³ b›*]¡UdS'ƒ±b© © ¼>‚-V»°(BZU¬ò(½ûàZ]žš½ÍèÎEÙñ1˜®W(BMÎL¾fxÏ;·á2û7ÁÏ0?šSâf-_û[¿2µöà{û °*m€sÉ2U:±‰¿ªúVtIJÈ‚†àXIc;™²÷&Ç>M mÑ8=Û¢O-P–Úð¸­Öïo±(FÄÂE.s;Üi>ø-v‰Š+5úÜ%@Âîpfû'¶n(ÕmFyWྙ¢ŠàxQ.yè(u¸»’$¬Be0íÿî߇~j¤®gs¯ÐHN7îgøB½pxL¡/º^œQuíGSôE9ì*6ÿ¡û +ù·3Ew]¯mÚi‘ßáQ›£k …÷ „íå†÷„øñcžÖËÄÒÁ÷qc¨L£Ø)͜ޖãJó¸²ì U­î«h”¹ Sƒ[ΚS¬1“eìÞO¬¾¾§?l<Œ~h>äˆ2JÇûH˜ò™C’iŒ€.¯FN•&‰"ñF$ iáMØ Aó4!릟­h¢—ÔZ­×î¶-ØÚ QcõAV\mò û –^î.E{3‰óÝaHãïÄÄJx¯V¤Ÿp”÷5Ú_§ÂìÎ¡ŠšÁ#~ТÅ<ËE¾»…¡mñ›7˜øŸûìÜÉ×~jg‡Éß µ³X gãïçˆô&ÀÛMHñlgà¥*•<Ødÿ£r‚}ÅÔáP+7ƒˆ}6sø*MÙ!Û;þ}#m‰Œ/–pEœýTYáJ #æß§¬lùá³r·Ê­Íâ-î»n{u¯NÐÞÉé¬b¬©[µâ³4€v•kÂ+w{u¥hzŒä¦Ë´u†¥(…‰Œ¥˜£çÇãs­©ÃüX7òÌUc=R%œAçX‰.ãƒô9=Ô‡=®L7^<®—4Ißà†ê2W½³Z$Yfø¦Æ¬¸\Ñ'h)vsVš ‡Þc±–sÙ’~ïâòEsÄÅÂÜÇ~qÏ!‡¥?†×úÿF$¶³ŽwQÌyÿË×ÿégµµpÔµøÌÍX–bÄÑ¥Ól8ÛãDþeIæp€$š¬E‹¤¶âhõÕ"•/_ÚͶ°,óÞÞ.‚Z@ƒ¯k½RUQ¼BBU(òN©áxl’W=«ö<´(ô(‚þ½åßæáiº) ˜ÏãäòùÒý}ØŸõçLþ[|˜ÿ½_“§Eü\|á~†é+.‹G•!d[êFÉf®81§¯<1ç¯@5'¬„È)^ÃDç"6r&±¤%5­ÓÇ–w4±QKNS´ÇDÎ2ÚMFžvXÈFÌB>ä Ï.±R²‘‡º”¬g"¹ 8‰ñ,”…ò’äH6ÊÞ£– ñ zg3gx;f¸ý²ÿh맨ÄÊ):ú™³©À«Q RϵmXŠ]êzdjñýRø< SK«ëk}žt¿éßÑùy—>¬Þ†~Õ»0ÃÒë_*ýyÖ]ÙŽ{{‹Î¦õHìµ>ì‰â"¥ï“sy@¯“±Œß¯sS‰^c¤^£¦&zÔg“Â;‡Þs¥*Ñ9–å(ú\7½³ŒO”ª‡<+úá ¿b¸g#¼r¨ËA{Q'ñÍ¢=“¨ÏAxGQ‡\3ë~Ë9ŸÔ4˜w8É­›w2Q_;ëpÒ_kÖñ^?éx*­«ø+ùHmù@ÝsÈCùŸ?X>\$¬¯t<`Þ>Õ4°ú½Ð ó_sæqò§ž…þ𨟆òΧ¢cdÒ“Lè ¼µÓ;}-ù žcGÓ#qÒ4Äâ.œyñ)á@:¬ËÁÑóäÅE„u(|°eÿËœ¯Èsß”+8»ç 5ìÈÃÆS)Ç7„Û.$gnÏŠ› ®Ëš]ˆœ+ò·­3ì>°³±›~àN¬GÉ7Š[°´£Êí€P¯\¿ìín .²Z~á—¯ï7ëyµš­\³>­w מí:e¶oõ©? Ç=œÍ’ÏéBÝ\}-Œ›l|Bð¡:N¡™Lq5 Âr1ê[v¤²Îe¨`õ¶ù7D»v5ÈAQ,™Uy+æÎlÁ[l+K…(AdNZ2CÊPOát+! WòGAÞ¥äâ|(!´àöDä#+"bVd‡QÓML¤öl«°<‡pÞߨ±Ï›?–^ˆÊÚJ*o}­Î»æªÂó,JêbÅ1K¶?vNyî@ÔÆ­¨RX–;¦_·ÝÑXUÏŒÝ23aźæ;¯xjçøµÈh¢µ/‚¨¦¨ 2{Ę¥K#3ï³Ö‚*–]vÒÜDP«s"t±Ö”Ѧé‡qB"’$Æ CQZæëö) 5©Ä2Òj³’3QÕ ÇÛ|²~óc—IZyŽˆ…Ù.ÛoÇQ3Ž$¹å– gÜ-ÿˆÍ –…¡¼å'Лˆ°Þ~±1Ö‡ÍJ`xV§¾F8:³cã률£c(c"_OÞDz\#ƒªµViì;Pæ¼aÄ"‚öÎè¡%Xj8×€^ÝÍBù[Yò½ü°"gÚ´h㥙JÈA¤d<˜gÍi_Òä5$—?y‹r=,ó$pÓ•°óâÑeÝzöEï꺎ôVÅj„ÆLåóôvÿãj«o«wQåñcÿ÷gãOÓìÛÿ³¾s•ñrµþ@r·nÿfÿ~3nû^K¿Ó£† _š:ÏØÞî£í}ñx2E8PµÙ Â| ÅèúdÞ¯à2Ûà³b‰D~.¾a%Ëó䘴|‘ùÏd¹ßC °‘kwÒ[ºaZ| u½6b>ÕN «0Á±$S~äÀayåCD.hÆ<‡ÿ£¥uÀLqLÛ –ùѱ‹…}E&Vî{ÂÒ7¨©jÇ-[vö‰xÖí<À‘…½êÑ÷·q¸â&S0öÙ¦¹u³&Š1©ëé__*PùÉç”ÁÀäY­qy¹pœêÌŒy®ß…åÇn¢›M¾ŒlÏm®¦ŠXEg üÕ²ÌrÓwº/zsz¦Þ‘ ‹Ü$¦e €EáV;Iv¬î+Z·PS§9î C-ÄÈì9íC¦ÎŠKJ yPv HãEe>;\”™v\üÈCÞG?_ŸE<ÿ#±£tú)±} H™Bó?ÐSíNo9—ðÆ‹ ë¸hƒòŠä·?,š]3Ò‹ûÊ0F[oh&r;MyÉMÜB±C–ÀûÄÛÒQ{)A];!”bJ¸óQ3¯³ PYáò Èè´ìKn¸áMâ>ëlà Ð>üTágøy>K^0‘„ï½_æ"åMúp…$uY&ZÇÖQª¬£NÓ5\€¹p¥X €«†1Ìw]„1ô?6CfÙjÍDÊâ E yÐ8ʽ*böÝb\- ò]GÝxÓ ›®n›®fWJCŸíH}êÚ;í/è"6ÁÀÎ` ãÈ’¢}èˆÉâë—€[" I`´\<3°2“ÈÓDù3¨’ ÷î:r•ß%3É÷1Óž(©¤L`„*h Œ8þš)~_P:Òãà×Êjðÿ>¾y¦F¡–™Á q‚ÐHàÁsêÃPóÅi4^€â±&ã¢VM";[ÄNd•86ÓÏ¥šÜYEó¨–jöp3kÒdëÀ¾‹ E¡‹Qÿ wò¡Å|Us~ü›¨Ϫt#ÕSÚlÅåÛagÒåd–Êõ‡¦qúÍâ8Åñ LŒÄ¸àPþáó™„ú(HZ/܈‰);ºPŽd¡àùŸƒk#"Î Û*%=2o°‹E~g«¤vÌ™é5e¹”>RÌ{² ѕ똬Ìgl¦6®¶©¤í퇪ü®ƒ/xhŒl¯4DQ D©ª6ioé…×GÌȉ0“'Ï>`FÒps¥-b@âŽã:]©!”àDc£4ƒtèvqî›ïl¤¦ÙD¢`©aY|öë„0x²€(Ñ\¬ŠAŠ£z|8.÷Ñ6¯ƒÏqSfI™»aI|˲N]Ù¾amöc.v*Ц:zd[lªµƒ×EU²7÷&³ÎBÆœ&ŠN(M,ÕãmêÛGf®fÔоCr/2ÿ Ò°¶ò:r¨Ð\㟂pl¸D}QܦÈgíGì‡ðŽkäºÜ¦É¿ëŸÙ—n„Ü“àà>g~åMAJpJ˜Äü¢eÖ1mRŸØl÷(½”,(K ¸C°àx:ò;ÑBd–¯ê'˜ƒ’wÞ$)· ˜!~&˜ónm®´B㉵à4¾‡ÉL©›`tšÑ8Ýe`€•yDËnòƒlÎ0Â?c déý.é=hÈ衆§°[+’æO?<»¡êð€Æäöæ÷— JîŽ841ô>n R¾‚÷ÅØÛ0˜â´Ÿ¨t­ðÿþL‚yÞ´t×ô!J†Ž©¡ÛÖ8QHP¸®–v[óhÀ"n…;Ã^»ýCï§xÈ” ì\6Ò''y69kÎ0Å ÖUnðãF}¸ï,i?I_8Gíþ§å&>ºHÑM~ ÎÏm8Ó;ﱤ˜÷¬~ràÍST‚$cÊ5"–Ø ˆ‚zœÜ0Éh’ê6v˜²´˜¡¡ø5°M:ãûµ›«Ø¢·ÀÙg›Í,¿þjÑd{é C ð]ÞBžäEH.èAîîQ žê‹Un‰o°ã(X²]8_ž/ÆÛŲ(²Äšr&rÿöPb2nbøí 66™lHCæà›¶àÅ„.Q·ï²O ,ªÄÍiE¦s—rYbEXžwPÈÔ F+˜¿õFó?‹B©Õ;¼ÓCÂÅ/á®ÓZŒºe–s¤GJë»~NöÊÄkHå®Ë•b±gÅÆíkr‚…ì8 .l_FÙÐéª Mw:ÎfC» ¡l:˜Š§€F$B…Å*çÃVâ•wÖg³ËÅê´M2¸-»ÃúÔÅ£"Ã_ï¦O5›½õÓí¼¾vÞÞÂ)»0,óÇ/¥ÛÖjMÖ•¥s+ü…@ï´³EÎÁÉeýù]Ôº_U9êWÊ;ýúu‡wc½’OÎ瀒›ôîôÛ’ö-ˆu®Ž\¸â€ÿ$¦j,Eýƒ0gˆpE'{t!µÇÕ­ І†_¹e³ù*pÑ:P¡q`㧷ܾS“ÃmH\Ì·pÓ°;®|,ÔY#è’DZ‰èàS«îJ„Ú´IŽÊ.Æy:JcÛOœ¹²z7ì¸Îb 0tš%kÎ öªbo>ï¯Á!ßUtFz­º)Bm1ׯ£a0zª×òÞ!TÂ*œW v/Á3W¡×H&2Ñrqß¾ÔÏjÉmó†cŠñ¸Ìœ+S2î±^ꨭÀžÑôÓº<¹9›D²w bpaÈÁãîejªêÎûíej¬*ÛÁQØþ¶Fw_ž­fïÓ`VßTp^¿1.Ëìg+5{L*5»š}®¶iƒ&Š×-)æ¶öÁºD`P©l“$ Aàp%ÈfŒð¾T'”-Ó+P˜t/{‚ßóåuó­Àœ@œ^H”AÌÓdõkºÍá8QÜO”âÜi”#FiFv"óõLÕ¥“]ôu¾ª‰ÞÜ™™©fÜTùy%‘t'Ooæ‰ûò —”¿üS6¤íT¨O¨ÞahóÆó*]Ð^ -sÌ/¥q8 VøÎv:J‚¤XÁr‹†„#8Yߤ…§Id°¸ÑiþÓÂ'„'ù‰7µ;¬&›ªÑ^Ì|‚¯Ã&UÎ)e)þ[/¶²U­íÚoÏ‹I§ÿE$Ð]‹··šÓJ¹e¸·P)®Óá>\Ñ{> ¹ÿ^÷`4ÇøÁ·¦W;„öOÄ2jC'ó~434·(̆ö]ôV‰ÜÌ´Žé>BÎám˜æ7Bb¬ uäSjoÎ~ª6õˆÿ=;Çw==àüpMÞð„0;xxÒ]nŠL —£¶è¨w!0Lé Œh®\Ù$›a…î϶̳ áZžmïOµÄ²ˆ¨gæûc 'h€3øMiˆÅZDrÜã$iy†*©7·'œz’úd)nÕ~êÔ/AmÑ£¤»HÖŸ!°fD0½ƒIùäPj÷_H¯4ÎåÈcÁáñq)á'/Œô ]øñ<qÁ2ˆÓ÷ئÈèô$!g»tˆ•€Ä>bªæ]Ê͇½†%]Î!Û»}iȸ˜)±·ùŠÜ/ð|ô)$‡–†AF6ñÉ ¥~!á¹36e+¦ì òülUGEl#iÉ–ºÎIuyDf‘±}u §‡K±±¦†{Ö1IKÂ=q©š+áÝeœïê;êV©#Á…µ„×C§3¬ìÌÙÖÌÇ4ÖÀàA¶Ò‹¬ Ï29F­‰«1á¯Äˆ!¹[SÏØl¦†ß³‘g€AëŒî¸ØülàŪêwJ8•4ÒRve\x4½ˆ©šÂë¬f‘ qSñ¢KŽÆt%CšÇp0û̺)s9¾‘)9=¹2æÅ Ü·Xš}uµ—f1¸ q²­  (1à¯}vúH£Pz_²SL“¶EìУ¶M‘¬b/Qø ¦—5)¹+ßš¨ÿ;Ïì¥y3¬n7ZM³×~´· ù11mN¼0ÖON½Q¬qM”®¾³N4s®ø[9Ǫ´Q«éh´dܹøáÁp^¢¶DÅôóŠ?ú(~$ n€F8!È%5²¿ø¹+'ŠNDãgc ™²¤Üg½Áí3ýŸÓa€¸.yŒé0îÌÍHÓùs;ùÉp:¿2²}tP}ÞW ñèšà“Ö^ìÖ.t‘@RÄûoàûBdqiG²!_EmßUÑ͘ẓ–OtùPÄ÷„Õó9¾N£%aŸ‚—’œ »B@jÛõºE (¦R0An¢#“¸éØ\â+±øÉ°‹žx8º1×ó?šªF좞A‹Rï^tl^Äk¸Ý…—Z·3 =LN;0þY6¾Eš<ê• g"%cß®©Ógéƒþüó²iNÏã©c2ÞÕÎ+òC­›+´¯úþþ¯ÏÅÕ ù»:(ùЯää˜1KÎ)O m«ðq)òª›Qш{Ü ‘g°¸-V\+ôøCömž3b·úÛ}´C![jùÝz• ¥1=ùHt¸Ôà ‡¤d¹ñ=ßk ¶rè:Éí åHúöªkBÃ$o8î:VÞM™OÌÑçõäÕ7l<Þ«Èø‹eªŒF§7u§ª ̉²OÝ_6& “¹eøŸüg¬Ù§¯_>ÜÏ9p½9ÈEqj=)´Ï<¯ 12äHøÓà6,¹ƒA¯W_[£ÛO®ûZ]Ÿ³ïÕçîÛÒûGRÛ2drBU7¬òJí›ÐI²+ýŠ ­Î?ŽEáŠÕc•à謋 ­é¯fðM º_}™hP¬2×WÕ%± ûØ­¸¶ø-V²GB+³ QXñâÔ×ukÀ9Zñ«æýÁmý ˆ‘ëX^yî¥ýWkéêÍíñgݼⲕ íº>êÕ³kÑϸ Z‘Ͷh †Zô…݉Á6DÓ€ûân•THµ)¡×(, Þè úñŸ¯3Õµåõ²·èBÿ“؆æm»V„¤ž´˜£Ê´VQ,˽þ•Þ‚Ûø—”w‡¿(FlF=Ñ¢EãyȶŸíðI,ðJÜ~DIo)þ¾ Á\¡pýTͱ(Å›Œü‰ˆ¸¸i§º Ç9g“ùdF>Úù©L*­åhÌQ¯éÁ$¨ïÍqn85‹]!=½£¯‘Òº [þˆP{Âdn ¶W[œ|&^Ò/y,&°AhPð1àé™øÌ¡ôŸõIÙA¬íúl5Þ¯(4.8Z.aôþÁÇŸóá¼Æî9~ýhS0* 0í10ÎJ·rÉøïlº—£ð‰’̉x›÷¶9;ÊîåEÜèKÞM Ì‹ŽúR}™ è‹õ2?ÔQÖÄ ‡VÅþÀC²(m »â\‚Ÿ5Åo°¨»âw’[nµëé?È(~ÑÌ(~ñtŠ_¤QýB÷+ÿèCò`¾?3øÂý{`ª¨Î¥BT)Ïßøþᣳñéµã3£ïSY¿¢·ÔÄ•?jyµ(–.ªåî‡3éQÑv vËg~ÃâuÀØ„n0 U@-Ð CÖWäçH¯â:߃Ú.³¯¡ôÙÄ_Ù”óJñ7uöùÔMïÃÅ‚vWæƒìäøóc 8ñn>»ò(’Õc²™g ^;âeÐ>]o÷Ÿ®¾Ï¹ÿ5¹ôg`Tñ µì),½£q8pýQ1get'&vBr ^¤Mgàÿi¼cÊ€†H@ÿoWîN.ÿk¼Ò°µ?ê@üÍÓgT…Ò°Ã!Õ¤q …Ë2swkŸ$”ëâ0QL³ÈFÏT‘¤Òswüû1“J³)—l‡^ÁGšÌñ<ý å é–¡.6C£ûÕµ„íéÍ‘övðp€H¯Ò±fÔ×ðpöz=Cû(]*]š"uŸ$“R«ëÐ¥ü±cHÓrÌ“ANˆÍKI;iJ"rF«“–³p’'M¾êKþS4ÑÁÊ&u®¤ãÐI¡Ñ¾ÿ4øÛß$é‘”#$ ¢ÀýÁG4¬¥Òhb•0-dÂ! ̤วǤ§…€ÄŒ¢IÆ Om€&' xPPžNÈ"ô´ofÃe¸cø¤©¡ ''¯^¥2”Û¹ø<€œ8ùªžü2VYoÜž ÂÎú«FÂ’Í­ L­+«0T+`iè<õ0C½è^ Òa€²Œ£èdP‹¶4åž«jÎÞ9§##ìÊð@Ô§';š`䋵÷ïêë•GggGWo?zCîè œ¾ªv?ÚzxAíó ˆ®Öß·?ªvÞ?§BîOÉz'íL<;n†<Ã5›¢6‘ʋ̜X ã¬_ÏÖáu¤¥|Ì’6Œ™'s¡QZO ä#ófßµóÓÛ •~">,‹ICqHËÁ#$½,k5½$Öò0Jù5å+‘gÀM“" ÍšW$bÞÏ×#¨¯¡1Bë¡8O|ÏJWoTRú—óò4(S}âl3… ™ô„Y -8ÆýçÅÛé¹Ú'“{ €HåNC»Æœö ]ö\c0†Zõíâ|up×ð{¿ðˆÄ‚ª´Î Koæp`Lã´ 8ÈB–Ubxa_ñ@àÈUž™Ôœ²”#íÒ‹lלŒZ-›Ô¬—N¯o’;Aµ‘„,º—²–J¡ìn&SE¥­†FÉJŽžJAsûóäìÇòmw8_]Üç-ñ»òg\ïq‚•k®¢¥R],on/?ßuß_$ª³d7ÐÞu8ëyDýK=o“Å2§ <O¹æ%²B»öA#Ó°kºíÌ£òoem¡…]}°½÷— cýeÚÚË8¦Â%ôî ˜šôúÄk¦·•D\q…GßjŽÆ` IFué³l£ÚÑÛûã¥ðkgŸ‡ùŒÍ¶/ÆúTöz´f†´—ͪ5‡èXft—òåå™ÝÔ*³~QÀÆ‹#`¦˜Âš^þ™[!kKj:¸ œ·^°­dbWûýܳuûóÕ„ê’Z|°¸í KD/ãó¯0ÛhmI/3¤ŠüÁ/²89Të0çJ¤èĘÎÅyÛQR"ñï~¤]çôÁ×%GTV×fÕµŠzc%ÏlhFÂWÚ£¾u‰‘¨0¹E.ÝŒ/ŠAñ2ã¦l¡”Ã$mÆÎ©R(ÅVå×’òñ&ï?åaWü]}¶h0/î±!4®[ÆÊAãAÁl¦„ÅáÄç9ÐP!Ÿš\MK«§n,}¿ƒïïûùŸ,ƒ8 g¬:Žƒi 4*[ޏüº<›#¦rؽœIмL÷ pUÙ }RÖOØœŽñˆåÓÄ-E7çKË̘Å@&Ønw„ÜQAÿÖLüØ5¡ëÄ©m»<åµrÇä:/+Dïî›ýDßï¿*!㬰˜Í§¨¢ƒøCˆ«_ ÎzÑì±K ¦‚ÝD£¸Ðw—¦méþ˜}ûþÁTÓ*˜øbiUÔNf»°#‹ƒíK#Ü‹q,ðfgýy6ÊÍÖ‚þzÄÄÁÙµ¾P9 `e#‡&Šèlê FC B ^æ¤í¸zjLÚ·]žÏÂËøäN5UÌü÷ –[]_‚—>Q¢WÙjàç0)×jnÓ8…›¦¢þõ3‰xós(»«Ïó[ä´}Gª©òö‰pìzúÚÆ8=òûïT¤yìÈ3ÚHnÞp‘–¶2]\zÈ,"ÎÐ_Ú<‚üÉŠJ†ü Fw¸•ú½>ìŽS1õ¢\P£!æ5»K¶ì«,¶î9I–mÛ.‘½}çÞžôm¥¶* 6\!Ú*ð¨w,@ 3í™AÂÿeÿ+ Ьë¢Ã¶mÛ¶mÛ¶mÛ¶mÛ¶íoØöØÿÌ>gí5Wrræ¾è¤û²ÓIõSõV÷æ(§8Öà›¨mö6iÛ‰ü²×³òÆ[ÒBT U¦ž¡,' |#®r|Õ èéûg%ÞÓŒÖW¥ikÏò÷GGtIPRMnÔ6K,´z´LnX¿ûEw5œW×<Ûßí¿š¿ðÑ©-[D–½Ó×9@uƒ{ïÛ\ÀZ’¶¿Z¢ÝZ+*„ÑïB=üZ¦¤´Ä6B!n ÊžGÞƒÚ/R?õ•s0ü²òc.| ü©‹B`ï –5¿H©ãOêCë–úç‚ô^p ¢IaIUƒòb-û)^â§r;ž¶ñ7¥„ü¦å«fWùÖK…,±*y»U«Oæÿðw'Ÿ”åBöb¥Œ¥`³ r…&ugØÐθÿÆåzj™(ý$ÔØóÖÐ g_—+} WDãÁ»,#…NŒcó"ÊRÿÍ:èúó¹—@iZãQeòUì'ý2¢¨4ûT ©gïËUt³öÅýßUçQéó¶(€lÜÿ©êü…ñ¬æ®Ž8êmæîêíåxR©„ (œ1 îvw!ÃPHÎdG”’A BÓLqæÞ®æìœM™7i´¨ªœ¢¨ê”‚ÎŒŽ¢'LG¡|ÇþE梺ZCËå çî²ïÜÅ“¯o—êÇúÐõþŸ×Gzà„?Õ³{ôï 1•ЧZ‘öíbkTm*cM”]kŠq˜ëb-ú¡M”]rh[„ù¨…œËYHèYTÄÙ¨…  úy'ÕIÎù" ëC$|è{ƒ7à±M»™DéùþsójSEzE±÷/ÉÚ]}ñF„~‘ôåËvo¶EæË·ÀÞksÂW?wÚ$ø­wÙdõ~Ъ¡PÔŽXVŸ5´æàu¾†Í)“¿ÍC$m³Žï¥ÈE,߬ÎÝm „yù—ú©èÆÁTD¶a·YgÅgM¤C5ºhÇjÖ-ºšHÇD*ºˆÄ.Ý£KõЮ]m L»“²Ýp ÷=¨/£„]IkÓ¡ õ‰y=µB%”îðe-¼¬²î9˜ºGc›¶ê»²nI±ëÂæÛOŸŒ ·DrÞX’Ì Ü –ª¿#}O—§¼®íîv¿W(õz½O-+´ª•‚SZ¦õq£`虨d o|zŒø CÕz´ÑE¬/ïiñá1„m$½¹Òº¡3–²<ìjc/Ö‡äy)2ožlëþ4™Ò¼¢©ÄòÄÈ~¤©EH­ÔX/ZZ>ô *w27·a‰ w\Òdð¼6;WÓþÝ{–IRi58r™$¹*rT©œTbkþÅëû@h%½¡] jµdÿ´Àn4¿^q´YlšPÌŽ}ã°­Û»iÈ‘ØVdJrÉáYì²Ìô‚$2y¼ñít5BùOßÌF/¿tÇ`èWL5XwÞLÓSƒ;´h¯½¦ñ¶€nV/Ã¾Š‚l¥‰s|ÕQC”ÀŽmT 1 2ЊЀÖJƒ¯ú >‘¤i¸ ,]jëëÕQYÒêríÖÉI|!䙊¿$¯ ÒR‰’Ì}&GIl@rˆo÷5¡€îŠ˜iœ¹ó ¾ZõFV¸í~‘ÑÚ¤¿ùÒúR&E&§BI²÷W"G½@‡_éuõAÓÛâÓñz™ú0°8_¨â:N+v¬'?L»å¸ÛãK⇔Ã嵈ä¸:0q2”áf ±%`¿œâA¬Jþðnþ2—kÐ8ꉫ–3èükéë‚Tb®CŠmi¤ =ŠFT’ÞÔ† *?³Wãû‚ØŸ{L“N…)’8N‚lRW&~%Å…wXÞþ\ÓÊó]éæÄz`F†ÝIœTÆ?ÄqÅÓ+ Y(+ŒÄ*œìq«`ì`Eº ý¹qýUQë=­¦ª×ž—¤^+zmiíf<ó‹V$¤°ŒH(™“,qØâ†[Ì á/xhÀ%WŠÁÐΙËÌgA÷<Qœû@œ}Xœ ›EþPaîlÓÙn$#‰º9° J)r¬1-w›GC, €6îÔDWP³’ ‚ ÒNFá`Ñ,k¯Át [Ó·;ˆ põ«€:A¦b8s5ñIÒ4€„¶V!Û^„&ïË "$>?Å}óà–pLE—|9à'øj¡®; ã°Ðe€ïáû«ûrûøÖµÍ®omÅ, «]_ǽØp£©°á9’¥%Nc"œ-ÝeÇ©¯6œSd7ÎÉ4›.·"€DÛóp9-ì¬]À[¤iv‹ÀÌ„ ‡ïS›©àÊÂÁ;.BX•¥.nµ2 0-.%êSè-¯ôGùW”Öçt¨Û¶lx€”Gr¶5>\‘Â{¥^)Ä0ðlP=÷ºjé3$b§>߱ŇX„N+VœÃ/x®i]±Z"r cö67.w±Ì]éMhÍȰ&8ÄU¬"8!Îy+Ʃ͘×ÛÉ›rRAÒV8âY0IS´É˜NÚ ZüÒäÆéOP×ç{&8K’(= 2ê%)è±ov2ü6-X¤¥‚òú‹Ð˜Œ“iï»™=lŠ:¹J[ X!&´¤ƒ2=?΄Fèµ]¡‰§Ÿ0Œ»1®™v¹VÔ)W3ÑkêÔWk]¸ªµj #æE^Ó tŒ^Ö=ÄÈè: ú6üï°ÅmFù€ÓÄw×á`»!?avE#zú¦„7›² ‹tƒÄfHÐI/=Ê´åWÅüÆ‹Ìì¥IN|]”çO€+„ +ÄÖP§wO\Ì÷NL\©ÍÇÔÉe…“5[>ⱦ/DV% jɹ>ÇÙ`?óß‹Œ ¹ÍqLê3>ò¦aËÅKsŽe==â—‰DÌH€Ccé`+(Ó6˞˅³n ia¹¥™È#}\Î{ UÑc éÏ|~G±^—#~c%‰ÍIÍÄp-’*RúéÊ ÔŒÈ¶²SÔ*)QŽf¬R$·Žn´I.QNG@t·¨(ž;Ö‡\P9…áY“…K1“ƒ=Ðû4Gƒ3¤_ïLÿ€ßÀô„ÖÎí ÞÙôÆa [ÝõܸY–|a"Ó-üRWæ´Ó¯™ä¡f‚´Þ€&Ó/}’á¼JdæñwWTöp,:l28§‘‰¥˜ßŠnyõ¥ôLH— qáçµÎ;ˆÊàJGFÂòÚµ'4ÛWº&î\ìÃsG†ã hœÄÇÉwF(`*Ûœ ubtЇÉ7;·8ZžšÝ_=³XZÇÔ¬ûf˜¸£8s×7`¥Ëw VNVþZ {;]b?‘ $žB†T²–{9É3}DQ0fté+/j“²n>¼Q¨ïçD¯¿±š .\j4ñßnÒËÍ'Úe„ût„hTË/B•d‰Õd1Vå¶æ:1ð[cñdâóÅMhÑ!˜ØU>žû}•<~îè4x—AISæî¿Ü!ó‡?³Wù ßïòb­Í1ì,~ÿì?°ÄýòÁ7%ù}ÙÆú=ÂJžž y#Ì“rlÿîʯÇÑg(åßÊoøû†™ÑïÁŸlòéw¥» æÎ\°î¶+ðQù¦ãTsú7Û÷8_÷„{«Xöc%Ú“²äI/Vð[í3ïòûÝúu]üƒwfÇÉ:–NCF{W+C¾cPËý&ÆYpü9ÁOvY%eíRõ²0(ëñðŸgÇåNB¿4EÂ…"`Ò}uOÚõ‰¹ºE5Ð,‹òÊ…ñOû¨¢l[{~¨líOÂF»˽FcüGœƒ &»d0‹¡¥§¦oÈO¹ÆÛ—4¦l¤Gñ8ñgÓ=7Å.òžËÊ>—aì[× ü½zò=ðèSÏb—öîyú¦Ù¿èÖ¶@õŸtó£­µõ0Þ¸)”»÷Å¡G³)ÀÐÿÂ÷€QøEÑG忯A¢ò]‹à©9ÿ½C9X±_=¥Ž®š™þ*9×jNú_™Ú–…Ö ™à;DŠeäeUÛžZýÙQ(ˆœeB‹Êü¥³d[áï 4Ò0WMË{7$}%—»°ÛY„ŸòÓéTiËcóÊcy•©ñ ‡ߺ"ï £•ôzryÓjW?w›ÐnþýV½XʰÝF`“¨ÿ“¯1œLM, ÿ¿e³m6„ß[}G¾ =[NÊ0—sͤÂ1­îˆeTQDˆ—¦Í‰"1¥Æ^Ã?_w¯Vò9éÀOa‘Hwø|ïÇŒà5ÀœJ“Ú6v‡/‡‹ô÷‚ŸZ˜¨ZY ‘KCR¨íp÷½ÏŒlXàƒsˆrȇËŃú°‘ÃÖo\4(ÊroêBÕ(hF°M X$–)ˆ@u>24>­D”Fø†”7<ÚeŽGÒ€…ej -!{C²œlÄ3ý­“Ûƒ:]DÄWT¼Ø"˜=øyº1dè¾D9•>QÄ,`,¿¹R²erëHGÇÑ”1#H)`ŒËÎjŒû¥ijcÍx óiBÄ… K´å•¶Õ&âlöP Ý"0s aÈà~ü¹0ä¼Cï<=^ì¿. ùòôôòâêÃÑ1ô‡6<\øz°ãÄ^_Àrâúó‡ç=àuE¦©§ò$¤ÝˆÀ¯› ‚*œ„ÉôÂɺ4¨Ì!™]¢¸Éœ zÃ,"ô)t%³Mƒ;¬S³=î™Ý³ã­¹æ–ßQ­†9 ‘¼>¢I´Då- ¦pšâLmö›”×ÿ’çÃÇÅÅK#Æz‰&™6¨O+SýÿíìR“µ”¡ì@›¹^ÞCÿlÜš›ý3ê'/:x˜ÿºøK¶*ùpeŒ¯•Ö¶+4ò!„þÞ!ƪ܌A yR¤^±rÌwÈÐú„5’²»XìkâN¸5ýB0Ó£ïXAi8ž$‡º]‹Z˜æjÜôˆÇÍAû•R>Q_jqŠ!ZjöòH¡M­m¥vlÞI4þ´yS¢Áûøu"hjª1Ë€bi"½ûôÍúfY½úH,(ú)¸³a¼úÛ¹œpÅwÛʱ“”àžSúÊ7XQYá[FJ´tY¤°iRÈ®t¹vÔrèG ŸÑLÄHkÔ ×8´M/Æ­òyöÕ‹ò∋¢Nî8Ž™AæR›J[¡Â©sº!ïý ¾•ïCX¬ûEKe¥i5&Ó;ôÓYä%àM]့¡¨‘ê¡I£×dÐþo:'=êWhf˜Ö¬Wƒ¦á ”íƒÛÑÀ:_Xè8NzeþP0CgææÓdæDgéÝ»‚Á‹ :¿(OU/Çü’÷R(iÅK§Ó«@I»ÏAzé¶Î<õßì*EL•²õº÷ü¥kíLWOÎJjé°Y's‹žä¬>H®]{á$·ö¡* …zÆ{D­¡ôã…V«Ä•Ô\(RvIKÂʬÊ/>îŽÂ¦´.ÎXè(6ߥíõŽ/€±UH LÃ#_6æÌψ´‰ƒþg†žð}µ°¿ªÄ×N¡ýJÂèZçÞŽ ú˜Óy´EZ=ö{ܱÊ1ä 9pËï@lžZpQÜ J=NY`Ãy£Ômè«]Š[ýáñnÿk÷É•Ëçá‘åyÊç Ó 5w»TŸxöØñq$–x|= ]¼°¬kÖ·fgky÷NØ}ž"w”fÜ]¡ʬ‡Ži†í”s!’à[²ƒwƒ½-²5Ó󰦿sûôõÄ÷ eºgŠ÷ü~®ïsZbØ”¯agö¢R¸$¸SÅ¥ÞXÒ”çNàIטq5Å›]h™ö†‹Âm”ÕÂßä,éöní\ñ9w¬Hã·ñWܸ+n6¨'3.[¿;Ôß·Ù»Ò }«…G Óðѽ’^0°Tu¢Ê?¾Ž+ó„¬êP«=%ús1jû„4ƒ§Ã Ý—©lwQõk £(P»Ò =lUŽ3ÊÇXõ–܇Þö‚‰af¨l¦ö[Â\QC¿ê·I9ÐÖ2^/‘E•w×@¢'Hwò X Ÿ2˜¾6ö¾Vï²¥ŸGåîW Ùç/O‰­õÚ/ø–1LÝ´ì¹µ.îK©JýyÖM¸Ä\áÙÒGê¤n”¬¬Àú–õ:æq‚Ahi>™÷?ðÔä*£Å°¶¯Ø¶y¶ÊÛ5ÀíóáÝp¤9b®k"Iðýe¥ &{-Ãô.x"4h˜R6—7‘£XR&m•¢›‘­ËÄ)=Gè.›äîøw›-Ã7é­žCJˆ} ?vÍeŠºþ¢|›ÓäÀ<Ÿ&Ožì„Ýšyz.‡­Ý.ÔêûÊŒX•žU/<S+ãÇΕ¬x$§bÛŸÿ÷”¤ˆÈ€ï"ïü¯kÊxVóÏ~{ •_)ÅæÃûzÄ.›Ì¬­ ™IÚªqƲÝõêÔåj÷ÜmS‡t …ò>®±wcÁnà7ïóËòoú7à¶×'«é¸L †îÓ‰‹ïîÖwïMù¦úïã­—ÿMó¡jðMø=ªÌÐù4Í7¸3ì6{†Õµl'ëV{¦Õ¥n§ë–=c{á‡ótîâ—Çù̵r§ì–¾a‘›–Ñ]R9-²ihµ(ƒFÏô˜õ´®ñötÏLÉšFÏüöÔ®áAötOEÅã·wRL{fwI¹¯,²žùÍUÖGÙžù ‘­rÌ=Ã[\VÝüA—mÏì–M‡cߌð€iüwnc=æg²U“a¾CvÅL²7¼¿ ààÛã¦]æ\ :KgBæ(v/ò¤—Ƴ̎NèébðŒ)ŠªI%vsZŽ,‰N,ÎÒ N¹ÄÔïiaÞ¸2/ZÀуáùßm#QªÒ8¹_°Rú‚ƒÚ†[,[…²pG£1áÐF%1Õ†%Î`$[³¾@`SNùœ`p÷Dèe€zeóLNðÍ{TmÒí¡›Í 7(ˆ,¢¸ðÚý1)7Û›¶+NTì93–'|왨$ o¼À/Í4' ’ü·Þ 0(ú*œ68ë#qüQÁ64!·cÊÉøÍñ›[ïn¸.†ËOÿ &yÿAäõëò‡lWå඘‚;9œÝ1àœâ¶ØdE)æ®BLLv@Ú@…!N B¤¯#úVݹ¨¤úýµæÊ™ÛÐ?Å,\±É´¥ù’áï°p¼y&¯!Oœc3’¤&¿«ñ äõ¹"³W/N²žž£bÜ ‘?éæz†oË$´G àèn‹ê‰lÊdŽÛ£p<Iê6w»öÆ®ÝSTÌ`;HÛj©7³Ò6/¹Ìn\Š×kå©w†Y±¾”驘ôãßÞF¹'¹rÁI0–øMû3 ð€"p-Ю\Jwÿ5í£’ aº¶KÀLRðºÃ¾=”ƒçßü}NŠU:=r€vqË„ŽhˆG¡”Ï/ɘ†ÂiÕê/ç‚ôàÍÕ¹à‡Òï‹ 8ºðY<(’„”ëŠ1ÑüS—¡Å9„!%ÊJ'÷ˆa¬†äê(;1Qý÷ü¤ô5œ£XG7F£󎎰l)½P‘À"º×æJúh»8_‰²KqÌV$ë::îÝÿ&¸Ó!!X¹°bòPá¢XÁ”èJ ¾<–c,õK%³¥/ëÙºÆU3‰Årà/p”9‚”Á¤‡ÕSI¨_‡Å“Ù%Ið‹ èsj¾ËqéÚZšžû,7ã;Úôà JwÀLn ÂH2³ÔX.æ1ò$ã;¨¶Š¾PõO®ÇšVŠF6ô^¶Uý˜e¸£dhLpØ AþŒŒ­½APÛÍõŒÖÆá . `N¾áãE{]dž5Š´JsØFkVë_/À¾™sœìê:•¡I.•¡Jt*C=ê(Cî´rú «.˜vLÝSGáôAgL˜ºa7ŽäžÙ|­k4I$oÿ‘+ÅF_ølÏ«Åfå7¾ñŠ jÔÚm£Uhõ ‹·9;ºÔ¥sá èwÜÉqÁ{oÖzÍÿD%k/Ž<øÂª‡"P¦NòOh)fƉ«¢Té:ªŒ¸·C9Áw`hß›‰´¹U•Qlù$ž‰‰°Ì‹¤$ÞùDE S5OÛ'†SÆ4£ÓZ´¡øeñ³Øw´êÑå™$®“¶£ù[œ»ËŸâÙ_Ôѽ¹5¸9ô÷cý±SÞ8g»©Õ—=þ†¢ú;ü€Œè—âY..7”-gLÈ.xÆõB¼Q#0¿ãÎÓ×Y Î颴Ë\d…gjª(âõV–X`Ý.äDÅÝ7ÆÝ^ð)4©þ’ÏFcaÇ¿ãÈeø^Ò°¶)§JŠ‚TªÅœ"öͯ<¢ eär±Î»Ä|R*ìjD¡òºÜ4.Mxåêåƒ(èì¡RqâÛ\ÖµÙ¬FPÉWÏ.µ®8üF©.1*‘PÑCñ¢Kì‚*FÔþjªÄâ9†k_Ô§üwHšæa ä¥Û†ÙXMœHB¢zMþùÛ¦ÇÃ#’щßÑ¢Zí“Æ¨Íiž/“iÁ8éxSÚ~•Ψ@ùL˜óàÚ9TýP¯Ô£û})õW%ž½¬#Ķ—)†È“j¨äÖ!)²v)²TeQ{íìªDÉýˆÐö2X<ÕíͼÅ1ÇZ„¸6Û-]nû†"¶N£¨Œ¹z=šÒ… ‡‰B§ñ>/þ[çˆóK¤WÇ¿ËÈðórÍ ÎD•Ø¿×h<š?ÅÊ!hô¤o\‰D½‹ht:å<“ÊáV{M± irÿJô ÂÞLM¸˜o!UÇþd›ÿÉŽo½ð7²ûEïYjÛ¾?þÿ‡Ø¾L”Æ|ÛÓÁ®°Hÿÿ›³«³é++RàÛÇáÜ•btf¬wT4²Ñ™‹&‚Ñ*²ig®HTÅ©?÷ßÅ¡P¦a—»2û6>ܾï@߉Š×?ê”a¢K ûq`~ é=)HÒCS¥Faщ^A@è?$@Î%EFP¥ÅÀ(c åk_$ög, ÈùÝËK€Cò´Ÿ¸úƒBbg&¢î"ÂN²#ê$gÇ4d'”†9 uJ!ˉNLde—îŽA>Žp“ ÍÁ‚ZA=SòŒªr łƬ/epË“¡Ü løS—Œa¾ƒvÄYXŸLÄj}$F[¦a íiþfß´j ¢¥//$ð߬LtÉo½ DAÑq@ÁMÚè—6GÏ­‚à —8JÇÙÕ6Ҹߖ&SˆÈIÊœÒðåÕ CX ªÃPh9Å»Q‹eF¦)e“0xx–xÐT ?K}ß°|Ë„ü¢'_Þü¸©Ð=¬6†P!b! q ”2#SH±æàÈ¥®?x´Õycð«ã÷D†%’€ˆÑ¡Œ¿ÅR`¼ïpØ^ƒ¨çyOm\·n˜våZçŒg¾Œƒ­)´—µ0Ér¶,ã‚ܼÂ3;¿âD÷%/@4‹÷èH~½²Nõ¤ahXå³a¶®äÊ83ñüºŸ\%Zðò”pö  ò'ÿã#ðØqrHï'Üæ…®¸–Á…B¯‚dÊðE'ëÃGºÓ¸‰-WÖ­\ GMø™WóÔxºŽ.2–!àW­ÉšE† ?ÿ•:dqå¬(ÙWÄ®Z%W I¡²"ø×8lîÉPÿ´!Twî,b†dpÙJgb°9VðSÈ®5\Ü"Û:ñµÓ9dG—dm¸Y h@UF8òä}Ž_ú›ðö6®U´&m…!uÚE$Õ€à%]Åæ›X™L­BËýÎÖnÙ·6¬Ṯ\Îÿ:‘åäüé<:áiæŒ8-ç­@’/Ç—Ü93ePÚæ{®¯¾‚âíVÝ*c ´]ž×5›cGÍçv;Ö ÊgNqî©i¾Pí¤ pzwE_3'ª|®'{Ve[ˆ:µ¼¶f'—í&&ÜÑ /]Ó¯Šh§¼×šg^¹xQ@²%/Œ%ẹðºkWŒs=î›Ëvª¢-÷‚N8@Â…w9N!ª.µîM'à·Î!B ÖIVõ"è,i Š™gg3uì˜BÉ:=çv|ôÂ\k—ý†[ýõ†·…°—eíµßæY(ijzêQ?W²\+\SžžËA‹éÁr½#`1ãÒæ ú冀ïŠúSbŸ®sz¥ÓbûèjÇ”;F. bÁ±!B­¢zÏûã{ÕÃ…:J3£g0e¨QWôøî¨QŠ·ƒo‰d˜fSâÒÃA¶ÜwëV¾Î:†`ï!ȰçðŸÍQ«#E]OHñajeÉàÜh¨rm, O¦‰þˆ"î¶̲O}Ø>ôŽ ®™” ª‚Ú©ãAq¤ËVÁ$‰^¯9…_6=ü«œ%O”ø‰LZ¾zKÝ®an 3mx¼pß?Çßk© FÕe_­`V)J?„¡ãH\†š.$æ×˜ Ëà<{5,ˆ¶6ÁÁlõUí*“MUaÄØ$xóXÌ2†Õ.Õ/ÚÍÍs()À@%ìéXR?ga`7m¥=|PÛ¸û ;Èœî¬oV™Á¼™éN«s°2zý‰ÝÀmJ›7hï¨hÛÍYËlš F>6îñ9Ô_ïI‘òDJþ 逈À“ž¶É¾Å|7ó|»\¡§%ØÛýß…ó¢x:©Wwêž=uû×çÞò3.¯ͧ?ÊG˜`&8yƒÊ cØV’€;ó‡Ÿs?Wuñ¾¡¯—ïmäÜ]áÄaØ]ÝyϸÚiÂ>’ÕeÇÝj|ɼÿ/sdÎËë\CDþOì&þ?—¦ñ¬¦¶=Ž$jÝíEçg×ä˜äHX.G·!Z9¢"@c÷5)vãFÅ $ëÍE–c¼·—Ywµqö†¼&äÃ0ÄÈ0ß}€Û(ðÑÁ}‚Ç.ï~åíë{’lˆÙõFž“ª«ëêÇ\í§òûÌ ¿}Ï[v›„?Ô³g<ßÅÃùrm]„Ôs¹¨µV<-âLÄoÅìRÅIÍ9Æu¤|¨ÕH½–ó54çxsª^y-{•HÍéO Ìe¥ÕÚ>¿Fd‘` 9­U’Fi1¦Õíd\6ˆT½Ê2Kó¯ƒï¿W[ÞÄ'+~yÖ¤CÄÄ&G.P‘q &—¨€¸‡’‰\'(pJçò,!Â<Õ[Èú±\ËCYGs±+Ø„56%%‚"xÁ9íAÿ° r· NÆQÐ\è–BeÖ+Ødµú*¤‘®U¼¶^°®ªˆØ¤Â›jªŽ8üÏ)Ç ClE¢Ì\Þ¬¤ÂnL‰xOñl>˜Tä._ω °\°Œ1Y˜6[2¿Vv^~µé@|²nénþŽ«Þå”]»å[$É<×îífÞó7pCgr’@³œ'Ïóà{£~¤éã"EÖ­àøÔFcÉæbs`F~ÇPáã:ÈM÷<¬°AšÁêjÆ–R+v~-í} ß?sê™ÐZÁYÁö ÁõÄIµ±Ò8|‰… +–HR\™ÊõA H\Gáù͇Õ5E¹“ÏF|·Ìã·]‚4DìúmeÌá±IVµs *ò`'`ãhNû•rP«úD*¹xÍו Wðx_û…CFëÀåÒŸBy8Þ†A*5P1VLbp?hïlŠpcšZ¸+™„Wh7Ï!¹AøÑ¬¾>_¨ØÎ©=ŽI§¥U»@ÔUUÅcY~›1&¯ëöÒ6VŒ­à¤A 衼|† õ¥aÖ”’RÉ!SóÒ‰\è=èÍ q€ýO®Å¼/ü‚Öƒfç–Qó‚Ц0¥˜"Û/(F fp^Ëf픨;Yª+ÇžŒ§qï×z<<[„ZL{¹BÛÐκr½lügÝ/1ÏY×è¦þ’;êrGmúüÔ„Öˆø€_¯ÐÖn~ôH{Š%¡Æ? c{g@N¬ÒÜçsøý ˆL–ó¨VP©oºQs ŒÏ”òã߃„§c5Æ:h£¬xÏàzH À²ØèÙ?TYÒtÅQ§Øá[ _\Y¢,ìqXÖçÐrzÃ~LU£Uª´8¦ºq¦t™’>¶§Gå¿+"¿DÑ–CB‰øIéJi=)Í'¥I¤ô«4ž•¦“Ó*i=2å“Ñìùš‡³NáKœ·3Äõz90"ƒ;ÊY,¹”1%Ì5¡Ü~ÞÚ^{Hvÿ¸krÙn1¿È°ÐÎCŽ•øNȉàëè/O@uFüéí“Ͼ¥Ýtã©MÆÏU—&Ñ{ªÀ¾h¤?yŒS5Ù)ðBNâ¹ì´×òw¸*ìá’º& ®‚ç¹"a{Í|2qÛ£^)äk¾û‘“)„éÍžÒ»m)y/V—9JãFûb°;Q(o¦$ ¹®a<ê˜#>ëŽ8†ëæ,Ži1Arúv}RIôš„ÊaSù!<¹ÿ½-ð$°ÄVâ²CuF¨^pk¨Ê"¨^`é_LC~óÅ6FP$ýbõ3åboÜ©ª5€‹å9ÂF‰õz>Iè’Ë£öòÌ›2²$sXýš1ïS—Þ•þÅs~4jMYÕ^DÊQÁXAÁí6LÁ™ŸWѪ›¬gz6(¡×Ýÿ²ÅíîÓ$ÏàmSE, *±¹[½óFî÷<.$¦µׯ}ç§¡rJ¥ŽæÞ§–qP °ûß<¤Š¹I‚ù§~k²i>µQe‘º5Ú¬±`þÕ§Ô,Ô©]ú5Êbæÿ¸é_¥O4÷PÁMõÍÚ‡¢‹ü£|ëü–z3î¦è#!D?músdy‡Mq´ß¨<~UÖ«†ª}k=)­ZØýhÅ7«Â°*ÓÒc,›E ‡_‚L ]}Vq~òË7ÕcÖ+hqÆ®Fgú¨åLvxs ôÊ6Ý5^k{ù»1˜¼çŒž&Òrÿiwþö0W2<”øz ?õ÷µ)¸‰|NM‡¤FŠâωzù£¨Œ9ÈFö\=§˜`®.qÈzgLÓJû€×WxHs׫8@ùóªj¬8Î:Ô4YÎÙù}ùž½—i ÿ³1ÒøÐZÖéa‰@4ûµuŠ@ÜFö¤G5I1ÿåƒê$Íc_è¡l¢Tp~-n˜RõËvÊ« 4ÿ“01%ÜÙ=ÓAÒ=æ{V%-¶R­vfvVkÕðÍýìâ ùKó8Œ¿é«q×—Sk˜ßø½Ìž÷^™þ@/@à“¿ð¸?}Ò›Q–ÿb[[ÏïÕ3ì Í.J¦Èj=S¨ãì¼¾¸û±íãé@sr6::Âx²Õ5ðI‡f]xµ| rŒô;{ÐÖð¸¢þïEó&/(a¨ÿ‰,äìéìbjû__C•ªùÛ_²¡üäË;¦ÊA@´X·WÚÛµ,©ûÕPÞT•³› !óº™q—^_ø¾érCÊn÷\ÊF‹È\_9Ïóº9­pEN¼&jÃ’ãL Q¦lXà$d&ˆ0j‹Côþ‹KxŠZІ:Ž ÙH!æí(U"Ä‘ë!ì;H–;ÃÛˆÆQj§c9‰2‚(3F‚zf,É#D±Lc†â.kwËdŠ'¼F0ª…,‚Rð'³Înz[(W°OÌ«n”›“Í~×âŸUï\÷dÏ-_qkÄzë© Ó5ø/•_ûPןklnÔȇww¶ìxÒÛæ6XÛß,`:û¨.šãe¾ì‰á8µ‘ÔìMøK u7ø5ÐöËX78‡¹—Éü8ʱT2©&&ŒW°w˜gnþÙ†jØyÚû´2çytÔÊçz*GwYÂDܪûV˜…‘rÁÔæ~þª¬±§G#Šçií>ÛÚþOÜÚlRðÿˆ¡ÿ'¯þŸ@dܪ¦¿-‚‚·-É&×d³µ$¤¨© Z×ÕX"…Ø+©°QAýÐPÁ*ÇãHöŽä9¶­Ûõ%äñ¯ü>þý¿|Iß—foÕ$$$ÙÌÇãæÿq§cßÝìéçóýxNUÅÃõ—ß'û@Ê3ƒÏjƒcÕº¢]Ñ0n²ø°iìIfã¶vŽú…§s½ƒÝ1ôO±ÝdÐ\/Ñj=kn”Ž„’ѶinzÓ\/¿ô4€$¾ƒók—^Fv<2kR ¦{“uñ9Æh[ úçlS o ½—N?ê–Á´Éê[Îú n¹Aÿ[¬ÓÇzT¬µ3D@×!ôÏ)ݦ~åJœB ¿%AQâl”ˆ.^\ þúý¡×Áµ»·NtÞ‚%Ê£rÏ"•Lë2å©ãI‹"/læy]øË®h ŽjÅ}׋±Ýi[@l@ Ôù OÈÍ·ï<ò$R²‹ÌT½hiI"€%J[Í#õô£(Óñ³­äœ ›Î ?0(IÓj¦‰k^OÒ`’¨àQàájê„ßÙ9ìk’g3=—o’yXè3Ž\‰-a걬ОØIENìJ}Ì}4f§_ ¹ì·ÆÆ K•òǬèRyG aO¢b#üÁbQ‰³kpK†(–im-3ÌñÊ„FÁHG•Ò*£J{gÁÂ}ÔjI¡¿ì€9z[Ì'€eGWï·*Ú %kCÎPoÓk+ÚHÓ%z»|<’;Ñ 3y×ÙŠÐ ¹Qe£¦ì—}4':í“(…:Úo±£;§úJßî]XèÚ0QŸÀü-¸l’_3ðÀíyѬ¾¡›Xå6À\+HÁá]ÛÂY§²&Q Ã×AòV^gŸ‡Ë°²iK¨{îBéû®WrØŒŽU^%ú‘Ùí¹4\õä¶ ~zabi ÉÓ «3¾#ÂR€‰F >d\”Ò¨`éûÔoà0Ôdˆ˜Ìž—Ï]¸­<1œDàa4|$ ÜbPb_K€z²ì¶}ÚÅlx†ªƒ]£nãC’?r(j¬ˆ•ȽäXA ŽŽŒÈZ¯€¿8‹ Ü!ÀÓÓÁ¼fçª4X„•]§e`|ér9Á™£ÀÙ*^Dê9éä*ü^®§¿ãkÊÿ¾O»ŸÛåvBß…YšOaE¶´X}D °3e¾šíU†ÏYðxU`Äæ,ßàÏõvÑX‹¯øÔÌ3çt,æ¡Nû§ ôcÝ%kxkeýŽ( ÕJ“ân¬ÌÉãßõ{õ¬<Û‚{ÙM[ðM—|¶Sw\Ÿ?ÌIb9ËQŠØ9Z‰]¯6L¨I$,_HHÁ‹…×OW^eïóòpŽg;›Ö²Å—aR¨è ¾¾è*>¦°o,l³)–Ù:Ëlíãÿîcö_ÍšèBº6© ˜ÿ£fMW#W;×ÿ™ÿ{³æˆžî¦Ü6Í,šfRÑXWGD2ª(#D‹È¦‰ie…^ã_1åvÒ~—.»Á/BøÑƒ^7Ÿû£ž1üœ[ª Ϣàî`=MŠP¤ÆqØLIäÞ†ð ;M‰Î"„ž"¤—Ð! 1¡_*·í{¬Tè‘Ã{¡%HÓ´õOVQxŠL 4lgQEe„¦P ”eY #„hiÌ}‚ÐàóHeê{Å‘o¥P ‹ eÐ#fÝQ‚-.[~äÚ¦c¹2J¨|Ms_±Üˆê™ìßõ‘“Ç n›¸Ç1ÿ©ŒQE1eÌä$D¯Œ ¦t”·†?ô$Ëñ1Æ ¥UdÆRR¬“.°õ)@ø ÄO!8>Ü×0Û¥Çz¿oGg˜{¹ºzyúúO4!L݃ç^ÿ-<ã ”ýþ0ýÏô›BÔì‚|&ËoqòÌ€qðfÂRp£\eLáØè8'¦2F47g0f*¥2Y&Ôn´‚ôR™-kÅjOô^j°ìàveÞ‡‘Ü00aªÀüŸà$y ج—~¿³$òØ“ü9ä:šUG~Ć-"èm¿÷ˆó,Ò¿jHY¤§zNv}¬F”‹²d!LLþ"ÆIdØÒLçvûš ± „‘7³çÜ·¼îñÙ,…«ŽE†Ôð( s.ˆ¬(2F;EΣíëA(&¡ßokÂ8ì_ü÷'åpÄœÖK_}EÏðIIJ¨ sãªàKçKøŸ²á£¬¢Îöê%èxÒÕÎ&Ú N{DŠËd®t¡=:qí¼Ü³ÁtN{¯RR øÌ¢f­¶äí)ÏÇ(’¨¶*54efþtYKN]Õâ@IÖ5ã§á™®u=ÐzØ<Æ“Ÿ> IyhSs¹ÅД¡5³ ±±ã®¦[§y÷iÌÌ[¦„}21à×~¤’J$Yg’íûþ&ƶÂÇ“‘'*Fs«ª3FÁGD(™Rñdh$)”QkTÓ ÇÞJsuK2t óÙK¶Lñœ’L—é}Ž5½«³n³Ÿâ7ú ·ÜYr^^¹=4\«ú ]†t¤žt6ëyöì¿aÿ;?ZÔ:BÈýOÙÿÙgµvíqÔQû›ó÷ZqÉáµ—(‹Æ(oêrÁÖá$Ae+àÁb#·´T†=În½®w3ofv7ÈØ¤BŽBÕó âï#F¨ãS§€}Oþàž¸ÝÍÜ^‡Ñ`3“Zß¼n77½çß>r¹é~­û~?Ìë¿kHÔ÷öKݟݱt>YEÔ#vŸ·ÀAPºGC”Œ¤4qTFìB#Ak£öb6rôQœÄZ`ÓFÆ…x‚w³¦…Ö2´$+]Õ/e¤rQíd'4ËÖ”/sÌ‘/u¤rÑo}š†JS¼ unkèxöú/Z{­J³Êm¸ƒë>ç„ÿ¹®’ñì`¿[5øi'+Õ¢œÿÀµ<*c‘\ ´ Û N± Ø…mÐ z-L‘Œ‹’¦ù´CUÖŽtM­·jè(´6kÃj,DÄ:âo³ 2þ`¥¼R “dFel“ƘN‰mR¶«W¿›aZÃ|2µhY»“Û¸õö–uï_Ÿ{µø·Š©s!^dA#±[E<ïú$n.4¿àºí+~}œV£Ý í±þ;ÙS¸U•#è¢(*ST¬y*Aï(¡Ø>Å*‘ÎwJY/âêÀïÈy£ »Â³1£5~§ »"O®5L‡íRªHE—j5ªÕªžC=‹Ò$p¶†™8ˆ†18ÇHX¤ È-½á&8BÁHr4Û±jdî\Ö‘áîa WΫ7óŰl.àÍŒ¦?î;³¥«‰Dïç ‰p»äwÑîÛãJQÛºÙ‰&2×Ðv ^¬~±ÊÃ{ò¥ŸTC-V›òZóÔse¾RÕLRt¤5åФüÚ-I×ÛÖÙ·¨ÙõÃõ_[×c¯tʾ²G,|“Ér7í’ÎsŒt¤Ì¶›® ¯+É:‘¨ó£ìëÊ=GQpC¢ÚCÀ¬hq²ØÐ˜K99'QjZWÒu>xË9_ðéq3#t7…âî‘pCxÅ)µþöÙkª" tZ.d5áÐÚ©¥\üDy¤›ðO|^cG~òƒtž…ÿ¾HâŸq1%)ÀôßjÃn9²2¤½2ËC„{û#T ¡¢ñ°-q³‰õ̲úª(÷ë×>Ш'§u(µ¥ž¬¦ÑÞÃ|~ë¨Ýí4ó8Jò¾£}Œi¤l!;‡p¥íÕ6Ú¬mÍâýZy¤ý¯ò`ȦêQàqPŠÄQAô¦Ì6 mNêø Y"R®”Ï÷ËAl QÇ n[\âÿñœø ï_ô[HÕÌã¡€XŽê¨0ÚÛñÓã OÙáÀ“èè¨ÌØLu!È›‡ÿ£9"­úâ£Ù#÷6ñûYCìEÛH§—vb7°xӇ؞iç<Ú™ë—«àŸø°ðž^Ìžøƒ#Ô]W ÄÌcÏ]v #ÛvzüÂÆ--¿ïÍ÷9©vÐè~óØAZ*]M&}d&/¶¥8¤QxìnR6¬%ÓäÈ~îb2qsˆüÉ%þ (‘·÷.ä ÂÖiy%µy¶Œ³»e[?kδš!»Å©7¯Üõxç¶~åùI0¨Hâ ÿomv5<¬¡·-¸µ1•b5–Ôê$ :zLz GWŸÎG0ÇìácŠåø’á[ì²5 šj}4œÇlÍxö®ÂÝ<Óðfª×ÈœÙÃkÒ.Fí9yð(Œöœ<\u\9D&ûÑëÒO> 4`³;…»øÓBqx_ :>?n—»³Ù›tÑ:m[_çD𷪛‹T.Xc|ö‡>?6"ÚˆÈ Ò1ìáAA(L/3ÙT‹¸€ñ-ïóø×Ì|X71'Hûð“Šæ50Â:z¬ZÝjô¼o,êŒPCY†ûgab£5£¥üos¢WÄò#xtF–”J ùöR¬à¨ÜXá¡õ.NxTX¡AyíÝî±Yêq#ã{"£†IމGc8Ù_úþdFÔãñ D~MìÁ$w%~ Ò´ì@rœü[ìGcÛRÒùnd̺BîF:™Åg#ƒBˬ!|…¬Ø…±õN´ÌG$má iE¡ÝÄ„ äËÇÓÌöiþö¯ƒ1LÕ¹~~û8[‚Ьc0qžæs'W&He f+]+–e²$aeXÌšv–Sbx ŒE àÁ[«Ñ{ ÇM;›¨$ÇÉ/Õ±Š` K®Ï&„B?ðß rS-ÏBÐ׺‘‡Ú WÝÓ\ÜÝóùYn’næ _š^òÅùîýÝ¥›jþÔ°“컕Y¾/›v0ÃÈ>qøìcrÛÚYÅWd[ÇËxø·Áß²çÞs|$Ðâñå §ÕðÎ xç_ÉlÔàþÀÞ<혚å¹*–㺚¡làP`±Úܘ[,Ö0y›Ä´Ìr<­>ï_vÖqXõŸ8`¹YÐüÏË$¾ÞcãHïH”ge~ìÎ*¼ðžY˜x~ñA·Z‡ŽŽ–f-- kºÞ÷ÿöŒ·\Q«ÞÁ?À€úÿ7f98Ù»ØÛÛÐëë[ÚYºèëÿµL“í¶Dî>¢}-ÕÝœ(¢^K¸è¨XR¦-ˆikK@M’H+©>¿èLÑ%t!ŒÂ‡Ù‰éYÝœvÜÛ³³åß'S™÷XÁæ°Ê°}[{´Žò¨—ò¬`ºÈ•ª©4é’QýŸˆÆm›»)4tÄ¢ßôG ™€å¬‚؃É âüzUL€5Š™È·›—P Of×ud=ú42ŒŠÔŠÚ›¬õ® †`púé@¾<{ñÆ™mŒtºoç…úûæÛº{v|>&Šo'½9>y’+Æ!§ äKã„6«Ç[ãLÐdNpqÁ×n ›9‘Ò³||p†²›~%O×*¥-i–4;Ö[f¦R=xLÃïFå/üòOÃmÔ‹6ŒµOÜ7 ©6}…³øÅÓök”Ru„é×DýO4ˆW›“ÎaŒçö”FaÕ“¤Ûƒ­ k?„å¤q4œÈ_̽«GKÏÃ?Xßÿ„I½¿œmñà•ÏOÍï¸òÄ EO¼wH²­Aÿ;r7ž¶¤™L–a%$’æR‰Txþ¬ÿþnˆ#Q)§¥ñÞ¬áÀ£Æ t4ès@ÎÈ1ͬòå œ_=³”žÈ¤Ç0œ9 %ž ¸¥Q[´Y:è¦íýêÕø%›D3CÄRÈêw2¬e’rd%2ä¬Ä£¤¤aQ޾’Õl:Oùfœªä]½üwAäúž9x€C6ÚIžRI­¦r¢©I–(ÊBvã. ‰”G.ýÅŒÊθ(éµX€%`„?4‚%ED¡„Kúƒm•Zq!>¦o´Q#ï¯ÏÏ.t&oK¶í8ÏQŸ¯Ù‡Åƒg£Pˆf²»¤ŒSÎÉs{Ó”Añv¾c±CVIf¤9&¨»(”‡Þ¹"")3Ý –n8f(2E=“Å #ÕÁŒU(:½CPξû‰èáèãhºwžï‡›nÂ??.<„ö³“ó›‹éãÇmå÷×(œ¼ßìï«÷gÓtTa{4Ì#x 1 1.Á,]…*BYN¡ ¸bK–0GÒf¦E&éz‹´‡‰ ³'ŠñÉÀ^HÈbO»‰ÁŒ—9ÎM''7m&6oÚ62{tÊøI¹ú¿›1°‚43ÐRð1¤yK=^@ÀQeÄÙ‹1é3†qG™ñë'¡rCLôd ‰ÂG÷˜k\"÷·€ÔSK ?Re¤S?}%V9sf •Sùž¯°)™½ñ ŠR-+MëcI…ÄÐã"«œe" Ê„¾ og/Pý*·C¤† :‡éìa@> ÑÐó¶ C£p$j\r™_ AýrCÀ'YpÍ´6ä¡ §´RQ¨ä±,·Éƒ²€‹ð7qàEÎKÍ‚2¤©„ÔµKèE]xztÅÕ{ˆMSÿý€J†qü@Fã¬Êùž-yì ŽK¥Â•=SEtŸ<´g0Jïtcëf’3½|5Èd )\P+õ1êµEÇÇ_¹AHÚ¡ô†ZŠèŠSa•*†Qûœ]:(Ÿ“á ÷jVºóØ57D3NX²‹h ’d†¸à¿èÖn°bJ…ŸFqë›s©©!=x¹ðÁ‹ÃIãã—íaöÁÑ{ˆ½\üdà#H׿Nÿv^p[|R—¯þ„bx3iË [عpôÁêcv`0°lÂø²7Ï;3ÖCQТ?à,cµØ^pµJ°[‰¸1:rHdê©_$†± j†¢’‰Á‚tš“÷e)Wj¤Y®!ÿB“傲)pØ,Êaμ$Ùd·ìsH,—UF"…ÊØÄ/vÈ^2(€[I™rac€ciVÏú¡ŽHéEþޏ…ШËúmŒÂ3†WHP`\¹!EñQ[u%Àí âApxH˜?¦Ü“:9DÓ[AzX|áŒlß²,vSsäŒVGÄ}ªŸjU3D†vºW^óSÃCêëmà^¤“&‰â’{î\,Œ.>À.[9æË·îJA(ñãpE¡€)(…î#áSÃôŠ.Y:Ãka• ¸gd®åõÏ‹ Š gÖ*z×HwõûÅcriã>þ޵žÖ¥G=üèö¼=V†ü5rX?±ä~«Òóî—ÇŒ¥Ðý;_õáâÑçWÍ™Í÷w¼u"äTQGÝ„´ƒ7TëGEÒyów§·Éê à>Ü •¢ ;\e¸[â\‡I·d‚K†ÞñËè¢H(1ïñÓx~ŒÒ6Œ³¼°ë«.$@£¨¸ºnõÿžåvÏ•Ëf·w¥Â¸m2ùŸØ0KÔÑÈž¨êz´…~G­6²ùÙºªcƒ_ðÃÚë­Ì­éè–n¤áe&M°@ø't¡Ž¢+°b¤“iÒø¬8ÖÊ¢`bMä*w!¢ìšKu>+W'JØvÔŠQ¯ªT3Æ+½®)‘þ 7&G¦s©í¿Î®7` ]éo¡ØÕ È S”%ûªt襇ö}k•ºÿP+ G†±>( iu^'Ërýͦú¼ÃøØY•‹ëVÖ¢rCყßs40t֮ݸn¤¼¯víj5¿åø:Š D&WœÓâ§.Örr½ÇÍÄÝÃ4'ÕñïdUéY#Ž?ãíÞžG•hr‚ ±©ª«Åú8x9Ôž”iðÒÁ0w\˜…¬f¶¨ý¾°ÛRþͲ©øèàª1òŒÊk¢$cU‘5`öVà™‚ªS ­¹¸Ý2Þ¼ø°Ö«e3Ý_YÏ£o:‚ëÄw£y´9µr (Y$œKo@êŽ\Ðrå°]§?æn ïNÝ–û©¿©Íf–úÎzVs-ðä}¹Œ1D÷ СØÉ¼GQß脨óL°«}&ß»ÀÈ̲9ÂKd½¡:n~‘Æ"=c”Í<-B<qëì©> ³ÓxuäKaòôƒÕÔŠ1®iml67ínJº ÊJ)BÜÕN>tˆŸ¹¡;2‹"#ت­_©Ô†7ô“'Ê ÔFÔ¿¤½ã ZŽbàÜšˆÚš†›Ú±÷ý^IWÐL9Fó<ņ’{´èßÓlÍ"xOÍ8Õáðå£.eʉOÛDAƒ^@x‹òb:¹*W %ê™"5’¾"¤Ów>ßžÀ5¨á±9üÃô÷‡A“Pn$5ˆ:Èõš…®o¸9ÏÆoçûù:zðò`ywØö7fpÐÆf‘ìu¾¯ªA±¹ ØôôP}\<|h°÷>S·óäüuLq-Ò"ûPI[…Å—½D·",SP/«ü € Ü å W“Á5`R’©?h`óŠ˜åÄ¡*Ën+èÀŸÜÏKÀ-MžÐøw¢ký{+hqMOÈÀ¢/t eE+ðRý¸šLCËûñ5±J„ªHD²„1Ù„ „Þ²Æë‘âÈŠÁ^UU•2ln.û}¯:ÎçÌÓ=Ûz)Wx›”¸ßùtœÊ%¡äžŽÍ‚ÑA¼–O=šPá&-ƒ »U‘Z‡Ê~¦PÖ%Øãª¹ø¾x@nQòñ®Šª}ŠâYý įóˆ =x,EãRo tg©4ìßàF #ÞøL,¦Yæä£¬vœG£é;ìw“[©«3dÃ+³îªlÒü`†K‰m†¯Q_ÅèÚ£ k·ìã¬-+ÛöÂÛ{âªmº˜:E ÚÌ5žšw«T1 ÆJ_¾|¦½â… Q ÅM›jTFn X^ÒKQÀ}³ˆ¶‰<ðà…º”òb‚3µ¥øI”â„}•dþïÔ‡Zzrç¥v€xšö”¨M›£ÉaÖÌ@£îtÞàTÈÉì8 Ö±oš‡çDNü´MM¸µ à_W³]Z´NOiÌ=•k±d»ŒUèI3زÞ(éjÕDnŸ)E"ii¦âX£=Pµö¾W˜ºj¦˜Òf5­žh¤Û¤ÀvâŠY"\ë/·ÇEE=ŒÛYkìjõfÎi7õ«Õ÷fpk’ v%äðPa"hJÀÝê–çIÄm DÓ˜yxlzFÏ•Ž%ÝjÖV¢0¹V)ifÁALG„tƒ'€n@ؘ.ü¯i"ÝÆ[¶ ªÂCä,ed œÚnu؃óã>…ßÜŠ4¿ê_Œ::ƒu–¤Ù²ÖP L¹Ù‹âÔÆ€^#WE*Æ©ÊéJ½3áÈ-Ñ$ýò•<¨F¼ÁÐàlj«6#3ùøR=/ßKD™½-5çsº'…æ™ Rˬ”7£núB|—¯¥bÒˆwX}[¾wVŠ™$pq%u(½¢­s¨~H#“*†=­œmªYï–èÅ$œ,qJý¦'F^2ITO÷2c ¡Z|C”ÆÈaÆ'm€aŸÄ8=||Xp:HŸGڰ¶j²›¼e}.xŠçÜ£h'äi"±Ó˜—ѳXƒ¾óó/™‚DÊC@'ù¼ØkÜ@ïžbÖâž%æ8ús;:"ÖóÀöÍ‹77Þ6–1÷ȵ†T_ï7[æ’hŠîMœ¸¤¿}¢ì#ÑÕ]oJÚÆ n0H@Ç ’–|þ5ßï-b®\_yük_yò]þíM\~\ß_Þm¬{Þõµ]7Uqˆg£ô´¡*3÷îû <Yœ׃„ÚE÷( 4¹×(hë8“â=‡Á…ßÿï´Xbê¤!\!õÔ(ÃŒj…øÇßI„Cbê=,Œ€`O}Y8ºý¾}üŸÝ“Ï{ùŒó#ø¼ó6Eßÿv›Èwf\š<ÏzËus:Joþ·m%ĽHòà ¶—è1ð|MõZmãB ´êBÛâC[ˆi=2O1!­ˆœ?·,|OßÛ «W¦Þê69>-öQ”¼ei!wÌÅ.ikÞú>äF£šåP +ëG¯=×.,¾ìи¶üÅÜ횇Wêmìp-ï'ÅŽ÷#¯×g®ûÙù³ô³ÄN® J°Ö2Ú—–Ø >‡m÷óE1¹üë§öñrWëÛîír-¢ÌÆÚíž\ÿ—ÇÙEË{F[ÕH]ìmØ·R'Ãmj£{”<É%5µjQkÅOq[Þ‰ [;×`5Ĺ¡ÈÜÎÉû¥_ÉåO•#¦ñ¦u®È@<&ߥå^a}ÀmÔܪ 7 §:ÃÇR„¢zdïë¦kZP$ü«†Ê:¯TÝGRž-Áüö}ˆA‰N;ViËcïRGJT-‹þ­c|êã_ÇÄŸºgoço|öw¼S2köx‹tÎPÙ00$ò¥ñ½¤ Aƒð™XÕ`°bœ fR½4³Ô†'GÖ™‡Cþ?ë%$÷~šŽ’g'YIW,†_^×\yD£Î.“ÜsœPãýF›‹™‹ŽG CEN%šU„)%œ] æÙ.$ˆíŸ*ͼF˜^Iâ¸GÚL@ ÇLÃ>Îïn³q@h$ôè!‹Œã0îƒ"Ï0ÍÔ¨ Œu€ðŠ>Œj•Me5QIó€ý£±Jl–™ ÙŽB< ñx÷ýï pÝ96¦<ˆàb”£4&o >úxªºOÁ1 ’–™† ëÜqe>V©Áeyg”‰ìKï/®¸ºItp×_–^ã;2Xamœ© œ÷’À‹#7bˆSÆrQÁ¡l‰Ÿ6Nê2LVåµècTܘ‹¡qA=&ãï¦u&*r¹ZJ*î6Gœ ✾kÈIä¿æ±<ÿ¦P[&Mžâš©I-4vóšìÆ‹r×Å ÿáƒï¡ü«P?cÀp:nà…÷g¶4T§C¡cŸ›»ÈS'ué8´*Kãè ŽÞ”í°©‚×Í„}ó¨jŸJ òFnig™M‚*ÜãûÔè½Ýrà¿xFx"nKÑi'Àäë]*ÇÜ/U¿„æR5¹œJEQè°¦óÀbYN2Uhf‡õuaËê<âÜtœû”ÄDGô²&¢ÝLOé™FS(Í‘ÉØmÈãëöEù29wòcÀ¢ñØäþ™úXkè'Gœè™€¨êó¿^W"™)×k§Í¢oòƒñ½³¬¦‹ÓwÐß]>Û´øƒ5ûsßàùNšB·t´t $¶ é¢ë®›LxCf¬…q‚`»oI{KÓxÍò`ƒ(õæ(„voˆkû 6fý© ª5hÀè§¨ŠšOÉ–1…¯F¬Á,[¢ÅL¢¹lå’±®A¯„z*¶£‘MêŸH=žëÊ™òÆ3R‹ï8²õú:ý8 «æÚk®Ú*,>Ãd„›çAa×þyëðCj3N£òvIo5æ§éÐÕ6xí¨ˆ*;„$RAÈC¥RZJ¯ F\DûV3Šj Ñ,÷uë×)’õWþW³–±ž¹3ÎÎcìØ¾T '[ÇöÒFS“²ªú¨Îm2U¾‰íÒG½åí?t·jÔy¤cTÿ&©Ø:ý¬F-AæèB·l1{˜rcšÛ­ˆŸê™8G¯ ÄÆÀv€ÓØY–±3§Ÿüe\tŒX]è5±âíF÷Ø”Ï~M´! fcî~ú£Œ—Li¡w}Í·õ5E)BKÈÍmS•W£~_ýïäŠç¼6c k]–•Z-Úì‚Lÿ(”oI1ObÓ¨  õ¨?ñÒßUÕmVÐr[}îcKj3ôüË÷2ØÖ`‡ÌÀÁ&ŒVˆNëí=îk–†ØëêyŠS7ZE]ë_—×ïôb2–ôw"ÿT©Öº-»z,tƒêõºèÒvÍJ1•Ç\ý—ªÌÿ¼W-ì’pÖ×´lÔÓÔ韛WëûÑßÕ ÝvØ–®'¶Ôr¼üóyë[ÁBâÇX÷×VÝÌãMÁêÙàéΟÌH>'"·,ÕÝß/Œ£È'Ï'4Yán½æõ'ó«sF“—¦fY„þ­ERœ'Óé{Yëx&³¼úïPn½îóÐÝ'Ç$V4µ«þ»Eݶl,Gœc!•_Ë>p# gíñ%Ë¿‹BŒYD~y Ï„¶Ïo7ÈëRÒúšÃ¸ì¬½õKw[ Ïã¬Õm1 /ð)Ûä͘¡1`[±F\‹C<þ³&Àô@Óµ_Á-ú4P¢¾qUà ïQ®gCÒsôÞIJvÃzÿ"Ävc+?¢“I¿^lžÂØR=ésHè,âØRa¬&mRï9/ד*uåZB¬8 û¦ijº¤â×BÅÙÎccÿþ-ï.à€îÿ­Šh|ãëmƒ#é¥ï:ïð¾¦P"YTbÛB]'çåE£¾Å]'qsi':)kÕÉnÝeϱ+ ÇJïú¦pB—ÉíMιˆô'†„áþAˆtømÍíð»!™3ŽŸ>íD‡0‰tö嫺ºÌì#áݾ³­#‡¤­»ªª¯êû²úVUù·¶ýþ÷}Äþ(ô#¹0¯~NžÁêp‘O(%dRì.lûcUS¹L”}ஜ’T´CÖd;¤°¯dÔ¬kãaN·;É©K´=Ê ©x‰ö/ñ&Ñ5©h…zŠ\ä5D¦=RI+åK¤M>E>êZ23©ÔOa0Óîì™2‘—t*˜ˆ›Â™[ÄR"E>úÚ2ó¥”¹ˆ‡¦i¸ÈGÍd>ê1b".â‘3‘—|êLôµ‡Baºè;j¢h*mB«ÈGÑ„>ÊIÌ„¹ÈGÒ„>òÑ4±—†Ð-r©ºè;k¢ly·ÈE4E\ÔM¥Ì[äÂU±ªI]ôCšè‹T±¬i]ÄSÆd\ÄSÇd\ÔSÈ„ÜäBO1 åebnK¡V‡eb/ÖoŒƒå;{È6}{¨´p?©Ö½í!Ô¬bA%&Ò>Ú2a_^.ê­e®7Ù”¯rÇ0×›MŠ·x1×›NŠ·zÇ™ëm(Åía_‘s-ÅÛtÆFéb´÷(1t1XoØî³C¯PÔ®_J&ïs†öÕ!wé¥VÊÄ27½‹•_ëé‡á"´˜ì‰ûñYW\»(ëŸTíL/{{À 1UtôjTˆC<Õ‡U§Ú£Ð]®U] Ê©ýà[u_Ȫtü¸kÍi5霼âÜÛ¹*s½%ʺ©gXF‰8/ÄÏ"^n—¸.4÷Ê´÷(Áɯ(Â]¦»º£´Ê‘ˆrÒt¾Õ”×þ“â]¤>Ì×yðMju!{ú¼·à= @0¸  ¹¬þžÿ„‡OF5 4Þ?Øëù㦟¯§Å#âëŸpÄ*ôÓ“sơƓÕa#ÉSƒš·ï\­wMÊ\ÕéÀ³ŽUŸ9U*»:#—CÝÁBt{í"ëÜ,Háþޱ”¡ôM ö·¹¡£¦æŽ·‡B …'bkfäd|y;ÙgØ£Qì¦2»®—nvà¾VBQ¡Fx7N–þf7j2õ89%3¦üêV"ˆïSåGñ?Gh©ãáF,·ö²ßO¶<¼<€v™-mý²0Ì”=l5(4ØG¥”rY\›s“!rnÞ–-±ÿ¥Þ~Ù+ "d…¯±Œ–G°J°¼%ÔE3 C~åeû»_·:x“¿“Ž–Ê6P‡,Çi–¡ŒWä uº)]$Qχ°Ää€8½oŒÚuß÷‘â©{âØ½Ùª,y¼UkZ‘ëËøíÁfè¬& âûM"rùW¶YOXÎZ‚E«søìªqe°l]á4TJneØÑÏO€r «úé¦A»: 3‘» ^Ú9'v¯Îâü’w¯ÐRÅØɨ½õF•±l žF×)_&9kõçÎÓ œÔúcL-K™¨’9Z¤ßûí1¼I‡ž Fz#]ï”æC%ß+1Ž‚ã+›MCÐGN© Λà»YV@T“&° Iáóæ{õµŒÚч“zUZåú+ÖF5j™»Ö0u©#yn~Ùg-ZR©g˜-ÒÂX[}~`èuSyºáÍzÂÓæ/-ù»áOGEB~Š½Þ­`¶"4*{Pgõe&8‹$=`™{†œM N™Û…j«<¹ÇE^OýžyÔ.‘xtÓ>÷¡Ÿ°õ B±ó@oM¿š.›è˜£IÖkÎD÷›K‰¯ê*·¹ÓJÕ[  TZ•¶,CÕa€ëU<–¡ª›0üÉ °©Ù°ìN ÔHõ«ºA"=Ø ÄnT%¤<ê@Õ´kn²PW 5ä^÷—™â¥lúä†ïß§S…zUœ†au%N¦H’6ðPj¨­9“°Wsô 9ýF©ÀÀ׌Al°Œ›©÷œ©Á^v >V ©ªEnXºS©W{­ÕR¾ÏF¼9]ƺüú/µ­´øbw_hö§;;[*1nu¢ºF{@®uZ-Þ±UÙ ¼J©ºd:f;ÕZê9³°©dE"S*ÓÍhñÓÜŒnÐC×jñXÂæ¬Rui=á1 ö´Ó‰.Àõán—ü“žwÞ;òÐ+¥_¦4¬ÖÂ}üd"{Wœb÷4µ6êÃyÒ»'´Õ}œ+ÅVK­Æ@Mæˆ, ”Ɇq&óÇJ+¸Æî÷¶KŠj=ã%/ÁjDî±ûPªø^êõœ ; ÿZf~‰ÞšqjÀ‡ÏÉŸ9"ý ÚïbÅ…iÁ±˜)-ŸYŽ æ±~<¬î^TÕ˜÷kÁ2êB§ÔëÕ¦Dlœ"xJcsî:c:†­ôJèÁªTªoß ”*ÀÒr­ '…âP˜ÝÑ~ æ.€´`ÝÄÅ[Ý~æŠ;ßrJÿuþ=‹ü!‘ŒI“D½J´Ê8VÝÑj•°bc©‚ÿ'(î‚–Ïã²§étKO³ï½Þˆžv-­#©ÔnÀ³Ð$˜"K¶%î•üoŽ/0oÕ{¸†·FõªA#M9…§Þ¬ýÂÎ;j„ÙPåC¤^NQ1WÙ;³‰Z´$ î”Õ±ŬNe¶N·âbxC㠇ƒ­:_¡€Ô—S–u±_ô(hç –ó#¤–Vœg‡å-ùŸ›Ãºpf¬éáÓ7gž)ÛI/q êŠ»½on9¦d(Õq‹ ÍkÆ­ ð<Üla0Ùã ­¦`LÆž³Pg¬^lÞ¸þ1¡À‡ÒQƒáâÐ fíLºvî „Áœ¡*Þ2,sA»k\GâÈÃ=1ŒóÇ’–1Ðhãj@” k™1€P}‘ñ‹Ñd®†óä&=*õËMãÍ6{n63òÞç§Èmq×£Üé±-\6d"CXÅ-¦Ô…yü¢ñe»s»ÀBXÌ“^Äh|媟påOwÍ }ã(ŠFÀ`‡™j•ÖBpú ·V{­µŠô›o’/Oмìz€ŸŸ·OMó"ºc†Ø¯¾·@„Eô}òÊûEô=0tD Û°†Ñ4?NX)Z,?VÔu>0—L,¹žù´ßîa) ?}ëwz„ñÍ Çš¦µ· ]æTOÂoµ]¢fKTÕàžtÑSé*Ë•”l µyúÚ”AàÏXƒM0±³9Æì+¶ÇÀsšg¼k^Š"QüÛ:üp¯2ÍÈf2ìÎàÀXWÒIæ;šøà5 zŒ3ÎmÿqJµ]o ÈÌå½GÃë¼µýªl¥éVõûÉA¢uQû1G1~0üÂMJâw×î/F‚¿Àç5Z`¹h—èâ=ÁvR<£fŠ%“&oзA $Y™Œ` &“c—MÊ éÁ]À*^'MR—€Ðf8E‚‰x(ÍBt‹c Ýu‚Ð]ÚF«E‘9I’i­ÅÌ÷PØ©Å^uj·¨8!”¦‚d+8V/Ã{ƒjÚ¤Y)Q9Ë.Йã×k1ÄZrDIrUûv…«2 Õ×vÖ ’5/ã 9úž§lÒ¸ŒAÝþ ­iƒÖse¤ìCãËXd¬ù¾˜±Å³tÞìØÃÑ'þycd>ˆ[àl?þð­NwÙX]+~·Ñé¶ Ðf™[†Hº›ˆü6ø@{࿎L„ì¥íÿ$ &ì„k^á×#F»ª|t³àH×WÒ|»A»Ö<Á—Í•hœµ_=sÈ4K}ñ7³È¯)žXvØu©§zÏ–éV—{¹*s Ü_‡M#ý$<®÷ZStíÃ{úþ_ôoþ\¥¼é_ü°änvpŽÀ×»!Ñ ”0Loo^"Y¼:Ï<„Ì;ÒOÍðt›½É@¦ŸŠA\aï«æc{zìnà#§ [ú?]ã{µ³ÍFo(ÜÍCˆžpqÛº·ÙÜ!ˆïæûËù\Ïåø÷å{år?°Ê^nòC…Ðo˜„Ö ãF‘»‚c/znËFî;‰Ã‚vŽÑ]tá+;Å€þó·¶`>Œ` èRyhÝÏõñÛŽò÷(y§ð!~1=ìlËGd¾_#²cÀ³¤ü¬Ý×4£&ø+¶cà?Úš„üoƒ¯ô†1OÕø‘½†·£ iºÔ1x‚,ظÜÔqV?5v¸(¾˜rSd_§±!D/€¹z›}Ñegêöäa‹Äï]ýÉò‘½ˆ„€7q ›¢°×ÜŸíM¼Ð*³õrѽÎJì¯sÑ÷¬¯:ùèÝg¶½ÓÚ(ÙêYsÓŸýÙ†¿\ ¿SE0Ür6÷»é{ñù§ÁÚöéUÖԣǎ†ØrDïÁÙÏŽ™,RšhQ$6ÙÃQñãS ýrªø| el»fåcäÝ*“F Õ`¢Íh®Déû2¾ËCp#»3ãÉÞ™ßr³ÅA­Z~¸U¬¼jÂôcS£Moe¥U2£ÕÌ‘*ý¡R}PÐÆkA²¾ÎeÕWǼn— •_6/Òöô=OŽß—ëh}q(ÝF›oh×ÿÀßðw²µõæ}/Ÿ†Ž|TÂgŸ²úUM¨dzD¤]é2É«±/!arön` ]ΊõÈ–{4,}È+lXvêרâ=¬g¼.o4B¬nê–Z7DYqSõ|V(@êGEjL /Ç uan#ÈÎ0½i´´±9€-`:z0! -Ÿ6®îsÜD Ó«–Ø•¦C±¿› ßÐõ6lëAyhK°*oø¬-ö’œÿ½¡{D-á­DxߢK®ƒu°öè*KøŒáx…U˜¨©œM/Ø}}® aèЀšxÛ;žß{¯yÚöíQL—ÞÃìFù˜zB1xZQóÇok•»UÛt Y$]„­ŠõæHI“÷Љl똒|.ÒQ¶ûHtMÅã¸j¸ ;Ç‚ãw”[?±f•ï«MÃ)N|ŠXðˆ±Ÿ¶ÞÉv4¤ÙRp°w[<©ûÅeK_«ãô,–æ=ĤVy.XyˆåŸ-t²“gSQ÷ /üg©ø%ã( ‘¯ãac‰>(qMMñÖ_Œ\×/AÁúOkÑ©yšÄ :$§Z±îiFÕrœT»÷Øà=¬§,ì…Ø†\mw¬X ¿[ûÌÂ$R¼ö^=U-¢E„RMäéÇ«·‘~è °y#‹éµ‹Æ%È dsX’ÌúJ¶–Ìò9…ì‰OˆLGþÊb‹Ó§Üªò§.оë(D-™e+ÝŠ·}RûaH¼T3[ªsx2¯KÙ”ÇD›Ó§^0=o"Ý6éÝ7Øe1"Hø Á!ÛPB•ÄS§å”ÖwL¦•l1”ÒžÓiÄj’A„&ÎGM6µ=xÆýļÁããïŽÌ猆¡ðL£Ÿê/$a.C…·óN-2ì-ñ¶G~˜¹ÅƒH¾†zŒ`°q2Œ26ððúiâvEÿ>Æ…<å”ÄÌó~ö¾†úH_1)—„&ÉTvB¡&%XÂMH²…™¶°1%Ê\š’“o¸6G'á,z÷Íl)·a‰!j"b þ¥5ì3díbOa)ßÕWº¿J” ÷»\¢³E¦\< š|"Ü@Ý0\Xºe<Á—lü²nÐ=U÷Õ% O‰ µ£¬¯ä!6¹+4ë~ÉrL¼=ÀñoÉåoôÁU›û½HÛ å€TIÈ$$¡Â`Ðç%×Ä*O0æý4jê'CL ¸•ÏüÖÍà)MO ŒÏh¼K„+p*¾K¤ã•·àn¢9 ˆ%K„$Îpƒ¿†ÄŽY«D“ý¨¨ž. ÙÂ8í¬M'¹ŸÞžM*-&ssSu»‡ N#ç[pê_“ÖÈ[˜mPÔ‚Ë[mxÅU­îùs€C¦L‡÷G‰ÎàsÂÅY@²2g 4Ò ÿsl н|H"­e¥² UA›»€\¬Ù‚ÐÇÂ]|\¸]BöVÂfz§i ÒIJKõÜä4EÅCsðý!²ª¾®‡ƒe˜”‚•ößìR®ŸÔeuù{Ü„ûDúš¼`4¾‹åDïÇ1ˆ[É•³ S7PÙV ¸vŠ5¹lT9 ÖÐ6øóŒ}ªuRnÑFèΔ»ê«>žXhÕª”Kâp€ •;ݰMŽÔjU£ ßa» –ðñúÕbM˜ã€¬SríU$èõçÕžç•TŠsg7Ÿ‚àLÝkü+<}tÅʯIP¡$á5LÚñ2â„{=}Gr¿Õ ý6…ˆÒL»¿G9¢·Ë$ù'kã\*yYöAÿè-i–Ï$—ÝKòÑ8¸f3Q[O橃.ôÀ2zÆq¤bvƒi´}:oƒ3_?|cÛ?™!qý±ÊØÀwä=#™ïõÖz%3 ûp ÷[ËãË‘°H2pgotDËÖ^§×kµ ýäü–†~J+%8§<Ëõ·lU¿mÄ ,I´è†D^œ[€Ž½@UL‚Éù¡W›2í­oqÞ±;;­y“Bå1Þ= ºû¶ƒc?s¥û[™ÍaKbiîA¿Ì¤ñé:VŠŸÌ -@š&ãöí……ôi}2¾1XÞë¹ Xfë÷Rù¸ ×;-¥ìLCªK¹¶±E8ž Wi"’ÔUÔë—K¢ŽšXÇ\*QGP$=>K×8‰I“x‰ŠÂDÄLá:Â6æÑÚ uQ;>£J“…óRuæÒ†8å,î¹åTÄóOÈCcækµ@ büŽ—LůþÒû8úEÉrÒûÕmLÉæÞÎÌËÇɇÊxð’رyðzdD¼OûÞJÔßš«_-3§òÃá3Y¹Ó‘aïg€—9¸d’9T{™Ý^ç ê{ï%“ç-v›yùcÛÚ¾çpö¤ÜNVVg…PÜ}¢\¢¼×« 9äØì$ì`FÓÙšt~V£ÓKÃWIûÍ™îZCiêaCìŹ¢‹ç9T¿^&´DÑ—ao[tž˜.=˜>ìÂáB½rÍò¥‡¾|üLÞûâ©çm4Û¼sþ4zœg^<Ö¨9Û¼;Ù+í÷ìõAsQ$NËÝ` 2O•]¿gê÷œ÷^šzW¶%-6ý®Kª†'!t–…¥QÐêã`:ÂIé¸-è^óèåîŒÉim¼vWòCv´™Y-¼v”†š5i]nµ‡#•Fu}jÔ‡GçÌIÊ0¹,QâõÀá9ËBzæ, Ôrç•Ýí醭½•— Y»›3 {ò|ì¬Çº3s{ÝÙZÝ~_â&wÁäg-gµn¸/óbPY‘"É«&dbþeö«~¿«µ™~Ç¿ØÙŒ>Þ61d66^fVNN_çK7ÃGh[.sK5`TbÃl‰¾æÖ óÄØµ ÚQI8èÉ%“‡ѵäL~6pÆiLjØZJ Mñ¸˜YcÚõ_[OÇç¨ù| k‘l5›Îv¦0kKe4©»?ßôó?<að§÷œµFËÎ ‹ÏÅëaò°-Dõ’Ôd3zã¡sbnrØ+°Kï®aÂc)1½Ä<ìuÿT`aª ÉŸÊÚ‡Èkt[^§õó û[÷åqÕœªñOi´FAÑâiÊÕÜJ½²4¼c¦÷¦#ÓÞ£´ó­=N´;páåA9 ÖŸ8ná ݃>vÃSÚkÛF>¯êÉrz1¿¸<¿Àxü1e©­ÄúKw[»ç~'ái† ê×þ¡ãð÷ïñBž«®à8»Á¾ý¼|â7wÈý!–¡Õº.înñ”Ç1Œ¡Ñkg4õ3xˆÐ{Þë+v܆_Dô4w,¢t¼½ýbT#\êð4øã0ɱ¼eòµäoyYCýòýQæìƒ‘ghÄ¢ tM@I1£Wd{quíÃ_*¶SÖèm”© ˆþë0éÂeóhÙ>ßšÁx6 °þ%A*úä@Д‹<¶¯Øw¯‰f²PÈ”Ç|%·c«Ô»èJyd¡ý† DazûÅõU§]6W¨Ã(×ÇÄõä..2e®"èbŒ¿yÛ»î_X´¢Ún²°.þ+"ä]Fº×c‚ûetn¸[£ a•“Îk+ëºd@ $js†ñóùìŠ&Ñ÷m$@ÃTB{èƒÛÈðÛzzü<qŸ4½d07ÊžrYÖß{È”„ð÷ãéìãg¥2Nßœ{¿(eœè¿=½Tïο‚Õô,§úMø×Öž”@$oÄÈL© Z‰ZÚЂrø" ˆª¸Lo̘«N…çà†Å¥¾]gè)"AV»óÞ™š^Ó_£Z(AMŒ8¾-·Æ ÍkÝz•*­jU~“­i’´º¯îeôf¯u¦EËÿ@®‡ã™@#Îa»Ÿ2­1Õr¬h)¶´{ÓÏ«_îÑxè3³ò¾{&n&}wqoi´l¸8'>YJ¾ë¸ó……¤l˜ŽÓÂqej÷ ÔªøÞVx9lƒѰÔr¿Áâ@ñï e¬Â°ÿŒÏɉ–† ‚@ñ£ß_8i±­Óš¸jÂGŠQéµ>[¼¬%[ÏßAX­¡ˆLen4­]EЉôTÃ1g´Šñåïzç̉ê4_ûÃiñþàâý᥀m/ {QÐGKö'–èˆA‘j`E2´d&üN°`§üb.oçµ”íµœµKXŒ—Å/£{Àn›Ð¦’‡'¨&Ö¶W†­LÉ,~äž,»÷b"ðÑ·#0xQ/þÑBqçwAª=R- l­q²PÖŽaîeMŠ,&“•‡y’Á¼Ö7Y£.„‡¥Sün9˜ëñW¹Œt¼®&çé¶Qü–*üª†²±!üËøK>ð`"l†á?Í´yyx4ŠÆéÃ÷JPGC$¶éh{ ïGFklöÍM ›Oøzs¤Ø}–Ž`¼`ñš‘sFoud²R«q´ .Ëß¿ø£{ÊcT易Ïå Pl€›„& QßæˆŠí22×ð$s ’M@t§ž×I$a"vj‡HLû5ÄãiLƒ/’ƒñå*Å ÇÔ†£²¦¹C£ey‡âR­MÆÙ/ËR“M]OËr«©p”Nd¡ã†¬^3-¤{ƒøg˜ûÆ'òOèä^«Ü(ðZCùÊà`áÙ¸íßa`ß)<©ËÕû XÄ‚#BÚÒ@ˆ²Xr±~Yå±²Çòƒf§°Xý º a¢ð/}ÄFæ¹T Irm hžDsb|åz¸$6ZªOÑ\>œ:’ƒ*¡‘(V‘Qå›(ÊÒy¢JjÚ™Bp툱$eîs½hGe6*Ã.¬6Ok*|üÑ)aŠdV¢tKš~ûØ+¦Á—êŽU§Z9§Ç_auÐñmVÊVæƒ6cé+ÅU•æïDUÔ§[@w˨kÇ/ïC‘Önx´Räx(§òòº9ßqí&±MÏÛÓ€\iÚ[lGP©ª8u~õt ›“v^jóÊ‘q8²Ñ\I…Â^˜eÌ“U f#ÒÈ·¿â£¥Ëo³YÒ‡·_œyœYØÿÅÚ; lM‚×¶mÛÆwmÛ¶mÛ¶mÛ¶mÛ¶æõl:^¯þž˜eEœeEfežŒ,1jÂõmëÆMlI·ó>h¾xÏpYý¿UeÍjàjËõ?ì¸?âØ‡t€«0JƒžR.[òÙbß²MT÷¶ñ2$H¸_áÈ y|Þ]ÝÁ½ø5š^ô.s×Ãõ›ãŸ¨·þÝ™ˆI+²¸håBg—ÛÆãö§ëlõßRç >³…þÃ|Hkô 0ld¶JcIίýìëS§œ¾7ñù™\j)ã’Ÿ±=çëS¸Ú}¶ ÝÔÇæzÿµø(» #U çé l‘ˆöžr»O[^ÚÊ[ôGÒØ)µ5sÒmáœYµ+jCZô…½]ÀSÒßÉ¥¥1ksu«¯ÉMdíÅQDsÓ÷Sÿhó%§Éåpw3ˆ…NõÅRÖÇR`éXÎ*=mü> ^žL9i¨ÁÑ—–Yn¿™çZŠx8 AˆW’Ò ~„F"g>g(=ä`{S‡wÛ-C‹Fe5y€nZÑS°âéÔÚ x½ˆœ8 óã2Ž˜74:ŽºhsàÄ:ÂMAÀ['®‡ã5þáàõ…µÝ¿“á±+&+PÜ%²6yc[Yú”¾éÈO×ÞŽCçí Ò€Ø-‰í/„i1Eó}rû^ƒ(ÕæÅýfV Ës,3/³=;ŸÏ 7ýAÿwQ·zØ¥&8@Ãÿ bdnb`ílîdâèjaô¿RF+êWÿ«Žòï¶/Оo]T‡œ2´ð~ßÀòm‰»>ÛTD¢YxëÚX¸‚Ã6þÏGL^Ò÷¸Aä™0aºëhçg°3ŸÏ,p†ÄÑ—$;ß ‹Ôm<#I|Îüj t U10Qo)Ѻ²aþè?ü¡µéR¡uõÒÜd‡·{ûM—zÕÖ¡÷o–àÃ{oûEüΟ¶üzÓŽHA"Ãã»`G—NQ0oPZ¡nÁhbž3U¥ÏZŽ–̶„AëN@J@ÔÄ% ")}“Í€6Àþbe7mû«ó¢#¸m¶ñv<éYQ5dç›è±Y{¦ó.sQò¢ƒ }& ؃+dÇP½ü  lD¨VB7FµÏ Dåq  )q‰Lf¨ˆôwgQŸzêLrwÙùì$¼N 2o‰ÇR€ x¤EÒ.¬áRܘÝõ~²ùM:gŒn¿¢ö 0"Ä£ÙìüêÇh©gæ+¦Ù´ˆª ˜•óÃTLÜ`RññH¡Ö|äÞø&c´î³ýO>³¾­D*ŠÅ<í&ôDL–õÊ•ø¯ŽÛ¥” y_CìÎtŸ6ÎŽ¬xUuô? Ðt5+–¾á^EߪB;ÆKwÃi5¶2 NÿLÇŸÌ…KÎXA!ÍlPHqQw÷åéVmøÊ°"u‹DÛ8¤!ºK˜–…Ì2åDãEŠÕåáÇP £ïY¯RùBí 9 (ÍÚXzTšLÐ7»Á‡>×ñ6+ogáY±Há Ón¼Ù2´vRßXN@§¶sM¼2Êi¶?©’L:¯þDöÇE;f… áÁýO”e ‰õ'D]!2׺BÙwÞú¼­Ò&À§Íó5TJyh¥ÇÍi«l;Çîø”C¬â̆7…R•x)õ›í\ÊéEÌ'ïŸY0d““}ýžçìÕ-Í ÏóÏ96Èó-®§,.%p-•tþØ>Ï_Çêó›”õ¸?§—ÅJ ¸±;ï¿/ŸôÏs^rŽö2ÜCN8Ï~²ÿ%Ìg§0•!–Ÿ$ïÓnG0w8摌„f‡Ë VpÊ!'â­j û ÉÍN,Ñ8¶3_—ÁÒɶÝñ…+—}‚&pô#©ù·Ûÿ6ohësòUÞTz’@Š[„ôÁU^»íÓ’GeÀeûHÑduø3ñÇ^€Q Ë“Š-Eê’NOºG¨×µ¢™À©Wô"h†˜ZÉQʇ™· Qø´)Bg§›n€‡ª&-©§”•}T#tlfF®¯îj؃ ìÖÇRªu^Íã.…¦¸Žï.Z–F_L0¡É˜(U…C$-ìB^Œp!¨0¦ˆJ(tј_¾xþò­2åÎãXâÂÃÊÉ.Ç0ŽII©£×  ·iU>:`%¼ó5“¬wư­3£%•Rõô8²ðÔd'Ðå 6åš8˜¸«9ñûW,€ùÚ¬\ × æ†“ÝU¨W>o˜MfT,€"—Nx×jª`e«rV´æ` R“ýöл¦.™u¢7XL;êë28’4€ ñf«ÐÖJ-=›ÐM»O~_},ž¯ÌÔÚÛÔü‹VÌ· Õò“¿æÁÒ#%¹ÞõÑrñYÙGìŒp¯Ñf_ü­Ëè-zûÌðM p7Á§]Ú½Š/YŸÚ)©{9…gHë¼z¿Ûio9ú¦ŒjÙva«=\‘ê<>· ɯ†øŒŠWªóüg ÏŒ„f9™HAq¸e#.¬nY­yÞfy¤qÓT¥¶oßÄÛuq‹þðú®…‰•…É5ƒ‘ySÊ»4/»|~ à;öDsvã ý¼gæ¸I×´«å P̨p1:ó,Ù²¬D&ޤؔuÊݨ`…2@öÑÉÄ+Q&‹ëòöbHq>³¸S–ãú~j:¯ú” ðY¿ës,bŸ>Õ–ä"¥³,Nže9&$²ånžâê)l§d´ ²ÔÛ ºlƒ³ƒÏϲig:¢{qÏŽ¿>P¦bÊ\AE=]dÚS6—È0Ï|B+îšÂöÒš{Í*Qµî,Z@Šj τȡyf¹£M ™,×K·°•Ò?užU)-n °(CŠ™¨EI}đƩWZÞžk.B“e+b CMð¡²B£¢†ì •ô²%ʃ–©äDÜJ…WG”˜ ì;éP‘V³Ë]ÙgMÔ¿Äß¼Ðâtû¼r1=ZrŽÖñ´±3nÌzÁÚ·I¢(×ë³”9ͧˆ±h%SÇÔç3Ò#Í fXÅÃGF³XVÐ#‡ —ÊÎxÿV]åìJngs¬ÏŸWx7= 3Ъ‘žŽ2MF¯AÔjùðßQàÝrW¤HÆÿ±'Ž8OKÜÕ2Ø õq½:–b‘ í¤qP¦~$HùRÛ* ·?Q„hlz¯\µžùÐudg &+—Á¸´­hÊ…ïUëxôfÉ«›Çã8—Yâ¹"(*É ƒRä·÷‰yäøxª+|L™0ÇáküiÂÛøYÝý¦WÞ ã³DèB¡·0‰xóàÑÍ'5ö{µyÉ(ë°lúãÚPõÒ©Ž…#A^Ž÷Úd¤«ËoH{Šgޤ{§[ÒnÍ£i3ÆÜ¢ë@q0…Î:…îá½:'ÿb”òL£ª«øÞÿð^ÿ—€ ûÌïè­èËʘö¬ ÛÓ°¥àÝhy°=Âm ¾cÀ¤aµÄ6¤µ|˜7|/Í%–ÈKŠþAÒÙßçD²s¹…­* =f›òEÍ­ý}ÿ 9[UƒOüA‰”möØûòjÿû‹0A•ï~\9¾j!M„·þ oö°Š! %*€÷1²!Y¡N3‹Ž~ήG‰†^ùÍ–Ÿ*GH>%ÆùäëÒoëú:ÂÚÞ¬¢¸yª[¨öm§Ríó´eUbpÚ€¦Å]™t?9.ë×Ç?ö„oh;ï·¡žüéDåvyíøßÞL‰‡ý„8<Àÿÿ¬DÒÜÎÉÙÞÚÅÌÂö•HjÙ8mµ#þÖêP1·8'`nÉ0©é$«:‹ÕÐz‘pºUŒ Hˆ†iôª/˜<îuAû“(6¦p8­É¾  MŒ6¬û¼†Ì#;Þr:¤7%?ûMŠÎv| :¡ÇÇsD¯6°ëŸïß‹…|sD1”V ƒeßO;©Ç3¤L&Òa\ˆ-kÔK„u›9sÐ’˜OŠÍ¼ÊõàšK¬‘@(%’¨»Cfxð_' &»: š{±Øchx\Êg¨÷#î–¢”S΋ٳ*]@ RÍÉÝ6™U­Lj«SGŸÍy ¤ópcãÁ‹& ðAL‡iƒòI!!5ù™éÂHõè cðÀõ€RútHŸó˜ÞÓÀ*ìª\—è2Ü!,oDŽÂ´·ë&ŽîÚ¤ZNN–±VäÓaÅ›ú †&‘y,åÁãî?<ÙÞZJúÞÆý,8s¤ÇG1röbôàʆ¡·sþ,€?Μۄ1È£Jñ‰0N z–‹žÎ’E[M Ê#1ŽeyÀUS§›9ÏŽót9L"F¥éfeø»ÓÙ?ô”¤9Z‰WF5¶¿Ž Ú@vh—AÛ‚Ñçš;"*& ]íSʳýkg •YŸ:ûH‘RJõ”JÕûËô?˜¸Ó8툥K .Dý2F±Y ÒÔNœ‡?sB³+jVÇ s’2øÈÜÍØÏÃŒC7òbº‘튚Xîåéâ½Nú½¿¶äYŸý€£s,å›ûîŒB×]&\v±‘mÎu«Ó^æf0¤3 Ó5Àâ*˜£osÌ0¡ìôªýÇÐ^sœ`lM‹ñ²$2xȽ¾)äëDdk{’T’›w<Ô¦ØýpHùõܨ*«)Û òÔb7t²:à†Tt\þ{¡ÏÒ"öó$ æëÇ!×?ôo ÿñîÕãýHTˆ5|*ê!ùy\/V§Çš¥vTèëØÐ{7o³,¨b§çÔˆüßr2ÃóǾÔÛÓAš™bh±E,ö7(Ï æ×ÑÅ}±O &ì˜[àÃÇ‹é²+[šZ}°“퀚‹Sщ™„ÉøÆˆEN¾Åì`ÛþN¾›š}tÙ†9Ô?&ZúWøßN?{ˆ9û÷­u>>ìÓN±L¹¯)ã=Ày¨Å:%å÷ŠfÅ ¨wŽ+béÍ¡6²äQ}´mB^5\–Ì×0â÷R8H`fUÊ=8º9:¾ì"a¨‹D-Ìý¹Þ)"~múI\SI=Ä,lôp&ž—Fdît'!ÆŽ„þ[ܧ4£r–~çwM+°œ7±¡¤ã­sRØnLކÁR‡Ý?w_[½À®nÎ.÷¡¾õ¤ƒ>ùócpGw{åob¹qÎa¢hºˆ7ì«Ä³3;¬˜Ð#¥p¥;}“4_¶·@œ5̓›ºi‰­0Q;ú¶¢Œ¼ÍÙ@0À'ƒHäKòê¼<˜Ò±»ë®zð£Ð[ <)šï#&þ’RÉÄ]ƒ,xÔ߆/u>Œ`¢ÖP]Ë­šàæðþìÝxWq–Ö3WqœÏi´,³£Ó‰”‡•™H…t”èAÿød%øÁ°²ËëfúæUÒ lÀSeÿ·Ü©mÓ.ý A|Ñä•"¾k­òùâ·å'B%)â¹Ùh yíœ|ué:õ4Ù|K°ðe©œúg »M/FŒ’^”ê&&|`ö§T>vM¨è¦'ˆ¥ #j½N±è’ÙÁÓŽ0ÚĸÜD·üž—[ A˜U …>x;B6«I €7å*TJa^RòîD÷ÙDÛóTÛç~Z^àGiwña¡Á7”^òxE¡Cî«D5RòÃŽº5ÝýyŽýû—N ÕŽátXe˜AI7Y-™ª^¾vþuÄwjOœÚX{T“œöJA”è4ÄúW- õœ¸ôãJé‰d‹Ösû$«eØ/ϵjÔÂF •”‹•­ÒãиÌã”xr¤òBü<pƒíq¬ìå†v8 N)›ß*\3ÿªLH ©b÷PZ6Ò fÍ•*á¬4)mE°ôü B‰œpËÉ=“Ѩ앓Ê×ÀTû¥¦MÞü¥½sU’bªCâjÒ+¤KFínbàà =CÝÜ`©b´H†´ÝÉï%²‰#éÂʉЕ‡¤2Zš$[Å©“c40’;á(U±[ ¦ï|&”®ðáÚôuZµ„½®òò*#(†»Ô‘TÐNvØ,[@“p‹rlæäºg˜±Ì#ÖÖùI÷2!þÿ¨1Û^fi6×#Áf"϶œ?Ü5A3ÄjYR2wHÄèX/w–ó}ÄfÖØG|mRl6¶¨IF{këù¹j  ‰€Î:‹µb ôžqt‡â™Ça錳Uj Á|«2…Û +'+Qñ–iñµ…h“â¶ùUÓsí)í‘[`¯‘Ö{L{%: n.a6ñíÔkd3ôÃçzŒ£ïULÌK`SU7)0Fš7mª²çA›®oL.¬ÿÆz¼ŒEƒ©kdžÅ¢D I6K:ŪlF$-uÔaØdÚ­ H%/¼NÖd òy¦°Tâ'W,þ%ЈÚt80‰QØ:Ó×&óôß(¶®®ƒÝäTÑw “ÐË›˜ªƒ ”6*"Üiäƒ$5#,€õT£á)§#†@ êlÜ2Å*²„CµØOë,PºòÔ¹âA¯Œ˜1·®ÂÐè°:Uæ| tºTo„]¬šMiy(c1ëÌèö™“)XuY†(`3ÖÞÝmûÃYe¨yËÁ¢”¼IØ¡³æ'öغ ´_ªPÇÓ¸ªÛÙÉ=|öð2¸´¤7þ]TÍÔX²‡Ý¿˜#G>:#šòQh'©¼mØùÜôQôôÚé·𮼵o²®L„jöœ›¡ê5.C¦(”‹ßÍãÍ_ªÅÖ§Œâ…b K7+òPwð;¢0¨üëÆbñèÅ2ŽAD£úåE[\µr´è°ÞÀ]ð.gwt›Àä|Píê‚ù2}Ú™7«iÚ“¡-¶cÜ¿¼Ä_ }»3lÉŠ®ìj] ï¼^—ñžº ¹ µü¦•z µï·x¿2É\«P3Tuœ*ÎhjŒÙŠ1—N‹Œqƒ¦SßçÖÀ1ò³ (5(TKµJŠj ¥ÃŸµCX¸ žÐØ×¿þç3ÜAtˆ4ýÐ7!¿´ñÆâ‚MÂÝa`xò˜ÒN…Å …úòE6bs§Sž_®Ì\Gn :ìS ¬Ñ[QS>n*+V .ZMAÇJ‚ã{²Ÿ¦|"w¸ Æ?ÌÅØÅ=ÆU^5›•.øxã(”»MsŽ \%‰q‰jŒBõŒC¯c5¤üvR¥¤Ý§™e1‹Çá 5×ë]áóÔ –ðã}6Sl9•¾Æ»Ètêžée„ÉSHÓr—ʦìÊd¬ µ·_¿ä£• â+®óø‹#%¸n99·¼ßP^é÷Dä{X*tË3ùæR°Ì0ùµÛÛ }”ÆqÕ÷Õ4éõÁ™í¨˜©éÑ'Å™ª·£vBäwÕfRy&èË;Ç¡-œLÝވƙªdðSÒ[ÚÔŠfðXò-´´ÿÅ¢/ÓMø\ŸÂ&å¥(Ct¾@ÙjÇ O„dÞŸú¦uþÐ>½Gš‘û9z³Þâ›ÂVøa˜9˜Òº'<î$×UY*4…¯’­WŠ1øgÂætÖf/€¼ð/B¹Û²ä2<Ý:fýÏ ´tËS8~Ž+]ŸÀ$^(^ÂoГL ®ÿlëß°ZúA‰y䓹j3ëp|"b™Rò)²ïXïßC¾"K>ÑPI· *ößÊ; Á²9PÕÜ«ì'©¶hݶœc™¼é^*ur÷}ø$°:êo‡ÀA¹‹Q[·Àœ /ÆcôiÕ9mÉØWMØÓÇ¡FA:T«7jËéÀ™€©PÆ&| Å{‡NYô(£ŽÚúró)þd;ÝqJKo¸gŸ×»ç²a!ºæÚÊzË_cù ®)%åü?èz­¶‹Ý4ìûýÞ“ˆba\WøHËC)}zÝ6ù ¢…»Ê÷)4ª ,P„9Ê}X¾[±eÐH<“0„ ¥ªLЧ䴣©ù™ïønª= H²žï Û#FÁ}ÀÂfes!À—Ãûz™Üƒd›Ç¡Zf­ß:Ì^m*ò ³²ó³á(äyƒ Ž}ßÖèEVx㿦síÍ2`‚äF Kå|?y*½J½ÁUGoÑ šO;A%WXA'¼H?Ytšóò’Øñv`_,NÉÏà“`Ì|É'»6bðÑBD¼D°œØ,*ÞceDý*C<¿ÝA¡ZŒ¸e¼dk`µ.]Úú5¢s]DP¤°F7µÚH€HÊZð5³ J@È*€.ú\çñSäýO_Ç|ù¬;ôoö!V_Û¦ŒÐ5ЦV WŸyR¨³¬B¼ŽÓµï™¡ù±|2*Iw’öÑ9b6_×0>Kbç';(û£Þ@I׉òàË=“ñX󸼽·üÅ""~ß^øÏ£±h€–®BƒJºÇ% …#$E|¤L÷ÀÚ§û)Øá˜¢Æ•M¹0k[—ƒ3È‹.·ïµœFFeD‹…p}©ß~Í0÷\ZµœIñ”¤F󼬟†ÇþÇ^x?JYOða)mE ü ,Ú–WýP‘"ˆaŸÚr{9ùhÿÝ´e|¢–»Š<®+ÖØöQo…娫1!f¬\–¥ªu`Û"3é}_ b};¶éV×ÿ+^‰F7%8tþ§fºÍn˜&àHþ·æmkLRZ’s¢rsR¥õúÐØ pR3bx~¥åä˜/ŸkqÖ®bJ?¨hð‹Ü‚öYÚ³8ü÷Kñ ,%t˪Ôè%_ºµšÄÐŽI|p×| [b:µÂŸx§!Íe|H׆H›ƒºhÚï§î’â¶ÌRV%4ý!n…Ix«ãÕ›©³ÄÄ K©çjå<&?CIݤ¢N`ÀÙ”Jróoðÿ^ÔP°×üçhÄö?kdù/InÔ£5e‡#æYw‹ Çy|O¨ˆ"Á«õº)U'¢èj<¸ê¹†'EÝ;_Lâ…dgçÔ)Á¶ñdïOºAÜ "Pï“H ÇIúÏqœ°lÛqšòßoð¶ã )'çÞ]Uen.¦"Ú~¤aEeg®žÏÞ—2åÑÐßÃ]›Ÿ ò¥\¬/Ù^»ÄÁ‚žP° òR7aeèlV¡å¥·¬²S6»H:ú  .Ø$'"š°¼2¸ˆ¸r˜±n°ˆ>ÚŠ>â ¢7Á`by\äeyy\Äez5m¸½ö2¹°‹`*¸ˆ»Ä2¹ˆ‹bj˜0éešÈK# zmåR#Í u!q%s#Ñ †Bò §BU¥šÈOf¹n‘©BOæoè½ùgÖ 6úŠÖ`QíÒÊÖ`‘ÅÒnry/-Oæ£Ø`QÄ.â.ª‚š;ËD\;uåÓ`ä.2žÌ=òÁÈô]kùdèÃuˆizʽUt@ ÛN÷ˆ:Áùçó^<ˆzÅ¡’ƒgzxù3ÙðJ‹‘DfÌ©ù;B mìÖgW]=ºy6Ö7]ÙÔòøt5ÜðRÔTó6Q7ìÏNVOÆíH°iù“ò7Ä#•o²k©ùk«í¸¯NZ âè¢æ¨^W'G=™D|J¢f5?б]^¿†˜ŸMòÐüü,.ººÈßú¾ö/Æã 7-q´~öÌ)}õ¦®;[HroíPK¶xÜFç_ñlZ:ÜTû|JÅ£w¼Ýß‚âÐn}íß‚0¬ü:äãÐ- þù cÙîÄ•yR½džy^ZÍP< ñuöOÁˆ_€ÁòN^u>–ß: ó@çîAeÕ]*‚•4dkª]³I/¬Ã¾7ÖžÓ×Í ®W?\?Ôôʹ´!^/Z”Ù1H3žau¶ú`YLQ+f ½Bãy‹Ïø'ã߱Ġ¢ÚËI³Ãù" ªOy[â…ì×-ý“‡Ìð:¼›MìhS½3Ò_`bŽ+‚§·.yáÃñTH>=÷Ô[êwOéÛ!²%¦KªåŒgφº%¥‚Pkó»?ºV]ú<~ìQ2ã¯Ì´ar´CQú¯´#—쑰̽•ålmzNÿ‡âŽþíÊ´GÛÊA#.:ð.üçVR¦Ö¡*Iu$ë[þöh;Y–:«’ éÎl9Š1Dx¡8Ò¡Ôϧ~X!ê7Z“dMÍ©Ú[1ßÅXñ/[1›=(×ø»Ý¢¾Sp±^·1ÃKV‹ðs„/‹¶÷±B5þ×$ók ËÒr6ß4ëú¦ò.zÜL*Ï‘ÓižJõ1Þµl1ìWM-EÊ:[~ƒ9³6Ì•à$'‘Õžíú6ÀòEq"¾ ‹ÑÞàÀýhîaÏc¿%°9溄}‰e áZÜM- Ä&‚NêÝÔeÎ~˜r$`RÃxH#ÍF°U¦| £Qwd«â}I¿^ÂpâpnYÇý¨¤n÷—2¢øp¬“$@wð!U`õ+Ña@#á¿þ‘D*C‚LK”ïÐ GO*,¥I…#±ú¯óâòË}¤} Jxhí‡ ’Õ§î‡Üqd2\¸¨ó¿`&qâ‰hžß|¼>ƒpú†ë©C},Ñ/<4îSôRíÞ‘ï&¼>?:d$uu,Av…èqð¯¤^Ù¸q†gÞç›K·½BÙI¶jV^,¶Ñ椸£^*z+_èß9N`Êõ$˜ûÉ_ìÑ}×-<µÖ˜U›>àTè›ÜøÖàz~A g3è@„ǧzCnx¶Õ¶)ê«d-qCzhK‡õØ&×pï5'–/©¢Ä⌮ݵ ‡ˆ0¥ÙVÂ@¶ù  ˜QLÿˆƒklϰ„½BÀŽ:bî2 z€àÅ_õâÀþD'áa¬¥)‰5‹?é(~…¥"…ôLëklˆ-ŽG¶Ñ“XÛ 6¨ï$bÂ\–1È«: ¥S™J|öÚL'SÚ2a.—í_ª•:òØ-Ù—gH?Vf=ç… ìÆæ¤!kIÁŽhø!ù´ljBÁ&i!DÈ4‰ßX–¿LŸ>áW¥ó3¨.ÓËlŸõ± D4U›=–ïy(C×Å1ªÞµ5›‹’4©0š@«ì2Ù]žúÍÞ(s» ÀDN2’„%±¹‚'¿üx¡mG+_Ç.=s*=[µUNåÖ65¼|ë·ÈY¢QܹñÈVòõ—iŠ –Œƒ!¦ó“©¾Ëˆì•ð›èa¸Q-3V êº:žJk—ÍYøâÎp^E\pI .­ÆÁT/íb '‹Jg@( ߘŽZªS|ð¦,„@ƒ¡…¥_JŸ#Žq™'?šuÁÛ5Ó0NÄÃ, ÌDœÄñFŽ ö2ÄB+²UÝI,ÚòÔâÙŒà0¡CaWœÏGõ‰„{7¯éý2"p«dZçX|qœÉ#Sn¢¿,LÃ:¢ Sü××£SnC ëöºÖÅûòÆŸý#5ýÒêj~W6ê×ÉQ!Uâú¡ƒ€§è›âUóÞù(„«^ÈËèÌ»ì˜Ì ž’3f=„Q™~즶ïBµZ!ØÝ!XúÕùªTBHλ=ªy±ôµèn ãyš™>ТҘÃåÏ 9ÅNUÑ3žƒ#p.¼Hk °9Ê­ }È‹&†+wëréVRöcEÛx€S~ ¿ÕÞœ\†t@Ä·2-1ë!úÖ>áÇ”ùÍPÑFf8E0Ótä’G©³6¿»Á­1аcÌæÓLý[¾¼ßó"G¤"ÊìXeÑ X© ºAIbCb–̼tUÖ&c6a7Z©‹éG¤#~F&Ýi!þ‡"Ïùûô/>mÄ©ÝÐcS)É’–#-G½Þ5{MÛãÕšØ~\ÐX…\aƪۮû„Ñ ]%/± ['·ÚúöÀPX˯óhñÂjynÚ…'Zu9ß;ÿNN²ËÎUâ0 Ý[Z~¨ÌÜk2·X “›:<Í<©ö'¾Šúu.¡h›Ø'vKDê¸/©XSóà†OÁä~ºŽý¨¦m‚‰¿H‡€VÃâx|Ö‚ÚÚtœ©ÐP£¿"÷°?¯ø ÏŽ9–© ñqÀ0ýÌ Éܦ)ßi¾„áB¥Þ©·åñ,còhÜêó»uE‘Æ-¤ Â0ô$b‡É#?$L‡X] ‰%˶ÁÒ+¹Õêúô= B¢–ócÃG—^XÍUº'·ê¢~“Ðõ9 ÷ h:›kÚ×F<¢ÀeáÇ]Ùy{ U3VÒ¸ ®ª‹aøO6ëŠ½ææ¯Í:–qNžFª·M2Ú´¶Õ¶‘«MtëÙë“A¢&† “4Z*SµÅœ³2I¶ç.rŸMÖFR¢×»Õ³mLâTZLf—'Êé¾±W ·ûÂÙö¼!¤ýyáXW'*óN$ṡIõ†¶eDqæ—‰$èR†‹Y\è¹Úá$­°£S DóD"n±&S«å´ï™,["tú&߉ CeÎ-ÎÃ} J`WÀ ¦¥qmYõPq(x™ôd& b#ëHÔTUy kÂÓ'˜iY”‹ ¤’^J‹KÔÒõƒmj¨,îUU¨’Lï?¯Q!Ä:Q;r•Gæ  `Çê&Å0i× ˜á›d ÿØ·Z:o=mç/`:GåŽVÝŒcìàÛÓ,å@])‰iDMåj­3sùºã‚ØtE×sõÃ9ðÕ‰¶–ï?"Dñ?‰ˆlú"¡Ê4Þ—œÆ^8ÝùÍÎ@œ›ˆâÝ£$Á<ºE{ ‰C‰Œ¤AÈêïõ©#Hf£8N\÷ñK ?@Y'¥P´DJÊhKÿ^,d©{1±Y –«Ô‹DþJbÃ9Lr‰£=0 ’kw9@þÚtÂÏI3î„ÍGÁ™23Gcd¯@ć8xÓ!ûûÁøÇàot²OËÖݰÖDæ†çÂìZ‘©OzªÁÂïº jýe2íxM¿T†›XŸ0Í(¡¸ç#E(!FBæÕ§—íw˜yjœÁ»B ööe¤L½SÞmQ:êHúÜ`KQeÅÖüy¯õ,jˆY¨Tž‚4é®1$H”iêŠÓKÑÍ[ÌÛñ…=‹xlR9(Øm¦'ÅC ´T¢9 }dÂ%,Ÿ[fž:ðº§\WYÀ8ÒŽÙ#ÌÂÛÊLº¿µ7@-ƒ7«ê¢ûšà’€ †Ë‹=ÖO BQ1‹Ö^Ïý:8jžƒ´³Å‹„¶?ÂfÂ8¬ù(LLât ^c«í­·5Tç}8T Õî?A·("„2•†°o´y²ç1ŽÜá“j RÉEdÃF¦0Ú× Ö1Õi‘Y™s$©f1æWÍÊÁÞÜÓ¯<#¸‡OF²œå‰-=¿Úƒ'”ËÞæH²-åžäáý¯ {Ì~GƒñÌ<ï­ËgLª3 æO³*²Ìʱñaºóˆr“V‚Ä.Õvô¬³f—÷lhšµRv¯ÄJvCqÅ1>;õëZ,üçöÙ¾a2å׺·*>ÊÍì£ö $÷¥Ê燽Aκàe{KM|Ü™à©| ? ­_Ým²© ´^µ ¶gïý“ÀAnÒÞíutÅÔÖˆðÙw±†bEÒ¾5 Ž,,g\‰6®XxÑ:UMn.2›4|µ? ]Ž g9´¨W ünss,?œüDqK:I÷µúÀ™OœR†cŽ¿CÇÎgšò9×n›Ýèá´ú JË1M¤ÏJc²Fg²…×'ø¼qKî’NÛ# ›=†ÚeWÿª-ygmœ¿p4׉ý$Œüü@2¡Ô¯èù„óS¦&Òf›’–¡½ÑI&Žš!ƒRKfµeWyh6?{‹2ãQ ?.!‹â_Ñ×gMÃÞø^3åÛJr¢ð¼pIÚßň`úXžsú¹Hgóô©(Ì©²[|,|hª û‚í¾Æ=ã¾ø~>ͯ»s€Š'ÊjËñTÙÔØ{5‡¤®RÂØÄǾŸ=\Dú&ZÍ.ËõÌÏ0ªHôÚ¶ƺ#§{nu±[u4N´2ۀܬŠãÓ‘/ÀÏúúµ_Qì(o†¯»ë9Šþ)sÓŠ€•tGò[ã–¹•ŽÕ)™Ð•î RN/>êö–céiðnuæœID.‡(x;0›£Ä›‰ òô©jæ…Ñ×âÙ±ã„)Ì:\ÊÔá*&Ðs®£CŽ>¸í¸¦sPìÚ~:¯¶Ĭ™½naUæ¿b\ø.%Ñ8nÔkZ§s-ÖõÅ ß3­v“6ŸþjšÛÌ®±&“ØC¨;1âHH÷kÝ < –GTi"ð+’P–<Ö¦—í:»·°°rêbáåÑÎ&—F¥EñÓÊ „E¤û?ó¡?I©ßìAájþ9‘øÕœÔ™ D9 <ê,ß8õÑ;2ä=d´®é-áýñão<8u {Ê(ýüŠlª|Ó}~̸mA®<'|›fF]ú©”‡- Ù©³ÿIÐЊøJßÏbgÒ]þåá7móôux62LØ'Rˬåd±Y;„XK‘Àü‘"uÿÖ#>$«£ 1+'ô#OT„ÎèI¥—Ð¥ZiÌò§$þ8 “Z— Ó£K‘M³~Kó¯ÍjÈ×éAmx—ѽýýoãóÀXÑAe°º €ü`|ZØ;ý¿)¤+¥£qÄ¿Û>ÃI-Ê-l úë!È4÷õl™ ÓºZíXIq£yB©HÑÀ“Ö÷½%ÞÌ1zà/ñ¡ò®–ŒÜ‹Û‹#¼n¢"ïðí«¬ôæ8N· ý®ÐW "jW`=r¡9ândbD–΢Ø]w‘Yí¬Ïh.’\¬„÷ÐDó{B ¬ib0q½»6Ws(Ŧ§´Ð.@DÆÐÞo¼…Q.(¶a8ž¸MŠDÇX—C’gs¡×Ö_q¢ËßÇ‘ÎRßê­SàO²†+F\–˜ì†öÕo¯ÉnSŒÃ%Ɉ2,a¬ 4’~P¾nΚ$ŒMÒ¬à‘d,¯Vu™¾›ã[õp_ W¡-ñf‹à…u˜"‚b…kÅÖ$ ˪±¾v5<Ö»aOÈVòB£"F>þܬÑãwC®\šqD‹›¾Ÿü7xxç‡ KúѼ˜ ož‚޽-¿½›—®žÕ£#{Ûô³QCwm˜™© .»þÑ žžºž±ØÚì­ÝµŸ±>‚-ÔIŸ~¶ó‡û$Y³#¹£Î3ãOêÉ y±´§‘gœð/TáôAg6öØ€í:½lXtò%ÌXß%ólèt¡-´ÈÕÜõÅñƤ­Òè74.h€ÝâpLk’PËMMÅf#_ƓӴ¢Æ<;³$…è k®-Yc–éè šl…c2h#6ÌÜ '#°¨ø8Ø°Ú ÖýÁŽéf^t†ù^·n1²—ûŸvfs„÷ ‚ur£sžÊ;hk½B¸Ý!+¢xø}èÕa' é}äOpÄQHp,‚^ýk‚=ˆ•:&2öUÜØüX«±yÈ¢`}£8ƒÔ(UéOÞ·y^­é‰h@Èø Ãcþõ“ ¼¬|YJgFÿ4äué½ñýÐõ½"_v¯mê×Ûm Yšº®€ý“Ïäþ$O·]cXß¿Hë`ú Níå#°aß<Ûà3¶4öžè¥!dóÌ"µˆÔ±dÑ8ñ1¨;MR@)€UÓªÁ×Î@VÊHdjýW`<€èÂ>Üöý¢úPõŒš´…²Ú§£Þò.CEì÷Î>$] ©`B«ï¶È1Zr\|ç â>è+oxŠ ½©µK ¬Â9¬#Î)ÿÜ#?º…†ÑÓM£iø»ÿ’®‹Ù‹‰¡þ°ådKO?ì¹@ßfƒÄ›6¾'6p`É—&ÜYx¬àÄ7‡³èùueÑÒ(OPdyƒHýWMjS”^áä=á W–/Ç¿º&~vŽÙ»&À»&º&ìÜ’önÈúС>µÌS‚önÙ6¿p˜ákI^IÅ´Lúxíâ2ûM¥Ï<”›^? ߦû]yÀ­ía¿yÞ¨óß2B¢3;,¿9éOÖß9ðd%·wY¡‹ÊÐG­?-x¨(,¤ÑT$©ªw/í Ð6 ÕOçÎTÀ|Ör`ýjXKU—˽<©d X)¥¡­¹´ÏX¸YgsÓºþN£åón/왪Ój•®¼Ý¾swFF¾AwÜΘ¯qÕw«Ÿ˜‹€(¸6}Ý|ä/RÅ9«KºkŠŸõ¤.Ö#÷w+]®Ö.WÏ:ywâúT>VØÓ¡Iÿâ4‹K[Të†4{à¬^¸ªW®â¯^¶_t_€ç¿þVÞwWâtntºÐZ”Þ?˜)J¾F‰—ýêêy£š1ð„\¼lGØPoö/ÝʉGjAß8J#Fª,Z1_V±P¦Z á[/æATHs¹ú{·Õ±¿X˜Àè˜ÙǵÊ+2DzªïOOôŸvmª´>û“}ßeòž ¶zbsƒå‘Ú¸;to3Ý‚<‘ÞåóÖ‹sÔñŠb*Ôc…â,»í¡ ƒÒú­GÚ ËÛ&S-ÎTjÂE’•Å÷©Õ ‰ =-ýDþmº•H|ä¤MJ"Ûéjfˆáð&c«hÄ m*b?N)D”ZÒ×q‘ž^d X{HxÖ*Óòƒ\Ÿ™‘²Tà SÉ·á*íè}‰¦ÊäTse ÇʤoÃn]$©£2§ü#öwŸÐËÛ¼£ŒM;nÔéî?åÞ&~Ž…âÊ]\^x‡±Š°~@X…ƒå*žË+$€›õe¼”«ŽŠ Š9YµÌlè†Á]'æ_ШUÒ ÏU)ôMÂõ©!%U›ŽÕöÅÑäYï4uañpWÛ]nêÙšÞá·l«´ñp–=ÒqúÎä¬Áø!µÈ~´êÍ~8ÄaþwŠ$³øžÆÓq®jMKADŽ«d]ƒW(žRÒŠ.*)nàöÁ»WBu®i9è.@þÞ6Ÿ„ö:ÎzÌM<˜í{ ÉDƒ+‘:Ž=‹Å 2ŒF~†¤ º)a1î&,€þ-n´º7’ð5ÔšÜnvh€g…"Âaë{à ×{;ôçóþ7c¤$7w³ùa²€`™áÐ’MÊÕ•Aô¶ùe©oAÚï3Ë8âÎ--Æ€‘v÷sïÌxB0‡Å©¦3#Á˳(ó­Åßþ–rêcÝŠŒ޽H üê*LËsq“\ëãKAÞÅ¢†ø "‘hà»ïƼ¡Ýô¼ŒGG³ÖDЕ¤Ì$œq¼ik1½#5§ÁòU¢,÷VÑ™ƒg¯ºÆôÒ–U561ñ…æï²Cñ5ÄÞ3@–©ìLTž‚Z¯_h·ÝÚŒL’˜¸+¥ ƒÒ^š~ʹ¿ ›£–Uæ¸M¤èa#ýÇ47Z-”‘qn,i*„±Ne$ïôq0=¦lìhª:Ó®®kÉx|3P ¤Ogrà¶²íE¿ûÅjI–\ïieVoÛ„;çúô¤‚VvdFZµצî(2b¾YpÍ›(:“}Ùñ~ö ¼>ÆN'pøÁI+èÚS…_‡†Nš'‚ÝÀ2Idw{C3ºŸÉGʇFêå0ëÅ6k¨iä¢íA€äÛ°°¯ãh.Höܳ—õeæUö¾ª(In40,1è7Œ 5€µF²'f-èiUšHnß š*ã~f>Ònr£Úy€Ú§ 4H¬Pó÷¨Z¡lÇÒ\¸_xÜy¬3™^¦Ò–kdø}òÙ!£™ø¡Â@rhê•^Ì›z§‚™b4bú’>/)³’!]¼×â{@SØØ4šÓ¨´ø\À\d5ôŸöäx·]÷OçXV¢’ý¿{½äñ1)M‡mwG*àІHôV_W+ÈÒÝX™øßôk?œ+` _>€߉–ó’M\w]û¸•(2OÄnv“‹e©µ;¬Šv¯:Yì”´CïoBäðc2i˜o®‘U›úÛuùQ÷ß Ñ7º¢ Ê< §H˜%Ç‹8Ë|weXiÈ¿šµpgO %eÁ ?¶Eqæø\Ä ˆåIáˆ.„€ õö=l„äl“,tôISxo*Ë(Ø.Q«]!Ô¡vŽ–@¹eYPk^ƃã¿ål*gä «2Sþ¢ÐAü–XMÕÎÉHíÏîÊÑräw>ÎwVü„åD*/7E>1¥-׉´ÚeÁIÏØ•>ìåzàêÔ"B˜%u=g#F½Øqƒº· ^i•S©¨9ʼùõ{ÎR‡ #Šlóõ‹øÃãc€\”iÖ·çf»½S…=–»L’Z í3êHïMÉ›üT¸BuŠr€¬®fY_×ÿGÆw<Óê©^‘â¿Ì•`­º ±~`RsЇ™“¥®šþEêЉ­7b(z Ћu¥T‰Úªøí|ívOÁy~!eÏ1ø‚â ¬Æ+Ú[3¢'xÃ:v¬g^[ß`Y­#®·5Ç«áw–ÞÝEŠÞ¢Ê¸â ¡EE—òén„"k›2B")/Ä*{^ T;ü}õ6½%R I–ÅÞu4…L||b±ññ‘SSœe³k7Ïfk€;!4²Âb/y—*³cB¤{å…'² ÷øCƒO¹…•ýµ{W”O¨q†œXŽU¤)ëÈ’mÉå¤nRP÷ù¤&eQäÍ!-õpÖQ­­çÔEudxOŽnÝߥ ÿ¤öäx,É:¿z7Ü!r>hïé{–gZ?;w6e•qkB&Û,óhqniæF’>uÔ9RÏ¢·÷䣼ç’F¾¬? ìh‹µ[ŸÌ:o$cêuR%ñŒŒØ¿¿¡÷2T»éÞwÂS¹ÛÈk‰w=åçVfÊ|üçÄŸ¾ÙRehÆE0îÐDosBÆáLštG]:)§åŸú‹ K:ø¦†N~ ˜þ‘ 1dÉ ?]O ¾»³ûÜÏ9!OK8ä¸súrªOÖ`Ë &Ç›#5è)™ðƒÊœXRoJÏ¢½‡gÞ9ãx¦hëkú¥fn#ÃÞÝy‡Ž ÷¾Å{éÇgb;ËÀÙKy7© ¶ÕL*é-¸±(ÞAa—CÌÅçü–D\km¯|ÇŽ'ÔÜ/¯UZäkŸÀ爪§•Ú½8ŽHÓþ¢ðß|o–ä3œÊݦ|1‚“úØ•¬q½<ù‰ -]¬þ€2o=:PÖRà!v͟ǃû£Ÿ[:U à߀sØÐȸW\…&¼Výü„ÉþÖÍõ¼ÖL|´1WY“‰T=MNYC‰ݨ4·Œí1øVÀèÑzÃ.1Äñ‡`ïâ~C¨N%+M>‚WR|С¶ º f¡JÊ©#ܹuÝÏ.x«É†Yp®XFdIÓ¬jcÿ=Ø7—ÏBþéÃÔú®)–SU ±£®'÷ø#ñ>У¿‘Þ5Túꦤbmq¶hÑIí ‹ÁŽ#m;­$þfLÎ¥5á½ljàVóD}½$’!ÂwüŸyeNŸ)bdNVŠÿ¹ì5ºÑ’rÂVsí…õ‡¿'ä#YH¬Qð4×:u‡è•^`;zÀ €ÄCÔÇÊ7^*zfY€meßÞ‰ŽÊÎŒ|L§iœäJQ6AûÔ0Nª¬~¤6eŒ¢i”!ÝRvìuêÊ"ý—‘1ITüÙx÷µë4wÄ}ÖºøÇÇ÷xåÆ÷¥!#Ìüì-*wßA,ñ‡rÑÖMm(wð–l¯X˜æeQ 삹€#°€ä%´MA›m°0ÙSmÞƒ¸ÁBœ­°€ƒìe$±æpë)r­sà <˜0#ÑÀN™©dh'ÙŠ2Ûl®‹ê¥F)ç¤|Z¿ÞE'©/šÎ2€=ˆƒêÅ,´‡rھna°XŽ<˜ršÌ!•FÙð†ÖÖ7rÓ7vcçôµ‡|Úün´Ù@%yñ5ØMV­¡P&£'ö!EØ»í¥•Î<˜y¼Èž¸èìAlëÅLéƒþU5Ý#ÁÒòå+é$¬Ž+ˆ%3³E;–)U@#5Ä`ê½÷4D9i¡ºßò“é²çDK—’ÜÜ4Ÿ¶3J±Á"h4ä$´Wr€š‡zI»g¥ák®pÍ&0‘jÄÛö†{•pÞzF¡¥ûånpr{)øÌÞ¿©ššÊ?I[sœšôFá%>¥©§íÝàܹÞ,8T|¦G“÷»n¾ÑQæXŽß£ÎÙ2 ÈFÖÔÀÂcÑ}SŽlS~|€ º^»RM1æÑò«Ôezvç.éMm¾ðX6ôÀ6RÂAøX_~•oÑ`†kÓQ@Ó%9Áœ%Z„I£×¦Ýt²¡âˆd­Ø¸×Šëä°×d?аóú´ý·úܲ"rµ¯¿ð^z£¥m‰3ßÅZ·ñ¼-Rh{¾Âc¶PJ‰®9&½ LÄýè³f¬"ù›û6.Éß„ý¡Ø¡¼=Ο­£f‚ˆ~·»E«âF z£ À­6Ôoá ü¹Ò¡X«¡«£^è×¶2yí]wyò»BýSŮ׳Ù÷iûwÙò»‚ÿZ÷KQy«Ø'úÂË]÷B¾)æóYKóR¥A=·Â+áXöíÊ…ÿ ¨AL}†Y …™¦VùáM¡fÖf³fÛš•œ¡ó“.«o`Á†Õ1…£iÀ¬GËÇÅ?ez Ë¨çiÒ oFÿLw‡" ?õ…ަA¡ÒEÛhTÞO'_¾ÄÈ¿y+:ï¤ùë g8«§­p;0;J5(IgeþH ÇÛG~#pp`åú|ðw[Ÿ»äM¹g"ÄQ%Cý‘c·Ç”¶·Qñ6óU}„c@]<8›ÔòX?¢ðIÑ|¢mbÖR¥^xj;—ÙRËJ­Ü´ÝdZb6#rÊ[uÑÛš’k¥uÐâ ^âøÖ{M¦ÖÜÁðô§ùLqÓQÛÅ»æÒvÖyûª½¦£»ÖÆ'^Ù |ûó·ôž—+Z½Ã­õÆ•«ÕþÛmx+&;$1ZBe»³Å6¸YpB‹>ÉÛ/╾MãNDéŽà.]3ß— àiFBaKq( HGæ\]Æð&~©aob\Æ?íÌ=3˜‚½'ó™„ f$§“ µèèƒxãŠK#N~íï{ ŒhÎ)¯šâÔl=Eq8êáLð;‘ÑOH?ŒœÒ!Ó!>€¼BѰ±¸¨à;Èy»{@–,!ä\Hvà Á!64䯖K™à$,Ø/í‰ì¬âùt!ÿFx;zrº8[\•¾­èF/2>g¿Ý"Ósb¶Y~¹P¬´iÌnøW€Na[ú@z™wT ¸ðñýj/FáÞ\ø½-²Pþš½ëè\ƺo¸#¢8H.[#Ö§nÜnàÈÍ܃ÖvÑvÀH¸©#[»Ù„ãXº°ü²­†ads…1— £K7¦ ×£² ÑjãÛÑWÊ´ý"c€vùæß™Â³Y¨ÓóS›ðñ®)YWm4eJ.§'Úy†òâ$®‰6¦äC$Éš‘ѦóulÛæW,n|öchgÎnº@ܦôøø[Jã`дïòÒ¹£cè#sXšx.‚ؼ °Ž§éVA5àü.7ùøc€C .½¥üDéÂ9q݆eXõÒ .5;»‚­*TæáMÌ¡pß ÿb̰V^](ÁsX~([fdƒ… M-+-bº/1Ð31œŠu5›œ¦€Ëü=¤‘f‘ËDƒÃeî Ú¤é[pÑ·àš¢OÞ5Z‚{–>¹g>9g}RöÚ¤ìݤQÉNndìÚ•˜DÕÈb0Âê.jñ tå(uä´äƒµJ"Ìuïm³¦pâž æ*wo›iS¶ý‚v ¶Î%ù®Uš¸Ù8ä•M4Ç0ÎP£"r/f„ÍüœuÏÖ€—¼©ÐÁÜͰÁo³øPÀ·b}O};B×*ÔH¥!È¡ÉN|lZf ¨LàÂêI¼ž•6Ìp‚h´g·£G-µš–xª•Ÿ`À³Y…Ä-¡šðã>10ø—<šc &OåóÌ¡& xaÎXË…–³d: ÖtL£SÚú‚ÃúÍðÂùg„‰—úÙÛ~23*"–™UP"Õ ©q[Byùù$Þ5Ö[­÷ö'*üÆ« ü>q&mL+A L?Á&±€ˆÊXß|(FÚú”hH\ãK(?0/LZY\H ¯ËÊUlêW›¢,R^šudÑáÑ# ê.­;‹r’'c¤hž’«&wêDÝ¿¨EC’á ÍL漞¡JÈÁ…¦É‹8Æ:CHÍøð*×ÌDzÍq†UÕvgUØðP™¿%Ú‹¾Á§ë´VÒl6 ¼# ­±‡´ñ ÖqˆGpTFÍ‹nח¢ÀÕõ0³ Ói…ÀSqö Dz´Èñ=€ù¨Ý0‡‡ÐôBí{ƒS µÍ÷‰¨«á a†ëLPë@ƒËýXCžÈÔÞc‚ßóúö1ú”3íd’ÊÌö ¦µ"FR‘ÙÃÖÚ‰R¢†Ú,T^ÈÞWRšÔià>ë‡Ü9¡ž?à+—±"TƒLèË_!•X;˜>/a—Iø•Ùš4MNùtÝøÕ¹/óÕóü·»HXÜ-š——“cËpÚ“Õ•Ø´ZqónÞ(­B[VÑÕGd"äd4*-m†ªKð´CźÓU5Ïà!ßC—Àx7Ù÷ ¶µ;ƒßÃê´›…ϺÞLÈs.[IkªIf"„ŒºâÉúÙ%n]þ’cD51¤«Hx‚©ª† dû ª¢ÉÉÒe)¤ ÑçhRñÓ¢<¯L>|©=Þôz”ç[!–¾Ìz3W]š TÍs 1Æ~Òoî[Õ¸FÆmÑ÷÷LΘ«¥GTt½UË[½Ì%Ù#ØL‡4»ý^`Ï×Ñt’3‹"½þµ€ *în¤¦áéúðãÕG‹/ŒGP7Œ‰yÃ$nF X0SY7DÁÇЈGÍ›SR©˜Ü𣬤de¤¡…aŠÅ—GÌþ¡ûÁ0YÅ‘iD2Ï׿Àô!j€„@vÅâêJòä/ÉåÆb„QK€ºÀöå·Î ‚. ãm…Hrÿ(cÎ>‹Z‹ø‰×ç1Nœa Pjæâw8žÏ;ëKÊ!ŒŠÛŸ¡¹TCMÑPý”/¬E°ò#ŒÒ‚ ”YPQ?ð©ÕÍúV—ÆÙk‰Öƒy´¦ö¿*àÒ=ö VÒ¹IO õ(DS€,i˜À»µí0{”!‡IW®Î´ª ʳNœ¯eoWUŠÓ¾Æ\пn¼r‹DyzñúôýM£òí|vÈ^¼V×Ú[ïj«T¼·ï%ú=½sTÙëFß>¯šóåæD‹TsœË½bxc»Dj–±6‚ÿÙê¬A¥ä8F°(˜¸ÉWLëQЙkj\9r òoÛiS¡²UŽ8RZREE•X PŸà_> ê®æ`“nH vˆÎÝ´ÔseˆéŸ–¢‚D{X¤ý2ssº üÃ8lº! tóö€Ëß4p]1cË9ç¬êèn®= gX†‰}yÊwM@Šz8Ë <Í@f×sd\0†à,©Àš1UÕ4Åá¤hÏ óWviOâó ÌYÄL3Æ(¡Æõ'b¦#HÁü˜…Vm^Á™dH•6úQ‰ _ÿˆ @“ v_X ø÷R£hŠÀ탴Òƒ)ƒ³ÃD`§®.겜n’Ü“ ôIRÏ%î)DÐç¼Í§È¤×/›`„™²ñŠ*” ð¤¶ußMõÚP'Ñ¯ÏÆ ’K"ƒF«˜ß·Ý³¸õ)?ªS’¥ç©®‹UuçŒÿõÒ¯M\ãµ<7(­$%Á:Èë ͱLZ±·ðÖLœJ\š„\_$Teùþ#Ãú Æ+ ¶ a“AEÔTDà²Ô¹dpµÈk(Sºg”0AÕ¶¬Â½}0hÉô·$+¦gH?G¸]l*Á¶»Ùþƒ†ú.ÏDUq(LU‘*à¶d€ÃG ˜„IŽx|2Ú$¤»j%õÂ@“–ª:·²Sv!QqX=чVŽôúÍ7l 1ØÓäƒ[ô>÷ˆ¨’%l‚;¼ñ„4Po!ÔUG’^ôæ„ú°Â ðPƒåZ;Ðmvèçü "ì‘xô,ÞdþrÆ;S}Íy÷;@$G¶¦4£‹(cˆÇ1—Øó¥+U•ôä¹…·ÐdwÀÎËQŒˆ¯Dã¸~ðÉ—Ó˜ÑøÄË!¤Ë!ôy âmRìaLnÜ_ì¾I˜?cËbFŸF·Ö† Ð ;¤ã[-ö…–ÛŽM7þ‘kœNÊ+˨W§Èä}n}–t#ja'—ž´sáó ‡,fù­9ÿhI™\û®ùF%JÈ? ÍL5XÁ1$,)£< ðpê­SO"Â1ÈŸ`°ƒ¬÷B3ÝÚ¹îMÂÍRœOJJ߯¬&…SÍæ9þNÝ¥òŠP‹EA ÛÕÇ›20NTtúg ÛˆUݰŽô4默n.a² ñÓÊ…³ÈµXú¸êOŒö]×ÎÃgÙæ™¬ð|"q$ƒËXý³AIÒ™˜Y8Á%lnfQ„x©okͯ› àE6³ë¸›aM §~ª6(„LQç$é:üþ\)‚1BkU•… q­c$¥•˜LÒºó‚âóèÇGuÀš²ªF jºÓ)^Ç‘vù/Ÿòâᯞó>kú›7BR@ À¸šÍÝ6Ú±e{¥]ÄL{£§ˆ8QçÖhª‚ø\ÝÕ7 pIï 6ÑMÖ°èlù]é²¥¤IÖ´ŸÊIžé’Ä;ƒDÊéN#<؈}4?‚xÝø=ïjCèR˜Ûá7ÁP™1½FSÐý­³Õ ºN¾I|){w›-¸ãƒ Ô«K5çw¹÷h`ù¦yë±³ËbÐCõ÷›cñzÐ1ÇpÎü0‹5Á-»{tˆí¦Ãà1–y=ÿÃtÅb[JvEf¨0­¹ÂïéÊ"ÔG§ct0œcö+ƒáº„ìp ehòñÚ„YžgéܳÅÂ/ý36ÀP|åÑZ㥠éלkì « n'7$ö”ûÞ3Å,hbÛ0ºéV3­öœêâƒÓAHø,Euíª\ž–wŒÕïƒâ‡È|Gw/¶ºvI½ÍÇ  y¦‡¼K÷7»3}-çáæÓžÞFÙ^;©W ~˜C7ß#q`£«¾Å‘ få> i¾›q"Ñ3˜%WD¢·çXUÕ_D¿Þ}fýjâH91ºDqõâ>Dü‰É:~Þt `¹µÓ(y¢™XÊçj¥Ïo¡H=%MüøÒsÛ¹”ßìÿÛhÃmWXJ)OŠ8„øm6&ÎÆÎzÿ«âÐÄQÏÆÂÌÑÀÙÂÎVÏÅÙšÎÞ£FUÁ©ˆ Á÷­nÞ}–´CÚ‹]f >[¨všs=(ÇøqxœÀÈ@AbØÁÏGMœƒ¬aÉY“#@uÏ·ëË¢„¹Ý¤b?±õÝ5p0Ye†ù³fyßÚ Y÷dŠŠŽžŽnš\{YaÄ}KVC’Z‚Ù¼2JÊWföÆTÔ^.DØ©ý™®,6Ç1´p0–]>WY[€˜RÒJU>޲µJQw”XEÒ`j¹ÔÅH€'ؽÁ»B¹¹îÁÆÇ|X¸Ô»ä²êééÆõ*!µzLÄWCȃ–\ ÂWpð&±«2_10ßMZ¿¼ 7ë£Ø8„º”Îÿ8ð0û0óN•a¤Ò„ËL‹ˆ¢ÐXšj@5æ’´Ê(ŒK¿S4¶Ïßþ ŸÝ‘N4üÎ/o‡ö` Έ_3tÈ”QÚtÁ¥Æ ±wÇŸw0Ž¡pD¹ûSz_‚)G4ó"›–âQÝIH|•ÅÄ*ÕP[e7>[p6¯¼œ§¼‚"cy$Yœ^(90EºÅ2Þ.èqóz„Ìé”»ÅÂäþ$I¶ +Æ0¯'JΜóŸ£6ž#?‰fVH*ÙÂä³<iݽBM€øüœ­{.1% [Ñ€>­»Çd&ß´s¸Fj3¾húmþ9¡bë¦|©Bu<^hõsùÐâ ^á1at¸Ò있GãÖ(Ý[øSÄ÷–>HRǪ]xc#<Ã"¥ ¬]¦4 Ði uî£aü6˜ÔߦÞçO.­YÒF³H#ÇNæìêBøŽXs …BHÉr,(©Š|¢ÅýNOoÿÖ}jùüˆ7ýž#²lPOß×T Aʲà ¨— x]iÉ`¹„¿_Y=ËÈ[À©^ß‹¾èÉŠžÚ|eõ) ¶‘¡äzÀíTyŸßÇJ×÷‰Ù"y‘ÕöŠ„ÅÒF!©07I…äsé°c×PUÀ­„W:p`Žmô×Àñüñ¾å„§ŠÄ€ªÄh07Î@ßÌfý y¹¥p‹˜™b›öGTA+*?f¢sÇrdbîYƒåŒN™ 4‹‹\ÛÀ:3~½ÐzÛf$‚djÖŒkˆ±Ôo1÷|UÊE…‹Ü§JçÈŽè<4¬«í,½<‰Ñù6QˆfM³Í^<¥Y^'žŒŒ…Ó§Ìg”‹àø{–ìÉںǢ²}½€jj€a45r‡daDz³zaú¦æ'\×ûõ„lø/p{kNGd—z3¬r> ïaW—ž@ä§¶¼·ÐïÄçu íä_×O 5ÑŒÂì*=ót¶]p™ë½Ð*óĺء©"i5^ž¥±¹:Û(lLc7ˇÈYº¹ÔèÌ©àŸqRég(Ã-O_kKnŽyöåÞ/€Ë[ÞóÝ>Á•OÛÙÎûÊÁ*ûáËûÁõŽz®–u/[}ƒÏëY?¦ï¶¢øæÄY:|ñºoÝ}3o)/½ËâQð›p·Ö–èýCéﲔÚZL§Í=êñbåˆ4Ì/دÒN<Å@"aÿÿ@:£Õ[ìÿÜó5Ã1ÙÅb êepƒˆÕÐJ’y¢`‰*¢†š‰à¿}ç^­.³0—.8XK’â{(غA­{æ·ƒaŠ~½Žá«œ‡ëÛëo_f§~«t?F›TÎwo`ün¸o§SF¶éÓl'ëL³!ïJÊøÒñP‘Åãt5Ã_(ìZ¿X5L1›\“Sµõz¤)ÐÓƾÉP•Óµõz0ÍuƒNR¶m}8š=c8š¾¡l&l[ƲA_PöÖŒ&Ѥ®éŠEã0NÖÌk&³²·ù*_ào·¿À2¥1î &˜Ô£ôGi0M»çM.Ë…ËYcc v˜KÚ6ƒ÷Ú%Þ›ô²°Â»¨¤Vx¯6T ͺ­œ€ Ü6Ö\Çû„r€¦_ƒ¶"v7š–õÍK‘¸›y¥®|®ðê3ƒâoDoÓ‹ŒI½›½¿KÑ,ø(Û VA5–Ó¯bPðßÖwS£Ksy¬‰vƒp¼sG@÷ÈÏÉ ”%OÖšúiù”S8]ã¾T nÑ:†Õöe½U÷‰‹ ï™Ÿ°2¥ØóÃpA ºA–KNb¥G¢ŸJñ3E¯W«ð§^óžXÍÓ$–À —Ï^:Œ ð› Ý\<–cÔ„\–מL±œ¡cr†¦šîfS«džòc[ ù¾Á,n¼eœŽÚ%عØÓ{|_#¹oÕ”äI¥/O<‰Å-dþVKp>ü0™M Á¦®‰ ÑÒ{9ÃýYN7ž1¿ñßLi°èx"гçqTòÛøöYäs%QüòˆDÓ„Õà‡ñFpÏò¸/еŨOŒpÄÄ'²y³lÎæÙ ÒÅÏt¢§_Nĉ¾î§`I')¯¨ªûçݫ朡¸~;%³ídˆ*Î鈄ՠsÐ}3_Úɬl·–(.³ ë4)‰}Wœ O-jï‰áÈ0—(¬rˆPÎøšcÛÓ fº&8/Y£ðA«æ‡EÅì˜ë•˜¡*ÔóRÖ´ÊuJ>Ý("é‘äipŒ„uOâ?ðÐèaò¹ÒUjàz8´°ùK®§8U i Ãi 7Ëe !M5j>ºg˜¨Ã¢8 ô%œ…¢J_([ä#" ‘2äf"iˆáC>ÐôHJ"«÷—´aëZS†iXÐ]ßÓêöåéŠðû\D‹z‹éªÌS¤Íö¸5ùl¿4*òwoD?”RÈ]Á–ºƒ® °Ì†o}.ÌœJàÑ¥×ÝÈšˆtwÝ ¼QÐí¾÷ £jç§0Ø«j¬H’ج״h¯©ùžÿ+ùÄâUt°´²;qý Û›I{L'd8 qlЦ³û†ýïòŦìß t0€zdÊÿLÛ¹ššØºþŽKÔ·l·D|ÎÔ`ÎÑ!̹áöA•µlƒ9 ¸Ø V—7S»¤&,üK©W‚\26˜Íj¹Ÿ!½Q 5EÁBýÎ"‡jÄŸÈ1è<,³‘Ï@j[U ;˜î®¿•„~$+¥©Lƒ¥kk <*?€PÓQà FQëH€ÛÏ$‹¸¼ïgóÅ l)¿ø UOãëi‹ÿÕr BÀ&„sÞ—Å› CH7ª×³×#ššÌ$à zÓ”ÏÈóÙ)(Á«`ö¨Ž£—•eÍŒ¥V“¯<²Ð¡¼\ܼ[²<ˆÝ%Ôó>ÒÆt¬æ¿ëuÈŸ_¡©‡z ¾†ÓµVøÓ¢Še$ »#^8 4Á*á(  b“â9-A©ŽÀùØRé·«CÝ·5Ü Ò ù]Ã#x¿y|ÞÀá…>}Ø;²ã}0ä ­8;òöbË‘¿ß€…&ÝÝ›ðtàŒÉ`þ&SÆ‘€†$ÐS˜òÑ%Ð@\D‡!~¦Ð9µ×'[Õ*:ƒ3Õ"Pf`]Ð"¢E0´b=K•ñ`÷x ²Á6Õhl0½¿Çʱ‘âu¸½Â±ê¬¹”_WÓAqN¸<ç–YÝQF*2HO´| ›^ËÕØÒ¡ ‰{4œ+ƒƒqXcG-kËÛSj¥Á›áÏÍQ£Ò|¯)?S±ùªÀbzBg¸Gˆƒö/ÉûxßPEWüa´ä…µäŽ/G8Fç&׋uœ£X6"9—'D‹IN ‰mÏP̨òO¦•ð^jc3©:?ÜãªîcþÓˆZl«ŠÖÇ,F =¤5ãšyú!oÇòµ‚hFžnî‘ ÕõçÁÖ‰>;q‚²¬Ê 6·‰KŸ¨îPªæÈ3ÉÄÏ^t\ñÀÀçÉT~ˆo˱‹ Û6»L¹dƒÆ¨7©Ó¤¾ÝO õ\ëq°Ä ƒ¼CÇøGH{<`÷  ¯ƒÔ(¿nc‡ ´8•‚8ßýr~­½ÔG ›z‚ì¸áGc¾þ8ãÆ,ºÈñý)eó¼qšapn9H†²jÒ’-Š»:çæÌ‚Ò¼˜B/xbëßdœ[à‰[Æú#-íu„Ò©õõ>{}ñF9éréÂIîܬ}йs”à•œò¾©6Š8¹²(GÄ·%nÓ¤‹ ZB?@)‚‡„ 숷œ/C4Zä6~ר±Êö;v/ÒÈžÌ5רþ¾“ïY&ÿ½~IQF2‘)§µÏª×Ow°ÜÔÈ*¶–xº¤fsÅ…èÛ!U%/Y™‚³ÁZ½íˆQ÷F±ª@ƒÎ¤b ‘XWE^¢tx°ögáHB 栱ѲkÕ¨C3n©nÞ:–¥"{ec–+ÌÅ…¥¨þ,üºƒIÕ cøsoBîÊטk½—œÔ[µy7À¼åŠž\¼æˆê rÏJÝ,5‰Ò†FAˆXâÇ!µc—dMunïÑ]A~EËuì÷dA¶soؤRþab÷ðRLžŽ‘Rßt ÷hZÍTÊ—&¨ŸŽLFªØÌ«lû÷Û°èÝÒœÂDaÊY PËäº n2l ÛŠÄgü{êÔvuh­ra^W“å+#Úœ:±¬‰îÙìõ´šáZ‚=Ÿ£ÈÍ×´òg€“7—Þ£ƒÚ“÷æ jœ/¼'ÊÈiòÅÏÓ¨X¸è_™q5A­U8ð÷{ïdÊÈ0«ÙèÃÖ«£Šâ –£;™_×r¯[Æ5]âÛ@‘ïÔ¼.¶·Oɽ~9@Z# RËÞºÝ;ªÁ³ò=þÀ5s­èÿBÉÛ|”¥U7]6U=€ÞÃ⥹ñ?¨Ü:Mcœ*œ“M°.¸÷Ç´‚òJ¯’Žø¤ò„tjZòµ‘^ÑÅé²í ¼÷f 7÷ŽÕzö<>Ï—p“W¿bÿÌo¨uÆ3⺠v g¤8GÝ>OÓü£ÞæŽnûIlåWžÁ0adnÆW?_]80ÉW~“º1Bì9}5r›»Cÿ‡ºgDóÍqWü6Ý{⾆®ó¾R wýc¶á€®P¶b»öö~ç0»jJ…@õÃ.F-jzNGâˆv ¸Ü®‡¶l^]æE“(m¯¶¨."¸("ãdõ.ñÅÇFmö¨#XÜŽ$1rÌ’ÙOSeûsCÈq(zAð;´©Jû-Õ ÒUf¥^Ÿk~ÊŸfM¿m}|_;zwyû5¿1à ësÁýþ o2™} CŠ»zA˜¥¨ªŽ’¨¥W 5õ£ÉH .ìúí€`1kæÿÖƒ ˜q¢er@—Ð`´x*`Ø‚m0Z ·à‚»¶Cê‡àd‡nðüÕB„îW ÚÁuaƒùêà ñGÛ‡Áv¿=ßÓÙ0±‹ä*ð'7¸§bx¬NG%Õ-¿µ…Õz²êG`j4hê¼O¦ÆãÈV¯(ŸÀÁVÇquÂ;ÿè¿sœaµ¥Ù8£JR‰1qF¢•±Ø"Ò¸:CaíNÝ‹«8°¾ÎÀŸôêÎpŠP7ŽMîB>éøè›s}9œUæ _ÚÉ$ðGn‹Ö}iŠä…;ÿùnçñG'ncŒ×ûF_NZýò† ‰Ú26þâgK£f² í;àÊò7Þ•OÁºÏþðaÃÓ æK2È/[Çjñ!æ;÷œZêe÷pü |ž[µ&ã…EÉš/ .(æWºCÉвÇçäeÄ!öEqœrßÖøß¼K¼ðIÙçì|ï;ž>Þ¾ ™.šð¤]ÈÊ )Š~išÀr6\^ccŽ9Åÿ’ÙúÍ1h–.VÞ):5ÝA1ú i¬AêÉÿƒ´ DrYéë˹H®7Þ›A¯}Ñ€ªÓˆ V¨,¨ï73â¥H ȧ ‰29ÿD‹zfcZ`Û &‹E4ðæ°Žst+Mp«üÛV(KJVœãÆæ” Ú´` X¥oxi«ŸÉ{LaS¥-þìÔ]¨êÌo¾ˆwÁÍb·‡pEå{ÿ'KFdZÖŽôСès H…ÛaÀ%žî1? 6;­r©9ŠWÇ!(—D¸¹jRïƒJ.È_h~Jýƃ«ˆ›uÝ#`&±|ü­Ø­Ô™ãÀàô£~çam‹g–Q‚+šAwÖ›}˜Æ¦L„Ñ”]^-÷í /ÍÍ‹ÿ±é»´é…éß]ªÐåÄèìz_{<ÿÁ­ÈÒ_#‘ßA‹¨ª˜µ[›€=»ÖÁG?àµÔN›dûÄ$rÿF†1¬Pz/ƒC検^$xÉ99­@i¯œQ$ÙÉQFÖ§ô>—Ï)§ÿN&/‚a^¡iðÊ5§[–Ÿ[×'à3@”–îÏ»FñȪöqÉ©‡Û=C´·Jµfü¼JÕ“°Ô´ª»Å2…5¼‡±žg«8û°io$b×ÝB ÈØÖÔkûÄÅâriÏê«ûsÚfŸ%éoy‹ÆÅÙ DÀætäÔ²pA7½à¶àÀn }hôûI?ºeý‚©eïôD»ýƒa«ÕmzÕ»£åæVÃ53j±þñYñÁ&4¥à“ë_öf@Žsα9½v÷0ÈÇñn¼ïÈ]8+—?ç7ÿƒF¸=AÌð3ËÑ¡¤¸Ö€§ÑÐ_æ'RJ1fæì­“+ÄTBòü²k¨uNKãxKš@<¶ÀtãhY6_lJú9{©E¸‰5²ô)«INsøó/ÚœûåØ×Ó€Ld”¨ôÍÒkCϱœž'ÊJÙ’´(Ú˜å•\bÚS#w¾½ùÞsUƒ@›Ë»Æžœ”&‡å`SFh›ê%!2M ¢¦÷­Ïõ9ÿþÅ)ú’ÜYófpn®nž :×[~ßÂø¸ÉŠãå>RÀäZÍâ¿<*û `ˆïò4N n+ód㎆þ{!í7in›F§ˆ(wÄí…Ó¡3þf)ØWü:  ’·œ·pöëzèj²ùö@ñë ¤$OÁ’‹ ©FÐBBo–ç‚oZjµ |vb€>.{`cà ¡$FD+ÀXé³þ‚ÚK7ëùLˆ°•]ìuÿå"bgy¿[Œ-˜íÂYòæ•è"sŠ˜Ç&q{wÆbÌ6?X¦%xgBJÈ 9ÁŒŸ€rë„2yúæçÄ6á”Õ°.“T—ªFbPPaúNÙóÜ#D lˆƒøÍ¾~s_B•Ãq4š%—‘‡î’ÚÍ ÕÈØœX-QÓWAR9 ¸VÒ4ÃÄÃŽ©&IªÝgo+NQj]™ë;~õ;R•N-:rƒ[ÔÉ=Zµ"ÀExi›Yó"]! ¶I”G4¬Ž<ê÷×ð5wÁd¸˜zDlìÀÉ9[ÞSâßü‚e¬ÑRÈTóòë_-SF_ÊAµb%FËÌ-yóÕþ æCñ{@F ú²HáÏ?»’ûK¦á¹Šñ1r¿êù$åq‹c·à‹’ÁçF¤¶Á|bÉKÎÕ0³ð¬}†R|™?ƒ‡@ Éõï…ø¦ªJ±=ŒjÌ0‰ 73Ñß:hy©žd4ÆÀÕ&=ŧÀL_ÿ§?ž§F"~É#(òHk­ËôZ6]Ún®Ñ6™a®ÂÕ_öÿŠIò_­gÿФÿ3*w4qr6°·ø—·`Y9r@ ¿€AÈ*ÒÊWD%¹fKüêH‡*%Jü$9ÅœÉ ðŸNzÁ¨~¨hØç—>[f àX]Ñ}Rç§ CÜèÓÂŒL©9¦æŒX#ÏO4ØÑÞ<¿Ø½ð?•¡Ý¡‚OðÒŠ5¢‡93Œõ‡ŒBL$t: èdžWušO´ˆ0Ĉ€)›éHëOQrÀ•ÑìãB¢^Kê6H>;1AqøÏ¶,€âŸåk.|ǘ c!@){á\ZxðÔ…æÿâu¤r·Äÿ^Âëî5`"Z°¿6£\éÜ¥û5ÜP)ov]½@ jAãP^LÔÔJÄ&ZY[,,ßlk²¶dôD›>œÝ{ófû™nŽ=xܻ튖œ=ùsÉ«ZÜL¯KA\U"qNC]ks¼<ȹ¸¹|`Ðü|qPíL×´ÜÌ>AÜCР'Ë5i2ß«Z3f}Œ›“kó Zoǯâ8‰Iå·ô£[¨ÚåbkâÒ•?Í×(”(ƒSí!§Ýžª }Þxì§F‚¾ hȇ„üiÆÐ¸SöêÍpÿ½ÁW$„ Søì÷·´È6¡9>uI¶(ú>†dàÀß¹ «šµÊ Ð dTRØ?“˜£+k/†ºï×BŠ•@4 ®Ä(òµÄ,†iºU…a…Lš™f,ÆføÐ¶ø&Æ:‘-†}‘0¯¸‚\ø/,ký»Ä™!×#s‹ç߈Œ ‡‰×ÆÔA„ÀL4ñ_º7„ ë‚hÌ;ïNhqL˜/Gù5%RŽxWãÈÿ­MîQ> d&Ô6%¡RY+د¡ŠÝ³i´‰r‚Â÷"»Õ#œ7W˜gÇ‚}â “¦%2}¬"‘D§‰_%=Ö‹t5¤¢ö оA6huÞC SL㤌¹Uã×Tbk¢¸6rÃ*’-• 4!Mr¨cE!C ZfM¢ÃQÌ^ðýV¦~»â¦@EÝ’#9boJ­M{Àî-Ì¡B{»Æ1xs®âåwiªF_ƒ*4þ¡CZ ¼5:`œ_›à0×¶>¤¥ì¿Ì7ᔑrE.’ÔAþšf]ß_F$í v#oú¨çÞ‚7-äEc{U8<^B.µš*Ù]¢r‰®ÌÖ0›yBûZÖRס·V׆<Ù=›®íÓs«W.ýºé-‚jp%bDj#±¤<ØÈñ ®,QŽ]7†Än·#V„«ö éíÁ—h·¥ëMÐQ Ðdz9÷K (i¢X°Õ±/–Fy—‹ ¯@·}×,ߛ͕”„„༠HtL÷/M_|qỺ.ï-ùèµ qWÎôLy'/Mk=ÕB¸™éï–Îåû_ëZ²*ÊÅÔ¤4S³NÎB­¦ðÃÁÿ"µÍœ™<1ç’ŸKCƒñQc¢¶àÀn8󳿞cy+´_aeаN:¹‹¯ˆÎ¤q–1’&÷Ï•:GK6ÆBºXcÁøQ—DMfßûæ;ý5îØWG"·–ݱkÜ{w¹Øìèpµ­Ë¾Ú5¹{œ´ س­ØŒéö=:= ÝÞ~Þž#̹Zyìtº¢q8cÖõ%»%ýî¤Ó?DÀßê>ÚtàÒÏÉš’ZwÞÒr±í™kÃ=ÜÕÕóû^×ÚÑ~Á:?,ˆ‹=…Ds¬š[}=*áœjÛ·.hv/ùá }™B¼1•(¡±üJØï/vÝÖÚ:@àsÕ¯8«”Ý šrU¬·ÊÂ\ÓÝ4ê4Æ.ã¶IÙoœM7ÖõKÑÀ Â'6ôB»M†EŸ;dçDX§—qÛxÄÑûpó! ãóé ¦{t®óœý^Ô þœ]£*¨ÒŠ”;Zè“¶BÕDÈDÈ]Œs#á,Ö]Zn¶1lJ ÞÞÉ7½{MJ²_‰ê°t‘Ÿ•©â2¬íØöÔDZµÄÒ¡¡ i™8R*cS'­‘çÐõž)'yDèZñlì.€õYoãÍRÍþ=T*Z©Œ›¿)Ò+ªNôq£÷/¦ˆMV5Qî3|ÃÖMús;õ@±®P—4ñë«/&‹=×§·k›÷¡E Ùdžþ\;æÍÇÑljßHX_Ò>mŸ‚f§†*¾¤u‰üÅBUl/8øú6AÜßëP>ÐÔ+®OÑôò‹­’UoyåÖï:UÌ;˜°ÌØxÓ—@^ØN°¾Ðv\pßšáH$YW¤í®å/T‰ã°=wiºn[T;¡Ç„©ïÑ ùò‰¤ñý` œØgw{®8=LaV‘ð³KçRü²UÐE§aà>¦ÐÙuÕª^‰­……Òû•’D¿}(îgW[ŒbR» ðÕã…˜™šã¬±M·P¹ÓÃW~.,œ\U˦ Â6ªˆ…_»LÖ% V°øPÄ B½³{¸IÅÿ°%>K›J/Õl™)X^›jÜÆJê$³dÓèÙ Žž™Ê̼ã2s<ÊspšzÉáþýÙßûrÌQµ,á&•2iÁúó@—ì0n,L0¡ˆ0=PñL•Šb{òìƒhKûËó‡âÍS½@•|:ânÎ¥ê>@67õÿÕsÑh[ÊG¬Å¬/‘F£ÑzêÑv,"Q½*ŽtDvƒLÔ! Q‡xÿܘ)ó"E,3Gcîym]e–‡"R©²Ý¾Ã1g<"X{Œ½Ýž¼ûͳÓíµHÙ5ĵõ§µìkæ«o­gÃöûv Ÿ‹æcjðŒö}8ÏÃéÀ†!¯Ö#Ç w lËŸmˆiúŸyꇆêLCLöõYzžÙ Пoê¶OõæÛ§†‹¿ ÌøÔ4ÿpåÿÃØ; £ÛšmÛ¶móoÛ¶mÛ¶mÛ¶mÛ¶Ý=ûLܘˆ;O祢¢¢ž«òË•¹2ÈÚ(^%l}A *¾1r`›ïÑhãëtÔàô•úÜ?Žù->ÎòÂ) å¹>Ïà¶>0ÖüœÂ)Žâ¼<ÏäV”·´`,ÝK[pŒå™ÃIŽâ¼=4–ñ­80wqyš·&ߌÅ9»Ã õ9;4×øÖ”ÖâœòTW~–wf; ê314×à–ÔGChŽé 4gyÞiÚGEPoiþiÚGGÞÊ<¢C Jsuz:Gu6Gÿ¶:pÖâ\äTWaÞÖ'¢ª³¼ -ý+4Pwe>Mf¬. Û3o`ï’‚Ùg¾½u°Êq!¡Ý¾ƒþZØ‹Óü²!ÜÜÚÁÑ#+‰âíÅŽ¾®&Ë­ú«m¦å„»êP¸ÙÓê¤#ôw‘w©»§³vt|Ü!Ÿöº„!á‚qÈ}.*„àþwÒQ²e”ð|ªÜÔœCèÄQk7Ú'Ù [ôvAS–°–[ÊýІEæ5˜šŽÉp‡FG€] }~fI®L›˜©Zöt%iìef=¢;ó~PƒTÙ­ªŠm ;„¯lV|QÎÐ<nŸ„§8`g"Èmú'2B/S\9ÂÎ Z–ieøî_“Æ›·›ç(WŠ}9®§·Ë‚fvÞ©–ÖÒkÛ"yyÏ0ãMðŠ%†ôZ¡|°ì¹œN}…ÇÉ&ž•†î>äWXGÞGÅ×[o· Ÿ,Üú¡ºw×ÈË¡å¿Aþ$2Nˆ¾qйRN^ …‘ZÏß\eÐa¹}Ï.žÈÇJp%ïÎy>TòÄ<¸IžýÚ”²ØV§J74¨„‚{»¶Í`A»êeW}ÕK¢iòPçâ6|¡î¨Á ‹½2s?N/ãa;`f ñ,~*ÃYkÁ7k§©òCÕMº­®¦MJ¹Œ/WY)Ç1Þ º* µýÌê¢,‰OÊ™öTÓÈdQX`Ù{RùÝZº„—P VÙ±ª‡·´êl½iS¦¸ËPºâ€Uøÿ»•|{qX誀Å5ɱ¶J0»}f¹8 Rš¶¹$>jÓ¯KA¼6¬.Fn˧£³ž^\§Øßruªg™°ܾ)QôÉoÒ"® :€ÓÈŸé$Ʊë×â7Ï£3wVÿCÓYL%zhØõõKçeEø»ù"ƒk(+Ö’£HÍjN2X€ívÏ4 ¸Ìþ¾´PùwjI>E„&ˆÁ÷j‰Æ|]£ Û!é`‹ªæžý¼£V?„:¢©\u ñà·’”«MV*,ܧÄucĀެrÑÕòÊÜ2ôæ;®œo*Q$êgÊÀm‰ sùÆ\ÒíœZ*n=—Û®oŒÊ ‰7UÝ…g wú '¹Òö¯7“æ,Ô©ˆ*'-è‹ú.´µ/Tð ÝU‰UŒtõ°Æ‘wcØ`B~ ›j¡«©6‹”•{'Z;D·ºX•Zdû`—Ž£DEUJêT熭ÖM¾{v“Î^mÙ ™<U1{ óìz˜ ­®›‘¿‰ßéfrúûzûz;%#"ñÂn Ùkê6&"$´·žáJšvvú¾âcòô{¹%CÆë²²Ç#…ÛéÞ'5Ú¸u›¾“‘‘Ÿùȃ­…œ]'«FZÇ©N7ëp¶åãcddcr0e#bñYæbv“†ù±…·Y†®ñ²°††Ng“‚zWäëyê_¥IœMHéýÀDNÅY3Ošcù²@9ùºœÆ¤ÜA”´[ƒ¸pŒcj8.Søœ²h†@#T¢àR€ÄrK+Hº12P¡g%™>üQí»Uô8Ì VAƒ²¹zëÄ+ \ £z³?¨ë7&³lµ/† '+70g&ØíuOPª`Q+Õ½%`çñíuÈ}ä$`uæþ_ÿ'£ Ù®qoDLÁ·Ð¼M‚²Ž,vJÒ§Ô¡}Ú4‘±f£°y…ÌiuÑÁæ8æÆv0£q< [XˆìK¢âZqKaA“b=¥ì˜fCD‹&òÈ °1°{ã›XÉúj •9_¾IÐy—¶~¨­K3 nn¡KQ:Ò4y²ïèr¨\é߸é2ƒáøñ‹¾­w!ÿ~É8äè-y¬wE—ñW<‘͉aà‡úi¹þ>/p©/ œë´Ë)r§Týs'Ï›UêI×!¨ôãòÚÁÍ…€Ì¿EΜ@Is@·»ÌNjz|«S ¿µ°$_º'~"ßÐÐ55ÕWŸj|¬Ï¹Åã'뀢¶*²Ä*Â5³Ümx¹'â=Sò‘)wk!ÉžgVV{RH“Iä4Î͉aStUùÈj–Ìxc¿$€œÀ¶sëø`÷‡¯½Üñ»Y˜KÇÖT_®îÞÀ ÍJ m!Š–ÄÍJœ–—”p£U›Ÿßß5óRÂ)6ѧ^S'–<±S¹â×uuéΨéúù¯ñ#…LWŠ>ü9žüá2k%­>®HÎÌp¾ÿeÍ 8Ëž§‚ É¿ˆï릦[ÐbãøºÑAÛ+Ái87g >%;œaŠ_»!í?Ãê ²J9ÇÓŠ‘Oä4jQ2ÖÆ ó™k/Ü ØI×É‚6ŒAîó±¦nŽà3VÀãþƒfïöª.g×v‚³¾‘‘ ´.í:ne´ùÄ|â†QD2Ütâ+äªïDòb¼gOr=5dçžòytÒËú‚6@ùÔÐ{¦`'XLX¿e±Æe"ѹ$pJÀɦåQAVMÌ‘üT‰›—£ó”‰D|!¼‚+F;Y*Åg>ÞHƒÚòòù±ª‚ý6ðÀ2/‡K†þ'ûbZEþ=…’–ÛÅ mT>.ølóºÐ/VîXü„¬ìð Åž²BÇÆy÷ÌïeÙf¸giÜS®¬ÃÕ¸íH/5içrå8/¼gûÊGŒSÏ Áo (k{ÊÜ@ ¹©dN“Et€2!íM?uw_m´}ºûHWì᎟0j3¡N ¸Ÿñ«Ã_·}×à¤ï÷KÝ»B½íyz¼stçý¬Xô%-®Qp¥s¨”ñ S¹#*XEI:&Ñë„@›H¢ ÌÄÒ&¡C!墵ñí4~®ˆ8t½0ÓyŒ©h âQL#nWwø< æwN^Õàד§ùÓ±OÈMð:b£!£°|µïÿŸ®A¶¦!Ù ðB÷¿süü?ÕÈ-š6NG­ÿïÜáþììs÷×–ãm%îÈ)–Ò8ã¼, fœÈ`’Q”¤|ò¥¿]€b¤dç^è¾p"ô(WPÙqœÓ覂¡½¨ìÄ+ÚÜ {¥΋ìx©ÔžãKÚñ¯¦§;ØG©T3xáiE wÜ0–"¬…ìpVÒ!ˆµfï%ò<óÙ€ìî·YqxÈÎO•·ç»þƒTcäîødHE{(CÙT‡>pdâÙ‰ñ6$p‹ñ‰64«ŽX3ÃÀh}æhÖ„@ùpþ|A~RI± ’ÌÝ«ÿŒÑïvmÚ´èã=iôz¤PýÜx{qdN‹ÚêqóÈ’—þÐà*étÅãx ê(“ô¥ÔeUdrô)îØâD²k?AlZa¡´ Ó£xtQ™T)qÏ­‘ü;“ܘrÝ&à‡>ÜwÁñàºòJßÜŽ 8²`ÉÝŽ²¥Í޹:°öbÊßß`ÈûâóÆ’ªø ô©ÒCùÌ8'áÿš‹bÌœ&kÑd …›±D§9@x–T©+4Ê±È â&.x#;9“?í'g©‹³Ü—*æ6ïJgÄ$m¡7¤2¼ßjáÈ”b"<g{™í™€u°Ì)©þtwc_r#Ú¹¾]eƒ!Ž«ÂÂ#ÌAr °xäÀJë$ú#ó dÖ¯@¡º¸ùw#8 ò‚ÐìÔ?ö6Mo”Ú`€ S©C‰ªh"ðÎu{;I=q”6¥%A!G3Ìì³™†Ð`žpN}sõ<çý9+¨M ‹å® Õ­Ù½I6Ó€¡ÒÊÖïJͶ7g“é)T‘{‹Dp‚ü}Yõ«ü{›Kµ˜é†ED ï?cþÊr/Aʲ‹D&cš®SïO¤x`㽌ô¸Ræe4=/Z6’“ÊäK±”ÎK-z¾hOØr?ëo§o¨wk†FO_ýò`òI‡?Ú0çËôÁšù~¸¹ºfÍ•\×Båhgþy^øãèÅûH:˜ ‡G_öþôœË*ÞêÈ‘¹+M[²Ê*^.ÚêÀüÊ‹¹5íë?êóv }æþ~8† ÆÑzjš5wtácƒoÓî4IÅŒRl‚={ »¸žìÃÝ ƒ\ôi?Û˶ÓÀûú:ÍÇéÜ8õtçìÄ”¹ÍlÞ Åš®Kf8Û½˜8W$ê{Î ¶¹{þ§É^†h:úö¨R†LÆóÓœaËe۶Ũ¨ˆŒj€‰£ÜNá—†H\%F Þ}dÕ"¶{‡™í8A/à\"w êCÔ Šh A± Ç{¢+¤,Lom‘o Œ]ø¹·\ZÇ)Ò_ O>- Ђt0rÑV-^<ØJz&\bÊiVÕØjjµgNQOt‰u¬Â–Ä~޳'Fýá·d\‰þûò·¿RèÑdå¾ÈMz,\ wÝQ]ª.Ëá 2OÀ“ˆ0Þ ©z¼èŒ8‚¸žº±¤ Ì /yÓ¦l´þ¡Xvø-"ÙµàQ X‹ ?p9Œ »XK7­ÑÅKIZ·6YƒÜ3±g…kœØ¼‹¡Û\^¼ô\B?˜Ì?¬¡_Ð\ç(ã {šÊ×Ò‘§"ògRïE`ã›Á Ÿ ,’Ýßvd|×N`·*&”˺´M×…,…F¢û¾ƒfs_‰éc*$E–ÞS)~bþßÛš¨ŸÿtKƱ ÌNÁŠâsçŸ(}Eâ#µñ H¸xúQ 3|éû-¾Ùß•ç~¥ ç:ÑóùÅŸM¿lüÖ€€‰×‰Á¾øÒ‹ö﹜°ñ;t+æö (oM“P0¬³É5å+ìѳóÁ›än¶o@Œs1µ:¾Y!ôãØÇ¬¬,¯ž‚ ÿÔ.ç ÁÈ륊í_w­f›Ÿð »Jí((ªË{çƫٻ ýYoâ^oâ²'ý–R¶ ˆ›H¾¨sÕJØrâÃ.‹bû£xN¸¡…·L­Â-<Ú´E ’kà“s»¿6³º¤Ák\ø2—DËKª÷¡ +Ë©L|î¦f KoþÜa¸²é–:·2yubŒÁQ0>à=Øçô*&7UǰX³ÑkS3mæ枥ubgš¤@¦jk±Cú¶çžõÕÕ?}î 5»'òÉ%m¢åýK„²Ìç@ùÛd …¥pÌ•v–Ær.´”wÖ/ƒ©œÕ’òƒØ+YRš•´eDXQ œV1‹m¤2©UfU‡âLüx:èæA^Ùí›­i9­ |6O˜J«™8eåPzµä¾Ä¯d>³xa°2]å“õë¤Ôcÿµ³åÞ9äñ…>e‘YH˜Ò¦VRUbz\þ™Œèl0óT`ÙØpµ…ðö´¦ýG"cÄtò¨x2mÌÍQÁG̲ö_g5Ô_ÎÁéŠóÇ\’Å:nõØ*–aÅB I{K0šLÂçŸÌnŽNàÕã1cm«– OBuÑ¥²tTmv¸­Ò7^«o‡©óÒ]=qÇw‡âTÇ,!˵½°¸/—vaÇÏ”"Ùq÷”ÃKìnЂ=:‡§ôq‚³bpA´EK%à¬Бv%Cá*!ÞV°Ë&öLiÂÕ탚§©1²d-$òÍ¡Ò zgo%deKµ’K›WAÈÎꆕƑ¦ÀAY$X¨ÎQEÉjè\!:øZó»j¹Ê›©Ì±œ”U3£¹ÒŒbé£4à+· )¢ôþpX¼ÙÙ¹ßÓbŸ§Ó˜§“ÌÐ8Ž“¹´6Åí0êöGtáHáè¥uªöÜ0„ÔRpãS¨\¶Gê½~̓áí¶½·íÀrã%A? Ý~àIbæLô ãÚ6ã'[/¯ÅÍ€5dŒþòngÙò^°› úÉ©Jí@Ö¹ªzj1.¯ØÇÆhøGhP!ד6ÞÑš©õÕ7n®uÕ>Œ¬ÖÓR¿: Q"žù¨?å +š×¹ü¯,ÇÂRQ,ŒÔ,xö<Øy«ê wL©“SAÛ鯲.NO§! ²ìõ_¦óóÉ®Xq^j@t—-ðFzÝÛÙÎé+Żݴ!«µî÷N¦Å…-`½–¬µSŽÎ"vky¿î¾¦=tÒ05š¿;ètjîœCƒÓhmŠ™w‘§¢µð"G%kp¹î‹ƒ.)nAÜÒ-›g÷qEû±‰˜_S½•ZZÒ >¸6©#5[ÕêÕ÷¾Ñö]ÄÙå6ýêT <ˆæ"„z¬ÃË`¸‘'Fáí¯Í/gKGkNqÀ—™"ܰ4ëZcâ’–æ¸Zu=U1zçDVXj9›ó'±EV&Þ,¸Ts̪`ó;ZÁjZ bz¤Ë„í]ãè'÷–¯ê¤Ç|Qÿ3ÞJE”#…|ØÃ`±„šG‚“³¿·]¯»º½$BWpÉJb­™‘Ès"ñbSwQfâžM˜Â8Þ„pTe«êÎϬÌ(Fã*Žä@ÿ K#žRÁQ' ˜p§ˆ+é<±l¸+ÎHH]pøÆ—hT[§GâÆ`•¾+ùòõYŒ¢¦ÞÅ¢6ø@íè£Q*SyÚ†`W‡£KàëÐ&ÌÒÚSæÅM¦³àÛJ¥ÈòÎŽLlù䱨@ÒóFvTQbËlârE©c³ª}aÒ9ööà@Ðè vF2RcÁâlAºÀû†Q ¥š^¼¸½H~y¼cl¦¸î'[GÔ€ÿ·ZÅšj@vOþ¿ó?üÿZ5gœ¶VõüQ(“…§¿1ÁŒÂ~G“œµÇj " ³ýS^'òÅýÁ6$‹/¤³¥àyÉr,îfüá2´½,þüs½¬àoJÚ\Ýö¼*à¹Ã*ߟï{]÷zwönùžêõ~ŸGÏdâù1¿¿qøJÁö÷óÀlüE×ò *:á`uV˜çÇË=Ѱ4TðUá4=ÌU¦ÚPòäiEl1T!±Hm— x¡rÍ£Dç0Íã„Ê`b˜œcŽ œÉòsŠæ9¨D x!s(Í¥#ÎÍå„Î0Xb#Îpšã„Î@XdÃÏxšåþ¡°È ž±°ÈF­˜÷å0ÏsçBÏûâr±ÀØçÙw“‹QSE çp1«½äØî8ϰãI¹¢ðBÿ;[.Ùb/zÌ=•b—)LVGeô «¦’«z²XókqÖ‹„åꀒ‘Œ2Ç‹ZÉ]s5UÃ¥4ÛÌÝóò}ùÛkÙíỪ­›ÛÔ{þ—¦uã’¼kÍ|î ä×J½–]ò”ua;"v|QÅU‹ ™óÁ2ÕµñëÏ• g/˜É‡Ù3ª™M(U"™*L%d¡<‘©H*×*–bÀÄU‰2>[®H\4 ¡¦2ÿ)ÖÎ$I&’ÂØHU¤Q7!º_SÀÿƒÄL%zæÕ`ÖÖC½ímD Û¢qe¸e×É<*·8ê^u%Ú‹ÛÉa̼nŸC=ÊÌóëŒ@gK‘"£Y”A4'Tåhãx$j#Ü£R´!šÃÈjT¨ÇxHÎa»Û]ºckG¸W¿Û`ŸðÝ”¿æÝ?Ö«²Ön*Ü ¶¤¬UPN^EÛ†‰­Ã>,> ggùÈãRy­bmQ³€fÄ©ÒXÉ$ŒFáõä?Jt½-°¡! Âï7Hþä;¿½Ÿ¹RŠE(E{¯ƒ—yŽO}35M™…îotšbã/¤œ°¥Ë CL˜K…2Ñ¸ŽŽ¶1Úô›Ê‚FÚªTXmáS¸½@°Z ŠÖÚÀBï¼¾°$Š5a˜¢ƒ¹ÎõÏP„~ÃÑŠúÆXA[ÂM`k"0T=Z4Lé¤!'=óûKX/H®u6‰ÚÛ­/Gh‡h$‚‹€6Ç÷d‡¸aKàšmµ»Õª:DpÌž —nþ[h¬ßh/]‚,Ý–|];5?š,X+!&FW:i~Ò]–‰Ô™ÒW49xÍúþë˜O¾)böhªÍm5Tº_–KÁäÏc‘¤‹öLw‹Óü¾i>ê ¦6[~sæ›ÕÕåÚ‡7*£qÝÑ0'&tÌåæRMsYèaà°¨ku'äK¥Ší-òváU•v zæhÒwij½Q¢*#X´Âåz½Ù`‰àw\Ùéf¾¬çU; 0ïS·–~UD猓1$"G†ffåŠ:ê´»…¥­[›ÕêÁدHC§“ß­ì3_SË´ )Y[0L‹êP`†`—ZxšG‚&Ùþ:q©øþgHB#@nI]—ñfÕ¼¹C´!°ºðì<ù=¼K«±ø°F63Tr3ÙUrÎ\ #°´­$åAižé[øzÓxQq9 ê" ExgÈð f(ˆJ†ïɉ.ÿÂtÝüŽˆÎܼټá¨uÓܸÂg}nд°è‚k¸ò ‘Xò9bd num'^å$÷³Z gè!"c žü’ql‘2Þ¬Y!•TŠ’˜Ÿ†”ú!‚"%}kÄBÆ…2 ?ÛpÔúACfÄ“wàꆯXÔ¸VMý…úgÜ•¿7”tìã˜aWá5–¤fÝ?t•š!Qá°y<²|Wâ<g)" 'Iqf{êxð7±£½ÛÝìÁ72†'ÅÍËWÔžÁ›\ÿIŠ(Ùè¤ã¨ÞU»f†ž–n9õO›¯ÊUz YúY¹>Íö÷ʇM[n°Ê,à*ªï¡ ®€Úå3MáýHÕ²tohuп$Ò\®ABuËÐ •e|\¥Â,xÚU ùaf‰ŸX<•â6áaÆ "eÜ”ã;‰ÀŸ11™ïaŠà«É’ÐY6RÂËœþ•|Ã%ÃÖ?­X1jÜw dEþ)Å~¾pGg¨ÿ×|ŒZ"³üǵÑQÁß‘$ñc{ŽmwÊ0Þ7}ÆËäú©¿<Š9%ˆ|GÄ`›é>þÅp€Ž …넇{Þ«}¾›‰/Cù;{…8à-Ø8ÀäÓ¬¸öá^·xNZy"'ÜL¢&nÊV 1;¨Ð«YÄ ^jža0G¿¶¯H0]}Btc×Ü©ðx4[zsÑ»ðíÞüÔsqܹŒcå†/ñ' 4ÌQ[È+ô ÒG¤¤L«ë}Q¦¤^q&ÿ?V1Qô@ A _‚shˆq½g\¨¼AÔˆfû‰Xþ¬w¬ÜD]¥xC‘¯©×­á«zc)É›‚:quHeˆnÑqÄs$S+éËr¡¨6¨”%sÊeÜ\Š7UA 0 Ũ²1o|ÉybùКŠ÷€Ä7f$ÂúØús¦T÷ÖµÄÅãòÑQ·5ÑœÓïˆÁsyÙ‹¼ ÙB#'Ë{kpØØ è„Ôf6þý[KÄ"w6ñ&¨Ÿ0¯²žÜƒùæsÇS„Û2x?MVáM†Î÷ÜŲg¢µ:ô>©mä‚ç'zVi\œ^~§,¿íBuO+id<(T“°¹åg_ŒA|w*½„$&“ñöA; yrWâÌn¼ñøÀpùËEˆ«y;¨9>ÊŒó© Ô‰2þr‘ ÏŒF‹?¾C&’éÁ{ƒÃxO.æÛˆN>#}#|ñåz =êéV팂k“‹“Ÿ †²-‡yRªåðÕ ÃZñ›½3LÎŽöÞÀ*Wņüyïóæ. >žH÷v „˘ª ®€5!ƒZ®¦§ô!–{3¹\Μ,‡ºçel‡ ‹:üý+†(±ž6Õ >ŸÑsìç¹’»Â¡Þiúœ¶×lKñw¡Œ 9¤¹æv…U‹Bo ñÐEš®ÀyÚ45±ó¤ªr!ϳ'T°kÄ&' ¥ØaZ¡û£â>#Æ-)Qëj5ƒ¸;]PNt·ãÍB[zèèžð§|Ò>ßË< ù G rä{7"‡)‚6ÀO“€EïÒ®™ð§@¨ÄPÚ w§cËâöý”fÍ.5¥;ÞD·ø†uGLf [.]Ól¢øÊ´0žU§E_d H  ò4À¦äׇ‰±yK-OCýE|•¼ í"Î">X¼JÔ'£-8_~’ze->(üËJë>8R8%16ŠôV&ÏÆ^ĤçCÔÍ¿5áœuçðt&P*Ÿ•^Lûí™Ýúd/’_f-.f[F¦ª^¾ýÂ_±½„ŽZ½¦ï‚€"—r‰þuN‰,Sm ÔdŸZp{CÑÝY’³Oøùt¤<?FÑwún•O^˜9œš@[Ù_pf ª²º¸SòÁŒH?Á0"Ò½NùòÆ=Ññâ~XOa,(˜ÄÌæ1Nع²0ÉœB ¨‚½#»‚ÆäÅ`Þ´MøˆÙל—(®œÓ'Awz¯fÂDÍÜ‘z1TÅ€Ù‚énH-OlèÈÍ™²(6j\vò'·J9n.>ìäÎQÎænÆ=¥¨sõT16ª [Øå8èmþ/X{I:¢îØ ôÏgK%Ê—¤½Óe} –dxFˆ-¾CÌbÊ¢ ‰„AÓŽb¼ß¬g£›#iùå„…X/ïÁÑ¡ÃZà‹-0œ_èÁXqédƒ<<ÌâÇyàˆL–OcÅ“¦É“™?TÜüç¤1+5>ÍÏ`°Ñh…Ø^zô¥™Ò»vo–[tƒ}¯'Î…Q_‰+‘ìËЦ+’¶ ç\ä.d&IK GA›¿éPƒb¿¦Ë~ø%êGSæ’ÂȺürË<2›ê3K‡ñÊ \â>ý?í©b>Ь~CðÒݹIuû{>ˆG“ÄP_©ãtì¦qRP¼ï¹AŸØPYNË”§R V…³ z;¨ñ }êöPÈ|¢ò“ZÉü°eç±™h†Ml2ÿäÌ7ûëtÇ¿Ñÿoµ Éü9„ óð§–¹Y8šüÏ|< §-ñÄs·ŸH,.O W¥æc>WÕ¥k_ˈVT+)ÍW¹¸ÍñøÁt€r#œÝ½_¦¦[`Š-Þ.z€Ð0ï´à©ÇÈšIùbÒ¬Ò§`Ò·#c£#ï†ì26ÙuÍyV |ùo2ùã5˜yÆ~úƒµå`#F;L_ÛJKûç‹~2Ã/F/®3ÿ“%5r±¡¥©:i‘]ã§b„o£´ùå+p4ú€\oXåÙ#îTtš\TÈî*Çâ·Zñ*–¬Œ× m“ÄT•Ý:ªª&—ySd€NêYõ«ûÄíÖuú°4ò4Þþ‡«‰ó“,į—ûÃÃ{gg¦nc3ôëóëçwcgGW¨¾Ü¦¡¥½u¿Ül¼7u Ð’·)  p"¥ëQã@× 4À¥ldeº$.™±ùZëÖH3Ȭ“6‰A§u&J4‹ˆ ä~&9t ‘mùϘK¥;{[ê!!ëû!võÌZÀò즟ß]vnnM]åë;R†Ô÷Ü´G-Àû¿•‹"…½è±cw'A_#Ýz£oïv*%NÍÆ_³Íãèý8OL§,Qÿ’Œ±êNWHpÕ"1@b‹ÔŸÀÑ=Àü)‡û°Î>¬Áxú¿·Àì‰ôHßP}‡ ¹ËºJ1~Â'WÈ“Ì3‡Q#›Ë­S•`Å1gNˆ0>̵iÖT™"ª04£{t$¦ÛxBÎ$š££ÜZÝ¡ù;Ü™"ÎáßßWú€èíK…( ¾à ®ñ›uŽŽ=Y¹'J=â€YµåÀ-P”ESŸˆêõ׺=sᣈèkÐ%2ê"‰æ Ýõ—€¾é²ÎhììpÑÀ$¡Ô Ñùõw ê¬"²k§„tXÁ FfÛ}Ëó–,€ÚUóiV 5";¿šxrÅHÅ+?€rµƒð°ð:xø»0DÄ„Þï>&ª œ=ß=Í{9ûÜ«ôXªžÈè«N¼Y¥Täú‚ÉÏž…]%™¥ûKUº¥I3ô‡‰ÂGíõ­TIyÝ!; sQÎò‘Šp¤îF¨õ}uÕ+ØE$‡\³2Í÷zO£__YýSŸ©2È£Š©ðhjýCãT„‘±ÓÒôýÐ4£µ …ͱ%%;4xíkвžU`Ÿ>*A¾æóŸÛÝÕKH&ñ‚Øg*U¡d’AmÈHðßò)7Hrç>Ð9ßq§žå]èE°QtΠ=1 p’Ua áç8$™p¯Î;Pcà ŽïGîIÒÎgF@=¹»ÛA„:›ˆ{…ÉËææ ÙÑÉ­û5mÞëß.æÎ`@[žÝß-Ÿ™:1ûßï¡ï™èï Ù%xêl5¹ÑÍ”ÎÐÍβ P>xsAPÕJ —Ù°z±zñbé|·”åˆ.ïVggëÈчÎÝk¢àââè›Õ‰Sõ¤õÈ@›+[œ=Ý<ÃuC`cŽˆˆuv6=\ß©:qpcCÙâ|*àÉ0=ϯ?‰°r{[ ý§m—ˆæÐt ö–œG£tzyº1ðÄöyÚ¢tvýµXéCëæü㇊ŠNJ`½a”IÃŽß3‹ôć%y¤´úøxx=øºq6îß?‡~Ç~‰f)ÅIÂvpspsziBò÷¥ø·vöö~\á©øyùø¹9EŒËý[‰™<Ýÿá‚×3؇ÕXç5ßëeøKä^èøÅ„Ú%øøy<}sÂÜœLì!l1¿¾¬Yÿ·R÷ÞÜ\_GŸ1¶Ú¥Nºº$+\âÅÓ ñ*ê¹IïPû8*$›Z Šä0eÈ \M“MñÌ€ø.VxFÆÞR®+ý ñ‹dB[jLÉNUoQï]R¨Ûäe‰›³¤râãŠLĺ$*ï‹!”€ù€…ò™=|ÃÕ /‚‰ü›åBôÍGnD"°%ά„é׸¡?º,ýs+oš5Àg]áé‰%³X¸x­~]õ³¬¶]«p‡©Üg^Ó¾‚:ïlÁÅ0GίšîÂAO’e—‡òo ?'‡‡ßšoøj8c›;wÈnTfép•:â M0*òo/Ú‘ÐtÛ$Ç¡û!ŸÕaaȪ†Êœß=”Þ†cRñk!aWûRÀµÏ›0f RÐæÛÆâ{ €•Îo(ŠYùžþ‡Ew–~«1üÃl+ÚVè¡cQDg IkÞÖËÒ¥q°Ç£Œ&Ó8 ÛÆ*?g“þ°Šâ¢ËÙïD?/0¶÷<µâÂI§˜©ž- €€š íÜŃ.^®ÍÍä~9ÙDÀ/!àÀŸ“*ÄO(…¿6˜g“® ôQf¸ÜìñŸÜ„áÔ[“"]©œå2ª{6ÑR1Öžp“;ýâGPàeûJÖ ƒ0zÙ•8«*Som9=¾Žª# ®ó1lž¤§e5R;âšG†³&}<éâŒq!J E óPÃÇ"Í}pŽ1 /® ”×4†¹s‹ý œfL­JJÓEî SüàŒû…Ác܃ûŸöãDÏBC9kOÇhõ¼ ÀË0^‘'FOŽO Y‘oâM¯Ï œ‰‚³GƒùTÕézÝ CCÖi•‚mšèŠLB)»ñ¦ð[­}ˆQb¾G>##…h9VÅuÏo0xi#2†=$±6ýØí‰ÿ»æ«;HŒš•ÈSÙa#U¢û‚C…Рܰž6$±ÚϯÁ=OnZÄ^ N#dùEM&Ü¢J¡ûñߨÅxÛ ÀÚ½ƒ’þýÕ.ü×Õ¿~xppæÌÚFÆfG<úZò‡×XUñGJiøqîÒ…ÂJ{©c.ƒ#w>92š\U&ÚÄG°=U]‡¸ Á¯•쑵JC‡WýŽóaç Q w«O4xP­Íx ÞW–3mø×æ†P§þ¨äiôÉçÀCÒçÍFñ/\¡ ¦;Œ Å%e7…«Õä“¡ˆœòm¾Zâ {3bàHÒÛmS‘ŽÄÏ9kˆòžã]‹Ž|Ú¾æ¶L gŠé){¾?£×À%õ¢2îwOÕ20•¥2=oŸ ÿ˜b½]­óMí+lÛ³sÓ–v°Úƒ*¸ƒÛr–i}¯î,ÏTm{áÅ…g^> þ%C‚/Ø3ŠôTÀ:ôö!d8T¹'«x¦&C“™á=퇬Ë˰ rûÏÁ¼s»Æ„,Ôú-ON“ÂG6”L— A÷ôãEž¦‘v@l©,@ÍQuÝåéþš@[D§L¹X¼ðk©oM~Ö¦&!Ý‘ª9xGÑѼÞ1P¥A<'ò^!"aŸ7hmúËjMþßh½ÃÍý·U_¿»74©F>@RXxÕç·¹¾6ßß (0”îz´þž5Ë­šß™‰ã2x/|?oK>ˆ¢²!_ÒS­;ÒÂ=ðgÀj«s´:<ŒF¨æ?Šbâü(µ÷ß¡¦¸û«Š°³‹L®ÜÚ’¯‹O6þ𻲫±â•¹'CDµ­{èQdõ‘S![":@\#Å#ìÊrv|ÅkP1oV°³=rkßY¢^ö=À Ð_ÅP[%ÊS1Ï<ØQ7¯¿`›TŸðŸ0ãAzäp@œÆ•ìCmC¶˜%ÎÛ_ ¶]ÅÑ-Z…hÇW>‰TÁˆœ|£[S¾3;ÃÖ¾7Ô£­a3Ô  òàq$JÇã~̘Ôb¥§ŒUL:.9Ps¾h”,À¯7ð~¿‚rÞ‰Òò;ÀÞ‹XFMbà§uUA›—»˜pê$È@Aç‘ÑõZ_¶>7,Á‡ä­f[îN G%¯áFG$b3Öë/ÀÀ;CõPéèŸVRŽ¥r‰à«; ~˜¶õ8cSÁ-5;F7ø[À7y›¬¼tÄFŸ¨WúÊ+j“åþþXz-[‰ÉóÁ=Ä?¥X ó¬‚ËA§‡é¾•"Þû/%Ïð¦küVûq.MíløËÉ•æòŸÕ¢jÞtJo‰M Oõ&'>ÿˆ=onµ"µ½ZÛÚþžëêôäu*¾|í8ÕÓ…™-üOßRŲ6WYÙ<ÎÀ÷x¥ïì J#‰!öµÌ€¬ÜCS5ÿààÞÒ7ß–ÂìÖΫ€ïºârª_XÄ—bhµÌâ†T,zYiMsDša,på¾: ~ÇLzFÏç 9µÊÍE•<û.;fiŽæ lR2ûë¿?ïzĸ©š:¯¢ßBT]ùŠ;¡öá#ÑBªÌŽ^ů’De¬Ö»ŸóƒDáÒåQÁŽmf?Í_³·"åÙwo½vƾ€€ÐO¦-=°ø·À«sŒ»ìè¶2ë_‰(§ÜS›e‡™ƒèdãð[Í1#.2 •¤þ2ÕàçUƒ.;#ˆ‘t„‘Y*–DØì6ìÚ啕¦Üh;=ɽ曃Û7RXa ÷З#£™cþ-¶iŒÞ¢&Òé¯EÄ@ãs0è~íO˜C‡áï6›¿kõ"´¡U6×Î|´ù1–áÓ§I°]¤O;îH e™À)» xR|fà»]ÊOyc(ÃÔG‘ øŽ‘nPú^4÷~Ú¼Uܪ%-¨ÑÃ÷zúܯœìiòÂ1k!Ûëûw<ïg"˜Y$~^¢Vz† äo$bÜ*à<ßRÖ¸[ß$ö‰UP¤“ÀœL™Ä×êhòÖi4ÍX¥¯O9Õ(쀵ÂR™ÜŸGŸEzíFÇî—|Z˜ì‹e‡«K/šøa¥7ÁZ}×m~Û}áz'u/ú‚[Ø/gÑÙGk'6ª/øšKÊ+¾:#³ÕÔ æ°‘mEo«´è³Xëë=M!™KùØg™À{èÓåµ$y3dÍmÎ?ðJ/°4´ÏÆ Ö˺C»Ì¤ÌÞyÕÇFÀ‚Á)šk%Ýàè·8AјϸåÕ©(h°ú F­@PÌ䛌†Rw“¤¤pA¸_Uvmz™ lM*ÚãßS­ìÂJ…©ç°2˜}µ Æ‹:c{Üxúé‹C§Õk“¡Šn'oÿÁDS›©CCqLW®©©ËïãW9©F]›om.{¢ËëiÒ® C¯ä«ÞˆüabžÚC}F 5ÎnÔ>=£2þlŽÌ¸ÁÂD‚kƒ·õ&ûÐÏ.ZŒòg&ŒT8ÆÔCÃÙz]•Š_ñ»@m»çUžVÛ|˜íjñ>X£)ÿ0»»²öNògÏDƒ)Y!ó $ñÎȯùV%»{õõÙÉêKí°E·"…Q,Ü\sÆðt ¬ðaRž,ß}§Ìø)ÄU4ii<9iaüGDQFàÚÐ뾚– ·­-Qj±-c±½DCrWüIàÙŽ™ÞoJ€½#°0ÀÊÑ1àß ž‹)`ÓeËHÜ#j´ Ú‰x¯Sw— ò wÞû~5’½Éòø³o Û;¬I´5ú@žV“t7b_-Pfgz×i+CLD#°ÿ­ì–m›„¤ ·ÌýÖ­«RÈ aÝÜØSÖ¹›µnö2s|OVÿf)¢Õ¯³eÑ>ó›gùï£ËwyûLé ÞÈ«F¯í«ÿµˆÊ“üw0 ¡@"›àÝÖõfíöוZêIzD„èe¥ƒØ8‘ißì¤;¨ç¯á{(MÞY€},¢RÐ^cXy F©K?αIƒˆ²¯l÷GŠÖá÷#°·‡ënIìî}lÌËwvåþ àf i‰©·ºÂÚ*B+:•ßÇŠé;Ò¡­‚Ãà;GŒ_ûã÷ª*ÃDŸÄªžÊ±ŽÑZÐq ½E(éîËykD ‡pFL~X„+΀º¬A ­CŸ=á˜Øßù’¡ÎŒÙ ý—ñÞ°4[ ®Ö*¸sÝ+Ù¨áÜøîìa†åÜ×EáöpC‹<› c é_…ˆütó½ŠÀÔÕO¼EÄ„©° /^ÍOlЂ)!gx½:q]i¨<ª†&² il[ E•&û3 ]éš,á›Ëcµ9fS¥P]ˆB;‡Ì8eÞÈí4ýSup`îŽÃdŠ'ý„n—‹lu˜Ù»¯²ƒÞjíâ–SD"9Üï&yÎ2uf¸¾ªÿ.1äþ«~«zyçÇã´•#½wMê@IÅEþ.^@ºA'†l=`vK4kÞR"’€¼ß¸ÿŠ©˜ø„QíÊH Qp`V8ôιÙJ°eJCžÙAÒºÂÀ,ÁÜÓ(†+hE¶Ÿ9Ä2v÷–÷0VYn{o ò F Õ¼[ kÔŸcЉA9Ößl-¥%ɷЭñùpäõ/æxO˜Ð„hÈ»è<îñ€EKÚä¢KÚŽS“bõ÷Ÿa»3?¿ ƒö¦Ÿ£q@H…Q±Å&UIä{ˆñ²6% UM…PX ìò*²v«ëÂ4àµK}` ¿÷è…Ô©Ž£þ³Ñí~•BB6”n7ų±gRqSÌ’3‹ û~#Yû죻—¢·†–+¤@w ôYó5À¾|Ú Iz€»Ï·cö ³ý-N ˆêú|ñ¨sÈŸ¨Ùª!s½¹c%Ê,¸u™Æªð¢^Ž?iRÓ}¤sù‘áNÁòùÎÃ5òl7Ï÷{ieááàü ã}§+Ï×[òÎO± ³ÛõóÏxYEó ª'…Wè¸PÞ*Ü%*< Ót›ÒdHŸž.µÜ0¶öQ»Ü€øOádƒ|Ëಎ\t £T^1 œ¹œYΕóhX¡A¡¨$# cY2Êš­Zƒ|N$rD4 =nü_¾¿³³;_ª[E#™ÈcÞ)é÷Ç箯V[ÿKp‡#Ï€Á n¬èY´ì#bÜ•dµ"ΊîWÑŽ_×÷¸°ä$Á.PSÌc:¾ ºÎÕÃzýêJ]¤ô(ì#O,«.…+“†¼æ"á#…¢!'Š3O[ §@“†ÞCF@‰V°Èñ/—¥Ä±âAäÈæÊŽ2E³^ô:æKä bÕËï_1n½C’\³ ѾìnV6p ^x£h2‚«u·4˜(<¸ L êC®‚{È7ë^HL*D!‹\y˜x… ûɾ¤ÂÁù%ìä0o¡QS+Yë3$÷醆E+ñuY´:Hf3¥‹ÇN}Ö9¿ã4#ôÆJ>4¶¾×‘Hý ØQ© © Ä™i¹ÄؽÇ#Ãz•çéê{Dkl„•“!hDQmˆc“FµУã ձ͔±†ÑÆ ~ ;óYlÙ‡v’hFàP52•:¿s¯²æ8vÊÕDÇpò$dé]¸ÄšcUÎC ³tê·$H²-×^—nÇ¿sºÜ»ÈÃ7/Eˆ›™äò·±ß_—ßV Ck˜Ò¸åÐ5öù髸™{¨®r‡Ë®vw¡7}”É˼ÑÍ”`ë§Í^Ñ i> 3¨?XßèMЪ­ Ï…›—lÛ÷'­{ «í½RÖ–aŽ1»U‡Jœ™îýQ©àg3 :á›öJý‚ˆ]iÆ6“_vØèe®¾DNxÔ†U#áÞÆ[ûñ1€³£ifSE‰ FIÜHtn4÷-Ì›§giñK0² &³ æ´)ÿs)T@fS2ínx1ÖiÉÀž)CbRÎð‚ØŸ®àÑÞÉ×bé|$DB¬Õ±~®÷Z£Îeg0mmBD{öø˜Ëéèß„H¨7µ>º_uÖ˜!áXî^u㛣ÊöߺßOKJ…ÖÉVüëŽK—™ò¢ˆ+ Aá…Œzö¹gP0šÁ÷DOer+Eý O´4k2™V7þ{õ@~uk± ›ˆÇÀ'ØG;ë`ÁFœ¨œ»«R%„×džni‡ÙÖi.:V3(‡`-è-(£ïkCÍ[ž¶éf–×…W»È“Yª ÄuÄD³8Ú‹øƒȬxí¤›ñ¶JëŸE"9¾X¦n{¶Y¢§–õáåc4(Ä?­]“[ÿÉ1ÏųgÙ¡‚5¤jFiÿœ“PÚ¶úÀ(„Ppv1M}Bq¶a ¯½5©vÖÔ_ä5T‰c¢ µ˜ÇážðväÄÎ :>C…Ìhª$öýÔã­”O±]*,ËWYb; €)¾ìáÃêQ+Q<Âuâ$…é0™h,—££Ë#¾Éüš7ÖÍ8!/¿z Ë5;™|{ù“4ŒyxçžíÅ.@áê5g«hÍuŠeû–yùmÛŽÄôÔÇ`>vkÞÕŠŽ¢a­š[0’Çý}RIÐJµykçD:ªìG2½òh&£¡îõt£ÿà+[}a;àÍɽ½ZÆ.o°Óäx oäž4•Ô,Å|Ã’ïCugX6ö‚”ÌLßN®k÷Ñ—²ˆ×¶¹W£¡e¥ôN AÃw³åIvtËÎql¼ÎҌߙéˆ2{GÖùò†1a=»î-Øë>fABô¯D)!Kç,n„ò†¾‰êÞ&ëȶˆDS6/‚óÖF‹ §l®Ž¡}a/¯¶ÿwÕ•ÛwPÓDkX‹µIY, ¥ £ù ¶‡˜˜(-“Òɇø*¾^µ´Õãæ•Ô•'ÝÐØló÷ÖYwÕžÂÅÏ a¶S»á´ ðÞzÞ­ ~­¦ã‰žÑm¦&EÔâ]æ•·šJ[ÚCÍy„RÍJê¾”sšïgÈŒãQtTÉe¢J[¸T !7äÔzÃŽL‘ű¥ #z•,?öª/ߣܳ_µÚÜ)sû3Êk>ÆHÛ:%ÕA@¹ɵ噫ÂkŒët†ÿÃqïß*9³v)3'çÈ n]ÑA¤®Ü"‚åQݪŽH>ó´Pòã«Çê \V˜Yïµ¹‡“‹[=óî ~ÜƇpµ&ŠKÒH히žç¹¹1³k^×Ã%—ÐKàÃ30¯áŒ¢véórbþ¨š4¨‰€i âÊ”ÔQ¡Î?ø?4ÜoVO×ÓÃÆ1Mµ³¡¨¤¦£$bnÁE\žc+°»qd™-àU±ìñg䙪£›¨oÄ©«g|l‚%X¬WMHº®Y)n”×Õ;<[×ëEõ½oEåŸÏmM)•Âb‘áyÉplRÁ“ÛÅ훲OŠãG&‘—e)öÖLîŠ 4÷ôR0oj¥¥>wËØÀ¸G´6 úŸT &G(‹÷9î \Ϊ8ögWãÝì!±–@§³EÑì>˜bû,A‰.tdÒRBm=œÝWpQ cÿÚ4Æh¯´·i­.±Ó¶žÕï ;lS³p$´ã@ï²·ÐŒÕfE11ÒMÂè˼„ß@L…}\î¦æZâa;<™ñ)ƒÛ¹ |Â1eɆcè~Y½`µ~8Y“jâÉ,>øªÛC]и::¢–#jõBߎh‰Dz1zOd׊˜|ù+ÅuÞÉßXZ‘U®–ÍźW³ÂX‘/Áš*|£·ÇX:SζSÒšJþо•ffÈNa–‰¶»ƒ®¢éjsŒG{’âl+â¯O1|›O<˜§¼)H±½œP U[\0æÕõ%cTCn£mÿÞs¾ü¬N÷ÝBé‰Á08;öoOõÒþ-÷¦-ÃK¸t…“û²ƒI SÍÃáŽ@Lì|“ïÐ\²¸›®.£¥€¡ùV«ö²˜>ìÿ¢.ÇŸg`‰Í,ß´²9¬NXäÁ«X€¬P9ªpÕʾ5) ¾9œÿ61Vœ]Å6=™ô^5S¦Î’›îå–Ë+/èL“«dî/E’·€#wãzxëõήôöú´䜮šrEÞóJÊEaùšÇ—õQPèt®Ÿ¸+¹°]Å6@é'” s8Øéx¤ãÌq$ÚÁ’=œleeNâ=@VEÎhGào‡¤Xod5€È.'L$‹]´fn”¡íd qâ:všIÆ&—’æh(Z’а'[H€“4óK)a ŽÈ}WL]Ç‹EÔ E댠˜.H×'£`ê{xçÿð1<ÿ©MXÕÖŒU ;ÂF]FS ,"™H~V¨4Sì57OKBÌa䟯‘ )³oD(‹K»¯b~X§!ƒrq‡”'kÄ»Ý}FÀÛMÖPkàlßNØtž¢]gÍò—5aãiÙP&]Øú·%U[¡gYÅM•»B¢ ‘Æ‘ÅQÕ©Ð><6Ï϶äù®¾S¯à.Íô›‡Œßäà×€™.‡ÅšQå­úaÿlYµêÕ¤ùVo¶(o’ÛÓ· è”@Já½ÍC33Ʊ£ëOFÊ÷4”t}ñL1Ís'–÷óèÿ¬OŸÎ&…îð¿ï?àñ»µiÈGžý( îtR§}”^j¡ *ZÝŠX²3*Ç«·ø3…‘^C] "õ!%Py‡Jýî™hvsi¥H‡µŽáÑ‹Ìê‹ÆÆøu²âˆÎ©NEñ‘ÄCYÀÅÖ€C.ù—ÂÃ$ÅC©`"È@›{4Ð .ïç®4>,ß›ºÒ’wf’¡;§’EiFo¶Äíʤ&auáM.Y‰ŠRZOÕ–•h åU4ŸI§G\œg¾îèD-ê5ä¸GoŠ(D½…%xŸî½±D£/o¶ºãäi<*J¼ÍÖš«²[4üʇrqÙIÁ¬vdž'£¥o³¼‡! Z‚=°9EÕÚéb-•“Ùt¨œ²÷1²ÜE[}3bPŨtOâ´\·t±öôÝ7˜Ëçîæ\î‘Û[Vo0­ é'cÍFu­úHåÎ%3–|%eòX[gKð9=j‹¬NÆÝwDZ—”õŽÆ[™b´ ~¦»Fk_ó©†ÓWk?„§‡qÉ“õÒ^XöC€«ähž-üve’ï±K>ùŒ®¸®—´¬UƵÚë\3aIíÍÔÀ§`¶4ÞQQÖPt¨‘½HpÀ™Y¼“<ÖÁ8÷tBü&ÉÎnšÃæQ¨N_FÉ˼Ñog^„Â÷íH‚knI™H~ph ^»ØY;ÈEJ=„$ÃKM‡¤5_ YÓ%ïv€ ârvs€fÑg]? ÆxÍsBk>óm¶ÔÿÑ™^¨Æ^5ÂÊq­ZëpîÎ ­šDùhìÜ#yÁqLfbãhóª°mtÈ”7Òê} –•ßûF¹Æà—lÑŽêëÞ¾?²QeàìëàºË¡Ê` æ'œænˆR2.aìütú,ز™–o‘ù–°Ò-†b¥[ÌvÖ€ —Â^R笑!æsм)´í.îcæ«ê èÙÃ<âqóÂëžzÕ}ú›K/dqÒ¡‡ÒæƒÄ°$ùµ)d܇AEåPVÒ›‹T•|&ëžÌu€*|É'S X=–cÿü’Jû‹£yñtA:ŠÇjÝyÕFm«d³§ÕgõRJê|8upPµ0unèîëÊøiôrˆüz“»)]‚›óÓ“˜òD¢Ñ¨Yûäçׇ0Û¡šÁ´1§×pKæŒüJÛ‡¯Ið«K„f†C‹¢ÂUôÑÍÿK•[4ô¾÷%®ð¯p·kÎÛx^´âýõÎW8>>Ðæ¥…ADƒ(“èñͰþ÷ :¿ÔF‰;tøæQ-¹$ì¡—<Ê¿\8¿ÉéÒÁIXHNt/ª“w„;‡««/†ÞǺJ¾ª±§²bñ÷ÎŒ7o•Qóó«{eæÀ®²¿ôë@’tƒsT‚ªÚá22+âõ\óo¾Éj?oè*ž¯%ü£Â¦MQ{ðt÷åvÙ i¥æ™Á׸4p!f[zJ•:½ËØXpY 9{šß“ Sç, ¼#1áÄ6Õù·C–¯»°}½=¢T¦pY#1õ‚)€ÓfŒS.nhΚ£Ø; 3#Ûž_ã£ÝpƉäN£εrXƒ¨åyC8c,V¼¦#T-H ù~QOeBµÿŽüû^ÁÉDZ'sbIIìÃ;7ºªé0Ð~1u”hÞ£RÛÖfuðÅ€l Ýè›CП q0¸pôMœì¢ýr>x1?òŒÔL"Ÿl3^¿¾uöO  ~·|ØÅî×%òAUkE8Èrñ˜)¥ýX–DGE$•Ubñz €yf˜»:8üZ$¡.•ihã%¨4¸VyðÎvJ»×b¾ëBY—¤L~jâ|˾۵Së<§·döã(–ó/µK÷tz•+ó'xLé vïÐu‹Þ.—_W8Ü;TÂkPö¼l{*Ï@Rv×%9ùm½ŠÇì©=TRÖéì’WµvCì#ù —e?rµíöâže·‹Ë|àï°ï;.ôˆÏ™Ÿ-Ý$@:Ï%¯ ½|è<=gÓ…Ø‚–~Œ}‚è6O册¡ÒÜY´Eá“Ô^÷uÀí.°|ËÓãý’ÁRVûjqc†üP?*2úˆ=5)nÍ{9 ›iQDÓ…çkÝfßr2®—½ë¼'ì¬Vã<„”0–ÿ Kâ^È­ûáø×9æÔ’ïŽÒ`Ÿ½Ë¡á@`ÀýZ.C¹@ 𞄽«× ÍÜ û‰e ²É*3ã±¾.Àç'Ñ®Þ{ÐÒs–EÛsÊÓ§ÒuXŽø¦î,ÆØÐ„p§_¥Ü{¨î à b›Ãö`¦ñÎÌú﬑LI þ¹Ì¨æ`j"aß-—H¼¾4øÑ=ÅOå0rÌûIů€›’_Ë~áY€ù‰üeÚ³À¿+óSHQȘç#¬êî—ôW]ÀTÙëoµð€ËÀØù è6ÍñÅìo¢Ùw/@°0O‘©kUEE-´ðW=ºã¸¹«Ëkêìqo²t<–Z7¡UÎUÿ§þÑAC“òÆÎH•|z)ZSlàràO“Ôý(ž+c]-9G÷”Õ¦R~¯PrB 4´·ƒéößÔ‘I¢ñ±i­£G`z¡Gºà–Ì‚´¢•±ˆrÍ)½eVt§#àᛀ­3ø>Ë ® ¼ì¸žˆ,¦äd Þ?‰¬¢ì¾¸²RæJ8³nâ$ÔdÁ,(¡Ëv ÕŒg°kÉ:߬¸œTÝI5S©æŒS`ÀËr·V"ò0&O<4-«eÂåuw©½½Q/;¥ÞþÏݦ+8é]”K¾Üñà\ÅüÔ®jfáÐ5¡kGGõ*ç«I*‚ÐoëEôzÚd@["̌˫Yæž‘ —ðÑ u´gm4ë:}ùá½9"í7>€÷4žž{_øÔsÔÚ°N-+—ÏÉéËH¸þ ç܉_´¦‘;‡Ð&U`ÙáÖ0zµ„£`Xrm°Dãß× 9ÑÙdFÉ9AùH‰ó.bWöpæ¬fÿÀO݈~½týå¨ûÁá Ö/J\húD‡ _ÙqzŽ2Gº ˜,Íd[ŒÛ¶f2Ñ}ÂÈ%. á¶UA×½¬TŠKXØ:­—H’é=P¦÷=ש-Xnch>vw_Aa‘édÏQ^XµÂíð†S{ŒSĘ. ÊÉXVÀ\Æ‹žYI#Yìíò)æÏBöÿ@ziKDIq^E)håý%ÿîkŠ•7©„Z°VɯÓÅ"F_ ÷܉˜É¢5ìOÍ>Zm²õû1JxÁþÞ˜ ³åÏ–ÐnÝsã±}UèêêÑ¿O8”Ã3Éž"äáô†Æßßí;]2¾XÐ 霘 ô† ±Ìñ¨ætOŸúŸz# ŠáæÞõ%ØèðRn¥ ù?Ä;À®í;²£–ñUÑ1¾êgÙïMÍf¨[¤À¯ ¨À"ˆ¢/"‰ —ÐÐÃèåwç?ä•rœ-n9Åäìe8k©–9 s÷‹ƒéfCxýÊmý9s_ü²NÉü–IhÒYÑ5!"Ó.3%kc$kq¹ˆx¶÷ö¶Ür8|w6ÿ@ÿoÇÿÀË/vq€²µÿãÂÿi$1ÚévÇQsÅs¯úd@ —/—¹U$yG ±—D€Œœ'Б™ÊÌ4U4¯Ç×Wc…ŒàÆ“eÂ"Ç#wM|}àz0>âÐõëÊ yÑs½6v;úév0ü[›öÈðv«É_߯L_¿Ï¶½ª°vŠ)â<{½ ý Wj´Úlw¼f;Î|˜û˜™žŸø½ö‘QKþLÎïP?£ø¤Ì¸*K•'r¹+ÓÔR´rJ¢9ç*W³Ï–%uk`ÊÒÚ+ÙÊ/AU¨(ÄÖ0©·–'ë©`³ÌDÛ»¨lØI‹´™JU]Ëáè¦Ë7Û+ï”KråUW\+>ƒ©dÆ]dW\«>‰+ŸÃ+ïJx+{ÕR^+?‹*ŸË+ï”J|éë,¿T~U<¥WðÕJÆT~ U<ÿ­DU*+¿W: ¬ÀW. Vðæ[ÊU~*U>'Wò×HÊU}2W>Goc+·˜©ø(¦©üÄSá*¾ U:«ß¢*ßpªðJŠ{НÄV~&SÉT|2ªp•^Ž©\´WæÉ— Wú*¦ÈV~>U>wWæ)–(UþRõÈÙ*µ «üªxJÔ†©ÜUþ¨|ÖWæ©–,)rZâ+¼0W:‹Ö¶Umj¨ð—Iž{¢iúi¤èª¾¨)_ ªüwÑBÅO%EWåå©ÒYw…¾|T%O¡S¥¯\ýd‚QÅKf…¾b‘T%O¥Dƒª„ÓCô¨òYu>X[u€N¥³(3šõZµ¤Š*?¢½(3ÜNP&¸ì¿ê¼0M¹~•Ž Ì ö÷2,ê¼rwE ¹²¼2÷Ã`‹“Ü…Gù`eÁ½Ä‹EÉ-åKƒ¢¨\…}¤T^k‰]Å>¦vr¯èÜãÄUúqR\Õ>BÞ;Ç4yžü€“·¢ê"_GE`‘·¢Â@2°íOº ¯ò€CoOy ²wcq`RéîSën#”lÆ"ºµ… (}æbšõE$Ÿ±$¿µ”àï¹²¤jŸèß).WLÛ!úÔ&Ž2u ^á"Æ×II=â—‚—Ë6Ô¯3åÐ…*ê[ƒ5òkØt¢¦Yõ8îʵM¹V)È ÷³âX¥7]Ð-æˆð~€kI…WV…½”VffïâÂjrdÙ„¯ä@=,åÜ3V¾ïݤ Mý°’:æöž}0óºhmÔ0—¯cY´Yœ+wúРɥmÅzdZ¢‘e5Šžu#ʰd¹pÄ}‹.:ãZ…Ô»j¬Ø™%i¶zX¨ÀNÇrÇf9Óè 3Ãöž4«„>µ"ºžÈM\‘&É÷¨ˆS÷¸BB¡Ç#fBU¹\ùN ½«Wñe@=»hÂé¢Ãó«Ù¤gqÈëܲ/37£×uIû~fvnïðúÍÏîcBæ[|tÊzsœ; ,ú®ÈY·²)µzqzLpk0M—|ÄÔ[¦R;ü`þùôàz¹GÙ¥l ÒRïR£Ý!"ß_´ â™‘O°»âpSÒZÛV¹ÍÑó'˺ÂAvCÍäMu²øBÎsÃYèÚªÇó°àmÂYF®£<\µ¢¹[ÅÚKí@x³ÏìAA¿º¹à¸ž¨óø±úié‹ .ƒ`‰ò)Ø„o_“¯]õð½¯Ý);Ø’n5¯~ÿÐ׺ÛüÉžå‚çªsÊñˆ®5Ã%hšc5­Zh*ä×x±fzŠf,Ê(}Vç…µ›þpjV÷&²Þgó¹^çeéÖ²\U™ÝÏ‹.å¸]L2–)kÂMµ¸szžªhÎhŠ?ý GV¢ðüT9ªG[jo‘U×9Ý+oü¨ñ1˜dÜ*OÖ‚=4Ê,+¯©hñ¤Ý¬;hØTÏ´Ø®hÍŒá’!Ü7t‰ Ž6hMªaG½5´®a?²ïT,Ý<}-&d[”KÏr‡:»Ë^Ï®LÃ^‚î ÈÍãñ=<^@&åóë†r/7¤«t÷pŸ¸ªBû¡Åê¬ Œ$ †’Žbçà†¤ùô-Ò†÷É»šÉ×)’ª0_÷wmAÃéÂD8•^©ŒyÝ\hï9ïs¿Toov®þ– ªÕž ~ØìÙàh­Lx_×Áú®Xq±ƒ"ŽA[ꈋ`\ßr8ó[pÍMÕ­f¢SÎ6î—< V… ç¼à˜ú˜hÇΆ tÔ†’ íOÜ7ÆvĞ j韀p‡Ê¶ÇHR¸‡çÀ>òOÂU_ÊÙã“‘Þ§á˜$ïÛ7fçÌæ2 ÷ªo<2¦'z®4`ð;{èÀȇý]Õ ªl‘ã{Ë-â;)“ÉŒêrî\™>å‘;Õ~^OM  ÑoѰ±TÎ.21BJ˜®í6šw?ïŽ÷œç(ø",Ç iC]ÒÅïÿdØ¢ãóǤ9Ë Ï]$ ªŸ½‚0 ô¬“ Hý|å/ho.¯ÊdIZexÚ\~¾Ã«fÃU9V#Ø{ZhÂ’Ÿ…Ù’%sÔuP—ïd" ßW´×Ž+ –|—눃÷÷ YJ±Ò.uK¡’žB<Ÿd8o[0ìíÌad×ϳOZ<@2¿Á"¸&¡GÝæk‡÷ÄŠÃÈ,œÒ(wd§10¼_üÏ8ˆÂ³Æs ¾’ÖŽ|,ã n´è“mAh‚µOþ™hþÞÂÁFxzF9\ÖÜj¦„´çV­á3@½U-CcPìó;ÁÛ«`âIW>³îðõûö]ýå}E1ø´FJ´ ›>È€Î;8úƒ½`ǬÑm›’ã‘=‘ÙÆ”»5SŽÝvŽÑõÞÚ£|2ä{æ‡ Ã=ÈN¼ôra!÷æ’¤I åSgiu/w®°üå» ”ƒ/`› °·oò»Àžj¯\ÎCGµ Î )”uSƒks Î/KswÑ ¿TÇ<w×yºd*Mø„É{° &A”¼S\å Ø›oü ”ÿЉ,zÈ›/°3ìÆ§õÊA^(!5d«°ó(Æ“|XáËTÙBEMÍ ØÔìn9ÃÍà¹W)o肈«áßÕþò1[“¸AÑvxT9ìºxgyÝûjz‡Š|*¤õA5Ð JŒÐU<à‰ŒOð-ö¸)?ÄV<›ÇPMp{ W‘m€øVýïÁ†ÛÒ~gר5wº'³&·èšüµx9«9û±yÀ-:ŒsQw§zͼ¸ðÿE[yA—9cU_#6ìX«·Zýc¿Á½² 2f~ݺ9 ÏË ž° Þ)3R`£hR‹¸R@PÊȼÉ8ßÏO=ä—–î­}Q+S|1/öt I“%ãÇ»öŽg^?õ>²ObÑÝÑûjô‡ó]è"Pfµ/œ¨š]¯ŒFª+Š~TÀxŠ”—á•û„¿º¯²S9fK† ¶7Ù†l{¨zÆhЉ_E·z­šÕU+>@U[*ˆ›šÛ˜gt~4†SâI1`Õ“ ³ÆXí‘ø]äoúb M¨} ¹»²ï)¢tÃ5Œdx@9Ȱ#Œ¡ cBÁKÚèÈ|øåˆÉ<*5M£œáÍC)*4& –Á•)3TÐI=뵜ûs a fŸ3ø¯0“/q7‚kôó.?"çKº ·ðì®P •&[ctŠZ“1õ7´îÐ}Ú¥O4·r~PK•W=Æ~/÷Àb""MLëÐËyPÿ ̯pSL‘6¥¬‘Úa¹¸"W0¾à.-°ñ¸øÆr(ÌxË×$£èòüEWg¢x°ÂLº\ogŠ4¤ÖHqÄø2{Ó>,)kSfàâJàsv‘å-Ùîìø\´ä”'¸ ZnÃqWëì¤fKZy·—iõ·ª;¶§º¨°äþt¤ˆ5Íš‘ÀɈ#-T‰C(‡3ðÊp9œìóL÷|쨘ÍêÕ 0Z@€o¾Ë‡áb\šÖ1“ß_ž\ÒÏ,`Y`Ü¿ 5¢üe³ó5…<‡C°#9áäО¢ù“ÁnM¿=Wç%ãFT!Tå3€.Eá“‹;XŠ5Y“8XèT¶—31€Ìªna ³„šÛ­ŸµU»&R¥ë„:íµ¾ÉbkVFÂI'Ö:ý]ú‚¹á¤EwÞ÷NþŒ`¸Õi–!»F\‹a\'¿RÁaÀÙ æ«öàÍ|ì6gÍ™2ì™g‘…×B[è­(¶pG†yô)ÈS =gö¬2'4VëajMqìqö7aW·Å»æš‘fæM4d]=:ÌË“?SÕFTŒ -qe ½ySÜ‘B¹Ñx&äyÌ‹ÕåÝÑÄuW]©eæ£ïi\q­„FÈHÛhiÚ®Ø!Ù°Ä{¤FÜ)Fü ‘6ùáßÅ£j†žø1y”gÓ¡{¹æ¨_êZ.]=å^2u›´§ R4œ"è$ÉÀ‰%crSºsR~\ÑZ7*ìV3Ù=T‰L ^”ª lšRFåT¤IÌïä*‚–¸|amû_|g[ý.XÄÃE?—3€L~©,ÝV;·%ðÞ>÷¦™‡v—leÓù5;14bt6‹#Õf_ý$2Öê|¤^ù÷ßkÅ¡c¾¥ÝƒuŒš}J؇å¾›§¤éš/x .tòÜQbŒ˜jûkýoðiK,J6 Ø \ Û» ’ŽÿTjü8sˆþ¶¸íõŽ2vþ¬‰ÊØ9Úuùw®Ñ{jú‘œÍ©~œ8E}29Ì<|™¥©Ì õ“9ãT\—{ñ€n8K½¤|§‚ü›£eF)a :ªÓŠˆ&w%Ïð5NÕ‹'JÈ%Ô]i{ÎáÌ Q€qº¶;,b|ŠÍþz4¡ŒiŒ¿Á.ƒPï©Jƒ d~)´=)msQ˜X±Ô¢ZåV¸lÈJx ôÔN—[Ù-ÿŠ5-.·Ç÷{„‘nM®X ïa¾•û]\.·‘ަb/ÝcïÝ6z™DÙ)iðl' |ˆ&ùÚg—EýæÓO\@ê’’³å-^Rh¬K_°‘‹ËCÐçÊ`h¹Â>ó¡OÞâæny7XU£»ÝÂrŠšÝ4‚§A]AúÓ¯FQ3HTÒýæ^,Qü’§,R”z[¼¸ÐyÖÅÉIv§þ¤Q ”é»! ¼ïdËÖæ‘~ûCoµËÂô=(³ïL5 U¹©Ç:&Q®éúÀ‡¸m\T"$y¡¨'ÏѸa®† ëh­å2·°–î6m:µ™4ã; pà}azêint½><êþ OCäh\ízwÇ?GcF[4“õ(ý¸¨2WU6ƒGœFV¢«ÕkÑÍÐy Ó‡Úáìú€ÄÕ <¥Užü…ÑV([Ý“¡.f?ïyP©êÑ·i¼w ÏZ?íž;²™!Àù˜™lßÑíª ¶e§Ñ†oõ°®Èc8Ê톹®¹ŽÎ§þ½:2&ÄÒ*ëž>ºé]L‚¶ËC…ôûH˜MQÆ8`jÇ­ãá~ÈàZ “¿™‹•>ÃubR  ¿Ä†ÉGènÆ™ÎG–tûk¤+˜êÉú D.Ã/I·3=|Éê…©èÏ]ÒrèˆÙM 2"ò“÷=}Mhl"éÈ‘@‹Âõ}>„&q Mg$aÀ´;˜Æj8î|õ)HX4ÞWC¶è)EÙ± ³Ÿ$Ì`HnC{P‘zÂM-ô+ÏèÖ×s]ô/S0²³1¥O\â9ùØõ¿H\P°`Õë€!¾óSå?ÞÓ`ô»»{‹X½Uȉlf1îKpý÷Ï=‰ÐCÍ|ГN€·ÞëM"ãÏAydfÃÎ20c®Ó†°Í‘Ö9ýÈ•©dU‡åãž«º¤D“°$3`¬Ý èï_Öék Ð(ÃiàÖ@F.E'þ6šO Új,ã¬/^Ój5ÆCßýcÀ¦m«”5di„Ag]“´/A?b5Œœ^Î7Û \§{û]¸‡­\÷’.$†¤©—>ˆÁŠ/p0gk$Ê¢|'›oéiQJ—LÒFÔQ 4Êos_¾­!ÅÃ6¹â«2 „»¶qúöL ÇFwY•)¬…§²»7rpwp¿–âàXî„߃Ïzo½p”ø`÷>†æ‹®Z°Ÿ³ÂQ mª\D‚#5ÿaãzÕËëþÉ<é®7|`W±Ó/†‘¢'P¢Çt¯RX nD”âl¹×4`è¼”vIy‰~gáEôî†ýþFt´‡gÓ¹>´Qß4u‚÷Ž9yœóCv‚©D~n9l‚DŠüK%!±ƒˆß¶ò“ùþILÆøf‹Êìœ hEè~ŒC~ȨÿÃ>‘•œ3e‘ÿŒV™aðÒÜTzUmä.{gIh²âAY•ÍžÌ6|¬3߯³Fæ>êÂ@:^îšt·Žš“t•¤èe‡âØõùGv³ÙàXü <±ÎþF­}3*’ïLzò‘9³ÒsF†Ïº-£+ùãþI²Îï罓_ Ö§àèª3üä*˜0Én‘˜2H?lë ï@²+<|n`&_GOfK ­Ðé\Ðà·hÂÁƒ?Ýû$Ø(pD—×÷ñN[ê ž!­·FLŒQUŒQáí°(°"âmõî!eAçñqìóRv6päºÅåƒÏJÅû>“V:ÞwýÍ‹Ð@ùÇX xBpÍÙûfömi"2ƒ°ÇþuRs©`®c'‹A0Ñ>fg\ÕPNSFä­q“4“D÷s9…Pµk¹ ¼ÀC„ER5j ­.ˆÄ¾É¥½ã*ZKAa@¡اY´ô¸—`YœüýÏOr*n0‡úDsFV ¥õV™ºD¤C7¸£ˆK ªOYúÛŠÄ©ÇbØ_1Wzó_õ'ñÀÝš¸ÉE­žEyùK\⇬k 6nÔ/ãtûÍ‹Ü\ä'5“UÄšmìe—¨ÓL$¹îM>­þlè­¾í~€p¶û>MÁò‚›¬µc0÷tí2{ê?™Ë'ð ÇgÛªM«W°[v¯½5ΠˑъöíHÀÁnø§ó5ïÇŒÜú23'ð™cb–<ž,ÜÌ4¿[kÂÆ,[cÀ®3ŽBÿˆ¦LÇEm¾NKÙ›&.Q…«º›Ò²!ìˆ^Ê!¤t›Â²F²² …ŸöÀ Ô[/5p¦ûæ( i¨Ë´b-¾@‰DTаe–E˜K¤ŸF¡#oæàÊ‚€Ü¸G嫽@{À_¼máÇ?Èq¤¤ØÕìž ÛÜ]ôó/¹3@u¸A:ys³ ÚFØc‹vG²0̦0ûÀØC¼a #B4ØÏXUî8^²cáDŸŒfWç?‰A Ø=sç°‡Ev©QKG+áºÙݯçòÞ ÜM«É¼Ð•zNùÔ.˜lÍ÷©Õú)šê¶ó\©,CRJ›i#9@ô…D¹–¥ÇÅ ¤d‰¡•ê`•ê0ë[]V+×ÞF*1 ’o¨¬UF(p!‚+TuÌHªØ Õe˜bô•»~€u½¥ÑXiÀRâªÒ£¦ ­Nûâ-°ù@Wíëü‹=ÅPoVŒÈщî\ܱºõ¦=ݧ'ó껜 µ„žDè—04ì[QŽäÄè?êU~qXŸèùxuãš9˜nxä»j«W¼·ÁÌ·±ÄÖgsK\½k˜ŽoBó¬Þ¹7ß •r-¸Ë;‡†à.€R+Dgîž}ÿñ—|èˇ· ÏjßZ™o?þ·]yÁÜQÐâqΠaT^·Ö8ê(TгÐ_çLûü;xèiø1½LÈZ$8mI¢h4³6ÇЭŒ2GÚ<ž=µ:I&d—ÔÊ2ç4üéÇ€à€ð:œý[lá|{L?èy±Z«ÃW©×ÛðZ)šiœºß)Ö¢2êÙ:wìý¥~‚K—™ -zG?ã ÒË»sÖýň҂¿öNÌ!ÚžGëNù`çwu< Ï—ie…Ù¹wÂ}z. ödëùH¯Ûn})¯¥,Ú6¢s©y ÕãSãcãê8Lçž-;äÞQ;éèNœšjÞǦ‚þ8Zýl²Ó‚“e}ä\Þ¤VAÓŽìp )0F…˜†–Ö~ŸÏ§ßG–¸á m˜÷.Ðî&`bûý«-”>hÀ’”óß+F‚ßÚ9Éãl‹nõPÓ'M¾ÿ“jU@3ø.—Ÿ§Á'-PPÛŸÛ)Æ?ùÕC0vö[»Qø‰ÿÇúAÄ„I*ûœðnQl.+&: ÀÕP¿]ý>!ÿ¾æã]4bG¦=ÏûFûçÔß¡·Fà `Ík©ê»c¿6æÏ[Ñž(Ï7œÃmÞÍ'“ñîéæ, jhÇ \’¨y63˜Zž+‡Ñ|ù‹)Ø fШÌh~—Qü,ásùa3üG˜}#~¨Ã#F"ôá±Ã1úð²8cxQ#Ìá1ìá1ã‹ÄïúØÌ_Ht…–¸Ã# üá±F1ÞðD±,¯:Wpsgõ\j.3%x\zr˜Ìäe’ã}º§ûv1"\. %xžØ¤`vv  tB w Ô[–J$2šð…|S•Òtœïn2ÃSk4)YÇ`ÁNS±(Å*Ʊ8•h6Ïô© ,)}êêƒü3Si`»<@±RºÌ(¥Ì·&'Ç]Jêú.ò s;1½7ŽÐA‚‡™ƒ?—Ôã[qîÚÅæËíˆ>|®“MòJ:kÔóä,€"ñq±ê­EÙ§óþ×äMtÆ|õ€ÊVµØ4º}ñ1f•ög6Ð}c£»|³fµªah€ø¤®C¥xw.5 °qK\^ Ü⪉¦ú/vŒEá/Í ‰²Qd$XǹÔ$¡D§ñC:Ùý?íÒ$$lÿ ÖO#¸É#€×6€z,ÝR¿.÷,=s󄈺÷Õ’«Ö@×a¹m€«üÅxXX﬽ü)˜™ÂÅÁï,!@=§ ϱl.f„~„œ ‰Rû*¤"ÑôжÔa»·Ul(üÙoïæãA?€XêÖ.«¾/R5xª‰“ÛxfÉìYͳ7.næa¥%•âB„½!ZD/ô’Œð›kJ©ïü"Üõvd^Pøã&×c$¥M»Ì­ª5oc÷ë±~C¨ö‘½îÅ:«¦b°Ä¢»0þ'íIÂø ™0Î\ÐbUD$Á p¯®œ?>d)–\–dÓ²9-ÑeVoIš Ý]ÀžˆœVÌŒ“Ö«,®iÿlØîð¥¹kÚ›®).@§úó_:‚„ý¢W7už`u9›ò¿v}gÌire—2Z…Ì–%HðЃs9–ô&™aÞà%šDÀÀÁ BÛÊ*vXœ—LˆvoZ û V¾iH·xp.bÒ²`ælŸ(üÔ ú÷œ—¼iHj|#cΈRósÇÑuÿ*W1Ç•Oh•º_uªQáþ¥1¯9Ò0½L&!‘*Õhù£Bâ?ì†m½+·Ü=|ûÛËÝÏœ$*,¨;­3Xæf9 Àíd>°7b{Ì©ƒLwt½ AŽ]ùCö³“ŸB6n48LÜÀ¥7Ìfã[Q¤Ü\/4i©^A§‘ž®5ü¢3 …°ªvôBP·ô üQ •TvøI‘³¸N/ 2mÜè­‚W¹ìt‘(;&/[v‰YõYiãe‰ì(£ñ‡¸âèÈCâî¬Ù%¹le/LT£ÃR›Á€ÃkÐÞˆb‘ö ™º‘S"µá{4÷¨FŸ@ŸŒWÜ0…ÀR&«D†L¶®¦,'Ñ@âóLuy¹Y6e¿RLgß–Ø“ ]¨ä¿ ¸«;Œ7óÖL1/úך¡Pó6Úv×H¥êœ+ò²TÁŦފUšÞ%9ÖñRud‰æÛ †yòHaéþßõª£ÖF&›ôÉøªO‰Š½;–Ê’Cƒ/ËÎe”O™“«Ñ0D1¤DÃãó;f&k¡f„škÀ[uSƒwóõDrÆg¦Õ« óy09÷éõû ëµ+‹ôß?·ï+#Î’|å†Qd¬yŠn- CñæÚŠ¢årû+ÙÏ…×hÿ¢’Ë•ùŠ›=óîIwhZÁ°³øùöâ_MÖÞ]Í£»¼´×7²Ƕ„•˜­÷‘–=z²Ïq4Ä´òô}¸Â,à^ £ ãTsÓ‡úŒà~ÐèÚ12³‰iG°Y¿fbFåŸmŒÎ½2YÞ>iÔšmí0éûÍqÓñïÛ®J^=°>Rjp×jPêaõóôëת ìu‡pT­Ø %Û¯ØÿÈ^”³ÙŠøRFÃ/ØCk ÐiõÇTH­CºVÖé\ϳËûád«#©­™çÄ"Å ©ëµQIŸ4¼›6b;Ä-ÅYýåìt²LëtÓyèÀXN¹Øã6šbBÔ%"Œ½uWõ ëôXâÞ²eÿN•pË©_kI»5j·ú°òêÕ0Pcïç·>äAÇ-í¦ÝñŸÎÌöžétzngʤeqvÈq̾ÜT-Åz‘w§ î¸Ç  ³åæ?xÑû¥ÏÒ̰σ1W¬’î‹É¹+ö¤^Ÿ"…Q"IJ}“BùËVÈ/pô³DÒÜj–&Ì5·â€þÍ™('êùRF±¢Ó:¬¹™×dìZˆ!$0¤M¬cj ã²R è]Ö/“¢«ó­ý‘¢EÅJ`\[¦”—ó’ÞþÐiëÄZ¿¶ÚNh4缩\R`ÿíáúðØ–¾ñ+66V5Í¿&ØÊ܃ÉwyµŒ;ó`ÆSÉzò3îÖQdDù»EÀÁKÑõ•HücäË0¨&¢4îi_¯NXŸ)ê Yɦƒ€¹Šã0h|KŸrKy@jš¥Ë!ÈÆˆ‰ŽÕs•v$nÎ=$t–ìp¹³nÃÏÒ ê@0ÔÀšIÿÅ:ˆ†ÕÝ>³Ÿ\ÀZçð\ŽÐ2ľô‹^ ÃÕUD´È·,œvƒ®zq2™™ÍÉ{=-E7­›-y-IzØ\¹˜úý|”v‚¸È:=R;C7=Ó}2;UÄV—DÃÝ…’o=äŸùZÚýû^鈪ÈL¬Cÿþ4ÑMHwgR«]”*›ËËËÈ“ëá:OÎÂÊÝ rv °“þ½`~:Lm+ G(oYkŸ}‹·-/ˆ~¥QØ|ý,ÏruêÊ!ÏݺÀqÎu‘í,âAJ¬”Ú&}%ßT•ÉZz8M!wË9‚Á±^SOþrlœ”s-.j55uÊBêÿL1ãdZÇ«'Ü‘õÔϽUŸ4·Æ3sm ]¼A×¼š®Çˆ„ #Òi°|­48ðï— Š‡©¢U¼¤ÓZ§<ÊxndÙ<Ú"ÇÜR³–\:wŽ›[ËrGSêrßµÃiÜ9ê/ßÛh„åY `ß0¢7RT5êÎhß²ÃçéꊌƻJçÞ3’Vü0)zc (¼kUÿ…/¦÷-€zŒ+¨çZ€üŒ%ËË•µÃ £Y~ë=c¯Ö“”Æå¤I°*<Ü&i]5Þ6Òúáé!?g¥¡ŸŽB’¬¶òS¥*]3Ô¤#U>Ì~9‡ûA5¼åù–>Z²ÕÖ<•~=hЂÐÒbr.â!»)ñ»õÅe˜†I¹è؈bz‚ÖÊ Œ=ÚŸ‰M/[ü²ê’Ù Á4"_ ù,V1q_ô•ˆ—±­ž}c¡›ÊÊ,W·êT W ˜“fgfô©¡‹¡ø°ŽqsÔt©‚Íš}Í>¢—˜Rý໤±çÀú^—–ú_ë—Ÿ:ÖAÖ¤n"Söqvh,Y+0ˆ-†¿66ŒáeÐÕÂ(b=Úp<–"ÜÑw.–½{¶íßdªÃ'(f™¾ñUW8Ȥ=^ª×ù§Î]ßZ6[F±9÷éóp8^«ªÿÓ‰& å&M‰¾ª¾QÕnk¤ bs:á2—`­=DGu3z‘¯Ãâ’Y vAúɹ[뀗ºæí{ò:ž)zIñˆ¥´ ¢ÓÔŽ¨  x(ùÊPqBˆ ûn؃#2ïk ‘·ÎÞ?ÑB@áhH]¬‚jíÄýxÛ”£bH¸——a¹~væ{4݈ øaåç Ê¡Üú“®oƒ­̵CÆ9³ôÜ*Ò&ŸfŠnÅ+­µ#¶º×’ ¤¡ÎÏ¥a¸¸ÀM¶ž‘q9ŒWw3åÛkR½ýµÄ= =¢GÛ—CntžàuêºÅ¥\5RÜgJ èžq”[‰ƒù]"L…w+öŸTÕìá–{í·ð[ŠOÅËÞ¾N’Žó!-íÙ$«Ï©õÃÔ\£šAÔ {ÞÉßk Ù®–N–¶‰IŒsÓ¡ ¦uëO!¸ë,ä æZ[iÖË›»ƒ‚g,ë' ¨ì@14ÖÖFLØ©¸ü%æH#£‹ñh@{î&&zjör ·‡†ï­´V«²J ´:í+²Ã»lßÌñ:qùõSÍËà€–¢yÄ{Hl$øé—Š&ÕÛšÿ!yÝ\±Íp‡Ä0 H|W7N·@´;B5ÅŒ[Õ¦ýCßܶT Ô<|^8vw²»ÁÄ5]šy#­XÙ—œ^àR7lXtêI3,4AM`b²¤H’DÑVO11-Q¿ožFôùo™CÊ¿Æ3‚!ùbνºÍ²H ˆúüÂÈ XÙ&º0í42 ‰/ɹVD-,ݱûG’íž$+3–AóM45N^ ŧC«[ÃB"õcg’ÝǵÔZN¾áÕ°Á©_û±à"«7Þ¶±ªjÔÕ'DàÉXc»QâŸäs²–à—ž(ŠGT<À POx¯ ìÎÂ]øó^Øo^ wáó(s0^’êÙÈàJdÜymޏN{~‹îQ98¬¹¿LùH»n/ c´¡„Ão^Ö/Få}—tfÒÕ+Ù÷®H‡= L±ßÀñKÎxA€,} wW3êÚ;îAì?™|à-  ¬à2óýùÃÕô·Bª(4ðQƒñ¨-“`vêˆðMÀ8Ço€9B|ŸáYDèËÄÌ’úË £Ë "þÀØ€&+/†´©[-ïá¯Så«S5.Zqõ¦âgœr'ˆÅ þ”äù>Bâbs©hU4ž¼qR£2åÌ/€³Â©·H.UǤÔî™T:S½ï¶æš#Ðpà3£M†Â 5D(2~Æ(¸5 "ºTÕnsež‰qù˜¢Ï†UÚ¤ŠþZ4ˆ1Ã4ÄØÒ7›°cmް½WÂB ˜ ñ…+Zâ.¯V3•»Õv§ÎJˆm¦Êb‘¦Koœœã(6@3’šW±„ÁÂùmwª‚8FÆ4÷È_áYP–]f«?¼ÂX¿‚§½ ðßùÄðª„/y·Ggß…8³¦.ÈH5Þ ò­öÆ&.<ÁMk¥¤Ãaä‚Å:©^\"»™Á#d5–ÇVñwë_ܨxÉZß´n¤?\tr9g»%Ýtíœ#ↄm[²™l»,<»Ö_«Ë©¼¸`~6f}SÞ9Ó ¥G:øõBsm Ba.ç˜8n£úØNbÚÇSÃÙ¿¥HZ'Ä}ÇÅB›#ÁÆ?^²VQ@Ë¿†ŽÙÎ*9Ès«¾È«`[²H1§1¥ÎØf}cÒUU3 Ÿú°{%1 £W‰rBßm±hXËœ¸ö1[­e(#Eÿ®R‹º ƒÎÿ|‚2ñŽJÇpÄx­Âvd·»_\j4Mê†ûc·&”»]¯Q8Ó?bu>gÂ_4í~ÂòÿØtÜëJÉ­—Þß šyBÚj Úü²œ“íÚ*+’‰$D=N?œWÔd=—­Ù¬Õ9®),`ÊeœðØ$•y³0P½`ÃǘM¡¡Ð/‰p2’\ÄÜbª>›ÎìK·o7ÎVˆÍ,<'¹V2aŽã c•²Ûú‹/ÑeVÈa¶zÏ{š8¼x‘³ŸR÷ \®k'1á” â³?£í\3ÑIÇ·Ž/Ú²f¯]„öÌ?u®è4¸3üIU‰_¹¿ß$±ÉI冕ó`÷d`¨°îÿ&1]Åvd›™'…Éä„ɇz÷¼ÚLÎ=ù†‰oã“J¾g‰i„úIåÒk*¥W!È„õTs:XH4ÂYÌìØ'z×5“Y¶TkŽ]ø«ZƒLgx­pœ;|ë‰ÍÊì×êµn&Òæ1> 9o† œ¦<’n%SÖ~稌²ÿ½§qÌ9ª†Å«sÕANô à[ƒŽäH%Š8|ë}MÀµ:ÒA­ÿ™Gè±?#¼ÄÊ‹¬V8É>|½CK–¸Î¤Ü¸`ãXMMݬL1ý²Ü]¶ÈV•ÉøÓôŽßNÀU“1³I¼€´§|¿7KJ y&Râ+ÁØmX§Ä™ï+гž˜Bw‘P2~™F]Õt>ƒ³*(ïQßï ÀÞã Þ«Ïl";çôqÛ© RUÍŽN% }Æ ¼SáLoœ×#5Äh›e0}l Ü=‘9¹Íú7í0ô× ƒiý‘“¤J%Á8½‚7¯ ˆÞU„ªïJ‡sÞ ‚Ýô‰äëÌïÎ2È[ÇH±nb¯é_ýh¬aÓ—| ¸€k?b›6Ð9ÛH/Ç‹1@X¼PÞѾ+³i/-oW4º%Ïó øÁ+5 ƒ}#Ài±ÆýŠy édfôvÛO¼™Öê¼ µ¦ÿúaì› ì|á;¤ò³‹µ1¥3É“·‚Ñ~I±{wÚq¯6 1õucƒœ’8—<ý£Ÿ»¢)f©7 ³¹®|Wt/ÚªŸ>ÚSáÁUH7X¶NŸÑë¿p_Í[Â)™xÈ 6kÆÔ³>¾°¶ÌS${玖 Òâgk¹Ð¿*ƒ)ÀO )ÅÃÕ‡÷ßÕŠæ×¾jàùž:›¥NOo‡>ißöxÛΰ&J–ÕÛÜAÉãrª>zpÒZìb —*Ï™Q™$h¢wò8œ|kxƒÏ€¤zì\>7PÖ^éyª x#¹ì6ºÕÏbÍYÐ@Š?U¯pÔ Hry¦~xÔמ(å–{ä8ÇŸå< dâºÄ•mµ` üwe·ü6IÚ…£]PŠ5Ê`ÂæHÜRyk} TtÞ¿¾ ÕÙðìã6",6çWß9ºÂP×h;m¶@~dV3¬f[Þ8¦_´¸‰X'&\{Aà·U•P[]ö1uÛÏx5â÷⠽㻭ÜÜéóàrYî·'ýH¹ #‹¿ãy}p¸Í]:—¶~yy ƒmm¹á0·`KNÒÆ•»È‰í™¸ìBTFKžq%z¶"ͬëj*ëNfw8|Q¼Gïè¹ú^¨%+ô°Žx­LEŒ·º¨öHø±­ž*wÇU¾Z÷Þ¿Zì |….ï®Þ9û,ÈÁdØuüp½÷¾>2P}Nðþë&w ‹HJaS]}3ÿ™ õ®/å¸ÛŸz€ó›·h(S7“Fsˆ‚²åjÿaH0árEk»M,„=4á»øLd=˜yU<µ9?Ókk`íÊî=OtVˆÚ—ÒH±.n #&ÃÍœÅÖÍŽ%@˜Ib÷¦åãnÔ¨NÑ’*x¥hP%ÚÐ Oû5Ľí¯JãZñdÉõ>;>%”u`^ Nc“ϱ¼Ñ|¨¾öø3¶ÊBæ¾?Àš¶gHÎ?êš2(ßïò V)o}óçJ;…Žn©GË5|´>íŸßÝè÷‚2úAO‰Åìbü)å{Þü»|‰B7ãöóme>â ༦äRmÄ&ý¶ÞG2pu8]ËÐÝй ·Dã'¿¾”wcî6o:>‰ú·un;FM@=àȼJCëäŒî®æ™½ µFljÎLžÄŸ~wp߆K³= ò¾,´ò–dþ'‡… ~OhJéÚ×Q>¢ÿºÿž˜âÀeðt”âÌXTG}ºñãÐM?˜µs°½3£xí½}wÌÇ·ƒhFÝf¼­p¶1­k6î²µ•±~x“«âšÁˆÍ}Ám»ëaÿž;Éq½>¥$Ž%M«8‰ 'äé6G‚æÝ!—Ð'xã‹gDšA8¢6ìy¯5f²zÝYA3^“â sÚ»—ÞÖd׆Ò÷E¡mÙùoè÷mw×eÆ;ÏŠÑGg¥óØP¤ö8TeÚê]¾k2c øçaÞwŠí¡$!‡G¼úLœrâHuÔDmã ÓT°Ôºñ> þ”¯'N¦í*pê0^:v[âi|ã䉬6Ah´\ÝX·RÛ3ð|.à‘t}ó~4¾ïb`º›×AÿÔí÷»òtm5ÇB¹²rRO‡KÒÍÑfkßâÓ¤Zƒ£ãuðG+­~ñ5Š_ÿa°Qœ=+ñÈðÌ—†„Ò¤'‰$á–"g'a=Ÿ«¬ƒ–t:@âð å÷b/)GQŒÖvdIc „hà‡×ðfá{Kk”Ô½âHzlÃY³²¼¦ª´Ê\JtŽ\ôj×óÑÃu y1T9&í&pÑãHb¸º’ +{¬M hP‡3‘¸ÓªH›…! 3Wi—¥)sÇ6{7F» ±Ê#ya=Á—/žCƒ)P‹Êœ}Š+A~žÎÜ1,®ã²ëÒÌÒÑtñ=;{Õ ÊR @°óµ­½{Ž¥S·þY;#³›2|a˜ï¹kkùÐæž¦l:Õ5Ý{ÉO‡hu¿œ¥‰<¿ BΊ}ïÈ7š KX$×íðÕh´kʶÓ[ÌC²W ž>k4á9¸Ú=ôe öï˜C0r¢ZH¦Ïã«RV~îõ¸ãÍÌßì]’!寘/{åoîR¬ÿë‘­6êoÞ9¡©ÄFâqùd¹aD”¼×^§e8&[Ƶþª§p¬_©ìͽž˜ƒ):oIç%èt ÆÙ ŸóçiÆ•f_¾^å^ŽG·_ ªŒUœ‡òŸWãN÷ðÓå UléÝ.å®~ zGÅRÚÌ)ñšÕçÜ?õÚ<›n‡ßq– ÕÈŽ½”_YìTé‚ð 4.ϼwA~–Ú犡ƒ²úŸûû÷OoMœ+È ´8Ó ‹Âä.©É³rìèÿ˜¨yn¸#ˆ7šF¾ç½ÙD–-1#>SEŽ}>÷ôXMw4þ]œT8tîŠÑ9Þæµ îB¿Îä©â6¹þå±€œýõid½˜m_¤SlÛe܉?Í’Ù­z†Od¹^ÛTþÒ»ü¡³ünM˜Áp9%}/Òt™26M|:S¦¦ÏÁc“„ØTh"‚sI(o˜rxüˆýG5yÏ’öSC™ ˜zñ~µ6Â.–HŽõ?vZeÝøPÑî)†¦2cj(„ÎëÇ£´½òÅ ½q†öö͵Jµ^×OÊC€8Þáö¯ê9ÃàÂ,A^Á»Kr…PGƒw"žÈ%Ô`j÷cSM¯1tÆVó²Ùvrñý7ucéEÔP/%iGXŽ?kÙªf¸l¤Í‹÷:¸Ež1†³ C6hÔt}-%}¶A(³¾´ÛsÆte§~â9fðÇCA4,sÿ¨üìywî|}v*ÃîþyðeÓæ'õ /ü†€qê3ÛH€ãûšdï4^³ËЯ†;‹‘Ýà ÂÍh*²ù +ö„ÜŠ¹w®DïcŠ@¶åâÝ/ÿ4ÐRÚý‘x8Î|Noâ˜üt6äÉs¿…î[{'Ù0§xetÞ0Åã{žû#ºçõ:Vñ’#1'ì½ÛcØýÜ <¾Çfè°KxÉ£:nOk;ùs<$o›óМ¥O¬"œXe‹ÖZóW8µ¸3ÒXƒ‘§¦º(Ñîr1™ŒCçªXÚ©£ÂïÂs&´âŽmŽf ˆ½³›¸ˆæö‡-‘óÀ:M¨µ³uDc]°àzÄž„`EØä°ª8ï#]):[‰Ûü‹OL’ÐݘWœÝa© n±Ô›ï“X%ˆbÞOÙv®D\à,šñ3(^¢.;{üX÷ÉœÞ'qÛÐ$©Š,Ë¥/Mt>jï) ÐðŠê…ÿÍJçbj§Ôã^a|"õÚDäîÂS1ÓŠ[H Å^½=¾.LjÀÃe¥˜OÉð%Ií0=Ò6¶QÐ/üÓ?²O[„$9œ™Ôzb—SÙrôxLB}Ûð^lc”õÉz_°IÔ:ù±†r}åî*Ìe^ýø¸?OUIñÔúøk„^©Ð¡jµÛ£¨kÑÊŒîäãR!²Ø®gÛ"Ä+ ‰wWм‘wjeafIa ÉMbE`öt<¼$ß öâ‹kG‚)¿ >†Ñ ÞEñÐhñæŽPŽ’ ÊÁ–e|„EÛêÿñeš°BŠùÐ0¾çälâ¿Uùæ«©×ÓB’ƒýùìê¥ëÎnæC †€Tû&…K;ïwï€N3ìy77ÂÞób;`ù .KUXb³[÷¢-yz,0%Æ|cÞÊÓÚ˜ ý^—þ­5/Á‚ÕÃÌ<è1÷(©ÿzhª~,uøL–C­:¤‰ q~q`Ø"æ%1Öv–Ù«º? 62Av51Ù"ô©ÚäP¡q¥ +DJû_y‚jZ¼•!1’V>)^:ˆsÄvüÏþ–KUU,¸½e_-Žo·{bÊ”˜(ÃIš}ƒÜÝ;J.RªÆ¡Tæ“Ýz,á_-H_½i" Ü…JQG|WG‡î4f*=‹‹ÐÀbO.ö'™Æ€Â8D¸7×á?Lâ‚ÑM®N þµÓyow ¿‡rF{Qu÷*VwçänæŠwlØ¿Œ.f©DÅ{]¨#ñæd; H>CÎú/ QBmñ9Ʀ{ýdëOÅy˜8ÊéÄ¡V””u𫬸çƒ_€%98¸,ƒÊV®³&fRe¥ÅJJÙÿ8çà2izŒÞêô¬hiB×zc¿”re4¢òÝå•(ÂËnÿôH<†¿Gêq”Dâ\Òåvÿ8Ø])ZÂ×ݳ ½ÿ~¨¢ª`d¬ƒ(ô{¡Ðñï‹Μ†£‡[ëý˜¥ª]§ŠÁcö ³øÂÛôœÓ»ù¾øô÷M)[< ¶R4úž òdmn†Âçx‡™h離TàµðÆP¬¦¹Ÿà`¸ýÓÚþxo» ƒþÖ½¤ø¡»²˜G•î@­NTM:<ÉŸé=˜îT²"xe{€'˱f35¨O‹Ýëœ^÷iòðúüº2y$K>`ìߢؿUXëÙ9?G«ÙŽææÜí¦ÝŸC u)›þqu³^19¨!E miÚû…kæuÙß\^ã Í^tߦ|HÙ¿õ1ðLa^ðö˜6¾1Y# ´í“70áðÞ¯9¼+àø"…åàÝ×¥ ­’Dt¼ÙÉ¿³gôñîVÁ<Ög~·ð6 w æV°¹Òp¥Âa „á>1ÝFw9gÛž›-æ8¾q}]\7s^Ÿ6.Ýڪݽ›üº)®ÑºnçìüÔ„ä„ C?3©®k­«¹›¾éPbnÿ°¼°¼zÖ4h4ÿ5Àð5?QH¡:0Éa`¢q¤N€°¢…©l¤¯¢¤©C„®¬©®EˆmHˆFwF„¯¥g§¯¡Š…¥:3K0Ÿ\_3 S™://] ¯n]~c0°y¢Œ­¯¤…l%îx¯aüÀÿÜJ¶¸b–•8$À9ÞÿòfõÿkÿŽFæ®ÿUkóÇnSüÕíµ©YñZ‡3àù©°±¢àŸ"V!ÈÎKë…F,?[ë—QX½Êß›—wë˜>„ÆÍÜ?@/عqH95¢l:FÃ/¸”Ï@)Þ©ýñŽûÆxòd˜¢p  $éí&͹qó&áFÿc‰~ ´h¥¹06Áz0JLA’îïA“‰úÇ$íc Œ”½_ˆ% +2wi :# ¦Bø©;UÈ›Ÿh´‰ßýD…¼1q܇œâG’¼¨Ë­ÄnŽü»’8ó :€ö®ÒòžmˆØßMkw=­Õ²eFƒzÁoÑ¥TS™‹ ðáˆeâ"”^(ôŒäÞ¢§I´¤ŒÀ,Cb©áÁ‰Ó…Ú´ýÕS Rë>²ÜHd½äH'è‡tÝÆp¢¢§Pb(Š $&DÇç^ ÍD¬rÞñ`ÑmŽæ„¶”ísÅ$R´·$™£Eát&3«ÄZ¬ìž‚6(ây‰ð‘gÐòßxæBÑ(Xùü^µAòô|G†*Á"%aQò‡EBÎ$ú#„-ãˆÂš„<&xLÏñŽ$ “"Í-Š ái ÃèÒˆPÅÄèA”ìÜùÁÙá€ú7¦+‡šÇ¤¤iéEÌOá{”íê«Î&iâÑé[*2Ø0¢@d„€@÷]"í]DZ Ê é!@âÝ·”' `$ò§ùh ¹fúò?r–È´*$ ¦pÅ­ûÁ€‘À‰ Ö§€þP„K$ÝoÃ(»x´=D§ÐØ@A_ÈŒ½¹Ý^KWBáëSŒôk=è'ºW.DõÃÓÀnî.ßq ^œ½X9ÒäjŽt¡y¢ÆüpÍNpçK÷Nî—ºraK ,üÝ· ÷ÇGã=8²eEUÐÞŸ#D÷H0LL È[…OvY¿çÛò_dï{ÉHIiÁLòý»Dïï׆@D0ub²o H ¢½ðÏeÍGz%M÷ÝóËÐË›Å.î£=çX=j  …4ÌïIò„Ô„(ÇÄÐð‰ÃãÓµ’,¹ eÆÉãÚÉ ÌfðÃo‹0¹âŽSM ÞIéÃsã u·"w+Q»Yw; ï'Z›~.ϧvv:Aí^ òÓ1œÔ’c« QYû[Uwltû7åÞY•c£ØPhØÙÉ3Ù}|~»šï?œ½ßU;ù–ã3ßãA“Ùe‘Ȭ}?°ŽËª ÀÿĵóMk­¤Ðf:åÙmuÆ`ýé¼eu&œK<äÔWVÿà,–˜¥d´M¸TO°WhRpao¢zYæO©VÙŽjoá^CP…•ä–7c°ÖF¾mäÙà­EÊzÀI?½ØÈù)ËÑu!«9:¨ZsªõšÄYQ¸Œþ¹š¨ë<3Ä»l×9Ðÿ"?TÿC­ö¨Eÿd˜Bã.g²R±º]Ÿœœ3“ž3gHcšÕW üG›2ÿSù(í7tC±0‹¿vÊ·®ùÔüU¬„ HPB Äi'˜'ežëBdCé¢gOÄöר‘ï.ùvb³w¯âæ{âÙ×¶Àlô³ÎÀ² ^´¹-LN‡õŠ@oëGn*°‘\BçE†Å,æšØ€3ˆAš1•ÅÅ R¯jÐT±’>ÿõ2¥iØ"¤ I}¯G‰~¼›í»ÛÄÀc?AïJÑ9ŸEè¿ËéWH`‘b)Ä@*I™w[š¿÷E\ö0À?g©K1íCƽg—K©'Ž/ö¹hHŸ tm˜â7í7ë®;Èâ ^6ÞL'­·Ý.%+†?7tþõ–Wþ?æù…ù U'5ëùK7ŠV››ÐH˜!ήðj°Öõ² 8>/Àqiå?_N!yõ¤¿šÁj7_šc&W1ŠÆÛª¨ð¿ªÐÚ¿¤ØûÑ'Ìof?ÐÊ…}Í:Ûø~:]°Š[½µnqèeâ;DŠØwŸŽµ?dâK;dãcŽª;RÖ}{g¾†q'Rc»Ý«Ÿé ª1!»·±F7ÿèPvÿr‹×{ßpàQ ¯øþ;$¢­?Ž®ÃÜ“ýo€ÿÿ„,£Í;5Ô>¦/ƒ:¾Ãºá†¥yÙ¢pÛ%gJyÉòÅôu j¤.,-—ËÍœZ·Ú›7·U&7£.¼xea” H<ñÿ´”xþBA‚&øûiü~èºo@{ß³Ù¯,Aù´õÝ.·Ûm¯Û޳¯ì«i>ð¯ßï…¦;UáǤ¢9—Ïa;-Á÷g³ˆJ*‹=$ªÔ‹-ÃÖŸhõ‰t_Õjã»(KÙeGCRòêc]ë·ð݆Å)&ÆBM„Þ†E1&s¾{‹OÿøÖu<ß\®²áQONQO-â¬à‘xLºæQµÐý<öBäèô1__TÞv2Ösã C-AȇÕI‹ƒ<Æ"¡‹sÌäz&nÎÄØXh¿åèØ’"³¿M~ö¤XY¸¨"Ç{ºÏ :¤’Ë’Q:ªŠÀLëk• e•—ù!U„*zN]0¿Ü‚ÓËŧª’ÃÊ? ÞɬÆÚîÏzB£—ï‘÷‹YŽ–ãK‹>Õ’e\eçèg3QZå-ÎP3í~¸Èª?ouÜê> ½º¬zRÛº6VjRÍÕç /’•i*«<`ÏßWèt£l|àôU%o£",üù Y+¦Ý¤EÊå6Ÿ;ï2–ÅâÂé@-íIÍ…I¨Œà ]â¬ÃHG<ñÒΞ„©ç(1çÏn ¶*õ ¬ …Øë÷_Ë8š‡òqì–6‰]0xm ÐÞúYÙìÍ fßò8Ñ#™…bÕ';aDR@ Kä…wW¢êa˜ :•­_ò¢Âø+t­àöx°xÈ`#ˆ2;?üóŸ^.ª‡#DñÙqÐ˽‹Àg•O¿ix(rûFº8¬E‘Êé(£PaÉ+hn­Cùµ³†‹Í^G¯ÛYM»ÓÙ\^F>SYy½\RÈo’Fi¤Æå7ïÃùèhfEÑîÊ¢ <ÇMMo§hiíZ°ll&Ÿ¹¦¥ ‘ï01êªlú,êo8bv®÷IÛuã`$ůNÈsòM»0•¿Ër©•=‡¿UuîuÚ‰YYJ'o™ Ç ªÅ’êU!/ÿÊõ¸Ub)µ|4,qèUñ" ò¹ï¬ü¹aÞUrëEk¢Y aÜ@AÆIðÑBhœB§û¡ ã˜Äè'ç†-‘„X+¹zˆ>?9“Ä›§é©•YÇ*áåiõâ¾¼…UÌüoK•ñ¬%®+æ¥f E§³Œû«Ò¬%¸ñWI›†—®ˆ³L¸wÍE³Ç:ˆCWº­ ¸úmëýÝ‚‡<‡‹¤ÿ©ìLn?E¹L›±BŒá¤JY*²J+ %1§`c/§µì€Tq}t£]®œh´à?tì³æÆSìÒ*å@¼É›[×뙬z ˆ `¡1 Ðr˜©© ݤãIå1Å‹EûLH\¸¨u8ˆ…™CáŠSÈÎ;ŒõÈ,ðý¦& Ã}‡­>#sºSÅ•Äp`âK½]ÀG42úÄÄ.ó´Gb7ec¬ã„°dŒ+ñæ“ð­AÝÕ”;AÈ“¯z—¢IW“S€×0«kŽl|7 ˆÁ²6Ï$/j\Ê®—‡ÊUk9½ š›`ßh¹¼¾`)Õ1%ÄBf;9„6""‰ú÷›v•eë°Y‰<ø ²R‹¿nRŽ>& P1ÛxõvÓq;pÂ\vµSÜŽÍ “ì õ÷ù6c›ôµKåx®û“W’Û‹-E]À7w5ñÔGKqZŽ©N£â¥Ý€ùBßÚÐJïÜ£¤–¤šv±—¤¤šn»Y?LT6,ÿ!jÁ32±È‡ÕŒ™¢°b7@'¨®—WPb+¨K}Œ±r‡ÁÞ ? í˜Dæ²A(_È7<.ºèÒ˜žX eÍ(”`Šó´úß‚V‹®ýÓX†ÑÜuq=`H|ÀÙ=­éæçÚ+ZñÉ#;hvæjQ #@ýŠü¡gЮ׬d²²s:ïW…ùõ×3OãÏìóC%ó0½9¾“pk¹XpºÖ—«*(³®fs­ƒüÔÚÍ1a%kЗ6á˜ÉT8¬ÝùˆY1ßÈ•CÆŒBÌWЏ%¨,:ê„{p¬:ª¼·]Û/è•üºŠSÍ?V.ìb΋>äÁö/)°¨.x¤æW¸˜±ò ðck„xN/23ÑÅÌ6àÈ¡ºÇ9OXhêôôKjW-£Úé2÷ÉÑÝAŽÁ%ì:šÅô%±5§m½„„dçIŸoþ!KÅÓ±¯S®LÐ ®]YœªU0o†êŠ)ŠÄ)‘Í÷؉Vø±RÜÛx—ò]•Q()$@°6Þª©ífš70{œ×„L‚?ÁÊTÎiþñëÙ«c).œx\L`$RAt ‚á<ù´(̺›[*[^7¨ud:j¹¼Ù‚x«’J§^ýw(•­ÙŽŸW\®2Ìi5ôŠ”Qݘ-ûclPøØ•Ü´¿#&¿p £¡åÙD…òÙ[ɽÑ=@dâhnÉBƒ2i¨p¦>Ñm½!®+øª¢¼e‘NÜ<œ¢M ä…»ãʃϺ›éÑîMÝĆbcJmòP¢H[%÷mjLã«Xa—$™Ï—:¹†<†ç¢úd;IÔ#Iþ×úä U¸cÆV$ 6†Âz\ÜbèZ¶( É% –+ Œ…è -sËüTÝ(È äÜ?\>Ò0èZ¾5ÞâÏ$›ƒWË"@Ò)`0Q{&|9¿Œ{¼qðõÿËRQWà©ó¼Õ ä†ÏâHý”}¬´KâÍ!/³ç)•Ó¤œÛèr0§™wMâê™±Åäñ/æ¼=oCZFDñ7Oü/Œõ‡µ$˜Ù#àqé µ%õê;9Õˆ89ïâ ²ö’4%xKœNž‚yÉtµÖ^sJ”„)ç“•8„5Œ#@[`O`:¤ûBˆf~pó·‹Uäe+¨·Z•…˜/ÿœ›TÅÊ~?› Ç)îŽÖ9XÐi]€Náê€÷bCfÙG•6ÊH:úF$ 1[µzºç`fz·›©‡™k¨Žr…>U‚5ò8˜ãjôo‘1´Vv…g¦§Aêêà®,õ×8õo…yMúAñX‚ê´…Åž6ã©Ô#-ǬàŠ]J§ÑX¶€Î§,f|l ±¥Dïð@ÇìË»×ÅgçvÑ0FËO» ŸYrA&¾”O^qÔÉ ºvëZ[•Æ¿7•„åâž®¥·é›ËhÚ:˹NË#Å|°x¦[ÆÜ‘=4¢À«õ<‹†ö—hzìû?:ÏR¦±.ªß»²2ŠRŽ{G. Æ¿:Å)RÊÞüìv»8çnD5=~Û^ f+»Ižu;.R“7”ÿÚ’`VôIzSP"^íéÖþÂG’Lú‰Ñîd#›íçÔ¾—léYèÒb,œüËSKÚ>Jí÷:WíRîw2¹FI`c{5oÕel“Z?™Ÿ˜^‚tª.…’ )f”_–úúßõš[¶xsEÚï°_tœ¢ß æõ¢ÉŒ®…êÙ36[QX·àpÍö'ö|2‘·’ŸYKŽýÈìiyoùsüž¡D¥½]™¬ˆ 3Ï}€½§·k>@.F4ÞD¤=Ñ`)÷mGÒóí}²¤IÕ„8n r›‚ïºDÄàu—V¬®‘üúû†Pþ`òæ¶>`FW…O¥mDZ&£ë¢õ0æbâ~°ÿ§:ríWì€ ýƒÄúß°Ð=ìÿëñ?úhGÃÆn‹é÷VO€¢_CÐfdöuS×U‡àŒÝ¨Ë¡«"‘?ŸtS•J31òèê¥oæŸF")y×Ö Û'“ ;S¶;“±ì8]RF5JÇ»ÆÂãu„v5¦ZNÒ,$&ù…lðU ‘YÐAÔ;¢Q9r-$_0ïQ"(É ²° F‡xEŸÁˆtÜw$äññ4.6dµ÷¤£/\ 1Ãb.E¢µ „ªÁ0„×£F¥øxDÞ°Šâ-S#ìŠhÔDF{ÿ¡Ò,‘%Ô2ʹÒyM¥spfÎA›%ÏŽmíÊdg„zžh¨|®oz,ùÚ“>ʪïc¸„È—Ôú¥£˜9Ôç»3¤¦¨³’À¹&}ºäejÔG§Ø¼©%†=¤`…pû´òŒzˆký%`z”£¡Îô„³mBŸîtÙÞý»KçûÖõQà·§ƒ——COºì©±«ÔhÓÕÔ…!gWè.jGû¯°o®®Þ+°RÆk2Ô2~2ÌB45Ö fÕ¿zÉhAFßv(Ÿut=3ç™qNãÀ!WñIaIçR }¤‡rf,c'¥ÚÖ݆,6®–z÷'(µ$ (aF"!·{( ®|m" ʼnÝnZOiˆ\Üz¹ÓªÂ4q¶˦¼éÔ qïu‹²&$ i ".A}¥áh‚HäÛZÆð95“þ&p<ëŒUiî®e¤†ì:¼(J2Aú߄…FÝ —r’Ò ÒAñNÇ¡Bˆ¬vÛ½ g䤇›²¬¶°½‰Aì?G ÑáOÎwÑoõDß~oåèg!8Û˜‰“c´-½õXo\ß!2Øõ£ eqšý31€B·'D’õ‰G¦§§Ý%¤zÔ‡îø°É'ë‰B•8’Q—\|pD_ö3ÕRþ2ɺ&30ì—ä|íBPî<È”&™‘¢0ò>1ïô~Õ.¯WuúJ€*<íX÷^CŒ¹ÖaD3±-NSz¼b‰q@ i€üÕQÑ«®¡œçpв½À¼ß¥Jº¹iJ¬Ô1õÏl26 Ù¶693ƒÐgÜö"Mî߲ܹ#ìµw%›B ÅÜÝCQ$Š(Ä …úAóÍ•$³ò°™"³ÍþšS­¬ƒ,Ñuô î¢ª¨sö‡p ˜gþcŸ i¦]6Iß—êÔ섯Taºœ‹omb7©#CSª}œ:¬é'Œÿ¦Qdõ³àDC2L˸)e‘ÊÆÏKÅ”­~œP|C[~è„{(Ÿ!¥÷Y†¦åE™k5ÓÄñß²ˆz’ê3\72<ŠØƒ-íAÌvú<Â.0©CÆYÁ]h¹lÔ£P¹1€^{#6r»Jép;ojñÙ{xôĤžÔKä¤VtƒJº³4"YyI#Æù7z5ÅèâzO})CåßZ4{»z½Gðü¹)«[ñ°`WÆD¢Z…*NÇúÒNBw˜¢’B%”‚G‰cåmi0D“c§½E4þ`3Ú㡱ßsàÿÉ’iCÔ³®ºãô¦5•tÜ¿Òv¹{aÍÍbײ¼*jÎið¹†UÐ —âTÐÜÇÌ)•ºÌÉúx”üŠ|z¼Ò%5ŠîyÛFN*=fW/ê‰èÆ-óRÑ-ÜQ+ZpHþ\<^¤ñ̇±2÷;¾Ù¸¹#Ú–Œt‡G·’¼ æá‰?¸Ksó»Á݉WÖ¬ÑgüÆbtÂwˆõs‚˜êŒÀ]¨­ŸáŽ•øãpôºösî/#Òœ]lsa&ó.ÌQˆk\ £Z™€Ó¯zT1¥ŽSæ4Ç……t†·÷w= †€pÓ‹ÏSËW뎃l"_O'P†'$ ÝÕ ä·µÚq$©íš%º?]nš¹ù~úÿpt<ˆq æIåô½ì-ÎûzÃýá¥_[ZåüGøQí„›àäÃR‘<¢bæ±ç+õ{÷º[X@ó™Y_çbygm¾)¬†½88´|Z{†Ë£*ªõç Äo˜­Õ庀a,ˆ/r¡cyr»ÿø—~ÆŒ«+»§Œ#ø¯cüÎRC~®šIø•Ú‘ÏXòï'$kyÄF#3v¢Õ@k€ÉŒHzÄK>€åà¨Ñ„3€è3Î!¦NúL½±×¼ûFMA4}! ¦Ì Éç³ Îd=O¢Äý1ÅÐ-½[ «AÍ4W¨C’}Q’ƒŽ#8é¹6®¶xlOÎrÙÔGpðN-6ЋׅÂïÄ4õ” É)¦üÔÑ&Go.0Kf`Ñöj“XÐÆŸxÕkUQÝyaïrþÔÚºSåí8n8 7"°$g;ÖKRœ]±Rüì“®±Ùîôô ˜uÞ´µ8Lú$‚€jïÑ{8*uu¹;ôCØ;l? Æ PíìƒbÂx<^ß‹¿{€= æ­ëÑÉ8k æ©[‚E:ȃ^²ß~@‹ä@QSØš²³™ðq<7ÿsó ¡éMÙ Ò°êeÔÅ,è“4 ì­S¥RJ­çt {ÇTQ›;ª\8Æm.üß|r½ù¸ FáÄ qÐåUÅ]L­Ëbé U·.+8ÍIë`LÇ×׳YØ$ŠñÝñ”b™:{žë|Ô©Âe#4eýfÈø=1=Q¶ÍN{üüï^æ»fÛÍ98€ óÿõ›¸;›Ø:YØÙÚ;Ú™89ýУnc‡%ô;[WNŠÏí4j”TˆŒ-kØcŒ^£zm›#Ï´Ä?5ôlí&Gñ¸WW©§Vo‡œ3«çŸQY¦›wiÙ篕@Yq2Ø¿¯T¾^è<ˆ^úRøTkÒ¡bØÁˆPâ ó©Y©HiL„qÌ¡NiUËûH-°ƒð(øSIt59”o|ƆûCƒpÏx!( dóW‰ëgwÓ—„Z½e‘¿ˆ#2Îq?–œ& X¡Ö¡/2i@<=‰õH/+8 +dúHú«¹|âßaag"½ÑÕ½mÍ ÙtK¦HCçD^Ng'º6ô`ì@«•Ò&2Ëú«FŠƵDµ—<³0•mÃO©/5ò“p|' IiN‘„]Ê^h,ˆMzšbQLåưJoPíÿªT$í‹!-šgøÍ›ùëýæ Õ{~taÎÐåÕÞ‹ýÇž{;ú~ 3z·Ÿ¶ð>g/šyWBô· !`á\’e¿k¯O˜#Ôy\¡ŽrD™Šªå¨>§Š—¬]RêhéL'”¬]ÒÀS$iýz«³ÉüÝÉBµQ7+}+‰²È¡Mºã”l]ईÏé½d¨QÏýÍh8£U’h¥¿<ÎñÂPÄ*[ϳº^R«W¾Ñ=y¿éM¯,Yi•á°³Á'üUÛs³5ðܔЖBHéÁ„ºZ³’ gÚ6tñÓµqcψùÍœ½÷›¾36æ×{íªU£:Š^o08M¿Ñ¤82ÅØÔ33‡É[BabBô× ü+°m¥pÖºeóõ±®NC]IÔ‚˜…Ê„¦uGyAXNÚ;†§°¾&õ€Ž†‡Yã¤4Õíߎ8î!„J—~°j‰«¢OAå‹å>+i¬hØâI:&ÐÛ°‰œ¶ÀVI©íÔñ悊ï?ºâÛ½Ê"bThžë:û@ qT:vˆÊ“nK¢ÎEQ5_בÌ!¶ŠX¬Î¬°€Hbbb}´ˆ ØÊˆ&ƒ;Ú¸Fh>³i£Ê¼ím}ow®)_—“Ì8W”~ 8Ð^¹µ]ä]è4;uNiËMšÅ«Ýr­t¶>͹چqÛñüås~P"K!tÂ8h'e;Y2^˜ÃFÅ•>aü¼DW”…m–sÖyº¥óLø…9ƒ-}<× ~Ŧ­ðR.¸LÇjò¯”h‹ÎÇx„µ°×°W Ù`ò;šÙ®WÉO؇*ÆùÒ27ÛœeêÒòöC Äuxø:ÚÛ1Ä)xé½¾\Ob3Ê Âø¸é›XVIX‰T£Jµ3¶bx<\°ZfPÒo,7^ÈÍ™Ówñåµ¥{Œ·¾{€1ÔXĉ“³‚ãÑŸÿ4œW—~&î= G{üðYíÑ~2wøã5à *h[wásŸ&d¬8vRîTœFÅ…@-oûЀÿ¾mÐÜÀ í{ð8çÂ|ŒÌò ¬ÊC¼·êíÍdÛÞc¯ŒW¯• Õp˜ë ó¡—yŒf£4†wŒ‡4ú™Fxˤ#g†: “ùw5h± w.•×°iú/238ûiêæ,3ªÓ}9×s+¯KïÏ»÷óÑìáã$Í5p·6#Â5É€*óv8÷w‚O’ÿoø6ñ;SMJáÛ:,9³X29T{Âz0Ò­ÏJú3H?óKnÏ+–Èᣠ·‚k pUÚÛ)~Æü­g½áôfÍwFÖ)–MæãàG:"ëÇ#.0¿— nÖ.&*—Ëެšë;E=é=SXÜfï‚3߸„V2þÉÐBu¢âà>4=j)ÄöW,'• ¾,¬?yòDÒ¤ÁTG[Þ&TûãÂF4û?N©,W¢ydBQ›7ÜfäL¶Z>Ñä#`cb›é^QÈOGÑ`ò¾iúÃ_ 4ô?N¯Êkø‹ßìxN3Ï÷Œ¦ä<\NU'ÿío?vûúÊwÉim(ÓÙ„¢c†\÷ö®MzØ–ž+Žv×.à+Õù‘LÃç‹>Øí]†ÍÓ 1‡ëŽg·o«Ä˘öÛ’¦É@o&pYò”ÚûUñ4³Î¨ûì#¨ÚÖXFg$Ò) «z¬8÷‘ïãT¨ÖTƒŽCŸ#“ˆB–oL1Ünp8QaÚõýÚ•€O/¯8û¼_mìÎsÛ{šû7€oà®·ˆàî·ƒ{ZPOtÊ’Áë°¯Ñ+>\Ë-¡ìŸ•rìÛXðö:lÈ¡…ög½exÑ.ºM¡³m—Ë_ü‘ïä5tµ¿õ"|±ùé5‘zì'^ Ê½hÿ@?^ ³Ö0n`ïXœË5óã¢_"Ûðÿî>A"ð%A˜ÿçËÿOr0jQ¿±ÅCÍÑP$ÿu%-³m„ŒO3® …R‰, ((µ!Õ¨¯lÖOÛ&ìXõ51cM´i„¨,”X §ø,«[P¯ëΡŸÏõ2KC!å’lu5s;Ýíp:Ëe2SùZ³üÂ×ë{Øê¥ðÃùþ…éûtky§!h¦ ¨ÞŒe·o¿ /';ìd/r—À¡Žh'„ÃTÔ“–Al€žÃh¤$}o·ŽÊˆQž?„ÐÜV'}óÊ1 FøÅæëÊÁÊ<å¤å%å”NÊá–{ ³ÈoÖàúù ’>é­K—:³æLô^qø×<1D§|¾Tiur'¤ù£gÏñÛ€!y\Oûè6¬^&˜}Ÿ§õ<Ê~h£êGR\ëNŸpØ”ùŽaD&krÎ]„™GûÐ2P›il¢yxÑÈî,m›ãFh“yÓœÕ@ ‡$4pÕg„kƒÎ×Çl€’iv›/þÙl¿í}„÷Q°pK0óP¾M-÷%iµVlËÓ$Žy‚bŠB¶ÍçfÒàÞ_%ãæ‡r™1VD©]ôÈ.í]侺¤;àŬ¥ÓAùÈTóL¦ecÆ««ºU[F©&¡§`Ü›….D6Tù× é! Ýts‹æ wbËißrY&ÑU¸â{dèÕ3É3,h8¯¾×Á(’kÝõ%V1õ¤oÛîiõÏ M}'[ ©bä%Éß«y•êêyÖ6“âU^cç BhPS<¥}Ë£¤rŒ:†È6‘m|’zÁVLÒ$t’y¥4›†;“,SȤуrÆírõ€þøv×ëù—Çwú—BMjY l↱'4°áêbÑ“…\ïY¬ý9’ªÕS |†ìQgÇ×¾ÏëOLË’}û1½ã°íWžÔº#nɸŠßã’FÅ >;y×ÖtnzËDYǸSëÕeJgèœv7µ GT@»H^Ón\Åc‚êqf³RÛá³ÙØ0J4do)6°×÷&\BH:sPœ¡6Â!GWVÓpáC,˜f›i}”Æa¿9'áñM‘Ä~ÅøzMø·„?uÀÔx1› “±/rGÖbÈÐCî÷Â[jXu…i x ë•~å ʈ¿˜Dѵ²ÒPXéä·’­¿ òÇÿŒDƒlT)Ô¢þÂ×?íC¡|eTû†ñ\A …n@|º©w›‚z¦Wõ¬PRbñøÖ)¥ {otîoMÙ+½_bi³J7‰#úOÊ“Cˆ°LÑe3a'Öæ R]3înP¼ƒ³¸>n^þr^„Â~<’J§cI¤N²wg‡)¶¡ã|̉j Ô+ƒna—¡Z§(|ª†CÈIzþ“lÒgÏ´ùé<‰kë6Ê+é5 _6=dœ 9ß•fÃAD?«ÝüÔºµ´ Äú²dÄø$1EÅ$|§«^q~ë¿×1¦ô»ÃGe˜¡ã³õwñ¯îiP±¯cDЦù{Zµfu´·’G³¶fK"®RB™¦QDÆ%Š]ÏNæ£5Æ^è²E­{ÎCZ‚Å/¥×ng%]W,GÜ5Tí:¥&“¹¾ƒGjZTøô†e¶&pCç@B«g˜]`†@x`C)3¢tÓ“)pñBU"3 îî7‡FÂKö3¸-¢¿ªøxn¤Ÿø0„Ðx©ãh•…Ü_‰yÊ•‚ÝZáø0Á}E:‹‡iŽêÚÓ&c øîh wi¡qê×8¾ ‡Ô0nÛâÄLÚÁ/A„‡!c(6®)o˜¨úo$£H«‡Uî^+<ý(\j9ßOˆTÍzýhFˆ…iH(LDs$ìÌmŸþ“Y–ZX‚³7x—ã+…ÚÆñ;ª¡é°G!> Å#$”)‹¶ÊÀ±®hõQ†dµª\sîµëßÙ£r®¯ÝºN9×××]¿²¯ã¹Î87×™˜é]«mÃ{ŒUŸ¼$ˆ’[_p€Qߞdz^*ö?„ŸQ¡ÙbÜbÜEÿªÒª9¯1Û5]k'§V(D/ÝHŠÎåªåä/ZÛ ø{‘”÷®ü'±ê‘o$bš¡ä(DzeÖ7¾ ‚£ùÛeMX™¨µçÕƒš„HÞÒ™á>õeòàç¹\Øpáژϭ,¥õjåR!jIâR‡7— ¥Ê8ìdeØ(ã“ǶŸ)нPRÆLO$ /F©G7®2}l´–Y‚~z$%zÆVz7ûøVÄø¯ñ)9Ñ\±úÀ‡Ôâ"Qš]~§3c{ .ÅHÖÙ£‡ÎTÞ4I¢OM>"˜ ϱ‘öià•×§P eYóÜÝ‹¾“Ç5 GHÈ(­S{ËŽ²¢~îïuü]Nµ`g¦2J·å&*ø«x}ùãM`é‡ä¨^5õGëȘ`ØmBK”Z;õÇm²Z‰h/ q>'Él¥¤hÖsÉ™ú°è?uúõ«:Âù†â©²œŽßš€´ÌÞbˆþEp²Í1†C¶·U‡(]½²ÙÈ+uýT¼D—y%Å`!éf õ°I@!( ’.þxЏë=t˜ÇÕ­]$é–eÖ*Éܹ\«áêÐ0%ôº(œ4’05q® í8ÓûÏr²E¶ÛºPrU*knñ!´`„]è·îÇ-4Wõåÿ;W3ùhÃô|Þ-¶§€è5Â~Ç “(c™›q&Hæ2Q5½©}ü"¶‹•Ÿô´*ÙÊ­É m3û7Ttâ6û|:’ÀíS7Á£ܾéïî »Xq«]»—ºvÂâ!²›hµ²sÿüä>ÑÌ79ÙÞžá½$Û[Qz†‘†[Zrý¦kËDQ.üN}2ÝDðZÎz“ðìêr«Ó~^ìòK"D¤È×£`1á7kã1b±œº–fG·÷ ÿßý‘&ÊÛ—ÿPŽ=ÖÿF&Œ©…µÉÿ ›u»#Öÿ3Ô‹ßúŽsr Ð&ü]ñÌ’¹=ÂK £QdËšDQLÑê îç.S”k¢P}ÿ™0e(ì‰W(·Þ«a~²¼Ä$è-³‰‹0Š»º½¹&i²KáC6je³qê) è»Ä©Äˆ*™d”«añ>•ñ sƒÅ,ˆ[·ÚCâ¢ÉÏÁÆ?GGô¬: ÆFÎòÍ$8‡Rí.]958˜ÞúŸâì»ë€ÓT°1tÊIEøI`SL½Õæh0ÀCV˜¼6°Ì7Ç·f¨7µkæAS+k'×–5«‚ô ¨dݬâ˜ùì­(ÛÐÃÑ= H)ißd2öóŸ!s9ç^;¸ÚN±ô¬RæÕL€)êd× TÇ%SóÙ*Ž<’A5ÚÓàj´Š$8gŒ~­zãë~B ÞÖ5~ÐØ‚>}®èRyŸìÆÒþ}nlhœõc¿-ÙQžèÒ‚1LŸí†þ7ö— óoÅ{:TëÁ–vÑ7A1?Ó#ˆÍ븇Œ‰#PF6ݰ"5?]å!ªÕKˆäe}ö•4†s)à¿„µH‡2%Ñ{æ«…Ù ØžÏðŠu"p’Ćˆó/"»Ök—K”¯Z=¼Aã`d2 tG>nx('… æš&%ö›¢¯N‰¥n/•RïIV±7¨Ú;[“è3n´G¼!Öp:(b°v©qrå Ts Ïö×5„ÑT[ªp˜É‰WUË2¥—ÍNc}z(^·?¿y‹/¦\îhÓ‚ñû=½‡»ñÚµtPÚsLh‘DÞM2+gÈE¼wþîÛwÒ ¡¢1·’§ÚQî,¯c`ɾèR dÇÝÕ¶R ÁÇ1ÜÙ?Ð196pF q.]#ìmø²¤‰ãï9Þú˜#a€cå •q7¶ ïM*{‚tžm9 Ð0‡Kû0MØÊÄãáj´Å9FOIKqÎJè½A‹NX™íu$¿«V>ÀÒH.óÔ¯½üíôÞ×ìyu  ¨ýQ&Â4¨CT°J Ú=~e·žæ€ ñcy}Í·ÇŽ×'»¹”Hó¶~ó?øùëP ³9Š\™7¡jµÑ»¸êÐP w]þNŽ B5zÚ#p¬Ã½¦'«`?s¯¥q˜<Ÿ~EK–hŠÎL£òã2~¸‘:JÕégûî}{ÊÀÆ¢ÁÔ&Ãô«‚E1d*»ñp­þkliˆ`QûzFP7\-QÕÊÊã;I~ç»­ôÅöxƒûì`Ò„T¡múâUG·8HÓƒ€§léz˜Ò”¼$5¹÷„Ô¹7å¸ýÀˆ^~˦ø<ÕÙUÓsgsáçŽÒdp0¶ÊýwºùDap3fû#'Öv{¼U›²y>Jç©Wi˜$Æ6þ"º¡Tn ÃWˆ”.ÎÔühì0Ü}&cÌBú … ]ž¤K?»*fœç’,ˆm()Zni3[¡*ÙÅ2»¯ë{?›¬ÏÒ&ù»O¾eã2P:õêuîì]…Æ´ë }à±ÎœfxW#›À̯~ጙLöµùðâÅt ‹’yõí ýïÇ[ÃRÈån/¯І·oÔ@<ú¨S//îaB-©²’ej¥€”GM>¬+š“]`öŸ6Í»mÉl-åoX0ìNÓ߈Ѧå43Ϙpà³›Ódâtß|Œ’¢¥÷’æÄ÷Ž`èneñêÿvo¬W|œ©ËR¨ Ï*I.WZ=Õ,Ôñ_z¢qw ü:j³ˆpÒ¬8Õå–x~´Ã+ «ç£ËèÓðà dö9»ºØqù Œ×3aÛ9T°ç‚ÖÒIiâb …"aÐÚEžÔz ³Æ˜ïPÊ…æÅV¾Ý΋’A*ã%0iØiäiÂfêû|c‘R LèØ —T2k÷Æ*¶¹NY5ŸJ£Â‰ï¨2^>LvhêŠ9ÑíËðçè›LÅd9ÚN˜O{:Ë´#è´yìuhµ‹;þ)j¹T»Q‰Õ}  ‰[5€zû–ð?¿š˜êäÆáÁªì“x8rÒ C«ÎèÛ65r–r§2l÷ÃÔ‚ÍÞ‡+kîƒqÈEuãºÙÇËtﮟN>Èïß•Œh)Bânê”tìç.ªGŒ‘V’‡ú°yÛã„b æAãÑÓ”q}s€ƒ°·\éð}GÂÝåmŸÙ·#øÃ¦ûÇQ~6 Ä q;ªé/|ÂZ… ËžûÌÏ༬ó`ð†Ñò¶ÀÀ×÷ZJŸÙ+^{ Íf‹üÔ9éüä#óÚÁ Ö¡-7@n ðJƒ¡)<ñJ"ÌV¡ˆ‹…à#^³R¹€oœŸSrÔ Þ†Ïh> *4« 36ÇWŠ)ÙœöΙ35Š/Öž{–‹QŒêʃ„HÒ€Ëz]†×}FVSbr›•ÎN¹)yŽBT–‹Ï£Ü&D'ì¸W¸ßž®G[°¿ã!Ÿýy–ájg’åC †Ó©èŒÌ6‚¼HwȰêέ;…f..é$ÿ\ ¼m·¢ÇÉI„9‹êþ+a_µ-[·"lÇ£zþõ53Årª§‡¤.vPJ§¨Ý<¤ÚdSä{&ÍgšÃ»ö ¥+ÕôÝ«¿Z´ZU ƒÐ6ƒì˜¶Wô…U<“Ù lJ0Öä€ùZ®ÁeUùkLÂ[)©ï#¢>ÙMÊé×ëq›æ·­K…ÖBYŠéŽo©Òõ¢1)gSEÆ×G2<Öåk¼ã¡bNá-ö+²¨5u6Ê3˜G„Jf÷xñ4fªÀ0j0¬Ïcfäš¹•âÌz,œL >© ¯¾) ³þ RooµÀO0ü²˜?{™ø¾”vÎÐÅ%ôidói¹¸X4h~E2šè9áŒ-£ K£üpc…¹ƒ€‘#‰‘œ¼¤–Ì`ù$«Œ,”¯re¡CÝù÷¦Æ2¹­ÅŽœvk˶£‘ôv­zY‡ý,–‹R}‘Ü;-Z•t¯À˜ÓŸºÆŽ¶r™tÍ}š~Ο¨Ö†k\»€[a½—8’uŸ9 áÞH6éCm^óu‰. Ö¯~ÄØX°IuˆAfdIâÙB˜<®E)]KüèQ@òqË¿D,ù£›|“2Xž2 ïYL¦Ñb…9œ°÷ ¶ŠÞqÌsÅ€Ö'g¯XRH†.¥¾­|'ø‘ž!tM˜Jö,(l“±\ü†…‹Î- +âñ»Kšnš÷Æò¶Á~a$5ËþXaœ-š[ª½8`Ìž®hVE¥2K4;øÏb ¬ ¡¶Qæºr-<Ò¶¥˜+“À*žcì‘ QYJ…Pô =04xF}úxoò±à!î¾›¢Ö ú†9­ß…Ÿ<×ã¥TÉ"÷'ÃZ×AS%ao^Dƒá˜úËN Á!¯.<ä Õ-•l‚Eâ j T} Kßyˆ±@§ˆTqLHuf3’÷L°¿¢?Ðo[K\$“ÐuoÂá1­í­±Á‘¶r÷~ú%ZPŒ9›«ëÅdU(˜1bHÈ©²ç['ÆÂ½‚ Z[߬ڰ/oWe56)jol‹ž‰ÓgTÛ‰J1£3Ôþ¾—Hæ:â’Y‹­Õë`­±â™¸±¢JgÌ0%;6ËQj…â1x̨ÏÄna^‰#ü8óMm1`IŽ˜å<ˆ >ñb_1ñ˜âˆ„H:À\ü™Íý³MAJ¿Â5AS äö¼…Ž фԮèà›#Á·÷Và1¯ôNðÖãöeà?–ÐÄwÐŬ¸ú3ì[{{‚mÎÈ6üO$Q¯í&íLNÊMô–‘XãJÈ×pßXÏŸÕp £¬ª‚šQ8Ìô?[$ȉ×ä/WIÏž‡>‹²¨“²øÏ¿¥ñž$ú¹Tm_E£DN5|` x=Oœs›„+‹~Ìä(ÿž…Ö‚÷8Þ}12ãÎ*!y^ñ ø®ë'À ×^¨ÉQ:¹OX:Ütk†C¬,I}ÿ­š >œŒXL#MÏÝTE(°ƒ¸âüÁí!׌©Å0“Á0Hž6Z}ÌÎ8S46±¦ðìê¶ŽÒ-Z#“våH8(¬@F\R4("g¨ù"ßËÿÑí2ANñÏRCÁ´L´ÁRÍQ8¡L¿fŠA´wÒ@ï±ØÔ¤®Ccù¨l6ƒ'¬™ÎÛ¡¥q•ý°5@o”VÎr’_¹œþ‹z„½œDú†±ûƒ"Oÿª@ãióŸçÝç™ÚŠ´³o°t‡ªõšÙÐ2ÔßóHIo æi,›‘¶Fë,"4R§Ô…å¼ñJX%‚+Hˆ\ìÇ ð¹• uw¾!rÇ ;„\ÁÔH'ÒP&ÆZ!°¹Å÷èÅko»\­^±Õ ûͯ;sl&†i`Û¦Õ§åÞ :{Ê÷p¤ï7”±q¥XE‰a3¬ù•úÑáõ q¢ŠyÆÐæFc.ivë.úÅóļ£F§ˆ£kþÐ"ªºãChT|gü†Ö;à'u0¸‘420ËåÊ…rƒ¢§4Ñù=~X:©}¹eÎZH.™qŠŽ ÜäHÁ¸y/ÕØ Lb¡9@Û:=bŠ 9„ 'E7.gEÔËԞƮ$Æ ý­Ñ nÀ¿ÅÁ=÷z¯\ ÂÖn>â;B‹ Œ,£§¯wRóÂÛÏVOÆMDµ|•N= ÄKOŒÅÎÌI/ºÅ’ ¯9 ?o6ói=“gŒNG6nc޳Œ~'#ßomšq¼´­gïÝHQ0êïÿxY˜:`v;ÊØw˜lLÅ(ãOÕ¸„o—ŠŽw±=*( 'åõÇÉB:zÇÙRºc)É©läs&/g_3ÏZõO0æ0` ’Pl[$ˆz± ðéà3e‹Ä÷LB°F8T3N„­=×>€’óH`ÆÃ9°ã9ÕÅ¥H²É;!¥„Qèù¡Jz¢C*#+ÄÍ ¥¡³KM…—a”D6:¢X8àMpW…7™= 14”c5jàãÔk“ÊrüÓZn8-[ÎSû6AÕñ±&ÍGؘ9Ü.ÀܸpM [nY~̈ê'V·Hî/b39QÁÈ“_¹-ï‹3Ô+¨-#¾rŽÓެŒ­?©Ï?&ÊOìâMí©™ƒí2,f {h}ùŒß`ž"#™qsìI~"u–anü¶ ÇâDkl¡RÄ•Žp×;d‚Þv‰;¬¦@B¢!Þ&dˆË¢û½|ø±˜g~J›¹Zc‡š$d.ÖŠ ß¿1ø®ûÅ—|K'º—mí[öš¼ p¾´rx¼¦^Pšž[Bcå½=mó%õ«|ê +÷‰c­¹ `,¬õDŸi§ðp¿35ŠM²½"øKFY@"e¤òßV6•.\ue哈§QÁãjÉðÍ1íYÍT¨H_aUóç&S^as„Ç*ØWŒ’ŸxQñåX³€ÉÏε #ÚìwW…FÉÔ];Oü{û–¾Í60Ðcº(½ˆS%>{õs…›´; mjNC^4g‹®µå^.(Qå^ÕêÙ[sL:rLR¬‹e†ÑfÓHÒ~Ÿ xû?[’¾Éà²p®@¥X>Í;@ýFð^7{ êºTk¹Õ½ò+Ýîvô’VÀ‹,;}ÜÉÿ9¾$[ÅQ¦|¥<–u?oþHž,•)Êuþ]æ¶iÏ"¸d7&ú®p)Ë[NÚ¸ðJŽS²VÜ—çßAÆÝóÅ®ß;×â>Bz§]‡k0/ê9ͦ³™1¥:¼³ååpñ¨egŒŸ=†dxû2òäut«dþ©ú²ú1ò½ÐýwÏ6zçƒI@÷?ƒþÿUxX›¸[Z›è¹š8þ×êÿ =§­vD¿µ2þ”Ùønñæâ© +ËOo%9¥/2WZ§#)zP‰vE`ÿTܵ½ž.q ®œYçIð,´®(üWºçÈïrF¥ ɤ#ºãc\ô\]¦ð:°ºq‰Ô=©Þ]sƬi2¸ ²'iÅ&ˆ½‡`È‘Ôb)²ˆ]zÇì%ã¡Ê Ä Ðcc49ñp!.–è_œÛ¦@ë Ä ²Ãþ@ªÆÃÄ×´dÚñ$[§ÿʈ ŒÇ^Fä õªLvÌÿ+I×é]³Ã98•kȬÈÅ×ݾ~ëÆÐ29†©ƒW\JÐåþöúøùûÑ*‹_x2™(ч¶=¸fw93ˆ²F©“8ƒÄ™súæeG¢¶C=°µ(0TMó1KÐK_es WÈ? Š‘»—Ä,Mè1íhªi ñþÝÝõe- ^íøèáëmìLͬ,íø‡£ƒýïáúòú§×—hì¬/±/'ç&ˆáš6­œbQ´Uxšó EݤDÒ Rïä•tT‡©qÖ6¥7‘©ã ^)ÀSj?ñ‘fmKÙM©AàÎ2‹ˆoï*†!èNïÈÒ18Ar‚9·lr@ÒT X°À£R~MÚÒѪ&¬›èÃg2„WKØ—¶±!_‹õÔ°{&UdÌvŸƒä_ˆÌ_r#Üð*„ÐmëÞ¬!;Næß˜qV[õ5QúV R:M¡ÈÊçj™{³°–/ðЩ咆äóÖ?æ8¤ý/Å{Œ¸ŒIìZÒ”}à‘W0zŽ•Ö)DÅ£ ÛĈç\%w4?ìxDÀ$À_Ê&¡‡¬÷®~G6òIG(<è3¯¡çX††<zܹÑ~äê ˜Ôö­þ@}ëÑj›~ Ø-KbXk};£ EÑâÃåb‹Þ‚%}ã¾±6,1Ó0B0^phà s…6æý`C²¨T}g–çk,9Ø7» Ä^`fgÕÞIcÍÞåf° «‚üØq„ˆ‡>éÐ<ÎöfáÀ( Ì‘ y:#r†Q[A\ueEŒ²@Z™_@ðf wl›aEö¬ ŒÂóÀG¾àK×\î»8ö <†!³ ØcU°;ÝÔ‘ÔhÚö=êÁ‘%Uˆ#÷Ä«OOÒüË¡NYà Ù`×à­MüϧúôýLB0÷}Ò–(6j×áãùdÇT(çÝ¡€¼ì ä;%®Ïçz'HÓ,ò®ÚŒ!}¿õ:&"`=I©%´ý%BfÔ— Ð?È.ÄÔ Àçæ ýÊ`6 J5„Õ®’®£ž§¡ |:µùA±<ݬ±ˆßr §BR(,T¤K!Í V„Ã…øëkù¹O –½œ(æ/ü€œJ×(½`ªõƼN˜Áv~¼žã#Gððô=°†ãrZ•CšOÚÓ>%¢pòúP‡{L´ƒ43B†¥H™Aa£¥„¹"÷ɇÙS0ŒY½u¸FR~sÆ a4åÝJí¹Þ‚ׄ!·ö¾Ißä^"öŒˆK=D¥sáÐR¬tˆ [T†§ô CšIØ 8¯ZG†°Èì›îVÏåæ:E"zŸdTÿÀúæ›õË„‡˜ ré´$¢@à“ÆôÕ3DŒ6þ:ØxÑ[4MEt²tš ¯ß«Å,u ð9¦Euë…omÝH4F½€üà ¬Då /çic+›êúfˆëXI`5MH„×€d‹8{-¨Êé]Ö7TM”r÷•N‰;µW/£„3*³tø§+iœn*ød{ód¶ªêÆö•êÎbüCó› 'CÑåùiî+*GMËŠZ¢7ÕÛÚ6j„m:[þ¦¢·îÝ–l¹ÐEhM+Û+´ÿXM½O\™É“xÃxBCÍšL2ÉW­äÊù“ªQceçQrÐ-›†™õ èáD°Ü“Kåâé¤M…³Èò&gdè£<á¹í‚üðË;ª¡Je÷ñxI*<ƒ™_õG¸ 9™4tñprãæÁ“«ÀÙS±hÐJ-4©÷a¹ªF — k]#^‚¤oº˜¾‡V¿ Ìqˬ–õÜJc»Yª~ÄíÊo¡D‚ZÙKÔ±(ã‚|C…Ö§ˆ¶sŒò®8Òü>³C àíοÍÖoU°±“Eb¨d9Q 'P<ß.ÙœÍÏ­ïéqÔï¶Oê ýÒ¿¢¹‰AM¶8¦V?MƒBxœj•  y[;ãc¹ù&|C¦©4êÍÜùFÚø"ÔÀ4½YïãB£H»ªbˆé]}vàF.ÉÍXòŽ/S=w//Åi‚:†jÕ^ZÉÒÅ7¿Â¶²q ÅbaÖ\GÔØmŒ~iÁ™>ûÓ²ãp5hgç¥0:D/Ÿ9ås˜VâøI±ªö%Ézƒ²^[ K£…3-¯I5FˆÇ¤”$F2Cp{iÈR$UnÏò,g2 ý$Ä\ù¡ÿË÷|otüÃú0½ÀLùÔb“ª37û=Ø6.î?>¥Sܰdê 5šNñ'$'ä…Ø†ƒxœXÝŸ¥* žë·ïW'Nüwz-PeþɳҴp©]]JMÕF¾~Å­¬_~BöúöãQQÚm6d]a©­#§Ü%’s~Ú8LÇmÒ;ºb×!±w|x{½ÏgŸuø.¸ÿ(ª&žjõ¿ÙMÙ±–®ñ–Ù‘-ͪʧfïîÌ…_ë˜_Ž8¦}Y w—ÝøœÓÒÓØ÷x¸ŠÛ;@c5™ÛrOîòÁ˜¶5­ÐEϪ=Ë<³zˆÆù]ïZaªÆjªä›ëÅÏr°Ðg³mŠÄ¶x½1”ãFÕ”„ͼ#ûxôi¥¯¯È¢yÙ:×ÄͱåÄÔ8Åmæ9:¹³A¿™6]ªáyËÎ ™øZ|Û($ÀöÐø¡*ÍÿèOÿCŠá‹ìaeüšUU™°­a+FܼhoØS•÷6EtDßKr`áÓß§¤GéÑ| ÎT½¥«lìH)ËëÒü"˜¦KÏÜ1K¹E‹$Õ«á‡*×0*U¨‚ð9 <øœXªšDðòØ"ݵ¹GßoÌ77–Wåq=¯@\â63Ä”ýôาԅû’“ S³³ØÔ†û…ýï^ð,èºÛ > Ãÿ$3Ñœ±#CõÛaÂë2»Ññ³1¼ k[ÛÐ÷2Š»™!ÃfÍré!Ó/Ì­G$+°{[ç¹½Às[³¾^¥ˆå‡ ¼…ˆ>ä‡ÛÞ@&§…@à:„÷ÓUyi6{"DØv³¬¬¼:òù½\¹¢ù±*þ,ãÓý‘^. ÞCÈòÁ„ÆÿÃDl†]§ïÏo…L€Ä&{'…€1OÉ·ð× ˆìíP(E|ùÌÆýÅæ{[&@ƒd†Nr×ɽR¨G·ðÙȰ³;x3ÙôE;åï:,mÞ¡N8äüðVÚ,ÂÞ‡ý&\ÇÙ Œ!¼¯šoìç|…⪡YÇ ]…x_Û_6®`a›´‡ûv’ævdŸœQØÁ9:M· â“}‰¼'og„ü%Q› òË¥Š øÃPÃÿs¨•àI±¸/ëpü†Öî€ø©ÕúCR£ òË bÚŸ³?zWk7 ²s:¡|Wë`„–õù•H\á“öÿ?({‡ a˜(H°mÛ¶mÛ¶mÛ¶mÛ¶mÛ¶íþzÿ˜‰˜™ËÎߥêTY™/³^UÅæ§ß¸%¹)C®ÃmçRX€7>€@mÊeµá›QQTSº®\Áb ]NM÷ôþ.¨ÐG%½3k“¸­Ab:ÿ±Åë4ñú¤Ù…½.ùrßµ±§¼)VWçÉÄ•Éç@pGûzÛµº¿½äôZØ»Öå“VU»5¿Íähéj.<*É“TöbF¼ü{=WK·£9Ì2$ƒ„óùþµÍahñMzAðî­i¯1+!o“â)DñÐ4|?¢“)Cwmë]í]Õ›ÚÖ»©æ[ E‚ºC2/Ä/éðn؆Å÷@„zOÉ(á Ši—ˆ• iW5pmm›ªæ·Ý/äŠë&¬¶4VU{j¤ ¶Ÿ}úØõN[¶ßÊð½'ì|±µ·`\¢¹Õ$—)åeY.‰Ýœ´PŒµûzÑE×3>¿®( ‹ê¿mCÈÝžžÆä•]YÖ"¶¤¿w ŽA›IS·ßñãgOo&A•–ÝT w}eË"«%†µ ZíD4­AäÒX Þ25®O°ÿ‰:/U0"ônr´@ª¸ü" œJ„d0”tß#³á¦r$ŸwÄ\©`*×Lø" ZA=F$€Å(ibѰÔEDSm°ø’ÆSu Ëÿ™À´¾~F-qÛe$xöe@ƒ³På£k¸ë2ÄZ7 ¬!wüOåXôEõW(«TJË"å^Ùÿûf¢e”š.ã2DéÀ• ˆ…nž$Y Ÿ_’©¥_òÜJpŸ ª  Äë^ôÃͺŽÉ9 øÆº¬Æû÷D&¨H u|··´ç<ß5¿§×Ö–Ö ßÕùê¥}A0-¡À™ŽÙœÖÞ…ÕÃõ çø‡BëÉÆ 6k8Ìô`­õá½Zód?ê1c0È9ºÇÃÜúqM”`2òïgKÛŽëqqeuqK[•ÜLnã®SÇJAR_9{“µ²I¸e…"kUSIûÕÀ¹+NÔ7Ϭ&œA99Hb`äQó2&¿iddKZ²_èÕà_<΢¢¾Qœ…]<­¹–öUñ eLòKõŸxÞ }ùËðØ›6hÞE8ž ‹üÈ$‚3"( l µ6`‹~¾q Ž%‹€Q¬3¨5*ÖþTã´BÇêuÍSS©Ð<æ%úûƒÝŠ4AX%xxi?cÑc¼½‡¨Ñˆæ@BØASñõ SÝDa6Ij`ê?ýX1ËËw÷Á©…\•yP‹]MX‡e€é4ü·"»­ìã a'[2J>'J4×¹ÓD)¥¶åÒ-Á „N7¯FjÃÒº<ÄòzèH#fãvx£C˜kñ­«‡½ŽÌrºk‘ƒÌ2T”G\Íæ[d¬”­yÒÖ¹<¶$Ñ~Yi9†Õ8¢Z9„Þì³>{yÌõYÔ šÔù`A›Bûüu‰g‘áÑ‚eËŒv)÷ËŒJ9m&ušO^ØA‡LXx¡6íâ"Íø0úí †ÔS_ã:šôáMásà<9”I ™dc]º© 6fŠðüIÛS¦)­eÂXl °‘gœLNÛ{ò‡Îú³ïºíœ0ͳóX…ã+²ÉO°|ò#K”-¸îZ° ¤d;¹ËcΊ¨æuqºšh´Ö•QD¨¾’#lÜã)€ô•Ã:h°¹[vÄ‹QÔϯä¢]X#zb³QÍvåÍ@è7ã…èü¨>±Èw0ì×ëdk¹S=InoJ˜Q7nDà ßÍÆ‹ cþÛg×ì¤WI×lWvö"œ¹ó®–Öð×MSK˜=þ˜v€¶XK…b!kOÑÎò4cŠªLfâ–¢›Éü¾=ÖË”DAB<0nÒ‰5Zú¥w‹“È2œH@{±hnòñ[„’Uùš—ã!ˆè,‹Æ×½Oá÷Kb\æŠÁvÀóC€›Œ„ªñiá¹UŒ¼N2xZO“£éXa<‚YÁ‹O¸›È$¢žU[Á{rÝLNÞ»Ña2ðÐQŽE ™Õªí¸fZ¸UÃÈžâªñÆüʲ>s³Ê±ohþÒúÁñ\Æ*³˜Å1ý¦ÎK"¹LÒ?ŸA5Üb×ÿ4ó]Ó£Eê}ë\Ëå•ZtÕÇAòûV¾yÞ“F©è8[<¡´”³&t)ÆpfZž‘Péó™¾iîò——†;ѦÏ8ü4AHIÁÐ壈’x¼B;’iŠB 1øÓÐϳo*½ç“?ÑÇûÕ÷»Ç+ànÊOEªWö2»í¸—Œ—ñQuÒs[ã²–‰J“åó:?qsB§çÒ¼ÕÃPœ/K¢ŽVæMû¤Á6Ç[†)¤½@M}&; ` ­Ø^ryÓßø.v@µSzN¥F”$Öiuô "†Îj‰ûØñ§!œÚWPRlí·è©¼U±š·UŸñÀ:y¢×¥†6>‘$íeŒÿ+u¼Ÿ†€fåVèn¿íä›AÂÝG4âSh¦â˳Ä'ýƒòÍ3x—ô)Å–ióâ\ú¼kéä-4ŠŠ½1t¦½ü¸†¹UUÆ÷Ì8ÞÙ_B,2õåLB…¥)Ÿª¨j>ÑæªŸ—š^Ú¡_ïáœù0¤</±U9¶,dÏx&g×sÇl.fÑÈ Ð9{Ù$z¢„#ö7i'/z„ÈgLÅØ&°õÈ/]ÖÔ9 ²¹f%µÜ,×%î‰@š±lºÝ¾ Òu8ŽZj Zjzr¸Æßº_CCY2}  ÊxÙ›Ÿäq¸s• ¼þÙæ¼šò‡`-à´qµ¡×ÐÖÐàÛ‹¬yo¦>Ò4EyÂ&q.^<þÔ$¹†p+ºãå’Ãa |Üsל.½_ÿF2 cÈ $KÈ/O)rA©ö@R/Ô»¹Ò%Yzàí{á¨:K•^:Jpܳà$[þòÒÚ6 é³Ò';ì&d¸õ g£*¯{#8ÂûÔã1P·u ÅQžØ¦Ï¶HèKhO™™%ƒM˜rUiàÕ¥£g=á¦@Ñ…b¯,£Kã¨ÿe¦Õ5h„ÅFê$†/ö†ËoZ÷b¦¹ðñÕ:µ{¦‰]H_ÆQ¤…Ú¥Ø2\+à-_õ¢Œ5_º¦¾`öZnõÂßW4’ò®­ó—Í mÀ?zœ=”W&mÒ ôZ—Í[ã ˜ÿd±Å0ŽÂÚØzÃ}ž­Y.Ù 0xÑwY„ËrK£ÙŠäÙa™»¡Œhmß…®|nqåK[vÏäÍ#eéy×·ékWÖ.XOïY«xÑÇ£Y}ÞÜ`–g|w7÷‹:œpÆøÎC8˜Ä30È’Ö:"@t ]0Vº̲ä7k¬ "3»B3ÆÙ;L†ÁÈ~ðŽçœš9¶;ðòv{AÐ@ÆÚ @ÈuäC{F!Vq~cÿ9`Gú€ä¡“¾õ´-VDS³¦Ÿ‹éÖ¡_®„*ŽŒAés„UØÏ–Â3:Jã9(*Û|kh“±´¡Jî%q¬`Oš…ðç± ›µôû¹„·0æø8ôÝñsè8ÄSÍ^:1ÝëÊ17™‰ï¢<} õöB²ßÓ¿·»¿sÆDZ0O==,f±ƒ»E&2.ÝœçÝÐÙ,ìôÊ·7’f¡:äÚõZå[»_a5„§ŸþÙe S‡[á#TÈw!ÇñŒ#B#öÝbÇÓ?, Ï£aä0 9‚~6#nB I½GghÄj·.)ƒ$ÆbaS±‰ÀÐ?O,s×ÊÃÖ²µöX:Í®ÝaÛdòLV/6øM˜4ôWÁ‘Òˆ=j¤Û;úœIÃÈk,Ôwn%aò ˜ïÞÑ̼¤Ï' äP†ª>ó¾Fkâ ³Z¢_n™9Zúÿ.ýk*ÍïO€àj©•í<ç”ý'6&ÿ ¨.\FÀfoù {ôokIBrÎX©I“{…G-ú‹K#o]Ñ\n]-®:-r[ÑqŸ±uZ….”œO#n=wœ/ÊdB–Ò†[ÔΖõ(Û,í~*M˜€N_”9·jh‹I_h;ÐÀ®¹MË5êhË)bŽQ²f²+N´AÍþ<ä•em_ecéèß+5mXxxÖú@׬ԵJ÷7,ÞU9MÜñ¾¯müT•'ÙÞ˜ùͧ#@¯žAÄ#¡(WÐùÑ#ˆŒËÛCÍÜ/Œw9æ X@ìæW/wï{è7ô‹Ò‚T/ÆË ^ñúôPA:¿ß/–W¾¡ƒ‡‡›%iõÌû£‹ÍÞœÂVijØi)áÞ¦—È*=fz3~ýCÑsÝ?U4ÉÏÔZƒö‚å°Qˆ¨ Žù{½=B]¥ä|zˆJÉ¢´h•ùð²ä…ü…‰"Ì7šE-4$m×`hŠí%Ì.Ð$Áo°gt ¢Jfv^«HãGêIåDQÀ H.³oŽØò‡n e3®]Êæj•>$ôCM_°IiJOEy7â¹ïº"éïºynÁ±IëÌ6Ÿ‹9ã¡»ä Ça6ÊÆ‰ ï˜!m»;!/«2ê3²ÑCqDæ•´c¦#”HƬ]6Báw‚I1Í£ì2éôuÚBχžû½ÃËn÷àdâãÃÀf¶_åÚì&…öË~:Ýù…¹zÒ³¡ù̸QÚ§‹Ò™†™c/ÓƒBG„2ú 7û-Be œ½Ç§TÔlúÔÀÏ$ Væ]U‰rvW´¤¼RMëZö1°‚…QapÇy:kúµ^êò—:üs-1óä[¢‚í°jš…íÀ‡æŽ¸€¼ÎY¥onnRzçìÕ‚-A»ÝqÛ•ç(îEÇ Ü*€3©¸SA»O뼘œ¥G ª]0°óvÚÐB<ÛVVi¾×;{ÈÞbÄ ,¨Ìù q)ö¸9úRÈ3>¿ ¢FeÞz{â›óC­ÚJ¹ÍoBÊ.y°â¹…Ó÷, ¯)ÆKîwÉÌÙÇÔ./¥`2¾e—¾áÜëÐ}?ëñÁ/é¤.Â@”Ë3…ß}…'Ñ rô³ý3‰Ï,šÃ&žOnž«<©†¸9ùy?0•Xš¥˜êV@Óž+^µ’*ÓƒšðŸËAýË® ËãVͶBÞ?_<Œƒý AUÄ3!Ӷˬ%÷Žì¡G‰EM0éâ:?ïÍM–ZmÏø¦ËsmàF¡ ÄÈi݇t!×>Øoòá3ϳ¼mhq¼xeÞbêÓµrOÕ¸a—ùŽx$eò*²SJLù0ÛÍ,¬¹ø2¿xrÐXŒñ{ù|­¹¬§iz?LÒTå£,á›õÄWûʃ—´jÆÜ"ÚRË*qš¼aïÉ{áÝgÑçé™!p¢åO]²^qçlÍ­­ µ½Ðd¾ý~ê5ßV»;'¥Çs©bú§­N SÉ5ÖÓCaÚ™KfüЋ‰V´ýÏÍ##-…îM QÕr˜4¦´›ÁN]ešV€áf­ã'xeäœw¨î…ÃÄåÊ‘í™(ºiùx|¶IBÖéõKýlÖ^«­,•¶Ä:é8hlë®Æ¤R.l²\ªùïÑQ U~¬ÃÇõ¨8ÔQëáæÉvï~.¾ô¹}‰TTÔ/ìœï À<7oRöý»iÞNYteOú8ŽýP>vPLo(9}ÉûÞËûÑÖÅ^ðß݈J¯·4êòtѳª•BõlNõ½ƒ®Vè~ÎüºÀñ|‡?78‚ùŸBxµƒ—â—ÜþÒåÿ•Rþ¿møûÖ 8€Zšÿ”Ò¸WkÖGñ>s×Òt3©”#99&¤^Ÿ„3ŽßAfbXCŽ/mT"Y˜)ÆÌË]|+Wsþå XǃÓñh$˜nçp$”ýЀêëøMÝIÈXRÛêê6¯òkõUÕUVåÏóɈþŸÓ?rBe~Ôt¾Oó‘óF8cÝhÞ–ŒÐ–Y.¶kµá¨êeÓ],o னŽá,´h«å18Ìï‹CŒh´\“Å L,óÅŽ< öbW4Ù‹Ed3— Ñd›ZG¨ŽÈ?X~Xh¯"(Y~ôÌ’« ûlw._D÷“–öe;v‰"nÍ43# °žú\o1l¾ýáSþ°0æ¤/‹â ˆÉj æ&ãÂú¸` Õ«JMEŠp.WRª&>¹\ämk„äþ0³¡Püøw¦¼g‘»ƒ3Ç¥+vá7´ÝW6wâ¼hH‘²g;jòlwAŒvìm»twv0i滶ϺìËêŨÙ캀jšz;}×`Š {'v¹‹1Htn˶S»È»2Í‘fQ,øñ€VFS€öÄuS‹7Ú¶ù xÜl.µ `“T.T•1?ØG®g+–#= .ˆ -à@½ÈòBÛËÜÑÈMfˆNŽÀ1µ ‡(¤l—X¥è("«‡þMH­N‡jº&ŽôÑJÎ{ÞŽùaJ<¿ËC\^–`°eÙ|,Ø¿Ⱥ[G:4nG%ýÝ ÞÇ]þ¿W¤{VºP~[ó±`´ïJl©dé OÒPÒ$hÞÞF8tgߎHÐû`z¸± š®¯ÜÆ¡iCî:Æ=Ñ:Ì# @íT4Üó!¡dàÄüÍ@ n»Ú2~î†Òøô ÀØ®-’Zo¥e6,SçsÁ‚xK®ó^.ɼÃ;.€05¬c’¤gëšÀ8ÉiÔJìR¡‰ßø"vdÛV(íÐÜs»w°ì6àšP8â>Çþƒc1ŠÃ%FtR±-¸pƒýÍØOx•è¬É ’ÑÉ`Ázñ䈲¡´·±Ö²veŽKŠYý¸._N~Pîh,áí4¶|á[!ŒølôJúAGÄõ˜§[Ìk)§æµë¥N´§J &ÊmP.9B݃ÞrØ”°^äR,³—‡¿¢J4Gº6L‹õ“ÝÏ*ù4>€¾ŸþgÞ7•ºê~Ñ}\C-µ=´Œ¿¸bŽhlœw¦³\ml`‚7*.åUÜ-›†ìø\[Ýg”-ñøó´Åҹݗs¼~Çv{¹ÚT(òÅé–/¼”±=’ÄîšDõŸt]Eõ{>§±×ÝNpžÚX½n îîœXB̳] yïž6sFÏC ç­¬'ÄY¦‚ò2¥ÿÌòÈö‡¢¹AzŒu)P- ?³éôðpÁèðWP÷z<Ä+ ÝO ä(„Õ uÒ†X#k߉ØþH«ö:‘n1;}úþÞ ´‚ è¤ÄR4¤òŽ´md­-ÕØªaT1) ÝT·HµŽGò2RúÞM©,3ï÷‡ªÕQ5`_Ì®®I.œöËgVÞ»-Wî ( ã–Þy³bdã2u!*å¸cKà±ÁfØ{Ò<=%jþúmðÆt{¼ù€c•]k¾>Tzõ ÷µzÉòE§H§¹»qMs¼#aOvòáÊw™Îý4V!ÝaãÁ˜ÎÇD|nÃv°\qˆôQÙ  d áÄït%t£ýDÉÛT3K§$Ý„1!¬$ÎHuv¶ûª¸×+ l“ˆØ ÊÀN>qœÍ´Ò°hö†«ã’-Wk%?`mA[ ÊJ™ø­TœpÑ3¦VjŠ$çžH tÜ_Wºk"âõ¥ÁNæ½}7°çÒ{†QŸ×Õû#>«Ã<1õÛlÒôí‡Ú0'{b‰µ8˜j£µ¤I΢””9®LM#/Øâ}üc1óù‹ç™&"ŠA{[æÃ{¡E7®,š_1cÒIá^"gñrUÓ[Á¶¶¤âŽ~ tþ§àÍ Ë äù¦U†‡+ó¦¿IqŒ¹;kile0–× Á7Ñl|/\ì²9¨°âô›>$À¹F•(kN‹É„ÄE„‡Ì«¥šæ'`®,ìV~Ê1Šãž#•©ÇÁÖüèÔ×ÀÖã`ëA‚ « 瀧²Eú! ŠÏ—+KpÁÉÆ€Ð1†g»Ç)á 뫳t ŽÇ7^ãá È=Ó¥ –é3§ùNe¼[ÚÔeþòpÛÜb` ŠjÎ<7õP…)Ѐž–O,•×0?|cù’\Ó±7 ‚ßÐŽyg«ÃüÑ7& ?T;4ºgrE!¸™äUAÍ@íÅ2O°qçnïZ@v*Œ6숲hD ]èŠSé#øóf&&Þ¡£y æE»¥½ißú¤‰á½z¶±sÈiw„¿MQùôÒõ—_À;.Tží›P½;QÅ ÓìDÅÆ‚CªŠ‹3â†K`&\€H†Oî€Lf‘Gº°h’;É$#‹ÄY?$ºÊÊ!q ‚*n•ÀË_%Jò:E‰É]‘*‹ä-°$*gñÈž´™GðÔNj™³|ÂhVqÿ…–^¥]ßr¸Ë€ŠÒèm4n( sßrØ£[.m:wóH€¬%/ÑØ ;R##¤7N•¸˜U€´7*ÎÄzm Açʲwuhìx’ߺ±_œP¢ }d ÏÐÜ¡¦¾u…žYÏéoæIÕ…ˆnÑÁ34ì;‡P•b[R™°ª¬ËñwpË·Ç~áˆ*$Aºæ‚”µÛ"Ã4v@1³ñÀíÚmgÙ‘ï†ÓŸxsŽRY…ƒsºwúd3õ(‘ÛNxÅNˆ/¸gÿ’Œ 3#¦£Šw9\[ž<¼n6S-FÄtï·¯|RÁg,ÿ£a߅椡+[kæË¿=!á‹ë¦T½Þš5A×~d“‡ ]ªèd_OŹ›P^³¼¢Ì™NÂV‡ž®éýÈÈ;„}ϜӱŠcµ…õ²“ »J ³ mâ¦=‰H¦ Ô† ïÛ?ZÌê¨øâÄ–c~P^î™—Ú#¦‚ .9ÓbM&¯Q°YÀ¶$¼C)Ì}†¡‹‘Ç‹Œ£Q€HcJºÚ£Sl>!¢nÈTž~Oíðäµ7„ƒÔ£àÏ`ÀÔÒÀNøbÃÛ(äVSÞh^Ã-§*xÊãÃáK ó PÎV¶ÅQQ´,|E?S  ;D;E’ž&hZ ¥¹çi°Ÿ†~¶~dz58†Æó]Í”(Ìê±ï ×þ¡otýÛ/Òì6.ËÔÒdiœ×·õ^lIXMà û0æ`ñr"¨þuqóàAô̳fœtf$•žõÊ8öDƒ¹îÚö4˜1Šë+‰p0,óG6ð½äÕÉpˆ¬`þ°6Öä~8 ¤Û Åæ>ËñSgŒ ¯+5Ô&¡rnÈÎúfš˜Í«vã-…3>Ë2”ÒžzÛ¨2”X׃Éd\ª³·ˆt¿_Ëúï­·Ûݾ–fÒu‚»ezä#g¨#ÁpƒÊ?Ü=‡ ãÑó*†Õ1Æ´3ê)Âq tJüvYàÆ× ‡(­¥|Œ­ %nZüƒ¥‹ŸAw­õtÙ4(39¡Üw*è?|ïJš®Ê¦<Ÿp¯‰ªVëùî€ËÇ5$³Ïv†É‰ôh'3©cUñ#»ípá¾Ph,› _îËeÔpÝy‰Ìägú]yËõ´²é cñ.¯ôJØ1W5l®…LkÍt 5òb ¬¬Òlâ!» …ÐDUþu ª¢W hÞc¯×׸Žº¯Jr­5C8†t![¦ÞÊËÉ{—­‰¤¸…öÚ ÷Í3}ˆ‡J± Ö/bOeÈ2÷M&ðlS ¨q9‰‰öK;(åQã¿|ê‚V'Ÿä¢Ô¢#uÊÑój´Û£’ö%—ôõA²´«Äþ¬œ©Ó(¯¥Ü˜‹cd=ŽÚš3“æ7þLò Pj‘½ Èbòmm…çeÐk5bªÜZày:ˆ\-±zܸB¢ö–·Ü=U储›÷<££WÄ#ó}Bþ¹jôÊs#;*=>‡'8©mtIoÁ8ÅšÀO–¥|EœÂ›$PMÃc„O—gQÁ)YšÉÏô[/e²•ݦ×"¿=‡Ͳbÿ£Šbá Q24UUeÎ3^Y×Wõ moê §ñÂÇ<˜Ü6öUâS€ƒ*B.v™<Í圃µÍ!B¡Oø›°'~1øÊ$BÁ¸ÐgX~f–Ü24½ÀÔL„æ'ö`ÂV‘xµ»HP0PC£kžB€V›@ú›‹Ké#ƒÀƒ§\×–—§[qß8;\‹0ñ[¸v5Âû†°`Ÿk(4‡uÞêœ ‡ñÈJöd#ev™FQ0c9‰\‹Ýõ7i¼ ؘÈ7H†ƒÔÂÄ{œËÒÄoÄõ®âSÍ­OìGµ¶‚჋ÉÍÈ ‘·u–÷}X KĦ›Â¦cûFÀPUày?±IiÛÿIÒŒ>ã5‚yòŽ•­‰S´ð]Ř›M $Fió¡=ÉÕ¼…@ÔXN5G7ß5'ÿþßÍ ¦Xx»ÿÊéÿ‹¹*N¦Îÿó³‰SkçmޤýWj…§Ý¨,Š–«1lSyãå8º¸“ä'µ ñTdÅý¤„Äêqé7³=®+\Dæ~\ײ2r_ÁøÎæ~„±¸ûÇêq£=›$6hIÃka•yxŸC“‘{äX@-¦êÝßæä&Òø‘çã$‘DMN:bsF‚28ñÿØp¯4cx8Ãä‰/ŽšEé¡P€?Ú¦†©ÔÂòeqÀËÇÇááá¹NG®ü:øÜ@ \|Xù˜Éœ9fÂw<袉šþdsÒº|HX¼hº@¨t¿~\' èß÷Â"qíw‰'šÁ±˜Œ¸q¸¦Iè¤Á­ç€º+Ä·Š91ç·OÎ}xîãäG»/gƒ|°fÍŠ'lf&]ÄæÊ’ŸxrÚ¡™²|ÈîÇ“Ÿñ ¨2Ânƒ.Á‰pMˆ›ÍÇ @j|P¦59Ñj¦µÉ]f'¶úÍ Á§ ¡ÛåB†bx°:æÖ";ÙDVE/s¼™Û"tøÇ°ëÀêó-˜`(œ&îù'ÐÎÉ_Al’åÉLVÀ–À̳]ÃVŒ¤­|aí} ¿há©å•WÜÑWs‹Ú® ´´÷ƒõ÷¢"îÉtý{ÃB<¼€x"qÙ—ŒÛ¢!Ã+ÏÕ¬Q¿¶Ë…¾%~î¬c^†ñXž’ŒÒûÆM–7ºËëŒó?ñîÑžÎz‘k’0ÚûF⼇€NŽð$…Ø ïvA:ÚÑ'{wv¦_ÍŸ‹³ƒé²ðÅþ8s_'.–2hf&/‚‰Ó—S9õcÐÂ(³HmªYÙçìÌ ½x1åéd‡=æ&tØj×:»:µ2˜…kñ“ÖÜX¿lâÈÉ`ÂpbƒÕ¬íØtiêbp.&«kZýf8ðsCΆ>LeoÙŸwrâéÉ‹A^Ûv‡î£/½‰ýBRgb¤Ñà0ˆ’KCsû­&&/è ôí.ŸûcóþÜÿ>å¿rÿ ŸÔ„ëÒZ¿Ÿ»ó»°þ†ßõ»vI ½mòdç9GÙkOÉ@&^0ÛÑx7´½Nõ®oÁ€°¾ê˘óÇÏÏÇJ–šÃ›!§D1fŒÙ€ Q Ë–A;ÿ~M}lÄ>KýÍ;?#•|Y¾t.…G æe±'/Ž/~X½<§*&ÏŽ~žÕ†M}ÎŽŽ~ØÄp1ÿ·r/ÐÅé¤<¯PH¢¡¢Œ‹Ø3°X$FƒÆìÍ®“;_o& júräâŒéèÃå·YÆå7”§9³±ÊxÚâf¦#,n’懡_Fp+Ѽða…ûòçÑ×ÕÒËë™W_€'_ú€èµ(Iý9Áêóe–k¡¢RÎ$øëÆèúŒ¯âî×LÊ8̓£*¥÷ÎlsÆìÀ†zw»àE0xÄÀÅÌàsC“6w„VXîéïï–tG³zÜç‡áìÑ1í+ÒžýQ­µWßl\½°ÿyƒÄxÚZ÷èÑVOž¬à©,_‘§@YB³·Íóžg+·Òo²eãò³j¼€°ú‚·ÿµ?ï?Û‡ÖÚ`þ½% î·éˆ÷± «G¹#ï6ÕXvÊ)$‡vÖ¼Vì|çgF,ùà<º±töfD)¤Å”^hžšW" A¶‰ýv1œç”ê¼86z1‡˜¨_F^Ì‚Ê{iP,Ϻdcú“ ©„.ÿp¾ ¿ìŒ¼1‹ÛVl½¡†ù×øï+´¯ÃS2ŸÇî¡÷PJö‘—p]/‡§ëSת•&](‡9Ïiß=LÃÍÊÏé_@—‹|g^Ý,ÅË&j‹…î"tP¤ï´»ìÙfà½cÀšimPÄþ60»&·ÞwË>±m¨¸Ï3#Lðx=-ß?QLF m°ÃÄ-›¶àŠE¨ 4T aÓ!w|üi|’ÏÏ@G‡X.=5ð`|3QDL8êÁU4ñ©Åݼ¡K澪ÁOÊ9ÂLç¬óY°¹6ÞäÀ ?ö\J7+Æu#«!RR¢Å¸M$a5¤!Mt«=ºXÕkÈEF½® æœ$&ˆCžH_üϾ´oë¾ôq`Y•Õ•¡F󽵇Ry Ô+Dà9ób¯€›éÖLi<–½‹%N³-<8ïPvø)€â\§SZ€"pC Й4Ù™á‰dM­x9x¿¸;ï×v¿7Y”ˆR”ϾH8ÑF¤L ûœ,r¿—‹ÖAEQ«ƒÚ'ž e#s1é>'Ig³|xF0= p=B yV–œ{fù[D *@¢Ü‰¬É3ÖF‡î{ô^’Ÿ\ÜNwMÇÝ‘fîh†xá!d df Øßé‚[÷¬¿¾1~g¦®aœw6ym&§@€é…¿!îß °Š·agÑ2ƒÌåYÏ®;ƒ·ÖWßv²l¨¥ïA—T-‡ª/]Â¥@~=8Ë…â×U‰õ „'$•†Ëø§€ Û«Óq&•CÏ¡1r^?ËÁa0HR„fu•Í(¶&¶ƒV@ùmQ7 ^jת¨Äá ¾>‚gš YŠh=Øk5CRŸØÐäÃõ…¦£”ïŒìéØÔ:¯q©ÈŠx!Ðí±× œÜ;>^} „›LVÆ+ƒÄq-Çò°ÈÆ@Ê8އÀD˜µy³n­ÄWjj²^̃ÿÐ,}¤8&àQÇщe[y V·^‰››o›%h Û8Ï|<\R²ñk±< Î]*ÐÙ DôQ@îŒ ?ò8C2_È1Söi˜N7ï‚k"Ê#œ'O¾â&\'Ôž¦§Ò»G—y Nñâ×Cx³UA“)¤œpƒ]Y (Þ&LôR4=ÊÒ¶ã[¨NQä„Ö FðZ‹£¿j‘“޽1të4Œl¨ƒ\p$¢Nç¸Xº)’ž¡Üã•úhåÖ©eëkntKµFÅÍß:«LQ˜¶¿»¡>˜L^¤ yÎlh‰*4&eI × +Z­ŠÎ–’SJ'uµ4n³ÇTøÎÚëÍœmš”F'UR¡a9Wœ^}ÀZÛµ8E4$¿¦jãèF _ØÎÀwÜš®üÏᢇӮÅkˆÈO/˜Ž(¥òˆÇ ˜ˆ™÷SíøV»õú|÷èøß– s³#Bç?i£àÄa°h)çmÅ[SprwR;›|¢XtUÀÚúÍ¡"ŸÄ%uÚÎzhšbί.?Eº{öväGñ†¿.ñ$sþ¹=½¶£k \£ºE¯ÔÛÅ¢Ztª(L’ÉQ$Á-ñáäãþ84§]†§Ín`©ðSÚî3AØHòà¼t{z+s²†ÆÎsjéÒ "ïW]ÿp§z«eê—<è¼,¾®`:!'©,óæoóRZ|«i6t· Ó)ˆ‘Ú˜ÖÆNƒD½&j/ ppðËÌ£ëßoµ–H;«¯·î£¬Ã%š¬¿MÚÒxšjµt ›zþ"ü36\ðÃ!2öª$•m„€2åÝ?Þ„é¼”N1­g¾Q]úPÕì`­ TùÚ´xlw!‡ê^ÁÚpRìB,é›ÓƒÞ;àû×'ô¾õš¾B «.Gð§Ëƒ2 ­‰1Ëÿªÿ®ß²” ‡Ù­TÛ„AjR8ˆØ9¢J½bc»‡Ú¦iS¨ 6øtä7Š±Ñ‘^bO¶¡þs…T–ƒ,û 9¢ÔbÞŠ­±ÁÛç)ïxÝæ ‹Rªƒ êžKºùåvÄtš›y§ô^qÚÉÁ²§ b‹lJFî&€ÅyçØS09IûùuRúáž¡™âÂL!Ïï†7/½¿Ÿ”"}ÞYPÜNÿ°ü¦Ã®S5‹ð×’]nOòx?Œ àloRU'Õ½ö~s‘r0¤RKqÒ”û&P²}ƒï™g±K:*zÜA/^g ¨v»µ·I›ÑGà3áX9†Zz:˜MµÑ+inÆÒCbÒø¤Üúq‘š:ì-\!Uɉq©ðx¤ky<Ø#ktµóüN0[ø„Ä]d§ÄšÔ&Ä-þ:˜®Þ Å5vüZ‡•ƒÇÌB[YÂÍŠ®÷d;ï$Ûji‰õÕNk§ÑÀZšãsK15VçHMKu—DÁrDæÎôj¡O(lÒ8“Ì}! æ‚Ä*0çK›ÉS*[^lj®ý-‚Ä´ ‰b^k-õ~s|ÆàÆ’ú 8žÿa6 ããÙi\‚Ç¿p‹àôðÔ䊈Ý"13¢¼B|)Zœ‹ß¾ØÎé¹°Îx-ÀSÑ’KÀÜpA·ƒÛéëu¦+±ŠÐ(R¥žÊaJ×±¾Ò©¸gÊ9u0û''½•¢_$ÃU»+…¾ã)/\ ggwzïÁUÒ¶õ½Ëd’&kÂdEIÝ3ÒØØà0»÷$ÿ”œ.Ï~3NÔèÚ ó\×’onRLñ!8ë†7íôømÄAÃVpOä_]S€d~Ñlýô©âª½áꖺ뛴ZZÙu‚ùe©Ì˜Ä§làgéRñt…yÐuŸ—†¡Æ`œž´C ϼdy-séåïP—iä]·šFf²ñe†ø“ˆ|­¡àå‘Åå–«Ú>¦è¦?Ã;JíþX{‘iíIv¾Ã1ÊTzh»¦Vx•WUµvÃÑ(DNãÄoëȯ눰u„ü üû*\¿¼®Bîáéðæ[icE=Ktj±HåÝAý±Ú× L#½soììà¢2ðÕR‰ ¶/ëUC{ÛˆáçAÕ“2é©’\³Ø¹¿M)ÄÃ?¡X¥•RÑμ¹ü¼ÖsÁ?ÒÊåd,ÖOu7zÔÿ‰F•^°ÝŸG‹ý€°!Q¹cÐhÍ\Ç-û(8¸ß°å…rxá?I2È2&´1ü5ïÕ”B¯‘?¥oºT«º'Þ[L¤×*¼c†"²($é"ÝÅ£±ÍË<š¡“ÊÍç7e®.Ý™€ÏU€¡úW³ì³Ro7·:¸|)UØü¦V»O •Z’Õ£[Pʽ–þâ²7Éò•Ôô¸"¤¬ðXcÛˆX+Õ¹¹Ïj•¾´}µŸ2–Ï#·|i ‹V¬â<ŽÑ>s¡X^pRÉXØAüªï¤+à"ª?I´§°ž ÚxHÊC}ŸÅ‘vä‘Jþ©â½=ëóLœð±v%)i½Yð»Ë}²¤«XT:ŸQ™Z66û_üÿ==ccÌï €Íÿ1ÐðéY¯jëÛàŒ{æï~ ãy’J•Hü>k5Ùh¡Iþ6šj§ÙäO»²¹–¼Çù´õèíÑMÇ{—ïòFÆÄIPY&–ÁØfÅ*¶€bXD·Il³P<)´XÁûÊàC¿”ÒÛ"Èß¾›¹ßÇ¥,¡Ð‡œÀš››÷e~þóߟýW\îÿw3nŠøó²pnûå¡i¸˜OT%”RÔE$%¥¡.* {ͨúÚA1:ì%”‚dÈK)5Ù°GºÖ=ê%•¢d( ßþ¥d¿‘t–Æ×¸G/õ»Su°ÁC">rf¨ÈEÃ.j©@ᎼJlô\A‹š-\ÄBÂ.j©Ñ}ÅØÈI$El䬒"6zæ¨ÈE‡ò æ)¥ÑÒ³Ù(U@Öé—8-­£‡V2F­ãŸùÝCT£W§c›}Í#\œWÿtl­Ó7wÊJ·P-t=$½kø¹”=D„ÓGZØø¯­A<ÔWçé“iˆk„soWÖQ­ •è·Áƒf¤á>ÝP–­ð&ÃC¦Á© “•¹À=”5ʯ”Ó÷|ÊÖ0÷@JOƒ¼“‰¾FYGaͲ®‡¼Æ¼£ZúXJPcÞ©”"Be ¬¢¦yÇ#Ü\å#uIÍ_އŒŠ4þ².çLý|Ęèv··¾æ|ÔØ(èý½µà¶Ô÷Öxçß`r™Gbù‡Ã~Â3Ï ^ý3éÞ„¸Nÿ€«°ÆqOe|§c¶¬¡¬Ó1´1º$õŸ(ÿ'c’ÜayEÇé÷kÍõšÎïe›ºu‡©wÛÙF“k„–óp³Vc™Uœ½Îö);ȉo¬@J5êö㯗MÇj×¥Wþxð98/¼bãCŸ#y5ºÓ³ç±ÛY®gn¸¹YógQâüÎÜñ w~&3Ô¨ £õû`„RµºBŒÐ Œ‚co%FèŽ Ÿ²ÎJ³½—zÅm1c͸÷õÖÉ øÙ‡9Óqiôq½üN6ǽªš}Çš7»ßýO´1ò¹¾OÝ.ögãR~ñÝZ[ÛïÞÁ"t^#9~—‡‹£ùÓb)n½øš¾VólUÿY¾ÐZE“’¦mG™~a™rÌí7ìöó«Öj½9•*·þ³¹ìøÓWÅxiu½¬N/“ãåÓ5rzÚ¿4õ ø…'Ù Êë³-»RŸÀ‡ìÁ³ÊP՚ڨ̓ժœ@ š¯·ªÕHµÒ§.­+d76´¿c‰HW862M ” 5^ò¡1V”¶±mí\bëÞZ—ˆ·ÿ=j(c Y=|OY°®kHÿb «^£bm*ûàÊP.F›ñªu ïÙ£öshQ#C;Gö46ô³×"x6†iÂö0&]JÝŠäØX5] êt¬Ôj–NU¿b-Glícíz–º~^‹º:ùì<‹N¶ö¬jRǤ“­só™ ÛÔj¶Kd¶Bv…}³ƒzN“µ½;ÁŠ´1X«8¦ÂÒTW Ó¾AsäçD˃Ÿ%Òe’›hU0Aû €h_(îäBøPnšU FoéÈ&Ÿû[ÒÎ<gd³ºÍ>#Îô¡f;`×¶_·†ÛêÎÛà3uª)þq °!ÎATÊ¥Yïá6˜üרD¤½8ŠläŒäÈ×|Àkæ%{W(7‰…/w”†ãJ_â J˜óMíÕ=¶òóòfÐÊÎd€áDõeì‚BæÛà#rñ¦ç£:ƒhR{¶š’Ò\cÛ žhUÃì¥7yƒª>l›”\÷vð”ø ï–QHlGÂwr”L—5EUK—0"^“{y´èÓMjýÑBRN­cp膎.Sœ ÁðÕ¶+‡R?"e ùEOžÄyëbF&–JOéèrîŠ~QW5¡›ÊøÆ“¦*£¿)wœ+ÃQVõC¨F„ú•Ye+:§AÝË‚™ Õ&IšãSV§ìŽF-o‹n˜Ê 6›°Ða¼ËñòG-Š;÷úÆqqC–(óôHi¸Ú §ÑfÕ~ÐqFQ )ÂÒ˜òJ˜Öꨉ¸Ÿ:”xW8p·Í}@he›ü öÍ¿h½X)2'0Q8xó"Šœ¡Ý2YXµVF Ô(™§o–XŒZ`ƒ@*éw~RHB?Þ ‹OC/Í=£ '÷ƒ,õÍõÈåµ›'± “Ó¨S«¤–>-Œ Kx™v#:õÛH81`{G`e[¥,^Ù²_h™™8*e :>úR½Ú˜/n³VB`Ó_1‚ àÆß¿0ô+åŽpC¶wom·4×Ûà/ÞšNìóì²vG;FK‚8)؃«x€?Žæé$e9J_)!\Å£Ÿ58Ê]«žQ–D¥,¢‡ØÈ ‘ÅDöJ®¤ÀTÃ÷MóXá“Ô33uÈËwM Nx‰Øìµ­2¶ÎVÔ¥˜”¡µµ5‘íðËä]&Ákk-SѸj­ÓôÖÔ0å0Ð… "ð-=OS¢jåÎðð·m"6ê}vgš±¯ˆ&"ŽñúÌÀIwìSæiì%i Ö§Ó€!Œ‡pѱ¡¶üÏý¨…Qyi~ð*2¦_}dCãP—„6cu®`îBL‚)Åëp*”_9Ÿ{ #µ-7æ9âÔ·ÇHßcÛúlÈÇ7¹¤XØÕ,ÉâP¤ ˜`¸]b-Ñæj’Ý^[nµ€KÒŠdXÙqiÚ i·V³š¯c+@Ÿ =cLªÜD$ü/ÌaêW[°·K×éä <,Ž‘š ‹^ŽOOÓ0d_ƒ ¨š‡:=A-òõ ֻɋ‡4 +KÐ…Edf\¿lQî ;‚‰ÐÞß7‰ëš¿ ;$ý¸‹’18H„ýtoö¢ˆÑy“3‹·nÞèªYŸÈ4Èט—3+Þ´(ÎìÑN± ÌMR\KMÓ|‡™{…ÅGdÐR,{‹Y’(N— ÑØ,O›1ÉÁIÎÙ3I#é=H¡®gë¯ë¡ÎÞô ?z3<?ôTëpÜ”OìKŒˆ¥;øªEK¶è€×'óþSt€ÌéZ¦v[‚9¨æ  k\g&nRåKƒ‘C½A:ð;ÄÔJ¤ÒbÌ»åÖl3«·(sä±{ELÍÌsP±„П±8ýWqÓÀ*Ãg 3ë&Á"iGŽRîZ©!9TËR ‘ñM83+¼±m'ôÃ˱W¶®Æ¯§ 3‘Ä+’ãý"‰2×¼zâРõl÷"ÃÑæÍ„zðHÈâww>ñe]ht ’dOw§“-’yØ:žîž§«iøl¹Þf‹õ(1aœPyïYb%@-îl™€â½¢gäÔ”®þfÇø]á¬)3ÔÔG©¹Í3‰u»Åf;¸/mŠb…k̈°zúXí¶'¶µŸ¶…mê[ìhÏ^A™UIÑ–·!ÄÊÖ•Õ†bG›,ä2^ä °ëi!¹¢G‚ãŠu¼søëœˆ• ÷’éfÉWùªC^Ä¡fC©Æ6¾FÚ[›)Ìñ曕‡›cHV’öv¢pYìÊ>¬ ÿÊ)<]‡½½šu™«¡@‡j€Ñ¼doŒ¦±T—°”C$Ÿ¯©ÄwŠ€r‚O)G’´tb¾aŠÕJ0³ˆ/Ÿî5Fil/`pP!Ô‡}­NÈ/ç6s>Áµ†¾¤C‹œSâÆ¸0úúæQ5ïã¢QQë÷+¤ÒR\î[C‰9ácT"®¬E½ƒˆ?¡Û¨1 ü1'HäΨTb@ÛÆ—ÿP†—³…d(í8l þ5d"°cåùÝE¨·vA'¶ÜË¿šÎ\›dtYI™å0#²;…Ð5Ù­]ÞC;p‰DV:qHÐ3#6ª–šêE’ñ»‘¬_Tè?õ¬À¡¿ä#<”Â< u¯¬ÐÀ]RÎqmüö×äB­$ª ù‡Øè—"¯é&ã»GjÙž¿þCø&µLƒ³ /”JK3|&è?Ñ«æ.^¸ìn¬Ë->¤ 4Ë0ƒµÎÞQ­¯ú›xÊ:3¥¨X±ÈE¶&j óñÏòME£p”o~CR^€<ð0ÌzúO]Õ&ˆû»|ƒñ»ï*îl—“êÇ\äÙ"šBP¾¨h¬î]ž‹›?z@èÖ\ øþ$£´ÁÅ_6ÒøCŽy?’’[¸eñèns ´Ä@¾e±ÐAø¼C®;3¨m Ïœ¢OÀ½£FAÀ(ÿbòûÙ™à«[:ŸÔáòŽ `hêd“|Ðö.úDÔK¸PÐ Ô-6v5è—Q»Êöd+îs F%ñ÷ä4E&4¥Ø=Í‹Ö&¹T@hï¥<,Dzs«Á¦V!c™'ê¶éSàMDéÊWɦšõk¹¡µ7àÅñDÇ5ƒ טW£Þ#™kÎûÎÓs£bQ/ñ®ôÖ žzµjÌ<+S`¶YÒP{5–¦°gð´üOÊ‚õ…¾ðC¥Ÿ,HyÐÊ2øÎ”/e±Ì*4K2_`ý­Ôzhlœ\gïqNÜׂ±mu1ó^+\ýb®ì¿Êc!ë¾/ù 0탟ÕI”i=!Òçã{1°ÜqEŽ!Çoá £ô…§ )õ˜yƒ›Z×­‰"°?Î@˜œmÒ\È`ÈÛ7„ܬ籤1yó§ç±6·;‰8)mèð³êˆWÇ ÿpsdÍÏ™î±;{ÏŸ—AK&†ž%_«l×]K>âÒ‘2DŒC‹ˆ_i~“ÕE.š,íMY’]^ }öÊÌ¡Ÿ£¿TÖ2rçÁË}vâ Å.<0˜hç5‰<àØ™—ANzGm /‚E<£°œ–YxôgNžNÈmÉØ»+Ì0ªœ©úé"ºnî/êC2à*(î5bŒÇ‰ÜÏÍÈÓý4A.©¶²xnLèl¸ÌÒÌñìD}–8³î œ# ìSÕ ¥Ñéß 7¹bRØñnq+:‘ê*tZeãTàhbî IöÊ3J¸kKŒˆ[œBt ? nž^(/9”`^äl­ÿ^ªá(.ézH×%n‰Æ!ê [Åc·3’n>dcMIØ䃥¨â²•€Ãø+ܪ+`Ac9ñÎÊõZaÒÑóÿ-, !– ‰Sãqh¡çœìr{*!ãšç(æpìÇ’BálgÆ4¬!z_ô‡éþõAÔcµšüã¶«y]1¹Kˆ(ý âUÇò] Xehp¦½9óB2¦D8†÷;­¬–ëz+¬Y„l —2ɼYÆS‘,îä(¦¤˜¶Ýú9ô—4Ù•WÔïWçQ­Œó~ a¿ëý6r®Ѿó‡kB> O Ó˜ŠÔÕðárÒØäGÖíÏ#NÕð¶_ –¾ R… Nß +'Ô–ñаõ ÇaÓx»M¾ZiëO;åÌOŸç„.»·Ø„‚IOqy …†\ц¸Ã­00û|&ëD^XÂ:eºÝf#7ìƒ=š€÷bTâØú,¶ÕwíŽ,¿Ýt‚HB=ÜM˜Ûþ¨òØÊ/ûK*ØÕd®ü]—6Øk[ë²>¿yI^—Ï«%¡æ×g±“—?7{Ò´|û>×ç«ãÕËJ5¼»9%5Ú¦§®Ýyæ‹ÒM(DûUÙs|]»Íƒ¹F ±ZrÚ/ê`â­ÒBÖLPÌæ¹Q×ëôÈtç§pÓQËz•'÷I6šª7ÜWàÉñ~,-ZÉWockùt†Æt1åO…[ñÆùC*¦3¨¯CPœ6»ÃfÆÔÉBóÂÛ3R(Û@œñ@ï–‡âmŒ=ü»†Nj'²ƒîÿD:ßXCå–‚ßŰþíw8[[a>ÁØ•u0­ÉòÇÛw:ÈÞHÓf“óOIðâÙG¾¤ú9Ua½û½ €à|!ˆ$œ(íîO`D’ŸˆùÉ—oLþ˜ÛE½d<ʵ“Z~xŠ^ã2&˜³¹Ð1-ûh_ŠFŧM¸²Þ'[¯“è~Ó±ãh’©Zn¶uNéCŒ$LR¬ïr5‡àŠ‘6wV‚ÈgúàTV`úŒŠìõ––Ì Àö³'[dŠñè ¾eˆ®ås$ó>±sÅ}“ÆÞ¡JkÏmu .¥ú k¸Ó¡Þ_*Ál@+šG¨ŒáSâ`ƒVp>ÀKäV9ŽÚ¸£t`<Ƈx Õ"õ,Ê_@нÖo2À9Ù Ù…Šß2ð<Ó ÃÕåïi! ñÿû‹]O{d†, ì×¾aÁÛKàbªaæ’Akõ?xk-Ë„¼&Ó ¼Ç‰Ërxƒn¼2ƒ0£÷ ^ž¾Ø ê)Ÿ ¦ÜÜIš9ÔbQB»ídÂO[˜OY”6ôñ‡5é BûH‚°ñu¬¨œÌ‰³h/ÂbtçÌÎ ØvæúýYyóç§iŸ¡ùcn·'lDMÝüuOå»·!7¶Ø9ñÍ„MÆîÏ|erŒ”ì)¬èÏ\¥ŠÌlgM<ìƨÎíÍÏQ¢ì\¾æäì©CmŸÐ£ž¼ÌÙ¦ë1§ðœ˜µ ãgû§¸r‰Uc+ZfPï³ÍË6?Áßä´Ç•þðß|QÕ‹ßYø8"›i?y9À)ƒÂË’S¾$RNÙ®íjçöÇ äj‡›1sÙe¤ÏžÎ¨ãk¦»Â‡E±-vŒo8¹˜(Îs¨ö—ýÁïæ<9bL/ž}&q5‰ŒÉÅ\lLÌýùk‰a«ñaPm=rÖIšEéD«£–ô%I·¤Ê^>¬!õQ¿ ñŠ£‰juñÌ 9òÁå5„QübÕh_ù…tP7Ïþ¡GõŠ>T[»‰ÁœuÓô=‘½Þìij[~«‚JâÂW’¡~ª8î¡ùæsaÏlŽ'¥‹*:mìM¼’ „à1ÍÀº—&Q^epåq¥J•}*öUä®4¼2´S &2þËÜŠµôU¯÷õ®-eË’òåÿ°ÿäyµÀŽ14çÿ…‹ãlajcó?lœ[mm÷m6$¿÷ú*ø$¥Ì¤ÔfË㺷vǹ/›5IcöÍ<ñ2 !ÙD*@1Åîß{næÊó¥iåÕhR(Œ_>˜ï~"JhMTîïJ*No²§+^níGÜ]î®q<ôm_J5ëѲääúžÃøh'í”F$Q9àüÈó¤òš)e=¦Ö³lrk»~ÌFõž†ÙbH샀¼Ï*ö,Š›è(Û#ÞièQªÉÀJ¦¹ï{TdNé¨Y6dÇ<‡6d+Ùi "Ä„¹ 4ª°EƘý¡òRþ¸Q-&ŸÚÔ­¼¸ñî]º¨ÈÍ õcø”csôøyúú¹ø@ì'ÛÄðE+3jж›$Œ¿EgÑp‡|õÒyä ÙÉÅÈçÂ]!~L•OÕjÔ”h2ýÜ[h¢g §¥ I0¢P7Ž .è'é×cR©ÀYí‰×Àû%íÎEKÛR¯àåãš•úLþ=þÚB™íôûw»›ÑôMéÕ¿Ïst–²Fv{A¶Q.AáÄàÙ×>õ…`Úq5¬§S…¶~]¸eùÈæªE€j¼«æ¨þôÐD;1`hÙ ø!1Ì5+'Peìx½†AÌ0µÀì¡XN=Wð»,Ó<3Ë']€R ç}ÎÓëåëu¹œ@X‰Æ ôp—8k [hYtS…K¨ø(ÈSvJE¥x·‰¦y Ø›ÉTÔ˜$‘Ì ¶Y- »÷2@sDlLÙ#TDrdîÉì¹aŒ(+ðÉë_Èo‰…u".aaÂμä LUÍ̶TÜàq——D~²MA^Â$SÒHÕl•aÉkŒ?Å…W^l>wN„f̓'™O'ÌRT™zDÍØ¼#˜_¡(cŸMm^O¾ JÀ– ¬†8¾†¿ Ôj}øS÷²ùX¿"2ÁÚ|à± [;.xÃÔ…XVþ=Ÿ¤÷yÂDMGÿ|ë¡%ˆ ¾“’tK5åËû]º‚IÕ³Ç$Û±6ÀÄg1ÁÅÖò ?(ƒA]øÚÀÙfC“µàãáºZþ™<­Ïà«[Ã1æ`DR“2µÕ<…ŒVÅËg û¤ük¢r¬ûÀòŠ•vÑ ò/L’Ê‹m½ò­ŠäÀ˜0îñWͦ*ÛÏXÆíÏÛ{2/äææõë£Ó=†Ý ŠãÕqÕ{;c2®º›<{ÙÞ+ѯ†CqÊÛ^»äßø €[Î{v¤ŠÄÌÛ†v£T°m VØœQK飶3Ø£V2õk@ Þ‹0¡ÂfßpˆTj ›2nŸm§ïðÆŠ;ÈóLç`.&­K˜3VU® ùMãÜ<ëxýõõ+›5gd}ó·RÞè•Hã<š¯@ðÅÊ®f¿ÒôqýCän¯hÎŽÔFV-à…©ãw„ﬞlôj¯´³Sçû£×@û&Üa!>T[Ìx¸É_ 7~"@“z¥µÂÈV€Ëú¿bÈYW§%©Wža,rðŒ nÜsa÷.âN½1kö€‚ ‹P-r ®¸½)ZcÀìbå–F@•0C]L1®Ì8)›I[paen¶ApSº§vðYÞåËÙC'_hqüú¹?¦Lž õ²ßŠÆ—šÒfaìGÏärE°ƒÙ˜”Zz×h±~ÇÀKÏ‚eï Ü•00±<ó£àùnbõaëÍ=m¬¤™w³B‡Ø ×Û[7Šó6'F5…U˜ß 8<ã…-Ù©4ˆØ8á˜?œÍXL7#ÀçcÿϽ»É.ˆ¾_ /4€ætU;Þ¡®™õ•W笥Ï‚_D¹°ZýBD÷)Ծ͂^Ø,%gTŠM¤G$">§I±0¼†FÝî“È8Åh^«z B‘;@>b+xV7¬–®9^xU`^ƒœõ^c$0­qv,75«ã^ ×÷¤,…Nh8kM˜V¥ƒrðËKo*WøwÛÎÛ÷’–‚Wþ`)3Ž \³YsãµC¶æo3ZkÌÝ̘SbÓ|äyÖÄiyðš_kÂ× ¸äÖaà¾<ž9ùMi¸H …?KÅH\೫hxz n¾6UàY÷3äÀª1"ø`ÂmKˆOŠv\æ؉ëwüNáµù\;WëéUpÙ}‡Ÿƒ¼J¤…•…¥oë~æÛפ²eÅÃk‡\N«ðï_g>Ï]óßá]üäo`ß¶È:éËö°K*ÙWM*"þ›¹ ûÄó,Ǧ2oÙñÑ6eFuk® ÕÞ®*rãyKÛ÷×=4{_!6™«‰¨¦'mê 2Ä!ŽG/û-õ+\J,¾È2‘" ‰;N·]wÀ¨¦š%É0M"@ž~ònž[³²‡òÀþ—ßPë=¾‘­ 2!%'|5° ÇTJ’e§uWFÉ›+&o*ç¶ûNÙIeÅ+˜ªïÌ©tè¶L]#X‘bg ĦO8x™ÛÂÒQØ%¼\›Ra@ÑÕ9“Pll¡2µW»é+¤ø†íê)ƒ¿J ?¶ð¿Ù*è°ëÇçYµ[Ù)z°WDјI[í'R д¥o.–hÁþ.ÒK¢¸8oþÓ”O®£vœ§³¾ ³ÄoyìÕ þ‘»Âu+¦Wï"ZÊs;*ÕQL° ràtqlËã Z‡KW—Åè󬣙xC]ë̓Ý£W~’‡“u£ö¥I/†—{ÚÞvÓ²p ÑéÂ=¬kE…‡ ¯Y§9å­óqSÌeUL©í>÷ŒbaªŸoAM ÇX‚¢ ö¬k´% CÚvÞ×g‹Köî†÷ص»W’{çÃïØ,+–—®‰P¿)ÄAÜZ³îÍ~'íÃw[/óp/fr«¬GÜ+¨¡¼@÷;ðî×À?û÷À\¼!OÈÅ+‘]ö»KâZ‰ñNy¢ãEDÒ®#nÖ‰5KGN]Šepï‰òHúµxM×Ç”Ì*”Se¸¼'ü†w€ßá‹h.(giðyëJu­íŸiE)Ujý®†Þš´U©b„,‰À ÕÀ1­“nFl×cþ@mÁ³òêP!ZØMý—ሕîʉ«Î–J1ã~WNcã„¶Aœºã¶k÷©áõð‚AÞe#›A'z¹a½ rn.Õ`ðÚ4]f8Ë«ËE1Ó½}‰2ÞXX1Û(=q7IV3çLéÉkC~·evÞ÷¬]m:<¡òÁo‘MÊ=ûžäTƒ wÁçs“†™ 2Î÷H¡Éðx²Aä QÞ€}Zß·ŒÜ—J± ¬@B ðå=ˆö÷ ÚàTôÓp¼‰Á]¯†Ž)ŠÑï£1¹8 cæ¸A#`ÝùeÓ£á-ÚÒ褘zv…ÍD“œºÁéåÇŽZ$—AŒº!ùqÌîØÞ^zÕ²nòà$•ÆC­ãL6`Ÿã—°`Üü êÌØnhÀH{Ž4cõÙËi¿ +¶:F®˜!»ƒ°ð}=ˆn —MLþ`f±e"„KkP‡o„¥œDÒ‰Çtí®/šL˜àŸ¯zÆ÷ó¡üj ~ê)—;Ý*ÔGìµÃí úÔ à.“Ò}³£Þeõ@BJN*Ç0EÁwȈG1‡ì¡jlwYtG±D9¥‘Ê»QéXõªr;;+ëívv œ‡‘йÙىܜ%õP˜“½!7~Îȃâ=¥±ýÞæ?¾$í‹c–‡˜T†¨#~A™Y’?˜ãÑçêè±rråÉÉà7ª‰àÄo¸ö›. ûä6XƒüZágéôÛV„Ä2|ÔÅÚP_Dãcôcñx|^/'‹éñx±™Ä)N?Wƒ<€YÉ•s Tz³ò}YòeöÈõõž¼æãˆØ µ3}àÌ;ö>wx~ì áßwøì•*‡Ð;#/ý}û÷°¯°ÈUï­Ü…Ù‡ôʸèijޱëºñfÙ'gæbná§¶Ž&»ï%´ŠJ€n3¨­…3q~hÎ,ì´·Ù­­¯y¨“´A€ßÅé§ÌL FQó¿sù înÉ7]CWÞ,•]œ´Ymh`¿!w$æ˜÷rKD¿({Û>)>Ê=ÇÀ¹ç–„-FÎÏs]0´ÿ¤ßk^3ÃÄc¸ëW…»³Ke{ôøúùømï‘MNøä×L£ó d“ö´Æ*Ä÷Ñ){²;N‹Áðþ}ÉžHª•î[Ìrù™ÛÑ™\z‡«,GäêyØqî…Ø€´½;Î éË« ccº‹RÈ‚ò¬hÖ‡ëtœÖ¼=„¥k3*|‰kn¨ž5°—Û#ŽgÔ¿Êÿ˃¥(ù¥h@¡®HŒºÆ_Ý„%¸Â9ùûßó¦ïb‚Çöÿ•êìTÿ:Æ·Ú³Î8ã®÷y¿h H(”K)—ˆµÐVá}-FQ1Ü -"aL­73Fà34åæ`UJ@Ó"t’¥GѹQÕJ“´á:Îú S„œ§GÍÿÕ¤©U:ÉïÞ»¼ÜÐOâ|˜$a¼¼»õÝúýñûÞ»4¿ó+#ýÿž§¥q ý¬^¾á~«âP†Êû•^B=ÒK=l7áq´é"W›4j#ÀQëEú©ÖÆG‚ ƒT%o8„~+¦(k û‰‚ ITWn Çáã" ÂÖl½ð‡Ïï˜)ƒ(cÎâ£ËY>Xôdeeyh#ÁÇCøiã¡ÿ ÐÍBø-Ã3§Â9 Š>Aïh—®$¿yL/‚^Z<¸èðz<°ÿ2<¤¤ùhŽenc4 Ï×E"V›1.¥‡òåõÑÿAÒå ÿƒÖù³ [“Gjþ+ÎårÔ)1õ2¡’]’¤rñ4¯Å5¿$öŒ»hêÇ7ˆþ'Æpß^¢QuR#‘ùÈ­äC8Ø¢¥ «@I˜æOvÒF܆cƒ.û·AyË´®¢Ð¬‹¨šøÃŠU¯æ¹]²QUKõfi«C¢€r{„YÅ:S ¯µ€üMó SÑ£1ž²ÊÃÛwíºæ˜ÔµK¢ç®ÚÆ#q$WÒ÷­É¿°<Û‚.~—z–m‰Š«œ¯V_w&º.­ÖôJ µKQ–îÇï‡ôË.³KnØOn÷Ã!‘´ð\19SGrŸQpv*„ë”'·pŒ\ŸÚžB¶~Ø¢ôð9ì!`8îÎדŠ7˜7r}­‰âÖÃrܱ//­¢©T§QL[8$›¥wà_1ùˆ0Ó2ÖÁP6Ö¬å=$êÃZÜ>夙R9`¿\ôáT’»ï12×¥?>õQh·lÌ ýýï§ ¿ªe¾ØæøhLÛ¾eßÖOC3óo>)ÀºUk•G IüYëmµç ô—/jvÐ :éÃutj®$Çmn ““²Ú)+Où©ÆFÖh͆cCßE€^ ² V~Ò … á³É‚vÍóHCwó¥;Òo&DÓÐPDP(@Á@*ºz'S8g" bÆ"·@ýL£×ÆUô×îtž¬~É‘hÕ.\1£mÁ9µIÌß@ÔOÚ-³¼eªªÆ:u]šK*̱N¦¡h§¤±gÂÝÜßÃÒ{muÆéãâè\©{,²[ª—EùžÞÞ:Î`Å.§Æ»R!mÇ:—§¥4þqiR£u»>Ð%S]/Úõ"îPæ¤ü–í^£»Vµw×n°«u ví2ñÒ÷´†ÑvkRõÔÄô¯ gÔÄS|"mQOÆJQyý‚ñpI>º} 7Û$Ûm·½ƒúð˜À‰Û¬\è4iQèw@Ú{'ü=¹•e’¨´½·ê%-‡£…60ôʳÊÖlìzXF¤3¸ÓOµûŽ´+Ó#ã5ÙAï»Er¿©nÔܾ‘8·°:@Õ5;9LÚKÇ}LRÀ1Fm‡Á©{ï˜Cz§ãšî6Ó_y¾òÙî\>§ Yß©[sERØj”x vb+"o> 6ǃtpÀqeÿ$wA•î&˜Ì±üÚÍËš¡ÄþYN©xÖ MõŽ' /*…ve{3^ýÍõÌ—§¡} lPôL‹»ª†6LŒ –¹“‚«’íVÄ44 •˜·j´¬Am¢vóü! ×fV±˜-¼˜×ã8-%‘8’JUúh5ŽFpm ¬B%@xÂ»È ¨A$©ù@ Òá–aBK’äpQÖn¢‰kt!¼¿ˆqò!×–Ñé =ï@±‘uLÿ7g¿üÖ[åb—TÚ—ÞK>eÙØÈVˆ¤^²“—ŽÌ[ôJ/Dõ5áÒ±‘Š;®Tx€µ´òÛoT©–ùë#qµÂ;’$Rš¬ 5KT bôjMlarI€Zhy‹JÅÌêñ®ßǧý°ŒF´ñh\%(VŽ! ­²¬ÑcXµÂ}yÃÚb$£FWh·Ô3 4DIÖá~{… *¯ @„Õ-Ê×*FwÁY‰%,ïÞ Š@” lŠÕAÞx4å‹ ¶,7ãµ1/[Ì\e 0ký”nÜr~UG©ç‹ˆÝ¨¸ý+½ À6Lzg"Ž÷Ñõ]Ùi^;εÅKÈfUSCß,ýýÕ\l×T·¼É/jz¾Y4~>÷%LJ=ú öÜÌ"#ÀŒ#¥Ö‡t ‘àÁ©¨ÎŽyŦp«Ý¶•…Aa¨­˜¢åØÇúá¾@¾ûº93Äû1@P}OæYÓ¸öªØnæÇPù¤×}ÑHY›Ñ‚ÇVK0{D»Ýxh  …®yÖYCZ†ê|üPƒ7]bXœ°Z=•<'Ég43Qy®„Yö^•J~ÚxŽÑSçÈW)¯©zÜC«-vÚ{Aô sjR¼,©æóùC˜)…úçp7œ|´-D³„ЉÁ„‰Ïy¤x]Qƒ¥5H¿-ÄuB¼ö³cVB¡¼)+ÍËú¤)=úŠ=þ‰'!šemd8¯úú',Û®»Ö±ôV™¿ÞB|~þTÖì‘ïÛ”—oUëî3zó3ÜÛ« ½™þÞÆ+ùÌÇ+¨^\s ¥èþ=qËÉøÝ¨Ä²ÉŒjÎ×XÒ)Q¤ÂÀ·§¢˜¸“/]í˜aé#Ö·-oœ0¨Í¸F­xúrL dÔ¤¥§ØœâB*ñ:Îàªó¦^ðu©Ó`úüD\N%<üe†Ào´¥}#’5䢙‹žv«á€`‘’ ïpG{Ýfž‘[ÌÓ&Q‚©zÂÍ­žOtm [O)Ô?Õ}e^! ßzdŒâÐ厅¢–µj‘+zQ¸0În£:ïÖßÄ›v¼I(’. 챤)EéQ íFÂÚ®lç5,rë‰üb§;d‰¢0­ý3LøÑŒömL÷ßß^´SÕD-ceêⱌ´A´@ÆŒ4$𧌛€)à¿ µä û\W7^†üìb{cµÉùYË(†z×®†æù?SÑk¥ b䇷 Љ áD^¢¶pKóÅNN 8˜xqB†(`ó×$+æCbås¦›•Ï9ÙüK0²´¤ËYNBÊÏQ ûpåƒ%“ *6aEVÌ(1ŠôeTÕLÓ­².3ßlxû­9Kuɯ€¬ßŒátÕl.† rس“s¹PÆ^Oº7æùÁh±×¾rÙwM’áj<>"kå3ƒÃü”§è&õö ª¡Õÿ¤%¢äHÆþ…âzÛHä È‚“’Äc`ç–vÈŽAg‰gbÂA¯ü3Ý>nØ¢ÅÊah è¾ÜÃÅ"ÙNèM?Y”†LÄúʳåÏ3¾/ù¤ÊÃ,”méf8†+WEŠR}À¶ÝZЊ.(´ ˆÌ½³£Ž®!l{G,ž’eB q ê4Š:0Æ¥ü”J‹» !NÀØ1å v“D ôU9Å[1/N Ã6”·HÃpÔ¯>•u ×Ó# Ù§H½Ð§X(Î&z@²W§”ã°ù32{Ë­Uèˆ×îµÖB×;8²l³ž\ŒHÊ´‹ ÙP~Ÿiq=m¨‡ßµC÷yÜ7€Æ0+"1Ç ø°×¼«¢Iü¤^̇ôÄt/ãxÖM=7¯Îd8 Ãåtmœö‚a£v›všÂãäOòó/õ8‹0'}¬¡ÉSi®ºˆìû‰èqõˉ™Tƒ53QG™™À˜™ zÖôUb”úwõ6â¯oé·,ê][µÑDeûÍ$Šël³Ï¨)}>Ï'¢´'IÊ„S²ŽØr«’p·›÷*y[¾­Ì’>f‡œlò˜ÔQ,ä³Á²/ ÙÍi_s“§8Ú¦É|8ƒE[bÒåˆr… ¬Ò K&‚5™MKmÆ|„.|±^T9‚Þ$e\b}D@l³ò 3³H8S¥IÄE³Ò792zWBÏ,þümºl!M®›ÉªSgŽtóÆK“4S y€GWtz€sÿ‚¥•¯“{w—šMò <’­• 1 m•œ®-¸ŒÓ #÷ÌG×),¯r1•(b#$¡ÖBu@©•astõÆÖ®®0yËk)-]ä0@qU"%#n¢=/^;¦.,t>ü:Øe¹¢šê¢€OÊ ²U›D0=CHز Ò–ñaØô9Ï'ÿ÷W9¬·<&Ê6¶t…(% ÛÛŠfc¥ÎüÒÌu²ÀrÓWKr<0>õ3bÁ?õ‡Üȧ64óôx†Cä; ñ×n%’{€['8œÄnŠëàˆëöMûÒ‹;¾©m™-Ÿª¨z÷ÐɆdæÍS—/;ÊùÒtÇfë¹+Ké‘p~à 剟pã_ JdŽ$òqÜdiòmQH›¾‚c<†úƒ­~K`¢3{Üù g›’q œ#¸Vq¨ZãÚô‡ƒ!ÍáÎ: ˆ)|."+“˜ólðyxèÄâo¶x™ÉÑÄ¢Ô‡{THܰã2p‰ÑeO½>†÷ÚÃÆ#‘>§Œ¿ä}#[!"~O@øsëC3\Ís–TøÎ¥`ÁþàNü_?:̹Ç3Ü;"{ÞÄÕ‹ÑÇs´Ò…­ã·8%§?‹²pïUðœÐwû!¼^,‡íéúvcQ·}þìó‰阹œàÅÍt⮦ª`¦¦Î½¦éʶÚ0¢g6…?ÔL¥€çþpcæ¢8Ë4FïFŒá¥[ù/Crñ€ÀB `.žÔƦ“·¬Ñ$ÍôYåjŽhR|±{€“«ûçûPÛà38:ÈGÆ,oº`h» Š— ¿giA°«ð"±Š¦˜ÉäëÖ‚3€ æü›Àˆ‰¨Çûƒ…çC„hžr« !™myzbSl…ë~ŠŸRv‘.ĉù—Ù9 X|þD¬%©`¡5ˆgVI¹Ü;fÒú@•E³mÆrO“FIßûÃéj5˜®#–‹ýÁ÷úµ@|7NP·1óŸTP¹ƒUÔ$¤r…Lr &ÙΖ"§²E¿¼Ž÷&òQ,3.zìË›T¨åÎ% …Z©40Ï÷<0þyê Žh.{¾@ëÀõ0a÷À3“ÞÚC@ y#¹µ”ÈkÏÓŠfº«;&[Ëdâ<.óëä,]òÑyè®ó—Fb29§~à:ümøäÒRJ˰߀ÁqÝ/HäzMžÙVZì!’ÓÃ"}aòq¡ú¹~5A¼S;YÐk ÅŠ«¢ªZ˜Û ­ p­xsèØ­p QðÛB±ãn´CT}Åò›/˜{;Ãë¼G +˜Y^ÁŒ*«”“VêØ‚œ9‰°œ¤ñ\ÎpSã•.S™Iã¸:~9nrËýà…Ô°uv1AÔ`Óóê÷±E•N{)Àú¯±¢Þ§ѹzƒCgOÂc?—a8ìˆhWO¡¢¯LV]HlÄKéXÒ1):0çëÜ5„kà&þ¦ÎËø¦(ǧ!3Ælà 9Cl?Ç;èœ=Äùo$rNNìƒ.ôoÂMzû£èEO-±ì}Óíë@½,Ž@§®­ùéb`·ÒÒ3}èm- ô’¬üZV'ž¶ÿP”1:+%¾ÓÒ²^®;y”zýU€³sÉç®kŒ¯öËSçf;íûo·Ø¯*É;1@îÂñiö:÷{|~§J§Õwåú¼[÷n<ߥ‚ûÜw¨e¿Š èeÁ×ÏÏÁ7›¬„^Õ da²›˜ðÅ[>ùfô½·Z­Óë{=o±ìý3³þN¬º…Yèu-ß–^ÿ±+Û—ú~Ö¾•R¿¨ë’^'^CKeÜPÜž §ÓCôuQ9ºªâ3^wËæOˆ“˜:Ãæ~b$ÚÐä÷Ìþ3p[ÃùzJ,웞1üªÀ?#½+„#ý­6È듵 Št2VÌ>¨RìZ¯q4q<žC« îK‹ïÛèf yshZÆÈ}OkiˆÏII†óv±@ÐCÿÛ_Áú<¯‡ªä­é݆·•£sÅ|ÒDê.ˆ¿C—#äÕÔÝ6¡µ¥)Ýúíh.û…ÑWã÷S(^ã½fm=¿cq2ðå÷0È?]“Š›Äô¼  F<‡±7ƒêÄ~k 8ï ×ß ¾1àõ³©»Ðãh 8cê§ã%{îÿ |¡lŽ€ÔáÜ✔v–Õ!u:1{šàÅIÄÛ7\}‡;§OšÃ÷@ D.„8àÂ$Ü¥¸µßricT†Æßéuõõ· éØŠáê%‰š·”a+gócDŸ¨°VuÁÉH«M*š• ¹`d¤‡ªD¨Ê™@;£4‡Íƒf§Þ‹èpÔ"½Q±”½1&O…¬žÀZéTd¶¸­”oV•æ'aóxDlÜt¦6åx¼:ZAkR[ÍëF’ÇïhkÓSÝèëÎ:s:Ö±,R¢ßd;ÈUŒŸ÷²ZH Ó¦xõèŸ0ŽCiH~Lu¿/øŒ´µ33š¢Ö¥H43ÃÒ,k‹wi_J üõ –LVh °-aUh™ù.xÝÀ¨œ¦‡°(08¦€rX׆³…ÊLÉ2©–¶9Ú6ÀAej#¸ÖY½º~QÞdkY¹Ä6õ¾1 …`ƒ`‰‚ªCí®†‘“KÊEÜI«W‡TÐ0çŸè¤yM²‚ýÑ}QlÝUЊÊSðCÍ๹Ÿ‡ëQš|¢M}Ï @…ªÁõå,ÞÁ”ØGxóæéÛqv¢ŽdDüÈ^éx®6ŸbæãhAêb¨Dêúî/XlÔIm?¬¾ˆp%¤ý…$Å:½Û±Ë}£´B±( ‡éžD­ÏŠ ,ÉŒøosYMv>/žÌ\ )«Á°¼àwX'£|³—GdÍX^€Õ¸Ò=üA¼„Y+Q±Š1çÒR ¼½ÝNnf5\~Ž @oiM ˜ê>×¶žj3%ûèYo'œ¾ú1¶·¾äÂÚtÜi›àÖ3ì-\&H_d³5¶GBjŒ„S²gÎp¬ŠŸä¶ÜW†V³Ü*Ê&÷B“͸@Äʤ~î=÷"@œkæ?¬ƒÀH­]\`ɰPVUiNÊÉÆ58­|¥‹ô=O|çh,òûþòxc7üðÈ»d¦¸„Éæaão“Óq1jâ$éZçD‰¢`¦rê"(ͽú4¹‚Ç`¶rMIéØ,™Ã‘HH,…›©¶Í¸…ÛdÿŠ)®žx›éÍ0þáïÄm©j“qMf‰¾Îâfx:ÄP†óláåâA­b$lM¨ ŒØKk†œþ.~ÈÜ}ã%ÌyânRŒZ“tòq¾í ,&Z(sµ¨>[p"›QQÉcE¤ѵ0WT­ÚÅ·³4óïøù»ãÙ!›ŠXgr úGLäÉâ N!HWŒÏXËoa[Æ}x(¹œÌï®±iQJ¨Þj€Ì3³O;¸Kt÷í¾„±¹†¦ÏAµóª¦uÑ`<ïA–60|ctl ¹Â•"¼]³éxlÑÙAsbž“ã¡ÿ ÂùA|ØY…žgX êM&¨5÷<ï~ŒÅ0ž9qµ7Á›qãÄ£W–pO¸ iÓ«Ø£=WPFŒÝ>ð dÚ—ÇR{Ë—¬õà¾gøíf«5z?ø¼±5ŽUR R½2¥†à{168{8ϲ~6ïà¹i˜l…1P{Ж¯ÕÒ¸X{;Ô¯›qý¸ü:n¿qõ¼¶=}%lhL\ܹ֗– íÔ{âûèãâ Cø²¥‚TOYñ¯˜!ðÈIqUH!¼2¬Ø:0÷éÕDh™p—×Q4ÀÅ¢OmÅ…/·€j (%Q °n–§ ôø*9–K•õ.î;‹_WËt¨Ùùÿ¡äŸ‚… ¶v]pÚ¶mÛ¶mÛ¶mÛ¶mÛžó›¶m³×Žèè}ÖŽ>Ýÿ‰ŠºÈˆºÉ¬‘c¼ã©7 <ãá$KQ‰Fh¾£÷ÚæâU¤A‚ªnµ€Üc©«½°3B’„‡2ï ä¸!Jnš]å(s ÉI2`kz$ÜŽ#™¹ÕÑIÔÙϨÎéÐEOJEKz 'ŸD/ó-Þ‡)¸žÈ缞®û¦°]«ê½·36¤ƒ•VðFë85èŽpÔ® /“¡½¶T•x¡öêÛQ$êXªPóÚkÞØîÎ «ƒŒ:ÿˆ°]Žg¹‚Á&ï®åQ⽡ˆ•¼£loö~–$)á·¸{Ë+ã›%XÙVÉYÚ4Ÿjʵ¡ƒ"ÈÎÇzØü‡— Foߦ> •K¡ýnõʃ“JïÍç¶\ËìxÂ÷–Ò5½ðl©so|參KåA¹Gf‚¼¸©(63ÓJ6™‚½Ä¨ùÌ:¡.WäÔ·‚ ÙÇL'›ûrNjãLn½vf%hŒ$k»u!å\pÃH‚¦E TíÌ£Çîò,H{U#‘«šrl r­¸ÊˆQÓŒÄУSØM4[»Ê‡èïÄCÿ¸·_ÜÚ7#C ïÆÏÀuéYõ^²>¼œ|||’þ럧Ïa›œ?Žñú¦f/£çã:«^áXm«¬1•cž¨í]ïà¿°lk¹5Ä™¼æÀ‘_bÈòäá+Q+̶lÚ6j¸ëü¨«‘PtòÈÎèÑJ>º­nÅÓc蓉|ë /ö‰ ÿ¥›É¥@Û¡Îc-Õnlpd½IÙ™wmí`Õ½D\N¸ë ÜC°08&^°«¢¡¹Áîöc§Ur)µ…½M³5b÷–›5½ÜÀ ç^^\[÷÷;âÒy¹| tQXòÁ™d÷€|8?n>Ï?¯ÀWGõÑ=½³«¿V`27eàLÀpfIQàÚ4Ü//˜´‹§\Fõ£ŒæË&°M—åH¢&9ZªÞk&1ÃQÇÑGiXñBΣLk…cì·ºáݳ¨¯¢NÌÂO}wg• ödgu7Í80Ë•ø­æ¨fI…ôªÄ¿b—­¤W½£²ìfpi26ü°Sû wwì$þ]Fgå·”æ,uçL¤.ɤˆù´´Åº÷þ®îqZ„ÓÝH"¶ä;,KØ•°R! P‡³*(q‰,(+Eáõ¾p7¾+‰‚SŸp*s€/ëRŒñ!‹ÉBAbR|ÝÕû÷GeS©á7Bªübœ öÖú͞ɠÏ"/L…Øÿ(Kº©Ú¹Æc„ñ§4éÚ2¹3Óo¾,¾1ÂÐŒÆ~ ÁÝ|ÈØc–×ÊÛzGEuv$ ½3…ÙÇüDã*LàÀë=µŽg”Á*h/"¾åhT14UϽ:‹$–ì‰ –/&¬ªHž¨…¾oÀiÿ™åB–›A±D´hMŠ†ÇˆpÛiò ³Hó30å½™¶¼èÿ·ùDû*ºà—õpLæÿÒÅ÷jO;c‹{Ýáñ@(ˆH¢PJ5q‘&7‘F=ZYjÓK3YjK¦9…5•8H†g20ϱÐ,!×_\Se«$I9MúÂWnZÃI¦ÂWê(q§H·)q^û'ËUξçšLI¶ÅD4/./Çëûÿ|áíLýŒ¯ŸóÿuÊ[ ý5ï¿Á}“ùÓ‡Þï‹JG¼°Ðv±xJ×h“å#_q&ÏG¿ôL¢x GõZ<ùŽK5Ó°‰~VPr’¬ÂCÎ#é ­¡Ò<8JŸ2œ<¥ðõ©b”¡â¡îB â!ëE2‡ª—̈́܋Q: y„S0Sö@™–…°‹e&qËdZàÎPÄ*Ÿ¶ÜÇ)ÆÜÇ)ž¢à–ÏhÈ*˜‰ySCÃcåæ"ïJRå î²˜ê¡îRNå ïZNé ìbNõõ¨S0“÷4§d¢ã±X++£‡ºÇAé£ïÒ˜î"ìV§t’ô<¥tÒô@§h h¬žêÂøþ©Y7 ]²úÁ¯½Ãt Ù ÿG´A–¬·48àëeMò³}MÄ"ïݶj·¡ÿë²åWqãÛt+:$°QH-Hí®òû¸ùÀ9VADâ¯Í«#Ø‚ˆÞ¼D8æ^ßÄêåŠ/V"•(x¨zô†úBI!¡N°q Ns7†Fj]€²»sº»)½\j¥ö+vý©ÁþR¹d3Q·RìÆ U Uo`œ¡uqSÙxÖ }˜7Øò¼.ôYuh8M/è] Ó&EŒC°îNûðK4H6J7ЦÈÝ£m<-]ßaZ÷ݧ޲m½F(¸ŽÅÕî„Ý‚ååvÙò[r´îÛååoS/ ÂOÔ*xSÃÇ9·Bù´b‚Õ¯mñáïlZ°­B¶z$…W[kBkÐÝÛ+LtãCéq½<ì¡7š.^÷ë#zRÓ áŒ«2`A¼ 6€¸Õv¢'wsî¸hŸ"ê#L Õt-¯JÛ_ÎÉÐ ¦z掇%kÿJ¬w,‚øú~†‹cX„qØïA‚‹ª æEH åÏçä è’²§¬æE1xÂZ'µi©ÈK07¬Ê¼ÉzØA?Æm~ëBÛ;)g6Š/ñH6qg&o£AÌ/â!èô•â™3ñ¬ÂWB/)mI zªg5Ì‚0êÆ¢æß¾x'q†WÏã©©ãÓÐ+´ô¨sªôü C…ä¶2°‚ü‰FÈ.I‚:éà!ƒŸ¬>™™AêHUv)Ç'¸ˆ»ìO[Ptž3”‘Û_&™á´køh¤²<jú—B"8`•†V§âŸ3ýzúL@éÕ”¡K ÚÒ¿¨Êò "¸dR¤rÚeMõDœÃ>‰È¦$Ê^F]àŒwAlÓ]*ÊŒ×hà!E€ÜÑN³®ÑlD/?c¹Uy}:ºÂbà Oe9~Ÿ³Áé¹3ðäÑÔ T, Reƒ!à u#¿£¶&¯!¤C±šCPRMU%ÖÈžß´,Ö.‡ÅQµ†ŒàYBgU†ÕM(‡?ê·dK)JÃŽBИL'{™Så$³™¹J‹‰£µgÅ@!ª…šÀ É1džß¶åµµE?° •bõцïˆý+O°”³ÈddÔ Ùj0$"‚ÄÏ ™%Í‚oðfZ‰RG%_6•¼1bGDj{,f´#_ê±H÷}m„Æò›n°¨£œƒ³ý1g£•6tÔÃE£Å•\¼ZaÓ Õç¯ì—ÎNJ°&ÁNPÈäj÷qAb§–P‘2L´Ež9+ÙF‡]“Ö}'Ûàê ¥Ðt¥¸ ætÏÛ}ñ†AL £­§áó$Y¶ÂVLà"9|¬¡«µ>fÄÐW®ü.WOsU"eÇ„J/Á ã4_ðà„ ¡Q¹xêLVb±+œœÝï?À'[˜T €wÚV!WÀ"`6›dÍ2qœ•YvTm‡³ Tgîê=—; 9ßZdîü:«Ý=©ÃlÁrVñéÓÅ!¥ËWlÞÒ£aKÁ,.Ÿd…ÀTøwŸö.Ù5 eå.Ÿˆ8;½í¹ýÆbžw ï¥Å ]A€LjßÂb×^6° ‰„m¶mÙÉ}+6½Ö &äz ¼ožB§ô ½ DVÃAý:«`PËPƒë ‹ ÷\N¤úg>8CÆmQ‚¿¥/eŒm¸ÊQÛ(®¥@0ávþ“kr¼!©ã÷¹6Ö:)éƒJsG^r;áá½ ™Ù1Ð h ”JHaÄüŸÈS`Ò\°Â=³ÌFÇS$Öè8N½¨í/,?=Ä’?Ü0¡Âv¬/»AAå·G±ëÛ¨Q‘ƒü@Y‹[´zoIyc.÷*\ßzcó‡“ÿ¬x-;ˆ‘Üå,41îç~q ð^©2 xOeTO1°zöù0OšÑšî$fÑ.¼®—ÿEãü ë“ÿ+d§z?HX?[,™·í!8Çÿì‘¢ ÛñŠ“`²v¿¥ƒäs:ÿÂX"l˜éH¬Ž–ž§Ó¾Ñæ Š WÊU…ÈÍÄ*>3fÐL*èŘmN8èâÒgóž†ÂÌ/TÝ’uAŸ¾}f,Ö–ì•„·šÅxcR5X\®æp?§YÃeŒ%fȵWâbm‘AøÚ°Y.ŽüA3%ÉI?¥x¿!75œ€Ÿ|Vè%Åàñ;wY.io ™ùhT…ÜT •r ¦CÓîâùmW†ÿ²[´if¹Ø±ô(î<õä-¸nÐ=õ/‘‰;jµe‡ÃÍ]k¿pïþhDMàh{,@ÊÚ\°[ºœ›r%N÷.ŒU¤ðM1¦+sÊ<»rÊ×X‚*ýøÔ^Y†¨õˆ¹¼æ¤×R.îä¹Í×ÒÂÛ)«²[üômÄîµõòA£Ðop£¦éƒÅz óü®›öÞæÐ ;N¨DÊ$¸Šd*uCËÄVTiÜáMÄV§‹ sé ¦žX>°\y „žUr‹.rµ{À×!{¾)”ÚÿràÔúéÝÍ,zgxpk¿ï-÷kË †šÖr½ ú¹àù{Šž;Áè|©(bP(ôÖr¡SÍ•—þ·%ÿÛÐb˜ÎL9 –/Ÿ¯@ÈÛÚ ¸ì €S´è™ÍjU÷уR"‚*Õ@NæœÿžÛŽ„SÄ^0z‹ Kë3ÄüRÔJ|Ž‘õÖÀµ×—=h!Öý^³½ìÓàë(-±Û„ú¨†Ð”HÕ/ùU›l5´m#Rè;÷M–¸µfÔ±L—F@‘í3݆3Š Nú: ~C I¾hMD  ”M­„)¬…Æ?Ò m­¢ÌP—ƒ¥]!qØ[¢ˆV's¶çuâ•V÷‹ÌF&n¦Ü>8‘òyÓœ‡àþŸL¼ÅWuŸ^ßP•„츪ï£~Âl¿¡ÞÈÂ6DßýÐÄ>1ìôQÒBÀ*`DýíÆ|ý-âF¢‚5™iòpÂèmx¨á ‚ò#‚l,aª°m€Àš¤Ò„iU'ZèKÁ  2ÌÏ‘V`³œý¿b»ó‹ëÔ1= “*™PæáÍ3ܪ,’Yªø‡½F“4‹Šh5żDæ‚Îi'-9mSÍÆ1IîôÂ3 AObÇŠGÍÃõ¶<©V0«] )ÜðZÏLDïi‹ †† nPÜ ?/ÍëØ„õ˜€u”ÙŽéÙRv@µóÓâS@ü”ÄwùX…G9!UZ°¼r˜êYûE£`ˆí,PÌ S²ðžkj³Q\ñsâºõ?—Þ»F–o7*øåt¾1ã¸9ã'% J=Mg«æ¦WmMÀtÃwhì@½ÿ<ÍeÍ %í1ÃE/\G!Ý03uL~"š_ÛZF™ëÆ ©x^äŽÛOÇßKôi|éÑ“Œþ`ë[#6öÞÂFüœ>n3Ñ|Ó:%óÝÉІ°håFˆŒPÑ"‡§wL¬õT}ž·´\Q…Èãªz´Øg7LQÓ%I !& •4Œ5µ^7~œáú ÍŠæ›fE ÷µRHR#4;°ÃB=¸ôXêÔŸÉ7œö¼úª–ˆ­ˆ½ãÁâvP’á› Rß Èô/Æ7?ç÷xÉh¾^¿ Ý;Eˆâ&+[A”‹l•±ã.w~ ÊáG)í8¡xhýÛ E=ŸxôŒ¢§wi`à^Í>E­×ŠMo…/N/Ý&Éo¬éˆ˜ø}’Ž~‹ÌQ@C™ÿ ÁDîâ< ä'¼%Ú=³®_ú–¡ Õ"šM‘i5¢•¢IE2Üè? Û4—M@øb »‡; ¼8x¸®Š/W Züã8‰Rü±[\gœ­ý4¾ƒâ=Hþ³3”•ðAhÜšJ³p¤ÖŠÕÇq½Ït´`7rÕ{¯œÏWÎJ`=‘6ãY`ü<0®¹›æ^oˆb"¦ñ¨E÷²XMÛ½ƒÜº V T§á Û;í¹œÐw 2°Rø{B«íîEEŒ#—]py·Iž*ðoRÀPžðkÃt>éîç«%¸•ʬA•,»åœ„ó;dÜhÂþÑjã ~xaí½¨³ºHõ?L§•–b¢¼•ªŒë+Éï†È\ùÑø#Â~˜? ™ Ñ«ûimÜÐÖ£vÇzä)l4éµõÊ¥ræ¹ï8D©¨îªÃÑ4*˜ã"ttŠS…ĬLÅ ¥-\A ƒ“Í´mYIWMoò("ÕªŠIüY‰ÞÒ‡ýÂþ¼¶²ÒÎù{+0?øÊ5ù$–‡È¹TãW]•åvLæì»}džCc«×ÌÎ×(B…`øls16âe©U¶{W ¾ 2ϰa4è6_ýB¢€pìí7Ž´-TMç Ê0Á€?¯¶d¶PIå·D¦Áž‹úBìáÁ0(ûf‡·—‚úAoÕòêr–í%X “h@N«Q{û÷¼qæFmEàOTýÄŪp9I”Eë¯s÷ªxµœÖ/xµê½î/løô àö:¡UÁ:4:4jï0SÆ »-®²;¡BÿU(¶ ´çKê„gNöÐ6£d+[ŠÀú‚Òê%ÿG„û™eåïa{”{ìÌ€·èW3C…ºÙR&¦<æuK'ú¡˜ Î~ó<.ª#Ÿ¸I>2Jž¹ô²)……A¼»’s s2aò^°‡[ö¢§²ó|>Ž´~6ŠÐú`£c©qç¿þ€`‹‹`A2xãà— ±^_PÈV¾èóúÒR• 3“ì4ØÉC»´Á$¼ûˆÃ³é;Æ—Ó}@JK§¸q„~&ùð"çs€Ú/U,©Ôç}«2Gª€kéàzÌaÚX‚"Ïd.€¥ã¼NºÀÄX숗:œG' GeŠ"_ÉDæÔe£ÉÓS¾NT t:&ÈQGN¥q,\aï=wú5túþ§çg†î×W‹Ðu ò«‹Ê…SÃë§Îqš­x5|ê»]íI€òPO06¥ä {ŽñÃãy>gnU¸Ð@7‚xóîmªdæ4ïLQ“Ä^›ïúc\âM’µnµ| l€ †Öˆ¦ìBŸj7æH¡b篕ÎêVv4o@Í%»ždÊ:U$çn6ªÏºý `™çôo7ñ‡PÁkAð4øØm îë†Õ%Õ{Z¾ë#CIÞ:Þ½áQ8v%Oz‚cTÏÍÏOYSœ 8Ìs»…—Ö†»ý[h_CÃÖ»1©AÍtÁ!v½RÄÎB\­¨m"¸ûÓfnn†è9ÍÜ=ó§‹Ú¡_T2³ñ´\~Ä1uŒM˜_‰†Ñ]þàÀX]Œq·üõ^÷¸#Že.²~K·!¾YE;|òx¶Ñ¾êäz‘|Õht Ýìž’ Äî\3…æEUÃ+øzSC9ìX›n=pN§YØg*µN9°¢º±“0ùX ä.{š:Wõ§ý÷ÀrÇÅŒé²ÂYëû¼ïõÛ‘ÅòÀrmß ¤PØEñ¯î ½“n{\x97„VcÈ›§ÿж7”;•Ê5zæZkÕcÍ…p>»Y}Ä1ªñŠ’§³ ÆH2[?äd89ðؽîÖ}ÑÐ'~‡¼ûµÂÖ/@²”ªI) {~n 37 uxGÃè!†áìfvˆ;FŠJŸZ@_´3>2‹0.ŒA¬ ža#çcðÆàÀ²Ú'c™ûÍø'sÊ“¢wnÿÐÉy0 wŒv<»6âú­Gû£.$…Ä`þqRu¼ÚU¸ÔqªÒGŠï˜þ“GÐϸRåÇàN¥^ZUª{^x~ã?ØÿMÿö«îå „qZZˆþ¿Ð?CÓÿEÿôõ-í,]ôõéPEœ6f$½1ÂnŽK1ÍjœÃ8l• ×[dÒX¢òŽ~>åÀZ*èÃFÊÇã嵯>+«@íV×ò÷ÉO®ƒ¨E¡¯q± ÍÍ£7€n)ıæÐ&X…®eëú¥M!á'Okv(2ô¥žÞôW p[Ì®@º¬¥¬îSŠÑ  ;­}…ÇN!x%y[;3D¬\īت޺¨ô/&@xüZ#þz´^|ñ3!Ùœtj³šjn“fÙfÛðQ<¸œì±a1üÝG¸.ŽRÉÙÅKmdàãUb´Ù_e„Ú´†24í1þ•ÈŒËé…¾äË™ûQ.tÄ„zæ&ÍXçn(ñâ%¿íüâýðÉÁO²ïà±u?³q’t«¨ïÈåï‹'íÒ²RÙ“Ï÷ -a£›aR,åçHM¹Òi»– Ulã›TˆÛQƤc¢Ô+¤‡i%]°†´ ‹*{}9ÛúÁ*ÉÏïýóÀrÓ- ¼²ð-Òœ¬}j7¶™p#·µ%¥‚i›»ñ¨—ºÓ(òä³ç_»\&˜ÿüoâKÔå’=ÎÙ½uŠ‹½2i²ÊF‡Ùøûo’YXlt-$€+Þÿý\m -íþ3Ï\M[{"N¤ß<}E"-ÛbÒ\“’nÓQp6C·iLÌØ[9ÚeÛô²ónÃõÅß·ê¦ýSÚÎ#èØî*ºôòªRžHIZs0/•¨ábŠ*û…ì§!=¡^v©t°Ýí_V‚þôª”(LBhéw %=‹çÒDœÝQvDÁé~«VQHlÍÁ$Û‚0ªYÅdNð_Ú“R@EªtS:D' èæ†-Bƨ÷KщJC‰Ù„¾ýæ¸ nL_´±÷µµ«.3µ ª×[ˆ——£ý>þœçzÃéÇʉÎÕ‚wªÂûŠeØ¤É ”Ö™ ¬ 6uœ=LÉϺT†¢ ÷›6³¨â‹â‘&Xã›™Â@,èÔÿ² –‚ö¼’K‚™ú`òøcøx‡Ów=¶±ùà?‘1büX|0xàÅ„¿_ð¼†ëïßï qdC·îíK‰~%ÍŽ17l†<…3] .’ÊÈ”xÔë×s†uê:¨î9œZQ²r*{aJ(cbÁ|=*Öµì¯gÊŽÆhl0¾‘‡+Dˆr‡m}ž•ô œ”0ºü–²oŠ¥û|ÕhN”Óúl41Fäg(‚ŒaÒKiî«3—öêNøÄO×y{…ô0¯2‚Rò"jýú:2(;yFÇÕß­64ÓÖCá>Ÿ×ÊÅ["¥ëàÂǦGñ·IåšMZQFeÈ䌔˜FÓ¶ ¢rw<Ô0¥œûÿ#NÒt¬¼éäùÎÈ©Í3¥HceLñîžVé“Ú£5¢sÛôï?ŠýÓ´M¥Õ˜-íª˜ SIލcÆ›s¨áó:ß¼ýßs`¼åâüýp`ýß ÿz §ó¿Ž Ou{ß2xâÅóæþÖ“|¾µøÉå{ûcÇfSÔr2Æ><\t^b½-êLK•âΰxuäÑ2ì9ùœé%Ê«O˜R Ðçšç4uR1™yŽ©Ï0¥Åúô".ùT¶«¯ ›o<'ÿ±.ž½3zò¿sH~Ï?X¼‡àÓµ ¹×FGGÇÌ×€˜‡k‰­ô5Ï)`HT€= ÿÀm,åV°­<ïñ°æ5ÿ³Ú¸#¯’4+䘮 ót0 fTŒJõ9  Uãf’5%IPRnÄYUêµ!ÝW┄ L"YâÉÿ ½æ7VEEk쾯¡Úõ9KîÆ>fÅ­H‹®Q3_ h©ì¼…3 æ¥s—¿ 𧇮êñþÇ?ìWQÂfP(­ì),LÈÏÜ@WÊŘîÁû<~*Ej?&ÛµîiC}ógU#P ‹µctêE˜mEy ­Yjó¯ÊÊD<þÛ \¥ÔQß .õL㨂BžMRÞDWœ6šb©gJ‡vÖÁ†<|ŸñÀ}åÓïÉ|kÇ·ÿ§ÙÄ ‹`™ÎŒ¥éD$G°ù»ŽJ“q*¢¿Ÿ˜Ó°Ú7㑱%–ùºz)½EsÅ®‚Z»I MºÅå^È,%ÿXZO!?Õb¹6³Òñ?-N¦š $´LéàÄÐ?T™*J1ᨄWâ¢d9-JQ# Dm9Æf\™ &äR¹!Å„çD®×ë?Pþ¿¡š¯‡×ñAFƒÆPå6Jäâ†ÎÖ¢àD‹ôâ°Ü»Ü¯×h4o?q3 vˆ’Ó¸MY.±·éYúö;¸×})RiY€&Ûv£˜_RfȨ¸Mp˜-;2ª‹0R•suJÍÞ¯¼$šÌÄéX‰ãv+w8°6Úîp^¥ïJƒÜ—Ô¤KrüóŒMÑ'‰X¼9†Ô”Áö±Â… NúƒWó:/îfX:#4 4 óëÕ¸•ô±,â­|`„Ñ_<.â½eîgžöˆ;åœæœ`>„ËGA|”<¸t$nRW÷EËhc¥þÁÝ yCãˆ%#DÖ=‚îË86²Ú$²Q  ªîy†~õ4cy`<9ÜÝCÄC„öuG²ˆvâlÈÚ*÷‡²:Ü­1§–Ðb·}ò«.‘¤|Xð_'²Ú½Ûk¬¬þ—SÿÑçŠ6..R÷y­— ã- –iFåÏû¶¢ŠÝCMÃc®KÖñw•†T-òäÚÐÞðCu¸c®÷Ž‘ù´ b¹fïß^¶nÔhxØ Í;—Ë[Dm†uá"0V-±Œ†VNEW?È·Ð×–ÎŒ"T¯Dª_ºÈ{Ó–X 9‹ÂÞ³![:ý¸ebœ©wûc};»¬òÁÜ¥ÑHÙ 1ŽÕ6÷ þ•6íK®dv³&Žœ¥¡†|ñ…Š4D‹³«k\S`´1ÕÆàm³»……­·†ØÖ ßž‰ÞîN[)«v=%ã›Ù¤¸X-½ýFuûtì–Öƒ‹Y'¸°ªÜ¬+û´uœ‡žŽÓCNMqmêµ1ot¯Ûvõp9âíˆÍñRV˜;‹Œc¸ÍtoÈ^ŸY6–¡„$¨â½—¯­']¡‘#Ïaˆ7ÇPhjÜ@ŽTˆr¡'6 ÁôL>çxÐ Jvë¹èèæCürÎs*õï?÷ÄÞ¥S†ƒj³Ä®Û¹Ôå‹}b€[.ÿÖªº)-É<ò.*p¾m/v)Ì­'ö/ —Vž9BXYn¶¼@ŽÂËñ.Ô0UJ°]«”.‚ ^¦ÉfúŽ¢i*é}ºÂ3ÑìŸfEÇ©Ô& x´óÈ3‡ÒÄÙbRhx|³0ÂÁçÓm¨œx¸çˆY>‡v<Ñ™ëuª»×¼ðp¡'ĺÉô¥dù$*-äÐ(¸ºÛmê[ˆìa¤¹Õâdzœ½A ]]üªíÏmC$/#1}É£.Å£z#¡³‹´äîŠñHJ¹ÜGj缪Ÿ_<ÚŒ8ÙØgÿTw‚"³“î¢Ð7"æ°ôxð²²ù¢ ™îÂj*þ1‡Ñ;Þ m`K˜ e±™œ€p™XÜ„Šy@ÈX îD+79ÙâpN»^HAü ¦š·@?ϺhßÚ X*~Ñ>UBNŸ¬4c\úûê-qšÞîƒïáܽ¬_åbÂÅ™Cpš¹ßI]ZÃo† ÷õ’ÕižÈQR_ÕZ¯»;hP^¥EŸ‘Ù#à·<èUy ÈjgHDPŽB„ì[(€rOÛ$Ù“O5}üš‚Iì_>ÃÁÉeJµíó¥>¸[•ìo’¡Bû½1 ªÕ” 5ßž„+MÛdX+mÍ!Lñ–¤kHÚD¼Ô£m69òˆ¤Þ­„= ÎçœÖ½êÓfgÏÆž0s#ÚkÒ ¯7–µË“nrÙ:Ò¶¶ij|Kby,‰~cŒìg ‡O*Ów?$³Ž‚eëF¸¹‘ÞXDèO+‹lçKËI ̃‡•‘èº!xóÄ…ÖÊunQ—ÞÆÌ§pÒH$»Ca_ †ÿsÚd1ÖÈi<—Á^-ÖÊb)“,6˜ÁX#i¼QÅÜYžÐMÝ}é>°tŽð³@çþHä—Y¤[˜O%.ÍÝâœbtþ¨"ìǹƒõÁÝ)93Âí\°µ„^vam¯ª¯3Œ¶Åð&ÆÑôÊô¡bûŸ<ØNÈx˜ê¬ßæò㮽Qdð™³X².cd¨_¾$ÇÇFcˆæˆµÂØd¼æêNÅØ£• B),3½_Dÿ(ð…›‰å„ÞØO~=C½"•6Ç”§ÙC"lÊËÐð„kªƒgž[Æ ÊéTíøÂïÎω³¡îø}gAä$É0 ‘¡i¼šÃZ†(“e‡6;°I'ó +&j øh¨aÄÀè‚þ/³eH°NÊC`…‚MìP•DÞÁP¨ÔžM³Í©ÀÃDÙ£Ã$ôß-q¸f±õ×€‘x Û³£JÕ…9J ²£´â´ìŒØdžÑ׆ð¡d~?‘“EÌÐP_9<¤…K˜)ÑpÏÑW°$ÐkªâÈ ‰ð0M•½v‰öŠaë9õQuGY¬î)ëblšWfû3yï5£+žFGQìÓÒ!¯Nè³=è+ëÒ@ˆœÎv4?¶Ô!Ò+ójÐGü¬fp<»ZmÚÚS[0B#AùД+în ’¯`w}ÓÐiU Ñ¥ä¡eߺ68äz =­à„H”LÝ$ ý|³FÜ"‚¤KüÑyœu%x ÷j+Í×ýúËHäŠu—¥i·`<‘X›lò˜O«ð…ô•æà†šÃ¨&RTØ Jql³ÏNt²oô0O(–ôÎljÕXJ5ßyɆb¤4â2+õ:~²<’:n$PööhٺټE=¾'xC¤ß‰%AíÃ-p,¬ ÌðšO¶úÌù]ìºË¼™#÷gIÅsK\€PTš_‚®ÝJÉò„J̦váý±›W™¤æF6>‘˜>éë h$±>‚1œðš¼"K)Jˆá=Ÿ1¨NÌdp^$ä«,®¶¡À Ë©÷Í´nŽ$T¶Yë§1՟Ū Ä;ÝßÄmƒ‰½ºŸŽÂø Ñ»—/gGÒ ¼èÎ"UE2}Á©‹°P`M: þ“£³õõUí¦¾Ò099¶7ÄM8Ë"ðé!Öåï`ýªéòh–&µtòÄJ3T)eÌ™òiŽ'œõ\kò¦OÛäÚú÷|»ÂÚ‰nØ¡ŸÄ(#ݧä´ë éæby ‚„†t“$' „G ù·ÞH½ ‰Aù‰ÐF y:uë”H©åªdeé½´_Ñ1Ú1Ö73û ²Y°á`ÅaÓcôø›zº‰›©çÍ»JWóØþªÀý>U©|Zþ”üŽ’fcÆŸ9/·oäË%ÚðÎ w¢{——ûÍXŸY%þ?o|ÉìÜçpÙ¹zçw¹McçŸðê»L*ÿrl2šÑò;fïÝLIùćCo¯ÖÞ°"¬Ñ’»ìŠ3V°žê ƒŸÞu¸‘ÎWX)4–ZÒ-Ýý°wIþæ$Ö;jþ_P_Îbzëºé|Ç1"ký_ïzމì¡Òwþÿæ­ÛFGØC Pÿ?e¸³±›­ítxªÚ–Ý‚ï›:LJøD$·v…2Fh6‡Fq¤ñúš Á©#q"•äÒõ¸»„dÛ5ɺ}YÅA&³9Ý3äv³©ìðš’R‡qÏöC¡Ž»¹mT­‰dÕòßc{ÚGkBh‚ó`í.4¥:KP’¸J% 4RYËÇá :jÖ0y?|£Ã¥¼Ü¼Èz’S(KJˆ2Bs ÐNHËÂG=“˜‡„ᬋÕÎ ŸO5áˆr ™…þ¾öv°’¨YžÊÕaÕšvý_£‰/a²žfÕîλKf<]BïãsŠÄ§ü$IX×6e‚¼DÚ±5’¥ßòн®t¦<CVmdIc)©vÚ§@MîØ6›@'¸9GøÃv`ØÓöpLýÑèYø8¨îœÙð¤OÖð0L>ûÑè»a};CŽOpÿx26<…T¨ÝyYB’‰˜æìi B„¨¨*‹ #4#]^ IaBj*Îp`ÐTe²Lè3‘ȤÆ\K-Ø Å^{µ‰2ÃÎåJÚ`ýÙ) ŠE SÇóיņ “UJ§&俚·~šôPB°•k‹´»8åÙLJ3#«ñ¡IÐ7æD’‰XŠ 5~FxÐøØÆ›¥c|L(U«4ÁØ4\îܹô¯/€''ìí¸;±Aþ @4AŠàé¤&+bÿdó¸Ö|ú°æÑ#– ýù±åPÞs õ,‹¥H ß ‡Ô3Á(,3•®ê¸JKIWJΙÙöùÙr9òˆ²_ušþe…µq3†9„Ž(3Ç^{¼§Í Qs‡ìfÄjÊE>¦Œ<Ûz ˆÉÁËxurÚ©¿|G¹ØÞë*«~3íRË8uЦ3S@Ä}FJ÷8î9·phöÁÚI”›¶yiÚËÁ™Ь1hÃD“ a¼É .êJËúŒ†˜d/½¾"0",”[ YÔül32À’Šd”~5 r ÈÎò“Íø–.¯Ä1 ~3ó˜IHôdëºÕ˜‹û×û[“Œb¸(Aޱ a}“B-QA«äí´Q,KÆ_”Ìa0¢.ÌIôÄ@ÙÔëœÆ`7"93òIŸœ‹†—SÊê“å¿£wóŸ”=¾u[µü޶èg;ÊÙ›vGˆSóN†m;\W;gjC¶…Qha¯ ‹•¸ìä=a­ù¡[wnð›•DâìäÙÓzvàé›Ï¤Jj¨­:p¯Òõµç­|ÏfÒÕ£)”ɨoW àöÙ?üÑÔ Âšn‰†ÁÓeá×>ŽÈòO4¤¤ôàÝ(Gòµé÷­ð;·÷—ŒÆÅòí€*ƱæR·Å‚ ìgþlàc%“ëÖÿ(ç²1fdª§ Y)¾Ýˆ Á1¨Â‘~îäšÓAÇh4 ã§?e]{˜7ß44rzffbP=H³¸ªÝy8tóÛ|nëøü­hósv“v“*Vâ(<÷uPö<¹ÛÌït/#ˆ/çXû²§*ö¨ÑºC¦“ð÷Ð(ši›ØÙ‡8$¾-Œÿ(8ÃCmí\ôMSÐné~)Ö©ò)ssýÿOkÙirñ* ÐÿÿÓŸq­š¶ó–ÊnªÍ‰9!iK¨¥#8ÔmK$ôò…ÊT– ·°CrˆÍäWWÇîYå6Ì_8¹Þ,Ï#ð¾xÒV pÏzœ¼š÷Çëµû܇کþËèž÷£²< ~²ŸþýOjŒ¸¿ˆK™-%W“ v‘qŠ­²ËL“.b®ÔF]T!i˜s³úëŒó|Ð9C^ œÏxolyx2^zHáÛ/ ­Ž?Y‡)ºÈ]Æä®NEëÊ$òw÷É!­Ãܯ8R4ÿž^ÌÆ5Krkˆv|šg> ð`¾CâkŠüd.HFâ°KúbéfŸ’É#tç[¹•¬®CÇ¡ „>uÖ¯Kq1zê BÙ:ºÖ{0žTvEóÔ?ºÜ'<ˆÕÏb Ø$mRðS -‘ÁÔ(¿¼b³PÌͯ²{qÍÏÖ¶æv{wlÓp•†JPðà€™“d ³îÇêÒZeá¾…ÄóO=Üp…ÍøTäÉ”»ÙŒuLmJœêðqý!ŽÛ5ñ/—˜®Ìdw“ÓOT=EµœTÕb¤Á¦A'š¤:9ë'FCþÝZè%4])1Ê•FXèy&.âHM˜ÓЈ°|¢žé ! ÓÃHÊòñ–M‘@ øJ 7•Ì9ÚúÅ]½ü©gÁK‚§bW”û„²¾>Èv®ûÉ!Þ«©‹ÖÖeã•A U –_G2ë²Q.fG$÷Â4 Šê­KÈxãžÁ¬¯œE‡Xòw™óºíYÚoÀ¿áŸhÓ!zj€pO$“Ívñ–Œó‘±R›~G6é”é°QY•Ü\5Ï"éN€%¤=õ0X9‰ÖM³z*öù]ù¦Eo%Ð÷³©ØXÈÔ#ÎÎOàs ó$ã%îÅ×ÈŒ¼j¦]‚ 6߇:¿À\Íäªê-XîÓå¦ Åæ hÈd6j¢Á»I6Y€<ÍHÇR•ѽ­ÏÎÅn/¸ãÄ•?LyµÔ^p¾‹¬õ¹Žõb‚ zÖ»,$A›Ùö¶­‡Hlœ•sF¹ã}ÁÔøFÎPwd÷rlÏ 5~؆{sÀ}æúëæžf½Ó¯¿„ë‡_O3¬ Øÿº­à& =hšýfM á¥ww8,µO¬˜|èßUJNk0üþ·Áåyd à €þÿ>#;™:Û»:›šX:[ÿŸn—e„ÿåv)>²rXÂCJ– n€`‹R®Ê«Šåm_ïž¾Æÿö2ËU‘ µ¯þx½ñžo¸Sip,{Dø»Á4ïâÔ)~mµÄ6/ëzphïûqµ‡ÑžJ^Dî(jFòÒ% ¶G0ªŽÓ'“~¼ ï¶€µbè9µç²ð:_Øô8ÇᬨP\¶¤1×VêÀ˜`_fFO” É£C:=O]ò .¿Ìv•CëóþrlÅ #TJÞ˜p赚/оù_Þ—ö»‘{—Áös†ßÛHÒKGÓQù¸Ã&U` 0Uæ©™ì}wƒÇ’+Ò9°Q *RÎy—ëŸê&½mÉSkK%¬SG8I¾ðyǼcéE8ýæp`ž±/ß¼$[¸ <6M¯|œ$šÔeôˆeò÷´’7iJ©¼Áä“}}¥ Äé͸÷)Ó¹°œÑ **’ô:oUâ•Sø¦4ƒDn§s®Fõ()iÜKìí¿Â¢ÊÞŸN6p~±=ëû_²í°¨c­ŽØUv¡9*6ÜŠ†8Ûu1Ô4F*T71q§½g—gê*•ØxÂXCxÖϳ«gâKÚÇküÿ.Ô´èÕ =ÿÁÿ'4þŸ‡…±nü<Ô̘voÄþlp<ÂÖ4<‘7ÀC» Tû˜æ5×þº-¾ó ìd¼@ötXü$Çìz%Á†©†_”BF †•T\Š+A¸Iügd¢ÂõŒ¢œ®. ÔWu‡D5L‡îÒd(§Ðn1p±­ $iaí•|c3Ðͦo»ø#t¿è±"ö¨ó7öº¶ÕbMšQ¦…ô¯ù?Xý@è‚8áz-,1ŠŽzkÛNm_³çÿeƒ|˜(¡y/Ñ!@sÐýWÂÄÔÌÐÕÆå? ÑÊmí¾Å–¶÷BÍðlIËŠDËi»ìUu[·m•µÎ™Ä•™³R­@bÁH™?mEöµõ¦@ØwmÒÅ5Ù(ðö3ê"2§éÑ›­+%7ž|7!=§9´v1ªvÙØà¾ÂMDÏ“é*ÆÅBv—¤i²q/3Éo™3àâd'‰OŽá4ñK€£!t% ”–DS0$qõ‘²à-Fù dÔLðÈKÝB¹á}Wœ( ð d°µ´6¿?©z†H}rdž^¶¸19é×ÏN+W3y˜¬/##§G3w–^ýq*íí›EBRæo¢Sc?€·µ ”Ú×JNC¶*+cHc*qÇ÷ªD߃‰”²±E)àØCÛk›f54U œ÷’€ŽšdÌF:ø›E›VM8àÁ«ñ÷ä²Õ“W›Î<ýjÔG´.íøûòñè·£+¤š©ÛGØOW‡ ªˆå™/*Ñ`I˜Q Ü´­H…xÀ4¨€¤¨íÌ‹íu×d–”ý:nŒ°A˜ \/hJ&€¼à]o¿t+‰K™,}W)‚–°î".{°]Þµ3Ê@é09’ÓˆÙ['Zy(é;BÖO@üáGAF±§ ælŒ† ]!iþ‘2*c5_SÆ—~æ6°u0-‹¯»0©6$ÂHfÚ„]úÀŽX—`Ǭ0ºáÁžÜÄ žÓ`Ú‘í{¡åÛˆm¼ž€òÂi£yå Ù¼Ÿðöû«Œûòëcê°0Ûå¶è|i0‰Œ&Úös[ÚîÌq¦Ët )N®®·ðB¬çBÇ…ü(R‡õËÓ¯sWgNoλ÷¬ùåzôŸU+ x§ŽÎoO§FOÆ#©˜­ g>¯?'ûò×ʾ4û%ECÌž÷ãCL¿fw¦æ¼~_—Ö\šýÄÓè¥zóZv(ø¡F%hØ”ÑAO g£û¸¯÷Ë–ü_F~ýägœÝß•þé4'POjÿ`ãÉ÷÷eþ©f à9u¾Ö~ܲ0qH¤ömmtïб$4m¾8bv‡Z€k`=îø°;Ãd!ùH°*IÂÈŠèžé¶¯~]ž>7´À%P\jRsL¶àaˆY‚9ÈÊ$¬¸&  ¶öÃÉ](ܹŽ/ùÝžÿ×õó—ù^¶ÒΛ@Q¯Þü+m„xUå7Ѓå¶ùÖÛŠ¸9/Ì2œë¤ÙQ¨^ׯ3òe÷#¯ß=#{~¥çO ߨ¬ïÂg£H$Óv½ƒ¤%@4T‚Õ¡‹ñF¥»yi‹Fã ©*b€4bì ëß*¸D€ËÔED³ÃBËÜñ» ªü@ÃŽÈL0#i:MÉhÂY›ãøáÐi ‰œ&t50jhŒ  bå¾Ùwwÿ,È"Âò Ù†¶b*•\8Wݾ2ÉGi„‚Wáôç˜ áw± ],¼.’•Yèü2 ck„zÏåêºòšpe@dZ6€A$¦æÅœ<ó?Ñᬉ-¾@Eqd`¿ÇcC#NaNEßÔ!ÂJPYÜ*š}4udµBW%}ˆ{$U”qÊ ªy-‰sü1|X [ÆÐmœªÃ;U‹Š“R0+R”pU% bLÑÛÛp5rS3¬ 5›¤™¶B¸l= ¾´Q@Ÿð¶Q©"yp[.ÓÅ=ðŒNÝj›Ô Ç{EøK}‹LaUÛ_æ³hºK§¬‘¯d» ÛêáýÏØGÌ[¶¡”)K…DåÙMçûà~¥Ê–ã3ÌJ°3YÆLòYÙ¨æÒm/pÐ:"¦ˆÑÎÄ>{ºö¤Å¤a6S†4èð‘]AžcC¨›Ÿ3}¡¡&,H"€{² ÜBAqµ&M”,®ša©àl‰ö²H˳cÎç`ÒM¿"ÒxN˜Ç°,¹ëŸö&B¼j$zéMBðaÇBÅSõä°M”eh—!NÈF21RE/†Äa:UQë÷HÄÒÅ47Ø>YŒÜj4Hh†¬eÒàê”C×éPÌ—•MSƒ,› “¥há»@ýkÚÓâGh#{~>=l›³E ¢_)9cÙð¯›ÅB‰üAÚãÑ®épÂ寝×ÙöVz¾­T>™¤¨ÞH) ‡cDN´Hõ‹ IFâ4|™¹ˆ †”X¶4•#®´aÁâ 29Cþ?†G”Ò*ºWН»P<Ãt4)à8Âѹl‘Ô‚$X‘Ú j¹Î’¯n>?ê<æ™w÷YðºŒ`R•BäNíÄbâ J+ —Eèϰœ²ÉWDĉ¡¢KÆñôq­Ùéo Nwdã¨[Õ5®Ó­ƒµÐ“±¡íXïEëbørüV)ÿžR²aPÇÎýl¶E*s Uùÿ¾­h…q\ˆlrY¸%`vÖÚYìï:î¤qÌ€¶øBö„HX¬‰ËÈÌî>øûÉõ{qãûúý€:ïüväA9ÂhHÚjóad4.—æ+±ÎS‚P¦‰3[Òü;2«{´\ÎÔTPÏzö¯9ªˆxKG9:Ϧ5ª´)éþ!Du4þl¬ ;ö†)®@Éò’ðn(:9%\„.m‚<Þawo±ˆqn8n¨Õ'߆6[wëóP)£ V‰‘tª¡ ?¹cýABHQºýß9 ë€pÀÞSš‚¨HÏ=ÙøxVÅ” Ö86 ,ÞÑ9–›¤©Üâ-›Ûþ=m‰QœxíÐPX=Iaqµ™ý¿’Š*®ÃÏÉ«ÖBˆ< ОîÁ!g(°¤Óªæøì{J­¯i–·ÊMj”Õ"BUøe9^âäo‚x.¼VpúÈGDXøEÕÓèÙq¯Ï‡.`AÃÐf(ÄVùÊ“©žnýD;Ì"+oÑ—Š=Æ*Ù¥Óæ’C%Á¾XÕ:T7ú "ãD8q"â8 ;«ãáåøñÚÓ4w[k`økn%eBf¿.•‰`“}æˆ?šË÷#ÿ#Y®y8‹.ã¸$· ØD™¼óc ¿Íø½I~?äq5 mί”æŠù³Hݤí|/Èp5s:‹æÁ!ñ~Ðüv\ïwT¼£ÓlÇ–+,uq„ ÉMlþˆqƒ™W© — F™iF?À23Mº€5ŠênWÉm²}AàHñÅ4‘abüêÕÅÏ!zIüôÌêY£O}þd䓇ªÂHÝQmзþÜ{.o…YÚç †`™“²ì¢¯ÜÆ8ýR÷ åÍ…!`ÈMnŒ«vZËG ˜ãÖýÑåF%•tè+pÓÔ`S°Ì>ÎÑîX[°ú‰“ïÏÖ¶B|^Úø³¹EGXðœsæ¸ð-¡ðyG$»b‘¡«=Š,æü{ÅÒ’VÔsß{?[Á}Ôñ†÷ëá{c®¨{µ»u·ºêÝàŠ×Û@¹CkÓµº­TŸ49ÑÊçj¢ [U@,Ã&´·•+ïÿàܲAPÛj¼î€%°ë €+ôý‡ 'Ѻk"ö»]î½ÈE¥"½)üF³U„‘ìà)eµÚ8•>Ñ,èqÃÇ=rquãÿ€ˆÅ’yÊ×m¤L”  ‘HNXÃp0èX¤³5˜g lä+®¼ÚÂÑ[/x[™=bŒÙˆö¼5•×Ù=ú£½))¨[‘7ýNG»Ý-,inÙAsÜïîoz(ÿV|>Cž›ºÑåÂb«ÝïR:ÿѦI»tj¤|¨ÁD/ˆ}S½„Pô|@x í •ÖÞsÇ kj£æ§0&÷ŒRÂ{ó'Ø÷ûqŽXW¨’„±ó…î@˜(ŒS¾¥ªg¾åuÌæÜjú†µÙ§ÄrHèŠØú÷H%ª]«Å4Œ\±.ɸH Á·võý;D?È=ñfÂrt{¹ma:‡Šï¼P« =’gÜ¡ Ó1ì8t@¶>ôöó .>áŸh|]¼»¦ÜÐD`o_‡õ6û>G¿ÑœÐÜAà?Øoïä  ô÷^!ßë~表C„Ú,è±xMN]ðüN33Ä |Vö¸äÄø:©Ÿç©ã°ÉêŸ/Pê.e!*Z\*ý6¨®^ƒÜ@ã5ªŸµÜCS71ÿ¢ŒTÝÜÚ[M AQl«Ù’13 4âñ{ñù(r:>ãÞrÀ‰²PèG"‰_s5+iÛŸ R3H©ôñµb%ƒHgßÀ_ªNZjk†¬ù¢=³ïOSÅø¸¤vªškÞ=\XàqŸªÏþ•4k›D50¶äµõ=Bž9‡“a%ÜRÈ–iTTCš¤ÿ¼ŒŒÉA±JT–«Åµf·T¾WÐ)HØC+$†?æv%%ßAµê³(#Ëß’˜K¶ó:Þ$ÌLA+¶cBŸ%À,%ň·ë8àúã… #ûððß êËÿ¨÷?Cp†ÿ9šüßäŸUkÖ[ÜíP !@"0å0ª‰'‰–ÞºÀ„2©VÃZ©4•6ñ1ÎvSË‚ƒ™gF`8±%²„jŠ6•߈ºü¸!œ÷†<ßI®ó?¡êzï5Gj÷îå ÀâCD çÕ¥ßoç§oïjòÏ´æ;ÜýŽÇ]ä3¹|û™º–PçÓ>B "¶Æ6V¸ÝÙˆ¸|˳1qí&h›<ˆp‘**¶ˆa©Ñpq6i‚QDÄt‘‹MΡ†‘Œm £ˆ (¹Hu ûsd8ivÙè¨EÏKÉ].2–òi°Åb6"“µ™"ˆ¹ˆ•šÍìßWÄ\ÔjÎzFñb££\GKw{ˆ6Ú®çbÁpÂÁ5a߆¶uÍ\<’ï÷_qþ‰'Q’D˜@ýÀÓæe«@|œÌáήÔsƒnûmïó §¨ýUt†Ff·}¦u¿W‹ÕÒŸ¨³íÅä2ªõâçXÏŸÅK´VgÆmv]îz™LѺó,K²­~–Ýuÿ³ŒUŸëÛS©·õéfTd« · ä^R+HÈ4õéç!;FsÚmí÷l’)Ÿq‰<ð|ZÞ…˜©Yxyî’cýI¸vˆõIÑE pòç÷=n¢}”¯@ûÕç91Áš´ ‚R¦m«Ä5kÆÝd›9Ÿfo¿Ì4W.Ú¢öA#ò“š'·äf;šÖ¬5í^$1&kRr+ZOÃf"t`?æÖÃïP~×b]KÿÝÈ®P³çÑ©xþöw‰Ùô«'§o„~?‘ˆ^N%Òº?zÑ//O"Ü$RËØ¾W`UÀ ^,‘z¬á 1‚‹„«X‡ ‘Ð!ÔÉ(NèB‘? êflòÒ᥀IŒ‹ò[Ce;¡0͸Рlâ±s½¾¦¯ºæ\Úá.žd"žˆæD.… ‰s¥Å%éÀ²×0ÍÒL Ú»¡y}×ÛCìgìôîê*Prž÷ íáAµw-…ι¾Ýà –¹„ÕÍIÈý¿+¿sò—OUšÖPF+ŽÏgOÙ@wÐ,°Ár»Óów±ôM1)¨àÊÉiŸ×‘¸‹Š'ÐÚµI·ë :Hºøºm oGrµ@3’˜ ô$äg ÊÌq‰pg¯—®’_iî&¸€_ê¼›2/É’ãȃhù‰Q D}"FC þäA”‘ïˆÁ¨׈Á°†)!K¢ý4¨­4â…æŒ°æq(v’L°GžÇ¿õ²OBÞÌ÷—Nkv̵_»ðŽ…¨ˆÑ—7ðÉGGA É¿ñL¥Ñ$᨟²¥ˆ.–Qêý,A_ŽòäÝRÐ”Šƒi„‹4Q§ ^çú'"ìä¯ä„é³M)+–5áwBLTnSD»äsÐÀÕ1ƒ÷8ǘ$,#k!¦®¼ßAòLçþÏÔ ²”LpO"yQ6ÈLL2& Œfl¤É÷Ýõ"ŠLk…qÓH6:2VZ/éé瀚,&MîÆùë? ‰)§u¸á%zí~K©è`™þÁ2AFP7i㷋鮎8òS&<å`±²J+yù‹d´mm=埆ÄJFž%ºGªšëŸÔ„X)Òbt˜š[rm‹#1Íâœçàñxôfà͓«¨ÙæZ ¤'ÊW?ÎñjRûª4ÕÀHð?hоŠo¼ Àš¼ÐjQïÿޤûÉ›Ë AkÐÚòbʤøWêƹÅNýÑ×MN0™+?S*X%k%CòŽÆ°«¡¬^p@Å ïpŠ3šã•¿jÚ'õ ´å ¸÷Œ:‰”ßxzXØÀš•W\pqj7ÈOlä¦ÜÄDt@!#VþÞ®÷ }À4˜I.§rTýG#̺Ac3xdUé8_ÿˆqàJL~$é+ÎP+¡~4wòÕ–x‚Tìy}2< þ{…è4–ªÔMz†cep`š‹Œ–M{P !‡o%ß–\ÅQS65îD8V"8 8>J2bíó¤¸bô¸ÖªýŒb¢í"ã=•:ÈÉëaßP¸½²Ì)â4‡»Mä¬t¸Eÿ”év–Qî,¥Ï;f-ŸwüÝô\ä!sd²™~œ¾±ƒ±úç"Ñû»Š³ Ì÷Uàß=®m_  ·)#^„3Žó¶áßU>R>ÿÓ ”àG?\³ˆöt¤ Â(µß9-ÞR’Ù¥F~t88H±Dʱ¦%‚’]9è¢Ü¢ _oÿrªâÂüiÂëÏRì_`£ßÈþHãx²eëþ4(ëKËËòfäÊÏÿƇ¢ûÓ=S8ä’®K¼<©¶ELÁ4e™âFòÈÇÇ9 ÇèÒ?䈄FJ®ÄQo¥CtN õë –Tã:ªèÖÞ?jy‡AEàhÝUc¤äü´óëÜð!Ô væý‘ ‘ÖÃM— RLJ0•áŠ>ô yO“äp·£Ùà ӟfYÎÛ}DtÏÌf\sˆg2² J̸Édø¾*Aß#*’;t‡ _ (¤GÓ³(×Â!ñìó^³’ô³’ð˜X—ZèÿÛµÍ׺ %n —[Szò¾5mÜ »”+)j•ü¼¸?Û׫ð{¾¦Wã!p®Œg3ÒïÕ?tˆú=‘&ˆºÄ"P’¿xü%&ÉÀ$‘Ø# .ò2ž Ù¿!tA'kq{rj³1žO‘>]¦R§sÄz íeÍðræ# !k# 'ãF¿¦Š °nX¶S%¸æ¦ð)fçöË¥ÉÚœTiÂf¢²«Z%³”„Üéoáí—²,ÇdÊ5\‘o‘Ðgç¶UÇm“s|Ù®FÄ×¿Ô Ò7ךJ Éð¡3×-ï¼b‚œ! Ò/‘½.YŒèMŸ;W^X‚u[K“%/A²“¤ù;‰È¯ºž‡È|vA9±ªéá$|3hÓNš#MfYzJæ5™¿K0ükÙv–ˆ¶z|Êï¬8ÚLqж.w´¯Õ’Š‘ç€À(}%|>ƒ0vÞ Lvy†8 ½a·m²°eˆ %›)F3Äã¨We¤bpÝ{¹´•bà$Uñ‡å1°LN’Öu¥“uOr#—¤²[V׿öò#\’ii~'~KU3àÌn”;wÜ€ûq¶$‚—o@6Ͼ‘Û¯ÜÔ² ×΋Ú/çðö‡=œæjFsïiÁBÿ²{¨ÅVfØí]›¼ÜäFJøE¾ÖÍ%ñðN”€NKOAmá™rÐz>3_•Í 6ù2U¯r o"šÃVœ‚ T¥?ÉцQâVy¡iZ¼Ø]×¹~ˆlŒj:îó2/Š1¥¾$Ž3㥄º²n¢úbòŽgÌ yä?ô? 9CqúÑ¥À.­ÔItþ5,N˜ÆfN1*J`3›9‹aQd*ƒ,ò«ÜæˆÍ) ±ÌrˆÁ,c´ëu5ΓSŽ.¬pŸÃ˜.„}wŒ9U1ôE_~™›Ü¦´’]ÜîÜÝœ¶p¤áOË&ÎP²Ëhu¤žgÈH.ç„ã`Ýίøiß¿ª¤37NQ­ `ÊB;M !l+zoJXÕ¦<Ф'mì¼ãø+ºŽd¢•ªC(ù‰–Û〞v4rõˆí÷>¢t#ŽkªÓ ÷_ƒNœVçK¡Ç<°Ó’;¦õÒ£ƒc™=W¼‹Ï†°Ÿ†TF¦5Qàt}ºA¶è¼á¶EUrç\–§q7ÄÍ0=•4ÜTÍ!NèÖaÈÚ‰vÉ™W]®”wã»bï’ï(Èr~H þÁ ú¥ƒßñÁœƒ™®Ã¼‰ÀªfÛ'Qĵ¨«*^¤ÛT÷°ãV¡ÀÊóPZDr?฀MR¹€ÝùCÑK¨[eÞsµÛ©Šw˜Ì¥ é-ó¼ßºcD”Ä׸øÒ]"Ê5(¶b—çå•ÊÍys.SõD±:1 ä)ÜZ7Ž |lY0¯Ngö¦üSÊ>ۣƚØ?†D—Øçz$Ù†D<3D¦­ÇªñbÄäðGÎ!6â<ÄkÔ~YJ;ù*?EûÅëÑ®ëO[#ì4.¯J©"ˆàåî1À3ì‚_úð6÷]_ÿªz[ÐVò`òa4Dºë,RÊ÷8RM2„µßaFEÁµ§<ªª.d­ÈUxÕT ÖL(l7ªë)BÎÓRßiÇKãÃ9EÒªô– ùÑSG(´ÝõÜê'±õ÷e¸ï [Å<{?ÅN­U¦!©PcºúÝí¶¢vZ«JУҘ†!ïÀ×kNNwµ*%¦U<±Hà¸,úQ3ùz9êU>¹ÖG“LA)¸o0OEi V(ßÀñAí42G×eXY·¨/÷Ä/^(ΆºÎ¾,bCx°É¯^1Žœ7–ÝÝ[CIu(‰Æñ$´ð'•UŠuüe•ôK“,!tKR*§ÜãêJe‡GáBº×9+«FD=6 P²óõNÅNÌ£ßËúÀè+$Pãuð” Uº ášFjUÑbú#ái4蕼Å:ØÀ#ì¶²i8Éx÷§Tþ²è}–ºžä¡•ꎵ×÷¨.º¼’åð÷{€f:ØÎ?i·{0ßU3þj-Rc>xvÊ RZ§¨Å³u+±ÃîÍõ–ýúL¹4½à/Äoh.§DX»Ì_YENѬ@Ë Î(lDS9¬f”È<ÅSt•Y"~æq¥phMºË©' Ôw…«%¸É9ÝmùNÑTU]å Cd¨NU¥S§y‡?\nñt©±ŠöªƒªÝÓÇWS°¨YŃ<$na"¿y‚ZBÒŸ]Ä/}†±dŸ\Rú¼£Cº_™üß9¨yIGtÁc`Ç‹’ÄÒƒGÚÇ ’e[öû࡟®êÃU"ïûh°Ó¹ç‹“µJ¿œÀÛª£ê€§TB&bàúçRÁ›&j,óV¥£ÒkòIŽ„/C¶APPQFUî¤RçýÌ+LàV™Dk˜Ð–¹‹¸Èý%ÿÐc ° ¤*~Ë!~ƒ`aö) Ͳ:*žÝt&K»k+vâ {æ^eápÍñ²hÿ–Ï£3”g[g ª“€ä¢ò ñFL²W²˜ï¼ y2W7o¢Û1™djR›×Z#¶-n²ôЏù,eràŒÄPÄáÐö^µ¼Q‚¾L ° ÆÿÓ¹·Ñ3Ëøƒà®]¹òøãMjq'ýêñ?}sözwåÛ,ç[·Zãg=å.ìyç³ÇÚ·äñßÏ*”"ZïiÁlÇmæÄY"ÑÝWÕLòëÔ…iëj•ËzTºîi¢Ë®;F1x‘DY~¶¡31ÎÊÏb¯å댙SÌ=#A™ì+ÎГ\–RP·d{¬©ë·’v/^!PùÄß—â¾WNžŽªh^1ãzxxOºæ çÁÁÈ–›*·9žLÍfŸzß½8–`¤_î Ëô;[¸ ˆrÑ:éþÁ á{ ãsFö|m‰õ;fWìy;šØMmiL 2ø`+6 ‡ÒÛ‡+"¹ …—ܳ‰ŽòaŸdï÷AˆÚ£7y}º±W3ÕÒk¿SÅy¿ï_Žtd„IòïÝ CM m‚KIö©Ìÿ4Toüã‹nBÏ¥xPa…dVte÷¼EŠDô?a,Äo•í@Qí7§¥­sœhSÒoWKÃ÷®³,í{~éW“î& Ô¯Õêêc›ó–xäœú,±·œP© 3 ÍÜ›øÐÑìî½¶FøMÕ†`“\¬µÄ6Ôµ«Ž¿RQ;ëzIëm¯¬b}ú fÿ`Óñý+¢jzÄØ9Á;èsÜ–Q"u7Ÿƒ/|i8é½#F^è÷«Áú ÉàTuYqæ‡Áï«–Oô`.žî‡…"¦»ÚtW@p±]J5~÷³=²›Žk5Ã~-Y]òit ›¢€ßSo0ÿÇ¢a¥ýnL ݺ‹þ™¼1[vF2ó`œFµ'ŸÊ÷D­‡ð©(ü'ÜÂ%Å![ÅZEþ‚2=úýüß§†–IÙKì|Aÿç43Cc{'O:ÏX¥A¹-„Þ/j±IP‘KªB*U¸0CVŽTÜQlïëš÷önk™÷Ï8\pÒPßF±§×Ùì·2è5%[–ùU4:°3þý¸q-¬L-+ RÖf¨ò+Ã)c0d Ñ ¬k¬Aeš2ý­‹A`+a‡p%`.ÛjçÏd ¶¦ [ èRÁ¢}0¥<â}é˜) t„i¡“œHŒÂC·KI"Êã)&!æ!êª%_†-ˆºþÖV«õ;uÕ“&±j×Õùvåüϵd‚,âÁÙdïÄbÆ‚ùIÖÚ®Mår3j¥Ìf¨°4Ë’¼ÔÖ])çiÑÕàÍæ(K’´ìêªp³›•Ź/€¦hÈsÄwE𺞉x¯‘Æû³öæ/Nš†ç NþH´¡Œ/g4W`½{ˆþoƒm`’j ˜> Ý+òÄž6ÄäÍfˆªö ˜¡Œrž²^µ&Âäù ÐZ`(×AYÜ2BOÈÆãžý¶N°'?ÊvGúíM³ý·±ºlæžjýaĉc‡[¸9ˆ‹-´=éXNôèX´ÕÒchÊí³×0 ÀJÚ˜ ¼¹´@wö‹›¨pIŒDpû—Ù—Mƒék²Œ&¦vî8„X×c»<;Iù¸.в”ã)î}ØTõÅw½%"ïw¯zú‡Ô¶¯wQŽ?ˆ;,¦é· ‰¼´+Û¯’NŒN”šÅ@R8Ÿ×Öy-yñ¿Op]ì¨8kߣâÑýÇï/ÿÄ2Ó<|¯è—wNfyC{|ŒØ‚e°E̾ØU iZ¾Èããαձç¼ýÅüf_œ³¸ZP<ÁoÐÿ†ø‘!¯'€Àÿsˆÿ¿C׸UI6~™wÜvÙ®ÝbT€„Ž= Z‹IÈÇE½FCkRS`Š´¤Q»hsrÊÑÝ1X<òo> 1¹:=g&úÌóú¡ò‡qóã÷æ¶fsQÐ;HÍ$޲b$¼ŸÏ(±{à!XÛ_$q³pȤ¤Ë˜ÙäóE5±³íU?§Ñâ¤ÕkÛÊÃÕ²2z:2 0´Ù:õªW22ÏV¯D4¨š]!ŽU!žh2ÿØÌÜèC}£Ý¦fþ“6k¼Mgê:Õ÷fd÷“À]pà{P‚¦”³ oƒ‰°¢8/ˆ1ç”Â[äu5ÙØøŽ¤ST÷'¤zG×_ElôIÚgšõ/ÖSOS—L”J$¹£Õ?—ÔôÐåŽR´Ö:º†þèàKÒ¡ž9Rø36+š§¢¾GˆQ'̲|+Zœl:QC¡<(U´B»Ö!Ùj‡D³²•ô£'igàym¨d]ù]v£ÝÈÔÀ[¤=í-Œ ­[‚ï4›œT¨äØ-­Œ)›µ ÛðåÈhQ-é#Ã_±š_±ÆA±¶Å®ó?¾Ù/‰Ôž´Ã‡×ÎöI½')ŠÐÿNvGPRå8ÿ’“©©‘³Éÿ2ÜjÚ:o±!ýÖé7–òKtI´”°tÆÝnsŽºiÉ#wsκ.Á€F¢JÞ(4e]ø÷ë”Hôj>èr&3ŠÒ¼žl˜£KŸÂì†÷¡HɰÆüN„»&ñYŒ!ïpXç –J¬1å̲ÂxýÜQ¦ • 1áF@ôAûæQà ¤èå9c”Îä?•÷;T*à€ÙÁk‘ìKË}áWÒ2El`!*㨆@•ŽòòüC ÛæˆJ{ÇQ+ÌÃfîDBWýΪF@žUÈ*ô‰ê/Så4`cê:Õ*~zúguÂtTC}|ÍÓtù½3{òróÞ —M‹ŸA šôGMÆ¥ ,½ˆŸ/»0Q+ aÀ<é( ýèN*ŒKÖ ‡`FMÆi)TjÊ1é[3ÄÓIàÐ’³ˆa7׃k?†»òðpz0Þ ïèâÏßåÛщ7¼Ã«³»£·Ã³½½sXßÎÐõƒê×ÕÙ{ ^±ã/Cú/B ÑÌxÆ ^6ÄU¢t¾¼0Þ‘¤9f†cf4it 0eŠC%U&ô´‚êX‹½b§P'x·;T¶Öš‡Õã'Ø}@'Jˆ*¸wÓ+ ù,++„꓉Ȉù°$Š4BäH̘]ˆ[6°ËK˜wÁ D÷yk¡íè¿$Æz¦¨rMãÏÓø}2åøÂÞ˜JÔGb)'ŠXÝc>u¢d¾^¨µ#Miâg x̲{¦4‘š}_9MÔ+¿ ñ"ÑŠå;‚y—Hu/¦þ5¢å•Ç[…%î(£^%_m~BÁt]Vät„H:â'L©“²=ñîD{(ñ>S U¦Ø%ÕV%qaµâ‘<KS¦hTéÃw`o¡9úϧ¾øI*·bÂøpéM¡SÁ¶Nˆ™óëÁÒ€J£?Ù!ØíëUFc i’ü%„R;Ž2™‹¥r©ìú…ëd5¶únoþ ¤}#8!ÜÇ3NMuï«ã~xžüN zAe VÍ×cÇ„¤7p2Éö“TeHûæ¸ ÆeSäÁúLþÐnlVÖÏ´xk¿Èe¦Yosüå–ø€ ÆòŸÊ~KÌoDöÅláŸRÝÔÀªVƆ=n)ñºÊ8wÖwÚJžzÍ- ý˜%óñÁ¡¹1­Ë±h‚”Ú¨=T°ï"òßßKUohZ~8 ÏSp—VÇ3iü?X¶–ìª(öÄæ©gÛíÒP¸÷§Ðp"t{L´£‡Æš¨·$Ž‘ ™2˜U6§³›Øh` \ýíkDVk€~¯ É÷¨r;ð­I¼&' Ï÷ªÉÁOi„@§Å‰òû9ãêኅ–9Ém„@ÇŠ!çUÇ>æy?P%yeƒú€:öH©ç‘xsÍA{[3VXpl =3OewP*¦§ÆÇ*™ÄpÔs,¯Ý Þ:µ×Ÿel §¶EDÝžÞ-:+Q¥yR™óî{¸¬¼ßÁÞ%L“™äÑ¥›uƒ{{Ø>ÚÈjèÝÕs>i½…÷¨ó;{‹*½Š¬Ð•Sñy6—$ÇW]‰7×ÿÐŒµ¸ú"¿$úzl7÷'ªÖ)a7öÛÍ4 -díÅ*¬¢6O¯L#ä’Ú¦.ÙÑŠŠÉEhÒÞ?Ñ@ÇÈ•åzu.sWCèË”+âˆKG‹]Ãý¤Œíˆû™ÿáA=K@¤·áãºä+!Ñ·!+ËÆs¬÷ ä¯$ärÄ—ÀtÅĵš‘¸ÀˆÄ8’úòÊk£îþ#|=óˆºs­7ç/QeEým½ZO„6‹¨ð³xéÀü¢ö<œnýÚÚržþ$‚"lptH ïTˆÀšÅx}dq|š F~W¼Øô Ó¾Ï9àAì™6tá½ýý2Œ …V¯/ÄÆ½ŠÐ÷¾w›ì%oQ轫Œ=¦$-Ïç™[ëpvÉr¬ƒE2:‘·?çÏ‘éÝ?0}Ž…´8X\ØmXE“,§‰‚³‰ð9.=“4øå/ø ðAÅøû?D2Së¯+ÀÚÿ‘üÿ‘<Æ­úÎ[cˆýÄïâERi"Òq¹ïIe¹)mvE]y«És%&Ý’œ)A±ÜSJB}+¾¶ø\¾àý‚星+á~Q3$ô7Éú›;¼%&\Ñ”@C77·ÿ¹nì¯øÿ=öÿ~\žáIó£éôHÿ€ÚñùpBcZËçq^j¬ÙRG*ìY£3QÓ5¼ ‰ée¶N‹L6:D¾P×è⌣AW×èß`Å<ɬj„3÷ü¬ùm鉑~Åa }Ãó½aáཀvl($»ýëÎ&q4*[“ý™ŠýV û$-èÏ—ðôþ›Ë¹¤qrØÓèf‡TÜ·pçw PDÊg_˜Ûu¼@åáMÉèÝ£(ëä5ÌÿDÒ;§"³à$MZ9†-‡'Æ /B&'bmÒ”3=áKËøØT uK®•–¸Ž»k«ƒˆÜ±F^F¢ÔépmÌ©ƒ;”ˆV—Ä_ÃKË‚¢|¾ÀbÖ’}X´õ¶sVÆ„Øh£o7TýGT¹Sö¾ô5O˺ÞËçkw#†bê—êM‘]²>e'g«sˆE%†ŒA®ÿx!Úâá†íìvXÀ­Ë•‘Ðãîçëlrë(cI ÈôF“¶-Dtüô¯qOo†ŠGä9úúBȤ1{tO^ÌCCƒ Ô-åÒ6‚£¾ˆþ-$×0 *˜´§xÒ‡Å1!nH`ÙºkЄýq³Äg+Ø.<‘@J‰^H¸°ÆÐ ©žNÁ Ôy°ˆò爳‚.z•âÅctÏ\µÃ¿Îð:­_†­‘]7€áŽPÊSe¿X àý/ï9"‡u£1lŠü ›ŽøD9>rDªâoLÄ}‰È# Øèm)v;bª½ e¿QóÅ@W©NéåÖ¨ùZWW31K=•žf<ßX´œ(‘;¢§ô"ÝÃc¤lT„ïÑ&³@Ø€b)šôóIä ÂŒz&Å¢MÕ’˜5¿ï´¿÷Áwäç&‰•lã­éfA­#]¨§êhrÔà°÷.–:fÄ S^œi߉Å_¬mŒzFŠÉdi–‡w,Fd—Ç«¡Ov¥†¤^Œ¥ócÂ?éÅã ™'õ‰VÔ᛫åaŸ®&ëêLÉ8ªÇ.¡°It7ÿl€ ³œb­mTŒ#“¥KÙ™- n¯°®è˜9IOûA¼8ÞèË3Am0jtÉôÕÖ!^ €(ïÑ;Jï´*7ÓË•â)©™¯ô #§pþ’É/ÿ¥§sçÏ4Kç%†† ªÍ$ú¾×±îìª+³ß—š×°ÖÖf1\S¼(¿ÿEñD òö!¬æQÑ~ùômcꃛ@Ƀ:ÉhdWŘ‚À· ×Ö%Rk7Þ—¦åñ…sЊKÓ¸]2mó´ß&Ê*Ѐ«¹½iysï1þg•GU>ô¡+€zyÚZTU3ëÎ_Ç\¼…%î•P^[ÑÈU‰ (cÁ¹ý»:3bGïÌîb€LáŠÂ¬¬ Õí}MAU¦U¬QFQ:½CÉhÄ­'¢ž‡"À¾.Á&[z‘—¥&õ ÷»žÞdž¨ÐšG‰ •EÒoÛßĵ¡º«8› Q„À¾ D»±x±ü̱4ÐP àLŠ'ˆmÃ#hþÄ|M§“xïÐÄ»LâÈ«MV4:ê ‡Â6?Žm|ʰ|É÷—ß& °$hÈ6Çý”pA¹%âÚÕU­½£ÀN–5ކ…UÚûRq)v ^(æ!–Ü[€ ÿbPq#E†@m4˜·uœ L•3g†h;ÔË‘Äö¸Y*C_H<¾Ap9╉ ÃmÂ$–X¦E,þiùçÍ×R›ÆFq×]˜¢‰îE¹eR<Ô7Qñ(€O2¦Õ”gÂh1f×Ê¿à8ŒízáüNV‡ \QÍ‘82¾o!d j3T5,}ï‘Lÿ’Gб<ãéûÚFmìÊ®i·¯å_¤æ˜¨Þ1z¥–‹þÀ¯?Wû({nœæ¹¸Øe©RE?¬zÌZT¯q¢Ê…ÛᄦºIÖ³dš¡yäŠQïcËUHô£ÊùXÐG¤ª÷’ƒö“ÚþÕÂ°ÅØ 7i‰Uê,ÿœ2ÞÖ#M_B¼ÜrÚ°–y“V@Ù2wV, ˜ãp3ïWð‰o ß7¥’¦å`lÿŠÊö¼>ŒB† 1FVÙò(¨^BÒ\Q¥ä9²ÞIºÒžIeΑ¥F¼£±¿œfïØ%t ;ÀÕã+žº©@§45XÈ|£'w¼—™*ÝL¦nþoCð&‡HX7OZ—SXöwð…ŠUŠyå:¶ºšÅTÑZ}à^ÙÆ/÷£ÁMM—/ür5¿¶ŽO·7þ€±}ÿ —åeWa’¶X>܈š¡Wε_1¼oÀR9?]é´cyÈjͱ' 0(ç@åî à0w_Á¦‚b vH²U,hØÍîú:ƒ]¡Äxö<Ô£[!¹4 h)æ´V+Îr1FªžÕáôŸ¶ á0‚Èe|´äÃt÷—vþîøÞÈ­¼]­×ê˜vV…í —t½IAâ.Øî%ŸËõðÑm@ ÆšcÀkëw‹‘×d6Ì®^¨˜¦1mp¹¢ŒŽ¯¼.ÅÍ+™Ý©É‰|)8Dš8+W‡ªÊ.°h<)0#Õ»Ð&}ñ3µGñ+p°éÁpYHS0ÞWodšˆ¥ÅêÀ¶çY”FóèZâq¶¦¾XK1XH]l(L©ÍµÁNËè¾L¾7øéPKã¨Ù’'‘ï^Á?èX©å ñ•—ã|ÇRm,Ó÷ê§V\©om…õú{Ô€Xª½#¿P}ƒ ¥Aý@ôCYª¯t§S~QŸTÅeGJ\?ŒÃaþWœp¿õ…ŒT¨ÝO`xWŠ×Ê—÷{7îèAèoGPæ{(*1îhéÑí¢V½ïÜn ²eÎRYúÛà%³ÍSJË¡ÌrÿwH7•'™-aí‚}ì=rŽ‹ó€óéùÓq’¶Fã‡2#ÉË(«öó,ÖsaõÓqÜ0­± é@ïAì‘H—e’…Ú*; ƵŰTÇQ/þÿMÓ$l¼O@ˆ‘ÿç4ÍÞÁÔîÿMÓ4lí·Ø~óô•Öíl?L§¹L¦Ø#µ=1¯MDaÁhÙ5W$ªâ¶^úïâ’^×+“€‚$Žwn;Ïn3p†JïSV˜þ2(àЉ¤n ù99x8B”1ëS2faŸ¹vð¥Ë•F•‰MZ(_¡Ä|\Áƒœ†Q¿kî¯Ô1Ÿ,ª|Õ«6èWCï=J:á%‹S2ÏÇg²Ÿ^ä÷¥Š}Ï;q+’’m9có†¤È ¨#JEœYY@ˆŽîxñ°=œÃDñÂKœ½eÌFD‘¬0¤˜Sâ¨)¦¥ Ìíd|¶íèÓ¥Ó5Í)óÜ2 ._G­|v…Âk§lQœjü‘ V. 4b~=l¢ôÂl •³)ã¼Å’ú¥Ic"Ìd Òe[¡mS2ÈÊ€V7¹c:i!r~1ÂŒÁõÞÆ‡‘m{°w{zûzÝ+ü]™yð‡_ äêëçÂØ‡¯¨¿±þ.y¿Ùz¸¬Ä+î1üð©ñsQ@ˆß ›ÏPÁ2¹JŒo#<0ÔY¤×c†eüö©†N­YH•‹½ù´ G…0¤ñ(ÖŠØ¥ —₩fhaý<$7bu°âé ®ÜôŠH>ÉRÅ@à"ûcίŒ.Ñ™œ„9³ ]Ë}¿‚<ŒD*t{7?}éEyÏ ;Vgøû .')}€Óðñ¬°>¬Ê¢“k"”¿u¶Í ©9Ì[R{"ü‹¢ P·bVÙn<­+k©%T•ð’:Š|´ŒÔLa„ ®F ÍlnM….Ã¥¡äÓ–]i“^ЂܚaÃÔRÛu:6¿ŽqV¯Iïžê>¾H‹ ?x|™“µý>ïÔ±Ò½´ç¥¾%„yµHñpŠVš[ä, ’‚U2(Ã|ÙÈ—‰@ö‘áHõ:ú$ç²‘Ïæxõ-ÂLÄLõº…NbeçbÒá:ÁnÔñ rª´‰ô{½ÏêU™.ævI²Ÿæš^¸—Ù"f™Ek«‡š»öpeKZ¤ ¹øµ^^¯c>gîà?6 yÁj©u™Ó~‰X¶úŠ?éÒn5º„­Ó*øÑz3ÀëÂz`lj!ô‰F‰¨[¬È·ê‚uáÞ»aš…î­ápŒ;U`’_86’6¤Îœ™m3 ±WoÊ—šê² ûؘsLùxàÓš"9œÛ'~rC¢|Ñ«¢–Kуí}{ë×±ngI©›ÛÖt_†•}†KPÞ+2‡ª™à‰Á®š nBà3»\Ÿ»A‡ŒaÎ Ô[®¸(¥Ďgü3Õ€AZ)KÌÎI¾ÖݺH££ü%pz”u]®¡Lµ›1Ϲ{OÎsvUqö‰#¹Íàe’!h?×¾ÇÎ5BR0ç@n  Ü]»kŒ]V²(zšð6“âH'7 õÄ×YP2Çr©è©Ãö‚0’!…”H†ô€Ïã;;ˆ¤ú£•¢Ù¸#¿¢ÅØêØ„éŸØ`š-2ÇÌÞe(åyZu¶°lì‘758r%®æ¿ëðD(ì¤Ë±Y±‚œÄ y´K²‡±y¤Y“îbCià”]˜ÀE…ÆNIHpŠÛé©ç½D²²Œ¤ÖTuIźèä»ËèÚ»Š ÃÚMÍ’_ aE‰½/°Wøµvègq?âT¹ ØÐ£1Z#ýªibf¹F¦TÜB0÷ԙؙTìF LDS¤hç}„gæ³æMÿ@3 hA“åñ­Xåù€hqG|üøUÙ[;LÖ ¹/̦õ8‹ämÒ0DY‡ÈÉ›j¥CóÙ ‰Á’¢…µ¨}3¡üÅä‰ 4`& ŠäYS–HÖ¤U¬å¢#”rê€ÆÓO™Ö±‹{ÖZÑe ˜ ùßóKÁV *2‘.œ¨Ö´§pf¢E @›Ä®kŽ_á(Ã(ãY• ÕMDrƒ¿œ:Æó3­&„*OÙ5&v-z˜Áî"q&ÛöŒëa8­FHÜíi3x•²¹˜|mcæjPfu¸¶wØÄÌ xÝΡ}ƨõZó "0$Ǩçdó¸G®Î¨½~A6vÎV ÀhÓñSÎçu°ÔtlñßÌß Jy»‹ç € à›tưŸ—u”C²d‘ÄÀáZòQ]ðÈ–gÃû2Pò«ªC¨ VRù1VXeèáG'›*øØëân÷ñË„pðÍ-]†½e½la?Œ½¾Ã#Ýí÷é³ ÚQ]’øCü¦º¹c—è©,C|ÛÂla[õD ÁƒLM‡ Ñ»1MªKØÛW”°À Ø_b3YÈT½f“Âh§]µnåÈÉw„ž‘~p"E½¨0';B…Ôz-´Ýx{\J"1«™Ã Wײ½0?°"#¿r—Ü÷™sMHt³ð£Â{]2  NëÕÊ…‡…Ø6Ý=±²z¿™yŽ.>* eܵM|¤y£>*¦ßâvNÑ©nYú䯨¼Í¼ 6Ï„ûƒ· ”Ã>¹ ;Ïò!º¾¹u|…€ n‘6G´w·by9órÕ ×o@ &o‚?“¼Ã3¡>¯Ýÿ.¥`6'P•rzn/۾ôÈ1b9™zÝØîëRc˜Hw2[Úõ_ž³6¤e”Ì6¶ÐÄNS±sר|‚¶µK®n_ƒVIÓØîæ6¡ Íœ™Lh÷`tÇØmd&¥ý‡s4jf‘ñdšfákÕj’BÚ{ëÉ÷Ôj¨ÙÛ§)Ö×®HÝV]yÛ·Š=ÏïGg3úýŸ ]:A‡Ð8fí¨Pc«CµÒ+~¾´ÝÅUk2ÅÕi< _Í€Á|f5Ù4'˜XiLJx&µ?9ûçnÃé,©²§£Š±zÖÙ i˾Ï~&²ô 3Ô¶Ó…và•‡¯‡vG3o.˜T/ îÞ}3†þ~v-Âà™)«ÿ©ô2¶ó§]!8ÀêÿžrsüÇIkþ‡ýy cÞô!ãÏR ¥÷àïÙå(™ë'Ù Ç@2´%!:®n°{v¦iÅâŠØÏ Í ãyf»Ï@»QêTEŠÐi~)™C\SdíÚöàt`tû’(V4¨RÐgef}ÍãuɴɦªäR¬JÌö37’bLÑDI熈ۑaà$ $sR†dƒý2™ÚóQ/ó~d†´°rùÉkúiشШtÿÊ ›ùìq²Äùž(0H«•ÁÙ5”Ð%ä‚èâ"£|#ÝõøÄÃåŒÿ¤G±Uis2%Ń Ë…ËÁ§â3 |öYqèñè?©khheeU«T†³£’¦ÿe&¹-Uµ­5µ™×}3Ù˜ÿšQ0w~Öƒ©æó0xñS1µ•Ð0¹©&ôPÑ+å+W§Ÿ’“ ŽZ>ÈoÊÖ[^ÍŸ±†FI@КÑç€ÞßMm8³ b¼-uíÙòîüœÜÜœ¼ì<ßÀØÚ\ž~Î<Ûz:Åì‘Ú{ü€óöø2Ų|WÌóaRGs¤Ï°ß¡ÇSÚÅS¸S+"é9*4‚†…|¶O8D'AJNÓZ‡Ä㔢õhá±ã­º^èß?I’ K1²GÉê’l–6ƒš€U¨r¨'8•çfD?Ê+ÑSG_ú+`CÄjO+E^š}ëíÐÕŒK ³õ…×ü~iD›N4UŸZ÷z‡±¨2’x~®yu„ÍåE–"¹OÅPºLïA<½«Z•|müÐQ[/#?  bÚ"»`“ MÀÆ`ª>ù¹ÑY²vgÈ×| Ïƒ¦¸öÆÅÌ-YqúLGBüw‹õ"£0Äg­®×L@I¶ njeäw`̈¥Xà=É™À‰¡`©ÏZÄÈ"ò\cv&†U”mðbÅ.™`4ï^ØqJ&­%ð§Uép¶Ù‹_¯õka+–/©•Œ*ÂBîLqm{ö«‚Ný9}èAïþåY Á âd:4¥qxjŒcÍ“Û ã@è±GÝHñ´d‘¯—ïçãºû†É2Ô/ú ù*Ü7V,t Øà©X®×„Õ6À‰ *kÎIE’«.‰ì\Ùö‹-D0J³×ëjÂ&²„qÖáÏ>nVdð ¤ýÞzµÛpˆZ—j¢ëÔŠ”þ„K>H±S6ª‡å u,õð1\:·.(&@°”’»ÔãR’@ §µò¼\âø érì}‚ kâe]Â]Nµ›’²O&öõå”ìºYËaçP†­ûÃÈò´ž왥0’ý×ó·æÈ¾¤@…°(v[v }E¥KVðç^†ÓËsBBš$ŽºÕ —¯óç| Þm|Œz yU%Ûnùð»W5¦ãÕ¾\Š\÷vdKÉDg_^ÀVsú”ãþìí]6­Ö«iâÍ7o aÛ–¥FW¨PÞrˆ0–ÜPd½b8(èÓÞK-ç_6^@s$Üò¹Ž™å¼áí5~Ú¡ì7yÔpØÀ.ŒfSØõÌ÷£ÒüÈ^ÜðäÌd¯~ÁrÏ€eò5ÆÓ–5ö?²vDµ€rØ5—›U¬×Äë.lñhÜÓ€Ó¹ñ³Ö{xc<äM[ó眮ÄOV]Ä–úÃŒ¬L§€MgŸ?îkóõ=/¾¹óåž¼W E{$ˆ§ÁÄ´ºÌ°*’›(åÌIaЦ©q·¶§eT¡z^ªäxŸ^§³ìj_Zx ÿ6ǸǦþYüíæWžØ„ÏæÝ«¼}ºÃ˜¹à9ÿe`­…n¡<õÌΕ zëb Ŷøh?þâ4¢Ø÷5%‚úpùë…*ð!»BCÌi…œíÖ¼#DŸ뜎&gÃY™®2Ú–(?)BP·8 ÚNEË£øÁEìÇú–„kXÜ׿0a”±ÏLxeÓhºª¨ ÿ4HÑü”óMäôèæÓk V&ýÈ®uãaÖÞ_ve \A¼­BÊðèŸßä‘ð¢R[%W8K)¿aâŸÙ_0}Þº-îî cI?³ì™íš}¾K]–é~ÁÑLûõZ™8ë“.’µ?K”@ ~ûçr’(Ü\ (ìfÕwÎûùý캒[ÊôÆ:•LR;‰/}ÛŸ» ^H”ìŒoŒÄI:¹ïwvl]ûÚûÞø’Oû©‰¦ÜªöÂk«!ËdúF"=œ¨²?Öï¡ë¥þÜmº‰Ò ´˜ØÎ+ähÑŒüÏÅ#h O§+ÿíþ÷dìÿ7Úµ M9b5˜¡@"("‰Lª>B;ã'*ÅNžW+h»†ÈÒ¨`´Bk€ïE鸛²Òì#Æã³”m?‚g?¢ñ~$Ëwz¹¤%g¹y~þdê¾íÅlöýz|½ÿi{óŽŠýÁ¦!øúçú¸á£Ò»äŸùÏi£§ék‡êvô×ÒƒŽVÍkÑø¯©éÖÔ2ÔÃõp i‡cÑÛÖYt¤áë†Y@{Òl§ ´ê“ãW÷çÄö^ë’~Ý (3ÂÿÞ´€öf2ZàIq4mIµ¿+–e`ŽËò|´(V4ŠeÅòÂ&Ò§Nj‹½£1ep?àõ»_P>H!½’Ò"åÃ,Ãíe.DIñ¥!½Ù<Þð'7 (ð«Ü“édžrлŽsd5Hy“Â{ÅÛálVæ%5!,ºŒ_·I›Ì!,Y—ËLüöÍùÈ?5¼H³&M’ÕkŒ”ìÅ%Ž?ÛüŽkޱjLDýb§™i ‡í˜ËÅ&^Û¿Ý€ÿÀûn¦)°½Ó*% ˆ“–b9f’ê¤|ÁGî û|jgÌ^“m´…;„ðt\\‡VÈOâ½N“­2 Øò·MƒbC?fºP¯ÁQ߇åÚãýC[+æ6%:…3Œä0~ϵÝ÷D˜byqí{Òý66 wOêU¦ $LŒ $O-(;FHởxŽ&•ü72g¸a‘´Z*‰ ÎËôo¤4‚+…ä'ù‡™©i}³ÔÝõ§51ÀÓ 7¡`8ÅÞùó¡xIþ—S.'15ÊäýóР¶‘ž$¨¾¹un´×©¤MxG´"D®$\14RÑ5@pÌHœ IE¾ ™sðœ\ï Áa;¶"i¿w0‹Ä£•¾–ƒ0·#Ú¼½"˜*¨­DÐå´${§扱¼m¹J8&ÞͪæKLKß’^·Èª a•çê…±BüÃ=¢ã‘‚¢U2’¦F#ÌXzP‚.ïDƒÎ’¾®aæHjµü3X-ýëÍ5Åjß+A«wÄ3—~Wtm–þôhÆ[Áv¨»ÓùÝdWž¡*M—ìÀÛtTM#«ô‰S‹d½,˜M!xh®?Ý]ïLª¼ªI,fÏ_òžÇÌ;)" ›Ê)¨|#TãÃÇkËÃ16é7J(aÝ  Ý1Ñ õ)¦Û®Ü¯>= a×â2ï»…~Bëó²›é?_v ü2'ò×”ƒE½BFyYžùØ øøéû¡ã\þžÑv Fhɺú»Qz+VÞ%Ì9%”Ë:B²”dÃK9զѺ“’+×åIò,`vÔç¦tÜžU¼fû\­ØšÅÝÊ™Ç*Õ@ren9¡ ìZƉxàñ9Ú`ã.8cQ+ºÓŇzè} ¼epI'ŠCló}9Ü=IJ ZbN^€'G¬ë4•×/&¹7µHŽWŠ‘ZI8ù*“n×} _ OÉ¢²ø ?s'€êÝ5GM— íмëmïÓßLíA5[Çrñ‹„v•ÚaÏqÀZm0 ?ÏmË¢oÊkØŒÀ°”ѹÈÐ’™ùâÕ|àfÇ%Ø}iÔ@O'ÇÈé•5Ån¿ËK À²K£0fU·Ó‹“–'Ç»ì%Ú;‘@hàW>ãÎë2îã”5=òaƒµGÍ&ïÓ:ä…oN+&N¿âÿeZ˜*´ü'ô¡à+…Ÿˆ£Ln”&?ç‰/jch-§3ó³‘Qœ®;|å_C0Œ¥PýÅ“½Ž0W®Ý!;{KÔÙµH ·Á8Ëmײ,6±¯_^ÏÛ|qi°ÙM<í£ð`’äÆñ*>¿KAû¦üÔ\ßÑ}öUnÉeâ—J9$Ñ’FºX†ý¤5Â,Êûh1xÕ^^&º&b^ݺüÆ``óP[_I ÙJœñRÃø €ÿkþÈþ¯=vrq2ù!ü„‹áå]®ÿ±²þÓÈÿׇôDÿÿMûÚ›ÙUþ'ãÿ8vÆ?üßàˆûßønfðÿRøÿ ¯ÿµØýÇJøï/ü¯#ù¿_ÍÿÆ4ÈÇ£ð_©+ß#eã+a&[3–bWø¯Wf€3×à@\þ÷Aø?¾ÙY[›9ëYÛ™9ýÇß ;¬Äß[=qveËâ”›|žþ€ÈÊVGÕ¹T‰ ‚Œ¨Z’‡FM[‰Z’q¿;½×É@¼Nnr¾XsgG?ÎømNŽ\]Ù»rvf÷74s¼ñzeMS¼û”꡼e@7àD ¢çM %(ÀªÛèÄJ˜(ÐzœÐ«ÉÕÿP ­ÃŠ•­3†õ%éé€h¶Kð³²›ßNGm·oVD‡öëêìY;dÄG—¹<@Åô±Ñ"… #m*¾fK¾ˆß>ÄZ™ÃûK¿Agíõ€†£ãápûͦþ½§U‹ãCþòiBöúÄ«³0èÕU°_,½[`ÇCý¦Ž0$!§Fç([5ðßxrÄÕ»d …%×1†tX´˜ùÇR+(W"hÛ”m[Ñé4Q–Ú¾ç#­;¥‡Nöƒ®hoïnÏE/âÍÑÕ–Þ‘ýî¼ÿ^æãÒ2* Ë.Ø8Û‹R¶Ö·ªå¶—NºóÂa+}œž„Ž>´É<8nxOÑCÙfA ÀsýL$f帽 øñeÐá£ýd=ðÙýLf]ÀìEûeLßþl¼›.’¡Ý:}›ŠÄÆÆv.vµ*Üà/î¥#¸z‰ëÑ#Ñ+Fi §ü«ÇLŽ…D?w˜vcv¤ŒÄFîÝ.NŠ0Tž6n܉$NMM¶®05 ut*t!À¢Ç%Pºg¡Puü”ó2ŽTØ×À.@ÊÄ×-‡àJНaJËÍqZj¢ÖGÅcbŠ86b¾2O'F‘ëg^$bªGQ& \º—î(¨€$ÛtÊ·,ÑzhÑ``Ù>.¼P“gT„fvPéÆoZvD$ù3½Ðân4lEX?@MØ %ýHœØ¤ŽøzÍ¡ý¬R‹c F¹‰õ)¶+ÎHð:p¿#³´|làÂ`ñûe%äû×j©Ÿ-z 3 ÐZ°æ ¨¾@8Ž™sXÑm[á«Æt!~µ1ÉB~®mmþŽüª DV E!_ŠÉïHŸ'×ËíiŒ=&Óßé(?'ö]›Æ(WÔŒòŸ¾>îW'yŽ!<Þ¼««aoè3úûÏ)£1à`àšî:š¡ÜÆ:¿wÃöß÷Kt}Ö3Úl3ª]Ðîô´†Í™É@œ”uf¼z îÊ+‚þ%…'êWq:GlÔÁtk n"1‡½A€új]ÂK+1;`ñê]…Œ6hšþUl~›UÁ“›°¶’;Ù²¢¿= 80¯4áNV" Ì'üDsÈä%÷@¹ž>ÓA9,Ú”´«E¼ï¯ªcïŠœŠ¤fƒd”ï®áLº`ë­{ñ>‡²ÔŠ)ïèh. I,;ÊçI+Ü“Jï<é_µEw•Å ½'A@éì‡àVøKÃÁ°ÌMJýXj=ãñÂ7Â*X¡îä8¸ü°óðîáë^ŒÛ×ÈžAΚ­`’`JhËõz2Í´ÖYëÙ)²ädô›«•ïDµõ\@C4ÒA[QMV¨óU«* 9E—ŠÎó:ñYZÏ  ¦+ÜÏ &W=ÕR‹ŠH)÷LA“å[Uk)«úrYx] ¡L (N ŒBΑmoßÑ8¸­ÆÞbÚÓfà”¼i p“¹œœ{8§÷–£U°îò%BÿæÛF›SÐôA)) [m‹dºš8çËFÛw}òì³õëCJuI^‡PF5$SÁyâí ÿéü˜ÝA/]Íe̦¥QRù`Û@²ÈVÄ^©Ê¯“)>ý¨r¢›  ê¨ŠCÈúR@–ÇðÑ](‹güݨàeåý”²4!KÇr\I’Wvövå xÂ}9=–8e—ãÔU}ã‡9Û k@÷,´žÒÆÐ”ìsr¢BG-ßëŸYá?!jÃî­—ÍDC¾Õ›è‚Ø!¹à0LF¥…Ø]¨‘ì2;ñ³+3ð®< ËOä ÿfG“Á;Håœ^Ûñ®æµíµHwܶ;+ ¹ÑdÌÃú3‘‚c¬ìÇ>ö#Ø…­¢9锯®Ž2ìÒ›ê‚Õ€æÕ‘f-PðHH¼¥ÑÇÞÛJ¨¸ P>%G‡·ÙfÏb+Ež`s7ˆï0£ó»ÜÚðÖx»ùLŽƒ’ãùG¥GäaŒD²èº ]… *i¦&öU!¿ ÿ8ÕèÞŽ‹|•>ËÜŸöë$¬‹®j<òŽI_‘ ³îãUÇ’m}‡pí+gž:%Ég#F™Üw1Ë,{Ÿ@Þëèoƒú¢;öø¼\Ñ×™«–¡TwµˆXÚŽv.¼½älr¡\EŸÜ¢²êQ@Kl€Knžì6U̘Iïò÷²-"?Ò2²Ï‹h ­ê`­j5\Î×/8~G3úqÿÑp«².úâ2ÐéÉ8“³mûn`¿ÃÖK.ÐSŠP6k‰]®CIŒt÷.ê¹°!þ7\ãcÒù7‘¡ð° ÔƒŒEãø ˜/z8\iÊtáf²§û³Q›#ƒ\àÓEಯ«hgr vÿ§¨šWgñBlx«&eVÀí:¡Öt;™N$*¢Í‹ —I_|ô0‚9$ƒóåá|îϺèª|¤øíÎÝÕ¦ê3Ñ»Ã~AÑ}ÌÕUJ»>µˆ©$t-éøòóÌmŒü¦C¹þ±+—.a’°öÓëÁ¯“ŽCdý£3LS(/Ïfž«ó(x¨ë²°$1oY$€ò¥29Ô½öÊâþ*Qo]–U:×+3=½êèG*7º¿ø „Ê*Ç#Î}Q«['Jù"vø=ÝÝÜ$a€J’À{"UM4·ÀÕC»Ìs±¹zrçN´=È”æÔð2o!R¯š¦us¨¤˜UÕ˯ARôÓ¸Y!•d¨òvÀ¨YÙ”xï¸ÊN…BϬ,‘Ž j¯YRo–ÆôGvС.6©aÕzþFnºP8¦‚’Väh "±³K„™S ªÂÊ•-ôvêÎsQhÑ·—"E^½Wƒ <%Qb_}Kž_繯Zâ: ½ê“mŸRÛ{øž8øì¡Iië0±¬ ×Ïô–U2ÿZbCëSøÜ«Ys{½-Y5RZטÚrL…Xhf*lˆ-‘ÙálZ#`-9‡en_Øäå‰þ¢ h¬ S&ÓWk—4Û …Ifÿ¼†ÿÐþ« @kºî„hÆûïÿüÿ/²Z4¶±Æot´Åýê É/O[¨7žC’Ç"¥z…›…µ¥%5'Saµ6ü×I:öuãwˆ)‡“S”))Ô*%N˜ÁûŠ?¢¼ùËÌÐ@$”+! ˜™¸¶–––ÊìÈ¿ËtÞF°ú¬^Õöï`KñÐ…iÿ˜Èñ-Ÿ¿ÀX¢»í‡¦¿@-‘}0Gºõ ]36…–Ñ1’  ¨–Ëã)Cˆ–Ñ êEc0C\,—_šA‹cŒ¤ã¤£•Øb yh•è%YJŸÝÃÿ!Ë*iÆ…Q°KÀ°˜>· P@™U0T©@É*¹‡bš=»6!çr@Í)q‡ÔÉ¡@ÑXªwŠötKÚêp¢ãØ®j`¥{È>Û±`{ºØf“œÿ¥ý©žªGOnƒ<Œ†¦M ô_‚°éÎ_ç^û"¼W¬E±© øÎ;ëŒy]‹HÊ<ž¦EI¡ïܸK(}8K v\”R.%ms¯Kw—zuat¤mG$§U³iåúòÍÌtM½]—¬¬”hÍú<½æÔ¥e{EÁÛ^ µ?Ý({T‘‘¶oÜ'/I¢´ö_PòÍÇõw^€Ü*Ã>ÑϤ%Ëžöç¢ßú\y÷×ívtRÓù:¬ñÒ{˜…Uqw¼,N›™8xHÄl«µÆDXè 5ìÉØ¤¦Œû¨¤óPTƒ¬)êbYÆqtwnó™Ö\ßž°¬É­î‹í”=ÉñµŸjM˜Ø9>i°=·v ,öu/to/*ãôN84ÕÈšbf»±¾øÚéÿSzG™•ú®9ËþSqÚeí÷Žû"m^d³o¨Ô"%QOñO¯AAÉ Óï5[„SøvÚd(>ÉêdߢØGœ{:~"|‰l_{¥¿A¾èÓà 7™™W €ÙÓê‚+¾ÏІ×+—/š–ÜØ¯Xé@_ãðß {-õâ`íYMųQÿí9keĨ÷Ôôµd Â÷=ìPUÒ°L›„ç¹êÒÓ»àѬG ò'û7×”ÿÈ«+Q¯²¡ùT=¯´ÆÐwkçÜÒª¥»¬ ¾êÖ×»éõ›BQ±"Ñ|R¶7¬7FJ'Û•#›M>×3 ¿{Ń”.,C ýŠç%1IŒ2ûÏMøâ{Y¢«&5­´‘ðn¯­œOêÈÞ–›ýè®ßM+¹‘¬|“¹»+ÍØŸgv°Œ‰’ìTܪVöú<´r :8Œ†“Ý+Ie>wKÐrÕ‰>0™,)f€TkEÒ„HûµÔÐÂáÆt­˜Aý»¡úô¥ ~¯NÍø›7V—6²ðrt¦ÃB:ó”¨/J‘mò‰R‚ëx³C=ï=A•Üçž>…“°½Xvœˆº~¸‡:º~é”?§Ð¼shÁï¹²?€s%?' 'Ë>@ç6gQ2‡?ÃFy®3íЂ7{GÐ(ñÆ&l±[{…æ·ÛœÔ÷ú‹uÄ 6LÊxKg¾m'…²æ•6#?÷Õ•‰ÔÈâVôeð¨nÄ5[ˆM‘”Hè£îvkª]„œ{ª€`´#@ûI/ ³–èhUÆ“Wq¾¾Ñ ºÇØ4è>pŽ"¯ÒìõºOµ>/Ÿ9ËØ’$¶‡e:WÇþƒ!t€óhE÷;þ…g`´3Ó-Xm’3‹| ie¶2KŠÙO$<þ-—cýÎÎËlZ5^C¶÷Ø‘ǬBM¦òg^oùW%`×  UTó¶Ï,Òkp\:9ÆfZœ.‹¾‘záÞb-w¿QÏ\‘k„Wò»Ì=G6í_ƒ›võæCÌ^¿ÁÙ“-ž©µc?}—có¬¡z¾+"cHǪ́‰}°ì4Ü.Ø«&âÏ•þKfÐâ_‘J²O5÷ÊBŽóN)Úhjô°angIÙn!wòÓ2TÐ|/ˆ‚NHr¬”9þ­ÿz=ÄM2ƒm¶÷ŠÜuס€Ìvi)Î×BƒƒWΰ vmä¯Uï(Æ%–’CÙQG~­«ÞÚgÙŒi­_²öx2¼aŒù<»`îkG¶Ÿ‚=â@CNqzŽ1ô_Ó‘²×€­Œ‚QÀ%Ø•§”8‘”ãÚLo¬áÉWñªûÝd£©¼l—K§ì~X/Òbc‘tëË0¢”² 6:©ìÀ¡íâã3íÅÝçh½·è,xcnÞ˜~-€þGº5u²ð`µõ&œ½¨Óa‡hEª"…ŒÖ$ ÷ÆŒ¨%Ä õþ»n$h9 ÁЀ¤’ªjü®¾z¿½”»è§Š1WSöɾ†%”HæÅ‚´œ$¸µŽ¾f¸§Ð|ö.ýr…ZÙ_±M;ŽyÜ’,†GBbtø7›´Ìâl¼½ ‹ÈÍ¿å\Ñ(¦l¯h9ëöÊz!iÙ)ã²Â*´ëa[+a ˜Z Æ~ÌѺ1¯Š¼"\,‡ÌÉÄ ŠÎdôK¼4ÆWÈ+ø›8 1ÜÎd&{”7ËO>WÍ•°Žeâ9$o NÇŒmg?PdasÜÀ»ÛJŽÜ”X›–³‘¢@UUÕ DVƒ¨¨¨ÂEMåsÜXµ!Í(nÜ£eFÉ…Îñ&ÏOØ-ë–#ÙC÷³xz2fpܽ{1*JãÙ[_Öù g7Ktš$ÍšFO:z ÀŒ|î¥;„PÔåèè)ÌZZöÅäíÅyB¢ï»8⑯ÐÅÕ#Å-€})ŸsÝW¨væ5=F&.JÇ+¹(¥Ð„õwŠ'ºqÕRŸ€ƇEöÃ΢ìD]Íiƒ(ú;Š›ª6ˈ7Ìý-°}G›§ÔÏöÆ_0˜áâ]aÂP“¢ ‚Žv_QÞ˜n\Ϲž5µ‚+¢Ùbßq$mSÈs@L *5ä¾…G¬‰°ûÁÕPÖE%Ó” ½4ÙÉMŠë•2d0–¬çlåzÙD²o…©fR»Ñešæt•ÂfÛ ß& l7+þžœ5ó-vF,ÝíÊ@¯|"7™( a.|–`ϸþÑD‰ñùêVa4 …ãUP‘v–/öÇùmäTš^Ðn†»ˆ_U$¥Àá>¤¾•ºíÆÙ”²À¹´Ù,¢ C£MN¦eÚL¤3‘µVCÝ…£re3™åéA)´;'q÷tHð°V¶ýá|_<6{c2C™é¯aVõ×\!ÔÖ×(Mþˆf n„îzjþ-z2]°ÀI¼Ñ»fTš(TŸf±àRߢ€©9&]J“;ѤòsÏDÓ íx½6ÈÉc%ùÜ7 µÖã¶¢À¡ˆo]„“™Õ%f¯FzkýŽçNýÜáz?ôav`ËTtÀ—Êxüþ‰ûA,ìHÒã EÃa…óžÿtfwBÞ´8üÁücO%˜"àÀcúáËkoŽéuöƒ3· eLYþ𲄸þB 2ªI%¦8f)™¢PN]²—‡6VO‚äsÅÆåлv† 1:¥­“A!(?iEo̬¯„Q™ï–pŸ 'ð#ÉE ÜÄ‘ÃO nI‚—*Ö“dã %(R3š—¡Î«’g™8U«cÔ"¿+¥¶*cTF«E ­Ú³c±°~f¢0߸¨ÉÇøhÑöÇ6¸¥íAL>Ò¯¶ %…Õ®EŒn ]Hô(Âô¨²}Äìo\­uEžµöz¾3òýзœˆ…¬ÁéMMODÈlQPÖ|¾ÅG…ÑM%]€jð|#aè+Tîì_Ñi´Ìÿ™(vµ?…ÕÍùÂmþä˜LÖ³Z:a³8eÅTÉé·«!~“â9%x¦öåA‡H68žúý×ý\Šò™:,Ûÿ>“ÿÿ$}œM¬MlLœ=ôL\ÿÓõ?dÐ •ÛVkâÞ•}2Ù±jI™L×íg®‹ ÉK‘—ÍÉé+©®d°5+rÅãÁØÍ„ß^Æ@>ÄåAq×—ÁF®*R }úÌ~Ùµ #Õ#cžÚõ}žËÀ†EâÊÛõ}¦ÙJR™\nc‘÷‡<ñÊÖMLô©˜GÃtT¹äc4f.)Sn ·© êS  [6mѬš]èµ]7yÇ´èôð‡åþîc&M!xê J•~bâLR2“/ßÑT$ FÖI”I¦ðFtSWqgq<ÿ7¥Àñeà*b³Þ`[ÇF¬ˆ¡Éû¶òŠ£ŸÙÙí-m¸y£€®ö]q¡A‡“§#3o&_¤RZ›ÆäT"Ê÷'…~Å8*o“9T“tW*u'-{w•cöN‚²„+qxSWù>‰ìd¥x©ËŠ`j&Nõ†5Ày$#+{·!™ðëÒÁÊÄ ‚~7WÆ·÷¯ëà.Ž®.n®Œì¨Úõw"NNNž´ììÜþ•«û+Õ×Éñµû8{>TèÁÛÐ|’4B ·h¶ŽÃnf?7Ú?Ê4ïÔé$Œjª®Ú"«ÙÚ'-]ãzìÖãB &©t]ùEãeaX7@ƒ;Åüc4øØ„)»z6.=Á"F“®s,sH§©¨È…‘*ã•·=É|溕2Á¾:[(Üz®!Ä™wæLOFºGÐxÁÌÇŒôÌÀ¹"ΚCcU…Bã ä|ARÿCTÔâjJÊŒwbýN,ªû&î¬:K´ù´úê´õ챃l1Æ‚k¿º¡ ¡thý2ª—Ëw'°¾Tª±£¶{¼?oÖ¨89Ú1±¤§)½0kF^œ> 9Z?X¹9½°g^nÀÇ{’Ÿ×ÃÇo;²±#)K„ñú0þø²0£oˆ,Êâ wXu%{gäre`ìXN:ZÙø|ì' ŸÄ™§‚t»÷ËcglÝX½¸?^ã©rò€Ï bÞDS#ÙÆ-r™ÃÐ`ð¾%ù£ÂqZµîvÐÊžÊǓıç#Éý§ÍíFLv7“'â*wðÁeav‚Î t‚“‘Ï$ñ¥Þ}•‡ùÍùˆOêûí__D”Á‚O—™©Èào«ÜÂŒœÓµ”ñ>ffÝVDb¬v3WÀÌ*¬¨žÖ<ø¹X¹ñY¸½è¡¨¡ coÕ-<Ò‰´!í„X '‚=óLÏÇcÈ{Z…Dã+ù#aaŒÉw’¹Ãݶ_Þƒ¢ýúˆŒÂ~ÖÃÉ<6 ¥ÌDñ5ÖEÙnïO,€Ô"X0`ûeÜjR‚ÉÀ´Îù;LÔJ¹òcUÃlm{ƒnŸ:0sxŸÿb)WîôýQ7¼Ð[1¥ù7SQúýI½Švðrã\óâ^I½¼¾Šð^ÝÞï\ÅèþnNøÀÖÝÍÆ„¢9ooŽÃ=ÈÔÎæüÜ96Mð£Š_iÖ> A‘ýsûñÓ\Xõ}æfá,AûsK% '«¬ƒŠ@óQ‹î!R®©%ݵÜvEŠ‹]õ{–º¹Èy#A`›%ù¤ï Ã5¿±æ£ù#ƒ|XÛ+ÍúóNºe5@n¤‘W{Ão7¢ôB¯GοxX»w3R{RÑ$¢ÉñØueÎWr_$•> {}tÈžø«ßw.¢¢“5\æ­Ö-¹dšÚs¶áçÞ/».D˜D²Ygü K_ÕŽ\Þ ØÓî“3›Y§³¿F3´žù$wã׎ý{¢AWóO{¸x@ñ÷ÀSQ,=Æ1ÿ—WÁ—“4èD—Gâ¤Ä^í=ïÄ^Èv`+¼/[.„¹ö±Ø€6=,¤r®êDýŽ7Ò)¿JP^F #^êdoÊ*_ñîÿEus`’4ûÃ?tkVmF܇Èv0ù1À[ímW­ ¯ÉF-—4͆%/©Zl¶‚gð¶oº»·`æ/"JÆé׎#’c2hõLFdê¢_¡6 T°Ü çbÍHÇœ¦õ[Z*ÿ€.@Q™½Ä›úŠší)U±¨u ¼z›4ÞëèË­wülî±v_·«²p~a“†3³cÌúJca›ˆÐ–ðrfdá}Š‘Å3úÝÄN¶h‡ª"~z(ü¹[>ÅK±m^"‘C)»6XiüÌù—ųC#È¡rE#̱ë?¼O®Aˆì±o¼[sE áÞ ãn'T&7GÆâ}…¸b¸E€3¿ ]x×3|MlJé§YçºÝñá†~×7à]Ï­zhZè­g·ãæcMV/¬ü)%[2{à«;*ßœJû%ènœñ8²OV‘B wpts²³ºZ”¸«\G€ŸD´j£iÇqÇ=Ì ëV~PPÛãP.TD€Wù‚a"R<¹Xª¢á‡ëªb§È¯ˆ¤B ´*äч®={ÇAEQ̧ø„tW0Ôù &_þmjÍsº>$ü|[1‘8ë)8‘w֌˱êKM¨ŽÏie×*‰FSš—ŽŒŽ c±±Ì²ti ~²Žùï, aÁø>ëÞ(Ÿõ›«Á<^Ý„¤E( P£æ§f’,zåi¼˜íâüûƒ€ØÔ ‘ã³P;Éfõг…„¦‘jšø›Š¹±´>Éhýñ”þ¯Ö‡‚²ò§"'ÅÕäƤ;²bÛ-û¦°¬€X,€AÑZ+ü>Ì.ZDo— WuÐÎoå&3µI­ƒ.w–Vtœø\NŠq7¿l¼áþkãja.e±qͩŠ»EÈ]—õa„^Ô˜/Ü&wìI¦­þ°©®ÎåÁ@cýôŽÈV»Ä "kª-Lš ±à#›+R¸‘Ó¸ÌÞ~Z>6ØX;ŸL|Ç,ÝKB,Í ©¼Nþ6(+|„r¨/‚6¼î[ÒÅÆE‘±£`‘:mk~ Y=+ÝŸš™ÒiçvA„ýÞÙèâ /Üá¾ÄV‹ÀÈÕ鬭õ€,çl™Óe2qqêJjíåf%Ƽ~ƼJú¯rpfMõ™ž¹¤Uæ2á'Xp§ðêM ‘‰ÇÕ(SY¦ÄÚ¹h ‚Mà‰Ë;;9Ž(ó~áZ:´¬Èðß4öÓ\Ñõ+ß׫þÓhš«7 Lï €C}ˆ£µ†¿ÜysÉjnÙfé}QúŸÊÀ¢m[Šß©§&qÊÃ)¼×µQûb‰U«R-¦”øËä8Ä^…/ì`2ε ùCªú·TSM2ƒÞ—á·Ä™n`+À^yc¼âˆ0©úŸ9E ’Lþúu†Ûèn%Pˆ@×3\ÕžÀ­£%´4âxž¡ÅéÝ¢ý Ð*·2B+Æ_ pªf̰QüJ”¢ýÊç‰T˜2Iæ™Å+lµÕ‰ÅŽØD¢=Ò©êeC;Ã0¡*h4wèÄ¢ã!†Ñ²µ«N;4 Â~ÔŸ£6+â~ÓTûÑŽB¯ŒBK6?0z|ÿD œ\îú’a•9îeêPž~™Û‹¢òÈ‹º<]D.>ðp¸Rñz‘èÖ±UÚì~©&N;ØâA•Ñ£Ð}XÙÆ—GÁ6W14ªq´1´vi½wl Óçs¬õÛ\°]?m¿–w:{ÙgLGôU²ëé<Ú3¥¦™˜´cç”ô¹c^ýQUÖ$u>’bÓȼ:¼Z˜n\01Vb»4oó8Zƒ(Ú”çå686ÓÉtÿEZgØ iÒW\0xžVÏ–'c/ÙQ€gĵŠB§Ì ‡¢offÞÁ81åV©"ë¬X”ÂSýhÝa¸ŽIæë×°x=]ÉÀafCžâ1b½¬°T} ¨Q×b-/;ž ÒnqþMáx”ZŠåKùñûèV\rÉÖã¶±ù0ѺFè¶,*üÆÍ=¦Y@³âÅLjÏ'@¼Ó4Àmä§ÃynœÊLOi$PSe…ž¼æoØïÛâå#Þ|¤G±•W}<áÕTèÉ»û;^¡dˆ±¿YeT+Uвr2†X dŠ5¤ÊÓg ¾»1^ïà5é+!Ê€P™ƒe3‹\hUˉèêžûƒq̲çÑ;“*Ú¼¬U=1u@vêFª"„÷Wµþ˜r¨cUr·¥@v²Zc$b!Áì|’Šjlfpe†@ùkPR°U€Y>dm!!é ô€Ž¡`lf/¤”íèeKOõrèw©JÆ€8øÁò‘È oÕ‹ )=ÇZ†"ž]&väg™™$yŒ•T™ÎÐñ#uGÍ«Î.#‹É#¯8„«÷í“ï,³UFÁ+›…$Kä&›Ìàì×ÎÊiz”œç”J£K|ºUšwHí‡ðj†,b½££°ÑW—{M’+=ë¾ÔeþUZ†E>O| ÁRSà'TÑ›«‡Ô°Ë˜%³8¾ó\t­Š~áyMŒ ¶òs&ˆ û8/.zÊŽ¢w¯`j5&ÈÞ$»ª v qÈ+äUaj<ëâE®´•Ú¡=,Á²ZŒ5O"g¹DöÓ!A³hD€¥ë¢±€14¡ôŽæ!’ #Rsíœa/ÕsmÎXØ›uÍþg¾ÿ3žâqã­Wˆõ£YÉeÃÔ-UWîÜb‘•Î&¶å» /'ÄÂ(¸’uNœ—7x .c™Ÿo­JÛì…X'IT¨h: ÞÃ€à’¤Õq™×4-šßú‰}Ç"kïNêÕR ‘À@òò!·Õ“ôÕ`ùv>V%“;VMRS¾32Ö€•žS¯=t·V W°¶$=´öU¡k Ek¤¦N¶1Ø»â²\󟵂 ᡾ Ë”môX¡(07(Hg2}´É¼S:£Y2B…¯ýÍ÷94_ahò7uõ°¦ðÝÅ+m?÷ ÛÒ!¹ à†*ª¼áÓDœUQâŠLeT~m®°‘ëçÀõA-@c9>%ª$bÇŠ-Éä3,&ÒWG7rJ8&ȃon»HL{~/еfOv˜?G B,Ž2¦jô?ñhÜÿgDÌ3oö#!®‡ö*±zêCöÕ*‰¢¸¬Z@ÚE}fIgAÅèˆ,©HWãÌbiECM@áq2¯›Ó(wÙ!újKáüB3žÂla=›nýµg•NÜMQ"«QÁNñùOrnƒš¼ þ~¬DŒ ׋ɖ(ûÍÕ^Pu˜¥#ïx3ª º´Š:º©¢©]%ÉØQ’8?Éÿd<-b•bùœUÎÐÅ^{™Ø –V«Rµ‚NyÔXTv yʼêBƒÕb#–²$ñ.©ðÉZc·f/vHK«â𶇅“ÖÕUíA¼Ku¢Xuò¢pä¸&$Š}?Jîð8ý–Œ+Ø_N}Ÿ4q†.½GźQÕÝ®ù«œ«oï® Í;Ôª*1 To(FÇl4_¤äë« ÜiòùwïXãG%œÑeîŽú]êlÏ>ÎAݪ:|#tL ÓHˆÃ¨¡|ìÄ€m$¢:HoaJTØ6Í?ozkX(Ò‰E>ú¢ö/czëýû"¼–$¯ž¸êé·õZŠj­Jd%ú-O›ç=Ò±<©&ÿ§G–÷û6Dâˆ'FƒŒ)¨GÉÙ#‚ÍÓRÚ*|µnw4Øn/‡ù$ö^×Û‚BÌ®:Ä0}Ì«–4é5„ȺÀ‘Ÿëâþ3¥ü}v}—çÀïr6œ?'è”÷Kס}PÁ=±ò¤yºbK\%…ykóSð¼R2ËP–1m›¼æÞ‰¿Uvˆ–þàh gí~ûǰ•ÿ›õþW)(éÉ ,ÈbïôaÄ=¤ß'ƒzÙ%g\ñõà°— 'IÏh HÍ„y¾M-±û´H—©ím\à[~£Rw™Á}Ÿ/€6쒣ⱨQ´ªÂÝËÖeðêÄ¡÷†‚“w+Ÿmm\FEä7¦Ù ìÅ MÕæ2Gô,Æž‹‚õÐÜ@2‹Îrr†ƒ[ìÖ€QQïàJ[†º5 ”JÑÙgBeĬæÔ`qu #ÖÆ±Rˆ^Ê„Öyi%!M›,^Õ$m†Ê(~ã ô}wjG9UNüÙUå D¶r¤&TÃýŒFrÜEÙ„rö¼žËy ¤t*|™ä0nÝÖQŠ!Á*•ÀZŠ[¬Gæ¼j+ô,]²ø#·æ[hRvv†§Xàn¾Ë†ücyú:Å/-_’øŽ×Çá’c?Äw'kÚÔ2Ný_RykyHø×Ôô‰¤V ÂÃæeȹµðè6 ìéS2¬¢/Î .g–ºy*0 â“ÃÏqã~›„nÚµjp×”+FÙÛ¸5Ý:t¯Å­§ùß:­IþIAqV‰Ëö±æ¢ˆ{ ŠI°uú›Ë ¨ÙjÑ¿pî€KšÔ]F;x‚.Ï’™“ü˜zÿ'|•pîEt[õs~2†‘z}QÓ<³ë„sÁ,f$¾¥b>9R ~zFö¢ÖAË9ìd¤ê\eØ$1¯‰vus½Niðdz–…Jõ3®ÆY0–•Žt'λw‹ö*Zg™U^/¬zÃVÎ{ÆÏ3Q».!92Öõ“YM«ŸG\+ô–ŠOóýßɇåvNîÞæÎŸ c(!kÚ›oÌ$zÇn›F ”6]ó,’õžŽ¡#Xâ“傊ÿ›±Ø~ò¦óÁ™¤æô^w„Šˆ™ÁST['¿oÎ|¼ôîéi£ÜÆz;ø3Ø|w¾hÁR@=a¢$hº>>ólà4L…Ñ ÎáбTÜeî`=ýhq"KõNñ8XL¨j$/… £3 ãhVq²Š'·­;É‘Û&a%ó·-Ñ"±­ŠMo`êbP{ÛÀìåE ´dhÛ¡·ëy]¬ˆ¯3õcB¾Ž8#Q½cÖ9 j`jºÈ½w}uÒ¶µuêˆk-ëÌ2Vq!Áæfº­rO+U+€ ¦Tóš~¢Ö—é(å}dÏj~†;UÓ$#> ñ>R—°òÕ gm¡°í@!qʳ©î‘J棫[ç…ï€ ƒF[âR{O›Ä×$%52¡RîéacÃ(ÁHE»®µnŠAåôQô<í”Wè McÌB‘_"ÚÜJK‹¯ö¹ŒÓŸV¡rªZHë%£”CŸª½äjöÉz ³×ÙÎñ>óÕ"å^¤0[‡â n³ à´•ˆÝ~de²Áš¤3ë€à66°rZAÐ+¾»x-¥µíÖ*€r0‡âë q3úš‚ {ÂRCù©X$Ÿ=ŸI¯KÐÙç$:÷~ñB(¯C¯rà8Y\XzŸ`ëiç£ÿ¹IndkëåqþPM'…æNe[áU 'ÅçZ þD5âÒëŽAÜŒU ÛŠ‹ç¶:¤Þg«`ø Ȏ˦é‹Êí-×[ Ü„dŽ\/ÛDå7G)Û$˜E»©!™ óÖÏçÅ›dIùK‰à ZZßH›».1ªè^Ë3üuöìñÙÔÃÃ¥P›G›õCŸ¢ôÉE¯…T‡SGòPÛS ºÎ+Å€^èrŒž ÊÖ‰Û)LâI­—3VvýÕ3´›€Àá(í5ÇWD>5ßO,¹Oukì¶®=ñ€p|ùÙ×ǦhL—ÔiÙê\Ck¹Í{”UV}MÚ!´¡)³—_I Yû+ò.6FÃ%Ï;d—Å5ù”=Ęf*‹(ÖËÉXÆ'²Ø\N,Æ€ÌRd#»#Á\šfÌ0ò­›$&H”W>¥2FEÅA~&°N¾÷2Œ»å*Ær¥nø©àÆh{>¹o[3§¢Žy±‹ÍÔ´ ÇëóôG·¹K´×Z†v‰szù®k¥?k7úÇSøh´t(~“'˜åqèK`D/k¦ôÎÁ£s¿†\U­›ÙªàΓ®Ë¥î2Œée^m;’/4òpû­Š·Æ»}DÇþêF¤mg¾fd¬âÛt*îЪJÙu©îË@>æZ:…¥ït´ló¶-Nk%ÁˆS:,­nFff‘tZ§‹åf»·µÝ†Š’Sen§çÁa 4n/·ÇÃåqÉÔÑœv§ûÉÚd™‚‡¡!u饪|›®^·mÌ õ´^»çÉ!If¹—íyΕùVÌ­À®üRÔrÄNe^+î+ƒˆæ¨ykj•@³(‘],·T—f X]˜¶òö%HÓÕp"–! ™yïTÌxž<„•ôrû•æK@Š‹ôo*žˆ°qYÍtdH­ª¦ UiÃ-&ùLÏ©­¦\Ÿç®ççaSZÍ$&ƒcÈ;gÛ¹8nê÷3nêÇêI¬¤ÛìþíŒê¾†±ûlެåͽƶÌr1kŠˆm£Ÿ|írä˜þ…sÏ;‹Fmx†î|½º¯)c¨UíA=¯¶íªƒ%lʈ ›žgLäÑ:Ùùv(~½Ü!¤©;wäö¬9A½ßa¨1ŽØÐ<ˆâ€ùÞ@¼]¬µk˜¶L{_~Ÿ]³S_Þ¿+q$€;¨ ö=ºð[q ìçp7Ón,1ã•gH;Ô1Õõ’U8ˆ>.—·Jrx¨<˜ä ÷tÛç07û_ˆP©ÓOúë¸ Ã#¸hF ý”è~Õ Äƒ8(‹Êy&c–„–ÀG£IM ¼3(ƒàFµñPB-P2²”AUdKÜEšñz…êQ²DÍuXêé6K-üÅoaI/ã žFqEu_Ÿ¢“6o’oËoæâ} ‹xá9rX!¶|Ç1¥Ç:D„80tdêȸ˜ž—Ûš‡—r?jv¹,T~ðÀX@V&ì,饊ÞlU?9ñ4¬j@™ÛCož° ¬-íËz“j"ÎØŽ7TV“Hç  å;½Mì`Ò¡)iÁthF©¾-›Q/ð¸-Œˆ8§FûÅ'°I,eqÎpbå_¥ýÆ`w*Ré/»SëÅ/Â:¯–”l± °9>QÉC^…:F–Þú‚A‰Pß,CBs×w/o/@Œ›ö¦•£Ü½„ßÓ40^>1¹9!XVv£1Æ`B‰á+á/J'W‹gÊÌß-²j3Ó>÷iâ•â5A‘·Zˆõïwƒ"Ó J‡­¸Š>×c-­øìY.ÿoL~¯4÷ž–J\‘°=ÒtzÖ­5çÌ“éVÉi®Q7ÞU5$Æ2¦$Òãಚò,Ït4øÖ¬b!6±e‚*ý«„!ÔiÞ…¸ÔÎ â®Oañ®ZçÈlŸÉõèÂŒz_&!P}+Nò«qX¶\ÏÇ%Æ·ßT?•×ú ¶xUˆÒ+yõ•$‡VwF›F|bVð&`³„Á“nõëläüQZülhš$ULžD§¤!»=y‡”gÄæºþ ô‡•/]veGœÔâ9CA‹ÛµÆ+ oá¹°îÐL[G×LîŽåÔA’œ#Ù) ãyM‡‡Wb‚î†uª ôó½_ØÏ«0½?”"1Aox3pÂEyxRik½î û&äÜ×Õˆo! £7µì@¬x lÓxès²pü~œŒu±Úðè¡%ÙòãƇý™=™xòN£h†º›é-·HT/8ô’~}¥Ê´žøŠJiu™Oe¦âª‘ °Õé:¢žN.»1›\b÷†W5KG昬jwžÉÍÊè²à‹¢œ3bèBAɰĩâœt§¦Ÿ£Šá¼ÐÑœUÒjÕCý²-ª–Þ;(_ùñpvOgA¸ ´ô=Œ–\3ÍÁ™ËÁ½Õ·~/å¦Þ„ïJ›Öl hÃX?Ì8ŽÞZ^Sä!Ôò ¶¹˜2ÖÇ"»Ä++$Ò~&a¬‹2Ö}³WcnQh˜Y"=²úA×ZïÀkѲp@óȈ­êÀrcb|«H6ÅÊe™`Ju"šKGMdŸ\“˪è"Ñb^ÓmX7Ãx?{5¬·ð³åü p¹ Ô.ÎfS¸C|ß3ï2±¥ O,–ÇÆabÜüêdqËú¸OÕ™K²e´p.w7ÁÜp¹¼¨¾Ìh®e€í-ѱà2Þ®“Õl ”\ǤzxÙRI‹™ÇkÓrñ,M1°ƒ·êÞyÌj¶5¡Íâår؃%g’\ÍyÇבçÈ‘é4DV´@ÿ¸ÞîP*Ò ¤4)Ò'Ý«©N@•W³h¶×„Ž;œkØÎµ˜öiO ·Q†›ˆÄm;€h:n™:ǽ€Më|ª‡•ሟ&Ÿ§esÍ5~ŸÅÆ*V"èâó¤Ûê×a‰§à+-šØ%䙼w¶cà$9C*iQZc ¸%:Ï÷(‡]dÊÐm\êifrU¼‚ÆZ\Í;`-n‘—h‰­\¯#‰ öª€©'°>Kâ1yšœ#2Aç·ÀF±G^.ÊÁÜ´+|aNB¯ NYܾjÓÍlÍùÀŠ…ß)­rÖoOLXÖŽXlä nŽûAÄÖÛ¸½‰³Áølõ±xN°Ò‰rçð)!p¡5;²–{n¥` iÂEmûˆÝµ­Sž…qáî°ú u®Ý'œà|[ X.ðQ¢y{w÷ë:+ïÑ[X<Ç`˜Í :&yÉâ0êÇfÝgMÄSÁâ(JjS6„Ý0‚9và^3ëul÷L•ËúYL”$S=ÙƒPC‹@c틉ªèò9ƒÜ@K+S®[hÒÞ0rz2ûpåUèÙš¯ÐBMs½ƒ®¦|Ò¥]¡K»4…}®Ä_1³v Ʋ]_Ë÷ @Ä(œGzJ)´Œÿí]¡^ + p:vßAzîjv§f(ÌKRŸz²Q,(<¶q™ªTöQ‹Ä­7=œŽ^æ4žé fɈéט¶OTZû·* ùe!CoM?æ¿ó&(Êì¬/‰’ym¿Î](êú(qŠË@q®ÕÚãâ³ù‡É;ª.Ðq&d¿5ƒHÈ9Ež!­¸¿@-1ƒµˆy”“ƪ.ˆ[_Ö¹obÝ´&s|?ò¯¾>ÿZþ×ÿ¢ƒTCy}líVüYAQþ£GþŠA Xú¬J[ÖÒë©ÎÃåm$ƒY mÝÚÐIÃL !u0£È;µÜªâ—‚p )éh‰ã Ýë˜"qG€e+2³ãìʤÒ~€äküýW÷\©I¼¾yvxè»陿Ï[$Ч~§®Šòs”?®þ¥ÆÖÎúS§ÐðÄE¢Ìc0²í0Z­I¼°KzV?0ù8‰ľ4ðC]#v%(ÃkøxiÁ]k »ÙL‹ nÇSØSc•‹PÚ6ÝáºØÝöšnbvDr½|„,ÕIXdèM}“m<ììŸ~t52wßâý±Œ0\ñЊ}x@¦ŽÃLd5 •PÖy‹Lõéøp\d<±·–¥‘nÅo"9Ó:»ðM'Äg€»¢I’ƒ«Òõë{ëÐI.žÊ`¾Ä!~Ѻ9¬¤“}RïÑL½é±7dEM»°Mõ†bƒèÍ<æ¨føöÄv¼·Údš¸,?Ñ)`ѲKrCw Ã8ç%ºE nS¢ÑÀshRx2²?™ºçðbߥ¡s4.¨–3ÅÚ„'ú͹)Ø\•ù§>´CòmR9>…½ÈƼ›¼7>­I””ă~뻾"w2 o22Þ,*<ÙÑÆaBñ»xë{x;…84}% {œŽdœÈ Û1ëAó*U©ŠÅN [ ž¢Ò\ëІª‘N:Çdø®9h8‡ÿâ¨Û‘„Ýâ@;…>É}•n°ÿLk&}o–¯:‰‰e—À±˜j}ah'v#8‚A¹ÿÙQ³Ñ׿tò ã ÐrÝa€C&Š+ÉQÿ€Êúw|×ÀqÙú€’ ¿úÕ Ûª‹-ê ÿC¢–ˆhL C× B}ÜÂ<K¸ ªŠ§~ø›¡N]ÈwKl³¸Â.lå ÷«—ñ7ãðEÇ[ΙTâk¸>3)Z§q­GgÌØ4v…s¶*Û·qÊ‘suÙ|ꈆ[Öˆá}IKëæËn{Iê<Ùc….C?Ý©†žÏÈW…Xøp¦á_DUIdI~o°‰Å¶Øæ.S8Ìè(ˉê‡näëÙ`b¯•%û³‹5F9¨O&l ä‡y “Èc¥¯`I $´7(Âê ñ“…9Œz#ïbm”Dîð(GÆkÌÖ<Æg¥…6{,KÝU*×Õ žG‰€Q¯ ãˆÄàÙ.‘÷ YÙb~ñ\bŒ8€©z˜gåù”¯•šØÏb<úf  á h\%ëoÈ T$zÉß·/htÅjd!-x¹5à*2ñÉÛ™ù>„\ë>*ß9YͨåñO\¡ušòÝû!áø4†1IVMfñÎá®n;©fcò¬øÑ6>1ˆ©“Ò,Ð[Ž€–RyYL0†Š;Þcº›z%^a›]äÔZ‚´BŠšeƒ€W(ŒÜÝ‘¡g–óÂt;=“ç™0'XóíÌ-s fþ·N‚5Ñ»ô¬ŽÇP¹h#Có^Ÿ-kf×g£ÓÏ×Éʆֽ±zÍl÷æá(‡Íî¬÷C—pª ½“Öz¨t˾¹”ŸY3÷!B%òÈ úldÃæ5 •žÝ©SÂ#á—ö‡Pþ5Ä>&B@‹rÁpú\Èý¯=Â!ê–9ëV-þ5ÛšíÓ²sJÏ4Åv ÑE=—vÜêÃ-¥ ¸)¤£%¬XŒê½jSÿ™kî¸í91HëP¿_®0`QÀjìÝnïxü MK”¢TK*ù $éìÏKåKXü&@¤ÉôVjþÖ7ÌÊ{Ð`m‚\Ñ•fùà°–ÑI`·¯YòÅB§þ‰Cn”]z*‹®ÿRa=â°ÖQ29_Þ‚¢è%!²Ý‹Ò=…Òz§ ÁÊÊZ?¢ì™k’Ë.ï0eCé´jµ9Q»)`5AÕÄk«ÌGÍ=΋í&ý#oP›M…“ØMµÒÞ€ÐqlŽiù¥IjhfËzS ÷Á–õ÷µ¸Lt¤NÿØöG HU¡[4@6¦4²ÞïÇlÂÜÐy-—À:Æ÷ðeq½y(€aî\ Ó˜ÙÎÎj+«ÓÓJoõ)ðfÂÒ{e³')è—ñ³êu>b7ªô†îKÍ"O¶{n$wbô™‚™ê¡P{NU…8EÑôo sÐSÄh2Úæ!bµJçõ°xå*@âgâ—”M¢:†:gÈVºH>%8ªÔ˜F›KA»x7~H8l‡¤pþÔÜÉJÝuK%Io¢hîsK%ÉЗ¦îß”`»ÆË~†èƒ€9¡|}4i뽺$|% ä£vaH‚h¡ÒhØ,oï_ ò$}´¬¿ü0`Mj ‰Q(µêÓ£,й1Ì>KN0'6øë-¥ì€„—à1&Š”Oá ”³ªI'F5ì„l}å´îYÏÕzŸþ:EÍ´æ”ðŸS¯”#Y¿3 n‚’–ä÷aqm²`cÓàDGí÷ã W-¹ù¬߬tïk£6¶AôƔР'Ÿ–~ü=#¤Æ¥7¯ø€©\•¨NÚ8«¥…„¥4躙*¤9Ÿó%gü·1 ŒH“”v8cÁŠUÙ£›¦Àÿ‘= 5Nb쬥T2÷–PŒÏÄgwNæ=+ß¡7 ȉ• šNRÿÇ,ŒiÞb†ê4Jž 9müE(Þ¹ET¡…ÏÜ÷!Ô$ßg‰õL Ê8&òé&,θ<tŹ!Eé"JŽpÿÇ…3ÎÆ‚*t%i¦+k´šmÒ©k8í‰LypAbùÛÂe\6‘ÿ <{•+™Öx—£ 0uP]ðÉ[œ¨éöy_­+…ïQ†ÿò ñ´–€ÚW{ô 3¼p›vÑÜäýç^*^‚™Í_¬ÅòP°•T)w4»8êr8¦4§Çh™yj+ XÇGy¸dè…létÃÄhf‰§íóÃÐàƒßóþ÷ Þ©“Ø–‡°Eâó÷¸¬Ë7uØ<:²øw¥ÁÚºQEÍ&!¬4=)«³“V€Ý8Kôúl<ÎsÒl:’"),]ªo NóS¾QîÅùC6£lôÆH„¶è.yê’H^òáºEO®Ôì+cjŠ5BÐt(&vËt(¦²\Xª€üÏìP;IRWY˜¿ “Ú‰{+ »û°MLÿ}zojü4˜g½0á.4…ãRrhôn¿>fAÇÚ¡64FuÉW‹\jvw–ÞÁL_p”^ÛñÓRvEÏ…J+§%{Æôyö—£@ÅYSfíÙ)C$Ey‘‘VmINRqúöÌA-ÞžgÅP*• (‡Á¥mAÙœ »!^ÏÙ»ûöÜ>ÝS~Jýu4Q>·˜¼"{¥éÊsaÉì&SþÁCá\SýázÊd÷IÝœãÅÓz¿Ø<ŠXyÜÅ>} €Au•˜ø=žò­™7¿ÃÈó ‡ý]ÈòŽJ â(ªöBÍi`²@. Â>ái5Õ`.á‰U9F÷Ÿ_XW¨²øÂ¼˜‘:§ÓþÌpÆ—Ú@ãŽo–AJŒXQ›LÊQ¼R=%ò “‹›rOgýl.&E&J†sÁÀ`·åÕÞŠ‚¬ì­áÌðG¦íD cLÆôÎxq·ËZ R€éè’­~vlDß{1WÉêùZ† pR Ø€lX°ÙuÝÁýûw¨¼¯ÂñØ5RB­…n}ãa˜oO¢µJ¬Môð¦¡`p’¥%1j¬Ë_€áðÞruêÁIø’]g'«”Œ N'„’Ä‚ÇÚÓõÞ¦^å7kË›Z¨,Ï4F0.ÛèLØâ’\¦¡§aµ5sªp§Ñl×HCT(¯ëSƒXµ$O^ãF÷x±Z±Ÿö?¥:0,[ú‚— c'££Ðü=’Œ»+䓯ólý_X¦"6´µqÍ”ZŠkÕôˆY㦘æ]õ׊tŸBš)ØR›æC‡õùjð•ù2—ŇÜââ¶i®üo~ʺ’\Ý×*Ø$©LëYÊùA/)ÄITëD«0übÇ]E;¹Ü¶D5œ%5†$ÂÎs©ƒ³Ž‡ŠÔÆÉšÿr "è”>°‘ ÈMÁ]DQ £ þÂ]Ëý†eÖøƒšp+&ºéåõާU༵¬ÉEýžXv•Ä&0­R£z«òLá`‚²W0V²&uˆiwG6t À׺’"4Üi[cà œˆ(nÀK·¸þ½BLYðî1ˆNª½pÛå*-‹h…¥:ªãæ2&¬;whÿ„*Öh´Ä1Ña‰Ó¢ÄÑÖð„g#  ª– y™»dwêünE~éN™ü޳/ZÅ«šaXè*dZ?É´APBg¦»ÎTñÖ¤›€ˆ¾˜!¼pj¤Íå_T¸xíí!ß iúMWæQµñ$K°jçüwm‹p LZsj¿9´'@è“:¢}~¯Q$fJ«vˆ»«×{ <LJŹ˜çKQ0ÒÒ_ ò†qB†×m/}L{l]Û‡£ÉLYr™ £†ú’íW9=‰í®ê—VáÆË9]>-"¦]Þ4£]q (ì„Æ75• ž*¨W« ` ,ƒ Ž’½€’çvüìÝÅìa†2âÀ)å¤bÌÙõ*iT`™§Ÿ~ýéžåmD`ÍÖ[gØî©;‚_'6¥ 6òìQt[Kk¥ì?.}¸óSþ|’·¿ÔcÆõ¹Ïñm, ¯6!‹ Nˆ‚Ðþ©oÄL'ç1Rñ¡<5•â†QÏÉÑ4±wWÊ”ÄPæ¤ûþ ^½ HÇIˆ‹£1Å/±Òñäp™ß¨m˜Ûo~HI }r7°d_©³DvgM_¥oÓò0$ʪüÛ)Ö²=ŒééwÍë k‰Ûñß6 y†ƒÖîqÈn'oÆžYWkâ>>bjÄMÞ’¨ O®S„ª±Ö³A¸7~H—8•?º"ÎÌ‘*×*Ø$nVÆð™ìi(R?aqeÐ$•QÑ+œtŒªT‰ˆÞÞyùªAÿh üÄU»ÈptYÕiXÒ¢³u7Ñ©Ÿý]7ÿ‡ÞþafAÒ¤{®T-pgŒ³?.‘HkË ŠþlØ}Ü÷ûjAgDÀ] e$wûɱª€q©m8EüSO}2œŒ³BÏ.+lò µsìnçèêDy‰Œñ ï!ü0ðgÖ­ßP®èRFöÑëæ¡k•˜QÀPÂYŽ]XZM»G pš8úUxöŒÐ3ÇVÞ§;*¸Txðµ—§#µ©D§Ö*žòš8Q|ÆhH2Çî{ÏóM#·Ú“Jœ$Ipä¾Ù¬eûAÑ)Ë)Æ¡Ò4¬cÅœ9+Ƹ—ÊÈÙÃG¿ù¤ÑvÂÐ1ÄŽ~!%ÓÃþçÕsì«%¹Bé‡ÏïF­"ˆÄf$¼oMèÏ.þÀƒ½x¬Ö{Þ[H©±)•]Q©4öÕ i™éÁ‡XdSé/£)p{TZ¤ªåËB]Ó¹‡¾Ò§àu&“l w*~]t ɸòð9âÏŒF-šç¨qò>ž/GÆo>_Áš2.mhHöKñBÁÅláD‹@<³;÷–HÔ“®fº°oœ&kêQ¹(ö¡*Hµ'%ÌÐñƒù£Ó±óÓz°~1¸ž×W¢Ú%éñ¨ã8"Ø+6Š’4‹ª(O>[§ÿ ç.!AºEôá‹âÚiÄ~ä?¶ÒÆ õ.™^ÁïsGF¬Ê÷{J2Ûs¥ˆÁÊ÷pFB9ëH˜5 {¥™OiÃ/øµêÅœámQðÌ Ý¸x¸Àöw>Túä“›Îãû} ’´“šdtž$‚ÏÏA“½}t¡’Š{%‡2n}½½²E“«>ºïÐÝÂX¢yêÑào|ÁÕùgL³g—¦÷á ˜O½dWüç_8¤ß•€Uð³DŠã4ò=Þ€IEt÷‹÷_Ïro#¶=Cà`ü÷ &¶®ÿã6L+§­ö‡._PÙ[ðJt²¥]gï"Y–<¢4ÓÌdà¤ø`&ÐzóŽ ¿;ZEbJG²Gp…ãÞú;Ï`íGu š´Ð输Ȅ8íc9ãnÃÁgYO2cÂ&³5+¡ÝœÞ)PoBRm(è÷„1ióÔxmbiÄ&Sãì÷™H2¡G*%Ìþ‰®ûdˆù·¸Ï =ÿÁ¦é‰PÞ°,ÀHÚI$rr S0ÃŽI%Ù‘é 4lVS£ÿ †Íäw± æ‘÷™Þ[ª/eCóZ66²Ø¶nX‹U×å+W²–/ÙðÌ’.V ?1AZÉÝM4ÿü&9Šõø‘2{zÂ+–:K:— s3!ìPÚ¶«ÃB^ZK¡SB®‰>|m/'«ˆƒ›Õùw–ãÿ<N£ú<¾=¶¢ÅÈ5mE]ui=ZpçÅÕÙš-vä&M¼¬|\»ºú€öí&=¬˜=Æm¬|ÄÓË£ÔÈe~ˆÇZÿçíê2þ%Î@Ñ#ÞFìiê'Î#ã’¼Œ$ûìÄF–C,˜JAîò¡XW°žò+»Û2IYfM­¶÷OTäkÉݰrz€ÆðÁI“þ™QTQT4G঑z¦5è2üùHa…ÿÊ·uØAü6Äõ %ÊIßpæ•kUñyºªtí*ŽK¡95Æ0@Ô¦…ß¹4‡ û¹ƒÇqÄ$’ˆR3îÝ!XØsæ£àë…’S;ú_Šís‚öµÑKS™Á¸Ü“µªF Éž«Ý½ã×;,^öogŠ¥%ˆŒ2Be¶žu ò…*Yü¨ãºúŒŒÜîî7ð|€Ãà—ª¶Î-½9p'v¼cû|sfÑøoŒU™©§‘'ÙA[°þŒÍZv¹;ÒEÙŽ¶¶.° …W†ª‹Ð¡’L¡?ÿàùàzƒÒŽQ|5x?¡z7a~çñŸ¯‡V^¼91èJƒȈ½À ¦1H¡¦¹‹äìþœ?_½ŠÙrÏÓî-’~å'ož;'|n^hÇþ²uvuDU…ξ6Æ(.ÏæÊzté ¤®P¥¹Q af»ö¯¥GŽD“U³š`˰˜XóG¨L[y¦Æ7r¸ŽQD´íÿ‰ügáé(žMöuö¾À6.ä ¾, ®8Èýï#ydpÿÕ<¡¿+ü˜ˆÆ5Û_Q‹ˆNÜŽÌìÂQu±2˜«{ÞÉ™ºÞó22––dœl…øòØÏàtA9/?vq§sƒ&É( ø³Q|ì²ôµ>Ó °8#6H€­!!QÞÑ… ;Ù–Ú{?é| 2¾O…à ðX"9°„aµ׈³—˜ÂÉÍ ̘™°Œ«–öUW9w¥ŠáHâ½küBåc‘¶2øFîÛ¿Q† Ìž¥~@[Ÿ®‰îhC5Ð&a3AÐSĽð2Ö®+(pZ®Èò¨=Å–"ˆ÷â.Ñì8ÕˆÍdªÅH®»*G…ÒÚjð~+?aè7_è+,§tJñ¥Ì\I|ˆ'ûJm0NaîE)Y#AЭø>9µàt˜K!AЦ—=ÕüZ¡LH˲‹å¯cšÅ£ØâóŒ,8ÐÉ n×¹=T³|QîßÖàp¿ãÅ3\ #dHLÝX‡VŒoŸü1&Ýû+? MŠéŽÙ|•Y#3%v9Sä•gµ; èÏSd¿Z¬™b?ÊÚ}ÓIJ¯@•o6K‰Wµ4EEà¯×¤›Š?Põ¤­éYå)›î"Óã×…_ªz²£ò‹3yEÊäÐùV;xÓîÕ±‰òñ¸„Ò–ŸŽ=LáÙô„.àEþºŸ*†HÌ**ÐÄâ9%^¯Ã𥼦³’/‰vJ¬ »ÇÛ×Ïã'©=¡‡@õÞȼ*’A%PuEÀù2©´þ±E)®¾U‘Î?Þéx¥&ɹ‘’s¹½¡ó–‘ä¿CE>b‘ ^³&Î [%ø ×a@× 2Nõ½)À\´4XñX‘©”F”˜<7ê á7æ•kô­ÜøŒ¦ô›b¡¶‚œÀ„¶hküÞú@~j x£¤™:vò…Ú×0žŠ>ÐGìó€OJQZò`yîhƒ{sB:Oî7㊌Sõ̺TãäãµÜµ!Û÷²]übФ]É·$ÒJ#̇ñ€ç¼„^«¸¡QP3Ô! Š¿‘…Èå±$~ƒÅ7Ã0:k1ÈÅâ:‰n‘nc‚1Üô¦$i®à ÷ô̬7ÃÜ4ÑHÙUÉÐIœ¨HSت¸ì¬aRfþþTõ²`+àà"á<¶ô4NÀ#”A8fãéëqŽy©¶Ô•n;6ê…2ÃM ;a>êÄ :ÊÀ$³Ö^ó’⣠µn¢. ˜ nÉìò|˜vg1œZ:ª 6?kȦçs #/;Þê^ÚmVo?ʼ–Ñû.ø1¯o„—.ªbºhD<Ö“?Sã¦A:¥ÚôLaëœfçQêOZ½ó{?þåQÏ ˆýEÃb˜„Gô±²¹cp±!EüŒNÑúš~ó ]wÐÆ=E¼úºâ6µç¡2Öš_ˆGn\wðiVüþ÷~©Ù@=Êã…Ä5ê096öÊSg’+å›!¤‹T¶«f…œÞ À)nºœ)ÉÚ ;„?j_Zæ˜ošó®:ÎC+Á6R(rTq^ÔŠ©{CÉõ¾€†"3,ë’fÊ· ›iù»ö"t§/)fgd\ †Tá]‹@=B.«ÁÌX]làu:>ƒ³r£ä(R¡½ öIÇó›mXRžuù’o–Ó Ïö¯òŽƒÕÝÐÿ¼h‘``εýSÍêÑnF¼9yÛ"Й—ö(€¿a½,qíæI’KŽvzyu;zzj´ÕO¬âžMQ[Ù§8Y©P„t%ÜíÉÔÚ!Ž4§zKªßrOÐ5@Za7kAýÿm «Ú߈^%\€ÍDX¹ñ®ºÞŸ±¦”…guI®tž±²Å0y}17£¤î£ ÅšfùXZÒêuÓÚñƒ;ȈX Ñ:Gÿ:A#å—ÀTº-ýå~áô[]Ž@.s«L‚r™:+Ày~œ¦~ùZS‰Å9:8ΉÍš¯Ð1ðƒÕ¯ë{©lAjC.™û¾¥R(_¯¬tP¼,‘M(A °×Þl CÍ‹Müç™{W2EœaXãÅëÔÁ¼«³«–×ê+ÚÄ\#ŸÇœªÁgûvS‡?ù±“gˆ—=Ý;¡áLÆ ÈOçª #¥†µ’¶¼süFmu­k °Ê¸(lDuÃd7™Øíq‡ÂŒÊÿYcîôþQüÁö¶Ø×Jøñ\ë½>×L[†u±Ë2ùÊ0ç[×CmÉf+Ž·^L_êÉðŸ|²R»¢ßéOŸ…Û²ªÕJ7z¤qf.­±6x}¸´ÏèŸÓ£àmglØ&Tà‚ò5F5Á¼l¾EߺXn 5¸VÙ°Š¿UnøK/™ö È[jñÝÏкÛÜ6´7¸«¥+ì4ƾ†î>ò®âòâ/Wm$GÓ¯'S²ÈÓB~ÏÙ±²âðëàíF½ž ðÆÜåò,q\á«Çoî Þy¤ØN·s0R3ª÷ù–ñ”èfÁÐ roݱˆí­XgQ›t5›f…&ŸÒ}çû¯¹Ï›7Î9pÿ1)0ÿï–ÊF#š3NØb(~LMº%)/R¿‰7×  kì¤Ä(56!ê¬D#xX\Cîe-˜®¦ÕBÛ†>›‹=Ê~+ÏÀ5Ašeév"Ü,oÆeT ¯r¯(ñŠMeöPsÄÔc>‚ €‘t°ñöTwh¼mJxE¸§ñ¼§g-˜DE8”Dq¡¶OU§w±<7²¨”£ª,Œ @6›±Ð x¯·ñH·k÷2—™I#䥰)šÆôÇu "¯Š¯[ˆô&)b0„¨Ã ±.3”ˆ2Ígb±VB¬eÿ÷sØ.„Í'g…œn·$º3þžäñ\{'@Mß9^jó`«¾Ã˜ˆ KØÊ=³±Ææð¸­n5¢…c=̉×]Êû¤Ý†ñÕŽ³f"™3‘›ãmp›ÞWÚ¡Æ, êÀ¥f„ì‘=]“ ì=(ûŒÔñ/—,Pî™w™A¶FÇ¡ó[Ã!’Ó£˜zðÑXBãpÀ¼|kh'ž4êòº$4\ñ›è+‹¨½â\y´}6™°2cf6LÜôkÛ¦åľÁ©„W„Z[vSðv Ï]Y¶l·Gþ=‡hVAÊç‹ä« a¯˜e2C09º‘!†*ô”Ïgqüׯy¡&Ëÿ5^TÚÞU;õ êT¬Ÿw6ïÒì´í£ãx‹›ÄÏr*ßïÌJ3þ;’vEèRÑñ>2ÓY¢šX‰Q+ ‘”Á•ßÒS\;W¾Óç  çY©æør…-ú õºòj¸è³²¼®‹9­0I ,2sò*1uŒKŽü-^RsPlo„¡aC’¶·n¥‚¢±³Púd·õ,Ù…Â;ËßJ~ƒœŠ•Â0¿Ôç' ¤fnוœ0웼h~F(fGyîq0>Æ ÂäСf®ë 1[H)>fØS•€ùl-€£‘²¦!Ï€Ó"ô‰Ýwr2ø*<êÆ%Ž­¸ÅÄ™'ÛÌ¿¡åX~;˜³ Ú%ü4W)?öXc@Ê]Èx'z7h±•dâ­ÑÝòöÍãß iš/`t¢ýÏ'¦}å .ªF6@´Ó! ú¬ ½Ø¹{ #XÒõNïq~à½5Ú&9‘F-ÄÇÇ`7V­ËÏäÖóõ~땬În&žI?¡Ä!ð^žVH9âÑA—p»äÔdü›ƒ@â¾ÊvU$ª®Ë#ØáÁoƒŠJxsØ)$w /KØÖ°‚èºÐB#éшªÇ›…lc3QÈ¡Ú ¿mÑ·9.Ž{ «¬ gWߢ®˜m6d0j³fªÍñÜ‹_ÄëýˆÇç8öVƽµH(bÞ…¡K5¶vÊ1Á•JN5WîÕÌìvKÆâè¶r“Ã’[©Clph㙾ýìS`ð ´,èûW”³{Œ›‰žˆkå_º Ziä¢xYxÞ…uèõõ™'—WÑ¢¨žõÃSÂàëõdQ~ Џ\|Un³ZÏr/™°qÛ|b¤±Ð{ŠwHÈ~”ŸV3/VO4Ó)Ì€'™´pˆoW@_\;Ùv5¿[¾˜}¥£¾#…ø0ð3Dü5gæøÀï¬S®˜‹¹f…1šP΀íxšãxÞ]È·Á<ê N‘s‹GŽcü~£pFî~£=‡Aähê(É¥8ê]XáÁ žkjá{ßec‰Ê‚Ô‹œq C:…x—8Öêõ·C-ñNÚÓ¿5ÇÞ@Ñàvd6“§H¹Y¡?2—ûnXÒÿƒ²wŒ† ÚÅnÛ¶mÛ¶mÛ¶ñܶmÛ¶mÛ¶Õ÷;MÚ´ÉIÚI&Ùý³Hvp%;suL#• YËŒÅ(ðB 4°I…$§èWŽ´j*X"?¸'à&äI±[8r,À¡±S ‹€ ƒ ÓÊ-Ën½tJ(£ƒáÔæ˜šÎ²—âI=¦Ï!„?¹ž?º=j;xB’.ãÌð‚µ'¿EÀïÿ⬽Ffÿì•sµíP¯ñ¹à°Õ°+CkY‡Cn"(ˆÉþÙ k7ãƒIÿ jü¨´Äý¥=8›ª‡Ëp× '* ”âr¤9S@—~aр˚ýsNÒ Õ«r` ÂvóÝmÈ:LÕ­x­ÞÙÅ |å¼ùļ>½•‘½7ÊQw¼©6Û,+(å[åK³¸¢ë1+úèÅ}!Qï/õj­e ªp SaƒaüþeZŽsð‚®KÕ6vÀ•¦F§L½vcw¢”jE ¥¹/ªuôW¡`u £ #˜#E]®\xDr ³À‰ä&Éyq‘GýÅ|a^(Gizz ú—ùC•¯¯qQ‘eÐ/Ö»)7¬I—ï@„°&i7È/lŠ ¦úqíu/H ~ƒz§‡´L-ÌKO¾d•„9»‡ž£èMj'ÞNV“Ži»6ÕKO¶½{¹\VØå3R1;û¤8Í‘ï£5zÊÀvq*—æªcûÔ@¬4¶˜#Ô6r’;Ä—h²m™åHÂQ9©ÄÀE¨Øåq†ÏÍFWé Ñã©â›ÝUÑbh4&£JD"0²¼Ò4åvB‰¦MEÕCC¸Ñº> «l ÷fÌÎêÅp%®+÷È2|¸dVŽq¦¡NÞÎ^|™¶zþv¢˜Ø\Ä/¨ÇG§H;Ksá¥#ÿÉÔsá ŸÈD“: "€ªñEc†õÍ:VÛÐtîD«Þ°nÛt­S•„LHÏ-D~¢‰ÙiíROlå&u­ÿS.aS¾NOMS¤ðQË£j™®)[ü>`&;îɺ¼€6íœÄá‚Þ)]ýs€ZóÁ>®<|.]Š2vþâø‚ËÃcW¬|²ÎLn¯Ô-…e]­ÓB¾£€»ª êÔé=Ì]Y ¥#6c]ÇbùoI1à‰„Ayj#R÷IÌ£aïÓÛÛ܆MÓ¨(OwhfèT0“Ëߦ•AlÂ8ºÄýÞáÉÌ Ê)[º8¬ADX®“o0|>.ªsQÚt7«ÒZ¹§çfõgïîÁoét–´3XÒŸ­±ã<“›€û_»J¨T"‡Uâ˜CcÞ.ôQû œW1CóÒKþˆó‡$ÅÔ5DSH ¼×…¿6¿6*m O•¢žUqTá«ðŽ>¢ÃúhÒj0k­¨Rö!‚Æ5žÖϸ:i|yCà_Ü:í£*"ÓVW\…'m›yù ze¶¼0¡–w¢à¹˜‘²[¬RÃäŒWïê~7lg´0ò3; |Ù¬¨vœ_?–8Þß]³X(ë,Õ<¥Î‘¹Jëä(¡1ký4>4 K2Æ.sYýÎEõ¬@ºZj4¾”‹JªŸÖBEfâÏTUöuFûs„¡Ê¶MkQþl±3B š¹ÂÄ~8 ±ÂQ¡2õÌNw⺸ÖèuE†_V-÷ûyŸ€·³ÉQyœÛƒFÊ·mW­³0pZä`¤b_Ì•£ú–&r_a"ˈC_ +„ÃH`¡&‹8t o RdŸF£yû­ ’T³üÝk¶Š¢Ûûð j"Z%§ÿKG~­*›çóÅÓÊD.•¯±&”×ÊÇëó¹ã€êüôY·[ ­cv‹Z®Çg³€¿ ¥+Ôœ¦âŠÓÆë¸²|^)jï`SÔ\†lÒ¢”Ú¾º9ñX.7µµU¨Ïíãñq˜ú†¥fò–‚“5h”:½žs¥s“´*ÝÐÜ̬nEçšyZZÕì!’ödUSZ­ŸÇʉöÓmI(|¥´œ'ú—·¶†©xøW¤)Üþ®êž@‡Ä–Ô…«i°ÿ ¤ñ`±€ÍßHohѨ`•E ”rI…z™XiìÁßÃÐÿ ÃÿdšÿÃgØRHÂZÇÁÌOßKÂJÎÊÞR\®ôj&ÿ_„šƒFñ ÿ/jC®„ 1€@Âÿí , íLlLþ³¡¯µûÑhþûTH1T*YƒsQú[-¢±î£x&w#8‘ $&’€ÖÈÕ϶ê_`²YvVÏ”qœD~WgWçÿ^]a’‘Žº,«mQ?ÍÝäɘÅýVŒ:v]áÕƒ ºû[šS_%ºjN‹¨ªRÆ‘Qvš,_Dê I]Ñ+_Ü+ÚÆ;º=ØBW·]¾Ç3×F¬ÍüQ×O@H™æLSYyõ¦ÅÆ1@¡E¡%ãTÑ÷‡~³¢¢Ø‘Uo:ª&çlÑXaO™G<ˆÀ÷° J›-X 3E_€¾rNrƒàÁY¯-z—¾ïËû{ºÉòFqå]ofCÐŒú˜øWÑRÔšKz¥NJS·e!×ßWc¹ay›¼bkÀot“öù wt%×äí%În êë[Ýfa7¶È¿¦±êYÀÉb‹ÍÎvÝV°F‘;ƒi/@u5±žž¨óóBtß6wGFŒ»8º>þ®ÎèÝ»œ¼»=¼ºÂ¦'‹¿ëçýóÇók?\1ï2~Q~œ<˜BЙc|tÖW|.M¬'ÈÃk¢¹8êÕ³„¸(KÆ0Ç÷©kº¸ÛÊðn:CŽ¢º3Ñठ72Sµé¨ÔòGð„×YÌÍÌóeoy“û™4 n.m3´’-}¡ ØsÕ¶ë§Öô³Ã·êNÔ½¿U€‡;Ôg2ë*­_»á6¬|uçi!«Û4[Ƥ¾ü½Ç¯¤êíÓ'P¥¼ï×’š&G¿¬þàšÜC®,C¦ÉTRvgªþ“–ɬˆ¿ºŠ.j;;€h _­…på’õðï°UwÔT|÷¢¬ïk1–º3Â#â›4èÌyÅK<Ö ¦ÿ8'·XŠ%ÍÖ(ãÕ3ým GÔÀæ°ÛèE.„ÕÞY²Îî‘8·ÊšCÞr~pöVVœ=ènSEð £¾ìïgz_K‘ Á Fµ}“£°.jië³bºuôÖTcPÈÙÉÑß‹»¿\'HJÀë[Í7€Öí¸èÍöp:¹ñV8S‘Mwïpïbî¯0iQºû¹‚ÌEY)Gƒ-ží\¼<ðprâ^éçfý/oMIp«ÀWŒ9«!clòs½ýïÔ“t}H¹­Ö:´´÷å÷VUµcª2^žš­à© „ÜtÙΞ›M–Jä¥ F ¦(éÊQØÈ rO©h<ü‘~Ù´Qà VÓbV‡¢Nj^¯½ ô›ºWû¢Ú`tïäÖïfõf¦øÑê¥KŒzžzqÏîV5¾ÔVzØh/.v',nôsòŽêÜ‹ž ×ýÁFN>0ý§üÇÓªßìg_c—¢¦z¦'&Þìý‘ö–’zþx¸Ùópnòò4ååæfÙÙïÞäàcÅÏ–->Ýî\ü°`‹¼žüGÖþ¬¯N§~K©ˆ//GóÈõGÿ¾œü¿úÖ¤«”~Åz£/wÕàCö­óý<ŠØ3‰Ð4–0v¾9sNümƒäbwûÄ¿¼Ñ‹~ż¸¶»¸zeÄùõJÓÄɉÀàÕÓ aŠøí¹vopñq%ÊÿSû¸÷xæš'‘t@B‹úÜá¦õ Mvá}wRš¡÷çG…w»75 Õ”}y!–íÎÿnLl!Nc¸úx(£Õ xýkÞ+¢b»¾?÷¹Ãcîå_˜”Tˆ?·ÍÖËñ'˜…[ nz+3÷¿ø8üþÿñAŸF=èO±"óhٚݲ ̦MåII¿/©›OAýüÀGø³º`ŒØ@={Fqdz‹~ä –H'á.}h©܈´\—OQ_Ááà™D ›«[z¦^öŽJ²@:>´ñÆÖ\è«Ä‹dCªEcoè €Y¿€ ˆúm ?æžèyˆÏ 5%ÊI‰9Á”% #M"mÓ¯õ­8fæåø¼¦èå’é÷†\Ô™³ö‡ö÷¹á÷3FáÚçG0½2å”]€‚3óøâfa¤uÜ$™„Á /†ŽÎ`zñpþôëpAŸöîÖpÖ>ÍÝý½—°Ïy3É≡žç`“‡‡çqSãµD²Ðúã2LÁ§À¡"äŠ \‘Ÿœk3ÝTþ x±(î'½›C÷r·OÌСRÿèè¨Gj`÷]*²1pDÕVßÉQkŸt4† 62B,<ÄÇ|“ƒ¢ý‡úk¶› ”þz“'Þ'pîcÝ5§ x™=¦ß àžÊ\לÚY¹Ç˜zVDI›+-ßaþ믗+ŸÔžN ÝÆìH˜»‹ÆøEHØÔoãõ¸2º÷Þ€ÊMN?E,]àn´$qQúÀ ò?jÀCôñ•‹Êè·wX¥\¬§#”߀¿=]ùìåœJí«Zß„¿ξ™ºôyиTÎìÍ'<¥ =lÚÆLÜ£=à© ¼¬ó$ÒZsJŠáË¥•­ài‚²çûïëâ&ý»ƒyJ‘¡ž‰âÓâÝàâ¬^^þ.ž-~n¹78¸Ù@ðŒlW%K]_o·Xš(ÜxlìúˆyN!<Ò£î¾(Mž²íÛB4€ KÞ§²ÛKaÆ‘§ „!%‘µ-öŒBç/ëʈ¼L ªÉ$•–ŒRĹÁ|ý;)ˆ±f_­c‡ÓwŽ›Ú«Î)q_½|Ý{¿fg-;ᯩÄû€id×¾¾«Åøä:ŽÁ½0…NlCâ@éô‚ç¤<¶Èé|ɪ¾UÌß뉬ŸÝØA¶°1¥À¨NIÛ1r %€Õ @c°ñbÏS³uŠ£$ñHóôuþ).WmC0 ‘dòšWåû‡×<ŠåŽzý®Z)\n¬‡Â^§ EÀÀ¯$Ëa»${9õêú Q¿Ÿïìô4±G?.$ýü‡²ªjc¨ñ®›4ö˜cØcsU|øþó$"†µ°$Û9Ð[eKuïÌа‰¹¦ÖŠ_£]üÎ’„0™î®çœ\c^ Ș)Caò° )Ï͙ѭ6)ÃeѶjˤüeA¼WfÄ•›ŸÃuÊÐÀG‹ C Ÿ\¼Ö¥«°¿P´¨ŠarÂ…P׌j˜"å;WUXï ¢¼l1å"N?W;üvYY-šË%ŽñK,àCÊjkKtt¹Í¥_ÁgL8¶ÿ Æ5ø§:Á(MÁ¸UUgsÿÉËEuZŠ¢ãCs< ¢gÑåÙ@°ÝÃÛÔ”´{a2E_QÏ“.©ØËеõ7ª±ò¬,2¯åªMYŒ@ªEÞɰÖ"Є‰x®òì´,ñí^‡h„ 5Ao‡ÒCw좞$¨ÚתAî×¹~,ÚÎfmeIðPDê’ök6êMë’ÀÛöÖ9jqùú.‰ìª8Ûâ² A©Íc¨!èµ§?}üç`më‡ÑR1E•Ò6`Ãõ9wº{Ï—úÑKTç·*±š ½ÝD/Œ’~ŽOHnw£Fbyéo­Z±"Ž!ðœõ6¯–N ©Q„»„ZïLtæYÉ¥šÇ#ˆK'@Á"Ê*j.¦É Rˆ'«>!ÃvÕ4² œÓ»´Ñ*i´ÝÊT³Ø}žµNÂ×&Ü;Ck|}ðy!£ž™‘´r昺Ûp»í**ý%Ø'>/ «ú2QÛô[vÔ¸ ª.E˜^É ´¯zÌb×Z&fÕaœø:»‹:uVFtƒ xëðÊ\òZ¼Ìi·ü¢jƒª¸md*w± ¨lر„¹`è»=ïÊñ(óO§4µiÌ8´áWÀûkχÉé(¡B”#b @g°Ê“}DZ¦hæ&kN€2+Ú–,ÅVHmvÝn¤k`[f+„]>¦Ê¡Õ øŠ¶DlO—󑃅b5!WuWHÞi?´U“å ™ 5¬Û(" ½o1W¼FÁh¨ÐaÑÜÞ~º˜éœÓ;¯pèã’€þbbè: ¤ûî^XçÖ­Ú4“Š 0¯jžo (‹ú[ék½½ªê꜉ùéšÉ>ɬ…Hâ+ÁðßÎýQ¿løo>Ta]á®ËŒö:uѼJœó…KqãáãœÖã@PÛf{›j¸­„C~î´*¡gPÑ"Ló¾†éd ‘ºÁÑÀLÿ.Éÿ ¿”ƒ@Ðc­,FÝàù·³MIè }OÈÖüƒ ÛŸBv”ª0@—!÷´CÔ€á_8u¹fÝóÖF®]Ñ„ vHîç¾t\X&Õ綸æ|«² isÆްùXø¾uqüí÷ U€7dC(="a­ËKPÃlö²=Ê Ìl…57êd¯¶é˜E ]Н\A©âöbáùk³rZÙͼÕ¥³Tìë(¤CÜ3ù7³ZÕƒí Ó: ‰|$£*‹•TQ¶¡GdœÏA}zŽÀŽ˜ÓŠå/0³ÎÏ|bËQãxºD\¦”xÛoüC‘ž/W‰­[6€üM³ˆ{ãäÊŒïÿBgYAÇ5‡×bG_à»ó ™‚Äû;XÃæÌÀcdõÀ3â1çB)’M’Ñ]T É·­³’gŒ„hR‡-l8ï*Q^Œ¥A·l±!næ©ñþíŸq[ëÊWá´¥Víô]ð4l#YZòŸì·dÞ.-‡ˆŸÄÀýʤ,»±µÉ@cXße•'7‚>Ò`~‚AM»kE´È÷hÁè›JxS;¤ ‘˜å#! tyQ+ä”Òµ~æOPˆd¤Yþ²2úGÅYr¸Hþ£Q"ÕŽÃ ¨)ó†à~Ñ[ ëö¢­¤TûBæîƒÿ%K%IᮺkÅÓzß1žø_IöÒLAþpºZZÕpØVò~æå­¾Ú½Àm#£ƒÅJPà§Kßc}©câ>¹Pa! ,·hS®Uùv-¤‘ R/Ü#¼YÙ+Ÿ<ÐËbƒ—ˆÕÏAư¥/ÆÝÅþÓK,P7 êù¹`lM½Iƒ%¡-¡æ ’^Xjm–Ê#˜XòˆØá¢Ù†öN(Y:sfs˜¯ïqކ_'4o†F9TA›º>ÿ¹U¡þU ÏÆBû‚Ý:éoQÚgL6暢®~-£}_+ï³_ÓØ¬†ê z3,܃†A jAÔ7uí¿R”œþg¿¢|ÊŒ}k¡M¸f³,å¶ÓkQ}A4*-–aÉ,5£_)wÑ)‘•–èáNNælZN¶¬Îuu0@ó‹mV !fËJíêúv¯>¯¨9ÏÜêjø,©Åí°©öUÎ' Þ5‡:òP° 8g³§ÄþŠçk1µ—Áx”uIJ×@œ-IŸ#£qF¯‘†gùQ>®ÒôˇJ‹Sh›¼Klèm*ÅxñÎ0££“H“.©}&ÅKì(xX‚´¬<‚!J †DÙIüH'¿HÐ¥<òÌêkÌ“i½zà‰Œã~B&úv Ÿ=2 ¨S˜?¡ôç®*…}›j»¥ð§±Ðù7 E˜¸Ì¥™ëÛ¨‰Gõ6†•3ä¨uç˜6su‚”ày£=ø3êuéTÓ#x|J~I×¹…}kÅGÏyè•js!UWçè0óL»PÛ7zé¸\ò!8øÔÀþ"£Ÿl¶›¿~Ϧ!ës´¸b~£û Þb ¥œ¢7D<ý¿Ôu:°>I+˜Ãÿî!z‘3ا`=#O8œ°LžÛiÚà ¥$Áéc=ÍfsB£å¶),pó°ŽW}ìo ±œ@ÜÒoòû1¢ i¥_Hh˜ÈÊ]hD\Æaå˜"Ótü@¶Î˜q Ðþ¶êÄ(»¶œápƒ?dY®mŒìÎÊâ‹u~Ôˆ(ÄümO]Ž}w™¥„ݰ¤¯KarZÇ$X¶^Ž|Á±ôÃ}à·Ä𰓤#DÑ© Þ^’ÒÚVÆ%æB˜ðnó®›Nåê_‡ŸÌÃJú?´×yÿ+D¾+(=e ç ý)©Ç2šèÉÆo#D 4rE{'r˜R1Éo|ù.·´:ý™¦Ã~ÉÑVÈ漢ÆR­aòÍWºNöÏô¸4ÛÙ9C£5ˆ]ß:[×nüc&Q2ÛÒ {¯´o‡e¾è¤xkÚH·æ-D@ÎnêW¡!–ˆ»ÞZ?õÇ…„ç™E_ø ˆ8`açÂÑöï^Â~¢É* 6 §ébîÛ¡;$q²_5ö# zNŒi>½?]Ð;N-+ƒ#`Èp «*%#Ž!;–8ùk­ß!Ìu¢ïZæ2Þ9;”QU¢µøoÔ£ù{9*Ddp¦ÓLÉt—ßµVµ»ƒ,IJÚ3ŠoéŽuÔ.Œ‚°Õ¥Ëµà4 o«]RPáí> =ÆÞB7™ 4 ßF‘ç{x9G®[È^¶aé+îϪÖí—DÇrVÅ$¬‹H¹Ð@›«+)§Ip@À‚‰qQ0èMr Äm„OvÈb”‹´ŒcD€ä¡!Ygáu‹)ša 3÷)lvËHa‚$7Zýá+ªw‘\‘Å©ÌHC<:aÙ¬ÊÄ%öH«!º=KÅ Â0T”žª×œYÿ™ìÀ¢®^ª,ͬQÑÅ-U_KT>žÈ$lmÕ¥»6óó}ZôãÈ­0Ëh²ÓZXÃål{O ‚¿n–uO4¬*jd žvZc“+2åNÍCV±¼½œW‡K9§¡{TÌ™™t ›b­2¶2T˜=ëP9¶ïC[Ô¥ïÑÈ£,5É–dVKqhlüe.Ý(gú›Æ¢žÙ+Ì3Î7´¾ê•ÛfÛ¤>aþüŸ `š]0ÐÛ§M>± îÖhá­á6ÃOºo÷¬)Ë?™lsy2;!Ä`OÅ‹’dºíŸÍš>­–ŸOYÎsé›t7mI~§íňÅS—‰ÚkÔ‰ø"¸L)ef˜H®SþäÛ øÔ"VƒŠÁÜ\×p4~xø"·%#ŸH€“ê|Ì£pQ2Öî+ûà +2{ª: “ š놜6™pIÍWQoJ€×¨åJ$ôVÂ5°4«Íî±Ó¬±ø»4]ôbc…Þ“íM÷Ùc„“¬ÅÛÖdÞ»ëo¯Ãc\4Ó;1÷T^yWàêhí´VžÖ¢êjÙæLWh<Šh¶€[wü:º˜;s¶lD'"`ÞZ»ÛÖ7–áïZ©³í<À¶¦ó¿ *Sf¯$v’iÓw¼ kÕ¡ï7¥y¹Z"ñ Cc½Ö[Uäq]›­¶,1Ĭâ6ÛªF[lLQŒšò¶6áä,ø.»W‡WÜ rXf‚ˆ|ÍÓÆeŒs°þ)±:UÉq²‹± U–Q–“ã#u‘î\&Îè- 9À3T"à«,榹†p¼sìag†'î3_µ² ¹fd™U€ŽÌèEä­¬e•(ü_Ó¨éâ|cÌ’1nëa³êІËõØOLi=ž„Ò ¯º‘WnuT9DîSfª¥PŽà[^~h3Ûƒ¬3,¥eh›UÜ:?—ÑEnh÷(èÙï—9@dõÕ .âØ2r¨+cn:€b‰V–¶ØöÝ©ƒ9ó7´ÔYÅB_¨oŠ ¸µ¸]äÞ"MC BJÍ[Ê2&8 ‚‘,žKdá9ÇMAšæ‰M oá¢ó¾òÆŸhÝ6 Õñ» ø½óÀi'Ñmmؾ Ï…ôD~PD+ÚÎt‡Ù+?0ŽËW C3¹À¸÷{Z^˜°q_’9ÎH”¾º ƒ‘§ZF(Ìø2ý›“6‚?"X7×6w.ÞFoÃÅ:M¢F”2昘LÀÖÒ$t²‡m¯©9‹ïôW‰”Ó0¥ébi$Ò‚cÖjS„®–ø%têï7ýú™¬2úÃ!„P˜àÁ*º›&í!§ˆ¾…Éêî—ØçÐlxôø˜³7$[/.š²_ïÇÜ[h›ýŒÞÙmAnœ1âÕ 'ia·›)‹ñòýÿp}`sÈ­³RyBøžPÄ é2;éU;‹‰í¡óœ,ÜbEžw·3¡r\ë±2ß*£õ³T ôw¤D÷D÷ §Ý¼X=OôŽuº{m¨œøÎ),€†XTûíÎÞXÿB?¹Äué»`Ág"ƒ~Qií+©~ºìj‚"îÐå—''ð§Kž‹ScÇ ëvð'lb‚&‚á1Ó¡œé†Y¾Ù f3Êï>Æ}õhÆê|Û®{TáÒj$'ï —®c”’2e =4@~Vë{m5àŸT23#iüx ¾êÅ97¼¤Ô7—bÌš«ÒHI ˜(Ö`ÑŸMþXö* ,÷ÉBÅ Ôá¶ ˜ÏZÎÁù'éøÆAWã=[­§Í"5÷pGµ`±nºüºDüáñv:ýMAý5iõIeó³iYík1C1 ©ÏᨾZég¼knT—ž¶ÁÀ€ñÌfº]‹X=™½ÎÍAŸ q¶äv'kîU_¶ºïšåvÝúYxG{Í þ¼€ßïŽ#3“ &ol“Z]“Î?Cùdž£)9¸7" yÙŠcdÜóNA¡ÛÅèSïïåóèGCÖWÅœÙ'‚Ónóø ß5“ÔÓ‘hÍ,æ| 6Õ’×¢å~¹À¯–ø±Ós~Ln äì”muh£¼Â€FéKù2zćwí´¯-µqœ (ì¸ÑD5(û¼´Ö…›ô(Öê`HöZkEö ˜JW·Vl)¹‘ ·ú) ò™îÑ”tâO±Ð¡X Âpo|–VÑŸjèÉÒô}ú¾F&nÑÑýA£'·g*DöìL7®[^5ü¼Í@u埗-©•°ó>q©G¿ªðlLû¨ Ô&ã¡#`ªOó1¬wãxn«2š· -Àéyµ¥¤û¨³£ám¢†åOÎHC„kœd|`&|Üw’×'ºƒ¼ %ÑK·%$Ñ43 wãf²®d˨³¨6—;ïMéÜØ–ÍC^ŒTE[â1=)‡µPU*Û^®ÆXw­tøÞ ý±:Ðøß5ûŸA5‚¸òØìYòØñüoP”É„-¿“dRŸD‘±ïX½„ÌÄ1õIvË:ÁÎ(eù‹a û ‰ ž!wóÐk”l1Ähk¤ÉP ¶ß9§¥ød#tQ®SZYÝï7I8áãÛˆø^]‘¬a}èx¥ŠÜ osÉSy5`ß<ÕÚÚ-^k.;ç5¥˜á7KfL7yv0ÐcÌÂP·¬Í?ý47iÐØlØrNGãÈè/çõŠ8+ÑÈ ‚j>OŸ'׺æ’Ðó°…·táJ×:¨±>.çt¹AÝÂâþ¶v…HýžÍÒ°ž%¦Õ¤ßìž_š–Š “wÉ!•s7€;ß$©\iEFÀ¼ž(ĈH¯—¸ð“6Dõà:+Yò|eO9°Ëì‚Å” À@ z¬OI¦ j·Œ¬hð¦ù0oñ®_Üè¾Ã˾€î ç˜ÍEAÀDsž®+%ê…ŒH(ý§Ú€ü\-é.C ­€VP[Ížéaž?H»<³åI†XVå;·“‡ˆm©úïKfµ¹9öêj8W_³Z°åà¥}ÃC-Jïà¬r– ©L‚¾Í)ÔâAlN’,ÒBËrêÐ\ñlé8ñµ¹ Éh"F#jÃÎU0ò]¬—‹P1R`fÌÜ~7z¼›PÖfù:4c\‚l35½Üñ‘µTRTAK÷³ÇÎôØfŠSË+ëg×b‰y–éÏß1»Ï¹m~^`®mf†áHÏÞ³d™½…ºvò$AqÙQ“ÒÀ8§>F{(— Dz1©Þ3ç)TÞâÆ„ùá¦ã‘"†7çQ÷º¹8o­“ll˜dÂ:qú‹îþÇ«Ÿ¾h75Ã7wÎ…@w½“R÷™Äd( †ßÈöÒs7Q¹ký£~zU+Ac‹ï½ÉºOmÓ¹÷âM)ks‹Ír =…Dïç&Î#Wvµˆ¢a’q=oîTÔû;<"¶šöö¼¶ú¼™tD…9Kô0zgêjÇÇKÌ Nª¶›,dòuØë©Fš"p z¬â($C c°zõímŠgBõÆëæÞ¶tR](ämíP2nb‹\å¹Ek:ÕôÂhò[qi䘆‘]ßÓ²íê±V¦Ý7BM¦É–dY¨çαÿ›Õj3L!^ óMH HT™…;oo ¸×/ïÉÓ|(’êp0JQïpŒâ?—Ï™Û Öv3\L†YãsÊ $¿ÖK¿Nù'pOû[ìz&^ÏûdWûš×C x¨¯2€šK¯ÚkäüËþÊ)7·š&VÕ ³/Ú‹q£‰AØ–J_ÇèT1¿é‘„ä3ЛÜ`çÂR.rÞ°ù? Ú0²ÿœHUÝfýºïócVnHÈ<2:㲸³2í•c.Ý•Pe2í¡ƒæF Z¯ðŧŽgwbwò”W*H…ÆêWµuÚÑŠ/*~è)»X}lò’óh| ‚“ u!ÉÇÝ‹Î2cË› .4¤çêôîÈHº]Hy•tO¶|-;ïoaà’éGœQCdñû¯µ\©öI5uH—Fxõô-· ¨Á¸ÃAÝÈaüz®š´ «=q™;}†éHõüÉå‹\c&Ë]9÷E¸C„Çèá•§ö™'8\®8*Ók´ënÇŒ`»dÚM;3uJ•æ“Y1Rb©ÊS?• "ËÙ[Ø!)äÁVΚâ¨.’éR70@ý}û'ì‰86ZÇAk3ÓHDJŠSPªQ‰Þ!èú57"·Îçü§‰´Ù­×ñÆh¥\z¤kºªa¼±+8ŸâŠpÏì?Ø:Åï÷ôíÝÄ&c  jÖ¹bå}HèÜ6ɹbîT ­»TÔŠ€$ØkÞxªPd©Ð‡8 YÑ2uò½˜Ù¥¦¡çý럋‡]A/¾N~,]Cü šmú²%Ì…6ó8òžƒã¢Æ±YhQm'˘BAi´ŽwHBª‚™ÔÏ1ÿÅe¬Ÿëe2¾Q.”2ÿAÈîÀLº¯bÌ%ǵx $9Ì2#¼‹צ'?O&~‹Ha>M7œZ"úpE¸u~%Öé;ßÍÒRÚS„Ú_ñº–âõðq™Ç´9­òï² ÀQסÉ1¥üM6 ÇI.p!ˆ¸r™·6†èÂKwR³DzÊ|žÅ^[%MÀ‹7ÝZìÖw®’gð-æë›®{2owÞI™1ËL—«@´øX†ŠA c*8Ò,ž„›Ã¢af|a¹ÊÙ %vÑHQBER3‡Õ ú ‘X œ´“>®]4f5¨ÐšBø{sU’ÓÇJ;i0® Ë”Ÿ{.¹rt‹Eøut³1Œú µyÈÃIùá4ÌsRŒ²ds—¼êoCŠPÒÀÇ¥=ý~¾>g‰ê ßÞº¯†óÕ|(%m(?ˆ–K[V·e'/«ì-׌™™¦%ľá¶ÓËÙ…®Z*Û î›çf7¢­ ™4¸ÎäX®À­d"¼K /¨qêäK8)ȈAÚúàµWÏV°)èÈÛ‘òK°W=7¶Q †ì×`ÃMv÷…*à÷A;Á÷רýÌe¡¦°*Ãé¡ÎãzÙƒCbGz‘åöQ ^>'(rAÎoÉñ¸E›Ðì­ŠEKÎË‚©+ýN9ÍeÇ>èà@Ç>MO›ˆA¬ÿrÔOÐ.yÈ\½Ñ¼dPBŠÆZÜß¾IÛË^“ÿäqw½½“åjfN£ª¦|)`z®)¦;Ga•9oe.ÕÛ]ÕSÆÄž³‹ß)Ú—&¸Go'ý{ІJ800EY“Yó•À3Ë1¶ôÃäÓyê‘àU¬SÁóÅ=;\]ÂP–³¤Ïyýè_P£‡äX, }Ô…3Š¿ÏÖöµÿ°ß.d=*ÂpÞYÈn玼쿥o¡¤À–Š©˜LïR«®y…Ñ™ÅN„ÕOpvò`$ƒ …­P'»|æ…1 ë|[h’ ýë—å>í åŸVì«ûƒç¥]ÀC ®®tÕГ+µ23@j(jï>3?VãÔ­¢0>®.¦NdP]T®(F°çÈ“w»M¸¡üÇ;í¨kdx ´#Ñd®ðAw?'RI%¢ aUÁàþê{‚–,¸ñG¨5ÑŒ|U à<‘¬Ò« ¿ïf2f _GXã¤Z õÔˆVé-˶­·¯ƒg­j«¾i„¾e¤âå×@w­âÑšÑÔ‚Õ”‡òø™Yïo7ïm¨îAhôEÁËT±‰@ù\‚Ѧ9œÿ|3¶±T¨*³T¨òjK|:ƒT*[ ”/&]Ç Ý‘ÛVûíÅ·»ÉóâÖ,Uè.£Þmè\U¹Þµ‚P 3Xòyõƒ¡H\Á.ú-ÌAû˜¦.×~äXx¡•{±{T¸ìýv»Pz0ßzEìÛŸŒV(Ö+†Wt5Ž™”=¡È™pÑ6~"!7hA6Jä l¦³E“Ÿ¸Å5’guXTÙC ʺ9PÚ‘ªég\”gX bRq˜­sÜvM+¸2¥¶ÛêuÔë>J ·°õ•ÐeX»6k›.¤”]ް•Ç‹óR Þêoç¨\tÎFTm/>¥§¿?…RQ7¿zÉHO_þùõõ怼 ¼ÉàjYbÀ:ñ¹Ñ$ä󺃢֥'î‡905;<ÃÌh mx #þûA[MŸºÜq<Ó•‡!ÇE€ÿ˜65l ö~ïGÌà}·5cxyòWßvÛTäŸ#™H·…õ”{ŒÒöÒ½0M ÞÜNñ“HÐ/<*¢›«†£ÁCˆå…<¸ é sÇò»QЩë˜AÔ€Q¿kz?H¨ª‰IåwqÓÌø›úž Úª¶L›Ý57=X¹,h!¥þæQî42v ©Z™YLc?Žæœ¨£èèÌàîŽàš9bŒ#SíØh|L6è0àòx!œC×u9³™ÞïDZ©’ã>'©öˆÇ|ð¹šâ*è¤rí¨#Íë‹:ª×å!ùÅû$€CŸ&Ë—UÛÈy£ã @ ! žH¼Þ]­Ý5G\TFÂ`v{R—‰ÂAN.úÍkÀtS.Ò â(‚• ¾'9qò‹zˆÃŒêÑ6sUkåùQ`óžÎÅry 7sBµ¨ô ƒ& £ÖMnúaD7½Óxİ«õnÓ’úŸŸ!QÿvÜÀÏ7óöXŒî¸á8€ctàQ"ÈÆP¡u0¼F´ªïò'z¡Pˆp±èªSMÖ7@Èišò¡×¶ À€ðH;«<ªÓµ]2ôêû§ÔúóÅ!çþÉáµ¹Êeì860.`ì©lD—®KÄK Ûåf²å‘ïJ#ÃVYZ—cH(<V/1®xÎêb™•Ëiš6‹3Œ, äÊ 𤾴 Ç–xÆüóµ= o×§yÜzô©¤øÇµjú€”[pÐî–JD´n"6W_©Ì’ÿ3f/ÖœŽdÛ•aûZÛbÿ­ß|ý†*iDȲHOž }¹óÃ‚Š +H†ÃÖ2EÈéÄuêÿ—/z׸Átæ;)ù›à?fAª‹™Ø]¦åJ€ië¡‚ÏÓ \˜gbÉg¤ÃLZ³êæ‘Àã„ê-Aj­7J˜dYz vÔÃdÖÕƒòŸ‹Ì¥wâüB½†M²b?¿ðåû °‰ç’“‰ÝX Þ{첉®(¸cžÖ´ÖùI5ÞòñÑG3Žöê§„¹£|‹Ôa°¨.|kŃÁÏgýÜAä;tû§f™lxÞï3r8U ã»hzÌ6ƃòð·!×zð4³¦/r'—IQ A9w1ä|â×`š³¨!6r¯0î¦Ó Ð:ˆb(kç&`:².кšˆ.-ùŒ‹3Hî•ËŽENPýx¾ÞErL» éäux«5=þ€ùå8ä±Ì2ų¹±ña†Ó>ˆ=æ©”,>)ÑW.PÙv(QþåW?-5ãDo KÐÝ5­kÜø† × I«hr³Ë ¤nê3O¤œ.”ü%i½ò1&ëTä:NI'Z OÖi æÌ×û[ùš<Ÿó$âC2HêPa>iµÁUƺ]JKº¨¨Z4¤¡©9S"ÀÔ¾›Y8o³ö·|àÞk3Rù0.çgZfkrb„¨kèy¬r#—§pKÆî\¸­”AM }]Ú–Â=Xù®ûÅC­å8˜…‚‘X óµÞ@êåZÿîQl¸Þ Ùu<ðàpN° ÉÞÊäö}}V©Mc€½*€ÐÊ}œ~¿úh‘õ Ù¨ –œÈ"$Xð20Š&7(±š—¯—µƒþ 䋯»}Ñÿ#Xº>—¬–Ì%ÿß”rôGQŸ‘Î$óßÅxϾ9Ò*8î0c¸-.¬ÁÙëDçýÁ¿X[6H иÐÓÃÈN¨EÕ¢Z^#‰ZŒ,à A‹ÂŽ<¨Éãôµô”<^R!k°%²R²õŲ¹Œ’â€Ñ€~…C3¯Á`;¿``ÍH#…`µƒä/î)E“Áæ"Ä ‰½À“w9«ÈAÄûˆ“ôDIe7 tC¬3n«H噣üʰÐì¹TEƱŒááŠò3#wvMcúÈÀ•òLûΡÏϨåO”LŸao¡f{$‹ÉXLŒ;Vu öÎntÚÌ÷ô{† «Êœðâ—ßÉ~}î>é~þn®|H]~Ü\ü\Ü›}€RßLNAº¤#ÉLf@b·Ë‘!!é-p”¡¡Y2bóNß.ãÚé0}Úv‹ÿ:‰€ „œú¼ÃaD”ÕD¼M¸°{yÕçE?ßεRÙãE[†ÿ…&Œ¼ô>Þ±dËÓ`ûj&Øøˆ?BLŠ—±F¢‚°™µcîfmìVEð^RoÕæTY¹Ì¡ps‰KÉ¿ú-?-£‹ûã–ˆ—]¸óë~fÐLÉÚò÷3ØíÒà~¨ièLòÖaŸ§kaòü>C´õ%$):¼ëÿ¾SÛ@ÚÊI·Ô@ÐÝ&ž“|aà¢?ªüfž§Õy.˜³~YÒ|uÇ‘*ÁNOàT§O¥€cëI^lñåß!Dw?7² Ø}d@ˆÉø#­Ï¤õ$h l3˜hÆw¡å´Äð)ºxMdg⛇™" ö,_žÜ•ýù·¦¢r_‰.äU7g§£}Ø >*ü#N€qŸéõú$âÅÔÕ¹îÁ,+ZbOÛû Ú«òµõœêŵ,ÝSé-hr,ÏݵöÎË©MÚÜ•kĬM~q=kó¥¤8ºÑugoðUZLØI×k5DÜü|Þýý,þ^Bw;ÖŸ@ä[¢¼0=T@Aâr bÁ¼PiìV /´>½Ó”¬5²õÌL+ÜX"f/礩Æ=TŠúøzrj[pF¾ºÜÑæ';ª:/&è5¹æ%5´Ž„T´ó¥nÓØwª¸5­«Ý¨]ÿæô'°h.õ˜žï,´‰ÂÌ¥¶r…–S›?…ì3¯Ò÷™÷kìß ¿Ë$Áð"=e9¢Ú‡áM[(•Ε9¨1Ý…‰Y")U\'†AÂ}iõ#bõÂ'²Ï¢‰87HüvH4L"ØíLPg+øÈ”w`¸)jî±i)©Úêýly],ŽV=U:IlÇY¦|{oßQ¡ç‘yĺ´—V¤,eéÁÉÅÏÉurAPGwÝ€×of vƒt!R•X~ÜÍ$f&_“Õ=݈ÃÈœ§»w^}|½OÃ5W†™)}8xÆ–â?܈›X…e #?’LKÜÔ×ÊÖ¦Ê;Xl0.ÅL\±={ê0KM晇EßZÀ–ER(È&–ôV™p=$³¹^#¾RAQ…®m’œÑÇT:ñFÌ9$¨Œ£I5~E1 .=J”„¢·wRy´fdü>PŒü]c-NRùð|€Á…èZ;`‡p¤üÊì‹®Åþ*‹8ÍG³¤2‹^Ñ"”úv)H—¨ÖBº~ødÞòÀ£ ñÇùÎcxªƒ·öøžA¹›ÉŽå](„EcãhUz«¢?î&À…Í‚!.ܬŸË…=tW:“úž !£k-$Rçï<ªc º]ÿ—¹õ{K}ypDzÄ?F4Ò7ÃøIšP®É²†ˆ/H-e¶VR>“îÏH®ƒZhÎVåZ-âXH7»Õä^omË•ˆ™¾yûHu9Í܇Ìçz]jçÄl¼‡KÈ})ÕqÇAh›u…ÐS'Hî…Læz©ÍÖþæ0lQXëÏ·ƒºæ/…dì!•á<>¡2­µ•nU³æ˜x´‰flaunn¿Ç¸ÿð¡™g•|p¸R¦çÕ“ôY> ï0¹ýB‚0ÙbGF~Ø][t®“0a¦R¤y:­{óV:ýë×§‚“ª"+öÕxÒ$Ï…9 ïû‚êQ§ÍhH¼ÚwÆ&곪–æÓÁÕÄ•›Éá7òfô„_ËŠ8Ia>HHæpœ2¼WÁä&ß5I›fT瘨‹ÍX™c[ÓAß™Ÿç0ç ¥Ã㙬™1†çJøÀ¼ùÓx[3ký°l™ŸCAýW#î“Þ;¶M"ë—ïæ£oFÛæ}„ˆïdûv®–§ªªÿ™ã3É vp"È2°'#¤þÊB/`DŠ`ðU;U Ýdo b/8?j“_´§y±³^Žì0MáÂ’‚#“JWi¢æ?x†tºòòÔl Ã]D³˜`\ãgs-z¦p~qŒgu—®ëŠ»‘hÈÙ«‘2™ìIznŸ)Y¬©öO šð(óÇlQÔ„ôPzáLѤãš\Á]ÜÍ}f&”·¸®—D–¾rßgb;ÄäMØbY.&˜ Ù˜ ŠÖ©x_)íHëbŠû‘âèxº†K(:eÄÉÅŒ Æa2ïøäØãçpM‡†¤PÓ˜ñȳ{ËFÁobâZŒû3±ËþñN5¯Ã&óªäÛ ´ê΢ß(7çü;K Ç|¦ ÌUg#¯ÂS󫕆”„žË0I*Ù뉷 u6àsô@Ž JN]¬µà×/»åƒÙă«Â¶†³Œ.ÊD³H*ØM./ þW›ðjÕ—sV¸‚GåÜ-Ý4–P•‚¿Ót++žg7ºM ¢²“;à¶ÍXDŠ­{rÿü(IÑŸ{Z†ÛïÒüÊ*vÙbñ€t“âÎǃˆk^Ï&GøÞ•Ú뇋0=1Xx‡t`˜‡1„mF+ Ôä5¾ÜV@:NDœCú÷dníìvƒ 5]ïd¼Óï}em(V}rjÐ1\‹x´ª¬~Úø"{öΗg~çC&ØËéŒws™ž°ÒÒÒWúp^•oœ`ïS‹_ó£-•Ýo)}ŸF²VÁ’ĮРkŽæ ^|å’®Ò“TM—D¬NØàZ*é^y?B°–úȳú&…XÜÐø˜9>¢pã_Žñr‡éÚ»=+»ŒµÚ]wÓç\®ØŸn ÐEžë1ÓCËQ ï™~ÍÀ‹y ò›;ÞvŸI/*…-ð¼?„R7 1—ßh6WbàížÅzƒ%ˆAª¬Gµðbåѱ²¾'$rP±gƒ†œŠš L¤¿hrF¼¥¬é¦n­ðãT)³yèFm=Õ|+.u^³æ.,ØEðfÿßÇ42ÿœy¾-{f用çmÞ„jFcöɸÉÁ9?ý¬\‘$«Þ![ø˜jÃW\ñc¤²º’—â}´Æß•‚kø§l[?ÃuÝgà9Qlhà<[j‰zÊBOÓyb°Y'+A4dTiƒ½áÿÔÃ;ÈQpÅ~áj'X¾g÷œƒR+«À߉ùqë×6‰öZ ý{l½0뉵hÚJa÷­­º†í¿qgÙ¹Á´Å¸.7G¿ÛˆZ?£%—ñ‰ºš÷ÿ·¯~;†4³-å¢zç&;HÏÒ™‰ø…Àšö­#öedøãP#€žÆ˜ªdÁö0jVE³{mÈõ‘Ç^òFžè]uä(+TGRF K`• – T0N,'ØgÈt¥°+y«gêMÍIArF±ï·¥á)E®HŽÂMo;R#äQF5Ùa!EðÁ™e-œ…sÇÔ4‘–¿QàÚYë g&$´ÏGOGrñ7ò5ê²n±çÄ©½§ºP䀸šYdRSz»•0“={Ÿ=þêl¯e2;Õê­Uô5íƒHƪÄ<^ø\l+Ê<|ÙtRXƒÏü u`7£ ï¥A±BdªÕ|[â݇5”;|q±yÊ»zr¿<6p¥vÊ[§˜½Ê<Ø?šÙÖéÉ›ÝHSP÷kRB5ÍÍ¿ë3”‹9îk‹‰N•6Ù3ᯠߌ38Ëò›ŒûæÇÔc;à®gñ»‹2\ìÝ%ÞÿÕ8}Uðcd[UëàéσÊ+ÔÊH:ëc¨ihÜIäç|k6ª»DdžýÛ1¸âÁ0zgôPpÖWàpÒÏ0]R>]ÉZë7qˆ5^¿ÞHÀø1‘ØäÂKü-ÓÊ_­‚›ÇB4ªÝÝo ¥§¨õÓ6;wˆûƪC¬ªßþ)W›µðÈþ¥ÝÒx¾ÕD$âQù£Æ{Ü|ƒµ—‡ <.Ì<˜©Ë§!NhEjÆN‹àzžƒÝ¬,`õòî\øAW]{Á> 5–*rSÇ+Ýa\%wȱ^ˆ‰Vá÷\d?3ÀPçáåÞcÝÝѽ8õTµôÓ3µýv.ŽïܰiZR^nm®¹ä!‡âèúÕæC£s©ªd-v½È³øíí‚q/78O3KQ^`lÔàÝ«º]VÝÜ > ôî]†íµoýÃN]°å`ùÃ~c—-ˆ»€_ÏÔ9Œ¶àÌ ÎÉéoBú±ƒ2µ,-¿<“4k3Å’ËBŠ& ©²”lW|ÄBó"q½x}?þíô´ÒåÙ¤ùl7´$²ôŽÈ|Œ”×ÑqàØ ËJP­…uš¦×¸ >[üÕ]¶¦×° >kS\æ¥!³÷º >sS\Æå‘Ù«£2.cq©×ð4ÞBW×Ì"régq†ï¢J\æFQ©×ô4óŠ"3·UåØÌµ£²ïä4Ÿi¥|ÖF™ì &óW4߆qê®yå{ÓûŠùÌuÓf&»4¹Ì °Ù+pégv†ï¬ ¿…¿E%ù̵¤òO|ÆOüîšuj.û |ÖÆ§²Mϸ„6{Uþù—ñó¯¢×ÂLŸe…‘™Û¬’>c#Mæ%9ƒ×´òÈÌm[…|ú:„‡rÊy3ÙÔp6]C®5‹÷³ÉüÕùlúP¹Ïfx‘Œ_Ñ$LÕT³É¹´{(Ù4Ç-󑮉€Áe—×a“„]¾Ôk#çö$ˆò¿Ý£Ji¦ÞQK»×¦ÅJi¬9L'¿d?6X¹cþfô˜N‚?¿X•S]³zÒd?â3|w’ñYª3ÐèÓÝ£;Œ§¯šìÓßa›ÞÉâÔ§ƒº¯h•ýÆwÞêûê,&ãöÇ®Õ3‹x(3 suXOÆ<˜Ì'㞊m&‰y"d? äOe? ×.’z—@•ù.µâ>V­Ky&éÈôXNŽNq7ÒÖd?(ݯ–~WI•ýF[zçÚÔd?~j_4÷´˜Ok_€{[ì&ç2Þ)´ùϵÓœKÅä jË¿á?ìHˆËjÊŽëÚ"KBÛW©ØÔSÓ®bÔtèfÇJV¥ˆíÐÒ¨fÔ{w)FÛ´k4+Ö2kvç“»^šw95+GgV[»`qQëjѬ¦$Úà&–3Ä|M?TzØô55,“µ^F%þYoÙ¢OÅ|:m[¨«³°ne6géqʰ`½ŒªT-GŠ;YLM¨Ð¡e= Öè>RëQѲ›’¬L&úÈð¨×¥dÕðuÌÿcó&cav³³ê£·–¢ýPÛ ¿-.P¢W?Õ ö8³FNZRyhZ®¸€^¸"æõxb¨m—Ī۫¤]‘6…‹Ãß§·\š…•-»4ýXå½pã‡ó5SÿÜ,9iôiÖ;OƒFÇ~Qó­í-¦íR¦MzY½ø”ªé–$çúéÎo‚÷j\´a刕jâÐ׌Œjáð¢ín‚v‡+þ7¶µ¥V·u®cëÓ©LI3ùÛòÛôf­ËYÃïìô¸(yw;¹TúÖÚHãZ8·»Ž¦ttºþ¹ãv¥Œœ¿ßaq}4±ÎjŽÉËÉŒ¥;çÝJÚ»MíYt9\ï8\~^QävAgŽªëÊy2®ÓùÇÆ’Ë‹ª»¾ÎáIhe”nÓý°Ú@-giäClŸš‡ó1>Á.~ò'žË±8Žg d°‡ócaú±0IÃó„5RòHe.œ*ë¿ï=7ÙJ ù½Š‘˜fÕÈžfJ¯f7z}½@¦çË ¨Ù&å&ìÌ'Žžõ¢ZµK/ƒ^$”g1 ¨r†ê#ièâÒ|?Ú¬ÃKšÄäW‚Ôyó©eY¡d§ÝȆwLj³`^¿}ÂÑo rÌ$ôˆžÒö™™À¼° ‘KцEúAÔÏÝÛ”µ·°Ï9*·nlíljGgÇ&î0[A…Ô†•]iÔ E_±ŽVdœ>¯ˆ{tŸœÒ”d^*Æðy<ï#§®N%}r¤>ô*fÚ©n‡ßõ³qñ†‡Ž›ÆGU]“½{]r„VwQÍ"ž¦^T$@|M<z ÿVÔÿ]áÕBå.¿l׫f¥"´á‡&Ìu…mîÈh|8¿ É‘`xiŽNÆô‘œô} f¬G›f5l(é÷-ég7Þêf6£IiN'+#{fy›´Ú–§…¾ÍÎúºØqXñt41e^UF-Ï¿^•F6'7ÙÊå^{ôßÃçèée”øur¸þ<5³ƒ¹ÃÒ¨„ʺ§aíüp,õR9Ý‚=Vud¼Ô˜:²´útËèï*Æàò`äbø9X}i|t´0¼¬ÒTજF§×Ù./-;Å.c&*PnwòOþècÒ6J°:U+»èA ù cDèÔªÿ@ä& ĦÃXùæÇòÚ£a¹ó”¡ Ug-Päûsd“Ú¡÷:|)Æke'ö5CŠØmݯZm{ÞæÆ~läf¹Ó_àïçÈVs¿ÞÿÙ†®¾ÈgŒrû Šl5}€ W~ÿÓËëk&“%ԌʕtˆºÌÓìz: ³ /ýe¾Ú|ZkTfBL‘…Is`Måªn„ö}CGµÐ_¹¬F£bf­ƒ(ˆ«¤ãÝÊò¶3(k¸íÌC´ÞØ(æÝ­¡¥§M(÷É¿¦J›+!Ÿ¿8sˆ”lÓeªog@gTÎ(-Kbú[+ ¢k×KÞ0ej¤ÖQ ¥•£Æ]æ]5°B‘¯Yyô¢‚ =óŽ8§RD_aPQƒ^G—ÖãõÎäÀ§d…€fJ'N}‚y2€Z|cTŸ¢„À‘7¼*@Í–‡6{äci!íEçRýÃß%z#]_S‹j2î°Ñ&½&ÇæšÏ¯æ^qĺ„LÏÙ÷ŒcÏ÷¢c¶£ ¸gôbT'Ï"ÏZ,ãdš×œè˜]\y‰v,%Y«5Šn<®l šé¥ÆÚ±€,IÛã+ãå\Õ‡/ 9g+ÇÝ6›íâß4|m#y-¼çmNîÅFÕò³®õÅw¾÷î·Ü^¸ë§ ]í[:è_š¹œ¹¢]}3Ù{ „ ß^ È’hºh¸ô£Ø"\TÌÑ£§ ÓîQû ­öJØéîÌm#ºëc;ËŽoh–T§©ˆ{1>]gY« Ó€-}ÖL{û¨~ŠA@/6Ì$¾œ5o~Y¹K“% È&ÑÔ~fSþUwûºi™jkº3m õ…­&QKQÃë©CÖ¾bÊľ}¬òñ4™$%jKÃZ&µúËšã> ì;¨ ²µ3¿Ý"D@†5’î à·*ˆª¡dÑš£»}go!»CúCQž_Ó.›ukú+bÍÅçDh¡ÿΖ4œ,Æ5äã@ü÷FÍ1Â1qÞèa#Iz7¬61C£ÓÇ eÿ –ºøjy@Åœ–žÖêÑ' º3äfx¦ú:&×Ó×1o#û=ñ˜è0ÜQœ¯¬ãÏwÇ‹´H½ÑŒZÏ›Q¾ûmˆçŽu1V} 50D`\¢€h„o-—ÜABšŸk`ÂÍ}8•®}Tï5 X úd»‰{{ÅÖl`?ÁÓ²-«F9E* æôõkžÏïD²‰._Jôä¥Û²[ˆ…ÉÍ=H¤Æ±ú‘½ýêw3˜Nh_ñÙ)íûÊ28JQZýH¨Bâ?YIØ$AžÌüÁû¤íV>Î~‚‚¿°·ñô¬@J+£^IwÐrôsÝéE Š¢Ã¦~SË<¦ETC¡¸ÇºÙ•@p#É:iߣѾ ãCY¾ã¼ÈönoîÆY›TDšèJ@˜éb@"&UÂËnµõƒŠ´8¥V-)Ä׳™Hs殌ëÉÐ"ÿù¦Ý) ¾ÙJ¾ûü:ÿ «ÒÇGMeͺHá݆ ëY\E‡sx;½¹Å…ú:îÖÏÍÿ^5þsÄ÷×Þ_×*Ðj4FpTµø!½Eì?]ZTnÇ2©DïJ"OÙ=¨ÅëCYoõª#$€½CdÉ7qæMòBMð9*ìøZ×;R÷àfÿ¡ò”y-½¶»‚w/©5 €9¾Äó˜Õ=á¯Ë“mqèz•\º¤#®ê[7‰2anœk2\óMK»»@ÓA €óåeóag7KÅ~zŒ™Ãúàv>*Lèc®â^?ƒãv¹)wÄõsÖ÷ÜN|&ó¨FKŸJ7_L:5ýxE;î{JÝpp‚|com1§z2èÿƒšÆ¥œŽ:±…‡àVëSš m”Ðw®ñ2VMoSÃmk²ß*ë‘|óu²§¿Ë¨n;<•ÌE‹E °¶ãcþ\>¿T5,;­Eµ‹P ?ÒÔzì ™B˜r+¾ Íê•é¢-eGŠ£‹Æ£ÅéÓ´ÂѯÔáµ[›S?Åz1óíeå ãe¹üx;=ç¼.k0{Á}yv¼ù·‹Ï“ Ó±WA£jYT*Ðê£NøY“‚o5¾½nßQxuǯ÷Öæ‹+]<ö†v~ÌíX”ZŠ‹c…f ºœTªjìYéÔì£kº{ðh2u˸´ƒ6×fú’õ,wáµëTœ¥)ÙlH1õT.ÊÉ«›–,˜qfL ¶WfZøÇIЬun…f7ò÷áou’·ZGHe÷5-Y(@|Ť@+¶ÃŽ]C†Ú½6w aìüÀôOª€½mÑb¼ZòU.,¸÷NrcFœÝ?$Ï0#B¼œ>6Õ±–-œÉQè˜5—fÕô Émž2ÞÁ”5êqâ;D Ù¼ÝÔ°ÂO5Ë,Û«e9š°§Þ³ÂÚîÞ˜ZFSsÚšºf)oL%)ñ“~Wú%4Tc ¦ DkS±hÓj¸Á*5œë,[‰œ2ÕÚLØ}Q`uNwÙ”òSþ '?æxA-çla8™£¨³=ÞBÿÙð9¯ô/`¾ÁÆC#6üX¿ç 4ª2æÃ,÷ñ+°ÿâC¹rOÄ2 x¶Ñ`Wöéê¹û¸á’¶j.v*;÷½Ô¼,T+ƒ{ßE²´ÞÇR“¯¢-W„Ý—.oðÛá}´õb›©gu/…îøèŸG¨]VYS#¥Å‡tNî©?S*bŠj&e·"9)Õ{P ËvTAáÀ ³ë@7Nha‚òˆPÙø¬Öqf¨ys^õ‹vÇMrÒ£ç )wýåŒÓAê6ý{3LŽBA¯ Z²ëÇwG»Ýã¶ã=:m¾Ö×"‹fãƒ@z%ÐGJUJ‰ÔQ'jåêóuúa£ïñl©ñ $ňQŸ:ÓKõ¶¤Ê/÷-°Ì7ïÅaŒ ¥Œ‡­ø¹†àýUª²k uR(ˆªì—O_Z ›ûÕ Œ@o…Y8Ù/´"’UOiªDVù"G¯ª*x›´˜+Ôä¿Xý 3x(7åƒÊ\Tm¦É-*ìMžñ<ÞXjÚ lpJÑÄ•løë@–r‘Ræ°•èüŒ(»ƒy—+xJA ñQêÚ+‰ÿÛjc …ù0eÅè*눔FìéË¤ÓØÎ'Õ Ss+Žñ]ËžážÍ^iv¿þ!®¯NR¯Ç:n¤’Øž•zèŸçÖÖ]±â47ÜÔ~È?0òaVŽ‹ e1OtP÷ö€zD×%¼C>9/Oav^TŠ­¢øjV¹Ï Ẉ6aq[q`›ë¢)Aº\¿Ýîÿܬî·ä|/r, Ǩ>RöõiŽðí¬¡GZ¾/ü=±v Ù­~¡‚ *àJ$äŽ$Ø$B¶KGŽÁq¬Þ c˜¦‚©í4—‰H›Ê0…4©fY»ªÒèU3C£¨¡!}䥌À+¿éß æ9à)Å•Á‰ö&S ¹Û›Y7Ë¡ mHE`³RUsäˆÎ>!ÕÝo69jÈ%ÒŠ³ù¦‚ú3<“ÿÞ•X,çÆ¦àP$q.²] TÂá6ëyÚp °òX°Ò ô³ZÏ…¢š…ã"¡j‡©Ì‘)U*eP'åÀªÇ×(ïŠíJGÙ)Ä?¨¥DGí¹0©¶3u+g“¼³£ÕmÖ‚–ÁÞÔ|ÀXÓbܲ2ݬ6­xÐÃ*jhhRÒ¤Þ«$ØM$y´Òÿ î᠄䮣²…Ú¤xh#Öb+izæH‡Œb‚`Ëå#"â,¼|,ï´{ŠUè•b“£ìqž–îqNÖ s`ô'º÷`Ø\Î ù@ i-v§g^¡G i5¼ià£Ð¤É³'€ø~kÚ"§ _+šK=¨iìY-úÑ¥“µ+>˜¯8¿AõodVt7Ó?õnMÚIŸQ¢‡$ò¥Êi]upÔÌÓŠÆOîÑ͹ Æâ2…ãœR°/››ÅgJ8Ò)§zòhlÊOAc[Ø,øqZ‹PâM+ëXçm-‡æBøÀù8ò1œ˜¸ðÕÒ¨ˆ Wí{QKÍŸ‰Ö™73‹„G/_L¤U¶á¿Ð¯åóù`rì½¹§nßþßkW´}˜d+‘î0‘W²åiÝy¢}ž%ó.ˆÕl²ÎR ;n®#Z`ž¬ŠšŒêiv)|¨ —lÈå!!5±m.§—¦ø„¶n© ]Õ°–-ÿÍzÄ]i‰êxï‚:!´·àöV8…ÐçÄPè³eS%’þóc¨lõdzæ j—¯BÓÖøÁ8„©,>}•Á¤|TMŒ_hûlÙÛÒÞOáPFèÌÐ&ÓÈg|ªl©e켄QfåPe´G¨K¿½ÏT‹4dÑ@Ry’K'pC}äíäv‘Z!uÿà" gÜï ÿŠÏñN½x„>üŒa1ø‡üÂcZÀâ‡(§NÃ|t«ÙÚÒý‚•XÔ¬â@änÌ‚zæñO‡€'„ô6!A½Ö±mÒ^w„©ékÞ×p§ ïovyFOž¹,o›{O“Ôv”\½Î¯ÇVöM:Wr;ä~ëdxŸ„¯T"´§æ\—…&œ8ÊVjÀ“a1†€Ç•&>$sGz÷žòo7îƒÜ"}ë3Ç;¤xVùpqÙfÑ@: €x8®åDAS·ì YTÚHUŒo±0óÒÆÈÆædE\tC—ŸEX*l(o­§Õ­N xL]ÀÁmPåñª¶ªtGy¬oÄšgÈ£í˜6} þšºf<:Dæ¶Ù`1RŸ7Œü£ÃÙoŸ ›ÜP{ÀnJú·TòŠ{Jù×±ÍB1P“‰·SÉÄã&8ÐÇáèâ¡u -±ú˜ 8ËÛ„œñŽ_Ñ•€˜›“ÀÿËAdÄ _P«êÿgή;šTC8™ O ¬±ª Ž ƒµéœdTÜ Q-IS=y"ƒ ÀZÂM›²fмj-z `)†ÈÔ,hla%KoᜀΗŒ懰b)eýl{ H-yXËÅdÓ绞¾eŽÕ¡béúfçpkSè  ‡_`·“›âåt4C­ J\ Á?yñyÁ>޶ðüËØ¬êøjüÎêø~¿~ Þ³]5={Î.®}µ†¢§Ç+Ä![ÃŽðA»‘æ%ˉ-|psH+8å÷¢pÞÁ/ ßìºP€M³ïÆ×Weµ9êý™É"þDmKËŠ;0¯‹ñ2Ö P‘Cr2_#q«Ç­zDŠd°¹Ÿ+¾%9±å‹¿ô|‚ËÛ®’µW}ñä ’K„ŽAÖMòÛ5w|‚›'é;í{eÛ|Xé׸•n Wj¿<¬2ßWÀü‘Þ~‰å B]J£%S®á“¿Äý !>->B/]Ö…Yžcܹüä>Γ.‚ òCº‰w/(:ÂØÉm匿å +J8üZš‰»Š¿³$¾Œ§?ÿIQÓ9±‰§½QÊÆ¸HŒü9¯šQ³Æõë|‡Êïs|–È9Ú†A}‹?ùüÍWG0rç yv‘Ðr7ý<ñò¯×¿õd‰ªRøîJå,«yNüS•75•‡RÑZ#Ü1du™º&rþE{ÀCõ^- 00Þok{wɽÀ’L™œIš&£h‚øU±(>í]ÿFû®¦Oqumø†é¯rš­R—Àô(^t'§G-Äâ±W˼ôڥÇúÒÉ@ï­†¼M@ê‡â@ù´-IVqÚoCÃòSê|§’‡ æÓ\²Y? pg¸t·þrŠ/ô˱–î%·Fmw~!J,)ê¥Y¶¬) öƒãü”lŒŸw†¬j9ÏÚ‡”LÖ+å!Ëmó'Ú™_´G8Qö¬ösªÅ­£¶½aÛ˜~é_öG5yídl™<·žey¿wÜ›5ãóQÔ“ÐÎW" ‚XˆE˜Ë· ÈàTwX öæxiõbìÿ¾B~r~ˆ±gzTŸå'|×ĹʒàÂ)"%Åú/8ÖX;1UîF4vM‰¢óT«8òê©Z›E5S ”pg’“rhµ«jjQ8Kë‡èU ²kÏš$Û@0…-ÄÞrþ(´hêÏ ”“ñ„ZUÈ/ª º‚´gW7ÿnÞánœ•+>ÛG§l¶Û¿oÄ'̳SIYתõ•"î¢ÓG€âµqð9z]/vXÆg>o'2¤Ù‰ÿ-p˜R57œëøVÒÀSßµ«ÑÏ ¬®KhzoCŽH-ŽËšÔû»J„p禫U:E`¶wÒX… *0®pz&DŸÏU/k¥.B*Ñbú7ü—Õ¦™¦Y JöટÏÊ8mQkË™úIu¾«à&ãêy2x(ýÞÀ„9ù ÀPÈ祭Ô™ àvJ=u/¿ööW™0›½ÄYÀ*ïTÛ=ÑÔÇ_ï(p^oñË ’°øˆ×¿ØËÏëŸG_Ë ÅÄ-éN\Ê!Áé¶ÚpO±¾ièÙOO—ôŽÎÙœS[7Ȭ ]@GyQ×,ñ”LŸ$³LAzö¾s^úÝ:¾ÅœMOËÜpÞƒGÅRmÑPÈä)#ý¦Ø÷³ñøi"¤pͳêAzCQÉÚÃéYÚ­›„V1²Йz.ósç«”Py×,Ó$CÕ´+–$ŲÒ^íÚÙ“Ã[KN& ™âÁCo´ì Ó°MiáÒΣeìÅØ-c_)ÚûèlD¡±jxžu€-˜šRžO8 O-²ˆÝ›ý£þð}óÞª¨teSÞšòu< »“Y  Ç:\.ÜVR9ñuÄc~1^×SOgè‰À(\óiŠ9Ü¥WŸ$!‰igq·=ºÜ© (-ßK jiÏÔ˜ìPj:CóÇ»ý¢«ì—3¬üüq ¸-úSD* ²‡: Dí4%uÓIàX@”²0ø¡tÔIŒà¸é/h ]±¥Ømé¢$ü<5*I¬ÃIò·Y0‹Âú%¬€›k¦b8Áƒ6'­TLŸ½W¡'GèÉ5!$šr~Z˜çz§äîrá&X¾fì”v>çø^æå(0 v(¦ 6·ÍV%wûžJ|ËŸD÷KIgÒùÔ“a ¡ñ›“Ñ3`çÑ ÆÝ޽‚-tÀ†"dó_”‡Žhwçnþq³`Ë·ØSÏçׇÁ4NnH×rHugQÆzï‹^éV$FJþªHù²åòDb$οj«‘fϽd®Lšó3p*j#ß-ºzÆÓ(þ4v ¯|dîÏÀá¦Å¬¿=D;Üü/Þp2øµ“ýe ¾Þ{_nTr n»ÍSÕ¯yQ‚´¬íì¿=<*wwïéó]ÐáïŸ@wûÌë—âã'»n_£L·ÀeTdh©Ç§H‚޼+J3>zéÇ»‡³ÆòH8%€cG]P( “êc† Þ”…Üò ëùÉ{Ãoçx†øÔɈ’AXíQ¸óOvÅUŽo·äôdõ8ƒïmTÝù¨zyôÈ„»{’û:à'|Á=õfaßG1¸4ò¨¤°½Q>²;rç"›m<¦Ï^M|Z"a—äP.”B ‹L,VÇCµ˜µÄ}Ê-&°*¦…T°w¾-[csY¦ É¡É H6®Z5k|^Ó2f­;ÒšŽØî¾äI°ÔŠoYTÝoÛT-4‚~:éòD ‹ëÔç2ÐÕÊôæ3Á¿â4¥ä¦KÈaöl#4¨pÈ>ÁÞÜ.PEu/˜ùxyýLNãŒ|µqãAE9‚•dri‡[(A°"Ä yÑÓ”¡µ¯hÐ&£I¡*E$ú|°^P²Š =VSÊh$´ƒê«DÓ¾ÔÐ+ãø·–´GòÕD?)`óRoéÀ‚Ò×;s$ǵ,MB£Z³´Ïö²NqÑAŒÐ³Š‚”ݺ5Þ9§8é4_=ÃÒ™^ŽI,>bÊçç:GC+ 5ƒ,\ÌÀé礪eI)Sjí"/‰õ¤¨&V¼/Èž+?Tnpào¼~Œƒ¨Ínha‚JÜôØN’3jÊ­óŨ¿÷´­àcmk/piZüvžÁÁN7„AIPùï“ †XöžÁ¤~™òµÎìä°‡(‰°-äh’ÇF7ýúÅÊW ÅìíÇ žÙ~b«=êMqÈ[çb«ëÊúpm«A¸XªšÆlx ÷”Ô¡œÒ¬ØÇáUR|ä§ìKeìB:U#ÁÑÒïW;à ¾ˆ6ÆÞ_œä3y7ŒAÅ!¸Ê²»¾ü ÇçÉ» k°ÚÂéÉÇêR³T¹òc!£SIÔzFiÉjséÞ1•/aÁnÉ'¢Ú G¢‡~›M^×Π¯¬ÇÍKÄÞÛ»*ðã¨?ª¥OÙŠéWtbÊë¾æÀaê*€Ø%÷( ?ŸÃBxçŸ÷”À‚òVÍËmòHöóKíÌNmä7´‘Èø#§Ý¬—rëšÆ%¼3 “ÇàÂ0!àñ]4hóQr}È+Žõgá°¶2'[\Û™÷U䦻~Kêb­iÊÍŒZ1Ä×V.Þa ³c»IÉUõáæõ[$Ò¸r¢“\>sÙð¸;åWÇÉÏÕ}|¨‚w ùv:å«ÏM¿Ôzq<"ÉÆ?t7í ¨é@pÉ5äöi©u~§-z¦`˜a¥[]d"?‘i‘÷Œ29êZßzÍt §CÔ€¦-µ“+!…¡ÚX¾ªS ƒk Ü_có$@Å_]¾oÕªŽ>^YßIvºG1¸vZRhëäÓ±dԸѺ0Ëîf{õSÝ=V9Ò¶TÛòjP‚À‘S sL”s¸lñ†3 –8ì\€Ãø¬î$„}ºg‚dl¬Ë²í©ø¢Ù: ó™´ ’Q;„f3ݘ4t}TböÞØ 2¼”½V}u0ô«qÎË/.²²þ7•› " õê¶­æÒ‡Ûb™×d°0{HýfjˆLæN¾[*¶ïE©á’K '2*@Ïñ=ør…wç‰Â›jøÈ^0æ ª-±›,­ÞÁ“Ù(i ÈíШ½¬YÀźà  º8íÀšP_lÀá²p@€êÀA0y/Ù(ë.ãíP‚J„â»ê!ÍAróQ¢ôbÕñ<ø2ÿù™½ŽN^œF¶ƒš%,ɉ³ïxU÷dxÓ ùv¶TH¼¸!û&I—™ •è róéÇÞŸG]_Ϭž}Ëâ5ì‰Z¥ÒdA…t†ëJÝ"m«jÍ+”^ÙÃ'œÜÁc1Á6'Ð-~7ÓCáÓ“`! “+~J ¥ƒûºl¿ 9w\¨(TˆXE?C%­1\æ²X’±¢²»;ÉH=%¾f!‹ÆÒ¢(E I_r³»]8üõôÈÉgh4«š¼üµ/äèhÒv´GñÇ26/„yÓëµL¿34q <øYÓ,\ù\DLîà¬/„ó‘;úgcÓwçshSwgvoÒszûî¡5Aø½Ù bò1Xþ¼Í¨xþæâøvJí®ºÛ/ѰÂdêˆ`Ï4ÉŠ…é/íòjk•š+rÈM^XBö·`Kß¿/Sÿ ÛžãKÊã?œËM’TvC\`~ð¹ýøVZuo‰yPb8èZöAÿ¦É>º%Ý*ºêó=‰}üêÖ@™ò=ÄG+½>BúÍ¥;ˆf\K@ÇŒ\À:íL?¤õºá ÝÍã£ù;³ÍÚn%—aðR¬ˆî;îøv¹H¸°[ŽTør”/ßû˜—xp}èð ¥èåÛ&$C À°^ø…Œë«•*þv¿puw°àíGÚ5MõV‘s]µ<~b{-†ý¶ÓÓôƒÍeý¥ðu¾ý<ÞX'…\{œËÍ \Ú¤¨¯Ù®f “à À(Bä5ÛRýšÐ%%‹”Ä4ª@?I“¦°•H×Ë0 >XΪfõ\`žþ8+ñÕ­ÙòhcW$° 9ÉU·Èr ãæ¿]MËìqÊ»-‰ÊÑÄn]ÑšF~¦ÀJezP·'-]¢Ö3©ëoÿż xt:„eSÆ^UKÿÞ.žÇ<»ÀVʤØZé¼ÊZVÚ`ËJõO ñàq% ¼bmšŽÿŒ Ÿä¹q;´{¥¼ùpF#²†k ?1TÂÄ¥‘gBÁŒ°¡F]Q”ŸV×Çù<¹P³x{ÃÅÄ‚*¼¹—=Äü(?žIU°O5öU™°òͦ(¹{£kHp$@ƒ>oñnAùµù\žÖ£>qÈåÛCèÕ$ B’Ôÿ˜V 0-^ÏF^M„ÐeÂuDú’‹É@²èúÙÔª}™»†uX'ÐÓˆéZ›ÒîÙ{†on¼Çõ‹Õæó:©>±¶7Ïή+ s*uÿòî7vokç.;Y‡MFç5ïÂþ—S°9™Ùê‹>ý ti`³s[FQ Zç/Q­ÿšÄS VÍŒ`Y,µ“Ë.3õŸ I†­ ùžŸ—î ~Œ—b‘Bæ³1Ï÷gÃЊqøÔR¸>Ÿçû÷Är ´Á±­Šs|g£Œa\/õ:9ººöšËRy·Éóù³è”ÍÛq»B F”OÓšŽ'ú…:®ø¿»¾‰›$ÄÃÈaþÎ!øŒç ,ÈRzYTù<^öŽÃXq‘‹\?pƒÁó:‡wBc`ôJŽaUѸýý¥Lºµ4Õ`ÆóUgæ)å^”®¶n%ÝÕ{Q‹R©‹—¯<ïìõ«à@WÔ6ç)Ò¿š"Qh8Ï–"Á¬’žž‡ùWÄ¢M-“‡ìè/ôšuc‚i OPÓkèWÓ¯Æ-`Q”à Ï`¼ã'¥¸ðÖkïpÈêP[i’·YwCctb}û@2Ʀr?:®›˜›ó€^ÑK=IµBͼ•ÕÀO!ó^•#”®ÊBÅ2‚NÓ¶£†{á6T ¤.‚Æù˜Ë>lüéþ È06sü9wwÞ$2¿¡2 ˼ó){ì÷½>ò~â¿@³Ô¶õIGÁ£2sÌ/¡f;õ3ÞÇ™ÏDg‰z.ÌG¡†|ÑxiðÄw†Ï!”¶LÑ|ÈoÁׇí;ÝI¬M[ºNQ€iÛFó¤më†ÌÚ€£áDpѺtà£áŠD 1]ŠØ 1Cq… ª·¬w&Ó×z¹wG?_ Ì m7æG€lx(,x";(Ù<…”œ!”žó«²´éï„(’ ìxb9ÉïˆMïÜ>!ö<דÅ<§ÿéídqt;¥¸NF2ùÿ¨"h¶ÿT±‰~™OrZìeSQ-Ϥãªcyв_$¹üÖ&EÓ\>”Ü®ùUát/>«¢ÃcuM“7Ћ‚Ã# 6¼é+§ßšóm߸¡Oü“3RÚ!|Æ£dȉ§ & ‡÷eýŒb8;Âê¯åiå¿ .ýcpV‹d0QÙ§÷ JqžlQß´S'°ÛF¾VoÈ˲W†Ì’É…>±2ßSÞÖè™ßáË>”ÃÇÐB2…\øH}¥ŽWAË6òW˜*ê³íî„Ö™?/¯[&ÏÍkž £÷,¢Â][£k?†cQd'¢¼ŸD°+ÞtýÒ.‚þ,ÈÛíý¥„Øg±_Ç1‹K¦ìCÕÑ|‡ºÁ†u‹Ö[-¥¢UÓÔmµLý¾IÛ.mµèÊ›)~¥D§‘8\à«):aÑ %ŸBeà(>q‰~¨^^=¾|ÉèxiØxôÉW dãTãŠf¹žqÁ ^O½Ì° ÀS•ÑhÎ֞Єa ã… ½8k6Néï\íb:A‚±¿©Ä: _…ÖÅrkë\L§ç—× °¥_îÈ¢‘Ïô¸Åá öeûVðê÷2Ä_Cð!6—òÄO²\A…j–¿>š l”¨¢_݉6بw1Úo„¦2 5]jµˆºpL³¤l0\D¨n ¾ ÛØ·pŸš ðàC¼€J»weßf’(hâit3TFL¾“„¦+ÀBÊpIÕÜNäœØ0¾3—À«uæÍõ]¢Y¼§+ùøè¯ U™e8¬•¿uÇÅÕLdÿÉPt[ž•üòÃ<7„>‹ÙÈlÚj0‘¤½DÝ-ú±þÔíÀJnÅr £äåÛ?*u‘Š Pn¶ÛöCx'ÉdKÈð¿)ÀÍG×PÙ‹yëeAev1sà*” =ñZ‰<ÑÆÞþcý‹³Í“´ÑÜG)gÁû¬ˆº—â‹Xã ¬&þRÅÀ' ÆØ#^u}ŽpœÖÜ_Á’½­×›'1•ص¼sÈm||¸Åþ_ÁA?¾—2u)ðºmâÃÅ|/¶çBýôȧ&À ª¯jæu£Q+_VäIÛAkŸ9ï{ÎæpîŸ"'av{°GÁ§®ò0^èņ;éõ"À/¹Ma[Ä–jŒWO›P-< eĺ&.㻵 Œ«>c–×ûî¿Ò¦Zù³›- Ýq"TßÀfivV2¿šá6?ô·)i»oºßY~î6ÄØ!’NLmƒfÌ3ÚÑßkÃDˆŸÎžíâ­mœ©i»§KëÓ5ú[óþ¾I÷<0H]çˆ ”ŒsÔ¶,¬¥œ-ñó»¼ÏPžŸÆVq™¯^ÃÆUNoHùPʖƩĦ=Ò7óš<Çφ ãݧÝQˆ?‹Ñf2r `½aË2=æÛZK"ã ÈIFž‰,‰+¢”fÚ*L´ð“š6†N½q"ë'§-õùªÍ/‹zµðóÏî¨pBGúþ£Mßš·n3‹èƒdQD:BQÇJ|UawŠ(÷Œ˜ FŽ`ª),sŸBb>¬óÝþ V¹ÝÍ,)ß“í™ÅÚñaI9sôŸˆ7ŸSòˆ¬Ër˜8ƒNè2"ËÞÉ‚§óNvUdì0A¨ænÅ<*€jê>õYÆÄ2gº‚ïØºÊmla\‚˜G¸™Çs{a¾Ši6n·í;óMbýOœ€3ÇÛü{”/‘WƒùNŸ1…ù´`8ïôM+E8ŸŽ'0ê×Y|>Aÿ(à†sY£çYl)’ÅGo‘”ÑÀÖ%–l*Üw›³6°(a¨ª×Î~SÂÛüŒ«[¡›¹]§Ó"É‘ÖàÚf¸Õ592¬Gß²Šžc?æM“ qÔð9 ›ÅîTõÚ˜Öé]¹/çÄWN‡/= æú ‹ý‚ËÝà ~î-67gq“öûO-n³›%²§uŒ€ ÜøcÈjö°ön>F\Tš3ŸòÏ”¤4ˆ¼åLÕO¼ó8¼Ö3WŸöÒÝgñµ+GÏž33ņsK½[Æ¢˜y³´ ,ž?f‡é¶A*ß3Ã× ±9×cö74·{b£¢q`•lÓЉK6È*û) «!=žÉÀ§5¢efíÆ6­¼ÅégpJL> ³D7ZPÊ3”ÎGšiZY°5zêнjãv4ª¶4Û~lTòQR&4Ø…0ÚúOkÑ©6ó{Xš`ú÷17Â&“¿ë'Žã¤/?Õ°‚*–nˆéä…4r&HÇxÜÆöH£æ *YE¤ÇÉM€É8ÞBÊ UvÕN¬9 9€êæèV®dsßèûÝd²õÈJ/K' š„ö¨áq™°…ÒTŽDÿeï$ ¬á­mÛ¶mÛ¶mÛ¶mÛüÖ¶mÛönþä&9U§*ÉÜ n§æ}Ÿî®š®$þ‘{®ßžmõr,-ƒ’ÀP¥ªÆY¹EÜM´§”2ú¸ìïùV-‰{%›¸¢Lùè~׆ÞXÆŸã`&@ç‹aqõ"F×ûèa?W,+¹ÔÿéÝ hhé¤òt?–á¶ £ƒdãº;U½ç™o5Ͱ…%ö$ÃpÏ“¶E©x¼›¹±dã0ùžŒž†¼ØÛ6}1–4ž­ÚÀÚ;ØÝ¾±©gïßÃ<ó/ „ÈxS~9Q½›ðçá K¾{\œ íö í- r.ÆÜS(£’g¢hü –ùtaϹ !ºÄÜ+ÐL}M,HÓ7ÓbŠï3¾œËÞ—¼e*ðû¦pÉ^ñ¯ÄÅù‹­«ü¿k+>Y•^;fîÅ5èÌ7à„HÁ¾+ËŽup‚(…»MæLÍøGÎÄ‚©.ÊžY‹Ë€?ÜÀZÊýt·Hë]7¥ È>ÿT\{î´¬ŠÝ\éâ(í.¾5¥ìþþ)à.ÓN•’Õ Ág¾Í¾ˆ*¬`J=éI“>¿M"kX‰«ìiÞs¾Ô¯yWmypÞË`Ù_ÇŸrjg9˜Ìf?1çb@T]þ°j-ŸÓ®ó”R»_Æù”šbi0$s41)J!Û;âqÇ|Ã:Ä#ˆݢsž44ÊŽÌÑ„k1¼|¢–ÿÂm;·nµ4^ÉIÄ™¼“ág.oPWéøê¾×ãíº%ŸCMy!ôéïÕ•U8ݶ¢îk»ß²Ž»àr¼,ÑzIÛÙѱX »>n©ŸÂWYRHLYÆ?VZ¸bù~P¦ BöÎ~ƒæ _˜ ñy½ Òò{«+`í¯Í6R£ýž¹y¥Çü&Y¹d>40îw(Θ?Ê]— $ûPv“‡‚ Ôǯ¥©× Nƒû’P–”êîi`R«ˆ;Ž¬Ã˜3§–&ߥcÃæHw˜È¿ ‡Û·GT`²Àíœl ™sÏ Yðž³àn×j ’@ïÄÅ&4>×" áô þ'$ µ™£ñ»íÕT*åJ±Ú¨g»j¿j©NK§-§¥Å+y„;ˆù~ Ê>ùH”©O¢S;…4;»8DIÛc\¶'ÿíÌš±ºAæÆ©hÛ~Sôp¥'ξ8gÒ“ÇB¬£>ÿ§±zsWŽ=‹÷ø½‡ûÜ–.¥öÒÛùC5;ZŸãtÁòª,ëÕOxÒ<‹Xþ¾ÍA5 ó8í”ü ®k¼Bž°$hU¡yU éÓŸ&*ž‡0?Î`ÿºs›Åm æ‘x‹\˜»=³ZêQ~bÛP4äåN`ï mV¼Ô@-0e²Sl6à‘‡×’ndǺ½c[_4Ú¸ÚÓ½åëðÜzgC×EBhi˼MfRÌû!}! ‘ ÓbùÕtªU¨2Oã‰:l¦ŒSÌ{¤gBù°[,]HìBDK—ìˆQHœ;G°\ÉVŠ—¼©@C÷¹·°¼'çÉ•ó«D³Åî´pH÷)Cõú±ùºt7 ¬»äeÀNNŸëoID¥Ç¡‹ÓÇÀìy;¸îÁ˜•7+'S™«ž ÑSEvdMýôDôø3øXM|à?Ø‘2|N4ô¤†{c&f;l²cù¥ ³YÓÏÑ…5å<’ºš³úˆn²¿Ü·èW2Y4ÉžHw< ð®Pñ¹Ò#È·i5Bv®ÿ5äW"Ž4À„þ,á͆ƒÓÅ2vpÿþ•Ô²ˆ7çW4ÄWÆÊÂú¼CEb×w^…A;Ví´xi‰ ?¤(¯´À7àI0•NñÄkmæ ­ÿتz!g”j´Dùï‡òRƒwv\' è8 ì@ü.ºðƒ|qvy4Tp]¬½ “ý¶¨ÚØê8 }™kgÓŽ8ºÖ>/Ò;Â6¼Ö"†/CR™˜([jBjJÓFš®ýpßÐ *¥òa¡×$@èáæW-Ú"“çSjÞ¶7>´kdhUÈ¢”¹•’¿«}°oôúö*¤¼mb4t,ÉüªÜ\—Ÿ{ÔMͳºËÉÛ%¯c¹ÕÆ-ÐŽ{{šíêQu€MWLš›MÏRxHv˜Þ~U¤D÷ã¢ßè(n÷ž"íï÷™Ø*ÛȈX½ñtñ™r2€%•ñˆo¶W유dþFyéømiq-N7+JY­ØDomVªA¾{>"{F•JÑÅód¡mNꑸ¤,…އ8¾±‚RJÕ‘–Õ‰_]x»Î*ÅzÅKÓrü*ÐÔ6»–‡ÎƒO•Ç[3¬îÛBÇjZWÙw×TÞ¿w61ò<µŸêêžÎÜly«o6Ø}DJ7ù S|/bH)a€Ù•sY $äÄg‰.tqd’éÞþHÁ&–àÉ›¦M„4–Ù° …U „¹°ÿÞSþ‚kÎ f\S¦'þ_. ¦”»²§_ï¨FkÐtÀã-æOa˜Xy[Ë'Ñö9Ÿ:tÄ({T«×sq^ë'­«Ý®Ò?wÒgéxÓ80xsסsöðÝ?C³2axïì(8!­ ëÒé¯vf7·mæ;·uBäx³ïË4¼q|‚SÙoYiÎZ5 <h´4òû!ª ^ŨÈ^:dèé^FÏZäNå5ÅîóöÌ•}¡µ#6W_&@/Mžò7Èܶ•ž'Á›H•/H•°]¼ù»Þ,zV¨ª3Žš©Áû©Aܧ»&ãîaûfæû›ns0¨6q/&gxÅ­9‚« ¢ä²Y¼$Ãç Eá¸.eI‘[_pÓ3¢Ô¬Ó·pN¦à?-s%9ÑD…ùoYæ”:Ê‘Ê>¸?Ô8~t ·£Š´ö´ßh[³^žÏ ÷ªÁ«Â¾¨»9gÄÕ ›$­|¦s$§¼ØB Î ÜPK +Æ©/hM5W ãÖ.ØB Á=îܳÁ©á”ÿ2Ø Ý·rPˆ V¸èAÓÊDâ½#0%FMËÕqE0w$Ï °¸i-:d@²‡Äž©ðïé¶qøN#Ñþ8Ë]Þì·­(~½ˆ¬)›V‘|¾}±É>·÷I3ÈÓy–•S[ó¤°O/Z¥ªú0ɨ¾–uC২º±} Ö*¡ïtŰ:d܃ܚŒ×QX®[Ýc»ˆ!ûÃñ§Õ†Æ€ò'±ÔS4„M4 îVl)ãî§*o鮾hr¡«ˆì˜î´XÖgxÖf9¥H=»oC¹ßIw7}³-ç{؛ؤ%›Ùÿ>\ž±C:¾hËëÞÀrªÛݹ¶¹”ßæ÷k%¸Á_»Êan°•_‰ÏvÌ‘*CkÊÝá¶Y5øZ(«hØ•½6Íåüž]~ÃÜM¦sªƒ–ã¦vÙ¸‚—…/²ß´ ­õ‡£Ícˆž¬yÔ žÞDUÄ«¥A—ªvS˜’}Ï^rÍ&Î]VRp5âÝgÈvלóÚ–ú±nj»uoµ]“7ïôø§h¬’ã#&S}%ñP4Ž,6<éÛäÞÔ£½YÙkÍáìfQ¨—{»é·¯›7úYü¿•~&ÙeE‰•ç8Ð:»šáÖÎíJ=T.µþÐT¥bë „ÒRíÒ2$úA}ߺ֖¨˜ûqà¸,ùÐ /AÔ&¬Ý¨ˆᢺÐÂ2ñoMší•™sXs¶H.`èLY¤‰£Dšb…Útê'TÌUˆ¦ƒŠV ‹òå šÚMv6´ãTq1v²2þµÕ!•±ì«ØÁÆòÄ*EHÿz¿¦¸ë¸ÔùŸB»!S½É‡¦yÙ-½ú51hæGÓµKji EJàd߯¨ºkò«ÏXIÊ#±HËý̦ðümÛ¶C«G@,®(Záâ%Š›P-Í)³–iM:Ö,6:d˜€,~Åei]Ôo î­ã-û˜áw¡}ŒL{ë3³sð3YªRªÖ4³P}½*‹R*@P²/3ØÐÖlp¿¼ÚÔ£­Oäk˜¢…e«t!ü§p%é*• zÉÑ:…Zärþø‡_üQ( ‡Š(ÍÏ@¦^—ÒŠª8ÕÆ÷ݱ4tfË@2tÌCu©xðBºJ®¿*Ý?–Rhµ—ãA˜)åJc½¸DhÅX©/9&upàìåvZ\¬Ò&t6àòˆ{ªúuß3«…!HÝ,Dêõ¯øà?!ÝâþFޑ膟dú âØ'Æ} ŸFóÇÃLlýûÆ~(¶ 1ÇêàtoJÞ‚¾‚{¦y/ÀâßÈo8­DH7–—kÇ<þ,÷Ø8Œ§Bån;5ëH4²ëa؈b…]óäž{ "èGÉk#jiØh˜sþ~^i®p›l-óñ'k4“Ê­ äköO`³‘Æpåàyï9õ¬]%Ìîã¿×cßäE-q.$}ÒYPÚ¾ÒùÞù‚Î̆’?|ù÷z:zg€šÇ½’wxÅ“f„WÆ,çö ÆÞ]nW¼)›k§×Á9­‰_Y A¯ÑK Áá1Џ´Jjìì´~®ë²¶X°›¶èSQŸ;ئ €zñäYˆœú¥\L¢Ð¹Û_ûmÍY”§m»¼¶jLl»…ëÂW||–Ø’î!Þloø¹*ýe„xî‹úÍ!‹p”ðåÓí­Õd´iÁ‰¾wôTͬŽnܳ{OPdbâ«$³‰@œrºz=௾SO”Ÿ…€0áí"‚|åÐíd&²Ÿå\œš¥ pY¸Üæó'¿Eq&® á·ëFƒž™;—F´Äü¬ð6/«ï*þqÕ¿ÙÛŠ\º§3Qñr{Ýâü“•¡62:\6ö8“¤:0öÍ*è›JšŠ›ÁN)U¾IÕ™äj{~kûRâ)ë)pV­ŒI}ÄÄBCÕÄ'×î<9ìýs9£Žêaö$)):_º¾ï0Uz{3Þ“Þ„aÇLï”×AŒÅÁ8 ¦ ˆÊ!Hˆéô¶cqKk»:CvD0"O)fùAUõVÂu35ì¢Ö"¹­çó›(L+|[ÁõcÛçYžõ2îúž­¯ ý8lTC´³¨v)ØZ¾‡M‘g¯‡“FçòhzýÑ-žjä´×„s“I¼—o@Ù@iŽ~ðP³b¿k'®f}ÇËëk‘‡k“‡Ò5Åx4z4€l¤{á8 /Ü[·é8 ŸkW/;’J Y).½’/~~FVV7³";£Çéç)Zé…´Z6õºÍPVE—ÞÜ?x ±F ò¥6™ó±„¥êŽ+‹Ý •"¯Ú`ÈôÉi­ÆV¾Èì ZÌuû 'Êä'ÎñÍÏ«y£üÕ#ê?gœ³Áp'ï ±“Þ[Ôù®&L/ b)svÍPÀm`ÿ(á@‡_ÅêÃ+¯÷qÜqËInÛA|Pz;ãVí ›P=0À™þ‹iZP;Ý åhY*ñy!p¼ˆ¹üÜœn³vÍ‘”·?)NôvÚSÛìF(ÌÅkàL¾‚ôŠ=g‡ÍÔÎÕˆÝ{Ûh%¬~'qÉŠºÓí3XÕ̯†ÎÆ›]o¤bÊ_<7eiÎ혫Þ6ÈÅbÇ‚%N’ëͳƒurür[l6tS}˜VÓuÔu îÉáJ¼ÅÈ!éxj2ϨÅâãœtÝýiM¶òàv…<›››+ƒycV–Z/8Ô/Šõ ìçfU£À‘ +õ8H”dYëýkˆÁå°úY «êУ*=š­ÙöŠæ PˆKªêü‚«ó¤Äb@F®œŒ­Ÿ"Ú òFžª76ñäÓ?|“véÝ›Ïí(„P=l‰*HylTíŒî3Àk¸ïÙí¶˜ß¬X«>MÓPC0Ù %ë$ñ(¸Ž ê6ŸÎƒ^Ù |äåʃl‡§Nºu;1’+ˆÏH× ¡3úg{£M)ð jô i_\vÝ‹SCÜÎ*YufZI>Ïuˆ€|„U]ª•t® ÖuHÏç}€¿Å—‡ž£1ƒÉ½™L–JþŽ\C¯üg,¤7õ—EÒ·¥û½6¨)Ê^¨Ú¡¼VN@…MÀBöà(½Ñ¼†aÚfš‡¾’~ÈøÓ°Ee¢ƒK¤-«1±v[Þä•„@Çú*ßKß%’–׿"Öù`ú´,ª´ÙQNUƒãKò¸Žd3ñ$Ÿ6Ú:•=ÅÉó7ö*™úµkéH¼Ï'ó„”—•¿“Év\ßJ„;TJÕŠýÜBÕÖ¥mKi¬›‹­˜¬¯êÈ5Ï<8óöI ý ÀvüÈn! ®¤Õ‘GáUñ5ü}^Ç!‘oqü¬˜ƒûÍ¥Æ^òTÙTl4ë$Æ3b’f ?º…±’Ž‹UÞÕO‡­D(·;¡„WÌñ«¹§!`Ž–ïjÑ¥hrÆóËÝ?Q—”66ƒï©ðIkW¦Vu¶˜Öß ÉÁ7ì© ´àšk' }MõØþ†Çw1ÛkS± X?¥ndÝï »0I>Éú㡬\[4|ÇZxnQ|âhø'ûpÊ”GÛ¿‰ÍñŸ #øà‹#ß‹ØèPv«F¶“ß^}û¯oЋx ñ {tçŸ/rò]¬™sä•A»†¾Ž4—v¸®£ÿKêGvˆM®™Â­…$âÈm£½gþíÿ ‚È|ø¥<µ 9‰Xð[‰Sy…« Çu ù¥‡ÏlÑhÁR¯¼dWº<*o¼%E $q‰›k©W\—¸Â€=Ájøe½;^MTÛø°ê‘’•ê†r0:¢¿^0åÒþ¡ãîÚêE§§½o×2ÙÞ]2ìR­©kݪ„X¹v¿èÉÄ¥Íã¾|ÉšþªT™-¼­QÀv©W^_o‰kdñ9&Ù¿ù,ød;ª J•ˆ÷(ZÕä½å JUo®Ú©a Ÿßø“¾žßG ´àòGñ}]âÿŽå|v¨éÕ©[Õžw€ jÃVbþD÷3ÚßÜ~/DÛ¶$5w€x_0‘ÿäÏ|–¦b3'…å´-!†ÙŽb¼BDü›UT+Mé‹–ýõp÷Ö'`eTè›;©½¬"ç#^à=ø> ÐX’žñívÔ³Úß)ó26t²….ýþH-%•ÍDम‡Þ¦©·b¡c‚Æ”_°eA6iílh­À¯£`-ðÊ@0mþ 2äÀQäõÐgÀ¹ž8ÙžMn¾Aõ„½ÅýbΜ..|ìºàÿ!w÷_GÏ­!CtT’ïkyê«—åN”wï‡IZÃ,§K0ÀœŮݕ]^²E¢E™eŒ”¸ìàÙ:{$ñ!KiÞêúZ¢˜áĤ0ã±m;ã0í°P¢Ã½Џ^&®œÆUd±sº«˜¼sAüs› (  oÇvëøvQ“]Õ.GÌåkp=¶k›é8' ³ªúÀàußNŸxmÆ@Pz äþ‰ïòÌïßT«Á›Oxu0ž ÎbŒÜ«zƒj õF)hL“¬ÚtBà R\êšì£Á²\[Ê+YþȪL_àOn¬kù‚¢P?­žq®Tå]ø^ °þŽÒ?ʇ¸Æ*„]Ϻ½–- úóÐwºÔá¦úzôå_=AŽÄmxqýÔ¨{fúNd´ÝϦ٧L÷i%ÿ*SîÝŒ”Vo?²ÍÜÚ›/\~ߊ_¯ò>n=¿×“¾, E/ƒ 1Gˆž@âj‘øáhh¸ÚXfhãÜ U5Ý:6 b4 cÐTº0\’‹Ì#•ÈÁ‹CÓ- PÙ«-ëI‡¤0LvÑ­ìŸ\)ÌYÒSþ¡)™Ní}˜Ñ’‰]}nM½\9ãW ½úq¶°®Y&Pí`«Ê°Pw‡}ŽØIR–dçBa>&ÄøÊ{êÁ·¸ÖÄ‘ Yí4‘ÓȬHÌQfÜÌ÷~~sÊ6Õ}aðBò¦ ØIé¡ß¶zvVØ}jÖU£ @IgŠÐš "ñèy¶›áfŠ@z™¹¤p¡Ú)£då>HTм€¼SÞ+¢þ¬_±½4ÀÎP›lUÛoQl^•±¡M#ô×cÐ'5v¶ê7e–oÑÈ#´‹@£NeÆàd©Ž_—æÃàÖ]¦„|ß=KeÃÄò4„{̇ãP]ÄOúîï"š Ÿ S08»³Ý˜©ª™€Èe©š _E*Œ<k’Ê­£‡T–å‘êWÇßúŒ­SEjsõ¶@¬B_øû4X='´2‘ôŸÂ.V¶ç»UYxÜníÐò¢UÅIîm3‡Ì“gaGÍ&DZ:¯›EØþu3j ½À㥀þ÷Âòn^²µwùGª…nɰN)À/s¶ëW8”¹˜t@R0e|Çt×C{æ]7ŠH›ÝòÙmí~{6㬆¹ônÓ-^=•Úå@ðÙíØ ³Wwi¾$o•©ý™KQÚå#åžsÕ ¦|^<Y©rXFÀjWú©B›ÛqGŠòcÍ+ª¿Rc_–f¦Zت4Š\’Dþy¡ÝÕæ ôëÿÏç\5¿€Å ̤´6Ó WSZ}œDÓ /üFH®O½¨ /«’÷¼=)Å´‡M³b†Ó_®8¡~£ƒúÎѰBLöQÇaubæÖ¡–ùÀwC‡ÂáEýšÅ%s•|“!# Æ®J1»4`w׫½•n9µ¡[Íâó8ôÜHÒìŒú_£(jú¿ó«Ã$–ZÓ ÓéË5ÍÑrÖ‹ÿæÆ,Ò?W*[äâº,~«#v•Ò­a‹Dç=+ùM´{ˆGJŸ~·ôRµ ö•ͻ﮹­"&a™ 2…Oy[¯ñ9ª´\D ÒƒÙËÌ v[Mmõ¬J”#³ähõ2º4)WÎÔý·÷’ê0r9ž>ƒê!Ã47H&ذ2™½qM÷SÁxyoÞaÑRIÜy/RHäŸZÖ¹ýùóé5gKlC*~Ø죵‚À@ª"†Ó¯Íß}eµF›Ù7Cû·†-}w5„7©»Ë½ '·ý1η¤e e{ÖSš×f@¡|‡\) sàÞÇY#ó?Ï]=môïØÝðÌ%Jmñ@²»eœ>>¸ûzé âÖ/TŽ“ Àãö¥mBý‹V\|òö“%“ÿjêU¢`Ëtµæ!å’Xº,sR‰Tò…Ì’Uó{nÖÉÆiØdpð>/!±¾Ÿ_µYp“­¡›7å\b¼®ª"AØð72دN×»¶Uziµ²›Ûªô¬¿ášÇ–®\6ý3þÍ÷Al—­ÛËXÛï¦騑ƒEž†nÕVû¢“lûÅÏ«‚c” ÒÓ§žqÁ&iÛq íÔîÚSAÒ½A·OæÅj›ÎÎ^jwTÇq· ºNh×AuSÐ{Blg¢„¹Ë†¡q-/Þ$¶zÍà4©:ÛÞèšl+ÄÅî‡agÖ‰6œŸö8~ÉŠ#™~D‹jZwœ|ûc0úõÙÙ¤ÂˬÎJê—iÚ~º|?1ÚÀï~m%Å<Ê%ËçOÕyû.ÿ|âUôÉŸä¸Î•®îݳ¾Áº×1ë¤u‰þ®ú~ÃHˆ›ßcfµz‰_3g_£:w=ÿº¬OØ=žÿøÅâ(]Pë_Ž_ýùÇàH<ëû×Ú`™ücÃdù©¸ã‚ùˆÖ?Ìaßr«Qy=Ѩ +ë]žëÐk§ÿZë_~Øìé=Ýßé·l¼ÚI@H%Ÿ '£”áЬ}»’=­ß»’¾ UTwÄ_ê™(V2½ò!×¥å–g»] 74îÝx0´­¼ÒgÁÅ<°d¿@Þí:i’ÑfýSŒV¶- ì¿ÖžæÚà‹ñ‘r ·Ë´2­W& Û§·ZY×Ãí «HXÁmGfHØ@7iÖÓ¸ú˜ ¸ªÎT\µ|Ñp·û‡…âúe}³ÜÔ¸bR‡/ùè§I`>{ùåíXæ6iÝ¡ÖòÙÞà“qd÷¬Ï}m”Wqo;Á[Øn&ÔÖf}¿—ì2bóM&P5DiM÷®EÝ,Á- À¬pàp(]Lݱ6#|k«p]älËY ,·ç õeÝÚ!ø ðs|.à¿¡k›b}Ù©ŸÓí‚×i³$÷Ä–{SÖÍÅé’õ\a˜Š™eæ±±ò¨£®2þB_ä7¼r»Òàoúvñgz€¾bösìxpFâ¦lÍvZ-UñOü¼—Õñà7±o×gÞŒù“îc9;þ¤¹›ûÉ×ñœòüËË™÷×çãίðïÇ÷oø#òîÇÍGà÷n íGÁÝÐǯ_{yí{ ý_ÀsÒŒ9ׯ 6Ç2{ ä8ä,æ¾Á/©që¿ \3ÌF~é0“´º‡fÍ„¶ ºóu3çyê97aw¶-à— šä‡¹W!k¶.ØÔ}oTÝÃ>þd`» «°#åŒ}ø£{ 8üƒ‘y°®ÒÚ˜‘1Ÿf¤=©g-«M‰‚/RoÀð.W¬XN—J\›Ÿ,#½¾- h®?‘9®§µ%濸lŸ‡_ÇAÒ~ÉôÚñjè@ä:P¹Žƒ¨ýè dÛy³Ãxèám”8Ç—ijzIÜŽ`µæ®¹ÃUò@º%W›È2UmêƒÊÍòÛÃ;Ö,¡V—# å’*nZ›(ÀŸŸ%_W†ÚûäÆ:¼½;˜HƒFƒÛ숃ÅÌùæWq¶ÄÂ]Ó„ü&˜ETÄö²ü¾5ðÍÑȱY/M7šRY\Óêü½·ñ>ºŸßãt³†6>Þ“tÛeý?ÿOi Öd«Eÿ‹)¶cúŸßítÙÉoSìGzé¾¾{ÒÁfYÂÿvOõ’}}.ô¾SymŒ[\j…—šJÆåØ8GÍzn^¼¢ó,Íÿ½š¯>¾býÑû©·Åºœõcæ¤2RåxÁJ†å=¨…n<¯‰lýL“?îx–3îoùXûÃ2µTéÇ >$¶ 3q©“¦þ*mnÂ>ræû>ÒÍ4C±ÐŸXuôö2ûúõ˜(¶ÐþtçTüD¿?Û¦I­):Ã#ŠüaƒåSÇw×Id€tw¤mHš7|N«?#½0‚oröÜâ¶aggF'çýâ]dü™>™Î_9°kmçsÃw˜ÈìN´5¥Äþ'ÉÏV±³Rûq[µ‚§vF\;Pkí} 4)z¬·ø}e: ‡ØQš˜â ;¿;„Ö榜vMÐiHAx²†Žz;¤Õ~쀹’/mgÙ÷^Ôd΃ó¶{wÌo r úÃñÇL‘U–Ç`»“ïÄjTe~·òŽb¾¯oGg}ØÌwDÝŒXCÁŒ+®.Ô6+ܸ˜¢ò=lPèòb@ö+qüo_é—ÐÐf}Ö¸½_¶Ôµ“JÎ(÷!Ìt\Ù-bS…]üÌ7Ÿœ^o_ïÅ„$ —Qªf£;½>…Šúšjèê(¢µì,߉]üA2¸ý^½üï «ÂîþB̬ƒ…tƒ$Ìȃ;v¡ÝU™+L¤.‹‘Êyåñc=i)‰=“vbÊ—n"ªuîš'¨rd¬µcw~ô<§WÎ/¥ `Rà´ê=dF‡úhîåWà ®×º¦”½º‚/ à u)]à;í àˆevGna¤s…Þ\5ªCD‰lmOÉ•zDÿ¾eRFz5ç¢qìô•dL;œfVªg‘¥Ô²'u÷v|s{Î¥@÷&ÝÈz\JíVïR;aAc%ÑÀYJ‡%9‚ß›æÁLÔ³  Èéu¡ õz.ãwBzç%Öö‘3¦“ £ˆ0ªÑ…ð³$†Œìo‘ùá(4;‡1 é æ$BL”~;ô.¸tâI%y&Mµ‘~²¦{ÔwJVâ…Òú«§)EŒn6W`I£žH»G­ ‡ðÃ'Œ}÷‚Õnüu áI¦A²“9‰+Ï¿â¬Ú¥:Ù’k]¥œCR‰ªe'ä%äØ|áöq²ååLH`)Ò¦ZòUÑ8}ƒ‡*´œTLw~…ÜÕÜÏŠ‘wH|z7<¯Ä†˜ìzñ¶ Áš%H ßlØ/XV%;ð£c Vá˜5’\JÅÐÁL+ŠÓލ‰([ú~$Ô© dZUñúœ,#9Içqb_Ž?›†äEwª¶£{žY:n°¥°Á½ß„&7öÌ ø‡ø|WAË•CÈYJkìUOte~>‚ "ñ©ƒØð02*̽vHð•L fÿmǤíÕüð5‘ Ã?}æñreÒ `aÞüªÁ:*ŒæÁÿOgü©bL¯}1Õ f–`ª;ÝÜaeh0A b0£{MÞþ)¼C„OàOÔ™Ê# ìY0Nθrï®úr%)1'4…é%X>à¬ªÜ @(D¿_xï†ÒPy¼]•údÜNµÿˆ¿Âþœ“ÒˆT¿óBãÃa*]ÄÆ-´j—OiæZÍEŒÜß#Ø¢…‰í@ÜÄ |éX…«Äº gD÷}QâsíTV‰ñÓ’á<õů2ÞDǸA8AxWÇ¥øl(¿Ð5Æà»€œµúQTUƒ3*ϺççÌ ?«ü‹pÂþÜ…¢NfWÒñh"žÉÊrÄU½´ýÀ¬.K|nºGy©1†ëž%Õ“`çáÅ;Ãè¾ó}ršª'4 ¬Jé<26& Jõ×%:XÞU…γúÐ ´°ˆFNäFÊŸuqàòr7“ZôOAÖQõ…axÒ…J#C·Ž‹”V‰ñŸ¯–Ö’Îü”àx#9l°U™6phùΓk”.¬%½d'>½s”£|;h‚í U:Ù¼2Xp.çTt¦ÈFÖ©5!XgŽŸä"ýGÑÊH*²Î@U² ï6ä¸íæ ]r¼½ºX€·jÁz+Œõ†óËÇáT¶ÎòÊÙ½}J@õ\}ß³®@A-}nÔÐçþÐdCðd‚¨ÿɽÉòuö¶ÆäñCÃÊ+÷añ~OØá,EPg½`] MˆÖlr?ÞU)èiÅ*}äï”à…òñäWoâÊ#'+Ò}î3@:é —DJÿƒ‡Ûª†ð_¡öâÇwé4‹C¬ù÷ê÷Û—–¬’pµé}DN®±!?¸äÙå¯f(Ê`4ûÃÉhé\CŠÜO…Ô|ýÈõÇ‘ñQ¢–öÍXˆÎÁ>|<ÿ¯$ðJ~Mð>…c!hM¯aÃü@´ 9å:z T”±€^âTt´ÝìÁ?Í|óÕƒ““pÚSÚ¶¬y²hbAé½`;Ò»gèhy¿+¡ÁC÷ç;¬ú$•YÄ¥¦aµÿXìO!kµ°“Ûéï„&£‹¨ó*•¹IxÐÑ9Ž~†w‹Lsù€7íbop—u$ìZ²o‰‰07Ý|\Ù°“B³5³¢>?K_œeØÖ‡åõ3jT"jꀘ'Âf¼E'‘¸WèþÒ¥ö匒xkþá゜'4’XBðóëäh¥öºëƒ;W™ª>Ž.m(Ötÿ|ÿž¾¥âNT[ýÊ€·è/¿?MS ÿõ•ÇÎg¤ñGG”9’1?]ç[¹ÂÔ¯SÃ@Õ8›œ[¸óH2©ôHæÈMº0UPÊEG°oèfÿwE|ÖÖAÖ<„ׯ¯p)ɵ»7x¡1[™—Îý\é ¾õ1aà}K7èÍÍüÈ ˜k·e=ùÈ]Q =Àª]aõ{Á¿(íËòÇÄ (}Åñ/dkÛÎE¡Ý•yŠ›ÌÚL1ç…ƒ>1éÁ³U×zÿ̘t±-üDjPäÌ¿$WшÊí Ä`¶¼4E¥Ûí#TÙùsb)ñl^ R£Å°jU!ÎaÃ×|¹müŠV™4g'ʶñg·¬AÌèiLHKƒÂ*"@€ŸR6t¬ -»ÊûSoïÖ·b •*WMÁ´A/-1où¹<ó½EAËê<– õÚï™ ? «íO%8þˆPz”¡CÛÆº– †öû‚¢É¢hDÝÏal(þ…çDèËgÛ¦$wГ~ÖX­¤Vlþ6«l YnRCÅÃE¬Ê‹z§•ÕM…Ñß´¸š™Y5¢fv®*Û6dw¸lO~Ï4t`¶Hš~ÚáĬP·*Ÿ\Ï0PE†‡¡ÖI"%{úXð3ÿ“f‰•_!Ju«ØšN [môkÒH€£ÆÅB6OF*”UÆëÚ¯)Á5§îÂëI^O!ù¼i²«È6È,øìÙ6­¸ 6”½eåf¹Ô¯‹™¯iì¦é"^ÈoŠ¢ÂÍ €´¨Ê[Y{±C4¯Á•ƪ-™½=šÞ¦rqJrV4TúÖ6T¨öùKô‡"².~&mœUÜr£D*Fnž\PÁ`ÔpH—Z0Z&ýyG—õu&­ZþÚ. (Þé·£µVTÛP7 ±õŠöøQûêM×ô¾öwùê%Ù¶zŠyV7C6kÒ.žt:žê…jq_iGð¦?rÕB(ö¿f/nB[~ovÍZû³ä‡Ø&|\½“›+Î/r9jè6;ÊÁd¿«!aq®W5¹-ÅZëøkpWìVÆåÇb^8 Ž#GXÅ—ÈÊt³Ž†~Ú6%–mëT¨Ó’ÅFq‰¼åÍÁêÊÔ;­~ Õkú÷¶`Ö„|vÁð¨2#WÚf›së«Ë´šÐ?›ž[Û0 AD kUAí–Y¶Ö‘~¼–xÑk¡:¸yp B±(®çÊ8Ïý7±DÎU[U¸-Ì1 /5ˆmâWE©ÜZ™z•e•놔 ÛAdWˆ_x<&a¥Ô½2fYÌTæE>‡ÁÚ]5ÿØ@½3ð{mÔaŠÞ^ _‚eeщنÚá}šR¹hÓHøïÄ@䦡 3R#¤ð„Ó+ÈÁ[‚­nÔLÔ´P—&UnW¢ §ô¤7NŽÝ/­½ÌU=ËÏÓR"Ún%b·<@EsëYæ[žpúL/–Ü|¶…V}KC+¬ûœ‡jtEZÿø·1ÕA|jÐ<”§µçFº¿ ]L €E;_¯xÁÇ*Š-ÇTNÏ$Ê+|¥œÕ6”ØN¦x Êo0–ªÕNÔü€V7Á©®0i ‹áÖXÐfê"(d»a ¸‚ ½ÒVKF¼Åå6´ä×ôN¡?§Ÿ©vR*Â6zvýÿgÃ!‚V5±$k¼ÿ·èĸGKÖGRÍOõ‡ØµëÅx"é¦qLù¨qŒÉXúhÇÐmfÊ‚&–´ÁT9i­¶ŒÂ½Tk¥êö²r²­Ó2]`È-ˆ b2Ûáh0u_À€€a D°[ñ~wjúÒJsCm3‘»üQUí·ÛçΛ?3?Fåïa¼oàGПþ§aŸš(ÀëÕFp‡iŽmÙöI'@ Q‹´"¦t:…°s’A™j¾J¨gR»Hy¾N° m!VÀ岸J/ÄfóH_ýI¤¼ðÒB^ÈiÓ¼ÀO¬‡Z˜ôƲB¾úAļÐîòLÅ£èiÞô²¾Â ´z¾â%ºJ/ô\ÕB?è“T•tÁzÞ­à¶Â ¶h­Ðó2]Õ¤ˆ½ÀôR]Å¥¨zÞùÒ^è k¹žòC„y¾îåJ?|.1pÀaÍ“S0Å7À~e/È`QaÎdAóÒE¬r@S˜7]ќþ¢Þd™‚ãr$æ“ ¤>ˆclYóÍvisÚºãïàoß¿š™„€¼¡Ã—<Ò{gËÑœØæI4Ò‹=¯GzM”dSˆŒûR5º”$2™õBšñ?¥ÖxÜ,/"î܇vGO«pë£nØb¸G$!/Â&_,¾ »¬¥OõÚ¶¤YeG>Ÿò#iZL$Ú¸Ç.â÷R– —ófŠA‰åÒCó#a_yda‘sYjÀ-Ñ#†rdº±ü)Êf²N\»³B‰NýêîÞrŒS{ÆBËöûwq 2$ÇœÛ8c‚vKS[ç] ý–æÑ¿¯›±§ã´¦v[ÿ îCÄ7(6/ÃZ9a«4Ch5¥Ý6ºWûzæBÒÁžEÎü¬Ù'qÖè±]lp+‰PæÄÅÝs“½¥ï¿æý–˹`3û‘ÿŠ4 Òi“aýél»rdã..gåg‘Ô^)Ö)§»Å#ÜÄ¥“8ÚSBˆW¶¾ÐÅW² ‡Ä’‚¡Qƒ *âEÔ5Š”ü ‰¦ þÇìãh4„!>*‡>æt\æ`rÈÞ&²Ï’E=+–Ù²T¬ÉHÿ³AåÞF„Ë/e\ÞH‰¥]ùd£l€éš16†oç)\R)‘Û0ôÃÌ_WúÏŸäcQ’ýH…#WƒK>äÓ#­Ë^ëQ´ÙÔ¾Š6íúptc¾IÉäpÒqæËàFÃj£:éµ#›P+͸2A¥„×$@Ú›0¼ñ¾Á}è©#H…I¤–P´8\@dñîJžª(Ö8<Ýå¾úgæLuézÊlÅÚj…6ÚÄu.è&»Ázàð¨º!üªÝSq¿iÜ4rŠé}àÓ ]kè™ ö›Á8Ék‘Š˜0 dn,? ¤Ý„cY ŒåÈ$ÜsSas’~PŽêGN˜A;ÿSQ¾Ã %p ›|ß@ñ¡Åã¬ó ÖëœU§4 £ ðj=%^íÊ´éî…cÚ¦~ƒxûU¦MgîêÝ6h@¿  ¬'¢JXö0ììa¶Ó&`¨QDòDö2œC.µVJ0¯¸Q„ªda¿Ëñ ïÍ/ÿ—bü^]œX˜ÉoÌPNðQö‚?R¡pËo=ñ]¸Á£iȾ¸cE’ëÊg¸ 1Ð+¦Ëoè½R`€WãÛ†ØîèÐ|å­ÙýÃ.è0«i#ÛÔ¹›(_á´C!8@Rr(kÆ=*ÊZå¾.¿4è5¤5ÂôÝÑð€(¿Þ‚%ÂÓÔŒ[R©8cb”†°òៗ·¼Qo ûç“*L—Ј*Õît‡Ð?!íGæÌ:Mu2MõñMÔGÍ›ÆpLj¬î$õä9pVPÔ´§âèdï0ÇÅ’ž—£ä’.p’H U¦è 0[èÍp4fÀµ€UQuÛ:4™Ö~U66 mTB_˜®ÐmgE~ò½baLr›€ç ëÔñ(£‚@¯05(☤8;m\¸øU³0MÈêšÊk±2!XÄXð (¬är¦ä:8–ܿ뛞7™¥9»›ž1Ù3þ¯É<¶aÝ4ŠûœBóôçœdLš¼øT14”PV(ÝÂ6z]6MhžSÊ.©³I³·ukcy†\ßÎßcWšváêLUL Ø¹ì±êŠ7Ø€‚™æÑó¹^#YMæÁR^J_Ò?ÎËðQc[ߟ‚¶×!b¡ö^¾åaÝpzfGŠòg"U³öØ4¿”Zbè‘0ðdkÏ:\VúòÄüõhŸå‘ô pI"yÚn¦™d¼0•ÃR°>*ùí ©0‚§9âi©ÁÁªCVIØ)øZJ¶🞠–ì>ÏåN8Ñá´Z*Ò©Mê#˜ÆŠR%0·©mþÖÖ}ã]Ã)+)FqÀeœ¨={ „v‹®=}wüº]= }-—Õ;nÙÔ•ÒÖg B¶‘‚ƒ7¶¤YØÓêˆ[o°üïåáÕå\5žWÜŸòõ”…0vsáñQ²·¨¼ÝÂOùâi˜B¶…Ê«)Ù4‰ÆØÇb‹ ܲ](ŒS¢GJ'ÉÚNÕõˆBÔpÜ·s¦©»µä¼¨å´6좩Nx"þ§ä8ébŠ~{íÊA1ŒÊPSõ’G@ž¤îwIiw{ŽÎ¢ô«—Ρ!HæíQrÉa¼¹Ž~ƒ]ŸpÔJƒÔZÈïXMâwvCݱÚ5„aëUÍS5¿qŠuÿ€¤œÂt±WŽ€’vP´Ãò—¨Óä¢KM2m¢˜’*ô)®&¡&©+„oOÿV&Äp5E#™&‰²ýÄÖ·š?ˆçẉ*¥X,56`ØÄr[F+…y1ÒÓ.7±AÄ`±d-b¤ðOsõ}i"EÙ×á5oÃý†çà»P>Á)é(W.'mÃAcùºZß2v*°ga6\H“HîRø¦ˆk¤.YÎsð%mB÷G°«T'…nýD@§x>f'׿tÑD{þbT›Jã–ÚczÔ°Ü·M½^`Ÿ/™­QÃ÷=ýC‚þ2óˆdC·@ ³Ø3— ý„“vÖè³dVÙ!ãÕ7š=¤~dšâé•{M“åþ™V¢â ‰)øø vÛM«vìJv2B¬!”§!.â5 ·Ó|‹ð!ö⌿5« ­þ)Ô‹8èñ+ì2k¼8Å)™L"Hr³â‘…‡¢åÿ’v¯*Vr:eÃÐ,ÜÑTPêIçÊÎ.Ìr]½SÁfãò; .w–)µs¯Qè®9æ)ªü©pȃ¨­$‡8ºÁ‡ö{¢MÊæ=êãês¡ÌI“îï–¼Œ';tŸ|Y_íwÁßÕÔxMIC»¯¥÷€KßNþˆÞP¡²C9„T› Â5ò1¯PcqP%š_§Àð7äT‹XuNžÆÒ¹ñ¯½¥a‰yWkàäTuý,þË«õ^¢ˆ=,H¯biŸx¡÷ÙÕ§„†öüÒÛÝYƒSK»9DÜŽ5NÕÕÚr'tn-fR­é}ߘ›fˆÉÀõç©ï»Ú KˆÎÑP§IÚðW#Ö<‘Ÿ²G¸—ñåøj“Ñ[æÃÞ«5l#´¡v OíÜ#6w{E'øÑôTV_F)TGÑN.!^Wîw4¾ŠÁ´‹úé1ôjWíÒÝi·ÎÚ©ãtéŠÂHR?‡Þ&:¡H¥î1ÈqþqæfãÓJ…æÜñ¬]â«CøBH6÷£JBi]; 9xÁJÁuß+Ëí(‘•ÃJÒ»PÇî‰ø§b04/³ xÖ·ˆð䑃å`2WaõZê4Ã4aü¬NβQ)+g¶a)µ8*éÈwPN”ž¸m œi“|ÁjÌ¿H6„ƒI©Ö‘]€|KU¤–§K|`€îÀGAZÕàmõ”ˆ_*Ôe)oD’P6k~>‡VI䬤ŽVjô í ·ÿºØ&â0Cÿä:O ÷f~a|ˆPž÷Áa®)kÕÅSŒ¶rN Éľ¹ ³ÎÈmWU>øSi ÐÞ'üã“ XÐÖb»ÎÚe»EüŠ’vô^óKõžÝ$¶°*wBa*MxjÈÖŽoE•EnYîûIçÈUÏc(»ž#JHܽ·ˆŸýñsD1ù*wê=ƒ¹ã¸ô®`=å7Ðð™±ËÏң칤ͱüü ‹Æ=6ؤß2ñ\ÕbÍ4UocÖjÙÊ,]Ceƒ8ßj©zÎV›ÒAQáæ£.’<¿Oµä%{¤Ì˶ar¼`‰cZÌ1*æ˜C{hi!ëð8tl;Á’.[,¥ lQýH¦®¨ߪÓ.“&}¡Û. º!ë Õ”f'â=fù«êk`¾<ú“3‰J‰šÂîì]  MŠ;Y ó  )K¯úþ§j ×JÕ,°Ìy[—¯¨í?¾WvøøG_‘?…u_ÎT‘ß2÷ÛWð£^±ÎOhAîòÞ|V¸Cj1—°Í&AGÐIaÁ ¾Q²]WëÜÅ5¡MFXŽSœ³(*’ŠWERûìUG» [|(‰ÓM´Ø·ÞÁÈͰûÙÔÕÒ0þ¦÷‘á8M²7{0tîæœ`lni¾%“êÆ¤ðaœº}#wk‘@šî<é(îàaLWPA¸:_Wù/ဴgVò$äô ÃçÞy, ÇÞ /ÇÞ^b¹9—\Éáà/«qÓ+oå@¡Që€a–¹³&Ù"fÕ!á*Èo©˜+8B]ôÌOΈÐgŸðè¶/˜›Tm ÎðÕcGÎÍØ%ê’¤$ðóg€pz¶Œ=G¶>-èôXòÞVâ©–:½Ù“°¯ AŸA§åI} Ü?7ê.»rïËä_&²>L¨ofë§¥_a‘0¥t'Œš¼ð&!ËòAoLE t¸”_ÆšØN¦‘ˆ`õ¬ý ¨pªó3èlæ ,#‰Ž ‰X>¡¨–hƒsŸ¸ÎçaÈ»èÏo;"}ìæÑjË!bS÷O©xUÚ?ÒŠf¢49ôzÿñ/£9¬t›|ô¹Þ…¡Ö=VÎx³È¤ƒ¤'"oÒ\r˜8òvkÈçæc›yÍD© âNÖQ솓._/|7½àççÃ#Çuꥥ+–öŸcùƒD®Ð,ƒü;þ…3l h¯¼ãã1ÌuÜÉ|¡ÍŒœ ÕN¥¯÷ä=üëCòZôг®ó|ƒ²ã† èÔfñœ{¥’mIÎq6ß|Z·óÜŽDW"‚žÄ8$æÞ¨cÇmß9O4±Äd»ÐËÞ¤¸¡\(g3ér-ü^Ÿ½â¸UòŠÙ˜nÙrvÿ2 †j+ÉÊÛÃßÁ½ër’Ž"C2³g½]1ºñ½Î ÊãËu£<œÏ s°ÊY¾´U˜§À’Ñ«y´øŠŠY̰ ^¼pc"l7úÐÇÔRø¥<øõ'3:¥–íó¥©[™}Ÿ}6ÿ^©žîéŠËšJ ‰u„Ù q߇]]bj©¸F@)æ1ùt›Êk­Ãà­?íÜgV9Ø6…ìFŠ/bjû`7I¨Ôü÷PåO2vÌšæþD~"©›Æ¢RJ(â'™õ†ýFî‚ä|zö?q 1Ë:oÏkÞßUÔìNGf(äÌŸÈÕIÜe\ûD÷ôø¤|ð[»Ðêëô¾šä•,í¦ª©kêûÞ}ËÈDäÈtxŠ(I5¾Ø}—q׉9Z’%Ô%‡ˆÍæùùÝÂ{KÄ]ý…wÎ:>ÍÀÌTlá¨0ü꺇ÍQÞªt¦§ZN)÷u˜û÷Õž6¥YÓ#ø¶sRõ¼“Åæ€…¿ì>›çŒQŽò¸ìñcÏÀÞŸ¯¼¿ïª©‡÷ 0K\:Â*TšAF|sþè.ÿÞþÊj„J9xu³ñ²ý¬?s/¬ÿŸ ´ÿÏjû¹yæ3(À2 ÅÿžÏr0u²´7±4Ö·ÿoeèbioGçà™ª®ë¼Å†à³Vß1 _h”™¶‡¿Í¥Ì®¦$«uÂdgC±òÍR ürѵpuàÁÅhØ~ì›èX+ùàOU˜pù¥ÉÇØ>C+Ieä<ò0+1å^‚8Ò-¯ÖLªŒC9/ù5iÙjžÃL€N]áÍ[ºÐ"ݧ+/¾ÄÇÀcÖ'¿;w>ô²³óâÁ†%gé$^O~”½`Âyt «Ï¿ˆ_ü ›ÏQ2¸v?¦¤ùK@YÃÓ˜%˹G”0™•@çI£„H\o¤×ÈYÞé³_%À¥â” m])H(Eæd˜ƒýì§T¨|W—&»t3I“Kwc••±ˆ™l,â& G%ü_ ‘S·ãJ®ÊDþj_êAu‰Ø …<ˆÆ‰àk@væ\— ³4¹ÿá%ÚÃ+—ÃXá<•š1PÕæNp÷wÚx¡ëVcª×íqðe[ßÚº¶‹%T9Áð#* n¶MëÜXo vT2Í‚ßíÁ&\MÛ`1Àò“º©6у+o6$Ý.¤*:_œ#<bdf £žj¢2RGk¥>ž†.†Γµ/¥­ŒËvÑMœ%¤ß?£%ùãhÓÔÔeÇSƒÖ7 ´€"D8–›OЍmH?éX (RkKq^ŸïÛ‚\ʼnµ/ÜÜÛ±tV—íþ$ßO7ˆÀ¬©£ëÍPPŠ&R„D <ÑÜx÷ø¸^ñÁ NQû#w.Š!Ö]?QlB¾fé׎ S~*¼C¿ ýñ¦@Ð:tž6Ò ùžlÜ$Å“ ¡«?Ä.gGO‡ê€uA'†EúŒ4ØÑ@ÿÊ c톲~j¾­¯ÃNÄhµ²¿Â1þœpVmA€®åÿî8Ô³Âs ¯cµ‘Óßd¸~e¯Zç†Pì³÷Á½/-2Ð| þÓçÚm’ ¾_rØñh+~Uo;¹„Ó3 #˜‘þôz•““;WÞÙÀIë<0Çž¾é_tM®e7•^Y8³‹0çd¤VÙ³5¼{q³î,»Z!Äü¢¼3ÀÒ|ZO0oñt…hÓ˜jÛ{ør}¨k¸†oÒ_)×ýÅ•}fXýªnSlÛnÜbŠ2B ÷iiTn}ÝÖܹqË¾Âæ¸ÔnÝ*n°<­ ÚP¬9”œÑ¯p{j{*Ïr3Ã…“ÿ½,_Åï›[^‰q€ÚçdóÈã0óKÿQÝRÄ[.›3ÏIWTÅH\lß`UÿŒÄ1¿ŽÆç [ß^Lk½Ýâ“}Ö£ÃÀÅ›cd+òßßeËÆ|Eè·C-¬íÄõˆûÇ›À il{#\Èãò xo€¶¥YïNˆê“dÚa›žw¥©¹igßüsêFæâÅF ,~/ß;¶&¾Ü"†•&ê÷æÔš åݯàv¦2¾•×ϰbRK$·ªê4„ø¯âzµSxÅ8Èá:¼=µ,Ýåù÷Ûƒc}tòaÊ;‚{Ÿ¼*ÝÅ‘€ÆŸqµ±ÜÖ¦:G£§hàÕíÐĉq@äøþ44,&zÿÞ¶ÿ„—òÿ‡ð·ªiÛ#«#ä5÷rφ‚@õàRQÅlrÔªB ‚†BˆÖâ´B¹J·gï{ß¼Ôm8Ã0[©¾Ãþûžçï뫜ÒßL_{Š©½ZÌÚšÍN³3—߯9ý¬œhó{'ø‚~ï öµ%¾¿##¹8é ˆ3Ø Z TäA`¡Ð V€ä"ÅELÎÄ’z@ÞíÉ…ÆÝ±NQ°½£÷¹)X[àŒöäI-:"NˆdçžöJ­Ãwï4Jé /eî%dzK ˜ˆ¹Ò(€b H¢b½h§Âæ2¨«0Us©ÞOM/ ¬ Ô7Tzô‘6¼e©"ýBIê¶#[‰å-2ÝN¶×$Ù•8¾(H/¹Ã¯>’‰}0Å Vºt‰‘vT˜Ð{¥Y%&§‘~Õ¥Q¹Œi}ÔrÕ·¹ãÛèŸ!OX饛~v,8Ç®‰3o[DQñ°’ýh‘ÙˆŸ ¤È…‰öŠÜ%éZ‘(N(³GU”kWB¢vïSDÌ+àn ì6'hdúß%@ÏW;€0ôçŒ6ö£ìK(”€‘z :…†-X±×þËcK÷DòGœQ¡”^>›Fbô–]Ü4 ny<_›;( vìž_Ùt”IOäVâ XðP&W¸ €Fº{Ô-¢ ÀÐ5ý»t ÛöPü^®i pµÀyn:ƒˆ¸áP2Ë\Uèsd:Ë|Ifö ²ïkê¢áU]&égZØ’€#<·Ç}d«Eðõïð|_a=ÛL×®ZÛrÊ—Ei¶“5«>³‹eîàôÔÐ>²<$€Š9Ò![8F¨6R®‰ÎºA^äIø#Èä›4² u œˆ¿¿Fa•ÅÝ*,Å€›l‘¼þŠ.·(:£ €'ø9w§(Òâ"ñ΀¤Fƒ±Êd‰AJXÏG›£hø”?Å 6L #Geͨëÿ0„LZ³9§Šp<Ö+OœÓáõƒM ‡¡Õ®'Ü9®˜8ËŸÓªMûM#K&‘3AXØvýøŽÎÜÞÞNá/ÑìÀ)Œ1®†,CtQzH´Ë9e =’|¬”²Š•]o†U‘ x!oTc{ˆÙ:jØ”vî·ŸcÎV ¡«k¤,z¥BTH`Å&ÙïÚñžýµíêYê=ÛÞksw»c‚‚Ìö±Í–ª,<Š’ð¦j ñÍÊOZ†Žêøƒm4Òrñ£°‹2¡8rþ5÷Q{jË%¸¿KáffÓÝ’¡Bsúãšh_ÿl¿)Ù óÞ´'²µlQRÜÓ]~¹bL§­Y%¾¶`'ŒÏ-Â7 „“É<ª¦3eTµ¾¦>ÁWA“bbl…ª#±.ÀÖ)ŸžµÀ©BÜUþˆ9YzÉô…#Ý™ÞQ1aW¬¤œÝy–¾,ù-vÍpÔÇP°ȧñÐ)¿–¥l¤ðÕúÇ0tËD/bÞ”Sª9kìoÖcƒ:;å R"ÇG#ô˜ãd­PŠI‰}}í-+Eú3–è¹Wé9ÉPà¯[ËŸŒ¦ß‡ `s†µÄ5|ú)¦|fcÿh…'A?Ü-Ö­ð/«ºL­F¸tTKŽ9„‰ɱ¿ ÆŽŒkÅãúòÛC`¥¦¶Aú¢Ë‡z¿åÁ6ïëÅôÊmöþ„·±o ½ 1|/×*ci_VÉ<6=½\IdãÔÌR¯.Ž×Œ àcöCiS@ý,<÷èµ+gn&ûÛ’ä¼íµ¥Ùb'·[;¿‘­†ZMø^yˆîŒtsä—¤„¬‡ÿ‡ü¿v :ûÖÖ_$¤ÇÛîVÊ'$£_zÏúu%ÄgMSÑ#_D‰™"}*ÔµZ­²vª•‰ß@9™ŸljWØIÈn*ZœØ†Û$¥ó½ïªövx¿;—í(8ð†š¿µúáëT +¬þ¢§Ô²ˆk5Kƒúüÿ6·n ¥æÚžÉKp€aL¢ÿÝœLmí]L Mÿ“ÿZ íäÃT¿3õœyQÚe†¼¼e;GÔ'‚S*H[³ Ó R„ÂùQì¤×"šà]®`ÚÀ®â]lö÷)ìÃ0ÕI€»Ú+v‰ ᣒE‚älj¨Ì]ôz-Æx~¦;kûj¾%èa>Ýîø@?_< ·¤‰ñ>`'{±]ÛBþP¦Á> hÀOÿ0–„.Ü:¸Èæòé0æáoH}¨®Œ»®Ó!Œö›Õ¼b:ÞøBi£¶Ê²†é“’s\ô Ó$+D”cîºë)Q2" y\cÝ“¼Å𯳌üÇñÙÌë|3é1ÝÍè Œ²·+Öß¼ZöóÌž]¯6 Næ7¿f7ó¾í™56Κ/ÇV½u8™~cŒw`¾4ö`X˜ß;߆Äz¡÷{7„^Ü÷Ÿ¼`ƆNíZÜ7;Þ?•¥ÍyŽ(QÈ —òaäÏPÔ§ý ùòæ™°_µ7‹—V‚Òá׹ΣÍ<ò%‰7oè/ô5Mþ޶£i4w–?á»bÞ¾\_ ¾ÎÙò—IG;sŽ·Þµ«kuž”¬®é'†ëmÞO¯þ5%iy°âl¼‚Gg⸢VØ‹r2ÒÉg °Â-QRü>ùx»þâ:j¢B]÷Œ{î°0!,ÃBí³zrTrÌ8,Pv%ÔxX¨ú{DΩ£ &§æ{·žyü¸¬EkÂ?%±nA"¢øÊqH$öԻη•“=s#3§¾ýI')or,L* ó—½ó[\Lh‘Gˆ¥î}r’CÒ©ƒâ3 „ý[JZ©Oå’éïm°,u ÿBp¦HÍÌà{lÂ#q™î`ôÇDUi¸´r•þõß2Ù;ÓcKxh82˜¶')™¡±˜c>ΘýŽ›MOuÇ)e\ϧµè/ˆ’„íoJ¢ …RW{»¶ÄàïôÛ¯õ ÙßùßiK 2ñ–|s;j†?b> Õ_°ÈIúkHFå^Ž7Ÿ—$2Í%Ny,Ö«Q +|9ŠIl“°¦$ÇÓô£ÝÀèF dÜ‘CœTÀP¤»5ázÌ :D{9 ú*¥™…Ïþuþ®\KIåªP–Ť— Föý¹ª°Ö-9b•å’¬ð¢k3\ R¢®èRV¿§E´9­÷Q÷T ÙŽ/vÖøÙi1ê%vE-H¦Mp6#ºÊ“Xëˆb+¶¡="Á й§Øç%jE BêoËÖÒø<‚ºõ›Ž¡^@j„ËᥩÞpé/ jat2òÂ\9TR^ÈÉ'ò¨–oÑ›+a–ê²ìÈæ…J¸^“µ’¼ÔãFŸƒEˆq–b"ƒ‡æ9‚©™X1¿í`t…«üv{´/¬ŸªN÷Ú<îé™>¦B&Â@‹O5¨žaˆ ¹£S`µBè´Âõ™üè.\ÆËCŸëC|\#ׯŸz vZjL¦&Y­ç/è·Þ·(‹Ù}i+[K %¬eÇU0Ûär?EQ é²99Ù”Eš·#Œ"7Ì 2!ü¨ ÈòûsL‹ D ”É£È6¢­®è“[ ”RΛf•Ûe^ú»—ÍЊŒk¦ôN ‘„CLrç[Z0¹ ²ÁFý¿<1SÄÊúŒ³&²Í´Ê¬œõfñ€öÏùŒ &%)ï+QW’WTdpO=Q#œÝ¾yIm%-9\©{C2úWÅvcmœ>ºñ^ú°Ø?©öu¸_Õÿ=¶ÝoÝ.À2¯L¡ÜÊù/ýÞ¦Xu£¨/ù :>Ni‘ž{Â*[ò Ã&É^ã 2CfÿðF¨T˜wåú:E.^sÐßp…Ζ"Cgáµ3m/IçR/Áx}¥‡’~.Œ›,ù­±U^{‰OÕÎè2[ Zjëž+²s öœå4Å/9X‘½½®Î/€LÄÐ#ÍÓÇÓþ²Éº Í÷A•zD%³¤W$¾º¦¶ŽNMþV½)ù°mé“HTe·äpá%ë°³ùë_5áí~~Òú…ßèc§§ò÷ØnßÔ‚7ÝÁõ ¬'ª… {Xrmh‹LóýÄ9s²Ý N|øY˜Ô±3D¸íwe­k¥ò­)·´ËÙì+%YÒ¤«Rl1Þ@ê¸ÍK5ú’Y¹ór`„Ë“CíúÆÿÿ Ë@µ¡1ˆÿ?‘‚q«†´3Ž$â쥧…Ø]º)¶wO36Ú*õFÃhãCØ^=hÚ±E¬­'š¸½õ?Ò·Šrïm]cé‘}Ø}pŽAýýÁ\"ŽÌ¾þ@:‚¾šÛÄcto„¤*º¹ª©©'骪ïcë÷ùÏ»g¶“~Pß>ñù*”Dñ(¡"µ‚º¸UÚËM%vŠvZ:h•ª$©’)Êe³­a|¨–•JÕT´ Pá•ÜBµˆ—qJÖËÏ4Jb;ŽV±ÙoH¡È˹%«%žôÊ¢;–V²Ùo„PæÜÒµš—y«vµ Öqöy›ý4 x(±«à âÛ˜ÐùŠ…óÂA ”f)Ú*RÆékô'/’ZÔTªLêia ñ¨³ :H;иÅò’¢DÿظH½?²¤BS¡žI¡*˜‚䯂àPb8¡¸ÑŸˆ™ p ¤LLÈÚ¥;ƒ´Y(ì_}(f…Éu]A.æ¸Åñ%êFëÇÿDزš¹UÜ%îk‡dKî¡ësÜûyo=¸¯ ¶œ¸Ý<+óKÂf”ñ&ï¿€Ìxˆ™#ÇE‰«ªæî—WÌÍp¸OÝu®ÿ Êlð‹N˜+µW(¿Ž|çm·ê&ÔŽèÈ`Ð?]&°¨ÝWåù‰!& >D]GßJù‰[í•)-c’ÅuòBÿšY»¿¡Ý0£ŒFî õ/% äæͲŒŒl—ª(ŠF­‚‚9\Iá?ï¬[~ñrò…ó®?·Ö®ÃZWž2ÔÛŽJÎdT›]«"çëTû:zö½Ž°¿±*|ÅhïËsPÝB’-K~Þ$œ;…ÎôP5ŽíËw Ò½»?ób{`\#Hظ,(tà‚íOV®Ô¢-ï‹#Ð Ö{yT2aÑ×\Yue WÈm·õ´µ‹ú>¥¬·5R`[7"c{ÊÏõÆ5¯;a=¸lÁ*ûo‘;Y¿%‡¤òùL®aÔgÛá21Z-Æ×u7 y Éj<Ó¬Í2ÖL=¶8Ñ™ÈC½~‰ÆÙ›½ÞÌI*åå#ÎûD RèÐå0a€\ýbûˆÝŽÝwaÚÊ?²¼IA=?¿¨@8Û*ƒŸÓ}§˜‚a^ÀÂ`htÓ ]Gçh|®O§!¬}÷-IÛÑœtÈس†¯s¿´Z˜wÓAòÁ0C!ßÁ¡ð¢¡€cuOfäÅuøv¸k,Ïu\e‰b:6²·U§|‹YÒÉóTµ$¤(#:ÁÒÔš¦¨œÒ ,-ÍîÅùy|«7,ÞÚ òÊh‰Šø˜§¼v0‘å¨&¥9böÒ!¦ §|'¾v”Šl‘¸â¹0¡ï;Aåë¡Hˆ¾ïL¦¨¨¬ÖFxµ•‘ )SŰäö= ¶Èó›å"õ”-ÿ"HŽi:”#š¨m`q”™\¸úáÝ~+ í?ønÖÏÛžR˜ž÷æÎ)qf¶M–¿¿$RJ%pqýÚ¶ÎÎ §7]#‘ª×ÚšbÞEHŸZôIqËOê*šFÅÉüÝ_èƒ9ç·îò±wå†Á]}ólW+M:Ï™*xx­D_ö‡R_ˆÒÝá–ĬGh!àN’D‡õ'r¦2w”Hƒ>c°s‹p:uõFÒÉØÐã}‘í[Lo3æ÷²| MUëîÃxŽ. zG_šd¸lxg«6Ù¡'r„•Áƒäû†âœv9vÃ*–œò¼ê›ÿ€*„˜ˆysÉv·ž-lÆØ–Èxô+èÌVBsÂÓQÎ$àHˆ¬Z#1ÓÐk“óå<ç˳§©gÞýfݰ¬ÞÑn¼á³ålU¿:õPb=»rGåoáŸ1sr€q‚~$^“úÎ]tZ~òÈñ$ ôŽoÜS/?rK¼‚zô÷2-Sw{“Ù†•'ð ïØ°o4’Ý:ŸD2ÅXñ‡g/[¿€5Ëü[qézuS´Žâ5laöÄkªZ«ôXÏ8†ÕWYÎ>š/kºŽf)’\yZÃdî òë×5=W¨¦µQÚqÖŽÖÒüõ9il%µ¹² ìqÞ;üœÎŒnbýÖ̬œ˜òéa1rNïÐÑf¬ÅëŽÈ?î“TÃ^—ê¼ù'SÌBš¿Á8ÐnÁÈéÒKä^7BbV§àæñlD˜@Ž$X›SX9@Ó«‘úb:n,§;°¸;r1kÊH' Ò%‡„ò˜&:RL•˜ÎŠÖc^°‚qòõr(ì‡È}Æñ_³?ÿÐ+I9~ÊüÒçN›f¤ùO:L·¿2 G,:»+sÆ–ç;(¾u·?N= ðWât‡MAæÀ÷ ™ã®-ö¢—;SÅ‹y;˜û­£øõçý©å6Ÿ÷÷5Ú÷©k’0®»ÏC91…G©¨-;¿ Wº49j$6BBÃŽA#ÁRYoþG‹ÞÕnKæô¼ý>ìØnK£‚AÛO¡Æ9)_Z*6üˆÿLÌãœ^Å·ú—Ìõ¾$wÈÍâ8I¸G„”^a ú˜ J¥ÚH˜¿¸-zE7LcÀè·ßŽ\ö!Äí ²ô§7^gv{ß¶¹OûÿÉ9M\p¦5©`e*:ëd›Hqž~¬3ŒŽ…J²v”P ºï‚èŠÑB' 8r ] ü ÷²e"/K#`"¾…ʉM‚ÙzçÆy‹ì~¡Â¶-&K!ɸiàsÅuþ:ük\;c.7†þìôä"õ'ܱ… $o$Ç!{e{/8ZÃÑ2tNºõ™¬V)¯¹L4r5;ЕéNÓž~ÄQeã¡ÀÜ ¡ÿ]ä‰&xæÐJÉ.p»wD »ëÖܽæNlôÎÐ Ér;ðéØXÛ¶‹»µGdV/9ºº²G VøG±@óètâz;ó¸a:VHhMq[ú$"ýêJnÉïx9{fô±§,gÝn-ý–ÍîDVî)®gÓf×v Rƒu÷üÝ–@¸ß{§ÿeÔï_1TŸó*Ëþ)ÆÙ{îQîÀ~»í/úÿ¬!ÃmÏþǦè¸Ôÿ;›:›Ú™è»˜Ú˜Úšº8y꛺ýwþ¥³4“·ÚýúôxE°¨Ÿs¹o†Z×*m¦Œemt½’ŽN#¥!y“ ïMJ“ÿvuH£’rå>Î% »Þ_·àïY¿(H[§a²Ô¨ ðE4ýM›iSN/?/³zW#ofÔÙëí‚ìKªJÅÆá¾ (XKj蔦¯f¨ò® >˜Lû9rz¢ìÖ3Àb¾ËŽë-¬Õî©K [ÁXY¦Dà`zÉ0É¤ÒÆ‹&Aé‘ ¼ ÝÜ0íI„û¼Œ9âN9zûšpŽè˜¼ÜFUÜéõoß¾"´Í‰+ÛèÙú¾<˜õëNù‹†Ü&Ë™Àk3ƒè[0.Oû=BI|‚õN}æJŒ|ªú0%oSжå0OlæP RU,Š‡ËØÄ'¬º‡2†3èôšlFЪ½yµiÕ”[<ýþî-gøæððͧ^Œzàã¯g†oŒz<4õïÖm>¨¿,}¸M*M÷­KÇý]”°KsjâpÆKœ%Ô"dH0Ô;k?ÐÖÕø‚©qÔ©BÊf–Ïõ¢Ôœ!Œ 䃣l^ñt7Y¾> ÜÐÀü=›7„Cƒ]f튳Ñ9|“'R°ŠN훕û•uz|ýTxaåóêê˜H¡G!§9 3‘Sˆ‡e¥?:·íu±0˜bÎ,±¶Ð•eÞÃ7rüŠ–ð~Ðg4Ž£ì.P+Ìe&'Ç«‚\êRº¯ÅÕ°©¤ù»Ä d6ø­+sNEMáÐÉ£&˜ÿ16´IúÄkÎ ÒfÍ©««SšØ@“­*”5â@1\î­>ÌÊb¨KaeŽóÔÅŽ“»««CÍ!S/¹2Ï™Iôáç„F“rŠcèe0^—é;°`müÕGâc˜½#‹e„*v*£„(çÕÀ†Ml²è,‰°~f"DzQA“[q#Œa®it¨mÖI¨‚ð#Ãÿ~1£wÒúÕ¸6}…¶…]'V¸®ýF¬ÝáŸ5·ÿ'i޳·ç~øáÑ4¿_êÝz>9Ê‘{í© þJã°øÖ$^^g’ÇþŸ‘Ѧ<¦½:õ‡/Âß^m~Ox°ße_V{#ùæÑ­ºÚ§¹W!qh¥ Ž1øO6“a𾱇§ÔYüXˆ£YìG4=É®y£UtŽ›Z+óÌ—}´¦ê7â&Âï€o=«nSºžXÍ ¥é-¬*È®‘:¶SŽÀfð ^j ˆ›¨L¸7¹A@™³7Lì¥ dàFF,È5Rµ’ø-E¾ú5ÖËýZ›Å”@´ŒÂèhrÃò¸ Šni\ž[\v dî°‰jý‰¤ÕMŠÐ$¬Ûb¨¡–ž ¶Hѧٳ2–6Þèpc#P¿ªÑ{ˆ·´, ¦€â1ò±µÚîÀÇI`C£’µ(›³•ï)ÙFâŤÛÃ/ !Bq·'îÌ×¾) C¯!,óQNŦp`þýA¤­ãv J<-òöÈg$—‚#¾w L¯áeH5c¹2ÚÛÈF¹úŽÐ8Û†?ÙË衬{©HDÛjŠ“­$&×2qâÔØ3ü 7Ÿâ,—kp²˜à'T.ÓÎ0?¡Ýs⨡Z0C¿¼Âh_3ï9϶–åj³‘KÐ…Ù—RH8/陼þànu0e¤äêÙpá£=|’¢GŠôÈQÐÇ;€±¬x?g§îƒù­zOÃUÌ—X'… U†ƒŠ!Ç*Îp’É ¦´¡"ª±B?¹¤éÈœ› \¹ÆV)D_¹nv BT,ªÅÈ$D¯:=dÀÇ(­ SZA ô ²8>Èí™%÷ 8kO„¹¯ ­2';Š1Þ‰ãH”;¦ÙÍî(Ú.ÍÎ “ŽèÙ¡ÌÇ\É7D,uk*rgrÙx!|ܪ—X¢D©ö–ËÕÿ¸†*À¢FÖ@]y-ñS|ßö2·ë¾7\vÃâ³)ª÷¢§‹ ŸcøRÕ”6ý®R´=dž•Á áµN¶^{þ‰$š6«ª Ç–0å⎮6£õ«­"žáI¡ˆæIÏ’B}Ç(ãpÉÂéWÑù^$ 9}F³Íœ¤<À(¾ˆnÙÅÿèhëÎag+GÃD5—Â^êí”p‹Ë¹Çjc®dÿÕJîpd.F}·þÄ·t­§GGˆ‰_KþuHbËw¥ð†sºi‚À¥ÃÁ ûÉíÿ J$úþ1s}E°ëðPŽrò‰2à’Çïtަ0tå,UÉBz¶3ùŒÿûÞŠsÝ9¸Ëꨉ¹ %GÈÂË…¨ª`öŽùewÏ•å“0s©#+iÊôÔ=×mfYˇܲX,¸|p` ½)æ"Дõ6®H¿‚(4š€Ç½3ªJŒú´¤^Ï)•ó’RµŸµ´®ñÄ%íZ¬°ãº}sr”whh}îó&E楳' ¢òº[ÈéøÎ÷ ¸T%ÃÈâ—Zïy56í^L€1óÑ¥9³°qfÆê `p´Þ*µ\Þum½cAã¶K‰&øáç1Çš<îûŸYÎ2#ñ}þÿHò€æÿI·jh;o¢î‚Rê‘DD/&Å:3q©,Jæ®Î$Ú¶ª•§•3ØråÈ)BhH86JK»”^Èä^ë?%åϯu»~.ö-ø/àÏç9£PnÜb›Z·Fƒ¾:ͺ÷>ݤý6w¿ß½õý5å#}ûÁö]‚Sø¼ÏiŽkôU8¯žyîùR×§kô‰¸F‡š¢Ñ6ü­Ó56 Q]©kªÿdyj™æŠF×ôYlBÓè)¿`Óh™ý˜¢±jöY~RÑè*F¿hÏ×5Ùržeè7€5¤qñyެ³ÿ¨êL½Õð¼Ó_·pª¸‚F1ll%[n ºáB­½á~jÕ U·lÍÐÚðbM,¯"eÓÏ ?h”ÑÉó­NXüÔ²È.²aI¦bPç^‚V|ÛÑŒ%Yi/HÈOŸ¥œß–/¾àw4âOºp¯2ávä=7àû; ¾ÍowX̬3Ú€¾øn<úËDGüÔÁ˜’¸r¦VÙH<Í2¡³,D¦‡Ìj€bûJ†zŒÜr@üÄêò+üC{x??*Îßs³óo#|{eÛN?RYTv‡Û]wfG™>;÷2¦\Á”}îbXÏ[žÅÖwxÉÚ°t4Ùb¥¼¢sû†š`¢ ŒàOÛpD)ðyo*¬°˜uy¹‚À$J–†H´Ÿpȇ!œŠ@¨aayVÏè“HE{JwBa­ê×,›ô²!+§®L´‚`*ÇÏ…\•KG¬½‰tнQOÜ–æ;éC-œB ú8&Îõèà†@5œ ãY¾ Ø$á2åÄ}Ôj‰Dz4˜d"¡2û-|¤lyAÄE*Á…ߊñÚ:.„ØwÒFÃb£ùß œ sÜâžë€ðg„n1Ëpì0hàëÚ'gŸ -–KÄà’¬â;}±°n9qŸGl7zbbM}EcÀˆ6e*)„Zž¹VÖš#ˆ¹zæ÷½«yÀ®A·ü°¶€ææ!O!„ô=Ãa°Z‘¹)÷Û0×eÙ6AgÓGHyT®ÐzA»T5 Æð 38aÈ‚ÿP€÷w™;µ~w·bឺ$xìsÊÀØË÷Ø=ùd’`†L-õçMç÷Êü z€+þžEˆ’ÌA¦Í=!Q*˜ mˆ$¬¶ÎÅ¿ÀЬA óè!`ê©®å<d-Æã;›o²ß%;L.½œÐÓ„”±Ä\Í•¹Jˆ5’;¶u@3¾¾¸‚ZtÎü°J¤²'ldp[pÒ+{qöð,ø>O;¡äW*nöš ÃÏáŠÏÓð\Êá •÷¼ó„=oqy:G(.‰bÑmœ#D_ÝpHì˜gJ¨j4¿ßÊæÜô+I'rÂ%{3¥C2¶`êtI KœÇ´ ¿)Å}5ïÌ]À@(ê Äe‡Îú§:Ò8lKÃÇ/ŠuL+åùüØ ~u縬ZÚ 5i*îJAÿøêç^'ÎvÊ¡x/l³‹zl3çhÓý×ꯩ@¾?•^è9‘r|CßøÍÃuo ûi/Mp‘ N®ú囆ŸB1Ï7 Ä™s› Ö•q&‹4¢48¢2‰Z 4Zy¯Z½«C¹6õ¢Ô£T¿°‡ö­]˾u欽®Þ¸>½#“~l^v»‰É®؟Úþê.óša†÷à¯Gƒ ^×û33€ARÌÂ1œ@r!ú¶kÿA0Nÿ’*ë'‡×ÊéÀð*¸“n¡=i +÷GÓÝo!ègzyòø?SšW3Ø aTy8ÿ;ˆº:˜º˜þGž;¾ÖÎ[Iûî?Q\ŸÁ–Qi]gfî7¦c•¶§u4”Þ_

‡%¯°V7ýÙ$Ù=î1û‘Ø$lÛ>©’}_˜¤« ‚‰1šùó t¦Û.âŽ_šdÛ³yæFIw¼M=\<@‚N~eä~û8¹¹fvçÍÈëéæÎù߈%³Dy^òTOµi™ØOqWV5a5Ãsë =#¯V±šwƒF®ot×DÀ€¨vüç]×yU6“¢™DZ%馨ÙMvnÙn¹ÔTÝÁÔè.'B,—!$jd_9CmIÛ‘ºÊÞÄuÑ3-ÈIOçÚSÇ×ï?>&~[¡ßÍ9:8úº¹Ó‹ÕðOœz%ÚÆ³ìY^·/×V~tQø—fzäÑmU ôA³Ð}Ù²m8ÈŽ/Çç$Æ»%¬ &Ót~©óIocn|íõÓ³ÝÔõ§g†Q5ÊP.=¬JÙÎ6zòvïÿÈŽ‚º!)È<=óE1 ÷ÍGr!Ñü¤,z(X7È‚QµxïMqDæl‹Æ?·#Ø  ´ï–ˆôAΞ ÚØ5nó±D¤:mnZsúeýriaèŒxE¾@W§` Ãu#¤8Ö4¯IÄæ0?LøøóÉíÍÊÉm&Á0¡²!/€GóØ+”å«à^¦+ m¬¢›´ 0îâçÌìåèêQ0ÂÝp ,|yúˆf¤[Á ·ŽÇÙƒƒƒÕïÌÊë“ÝÉÇÊÕï’60kêóãå žþ&~ÝHnóñü9¼Mwàwò€¥æ1 ÞðçÁŸ¿Ÿ—}ò¸Ð&²6.Â9‰žá~|?3k}‚“TšC®`zͯ½€k’áºýî_¿ßYñêåP<ÓÇøsóºÿÊþ)X˜&l»·mÛ¶mÛ¶mÛ¶mÛ¶íýlÛ¶=ïü===ßDOL÷AEdEdd­¼jedÝ‘«x9óô»¸ùžßößN<ÝP±: wH¿Ù¹¹9T—’„#À ~Nߟß×ÏêVI=¼¼HX~$•ŒïÓ&ЬÁß_åµÄÌþ*îâží\!¬\=;0p|¸884ͨŽÒÜ)&•ç ­õLëñP¾½Éoû£d ¹›}zz`Ö"<ž®N^¿ÇV¿ÔNß—åÉÆûéÉ×8-C„6kËøqT|áï$v’L5ͳRÃð¿)Þ»ÿÖoˆ«#[ý#ÓV[ Ïex[A+ß•ÝT[3}áÅ ’ o\À•€[·)¹ƒkw·UŽ®}œniìg“¦‘>,öÉl£GcW{RÀ»¹¡X‘±ü0”í°'*áÚ%-þin›44Õ 6Öٓ iì2‰Xýý¸r`É p’ÎEò¶QlšÄÙ‚D×I àÑb)ü)<˜_HŽ 2ä3Ô„yXÌŒà¨\yEc˰bÀü{£fé3`?A’]µÝŠ)aÃr›fNŒ»¥‡†«åàÃwq‡†Š"I{Ì þ2R.“í%{ÖÜ0ÁŠ~Å` üºÊnÚ ¨…U³‚š¢Õ\]‡¼“2î† dl+õ;®F°x_8 ø· ±_.%ÈÆ«hÑ:<SÑ 7w ^ah¿%›|ÇfA2ì›…|+jX”^ƒã#‚"ãC3”3Eq%üpåôŠ&rýÅèVòá«E‹ì·|žû¬þ/…é¨k°0 æEAÚÎSfá£-8óß3â/ÇgP¼äi‚Ìà™Ý>E614CþÚ¨›d7•;.NÈBÆ×‚DZ-Tί‚|Àðž/ 0*X@*+œ—c?NØfBC+"4K2Õë¿•XÕ-e÷¤KV-3‚oZ-@c]ó ÙÄÇ(…S„qŽÆš2ŽÞK3‡ŠÄð}3ôTž.JÓV´ôÚÒBQ[-›@’¯£tÖ¿Ë2ÊÛP§ïÑ@Ð[Hî•G莥ÅRFqYHæÃW-iIåmûùkôã%rêšJ{‹iÿºeí Ì’‘Åaã†åh­š+ßy]6àYÆV {ò|çÐlƒõ$ê EÉÙ2¾ÕNGòñpß•Àþ]¸,ú áãš/oÃÌBBóžÞ¶l6²uR¸k2Âñ¿ÖïÒ—Gí}o¹6Ól™EŸê˧ÈîWï¸ôQÒø¾'«w(†¤‘ͼ»jš7p½$©|Áræ-éÞQ6ì(k¤{qÆd2ŽÂÅëšÁvGNº¬5â´{pÒytdë½ tHƒÑ«m4é:H&LÝÓ‰»‘põº|v,m1ïqE—ÃvË?…½¿Ù‹t§UñA\H‚ÐxW°µù<šÃy4¯Øöm Ù&@®7Õ" zN ¢xÂÖ?B`œÿ\Z±Ád‰¼& x‘D 0¯ÿ Ý^àH(¹ð'{Ï'î¹µ_ÃÁj¼X[ïo"úå"Ax!›v†ÞKÜC2óV„Ñ—@áxŒ´˜ø å\bÈ·`¤J?åÔw,Ì„øbþxL¼A+1vô%§Íp:v%ðyßî†ô=ì­˜IË¿¢’¶‡×ù ÇÇÀ<‚ýRW0õEØÂšMæ]# ªÙM2X±cƒ—Áà°ª‡0² 9‹ûÊCïì.ó>é‡u·õ¸zµciu[qXvbñíJNbºGöÌ…0’]ü†fß²=÷…*†fÇŒuø$½G þϺ¯ƒN1§ ?M$ƒ?JyŽ™èÒÙO‘¾ý:þ¦ÿKâ¹6Wä‚åóðÇ‚ýÌ´DAWBBˆ\ÿó~C(O·ýÕï鬦ޮÒúïÒVk‡lh…=dC£Ô ~1‹‚Ö ÉtßÈgð7dv´ ª¤3¿Eí«k]n;VH«]èßoæ•ÒÌß»åpÆ2¶ïè?Úæ09àa|(ä“ÊŒÐr8šèjH{Œó‡¿üÒÛ¤Óþл£[Á iˆéh²móéë.ÝhßÚaW²\ÜŒ°&ÊiܪÝÝrXO[ŽÄYÁò9(&ÆÎ]ù¡[jùÐ^È´jÑOG师ƒ´¨wkÓ;ˆ@vMbc`)Ó& ];æ©Ù@éófm3ƒÅG¨¢°œØ`âeÓÉ'3œ…MÀ.΢8(0í£M¾"%{:8eñt› Y2HñZ"1šE}%„ ÚæµdÅq‹R*ü´~#¯ÜñDÝ·šêÚUècƒi—’¼òƒŠFý Z|Ì/|©g¹iV äpªðü| Û).Â%»þ1˜R$†2ªÄ§‰þ`‡Ç$ØßÛÚn~€ð„¸ÂôÓWH2e>ÜŽ³ý“Ý‚ÛÔÀ±îð“l?µ5}2çÖzçõÑU1¼t€ Ì¿´«÷.n]SŠZBÚˆÕq­ªŒÖV£ ý=×»Å(€åjºKN—#ÿ†±SKÎZ<×M…‰ÙX²H\ˆ%‡ä³Žó°æeg°ÄIÊýbôäÆþA¯Š„ÈîHôMídDõ#o[¿¯PyÝ{fq}€.lÏÑfhõç„â¸A%¦°ªÁ«j·dš!îÊÍÓ›×°‰B[”ãôMP“õ¾wšØ.}¶ÝzÜ#b¥tÐ_…DØöÕÅtP Û´Ážò“ År‡q0‚x;ŽšÛp¨¶ÑfÓèÞWÄáÁ÷çÝcS©„`Ø&ÀïFK*ˆ•ÒRSN•Ը݋Ü0dX´Î"r½q;îF²i¤¸†ù$ð%µaàL=3Ýs`Q¡JZÇ€šÚ Xéš^4ѰÁCž‚‰!œÑzúiçbx;¨;—ž` ªàõ¤OJà›6yF4„ƒ.Ì­J‹ á4‚dÞ•YñŸ^pLëfGöþÜÑj¤VX°°s¸‘q®X™ŽøÓžžËˆc~~³xîZ­ÊX1ð¢d%¸,Å Éö8†>“âÂòr¼ðyžú°ä[q<®|è’]À­tÒS3Ô+SGŒç.B¬åéæ=Ù¼†ãÝ ‚7¬Èê©ka*z gÇòÈÇXnƒÁÍI@pÚŸø¶ÚXeh³ 7vuk¨ÛÅŽ¹»oËS_¯È¶u„–ÁÂk–¤".žr~“øä¯3¹S߈ax?‹BÇÓP7¼SÇ,1k‹Áðë·Åæ·|+ºy@l!컋gžïgísèx™$µkH5¦Ž+`{, Ç #̸kZ$rš¸ƒ‰*…âèœb!|%_0º ùA®ˆjD.Ž{¦t_‘ŠÝAºµ]tðûWÞ”ª ˆ@+Ÿ;V²ïfL"¾ÊBÏp|»[Í#¥Ät³ìí­}éûªõ2sýt pé>>×õÌ…½;~#a3Šç'… è4ö6¼i,¾GΨÇN¸´”¶ 83zd„ƒóþyüž`ƒ-îmg6  E|DB°`‰5KX€M8S!wh¤¼²FâO…Ng¡ì–úÝ‚çÃñ"®3œWx¸ó Ô Gîÿ2ÌÁUŸšS²8–Èì{Œ*(rÃŽžÓÙã%á5g„4&ð³ ©-„´»JkP„¤ÈÚû| —i ‘­Xærþp;‡"½‘‰‹õkð³Ë‚Iöº´€š;‹ÜohL<p%—튱-­LWlƒ?=µèyo8æ—’f+œÙÌ‚Œ<Ñ!nT¥È é”Ò’wtÏÁã›”?Eëbîò¨™÷àµÒ‡"wÙóÎ"6è}«ö7ðAwÀüÚO(0z4øbáÉíˆA)ÚK/¿q’E»tËçÉÀs”%è*eí¾Y.o‰S“à”ª ¦î#ùXgoÆ¿‹îGAÓGÛ'T CZgà”ÎDólç®Íg9]Öúm!¥+Pû9-a¼ ‰ízr'-%ï­Êqœ¤¸‰3±$GªHÉö¦U¦RNÊ¿ Ùtc±¦5]W‰t[€)B½z…#_dsvEª¡¬ôO_™Ä|ºû”ðûÚnM¤mÝdàG >¦JY{^9TÃöòßwŒI¤ ÏIÚXdæ) Æ:g_‹:—¤Úz¨£:Þ%s•œj^¡6¿Ä+ˆe%´Ï¢Jê:õds6>‡íÔí7Vjɨ’wÄ•®ÚôD  „C2,þߺ*hIW `*ÌžIšª{æ®>¹™UA„fÐ#]ƒK,w‡fZžŒlpµ“ú òt¨™AáAöÒLø¢¬=g€lo·¥:$´ðxwûå7¶2¼òw¼wЖi 䯒ô¤)Á'£.n2$ù¶žq*ð]'ól­' šrÀ‚h´¡eÂ÷b£lêº=rÀÛ ëk÷ß±qAÙi°[¹zE’Û¹¦}b³©¾~‚Ä)Dlq%•7Ù°øÒA¬¤Ko…–)‰ª÷š'ΚÕg…DìF’çÆ*Tƒ`‚vâe<Щ8Ïm¸þ£Q¦oÔ$¶Ïü”ߺleô¯E,ø\Ì®‡Š od”É9ŸÌŒˆoÌݺ³¬ÒrvG:/7š8bÄ GÌpmÛY¨fW@ajɤ8×pÀƒl¦ÎFÓQªM ä š& -H±4¬%!=9ÌÃr²B¥Åù‘)ÿÑ8ìrìŠúÖ)\þI¬XjnÔy·Ãá­îÝüCÒûzáY©ä÷‰dï5ð§ãTï»»~ÜAkÍ#@‰c6«[¤% óåDÀ² F­¤éøq£-¿=. Df¸ŽΆv^'ŸæRÇiÞò¸ñ‡ÿ·j½wûf¤æ"ǽSq£tØïêâ|qT0?àÐfÊÙ ÒýuÁêø9¿¯Çô®ä?=Ç ûOðK*U€¤¾S”Î}ƒR0^·‡æm½¹þÓR¤8@E¿Ü5‰óÜ‚úòÈ·q9W{÷¿ŠÎ7>A3‡–U}êåfú˾á!cDÓh)Ë,,u•b¨j£kò küéþ%c¦´Ïú!{>c»vüTžo“~úÊòˆ÷a6µ„ñE9Õù³ƒ>Èâyœàk¹Ãþ”YêúŽ'™3îUåøÍ‚®øËßeBánS­ ¢Œ¯ps†¾Ùw&R5²ÇÀQaõìPaD¦±²™O0G/{”Ö ¤vÖ~9q>µ#OJþSáNxA/ð×Å[Nw|ÿ¢ñOÞP]?.|ë1ÓŠæ¢Ù¶Œã‚Øe{Q­Ê™Ø·y‰ÌCú,¨“%S¥%Ô1DæaNl[»%AšªììÂ9…Ð*À¢ªfüHkÝ?à¦Û“AÝñ2<ŠÝKör¸kRRÅD ¹F—¼·hDïWÏSÞHC¶aÝI!™sûK#úyx<܉ÈrƬLMFwî³»óÀñ¡âˆú.dl-´“9O²Wt˜~«ö’Bçn¾®1«%>ˆ^ ïy¯‘]û>¤æ¦B÷CWÜÓàAÕצ :B5$ÇUÇ’xï˜æ4£Œìøò§Þd.ß°¾~¦º¸¹ OøÊQ,},é Ïb^%©XãóÄá–͉{Yï³n(³àSÄ—5ÀNèí*˜¶Îèÿf¯#«uLC¹âQx[–PA (“}¡À¼œŽ& ²]°g=@uñÒ{›bõ±K:ËÖ3)°û(_cGÃf yìI¶Þ亂-êÿÚK•?Äû$xbÌ=•$܆dô©8ˈ9•ÿ8=1è šaÆ^Êêð°tô1á«>È"44ÚûPûd~z1åçÏïô¨ïO]m²O®jÚ«eÝ|5od'OW{¸[yŠ•yé‰îêã !+áÅ0pŒlD)3{DeÙý³ÂØÎ¿z–ÃmzÃ\áAXÕX˜›$Vš¤z¼Ž‹‹QÓÕ}Ø™å]6Œ±¸ƒƒ5-ö¶8et8ߊ6w…¶Ê~˜™lácGK¹wßkÀpÜ_âÁ¦£k’µgv Áh§Ûº#íÁG–9-ÀÏmî#ÆW‹+C¥ÈYLôH@š+–ŸÂ¾dÞãÌ ôü0™‹hõ¸Ê­V‡nݘS>¤¤Ü[=\-€ña´^Z=|˜c¶<¸£< 9[uõ`ØnJ.¸·Ê˜yTåE<ÝòkKgµ«Ô•¤…]ëùaÈAw`öè%$ì8d´„¾žhÑõ"ˆ;)nZa³Å™Ò¶ËŒ˜BVvÿTͯàÌ]TëÏ+Ÿ\4´cƒÒ‘@<_̸}Ü™©>„Œ.K¦©^S'Æ··¸€Q´#/ÚYð’ àè =šE¤üª«É†–²ïM€¤éè0ÁÜÌnÆúÜ‘¥öѧyÉq(ÈËusH¨'ï<È$èzYí4Ø·±–s˜Ö¥e& ÅQ…9{¯…4ÎûÅæhÃäw>¯ ×ÚcI™or¨pC HÁ3Þ© õÌ)C½±VM®_8.F1Òãn?rñÜN”41ïCÅR~@w‚)ÞÉÇïáý™ÏoÆ0¤EïKoÑxÓdoù8ÈÁïj^[T™‹½‹1¤±ƒi› S'¸ìmû¬bøèS| ±IÛ»65I8»R ¯c«ü¢¨ÆÍ›¦äÙ¶/sHE¢EªÉ½nVBV~62)Ó°°íJƒ£®°Ãµ'Í›ˆíµæW÷‚òr,Î Ýe ûò†þ¦q”‡tjÓ¡³ìh4˜ÄÆS_+”P²t íÓTŒ¾ô…'„;2óN]énª@\_.[R¿«(QÜQy)³tËpŸ-“ó}:.5'¥Ð0JuZ2«p0°t¬ ‚­ ßÅ\4Ê „Û¥¿8ý,äî– åÍ—¾ºêq8œ|\.¬Àž2_o¿³53¤Ü—­|t©´ƒàØ ?݇m¢ÌD²-d5#ï”ìžØK†Vüç¡3Pê›ôØ,~xŽ8NkaÞ%•*V {o¬çS9©•62€EëRYL‘Ï·˜TþÜ&µø·À:°ê¡'uÈ0@­EÈQc+¶NÛמN‰‡”v/Þß'Þymt1ë­K‹²ûöê”é«4\o¾D;‰â:<·rÂT8“ƒÍ>õÍÏb]ß“g ¡°T¼›ÞFzŽ·búÅ¥»Ïë9\à E€¢¸­ˆf$ÿ¾¡žHD§•ì¹n…œÒÝ(*½´Œ˜¤ íµG|û©‚1,£^C¡Ê~®®ì¹ta¬0Ù O8î2CÖ /Œežjø»˜Î"k<ëˆõ–˜Ñÿ½Lñ†“Éy“p£¹g<Ë«‰ÄŽç(u–hµŸßëv.Šëdó!R6P{µWBeûdœ Ðñž‚5÷X‡D§‚ØØ{„Œ8ª|â{q¼TH“Þ™EˆUù©Ê•Åÿñ‡ñ‘h½%XzžÒqAÂgøªüÂXÂZiýTÄÑÚéÖýfk ÉÏ{Œïz{ÎáyØëüÍ'Éë±ÿi'ÛŽY¬‘¾ üïJ“¢'ÏBÏiÔ¬_,B‘÷#ÎPL|¯ümÞòi’Í÷ÇÈ7dçEUúÕÚØtS£ÕK>~Bœý²è&]~QIÖ+·"”£ìKõqrP•Ί^;‰¸W~ªÄ÷[lèÍé“\?ŽœL ˆÅaAÈs”߮ՓÜáªz\Ä’ · ~Ý4êóÍ ˆ ¼«æÉÀ_Æn—_ ½â9Øm|tꜘ# ÇõÀãi¡ù©µv ÀOØ/àp¿¿üúçÁŸÄ>μÿ”Ük¢Ð#9J³Ë2#Ò“˜,†H%šÑ[‹—TÚÕQxaƒ‰5lSOÃÕ& Åà-±4O{PǸ}êM1rÜ´{çáUvRßðŽbÊ+Âg'AkaÖfëùÉuÝ{Mo}-d; ¸1c``Þ¯zz1~dÈ‹'*lêÁ¡‘=nþ ŒÄ·Àñùý„Þ|á€â´ëoxC©ï¼¸—{½×„iüy³Çê},¢¦P•¬€=â‡4!Ö5еd×…¢Õ{¦Nª¤¶C-9 û9'¯ñ-öÞÖùþÀÿ,¤Ò–žÖ*æ8ÏùÿyˆúÿûÿUãV_OM¯½êËÓEeQE"×6;©bãº\iææaDÊHn¤RJ%aÙù©ÞÙ:¢Ú¾ª´‰Ü&n¶°> mkêF#ÝͯfìsD6$q¶8$¯ü‘Gq l64×5¸ô6‘¸Ä7&O?}w½ê+$¥˜ ÿ$.+VªÝ}î¾üöú¤~Lý÷îpý˜IKÿ®Šüíúï¥(󹪪Tvñ¸©î2ŒÕOts”>R8åª}6qÜ+}Ò9oÕ>윷‹yO;ÔO¼gÅN˜O;äO¾fàR÷ ãš©òYǨöæÇëªnUZs©äû©ø ptTÃTLÓ§î˜æažRw¨ú·t0¨½ULë§îXÑÅgGSñ™Ñß2£¿Týøõe*/U¹+ÁUo¦ð“ÐËU_ªNTøÈÏt«¼DV⪾9ïÔVŒ«ÜtNâ«7!Sù ­ÈUKªr•>”«Øê”êÄUå«71Sù‹­ä+¿•ZÕUç«69SùÉÏ0Uyë­ÐU~ƒªxkå§WøT}ÅVòT?ÌO|«øÊ­æ«¿ŸœòTWä+6¡©þ‚rÞª/£«^“¦úK¬è«¿§«^£¦ú‹¬ÐU‹ªz«ë«6­©þ²ªzëë«7ÿëErÕkØT¡2Õ½²¯úήr“<¡¯Ü,™’—êåVu•Z¡«þ6Uí.zT?¾ ¨ÒSmµLÍO{TýI©ê-}´ŸüH¨ÒSn½¢ò×]1Sñ[±«ø†Uí®|d/¡SõÕ^áI};öÑP5Wµ•;9;Ï){K{WùÅ=¹Süð©âCJBÙÔ<{:'‘ƒS¨Ìfùb£tìóÜÑrÙ«F»Ç¤r±Ð(²÷3Â'ó3kËûû$güAxî8oB5Å`8à¾ëë…A4#,ÝÀH×!)cÊÀ3Ì‘1Á„F8×A0cƒ !dpnÂr‡ @HàÞ˜Ž è@àÅTÁ÷Ëw›ãåݳ~‚(ÜœÁÁV#¤â²“Ã~yG¿·G«xŠƒŠ|IÞø»Ü`áLã„ÀŽB ä„0Šï("Dtq؉Y㞣˜|¼frùJ»×ï `a ÂÜPC„2IÅ““¬$/Èi%«)TTN³ŽP68à0,Ž ÅiÇk:“Á|3tpnԚΠöpnF®"1ØøN¼—î°ÙãîÙÁˆ<¬Ó66e3)=à‡àÛl—c\†š‡°XŸw ¸4þ§Z7à+ukÓ/¤}ÂNÑþ­iH‰VHÈ@µ2?„OïKLµè†\ß9êÑ7^–šxªêèNÁÇp¬Õ(!„Ò‘ =*ôjÜïöوͥ'Ó@Õ Knñ]σONÿHgï"ó)*usÕ¥bÝrXv=5ëKÁXÉCZ(ž’Aàƒ/Ûj…ïKê!àî&¸4ÙL¶aüp[òã¤2¼c´W.õK3´¨ ªÏíÄZáuÕp^t㪦wPönTkÛ¢¥²ü»ßÕڽέ6WC½¯\Yå}a;’“‰OpSq©¦ª¤ø5þaôÚæ¢Ë©bî—/Stì‚.éÝ©½¬É€êV ŽÈ¹îGTææ<4ø)IÕH/îxøÍ2ö:ãù?Ÿ¶¢Ð)Ñ^m_¯°ëñ.¥žse-¯f¸Æ%¦½$¨fw×YÉr[ÜWô½Q=È,µŸ‹`Õ{÷Ã×—RîLÒe+Z[:(ñø"ä €‡H. «pÅš<ëL6Ö¹:J+±(p¯xÑkõçî×âȽV¢òóé^)/*º¦§ð¡«ÇW¶êÙ _6ämO9úÑŸ^y«rqÞ¾Vªµ_qÀ1Úvé¸ÿ»iÁ,Õw²Ûý" ÿ»²î¥ò™uÔÚqU¹š¦sôˆ)bIs Ï+hÅ]zK¦â½í_…]•·.uÂòáçv“X—W\¤ï+|¯³»ÒÜYõö?^­@YWW4ÄBùisÆÿh+Þ²ÈípG¦ÜIf‰“ðyÉŠ;‘$ª|Bù‡ŒÔ3׉2ûåóSÍRAé/Íy¥éñé(ØúK"4µ*¿9¦…æj÷€&›îò‚©ê™ÖÉkÝÿ1÷"}fƒÜßÙ¹'³ó•ŒB–‘»8xÍRyÏ¿(¯/÷ÇÞ®ç<Â’Ží'Së]+F‹N ÎÙ€3#»mÔy{qr"ù´ƒe‰ÒF»:wú=ìÕªø\‰wªöe“™)†Òc4?wR§ Ê9˜ÐéйX÷ÙJ€Ž*‰úô Ô#²kN5 í·’qßRKãÛÅ*[«O¥æ¬Tµ³­Õ•š½Bn–kl–›½Õj9ôÊùÏà´ïÛOù#]Y{YúR‚F€Ç#ìb'[:¯¸Ò£N¦È SûJ öy"WUXÑð¶,z™^„ö¶ß_¦kZùcé8QíóÀ¶á1ûf"'ÉŰ;ä{[ öáÛº B\šó‚cjyů²é¿©KŸ¯Öø/˜0›t»è+WVó@,;Ɖ\±äV‰¥xÇ@%\SV/de¯ÙøŸ×Q£N«Û– Š*e@~Ò·ßæ«›t>7ÖGŠføf—Ò1:´HˆMûoV JPÙsV£™wç턼¸Žy岈·¨AC +³òú<ßÜÆ9£â—‚õGèÏ×5 –ªË4E(“¿ƒÆ%PVà`5a:ÚôIÈNU‰ããV‡PZ&±ú”µª캄ÛóÞÒ–r¢ óžˆ×ÐÑÑ:õàT¡õš4â1M…0m¨A)+ñÑP«új–©ˆZR¢Bð°$t„©*±éo‰u=†wx×–à.wTb.ÛNEŸNQ–ò ûBD©4½.._Û&W«ÃÍí•¡gØ{n䵎®¬“‡ä‡ºƒ1š-ž˜8ðÖõ錇œªâ‹]fŽñåôhÕŸ*pÑËjœ–KĵÚõ.mK„œ§Ã q!º>2+¯€!k¯ÀÜÖ–;j–|Pp–â €¶áMtKÓ´¼#¥.ëx'׋ˆÞ.ÿðp›ÅÇg'öAî‘ÂnJžG!$&+m1 (»sÍ`rõ€\þ)aA`¿§XÜá­H;k »AÜׯ¹Èïb‚Äψ@Ëv¼EÏù¸ªæ’ÿ- >0sÔ-SðUC ×}ó°ßp®|2ˆBI´±N¼yŠ‚g„ÁnDÓMFÓe¡û­„†wÞC ˆtüÙáºçÍé'áº]Üñ€˜ú9ÀÖK«ëø/Òö]ZÚ-ˆò:Ôî¿öL‰¬Á{\à8üF—yzË<]z³˜"•I«´…&:ýÂuŸÈìj­ EØB~5º¶ÊN…º:t4K8¡wÚlíRθ!¡ì×"9Ò-²,FÖ襵rCafKUµ"ÝÃq‘›ºÖ½zÖáTÙ%,%ˆk?¤èw¬!hØkÜá†_\Æ%›äÃï?ŠÞÚ@žÞ<îJ͈¦­Âê÷AjÃò2²l?®'ãC-C7;#»©KÑÐÉ‹HözYÙ¦Õ¡ZÈÉÅP-é2Æ.|ÚÔ{¨-º S;1kæÄ~KÈâ´3dànùêh×m©«™]Üo f˜› {„#À™ß’0Ïš0¼ímE~ úÄ0$Ô‹ч»}²CAƒq3›ÍvåkÕûG*¿]5±Ð«tòu\)ª /ÄÛÖ‡§:ÍŠ}ƒm©¥°-Ä£Coøàa[äDC Àn:Á èX KíHS¶V;$ø¦†mh€û´}}QÓq^Ý2%6N=äzž<5Ù‘˜­N©ŠnÐ<×»³…W|¡ØsUEgcu7¦E-½i› hÒÙHÍ3»)âQ»Ô›%úÞú¥å?è•od•ÕT'°nðœ>}ªå\W·F¶Îè–~?óh.³xîsI혿gno%àݤ¹ô)Ža·ri/K¯„ç%ôç!à9ôáO«´—)ËgQiëg%ºÖŸÛ|]д.!·ñV´È^Æù[òY׺Ø%Õ ªÝç?¤5žßÃD´ýKð–(AÞµ)ºñr×cŠ1§ô ä aÍK³Xæðx¢¯BCqßu|µ&žŠðËj¤7Æ{‘«M뜑ífíú´eÿvðV>£Ô¸Õ0ôÖÒKp6bçÈü‰M@?‚&î|oQ_õŠ–™o’s…4hŒpŒpŒ;åïƒIz! RÀÛfÏÐU–àÚ™ú Þa©™+Õd\6éÔ3ú˜ŒãÉéõFo {hMÓÛÏZ­eº£!7Ù6s‹Ëû³4 >  ¸$­§¤8ÅwßgèÀSX¯бd¯˜X†¯³Äñ»h ›µË>Gu%“?=Åj¹3ª¤Ò­pÏ‚íb„GôÈÞßÝ;.wש,¢©D[\Óñ*›éT›=€ÁBö iͼÕmÄac?!D¬]W3\Ø7·“óB›Û²¼Šýì µhvj›$V<”S«Zv#:ˆŒÛ×À¶=2õôõyq©…Ø+4®9al€÷Ë‚@XÕY[lÃÕîÙ£5–Ù™ôv×vÅ8ÆqÙ†4c†h  Ű¼òÔ ÍjEBW‚[žÝ°a`/!—†i ©ñ¶gò†m‰…$NáC Ž«È¥AÖÅk¨Žp¥É¨Šæp®Ý«óù&>+£%ä,fµ#öCø-õˆO™WÆkÈcÕ× ù1¾Ã$ÔïÌuyI@· çÑ9ÄÙã0ú¾Lõ%‚tk¤T‚‰WqM4»ëZÒÆÐ4ÓÜt´¦Ù œÓv’l~ç0È"©l`ö¥AäŠO@Î"9P`¾@!¼eKÁ­#yFiÛm‰[›ã`‰$Ÿ1oÜoKš[¦%q›Îækûƒ6¾*]Ͱ3(†5¾‰‡×‡yÊà›§Çd™»u÷S3æðЄ¼ÄtËçðlú³­§I¬¯˜ó>R¬~¨1X[M.–Yžô‹8eE’M‹>ÒœV¢P´e˜2 ˜1Í" Ix‡Û¶[ð !Ð.s*¬— ض—¢¦Ü3v9µ†ÀðÏÉÊ’d]d‘•Ç:¬Bð÷B–®#éJx[N(f,cÏêðx&C‘"ȬfaŽ0§Ãbˆ²¬•¾#Î3µÉ–IâèHY뤷¡ì² djÍ}j¬÷Ô¬Le‘o‡‰ÞàE… »žìg|µX€Æ^¸ [†)«QsÛ;þÈ®|¾Æ®=¯3î"àÜnFŽ0nwª ê@®_‡~Œ®„Z{ßiS8¹ÈË–…|O¸dèn¼–`¤TWÄ(Û´CîõѦ¶WcIy×¾ Âû¦OÆÁgZ¼øm9R&Pªíøœ„zÿbÈ_)‹}ÜxL]†q§BÖœJ* ªjÀ#?p™…vÝíÈí?‰xêií"Ù_`º–~æÝ×ICäÄ9…ø{ˆ¼“{.èS ‚~ÖG£JLÛ¹5Ò“Ýý û¢VòUifXê5Oa®v¬Núšä08èqV>91r,3û@üÁŒÍG†}øÓhyéB¢Ê,Í1(ÝE‘8ØÐ|S®×ø°•“ íþ‘*¼¡²Æ‹ƒ»Zç|xd‡ÑØ_Â+„'H覽ÿ“;±[î÷d¸8„K?ë7IRB|ÁÏ=Çó[)E‹l³l ó;çš Âhë×ùPî|æDßïóécÇ'¸¯SÛ¥½®b~U¢¥e[dð]šŠÄfUÁÓ3aÏ(62b탽h%ŠÁ# Ò š’ƒ‰I Y HÖiJÆ™RаEïIÙÐ é|EP¶ü¥YšªâÆ4A‡¢Çhs‘š:oJ÷à¼ÜXŒ´ð8ã†t~~ñKÒWÔ%–3,âÀX³]“aDY· ’+»Û u©ùQb¶”N)„lDD”ÝÃ<6eÕÒ~je<÷^“Õ¿VÌY òa¼€1í¡ŒœÌnV׫ÝÁãk[åp²Ënî)”V ºZ8ÅqùØН©ùå-U­Yz‚)©f )¡L)ãÝnÖÂöF爺ÅV‡/´¾˜ºÓÓñ•[~jÍÎq­ Iª_ý`d$ ΞÜ.ŽÉÌRÝú¢DÀl /"–´UÝV­­_VþµXËâNM­„++“-÷xþ}£Zˈ|0§¾bxZW™Jò›×æ> ŒÔ{-áÌ…øDÓZßH´‰:~/ȲHèX‹¾#ÔU ÀË!5©sÿ¥UI€yü6¸MÕ[F‰ÉVèâÌ@.E¨¸ÖÓW{ÊŸtì©ÌÅõ‘"z‚탦˜ÄST?P¦7BhÎãɶFPKرV–´²Òܩֈ&PÒˆ žAF`K¥"8³¢ÝZ ë­i³\_öOâòÄ m@¡ÒÏ÷oã%g—a|ZÁÆÁÆÅXìƒÈo_ÍÄ0'b½‹ÍÑúT‰ÈÈÚå×ÞÊÍn(›Z'¿ƒ!5´†eËõœWa«–üùáü…£™}¿tçÇ:…T²k×aœÀùGëE GB9,¯p‰³€ËÓ|„F+†­ö%ó|ÇcãÑÈä ù¤!¡Á4ãÃ{l7*86 ²ïøªE÷"ǺaCO«-.Sƒ·È»Ólx3‰FFRåexÇĽy8êf)µ¥í„×…;ɸ9n¾Ùv㋹ôÁ6PrbýäG• ‘ë]óXØ>¼ÙwkP¯ÍÚïá¥:8ψÈ·Øæ£Â,'WéT€mK«©B<ðûG™îxÛÁ–$o “|áI¯ /ñö\F³gð‰^«&ø åÞèꄇ¥¡×¡µP¿­¶¿%·æýÒ²½&Ù±]Úý“2 ùRϬœô3velì¿Ëôc¼/Ãdv¢£àÞl&Ú÷4cTò9¦äiM¯b¹d´fv@·yøþŸ"ïñ²6o•6¬¸³x[N·†bn¶ÓÝøì²2>IÍGd•²Ëή€ÌÈûñu›`¿ë†åóÜøÙI¦Éǹ€ÍKÞšŸùæÛc°€°GÄfÒwŽ)RcbþÈiãSrÝ~Ù½€úa©š×w}×v7KÙ$#²½8äÎÿ E>!R¼ ±wŠãnçñl§ì;á²ZÒk³-¯&pXÓ~½Ågì¼ýcÜQÊe“Ïî|ãZŠjp{o"˜ à¿b~ðR¢à°ü¢ª¾rºdÿN+K›¯ß¡ÿê¤($ñ¶ÚF=!p#PÉ’ÁJpBphü~›OÌâF™f`n>w¼ŽÙÃÛÊEúÙ6ãfOpðÅ5sb'ØCÄGhJ}1ŸË¢GÓBÀE#74lZ*)M_¢€ Ë”#ŽçN¢úꉙ¬16‹G™éfݬ!A+ù¾xŽAÏQv‡ã)Œ“‹‰#ƒlÎGö^¸‡?ñÙǒÂD¬Cª<þ‚í£8Ó…±´O°„¸x5D@ºÎÇ!œÝŒ¿ ™ŧqbæb´þAÅÚq_-&œ u0Re¶]¨!uOhXŠÓ‡u´ÕbUƒ(³Y&ÈæLñª;­¸nóÖð¢ñÎt‡ÿÆO”)?sKœÏÙqq#U·©‰õÕ¨QÚï\%m ¬ÔÚ.¸»¡æó>Á§^ø¹×ïIš| ð»}‹F%Ë 3S‹oE¢÷V=ŠÃ´¥«àD´Ã·ÿrJkÇ…±@²~#om$ýIËÿ(-÷^ ;âÌRHºa¤Äœ´·ÖäÅ“Ž<ëåGþž^Í#uK3wOS›óŽ2"¥ÚÙÖ¤ž!ôf0É̃Y@XÊxÆ£]ån{·pܵ,>¬wƒC‚Ã^àL£xPN/DÆ ¯â åÝ­‡»ˆ2}¯nŽÝ°ž~óðþ¼1ìv{öò¹Ô3¼ÑÀñ"†È‘B¦ù•™—ÕñR,þLÅ„Û^ÄÈV«:7öks ¶-ªîÆ«]Üv”KV†KVVÞD%)DBkµV§hÌÛçßàR_â rÇÄsš]mÌšáÍ–<‡Ç•’›ÀŠ,\­éĺ«R {©U%‡p5œiºßésW‹ L¡QPš"Õ7lV‡‘Çô ?Q´¦ß«ôbO+Ã0 ˆY®d¥[ÙÔeСʋ¦ZÆhòžÀm˜±w5š£`$,›ÊìzfµÇ¾­//¼öA(„S÷x#p¹a¡¯ø%²û½^åæuHdzöû×yÙ¨€±"Öã‰h‚Ô}F‚{ChØÇ*Ûj—±éòФZ-û]?“h@dc;¸ ¤ì¿ ãßà5f«½­÷j!N³ñ»,ˆ…·!ÇÂãv<ÁèçæÖÏ#ÞýR…üL2hlÔ7å«eÓºE˥ш7,#=ØŽ{ 7Dÿô÷1Íê¢/Í3¡ƒ¾è -„Bž" ˜m_Eji†Ëæ¥e ó$®ÇŽzXfà|qå™_5Œo§Æ(1\ÛiÙƒø}¡‰kP y<Ô1gE³Ækë½ÞÝX¿ÇÅV´l|# vlÒ«iÅζ=Ì–=³Â•7º¼YÅÛ ¯+øö)b;û÷˜UB7ØcÎáÖt•ýÓcÌî¼Q¬Îÿ—Ù±ÊÁ‡É ÌEQž.l¨›mWN Á/ømžñ1ÏwÈß‹£!°rعƒÓÕ-½F£YÙò$ËÙÑs~¹w£8&¦±¡Y…¼‡‚Ò&—ü•˜ÌhMÏPo!Õ' }Åv«fF÷ÆY†”w[p\3söÓûÃc•à5/wÌûQtåñÆ) hüìœ†Š¡Õn]Í•>rèõEVÏ}u.˜«¾˜¬ðÓ©W«¹–šzíÇ ´—g+Î/?ï—yÍŽl²‹øVÑ=ÝyŒSðÙ€sìjȆ鋿sÓ¾â‡Rú&KkœUʻ¥«CñAŸbi;üøUoD"lj¤R'T@ñUY¼vØ¥-yA«Òy¬–P’´}Œ;aÕÃ|1‹<)«:Z&ù×xÊÅQFŽ< ÷P³›`žÚ<©CÐÁÑ|_l*FƒƒŒ$º­÷­‰,ÏÄ\ܼô«Ñ"¶="dXè=’‰Þ(œ‰¤ÿïÛ«Cbm/;Çëi5u[µ™ô!é˜ Evî•ÊÞ¸¬œ\;ÖŒùÞMÚ8-^%û˜©—hÒFhã–GÐI¶nšÏ2}Hc¶0Îæsw¥Òþv¶2²“;W²‘HÀب‚xW´{`Á:ÎàH§GÝÕöòó_ΛÝ`_¹ƒóTv#åb>x GËÃdŸKëBJŸ—þÊGù=0Ü<· >&Ôøð;l1­KÁbª‘$Ä`G!`SvÒØÊµ=ë—]&ûzoºD^0u1ªãµ‹éVÚ#¿ºÃ+q3 ý  ù ï~ WñmĬeðžÌ%„JX’ DØ–uÚŠ®æW=Æ•Üî‡ãØ~Žz¼ÕЫfÀ%­Ñ>µ*¥VúÓËŽš{þ°XŽwpŽ™Ã ÁjÎ*Æí† (£=ܦ¹_Ö€kÖÛÏ„¬ÇÀg ¥ 9WáÒóÛO¶þ‹ª ’”̭ʸ›N2^*—¬\èÓ¦Ah¼Á¬TËÛ”#!þµZS!©wÇ–Å{¾²yƒäqí6¶\“c¯6ë\UbÒíæ£àŸgÜ"µõå0« ñÙ P·¦‘+ˆÙVÎ,½Û í ®£Þ-êÜÅìGЈócž‹Œ25Ìûkh3Id»%I#*át®ÍVÒŸ¤×'CÏ¡[ª÷À ½Öî<®ù²°Oßø K6—¨;\¦‹ÀCŒæ«AvcŸÿB#¡å.KNœ°ÅóíöŠº¡ò£¦©ø{Û‹2µ:H‡ð´:I‡¯Jù&âýú¸õeµÑ~²H‹3Û¾øùsvxCSÂx•8·•›õçõXHHX*5m˜v†¡™Ð³Mâ¢RdFÕÖlÎd÷´äÝÓA L‚A¶ÛkÏØ—?‡”½Ú¸RSøæý¬øÊ‰¯ ål›îëÇ…8ÚÊ )2¥Ó„ß L @ÈñVžB(·i;Hv³Î÷Ž\ç˜Ñœ˜4ƳUñ·©¬ý¾ÿlÔR ákmßÙ"ý:šï.O·mÊŸòó36%²OæÔ#Ú¨Ýî©C†(.…B5¢/óQÄÝxJœŠlþ¦…‡ÍŠê•ï’é]!niý!¸w)6¨ùÇ©‹ˆóédöwj¶Ò)yÙÙ!,åzìàâ=N×là/ÖW=³usD R2"Pf%éDˆ0™Hg¸óÖV<±‡ºèK¸¦À?ØõèÑ…‰îÌkÓ’yŒÀN0øý6²vsŽ7ÌÒ`¦î\@væ¤WèÌÞÌ™…ßëƒ2êä9œ‰&îs}ù2oÞ†à¸n¦Ê@—Sƒ6séµð&z31æ/ð¸Ëâ’gJ¹°ø8 &ÿõoÕKàß>ù„Õ-ÅHÃýf¾$¼|W} #ŸÅȇåÈ‘wM¾Y’ÇãrW/Ÿœ[•AòIwm¿U“é;öG÷,›aª•[ñ’©7³qA ÒçvyR›¦½ ²+W!m©þÖ<¶8º µ“¸›úFÜAÓ/jð¦¶ëœ© >6H§š;ù •3ðÍ>:ÅGK僵V€ —q ª¦Ï)¬³Žøì·;`…Ã. ×Èê¾­s‹£ ›5©,ŠÀ £€z†­pMÜ0Mc¡Òàÿ•M’¾Êãî F d±«¦'Ý¢ð©Q^Ÿ^_EšÆºb‰{݃šÉÔŸ±cA´€yV.ûAá²[˜½Ë•²µó†NjP¦†>1q®18‚Ò)F¹.gs“½äÀx‡±¹–Âõ˜±Ü9ËÓÏûÝöʶ%ÇL6 Ⱥ_¶ñp( óº‹óS)ØPƒ;%uÛhrLx¼ä/ªÌéØç µ^Ç«ñ¸MMjëõU¤r®ˆÜãë0³hàÈã-ÎnU¦U5§{ç}ü¸l\Ãk Ôò¤_MºÆýåÈfš£áó£gØI8l,H,nÏkv¾š5€zÖþK”‰?,Ô"лÔÊFÖ´=ÂaÚF Icw_ž‰ñ˜ †­HãbÊè*Ù+¶ªìýGO G 7àa¯(Þµâë¢ù.,ñ\Þ’ ×n ï`¡’çú+ŠÉSij7ËM\CÜ:˜ÈyÀ‹DWÌn¶{­ä-N L¥4 cþï¶.jAþ„Ës¦¿ŽÁ ³ƒ\® ¾FÓàò‰õnÚÜóÞ5®ìgd æÃ}ÿõ6ÏhA°äkúÕÝÅø ¦ÓDO Xñ™xÛEìÿwÆØt]5\Çüx[þj×÷¼ÇìÝd'¯yåTþßÌ‹ûý-‰3ŠÜœ¸:Ú®aÇÂÆ§ßÏ‘™ç“ÊNòñ°Ø85åàß·äxìæò¤ê¬/ EmÜBÄÑu!ÆN àÿź78®qâ7ø~dò!nÕã0m—éÚo!¬õÞ"¹¶}·ÉàÈ–D5ƒ Šßƒß=H™e¸gäÄh¾ÔoÚö³ÑÄkc…¯úݘ[)!_o¿/pœÂu½í ÍMXc)W€•±ÊáËj[ž‚²âÍþÚ¿á¶ÊYá°Kîd|ך«þ›Û8²fœ »P¤Üv¢ÏST_–æ­ö‡©r¯Õ‘D~æÜ˜^s,‡Ó§G\à«£v ÚNÁ7åú é›L p3)š±>Rªi mhb²}¢=Ûz:5`xå8a€r,]/û‰CFÅTtIjœl`Ì›pɃ¹ ™8xæ±¼µ=ÌPã·‹Béò·¬ ¬Í ù¸˜8™_Ö¹JÙ†¨¾D¶#a,–óû·zÚG´M1AŒ  ;æ{Ú‚‹ÞsnÌsX+ïúZz¦«@òâýÎg„F)ÁvýÑ;¶_/åK· ]øV•„èj)®?o¤GWŒ4¤â–_—ø)uÍžnšÃÙ>»··Öã¹Ýs{}ëÈ~Ï; —®G€-A;›ƒ¬à®8¶w¬H>²ÀíÐc‹kƇk5ºÚF‹:fª>ÜÝ»¥±ƒYÀ¿Ž6ØzGFFx|Ù&–¹ŽIü+ê‰!Vg$°ˆV½ß/Øê­a1Ž»;èíˆÖd³ ¼ÖøÄ0^"™f§¤_VÊž÷ŠJ»×N˜W„âÝxÑ8wº ±<ß±¯! -6 †ÍŠðÀp­;:ýµ;ëo»þn?pÜ(‡´eDèriÅ‘7ë8!eèÉ>u´ž,µ$•–Dç7q‘{:±Í6¨êìZ–á/ Ë&ˆ}83Æz„±4(¤•ÁrD€vùaoz¡á× Ö£¿ŸyÅênDmtzai†Ol{‚ U&È]Q²È®òÂ&¸ñS¯§¸þ²Nç¤Ë=Ý®\5?6l£ýxæ1 ˆnl°ä;º9Ͳ}´­Ä»ºv¨Íoæ®Rwñ –ªû Œ:ä®m*"â’"sƒ9 -^ž¶ÛñÍõ&0w«CíÛ¶Ãu!(ìUR4l•jdSd£Q´;wŸ¤Èq¾[1Þyxãj|…³È²^Ît¶¯&Îv=~pQ-‰Êl!‡{¹†ýIóGh}“dôÍ®¤FcmsœúÉEz³n·è~aŠë ’EéKæ}¿<³«pyµð–m]ÇÃ]ñõXƒÈX ‚‚ü_Òps¬4XŠþ3ˆêø+c¬q58Ãè^â௯7+kµ®ØD8Çèf_¥V?eÑÀ T¹õýfÜ/}Ĺ—Ò&n‡—3šØÞã†z~õ~r Þ('zeÍL–ër¯¶A{°TZBÕÂ8`Îê#§A ZؼnÜ(έF¨øÏÄ\åCM¬F{§F/8‡'꞉ÐY¾Øúx@O½“°±Ë¬©­_ó íñn)KMã·ù¬FA{ÜÙß1åt˜üVÂeœ=b±T`òŽGŒÄ£âX'Ðñ†ÅtÑè×ñY@8ó…k=æÆ€¤ &Ž[ ó¦7pŒãv óØfŒøÝßz.,ØûXí±è,8wå¸zß¡æ;£{H" “潘1ßšp£›óîÏêDÞ‚QÃÍ!Åö©Ùºš¹[›ÿV½ÈoK*mï“~pCO¯¦Ùö/ÕY‚÷ bӠز¯4œ8Á–ÐuÈ—ÑÖaGš †…¶—dÎâ¥VS—,{ÙÕ ŸÛ¼#©n(r–ªS–·™Öƒê M®t{,%fp´¹ÛL%"VÐJmMq2סêÐ¥u4ôšíŠÙ1Œ¶j¯ƒu‰“çÓ²g¤rèÙéƒ}0µHsF€žº>h…o“‚ú¶ 25üzlšp­É U[ølµY¶.mk"𵏆V¼“úÝ™N¥z+üRÔú¶-ÏýÅ‚†cN,ÝŸÊ7yè°ÃHV&Eûy:wýC©¾œh@•A»9¤ÁNØÈ9³êT€ÆÇ!å‘ëõÓKýqÌ¼ØØ× ü2­ ³ç–(uQ@“NÆnh»zo®Õ·sC¬P¡øqëI?0xº½‘Ö“ Ícð @B™ºLZS{ß ñ¤î¬·âðA±-oòÉ··³oÄ\|Âü˜}3wiØ7Fž‡ÕÁ¿»p‹•ïÍoÉý\Uø¤Ç-á=æùùVA)¿è¿O–cómC ùç÷OLú}è¼Káøº|ZZ4n"äxº0`æ<¿i˜ûHªfÅù,lVwݹðË=D PÐÿ`¬øÿΑúÍÜ£QÑ$ߊƒ"ÌÎð~vÓš ë{:ɸ~2OK&E>“"þŠñfM Œ„! éØ|ŒuŸÎÌð_JcGaÓ~"6tlœß¾ø‹4ˆO0~çîÆc’~{(åcÁépr?0J‘í ã3ÀeµW @#Yd³ àö­Ç>•ãPÝ^“¸°»×ïÌù{i.„ŽâÉÝý,?ñËôùÙÐþwô>2îòj¥ÏˆiÜ•ãëiw•i°YN˜ùãðÙJ8]wó&·HÛ+b?õÁƒE´Óî ì¼pýKÖÁ¹ “÷¿·G[ûÇæ÷(nY~rõÏÕ{ýcŒÞÑJØÃ—“ß?î8FA˜,iT*Š{*%”rzšwdԑȧéÖ8aYú”3\ñ>ÆwŽ¢áý_’ͶGÿ>2~·å&WŠ%dÆÁì~-h~åÅéø®6s¥.l 2]Èz϶.·.íÏÝïþ€ÿêÞýª{¹Õ9´Î8@7 ¼ÿ“ê^Cz}}K;K}}:OÓ$Eûl”н'긤tYYÉÍðJ­|‹,K \BÛqp:R#ï6Ì“½;!¥ÊÆoŸïÜ`ôu nŽºì€«—X?çƒvÕ„Àæ±Ê±y´fP¼‚›J‘ri'X¶ª„ÍÃÌtκbÖ¾˜ O–?/j¸à àÈsžHPo•m¤óZì¬PÎÍ´T:Þp³®S¤ÈÑ«!œ ¤V¤ÞTÝEpT²ýk¡†=õê¾;bÖA±±•êW’çêp“fÙ§›Ryñ<éÝáÅü¸\^5(W„)µ¸*7’f±Ý]G]zF·UjñC¥&ø8xÝw•Uî\“_Š„®”1ž¹ÄcçíQÏ$úÑ#Ò¿™¼WH÷nŽò-œ ¸lš^ùWÉ´©[ðÃgÔþ¤g?Ȥªc,¯f»ct <ë§kòO…Lä„v‡ÑÐã%×r+!Ûߤe$ð´íÄŸwæÞýÔÃ¥ÇÄág¬ï¿Â¢ÊÞJ¶øqB–eæw7Æ<13ÑcmÑ+hŹ_ÀÿY§^{aÿß]Òþÿß7ilü;3mzX€ž=#p 02J#@H¨LrX˜$LU(¢di&å§$eæ©'g¦gjªÙ“égåÕåg¤j©H¥ÁÂÈËZ/ÒO ÈÏÇÇLßL§W_Ð\±GÆÎ_ÊB¾g"Í8¡ð?©[̘҆ùºóÿ¨£ú?韛©ƒ“½›¥³¥½Ýÿg¿]“í—Bï~Q‰Ï€¬–ð€,ÐF¥T Ml¯{—ÌÜ3u×ýy›å”}Õž¾O½ç¹ƒS‰'Ûžš{üÀ÷LDÇ2ÇžjJi²sk¸k ×X‘Þá[‹×<™Âñ%âN’iTFο?¹cʃ1±á{…Ûà°*î5 _XUN RQº„0JîÊXÍ“ChñæÑÌî@«A¯hó³¤q!øÃËŸvûôi %F@²1¬Ð/I\¢»mZç¢_°Áòø:× r‡özIâºð5N+Gò(S1"ÒÌwAvKÒ»r…ÏmB®•¤Ï$o½ÿŠjú]r>”\!aµÔFî)i›Œæðà7n§OýÀá/ܲO»mtÌ «eý²ošdçÊçNãïY§íÏtPë‹Kæ˜þE¸ž×ø.d '4kLŠ©åH·÷Z j=…”pÂi”»˜sUoˆN?/Û:ü…I•½¶•oâøàû²ë·«X¯k q‚®M>F¥* Ÿv’«(½‰ëÕú‰*“>‘ o– ¦²³„„ñ´œOG;n4Ö E5¡H¥’ú„ä?ø‹<ÔŸÃÿO~RV¦þ»#ø êÿ[Lë&4B1„-s "‚8::¥jÛ #‚#²³ƒï€@®¬jUôÕ‘_ZdE`$Å#<þ–ÄËy÷ìwûj)¨™ìÌÁ´ÁdH (¤H&³Z óQ2‰éž#ÇÀU¸(ž¢kolIz¸tÆÍUÓÄZ õ¬•º­®©[¶À÷$|‹Æ?"<>¾ª…’*r…gŒ¢¯$5Ô×*ÕÛ ¾o9ºäÊ90¡mb<¸‹£(}o#F¬nLDk[$®]çTo^xÒg‹»$£ÿCâÖ#¡„Ùì‹Wôß ûÿ?$†NÆÿ Gª²®ý2Šï|}G ¼u+ÍM˜@…|!6£Ò?7ä‚)Y;%é8[sÍrü+ ø}rr>¾³mïøîûLs1MUŽÿ°ƒ°éOÄC¤Rè.Aô1©zXzu8º°3{¸}cÓ¦GRí}Øy­™Â4N,êf›Ðîd„¾¢Fðh ^øs›x®Ÿö‘5VX%Î Z$Ðv‡À€xL+p¡2*ZŽdL¡áªÔ$ÇþRéqˆYˆú‹jŽÀ-•Óµ)N^,˜¬²H k6¥2œØ8»¤W®n>âá›TmÊ-†÷æÓ0)ݺ@ËÇäFÒN”@Ó€T°‹Ê»vÆmGͰ}ïU†à‘$ç+ýÕ[Íf·ÛŠSa€ÖhÉóf 3\ôù$M ÷Éï£c78÷¬ò§c/°dÊò U&y2JßÞpŽÞpyI/T ¥j 0üÖ‡ßI’7Û‘í7›°÷B¶°š±tžâ.Íf¤Bx ’T`,ÙÑZü2RoÎF=ǵ²AŠ˜ª¸:K´“ŠX¯ý]úh5hpíÄêòâøcÑÈo€¿~¸ˆÃ_TA2 Wn¶KR÷ïj~+×I4§vgš¨"~ ó =eY»ãáƒe7Oö+û«,I»{¥>y¤›É¯Á½ ¢êWj¤%ö‚óvæÃôP5øí°¦Ù‹z¿ÉKa®ãî"âö±dyo\)»,’ü=€]6 ‹ÀÇHóröÚD½-QLéâ¹äHü6ÝV‚Ï&g[“øîò~×…ÚËÜÅ/nƒ‘2­žÃæÏ?äªÂ¨˜žÛ ûìÿsâ{£s·HüGù.ùÿeÊ[•dí‘pïì”° P¢¤à Nv «8DÞ& +4iç6%q=ù[^Û@K˯8ôߎH`Kq*Mç72Ûïf×ÿ:W¾|ßœá¦޼"§{D‰˜˜Á˜áÍøJúœ½ €)¸Ü Î…6AÉ|p3}€9€ß3ì^)® ’>C1¼:ZÛz]•q)‹«yéCÓb{=y'hµºcw“ñõä8Êgz‰¦Þ%RÅI½DØøHˆßžrh‘%ά¥º¤S˜2ˆmã]ˆ<à Š€Â Cµ 7µØ"—`eÓ*•6hœ Ù *É Æ~;TµFŒ cb[¨¶’{“Í¿6~—5äÊÿ6Ù–“‚´QT—·ØÚvw¹ÒJ'6¨Z’KM¯RÝÀÉ&.nt„§$4‚•ØÁöaCqžÊVNU:¢o÷â×ÓʘUÃ×0ü¾ê˜ã5ѶMºi`ÔìILBgš¶Á‰’jÅŠ¯)¥÷’½øg*&Þ!üTN³ý$œ¯‹«äGC(ꡊªŠ©Ø#çªçMÐÚéÔoÛ2àýŸ.âÓ8ðÔ¶AJ:éºúÔITS??‰ZêoëYö ·ýìýÏÈ•Bø9ýÆsÿócÃÿ¿`4¶15tú_ÿWð*Úo2`øÌ׿ƒÐ˜rUfe|&,ÊÅ® ŠžWI%'Hö‘äÅÜlÈY¾^½qвXú9>}ÜúÜ«çašªüªb‡xÓ]©GH§ÐƒûfPî`è©ÁÑ'Óßg2iažv$ÑÞç™SО*LãÀv@®ÒTß7 ±³¤í;`Æîöþ(Ù¿'b`©§±¤o):@ƒ¢C¼gº šXUƒš3 ¯;ª·Ž_Kyì*Wć˜…¬=«æ¸Î}Wä,//Z´o‰Ö bS*lj€·™¸qËÛï%î»M£XÐb|k: «±Ã2Èz1µ¦Ý`” j9O]í`ÞvÐßgÞdMvº†Ù[½‘Õs=ú@td„èå%½î+çYJàÈO^Ú=t•{tÆœU³…Ž"™EÈ6I¡9~Â<|cö‘YÕ/#õü/vûNêlƒyÙQ[/h»×ixXꬬ\܇…' 2DŽ …T؃¤Ù_$ß-êóÇÔ“%ŠR|Cï(ŸXc+µI‹7 ‰÷ÐùW]Ç烇*n6Øf‹pZé·¿ <­ ü'fóK¹—(BîyþðÉEOŸ‘ý)ä¯A3 \¸™gZæïu†÷¿×6«ˆ>ôœ'w¼x(¢ª±äÑÎO¾&ãÆÅqqêÞ¾‚ˆkÿ ``r(¯¬p៥/©uUÝ?Öõ,(¦÷ôÄ^g ×mðUbK|¼˜Í„%0›R«ÄáøM.¯G,Ã:kkf/C°Š5m³ˆÙ gÉAûÿ™­k;(ÇGÿ •ä8ÓþßÄÙ¸5IÖþBظœ›6Õ €gJ¸ ºŠ£„U`ˆ×D^DBP6u=¹ŽÅ¢“–nù/ºÀÌŒ_ TöFýwb&ÏLþ—7;?¾?Á ‡þÑõ¼¢̀̀ÌM;0$[ˆÈÄ%£‰1‚)€HjS>t@üœ¨|¢SÿCg ÝpJ{×M}{¬w¹Ž¥ëÙݯŸ²‡ŠH¹¹Zõm°ãðJ¿!¶CàSʼ"‹YŠ"“$F A.ƒ`Sž9JK<:øÄÆ+I‘zŸL)Cï]Vi`QK­•ˆõÅ^±ÜXÚ$i/ž¶ŸÛË! ¿gÞù‹â^˜ÜSe–ÕY²éImq A¾nûšzóI§ãØÙ±ýYsxµ#·ïö‘Áwh‰\ajh€ºÝfùöz͉/‘Ø'ßbggîæÞ çaŸªZü®f„`×a‹GP§Ð]€íbPó²õhuq`äñwÀ§.N5Œ£Þ²÷ˆ[2Œ¥YÐ&É:‘ÞÎ ¹G%åÐ>÷0÷þwÑOÖjU`f0%hœHÑš â3£È½ä¤j=˜9ƒ¨¶M«I+)‚ò¦ Õê³sŸÕ7%ŒÓ kú«ÉU.Ù©Kcºüw»³¸Zîè½ÞªÅ»ðôe9húHçÞ(R!Œ@´>aß:4Hx¤;„  3â2ˆ}Pà @à„¡\E†[Z¬ÍAd²®•ºYNÐ™Ž ë àµlL| ›]çM¼»|~]É·<‘+&Hæú[bìÙK£®hÓ¨óâ§©±—ô¨{%IxÈs¢A¤Ñ?Ñ4f@r >Ïû áðÌ·&åhr 9 O“Ñû‰û-ÐÍI”@Ã6×…P#þøÅ;{²`w`ömŸNž:&æºU¶´_⛿—ìUp©1׬1¿Ô:ÂúB£§HMES­­W‡TÕÖªS"4£¸ÖÅZå°öYD¥½'Ó) Ž*2TËZ=Ü(N)¬¥täiðL}¼«ºº(M•ÕJ1})»Ÿ_*M㽞l#]ë(† ]ä¦j,®¼»(©6°v•9½A/ý‰ ÏðšŸjê5_„€Nð’ÅO¬E=TQU1ÎØ¹•ò÷§äÛô|²cÅþÿ*šÏáq:ðÌk!ç˜ò¤§ø³§¿¾O“^h·þ`ÿgøŒÝqA(“þ_BÒÄÔÌÐÕÆå?&{µlã7;Ð~ùú‡ÒýÊXd´+µ\ïTŒÌ-ÀmÃ+ÁܪUï( "éålÁú7ÿüæszº+.I^Žžn¹é'X¡KŒª$ïú+KŽ™.x/f)ÖÈéåô mH²gY=_?Ë>B"Í‚°‡æiJt#JxŠ’Î9I{I ÎX®.ö*Uäv=ÌKBA`×V,þ` °“Lu,’¨ d)£) ßeY sBŒ2Ghö I­ËḬ̂ÀGðá„d\þ.ß³Ñ.%H…õw®N~Ù¹÷êy®";­ÆÒ^úáé<‡ÿR¬Ã|ÄŽ«P]¼¥H"¸6 }‡ ‚tij"&0KÊC»Ófšù¬V4»PˆÈXN’UÒQRÁ™Ð!tÉaõ"bÆ`§ïZ0\Œ­ ¾_ž¶3~lv÷ÞÀŽÿFlo'Egœðt`m‰/aÿ úáÂÐs‡U`yåKãg˜TˆaΉ¥¤ad@M'HYL”Ñ€t¾ÓÀš¥ÌŒDIÁ&„L‘*[È€^‹ 5“.a«¨RÌ[Ü­VÉ&Òcó²ÛÝß’Æ‘±C"Tfô¼Îé@2—5kÈ4™ö,<]£>Ÿç¡d?Ìí±QáãðŸÏz5ƒ×5£àüÞSB¢ öÃ'”eN Ãã¤ðæË”jÜbŠáœOy}*Ís±mÁ}…¢F|)Ay:T(„Uʨ«|µìŒ zw7Ãùóи½àÀ ›lìWïÞ?:ÿ­öwtþ>vòß~ ïºîý-aǵ'''GÈ j 3ç•tµ‚üw¼~!Ãð³‰F{Ê) 0¡îÍÝN„;?®"íË+åJ)(Xö\”6iw¡›Ä@ÿzb†¿bÈÄü7¶úÂõ …,çY£Œ=åQ( Ï:Q¬a8\­P$Ãî×;Ùð!E¶ ²A@ ÷w´²6)Œ²çP¾r˜5î)ÃI°N/xÆøg\ìοC5ÒP`ZŒÿ”èEü-#'óYþÉÅW˜à d¾þ¤=´—Q q*Ÿ ~Э~H{W³4°¯6˜oò/;Ëõí NÒÊý¬¹Ëý~rÛ‹»ñƒe=Èw/å;‰Ã™âË9qbêG‚²*¡O˜[æžvà:yi X­Y>Mu=_nçNŒîz^kÝ0IRà˜VÚº \Wùd:%ÍOŽ|{K› åšÿµloYÜ[2à¸ÓÁÊîq´Ë ßh½ª¹#ÆúR‚8Yi@cršþìç0Ž‹ î×À£IH,+VJÏ90|?ÓI7-²f‹ãY¦ø¿[ïsÙ‚T 4û EaFcÒmMxÃŽw=ˆNkŒ‰²@…=P9µ¹§œ„òxóU§e7…G¯¤<¢b”[™Ìs [] vÔ¹Q’¾2úÁoR <ݦ;¦­•!![:BƒJÿ— ”«*Nî%³’Ú‰aàÒPHZн5¦õÂ(f_W×ö F4VÉ6† ù3£H?BB!œûè%‰Þ‰†˜ /xš`]©‹vü{ä8ö¸MùÁ;{í6»ÝEð’PÒEõÿ¶ó¡K@wpP%y¼Xºˆ Ä`Š¢‘´UÇŽDç©Påé|}Ôr{™ü‡•&<è¡@–¿4<[7I#SÑ"í¡ñQš‚ºn)"Ç`êƒÅqôèèäRy·‹J«knÃøß‹6çývœ¾_®w®Ú9¹9ðÐÇt³ ,ªw=±Q¤ùŽ™·üÞx¦Dp™wê ÆÊÆ' "ÌЕMײAÎç^0lÑŽ²D$&šEå,ÊmÑ‘¾ «(·¾™º¾ûßÝW°u«†ëBPHÓò‚v´Ž ‹%‹¡Q Ù³\ux£Erðjš4ÈÙÑkŽ%¹8ˆmP-.Bòƒ[ ø,¨Wvꮄ\iW/†7ç÷×awU,{†×†ÂùžßÆn÷Çàï-Âõ%YžŒ±*ŽšN@ñdEŽ/¡-À×ÀdÐI=u …]±×ŸwΔŒ‘ÄRËΨL³ÝT‚ø±¯ðIK®>Vÿòc¼Ñ;3Éxªeï\®JQù¸M«•§}Xû·žÒ!A¦Óº'r`3y[ô`¤AëlWDt©É¯qw(ãÆQ–Líý¡Æ¬¢_ ưª†v¹<14Ob¶Vz$öã¸ÛO Ýk!Ðzlé™–!¬•å:óÆÕ4dda"à iÌÛXŒ1O3µjB’Ü%”/ʡԮN+Ïò¶\5èñþ3‹±æiýar¶LâãL(QviÀªžª36-”mszë5>Î…H¡iÍ;†­G³¾¨•µÔä›u¥Ù{´4™uë·ê)>y0«é Éo™–33«ä«Ì,ÙÓô  f„‡D/m0}™3´Ì…X!^— G€èÛ9ã[²­ÆKXøÓœé<'qW$Üpár#æ?¹ö–òE»É+æß ηx»´úÉ@\Yúà0H1c {ÞÆ¹8´Ò·NY¢¤ +޼¥©sg%þ¶1˜³£dc¦RN<0¬’< #|ò¯Õ‡Þ{X ‚¾ñZš=u“ì(~P·¹j”dÔ®€ž*`=¾f³û+Q{W”©î´l›=ñ8€j¤Ð²rRUK€D½ÎšU§‰+¢1""Ôaf§Àðœç-ɆÕ_"¥P>4#¾äO@u?s «>*W¬èq¸Êo¤^3{¿ì5ïrU»wÒš‚±"üb°’Ž˜ÕýÊùþÿÚ¨»oùKƒp£ü¿´yûˆªq¯Ö´=ö8fþIŠ$/­¨I\·d¶ˆ›í’è‰@R[)ÿ‚£šØ …õm‚Že§ô,Ü‹D©ŽIA»Þ§ýó*á9©@ ž—5=è¦ô¢ð­óŸ>{³4i)'úBYGžúÜܾ½7œÍ¿¬ºŸƒ«µ> ¦Ÿù°»øýì_à(€_4ƶÌÚ6M×lÛÍ2=“lQeúÆÙ¤ÊôW­º†Ù¨‹J%ùX[ù¦YÉKš‹(×òS–æÛf‹0WóÓ+tM–ñ.ôõSкF‹<×ÿZAVéšÏHWtM³¢ò æ {Ñ4 ~‘­4h*g!òìôÍf^äͻԵ/¥”ïúïím&‚ªrã¢Xd—a‡‰&K2Eª„H…]Ö -ÏI-ôÃ/º ¾¼¼;Ø0½P£?'­©ñêºwsäðø¾=Ÿ+ÚÚ~—¼Z~ß+ºÖ‡,ßhûÞàšº>q¶Ðì^ ­ÊF‡ªdÐ×qÞém•€œ«é áÖ„NÌ#—JˆùãZ^S•ƒ= v¤M!O‡zúSTÿBúï­Bþ”ýSpݶ%ЦmÛ¶mÛ¶mÛ¶õ¥mÛ¶Í/mÛ™ç_UqoժػÎ:óa¼Œˆ³£u´Þéä•ûfÒ©»lž‡(,l@ ©.1^áíp»k_o¸aÿÚ±Ñ)Óó`L‰ÍÙƒàD4 xü}øL¬~…‹ô ú ô:os|fêÒª·"-<œÜ8. q‚¨â%Ç¡àpØ¢ ™3*µ `RW,‰,¦8±üˆ"ö2*&²fvÓ1`tÙ,}ÖÂÛZ¶$,à…Æ,3 %«CžYEéiÐ6’KUã·SA¿V9tiàÊÈz(?&ÜÆ¤1ƒ­és R7 „/Ç•š«:Üd¹¡¬’Òÿ2h†MÕ£R•Y§«×gJâw¶ÉæõÁq<uË`2t§ßؿݟåšÜDÒÂgyéÚ~„0Ë]“ ò‘Eíuèxöþ,e?“Tjs=rÜȪ™|7”Ø}=$88`)õgïg!F@‡É² ib-—ªNQ7*‰™D¤F.Gm²R·¦²©^n9Ö.šLY Ë÷¦¬œ®7È¢jŠ™Û×gFr mZæŸ â¬¤™?}’¦É(›E£±`… zE‚úK)ᛟ² ¹„º)³f›Q‚ÁàÑ%s bX;(%œ!œzL´ %ÚÕº°*|‘޲£Ë/Õ!bñQ,­$ÿtȳõäƒÕjÌ_xñì ¿Š7%­p-`B‚^¸›œ›ÅffƒÌÃ?ÌJ­% Èó§µÔPl ;fy‘ S­œÌB‚¸x‹íå Ö²%Íèx´±ëÅàú¯uœÒùÓËŽ¼ÈÞU¸â‘#xë­½(O9w™ï öÆ€8™\JóÐú½²V~­ fÒìí³ÂÓN´ŒöP€8ÞìãttJgsÔÂFBÒÈB7&¼n&$5’n™m¡¥¤d)€®[{>+^yòâ/ö½¬HÆÍr††Yš)áÆuÌU¥â¸Ä73ì–¨›Ž‰D©› ËÑ_€‘;Ö#â-¡óm"â¤ÞÜSµÆµÎ‘ÞAv§ÁE©GrÒYí]Ho8„é›'eº‹søÇ!þ>ß«·2 Ü€6`hPw.ItNze²‡P-òrjGáÌ„qݯ-<›1RCä݈¬ô»y)‘µtN+ž3Ø„N2’öÝÁÁ‰aϺPé>>¶GÅìë¿c-ŽÞÕÁŒ²yJ€5F3Yøz G×ΠòT³h‰»9ˆ¹TÝ9§£L ž¾ß| ü©³pü –0¢^Y_¦Â&Dd¶}¨bÙÍØl"kCnRx¢Ì‘ÚNrM”²Üx4W-E&ÖžÀ^ñá½— á’O~<¸vFÁ¦r›Y?e+šµ=ƒønÌwË®9û·zê'˜­J™AQ)BYóKƒÈn0È=žªËò| Qœí•ä&%ÇŠ¯[åS,¯œãdÁ€¹—mAc).>yÕõ¥êaã^D:Ì¥ëÈjJéO”^Ý5¢‡6•¤RuØÄë¨Gˆ÷ÁavGwó$’Ö„Œ?TšqöõœFž>®¢z¯66ÍYrr–äVäG±B9Ey‰HQx‘á¦T Hç’-èá®\PÎd¾¬–yòá–ÁàòfzÔß¼ Ú:Xüéò”Õá ë2ŸW€Ûò£# “ˆSsp©æ±"$fÄi?dçæqP»Ùõ<Ù–·¼ÌÐcû¨FtÉR„h–0÷òž!ÈÁ»–®; YâÑR§¯Ü%°0\kùu=-Àneû\…eÕ÷d&Ö±çINÖ²ŒK}dstäÝ<Ç›*ôo¢_jm+ß%ƒjíwhŽ]PÌæ+pYš\šCå°RVŸ –Ä$eÁÛ; ©’›®U©ðO²•W,)͇m™ƒ¶y%êœ÷­ó8>Ï۸ϜJw(9d óÞZ †7¡á:l¾`‰G6Ÿ!¤¨â¼åµW:ÿ•c:4¨1Dbé·»BÉ|‹:2I dk˜ብê¾õÙ³76PVÁ¡òݧ¼ëRT‡½¯Aj]ʺ8UR±Xµ qeE'\G¥Ü-!ÙCÚˆÔS*¬‚ÈiÅ]éóžÊ¸Î¥ç¦ÂÌ(dÐÝ‚éyËß³’Õgã¦Ì`¢hçêá“iyÏuâÊJqŽñð)…«Î&„8Ë19^LÌÿ¾chMë²orô¬‰ÖRºS¥zAŸüÊÞ‹¦Kî×iù Ôvhe¸é5â5ãÌW‡î’°M‰½ðŽmŒéé” ‰€- ®0`s±xЯ XÜ&¹'€ –Û+tGÖ¼ p'š’oð†¤ ŸÎr5Œ\½C~Õ>gG©e4Fàš¬2A¯93¯mØÕg€-à–Q‹ô+A¶ŸšÒÁ_‰2&SÂG.i©/J4 Qu¯G‘Ô=Ü+Ü>’ê²Ü¹ÐÏÈ·ñšKo"A{T/Ù,«{ûÞqºðñþí„ËåD¯rœŽYÜ^ž06!+·É–Î]ÉW¦/ ›§Ú–þ‚ «â#¥¹”aØŽ¯ïßÐ5㢂U~ˆWkx¼ÁìÌ×培žð«nîØg1ÓâKåKj["ª°@ü[±‰ !›$Ö§s}®þ†ÓÇHÁ–WY3IIa¯µÖïÑmYÏÄSMuÿ¦£ áÖ”?UD—u§@7Hz›iµÇÿiÈcåD™$,U\ž¬„ˆîŽ)°ä]Ê[{¢ÏÂg çaçß7ʾ,Ñ¡2ÐU’-,˜Ìã?—Ñ[`¡,%uze€6¯|ŒÕälÜ õÝYw.–G¯t¬oY³B9^] ¯.®Ùú­¼pwQ9™Z±HuÜH)©óØ +F’ÂM4ÂBHP4ºÑͧ†OØýåù$2޳RçÜ7Ú­Á=š}c7¹øœšÅ6fbaß+vµ±[z“álV#Z~6õÙoZ˜ÖÒÑ43Äö B=  È™Ô$Ö%úƒmå±…½ùððpÌ'2Ï ô‚.ÓÓ]–ÕÏ݇cûÖ©†O¬°2'¡l£Ž­üô–©Ö!$èǘ¨³²)5ª° Dù4µO”°X J,”%òÂòæó$·D7,oÕæQ¢¡rºìsæÕ3A͹ñý;4¹’èÓ!ãÕ¦¢NŠ'u°¢Û­'71v÷â4“ˤ…[ñs}ÉD2_p‹UÜ;8Qä›ZßiBG¾J£$~ø|®>—îï|ÁT2úöÆíÆGÇãRxÜk«†V'ªÓ¾Ë×òBDùZ#$6®tI“ò©Ø¦ä‡¢pZü§ .g²^™ûF0€Ó%ƒ¶Œ›PwçYp]ar0.Þs6î¬÷Sþ$^«È¢ëæäŠ[ÂÖÝ ¶1%ì!Yø’)\ìd)v٣܇ìØ-Y½54yJ7´îÓ¢M †½Ê^?P°VŒžÛ¹I}o@,Z§Þ܉õÄ7P4-M:UG4”UÙñ‹ü4 ªÕ#“‘ð¼°)ÍLËî€iÈÖäg¾àzkw¸lí9ÎK«ï†3gô"·ÎWzø<Üøš›B ´±ä*Q¯__KU‡³YŽÚYáH,Ð*8œ^wýw£©Æf÷ªp9¹›pÌt]ÆàÕ"_g$*B•²26~}ÄP’B“ü&÷9«ÚöO]Ó®~cBŒÂ~šÓù´~·ôjR–WºRb/Å€P0N ¹ãÕDtŽƒºD÷ ¸H'Åùsâªê|£Ícæ µ`ÎÔ:&¶Y||¬á<&…¸zÕ©»Ìš´JÑ7™¡ß¥•Ž‹Ô ‘g¥6n@âãŠH+GÈ9DCvÉË vÅ2=Ù.zs‘”˜«Ò¿2øcÕKBëjµØ§scփ͹ºË%vnt¹ŸùR§ôÓðYÝá^«3~ûn³2ßÀ°£î³yf~mŠø_Ù‡›˜M[|ÝZ(²•J¬ÀO&ÖØÛ¨tq¨À)-o ß­gàÖJ›Êi?6Ý3™ñº5ºÙïøÿ^†á[¯]ßXûÊ0f†Æ.öNžÿ*‚«,Ømþ«®^½G˜HQIDEÕ! RBùç KžÉýzƒ#Ô×ëÝJ˜²ü:ËÍÍÎÍ­Ú+ãU· f·™Ãí±W¤5E•…IªìZÉp@‹hB$j»WBS߸ ®’µçpØZ`˜Úøƒ& Ë™6r#(”!Vt€e”G¸c^@G˜”Ír  ¡e¿üÊJ&:M¢ƒ%f!ö‘*{ß> ºnb#»nÕÚº^5íÐP¯T¥¦ èd}ørd°IÚë6¥CZÆBù™m/TLç6 <‡ÜHR!³VÔ¦,ÎJT»Us©\m¢U– A\»75“›Þ¬*ÊŒ4AGV¸º †òyâ·@Kæÿd̜ۛø#×s?Æ)äÏ„÷ÆìÏTºÄëqhþv+ÐU;°¦¢ùM’¤Íؘ Ad¤ ±HÆsB8Cª†©MX}M%x(e5Ô¢[Jæ‰Ø@â3$› öíHÁîn¯µ^º¿6[ýˆáÓѺà YŒ|Ö~m-ÄÕôg;dúN¤‹ÞÔF¢ILLÉj[<ò\¸2êœnÐŶnGB~¼ÔÛÆia{œBš"T95©²;ò{EÉøEJêPi”M©»Ïm¼éÜ„ñ/,ö{ß‘vüzèÝë8"hÝ”Jçö“÷ƒ¯_eÝÎÒ~² ƒl\…Îþv‹øóÄ'Y…â¹ P.y†c0Ã4’SÆ.–Ù°‘×&7/Û´Äy­÷;dÒñŒK£@SÄ~d¤³|õÁc*tGævXœŽÇT½#õoì—´u6éÄDM¸ëJ—'ü¾ö“—Bvµ ü”lép¦îêÅ©”r¼„òh«—:nï3\IÕlÎVoO¥»|)5«Ž–@Å0_†E÷ü/¿2ŠççÙhÇQ£ê‹0ÞiÔ·¶O_b|6ú~•ÿ‹6¾»íçÁ›Jâ#ú¿ç€dì`S½þÑ’iÿ(ô¿´Ä¸EyFnKág•_Rˆ„@®Üʾ—’•ê@œ DÐ ü5ËÍ{ÃDÕî‰ðC†÷Y÷àzv“°aŒkãþˇÏŸ™ÍŸ–ÍÿÇã“TÀUóÂGü·V¤ü(“8ÆxPÄf  aëÎf˜Õƒ•Q ÁuÖ#v±• }ŸUº lAh;lîšk˜è€ÔCºd'@Cl;pMp;t ô­VO¸=Àž¬5D6&;͊.œR¶5£Ør–Þ#MÜò ãˆt~ÿü,Ù› ïÆŸ'IÚùó`7'ÖÔü†‹†…®0Ét‡•ô¥iï#a×ì\W¤Dþ‹…‡ú[ âÆ<˜‚òF_¨™jæmãŒtªåžÇlºqŽ>Zm±3©½¥«ã±ÉYÀ˜6%ã¢á/Ƹa ¸ËJ2é$ÉPÊ~bbaia 6ö^qAÁâr[qµñyñ\BÚKe¢¼jV#È"Ý=«¯³ÕÅ„°äç1µif¨o[jgËÖÖö꬀bꉧõè5¶l­²z«ôFR³Õü '6 ÅJ‰ÝÈ)6‚XÇô¢ãÁ˜Ž½€én*è˜Jm•D¬ó™ð<¿ÚAc„Èz‰Š¼ýí»û(÷–/_æÓwèhËZÞÈ‹ý·¾ðï$¼Iï“þµªÕàñ[ÝÝ['r,c™ið†™ëªÙeµ‹fÅZlA“”ýR­ ɃÞ3iG4Š¿ìòÏ–½Ä!‡ln³-]MªôWû ¹œ$t„mŽöÃzîB%&ü{ª>Dņ«¶|ó% ‹¹Ï7þÉ™àí! Èn‘<)ù#›FÙGM&óÆÿaçÁÝÃdb þ£^'W#W;׸FMÖþ ¡çS}z2¼¥¡òª_£}\Pv±v^…Ψl F K„B‰”’û¦î6ÆÄüÚ#¹5sþènæÏ»ÑÇ]ôtµcéÂïŽFvÅðõrߺÝË¡’Š6µZ)øÓæWðýôÑÕQ(úX;`{“°MC¸ØÙ… zÄ-Òw¡7Øš"ŽøQ†½ƒ6T˜>òË|$íùQáÂâ`!*ŸñúV“ÒÊT.áu ýVK7DAŸmÁŒtZŠÞRýCœˆZ&¼’×g–M¶D‹ ±ÑZ±Jh—¢?G\9ºþ¢![ÄîZÎ÷ubZT® ŠÈy24‘%HØ hÓDמC>ìj^ê„õf؆‰ NçÝ©S£ø‘-æâ¬ø` :BÂs™ wÂ<Òf„†ÛäðK]œÛcmmŽ\òµæ =¡S–^©ÓÉ–R}~!ßüäçªJŠX>å´äII¦ÃÙ¬ñ)dÄÇW2&„ÌTÒõXB뙬›³&±lIqõK‰½8 (4%w;rÐP[.vÜ9ß\/§VÁv{—j(Ç)9¢Go)Åbž ¶ýÈÖ³¢r(×3;ò™¸¬ºÿùYáw0"§h¹¯PÖ•ÉÆ-NÕ5šÒfrB,“Ct+e5ðÃR°ûñx]Gä³ZC¯^™HÅG; ½œ¼œºXrZ4RPѡˋôhÀá•r ö‘eyw+Mét7c§úþ[x{ûXH>w:LLè‡"ñõC+qV‚HØ­vÈ yÆÙÏ„l)íÆ¢Ãi$œ•&ÒE <˜<¶·ãÛˆŸ6Ê‡Š£çG¶»;òM·Îv˜êðDeâ^PÊÏÈä’ŸÊØïP²¼t(5ˆÒ—ax„.Ý íÑšáÑï¼ïPÞ)üÔøî'zà^~"mZ,†Šp†äÏ9¿‚s¥‰?”(!ä\øpÝ'¦(Âø• W$_èôKϘ·ƒïŽM~c_/w—ãs{ðý3ù|kËôª´ùó^V¸ZåãIMH}óÏ›:íÓ¤ŽÆçæ–ÌüsÙcðAƒå(&Œ±é~"÷ƒ¦0ƒF7bˆaɤ!× ìøëãYìg´mÊuÜšzcn=ÝÔ1áõ·uÔ5oZÜD³$æÌrQ:Ô˜^?˜Ä3F§6õ2Ñû`†PÄúqÁH†àÌÇP±ª[-²·úC‡Žb:” ÔâÕ¬p»H‡ o ’ñÙA oÝš–Ç ¿ÆîB~ÆÏXD„ÝŽa°y÷ñ?[)ša˜±Ä—ùBllü–<4âBÄÕçY˜ÌÞT{54»¿Àÿ–iVЖÜÐôßúÄÿ’€gúÿ#e¾£ýORfP²dðÿlÿÍ­ŒenwrKßpÄ}¼Ìt² ¥_µÊÝõ¹õ4¯wò3¨Äüsü þ&GÚµGµè=&6¾1aÃÌMµÆ#`?N+t![Q7eÇŸ<¹O¦9ø‡"COl©-oÙÐg`Òp%He™h¯B„^MÊý‰S›è‰d¦ýÄ(©|äpdðz¤|³üïe…W01±ƒâ¯cÃÙ'#±ÍA»^³¹¡Æ¶i•«^õ.åÝÛD-#ˆÁ× ¢õò7À(µ¼å‹ÔFNp¦+ÝËŒPk®ðU5{Ì…S>̧œ^êsºsŠ…îè -ué'm£Qí¿™—ÿ%!³¶Œþ_„LÚ2µRk’OÇëP¬‡ÿ¹TÀT¡ƒYáNCI´ä¤ÑÕÜ*d“—X;ï]‚ðg:ßÜÕò\‚éoع‰½w-m¼`Ö=Vw_”þÐhE³CM gÅ:îÀ× †v㎤º¶d·$O—y”tòdÊG#ô2…üo˜|¬Wëx2ÿ¿›ßºpó–N?ÿ­ùýoè—± Pƒ aÉnˆ¢O°84%.Ž®ó² l `‹ÅÅͽ­ðùè‰A?Û¾8Û÷º_Ù²KÙ:RO² 9ÂmЃÄ*Æ.£Ñ9Ih ’ 2~åÅæ±Mî+TÖë±Ø%Ü$Çê?gÙË“.¼uÅ÷Åû&<äêz^—aÆo¹_…;ÀQr–‚ÐZò|q®}iJšªOs2Ørâ̦2˜Ç˜Ä3{䛫j.Z7'‚¸_öÿŽ{Å1_ÿ‰ø÷1þƒ·ÛØ»šüëùÿ<~FcËnK ÁwMÆ%)3 ‡bûBÐõJ¬Ü< ™˜ÈH®¤†2iGWJ ø×Û< =‰t*ÚExêj£#»CüªèΤú@ i€¿İð)– á|Uð2 gtB)jB¥éÐîVWô‹%M’¦ÍÑ„><(¯$>露^p°‡Åžë Z"ôàjýœéHõWêÊ(bšã¤™ÏD¡F“  Ä"P°7<ÒŒÈp·GZH8^}è¼²³ ÜÁðžuÜ"Î1r7t¥SŒyÚB–UN6.-t8¶iކ\Ü Œ®.Œü¸yxyýÆ'” %Ùþ­Š‹k·  /Ÿ­p¯Aƒ‰àîyÆ“®D(Ë9ÃÃF´Š>íÇ «9êfÁŒ*  z|+FÃþ¸Q¬Ø"8£]8;~-ßnýÖÀóóÞƒ™›‘úÚÕÓÅÃ÷™‡k¤û©Ã{ê7c·Ï®a@í ñ_b1?åxì^òœ+h‡²é^¤€‡…:‹ä;È:InpÌ[¬SÌk-så‹eEY˜½þ’[j~”æÜy¯ØºôŒQK§RmÙH ³E æ\Æë<í”>ê+)¹Z¸ˆ•\¶[‚@Ç`…㇯K 92 xhS•ø°2­Ð1‚íyT+êš·­Š2ÄÑéEn½I¤÷dCðÙjßuïõ³»M`ç_”ÚÀòŒ¼™…€´ÏÊ`NŒìD 0©“kókžÉb Û¹øO’°lIÌ)¸Ëõhaç.d* VÕBòuÊÜ£þÑÀ‘Ú*úÓ›÷»ƒéž-{ox‚¬t€Ç=š,ö[ŽëÉjg¬ Ø&~‚&áõB/ØèÃeôÄy>‹;)’Çõ’ÓZ:ˆ¾+OÂ1¾’6aŸ¿üžŸ|ßÅB¥p/&ö`g[PÃy>˸§û?y3`ZT¢jëÚsƒbr‘[v©æ,“ñ” Í:µv‘»Èõ,ÞîvçÌÅŒ¦©;µ‡ “IˈŠ[jâ¢ÑáHí}Ó^›âw܉$o³ ›ØH aŠU/<+¹U,cgOSJÁÝ…†kˆVV:e‰ ù,ɤª#¥™5Ö\ û³AEú D]˜Ü×`‰Aýt÷»™nÑ䲋H:t/kE7ÄUCÕˆâk××^Y³£‘"­ù÷®Ç)YK–¯pinÞÚ€œv ð[Ä*CSѧLÕ~3ž—Zj±Ó0Û7 ÊxkF¥PÉ^áÖGe§†ÜcÊ×·;à+­8,£-û­‡I]K!{'¤+vmÜØ:ƒ lRÏö&P€ü²Pwý”S[¥7[¨WO„{f•Ö¼”¦—A€[·&ŒÙÞÉadE¸½U¹+~6çg‘£d•ÆF‡òd‹`”ù?ì²gt¨D/%Ep³‘oï]ª%¿ÿvÙ¡_C"¥Ó\¾R¹Ž4V?|¤%×ÍO¡®íg§Ãf>î! ä|jfÀÖ”Þ¥«ŒÜ_W""¿fj,†NšãžÖiˆ°Å’“•3ºùÚÆ×´ÔÀ·=Ö#Y'8ã©Ôi÷’c“üÅ©§;„ÏÒ&õ1p§Ö³²O¹R.¡­Å\TÈ`¯ô_^öüô\j‰œ tj‡Ýû3Òi"¦&­N:’S:ѬËÍ?a鳓ålù}Oõ®& {¬4Ã&7ÑVÕZ`2ŒN/'¼ëóHª³hž²±Ô^ÍÏhßgøò‚7Ù\ÃiÓô—Ç2“G†ñ’.HU#zx\ãé2Šû*á9ë[IêwY`ï’í¶PÕ–{¦‰Âºâì:±ý-FjÏn·¨Ë^Ìð‰‘cH^ö1öuœMÙ@"ñv Џ@1ðñš”XݵûÝoº’Èn‚C CÈî°y“%OUOø‹.Φ ‹_l ‰ÊO8M&§ÙP¥ê FµSm!v¬nݱ¬„MøåÇPm{1ŠíìõîH¶QYÖOLy£?$9N“GÀ‡¢_zÑDqÝEà_›?-÷7%ÈÓô>î@ÂʸB÷)cÞxö…:½2ÄZ¯bUŸ¥}Gç»W†d)x5>ý— ÚžV6ct\‰ð2Äø7§]I[]æk®=oôÝ;»zÎs¢‹ô³Ý-äʲÙ©Ÿ®ä¹e ˜9¯KXjª…Õ—·ª©17mljˆrÒl1ik?(Þ"¼>_NŸû³Ð«hkÅiþˆ!"¿P²UŸ9èXg~@ÒÎ^í4Í|¯ë<áÆ­éÞtàÞXîÈšý"ÚœÛý š.Ôªj.¬F%=n×¾,Î\éÿ;r ó‘å¾û'hGùo³ÿµ÷4nÉvÆC ð¾ƒàID1•zØ<¦P²OÔ›2!4VåTj”ɄҩœAâ&@ßNw7{”>úMŽã|K‘–{§çüHpy½¢ÌjôI 0hg^ZÚb^ú–¯ü͹ý:ݵÿ6…x/‹ê“¿/[ ×ìÏ÷AcžCÅPyëD¤‚©$xèÅŠ¹d°ëÏW”`ï( V[XL´ÔE¸€\ߎ{+¾wÝ ÿë;ÍÒNáqÁ‚š‹é¹Dó9¯+}\g€ mˆ`–g,Þçúm21± ]B^,w®üÑMÇgÖÚOXï‘Pû[t~v¿01¯<éÏžÞÁ¨otœ2u¥kíêÀßóÄáFñ#*~âÔµq/¨Œ=2‚šüMÃCüW •5I¶¡çfî‘83¶Ù$JÌ/t+ìèžiøîÞ9õNü7E7§Û£!ðÙàÏ?NþþKßÖiRv®¯Ž]lÈîÌ”«Éè1çåål"¸‹“Ù@äƒe¬Ð–ÎŒ§Îûè2%½À]PGYµÊOedœ¯ NUPrPo†ÉæòrµìÒSåz;Ë3.yz…n±Ü5©ÖTèYgÒƒÑ)TF£iºEj(‚ ë"òçäÖ[O ‰ÜŠxËϫ֧B3”ÆÛ,):¿Š¥”̹¨j¾«*RíE…¹ÍC; C!f~)oop®k;™/[üší;©to—±F ‘$h{ºÒ¸ž’A>?.ؤ¶4‡ƒJvµ&®Þ€»;½?§ãÜ”cQE b»ªàŠÝíº;Ýòõñ5®`_ÇqiRaÕ¼8j_gº”]Å“Åd»”ÂÐ2!}ZÞ™l‰×ƾƅTËoyÛ| §Ù¢ŸV-nš-È@ʹx†„ËXð Y„É„no©4Þ(Z ÄJÃ}ªÑïï×GÞ0G*ñcM\†ó%°83_€õ†}+r S “'AŽEÜ<ª2èKèJಃ5,Or ƒ å/¬9­I¬gR1@™a“ú' çW΢1.L„Óüs‚šYšøK2›)ÑlƒBté%1KžL–²]¶¦·›¿»õ? _“–ÇÜ<*$Ë_ô¡ù4<³DÝ%¯uÉ$T«£<«\®öì!óîìKˆÙD„u*h¢´×;ÃøûBî'Æ + *X .|(1rëï³ÓŒš¤Æ3™´K]9ΡtTMÆC‚ÚæzÃ-‹0¶§‹X¿Á³ÿwDœÆhed0[2ÙRÄY×7}>†é ׿qGXB6 €-«œ‹d5;A¼³Rü>]Â,P#H^†·Ó•ê³²ÜÔoí˜uË0y) ‘®’—‡Â"!sÈ®˜ a´ˆù,¬Nç>R(`êG Òôâõu™˜›‹ùƒªIQ[ZÉ?‹Óp©ýl¸R꺰y¹ÆdKÉÑÂä{c~'3ÔS€Ô(b…›ë4à]óP>¿MGË;^ú€üe;í wDó@ Kk!¯–e¨ïbãêf7eWO1Îg IÎ3€oEÔé¼W£r½õ^#o%Ц„¬]e¿•GíUÅ—ÆD šS©džîÆOö´K’`'=¶°È•{s᪄¯Ëñ¡j:ºáTŽÓeư}r4ê0½‘˜J)-óºDÉ*&eÌmÿÀe £â5¸‹Ò‰+éßuæáoÁÝ+³k¶;¥ÞïÒ4V1.nÉdµ‹›ÄÖ`&Å,Èþ–!êã(Ï´MYoIâ¶G»k ¤o¶ŒØ¡KJ2f…Ñ}ÀD‘á¦ÞµÎ+R9Ëåh~)“¡LsðÜSj\£ŸR’óÔ±˜q€Œß@V^¼†ÓÃUÛžfE¼·¬@~÷r"›¼â#Ï—pŽm_(¤eþL‘¦‰bãÓ¯£1„ý¤R]kmh%Sk¿ Þ¢Ž?—OÍãœËr Í'ÞfÝ3 ~(ÀÜv’AEKŽ)Þ¬;ÑĸϰÊ >ïJ(crO±DÜrZõT$Áqºã–ÉêœðB9Å4h*¤xFq !ýhù¾qîÑ[¬£xm~);áü eø^ns‘ú M"êž­& Ëc~&îE©Kyê‚ÛÕùq~ú#þ± *êû€|Î'àŽc¹ß`¿Ð`¿ßðK´\Á^ž(ªy¤Y{òÎëìáp¶w_c·p¯‡·¨Ö]†¨1F]Õ6N)¥g´­m^Ö™(L,s Ä4‡@]*Øš½€ÔIö‘zÈÄŠyUÒÎc®90e”I·÷à¯$ú‰ɉd¾T7ü¬,žG¦¤ÿç|Ž|âȧr¿eÂî“c—®ñ0[c¤“9Y(Q»MW`RXx €ÅnÙº Ȭ†ùRµm§œÓ »uUÃ1•ŸìȘJ·2ÀEªrT)YÎk¦’î-gKHac¨æº¬`Ê…Þ EïÚ úâøÈŠ¡ ·†7fãæþTÑ·3•ö[¬¹³ç1òÅ‘jæ2µÕçèÁV{´,Û›ºï_¯¿˜noƒÜÀЇåsätú;춯O¢gf‚u%~¯FŒ\0ªüáÀÛo ÛµÞ坿ãPáÿÙ{ ÉQ”w\«¡QMcN¥~oûjû$ò{ºñ÷®ñŒ¼1@Ëœ½ 1Á¬ªa½mh=b§¸ò7y΂.î bÕøp](7haøµUO´äl S_¸î3%Kv×”è™O[$››•3ZU†¦Mø>ÑëÈaªCÝÁíâqxÁà¬ïšE,˜” ð9PA”‹Y9`÷@Ø«\9úôg06{Á})à³A|ÐÍ"ºÀ›\9JEž{ŧbNÇ€0Ÿ!Gð1>êÄÇK2ÌŦ†|c] "AbŸ©eLNŠÙîIÖ¤´ˆÏãît&<³¯g󘿊íÂÅ]xÄÔ ^L Õðd«gÜì­2ew}geß9fqùÛ@.œ=`lŘ òIøÜEÈœ‡¼þ|bõå’㜰E>&.LÑ,äjŸ§ÒA‰ïP|»x¬AR…”ѽ³ÂÈzsøŸ×È<‡z±Œ8ð½”$/ Îcã¾_¬B¢o8—LMÖGºÍê“9|rqFØœ£—˜ È»ã?ؤ‡K‘£•ˆ2@õŒ‡È˜øëyí3[{(Ú-© ÷ÊvÏ,?àX9áyË·Û#eïêЪ8!ŠÃ)žf…•­xe”CàlËc™¦UÝ‹´‹{šM;x¾Ê¹œ¾h”süÔаÌV°ùuGŒ?˜ž œdP‹†Vú0iá¼Û3?*ºÝ?•£!³›GËõ-#é. öå¶ãÙ€aI?3¾fÆ=,ľ¾Ip6Ëš¦²i›ÂוžõÇôÄ…q¢™«i–Û ZLÿžŽiŒh0º²®€4eßÖ} ÁM€ÞtýÖ¼SÙ—x$YI4ÂTˆ’š§"Ý%à/Þï“B…ÜAMÄja%Q ͈ùê ¡Â¿ÓA¾ÿT‚ˆ~s™Cþ%‰(ˆ†½ +Ǜ룋ÚÝ)/ÔÒ¬&7ºôûeàõUÛh¿†VÒÜPfý£ý…jmïîFUê7é§gл1‹ËU;eÛMÕ4àõhOdšLa”øaÏÐíxDÑðØÁZô*MY™µË¹S½Ì‡Èäë?ûëâ3÷B‚°³×ØŽõÔê½õ øï.açä™Ì šæ¿†ö_Q«v°¶ì¶ÆŸuõ ðdLMµàê»j QUmI©Y§6®º(œH…uf磴Ÿv½guÆ÷(¨×Û eg³9_·ÎP;h'O©v®9=™{~ßFU¢hÃC+i*#ÊDù¼X½Dÿ–1Î`ÊJä7ùÈOtǽ©‘‰k1ÛQÞ#ö+Kÿ}£Øï(Ÿ ô"óœ¡È—ÀÜ(Å$}’–‘–[Ì)˜^¤ˆ§ä,õ¡S”¸oR´ñ™GìO¿³µLŒ€ºo:íîîèä‹$‰€²rO•p{öóìÒ¨AO¨_ œ£0..ì[³­¶Ë•Tâul¤j:“ˆŠéŠÄ¢OøkœúÀ²+ 1y…¡ŠROBy¨ÚYá)®‘ŵ·à(Êybݨã}tûøìÄ ˆÃO‹ïßþû$á—ÅÏ3­?5’µÍŸ°õ¹²óàèÙdo?áóÌü>æäÊNˆ":6OoúœH•HFÑÄZ‚ ÔJ‘Ê"×f ãsÜŵ©ØHk‘ Ì]\¼BhZ.vD@* O‰¾Úÿ!*ä¯dããsâm¶ëÜ5á )@C¼6ð„€ã™Ëþ¡*_QŸÆ`ÒŒd/.à®è¨TÿBIAÜ»¦/ØYÎ…e[u ¡ü%µœôó7?úGWEúñº>ó úJáèŽ|7)ñëÈBíÊ­Ÿ`ª ½ß:׌ÆÀ;Ëô-â3‹ÞÜV´mžû¹éÀÍ`·õäN/: šR.o/:,.‰…žÌÀáöÚ¤Dò6/Á?v>Ý-uïwŸp¦1EAkPŸ°Kµ:ÎÒ¥·¼²0kÏ]oX´´wÒøf-/@ýþ‚ä%òb™{Røýä¨T)# ó¡\Æ;¥f`ôrɆ•ãêñYèSžJ¡ræÑ=zŸ=Ë_£Ý= æ º<™EÞSˆ35j°^m‡X kÇO‹ÿ¯?ÄíIüãÝõÃwÐt„;ô¨"Î žgÑÖëkÞÍà±Õ;†_6?ž@Ã}Кú–>$ zDË»NàÏz¶7r5Mªo¾Ò{‹§ûýŸ±c, Õ9|ÿ…z¡äè sÄj1@þq‹=ô?»]÷±õ‘!v’™Ð¾bM5^ñø_•±†fëü7A.ÍEÜ·ÂΪ§/œçj$t1¨›Ô”«ƒ/°_ãˆÅï­w=aü1µL ² |e¦öÀöÛt£OBÂÂF½Àå…€†¢EGZu ¡>Ë݇+ÎéãYc„@óÕ ÖõphU|ôñÙLSO :´ fÊ›àÈÊCްýÙ4!k7„¸iÛ)e&©§CÞt¶_ø—;ú¡©â/b’ydŽIí·Çέéä*!€JP_|êž©ªé,TiÈØºœDÁÚEVU<ȇ/÷q?< ‹ƒü-*`À ƒ8&SÏ…j””ÿÅIÔã‰ÜD—]“ÎäчO'ÑTÌÉ€úöAÕãA£†Øç°çÜ…X?Ovs¿ìÚ¬Zt¿ñàH¹j£R™¥elßB’n"ŸA«ß‘F–"î=F÷È5…ßØÊƒöDÛlÀ¢9YC|IfŽNyt$EFeg¨¡“}½AP$0YBš¹'i‡؉mÛ*ÿ²ñ!ÙóI NYPB\¢“Œ›™WCNL1Ü~£– K]òÏi—¼]A¤‚ÑVŠ¥©´*Ô=D*±›jÏðEúR·h¨ó*G2çˆ#(±pÙXùbM EªÀÛùË3îüv ¨"TBžèê³ÎÁÓ`DyØüé `ÜH}‚?—¶.‘ZQH.W8ìz=¤£‹4âÐdÙÄ»Âoˆ¾9U`”›£Ù!ˈ«jàÐBaPÆ Ë}¦(ÁÕt¾‹±ñƒ:7Ðxj}9ån2Ÿ³ҟK[+¿4®‰ãcéò˜,» ½T€hÙrc?–i áv,­M|c* €p¯eÛ`nƒåɆ +\"oÊÄ%ƒò&çϼ„£D¨d'l׌š€%ê²-ÎGÏë4º“'ùb¤%ù2ÉÇòpçRp·êW9€|ÐÈQP‘î„(F˜Ö5õʨèO²ÏŠ!TJ²ªB ¾ Á©¨‰~í¢%»5ð<™lnùÔHz8hããàsÊr!}þév$AcÜÛ„Îh–b›˜h”kÈÆƒ¿Ð’­´ÃË‘™„?ëh‹TÅ9¸’çÁ7ŽËÌ„œQ n¶XœW®£;žh(•ëÇ IúùzŸ-®Çz®-ïTåÌõ¬°«[nFoˆÑ檚?„@R‘g:DG˜Ý$;¡¬Eª½j§"è,ѹ&tÝ×Dx„3³ìPU…{{.1¸™ºÎºÉ°ëÆög3É1Ü´cW㼤vУõv\¶±xG+¹Â<§‡´!®øŠŸ.¸SÚs2ŠÝ‡a’Í'wU%œýF4lL‹ÌVÝâó Š^y¼ÇñU§H…>Fpª,6hKzlÑLŒù2ØÛHV ÈMÛô/Oy¼E«ÙÓOCÅ.¥ø0ZÌÞ*ܹ(‡SC“Í IEâ¯Ð!§¡'îl$rÊ'™!L>H<ëk#—2¶^Ä„g^GSø5ïiö/`]ºªŸ 9X v2øLÀc- ûP®»fAêBUŒœVžÃŽL>­7Í—¹Éƒ!Ÿô$ÃO@Š5Ëô-téú!˺ÏP6<(16äÌÿ†²^&XV n›·üÅ qb;•¢@6ÚuìS°pm‹HS’ßÍÚRþº]&*ä&40`þ‹U,ÙüæÇÕR¨dgq}ÄÆÿx·pj~=¶ÓG¹êg^/NGºç]$rš"z^y¿©O‰1{~÷ø;¬Ï)‰P=•Œõ)Ç€G4jî¤ÌrúìcüЦú]M°ÛÅii´©†3ýיЌÊ;³ÅàGoІˆ»2õLŸHVï`–úÑ·Äô’M|é Ī|ÞŽ!¸çe*ì¯Ã*ºž·#9èø¬^þCVà0Et•ÊvBÆ÷ÐvãoŸ‰thf{Õ;ðá3H'G³=ßF¸ÜLwª®ÝïÏdôŸQû¦žMµØLÌ&GM÷+Üc×Ó‹eo„ñ×µAÆ[KA7BÚêÔuª[kÔ§_‚y~²™ßÿc×€œý%1À"ÝÐ(ö¿ €hÑšqÞs³7àß_ƒ‘T$£Õ #Þ*Du̘ñÒÅ&—KK´¨Óa3 n<Î1A‡†ó’ˆaèǰP0REZ Ÿ¡L­Ù¬´)i ÿÂå$Q­P! ' ÏvšbÂB4FMÔ^5¦6â/*£rëw-ea•ÑSÐYØeava[ÁîLÄ™(c'™×ÚÓcuDmô‚ták’Ÿ]äYFýå)'"§ÑSÔYOaީ蓵‘‹ò“Ýâã?wPi½Y ­ß¹ê#›7ki õÈ"6‡úÃXhˆûPg"ðŸíŒ‚½²§Ü©vNû¦¢‘58 ’,uÜýžíþ%“.×ìéM¢Â‘ÜQ*“έpAvR¦í|Ü›0Ƥn’wã[ßY nñ YAµJKäŸ5Ù‹¤€bÞ? X"I;S¢3b—MV¡½ K}J–h›Vž þ–W)Ï{V:9s¢HM7áì™·½¦jñ=®âY¢O‹ž= ªeÇæâ¾½¾á}zqãÛ^±µv9—Äó0Ä¡¢ŸæÁüO„iÕ=`ê<¥ÎË\ïéñEÝÚö‡þîjó1nBÀZ­ »úÌZ) ¨èƒOü"× Å¸bºÜÀ·³Z%ä†Xš„¼·°‡âöñ³<=[bðÕúZg‰wÀ›vo×܃£]2DôþD ÃúÚu `Þ¦Ön×ìvh¢:B%©ÜWE¤‰¦·ð«´_­ŸdÇÜÙAúØ£÷cÈ”ÕÂÌ+òHShí¨ „ý¤Iì‘e˜%PM¦Ó:£+ðè¨ÅH˜°¨‡D{$íOæ‰ º&˜Va‡:ü:_]ºí=îy>iåáÙ~ù¥'mû9¸nèÓ•räNæWq÷ŒÚ·´ñØ4•!«n ͸þnÙÂÖZW}¡µ±‰ö°[  wíÞëÛ5œ#g§y‰ÆÊeÄÊÝ0˜¨E’1£ÂPdYnTˆ²ãG gõ5ŒÔÈPŒu²kõÉ("‹+qbµ gp6CäuhÅYG,ky0Ì”<­Ø·’4jl¡ØtˆíÒ½ÄüÃþ T_º:Bùx8ÝØñ V¯OC>C‰ÖÐ? ±¼Âzå`=æLô->å¥FnFc„窛ý0J޼y Ìy—Ä‘ã+’ÞÓ Þ£sô¼“¯ù…iŒfFߥ¹rÂÀç28M(ÐÚ~h?tŸ¡Õµ}úù®Hõ½7|N^äH²âr«xÇsºšíèêbXØ$ãjd}øvØð´4­éLÃ0›‚¿ „sqbvWgû¤ŸM!·;Å'ÿ÷Õ˜%W ÄkâäÊŽ‰oÀfâÞ@ztl§¬H‚6{ ‚`c-% ×Ì:5¼Õœ>Ç\ íõS ¬¹s†ÿ'Ö|ÛF~¢ÛÉ;Í2>ÙznÀ¥L »Õ”>ÏŸùNxód ‘trè8ÕÙ÷ï1Ýzì-?xõô_A7X^Ž)8x“)Þá÷†ô$°©O•­¸Ó’Ç_)c‹®¡ JcÈx}âº+ª2Þ$ŸñU‡=€5S ˆmÐd†Y¾H!âJù¤¸ÎÂ+ â”f¤ˆ™„éÅÑtázi¸žâ’¥ÝcâEZÇì˜&£öÌ™xᾚ;Q/çë#’⽌xi ñˆð¡Ø§;¼2ÎçbYÖÙj^óJxTNÞBíu¦ÏÁðI¼ý¶{.Å3·Ú¨É.f½º}À¸&ÅÁYHê»ÂÌóIQcÝ<0튗x~h|Ûî¢Wq¾W®±vØmyÖ®E»L[¬£Õ¶ôš¹V‰x•»xÍõ›²ž~ãxÍi·¬Wa/¼W´Z8èÊQýŽj5ï&Ü÷Äšwߢÿ`vI\«†]1þ·U`¶ÿ7ªáû¿Pä¢2è·2ê³²é'0±-ï®Âd²²aLd¼ðLIÜ™PšR¨t"ë¼îIgC2”õHïÕ{ckADi4”¶Ö¸ZOTZŠkS‘›-æED‘ÊQ e° “P:‰M¶#½$>Í7<5Ð.‡¸ÜUä:”ËîxY ½æZU:‡ò öiLâÉ~â_1"ëb!+ÕÑ.ro–4µ$ïp¬5žnwÛP¶jk‡Ó¬‘iÁژͿWþÄ}3÷æhë”èÈ ÎN‘ÄØ¨6j†x4ÍÙ$ƒ¬Î„©¿³¾f ¾Ì0‹ zÍÌ܈¯‰Ú»…×€!ù&÷c#-ã9\&wË_žRO¡µô,E)”,ÎOÛ?å,aÌg€0õ:¥vX8ó@CLÊU±#¤SgÊM¢@½÷$ð¼ Qs¯Z%÷³Bâ€{TÝß{q^¤Äüs }Á H3ã¼0JÜ¡tl™Í‰3Ée8A•Œ G’yWQ@š„^ø\¹xçœPU×¢bn=u.žBâ„t¬ …ÄÂK=1§KLjƛ|Ö-ÁÁ¾AÖ”æ6ÅF×G¥€£X8k˜/Ä¥fB<—9ªûå—[ÚÌߤl”ŽnÕ…7±.šÑ¯†4¨‘0[¡ê~åN=åäO…„‹#Ó *ÏpÖ)Á¥›àèï;YU>ýdAÂŒ~ý„ä3M-r†ž¡!9M¯Àç0£UÐ7¢Î{ øD=eTÒס¹  ûÊaL–“úaÙ)?òh‘Hà´ÈyÑl»ÔÒäFx…“Ä|Æäoø…KÁ¶1ÄVísYãT'ª+cŸùRÅÝdúR¥œÁ~îOMÀbžê 91ûd¨RÏË„pQ®5á°;†–©óI<ÄŽ^µH·R#**n^†¬ïi)w EÉÉ[TAñÌ*&…ÏI5RgšÑFÞ\q²þ”Ï€’¢ËvV¡ýDç‹ÏÛ \–€xþi¤dÇ€˜yíèÖ¤)VåX0J'²E¼úY\}'| žLâ»Tž«Í\©¶jø7¸Ø­!@QñA¤ô »j§F:è—èI¾æed”¬äâ2«LÃóüwÌS ÷k™g+"¨8«V#Kм¡Þ¦%ª·åôÙVíÃêÖ—ËÓþ«]ÔfµÃìZ&›ÝÉ Å]4çL…ïWVãîÀ ‘H<¦5D稚¢Âéz ©`Þà:Pâ‰PD•½3[ùJz$Rà*¾ø<ä+¸?aSèÕf“äÅÆl(]š«°ªyL½PXØ B‚˜Ø…™' Þå9Y¬•Û~Aº9Ósþ!g„³ÉO |°pf¿zïªQr&ª0½,’èÅSùqÂx=™¶N€îÚµEe>G<„¼m{}$}b¡_-/HëD¥yXë•QùµèÚõkI)­\º¯WÏü\§•à|Õ3Î,š€äáßvÒÅ%È8L#ö‹ÞíÔ%ì³%($ÖYÇKÏ6[80Ö1×›w#…y”ä óŽ=—¼ 7ý=½hÓ%%UôÍ“‚¤÷Á‚¿%åÁj”ùõRêm‰Œ)'tbú£úü¼˜Ô"M0¹Œ˜æ;ºb’Ó‰”¯Èxx&ÒK¼EM̰Ž`,!ÁšßsÞ£wF÷ ‚ÙÊ\9Ô :3Ôñè’ÖÝnûÐs4Úögu MÒ‘ê4}ª|KFÃÆÙXH¯IÅyʹƒ\ŒqIÈßLüùÃ|̉I–t•,š¤šs‚Ž3ô’œHDOúùE qªJa¢µÕÚ¸QÔ>º¬? šÏñÐÅÓe:-‚¶Ëtóv ™£<7Ìf·w¼ŸÃËÈ$tåXË@×…oÈÐKCláöÅ@®°æÉî•C$ÀS:$LàVY?Õ¢`òæêy‹)“ÆÝûL`û0ðÁo²y*š!¯¬å˜¢¤‰KaüƸCû›40ûÁß}÷7´†â¯Ÿ…_äÀ[“[ ò®ÎUˆê?U^œYWð~zTEnlfñòó¥ gÐEãÕlœ#=Ð9e=IJ‘Á»½þVúò—ÙofäaóO<íøº]ÿ˜üîÃ÷xsû·Ï2A~y ëLØÕsÅ&Lit.N@ªD+§1¨%Ÿ†ÊÉÏ’5ÓùXVP>4—yô˜$’¿Ÿ—,·ŒŠœù Þ»ªe¤…VO6ZÔ$&U'vÒÜiІ¯œQ ™"¨ -ûÿ¡“ߥq9ý ¿k€? šE·LvSðrê|€x8m–­kš=à竈¥hzuá„ù¦ÕÀŽýš­ê”'÷n~ÂÅmùýÁÄñQ&b1AõˆqwÝÈÑÊ6ø™0DÖf°.4À`9ç|•Kß©;ÎdÐìáÉóÉLÔÌ5Ã,‡ÿÑÏç÷戃åTT]·Å)ã\“kqÆpô¢£r¸#¬xDÊ%̉L–L᪶ö­69kJvˆ9u¯FÐg;ô©ÔLöû<“¨‡ÙÍKë.« Ça²ôÉ‹Ìú N.ç8\5€Æúx.Òh­ô¡¥ää7å<ÂùÉú¹Ö®ƒ6Ø´eðŽÔÔ­kØGûd®ÁÇ¿¿¿ßÌTv·êŒØTÿ¹ –Ì€ü2чŸh>VÀ”÷kÉ'L KtxA¶­+,¢T–ãˆ2™›š¨Í® EÛJ3…“eø%8Kyâ3„#d¨¤Ö)[¥jïÛµcUC×¼éúñ«V´c1Óª¦m]ó¬V´b¡u¹ºMæ6ÓºûT,ßý±ÎB¢™>Wm³ªò:×XÑ”ð¨ºáÇ„Eê˜ÞW¨÷Ô$g®çåçÿ­[ûÑûóeÇa1ÛyœÉœµõô~`ÝgUÌù±ªº ß˜+X[®ëïáôý¾óæq­¶í4|ö•ô#ÿ™TSRKQ/4óU‚¢ÏêTò²úØ…©½v,ï1Jh]ïõl “áÃ⩘ÙsæK£<Ê ÞíNwÆþÒEŠÕõEˆÕ+ ]œä[Âo2˜INäÎïq7Á{ûó÷;ü¯87<þMggu|ˆ{iW{×íµßÉÁ#›®'FÑ'k„ìø­EÑ}×ø³MûÄã÷.vêkT÷ªë'—n4?6]-»õ/ü¿—Ãuu£yZHÀþƒrøÿš2Q¢¼`·ù¯xê:Àù¥­J¬Œ÷ýªEŠåÍ•,”L‘È4 ÜM9¥W—î× `¸*ÉièS÷œ§¹Þ>;!‰ãmI%Ö°k=UôwãD”),xã:…™{rK–ƒ!¢)ÔXÂÆT‚u”0ÎHûJyíw2%B:Þ¼ØE|V.½…ƒj(õ„Ú{BÂR  Só¯'Xá61ˆxŠ™z[L-áHu-!–AAo8î„p­a3m«\·î]¦š^ÃYÏ!2]š *;‘üXòÁ÷¤X­)¤såJçsA’6®- IEyÕó¦bRHT”¹–†_iI–£Êè2f¤‰(ªF¥Í&UBø¡-Îá¼ø@ tƒŒ0ýðXw¶oÑXô?†­:ýS|ørè?8bMç9}wæÊÛ;CP»ßç9Ð(EƒLO¼é£üY&dbX¦¤âaH ™Š°BD×Ñå¡“”$F°Sq¶³'a•O˜P7£I±K¯YÊûþ<Øx=R±Êk÷´;x®3L!3µ?x¡G¸Ìg'`× -ÆÜàF;_ïïØƒŒ…’  d¬H¯g Ñ&G²”<0¿ªÁ›Í´?ø É…Ç»·yâÍì†âÃ}M©/ݤ'G¢:QëÉ +™x©_HF±›Yñ lÌ–¢›F`š{oÝ݉A£ac9Œ«Htdެ;Ö#«„¿Ç!ÔR‹mÅ¥CpnÌ(V͇øDŸŸõb 1{œïèíøl:Ö¨‰s¼a¬ýZ‰¦ánl—mœç¡·N~Ö:j {¯1Ô#âš6òÛºSáIË)Ï72êgehŸ¶Äcq †¢Ñ~ À ÓWïœ?½–[™ðŽ"/ØÑ¦z:úOn{iÔ¯l “®„–™®D‘Vøä)Ö¥ÊCü€ý{ŽUðóIáÑ·ùr¬ÿ>`ÅnI!·—[2™­R„ÔO¥„]?KGa°²5B‚Tn/Úñªå²þE쥳5{rÉñ@×`]s ¶ÄÁÄ4þÄÞÜ­×^ýwáÖï·õ•0À óÁŸò÷@+JÁ£?©_ X]6®°ÄWZ YY Zz‘­Ý“™ÀðR`´M{ƒ¬VÂ{ï…à:x \H6mž-Íge`g8¹‹v‹œÿ8kS¤r0%螣´£¦¨‡päÞäe–ó±4Ü^nødÓÏÿ`šÌ\™Q£KoF^^U/h¤2 ÃïÏct±„ÇËæœ™µ5†'Åâ¡)ÇÒ½y”A XHXOñÅ =,pÏ(|4a˜sué|à[á9nøg¹`C ìóBHdùY= Â6Hó”Þ•¤©‰&Žx8â\x•&µ›}ß»ŸLÓ'ek(°ÁF€yâz¢”Lè«wT™—D<°¬[’èö1—ņ˜°F Ñ©ÞI™ ¢Ú:˘UTâ&jjŒªçÖ¢g_AlO E\‰F $ôô6‘¤šÞ>÷\nœ%Nü÷%SÙdUkœúÏ[ëÇðÀcÕÂ…¢·9Ïmwáb+fùÚÂ’W±8W±SzΉÁ#*rTA XËo >Ü+ϱáÄ´I@TÛDϸ‰øR¤œà¿SÄk :RZšþ‘LâÿZ*Llÿ¥Ñv[ "·?Q¡¾–ˆênNã¼qÑP²ÇƒNHG(óËRD‰WNªšßt¦èº VáCìÄt‡_¥vv«kmwKNª4Àq(ØææÑ@7±bØõ!Ó$úa….RÓÖLCÄ‹ œÚ%Wï¡NÒ¾Ñ1tás°¸Âøò52R-1 7]{Y'­B˜q%‡ÞöãªEjDnMU{V 9ø`›~»µ`‡Ê!ÙUê6›ªošfØè[ó@<6 ´a!ü_ÃH.AŽCÅ«ŠZ­Äó­„2¢»1ÙZõ„®lB”²úMâxû(/ùsLüh:SB=µÔ¦÷­µLF”xr‡^w†s¿ùfà7ZvðØº¾è¸MºtÔv fòöÅÓviK¨8Áäs‰ú²4ˆÓš€îaŽgŠØR’¦V{*­^Á'¤pî$r ïÍÁ¨GJXsöÏÙB^ ’*z¾;ZúÄìJ­¼Q݇f£ÿů®á¬wÍÿy ©iø–ŒkÍöHUب(VÊÂe $Ãz'õó·®ÍúþwcdNÌ\ÿÏéöŸä?ûíÿ;-\ÁÑ}ƨ1å€`‘Üá°ÂÁ›‹!ÿ'+Üq!ïÓîæ¦ë«#µ‡í¬ F'0œ5$dpœhÙÈeb &GøFWCȵ=½fÏ}¿XõG‰Ì†`˜ØË46ºh~t:‹í\–¥àq‰ò›¤±ø×•E­Q+‹p~îÝm;µ›&c1ë[P“^#ˆi6–9ÐySÞ,ÁwkÂbÅ0µòìߑǴ7j4Àö?¶—ðÿòbãÿc5T¦¶ýšß\Ý^%M[kR³¨¢ [¿3-Ye’K” 6D”ÈVöx¡”’cèïg®°åDz¦® žÖÏëÔX‘ø®4hºOéø†?•à„¶šÊîøF6ÚÅÞ±ÓìT‹Ác(Ñ\\œ>çÑ©NŠIÔ½r¯)%-ÉŒ„ª‰ BKþQµÌ"hMžÌ¸½Šù\!1‘ >èa^ÀP–ºIfµäj (šLæeÁ®’Õ TÒƒifS±¥&c˼M2„Æ—æw*€+Щϟƴý]¡Š‡£;‡-ÑH5¶#¿ l!Ï·—ƒ¯çã™KJŠêx '*RÄ'ë ¯62IÈâ \‹LrêAPÉ“ AæÅÝTƒ(ÔGHÁ¼$Á@éÑb¤«Z¥Ü&«ë~³t€íTŽ.BtvÀ÷×™‘Ÿîöïã‹!Úw7__7Oïû0ÝäǪ©÷gÇ÷Ž²Ã¦ß>Öü”­6|ä+œd®Aœ¸‚@´žˆÇ>|à¸È’Óe¸G©F1GA'œ~ÃÎ3éÁ™{S²à¢­2Ǿß]Múýq†qqŠ= 8À7«°O%Ï|ðòp ’ˆâ1 „†¥s0žðL†E‹RbÒ\%åЭ#¦ž £ÀÅ®¼!°fÂáw‡ê–rpÔ€qŽ^E¤9‚Q–dÆa9¹æÃYþ¼HãŸíâßàOÒ¬K £»„6ó– }°$zd~FzIÑy ~ƒW[àŸ‘cæ$r˜*;âHeÍ‘•{ØÌiˆ[Ó9,úcHR ¢Æ.´1'-ù`$û'™_^ׇ"ö¬âBË…Áƒ.ãÊ6d` ¤·”’5à$ŽéêbÙ•[erŸ™Ñú¾¹<™ýâù[^›†ªH掑1lZM@dÍ¡‡)­]Öõõk ¥w^š|Gtï•DrGîKØL_¸-Ô/±0“¥­_É=‚ŽN•¾j]F•AFÃ3—ˆ–C]qÎVßHœ¤¤íß/`xÌâŸÕ%½•ŸëG‡{Ü+Œ®âXŒ€WÖSi—0 Oá¥è ›øìf×Z²q”ÚŠTgÜ;„€Ë'h_óH닯§’"nžò=)K‚s‹Êè »Šh·à¸zÉi’îñ¹ˆN”d­Ê¡=ÅK¼a¦‚柵Vº?† ªx”|I¬JV“š00«œ³ëñ"Ùð¡0|ÕR5‰ ï-3š[)a¸zEõ‘¨ûÈ=§@íTT¶8¶VZƒ•ø“íä_ÝÎu\gâªzå¢Hä)†Õä­Æ‡!0@22¼{™Ë¿á ‚4hQÙ bÝ6„åÚç8·¶,qD™9zòß¾—ØUkIÏqþåÜe¯5ý üÕÔñ×(‚šbÖ‚ÁUåÏE¤Cu –…lÁ4h“΢¶âïÛðýÂ[qÙŠ«KˆNÕdÅò/Ö+D%•¢ÿ’Îb‘”º@ò+pcóñçs5>Œƒ~tOLOw§­ñö‚!ýû5ɳ4{ø)¢ŠD2…úyPHFǹ ”¸Gîcjàœ`Å;’’DgµKS¦LÅ FƒŒ¾aÑ!ú\©ƒ¦§çÓׯãÙVUúý:^pÚ/e[ü¤ˆàµ1hAtÙþéÕù©Å×÷çãD<þþë!þ¡eòÆf ¶Aæiœ „÷×ÁY{¢üOtó¡ªÈ—jØá>’ KHÚ$sY °.~$ÆA9â±£§œëx30î#HH¼)€p=úäšL¤ùÉ@s¡>UÛ@구èåàbIXå- ‘vÈÃÒÀ0]¼ü qHå›fÑD™} T¦ø¯v¤ã$™¾ÍÈ^nàÅ_™Pßd@ïY°ó#|_yì[®­Ý®š¹0©µ”T8™J3ŠK -<¦3yAóù5ô†|¢ÁYaŠÐ¶üaUâMó³úrxk/ï`ŒÁ?¡çDŒÓáÇ0V–šT‘âV^ý4p tB˜&ó~»ù^¬àᎱ¿X¨;, ºígX+ópGãgkD‰µ”M!†¼Û‚fuå)èV¢â¾þbNuðÝŒNOiC-ZÁûrKTØ¥ÚMǯàfcÀ4—]öÒôÛÉzÐNÔ|85U¤{âR¥Ö4ŽèÃöˆ?ü‘µ¸íÚQÑKàR;‹Æqù ¾4wÉuJYK ¥ö¹ñ/RaÎ$¯$ # E‹¥‡éûžÜá{b@{ȪOx&.Ô€•*³ÃŒ»KXAdVãSÌö=˜?ž]®êÙ‰™?ʼ64ûj ÁÒÂHÒ¬!ÓCvK¢Qÿª¥¢Ú€s×]í¤¬í:êÈ\†sÞ88gCÀ¦Öeô*¸¡ƒ¿÷ÇMÍwGK|¿’p;Uõ8Q–ß¿<˜òcŠår­Þ‚ÊWlüÒ´ „tøm×JûJfýªq¸C+o³ÌèÎyž#}Skú 2åɦj6Q—«97wJÞѱë6¿J¹(;/ ct´Õµê~°œãÕw¥+ þ¢•{ç™Éwá¢SžÓÙðñõöÖB\šás¨÷'º •­û–h~ÛÈ{©{¸Ñø³/÷•Î<Œ…Õwí.d2ªˆj߯¤mi÷¬—8 ޳{Vï2½×¸±X¥˜3ž‘ãÞ¸W'ë‘ÜÛsö&};í¹’Pƒ}*Ñ6¬¥mþ…¯äm5Îä/·˜ïµ¯ŠLð­8ÿÝTe¸ºÑÔ zšÍ+zk{õËÛĦ®ÿëçÔ wv”Ðdö•î›,“6` ôÜrie¡K¸“Vdj" ø¢ŠÍ¶mkå°Ð½beµM±û`»è»Å=ùCõº­BÍ1îi|ÑÝiáô†Oìúãl7|ý0;íè<£Gv„êòöõÇ]#§¬™ËÆä´ÿ±«¬©]eõçcº kó¯–Úé³vÿŽRá·ñ¬÷`£K,ß}ÊCP4·LÆ¡{ôÞÕÖb;ØÐ$×ÎÙ–-#}û’8oÉÓb3žYјÓÑr9±5„¢Q, ciáç‹a§¥|ö†±Ý+Ô†j•²¿ÿ‚Ékûnl·ÈW²În¦› å[Ô¶¢öKô‡=µÍïÿ1j‚ž8QÀ‡ €è?åÆ-šÿcÄD.¨@¤RŠÄ…á†ÞT)Œ&åÝH"îX~äÒ5€^³ 6BA:ˆ[A¨¨VqÕÁ[Š4çq‚öúo€ \Ŷ³D\Çi’ö¢þœ1:A¦T †ÆÍìÓÓôÓÓáeÿÏ¡3ÿ5ÄwD_Û9¹¿|Þ»ˆ|„ìçM„ìX7ú°Ù@7úˆÙBwúðMÊ:môL£e…ÃTózMÔ ;šˆ½Ø“ùÈIµÃW!„™hú[Ú°ºÐ—NDžÍ¬¡NŸhįè/Û\OiÓ“E¯ûoÎm q:É9‘o¶È¹xNÕŽL ßî€ÙX¼ÀY‰ñ}:›Ëç Jé¨SúÈ ¿eª²ÆôIÌÝÔõTßtd>Ù—¥Õ}&º_êzÿýiÿ}´ §9ÅÜ£ùŒÂO2-¤ ~Zæ^6¡âQ›þÑËcéwҽçé>ýýê‚ÍŠz½2χ!y¤é¶,6wx0å;Rßnš¼âšÿ ¶ù“pš¯d͛՛d;> SNÙÖîþ[”…OY>+xŸ{C oNÃÔ˜¼5]Ä2¢8hwòúkˆ„²%à y#ªÕ÷"b"hΰéW&R:´¸‡ˆà’\­*=òn,Je=š‘Ò; /_lP°à¼$ðîuÝðâÈäGçeí$B‚µrT8ÎÓ Ëˆ‘·ˆ~Iß-¾K ”ð"–ïù¾^_6y­! ¹kŒŒ´ÊSç9 iŠ%’wV z»þØôW‚ý‚±5ÆË×3­Bíó …Ç<‡Çwœûi˳·wÏ÷;d{K¬´~óš#Ô¥¿uðK–†‡FhÅi`…H°Zöã,hY0¤ô;°FAšJ„®8~€ðà_ÚÁÖ8ÜꜫÄYÁ©¿}Ú Ìš´‰ ëO*€â$¯ 5á³³¹N4‚B«”ن̞ØÅOiœ ˜i£ŽòÌÖQà©OÂgö±‰¡‰Þ£ ¨Ù3 ¥Ùe´?²rG#k1..&ä`j„‹Leâ1Õ»Ð'šw.;ÎåöüÞ†âuûpŸÇIíe¯"Á%×å€ÑíFU[÷á6sx¹èÚ°Éó›®2J±É¾Ç®I4šfå}T½ë»è6’3)1Å1l…õ]q ¹£–ŸƒY$$ŽƒÓÄzŽÓÔÙñÆ o:åõøfck¼@’ž¤u¹£©Rnæ%Ãä?wæòDÑAᕈŠjÆW~ûí‹ä·&xî.G³ëñ)>¯@ò!¿åÒ {‰5=¥ÔrÓT¡Ôç£ÛÚ(¡ 0\Qèj›]^º9îŸ`:Î9/vá”à[McZ Ŧcgvå–HeQÄŠBºÑ¸µìÇ=ëÊUã\`Àõ°¼´¼;5 $q/í¿‹ïfŽô¯oò©wÆÜ.°n¤§±¯låW;¦Í¯x]u¬÷| ¿¨YÍg5]&åf*ROÎnƒš„ø»Ssœü¬ä*œgê”qÜ…çòû?‘ML~'qq>ËÒÔpÿúë“[ýô÷½\&“×ýåâ«åïðõ)*(Ú76„3Mð@Y¢~.—åÑ‹T"嚟•“›×ÀŒ¬‹D–¦~ËG†O²(|°1Âæ —¡‘ššDHòT¦È\‡8wq‘ ¡9"*—¸éÝ>àµeˆÆùŒÕ!áY1Þv­«™´ÆéË¢AŽˆ€ 8žšcàÔlèð³1V'DïŠ9¨·¸‰Ðy Œ2 }É&à€‡BXa£KšÁITGðÐE?æöÜý ³hˆ$év´¤CCó¨SÌL÷~Ë1™ÝÒIÜ»å!¢—Iº´±ñDbèhOÁHÔüä5ÄrÕ€™èí€tÏ€ ³DvI}~ ³®lˆå ¢F,©¹\!˜ŒÊ…P®Ç\mzJµa7a8mæGó²6B©Ø×ï`_ÞEºÅ\’ºbhP:ÍmGGÁ»œ”³›u7( éf-9_ÂÑ|H:@ve<ïtÙû¼þÔÈø1 jÒø‰˜¬?éÉPºø–Š>d–œWmAoy¸¤Q}2 ÜÔRêÇP8æÔàÀìMÎÛk\5A®ˆCzA öQ"óƒ ¦\v…’®åJQ»=®=Ì}t÷œ"Cä† ýˆ“â 5JàÛÌ‘z~7ÍÃÔ}7¥pš¡ñD»Fsý)©}rzÞ*œq¸ÊàŠÒÏuä!qè“&Æm™™Õëk4Ìz®ÕÚÙ "ÏóŒGH޽ޡÎɶÐîD{´,ÞÖ }ä‘|ŽPä…_휼B:¬ Ä9fÖyF“LGËžâϧdøÙÍwÍT«æe0àqè°îœä¶Ç·?$a4y^­UƳüÒßíé}°Ú GÃ^äýùkˆ?å…ò&¿!ùˆê-õ[fÛ-CÓËaнjï6ö]:ÈÖ+å¦ÑYSÃÆ¬&ÊœZôžb9IÓ™ä¹a‰6…Zõºé>XH@îVÐXZ²zj+e4N¦‰RŸæ¾À"„[ꈦ„]˜½fŒªÚjŠ¡iÔ•ÜiblÖMæ"yS}deçÙù¤y'«arƒ1õ‚Q…Ì?úgø@ÚZÿ¨*rø`ì+(‡ üƒÚ¦¹’CcJ ‘1aÖMýi'åR7…¨ŠlcñÑ×lœ6g²Ñ˜À`¤‚â²j{m4TÛ…6LZ]³š¸sO‚š2²()¡ïÔóª)Þ2 ÅêèI™°„:×ÄŸn®—&n’‚(àû ójjº×h]6ªimëOƒ‡!⟬dî/Ç\þ$zØjÒ®|(Vâ<Î’oÖE?в¯dŽÌö*„âÞ-ÙƒÞ.òšºƒ| e€~ÜoXŸçv‡ÜIŽï/îCàÚB9f:.¤§¤îÑȦL˙ҹÐK4~ªRp®ÍÇPª7µÿÓA4ŽÓ þöw¨ ‡N€14>„XäRQô+âÓh' ˆ™£˜ì’£µ\j:,;¥,’ Ïü¸gB Ès÷6dfÒš$Ô²Lyñ³Ñû3CØ.œVUcù^fÔ‚áºÇ4,F•ÏÓ¼¢Q“Uèƒ!”ûÇFÿðb5Síqí§#’;Ћß]ê&iÂ.‰¿×’¯P®Ó°è–ŽÅáXëU€³ûz‘…a’€VjGçi ñàÛ± Ç Vmä’ÎKçG×è;€5X)“é1mr^1JôVÚ„ö<öðõ–‘ˆªǸVÚ›í¥YfôJAÙˆ…iø‡øyD¥ÕD|Cž¡H†âŠ>‚uG54J¤ܱ £„ÕNB§Ñ$ÞPí_bqØû:ƒÒœ%Qÿ}E\=T° .£!Le “:תõg0ùè„Ú Öw G MŒÍŸ*Cpý~¼¿ÞíövšR`~$r'E—š™Í”:e°É6Œ5w™)vþœYg÷`ÚœMTôÍ4ý€7íi£ìyÄ‚Ûl4¼ž°ˆ ¯Æ£6y¾Ò¨_$N0¹‰æWB¾$P8H‰4éýB¦ýÏ:¸3ÙMK_¯˜tæ[Ü*5°«ã0Èñ†DíýYÐô¹Š†¢ƒÆf¼ÉYEH>™í @áJR¯Œ;afÎ9I¥B™Å §½zëÏx¤R]¨ì×ýéSžº¤$ûrFF<'“ˆuàõÌíóä %Vòm£ØK4y(-y[Ië•F[§3‚û X›j0’U¼½²•ÝÂÇ×òàŽ™k¸ú«q .'Éo'vŽo:ªw`]8õ•§Žñ4d ©R矶p¥®Ù#:¦Øs77—ǧboFÏ,9'·¶ÒXvyÝTNës·U°.·¨<·˜ b(šétv™oFT߉ôéÊ-"XO þ VO»}£OÇcpåFÂz.ß0;š õÖ×­âŽÄïÔäÉíÕÙ.‹3œ¼Þ¿!Ä{“ÑTÀF« Â¿ ¾Êš:˜Ÿš^ÕÌ`®…u7¦ÅrþP±$ –dÑx'Ú~HÙCìA²šý¤Üsæ:©âéKyqÑÞñÈaeí1Rñ­ÓÈ ”L¨nGßë’_Ç-Ú:Åp7Ì­ ùÈ€ºÖ†œ À£Ô—3](0…ðš*ee”ïíÕóÒ馿W6é0ôYu|ø´4*TÌÎo] Y¨äÔþ-¾íurmˆUνÖë^ñQÅp˜»6àbûÖ"¿I^­=Ÿ²)³2|>йAlw!²n{•W`¶Sý[6‹yýÊ}ëòdÒã©l9©¥£™í¼ìª¸M÷ƒý¿BÐ/¥¹tf.j`Zðÿ{ú¿jò±*‚ñ›=§ê>“8@Xã0¼O ¨©aV…¦ùRùªòã$rwT M­Êa5»¯M92Í;3 A»¯_™ÙП%ò÷g¾hyšT Hb¡Ñ’,ÈÚÝwú“JR¨³‡˜©ñ)¦­’Ä»˜‚äU¨B†þÜáíµIÿÜá2e‹£r¢ca‰&QŽkJö7¯ösVJ‹yHí? Ø-%…HÜ—” xË"È"èjûÅÖ<â4×âßÜÄKT¯+ŒÔº¸¶ÅœD«6bp7“ªB‘|w"ÕÜÈ X-''ñVÒL †€(IDYkaôˆO°Ž@Ș‰Q6šÈXN¼ÄöBT)‡çÎb ïïÍ!E áÿd*2 ÅÍ-œXöìôŸjâÌ©ý±Hìç {÷zư-|†Úåë·yý‰Q‹'ÎÇ@µÖ·²|*’03*Μ )x Ë&P]J9¦Õï¶,@Я8²·¢¢.6¤JI'”ž *µÅVÙU±XŠÖµâË}XÝ»®É s%P$éî-N—VÜ@Àš‘Bõ³µÎ ]®GZ¨•éi…²ËçK¹g¶–3™ÉHŽŠÃÄW'²–ºÊ'ÆT¦ }êNKúZ’ˆ*šÞ<âNS /¡€b úÇÈÉ­Ó÷3UI3úùäò4=ð›ÕfóŠMéÝòÙ¤OíX½tc¸öùh CæVB1*Ѫ°³Œµ¹.n1ªœ@µd˜ü¡ùˆ0ÛuØqɨñå÷Ôs¦(ÿ‹ißüé¹'ø¹ž¥û7åxv˜çú¼/¬Õ¯L½5 íìÓOÆ•¸£F® Wï¸Ï¢Ò–:–cœD«-Fu˜{Úé­`š'ØÚ“!GÈ ©_í@°?ÈšÊz‚¡4DêæÎHGá ‡+ê˜èýw~giÌ“¡9ýçoš›æ4Âß¾ZdI†ff[ž“Dº¼ªG˜ºà¼ITÇÖØ‡Ý¿¼Ã¾ä…ÓŽ‚­'/*ï™n·Oï•ÚßM=EîÏ›¤°˜S£½º*iuâ o mâ¶-–7·_Þ€Ú¤¶‰×~üçâ˜=­§ÌiÄ_ Ïö¼L¬>Lȃþß» þ·ö—å)¹-”žÖdH@ T…’Þn¢?d%VŠè¾¨„*nçƒØk‘ä£ ­æ1ù¡ªÏø¦ßÑ~Þ·ëŠÔn-¼êî³Ûc™ÌƇ§>c'cŒ)ñÆÈáoadýòÀpÖ{ù :àvýŽ qU{jª!€Ý` µš"*k„v[ívÈŠ0Õ€yÁí vUúO õÜ 2Öé…¶ÁÚ`±¬¬…æ¬q·I/Î]²ÆÉ?BåÅ…å[çaUêái³` =àb{šúZ.ÉÍ5ÅÉëcÍÌç<íñ=ìáÁíÛ[vÓ¡ýûá}·6¸1ÄT (šBÌþ>ôgÒ–¦¡-p„ËýæNrâGN›§Šˆ…R%Õî.èPéÊ» TM®™ûÙ`“„YÞ©ƒ“Šž:€$†ª©j•/Z1÷VHùU¤“ª¸è¬Àª T‹ÄÒG‹Y4–ª¥,PRN˜ä’¦ñ­*6àSãâ654“Yš-°ÕDÏ"¨M)"E}–ð¬H=8WåI¤å˜%¤ 1Ť„0Ÿq/ÒàØ¿î´’„®®‰^нáBÊÞKÜCVëêϪÑn¦´X³‹äqË@Ç)ß\ñ )QÜzi~X]O%W±^V· qh÷i]"ЋÖvkº¥r!Ûþ¹¨"ãr¢Wd’´uu¶»ÑRÜþs”í‰Ïìº&UµRà/‹y6Cï1­(“¾‡—×·~Ç–û¬ñò5ç9Šr^.z»|k*xõJö£Qº!úú$ä ›ùñ:y½(j¥hmùôwEÖWÕú‚vfùÖ"çø¼¤¤¶#¦ý,íÈFªñœá˜Åÿù6n‰C.Ù®pB‘à…Çt»‹7x²âhh+¸GBv+Šo4B¥\Y¡2Z‡Á•ÁŠ•ÞÛ麤¢þWó€kÕ³LpvÃ:I.áÄÖWÓÚššã®cÓþßÝRÚ.@>þÿEœ]Mÿµ§HÓÖ~‹éçFŸñ” Þ·%µØ}‹û¨Ü#ÅÚ&‹æÀ$zäï É$Q$WRååÈÏ[NVÒµN|sØŒyn¦¡Ü^Ñ]ê2 Ã7¯2Žh’oàyy©û"w u¦]ŠF-L7oß›¹í{ªJ…Ê4DæzhÛàiä  ‰d?jj¸åä*½?æÐœŽ¥œ}3Æbß½²ÿýr}"æ° p$K‚eUH˜‚Îz^˜PoÌ’Dýði=š™ieŠGùAºP¨Œ:³NÞAY}/œ†&/µñâTSÓ³×5#ôj²•'Ú1ôóÆõÕʼn³`¿ÚedûjþÛ/SÁüò)l=ÊýÛ¦¢˜ ¬ 6U\¦¤ÇšF"͘Ÿ6óˆ¢4A³â's\“”‹…³/`g4é|Þ¬!ÛíÐëÒ» ŸnžY<ÿ6?ï`ÙùÐÐÓ£sø'x{g_W.ïìÍíÝxÆßo þÙ›Ú±þÕvØß$ªÁxÐŽDð£&ÎS¸…“—I_D(ük©4Lm'É9R ¸b¡%C«ôü4‰áÁu‹yÙ"žËžðm[,¿ÍŒTØ`w{ ¬ebâö»ÙݽI¼ äÉ~¦¤¹c 6ÌÐÒpG„©áè#§HmFØ÷­ÀãLÄÄ—§(qÄž$ç‚5èÔ˜.UfŠ~ÿ쟀vâmû¼éS¢mè†Ë¯û}¿ûÇ™!ùòõ¸8.ñ–¦5©™©ÁìëºSq˜x‡–¾Š}ŒÕ¿âé=x#nýÂ1¶(´@¤ ¿ 8º@Ù‚Ý'I/e뾿÷ñÑÎÔbò}»yÇI/(Œ£©2©aµ šz$“ߌêª2Œq=RéÒWÂkÀÄosŸe#îÀ p-ä«ÒðÁï×°:>æ‡$BPÌD›³JNÈ£‹+0ùâÓR6Xľýùg°V€3…‹! M(È14¾ñ¹B;„ÔÂSÉ“àçÆß“+@ðTG«n«Òz(âŸx{HÙ¡£Š>÷ËoHŒzjb.¡jST¢ .¤‹wi ªúaM©… †·‡Õ`;)‹ž·;«&µö2ù™µ¾ Ya©}mí*z?|“‰ti bM`eŒÆ•yÏ4:%tΠdÈ"³÷Ú$ˆ«àÎ%>èY Ñ@^þ¤¥F[Ü€QÙiWp*ÿLêFªp.ØF7™€ õÊ‹& G‡Œ ‰$ŒÜ›}>Bú HSM01³é%øpš’0«ª¤iÐ !CMÃi yFa÷[–ŒÆ+ånè’YUáûÚ0ñÒ-2ͲVP, Ñ¢¡´DÌL#¨ÆnŽ.½wE·=kà¼ÖÓ0s©÷a¯wÑE¸Z¥#³*Y9ŠP î¨´‚ÜU¨p·D¯ w¨4Àj›Ø‹Øz~Q3žréE¶‹ž!üÕU§d7à5'‚Õ(±]3r;_4”hD"f'ºˆ[äžç_ù6¶’sÇéJgèwE¾[¾÷#NA•?.ÔÔãF™I Ëþ¡Ú`üýÐ^+Œ"v\#o<ö«¦\‡(‡QN)kxeðOÞI <A—Òf½¡²fÌ5œ¯¥pýaÄ`]-\ R2ù ëcguYPÒ5Æ«îØ ø@ãºouæ÷ø9zv„ó\±Š×%[g\£²2›Ù)¡íËý=Q¬Ë†û3y±Ò¦ñÝv³‚ ` ÈV6Pÿ¶}±­U2L­_a¸¡¦ø…ß…Š¥Üà}¥ íy@Ìü%áWv-4'?ÿ>ó-OÊ …F94ƒ§êÓD%NÐÓ~¦€€kÈ+2VÒƒ[¶±J´¢ó*žÓï²U¾÷4VJØÊYñ÷¢¦¥z5q –ñÄ¡£²ü GçT³¦á&9nŠ7FÜÐ^»±adú0x¸©õöÔw0_m­;IÂ:¶ ÀšÒ »¹ú‘hðܰ##+h©LÐ!îã…¥Ô@k«¬‰æÜ>êô@Sb9Ϧ±fØuoJîMãÂF\;¨â‡Æxòg°®žÁVUWú¹±è9õÓ&Ú*02´4#¹—®Åu”‚¨µ(h~d¼•@ƒ£•D$™¶–IT0èP‘NÕNp–WÚ +üVD†Urž0^±÷z¬Dõ8CáoH3ÈÒXé'6§†úòl lKO8Å`pûüŒ¸;5¹t«¿üžÄH Œ5ðò1¾ä»˜â)BmU G‰åŠFê–QÓÈQ36nhõ‚År%Z¨˜{Õ~þwE½}@0L(cd#O-OPºº[[žÍS%xKVA{¨Àœ‹e[™tzÐúmGM\°YÍER¸8gYn§•6Ñ«‡¸ûéT¼]ƒ ‰fPbÁÐnñ­ÛÍûʲ¹ŠÉ§•p ƒdí«³“µ3ÏËœ ßœOž5®™¢Ê%‡Ó:š’ó-c«_®'0øµŸ'Ô>ä¶ÒÄlïkQÁtVæmt€Gœ2ãÔb@fÚ dš1V :¨TìRYEO!h8Ê–-þæÇ*m훊%E´?>ýØqåo7 ÎÞGZ°ïA¡-†ÓV;kÀ›‹çýÒî_¾¶þJÎg2°Õ>f©MknÙ²‰Ÿ±¢y~ëŒÒK[oDÖfð~˜sp$WpFOñÛg/ø¿¨Ë—ÜÏŠˆš\žô莀¯ßœ@Gƒêîè ™&Úåí^PóàHDÉm,MÎ1¶æÉ¿2 Ö«åî`.³¦ß|v‘ì(Ž'Z»ØqÞØ¢r,¯>¡)ÑÛ¡ÈîÔxKÞ»ÝQëý>àÑÍî¶lIεù©«‹þ¬NJý-ë³wrOúEì-=Å4ŸN_ÕïkåIáz¸,'–µJ‹(°–è"Ý­W˜¼0œ[“‰ýU_XVw߯ßò­:œS%q—úòv?'YUv]°¼pSö዆Às`NÛnvú™Vu=/In»ZSxùÙ1±6»•ÿ÷8aû;H¥ëlÄ… @ðÿŽŒkÔ5ÿU¾Ñ €üË.ªõ"†H*†@‘h"íTbíTEJo¢ªáH(=  ãoa¦F¤ûÿ°v&àP½ïÿ{Ù÷}ÉNY²/…¬!ÙwÂ`05 3cÏ^ˆì;i±²²„’¥(²&{T(D¥þ§o¿ß·3|F¿ëßuéê‘ó:ïsß÷sß÷sžãL,Ê,ûgb^âÂ÷«¦Ûܸýõ&šH œWÎõl®ooHĄ̗¨Øþ\ïwòC×/(&sêìT>×T¾¥Íóê‹ðc}a)~ƒ|ÑñÄ!´áÂòéø±¹g¦ Ô /GœÿjKBn%ŸÞ¾pøšÉóÊS¦Sw!o¾¯9ä®\Ñ¿A2™°õåâpÛ­ŒVôʈë#´úÛ3)´ kµ_0?aŸK‡† 2¿<êNw]Û„Ñ÷kLGcÕªC/íX|3Ð’¦WMz¼@¿N;ÉyÁˆ}±/Œ¢7Èüõí1Y;:?%ÉGSÝS]á+¢Ö‡ìØ BNQ“‹h‹ÄìðÚ²tŠ@)Ÿ%CUðÂÉ 9^_{¤ 9´L“lyS­Ÿ/|ê}¬s²ª^«þcirêå£äª=-¤EÈo«ö´æ“:rK9È=%Þ¢ûAŽ>Âx$#b†´($ÜÒyžŸ¹g²¯ºf•%z.»¿ê1éçòÜ…1¼ÊÔÀ&å2ºwrµ†Ä7áx¢Ûx˜Éá‡õYÍ« JÒuª'ß4")Ru­\¶dL÷›tÆ"Jbœ/M“²=²q$ëdÏà‰Ïyð¹³FíÈáJ ‹€êì0³àà»~vÎ4èke^É霶‰k6>U¤Je}¥÷æƒÚ_Ps9¬ó.^k¿pÙH«hÖWöúñQçúôËäé­Š‡-¾W£c囫%è¿7 ŒË±Âß°Ï*9Ÿèhøó^GmG—™3g5O¢qdžš—¤âóÎ:“U ÒOêß=š†&d0awF¯BáFQ²×$ŸîÒký1Q·i&«Üõ­|0mYq|A+’ºëõ)“Q®G‘Õ«†ã}c÷d'# ŒDð603ä„«´x'KÏÌ9¼tÌÐ~¦á¨}|ylY¤^+f‚tý^KðÓ’»-O‘峤7îÃ(jQä+§ªRüï5™Q?šV|—úæäQ“&sxžîªQ†gïpÆúݨºÆFæê”5±m^W†Ô¿ÌiÜp$oùæ ÿúo<ëäçq/ΖÓsÃÓ…ãÅr£ý>(ç[}?<™9Zß¼’Îû®~§9æk7:séÓ¤îÜúÄfåXо qÚBèàšéjŸ­ÁxÕîµ9º…>%’„C]§ýç4 B–ëß$$¿è‹í20J*ÒÐôú²XK§߬j·CïŒ;ݺþAig|MÍô•טñ›¾øÊ¨¾¡M¢¶ÓÚㆌœ ÈAtéÇÅ,¡ÕOÚG¯”ðe=iü||\ýÊ”ðR¿ úMîæ·úsd^]#/¾6IX—3jɘ޾©ÜÍœI^AüRY`©þ(µ»ÿÕÛ·L5ì8®§ß¨“„.Þ§÷¢Î^¶í(`Ë–ÄÜ'ñrz³(cKòÀé¼.¡ÇùÅáO9¥DÆÑcG2»Œ/ô+X6}s¿ZP–=x} NüUâG9±§ZcÎû ï·§k#š¹¾H¤¿?—¼Æ+7ÃÀ¿BdO,´Äz?-ìäÕkJâhþ7sÛŽ[ÚØºÓŸýHÎ83¿õ1úNý6´òÝé:Æ>“ />ºÑãùÔ±ÕË4¸6À<.£H”ÊýS•qÉe)ó|Ju¡/)W³§R›Œúë+ŠNùŠˆœóm^ox™ò¯ïh ç¾ôÎ¥(.Ù©k×ôu|Ã¥ã[e¶H×?ÜÑÆäS¦²ü%ˆJ®Þ·R±þbÔÎû1•Mò‰.Ô^ŽüÞ ·ù…Œ6×¥o4à íl4—Ò‰½a3æç6”¢èÛfû}º†R+,¨0˪³ ‹P_~¶lô1z¯¿¼vLY×µEÓ½]z#è„P_mœªºê1í+› ²…H õª3±‚–›ƒ7žªÈE4Ž¥tfB§;Î0œ©ŽŽ­ /HžyËŸÒfËT;q½™‹­!Oú(¥ ßµ>"—¶šÙ”Γ,êèÖ´bV÷äaW¦ÕÙ¤‚ãfüŒL¤ÄKœUNNè"o}€§V{¥& z¹ÀIj׋e%§<cÜ„ n—½W.͉ŠîPÚbÒ89æô9¹¿˜¿èe{¿JW¦tA»œ:á!åÏ¥g²EÇGÊß+¸¯ŤVœçŠ3€w¨T;×-R;ªŒ…9ô<Ͼ"˜¢¿n.Û¦`ØÍØô8t½Ð¼"­w./m°:¿fää# ŒKæçE&µ7ÖØ2à²RFsÌæ¯›o£‘ai')½¥%™ˆw»”)Ò¹ÝÈ…ZÎÌèÔ–þ†üNÛunÆÐoôeåçé¤Ñ§]_l¾¬È:×MÑ% 1gj½9ûͺËzèÛ„¸ë&ÉÍ/on6݇›••ȸ¦|°²…]°cøéÓa]=0Ññ}L ¶lšÓhÈ˵պóñgѹîCua¶d=ZúÇ`³â½.µ·È‚ñË›[鸚&o¾ÝìLwyË|5³jaNÉŸH¶“[“fé8ÒÝô@û¶cï̱¼±äQF×ÂWÓ¹n°¯ÎËpÍ>ùyIÁíºYr`]˦ҷØ8s5½tûá€áƒø Ú©.²uGáDÜ·tιzØŠ©F&ä9ºIH•ëðv"ìÅt&e…Í?ÿ˜ÓCž<¶Á–8ÚS'Ô,ÀÝÍýó¥ëû¥•Ùý/ è} ›´;]ï¨ º¿ ËexýÙe‰§÷pPж÷:‹caƒù±háÛŸ}RI“R'ýn-|ô/…epVÚ.¢ˆsR1e§{g<½Åú)¯°20‡Ÿ^î|C^†<[Þ!è¤%#ý÷{Z÷N¾’,ÜЊ;´ÓÕØåpöš‚TóFÁÖä²Ç%ÿd³o›=Œñ;^^üzÂŵ2~ñA‚qíïMŸ\½UàÀFí÷ñD¢÷ §Ó\ÊⲡƤ-}5ýzì~Ni–Ób‘Ž7R¸]Ú¥9ß’¢]t8Z÷2Íc’t³s9Ÿ]€Mýå£ÈŽBñt+©(ËÒ0 £ní¢'WyaY­û1›{ËûPDçc/š§3õ¥*ÓŒ'Ñže?l|ëžÖ9†YÓCsë\˲ì6:!üׄÔßo½åZ¦»ÁTr”ýÞ™/šï*Fm¤Z·÷åÔ–§1ˆ4ê«d>‚. 3w*ÈÜ›JL©×'Ìß³‡ø£ä3"?>êqTœJWr 9mvÃæè#v²Ñ>ÇVsMTŒDNìO e7ǯ[ TûasÞCáïsó™PD;ÍׂDšƒã¹ÒŽ=¤n`þÁΘB"sçt0™ÉJpÀÀ„Ëì¶¿íÑ`e†‘qúÚ&2ú a »Û¦Á7ô(%-$ÏÞÞQÞ2úØ~f¼WêèqšÑg'ïèˆÀÓï Áó´ ÿRH53]šÖ#OY»ò4Èï“ÍC÷ït'œÃ„¿¤Ç“MéUÏÎiR~ÛT2U±½ ûäŽî[ÏCo$;—\ØÆ’zO™]sk$à&йC¦’KÖI6zþzïÒ5©¸ÕI9¶Ñ±•qš·eyk-8Ü´@À‚ Al‹wcöô„Í»+—ZÐF,|ÛŸ,F )Y‚Oxä¸6Û]hO½¥äîÛuAR̹7¸¹Ô9Í/kb@v*µþÓ‘7ã·3]›;,F>ë2•Ù¸~LΘº/¯Õ6Ø5'ò*èDñĺ…@žDÄf«9æˆY-ï‚Í­7C¨ÍÓ´¶l_Ü>"–i‘gGs_fe½ä+pQ­¯Õm*‰Ûa»t.,6w˜ÀCvøî)‰u¥û/ÊÞ]ä¤QK>L'Ã,ÏdrY=•Mz½Dœ—ö±qõ@]Mâ)§øºµ ›§7¯ðj¹|¸fÄŨÐçøäÑЧc”/™æÚ¡rÉFÄߣÅí"¦ìL%É’Óª?—$¿~­R,Ñ¿müõAî41꣣Os½n㑉ë?Aïrt5r¨{¹Tÿ¾ÿç3âË-z‘lšôyÞï_W~~UÏ¿ãHsVBº–¿ÈÚTU³àÆ!7J6†ÃÇî•59¹~بÈȈcKÎn‹ãýyÈ⑘g(ÛOÂé˜Ð/“_Ç'™‘‰—«FNv­4ùsÑ-÷ë¾K˳Û.i¯â"k^iµ¦¢â ¦`”Ê,£’\u5¹+:Tµ®Øõ•¬,ϵ¼ñ6}tÕëØLc>Ý+i–®D =CXBËI¥.Æ®‘.‡ßbÿ–ZIשŒ-÷pÇó$úÙÒ¯ßzË:<êóêZDBãªà «?næhÍ(ÐWOüò¢éÙꔚX/_XTdþýÞëG¤Jú[¿Ð%xrúêÆá•­ñäö°n‡ ëʧ‡ÝŽ;슄gW.7ƨáÿØ"Óc~-5"ñŸÊs~¤Å‘‡F}S­ASW(y?NSò<<ž‡V5pØ™)‘iò;-­¥ž@ðKÄSáPÉ5ˆ„TÞà¼ê’¸¨·0&ÅTßT¤2˜úä,ïŒmî𣶾y—n·}7óIvHÊMÔDº+”öØ ŠEsjˆðÛ9ü >aýüñKÇ ÆôËÞk,zzÛ$·*öUžL ¹à)~¶¯áÑUÈZïLxgŽÙ¥Òü?–D|£<Ãî‚GáÊ-ßÚŇFÖxL¨c5/þp¶Ap2±|bÈíìq’gÖ£e;.,'•dqÈmþÓ+ÿ´Ž~—ùç5Åꞯє¿Ók®0¹$^\}iåÉi«œ‰j•è¤çu…–#ʶäÑc§È»r§6ñ˜$£ #›èÆšM½â$ºÕ5phûCŸ]yU3lhi_ÑHÜ}Ÿ A~Ò»kRÅ“·¢³’¹>øˆßiÕšS©lÅ4B5E¸ î´xþ1/ýÂk¾Ó}|³>³Û ÷.dtË ø(^¡>i,Û T{“cÛcþxãÓ¸ÏÉõ÷Öžj°>K#ÜŒ6ûÔ9‹û”oLrò²™>6’[äç‰ȨoÌŒ‰~°feÿ|jÇ?bæÔ‡~Ö ¢ç÷T(n„•O0?­›7ãã}cÂοs*àƒ'¾W‹æãv 9 ·U3ª »"hñ¥Y‡ðw )¿nÎéµò%*§†ô Ú o³Vs§½}gÚÔ•R[›“¨Èù¸'õÛÚëŠáÂzÊøWRö·8ˆ>I_VUT>#÷q¤¦iÂæî³yÂôxÊy9ý)Ûð'œþ9aÃ3ÕÖ¬’, oH$ñ {äЬ¨;WIñùû¶q®Iž[§ìŠo¨òRЯûÑ3´Ûýªûô³Š7üâ (%ŽzÚõ¸å­ÎpÂ}„­^Àh÷E59ƒ;„s•†ÇV³Ÿ‘ú—ÈöE×çROr_vÐç›(†¦<ˆiÎ!,xƒP¿\¢•Éhð¦yf²ab¶É.IòRQ¾€qoâðcºb2ZQ„ ÏŽÊ3|9~' :ò„×Õ¼ü˜‹¹f5ÂVÒ<ÑÅ–Èb.™dÝ©XWbT3à{–_©³ÄJFœEf+_O¦Ð¼‡RÀ xsrãXá^rÉ<1½ç¬_¥KŠZÓ¯#—ô|…3m_0ÚQyöÝ¡SŠ*ä¸íÕæv;×úqÃøÓ©—.’Évø’¿ã¿ÙžßcœèLMdsÓnŠu`¤f³}uÛ%U›m2ÿ¶qhÇ`aF! "©TßÄÕ·ÉI>v¥lfMúx+G÷HVÒRÇHaÖËZŽ¢§×>¢¼²rãáýž :õí23Ò€×{üßVø£V„º?OßÐ(Vh}™V*n¾Ä®÷<6Ÿ$¥R@BÁQç–Â'Sëá°÷j¾è{ÏäKxÎÐÌúÅ“Ž}.ž?iPÿyÜAIá&•\'acd&ûRž…§“ÿ¼ÝôC·P¿‚àz>®ô³LÖ¡”›•œKôùÓ¢nXÊÉö0%Ò2ô™¡G,ÃÔ„YdÖϾ­÷.Þ¨«îM\Fˆ/ß9Þ|<ŸòXˆFsRD¹¤|±åK[ý‡ÆÈZÁ³¦DÏY%JÐ1ÜuQêõ¢ÆTý&R]¾ øÝ¶²!ŠÈÅ–W½®Õoï'ƽ›jýÌ›AñÓ?ô¹ ‰“f|óÖÖ‹†¦¤Ü—Aï¿Ò~®EÜÌóÞ23[¶7<ƒ‡OOðçý?ûƒ•Êñ |„ òçÏͰ_ÛÛ»Cáÿy=Ã?üUQ·JÄ¢²€QðE¾ëÀ!¼]:”•àæ~æðE³ëÈ,@„æéÓ¢:úZâ†g~ÿ|ø(«›ý'|‹~×áï þq¸‰™±º¦‰Æ&$K? ÎáØE >ô‚3Ìæá ópò·:… è½Péƒ/8Ù€ 🠥ÿOéi€ùÀ¸äì¶GÎ?pOQ4Ôö õ]Ï8àXoÀxl»ï?„Ýèüã…`É÷U:À(øbßJ!ýWÐ.ÿºËÀ¿NjRM tŸ€@˜w‘Na“þó÷zÚýe§2@ ¥~Û÷…} –˜!ož/À6u„ˆÀ.Ìš6Æ éîŽôWý58@:Bè=¤}Ê{ÍWØ|Šà÷kÆþ0‹¥Êt;r¨±!€úàÿîQÿ@Se÷…þ‹?t,ú Q*ä÷ÃpxÎòáíøë‰¢‚Ê^DDˆàZ‰]@f…}ÿØ£½==‘( ÌÙÞÅ?¸‡Þ3 ¹³~ޑ܅G(ÿðXò·l£<È€T•'ö{Óï?_c_¾“+ éí¹‡Ú†»Umª€!ÈÄ~ßIûC{gˆ›†%ÎÆä™€c—; ŃzÂ÷Ð'ÙôJ‰ZIý~Ï -ê@@,‰>O زB ZžˆÈ.âùHD'¤‡ ÜÕÅ Q{hõ’GÆm!¶ƒ@Dw‘'óÿŽŒ%:=ÄÅĪnÊß5ÿÍý!`î0ÌžKV´¼¦_p) c»¸½Oÿ†‹%øv©gÊ¥=€ß8wÛû÷VØC¤IŠ0› L?ø‡5?Ž‹…•GÏÑ6¬Ë9´‚œG1«ûœ¡(Ä  N{¥øzž¨€ÉA Σ+ëebYPŠÅ ^ R?E°+¶÷‡º9í5Ë9Ç]?EÑB ¢` &‘áá`a%øâkEw‚~%xBð”´àÜC¡(4ŠÙ+eæW}ÕÔjPéùwŒˆe=Á<ªIàR—‚ÁW\È»?ÒgïÈÐ_`LQ 8å6«ã„aE tûÙ²#$ˆ5+8Gš!÷§ù9Á<1p¤Çò´ïDêB/IìZŸ±¬GB%øÝÁ¾`HؾDï}*âwÄø<@›¢טìhÜ´]âR²µ¯Òå_Óš‹ ¬øû”°/î¡\ N°½ú }‰[@DŸ"ÇßÅë#b9ÙµÂ8ËÈYNªà4S—¼/tuB"0§½ë‹hu,à‘V)pš1(:(Ëš¦eÆŽ€%Qüþ å?Д¦A%’܆ÆìeÙY|ÁžÀÌ>Mîµ4ÛþžŽeåàgêSÚE§„ÿ†w…íe_¾r…K¨ ]epœ†¿ÀMÚD.VÐF 䋃-[ñv_œ' …†£1ö.pÌŠ@Ø£¼{Çl¹løc@ì a°e)VÿžŽeÙíU—h<`)xÖ\}Ž@ð÷Ãÿº÷¼‡ÔõÞï¥yÀ ¥ÎÅçq±°„ °Èùóô5-x(H‘Þ('˜7øö ëâ¡Ü^@ñ9Âøû¥³ ›Š†bIˆŒ,•1¸Ó,WߗІ{¸"`¤à+Œ uÞCïä:™¥-@¦"wšùZGÆüt§ÃÀ¾¦›â´Î¾èÿ¶‚ûUÐÄaúïõ5Xï¢N꜊5ÅJhÐó(@(ùQðŒ0ÝëókìYH}õɺŒ…Áüà¥k¼ÓpXmü^Íÿ¼5v÷Usaöå!ÑÞ8âßVÄn™Ð»Àh^ °ùüËõ¨s^Þ'd&O-Z_\\(Âî±×ÔJz÷í¡ >ާ¤€C±¤>fdéq¨^X£X0N*ÊÉm¡ŒTm-‡BHN ®Ø#aDbÉ䮽™P$ª@Y0S Óî ß«—OôVëܺ€M°—Ø3ÊÄJo)y˜Ÿç¨Àë@»»¸ NõŸÿÝC-¿IåRO^ôà8sÿ¯ÀX’9flU…‘í0ö'‰‚!÷ªªÏË{;ÈQƒ£Úr`(–Ô²Ò‚[@´b­;pQaŽpè^É á˜p³¸þY*°ÔÛO Åʨêjøi'!ÇE0µ¼7õ?ñ¬õ•–¹y 1B4N„1d¦b‰í¤ªZù xk„,VÏöÏï΂Ś> ÎB¶œ,öªóÁ©XAà‘9Ùp‰ÀBÁb=/àÄ¢`0Gô^ÕòÍ:  V5:€Å*^>8Klø,ͬÅad’œ_ãïàºBá{u}&ßy`‘qYœ …*ˆÄ’™Z•Å3@'˜iüŽDûíU°®Ý’}tp?-5x²Â;ÊÄš¤TtW Ƚ‡ÁÎ?׃ êEUp¯Ëm¹äP! œÇêªÞõœŠ%¶.׿I8°^—e[Õò%.¬zï8ÝbWåÒ'تœãeb ¥{ªy„H2`«FÏã‚"=a{KÕÏœ ¬«تã§b‰Õ’Ìk!Ú•kL`±xä„Àú¢öʬgkó»fq€ÅÓœŠ%–Õ„â¡#0 ž2`­²¸paQ0g7è^Zùná#€ Å ¶Àÿ¡ØE ¨.í u™œª®‹ã¢þÏ/F…fôؾ\Í ž]²DbÉt­ UÿÕ^’‚/þ‹ N¦?sß+Vk)÷‡@$ÈÀîÕ88ËýLjç)@^eÁrÿ¼6.ìŸ=ÃZ¶Ü-ðäܯǶ@‹þ¡»¤rÔ‘ýsßxwç*h¹/Õ…Ä ÿºÊz°Ãwé"0ʃ€;׫¿cY—:Á ì^aàÕ{»5n²+аßïN:çÊ&>äÛ…>ìû—h,Ù',£–¬²Å ¾Ÿ…ÈÂÍvƒA74 åwÚKùd‘í- :üÙÁ÷³è ÿžŽ%þ–¿çSm T,«€m~¨üx$ã‰ðv…ïՈߒ{R dÁ6ÏjüK4VÖð2k+«úÐz9ðDL~› wwÞk‘cªôŒ—QÀ*›±S†bMÄÁ”¢¤X M©Huuý3nª; ýµ#fÿˇ0”½;Üýµ=aÿkþïqis­§C¨í:ÝäÖÿ‡ÓaPuö& ÌÜ£gÚË?pŸéãóðÙ«+r9L*Rð½œB¢ƒc±NÃoµ ÀtºÇæVãæ¢`@Øs?ž Ä†ÞÈìàt;Áø7\,ÁÃVº-@èlˆ#=’7xŸ ‘I… Ìj©±"]âÀP,©´në—%ê8 –zÿnª/|ÏM²Ðð¥orÚÈa°ÔÔ¡ÿú0ÎnjÀÃ}©¿®ýo¥ñZ ¾?0ʆ€ç‚~ÓÁ©X[QYªÇ´1»ˆµF lÆýuî³—]_ê| äˆU°Ð]†b…@Ã!þ @‡ØÆ ¶@âKT'”¿'fŸx%,¾îPLQvð¬uø ,–Ü-뺋@–+:é]Üsïppa~˜Ç¯Ü@¬9ÁÐè}”>L­’Ä ø–Ì®3¤­ýß΀~¼¼œÂ“ìÉ ¯8NáGÀöžÃÀl) ÄØ“õĦb‰½:±-¥ IJ¸Y’ Æ…EÀüàŽ˜ýþ}D¯}çÑ\|àfé1ËßÓ±ÄkX 'Ö)|à$¿%€ïÃø"Qö1öJM8þ) 7Š‚{ê3Æ’,ÇGå “Ï€ƒã• ò¯¢´^ÄU'•„MWLNÅ[zjÜé×Cw `¬,Ú †@ì£ö‹–ê ZȳÇvð_`±äÖ“Ue„R@ ÎÂ`ÛÞKÂÁÅ“{µ&l˱u@3õClŸ‚ƒS±*^¾:ûýû@Å«ÄzüSƒu†Âp<~Êóý¤½‡€ŸÖq«;Ëš.‘q±å@fðf﬛Öï üõú^7*øCñtzñ¼³®Ô†‹…%lÜq†­ hàŸ‚a{÷…¡|ÜÝ÷P–•ªŸ` ´çoÉÀOf´à„a9õ;ÝsÂnÀ© €SÅwÑÌ^ïKûßýyg8ú¿yX”©Ô°Qáa­kGÿ޵‚c ìš{²àÕ¡ÏØÁèûo«| âši†$ÇÁfY}ÿ·l¬pä—»‰ìDþä@ðý·X®\ÚŒžÌ]O^Cø·l,á3¤Úºw€>¡ˆ,|ø€ð}·[$k~xSóã3#Xø(Õß²±„k#¼æ~½ò•,\†õ`ðýojKJ*´^–¯øT`á.Gþ–%œóyh/ß0‚…«ü¿ÊÎ5&Š+Šãƒ.¸¨ *Š[d*Q´­&E@«ÄÈC-UJ\̺°Ëºw–±•ÚÐÄ?ˆ‚‚ж°±Zø|ü@D!šUãhë#„T­ÄhmhzF©›=÷ÎÌðé&ì/ÿÜ{î9çÞ9gæCv8ý‚;rÂ`Ï/Àvúaá‘ •²ô*„~J[òòí¼”+Inp8r9Z¯Â‹(ž¤@÷#‘c‰(P¸‚–RéÜu¼µFBß“{<ßà •”ê×Å’T³AJaíÓ©Í<Œör8,•c‘Ŭ‹ëƒÂ =™ùÄ2*l´øPŸÃ›i×ggÿ¼|’ï¦Yx‘}“Xx„@mÄñÞÆ@Ž+²—¶_JßHéßVHÑÄv[ûúŠCÞÕλoUžR6!¼ò¹«/Á…âÖ¦ÛT8ýìþ³üšp*ÒÀÜ0Ü79ªý ë…ߌý­{FùMóÿÉS/Õ#¡½>xo;Œj8œV·+Á³÷uQtù½UWåŠ×ŠŸ› VcÜ(2G^í®Ü§ã¸á7ØÌÿÅ€#äEÚ“Çg‚™‡-Àí…üz5Ž×B/\nN]U1…ã²c×ࣗA²†ÌÕ‹ HÞšˆ×@.•e3Ù-¼Ñ’©‡8ð¢0-ms×% ¶LĦ“ºJ^YÔ·uÿÁ벯‰nk‡ð•o2d ÷ ™?/WÖÁ†ó˜]OI ˜Mßþº^á“~È*0Ïd5²øcñ¿à‡|ÒÏ>¨LH~Ò¸g'ìÃ#çqçK•¼Õf¤?,Ó­¾¶¡Òò¡ÃØÂ›®K“¿½¼ ”M2ø¨¨¦¿t9!Ì©ó®êaCc°óRûRaF“ÍÎX÷%%òFåÛÝ0 öÀ†3ÓO —Ø2š³;´µ 6Î?{_8Y,RÚYÓ$tŒüªÂYÔ4V&!4Éoäq @‡TØÂk§ËB%k&ï ø?~óš¨ÂÏ*s5ÊÈ„è-é ¾艽çÑ@y´XÕdVar֚ѩ@×dZv*!6øÊó%àCRÂ0vfˆ,VüÔ<ÜôÆk?¤xKtÌS€%ä.»Ûp÷7˜…›^Xn]´,Wü¬¼ÆêS¹°=*,·k©,!w\AùεpþÞMf—-—åŠy\Ym«Šê}5žíJv*áÆ²~:·ï3ω{ FÏ9ܱy¡” HÌfOá´ú48Ó÷ÏÅ?'w‰ß ÄŸ×váâ*pDŽŠ¿•æâuÔŠóÄ×ÊÅûè ˆ¿ªãÆžŠ”Kâ×л`igdaÄÒâw»ºhëZÅi¢ ¿2Ï…KlgÀcux èè¥SM›0½çWL‘öN¶—6"^E)J©òQ…t·Wbld›ôׯ mÈ*=´ª|±fÞ{3~3ŸÎU 'TšK1ó&Áة̸(f^crÿƒOtFgƒÂ⊖%ôÌ;SO Ñé+¦ƒŠ§Jj™› •i¹Èd«ÙÏF¡=eʯ¤¶mƒö} UAÇã ù¾^%ŽÏ…6L¤èÑ@øÎ÷áe¾ÛÄ›å¶*Ce{™CαÌ4TÌTÀ˜#×6Zb¾ßq¿*yâ–±a&5K‘®¯-n“ýbÅEsž(LHî±Na©‘îQ[@wu±çÞ.ú1èNH~\rÅXe`îžËÆJ@W r© + 1cIHz†ã~–ÚØÇíPËF¥¨ésµ“%†ä.áÐkÌ8³B?hH†»Ë|B*^×Ó¿trËs:±Ž…õV°ò:~jTcúSÆ1¢¦dÔ XÛ Ü›C· ]mò2 '‡RÈc0!«xm’(0gkr'ÐŽŸXzù¦øéÝÈôWXÛþkYçý@éMù¢¥¸7ämÔ!²ª™¸¸…Æ’Û°ƒ%M]Ke0Û&´+’ÛЯ?ð°‹–ñv“<¸Õ¶>½ùtô PK šEUýAbin/UTo:>cux èèPKcRebK+ª¶ à "¤>bin/WALinuxAgent-9.9.9.9-py2.7.eggUTê$@`ux èèPKcRpôŠ¡ÓÉ ¤D· manifest.xmlUTê$@`ux èèPK]¹ WALinuxAgent-2.9.1.1/tests/data/ga/fake_extension.zip000066400000000000000000000002511446033677600223760ustar00rootroot00000000000000PKÓ;Uq%Ü97test.shSVÔOÊÌÓ/ÎàJMÎÈWP*ÍsÎÏÍMÌKñÉÌ+­PH­HM.-IMQ(.MNN-.N+ÍÉ©TçPKÓ;Uq%Ü97€test.shPK5^WALinuxAgent-2.9.1.1/tests/data/hostgaplugin/000077500000000000000000000000001446033677600207675ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/hostgaplugin/ext_conf-empty_depends_on.xml000066400000000000000000000064561446033677600266630ustar00rootroot00000000000000 Prod https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml 2.5.0.2 Test https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml 2.5.0.2 CentralUSEUAP CRP MultipleExtensionsPerHandler https://dcrcqabsr1.blob.core.windows.net/$system/edpxmal5j1.058b176d-445b-4e75-bd97-4911511b7d96.status?sv=2018-03-28&sr=b&sk=system-1&sig=U4KaLxlyYfgQ%2fie8RCwgMBSXa3E4vlW0ozPYOEHikoc%3d&se=9999-01-01T00%3a00%3a00Z&sp=w https://umsafwzhkbm1rfrhl0ws.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"commandToExecute":"echo '09cd27e9-fbd6-48ad-be86-55f3783e0a23'"} } } ] } https://dcrcqabsr1.blob.core.windows.net/$system/edpxmal5j1.058b176d-445b-4e75-bd97-4911511b7d96.vmSettings?sv=2018-03-28&sr=b&sk=system-1&sig=mOMtcUyao4oNPMtcVhQjzMK%2bmGSJS3Y1MIKOJPjqzus%3d&se=9999-01-01T00%3a00%3a00Z&sp=r WALinuxAgent-2.9.1.1/tests/data/hostgaplugin/ext_conf-invalid_blob_type.xml000066400000000000000000000121651446033677600270060ustar00rootroot00000000000000 Prod https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml 2.5.0.2 Test https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml 2.5.0.2 CentralUSEUAP CRP MultipleExtensionsPerHandler https://dcrcqabsr1.blob.core.windows.net/$system/edpxmal5j1.058b176d-445b-4e75-bd97-4911511b7d96.status?sv=2018-03-28&sr=b&sk=system-1&sig=U4KaLxlyYfgQ%2fie8RCwgMBSXa3E4vlW0ozPYOEHikoc%3d&se=9999-01-01T00%3a00%3a00Z&sp=w https://umsafwzhkbm1rfrhl0ws.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml https://umsajbjtqrb3zqjvgb2z.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"commandToExecute":"echo '09cd27e9-fbd6-48ad-be86-55f3783e0a23'"} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"source":{"script":"echo '737fa9f1-e9bf-4c3e-ab1f-9e03cd0b5b40'"}} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"source":{"script":"echo 'f6a0a405-c028-4e68-bd77-5d491fbbd9cf'"}} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"source":{"script":"echo '338e316a-01bf-4513-8ae1-b603b09ad155'"}} } } ] } https://dcrcqabsr1.blob.core.windows.net/$system/edpxmal5j1.058b176d-445b-4e75-bd97-4911511b7d96.vmSettings?sv=2018-03-28&sr=b&sk=system-1&sig=mOMtcUyao4oNPMtcVhQjzMK%2bmGSJS3Y1MIKOJPjqzus%3d&se=9999-01-01T00%3a00%3a00Z&sp=r WALinuxAgent-2.9.1.1/tests/data/hostgaplugin/ext_conf-no_status_upload_blob.xml000066400000000000000000000045351446033677600277040ustar00rootroot00000000000000 Prod https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml CentralUSEUAP CRP https://umsanh4b5rfz0q0p4pwm.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"commandToExecute":"echo 'cee174d4-4daa-4b07-9958-53b9649445c2'"} } } ] } https://dcrcl3a0xs.blob.core.windows.net/$system/edp0plkw2b.86f4ae0a-61f8-48ae-9199-40f402d56864.vmSettings?sv=2018-03-28&sr=b&sk=system-1&sig=PaiLic%3d&se=9999-01-01T00%3a00%3a00Z&sp=r WALinuxAgent-2.9.1.1/tests/data/hostgaplugin/ext_conf-requested_version.xml000066400000000000000000000232641446033677600270710ustar00rootroot00000000000000 Prod 9.9.9.10 https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml Test 9.9.9.10 https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml CentralUSEUAP CRP MultipleExtensionsPerHandler https://dcrcl3a0xs.blob.core.windows.net/$system/edp0plkw2b.86f4ae0a-61f8-48ae-9199-40f402d56864.status?sv=2018-03-28&sr=b&sk=system-1&sig=KNWgC2%3d&se=9999-01-01T00%3a00%3a00Z&sp=w https://zrdfepirv2cbn09pr02a.blob.core.windows.net/a47f0806d764480a8d989d009c75007d/Microsoft.Azure.Monitor_AzureMonitorLinuxAgent_useast2euap_manifest.xml https://zrdfepirv2cbn06prdstr01a.blob.core.windows.net/4ef06ad957494df49c807a5334f2b5d2/Microsoft.Azure.Security.Monitoring_AzureSecurityLinuxAgent_useast2euap_manifest.xml https://umsanh4b5rfz0q0p4pwm.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml https://umsawqtlsshtn5v2nfgh.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml https://umsah3cwjlctnmhsvzqv.blob.core.windows.net/2bbece4f-0283-d415-b034-cc0adc6997a1/2bbece4f-0283-d415-b034-cc0adc6997a1_manifest.xml { "runtimeSettings": [ { "handlerSettings": { "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", "publicSettings": {"GCS_AUTO_CONFIG":true} } } ] } { "runtimeSettings": [ { "handlerSettings": { "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", "publicSettings": {"enableGenevaUpload":true} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"commandToExecute":"echo 'cee174d4-4daa-4b07-9958-53b9649445c2'"} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"source":{"script":"echo '4abb1e88-f349-41f8-8442-247d9fdfcac5'"}} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"source":{"script":"echo 'e865c9bc-a7b3-42c6-9a79-cfa98a1ee8b3'"}} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"source":{"script":"echo 'f923e416-0340-485c-9243-8b84fb9930c6'"}} } } ] } { "runtimeSettings": [ { "handlerSettings": { "protectedSettingsCertThumbprint": "59A10F50FFE2A0408D3F03FE336C8FD5716CF25C", "protectedSettings": "*** REDACTED ***" } } ] } https://dcrcl3a0xs.blob.core.windows.net/$system/edp0plkw2b.86f4ae0a-61f8-48ae-9199-40f402d56864.vmSettings?sv=2018-03-28&sr=b&sk=system-1&sig=PaiLic%3d&se=9999-01-01T00%3a00%3a00Z&sp=r WALinuxAgent-2.9.1.1/tests/data/hostgaplugin/ext_conf.xml000066400000000000000000000214751446033677600233270ustar00rootroot00000000000000 Prod https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml Test https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml CentralUSEUAP CRP MultipleExtensionsPerHandler https://dcrcl3a0xs.blob.core.windows.net/$system/edp0plkw2b.86f4ae0a-61f8-48ae-9199-40f402d56864.status?sv=2018-03-28&sr=b&sk=system-1&sig=KNWgC2%3d&se=9999-01-01T00%3a00%3a00Z&sp=w https://zrdfepirv2cbn09pr02a.blob.core.windows.net/a47f0806d764480a8d989d009c75007d/Microsoft.Azure.Monitor_AzureMonitorLinuxAgent_useast2euap_manifest.xml https://zrdfepirv2cbn06prdstr01a.blob.core.windows.net/4ef06ad957494df49c807a5334f2b5d2/Microsoft.Azure.Security.Monitoring_AzureSecurityLinuxAgent_useast2euap_manifest.xml https://umsanh4b5rfz0q0p4pwm.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml https://umsawqtlsshtn5v2nfgh.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml https://umsah3cwjlctnmhsvzqv.blob.core.windows.net/2bbece4f-0283-d415-b034-cc0adc6997a1/2bbece4f-0283-d415-b034-cc0adc6997a1_manifest.xml { "runtimeSettings": [ { "handlerSettings": { "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/Microsoft.Azure.Monitor.AzureMonitorLinuxAgent==", "publicSettings": {"GCS_AUTO_CONFIG":true} } } ] } { "runtimeSettings": [ { "handlerSettings": { "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent==", "publicSettings": {"enableGenevaUpload":true} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"commandToExecute":"echo 'cee174d4-4daa-4b07-9958-53b9649445c2'"} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"source":{"script":"echo '4abb1e88-f349-41f8-8442-247d9fdfcac5'"}} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"source":{"script":"echo 'e865c9bc-a7b3-42c6-9a79-cfa98a1ee8b3'"}} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"source":{"script":"echo 'f923e416-0340-485c-9243-8b84fb9930c6'"}} } } ] } { "runtimeSettings": [ { "handlerSettings": { "protectedSettingsCertThumbprint": "59A10F50FFE2A0408D3F03FE336C8FD5716CF25C", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpddesZQewdDBgegkxNzA1BgoJkgergres/Microsoft.OSTCExtensions.VMAccessForLinux==" } } ] } https://dcrcl3a0xs.blob.core.windows.net/$system/edp0plkw2b.86f4ae0a-61f8-48ae-9199-40f402d56864.vmSettings?sv=2018-03-28&sr=b&sk=system-1&sig=PaiLic%3d&se=9999-01-01T00%3a00%3a00Z&sp=r WALinuxAgent-2.9.1.1/tests/data/hostgaplugin/in_vm_artifacts_profile.json000066400000000000000000000000221446033677600265440ustar00rootroot00000000000000{ "onHold": true }WALinuxAgent-2.9.1.1/tests/data/hostgaplugin/vm_settings-difference_in_required_features.json000066400000000000000000000247711446033677600326130ustar00rootroot00000000000000{ "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", "inSvdSeqNo": 1, "extensionsLastModifiedTickCount": 637726657706205217, "extensionGoalStatesSource": "FastTrack", "onHold": true, "statusUploadBlob": { "statusBlobType": "BlockBlob", "value": "https://dcrcl3a0xs.blob.core.windows.net/$system/edp0plkw2b.86f4ae0a-61f8-48ae-9199-40f402d56864.status?sv=2018-03-28&sr=b&sk=system-1&sig=KNWgC2%3d&se=9999-01-01T00%3a00%3a00Z&sp=w" }, "inVMMetadata": { "subscriptionId": "8e037ad4-618f-4466-8bc8-5099d41ac15b", "resourceGroupName": "rg-dc-86fjzhp", "vmName": "edp0plkw2b", "location": "CentralUSEUAP", "vmId": "86f4ae0a-61f8-48ae-9199-40f402d56864", "vmSize": "Standard_B2s", "osType": "Linux" }, "requiredFeatures": [ { "name": "A_NON_EXISTING_FEATURE_USED_TO_PRODUCE_AN_ERROR" } ], "gaFamilies": [ { "name": "Prod", "uris": [ "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml", "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml" ] }, { "name": "Test", "uris": [ "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml", "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml" ] } ], "extensionGoalStates": [ { "name": "Microsoft.Azure.Monitor.AzureMonitorLinuxAgent", "version": "1.9.1", "location": "https://zrdfepirv2cbn04prdstr01a.blob.core.windows.net/a47f0806d764480a8d989d009c75007d/Microsoft.Azure.Monitor_AzureMonitorLinuxAgent_useast2euap_manifest.xml", "failoverlocation": "https://zrdfepirv2cbn06prdstr01a.blob.core.windows.net/a47f0806d764480a8d989d009c75007d/Microsoft.Azure.Monitor_AzureMonitorLinuxAgent_useast2euap_manifest.xml", "additionalLocations": ["https://zrdfepirv2cbn09pr02a.blob.core.windows.net/a47f0806d764480a8d989d009c75007d/Microsoft.Azure.Monitor_AzureMonitorLinuxAgent_useast2euap_manifest.xml"], "state": "enabled", "autoUpgrade": true, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "settings": [ { "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", "publicSettings": "{\"GCS_AUTO_CONFIG\":true}" } ] }, { "name": "Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent", "version": "2.15.112", "location": "https://zrdfepirv2cbn04prdstr01a.blob.core.windows.net/4ef06ad957494df49c807a5334f2b5d2/Microsoft.Azure.Security.Monitoring_AzureSecurityLinuxAgent_useast2euap_manifest.xml", "failoverlocation": "https://zrdfepirv2cbz06prdstr01a.blob.core.windows.net/4ef06ad957494df49c807a5334f2b5d2/Microsoft.Azure.Security.Monitoring_AzureSecurityLinuxAgent_useast2euap_manifest.xml", "additionalLocations": ["https://zrdfepirv2cbn06prdstr01a.blob.core.windows.net/4ef06ad957494df49c807a5334f2b5d2/Microsoft.Azure.Security.Monitoring_AzureSecurityLinuxAgent_useast2euap_manifest.xml"], "state": "enabled", "autoUpgrade": true, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "settings": [ { "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", "publicSettings": "{\"enableGenevaUpload\":true}" } ] }, { "name": "Microsoft.Azure.Extensions.CustomScript", "version": "2.1.6", "location": "https://umsavwggj2v40kvqhc0w.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml", "failoverlocation": "https://umsafwzhkbm1rfrhl0ws.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml", "additionalLocations": [ "https://umsanh4b5rfz0q0p4pwm.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml" ], "state": "enabled", "autoUpgrade": true, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "isMultiConfig": false, "settings": [ { "publicSettings": "{\"commandToExecute\":\"echo 'cee174d4-4daa-4b07-9958-53b9649445c2'\"}" } ], "dependsOn": [ { "DependsOnExtension": [ { "handler": "Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent" } ], "dependencyLevel": 1 } ] }, { "name": "Microsoft.CPlat.Core.RunCommandHandlerLinux", "version": "1.2.0", "location": "https://umsavbvncrpzbnxmxzmr.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml", "failoverlocation": "https://umsajbjtqrb3zqjvgb2z.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml", "additionalLocations": [ "https://umsawqtlsshtn5v2nfgh.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml" ], "state": "enabled", "autoUpgrade": true, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "isMultiConfig": true, "settings": [ { "publicSettings": "{\"source\":{\"script\":\"echo '4abb1e88-f349-41f8-8442-247d9fdfcac5'\"}}", "seqNo": 0, "extensionName": "MCExt1", "extensionState": "enabled" }, { "publicSettings": "{\"source\":{\"script\":\"echo 'e865c9bc-a7b3-42c6-9a79-cfa98a1ee8b3'\"}}", "seqNo": 0, "extensionName": "MCExt2", "extensionState": "enabled" }, { "publicSettings": "{\"source\":{\"script\":\"echo 'f923e416-0340-485c-9243-8b84fb9930c6'\"}}", "seqNo": 0, "extensionName": "MCExt3", "extensionState": "enabled" } ], "dependsOn": [ { "dependsOnExtension": [ { "extension": "...", "handler": "..." }, { "extension": "...", "handler": "..." } ], "dependencyLevel": 2, "name": "MCExt1" }, { "dependsOnExtension": [ { "extension": "...", "handler": "..." } ], "dependencyLevel": 1, "name": "MCExt2" } ] }, { "name": "Microsoft.OSTCExtensions.VMAccessForLinux", "version": "1.5.11", "location": "https://umsasc25p0kjg0c1dg4b.blob.core.windows.net/2bbece4f-0283-d415-b034-cc0adc6997a1/2bbece4f-0283-d415-b034-cc0adc6997a1_manifest.xml", "failoverlocation": "https://umsamfwlmfshvxx2lsjm.blob.core.windows.net/2bbece4f-0283-d415-b034-cc0adc6997a1/2bbece4f-0283-d415-b034-cc0adc6997a1_manifest.xml", "additionalLocations": [ "https://umsah3cwjlctnmhsvzqv.blob.core.windows.net/2bbece4f-0283-d415-b034-cc0adc6997a1/2bbece4f-0283-d415-b034-cc0adc6997a1_manifest.xml" ], "state": "enabled", "autoUpgrade": false, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "isMultiConfig": false, "settings": [ { "protectedSettingsCertThumbprint": "59A10F50FFE2A0408D3F03FE336C8FD5716CF25C", "protectedSettings": "*** REDACTED ***" } ] } ] } WALinuxAgent-2.9.1.1/tests/data/hostgaplugin/vm_settings-empty_depends_on.json000066400000000000000000000061021446033677600275550ustar00rootroot00000000000000{ "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "2e7f8b5d-f637-4721-b757-cb190d49b4e9", "correlationId": "1bef4c48-044e-4225-8f42-1d1eac1eb158", "inSvdSeqNo": 1, "extensionsLastModifiedTickCount": 637693267431616449, "extensionGoalStatesSource": "FastTrack", "StatusUploadBlob": { "statusBlobType": "BlockBlob", "value": "https://dcrcqabsr1.blob.core.windows.net/$system/edpxmal5j1.058b176d-445b-4e75-bd97-4911511b7d96.status?sv=2018-03-28&sr=b&sk=system-1&sig=U4KaLxlyYfgQ%2fie8RCwgMBSXa3E4vlW0ozPYOEHikoc%3d&se=9999-01-01T00%3a00%3a00Z&sp=w" }, "inVMMetadata": { "subscriptionId": "8e037ad4-618f-4466-8bc8-5099d41ac15b", "resourceGroupName": "rg-dc-qphvx25", "vmName": "edpxmal5j1", "location": "CentralUSEUAP", "vmId": "058b176d-445b-4e75-bd97-4911511b7d96", "vmSize": "Standard_D2s_v3", "osType": "Linux" }, "requiredFeatures": [ { "name": "MultipleExtensionsPerHandler" } ], "gaFamilies": [ { "Name": "Prod", "Version": "2.5.0.2", "Uris": [ "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml", "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml" ] }, { "Name": "Test", "Version": "2.5.0.2", "Uris": [ "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml", "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml" ] } ], "extensionGoalStates": [ { "name": "Microsoft.Azure.Extensions.CustomScript", "version": "2.1.6", "location": "https://umsavwggj2v40kvqhc0w.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml", "failoverlocation": "https://umsanh4b5rfz0q0p4pwm.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml", "additionalLocations": [ "https://umsafwzhkbm1rfrhl0ws.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml" ], "state": "enabled", "autoUpgrade": true, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "isMultiConfig": false, "settings": [ { "publicSettings": "{\"commandToExecute\":\"echo '09cd27e9-fbd6-48ad-be86-55f3783e0a23'\"}" } ], "dependsOn": [] } ] }WALinuxAgent-2.9.1.1/tests/data/hostgaplugin/vm_settings-fabric-no_thumbprints.json000066400000000000000000000215441446033677600305270ustar00rootroot00000000000000{ "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", "inSvdSeqNo": 1, "extensionsLastModifiedTickCount": 637726657706205299, "extensionGoalStatesSource": "Fabric", "onHold": true, "statusUploadBlob": { "statusBlobType": "BlockBlob", "value": "https://dcrcl3a0xs.blob.core.windows.net/$system/edp0plkw2b.86f4ae0a-61f8-48ae-9199-40f402d56864.status?sv=2018-03-28&sr=b&sk=system-1&sig=KNWgC2%3d&se=9999-01-01T00%3a00%3a00Z&sp=w" }, "inVMMetadata": { "subscriptionId": "8e037ad4-618f-4466-8bc8-5099d41ac15b", "resourceGroupName": "rg-dc-86fjzhp", "vmName": "edp0plkw2b", "location": "CentralUSEUAP", "vmId": "86f4ae0a-61f8-48ae-9199-40f402d56864", "vmSize": "Standard_B2s", "osType": "Linux" }, "requiredFeatures": [ { "name": "MultipleExtensionsPerHandler" } ], "gaFamilies": [ { "name": "Prod", "uris": [ "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml", "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml" ] }, { "name": "Test", "uris": [ "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml", "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml" ] } ], "extensionGoalStates": [ { "name": "Microsoft.Azure.Monitor.AzureMonitorLinuxAgent", "version": "1.9.1", "location": "https://zrdfepirv2cbn04prdstr01a.blob.core.windows.net/a47f0806d764480a8d989d009c75007d/Microsoft.Azure.Monitor_AzureMonitorLinuxAgent_useast2euap_manifest.xml", "failoverlocation": "https://zrdfepirv2cbn06prdstr01a.blob.core.windows.net/a47f0806d764480a8d989d009c75007d/Microsoft.Azure.Monitor_AzureMonitorLinuxAgent_useast2euap_manifest.xml", "additionalLocations": ["https://zrdfepirv2cbn09pr02a.blob.core.windows.net/a47f0806d764480a8d989d009c75007d/Microsoft.Azure.Monitor_AzureMonitorLinuxAgent_useast2euap_manifest.xml"], "state": "enabled", "autoUpgrade": true, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "settings": [ { "publicSettings": "{\"GCS_AUTO_CONFIG\":true}" } ] }, { "name": "Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent", "version": "2.15.112", "location": "https://zrdfepirv2cbn04prdstr01a.blob.core.windows.net/4ef06ad957494df49c807a5334f2b5d2/Microsoft.Azure.Security.Monitoring_AzureSecurityLinuxAgent_useast2euap_manifest.xml", "failoverlocation": "https://zrdfepirv2cbz06prdstr01a.blob.core.windows.net/4ef06ad957494df49c807a5334f2b5d2/Microsoft.Azure.Security.Monitoring_AzureSecurityLinuxAgent_useast2euap_manifest.xml", "additionalLocations": ["https://zrdfepirv2cbn06prdstr01a.blob.core.windows.net/4ef06ad957494df49c807a5334f2b5d2/Microsoft.Azure.Security.Monitoring_AzureSecurityLinuxAgent_useast2euap_manifest.xml"], "state": "enabled", "autoUpgrade": true, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "settings": [ { "publicSettings": "{\"enableGenevaUpload\":true}" } ] }, { "name": "Microsoft.Azure.Extensions.CustomScript", "version": "2.1.6", "location": "https://umsavwggj2v40kvqhc0w.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml", "failoverlocation": "https://umsafwzhkbm1rfrhl0ws.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml", "additionalLocations": [ "https://umsanh4b5rfz0q0p4pwm.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml" ], "state": "enabled", "autoUpgrade": true, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "isMultiConfig": false, "settings": [ { "publicSettings": "{\"commandToExecute\":\"echo 'cee174d4-4daa-4b07-9958-53b9649445c2'\"}" } ], "dependsOn": [ { "DependsOnExtension": [ { "handler": "Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent" } ], "dependencyLevel": 1 } ] }, { "name": "Microsoft.CPlat.Core.RunCommandHandlerLinux", "version": "1.2.0", "location": "https://umsavbvncrpzbnxmxzmr.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml", "failoverlocation": "https://umsajbjtqrb3zqjvgb2z.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml", "additionalLocations": [ "https://umsawqtlsshtn5v2nfgh.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml" ], "state": "enabled", "autoUpgrade": true, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "isMultiConfig": true, "settings": [ { "publicSettings": "{\"source\":{\"script\":\"echo '4abb1e88-f349-41f8-8442-247d9fdfcac5'\"}}", "seqNo": 0, "extensionName": "MCExt1", "extensionState": "enabled" }, { "publicSettings": "{\"source\":{\"script\":\"echo 'e865c9bc-a7b3-42c6-9a79-cfa98a1ee8b3'\"}}", "seqNo": 0, "extensionName": "MCExt2", "extensionState": "enabled" }, { "publicSettings": "{\"source\":{\"script\":\"echo 'f923e416-0340-485c-9243-8b84fb9930c6'\"}}", "seqNo": 0, "extensionName": "MCExt3", "extensionState": "enabled" } ], "dependsOn": [ { "dependsOnExtension": [ { "extension": "...", "handler": "..." }, { "extension": "...", "handler": "..." } ], "dependencyLevel": 2, "name": "MCExt1" }, { "dependsOnExtension": [ { "extension": "...", "handler": "..." } ], "dependencyLevel": 1, "name": "MCExt2" } ] }, { "name": "Microsoft.OSTCExtensions.VMAccessForLinux", "version": "1.5.11", "location": "https://umsasc25p0kjg0c1dg4b.blob.core.windows.net/2bbece4f-0283-d415-b034-cc0adc6997a1/2bbece4f-0283-d415-b034-cc0adc6997a1_manifest.xml", "failoverlocation": "https://umsamfwlmfshvxx2lsjm.blob.core.windows.net/2bbece4f-0283-d415-b034-cc0adc6997a1/2bbece4f-0283-d415-b034-cc0adc6997a1_manifest.xml", "additionalLocations": [ "https://umsah3cwjlctnmhsvzqv.blob.core.windows.net/2bbece4f-0283-d415-b034-cc0adc6997a1/2bbece4f-0283-d415-b034-cc0adc6997a1_manifest.xml" ], "state": "enabled", "autoUpgrade": false, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "isMultiConfig": false, "settings": [ ] } ] } WALinuxAgent-2.9.1.1/tests/data/hostgaplugin/vm_settings-invalid_blob_type.json000066400000000000000000000114371446033677600277150ustar00rootroot00000000000000{ "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "2e7f8b5d-f637-4721-b757-cb190d49b4e9", "correlationId": "1bef4c48-044e-4225-8f42-1d1eac1eb158", "inSvdSeqNo": 1, "extensionsLastModifiedTickCount": 637693267431616449, "extensionGoalStatesSource": "FastTrack", "StatusUploadBlob": { "statusBlobType": "INVALID_BLOB_TYPE", "value": "https://dcrcqabsr1.blob.core.windows.net/$system/edpxmal5j1.058b176d-445b-4e75-bd97-4911511b7d96.status?sv=2018-03-28&sr=b&sk=system-1&sig=U4KaLxlyYfgQ%2fie8RCwgMBSXa3E4vlW0ozPYOEHikoc%3d&se=9999-01-01T00%3a00%3a00Z&sp=w" }, "inVMMetadata": { "subscriptionId": "8e037ad4-618f-4466-8bc8-5099d41ac15b", "resourceGroupName": "rg-dc-qphvx25", "vmName": "edpxmal5j1", "location": "CentralUSEUAP", "vmId": "058b176d-445b-4e75-bd97-4911511b7d96", "vmSize": "Standard_D2s_v3", "osType": "Linux" }, "requiredFeatures": [ { "name": "MultipleExtensionsPerHandler" } ], "gaFamilies": [ { "Name": "Prod", "Version": "2.5.0.2", "Uris": [ "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml", "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml" ] }, { "Name": "Test", "Version": "2.5.0.2", "Uris": [ "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml", "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml" ] } ], "extensionGoalStates": [ { "name": "Microsoft.Azure.Extensions.CustomScript", "version": "2.1.6", "location": "https://umsavwggj2v40kvqhc0w.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml", "failoverlocation": "https://umsanh4b5rfz0q0p4pwm.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml", "additionalLocations": [ "https://umsafwzhkbm1rfrhl0ws.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml" ], "state": "enabled", "autoUpgrade": true, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "isMultiConfig": false, "settings": [ { "publicSettings": "{\"commandToExecute\":\"echo '09cd27e9-fbd6-48ad-be86-55f3783e0a23'\"}" } ] }, { "name": "Microsoft.CPlat.Core.RunCommandHandlerLinux", "version": "1.2.0", "location": "https://umsavbvncrpzbnxmxzmr.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml", "failoverlocation": "https://umsawqtlsshtn5v2nfgh.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml", "additionalLocations": [ "https://umsajbjtqrb3zqjvgb2z.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml" ], "state": "enabled", "autoUpgrade": true, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "isMultiConfig": true, "settings": [ { "publicSettings": "{\"source\":{\"script\":\"echo '737fa9f1-e9bf-4c3e-ab1f-9e03cd0b5b40'\"}}", "seqNo": 0, "extensionName": "MCExt1", "extensionState": "enabled" }, { "publicSettings": "{\"source\":{\"script\":\"echo 'f6a0a405-c028-4e68-bd77-5d491fbbd9cf'\"}}", "seqNo": 0, "extensionName": "MCExt2", "extensionState": "enabled" }, { "publicSettings": "{\"source\":{\"script\":\"echo '338e316a-01bf-4513-8ae1-b603b09ad155'\"}}", "seqNo": 0, "extensionName": "MCExt3", "extensionState": "enabled" } ] } ] }WALinuxAgent-2.9.1.1/tests/data/hostgaplugin/vm_settings-missing_cert.json000066400000000000000000000061441446033677600267150ustar00rootroot00000000000000{ "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", "inSvdSeqNo": 1, "extensionsLastModifiedTickCount": 637726657706205299, "extensionGoalStatesSource": "FastTrack", "onHold": true, "statusUploadBlob": { "statusBlobType": "BlockBlob", "value": "https://dcrcl3a0xs.blob.core.windows.net/$system/edp0plkw2b.86f4ae0a-61f8-48ae-9199-40f402d56864.status?sv=2018-03-28&sr=b&sk=system-1&sig=KNWgC2%3d&se=9999-01-01T00%3a00%3a00Z&sp=w" }, "inVMMetadata": { "subscriptionId": "8e037ad4-618f-4466-8bc8-5099d41ac15b", "resourceGroupName": "rg-dc-86fjzhp", "vmName": "edp0plkw2b", "location": "CentralUSEUAP", "vmId": "86f4ae0a-61f8-48ae-9199-40f402d56864", "vmSize": "Standard_B2s", "osType": "Linux" }, "requiredFeatures": [ { "name": "MultipleExtensionsPerHandler" } ], "gaFamilies": [ { "name": "Prod", "uris": [ "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml", "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml" ] }, { "name": "Test", "uris": [ "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml", "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml" ] } ], "extensionGoalStates": [ { "name": "Microsoft.OSTCExtensions.VMAccessForLinux", "version": "1.5.11", "location": "https://umsasc25p0kjg0c1dg4b.blob.core.windows.net/2bbece4f-0283-d415-b034-cc0adc6997a1/2bbece4f-0283-d415-b034-cc0adc6997a1_manifest.xml", "failoverlocation": "https://umsamfwlmfshvxx2lsjm.blob.core.windows.net/2bbece4f-0283-d415-b034-cc0adc6997a1/2bbece4f-0283-d415-b034-cc0adc6997a1_manifest.xml", "additionalLocations": [ "https://umsah3cwjlctnmhsvzqv.blob.core.windows.net/2bbece4f-0283-d415-b034-cc0adc6997a1/2bbece4f-0283-d415-b034-cc0adc6997a1_manifest.xml" ], "state": "enabled", "autoUpgrade": false, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "isMultiConfig": false, "settings": [ { "protectedSettingsCertThumbprint": "59A10F50FFE2A0408D3F03FE336C8FD5716CF25C", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpddesZQewdDBgegkxNzA1BgoJkgergres/Microsoft.OSTCExtensions.VMAccessForLinux==" } ] } ] } WALinuxAgent-2.9.1.1/tests/data/hostgaplugin/vm_settings-no_manifests.json000066400000000000000000000053371446033677600267170ustar00rootroot00000000000000{ "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "89d50bf1-fa55-4257-8af3-3db0c9f81ab4", "correlationId": "c143f8f0-a66b-4881-8c06-1efd278b0b02", "inSvdSeqNo": 978, "extensionsLastModifiedTickCount": 637829610574739741, "extensionGoalStatesSource": "FastTrack", "statusUploadBlob": { "statusBlobType": "PageBlob", "value": "https://md-ssd-xpdjf15s.blob.core.windows.net/$system/u-sqlwatcher.f338f67e.status?sv=2018-03-28&sr=b&sk=system-1&sig=88Y3NM%2b1aU%3d&se=9999-01-01T00%3a00%3a00Z&sp=rw" }, "inVMMetadata": { "subscriptionId": "8d3c2715-f063-40b8-9402-49784992ae8d", "resourceGroupName": "SYSTEMCENTERCURRENTBRANCH", "vmName": "ubuntu-sqlwatcher", "location": "centralus", "vmId": "f338f67e-5d06-4f13-892a-ff1b047ba5bf", "vmSize": "Standard_D2s_v3", "osType": "Linux", "vmImage": { "publisher": "Canonical", "offer": "UbuntuServer", "sku": "18.04-LTS", "version": "18.04.202005220" } }, "gaFamilies": [ { "name": "Prod" } ], "extensionGoalStates": [ { "name": "Microsoft.Azure.Monitor.WorkloadInsights.Test.Workload.LinuxConfigAgent", "version": "3.0", "state": "uninstall", "autoUpgrade": true, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "isMultiConfig": false }, { "name": "Microsoft.Azure.Monitor.WorkloadInsights.Test.Workload.LinuxInstallerAgent", "version": "11.0", "state": "uninstall", "autoUpgrade": true, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "isMultiConfig": false }, { "name": "Microsoft.Azure.Monitor.Workloads.Workload.WLILinuxExtension", "version": "0.2.127", "location": "https://umsakzkwhng2ft0jjptl.blob.core.windows.net/deeb2df6-c025-e6fb-b015-449ed6a676bc/deeb2df6-c025-e6fb-b015-449ed6a676bc_manifest.xml", "failoverLocation": "https://umsafmqfbv4hgrd1hqff.blob.core.windows.net/deeb2df6-c025-e6fb-b015-449ed6a676bc/deeb2df6-c025-e6fb-b015-449ed6a676bc_manifest.xml", "state": "enabled", "autoUpgrade": true, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 7, "isMultiConfig": false, "settings": [ { "publicSettings": "{\"workloadConfig\": null}" } ] } ] }WALinuxAgent-2.9.1.1/tests/data/hostgaplugin/vm_settings-no_status_upload_blob.json000066400000000000000000000051621446033677600306070ustar00rootroot00000000000000{ "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", "inSvdSeqNo": 1, "extensionsLastModifiedTickCount": 637726657706209999, "extensionGoalStatesSource": "FastTrack", "onHold": true, "inVMMetadata": { "subscriptionId": "8e037ad4-618f-4466-8bc8-5099d41ac15b", "resourceGroupName": "rg-dc-86fjzhp", "vmName": "edp0plkw2b", "location": "CentralUSEUAP", "vmId": "86f4ae0a-61f8-48ae-9199-40f402d56864", "vmSize": "Standard_B2s", "osType": "Linux" }, "requiredFeatures": [ { "name": "MultipleExtensionsPerHandler" } ], "gaFamilies": [ { "name": "Prod", "uris": [ "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml", "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml" ] } ], "extensionGoalStates": [ { "name": "Microsoft.Azure.Extensions.CustomScript", "version": "2.1.6", "location": "https://umsavwggj2v40kvqhc0w.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml", "failoverlocation": "https://umsafwzhkbm1rfrhl0ws.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml", "additionalLocations": [ "https://umsanh4b5rfz0q0p4pwm.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml" ], "state": "enabled", "autoUpgrade": true, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "isMultiConfig": false, "settings": [ { "publicSettings": "{\"commandToExecute\":\"echo 'cee174d4-4daa-4b07-9958-53b9649445c2'\"}" } ], "dependsOn": [ { "DependsOnExtension": [ { "handler": "Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent" } ], "dependencyLevel": 1 } ] } ] } WALinuxAgent-2.9.1.1/tests/data/hostgaplugin/vm_settings-out-of-sync.json000066400000000000000000000071721446033677600264140ustar00rootroot00000000000000{ "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "AAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE", "correlationId": "EEEEEEEE-DDDD-CCCC-BBBB-AAAAAAAAAAAA", "inSvdSeqNo": 1, "extensionsLastModifiedTickCount": 637726657000000000, "extensionGoalStatesSource": "FastTrack", "onHold": true, "statusUploadBlob": { "statusBlobType": "BlockBlob", "value": "https://dcrcl3a0xs.blob.core.windows.net/$system/edp0plkw2b.86f4ae0a-61f8-48ae-9199-40f402d56864.status?sv=2018-03-28&sr=b&sk=system-1&sig=KNWgC2%3d&se=9999-01-01T00%3a00%3a00Z&sp=w" }, "inVMMetadata": { "subscriptionId": "8e037ad4-618f-4466-8bc8-5099d41ac15b", "resourceGroupName": "rg-dc-86fjzhp", "vmName": "edp0plkw2b", "location": "CentralUSEUAP", "vmId": "86f4ae0a-61f8-48ae-9199-40f402d56864", "vmSize": "Standard_B2s", "osType": "Linux" }, "requiredFeatures": [ { "name": "MultipleExtensionsPerHandler" } ], "gaFamilies": [ { "name": "Prod", "uris": [ "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml", "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml" ] }, { "name": "Test", "uris": [ "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml", "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml" ] } ], "extensionGoalStates": [ { "name": "Microsoft.Azure.Monitor.AzureMonitorLinuxAgent", "version": "1.9.1", "location": "https://zrdfepirv2cbn04prdstr01a.blob.core.windows.net/a47f0806d764480a8d989d009c75007d/Microsoft.Azure.Monitor_AzureMonitorLinuxAgent_useast2euap_manifest.xml", "failoverlocation": "https://zrdfepirv2cbn06prdstr01a.blob.core.windows.net/a47f0806d764480a8d989d009c75007d/Microsoft.Azure.Monitor_AzureMonitorLinuxAgent_useast2euap_manifest.xml", "additionalLocations": ["https://zrdfepirv2cbn09pr02a.blob.core.windows.net/a47f0806d764480a8d989d009c75007d/Microsoft.Azure.Monitor_AzureMonitorLinuxAgent_useast2euap_manifest.xml"], "state": "enabled", "autoUpgrade": true, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "settings": [ { "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", "publicSettings": "{\"GCS_AUTO_CONFIG\":true}" } ] } ] } WALinuxAgent-2.9.1.1/tests/data/hostgaplugin/vm_settings-parse_error.json000066400000000000000000000063101446033677600265450ustar00rootroot00000000000000{ "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": THIS_IS_A_SYNTAX_ERROR, "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", "inSvdSeqNo": 1, "extensionsLastModifiedTickCount": 637726657706205217, "extensionGoalStatesSource": "FastTrack", "onHold": true, "statusUploadBlob": { "statusBlobType": "BlockBlob", "value": "https://dcrcl3a0xs.blob.core.windows.net/$system/edp0plkw2b.86f4ae0a-61f8-48ae-9199-40f402d56864.status?sv=2018-03-28&sr=b&sk=system-1&sig=KNWgC2%3d&se=9999-01-01T00%3a00%3a00Z&sp=w" }, "inVMMetadata": { "subscriptionId": "8e037ad4-618f-4466-8bc8-5099d41ac15b", "resourceGroupName": "rg-dc-86fjzhp", "vmName": "edp0plkw2b", "location": "CentralUSEUAP", "vmId": "86f4ae0a-61f8-48ae-9199-40f402d56864", "vmSize": "Standard_B2s", "osType": "Linux" }, "gaFamilies": [ { "name": "Prod", "uris": [ "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml", "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml" ] }, { "name": "Test", "uris": [ "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml", "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml" ] } ], "extensionGoalStates": [ { "name": "Microsoft.Azure.Extensions.CustomScript", "version": "2.1.6", "location": "https://umsavwggj2v40kvqhc0w.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml", "failoverlocation": "https://umsafwzhkbm1rfrhl0ws.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml", "additionalLocations": [ "https://umsanh4b5rfz0q0p4pwm.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml" ], "state": "enabled", "autoUpgrade": true, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "isMultiConfig": false, "settings": [ { "publicSettings": "{\"commandToExecute\":\"echo 'cee174d4-4daa-4b07-9958-53b9649445c2'\"}" } ], "dependsOn": [ { "DependsOnExtension": [ { "handler": "Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent" } ], "dependencyLevel": 1 } ] } ] } WALinuxAgent-2.9.1.1/tests/data/hostgaplugin/vm_settings-requested_version.json000066400000000000000000000167551446033677600300060ustar00rootroot00000000000000{ "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", "inSvdSeqNo": 1, "extensionsLastModifiedTickCount": 637726699999999999, "extensionGoalStatesSource": "FastTrack", "onHold": true, "statusUploadBlob": { "statusBlobType": "BlockBlob", "value": "https://dcrcl3a0xs.blob.core.windows.net/$system/edp0plkw2b.86f4ae0a-61f8-48ae-9199-40f402d56864.status?sv=2018-03-28&sr=b&sk=system-1&sig=KNWgC2%3d&se=9999-01-01T00%3a00%3a00Z&sp=w" }, "inVMMetadata": { "subscriptionId": "8e037ad4-618f-4466-8bc8-5099d41ac15b", "resourceGroupName": "rg-dc-86fjzhp", "vmName": "edp0plkw2b", "location": "CentralUSEUAP", "vmId": "86f4ae0a-61f8-48ae-9199-40f402d56864", "vmSize": "Standard_B2s", "osType": "Linux" }, "requiredFeatures": [ { "name": "MultipleExtensionsPerHandler" } ], "gaFamilies": [ { "name": "Prod", "version": "9.9.9.9", "uris": [ "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml", "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml" ] }, { "name": "Test", "version": "9.9.9.9", "uris": [ "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml", "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml" ] } ], "extensionGoalStates": [ { "name": "Microsoft.Azure.Monitor.AzureMonitorLinuxAgent", "version": "1.9.1", "location": "https://zrdfepirv2cbn04prdstr01a.blob.core.windows.net/a47f0806d764480a8d989d009c75007d/Microsoft.Azure.Monitor_AzureMonitorLinuxAgent_useast2euap_manifest.xml", "state": "enabled", "autoUpgrade": true, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "settings": [ { "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", "publicSettings": "{\"GCS_AUTO_CONFIG\":true}" } ] }, { "name": "Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent", "version": "2.15.112", "location": "https://zrdfepirv2cbn04prdstr01a.blob.core.windows.net/4ef06ad957494df49c807a5334f2b5d2/Microsoft.Azure.Security.Monitoring_AzureSecurityLinuxAgent_useast2euap_manifest.xml", "state": "enabled", "autoUpgrade": true, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "settings": [ { "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", "publicSettings": "{\"enableGenevaUpload\":true}" } ] }, { "name": "Microsoft.Azure.Extensions.CustomScript", "version": "2.1.6", "location": "https://umsavwggj2v40kvqhc0w.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml", "failoverlocation": "https://umsafwzhkbm1rfrhl0ws.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml", "additionalLocations": [ "https://umsanh4b5rfz0q0p4pwm.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml" ], "state": "enabled", "autoUpgrade": true, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "isMultiConfig": false, "settings": [ { "publicSettings": "{\"commandToExecute\":\"echo 'cee174d4-4daa-4b07-9958-53b9649445c2'\"}" } ] }, { "name": "Microsoft.CPlat.Core.RunCommandHandlerLinux", "version": "1.2.0", "location": "https://umsavbvncrpzbnxmxzmr.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml", "failoverlocation": "https://umsajbjtqrb3zqjvgb2z.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml", "additionalLocations": [ "https://umsawqtlsshtn5v2nfgh.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml" ], "state": "enabled", "autoUpgrade": true, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "isMultiConfig": true, "settings": [ { "publicSettings": "{\"source\":{\"script\":\"echo '4abb1e88-f349-41f8-8442-247d9fdfcac5'\"}}", "seqNo": 0, "extensionName": "MCExt1", "extensionState": "enabled" }, { "publicSettings": "{\"source\":{\"script\":\"echo 'e865c9bc-a7b3-42c6-9a79-cfa98a1ee8b3'\"}}", "seqNo": 0, "extensionName": "MCExt2", "extensionState": "enabled" }, { "publicSettings": "{\"source\":{\"script\":\"echo 'f923e416-0340-485c-9243-8b84fb9930c6'\"}}", "seqNo": 0, "extensionName": "MCExt3", "extensionState": "enabled" } ] } ] }WALinuxAgent-2.9.1.1/tests/data/hostgaplugin/vm_settings-unsupported_version.json000066400000000000000000000062671446033677600303720ustar00rootroot00000000000000{ "hostGAPluginVersion": "1.0.8.116", "vmSettingsSchemaVersion": "0.0", "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", "inSvdSeqNo": 1, "extensionsLastModifiedTickCount": 637726657706205217, "extensionGoalStatesSource": "FastTrack", "onHold": true, "statusUploadBlob": { "statusBlobType": "BlockBlob", "value": "https://dcrcl3a0xs.blob.core.windows.net/$system/edp0plkw2b.86f4ae0a-61f8-48ae-9199-40f402d56864.status?sv=2018-03-28&sr=b&sk=system-1&sig=KNWgC2%3d&se=9999-01-01T00%3a00%3a00Z&sp=w" }, "inVMMetadata": { "subscriptionId": "8e037ad4-618f-4466-8bc8-5099d41ac15b", "resourceGroupName": "rg-dc-86fjzhp", "vmName": "edp0plkw2b", "location": "CentralUSEUAP", "vmId": "86f4ae0a-61f8-48ae-9199-40f402d56864", "vmSize": "Standard_B2s", "osType": "Linux" }, "gaFamilies": [ { "name": "Prod", "uris": [ "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml", "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml" ] }, { "name": "Test", "uris": [ "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml", "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml" ] } ], "extensionGoalStates": [ { "name": "Microsoft.Azure.Extensions.CustomScript", "version": "2.1.6", "location": "https://umsavwggj2v40kvqhc0w.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml", "failoverlocation": "https://umsafwzhkbm1rfrhl0ws.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml", "additionalLocations": [ "https://umsanh4b5rfz0q0p4pwm.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml" ], "state": "enabled", "autoUpgrade": true, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "isMultiConfig": false, "settings": [ { "publicSettings": "{\"commandToExecute\":\"echo 'cee174d4-4daa-4b07-9958-53b9649445c2'\"}" } ], "dependsOn": [ { "DependsOnExtension": [ { "handler": "Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent" } ], "dependencyLevel": 1 } ] } ] } WALinuxAgent-2.9.1.1/tests/data/hostgaplugin/vm_settings.json000066400000000000000000000232631446033677600242320ustar00rootroot00000000000000{ "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", "inSvdSeqNo": 1, "extensionsLastModifiedTickCount": 637726657706205299, "extensionGoalStatesSource": "FastTrack", "onHold": true, "statusUploadBlob": { "statusBlobType": "BlockBlob", "value": "https://dcrcl3a0xs.blob.core.windows.net/$system/edp0plkw2b.86f4ae0a-61f8-48ae-9199-40f402d56864.status?sv=2018-03-28&sr=b&sk=system-1&sig=KNWgC2%3d&se=9999-01-01T00%3a00%3a00Z&sp=w" }, "inVMMetadata": { "subscriptionId": "8e037ad4-618f-4466-8bc8-5099d41ac15b", "resourceGroupName": "rg-dc-86fjzhp", "vmName": "edp0plkw2b", "location": "CentralUSEUAP", "vmId": "86f4ae0a-61f8-48ae-9199-40f402d56864", "vmSize": "Standard_B2s", "osType": "Linux" }, "requiredFeatures": [ { "name": "MultipleExtensionsPerHandler" } ], "gaFamilies": [ { "name": "Prod", "uris": [ "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml", "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml" ] }, { "name": "Test", "uris": [ "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml", "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml" ] } ], "extensionGoalStates": [ { "name": "Microsoft.Azure.Monitor.AzureMonitorLinuxAgent", "version": "1.9.1", "location": "https://zrdfepirv2cbn04prdstr01a.blob.core.windows.net/a47f0806d764480a8d989d009c75007d/Microsoft.Azure.Monitor_AzureMonitorLinuxAgent_useast2euap_manifest.xml", "failoverlocation": "https://zrdfepirv2cbn06prdstr01a.blob.core.windows.net/a47f0806d764480a8d989d009c75007d/Microsoft.Azure.Monitor_AzureMonitorLinuxAgent_useast2euap_manifest.xml", "additionalLocations": ["https://zrdfepirv2cbn09pr02a.blob.core.windows.net/a47f0806d764480a8d989d009c75007d/Microsoft.Azure.Monitor_AzureMonitorLinuxAgent_useast2euap_manifest.xml"], "state": "enabled", "autoUpgrade": true, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "settings": [ { "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/Microsoft.Azure.Monitor.AzureMonitorLinuxAgent==", "publicSettings": "{\"GCS_AUTO_CONFIG\":true}" } ] }, { "name": "Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent", "version": "2.15.112", "location": "https://zrdfepirv2cbn04prdstr01a.blob.core.windows.net/4ef06ad957494df49c807a5334f2b5d2/Microsoft.Azure.Security.Monitoring_AzureSecurityLinuxAgent_useast2euap_manifest.xml", "failoverlocation": "https://zrdfepirv2cbz06prdstr01a.blob.core.windows.net/4ef06ad957494df49c807a5334f2b5d2/Microsoft.Azure.Security.Monitoring_AzureSecurityLinuxAgent_useast2euap_manifest.xml", "additionalLocations": ["https://zrdfepirv2cbn06prdstr01a.blob.core.windows.net/4ef06ad957494df49c807a5334f2b5d2/Microsoft.Azure.Security.Monitoring_AzureSecurityLinuxAgent_useast2euap_manifest.xml"], "state": "enabled", "autoUpgrade": true, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "settings": [ { "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent==", "publicSettings": "{\"enableGenevaUpload\":true}" } ] }, { "name": "Microsoft.Azure.Extensions.CustomScript", "version": "2.1.6", "location": "https://umsavwggj2v40kvqhc0w.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml", "failoverlocation": "https://umsafwzhkbm1rfrhl0ws.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml", "additionalLocations": [ "https://umsanh4b5rfz0q0p4pwm.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml" ], "state": "enabled", "autoUpgrade": true, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "isMultiConfig": false, "settings": [ { "publicSettings": "{\"commandToExecute\":\"echo 'cee174d4-4daa-4b07-9958-53b9649445c2'\"}" } ], "dependsOn": [ { "DependsOnExtension": [ { "handler": "Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent" } ], "dependencyLevel": 1 } ] }, { "name": "Microsoft.CPlat.Core.RunCommandHandlerLinux", "version": "1.2.0", "location": "https://umsavbvncrpzbnxmxzmr.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml", "failoverlocation": "https://umsajbjtqrb3zqjvgb2z.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml", "additionalLocations": [ "https://umsawqtlsshtn5v2nfgh.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml" ], "state": "enabled", "autoUpgrade": true, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "isMultiConfig": true, "settings": [ { "publicSettings": "{\"source\":{\"script\":\"echo '4abb1e88-f349-41f8-8442-247d9fdfcac5'\"}}", "seqNo": 0, "extensionName": "MCExt1", "extensionState": "enabled" }, { "publicSettings": "{\"source\":{\"script\":\"echo 'e865c9bc-a7b3-42c6-9a79-cfa98a1ee8b3'\"}}", "seqNo": 0, "extensionName": "MCExt2", "extensionState": "enabled" }, { "publicSettings": "{\"source\":{\"script\":\"echo 'f923e416-0340-485c-9243-8b84fb9930c6'\"}}", "seqNo": 0, "extensionName": "MCExt3", "extensionState": "enabled" } ], "dependsOn": [ { "dependsOnExtension": [ { "extension": "...", "handler": "..." }, { "extension": "...", "handler": "..." } ], "dependencyLevel": 2, "name": "MCExt1" }, { "dependsOnExtension": [ { "extension": "...", "handler": "..." } ], "dependencyLevel": 1, "name": "MCExt2" } ] }, { "name": "Microsoft.OSTCExtensions.VMAccessForLinux", "version": "1.5.11", "location": "https://umsasc25p0kjg0c1dg4b.blob.core.windows.net/2bbece4f-0283-d415-b034-cc0adc6997a1/2bbece4f-0283-d415-b034-cc0adc6997a1_manifest.xml", "failoverlocation": "https://umsamfwlmfshvxx2lsjm.blob.core.windows.net/2bbece4f-0283-d415-b034-cc0adc6997a1/2bbece4f-0283-d415-b034-cc0adc6997a1_manifest.xml", "additionalLocations": [ "https://umsah3cwjlctnmhsvzqv.blob.core.windows.net/2bbece4f-0283-d415-b034-cc0adc6997a1/2bbece4f-0283-d415-b034-cc0adc6997a1_manifest.xml" ], "state": "enabled", "autoUpgrade": false, "runAsStartupTask": false, "isJson": true, "useExactVersion": true, "settingsSeqNo": 0, "isMultiConfig": false, "settings": [ { "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpddesZQewdDBgegkxNzA1BgoJkgergres/Microsoft.OSTCExtensions.VMAccessForLinux==" } ] } ] } WALinuxAgent-2.9.1.1/tests/data/imds/000077500000000000000000000000001446033677600172175ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/imds/unicode.json000066400000000000000000000020031446033677600215330ustar00rootroot00000000000000{ "compute": { "location": "wéstus", "name": "héalth", "offer": "UbuntuSérvér", "osType": "Linux", "placementGroupId": "", "platformFaultDomain": "0", "platformUpdateDomain": "0", "publisher": "Canonical", "resourceGroupName": "tésts", "sku": "16.04-LTS", "subscriptionId": "21b2dc34-bcé6-4é63-9449-d2a8d1c2339é", "tags": "", "version": "16.04.201805220", "vmId": "é7fdbfc4-2déb-4a4é-8615-éa6aaf50162é", "vmScaleSetName": "", "vmSize": "Standard_D2_V2", "zone": "" }, "network": { "interface": [ { "ipv4": { "ipAddress": [ { "privateIpAddress": "10.0.1.4", "publicIpAddress": "40.112.128.120" } ], "subnet": [ { "address": "10.0.1.0", "prefix": "24" } ] }, "ipv6": { "ipAddress": [] }, "macAddress": "000D3A3382E8" } ] } } WALinuxAgent-2.9.1.1/tests/data/imds/valid.json000066400000000000000000000017661446033677600212230ustar00rootroot00000000000000{ "compute": { "location": "westus", "name": "health", "offer": "UbuntuServer", "osType": "Linux", "placementGroupId": "", "platformFaultDomain": "0", "platformUpdateDomain": "0", "publisher": "Canonical", "resourceGroupName": "tests", "sku": "16.04-LTS", "subscriptionId": "21b2dc34-bce6-4e63-9449-d2a8d1c2339e", "tags": "", "version": "16.04.201805220", "vmId": "e7fdbfc4-2deb-4a4e-8615-ea6aaf50162e", "vmScaleSetName": "", "vmSize": "Standard_D2_V2", "zone": "" }, "network": { "interface": [ { "ipv4": { "ipAddress": [ { "privateIpAddress": "10.0.1.4", "publicIpAddress": "40.112.128.120" } ], "subnet": [ { "address": "10.0.1.0", "prefix": "24" } ] }, "ipv6": { "ipAddress": [] }, "macAddress": "000D3A3382E8" } ] } } WALinuxAgent-2.9.1.1/tests/data/init/000077500000000000000000000000001446033677600172265ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/init/azure-vmextensions.slice000066400000000000000000000001671446033677600241410ustar00rootroot00000000000000[Unit] Description=Slice for Azure VM Extensions DefaultDependencies=no Before=slices.target [Slice] CPUAccounting=yes WALinuxAgent-2.9.1.1/tests/data/init/azure-walinuxagent-logcollector.slice000066400000000000000000000002711446033677600265670ustar00rootroot00000000000000[Unit] Description=Slice for Azure VM Agent Periodic Log Collector DefaultDependencies=no Before=slices.target [Slice] CPUAccounting=yes CPUQuota=5% MemoryAccounting=yes MemoryLimit=30MWALinuxAgent-2.9.1.1/tests/data/init/azure.slice000066400000000000000000000001471446033677600213770ustar00rootroot00000000000000[Unit] Description=Slice for Azure VM Agent and Extensions DefaultDependencies=no Before=slices.target WALinuxAgent-2.9.1.1/tests/data/init/walinuxagent.service000066400000000000000000000007721446033677600233240ustar00rootroot00000000000000# # NOTE: This is the service file used in current versions of the agent (>= 2.2.55) # [Unit] Description=Azure Linux Agent After=network-online.target cloud-init.service Wants=network-online.target sshd.service sshd-keygen.service ConditionFileIsExecutable=/usr/sbin/waagent ConditionPathExists=/etc/waagent.conf [Service] Type=simple ExecStart=/usr/bin/python3 -u /usr/sbin/waagent -daemon Restart=always Slice=azure.slice CPUAccounting=yes MemoryAccounting=yes [Install] WantedBy=multi-user.target WALinuxAgent-2.9.1.1/tests/data/init/walinuxagent.service.previous000066400000000000000000000006771446033677600252030ustar00rootroot00000000000000# # NOTE: This is the service file used in older versions of the agent (<= 2.2.54) # [Unit] Description=Azure Linux Agent After=network-online.target cloud-init.service Wants=network-online.target sshd.service sshd-keygen.service ConditionFileIsExecutable=/usr/sbin/waagent ConditionPathExists=/etc/waagent.conf [Service] Type=simple ExecStart=/usr/bin/python3 -u /usr/sbin/waagent -daemon Restart=always [Install] WantedBy=multi-user.target WALinuxAgent-2.9.1.1/tests/data/init/walinuxagent.service_system-slice000066400000000000000000000010251446033677600260150ustar00rootroot00000000000000# # NOTE: # This file hosted on WALinuxAgent repository only for reference purposes. # Please refer to a recent image to find out the up-to-date systemd unit file. # [Unit] Description=Azure Linux Agent After=network-online.target cloud-init.service Wants=network-online.target sshd.service sshd-keygen.service ConditionFileIsExecutable=/usr/sbin/waagent ConditionPathExists=/etc/waagent.conf [Service] Type=simple ExecStart=/usr/bin/python3 -u /usr/sbin/waagent -daemon Restart=always [Install] WantedBy=multi-user.target WALinuxAgent-2.9.1.1/tests/data/metadata/000077500000000000000000000000001446033677600200435ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/metadata/certificates.json000066400000000000000000000002051446033677600234000ustar00rootroot00000000000000{ "certificates":[{ "name":"foo", "thumbprint":"bar", "certificateDataUri":"certificates_data" }] } WALinuxAgent-2.9.1.1/tests/data/metadata/certificates_data.json000066400000000000000000000112531446033677600243760ustar00rootroot00000000000000{"certificateData":"MIINswYJKoZIhvcNAQcDoIINpDCCDaACAQIxggEwMIIBLAIBAoAUvyL+x6GkZXog QNfsXRZAdD9lc7IwDQYJKoZIhvcNAQEBBQAEggEArhMPepD/RqwdPcHEVqvrdZid 72vXrOCuacRBhwlCGrNlg8oI+vbqmT6CSv6thDpet31ALUzsI4uQHq1EVfV1+pXy NlYD1CKhBCoJxs2fSPU4rc8fv0qs5JAjnbtW7lhnrqFrXYcyBYjpURKfa9qMYBmj NdijN+1T4E5qjxPr7zK5Dalp7Cgp9P2diH4Nax2nixotfek3MrEFBaiiegDd+7tE ux685GWYPqB5Fn4OsDkkYOdb0OE2qzLRrnlCIiBCt8VubWH3kMEmSCxBwSJupmQ8 sxCWk+sBPQ9gJSt2sIqfx/61F8Lpu6WzP+ZOnMLTUn2wLU/d1FN85HXmnQALzTCC DGUGCSqGSIb3DQEHATAUBggqhkiG9w0DBwQIbEcBfddWPv+AggxAAOAt/kCXiffe GeJG0P2K9Q18XZS6Rz7Xcz+Kp2PVgqHKRpPjjmB2ufsRO0pM4z/qkHTOdpfacB4h gz912D9U04hC8mt0fqGNTvRNAFVFLsmo7KXc/a8vfZNrGWEnYn7y1WfP52pqA/Ei SNFf0NVtMyqg5Gx+hZ/NpWAE5vcmRRdoYyWeg13lhlW96QUxf/W7vY/D5KpAGACI ok79/XI4eJkbq3Dps0oO/difNcvdkE74EU/GPuL68yR0CdzzafbLxzV+B43TBRgP jH1hCdRqaspjAaZL5LGfp1QUM8HZIKHuTze/+4dWzS1XR3/ix9q/2QFI7YCuXpuE un3AFYXE4QX/6kcPklZwh9FqjSie3I5HtC1vczqYVjqT4oHrs8ktkZ7oAzeXaXTF k6+JQNNa/IyJw24I1MR77q7HlHSSfhXX5cFjVCd/+SiA4HJQjJgeIuXZ+dXmSPdL 9xLbDbtppifFyNaXdlSzcsvepKy0WLF49RmbL7Bnd46ce/gdQ6Midwi2MTnUtapu tHmu/iJtaUpwXXC0B93PHfAk7Y3SgeY4tl/gKzn9/x5SPAcHiNRtOsNBU8ZThzos Wh41xMLZavmX8Yfm/XWtl4eU6xfhcRAbJQx7E1ymGEt7xGqyPV7hjqhoB9i3oR5N itxHgf1+jw/cr7hob+Trd1hFqZO6ePMyWpqUg97G2ThJvWx6cv+KRtTlVA6/r/UH gRGBArJKBlLpXO6dAHFztT3Y6DFThrus4RItcfA8rltfQcRm8d0nPb4lCa5kRbCx iudq3djWtTIe64sfk8jsc6ahWYSovM+NmhbpxEUbZVWLVEcHAYOeMbKgXSu5sxNO JZNeFdzZqDRRY9fGjYNS7DdNOmrMmWKH+KXuMCItpNZsZS/3W7QxAo3ugYLdUylU Zg8H/BjUGZCGn1rEBAuQX78m0SZ1xHlgHSwJIOmxOJUDHLPHtThfbELY9ec14yi5 so1aQwhhfhPvF+xuXBrVeTAfhFNYkf2uxcEp7+tgFAc5W0QfT9SBn5vSvIxv+dT4 7B2Pg1l/zjdsM74g58lmRJeDoz4psAq+Uk7n3ImBhIku9qX632Q1hanjC8D4xM4W sI/W0ADCuAbY7LmwMpAMdrGg//SJUnBftlom7C9VA3EVf8Eo+OZH9hze+gIgUq+E iEUL5M4vOHK2ttsYrSkAt8MZzjQiTlDr1yzcg8fDIrqEAi5arjTPz0n2s0NFptNW lRD+Xz6pCXrnRgR8YSWpxvq3EWSJbZkSEk/eOmah22sFnnBZpDqn9+UArAznXrRi nYK9w38aMGPKM39ymG8kcbY7jmDZlRgGs2ab0Fdj1jl3CRo5IUatkOJwCEMd/tkB eXLQ8hspJhpFnVNReX0oithVZir+j36epk9Yn8d1l+YlKmuynjunKl9fhmoq5Q6i DFzdYpqBV+x9nVhnmPfGyrOkXvGL0X6vmXAEif/4JoOW4IZpyXjgn+VoCJUoae5J Djl45Bcc2Phrn4HW4Gg/+pIwTFqqZZ2jFrznNdgeIxTGjBrVsyJUeO3BHI0mVLaq jtjhTshYCI7mXOis9W3ic0RwE8rgdDXOYKHhLVw9c4094P/43utSVXE7UzbEhhLE Ngb4H5UGrQmPTNbq40tMUMUCej3zIKuVOvamzeE0IwLhkjNrvKhCG1EUhX4uoJKu DQ++3KVIVeYSv3+78Jfw9F3usAXxX1ICU74/La5DUNjU7DVodLDvCAy5y1jxP3Ic If6m7aBYVjFSQAcD8PZPeIEl9W4ZnbwyBfSDd11P2a8JcZ7N99GiiH3yS1QgJnAO g9XAgjT4Gcn7k4lHPHLULgijfiDSvt94Ga4/hse0F0akeZslVN/bygyib7x7Lzmq JkepRianrvKHbatuxvcajt/d+dxCnr32Q1qCEc5fcgDsjvviRL2tKR0qhuYjn1zR Vk/fRtYOmlaGBVzUXcjLRAg3gC9+Gy8KvXIDrnHxD+9Ob+DUP9fgbKqMeOzKcCK8 NSfSQ+tQjBYD5Ku4zAPUQJoRGgx43vXzcl2Z2i3E2otpoH82Kx8S9WlVEUlTtBjQ QIGM5aR0QUNt8z34t2KWRA8SpP54VzBmEPdwLnzna+PkrGKsKiHVn4K+HfjDp1uW xyO8VjrolAOYosTPXMpNp2u/FoFxaAPTa/TvmKc0kQ3ED9/sGLS2twDnEccvHP+9 zzrnzzN3T2CWuXveDpuyuAty3EoAid1nuC86WakSaAZoa8H2QoRgsrkkBCq+K/yl 4FO9wuP+ksZoVq3mEDQ9qv6H4JJEWurfkws3OqrA5gENcLmSUkZie4oqAxeOD4Hh Zx4ckG5egQYr0PnOd2r7ZbIizv3MKT4RBrfOzrE6cvm9bJEzNWXdDyIxZ/kuoLA6 zX7gGLdGhg7dqzKqnGtopLAsyM1b/utRtWxOTGO9K9lRxyX82oCVT9Yw0DwwA+cH Gutg1w7JHrIAYEtY0ezHgxhqMGuuTyJMX9Vr0D+9DdMeBK7hVOeSnxkaQ0f9HvF6 0XI/2OTIoBSCBpUXjpgsYt7m7n2rFJGJmtqgLAosCAkacHnHLwX0EnzBw3sdDU6Q jFXUWIDd5xUsNkFDCbspLMFs22hjNI6f/GREwd23Q4ujF8pUIcxcfbs2myjbK45s tsn/jrkxmKRgwCIeN/H7CM+4GXSkEGLWbiGCxWzWt9wW1F4M7NW9nho3D1Pi2LBL 1ByTmjfo/9u9haWrp53enDLJJbcaslfe+zvo3J70Nnzu3m3oJ3dmUxgJIstG10g3 lhpUm1ynvx04IFkYJ3kr/QHG/xGS+yh/pMZlwcUSpjEgYFmjFHU4A1Ng4LGI4lnw 5wisay4J884xmDgGfK0sdVQyW5rExIg63yYXp2GskRdDdwvWlFUzPzGgCNXQU96A ljZfjs2u4IiVCC3uVsNbGqCeSdAl9HC5xKuPNbw5yTxPkeRL1ouSdkBy7rvdFaFf dMPw6sBRNW8ZFInlgOncR3+xT/rZxru87LCq+3hRN3kw3hvFldrW2QzZSksO759b pJEP+4fxuG96Wq25fRmzHzE0bdJ+2qF3fp/hy4oRi+eVPa0vHdtkymE4OUFWftb6 +P++JVOzZ4ZxYA8zyUoJb0YCaxL+Jp/QqiUiH8WZVmYZmswqR48sUUKr7TIvpNbY 6jEH6F7KiZCoWfKH12tUC69iRYx3UT/4Bmsgi3S4yUxfieYRMIwihtpP4i0O+OjB /DPbb13qj8ZSfXJ+jmF2SRFfFG+2T7NJqm09JvT9UcslVd+vpUySNe9UAlpcvNGZ 2+j180ZU7YAgpwdVwdvqiJxkeVtAsIeqAvIXMFm1PDe7FJB0BiSVZdihB6cjnKBI dv7Lc1tI2sQe7QSfk+gtionLrEnto+aXF5uVM5LMKi3gLElz7oXEIhn54OeEciB1 cEmyX3Kb4HMRDMHyJxqJXwxm88RgC6RekoPvstu+AfX/NgSpRj5beaj9XkweJT3H rKWhkjq4Ghsn1LoodxluMMHd61m47JyoqIP9PBKoW+Na0VUKIVHw9e9YeW0nY1Zi 5qFA/pHPAt9AbEilRay6NEm8P7TTlNo216amc8byPXanoNrqBYZQHhZ93A4yl6jy RdpYskMivT+Sh1nhZAioKqqTZ3HiFR8hFGspAt5gJc4WLYevmxSicGa6AMyhrkvG rvOSdjY6JY/NkxtcgeycBX5MLF7uDbhUeqittvmlcrVN6+V+2HIbCCrvtow9pcX9 EkaaNttj5M0RzjQxogCG+S5TkhCy04YvKIkaGJFi8xO3icdlxgOrKD8lhtbf4UpR cDuytl70JD95mSUWL53UYjeRf9OsLRJMHQOpS02japkMwCb/ngMCQuUXA8hGkBZL Xw7RwwPuM1Lx8edMXn5C0E8UK5e0QmI/dVIl2aglXk2oBMBJbnyrbfUPm462SG6u ke4gQKFmVy2rKICqSkh2DMr0NzeYEUjZ6KbmQcV7sKiFxQ0/ROk8eqkYYxGWUWJv ylPF1OTLH0AIbGlFPLQO4lMPh05yznZTac4tmowADSHY9RCxad1BjBeine2pj48D u36OnnuQIsedxt5YC+h1bs+mIvwMVsnMLidse38M/RayCDitEBvL0KeG3vWYzaAL h0FCZGOW0ilVk8tTF5+XWtsQEp1PpclvkcBMkU3DtBUnlmPSKNfJT0iRr2T0sVW1 h+249Wj0Bw=="}WALinuxAgent-2.9.1.1/tests/data/metadata/ext_handler_pkgs.json000066400000000000000000000002701446033677600242560ustar00rootroot00000000000000{ "versions": [{ "version":"1.3.0.0", "uris":[{ "uri":"http://localhost/foo1" },{ "uri":"http://localhost/foo2" }] }] } WALinuxAgent-2.9.1.1/tests/data/metadata/ext_handlers.json000066400000000000000000000006611446033677600234210ustar00rootroot00000000000000[{ "name":"foo", "properties":{ "version":"1.3.0.0", "upgradePolicy": "manual", "state": "enabled", "extensions":[{ "name":"baz", "sequenceNumber":0, "publicSettings":{ "commandToExecute": "echo 123", "uris":[] } }] }, "versionUris":[{ "uri":"http://ext_handler_pkgs/versionUri" }] }] WALinuxAgent-2.9.1.1/tests/data/metadata/ext_handlers_no_ext.json000066400000000000000000000000031446033677600247630ustar00rootroot00000000000000[] WALinuxAgent-2.9.1.1/tests/data/metadata/identity.json000066400000000000000000000000641446033677600225670ustar00rootroot00000000000000{ "vmName":"foo", "subscriptionId":"bar" } WALinuxAgent-2.9.1.1/tests/data/metadata/trans_cert000066400000000000000000000021271446033677600221340ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDBzCCAe+gAwIBAgIJANujJuVt5eC8MA0GCSqGSIb3DQEBCwUAMBkxFzAVBgNV BAMMDkxpbnV4VHJhbnNwb3J0MCAXDTE0MTAyNDA3MjgwN1oYDzIxMDQwNzEyMDcy ODA3WjAZMRcwFQYDVQQDDA5MaW51eFRyYW5zcG9ydDCCASIwDQYJKoZIhvcNAQEB BQADggEPADCCAQoCggEBANPcJAkd6V5NeogSKjIeTXOWC5xzKTyuJPt4YZMVSosU 0lI6a0wHp+g2fP22zrVswW+QJz6AVWojIEqLQup3WyCXZTv8RUblHnIjkvX/+J/G aLmz0G5JzZIpELL2C8IfQLH2IiPlK9LOQH00W74WFcK3QqcJ6Kw8GcVaeSXT1r7X QcGMqEjcWJkpKLoMJv3LMufE+JMdbXDUGY+Ps7Zicu8KXvBPaKVsc6H2jrqBS8et jXbzLyrezTUDz45rmyRJzCO5Sk2pohuYg73wUykAUPVxd7L8WnSyqz1v4zrObqnw BAyor67JR/hjTBfjFOvd8qFGonfiv2Vnz9XsYFTZsXECAwEAAaNQME4wHQYDVR0O BBYEFL8i/sehpGV6IEDX7F0WQHQ/ZXOyMB8GA1UdIwQYMBaAFL8i/sehpGV6IEDX 7F0WQHQ/ZXOyMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAMPLrimT Gptu5pLRHPT8OFRN+skNSkepYaUaJuq6cSKxLumSYkD8++rohu+1+a7t1YNjjNSJ 8ohRAynRJ7aRqwBmyX2OPLRpOfyRZwR0rcFfAMORm/jOE6WBdqgYD2L2b+tZplGt /QqgQzebaekXh/032FK4c74Zg5r3R3tfNSUMG6nLauWzYHbQ5SCdkuQwV0ehGqh5 VF1AOdmz4CC2237BNznDFQhkeU0LrqqAoE/hv5ih7klJKZdS88rOYEnVJsFFJb0g qaycXjOm5Khgl4hKrd+DBD/qj4IVVzsmdpFli72k6WLBHGOXusUGo/3isci2iAIt DsfY6XGSEIhZnA4= -----END CERTIFICATE----- WALinuxAgent-2.9.1.1/tests/data/metadata/trans_prv000066400000000000000000000032501446033677600220040ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDT3CQJHeleTXqI EioyHk1zlguccyk8riT7eGGTFUqLFNJSOmtMB6foNnz9ts61bMFvkCc+gFVqIyBK i0Lqd1sgl2U7/EVG5R5yI5L1//ifxmi5s9BuSc2SKRCy9gvCH0Cx9iIj5SvSzkB9 NFu+FhXCt0KnCeisPBnFWnkl09a+10HBjKhI3FiZKSi6DCb9yzLnxPiTHW1w1BmP j7O2YnLvCl7wT2ilbHOh9o66gUvHrY128y8q3s01A8+Oa5skScwjuUpNqaIbmIO9 8FMpAFD1cXey/Fp0sqs9b+M6zm6p8AQMqK+uyUf4Y0wX4xTr3fKhRqJ34r9lZ8/V 7GBU2bFxAgMBAAECggEBAM4hsfog3VAAyIieS+npq+gbhH6bWfMNaTQ3g5CNNbMu 9hhFeOJHzKnWYjSlamgBQhAfTN+2E+Up+iAtcVUZ/lMumrQLlwgMo1vgmvu5Kxmh /YE5oEG+k0JzrCjD1trwd4zvc3ZDYyk/vmVTzTOc311N248UyArUiyqHBbq1a4rP tJhCLn2c4S7flXGF0MDVGZyV9V7J8N8leq/dRGMB027Li21T+B4mPHXa6b8tpRPL 4vc8sHoUJDa2/+mFDJ2XbZfmlgd3MmIPlRn1VWoW7mxgT/AObsPl7LuQx7+t80Wx hIMjuKUHRACQSLwHxJ3SQRFWp4xbztnXSRXYuHTscLUCgYEA//Uu0qIm/FgC45yG nXtoax4+7UXhxrsWDEkbtL6RQ0TSTiwaaI6RSQcjrKDVSo/xo4ZySTYcRgp5GKlI CrWyNM+UnIzTNbZOtvSIAfjxYxMsq1vwpTlOB5/g+cMukeGg39yUlrjVNoFpv4i6 9t4yYuEaF4Vww0FDd2nNKhhW648CgYEA0+UYH6TKu03zDXqFpwf4DP2VoSo8OgfQ eN93lpFNyjrfzvxDZkGF+7M/ebyYuI6hFplVMu6BpgpFP7UVJpW0Hn/sXkTq7F1Q rTJTtkTp2+uxQVP/PzSOqK0Twi5ifkfoEOkPkNNtTiXzwCW6Qmmcvln2u893pyR5 gqo5BHR7Ev8CgYAb7bXpN9ZHLJdMHLU3k9Kl9YvqOfjTxXA3cPa79xtEmsrTys4q 4HuL22KSII6Fb0VvkWkBAg19uwDRpw78VC0YxBm0J02Yi8b1AaOhi3dTVzFFlWeh r6oK/PAAcMKxGkyCgMAZ3hstsltGkfXMoBwhW+yL6nyOYZ2p9vpzAGrjkwKBgQDF 0huzbyXVt/AxpTEhv07U0enfjI6tnp4COp5q8zyskEph8yD5VjK/yZh5DpmFs6Kw dnYUFpbzbKM51tToMNr3nnYNjEnGYVfwWgvNHok1x9S0KLcjSu3ki7DmmGdbfcYq A2uEyd5CFyx5Nr+tQOwUyeiPbiFG6caHNmQExLoiAQKBgFPy9H8///xsadYmZ18k r77R2CvU7ArxlLfp9dr19aGYKvHvnpsY6EuChkWfy8Xjqn3ogzgrHz/rn3mlGUpK vbtwtsknAHtTbotXJwfaBZv2RGgGRr3DzNo6ll2Aez0lNblZFXq132h7+y5iLvar 4euORaD/fuM4UPlR5mN+bypU -----END PRIVATE KEY----- WALinuxAgent-2.9.1.1/tests/data/metadata/vmagent_manifest1.json000066400000000000000000000006541446033677600243530ustar00rootroot00000000000000{ "versions": [ { "version": "2.2.8", "uris": [ { "uri": "https: //notused.com/ga/WALinuxAgent-2.2.8.zip" } ] }, { "version": "2.2.9", "uris": [ { "uri": "https: //notused.com/ga/WALinuxAgent-2.2.9.zip" } ] } ] }WALinuxAgent-2.9.1.1/tests/data/metadata/vmagent_manifest2.json000066400000000000000000000006541446033677600243540ustar00rootroot00000000000000{ "versions": [ { "version": "2.2.8", "uris": [ { "uri": "https: //notused.com/ga/WALinuxAgent-2.2.8.zip" } ] }, { "version": "2.2.9", "uris": [ { "uri": "https: //notused.com/ga/WALinuxAgent-2.2.9.zip" } ] } ] }WALinuxAgent-2.9.1.1/tests/data/metadata/vmagent_manifests.json000066400000000000000000000002601446033677600244460ustar00rootroot00000000000000{ "versionsManifestUris" : [ { "uri" : "https://notused.com/vmagent_manifest1.json" }, { "uri" : "https://notused.com/vmagent_manifest2.json" } ] } WALinuxAgent-2.9.1.1/tests/data/metadata/vmagent_manifests_invalid1.json000066400000000000000000000003121446033677600262330ustar00rootroot00000000000000{ "notTheRightKey": [ { "uri": "https://notused.com/vmagent_manifest1.json" }, { "uri": "https://notused.com/vmagent_manifest2.json" } ] }WALinuxAgent-2.9.1.1/tests/data/metadata/vmagent_manifests_invalid2.json000066400000000000000000000003121446033677600262340ustar00rootroot00000000000000{ "notTheRightKey": [ { "foo": "https://notused.com/vmagent_manifest1.json" }, { "bar": "https://notused.com/vmagent_manifest2.json" } ] }WALinuxAgent-2.9.1.1/tests/data/ovf-env-2.xml000066400000000000000000000037141446033677600205310ustar00rootroot00000000000000 1.0 LinuxProvisioningConfiguration HostName UserName UserPassword false EB0C0AB4B2D5FC35F2F0658D19F44C8283E2DD62 $HOME/UserName/.ssh/authorized_keys ssh-rsa AAAANOTAREALKEY== foo@bar.local EB0C0AB4B2D5FC35F2F0658D19F44C8283E2DD62 $HOME/UserName/.ssh/id_rsa CustomData 1.0 kms.core.windows.net true true true false WALinuxAgent-2.9.1.1/tests/data/ovf-env-3.xml000066400000000000000000000037101446033677600205260ustar00rootroot00000000000000 1.0 LinuxProvisioningConfiguration HostName UserName UserPassword false EB0C0AB4B2D5FC35F2F0658D19F44C8283E2DD62 $HOME/UserName/.ssh/authorized_keys ssh-rsa AAAANOTAREALKEY== foo@bar.local EB0C0AB4B2D5FC35F2F0658D19F44C8283E2DD62 $HOME/UserName/.ssh/id_rsa CustomData 1.0 kms.core.windows.net true true false WALinuxAgent-2.9.1.1/tests/data/ovf-env-4.xml000066400000000000000000000037201446033677600205300ustar00rootroot00000000000000 1.0 LinuxProvisioningConfiguration HostName UserName UserPassword false EB0C0AB4B2D5FC35F2F0658D19F44C8283E2DD62 $HOME/UserName/.ssh/authorized_keys ssh-rsa AAAANOTAREALKEY== foo@bar.local EB0C0AB4B2D5FC35F2F0658D19F44C8283E2DD62 $HOME/UserName/.ssh/id_rsa CustomData 1.0 kms.core.windows.net bad data true true false WALinuxAgent-2.9.1.1/tests/data/ovf-env.xml000066400000000000000000000037151446033677600203730ustar00rootroot00000000000000 1.0 LinuxProvisioningConfiguration HostName UserName UserPassword false EB0C0AB4B2D5FC35F2F0658D19F44C8283E2DD62 $HOME/UserName/.ssh/authorized_keys ssh-rsa AAAANOTAREALKEY== foo@bar.local EB0C0AB4B2D5FC35F2F0658D19F44C8283E2DD62 $HOME/UserName/.ssh/id_rsa CustomData 1.0 kms.core.windows.net false true true false WALinuxAgent-2.9.1.1/tests/data/safe_deploy.json000066400000000000000000000010161446033677600214460ustar00rootroot00000000000000{ "blacklisted" : [ "^1.2.3$", "^1.3(?:\\.\\d+)*$" ], "families" : { "ubuntu-x64": { "versions": [ "^Ubuntu,(1[4-9]|2[0-9])\\.\\d+,.*$" ], "require_64bit": true, "partition": 85 }, "fedora-x64": { "versions": [ "^Oracle[^,]*,([7-9]|[1-9][0-9])\\.\\d+,.*$", "^Red\\sHat[^,]*,([7-9]|[1-9][0-9])\\.\\d+,.*$" ], "partition": 20 } } }WALinuxAgent-2.9.1.1/tests/data/test_waagent.conf000066400000000000000000000066711446033677600216310ustar00rootroot00000000000000# # Microsoft Azure Linux Agent Configuration # # Key / value handling test entries =Value0 FauxKey1= Value1 FauxKey2=Value2 Value2 FauxKey3=delalloc,rw,noatime,nobarrier,users,mode=777 # Enable extension handling Extensions.Enabled=y # Specify provisioning agent. Provisioning.Agent=auto # Password authentication for root account will be unavailable. Provisioning.DeleteRootPassword=y # Generate fresh host key pair. Provisioning.RegenerateSshHostKeyPair=y # Supported values are "rsa", "dsa", "ecdsa", "ed25519", and "auto". # The "auto" option is supported on OpenSSH 5.9 (2011) and later. Provisioning.SshHostKeyPairType=rsa # An EOL comment that should be ignored # Monitor host name changes and publish changes via DHCP requests. Provisioning.MonitorHostName=y # Decode CustomData from Base64. Provisioning.DecodeCustomData=n#Another EOL comment that should be ignored # Execute CustomData after provisioning. Provisioning.ExecuteCustomData=n # Algorithm used by crypt when generating password hash. #Provisioning.PasswordCryptId=6 # Length of random salt used when generating password hash. #Provisioning.PasswordCryptSaltLength=10 # Allow reset password of sys user Provisioning.AllowResetSysUser=n # Format if unformatted. If 'n', resource disk will not be mounted. ResourceDisk.Format=y # File system on the resource disk # Typically ext3 or ext4. FreeBSD images should use 'ufs2' here. ResourceDisk.Filesystem=ext4 # Mount point for the resource disk ResourceDisk.MountPoint=/mnt/resource # Create and use swapfile on resource disk. ResourceDisk.EnableSwap=n # Use encrypted swap ResourceDisk.EnableSwapEncryption=n # Size of the swapfile. ResourceDisk.SwapSizeMB=0 # Comma-seperated list of mount options. See man(8) for valid options. ResourceDisk.MountOptions=None # Enable verbose logging (y|n) Logs.Verbose=n # Enable periodic log collection, default is y Logs.Collect=y # How frequently to collect logs, default is each hour Logs.CollectPeriod=3600 # Is FIPS enabled OS.EnableFIPS=y#Another EOL comment that should be ignored # Root device timeout in seconds. OS.RootDeviceScsiTimeout=300 # If "None", the system default version is used. OS.OpensslPath=None # Set the SSH ClientAliveInterval OS.SshClientAliveInterval=42#Yet another EOL comment with a '#' that should be ignored # Set the path to SSH keys and configuration files OS.SshDir=/notareal/path # If set, agent will use proxy server to access internet #HttpProxy.Host=None #HttpProxy.Port=None # Detect Scvmm environment, default is n # DetectScvmmEnv=n # # Lib.Dir=/var/lib/waagent # # DVD.MountPoint=/mnt/cdrom/secure # # Pid.File=/var/run/waagent.pid # # Extension.LogDir=/var/log/azure # # OS.HomeDir=/home # Enable RDMA management and set up, should only be used in HPC images # OS.EnableRDMA=n # OS.UpdateRdmaDriver=n # OS.CheckRdmaDriver=n # Enable or disable goal state processing auto-update, default is enabled # AutoUpdate.Enabled=y # Determine the update family, this should not be changed # AutoUpdate.GAFamily=Prod # Determine if the overprovisioning feature is enabled. If yes, hold extension # handling until inVMArtifactsProfile.OnHold is false. # Default is enabled # EnableOverProvisioning=y # Allow fallback to HTTP if HTTPS is unavailable # Note: Allowing HTTP (vs. HTTPS) may cause security risks # OS.AllowHTTP=n # Add firewall rules to protect access to Azure host node services # Note: # - The default is false to protect the state of existing VMs OS.EnableFirewall=n WALinuxAgent-2.9.1.1/tests/data/wire/000077500000000000000000000000001446033677600172315ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/wire/certs-2.xml000066400000000000000000000123351446033677600212360ustar00rootroot00000000000000 2012-11-30 5 Pkcs7BlobWithPfxContents MIIOgwYJKoZIhvcNAQcDoIIOdDCCDnACAQIxggEwMIIBLAIBAoAUiF8ZYMs9mMa8 QOEMxDaIhGza+0IwDQYJKoZIhvcNAQEBBQAEggEAQW7GyeRVEhHSU1/dzV0IndH0 rDQk+27MvlsWTcpNcgGFtfRYxu5bzmp0+DoimX3pRBlSFOpMJ34jpg4xs78EsSWH FRhCf3EGuEUBHo6yR8FhXDTuS7kZ0UmquiCI2/r8j8gbaGBNeP8IRizcAYrPMA5S E8l1uCrw7DHuLscbVni/7UglGaTfFS3BqS5jYbiRt2Qh3p+JPUfm51IG3WCIw/WS 2QHebmHxvMFmAp8AiBWSQJizQBEJ1lIfhhBMN4A7NadMWAe6T2DRclvdrQhJX32k amOiogbW4HJsL6Hphn7Frrw3CENOdWMAvgQBvZ3EjAXgsJuhBA1VIrwofzlDljCC DTUGCSqGSIb3DQEHATAUBggqhkiG9w0DBwQIxcvw9qx4y0qAgg0QrINXpC23BWT2 Fb9N8YS3Be9eO3fF8KNdM6qGf0kKR16l/PWyP2L+pZxCcCPk83d070qPdnJK9qpJ 6S1hI80Y0oQnY9VBFrdfkc8fGZHXqm5jNS9G32v/AxYpJJC/qrAQnWuOdLtOZaGL 94GEh3XRagvz1wifv8SRI8B1MzxrpCimeMxHkL3zvJFg9FjLGdrak868feqhr6Nb pqH9zL7bMq8YP788qTRELUnL72aDzGAM7HEj7V4yu2uD3i3Ryz3bqWaj9IF38Sa0 6rACBkiNfZBPgExoMUm2GNVyx8hTis2XKRgz4NLh29bBkKrArK9sYDncE9ocwrrX AQ99yn03Xv6TH8bRp0cSj4jzBXc5RFsUQG/LxzJVMjvnkDbwNE41DtFiYz5QVcv1 cMpTH16YfzSL34a479eNq/4+JAs/zcb2wjBskJipMUU4hNx5fhthvfKwDOQbLTqN HcP23iPQIhjdUXf6gpu5RGu4JZ0dAMHMHFKvNL6TNejwx/H6KAPp6rCRsYi6QhAb 42SXdZmhAyQsFpGD9U5ieJApqeCHfj9Xhld61GqLJA9+WLVhDPADjqHoAVvrOkKH OtPegId/lWnCB7p551klAjiEA2/DKxFBIAEhqZpiLl+juZfMXovkdmGxMP4gvNNF gbS2k5A0IJ8q51gZcH1F56smdAmi5kvhPnFdy/9gqeI/F11F1SkbPVLImP0mmrFi zQD5JGfEu1psUYvhpOdaYDkmAK5qU5xHSljqZFz5hXNt4ebvSlurHAhunJb2ln3g AJUHwtZnVBrtYMB0w6fdwYqMxXi4vLeqUiHtIQtbOq32zlSryNPQqG9H0iP9l/G1 t7oUfr9woI/B0kduaY9jd5Qtkqs1DoyfNMSaPNohUK/CWOTD51qOadzSvK0hJ+At 033PFfv9ilaX6GmzHdEVEanrn9a+BoBCnGnuysHk/8gdswj9OzeCemyIFJD7iObN rNex3SCf3ucnAejJOA0awaLx88O1XTteUjcFn26EUji6DRK+8JJiN2lXSyQokNeY ox6Z4hFQDmw/Q0k/iJqe9/Dq4zA0l3Krkpra0DZoWh5kzYUA0g5+Yg6GmRNRa8YG tuuD6qK1SBEzmCYff6ivjgsXV5+vFBSjEpx2dPEaKdYxtHMOjkttuTi1mr+19dVf hSltbzfISbV9HafX76dhwZJ0QwsUx+aOW6OrnK8zoQc5AFOXpe9BrrOuEX01qrM0 KX5tS8Zx5HqDLievjir194oi3r+nAiG14kYlGmOTHshu7keGCgJmzJ0iVG/i+TnV ZSLyd8OqV1F6MET1ijgR3OPL3kt81Zy9lATWk/DgKbGBkkKAnXO2HUw9U34JFyEy vEc81qeHci8sT5QKSFHiP3r8EcK8rT5k9CHpnbFmg7VWSMVD0/wRB/C4BiIw357a xyJ/q1NNvOZVAyYzIzf9TjwREtyeHEo5kS6hyWSn7fbFf3sNGO2I30veWOvE6kFA HMtF3NplOrTYcM7fAK5zJCBK20oU645TxI8GsICMog7IFidFMdRn4MaXpwAjEZO4 44m2M+4XyeRCAZhp1Fu4mDiHGqgd44mKtwvLACVF4ygWZnACDpI17X88wMnwL4uU vgehLZdAE89gvukSCsET1inVBnn/hVenCRbbZ++IGv2XoYvRfeezfOoNUcJXyawQ JFqN0CRB5pliuCesTO2urn4HSwGGoeBd507pGWZmOAjbNjGswlJJXF0NFnNW/zWw UFYy+BI9axuhWTSnCXbNbngdNQKHznKe1Lwit6AI3U9jS33pM3W+pwUAQegVdtpG XT01YgiMCBX+b8B/xcWTww0JbeUwKXudzKsPhQmaA0lubAo04JACMfON8jSZCeRV TyIzgacxGU6YbEKH4PhYTGl9srcWIT9iGSYD53V7Kyvjumd0Y3Qc3JLnuWZT6Oe3 uJ4xz9jJtoaTDvPJQNK3igscjZnWZSP8XMJo1/f7vbvD57pPt1Hqdirp1EBQNshk iX9CUh4fuGFFeHf6MtGxPofbXmvA2GYcFsOez4/2eOTEmo6H3P4Hrya97XHS0dmD zFSAjzAlacTrn1uuxtxFTikdOwvdmQJJEfyYWCB1lqWOZi97+7nzqyXMLvMgmwug ZF/xHFMhFTR8Wn7puuwf36JpPQiM4oQ/Lp66zkS4UlKrVsmSXIXudLMg8SQ5WqK8 DjevEZwsHHaMtfDsnCAhAdRc2jCpyHKKnmhCDdkcdJJEymWKILUJI5PJ3XtiMHnR Sa35OOICS0lTq4VwhUdkGwGjRoY1GsriPHd6LOt1aom14yJros1h7ta604hSCn4k zj9p7wY9gfgkXWXNfmarrZ9NNwlHxzgSva+jbJcLmE4GMX5OFHHGlRj/9S1xC2Wf MY9orzlooGM74NtmRi4qNkFj3dQCde8XRR4wh2IvPUCsr4j+XaoCoc3R5Rn/yNJK zIkccJ2K14u9X/A0BLXHn5Gnd0tBYcVOqP6dQlW9UWdJC/Xooh7+CVU5cZIxuF/s Vvg+Xwiv3XqekJRu3cMllJDp5rwe5EWZSmnoAiGKjouKAIszlevaRiD/wT6Zra3c Wn/1U/sGop6zRscHR7pgI99NSogzpVGThUs+ez7otDBIdDbLpMjktahgWoi1Vqhc fNZXjA6ob4zTWY/16Ys0YWxHO+MtyWTMP1dnsqePDfYXGUHe8yGxylbcjfrsVYta 4H6eYR86eU3eXB+MpS/iA4jBq4QYWR9QUkd6FDfmRGgWlMXhisPv6Pfnj384NzEV Emeg7tW8wzWR64EON9iGeGYYa2BBl2FVaayMEoUhthhFcDM1r3/Mox5xF0qnlys4 goWkMzqbzA2t97bC0KDGzkcHT4wMeiJBLDZ7S2J2nDAEhcTLY0P2zvOB4879pEWx Bd15AyG1DvNssA5ooaDzKi/Li6NgDuMJ8W7+tmsBwDvwuf2N3koqBeXfKhR4rTqu Wg1k9fX3+8DzDf0EjtDZJdfWZAynONi1PhZGbNbaMKsQ+6TflkCACInRdOADR5GM rL7JtrgF1a9n0HD9vk2WGZqKI71tfS8zODkOZDD8aAusD2DOSmVZl48HX/t4i4Wc 3dgi/gkCMrfK3wOujb8tL4zjnlVkM7kzKk0MgHuA1w81zFjeMFvigHes4IWhQVcz ek3l4bGifI2kzU7bGIi5e/019ppJzGsVcrOE/3z4GS0DJVk6fy7MEMIFx0LhJPlL T+9HMH85sSYb97PTiMWpfBvNw3FSC7QQT9FC3L8d/XtMY3NvZoc7Fz7cSGaj7NXG 1OgVnAzMunPa3QaduoxMF9346s+4a+FrpRxL/3bb4skojjmmLqP4dsbD1uz0fP9y xSifnTnrtjumYWMVi+pEb5kR0sTHl0XS7qKRi3SEfv28uh72KdvcufonIA5rnEb5 +yqAZiqW2OxVsRoVLVODPswP4VIDiun2kCnfkQygPzxlZUeDZur0mmZ3vwC81C1Q dZcjlukZcqUaxybUloUilqfNeby+2Uig0krLh2+AM4EqR63LeZ/tk+zCitHeRBW0 wl3Bd7ShBFg6kN5tCJlHf/G6suIJVr+A9BXfwekO9+//CutKakCwmJTUiNWbQbtN q3aNCnomyD3WjvUbitVO0CWYjZrmMLIsPtzyLQydpT7tjXpHgvwm5GYWdUGnNs4y NbA262sUl7Ku/GDw1CnFYXbxl+qxbucLtCdSIFR2xUq3rEO1MXlD/txdTxn6ANax hi9oBg8tHzuGYJFiCDCvbVVTHgWUSnm/EqfclpJzGmxt8g7vbaohW7NMmMQrLBFP G6qBypgvotx1iJWaHVLNNiXvyqQwTtelNPAUweRoNawBp/5KTwwy/tHeF0gsVQ7y mFX4umub9YT34Lpe7qUPKNxXzFcUgAf1SA6vyZ20UI7p42S2OT2PrahJ+uO6LQVD +REhtN0oyS3G6HzAmKkBgw7LcV3XmAr39iSR7mdmoHSJuI9bjveAPhniK+N6uuln xf17Qnw5NWfr9MXcLli7zqwMglU/1bNirkwVqf/ogi/zQ3JYCo6tFGf/rnGQAORJ hvOq2SEYXnizPPIH7VrpE16+jUXwgpiQ8TDyeLPmpZVuhXTXiCaJO5lIwmLQqkmg JqNiT9V44sksNFTGNKgZo5O9rEqfqX4dLjfv6pGJL+MFXD9if4f1JQiXJfhcRcDh Ff9B6HukgbJ1H96eLUUNj8sL1+WPOqawkS4wg7tVaERE8CW7mqk15dCysn9shSut I+7JU7+dZsxpj0ownrxuPAFuT8ZlcBPrFzPUwTlW1G0CbuEco8ijfy5IfbyGCn5s K/0bOfAuNVGoOpLZ1dMki2bGdBwQOQlkLKhAxYcCVQ0/urr1Ab+VXU9kBsIU8ssN GogKngYpuUV0PHmpzmobielOHLjNqA2v9vQSV3Ed48wRy5OCwLX1+vYmYlggMDGt wfl+7QbXYf+k5WnELf3IqYvh8ZWexa0= WALinuxAgent-2.9.1.1/tests/data/wire/certs.xml000066400000000000000000000123351446033677600210770ustar00rootroot00000000000000 2012-11-30 3 Pkcs7BlobWithPfxContents MIIOgwYJKoZIhvcNAQcDoIIOdDCCDnACAQIxggEwMIIBLAIBAoAUZcG9X+5aK8VZ FY8eJV9j+RImq58wDQYJKoZIhvcNAQEBBQAEggEAn/hOytP/StyRuXHcqFq6x+Za 7gHfO8prXWdZW4e28NLt/x5ZOBHDDZ6buwwdXEZME0+RoiJvLqP2RNhZkEO8bkna pS76xLZE4NXyfxkeEs1vJYis0WJdt/56uCzBuud2SBLuMWoAWgF5alokN0uFpVgm CKCos+xv6Pisolc6geM8xQTYe6sLf5Z23LWftWfJqzuo/29glCCre7R80OLeZe5w pN6XztbYz06nhVByC35To8Lm0akWAAKU7sfqM1Nty4P0rwUJPKXo42uN1GKYbDbF x8piCAd+rs+q4Alu3qK/YaTPpMb2ECRMH6CYB8Klf/CbuWykkfS8zrsnpXT1kzCC DTUGCSqGSIb3DQEHATAUBggqhkiG9w0DBwQInjJWFaJcZz2Agg0QX6NlJUH17o20 90gfjWV01mPmzLKx71JT+hyzKr5vHywDSRI/mdb3RqA59ZrIKeyWr0HXEOuABlul nxjc/Rfk1tiLQwh0iqlOjlMtRsxS6yDA0WNwK2Y9gaXcdgDm5Vioai18l4Pd0qzK fsof5a/jEJyunW1CZK19QciwfQ2pS8QbRYgeLRZRft2I+kv6cWXlGS6YrMqKQC8t QMxnXR4AuzVllPLbbIgtM3l9oS+6jl7jKyKogeroJ9FNLjoMBJLldRLGPRhkCmdJ Z1m+s/BAVUH08qgj2kmHzucdULLjlRcmma9m/h91TcQCXHAavf7S+U9QwIyGRh83 t4Y7EqbQ93mOgjajFzILSL7AT/irgJpDu6CJqMu3EMNDA0mjxn5Cdvj40sufL/g3 UyBwqosmIwAPzNDmhPtTKvHaHfGY/k8WhoIYfAA5Lhq1z22/RODZOY0Ch2XyxQM4 s35eppe6IhnwyMv6HfrCrqE/o/16OrvvbaFQTeTlMvU0P7MIR4pVW6tRq4NEa5Wx JcvGutuMuzH1VMcqcKdc7wYyOqDOGU43kcV8PiALTIcqrhD8NDrKks1jSkyqQw2h sJQckNaQIcCXkUQecQa2UGe0l4HDSJ5gAETSenjyLBKFHf3hxiWBpw446/bODgdk 0+oyreZqMpRz3vn9LC+Yt7SuVbTzRdx7nlKIiNvo8+btOuVm44evchLFHAq3Ni8D c+tP/wss3K4Xdp+t5SvEY/nLIu11Lw44HDVMYTuNz3Ya9psL70ZLaLZM8NromnEl CUMRNTPoOC7/KDRh2E9d6c1V4CC43wAsRhksGJnSYoiSVAhaVgLqVFQTsqNHxmcg 3Y9AEBVzm3fZg6+DxAYu+amb+r8lk0Pp+N1t6rVbKXhkbAAxg0UDO3pY8Xcz0Y3g Qdd5rnHh1rJrehku7zTHvQaXEddUWmCoUGIXJ+bt4VOhErL6s5/j8GSG0xmfxgSE jnGj4Jwd0Vv19uZjsBDQ54R88GcA9YX8r48gr9JAwplrQ50m9KX6GwQhDRYKN/Dh zOt9DCUkqMqdi5T4v2qNTfkL7iXBMhsSkeYUQ/tFLyv4QQyli5uTUZ5FNXohOVAx TNyV9+gcV5WiBR0Aje6rwPW3oTkrPnVfZCdBwt/mZjPNMO5Se7D/lWE33yYu7bJ+ gaxRNynhEOB7RaOePzDjn7LExahFmTFV0sgQxwQ2BYsfI22cdkAf6qOxdK/kqiQm lgzRpDjyPIFhaCCHnXyJdSqcHmDrCjcg2P6AVCDJGdFOBvupeJ7Kg7WV5EY7G6AU ng16tyumJSMWSzSks9M0Ikop6xhq3cV+Q0OArJoreQ6eonezXjM9Y865xjF80nJL V4lcRxdXfoKpXJwzc++pgkY9t55J0+cEyBvIXfKud1/HHOhewhoy5ATyi9LLM91n iW1DaQXlvHZgE7GFMSCVLxy6ZopBbm9tF0NQDFi8zUtGulD3Gkoc/Bp+DWb2vsX4 S8W9vByNvIz/SWOGNbEs2irTRXccMAL7JHJ+74bwZZi5DRrqyQWHCn/3Ls2YPI6z lnfl15EE4G7g3+nrvP2lZFBXjsdG/U3HYi+tAyHkRN3oXvgnt9N76PoY8dlsNf6c RuNqgk31uO1sX/8du3Jxz87MlzWiG3kbAHMvbcoCgy/dW4JQcM3Sqg5PmF8i9wD1 ZuqZ7zHpWILIWd13TM3UDolQZzl+GXEX62dPPL1vBtxHhDgQicdaWFXa6DX3dVwt DToWaAqrAPIrgxvNk5FHNCTEVTQkmCIL5JoinZSk7BAl8b085CPM6F7OjB5CR4Ts V+6UaTUZqk+z+raL+HJNW2ds1r7+t8Po5CydMBS4M/pE7b/laUnbRu7rO8cqKucn n+eYimib/0YuqZj9u2RXso4kzdOyIxGSGHkmSzYuoNRx80r+jHtcBBTqXk37t0FY X5O7QItCE+uwV1Sa12yg2dgJ6vKRPCEVyMoYUBwNbKEcw1pjG9Em7HwjOZK0UrO1 yKRz6kxffVKN9Naf7lOnXooVuedY/jcaZ2zCZtASlOe8iiQK5prM4sbMixMp9ovL tTxy9E9kgvaI/mkzarloKPQGsk0WzuH+i39M3DOXrMf5HwfE+A55u1gnrHsxQlxp z5acwN42+4ln6axs4aweMGAhyEtBW8TdsNomwuPk+tpqZXHI2pqS4/aVOk8R8VE7 IqtBx2QBMINT79PDPOn3K6v9HEt9fUHJ2TWJvKRKfsu5lECJPJSJA8OQ7zzw6zQt NXw8UhZRmNW0+eI5dykg+XsII7+njYa33EJ1Sy1Ni8ZT/izKfrKCwEm44KVAyUG5 qUjghPPMNQY3D0qOl54DRfGVOxbHztUooblW+DnlLlpOy/+/B+H9Dscxosdx2/Mo RftJOMlLqK7AYIYAlw1zvqZo0pf7rCcLSLt+6FrPtNZe6ULFUacZ3RqyTZovsZi5 Ucda3bLdOHX6tKL21bRfN7L0/BjF6BJETpG3p+rBYOyCwO6HvdenpMm6cT02nrfP QJtImjeW1ov6Pw02zNlIZAXFir78Z6AcMhV2iKEJxc1RMFBcXmylNXJmGlKYB3lJ jWo6qumLewTz5vzRu0vZCmOf+bKmuyVxckPbrzP+4OHKhpm95Kp6sUn2pvh0S8H3 w1pjfZ9+sIaVgMspfRPgoWTyZ0flFvAX6DHWYVejMebwfAqZaa+UAJJ6jWQbMNzo ZtOhzCjV+2ZBYHvSiY7dtfaLwQJeMWEKIw32kEYv/Ts33n7dD/pAzZu0WCyfoqsQ MEXhbZYSCQTJ8/gqvdlurWOJL091z6Uw810YVt+wMqsBo5lnMsS3GqkzgM2PVzuV taddovr5CrWfAjQaFG8wcETiKEQFWS9JctKo0F+gwLwkVyc4fBSkjVmIliw1jXGu Enf2mBei+n8EaRB2nNa/CBVGQM24WEeMNq+TqaMvnEonvMtCIEpuJAO/NzJ1pxw0 9S+LKq3lFoIQoON5glsjV82WseAbFXmynBmSbyUY/mZQpjuNSnwLfpz4630x5vuV VNglsZ8lW9XtSPh6GkMj+lLOCqJ5aZ4UEXDSYW7IaH4sPuQ4eAAUsKx/XlbmaOad hgK+3gHYi98fiGGQjt9OqKzQRxVFnHtoSwbMp/gjAWqjDCFdo7RkCqFjfB1DsSj0 TrjZU1lVMrmdEhtUNjqfRpWN82f55fxZdrHEPUQIrOywdbRiNbONwm4AfSE8ViPz +SltYpQfF6g+tfZMwsoPSevLjdcmb1k3n8/lsEL99wpMT3NbibaXCjeJCZbAYK05 rUw5bFTVAuv6i3Bax3rx5DqyQANS3S8TBVYrdXf9x7RpQ8oeb4oo+qn293bP4n5m nW/D/yvsAJYcm3lD7oW7D369nV/mwKPpNC4B9q6N1FiUndvdFSbyzfNfSF9LV0RU A/4Qm05HtE3PAUFYfwwP8MDg0HdltMn83VfqrEi/d76xlcxfoIh2RQQgqxCIS6KE AExIY/hPYDVxApznI39xNOp7IqdPEX3i7Cv7aHeFAwbhXYMNnkfFJJTkHRdcRiJ/ RE1QPlC7ijH+IF02PE/seYg4GWrkeW3jvi+IKQ9BPBoYIx0P+7wHXf4ZGtZMourd N4fdwzFCDMFkS7wQC/GOqZltzF/gz1fWEGXRTH3Lqx0iKyiiLs2trQhFOzNw3B7E WxCIUjRMAAJ6vvUdvoFlMw8WfBkzCVple4yrCqIw6fJEq8v0q8EQ7qKDTfyPnFBt CtQZuTozfdPDnVHGmGPQKUODH/6Vwl+9/l7HDvV8/D/HKDnP581ix1a3bdokNtSK 7rBfovpzYltYGpVxsC6MZByYEpvIh5nHQouLR4L3Je2wB3F9nBGjNhBvGDQlxcne AAgywpOpQfvfsnYRWt2vlQzwhHUgWhJmGMhGMmn4oKc5su87G7yzFEnq/yIUMOm/ X0Zof/Qm92KCJS7YkLzP1GDO9XPMe+ZHeHVNXhVNCRxGNbHCHB9+g9v090sLLmal jpgrDks19uHv0yYiMqBdpstzxClRWxgHwrZO6jtbr5jeJuLVUxV0uuX76oeomUj2 mAwoD5cB1U8W9Ew+cMjp5v6gg0LTk90HftjhrZmMA0Ll6TqFWjxge+jsswOY1SZi peuQGIHFcuQ7SEcyIbqju3bmeEGZwTz51yo8x2WqpCwB1a4UTngWJgDCySAI58fM eRL6r478CAZjk+fu9ZA85B7tFczl3lj0B4QHxkX370ZeCHy39qw8vMYIcPk3ytI0 vmj5UCSeQDHHDcwo54wi83IFEWUFh18gP4ty5Tfvs6qv7qd455UQZTAO7lwpdBlp MJGlMqBHjDLGyY80p+O4vdlQBZ1uMH+48u91mokUP8p+tVVKh7bAw/HPG+SQsuNR DXF+gTm/hRuY7IYe3C7Myzc8bDTtFw6Es9BLAqzFFAMjzDVz7wY1rnZQq4mmLcKg AAMJaqItipKAroYIntXXJ3U8fsUt03M= WALinuxAgent-2.9.1.1/tests/data/wire/certs_format_not_pfx.xml000066400000000000000000000004741446033677600242050ustar00rootroot00000000000000 2012-11-30 12 CertificatesNonPfxPackage NotPFXData WALinuxAgent-2.9.1.1/tests/data/wire/certs_no_format_specified.xml000066400000000000000000000123071446033677600251550ustar00rootroot00000000000000 2012-11-30 12 MIIOgwYJKoZIhvcNAQcDoIIOdDCCDnACAQIxggEwMIIBLAIBAoAUZcG9X+5aK8VZ FY8eJV9j+RImq58wDQYJKoZIhvcNAQEBBQAEggEAn/hOytP/StyRuXHcqFq6x+Za 7gHfO8prXWdZW4e28NLt/x5ZOBHDDZ6buwwdXEZME0+RoiJvLqP2RNhZkEO8bkna pS76xLZE4NXyfxkeEs1vJYis0WJdt/56uCzBuud2SBLuMWoAWgF5alokN0uFpVgm CKCos+xv6Pisolc6geM8xQTYe6sLf5Z23LWftWfJqzuo/29glCCre7R80OLeZe5w pN6XztbYz06nhVByC35To8Lm0akWAAKU7sfqM1Nty4P0rwUJPKXo42uN1GKYbDbF x8piCAd+rs+q4Alu3qK/YaTPpMb2ECRMH6CYB8Klf/CbuWykkfS8zrsnpXT1kzCC DTUGCSqGSIb3DQEHATAUBggqhkiG9w0DBwQInjJWFaJcZz2Agg0QX6NlJUH17o20 90gfjWV01mPmzLKx71JT+hyzKr5vHywDSRI/mdb3RqA59ZrIKeyWr0HXEOuABlul nxjc/Rfk1tiLQwh0iqlOjlMtRsxS6yDA0WNwK2Y9gaXcdgDm5Vioai18l4Pd0qzK fsof5a/jEJyunW1CZK19QciwfQ2pS8QbRYgeLRZRft2I+kv6cWXlGS6YrMqKQC8t QMxnXR4AuzVllPLbbIgtM3l9oS+6jl7jKyKogeroJ9FNLjoMBJLldRLGPRhkCmdJ Z1m+s/BAVUH08qgj2kmHzucdULLjlRcmma9m/h91TcQCXHAavf7S+U9QwIyGRh83 t4Y7EqbQ93mOgjajFzILSL7AT/irgJpDu6CJqMu3EMNDA0mjxn5Cdvj40sufL/g3 UyBwqosmIwAPzNDmhPtTKvHaHfGY/k8WhoIYfAA5Lhq1z22/RODZOY0Ch2XyxQM4 s35eppe6IhnwyMv6HfrCrqE/o/16OrvvbaFQTeTlMvU0P7MIR4pVW6tRq4NEa5Wx JcvGutuMuzH1VMcqcKdc7wYyOqDOGU43kcV8PiALTIcqrhD8NDrKks1jSkyqQw2h sJQckNaQIcCXkUQecQa2UGe0l4HDSJ5gAETSenjyLBKFHf3hxiWBpw446/bODgdk 0+oyreZqMpRz3vn9LC+Yt7SuVbTzRdx7nlKIiNvo8+btOuVm44evchLFHAq3Ni8D c+tP/wss3K4Xdp+t5SvEY/nLIu11Lw44HDVMYTuNz3Ya9psL70ZLaLZM8NromnEl CUMRNTPoOC7/KDRh2E9d6c1V4CC43wAsRhksGJnSYoiSVAhaVgLqVFQTsqNHxmcg 3Y9AEBVzm3fZg6+DxAYu+amb+r8lk0Pp+N1t6rVbKXhkbAAxg0UDO3pY8Xcz0Y3g Qdd5rnHh1rJrehku7zTHvQaXEddUWmCoUGIXJ+bt4VOhErL6s5/j8GSG0xmfxgSE jnGj4Jwd0Vv19uZjsBDQ54R88GcA9YX8r48gr9JAwplrQ50m9KX6GwQhDRYKN/Dh zOt9DCUkqMqdi5T4v2qNTfkL7iXBMhsSkeYUQ/tFLyv4QQyli5uTUZ5FNXohOVAx TNyV9+gcV5WiBR0Aje6rwPW3oTkrPnVfZCdBwt/mZjPNMO5Se7D/lWE33yYu7bJ+ gaxRNynhEOB7RaOePzDjn7LExahFmTFV0sgQxwQ2BYsfI22cdkAf6qOxdK/kqiQm lgzRpDjyPIFhaCCHnXyJdSqcHmDrCjcg2P6AVCDJGdFOBvupeJ7Kg7WV5EY7G6AU ng16tyumJSMWSzSks9M0Ikop6xhq3cV+Q0OArJoreQ6eonezXjM9Y865xjF80nJL V4lcRxdXfoKpXJwzc++pgkY9t55J0+cEyBvIXfKud1/HHOhewhoy5ATyi9LLM91n iW1DaQXlvHZgE7GFMSCVLxy6ZopBbm9tF0NQDFi8zUtGulD3Gkoc/Bp+DWb2vsX4 S8W9vByNvIz/SWOGNbEs2irTRXccMAL7JHJ+74bwZZi5DRrqyQWHCn/3Ls2YPI6z lnfl15EE4G7g3+nrvP2lZFBXjsdG/U3HYi+tAyHkRN3oXvgnt9N76PoY8dlsNf6c RuNqgk31uO1sX/8du3Jxz87MlzWiG3kbAHMvbcoCgy/dW4JQcM3Sqg5PmF8i9wD1 ZuqZ7zHpWILIWd13TM3UDolQZzl+GXEX62dPPL1vBtxHhDgQicdaWFXa6DX3dVwt DToWaAqrAPIrgxvNk5FHNCTEVTQkmCIL5JoinZSk7BAl8b085CPM6F7OjB5CR4Ts V+6UaTUZqk+z+raL+HJNW2ds1r7+t8Po5CydMBS4M/pE7b/laUnbRu7rO8cqKucn n+eYimib/0YuqZj9u2RXso4kzdOyIxGSGHkmSzYuoNRx80r+jHtcBBTqXk37t0FY X5O7QItCE+uwV1Sa12yg2dgJ6vKRPCEVyMoYUBwNbKEcw1pjG9Em7HwjOZK0UrO1 yKRz6kxffVKN9Naf7lOnXooVuedY/jcaZ2zCZtASlOe8iiQK5prM4sbMixMp9ovL tTxy9E9kgvaI/mkzarloKPQGsk0WzuH+i39M3DOXrMf5HwfE+A55u1gnrHsxQlxp z5acwN42+4ln6axs4aweMGAhyEtBW8TdsNomwuPk+tpqZXHI2pqS4/aVOk8R8VE7 IqtBx2QBMINT79PDPOn3K6v9HEt9fUHJ2TWJvKRKfsu5lECJPJSJA8OQ7zzw6zQt NXw8UhZRmNW0+eI5dykg+XsII7+njYa33EJ1Sy1Ni8ZT/izKfrKCwEm44KVAyUG5 qUjghPPMNQY3D0qOl54DRfGVOxbHztUooblW+DnlLlpOy/+/B+H9Dscxosdx2/Mo RftJOMlLqK7AYIYAlw1zvqZo0pf7rCcLSLt+6FrPtNZe6ULFUacZ3RqyTZovsZi5 Ucda3bLdOHX6tKL21bRfN7L0/BjF6BJETpG3p+rBYOyCwO6HvdenpMm6cT02nrfP QJtImjeW1ov6Pw02zNlIZAXFir78Z6AcMhV2iKEJxc1RMFBcXmylNXJmGlKYB3lJ jWo6qumLewTz5vzRu0vZCmOf+bKmuyVxckPbrzP+4OHKhpm95Kp6sUn2pvh0S8H3 w1pjfZ9+sIaVgMspfRPgoWTyZ0flFvAX6DHWYVejMebwfAqZaa+UAJJ6jWQbMNzo ZtOhzCjV+2ZBYHvSiY7dtfaLwQJeMWEKIw32kEYv/Ts33n7dD/pAzZu0WCyfoqsQ MEXhbZYSCQTJ8/gqvdlurWOJL091z6Uw810YVt+wMqsBo5lnMsS3GqkzgM2PVzuV taddovr5CrWfAjQaFG8wcETiKEQFWS9JctKo0F+gwLwkVyc4fBSkjVmIliw1jXGu Enf2mBei+n8EaRB2nNa/CBVGQM24WEeMNq+TqaMvnEonvMtCIEpuJAO/NzJ1pxw0 9S+LKq3lFoIQoON5glsjV82WseAbFXmynBmSbyUY/mZQpjuNSnwLfpz4630x5vuV VNglsZ8lW9XtSPh6GkMj+lLOCqJ5aZ4UEXDSYW7IaH4sPuQ4eAAUsKx/XlbmaOad hgK+3gHYi98fiGGQjt9OqKzQRxVFnHtoSwbMp/gjAWqjDCFdo7RkCqFjfB1DsSj0 TrjZU1lVMrmdEhtUNjqfRpWN82f55fxZdrHEPUQIrOywdbRiNbONwm4AfSE8ViPz +SltYpQfF6g+tfZMwsoPSevLjdcmb1k3n8/lsEL99wpMT3NbibaXCjeJCZbAYK05 rUw5bFTVAuv6i3Bax3rx5DqyQANS3S8TBVYrdXf9x7RpQ8oeb4oo+qn293bP4n5m nW/D/yvsAJYcm3lD7oW7D369nV/mwKPpNC4B9q6N1FiUndvdFSbyzfNfSF9LV0RU A/4Qm05HtE3PAUFYfwwP8MDg0HdltMn83VfqrEi/d76xlcxfoIh2RQQgqxCIS6KE AExIY/hPYDVxApznI39xNOp7IqdPEX3i7Cv7aHeFAwbhXYMNnkfFJJTkHRdcRiJ/ RE1QPlC7ijH+IF02PE/seYg4GWrkeW3jvi+IKQ9BPBoYIx0P+7wHXf4ZGtZMourd N4fdwzFCDMFkS7wQC/GOqZltzF/gz1fWEGXRTH3Lqx0iKyiiLs2trQhFOzNw3B7E WxCIUjRMAAJ6vvUdvoFlMw8WfBkzCVple4yrCqIw6fJEq8v0q8EQ7qKDTfyPnFBt CtQZuTozfdPDnVHGmGPQKUODH/6Vwl+9/l7HDvV8/D/HKDnP581ix1a3bdokNtSK 7rBfovpzYltYGpVxsC6MZByYEpvIh5nHQouLR4L3Je2wB3F9nBGjNhBvGDQlxcne AAgywpOpQfvfsnYRWt2vlQzwhHUgWhJmGMhGMmn4oKc5su87G7yzFEnq/yIUMOm/ X0Zof/Qm92KCJS7YkLzP1GDO9XPMe+ZHeHVNXhVNCRxGNbHCHB9+g9v090sLLmal jpgrDks19uHv0yYiMqBdpstzxClRWxgHwrZO6jtbr5jeJuLVUxV0uuX76oeomUj2 mAwoD5cB1U8W9Ew+cMjp5v6gg0LTk90HftjhrZmMA0Ll6TqFWjxge+jsswOY1SZi peuQGIHFcuQ7SEcyIbqju3bmeEGZwTz51yo8x2WqpCwB1a4UTngWJgDCySAI58fM eRL6r478CAZjk+fu9ZA85B7tFczl3lj0B4QHxkX370ZeCHy39qw8vMYIcPk3ytI0 vmj5UCSeQDHHDcwo54wi83IFEWUFh18gP4ty5Tfvs6qv7qd455UQZTAO7lwpdBlp MJGlMqBHjDLGyY80p+O4vdlQBZ1uMH+48u91mokUP8p+tVVKh7bAw/HPG+SQsuNR DXF+gTm/hRuY7IYe3C7Myzc8bDTtFw6Es9BLAqzFFAMjzDVz7wY1rnZQq4mmLcKg AAMJaqItipKAroYIntXXJ3U8fsUt03M= WALinuxAgent-2.9.1.1/tests/data/wire/encrypted.enc000066400000000000000000000010551446033677600217160ustar00rootroot00000000000000MIIBlwYJKoZIhvcNAQcDoIIBiDCCAYQCAQIxggEwMIIBLAIBAoAUW4P+tNXlmDXW H30raKBkpUhXYwUwDQYJKoZIhvcNAQEBBQAEggEAP0LpwacLdJyvNQVmSyXPGM0i mNJSHPQsAXLFFcmWmCAGiEsQWiHKV9mON/eyd6DjtgbTuhVNHPY/IDSDXfjgLxdX NK1XejuEaVTwdVtCJWl5l4luOeCMDueitoIgBqgkbFpteqV6s8RFwnv+a2HhM0lc TUwim6skx1bFs0csDD5DkM7R10EWxWHjdKox8R8tq/C2xpaVWRvJ52/DCVgeHOfh orV0GmBK0ue/mZVTxu8jz2BxQUBhHXNWjBuNuGNmUuZvD0VY1q2K6Fa3xzv32mfB xPKgt6ru/wG1Kn6P8yMdKS3bQiNZxE1D1o3epDujiygQahUby5cI/WXk7ryZ1DBL BgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECAxpp+ZE6rpAgChqxBVpU047fb4zinTV 5xaG7lN15YEME4q8CqcF/Ji3NbHPmdw1/gtf WALinuxAgent-2.9.1.1/tests/data/wire/ext_conf-no_gs_metadata.xml000066400000000000000000000031421446033677600245230ustar00rootroot00000000000000 Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo WALinuxAgent-2.9.1.1/tests/data/wire/ext_conf.xml000066400000000000000000000034271446033677600215660ustar00rootroot00000000000000 Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo WALinuxAgent-2.9.1.1/tests/data/wire/ext_conf_additional_locations.xml000066400000000000000000000035011446033677600260220ustar00rootroot00000000000000 Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml http://mock-goal-state/additionalLocation/3/manifest.xml http://mock-goal-state/additionalLocation/4/manifest.xml {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo WALinuxAgent-2.9.1.1/tests/data/wire/ext_conf_aks_extension.xml000066400000000000000000000106461446033677600245210ustar00rootroot00000000000000 Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"message": "Enabling non-AKS"} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"message": "Enabling AKSNode"} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"message": "Enabling AKSBilling"} } } ] } https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo WALinuxAgent-2.9.1.1/tests/data/wire/ext_conf_autoupgrade.xml000066400000000000000000000045071446033677600241660ustar00rootroot00000000000000 Win8 http://mock-goal-state/rdfepirv2hknprdstr03.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr04.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml Win7 http://mock-goal-state/rdfepirv2hknprdstr03.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr04.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo WALinuxAgent-2.9.1.1/tests/data/wire/ext_conf_autoupgrade_internalversion.xml000066400000000000000000000045071446033677600274700ustar00rootroot00000000000000 Win8 http://mock-goal-state/rdfepirv2hknprdstr03.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr04.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml Win7 http://mock-goal-state/rdfepirv2hknprdstr03.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr04.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo WALinuxAgent-2.9.1.1/tests/data/wire/ext_conf_dependencies_with_empty_settings.xml000066400000000000000000000052141446033677600304610ustar00rootroot00000000000000 Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo WALinuxAgent-2.9.1.1/tests/data/wire/ext_conf_in_vm_artifacts_profile.xml000066400000000000000000000041321446033677600265300ustar00rootroot00000000000000 Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo https://mock-goal-state/test.blob.core.windows.net/$system/test-cs12.test-cs12.test-cs12.vmSettings?sv=2016-05-31&sr=b&sk=system-1&sig=saskey;se=9999-01-01T00%3a00%3a00Z&sp=r WALinuxAgent-2.9.1.1/tests/data/wire/ext_conf_in_vm_empty_artifacts_profile.xml000066400000000000000000000036371446033677600277570ustar00rootroot00000000000000 Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo WALinuxAgent-2.9.1.1/tests/data/wire/ext_conf_in_vm_metadata.xml000066400000000000000000000036551446033677600246210ustar00rootroot00000000000000 Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo WALinuxAgent-2.9.1.1/tests/data/wire/ext_conf_internalversion.xml000066400000000000000000000045071446033677600250700ustar00rootroot00000000000000 Win8 http://mock-goal-state/rdfepirv2hknprdstr03.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr04.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml Win7 http://mock-goal-state/rdfepirv2hknprdstr03.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr04.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo WALinuxAgent-2.9.1.1/tests/data/wire/ext_conf_invalid_and_valid_handlers.xml000066400000000000000000000056061446033677600271560ustar00rootroot00000000000000 Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo WALinuxAgent-2.9.1.1/tests/data/wire/ext_conf_invalid_vm_metadata.xml000066400000000000000000000035241446033677600256340ustar00rootroot00000000000000 Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo WALinuxAgent-2.9.1.1/tests/data/wire/ext_conf_missing_family.xml000066400000000000000000000067501446033677600246620ustar00rootroot00000000000000 Prod Test https://mock-goal-state/rdfepirv2bl2prdstr01.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_useast_manifest.xml https://mock-goal-state/rdfepirv2bl2prdstr02.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_useast_manifest.xml https://mock-goal-state/rdfepirv2bl2prdstr03.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_useast_manifest.xml https://mock-goal-state/rdfepirv2bl2prdstr04.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_useast_manifest.xml https://mock-goal-state/rdfepirv2bl3prdstr01.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_useast_manifest.xml https://mock-goal-state/rdfepirv2bl3prdstr02.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_useast_manifest.xml https://mock-goal-state/rdfepirv2bl3prdstr03.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_useast_manifest.xml https://mock-goal-state/zrdfepirv2bl4prdstr01.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_useast_manifest.xml https://mock-goal-state/zrdfepirv2bl4prdstr03.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_useast_manifest.xml https://mock-goal-state/zrdfepirv2bl5prdstr02.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_useast_manifest.xml https://mock-goal-state/zrdfepirv2bl5prdstr04.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_useast_manifest.xml https://mock-goal-state/zrdfepirv2bl5prdstr06.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_useast_manifest.xml https://mock-goal-state/zrdfepirv2bl5prdstr09a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_useast_manifest.xml https://mock-goal-state/zrdfepirv2bl6prdstr02a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_useast_manifest.xml eastus https://walaautoasmeastus.blob.core.windows.net/vhds/walaautos73small.walaautos73small.walaautos73small.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=u%2BCA2Cxb7ticiEBRIW8HWgNW7gl2NPuOGQl0u95ApQE%3D WALinuxAgent-2.9.1.1/tests/data/wire/ext_conf_missing_requested_version.xml000066400000000000000000000042151446033677600271410ustar00rootroot00000000000000 Prod 5.2.1.0 http://mock-goal-state/manifest_of_ga.xml Test 5.2.1.0 http://mock-goal-state/manifest_of_ga.xml {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo WALinuxAgent-2.9.1.1/tests/data/wire/ext_conf_multiple_extensions.xml000066400000000000000000000216571446033677600257650ustar00rootroot00000000000000 Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIIB4AYJKoZIhvcNAQcDoIIB0TCCAc0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEH3vWjYIrceWQigVQwoS8z0wDQYJKoZIhvcNAQEBBQAEggEANYey5W0qDqC6RHZlVnpLp2dWrMr1Rt5TCFkOjq1jU4y2y1FPtsTTKq9Z5pdGb/IHQo9VcT+OFglO3bChMbqc1vgmk4wkTQkgJVD3C8Rq4nv3uvQIux+g8zsa1MPKT5fTwG/dcrBp9xqySJLexUiuJljmNJgorGc0KtLwjnad4HTSKudDSo5DGskSDLxxLZYx0VVtQvgekOOwT/0C0pN4+JS/766jdUAnHR3oOuD5Dx7/c6EhFSoiYXMA0bUzH7VZeF8j/rkP1xscLQRrCScCNV2Ox424Y4RBbcbP/p69lDxGURcIKLKrIUhQdC8CfUMkQUEmFDLcOtxutCTFBZYMJzBbBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECCuc0a4Gl8PAgDgcHekee/CivSTCXntJiCrltUDob8cX4YtIS6lq3H08Ar+2tKkpg5e3bOkdAo3q2GfIrGDm4MtVWw==","publicSettings":{"foo":"bar"}}}]} {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIIBwAYJKoZIhvcNAQcDoIIBsTCCAa0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEH3vWjYIrceWQigVQwoS8z0wDQYJKoZIhvcNAQEBBQAEggEABILhQPoMx3NEbd/sS0xAAE4rJXwzJSE0bWr4OaKpcGS4ePtaNW8XWm+psYR9CBlXuGCuDVlFEdPmO2Ai8NX8TvT7RVYYc6yVQKpNQqO6Q9g9O52XXX4tBSFSCfoTzd1kbGC1c2wbXDyeROGCjraWuGHd4C9s9gytpgAlYicZjOqV3deo30F4vXZ+ZhCNpMkOvSXcsNpzTzQ/mskwNubN8MPkg/jEAzTHRpiJl3tjGtTqm00GHMqFF8/31jnoLQeQnWSmY+FBpiTUhPzyjufIcoZ+ueGXZiJ77xyH2Rghh5wvQM8oTVy2dwFQGeqjHOVgdgRNi/HgfZhcdltaQ8kjYDA7BgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECHPM0ZKBn+aWgBiVPT7zlkJA8eGuH7bNMTQCtGoJezToa24=","publicSettings":{"foo":"bar"}}}]} {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIIB4AYJKoZIhvcNAQcDoIIB0TCCAc0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEH3vWjYIrceWQigVQwoS8z0wDQYJKoZIhvcNAQEBBQAEggEAGSKUDRN64DIB7FS7yKXa07OXaFPhmdNnNDOAOD3/WVFb9fQ2bztV46waq7iRO+lpz7LSerRzIe6Kod9zCfK7ryukRomVHIfTIBwPjQ+Otn8ZD2aVcrxR0EI95x/SGyiESJRQnOMbpoVSWSu2KJUCPfycQ4ODbaazDc61k0JCmmRy12rQ4ttyWKhYwpwI2OYFHGr39N/YYq6H8skHj5ve1605i4P9XpfEyIwF5BbX59tDOAFFQtX7jzQcz//LtaHHjwLmysmD9OG5XyvfbBICwSYJfMX9Jh1aahLwcjL8Bd0vYyGL1ItMQF5KfDwog4+HLcRGx+S02Yngm3/YKS9DmzBbBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECFGLNfK0bO5OgDgH90bRzqfgKK6EEh52XJfHz9G/ZL1mqP/ueWqo95PtEFo1gvI7z25V/pT0tBGibXgRhQXLFmwVTA==","publicSettings":{"foo":"bar"}}}]} {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIIEzAYJKoZIhvcNAQcDoIIEvTCCBLkCAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEH3vWjYIrceWQigVQwoS8z0wDQYJKoZIhvcNAQEBBQAEggEAFqLDBFGeuglluYmZb0Zw+ZlMiMIws9/LgmurVSRUTU/nSleIc9vOLcukfMeCpMativzHe23iDFy6p3XDkViNcuzqbhlPq5LQsXXg+xaUrrg8Xy+q7KUQdxzPdNBdpgkUh6yE2EFbqVLQ/7x+TkkSsw35uPT0nEqSj3yYFGH7X/NJ49fKU+ZvFDp/N+o54UbE6ZdxlHFtz6NJFxx5w4z5adQ8DgnUyS0bJ2denolknODfSW2D2alm00SXlI88CAjeHgEDkoLCduwkrDkSFAODcAiEHHX8oYCnfanatpjm7ZgSutS9y7+XUnGWxDYoujHDI9bbV0WpyDcx/DIrlZ+WcTCCA0UGCSqGSIb3DQEHATAUBggqhkiG9w0DBwQIrL18Lbp1qU6AggMgGklvozqr8HqYP+DwkvxdwHSpo+23QFxh70os+NJRtVgBv5NjPEziXo3FpXHMPvt0kp0IwXbwyy5vwnjCTA2sQOYgj77X6RmwF6+1gt2DIHDN1Q6jWzdcXZVHykSiF3gshbebRKO0hydfCaCyYL36HOZ8ugyCctOon5EflrnoOYDDHRbsr30DAxZCAwGOGZEeoU2+U+YdhuMvplnMryD1f6b8FQ7jXihe/zczAibX5/22NxhsVgALdsV5h6hwuTbspDt3V15/VU8ak7a4xxdBfXOX0HcQI86oqsFr7S7zIveoQHsW+wzlyMjwi6DRPFpz2wFkv5ivgFEvtCzDQP4aCqGI8VdqzR7aUDnuqiSCe/cbmv5mSmTYlDPTR03WS0IvgyeoNAzqCbYQe44AUBEZb/yT8Z3XxwW0GzcPMZQ0XjpcZiaKAueN9V8nJgNCEDPTJqpSjy+tEHmSgxn70+E57F0vzPvdQ3vOEeRj8zlBblHd4uVrhxdBMUuQ73JEQEha5rz0qcUy04Wmjld1rBuX6pdOqrArAYzTLJbIuLqDjlnYFsHLs9QBGvIEb9VFOlAm5JW8npBbIRHXqPfwZWs60+uNksTtsN3MxBxUWJPOByb4xRNx+nRpTOvfKKFlgq1ReK5bGSTCB7x0Ft3+T42LOQDrBPyxxtGzWs+aq05qFgI4n0h8X82wxJflK+kUdwvvG/ZY5MM+/le2zOrUeyzvxXsHoRetgg+DOk7v+v7VsuT1KuvTXvgzxoOFF3/T2pNPpE3h6bbP2BUqZ2yzPNziGFslywDLZ8W3OUZoQejGqobRePdgUoBi5q2um/sPnq81kOJ/qhIOVq581ZD4IQWLot8eK8vX0G/y7y71YelRR51cUfgR5WvZZf6LvYw+GpwOtSViugl9QxGCviSLgHTJSSEm0ijtbzKhwP4vEyydNDrz8+WYB8DNIV7K2Pc8JyxAM03FYX30CaaJ40pbEUuVQVEnkAD2E//29/ZzgNTf/LBMzMEP5j7wlL+QQpmPAtL/FlBrOJ4nDEqsOOhWzI1MN51xRZuv3e2RqzVPiSmrKtk=","publicSettings":{"foo":"bar"}}}]} {"runtimeSettings":[{"handlerSettings":{"publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo WALinuxAgent-2.9.1.1/tests/data/wire/ext_conf_no_extensions-block_blob.xml000066400000000000000000000010231446033677600266150ustar00rootroot00000000000000 http://foo WALinuxAgent-2.9.1.1/tests/data/wire/ext_conf_no_extensions-no_status_blob.xml000066400000000000000000000007061446033677600275510ustar00rootroot00000000000000 WALinuxAgent-2.9.1.1/tests/data/wire/ext_conf_no_extensions-page_blob.xml000066400000000000000000000015221446033677600264430ustar00rootroot00000000000000 Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml http://sas_url WALinuxAgent-2.9.1.1/tests/data/wire/ext_conf_no_public.xml000066400000000000000000000126761446033677600236260ustar00rootroot00000000000000 Win8 http://mock-goal-state/rdfepirv2hknprdstr03.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr04.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr05.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr06.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr07.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr08.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr09.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr10.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr11.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr12.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml http://mock-goal-state/zrdfepirv2hk2prdstr01.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml Win7 http://mock-goal-state/rdfepirv2hknprdstr03.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr04.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr05.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr06.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr07.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr08.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr09.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr10.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr11.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr12.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml http://mock-goal-state/zrdfepirv2hk2prdstr01.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK"}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo WALinuxAgent-2.9.1.1/tests/data/wire/ext_conf_no_settings.xml000066400000000000000000000122021446033677600241710ustar00rootroot00000000000000 Win8 http://mock-goal-state/rdfepirv2hknprdstr03.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr04.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr05.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr06.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr07.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr08.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr09.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr10.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr11.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr12.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml http://mock-goal-state/zrdfepirv2hk2prdstr01.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win8_asiaeast_manifest.xml Win7 http://mock-goal-state/rdfepirv2hknprdstr03.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr04.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr05.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr06.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr07.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr08.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr09.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr10.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr11.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml http://mock-goal-state/rdfepirv2hknprdstr12.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml http://mock-goal-state/zrdfepirv2hk2prdstr01.blob.core.windows.net/bfd5c281a7dc4e4b84381eb0b47e3aaf/Microsoft.WindowsAzure.GuestAgent_Win7_asiaeast_manifest.xml https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo WALinuxAgent-2.9.1.1/tests/data/wire/ext_conf_requested_version.xml000066400000000000000000000035341446033677600254130ustar00rootroot00000000000000 Prod 9.9.9.10 http://mock-goal-state/manifest_of_ga.xml Test 9.9.9.10 http://mock-goal-state/manifest_of_ga.xml {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo WALinuxAgent-2.9.1.1/tests/data/wire/ext_conf_required_features.xml000066400000000000000000000041241446033677600253570ustar00rootroot00000000000000 Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml TestRequiredFeature1 TestRequiredFeature2 TestRequiredFeature3 3.0 {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo WALinuxAgent-2.9.1.1/tests/data/wire/ext_conf_sequencing.xml000066400000000000000000000054451446033677600240110ustar00rootroot00000000000000 Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo WALinuxAgent-2.9.1.1/tests/data/wire/ext_conf_settings_case_mismatch.xml000066400000000000000000000131461446033677600263650ustar00rootroot00000000000000 Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo WALinuxAgent-2.9.1.1/tests/data/wire/ext_conf_upgradeguid.xml000066400000000000000000000035141446033677600241430ustar00rootroot00000000000000 Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo WALinuxAgent-2.9.1.1/tests/data/wire/ga_manifest.xml000066400000000000000000000027651446033677600222420ustar00rootroot00000000000000 1.0.0 http://mock-goal-state/ga-manifests/OSTCExtensions.WALinuxAgent__1.0.0 1.1.0 http://mock-goal-state/ga-manifests/OSTCExtensions.WALinuxAgent__1.1.0 1.2.0 http://mock-goal-state/ga-manifests/OSTCExtensions.WALinuxAgent__1.2.0 2.0.0http://mock-goal-state/ga-manifests/OSTCExtensions.WALinuxAgent__2.0.0 2.1.0http://mock-goal-state/ga-manifests/OSTCExtensions.WALinuxAgent__2.1.0 9.9.9.10 http://mock-goal-state/ga-manifests/OSTCExtensions.WALinuxAgent__99999.0.0.0 99999.0.0.0http://mock-goal-state/ga-manifests/OSTCExtensions.WALinuxAgent__99999.0.0.0 WALinuxAgent-2.9.1.1/tests/data/wire/ga_manifest_no_upgrade.xml000066400000000000000000000014351446033677600244360ustar00rootroot00000000000000 1.0.0 http://mock-goal-state/ga-manifests/OSTCExtensions.WALinuxAgent__1.0.0 1.1.0 http://mock-goal-state/ga-manifests/OSTCExtensions.WALinuxAgent__1.1.0 1.2.0 http://mock-goal-state/ga-manifests/OSTCExtensions.WALinuxAgent__1.2.0 WALinuxAgent-2.9.1.1/tests/data/wire/goal_state.xml000066400000000000000000000035511446033677600221010ustar00rootroot00000000000000 2010-12-15 1 Started 16001 c6d5526c-5ac2-4200-b6e2-56f2b70c5ab2 b61f93d0-e1ed-40b2-b067-22c243233448.MachineRole_IN_0 Started http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=hostingEnvironmentConfig&incarnation=1 http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=sharedConfig&incarnation=1 http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=extensionsConfig&incarnation=1 http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=fullConfig&incarnation=1 http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=certificates&incarnation=1 bc8b9d47-b5ed-4704-85d9-fd74cc967ec2.5.bc8b9d47-b5ed-4704-85d9-fd74cc967ec2.5._canary.1.xml WALinuxAgent-2.9.1.1/tests/data/wire/goal_state_no_certs.xml000066400000000000000000000032571446033677600240000ustar00rootroot00000000000000 2010-12-15 1 Started 16001 c6d5526c-5ac2-4200-b6e2-56f2b70c5ab2 b61f93d0-e1ed-40b2-b067-22c243233448.MachineRole_IN_0 Started http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=hostingEnvironmentConfig&incarnation=1 http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=sharedConfig&incarnation=1 http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=extensionsConfig&incarnation=1 http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=fullConfig&incarnation=1 bc8b9d47-b5ed-4704-85d9-fd74cc967ec2.5.bc8b9d47-b5ed-4704-85d9-fd74cc967ec2.5._canary.1.xml WALinuxAgent-2.9.1.1/tests/data/wire/goal_state_no_ext.xml000066400000000000000000000032241446033677600234520ustar00rootroot00000000000000 2010-12-15 1 Started 16001 c6d5526c-5ac2-4200-b6e2-56f2b70c5ab2 b61f93d0-e1ed-40b2-b067-22c243233448.MachineRole_IN_0 Started http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=hostingEnvironmentConfig&incarnation=1 http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=sharedConfig&incarnation=1 http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=fullConfig&incarnation=1 http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=certificates&incarnation=1 bc8b9d47-b5ed-4704-85d9-fd74cc967ec2.5.bc8b9d47-b5ed-4704-85d9-fd74cc967ec2.5._canary.1.xml WALinuxAgent-2.9.1.1/tests/data/wire/goal_state_noop.xml000066400000000000000000000007261446033677600231350ustar00rootroot00000000000000 2010-12-15 1 Started 16001 c6d5526c-5ac2-4200-b6e2-56f2b70c5ab2 WALinuxAgent-2.9.1.1/tests/data/wire/goal_state_remote_access.xml000066400000000000000000000041141446033677600247710ustar00rootroot00000000000000 2010-12-15 1 Started 16001 c6d5526c-5ac2-4200-b6e2-56f2b70c5ab2 http://168.63.129.16:80/remoteaccessinfouri/ b61f93d0-e1ed-40b2-b067-22c243233448.MachineRole_IN_0 Started b61f93d0-e1ed-40b2-b067-22c243233448.1.b61f93d0-e1ed-40b2-b067-22c243233448.2.MachineRole_IN_0.xml http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=hostingEnvironmentConfig&incarnation=1 http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=sharedConfig&incarnation=1 http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=extensionsConfig&incarnation=1 http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=fullConfig&incarnation=1 http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=certificates&incarnation=1 bc8b9d47-b5ed-4704-85d9-fd74cc967ec2.5.bc8b9d47-b5ed-4704-85d9-fd74cc967ec2.5._canary.1.xml WALinuxAgent-2.9.1.1/tests/data/wire/hosting_env.xml000066400000000000000000000043251446033677600223020ustar00rootroot00000000000000 WALinuxAgent-2.9.1.1/tests/data/wire/in_vm_artifacts_profile.json000066400000000000000000000000221446033677600250060ustar00rootroot00000000000000{ "onHold": true }WALinuxAgent-2.9.1.1/tests/data/wire/invalid_config/000077500000000000000000000000001446033677600222045ustar00rootroot00000000000000ext_conf_multiple_depends_on_for_single_handler.xml000066400000000000000000000065151446033677600345000ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/wire/invalid_config Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo ext_conf_multiple_runtime_settings_same_plugin.xml000066400000000000000000000037651446033677600344500ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/wire/invalid_config Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo WALinuxAgent-2.9.1.1/tests/data/wire/invalid_config/ext_conf_multiple_settings_for_same_handler.xml000066400000000000000000000041031446033677600337340ustar00rootroot00000000000000 Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo WALinuxAgent-2.9.1.1/tests/data/wire/invalid_config/ext_conf_plugin_settings_version_mismatch.xml000066400000000000000000000041211446033677600334610ustar00rootroot00000000000000 Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo ext_conf_single_and_multi_config_settings_same_plugin.xml000066400000000000000000000040421446033677600357010ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/wire/invalid_config Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo WALinuxAgent-2.9.1.1/tests/data/wire/manifest.xml000066400000000000000000000061451446033677600215670ustar00rootroot00000000000000 1.0.0 http://mock-goal-state/foo.bar/zar/OSTCExtensions.ExampleHandlerLinux__1.0.0 1.1.0 http://mock-goal-state/foo.bar/zar/OSTCExtensions.ExampleHandlerLinux__1.1.0 1.1.1 http://mock-goal-state/foo.bar/zar/OSTCExtensions.ExampleHandlerLinux__1.1.1 1.2.0 http://mock-goal-state/foo.bar/zar/OSTCExtensions.ExampleHandlerLinux__1.2.0 2.0.0http://mock-goal-state/host/OSTCExtensions.ExampleHandlerLinux__2.0.0 2.1.0http://mock-goal-state/host/OSTCExtensions.ExampleHandlerLinux__2.1.0 True 2.1.1http://mock-goal-state/host/OSTCExtensions.ExampleHandlerLinux__2.1.1 2.2.0http://mock-goal-state/host/OSTCExtensions.ExampleHandlerLinux__2.2.0 3.0http://mock-goal-state/host/OSTCExtensions.ExampleHandlerLinux__3.0 3.1http://mock-goal-state/host/OSTCExtensions.ExampleHandlerLinux__3.1 4.0.0.0http://mock-goal-state/host/OSTCExtensions.ExampleHandlerLinux__3.0 4.0.0.1http://mock-goal-state/host/OSTCExtensions.ExampleHandlerLinux__3.1 4.1.0.0http://mock-goal-state/host/OSTCExtensions.ExampleHandlerLinux__3.1 1.3.0 http://mock-goal-state/foo.bar/zar/OSTCExtensions.ExampleHandlerLinux__1.3.0 2.3.0http://mock-goal-state/host/OSTCExtensions.ExampleHandlerLinux__2.3.0 2.4.0http://mock-goal-state/host/OSTCExtensions.ExampleHandlerLinux__2.3.0 WALinuxAgent-2.9.1.1/tests/data/wire/manifest_deletion.xml000066400000000000000000000006001446033677600234400ustar00rootroot00000000000000 1.0.0 http://mock-goal-state/foo.bar/zar/OSTCExtensions.ExampleHandlerLinux__1.0.0 WALinuxAgent-2.9.1.1/tests/data/wire/multi-config/000077500000000000000000000000001446033677600216265ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/data/wire/multi-config/ext_conf_mc_disabled_extensions.xml000066400000000000000000000104301446033677600307400ustar00rootroot00000000000000 Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml eastus CRP MultipleExtensionsPerHandler https://test.blob.core.windows.net/$system/lrwinmcdn_0.0f3bfecf-f14f-4c7d-8275-9dee7310fe8c.status?sv=2018-03-28&sr=b&sk=system-1&sig=1%2b%2f4nL3kZJyUb7EKxSVGQ%2fHLpXBZxCU8Zo4diPFPv5o%3d&se=9999-01-01T00%3a00%3a00Z&sp=w { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"source":{"script":"Write-Host First: Hello World 1234!"},"parameters":[{"name":"extensionName","value":"firstRunCommand"}],"timeoutInSeconds":120} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"message": "Disabling secondExtension"} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"message": "Enabling thirdExtension"} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"message": "Enabling fourthExtension"} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"message": "Enabling SingleConfig Extension"} } } ] } https://test.blob.core.windows.net/$system/lrwinmcdn_0.0f3bfecf-f14f-4c7d-8275-9dee7310fe8c.vmSettings?sv=2018-03-28&sr=b&sk=system-1&sig=8YHwmibhasT0r9MZgL09QmFwL7ZV%2bg%2b49QP5Zwe4ksY%3d&se=9999-01-01T00%3a00%3a00Z&sp=r https://test.blob.core.windows.net/$system/lrwinmcdn_0.0f3bfecf-f14f-4c7d-8275-9dee7310fe8c.vmHealth?sv=2018-03-28&sr=b&sk=system-1&sig=DQSxfPRZEoGBGIFl%2f4bFZ0LM9RNr9DbUEmmtkiQkWkE%3d&se=9999-01-01T00%3a00%3a00Z&sp=rwWALinuxAgent-2.9.1.1/tests/data/wire/multi-config/ext_conf_mc_update_extensions.xml000066400000000000000000000076551446033677600304720ustar00rootroot00000000000000 Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml eastus CRP MultipleExtensionsPerHandler https://test.blob.core.windows.net/$system/lrwinmcdn_0.0f3bfecf-f14f-4c7d-8275-9dee7310fe8c.status?sv=2018-03-28&sr=b&sk=system-1&sig=1%2b%2f4nL3kZJyUb7EKxSVGQ%2fHLpXBZxCU8Zo4diPFPv5o%3d&se=9999-01-01T00%3a00%3a00Z&sp=w { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"message": "Disabling firstExtension"} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"message": "Disabling secondExtension"} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"message": "Enabling thirdExtension"} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"message": "Enabling SingleConfig extension"} } } ] } https://test.blob.core.windows.net/$system/lrwinmcdn_0.0f3bfecf-f14f-4c7d-8275-9dee7310fe8c.vmSettings?sv=2018-03-28&sr=b&sk=system-1&sig=8YHwmibhasT0r9MZgL09QmFwL7ZV%2bg%2b49QP5Zwe4ksY%3d&se=9999-01-01T00%3a00%3a00Z&sp=r https://test.blob.core.windows.net/$system/lrwinmcdn_0.0f3bfecf-f14f-4c7d-8275-9dee7310fe8c.vmHealth?sv=2018-03-28&sr=b&sk=system-1&sig=DQSxfPRZEoGBGIFl%2f4bFZ0LM9RNr9DbUEmmtkiQkWkE%3d&se=9999-01-01T00%3a00%3a00Z&sp=rwWALinuxAgent-2.9.1.1/tests/data/wire/multi-config/ext_conf_multi_config_no_dependencies.xml000066400000000000000000000076471446033677600321340ustar00rootroot00000000000000 Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml eastus CRP MultipleExtensionsPerHandler https://test.blob.core.windows.net/$system/lrwinmcdn_0.0f3bfecf-f14f-4c7d-8275-9dee7310fe8c.status?sv=2018-03-28&sr=b&sk=system-1&sig=1%2b%2f4nL3kZJyUb7EKxSVGQ%2fHLpXBZxCU8Zo4diPFPv5o%3d&se=9999-01-01T00%3a00%3a00Z&sp=w { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"message": "Enabling firstExtension"} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"message": "Enabling secondExtension"} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"message": "Enabling thirdExtension"} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"message": "Enabling SingleConfig extension"} } } ] } https://test.blob.core.windows.net/$system/lrwinmcdn_0.0f3bfecf-f14f-4c7d-8275-9dee7310fe8c.vmSettings?sv=2018-03-28&sr=b&sk=system-1&sig=8YHwmibhasT0r9MZgL09QmFwL7ZV%2bg%2b49QP5Zwe4ksY%3d&se=9999-01-01T00%3a00%3a00Z&sp=r https://test.blob.core.windows.net/$system/lrwinmcdn_0.0f3bfecf-f14f-4c7d-8275-9dee7310fe8c.vmHealth?sv=2018-03-28&sr=b&sk=system-1&sig=DQSxfPRZEoGBGIFl%2f4bFZ0LM9RNr9DbUEmmtkiQkWkE%3d&se=9999-01-01T00%3a00%3a00Z&sp=rwWALinuxAgent-2.9.1.1/tests/data/wire/multi-config/ext_conf_with_disabled_multi_config.xml000066400000000000000000000205601446033677600316010ustar00rootroot00000000000000 Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml eastus CRP MultipleExtensionsPerHandler https://test.blob.core.windows.net/$system/lrwinmcdn_0.0f3bfecf-f14f-4c7d-8275-9dee7310fe8c.status?sv=2018-03-28&sr=b&sk=system-1&sig=1%2b%2f4nL3kZJyUb7EKxSVGQ%2fHLpXBZxCU8Zo4diPFPv5o%3d&se=9999-01-01T00%3a00%3a00Z&sp=w { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"source":{"script":"Write-Host First: Hello World 66!"},"parameters":[{"name":"extensionName","value":"firstRunCommand2"}],"timeoutInSeconds":120} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"source":{"script":"Write-Host Second: Hello World 66!"},"parameters":[{"name":"extensionName","value":"secondRunCommand2"}],"timeoutInSeconds":120} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"source":{"script":"Write-Host Third: Hello World 3!"},"parameters":[{"name":"extensionName","value":"thirdRunCommand"}],"timeoutInSeconds":120} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"fileUris":["https://test.blob.core.windows.net/windowsagent/VerifyAgentRunning.ps1"],"commandToExecute":"echo Hello 666"} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {} } } ] } { "runtimeSettings": [ { "handlerSettings": { "protectedSettingsCertThumbprint": "EE22B5ECAC6A83B581A7CAC1772BEBD0E016649F", "protectedSettings": "MIIB0AYJKoZIhvcNAQcDoIIBwTCCAb0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEC9nmIRFUMGqQOFdBaBjrNswDQYJKoZIhvcNAQEBBQAEggEAUrwxmEAEZuDxhX1S8il8AWeaSFhMDag2DbFEw9v8VCpNM1nhikc21tVaxKfkcugczJ++x5cfM06sMj+8aBisNFUEvAKZzaUvqH91ty7DN8syaZrs0bzx34p8O42bOtGC/UoetqsAlpYrDJr+UYUgm8QFJY8UDCpBHdFZS4bO4FQdAmJ9AVwwTzYT4KI96GNDiUS8skJMycUyFT1IO08n5bSOJqo1b0qUDz28JOQdEAriR+7sCry7okyajhy76FcbD16zhwsZVNzHcQm1IqPU2htlRcWR13RqHz7FUFMFky7t+ZxKB5TAcleF04yo4xkSqLtPDWZuDRu5gEHqom0kQjBLBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECNOB8r5YkBJugCiXeeb4js/ehiWBX7qWgrPlgETtKjUq5N1Z0fxPCjPG7SQb+BqQqiX1", "publicSettings": {"UserName":"test1234"} } } ] } https://test.blob.core.windows.net/$system/lrwinmcdn_0.0f3bfecf-f14f-4c7d-8275-9dee7310fe8c.vmSettings?sv=2018-03-28&sr=b&sk=system-1&sig=8YHwmibhasT0r9MZgL09QmFwL7ZV%2bg%2b49QP5Zwe4ksY%3d&se=9999-01-01T00%3a00%3a00Z&sp=r https://test.blob.core.windows.net/$system/lrwinmcdn_0.0f3bfecf-f14f-4c7d-8275-9dee7310fe8c.vmHealth?sv=2018-03-28&sr=b&sk=system-1&sig=DQSxfPRZEoGBGIFl%2f4bFZ0LM9RNr9DbUEmmtkiQkWkE%3d&se=9999-01-01T00%3a00%3a00Z&sp=rwWALinuxAgent-2.9.1.1/tests/data/wire/multi-config/ext_conf_with_multi_config.xml000066400000000000000000000211521446033677600277500ustar00rootroot00000000000000 Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml eastus CRP MultipleExtensionsPerHandler https://test.blob.core.windows.net/$system/lrwinmcdn_0.0f3bfecf-f14f-4c7d-8275-9dee7310fe8c.status?sv=2018-03-28&sr=b&sk=system-1&sig=1%2b%2f4nL3kZJyUb7EKxSVGQ%2fHLpXBZxCU8Zo4diPFPv5o%3d&se=9999-01-01T00%3a00%3a00Z&sp=w { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"source":{"script":"Write-Host First: Hello World 1234!"},"parameters":[{"name":"extensionName","value":"firstRunCommand"}],"timeoutInSeconds":120} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"source":{"script":"Write-Host First: Hello World 1234!"},"parameters":[{"name":"extensionName","value":"secondRunCommand"}],"timeoutInSeconds":120} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"source":{"script":"Write-Host Third: Hello World 3!"},"parameters":[{"name":"extensionName","value":"thirdRunCommand"}],"timeoutInSeconds":120} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"fileUris":["https://test.blob.core.windows.net/windowsagent/VerifyAgentRunning.ps1"],"commandToExecute":"echo Hello 1234"} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {} } } ] } { "runtimeSettings": [ { "handlerSettings": { "protectedSettingsCertThumbprint": "EE22B5ECAC6A83B581A7CAC1772BEBD0E016649F", "protectedSettings": "MIIByAYJKoZIhvcNAQcDoIIBuTCCAbUCAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEC9nmIRFUMGqQOFdBaBjrNswDQYJKoZIhvcNAQEBBQAEggEAylzH/UuK3909SCbqecUyrd+V6EqTJ7Xe7hzMtYtfVTI3TBDnDlFLLzazawgXpsmOV96II9Bk4Kpo7rvwDuZWZulYWuBWw2q8/XPIpZ+hQg2TaV5A2l9N4gBU6JQ/6axHjCsuq7CUOpK9/Yq019I9HP2SqK8Ao4lMEKLR4AGMnoc+x8aKuFvhn+ClYF/75Pz+h1kgVGvj11LMM5u9M87M6Fie6UlbpFjZmBuZaPiyhfAxQxWqsJBsZ8AaUYzy21Wh3YEC54Pqx3n5kOzMH9Q+G5SkgJQ1SBEhf0gLdzeIKX2jEwOAhrL1tVtglUPxHxK5I9NACCvxou7xRtJgvbsYATBDBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECE8EEkK33SSegCCJOYYNsSXz9s87019rTptS0oBvlZgijlB+NQzvNpzAow==", "publicSettings": {"UserName":"test1234"} } } ] } https://test.blob.core.windows.net/$system/lrwinmcdn_0.0f3bfecf-f14f-4c7d-8275-9dee7310fe8c.vmSettings?sv=2018-03-28&sr=b&sk=system-1&sig=8YHwmibhasT0r9MZgL09QmFwL7ZV%2bg%2b49QP5Zwe4ksY%3d&se=9999-01-01T00%3a00%3a00Z&sp=r https://test.blob.core.windows.net/$system/lrwinmcdn_0.0f3bfecf-f14f-4c7d-8275-9dee7310fe8c.vmHealth?sv=2018-03-28&sr=b&sk=system-1&sig=DQSxfPRZEoGBGIFl%2f4bFZ0LM9RNr9DbUEmmtkiQkWkE%3d&se=9999-01-01T00%3a00%3a00Z&sp=rwWALinuxAgent-2.9.1.1/tests/data/wire/multi-config/ext_conf_with_multi_config_dependencies.xml000066400000000000000000000127431446033677600324640ustar00rootroot00000000000000 Prod http://mock-goal-state/manifest_of_ga.xml Test http://mock-goal-state/manifest_of_ga.xml eastus CRP MultipleExtensionsPerHandler https://test.blob.core.windows.net/$system/lrwinmcdn_0.0f3bfecf-f14f-4c7d-8275-9dee7310fe8c.status?sv=2018-03-28&sr=b&sk=system-1&sig=1%2b%2f4nL3kZJyUb7EKxSVGQ%2fHLpXBZxCU8Zo4diPFPv5o%3d&se=9999-01-01T00%3a00%3a00Z&sp=w { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"message": "Enabling firstExtension"} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"message": "Enabling secondExtension"} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"message": "Enabling thirdExtension"} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"message": "Enabling dependent SingleConfig extension"} } } ] } { "runtimeSettings": [ { "handlerSettings": { "publicSettings": {"message": "Enabling independent SingleConfig extension"} } } ] } https://test.blob.core.windows.net/$system/lrwinmcdn_0.0f3bfecf-f14f-4c7d-8275-9dee7310fe8c.vmSettings?sv=2018-03-28&sr=b&sk=system-1&sig=8YHwmibhasT0r9MZgL09QmFwL7ZV%2bg%2b49QP5Zwe4ksY%3d&se=9999-01-01T00%3a00%3a00Z&sp=r https://test.blob.core.windows.net/$system/lrwinmcdn_0.0f3bfecf-f14f-4c7d-8275-9dee7310fe8c.vmHealth?sv=2018-03-28&sr=b&sk=system-1&sig=DQSxfPRZEoGBGIFl%2f4bFZ0LM9RNr9DbUEmmtkiQkWkE%3d&se=9999-01-01T00%3a00%3a00Z&sp=rwWALinuxAgent-2.9.1.1/tests/data/wire/remote_access_10_accounts.xml000066400000000000000000000065631446033677600250000ustar00rootroot00000000000000 1 1 testAccount1 encryptedPasswordString 2019-01-01 Administrators RemoteDesktopUsers testAccount2 encryptedPasswordString 2019-01-01 Administrators RemoteDesktopUsers testAccount3 encryptedPasswordString 2019-01-01 Administrators RemoteDesktopUsers testAccount4 encryptedPasswordString 2019-01-01 Administrators RemoteDesktopUsers testAccount5 encryptedPasswordString 2019-01-01 Administrators RemoteDesktopUsers testAccount6 encryptedPasswordString 2019-01-01 Administrators RemoteDesktopUsers testAccount7 encryptedPasswordString 2019-01-01 Administrators RemoteDesktopUsers testAccount8 encryptedPasswordString 2019-01-01 Administrators RemoteDesktopUsers testAccount9 encryptedPasswordString 2019-01-01 Administrators RemoteDesktopUsers testAccount10 encryptedPasswordString 2019-01-01 Administrators RemoteDesktopUsers WALinuxAgent-2.9.1.1/tests/data/wire/remote_access_duplicate_accounts.xml000066400000000000000000000014501446033677600265200ustar00rootroot00000000000000 1 1 testAccount encryptedPasswordString 2019-01-01 Administrators RemoteDesktopUsers testAccount encryptedPasswordString 2019-01-01 Administrators RemoteDesktopUsers WALinuxAgent-2.9.1.1/tests/data/wire/remote_access_no_accounts.xml000066400000000000000000000002151446033677600251600ustar00rootroot00000000000000 1 1 WALinuxAgent-2.9.1.1/tests/data/wire/remote_access_single_account.xml000066400000000000000000000007401446033677600256450ustar00rootroot00000000000000 1 1 testAccount encryptedPasswordString 2019-01-01 Administrators RemoteDesktopUsers WALinuxAgent-2.9.1.1/tests/data/wire/remote_access_two_accounts.xml000066400000000000000000000014521446033677600253610ustar00rootroot00000000000000 1 1 testAccount1 encryptedPasswordString 2019-01-01 Administrators RemoteDesktopUsers testAccount2 encryptedPasswordString 2019-01-01 Administrators RemoteDesktopUsers WALinuxAgent-2.9.1.1/tests/data/wire/sample.pem000066400000000000000000000032471446033677600212230ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC3zCdThkBDYu83 M7ouc03caqyEwV6lioWbtYdnraoftbuCJrOhy+WipSCVAmhlu/tpaItuzwB9/VTw eSWfB/hB2sabVTKgU8gTQrI6ISy2ocLjqTIZuOETJuGlAIw6OXorhdUr8acZ8ohb ftZIbS9YKxbO7sQi+20sT2ugROJnO7IDGbb2vWhEhp2NAieJ8Nnq0SMv1+cZJZYk 6hiFVSl12g0egVFrRTJBvvTbPS7amLAQkauK/IxG28jZR61pMbHHX+xBg4Iayb2i qp8YnwK3qtf0stc0h9snnLnHSODva1Bo6qVBEcrkuXmtrHL2nUMsV/MgWG3HMgJJ 6Jf/wSFpAgMBAAECggEBALepsS6cvADajzK5ZPXf0NFOY6CxXnPLrWGAj5NCDftr 7bjMFbq7dngFzD46zrnClCOsDZEoF1TO3p8CYF6/Zwvfo5E7HMDrl8XvYwwFdJn3 oTlALMlZXsh1lQv+NSJFp1hwfylPbGzYV/weDeEIAkR3om4cWDCg0GJz5peb3iXK 5fimrZsnInhktloU2Ep20UepR8wbhS5WP7B2s32OULTlWiGdORUVrHJQbTN6O0NZ WzmAcsgfmW1KEBOR9sDFbAdldt8/WcLJVIfWOdFVbCbOaxrnRnZ8j8tsafziVncD QFRpNeyOHZR5S84oAPo2EIVeFCLLeo3Wit/O3IFmhhUCgYEA5jrs0VSowb/xU/Bw wm1cKnSqsub3p3GLPL4TdODYMHH56Wv8APiwcW9O1+oRZoM9M/8KXkDlfFOz10tY bMYvF8MzFKIzzi5TxaWqSWsNeXpoqtFqUed7KRh3ybncIqFAAauTwmAhAlEmGR/e AY7Oy4b2lnRU1ssIOd0VnSnAqTcCgYEAzF6746DhsInlFIQGsUZBOmUtwyu0k1kc gkWhJt5SyQHZtX1SMV2RI6CXFpUZcjv31jM30GmXdvkuj2dIHaDZB5V5BlctPJZq FH0RFxmFHXk+npLJnKKSX1H3/2PxTUsSBcFHEaPCgvIz3720bX7fqRIFtVdrcbQA cB9DARbjWl8CgYBKADyoWCbaB+EA0vLbe505RECtulF176gKgSnt0muKvsfOQFhC 06ya+WUFP4YSRjLA6MQjYYahvKG8nMoyRE1UvPhJNI2kQv3INKSUbqVpG3BTH3am Ftpebi/qliPsuZnCL60RuCZEAWNWhgisxYMwphPSblfqpl3hg290EbyMZwKBgQCs mypHQ166EozW+fcJDFQU9NVkrGoTtMR+Rj6oLEdxG037mb+sj+EAXSaeXQkj0QAt +g4eyL+zLRuk5E8lLu9+F0EjGMfNDyDC8ypW/yfNT9SSa1k6IJhNR1aUbZ2kcU3k bGwQuuWSYOttAbT8cZaHHgCSOyY03xkrmUunBOS6MwKBgBK4D0Uv7ZDf3Y38A07D MblDQj3wZeFu6IWi9nVT12U3WuEJqQqqxWnWmETa+TS/7lhd0GjTB+79+qOIhmls XSAmIS/rBUGlk5f9n+vBjQkpbqAvcXV7I/oQASpVga1xB9EuMvXc9y+x/QfmrYVM zqxRWJIMASPLiQr79V0zXGXP -----END PRIVATE KEY-----WALinuxAgent-2.9.1.1/tests/data/wire/shared_config.xml000066400000000000000000000046351446033677600225560ustar00rootroot00000000000000 WALinuxAgent-2.9.1.1/tests/data/wire/sshd_config000066400000000000000000000047771446033677600214610ustar00rootroot00000000000000# Package generated configuration file # See the sshd_config(5) manpage for details # What ports, IPs and protocols we listen for Port 22 # Use these options to restrict which interfaces/protocols sshd will bind to #ListenAddress :: #ListenAddress 0.0.0.0 Protocol 2 # HostKeys for protocol version 2 HostKey /etc/ssh/ssh_host_rsa_key HostKey /etc/ssh/ssh_host_dsa_key HostKey /etc/ssh/ssh_host_ecdsa_key HostKey /etc/ssh/ssh_host_ed25519_key #Privilege Separation is turned on for security UsePrivilegeSeparation yes # Lifetime and size of ephemeral version 1 server key KeyRegenerationInterval 3600 ServerKeyBits 1024 # Logging SyslogFacility AUTH LogLevel INFO # Authentication: LoginGraceTime 120 PermitRootLogin without-password StrictModes yes RSAAuthentication yes PubkeyAuthentication yes #AuthorizedKeysFile %h/.ssh/authorized_keys # Don't read the user's ~/.rhosts and ~/.shosts files IgnoreRhosts yes # For this to work you will also need host keys in /etc/ssh_known_hosts RhostsRSAAuthentication no # similar for protocol version 2 HostbasedAuthentication no # Uncomment if you don't trust ~/.ssh/known_hosts for RhostsRSAAuthentication #IgnoreUserKnownHosts yes # To enable empty passwords, change to yes (NOT RECOMMENDED) PermitEmptyPasswords no # Change to yes to enable challenge-response passwords (beware issues with # some PAM modules and threads) ChallengeResponseAuthentication no # Change to no to disable tunnelled clear text passwords #PasswordAuthentication yes # Kerberos options #KerberosAuthentication no #KerberosGetAFSToken no #KerberosOrLocalPasswd yes #KerberosTicketCleanup yes # GSSAPI options #GSSAPIAuthentication no #GSSAPICleanupCredentials yes X11Forwarding yes X11DisplayOffset 10 PrintMotd no PrintLastLog yes TCPKeepAlive yes #UseLogin no #MaxStartups 10:30:60 #Banner /etc/issue.net # Allow client to pass locale environment variables AcceptEnv LANG LC_* Subsystem sftp /usr/lib/openssh/sftp-server # Set this to 'yes' to enable PAM authentication, account processing, # and session processing. If this is enabled, PAM authentication will # be allowed through the ChallengeResponseAuthentication and # PasswordAuthentication. Depending on your PAM configuration, # PAM authentication via ChallengeResponseAuthentication may bypass # the setting of "PermitRootLogin without-password". # If you just want the PAM account and session checks to run without # PAM authentication, then enable this but set PasswordAuthentication # and ChallengeResponseAuthentication to 'no'. UsePAM yes Match group root WALinuxAgent-2.9.1.1/tests/data/wire/trans_cert000066400000000000000000000021471446033677600213240ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDEzCCAfugAwIBAgIUDcHXiRT74wOkLZYnyoZibT9+2G8wDQYJKoZIhvcNAQEL BQAwGTEXMBUGA1UEAwwOTGludXhUcmFuc3BvcnQwHhcNMjIwODEyMTgzMTM5WhcN MjQwODExMTgzMTM5WjAZMRcwFQYDVQQDDA5MaW51eFRyYW5zcG9ydDCCASIwDQYJ KoZIhvcNAQEBBQADggEPADCCAQoCggEBAK/XWh+Djc2WYoJ/8FkZd8OV3V47fID5 WV8hSBz/i/hVUKHhCWTQfE4VcQBGYFyK8lMKIBV7t6Bq05TQGuB8148HSjIboDx3 Ndd0C/+lYcBE1izMrHKZYhcy7lSlEUk+y5iye0cA5k/dlJhfwoxWolw0E2dMOjlY qzkEGJdyS6+hFddo696HzD7OYhxh1r50aHPWqY8NnC51487loOtPs4LYA2bd3HSg ECpOtKzyJW+GP0H2vBa7MrXrZOnD1K2j2xb8nTnYnpNtlmnZPj7VYFsLOlsq547X nFiSptPWslbVogkUVkCZlAqkMcJ/OtH70ZVjLyjFd6j7J/Wy8MrA7pECAwEAAaNT MFEwHQYDVR0OBBYEFGXBvV/uWivFWRWPHiVfY/kSJqufMB8GA1UdIwQYMBaAFGXB vV/uWivFWRWPHiVfY/kSJqufMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL BQADggEBABjatix/q90u6X/Jar/UkKiL2zx36s4huPU9KG8F51g48cYRjrvpK4+H K6avCGArl7h1gczaGS7LTOHFUU25eg/BBcKcXEO3aryQph2A167ip89UM55LxlnC QVVV9HAnEw5qAoh0wlZ65fVN+SE8FdasYlbbbp7c4At/LZruSj+IIapZDwwJxcBk YlSOa34v1Uay09+Hgu95dYQjI9txJW1ViRVlDpKbieGTzROI6s3uk+3rhxxlH2Zi Z9UqNmPfH9UE1xgSk/wkMWW22h/x51qIRKAZ4EzmdHVXdT/BarIuHxtHH8hIPNSL FjetCMVZNBej2HXL9cY5UVFYCG6JG0Q= -----END CERTIFICATE----- WALinuxAgent-2.9.1.1/tests/data/wire/trans_prv000066400000000000000000000032501446033677600211720ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCv11ofg43NlmKC f/BZGXfDld1eO3yA+VlfIUgc/4v4VVCh4Qlk0HxOFXEARmBcivJTCiAVe7egatOU 0BrgfNePB0oyG6A8dzXXdAv/pWHARNYszKxymWIXMu5UpRFJPsuYsntHAOZP3ZSY X8KMVqJcNBNnTDo5WKs5BBiXckuvoRXXaOveh8w+zmIcYda+dGhz1qmPDZwudePO 5aDrT7OC2ANm3dx0oBAqTrSs8iVvhj9B9rwWuzK162Tpw9Sto9sW/J052J6TbZZp 2T4+1WBbCzpbKueO15xYkqbT1rJW1aIJFFZAmZQKpDHCfzrR+9GVYy8oxXeo+yf1 svDKwO6RAgMBAAECggEAEwBogsNKjY7Usll09Yvk/0OwmkA/YgiP+dG04z1SONGv Vu7kfvpwlFeI0IjKXPW+3e5YLTojS7h/iLM8VEnpWVFmWSfXFvGi5ddqfIO4nnhR 1KGBeRjOGsesLYVw6sNYaPXQkImuWa8OIbEnatbp0KDn/9+i4xOL3StuJN97Ak1u Giq4gwFbag4/QctBZ+5P0t77W+uzWcvEyNgK6rndfPWxqwmJSBFchY6O3s1l6NY8 vSmyYhYRgFXEgX0nDumGfEXsF1Cj9tzYT2DUZc2f6+UCtXCD49qnoKawLhCrl5Uh QGs82TR5FSn7zLW4MbFody6p8UDw6pYiWlPPR7fmgQKBgQDO3j5RCXf0276K56BA rFpOOmivq3fxElRVCSRRRVPKHDYKQiPKnNXoa/pSl8a6CfjJaJzkNj+wTEFdQRGm Ia123kR/1S21/zgGZNmbUGby+A4fKxBY101/JQweucRN7aw3XLKPXhOL1NPyKdWh dARvjZvEl1qR6s07Y6jZgpkGqQKBgQDZmqVWvUgACdxkCYEzDf3Fc2G/8oL4VxWJ HHr5zib+DDhTfKrgQyA9CZ97stZfrR7KYnsLJH8jnj/w/CNOI0G+41KroICRsnjT 5bm7/sT5uwLwu+FAQzITiehj7Te1lwsqtS8yOnXBTQ3hzaw9yhAsuhefx+WT2UCd Y8Od13nhqQKBgQCR2LR8s71D/81F52nfTuRYNOvrtmtYpkCYt1pIhiU94EflUZ4k UhCpzb7tjh5IuZEShtPePbUHWavX0HFd/G5s2OXYbnbM0oQwVdfpnXUHpgVmyhi7 WghENN1nqDcTbha17X/ifkQvmLxZBk+chcw+zcrdfowXRkCtt2Sq/V1gCQKBgH/w UK3C9AYxxgZ7IB9oZoAk6p/0cdSZPuwydotRDdPoU2WissTQMrAwbDhKWYg/PQ84 /6b5elbywB1r4UYbrJgTB5Qo9e6zxB6xvpYtoJpDveLUVAd4eoTKXHwECPEXMVWW 2XzqqjlQmIzeZBqgJwplD2a+HNjkrvzanzS6b8qhAoGBAIun0EEc/Zc0ZxzgDPen A9/7jV++QCrNsevxGH8yrhPP4UqTVSHGR9H+RAif7zTBTn0OwzSBz6hFbPmxum3m cKabsKVN3poz3TBvfyhgjYosMWvCHpNhif09lyd/s2FezPGyK1Nyf5cKNEWjFGKw +fCPJ/Ihp4iwacNU1Pu9m050 -----END PRIVATE KEY----- WALinuxAgent-2.9.1.1/tests/data/wire/trans_pub000066400000000000000000000007031446033677600211510ustar00rootroot00000000000000-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr9daH4ONzZZign/wWRl3 w5XdXjt8gPlZXyFIHP+L+FVQoeEJZNB8ThVxAEZgXIryUwogFXu3oGrTlNAa4HzX jwdKMhugPHc113QL/6VhwETWLMyscpliFzLuVKURST7LmLJ7RwDmT92UmF/CjFai XDQTZ0w6OVirOQQYl3JLr6EV12jr3ofMPs5iHGHWvnRoc9apjw2cLnXjzuWg60+z gtgDZt3cdKAQKk60rPIlb4Y/Qfa8Frsytetk6cPUraPbFvydOdiek22Wadk+PtVg Wws6WyrnjtecWJKm09ayVtWiCRRWQJmUCqQxwn860fvRlWMvKMV3qPsn9bLwysDu kQIDAQAB -----END PUBLIC KEY----- WALinuxAgent-2.9.1.1/tests/data/wire/version_info.xml000066400000000000000000000003361446033677600224550ustar00rootroot00000000000000 2012-11-30 2010-12-15 2010-28-10 WALinuxAgent-2.9.1.1/tests/distro/000077500000000000000000000000001446033677600166565ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/distro/__init__.py000066400000000000000000000011651446033677600207720ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # WALinuxAgent-2.9.1.1/tests/distro/test_resourceDisk.py000066400000000000000000000127031446033677600227340ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # # Implements parts of RFC 2131, 1541, 1497 and # http://msdn.microsoft.com/en-us/library/cc227282%28PROT.10%29.aspx # http://msdn.microsoft.com/en-us/library/cc227259%28PROT.13%29.aspx import os import stat import sys import unittest from azurelinuxagent.common.utils import shellutil from azurelinuxagent.daemon.resourcedisk import get_resourcedisk_handler from tests.tools import AgentTestCase, patch class TestResourceDisk(AgentTestCase): def test_mkfile(self): # setup test_file = os.path.join(self.tmp_dir, 'test_file') file_size = 1024 * 128 if os.path.exists(test_file): os.remove(test_file) # execute get_resourcedisk_handler().mkfile(test_file, file_size) # assert assert os.path.exists(test_file) # only the owner should have access mode = os.stat(test_file).st_mode & ( stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) assert mode == stat.S_IRUSR | stat.S_IWUSR # cleanup os.remove(test_file) def test_mkfile_dd_fallback(self): with patch.object(shellutil, "run") as run_patch: # setup run_patch.return_value = 1 test_file = os.path.join(self.tmp_dir, 'test_file') file_size = 1024 * 128 # execute if sys.version_info >= (3, 3): with patch("os.posix_fallocate", side_effect=Exception('failure')): get_resourcedisk_handler().mkfile(test_file, file_size) else: get_resourcedisk_handler().mkfile(test_file, file_size) # assert assert run_patch.call_count > 1 assert "fallocate" in run_patch.call_args_list[0][0][0] assert "dd if" in run_patch.call_args_list[-1][0][0] def test_mkfile_xfs_fs(self): # setup test_file = os.path.join(self.tmp_dir, 'test_file') file_size = 1024 * 128 if os.path.exists(test_file): os.remove(test_file) # execute resource_disk_handler = get_resourcedisk_handler() resource_disk_handler.fs = 'xfs' with patch.object(shellutil, "run") as run_patch: resource_disk_handler.mkfile(test_file, file_size) # assert if sys.version_info >= (3, 3): with patch("os.posix_fallocate") as posix_fallocate: self.assertEqual(0, posix_fallocate.call_count) assert run_patch.call_count == 1 assert "dd if" in run_patch.call_args_list[0][0][0] def test_change_partition_type(self): resource_handler = get_resourcedisk_handler() # test when sfdisk --part-type does not exist with patch.object(shellutil, "run_get_output", side_effect=[[1, ''], [0, '']]) as run_patch: resource_handler.change_partition_type( suppress_message=True, option_str='') # assert assert run_patch.call_count == 2 assert "sfdisk --part-type" in run_patch.call_args_list[0][0][0] assert "sfdisk -c" in run_patch.call_args_list[1][0][0] # test when sfdisk --part-type exists with patch.object(shellutil, "run_get_output", side_effect=[[0, '']]) as run_patch: resource_handler.change_partition_type( suppress_message=True, option_str='') # assert assert run_patch.call_count == 1 assert "sfdisk --part-type" in run_patch.call_args_list[0][0][0] def test_check_existing_swap_file(self): test_file = os.path.join(self.tmp_dir, 'test_swap_file') file_size = 1024 * 128 if os.path.exists(test_file): os.remove(test_file) with open(test_file, "wb") as file: # pylint: disable=redefined-builtin file.write(bytearray(file_size)) os.chmod(test_file, stat.S_ISUID | stat.S_ISGID | stat.S_IRUSR | stat.S_IWUSR | stat.S_IRWXG | stat.S_IRWXO) # 0o6677 def swap_on(_): # mimic the output of "swapon -s" return [ "Filename Type Size Used Priority", "{0} partition 16498684 0 -2".format(test_file) ] with patch.object(shellutil, "run_get_output", side_effect=swap_on): get_resourcedisk_handler().check_existing_swap_file( test_file, test_file, file_size) # it should remove access from group, others mode = os.stat(test_file).st_mode & (stat.S_ISUID | stat.S_ISGID | stat.S_IRWXU | stat.S_IWUSR | stat.S_IRWXG | stat.S_IRWXO) # 0o6777 assert mode == stat.S_ISUID | stat.S_ISGID | stat.S_IRUSR | stat.S_IWUSR # 0o6600 os.remove(test_file) if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/distro/test_scvmm.py000066400000000000000000000066131446033677600214220ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # # Implements parts of RFC 2131, 1541, 1497 and # http://msdn.microsoft.com/en-us/library/cc227282%28PROT.10%29.aspx # http://msdn.microsoft.com/en-us/library/cc227259%28PROT.13%29.aspx import os import unittest import mock import azurelinuxagent.daemon.scvmm as scvmm from azurelinuxagent.common import conf from azurelinuxagent.common.osutil.default import DefaultOSUtil from azurelinuxagent.common.utils import fileutil from tests.tools import AgentTestCase, Mock, patch class TestSCVMM(AgentTestCase): def test_scvmm_detection_with_file(self): # setup conf.get_dvd_mount_point = Mock(return_value=self.tmp_dir) conf.get_detect_scvmm_env = Mock(return_value=True) scvmm_file = os.path.join(self.tmp_dir, scvmm.VMM_CONF_FILE_NAME) fileutil.write_file(scvmm_file, "") with patch.object(scvmm.ScvmmHandler, 'start_scvmm_agent') as po: with patch('os.listdir', return_value=["sr0", "sr1", "sr2"]): with patch('time.sleep', return_value=0): # execute failed = False try: scvmm.get_scvmm_handler().run() except: # pylint: disable=bare-except failed = True # assert self.assertTrue(failed) self.assertTrue(po.call_count == 1) # cleanup os.remove(scvmm_file) def test_scvmm_detection_with_multiple_cdroms(self): # setup conf.get_dvd_mount_point = Mock(return_value=self.tmp_dir) conf.get_detect_scvmm_env = Mock(return_value=True) # execute with mock.patch.object(DefaultOSUtil, 'mount_dvd') as patch_mount: with patch('os.listdir', return_value=["sr0", "sr1", "sr2"]): scvmm.ScvmmHandler().detect_scvmm_env() # assert assert patch_mount.call_count == 3 assert patch_mount.call_args_list[0][1]['dvd_device'] == '/dev/sr0' assert patch_mount.call_args_list[1][1]['dvd_device'] == '/dev/sr1' assert patch_mount.call_args_list[2][1]['dvd_device'] == '/dev/sr2' def test_scvmm_detection_without_file(self): # setup conf.get_dvd_mount_point = Mock(return_value=self.tmp_dir) conf.get_detect_scvmm_env = Mock(return_value=True) scvmm_file = os.path.join(self.tmp_dir, scvmm.VMM_CONF_FILE_NAME) if os.path.exists(scvmm_file): os.remove(scvmm_file) with mock.patch.object(scvmm.ScvmmHandler, 'start_scvmm_agent') as patch_start: # execute scvmm.ScvmmHandler().detect_scvmm_env() # assert patch_start.assert_not_called() if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/ga/000077500000000000000000000000001446033677600157415ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/ga/__init__.py000066400000000000000000000011651446033677600200550ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # WALinuxAgent-2.9.1.1/tests/ga/extension_emulator.py000066400000000000000000000337041446033677600222460ustar00rootroot00000000000000# Copyright 2020 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import json import re import uuid import os import contextlib import subprocess import azurelinuxagent.common.conf as conf from azurelinuxagent.common.future import ustr from azurelinuxagent.common.utils import fileutil from azurelinuxagent.ga.exthandlers import ExtHandlerInstance, ExtCommandEnvVariable from tests.tools import Mock, patch from tests.protocol.mockwiredata import WireProtocolData from tests.protocol.mocks import MockHttpResponse from tests.protocol.HttpRequestPredicates import HttpRequestPredicates class ExtensionCommandNames(object): INSTALL = "install" UNINSTALL = "uninstall" UPDATE = "update" ENABLE = "enable" DISABLE = "disable" class Actions(object): """ A collection of static methods providing some basic functionality for the ExtensionEmulator class' actions. """ @staticmethod def succeed_action(*_, **__): """ A nop action with the correct function signature for ExtensionEmulator actions. """ return 0 @staticmethod def generate_unique_fail(): """ Utility function for tracking the return code of a command. Returns both a unique return code, and a function pointer which returns said return code. """ return_code = str(uuid.uuid4()) def fail_action(*_, **__): return return_code return return_code, fail_action def extension_emulator(name="OSTCExtensions.ExampleHandlerLinux", version="1.0.0", update_mode="UpdateWithInstall", report_heartbeat=False, continue_on_update_failure=False, supports_multiple_extensions=False, install_action=Actions.succeed_action, uninstall_action=Actions.succeed_action, enable_action=Actions.succeed_action, disable_action=Actions.succeed_action, update_action=Actions.succeed_action): """ Factory method for ExtensionEmulator objects with sensible defaults. """ # Linter reports too many arguments, but this isn't an issue because all are defaulted; # no caller will have to actually provide all of the arguments listed. return ExtensionEmulator(name, version, update_mode, report_heartbeat, continue_on_update_failure, supports_multiple_extensions, install_action, uninstall_action, enable_action, disable_action, update_action) @contextlib.contextmanager def enable_invocations(*emulators): """ Allows ExtHandlersHandler objects to call the specified emulators and keeps track of the order of those invocations. Returns the invocation record. Note that this method patches subprocess.Popen and ExtHandlerInstance.load_manifest. """ invocation_record = InvocationRecord() patched_popen = generate_patched_popen(invocation_record, *emulators) patched_load_manifest = generate_mock_load_manifest(*emulators) with patch.object(ExtHandlerInstance, "load_manifest", patched_load_manifest): with patch("subprocess.Popen", patched_popen): yield invocation_record def generate_put_handler(*emulators): """ Create a HTTP handler to store status blobs for each provided emulator. For use with tests.protocol.mocks.mock_wire_protocol. """ def mock_put_handler(url, *args, **_): if HttpRequestPredicates.is_host_plugin_status_request(url): status_blob = WireProtocolData.get_status_blob_from_hostgaplugin_put_status_request(args[0]) else: status_blob = args[0] handler_statuses = json.loads(status_blob).get("aggregateStatus", {}).get("handlerAggregateStatus", []) for handler_status in handler_statuses: supplied_name = handler_status.get("handlerName", None) supplied_version = handler_status.get("handlerVersion", None) try: matching_ext = _first_matching_emulator(emulators, supplied_name, supplied_version) matching_ext.status_blobs.append(handler_status) except StopIteration: # Tests will want to know that the agent is running an extension they didn't specifically allocate. raise Exception("Extension running, but not present in emulators: {0}, {1}".format(supplied_name, supplied_version)) return MockHttpResponse(status=200) return mock_put_handler class InvocationRecord: def __init__(self): self._queue = [] def add(self, ext_name, ext_ver, ext_cmd): self._queue.append((ext_name, ext_ver, ext_cmd)) def compare(self, *expected_cmds): """ Verifies that any and all recorded invocations appear in the provided command list in that exact ordering. Each cmd in expected_cmds should be a tuple of the form (ExtensionEmulator, ExtensionCommandNames object). """ for (expected_ext_emulator, command_name) in expected_cmds: try: (ext_name, ext_ver, ext_cmd) = self._queue.pop(0) if not expected_ext_emulator.matches(ext_name, ext_ver) or command_name != ext_cmd: raise Exception("Unexpected invocation: have ({0}, {1}, {2}), but expected ({3}, {4}, {5})".format( ext_name, ext_ver, ext_cmd, expected_ext_emulator.name, expected_ext_emulator.version, command_name )) except IndexError: raise Exception("No more invocations recorded. Expected ({0}, {1}, {2}).".format(expected_ext_emulator.name, expected_ext_emulator.version, command_name)) if self._queue: raise Exception("Invocation recorded, but not expected: ({0}, {1}, {2})".format( *self._queue[0] )) def _first_matching_emulator(emulators, name, version): for ext in emulators: if ext.matches(name, version): return ext raise StopIteration class ExtensionEmulator: """ A wrapper class for the possible actions and options that an extension might support. """ def __init__(self, name, version, update_mode, report_heartbeat, continue_on_update_failure, supports_multiple_extensions, install_action, uninstall_action, enable_action, disable_action, update_action): # Linter reports too many arguments, but this constructor has its access mediated by # a factory method; the calls affected by the number of arguments here is very # limited in scope. self.name = name self.version = version self.update_mode = update_mode self.report_heartbeat = report_heartbeat self.continue_on_update_failure = continue_on_update_failure self.supports_multiple_extensions = supports_multiple_extensions self._actions = { ExtensionCommandNames.INSTALL: ExtensionEmulator._extend_func(install_action), ExtensionCommandNames.UNINSTALL: ExtensionEmulator._extend_func(uninstall_action), ExtensionCommandNames.UPDATE: ExtensionEmulator._extend_func(update_action), ExtensionCommandNames.ENABLE: ExtensionEmulator._extend_func(enable_action), ExtensionCommandNames.DISABLE: ExtensionEmulator._extend_func(disable_action) } self._status_blobs = [] @property def actions(self): """ A read-only property designed to allow inspection for the emulated extension's actions. `actions` maps an ExtensionCommandNames object to a mock wrapping the function this emulator was initialized with. """ return self._actions @property def status_blobs(self): """ A property for storing and retreiving the status blobs for the extension this object is emulating that are uploaded to the HTTP PUT /status endpoint. """ return self._status_blobs @staticmethod def _extend_func(func): """ Convert a function such that its returned value mimicks a Popen object (i.e. with correct return values for poll() and wait() calls). """ def wrapped_func(cmd, *args, **kwargs): return_value = func(cmd, *args, **kwargs) prefix = kwargs['env'][ExtCommandEnvVariable.ExtensionSeqNumber] if ExtCommandEnvVariable.ExtensionName in kwargs['env']: prefix = "{0}.{1}".format(kwargs['env'][ExtCommandEnvVariable.ExtensionName], prefix) status_file = os.path.join(os.path.dirname(cmd), "status", "{seq}.status".format(seq=prefix)) if return_value == 0: status_contents = [{ "status": {"status": "success"} }] else: try: ec = int(return_value) except Exception: # Error when trying to parse return_value, probably not an integer. # Failing with -1 and passing the return_value as message ec = -1 status_contents = [{"status": {"status": "error", "code": ec, "formattedMessage": {"message": return_value, "lang": "en-US"}}}] fileutil.write_file(status_file, json.dumps(status_contents)) return Mock(**{ "poll.return_value": return_value, "wait.return_value": return_value }) # Wrap the function in a mock to allow invocation reflection a la .assert_not_called(), etc. return Mock(wraps=wrapped_func) def matches(self, name, version): return self.name == name and self.version == version def generate_patched_popen(invocation_record, *emulators): """ Create a mock popen function able to invoke the proper action for an extension emulator in emulators. """ original_popen = subprocess.Popen def patched_popen(cmd, *args, **kwargs): try: handler_name, handler_version, command_name = extract_extension_info_from_command(cmd) except ValueError: return original_popen(cmd, *args, **kwargs) try: name = handler_name # MultiConfig scenario, search for full name - . if ExtCommandEnvVariable.ExtensionName in kwargs['env']: name = "{0}.{1}".format(handler_name, kwargs['env'][ExtCommandEnvVariable.ExtensionName]) invocation_record.add(name, handler_version, command_name) matching_ext = _first_matching_emulator(emulators, name, handler_version) return matching_ext.actions[command_name](cmd, *args, **kwargs) except StopIteration: raise Exception("Extension('{name}', '{version}') not listed as a parameter. Is it being emulated?".format( name=handler_name, version=handler_version )) return patched_popen def generate_mock_load_manifest(*emulators): original_load_manifest = ExtHandlerInstance.load_manifest def mock_load_manifest(self): matching_emulator = None names = [self.ext_handler.name] # Incase of MC, search for full names - . if self.supports_multi_config: names = [self.get_extension_full_name(ext) for ext in self.extensions] for name in names: try: matching_emulator = _first_matching_emulator(emulators, name, self.ext_handler.version) except StopIteration: continue else: break if matching_emulator is None: raise Exception( "Extension('{name}', '{version}') not listed as a parameter. Is it being emulated?".format( name=self.ext_handler.name, version=self.ext_handler.version)) base_manifest = original_load_manifest(self) base_manifest.data["handlerManifest"].update({ "continueOnUpdateFailure": matching_emulator.continue_on_update_failure, "reportHeartbeat": matching_emulator.report_heartbeat, "updateMode": matching_emulator.update_mode, "supportsMultipleExtensions": matching_emulator.supports_multiple_extensions }) return base_manifest return mock_load_manifest def extract_extension_info_from_command(command): """ Parse a command into a tuple of extension info. """ if not isinstance(command, (str, ustr)): raise ValueError("Cannot extract extension info from non-string commands") # Group layout of the expected command; this lets us grab what we want after a match template = r'(?<={base_dir}/)(?P{ext_name})-(?P{ext_ver})(?:/{script_file} -)(?P{ext_cmd})' base_dir_regex = conf.get_lib_dir() script_file_regex = r'[^\s]+' ext_cmd_regex = r'[a-zA-Z]+' ext_name_regex = r'[a-zA-Z]+(?:[.a-zA-Z]+)?' ext_ver_regex = r'[0-9]+(?:[.0-9]+)*' full_regex = template.format( ext_name=ext_name_regex, ext_ver=ext_ver_regex, base_dir=base_dir_regex, script_file=script_file_regex, ext_cmd=ext_cmd_regex ) match_obj = re.search(full_regex, command) if not match_obj: raise ValueError("Command does not match the desired format: {0}".format(command)) return match_obj.group('name', 'ver', 'cmd')WALinuxAgent-2.9.1.1/tests/ga/mocks.py000066400000000000000000000137641446033677600174420ustar00rootroot00000000000000 # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import contextlib from mock import PropertyMock from azurelinuxagent.ga.exthandlers import ExtHandlersHandler from azurelinuxagent.ga.remoteaccess import RemoteAccessHandler from azurelinuxagent.ga.update import UpdateHandler, get_update_handler from tests.tools import patch, Mock, mock_sleep @contextlib.contextmanager def mock_update_handler(protocol, iterations=1, on_new_iteration=lambda _: None, exthandlers_handler=None, remote_access_handler=None, autoupdate_enabled=False, check_daemon_running=False, start_background_threads=False, check_background_threads=False ): """ Creates a mock UpdateHandler that executes its main loop for the given 'iterations'. * If 'on_new_iteration' is given, it is invoked at the beginning of each iteration passing the iteration number as argument. * Network requests (e.g. requests for the goal state) are done using the given 'protocol'. * The mock UpdateHandler uses mock no-op ExtHandlersHandler and RemoteAccessHandler, unless they are given by 'exthandlers_handler' and 'remote_access_handler'. * Agent updates are disabled, unless specified otherwise by 'autoupdate_enabled'. * The check for the daemon is skipped, unless specified otherwise by 'check_daemon_running' * Background threads (monitor, env, telemetry, etc) are not started, unless specified otherwise by 'start_background_threads' * The check for background threads is skipped, unless specified otherwise by 'check_background_threads' * The UpdateHandler is augmented with these extra functions: * get_exit_code() - returns the code passed to sys.exit() when the handler exits * get_iterations() - returns the number of iterations executed by the main loop * get_iterations_completed() - returns the number of iterations of the main loop that completed execution (i.e. were not interrupted by an exception, return, etc) """ iteration_count = [0] def is_running(*args): # mock for property UpdateHandler.is_running, which controls the main loop if len(args) == 0: # getter enter_loop = iteration_count[0] < iterations if enter_loop: iteration_count[0] += 1 on_new_iteration(iteration_count[0]) return enter_loop else: # setter return None if exthandlers_handler is None: exthandlers_handler = ExtHandlersHandler(protocol) else: exthandlers_handler.protocol = protocol if remote_access_handler is None: remote_access_handler = RemoteAccessHandler(protocol) cleanup_functions = [] def patch_object(target, attribute): p = patch.object(target, attribute) p.start() cleanup_functions.insert(0, p.stop) try: with patch("azurelinuxagent.ga.exthandlers.get_exthandlers_handler", return_value=exthandlers_handler): with patch("azurelinuxagent.ga.remoteaccess.get_remote_access_handler", return_value=remote_access_handler): with patch("azurelinuxagent.common.conf.get_autoupdate_enabled", return_value=autoupdate_enabled): with patch.object(UpdateHandler, "is_running", PropertyMock(side_effect=is_running)): with patch('azurelinuxagent.ga.update.time.sleep', side_effect=lambda _: mock_sleep(0.001)) as sleep: with patch('sys.exit', side_effect=lambda _: 0) as mock_exit: if not check_daemon_running: patch_object(UpdateHandler, "_check_daemon_running") if not start_background_threads: patch_object(UpdateHandler, "_start_threads") if not check_background_threads: patch_object(UpdateHandler, "_check_threads_running") def get_exit_code(): if mock_exit.call_count == 0: raise Exception("The UpdateHandler did not exit") if mock_exit.call_count != 1: raise Exception("The UpdateHandler exited multiple times ({0})".format(mock_exit.call_count)) args, _ = mock_exit.call_args return args[0] def get_iterations(): return iteration_count[0] def get_iterations_completed(): return sleep.call_count update_handler = get_update_handler() update_handler.protocol_util.get_protocol = Mock(return_value=protocol) update_handler.get_exit_code = get_exit_code update_handler.get_iterations = get_iterations update_handler.get_iterations_completed = get_iterations_completed yield update_handler finally: for f in cleanup_functions: f() WALinuxAgent-2.9.1.1/tests/ga/test_collect_logs.py000066400000000000000000000310411446033677600220220ustar00rootroot00000000000000# Copyright 2020 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import contextlib import os from azurelinuxagent.common import logger, conf from azurelinuxagent.common.cgroup import CpuCgroup, MemoryCgroup, MetricValue from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator from azurelinuxagent.common.logger import Logger from azurelinuxagent.common.protocol.util import ProtocolUtil from azurelinuxagent.common.utils import fileutil from azurelinuxagent.ga.collect_logs import get_collect_logs_handler, is_log_collection_allowed, \ get_log_collector_monitor_handler from tests.protocol.mocks import mock_wire_protocol, MockHttpResponse from tests.protocol.HttpRequestPredicates import HttpRequestPredicates from tests.protocol.mockwiredata import DATA_FILE from tests.tools import Mock, MagicMock, patch, AgentTestCase, clear_singleton_instances, skip_if_predicate_true, \ is_python_version_26, data_dir @contextlib.contextmanager def _create_collect_logs_handler(iterations=1, cgroups_enabled=True, collect_logs_conf=True): """ Creates an instance of CollectLogsHandler that * Uses a mock_wire_protocol for network requests, * Runs its main loop only the number of times given in the 'iterations' parameter, and * Does not sleep at the end of each iteration The returned CollectLogsHandler is augmented with 2 methods: * get_mock_wire_protocol() - returns the mock protocol * run_and_wait() - invokes run() and wait() on the CollectLogsHandler """ with mock_wire_protocol(DATA_FILE) as protocol: protocol_util = MagicMock() protocol_util.get_protocol = Mock(return_value=protocol) with patch("azurelinuxagent.ga.collect_logs.get_protocol_util", return_value=protocol_util): with patch("azurelinuxagent.ga.collect_logs.CollectLogsHandler.stopped", side_effect=[False] * iterations + [True]): with patch("time.sleep"): # Grab the singleton to patch it cgroups_configurator_singleton = CGroupConfigurator.get_instance() with patch.object(cgroups_configurator_singleton, "enabled", return_value=cgroups_enabled): with patch("azurelinuxagent.ga.collect_logs.conf.get_collect_logs", return_value=collect_logs_conf): def run_and_wait(): collect_logs_handler.run() collect_logs_handler.join() collect_logs_handler = get_collect_logs_handler() collect_logs_handler.get_mock_wire_protocol = lambda: protocol collect_logs_handler.run_and_wait = run_and_wait yield collect_logs_handler @skip_if_predicate_true(is_python_version_26, "Disabled on Python 2.6") class TestCollectLogs(AgentTestCase, HttpRequestPredicates): def setUp(self): AgentTestCase.setUp(self) prefix = "UnitTest" logger.DEFAULT_LOGGER = Logger(prefix=prefix) self.archive_path = os.path.join(self.tmp_dir, "logs.zip") self.mock_archive_path = patch("azurelinuxagent.ga.collect_logs.COMPRESSED_ARCHIVE_PATH", self.archive_path) self.mock_archive_path.start() self.logger_path = os.path.join(self.tmp_dir, "waagent.log") self.mock_logger_path = patch.object(conf, "get_agent_log_file", return_value=self.logger_path) self.mock_logger_path.start() # Since ProtocolUtil is a singleton per thread, we need to clear it to ensure that the test cases do not # reuse a previous state clear_singleton_instances(ProtocolUtil) def tearDown(self): if os.path.exists(self.archive_path): os.remove(self.archive_path) self.mock_archive_path.stop() if os.path.exists(self.logger_path): os.remove(self.logger_path) self.mock_logger_path.stop() AgentTestCase.tearDown(self) def _create_dummy_archive(self, size=1024): with open(self.archive_path, "wb") as f: f.truncate(size) def test_it_should_only_collect_logs_if_conditions_are_met(self): # In order to collect logs, three conditions have to be met: # 1) the flag must be set to true in the conf file # 2) cgroups must be managing services # 3) python version 2.7+ which is automatically true for these tests since they are disabled on py2.6 # cgroups not enabled, config flag false with _create_collect_logs_handler(cgroups_enabled=False, collect_logs_conf=False): self.assertEqual(False, is_log_collection_allowed(), "Log collection should not have been enabled") # cgroups enabled, config flag false with _create_collect_logs_handler(cgroups_enabled=True, collect_logs_conf=False): self.assertEqual(False, is_log_collection_allowed(), "Log collection should not have been enabled") # cgroups not enabled, config flag true with _create_collect_logs_handler(cgroups_enabled=False, collect_logs_conf=True): self.assertEqual(False, is_log_collection_allowed(), "Log collection should not have been enabled") # cgroups enabled, config flag true with _create_collect_logs_handler(cgroups_enabled=True, collect_logs_conf=True): self.assertEqual(True, is_log_collection_allowed(), "Log collection should have been enabled") def test_it_uploads_logs_when_collection_is_successful(self): archive_size = 42 def mock_run_command(*_, **__): return self._create_dummy_archive(size=archive_size) with _create_collect_logs_handler() as collect_logs_handler: with patch("azurelinuxagent.ga.collect_logs.shellutil.run_command", side_effect=mock_run_command): def http_put_handler(url, content, **__): if self.is_host_plugin_put_logs_request(url): http_put_handler.counter += 1 http_put_handler.archive = content return MockHttpResponse(status=200) return None http_put_handler.counter = 0 http_put_handler.archive = b"" protocol = collect_logs_handler.get_mock_wire_protocol() protocol.set_http_handlers(http_put_handler=http_put_handler) collect_logs_handler.run_and_wait() self.assertEqual(http_put_handler.counter, 1, "The PUT API to upload logs should have been called once") self.assertTrue(os.path.exists(self.archive_path), "The archive file should exist on disk") self.assertEqual(archive_size, len(http_put_handler.archive), "The archive file should have {0} bytes, not {1}".format(archive_size, len(http_put_handler.archive))) def test_it_does_not_upload_logs_when_collection_is_unsuccessful(self): with _create_collect_logs_handler() as collect_logs_handler: with patch("azurelinuxagent.ga.collect_logs.shellutil.run_command", side_effect=Exception("test exception")): def http_put_handler(url, _, **__): if self.is_host_plugin_put_logs_request(url): http_put_handler.counter += 1 return MockHttpResponse(status=200) return None http_put_handler.counter = 0 protocol = collect_logs_handler.get_mock_wire_protocol() protocol.set_http_handlers(http_put_handler=http_put_handler) collect_logs_handler.run_and_wait() self.assertFalse(os.path.exists(self.archive_path), "The archive file should not exist on disk") self.assertEqual(http_put_handler.counter, 0, "The PUT API to upload logs shouldn't have been called") @contextlib.contextmanager def _create_log_collector_monitor_handler(iterations=1): """ Creates an instance of LogCollectorMonitorHandler that * Runs its main loop only the number of times given in the 'iterations' parameter, and * Does not sleep at the end of each iteration The returned CollectLogsHandler is augmented with 2 methods: * run_and_wait() - invokes run() and wait() on the CollectLogsHandler """ with patch("azurelinuxagent.ga.collect_logs.LogCollectorMonitorHandler.stopped", side_effect=[False] * iterations + [True]): with patch("time.sleep"): original_read_file = fileutil.read_file def mock_read_file(filepath, **args): if filepath == "/proc/stat": filepath = os.path.join(data_dir, "cgroups", "proc_stat_t0") elif filepath.endswith("/cpuacct.stat"): filepath = os.path.join(data_dir, "cgroups", "cpuacct.stat_t0") return original_read_file(filepath, **args) with patch("azurelinuxagent.common.utils.fileutil.read_file", side_effect=mock_read_file): def run_and_wait(): monitor_log_collector.run() monitor_log_collector.join() cgroups = [ CpuCgroup("test", "dummy_cpu_path"), MemoryCgroup("test", "dummy_memory_path") ] monitor_log_collector = get_log_collector_monitor_handler(cgroups) monitor_log_collector.run_and_wait = run_and_wait yield monitor_log_collector class TestLogCollectorMonitorHandler(AgentTestCase): @patch('azurelinuxagent.common.event.EventLogger.add_metric') @patch("azurelinuxagent.ga.collect_logs.LogCollectorMonitorHandler._poll_resource_usage") def test_send_extension_metrics_telemetry(self, patch_poll_resource_usage, patch_add_metric): with _create_log_collector_monitor_handler() as log_collector_monitor_handler: patch_poll_resource_usage.return_value = [MetricValue("Process", "% Processor Time", "service", 1), MetricValue("Process", "Throttled Time", "service", 1), MetricValue("Memory", "Total Memory Usage", "service", 1), MetricValue("Memory", "Max Memory Usage", "service", 1), MetricValue("Memory", "Swap Memory Usage", "service", 1) ] log_collector_monitor_handler.run_and_wait() self.assertEqual(1, patch_poll_resource_usage.call_count) self.assertEqual(5, patch_add_metric.call_count) # Five metrics being sent. @patch("os._exit", side_effect=Exception) @patch("azurelinuxagent.ga.collect_logs.LogCollectorMonitorHandler._poll_resource_usage") def test_verify_log_collector_memory_limit_exceeded(self, patch_poll_resource_usage, mock_exit): with _create_log_collector_monitor_handler() as log_collector_monitor_handler: with patch("azurelinuxagent.common.cgroupconfigurator.LOGCOLLECTOR_MEMORY_LIMIT", 8): patch_poll_resource_usage.return_value = [MetricValue("Process", "% Processor Time", "service", 1), MetricValue("Process", "Throttled Time", "service", 1), MetricValue("Memory", "Total Memory Usage", "service", 9), MetricValue("Memory", "Max Memory Usage", "service", 7), MetricValue("Memory", "Swap Memory Usage", "service", 0) ] try: log_collector_monitor_handler.run_and_wait() except Exception: self.assertEqual(mock_exit.call_count, 1) WALinuxAgent-2.9.1.1/tests/ga/test_collect_telemetry_events.py000066400000000000000000000771611446033677600244710ustar00rootroot00000000000000# Copyright 2020 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import contextlib import glob import json import os import random import re import shutil import string import uuid from collections import defaultdict from mock import patch, MagicMock from azurelinuxagent.common import conf from azurelinuxagent.common.event import EVENTS_DIRECTORY from azurelinuxagent.common.exception import InvalidExtensionEventError, ServiceStoppedError from azurelinuxagent.common.protocol.util import ProtocolUtil from azurelinuxagent.common.telemetryevent import GuestAgentGenericLogsSchema, \ CommonTelemetryEventSchema from azurelinuxagent.common.utils import fileutil from azurelinuxagent.ga.collect_telemetry_events import ExtensionEventSchema, _ProcessExtensionEvents from tests.protocol.HttpRequestPredicates import HttpRequestPredicates from tests.tools import AgentTestCase, clear_singleton_instances, data_dir class TestExtensionTelemetryHandler(AgentTestCase, HttpRequestPredicates): _TEST_DATA_DIR = os.path.join(data_dir, "events", "extension_events") _WELL_FORMED_FILES = os.path.join(_TEST_DATA_DIR, "well_formed_files") _MALFORMED_FILES = os.path.join(_TEST_DATA_DIR, "malformed_files") _MIX_FILES = os.path.join(_TEST_DATA_DIR, "mix_files") # To make tests more versatile, include this key in a test event to mark that event as a bad event. # This event will then be skipped and will not be counted as a good event. This is purely for testing purposes, # we use the good_event_count to validate the no of events the agent actually sends to Wireserver. # Eg: { # "EventLevel": "INFO", # "Message": "Starting IaaS ScriptHandler Extension v1", # "Version": "1.2.3", # "TaskName": "Extension Info", # "EventPid": "5676", # "EventTid": "1", # "OperationId": "e1065def-7571-42c2-88a2-4f8b4c8f226d", # "TimeStamp": "2019-12-12T01:11:38.2298194Z", # "BadEvent": true # } BAD_EVENT_KEY = 'BadEvent' def setUp(self): AgentTestCase.setUp(self) clear_singleton_instances(ProtocolUtil) # Create the log directory if not exists fileutil.mkdir(conf.get_ext_log_dir()) def tearDown(self): AgentTestCase.tearDown(self) @staticmethod def _parse_file_and_count_good_events(test_events_file_path): if not os.path.exists(test_events_file_path): raise OSError("Test Events file {0} not found".format(test_events_file_path)) try: with open(test_events_file_path, "rb") as fd: event_data = fd.read().decode("utf-8") # Parse the string and get the list of events events = json.loads(event_data) if not isinstance(events, list): events = [events] except Exception as e: print("Error parsing json file: {0}".format(e)) return 0 bad_key = TestExtensionTelemetryHandler.BAD_EVENT_KEY return len([e for e in events if bad_key not in e or not e[bad_key]]) @staticmethod def _create_random_extension_events_dir_with_events(no_of_extensions, events_path, no_of_chars=10): if os.path.isdir(events_path): # If its a directory, get all files from that directory test_events_paths = glob.glob(os.path.join(events_path, "*")) else: test_events_paths = [events_path] extension_names = {} for i in range(no_of_extensions): # pylint: disable=unused-variable ext_name = "Microsoft.OSTCExtensions.{0}".format(''.join(random.sample(string.ascii_letters, no_of_chars))) no_of_good_events = 0 for test_events_file_path in test_events_paths: if not os.path.exists(test_events_file_path) or not os.path.isfile(test_events_file_path): continue no_of_good_events += TestExtensionTelemetryHandler._parse_file_and_count_good_events(test_events_file_path) events_dir = os.path.join(conf.get_ext_log_dir(), ext_name, EVENTS_DIRECTORY) fileutil.mkdir(events_dir) shutil.copy(test_events_file_path, events_dir) extension_names[ext_name] = no_of_good_events return extension_names @staticmethod def _get_no_of_events_from_body(body): return body.count("") @staticmethod def _replace_in_file(file_path, replace_from, replace_to): with open(file_path, 'r') as f: content = f.read() content = content.replace(replace_from, replace_to) with open(file_path, 'w') as f: f.write(content) @staticmethod def _get_param_from_events(event_list): for event in event_list: for param in event.parameters: yield param @staticmethod def _get_handlers_with_version(event_list): event_with_name_and_versions = defaultdict(list) for param in TestExtensionTelemetryHandler._get_param_from_events(event_list): if param.name == GuestAgentGenericLogsSchema.EventName: handler_name, version = param.value.split("-") event_with_name_and_versions[handler_name].append(version) return event_with_name_and_versions @staticmethod def _get_param_value_from_event_body_if_exists(event_list, param_name): param_values = [] for param in TestExtensionTelemetryHandler._get_param_from_events(event_list): if param.name == param_name: param_values.append(param.value) return param_values @contextlib.contextmanager def _create_extension_telemetry_processor(self, telemetry_handler=None): event_list = [] if not telemetry_handler: telemetry_handler = MagicMock(autospec=True) telemetry_handler.stopped = MagicMock(return_value=False) telemetry_handler.enqueue_event = MagicMock(wraps=event_list.append) extension_telemetry_processor = _ProcessExtensionEvents(telemetry_handler) extension_telemetry_processor.event_list = event_list yield extension_telemetry_processor def _assert_handler_data_in_event_list(self, telemetry_events, ext_names_with_count, expected_count=None): for ext_name, test_file_event_count in ext_names_with_count.items(): # If expected_count is not given, then the take the no of good events in the test file as the source of truth count = expected_count if expected_count is not None else test_file_event_count if count == 0: self.assertNotIn(ext_name, telemetry_events, "Found telemetry events for unwanted extension {0}".format(ext_name)) continue self.assertIn(ext_name, telemetry_events, "Extension name: {0} not found in the Telemetry Events".format(ext_name)) self.assertEqual(len(telemetry_events[ext_name]), count, "No of good events for ext {0} do not match".format(ext_name)) def _assert_param_in_events(self, event_list, param_key, param_value, min_count=1): count = 0 for param in TestExtensionTelemetryHandler._get_param_from_events(event_list): if param.name == param_key and param.value == param_value: count += 1 self.assertGreaterEqual(count, min_count, "'{0}: {1}' param only found {2} times in events. Min_count required: {3}".format( param_key, param_value, count, min_count)) @staticmethod def _is_string_in_event_body(event_body, expected_string): found = False for body in event_body: if expected_string in body: found = True break return found def test_it_should_not_capture_malformed_events(self): with self._create_extension_telemetry_processor() as extension_telemetry_processor: bad_name_ext_with_count = self._create_random_extension_events_dir_with_events(2, self._MALFORMED_FILES) bad_json_ext_with_count = self._create_random_extension_events_dir_with_events(2, os.path.join( self._MALFORMED_FILES, "bad_json_files", "1591816395.json")) extension_telemetry_processor.run() telemetry_events = self._get_handlers_with_version(extension_telemetry_processor.event_list) self._assert_handler_data_in_event_list(telemetry_events, bad_name_ext_with_count, expected_count=0) self._assert_handler_data_in_event_list(telemetry_events, bad_json_ext_with_count, expected_count=0) def test_it_should_capture_and_send_correct_events(self): with self._create_extension_telemetry_processor() as extension_telemetry_processor: ext_names_with_count = self._create_random_extension_events_dir_with_events(2, self._WELL_FORMED_FILES) ext_names_with_count.update(self._create_random_extension_events_dir_with_events(3, os.path.join( self._MIX_FILES, "1591835859.json"))) extension_telemetry_processor.run() telemetry_events = self._get_handlers_with_version(extension_telemetry_processor.event_list) self._assert_handler_data_in_event_list(telemetry_events, ext_names_with_count) def test_it_should_disregard_bad_events_and_keep_good_ones_in_a_mixed_file(self): with self._create_extension_telemetry_processor() as extension_telemetry_processor: extensions_with_count = self._create_random_extension_events_dir_with_events(2, self._MIX_FILES) extensions_with_count.update(self._create_random_extension_events_dir_with_events(3, os.path.join( self._MALFORMED_FILES, "bad_name_file.json"))) extension_telemetry_processor.run() telemetry_events = self._get_handlers_with_version(extension_telemetry_processor.event_list) self._assert_handler_data_in_event_list(telemetry_events, extensions_with_count) def test_it_should_limit_max_no_of_events_to_send_per_run_per_extension_and_report_event(self): max_events = 5 with patch("azurelinuxagent.ga.collect_telemetry_events.add_log_event") as mock_event: with self._create_extension_telemetry_processor() as extension_telemetry_processor: with patch.object(extension_telemetry_processor, "_MAX_NUMBER_OF_EVENTS_PER_EXTENSION_PER_PERIOD", max_events): ext_names_with_count = self._create_random_extension_events_dir_with_events(5, self._WELL_FORMED_FILES) extension_telemetry_processor.run() telemetry_events = self._get_handlers_with_version(extension_telemetry_processor.event_list) self._assert_handler_data_in_event_list(telemetry_events, ext_names_with_count, expected_count=max_events) pattern = r'Reached max count for the extension:\s*(?P.+?);\s*.+' self._assert_event_reported(mock_event, ext_names_with_count, pattern) def test_it_should_only_process_the_newer_events(self): max_events = 5 no_of_extension = 2 test_guid = str(uuid.uuid4()) with self._create_extension_telemetry_processor() as extension_telemetry_processor: with patch.object(extension_telemetry_processor, "_MAX_NUMBER_OF_EVENTS_PER_EXTENSION_PER_PERIOD", max_events): ext_names_with_count = self._create_random_extension_events_dir_with_events(no_of_extension, self._WELL_FORMED_FILES) for ext_name in ext_names_with_count.keys(): self._replace_in_file( os.path.join(conf.get_ext_log_dir(), ext_name, EVENTS_DIRECTORY, "9999999999.json"), replace_from='"{0}": ""'.format(ExtensionEventSchema.OperationId), replace_to='"{0}": "{1}"'.format(ExtensionEventSchema.OperationId, test_guid)) extension_telemetry_processor.run() telemetry_events = self._get_handlers_with_version(extension_telemetry_processor.event_list) self._assert_handler_data_in_event_list(telemetry_events, ext_names_with_count, expected_count=max_events) self._assert_param_in_events(extension_telemetry_processor.event_list, param_key=GuestAgentGenericLogsSchema.Context1, param_value="This is the latest event", min_count=no_of_extension*max_events) self._assert_param_in_events(extension_telemetry_processor.event_list, param_key=GuestAgentGenericLogsSchema.Context3, param_value=test_guid, min_count=no_of_extension*max_events) def test_it_should_parse_extension_event_irrespective_of_case(self): with self._create_extension_telemetry_processor() as extension_telemetry_processor: extensions_with_count = self._create_random_extension_events_dir_with_events(2, os.path.join( self._TEST_DATA_DIR, "different_cases")) extension_telemetry_processor.run() telemetry_events = self._get_handlers_with_version(extension_telemetry_processor.event_list) self._assert_handler_data_in_event_list(telemetry_events, extensions_with_count) def test_it_should_parse_special_chars_properly(self): with self._create_extension_telemetry_processor() as extension_telemetry_processor: extensions_with_count = self._create_random_extension_events_dir_with_events(2, os.path.join( self._TEST_DATA_DIR, "special_chars")) extension_telemetry_processor.run() telemetry_events = self._get_handlers_with_version(extension_telemetry_processor.event_list) self._assert_handler_data_in_event_list(telemetry_events, extensions_with_count) def test_it_should_parse_int_type_for_eventpid_or_eventtid_properly(self): with self._create_extension_telemetry_processor() as extension_telemetry_processor: extensions_with_count = self._create_random_extension_events_dir_with_events(2, os.path.join( self._TEST_DATA_DIR, "int_type")) extension_telemetry_processor.run() telemetry_events = self._get_handlers_with_version(extension_telemetry_processor.event_list) self._assert_handler_data_in_event_list(telemetry_events, extensions_with_count) def _setup_and_assert_tests_for_max_sizes(self, no_of_extensions=2, expected_count=None): with self._create_extension_telemetry_processor() as extension_telemetry_processor: extensions_with_count = self._create_random_extension_events_dir_with_events(no_of_extensions, os.path.join( self._TEST_DATA_DIR, "large_messages")) extension_telemetry_processor.run() telemetry_events = self._get_handlers_with_version(extension_telemetry_processor.event_list) self._assert_handler_data_in_event_list(telemetry_events, extensions_with_count, expected_count) return extensions_with_count, extension_telemetry_processor.event_list def _assert_invalid_extension_error_event_reported(self, mock_event, handler_name_with_count, error, expected_drop_count=None): self.assertTrue(mock_event.called, "Even a single event not logged") patt = r'Extension:\s+(?PMicrosoft.OSTCExtensions.+?);.+\s*Reason:\s+\[InvalidExtensionEventError\](?P.+?):.+Dropped Count:\s*(?P\d+)' for _, kwargs in mock_event.call_args_list: msg = kwargs['message'] match = re.search(patt, msg, re.MULTILINE) if match is not None: self.assertEqual(match.group("reason").strip(), error, "Incorrect error") self.assertIn(match.group("name"), handler_name_with_count, "Extension event not found") count = handler_name_with_count.pop(match.group("name")) count = expected_drop_count if expected_drop_count is not None else count self.assertEqual(int(count), int(match.group("count")), "Dropped count doesnt match") self.assertEqual(len(handler_name_with_count), 0, "All extension events not matched") def _assert_event_reported(self, mock_event, handler_name_with_count, pattern): self.assertTrue(mock_event.called, "Even a single event not logged") for _, kwargs in mock_event.call_args_list: msg = kwargs['message'] match = re.search(pattern, msg, re.MULTILINE) if match is not None: expected_handler_name = match.group("name") self.assertIn(expected_handler_name, handler_name_with_count, "Extension event not found") handler_name_with_count.pop(expected_handler_name) self.assertEqual(len(handler_name_with_count), 0, "All extension events not matched") def test_it_should_trim_message_if_more_than_limit(self): max_len = 100 no_of_extensions = 2 with patch("azurelinuxagent.ga.collect_telemetry_events._ProcessExtensionEvents._EXTENSION_EVENT_MAX_MSG_LEN", max_len): handler_name_with_count, event_list = self._setup_and_assert_tests_for_max_sizes() # pylint: disable=unused-variable context1_vals = self._get_param_value_from_event_body_if_exists(event_list, GuestAgentGenericLogsSchema.Context1) self.assertEqual(no_of_extensions, len(context1_vals), "There should be {0} Context1 values".format(no_of_extensions)) for val in context1_vals: self.assertLessEqual(len(val), max_len, "Message Length does not match") def test_it_should_skip_events_larger_than_max_size_and_report_event(self): max_size = 1000 no_of_extensions = 3 with patch("azurelinuxagent.ga.collect_telemetry_events.add_log_event") as mock_event: with patch("azurelinuxagent.ga.collect_telemetry_events._ProcessExtensionEvents._EXTENSION_EVENT_MAX_SIZE", max_size): handler_name_with_count, _ = self._setup_and_assert_tests_for_max_sizes(no_of_extensions, expected_count=0) self._assert_invalid_extension_error_event_reported(mock_event, handler_name_with_count, error=InvalidExtensionEventError.OversizeEventError) def test_it_should_skip_large_files_greater_than_max_file_size_and_report_event(self): max_file_size = 10000 no_of_extensions = 5 with patch("azurelinuxagent.ga.collect_telemetry_events.add_log_event") as mock_event: with patch("azurelinuxagent.ga.collect_telemetry_events._ProcessExtensionEvents._EXTENSION_EVENT_FILE_MAX_SIZE", max_file_size): handler_name_with_count, _ = self._setup_and_assert_tests_for_max_sizes(no_of_extensions, expected_count=0) pattern = r'Skipping file:\s*{0}/(?P.+?)/{1}.+'.format(conf.get_ext_log_dir(), EVENTS_DIRECTORY) self._assert_event_reported(mock_event, handler_name_with_count, pattern) def test_it_should_map_extension_event_json_correctly_to_telemetry_event(self): # EventName maps to HandlerName + '-' + Version from event file expected_mapping = { GuestAgentGenericLogsSchema.EventName: ExtensionEventSchema.Version, GuestAgentGenericLogsSchema.CapabilityUsed: ExtensionEventSchema.EventLevel, GuestAgentGenericLogsSchema.TaskName: ExtensionEventSchema.TaskName, GuestAgentGenericLogsSchema.Context1: ExtensionEventSchema.Message, GuestAgentGenericLogsSchema.Context2: ExtensionEventSchema.Timestamp, GuestAgentGenericLogsSchema.Context3: ExtensionEventSchema.OperationId, CommonTelemetryEventSchema.EventPid: ExtensionEventSchema.EventPid, CommonTelemetryEventSchema.EventTid: ExtensionEventSchema.EventTid } with self._create_extension_telemetry_processor() as extension_telemetry_processor: test_file = os.path.join(self._WELL_FORMED_FILES, "1592355539.json") handler_name = list(self._create_random_extension_events_dir_with_events(1, test_file))[0] extension_telemetry_processor.run() telemetry_event_map = defaultdict(list) for telemetry_event_key in expected_mapping: telemetry_event_map[telemetry_event_key] = self._get_param_value_from_event_body_if_exists( extension_telemetry_processor.event_list, telemetry_event_key) with open(test_file, 'r') as event_file: data = json.load(event_file) extension_event_map = defaultdict(list) for extension_event in data: for event_key in extension_event: extension_event_map[event_key].append(extension_event[event_key]) for telemetry_key in expected_mapping: extension_event_key = expected_mapping[telemetry_key] telemetry_data = telemetry_event_map[telemetry_key] # EventName = "HandlerName-Version" from Extensions extension_data = ["{0}-{1}".format(handler_name, v) for v in extension_event_map[ extension_event_key]] if telemetry_key == GuestAgentGenericLogsSchema.EventName else \ extension_event_map[extension_event_key] self.assertEqual(telemetry_data, extension_data, "The data for {0} and {1} doesn't map properly".format(telemetry_key, extension_event_key)) def test_it_should_always_cleanup_files_on_good_and_bad_cases(self): with self._create_extension_telemetry_processor() as extension_telemetry_processor: extensions_with_count = self._create_random_extension_events_dir_with_events(2, os.path.join( self._TEST_DATA_DIR, "large_messages")) extensions_with_count.update(self._create_random_extension_events_dir_with_events(3, self._MALFORMED_FILES)) extensions_with_count.update(self._create_random_extension_events_dir_with_events(4, self._WELL_FORMED_FILES)) extensions_with_count.update(self._create_random_extension_events_dir_with_events(1, self._MIX_FILES)) # Create random files in the events directory for each extension just to ensure that we delete them later for handler_name in extensions_with_count.keys(): file_name = os.path.join(conf.get_ext_log_dir(), handler_name, EVENTS_DIRECTORY, ''.join(random.sample(string.ascii_letters, 10))) with open(file_name, 'a') as random_file: random_file.write('1*2*3' * 100) extension_telemetry_processor.run() telemetry_events = self._get_handlers_with_version(extension_telemetry_processor.event_list) self._assert_handler_data_in_event_list(telemetry_events, extensions_with_count) for handler_name in extensions_with_count.keys(): events_path = os.path.join(conf.get_ext_log_dir(), handler_name, EVENTS_DIRECTORY) self.assertTrue(os.path.exists(events_path), "{0} dir doesn't exist".format(events_path)) self.assertEqual(0, len(os.listdir(events_path)), "There should be no files inside the events dir") def test_it_should_skip_unwanted_parameters_in_event_file(self): extra_params = ["SomethingNewButNotCool", "SomethingVeryWeird"] with self._create_extension_telemetry_processor() as extension_telemetry_processor: extensions_with_count= self._create_random_extension_events_dir_with_events(3, os.path.join( self._TEST_DATA_DIR, "extra_parameters")) extension_telemetry_processor.run() telemetry_events = self._get_handlers_with_version(extension_telemetry_processor.event_list) self._assert_handler_data_in_event_list(telemetry_events, extensions_with_count) for param in extra_params: self.assertEqual(0, len( self._get_param_value_from_event_body_if_exists(extension_telemetry_processor.event_list, extra_params)), "Unwanted param {0} found".format(param)) def test_it_should_not_send_events_which_dont_have_all_required_keys_and_report_event(self): with patch("azurelinuxagent.ga.collect_telemetry_events.add_log_event") as mock_event: with self._create_extension_telemetry_processor() as extension_telemetry_processor: extensions_with_count = self._create_random_extension_events_dir_with_events(3, os.path.join( self._TEST_DATA_DIR, "missing_parameters")) extension_telemetry_processor.run() telemetry_events = self._get_handlers_with_version(extension_telemetry_processor.event_list) self._assert_handler_data_in_event_list(telemetry_events, extensions_with_count, expected_count=0) self.assertTrue(mock_event.called, "Even a single event not logged") for _, kwargs in mock_event.call_args_list: # Example: #['Dropped events for Extension: Microsoft.OSTCExtensions.ZJQRNqbKtP; Details:', # 'Reason: [InvalidExtensionEventError] MissingKeyError: eventpid not found; Dropped Count: 2', # 'Reason: [InvalidExtensionEventError] MissingKeyError: Message not found; Dropped Count: 2', # 'Reason: [InvalidExtensionEventError] MissingKeyError: version not found; Dropped Count: 3'] msg = kwargs['message'].split("\n") ext_name = re.search(r'Dropped events for Extension:\s+(?PMicrosoft.OSTCExtensions.+?);.+', msg.pop(0)) if ext_name is not None: ext_name = ext_name.group('name') self.assertIn(ext_name, extensions_with_count, "Extension {0} not found".format(ext_name)) patt = r'\s*Reason:\s+\[InvalidExtensionEventError\](?P.+?):\s*(?P.+?)\s+not found;\s+Dropped Count:\s*(?P\d+)' expected_error_drop_count = { ExtensionEventSchema.EventPid.lower(): 2, ExtensionEventSchema.Message.lower(): 2, ExtensionEventSchema.Version.lower(): 3 } for m in msg: match = re.search(patt, m) self.assertIsNotNone(match, "No InvalidExtensionEventError errors reported") self.assertEqual(match.group("reason").strip(), InvalidExtensionEventError.MissingKeyError, "Error is not a {0}".format(InvalidExtensionEventError.MissingKeyError)) observerd_error = match.group("key").lower() self.assertIn(observerd_error, expected_error_drop_count, "Unexpected error reported") self.assertEqual(expected_error_drop_count.pop(observerd_error), int(match.group("count")), "Unequal no of dropped events") self.assertEqual(len(expected_error_drop_count), 0, "All errros not found yet") del extensions_with_count[ext_name] self.assertEqual(len(extensions_with_count), 0, "All extension events not matched") def test_it_should_not_send_event_where_message_is_empty_and_report_event(self): with patch("azurelinuxagent.ga.collect_telemetry_events.add_log_event") as mock_event: with self._create_extension_telemetry_processor() as extension_telemetry_processor: extensions_with_count = self._create_random_extension_events_dir_with_events(3, os.path.join( self._TEST_DATA_DIR, "empty_message")) extension_telemetry_processor.run() telemetry_events = self._get_handlers_with_version(extension_telemetry_processor.event_list) self._assert_handler_data_in_event_list(telemetry_events, extensions_with_count, expected_count=0) self._assert_invalid_extension_error_event_reported(mock_event, extensions_with_count, InvalidExtensionEventError.EmptyMessageError, expected_drop_count=1) def test_it_should_not_process_events_if_send_telemetry_events_handler_stopped(self): event_list = [] telemetry_handler = MagicMock(autospec=True) telemetry_handler.stopped = MagicMock(return_value=True) telemetry_handler.enqueue_event = MagicMock(wraps=event_list.append) with self._create_extension_telemetry_processor(telemetry_handler) as extension_telemetry_processor: self._create_random_extension_events_dir_with_events(3, self._WELL_FORMED_FILES) extension_telemetry_processor.run() self.assertEqual(0, len(event_list), "No events should have been enqueued") def test_it_should_not_delete_event_files_except_current_one_if_service_stopped_midway(self): event_list = [] telemetry_handler = MagicMock(autospec=True) telemetry_handler.stopped = MagicMock(return_value=False) telemetry_handler.enqueue_event = MagicMock(side_effect=ServiceStoppedError("Telemetry service stopped"), wraps=event_list.append) no_of_extensions = 3 # self._WELL_FORMED_FILES has 3 event files, i.e. total files for 3 extensions = 3 * 3 = 9 # But since we delete the file that we were processing last, expected count = 8 expected_event_file_count = 8 with self._create_extension_telemetry_processor(telemetry_handler) as extension_telemetry_processor: ext_names = self._create_random_extension_events_dir_with_events(no_of_extensions, self._WELL_FORMED_FILES) extension_telemetry_processor.run() self.assertEqual(0, len(event_list), "No events should have been enqueued") total_file_count = 0 for ext_name in ext_names: event_dir = os.path.join(conf.get_ext_log_dir(), ext_name, EVENTS_DIRECTORY) file_count = len(os.listdir(event_dir)) self.assertGreater(file_count, 0, "Some event files should still be there") total_file_count += file_count self.assertEqual(expected_event_file_count, total_file_count, "Expected File count doesn't match") WALinuxAgent-2.9.1.1/tests/ga/test_env.py000066400000000000000000000203541446033677600201460ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import datetime from azurelinuxagent.common.osutil import get_osutil from azurelinuxagent.common.osutil.default import DefaultOSUtil, shellutil from azurelinuxagent.ga.env import MonitorDhcpClientRestart from tests.tools import AgentTestCase, patch class MonitorDhcpClientRestartTestCase(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) # save the original run_command so that mocks can reference it self.shellutil_run_command = shellutil.run_command # save an instance of the original DefaultOSUtil so that mocks can reference it self.default_osutil = DefaultOSUtil() # AgentTestCase.setUp mocks osutil.factory._get_osutil; we override that mock for this class with a new mock # that always returns the default implementation. self.mock_get_osutil = patch("azurelinuxagent.common.osutil.factory._get_osutil", return_value=DefaultOSUtil()) self.mock_get_osutil.start() def tearDown(self): self.mock_get_osutil.stop() AgentTestCase.tearDown(self) def test_get_dhcp_client_pid_should_return_a_sorted_list_of_pids(self): with patch("azurelinuxagent.common.utils.shellutil.run_command", return_value="11 9 5 22 4 6"): pids = MonitorDhcpClientRestart(get_osutil())._get_dhcp_client_pid() self.assertEqual(pids, [4, 5, 6, 9, 11, 22]) def test_get_dhcp_client_pid_should_return_an_empty_list_and_log_a_warning_when_dhcp_client_is_not_running(self): with patch("azurelinuxagent.common.osutil.default.shellutil.run_command", side_effect=lambda _: self.shellutil_run_command(["pidof", "non-existing-process"])): with patch('azurelinuxagent.common.logger.Logger.warn') as mock_warn: pids = MonitorDhcpClientRestart(get_osutil())._get_dhcp_client_pid() self.assertEqual(pids, []) self.assertEqual(mock_warn.call_count, 1) args, kwargs = mock_warn.call_args # pylint: disable=unused-variable message = args[0] self.assertEqual("Dhcp client is not running.", message) def test_get_dhcp_client_pid_should_return_and_empty_list_and_log_an_error_when_an_invalid_command_is_used(self): with patch("azurelinuxagent.common.osutil.default.shellutil.run_command", side_effect=lambda _: self.shellutil_run_command(["non-existing-command"])): with patch('azurelinuxagent.common.logger.Logger.error') as mock_error: pids = MonitorDhcpClientRestart(get_osutil())._get_dhcp_client_pid() self.assertEqual(pids, []) self.assertEqual(mock_error.call_count, 1) args, kwargs = mock_error.call_args # pylint: disable=unused-variable self.assertIn("Failed to get the PID of the DHCP client", args[0]) self.assertIn("No such file or directory", args[1]) def test_get_dhcp_client_pid_should_not_log_consecutive_errors(self): monitor_dhcp_client_restart = MonitorDhcpClientRestart(get_osutil()) with patch('azurelinuxagent.common.logger.Logger.warn') as mock_warn: def assert_warnings(count): self.assertEqual(mock_warn.call_count, count) for call_args in mock_warn.call_args_list: args, _ = call_args self.assertEqual("Dhcp client is not running.", args[0]) with patch("azurelinuxagent.common.osutil.default.shellutil.run_command", side_effect=lambda _: self.shellutil_run_command(["pidof", "non-existing-process"])): # it should log the first error pids = monitor_dhcp_client_restart._get_dhcp_client_pid() self.assertEqual(pids, []) assert_warnings(1) # it should not log subsequent errors for _ in range(0, 3): pids = monitor_dhcp_client_restart._get_dhcp_client_pid() self.assertEqual(pids, []) self.assertEqual(mock_warn.call_count, 1) with patch("azurelinuxagent.common.osutil.default.shellutil.run_command", return_value="123"): # now it should succeed pids = monitor_dhcp_client_restart._get_dhcp_client_pid() self.assertEqual(pids, [123]) assert_warnings(1) with patch("azurelinuxagent.common.osutil.default.shellutil.run_command", side_effect=lambda _: self.shellutil_run_command(["pidof", "non-existing-process"])): # it should log the new error pids = monitor_dhcp_client_restart._get_dhcp_client_pid() self.assertEqual(pids, []) assert_warnings(2) # it should not log subsequent errors for _ in range(0, 3): pids = monitor_dhcp_client_restart._get_dhcp_client_pid() self.assertEqual(pids, []) self.assertEqual(mock_warn.call_count, 2) def test_handle_dhclient_restart_should_reconfigure_network_routes_when_dhcp_client_restarts(self): with patch("azurelinuxagent.common.dhcp.DhcpHandler.conf_routes") as mock_conf_routes: monitor_dhcp_client_restart = MonitorDhcpClientRestart(get_osutil()) monitor_dhcp_client_restart._period = datetime.timedelta(seconds=0) # Run the operation one time to initialize the DHCP PIDs with patch.object(monitor_dhcp_client_restart, "_get_dhcp_client_pid", return_value=[123]): monitor_dhcp_client_restart.run() # # if the dhcp client has not been restarted then it should not reconfigure the network routes # def mock_check_pid_alive(pid): if pid == 123: return True raise Exception("Unexpected PID: {0}".format(pid)) with patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.check_pid_alive", side_effect=mock_check_pid_alive): with patch.object(monitor_dhcp_client_restart, "_get_dhcp_client_pid", side_effect=Exception("get_dhcp_client_pid should not have been invoked")): monitor_dhcp_client_restart.run() self.assertEqual(mock_conf_routes.call_count, 1) # count did not change # # if the process was restarted then it should reconfigure the network routes # def mock_check_pid_alive(pid): # pylint: disable=function-redefined if pid == 123: return False raise Exception("Unexpected PID: {0}".format(pid)) with patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.check_pid_alive", side_effect=mock_check_pid_alive): with patch.object(monitor_dhcp_client_restart, "_get_dhcp_client_pid", return_value=[456, 789]): monitor_dhcp_client_restart.run() self.assertEqual(mock_conf_routes.call_count, 2) # count increased # # if the new dhcp client has not been restarted then it should not reconfigure the network routes # def mock_check_pid_alive(pid): # pylint: disable=function-redefined if pid in [456, 789]: return True raise Exception("Unexpected PID: {0}".format(pid)) with patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.check_pid_alive", side_effect=mock_check_pid_alive): with patch.object(monitor_dhcp_client_restart, "_get_dhcp_client_pid", side_effect=Exception("get_dhcp_client_pid should not have been invoked")): monitor_dhcp_client_restart.run() self.assertEqual(mock_conf_routes.call_count, 2) # count did not change WALinuxAgent-2.9.1.1/tests/ga/test_extension.py000066400000000000000000005476711446033677600214120ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import contextlib import datetime import glob import json import os.path import random import re import shutil import subprocess import tempfile import time import unittest from azurelinuxagent.common import conf from azurelinuxagent.common.agent_supported_feature import get_agent_supported_features_list_for_extensions, \ get_agent_supported_features_list_for_crp from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator from azurelinuxagent.common.datacontract import get_properties from azurelinuxagent.common.event import WALAEventOperation from azurelinuxagent.common.utils import fileutil from azurelinuxagent.common.utils.fileutil import read_file from azurelinuxagent.common.utils.flexible_version import FlexibleVersion from azurelinuxagent.common.version import PY_VERSION_MAJOR, PY_VERSION_MINOR, PY_VERSION_MICRO, AGENT_NAME, \ AGENT_VERSION from azurelinuxagent.common.exception import ResourceGoneError, ExtensionDownloadError, ProtocolError, \ ExtensionErrorCodes, ExtensionError, GoalStateAggregateStatusCodes from azurelinuxagent.common.protocol.restapi import ExtensionSettings, Extension, ExtHandlerStatus, \ ExtensionStatus, ExtensionRequestedState from azurelinuxagent.common.protocol import wire from azurelinuxagent.common.protocol.wire import WireProtocol, InVMArtifactsProfile from azurelinuxagent.common.utils.restutil import KNOWN_WIRESERVER_IP from azurelinuxagent.ga.exthandlers import ExtHandlerInstance, migrate_handler_state, \ get_exthandlers_handler, ExtCommandEnvVariable, HandlerManifest, NOT_RUN, \ ExtensionStatusValue, HANDLER_COMPLETE_NAME_PATTERN, HandlerEnvironment, GoalStateStatus from tests.protocol import mockwiredata from tests.protocol.mocks import mock_wire_protocol, MockHttpResponse from tests.protocol.HttpRequestPredicates import HttpRequestPredicates from tests.protocol.mockwiredata import DATA_FILE, DATA_FILE_EXT_ADDITIONAL_LOCATIONS from tests.tools import AgentTestCase, data_dir, MagicMock, Mock, patch, mock_sleep from tests.ga.extension_emulator import Actions, ExtensionCommandNames, extension_emulator, \ enable_invocations, generate_put_handler # Mocking the original sleep to reduce test execution time SLEEP = time.sleep SUCCESS_CODE_FROM_STATUS_FILE = 1 def do_not_run_test(): return True def raise_system_exception(): raise Exception def raise_ioerror(*args): # pylint: disable=unused-argument e = IOError() from errno import EIO e.errno = EIO raise e class TestExtensionCleanup(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) self.mock_sleep = patch("time.sleep", lambda *_: mock_sleep(0.01)) self.mock_sleep.start() def tearDown(self): self.mock_sleep.stop() AgentTestCase.tearDown(self) @staticmethod def _count_packages(): return len(glob.glob(os.path.join(conf.get_lib_dir(), "*.zip"))) @staticmethod def _count_extension_directories(): paths = [os.path.join(conf.get_lib_dir(), p) for p in os.listdir(conf.get_lib_dir())] return len([p for p in paths if os.path.isdir(p) and TestExtensionCleanup._is_extension_dir(p)]) @staticmethod def _is_extension_dir(path): return re.match(HANDLER_COMPLETE_NAME_PATTERN, os.path.basename(path)) is not None def _assert_ext_handler_status(self, aggregate_status, expected_status, version, expected_ext_handler_count=0, verify_ext_reported=True): self.assertIsNotNone(aggregate_status, "Aggregate status should not be None") handler_statuses = aggregate_status['aggregateStatus']['handlerAggregateStatus'] self.assertEqual(expected_ext_handler_count, len(handler_statuses), "All ExtensionHandlers: {0}".format(handler_statuses)) for ext_handler_status in handler_statuses: debug_info = "ExtensionHandler: {0}".format(ext_handler_status) self.assertEqual(expected_status, ext_handler_status['status'], debug_info) self.assertEqual(version, ext_handler_status['handlerVersion'], debug_info) if verify_ext_reported: self.assertIn("runtimeSettingsStatus", ext_handler_status, debug_info) return @contextlib.contextmanager def _setup_test_env(self, test_data): with mock_wire_protocol(test_data) as protocol: def mock_http_put(url, *args, **_): if HttpRequestPredicates.is_host_plugin_status_request(url): # Skip reading the HostGA request data as its encoded return MockHttpResponse(status=500) protocol.aggregate_status = json.loads(args[0]) return MockHttpResponse(status=201) protocol.aggregate_status = None protocol.set_http_handlers(http_put_handler=mock_http_put) no_of_extensions = protocol.mock_wire_data.get_no_of_plugins_in_extension_config() exthandlers_handler = get_exthandlers_handler(protocol) yield exthandlers_handler, protocol, no_of_extensions def test_cleanup_leaves_installed_extensions(self): with self._setup_test_env(mockwiredata.DATA_FILE_MULTIPLE_EXT) as (exthandlers_handler, protocol, no_of_exts): exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(no_of_exts, TestExtensionCleanup._count_extension_directories(), "No of extension directories doesnt match the no of extensions in GS") self._assert_ext_handler_status(protocol.aggregate_status, "Ready", expected_ext_handler_count=no_of_exts, version="1.0.0") def test_cleanup_removes_uninstalled_extensions(self): with self._setup_test_env(mockwiredata.DATA_FILE_MULTIPLE_EXT) as (exthandlers_handler, protocol, no_of_exts): exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_ext_handler_status(protocol.aggregate_status, "Ready", expected_ext_handler_count=no_of_exts, version="1.0.0") # Update incarnation and extension config protocol.mock_wire_data.set_incarnation(2) protocol.mock_wire_data.set_extensions_config_state(ExtensionRequestedState.Uninstall) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(0, TestExtensionCleanup._count_packages(), "All packages must be deleted") self._assert_ext_handler_status(protocol.aggregate_status, "Ready", expected_ext_handler_count=0, version="1.0.0") self.assertEqual(0, TestExtensionCleanup._count_extension_directories(), "All extension directories should be removed") def test_cleanup_removes_orphaned_packages(self): data_file = mockwiredata.DATA_FILE_NO_EXT.copy() data_file["ext_conf"] = "wire/ext_conf_no_extensions-no_status_blob.xml" no_of_orphaned_packages = 5 with self._setup_test_env(data_file) as (exthandlers_handler, protocol, no_of_exts): self.assertEqual(no_of_exts, 0, "Test setup error - Extensions found in ExtConfig") # Create random extension directories for i in range(no_of_orphaned_packages): eh = Extension(name='Random.Extension.ShouldNot.Be.There') eh.version = FlexibleVersion("9.9.0") + i handler = ExtHandlerInstance(eh, "unused") os.mkdir(handler.get_base_dir()) self.assertEqual(no_of_orphaned_packages, TestExtensionCleanup._count_extension_directories(), "Test Setup error - Not enough extension directories") exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(no_of_exts, TestExtensionCleanup._count_extension_directories(), "There should be no extension directories in FS") self.assertIsNone(protocol.aggregate_status, "Since there's no ExtConfig, we shouldn't even report status as we pull status blob link from ExtConfig") def test_cleanup_leaves_failed_extensions(self): original_popen = subprocess.Popen def mock_fail_popen(*args, **kwargs): # pylint: disable=unused-argument return original_popen("fail_this_command", **kwargs) with self._setup_test_env(mockwiredata.DATA_FILE_EXT_SINGLE) as (exthandlers_handler, protocol, no_of_exts): with patch("azurelinuxagent.common.cgroupapi.subprocess.Popen", mock_fail_popen): exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_ext_handler_status(protocol.aggregate_status, "NotReady", expected_ext_handler_count=no_of_exts, version="1.0.0", verify_ext_reported=False) self.assertEqual(no_of_exts, TestExtensionCleanup._count_extension_directories(), "There should still be 1 extension directory in FS") # Update incarnation and extension config to uninstall the extension, this should delete the extension protocol.mock_wire_data.set_incarnation(2) protocol.mock_wire_data.set_extensions_config_state(ExtensionRequestedState.Uninstall) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(0, TestExtensionCleanup._count_packages(), "All packages must be deleted") self.assertEqual(0, TestExtensionCleanup._count_extension_directories(), "All extension directories should be removed") self._assert_ext_handler_status(protocol.aggregate_status, "Ready", expected_ext_handler_count=0, version="1.0.0") def test_it_should_report_and_cleanup_only_if_gs_supported(self): def assert_gs_aggregate_status(seq_no, status, code): gs_status = protocol.aggregate_status['aggregateStatus']['vmArtifactsAggregateStatus']['goalStateAggregateStatus'] self.assertEqual(gs_status['inSvdSeqNo'], seq_no, "Seq number not matching") self.assertEqual(gs_status['code'], code, "The error code not matching") self.assertEqual(gs_status['status'], status, "The status not matching") def assert_extension_seq_no(expected_seq_no): for handler_status in protocol.aggregate_status['aggregateStatus']['handlerAggregateStatus']: self.assertEqual(expected_seq_no, handler_status['runtimeSettingsStatus']['sequenceNumber'], "Sequence number mismatch") with self._setup_test_env(mockwiredata.DATA_FILE_MULTIPLE_EXT) as (exthandlers_handler, protocol, orig_no_of_exts): # Run 1 - GS has no required features and contains 5 extensions exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(orig_no_of_exts, TestExtensionCleanup._count_extension_directories(), "No of extension directories doesnt match the no of extensions in GS") self._assert_ext_handler_status(protocol.aggregate_status, "Ready", expected_ext_handler_count=orig_no_of_exts, version="1.0.0") assert_gs_aggregate_status(seq_no='1', status=GoalStateStatus.Success, code=GoalStateAggregateStatusCodes.Success) assert_extension_seq_no(expected_seq_no=0) # Run 2 - Change the GS to one with Required features not supported by the agent # This ExtensionConfig has 1 extension - ExampleHandlerLinuxWithRequiredFeatures protocol.mock_wire_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_REQUIRED_FEATURES) protocol.mock_wire_data.set_incarnation(2) protocol.mock_wire_data.set_extensions_config_sequence_number(random.randint(10, 100)) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertGreater(orig_no_of_exts, 1, "No of extensions to check should be > 1") self.assertEqual(orig_no_of_exts, TestExtensionCleanup._count_extension_directories(), "No of extension directories should not be changed") self._assert_ext_handler_status(protocol.aggregate_status, "Ready", expected_ext_handler_count=orig_no_of_exts, version="1.0.0") assert_gs_aggregate_status(seq_no='2', status=GoalStateStatus.Failed, code=GoalStateAggregateStatusCodes.GoalStateUnsupportedRequiredFeatures) # Since its an unsupported GS, we should report the last state of extensions assert_extension_seq_no(0) # assert the extension in the new Config was not reported as that GS was not executed self.assertTrue(any('ExampleHandlerLinuxWithRequiredFeatures' not in ext_handler_status['handlerName'] for ext_handler_status in protocol.aggregate_status['aggregateStatus']['handlerAggregateStatus']), "Unwanted handler found in status reporting") # Run 3 - Run a GS with no Required Features and ensure we execute all extensions properly # This ExtensionConfig has 1 extension - OSTCExtensions.ExampleHandlerLinux protocol.mock_wire_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE) protocol.mock_wire_data.set_incarnation(3) extension_seq_no = random.randint(10, 100) protocol.mock_wire_data.set_extensions_config_sequence_number(extension_seq_no) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(1, TestExtensionCleanup._count_extension_directories(), "No of extension directories should not be changed") self._assert_ext_handler_status(protocol.aggregate_status, "Ready", expected_ext_handler_count=1, version="1.0.0") assert_gs_aggregate_status(seq_no='3', status=GoalStateStatus.Success, code=GoalStateAggregateStatusCodes.Success) assert_extension_seq_no(expected_seq_no=extension_seq_no) # Only OSTCExtensions.ExampleHandlerLinux extension should be reported self.assertEqual('OSTCExtensions.ExampleHandlerLinux', protocol.aggregate_status['aggregateStatus']['handlerAggregateStatus'][0]['handlerName'], "Expected handler not found in status reporting") class TestHandlerStateMigration(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) handler_name = "Not.A.Real.Extension" handler_version = "1.2.3" self.ext_handler = Extension(handler_name) self.ext_handler.version = handler_version self.ext_handler_i = ExtHandlerInstance(self.ext_handler, "dummy protocol") self.handler_state = "Enabled" self.handler_status = ExtHandlerStatus( name=handler_name, version=handler_version, status="Ready", message="Uninteresting message") return def _prepare_handler_state(self): handler_state_path = os.path.join( self.tmp_dir, "handler_state", self.ext_handler_i.get_full_name()) os.makedirs(handler_state_path) fileutil.write_file( os.path.join(handler_state_path, "state"), self.handler_state) fileutil.write_file( os.path.join(handler_state_path, "status"), json.dumps(get_properties(self.handler_status))) return def _prepare_handler_config(self): handler_config_path = os.path.join( self.tmp_dir, self.ext_handler_i.get_full_name(), "config") os.makedirs(handler_config_path) return def test_migration_migrates(self): self._prepare_handler_state() self._prepare_handler_config() migrate_handler_state() self.assertEqual(self.ext_handler_i.get_handler_state(), self.handler_state) self.assertEqual( self.ext_handler_i.get_handler_status().status, self.handler_status.status) return def test_migration_skips_if_empty(self): self._prepare_handler_config() migrate_handler_state() self.assertFalse( os.path.isfile(os.path.join(self.ext_handler_i.get_conf_dir(), "HandlerState"))) self.assertFalse( os.path.isfile(os.path.join(self.ext_handler_i.get_conf_dir(), "HandlerStatus"))) return def test_migration_cleans_up(self): self._prepare_handler_state() self._prepare_handler_config() migrate_handler_state() self.assertFalse(os.path.isdir(os.path.join(conf.get_lib_dir(), "handler_state"))) return def test_migration_does_not_overwrite(self): self._prepare_handler_state() self._prepare_handler_config() state = "Installed" status = "NotReady" code = 1 message = "A message" self.assertNotEqual(state, self.handler_state) self.assertNotEqual(status, self.handler_status.status) self.assertNotEqual(code, self.handler_status.code) self.assertNotEqual(message, self.handler_status.message) self.ext_handler_i.set_handler_state(state) self.ext_handler_i.set_handler_status(status=status, code=code, message=message) migrate_handler_state() self.assertEqual(self.ext_handler_i.get_handler_state(), state) handler_status = self.ext_handler_i.get_handler_status() self.assertEqual(handler_status.status, status) self.assertEqual(handler_status.code, code) self.assertEqual(handler_status.message, message) return def test_set_handler_status_ignores_none_content(self): """ Validate that set_handler_status ignore cases where json.dumps returns a value of None. """ self._prepare_handler_state() self._prepare_handler_config() status = "Ready" code = 0 message = "A message" try: with patch('json.dumps', return_value=None): self.ext_handler_i.set_handler_status(status=status, code=code, message=message) except Exception as e: # pylint: disable=unused-variable self.fail("set_handler_status threw an exception") @patch("shutil.move", side_effect=Exception) def test_migration_ignores_move_errors(self, shutil_mock): # pylint: disable=unused-argument self._prepare_handler_state() self._prepare_handler_config() try: migrate_handler_state() except Exception as e: self.assertTrue(False, "Unexpected exception: {0}".format(str(e))) # pylint: disable=redundant-unittest-assert return @patch("shutil.rmtree", side_effect=Exception) def test_migration_ignores_tree_remove_errors(self, shutil_mock): # pylint: disable=unused-argument self._prepare_handler_state() self._prepare_handler_config() try: migrate_handler_state() except Exception as e: self.assertTrue(False, "Unexpected exception: {0}".format(str(e))) # pylint: disable=redundant-unittest-assert return class TestExtensionBase(AgentTestCase): def _assert_handler_status(self, report_vm_status, expected_status, expected_ext_count, version, expected_handler_name="OSTCExtensions.ExampleHandlerLinux", expected_msg=None): self.assertTrue(report_vm_status.called) args, kw = report_vm_status.call_args # pylint: disable=unused-variable vm_status = args[0] self.assertNotEqual(0, len(vm_status.vmAgent.extensionHandlers)) handler_status = next( status for status in vm_status.vmAgent.extensionHandlers if status.name == expected_handler_name) self.assertEqual(expected_status, handler_status.status, get_properties(handler_status)) self.assertEqual(expected_handler_name, handler_status.name) self.assertEqual(version, handler_status.version) self.assertEqual(expected_ext_count, len([ext_handler for ext_handler in vm_status.vmAgent.extensionHandlers if ext_handler.name == expected_handler_name and ext_handler.extension_status is not None])) if expected_msg is not None: self.assertIn(expected_msg, handler_status.message) # Deprecated. New tests should be added to the TestExtension class @patch('time.sleep', side_effect=lambda _: mock_sleep(0.001)) @patch("azurelinuxagent.common.protocol.wire.CryptUtil") @patch("azurelinuxagent.common.utils.restutil.http_get") class TestExtension_Deprecated(TestExtensionBase): def setUp(self): AgentTestCase.setUp(self) def _assert_ext_pkg_file_status(self, expected_to_be_present=True, extension_version="1.0.0", extension_handler_name="OSTCExtensions.ExampleHandlerLinux"): zip_file_format = "{0}__{1}.zip" if expected_to_be_present: self.assertIn(zip_file_format.format(extension_handler_name, extension_version), os.listdir(conf.get_lib_dir())) else: self.assertNotIn(zip_file_format.format(extension_handler_name, extension_version), os.listdir(conf.get_lib_dir())) def _assert_no_handler_status(self, report_vm_status): self.assertTrue(report_vm_status.called) args, kw = report_vm_status.call_args # pylint: disable=unused-variable vm_status = args[0] self.assertEqual(0, len(vm_status.vmAgent.extensionHandlers)) return @staticmethod def _create_mock(test_data, mock_http_get, mock_crypt_util, *_): # Mock protocol to return test data mock_http_get.side_effect = test_data.mock_http_get mock_crypt_util.side_effect = test_data.mock_crypt_util protocol = WireProtocol(KNOWN_WIRESERVER_IP) protocol.detect() protocol.report_vm_status = MagicMock() handler = get_exthandlers_handler(protocol) return handler, protocol def _set_up_update_test_and_update_gs(self, patch_command, *args): """ This helper function sets up the Update test by setting up the protocol and ext_handler and asserts the ext_handler runs fine the first time before patching a failure command for testing. :param patch_command: The patch_command to setup for failure :param args: Any additional args passed to the function, needed for creating a mock for handler and protocol :return: test_data, exthandlers_handler, protocol """ test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_EXT_SINGLE) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter # Ensure initial install and enable is successful exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(0, patch_command.call_count) self._assert_handler_status(protocol.report_vm_status, "Ready", expected_ext_count=1, version="1.0.0") self._assert_ext_status(protocol.report_vm_status, "success", 0) # Next incarnation, update version test_data.set_incarnation(2) test_data.set_extensions_config_version("1.0.1") test_data.set_manifest_version('1.0.1') protocol.client.update_goal_state() # Ensure the patched command fails patch_command.return_value = "exit 1" return test_data, exthandlers_handler, protocol @staticmethod def _create_extension_handlers_handler(protocol): handler = get_exthandlers_handler(protocol) return handler def test_ext_handler(self, *args): # Test enable scenario. test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") self._assert_ext_status(protocol.report_vm_status, "success", 0) # Test goal state not changed exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") # Test goal state changed test_data.set_incarnation(2) test_data.set_extensions_config_sequence_number(1) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") self._assert_ext_status(protocol.report_vm_status, "success", 1) # Test hotfix test_data.set_incarnation(3) test_data.set_extensions_config_version("1.1.1") test_data.set_extensions_config_sequence_number(2) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.1.1") self._assert_ext_status(protocol.report_vm_status, "success", 2) # Test upgrade test_data.set_incarnation(4) test_data.set_extensions_config_version("1.2.0") test_data.set_extensions_config_sequence_number(3) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.2.0") self._assert_ext_status(protocol.report_vm_status, "success", 3) # Test disable test_data.set_incarnation(5) test_data.set_extensions_config_state(ExtensionRequestedState.Disabled) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "NotReady", 1, "1.2.0") # Test uninstall test_data.set_incarnation(6) test_data.set_extensions_config_state(ExtensionRequestedState.Uninstall) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_no_handler_status(protocol.report_vm_status) # Test uninstall again! test_data.set_incarnation(7) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_no_handler_status(protocol.report_vm_status) def test_it_should_only_download_extension_manifest_once_per_goal_state(self, *args): def _assert_handler_status_and_manifest_download_count(protocol, test_data, manifest_count): self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") self._assert_ext_status(protocol.report_vm_status, "success", 0) self.assertEqual(test_data.call_counts['manifest.xml'], manifest_count, "We should have downloaded extension manifest {0} times".format(manifest_count)) test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() _assert_handler_status_and_manifest_download_count(protocol, test_data, 1) # Update Incarnation test_data.set_incarnation(2) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() _assert_handler_status_and_manifest_download_count(protocol, test_data, 2) def test_it_should_fail_handler_on_bad_extension_config_and_report_error(self, mock_get, mock_crypt_util, *args): invalid_config_dir = os.path.join(data_dir, "wire", "invalid_config") self.assertGreater(len(os.listdir(invalid_config_dir)), 0, "Not even a single bad config file found") for bad_config_file_path in os.listdir(invalid_config_dir): bad_conf = DATA_FILE.copy() bad_conf["ext_conf"] = os.path.join(invalid_config_dir, bad_config_file_path) test_data = mockwiredata.WireProtocolData(bad_conf) exthandlers_handler, protocol = self._create_mock(test_data, mock_get, mock_crypt_util, *args) with patch('azurelinuxagent.ga.exthandlers.add_event') as patch_add_event: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "NotReady", 0, "1.0.0") invalid_config_errors = [kw for _, kw in patch_add_event.call_args_list if kw['op'] == WALAEventOperation.InvalidExtensionConfig] self.assertEqual(1, len(invalid_config_errors), "Error not logged and reported to Kusto for {0}".format(bad_config_file_path)) def test_it_should_process_valid_extensions_if_present(self, mock_get, mock_crypt_util, *args): bad_conf = DATA_FILE.copy() bad_conf["ext_conf"] = os.path.join("wire", "ext_conf_invalid_and_valid_handlers.xml") test_data = mockwiredata.WireProtocolData(bad_conf) exthandlers_handler, protocol = self._create_mock(test_data, mock_get, mock_crypt_util, *args) exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertTrue(protocol.report_vm_status.called) args, _ = protocol.report_vm_status.call_args vm_status = args[0] expected_handlers = ["OSTCExtensions.InvalidExampleHandlerLinux", "OSTCExtensions.ValidExampleHandlerLinux"] self.assertEqual(2, len(vm_status.vmAgent.extensionHandlers)) for handler in vm_status.vmAgent.extensionHandlers: expected_status = "NotReady" if "InvalidExampleHandlerLinux" in handler.name else "Ready" expected_ext_count = 0 if "InvalidExampleHandlerLinux" in handler.name else 1 self.assertEqual(expected_status, handler.status, "Invalid status") self.assertIn(handler.name, expected_handlers, "Handler not found") self.assertEqual("1.0.0", handler.version, "Incorrect handler version") self.assertEqual(expected_ext_count, len([ext for ext in vm_status.vmAgent.extensionHandlers if ext.name == handler.name and ext.extension_status is not None]), "Incorrect extensions enabled") expected_handlers.remove(handler.name) self.assertEqual(0, len(expected_handlers), "All handlers not reported status") def test_it_should_ignore_case_when_parsing_plugin_settings(self, mock_get, mock_crypt_util, *args): test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_CASE_MISMATCH_EXT) exthandlers_handler, protocol = self._create_mock(test_data, mock_get, mock_crypt_util, *args) exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() expected_ext_handlers = ["OSTCExtensions.ExampleHandlerLinux", "Microsoft.Powershell.ExampleExtension", "Microsoft.EnterpriseCloud.Monitoring.ExampleHandlerLinux", "Microsoft.CPlat.Core.ExampleExtensionLinux", "Microsoft.OSTCExtensions.Edp.ExampleExtensionLinuxInTest"] self.assertTrue(protocol.report_vm_status.called, "Handler status not reported") args, _ = protocol.report_vm_status.call_args vm_status = args[0] self.assertEqual(len(expected_ext_handlers), len(vm_status.vmAgent.extensionHandlers), "No of Extension handlers dont match") for handler_status in vm_status.vmAgent.extensionHandlers: self.assertEqual("Ready", handler_status.status, "Handler is not Ready") self.assertIn(handler_status.name, expected_ext_handlers, "Handler not reported") self.assertEqual("1.0.0", handler_status.version, "Handler version not matching") self.assertEqual(1, len( [status for status in vm_status.vmAgent.extensionHandlers if status.name == handler_status.name]), "No settings were found for this extension") expected_ext_handlers.remove(handler_status.name) self.assertEqual(0, len(expected_ext_handlers), "All handlers not reported") def test_ext_handler_no_settings(self, *args): test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_EXT_NO_SETTINGS) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter test_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux") with enable_invocations(test_ext) as invocation_record: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 0, "1.0.0") invocation_record.compare( (test_ext, ExtensionCommandNames.INSTALL), (test_ext, ExtensionCommandNames.ENABLE) ) # Uninstall the Plugin and make sure Disable called test_data.set_incarnation(2) test_data.set_extensions_config_state(ExtensionRequestedState.Uninstall) protocol.client.update_goal_state() with enable_invocations(test_ext) as invocation_record: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertTrue(protocol.report_vm_status.called) args, _ = protocol.report_vm_status.call_args self.assertEqual(0, len(args[0].vmAgent.extensionHandlers)) invocation_record.compare( (test_ext, ExtensionCommandNames.DISABLE), (test_ext, ExtensionCommandNames.UNINSTALL) ) def test_ext_handler_no_public_settings(self, *args): test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_EXT_NO_PUBLIC) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") def test_ext_handler_no_ext(self, *args): test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_NO_EXT) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter # Assert no extension handler status exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_no_handler_status(protocol.report_vm_status) def test_ext_handler_sequencing(self, *args): # Test enable scenario. test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_EXT_SEQUENCING) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter dep_ext_level_2 = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux") dep_ext_level_1 = extension_emulator(name="OSTCExtensions.OtherExampleHandlerLinux") with enable_invocations(dep_ext_level_2, dep_ext_level_1) as invocation_record: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0", expected_handler_name="OSTCExtensions.OtherExampleHandlerLinux") self._assert_ext_status(protocol.report_vm_status, "success", 0, expected_handler_name="OSTCExtensions.OtherExampleHandlerLinux") # check handler list and dependency levels self.assertTrue(exthandlers_handler.ext_handlers is not None) self.assertTrue(exthandlers_handler.ext_handlers is not None) self.assertEqual(len(exthandlers_handler.ext_handlers), 2) self.assertEqual(1, next(handler for handler in exthandlers_handler.ext_handlers if handler.name == dep_ext_level_1.name).settings[0].dependencyLevel) self.assertEqual(2, next(handler for handler in exthandlers_handler.ext_handlers if handler.name == dep_ext_level_2.name).settings[0].dependencyLevel) # Ensure the invocation order follows the dependency levels invocation_record.compare( (dep_ext_level_1, ExtensionCommandNames.INSTALL), (dep_ext_level_1, ExtensionCommandNames.ENABLE), (dep_ext_level_2, ExtensionCommandNames.INSTALL), (dep_ext_level_2, ExtensionCommandNames.ENABLE) ) # Test goal state not changed exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0", expected_handler_name="OSTCExtensions.OtherExampleHandlerLinux") # Test goal state changed test_data.set_incarnation(2) test_data.set_extensions_config_sequence_number(1) # Swap the dependency ordering dep_ext_level_3 = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux") dep_ext_level_4 = extension_emulator(name="OSTCExtensions.OtherExampleHandlerLinux") test_data.ext_conf = test_data.ext_conf.replace("dependencyLevel=\"2\"", "dependencyLevel=\"3\"") test_data.ext_conf = test_data.ext_conf.replace("dependencyLevel=\"1\"", "dependencyLevel=\"4\"") protocol.client.update_goal_state() with enable_invocations(dep_ext_level_3, dep_ext_level_4) as invocation_record: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") self._assert_ext_status(protocol.report_vm_status, "success", 1) self.assertEqual(len(exthandlers_handler.ext_handlers), 2) self.assertEqual(3, next(handler for handler in exthandlers_handler.ext_handlers if handler.name == dep_ext_level_3.name).settings[0].dependencyLevel) self.assertEqual(4, next(handler for handler in exthandlers_handler.ext_handlers if handler.name == dep_ext_level_4.name).settings[0].dependencyLevel) # Ensure the invocation order follows the dependency levels invocation_record.compare( (dep_ext_level_3, ExtensionCommandNames.ENABLE), (dep_ext_level_4, ExtensionCommandNames.ENABLE) ) # Test disable # In the case of disable, the last extension to be enabled should be # the first extension disabled. The first extension enabled should be # the last one disabled. test_data.set_incarnation(3) test_data.set_extensions_config_state(ExtensionRequestedState.Disabled) protocol.client.update_goal_state() with enable_invocations(dep_ext_level_3, dep_ext_level_4) as invocation_record: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "NotReady", 1, "1.0.0", expected_handler_name="OSTCExtensions.OtherExampleHandlerLinux") self.assertEqual(3, next(handler for handler in exthandlers_handler.ext_handlers if handler.name == dep_ext_level_3.name).settings[0].dependencyLevel) self.assertEqual(4, next(handler for handler in exthandlers_handler.ext_handlers if handler.name == dep_ext_level_4.name).settings[0].dependencyLevel) # Ensure the invocation order follows the dependency levels invocation_record.compare( (dep_ext_level_4, ExtensionCommandNames.DISABLE), (dep_ext_level_3, ExtensionCommandNames.DISABLE) ) # Test uninstall # In the case of uninstall, the last extension to be installed should be # the first extension uninstalled. The first extension installed # should be the last one uninstalled. test_data.set_incarnation(4) test_data.set_extensions_config_state(ExtensionRequestedState.Uninstall) # Swap the dependency ordering AGAIN dep_ext_level_5 = extension_emulator(name="OSTCExtensions.OtherExampleHandlerLinux") dep_ext_level_6 = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux") test_data.ext_conf = test_data.ext_conf.replace("dependencyLevel=\"3\"", "dependencyLevel=\"6\"") test_data.ext_conf = test_data.ext_conf.replace("dependencyLevel=\"4\"", "dependencyLevel=\"5\"") protocol.client.update_goal_state() with enable_invocations(dep_ext_level_5, dep_ext_level_6) as invocation_record: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_no_handler_status(protocol.report_vm_status) self.assertEqual(len(exthandlers_handler.ext_handlers), 2) self.assertEqual(5, next(handler for handler in exthandlers_handler.ext_handlers if handler.name == dep_ext_level_5.name).settings[0].dependencyLevel) self.assertEqual(6, next(handler for handler in exthandlers_handler.ext_handlers if handler.name == dep_ext_level_6.name).settings[0].dependencyLevel) # Ensure the invocation order follows the dependency levels invocation_record.compare( (dep_ext_level_6, ExtensionCommandNames.UNINSTALL), (dep_ext_level_5, ExtensionCommandNames.UNINSTALL) ) def test_it_should_process_sequencing_properly_even_if_no_settings_for_dependent_extension( self, mock_get, mock_crypt, *args): test_data_file = DATA_FILE.copy() test_data_file["ext_conf"] = "wire/ext_conf_dependencies_with_empty_settings.xml" test_data = mockwiredata.WireProtocolData(test_data_file) exthandlers_handler, protocol = self._create_mock(test_data, mock_get, mock_crypt, *args) ext_1 = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux") ext_2 = extension_emulator(name="OSTCExtensions.OtherExampleHandlerLinux") with enable_invocations(ext_1, ext_2) as invocation_record: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() # Ensure no extension status was reported for OtherExampleHandlerLinux as no settings provided for it self._assert_handler_status(protocol.report_vm_status, "Ready", 0, "1.0.0", expected_handler_name="OSTCExtensions.OtherExampleHandlerLinux") # Ensure correct status reported back for the other extension with settings self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0", expected_handler_name="OSTCExtensions.ExampleHandlerLinux") self._assert_ext_status(protocol.report_vm_status, "success", 0, expected_handler_name="OSTCExtensions.ExampleHandlerLinux") # Ensure the invocation order follows the dependency levels invocation_record.compare( (ext_2, ExtensionCommandNames.INSTALL), (ext_2, ExtensionCommandNames.ENABLE), (ext_1, ExtensionCommandNames.INSTALL), (ext_1, ExtensionCommandNames.ENABLE) ) def test_ext_handler_sequencing_should_fail_if_handler_failed(self, mock_get, mock_crypt, *args): test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_EXT_SEQUENCING) exthandlers_handler, protocol = self._create_mock(test_data, mock_get, mock_crypt, *args) original_popen = subprocess.Popen def _assert_event_reported_only_on_incarnation_change(expected_count=1): handler_seq_reporting = [kwargs for _, kwargs in patch_add_event.call_args_list if kwargs[ 'op'] == WALAEventOperation.ExtensionProcessing and "Skipping processing of extensions since execution of dependent extension" in kwargs['message']] self.assertEqual(len(handler_seq_reporting), expected_count, "Error should be reported only on incarnation change") def mock_fail_extension_commands(args, **kwargs): if 'sample.py' in args: return original_popen("fail_this_command", **kwargs) return original_popen(args, **kwargs) with patch("subprocess.Popen", mock_fail_extension_commands): with patch('azurelinuxagent.ga.exthandlers.add_event') as patch_add_event: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "NotReady", 0, "1.0.0", expected_handler_name="OSTCExtensions.OtherExampleHandlerLinux") _assert_event_reported_only_on_incarnation_change(expected_count=1) test_data.set_incarnation(2) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() # We should report error again on incarnation change _assert_event_reported_only_on_incarnation_change(expected_count=2) # Test it recovers on a new goal state if Handler succeeds test_data.set_incarnation(3) test_data.set_extensions_config_sequence_number(1) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0", expected_handler_name="OSTCExtensions.OtherExampleHandlerLinux") self._assert_ext_status(protocol.report_vm_status, "success", 1, expected_handler_name="OSTCExtensions.OtherExampleHandlerLinux") # Update incarnation to confirm extension invocation order test_data.set_incarnation(4) protocol.client.update_goal_state() dep_ext_level_2 = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux") dep_ext_level_1 = extension_emulator(name="OSTCExtensions.OtherExampleHandlerLinux") with enable_invocations(dep_ext_level_2, dep_ext_level_1) as invocation_record: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() # check handler list and dependency levels self.assertTrue(exthandlers_handler.ext_handlers is not None) self.assertTrue(exthandlers_handler.ext_handlers is not None) self.assertEqual(len(exthandlers_handler.ext_handlers), 2) self.assertEqual(1, next(handler for handler in exthandlers_handler.ext_handlers if handler.name == dep_ext_level_1.name).settings[0].dependencyLevel) self.assertEqual(2, next(handler for handler in exthandlers_handler.ext_handlers if handler.name == dep_ext_level_2.name).settings[0].dependencyLevel) # Ensure the invocation order follows the dependency levels invocation_record.compare( (dep_ext_level_1, ExtensionCommandNames.ENABLE), (dep_ext_level_2, ExtensionCommandNames.ENABLE) ) def test_ext_handler_sequencing_default_dependency_level(self, *args): test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=unused-variable,no-value-for-parameter exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(exthandlers_handler.ext_handlers[0].settings[0].dependencyLevel, 0) self.assertEqual(exthandlers_handler.ext_handlers[0].settings[0].dependencyLevel, 0) def test_ext_handler_sequencing_invalid_dependency_level(self, *args): test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_EXT_SEQUENCING) test_data.set_incarnation(2) test_data.set_extensions_config_sequence_number(1) test_data.ext_conf = test_data.ext_conf.replace("dependencyLevel=\"1\"", "dependencyLevel=\"a6\"") test_data.ext_conf = test_data.ext_conf.replace("dependencyLevel=\"2\"", "dependencyLevel=\"5b\"") exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=unused-variable,no-value-for-parameter exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(exthandlers_handler.ext_handlers[0].settings[0].dependencyLevel, 0) self.assertEqual(exthandlers_handler.ext_handlers[0].settings[0].dependencyLevel, 0) def test_ext_handler_rollingupgrade(self, *args): # Test enable scenario. test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_EXT_ROLLINGUPGRADE) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") self._assert_ext_status(protocol.report_vm_status, "success", 0) # Test goal state changed test_data.set_incarnation(2) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") self._assert_ext_status(protocol.report_vm_status, "success", 0) # Test minor version bump test_data.set_incarnation(3) test_data.set_extensions_config_version("1.1.0") protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.1.0") self._assert_ext_status(protocol.report_vm_status, "success", 0) # Test hotfix version bump test_data.set_incarnation(4) test_data.set_extensions_config_version("1.1.1") protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.1.1") self._assert_ext_status(protocol.report_vm_status, "success", 0) # Test disable test_data.set_incarnation(5) test_data.set_extensions_config_state(ExtensionRequestedState.Disabled) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "NotReady", 1, "1.1.1") # Test uninstall test_data.set_incarnation(6) test_data.set_extensions_config_state(ExtensionRequestedState.Uninstall) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_no_handler_status(protocol.report_vm_status) # Test uninstall again! test_data.set_incarnation(7) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_no_handler_status(protocol.report_vm_status) # Test re-install test_data.set_incarnation(8) test_data.set_extensions_config_state(ExtensionRequestedState.Enabled) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.1.1") self._assert_ext_status(protocol.report_vm_status, "success", 0) # Test version bump post-re-install test_data.set_incarnation(9) test_data.set_extensions_config_version("1.2.0") protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.2.0") self._assert_ext_status(protocol.report_vm_status, "success", 0) # Test rollback test_data.set_incarnation(10) test_data.set_extensions_config_version("1.1.0") protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.1.0") self._assert_ext_status(protocol.report_vm_status, "success", 0) def test_it_should_create_extension_events_dir_and_set_handler_environment_only_if_extension_telemetry_enabled(self, *args): for enable_extensions in [False, True]: tmp_lib_dir = tempfile.mkdtemp(prefix="ExtensionEnabled{0}".format(enable_extensions)) with patch("azurelinuxagent.common.conf.get_lib_dir", return_value=tmp_lib_dir): with patch("azurelinuxagent.common.agent_supported_feature._ETPFeature.is_supported", enable_extensions): # Create new object for each run to force re-installation of extensions as we # only create handler_environment on installation test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_MULTIPLE_EXT) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") self._assert_ext_status(protocol.report_vm_status, "success", 0) for ext_handler in exthandlers_handler.ext_handlers: ehi = ExtHandlerInstance(ext_handler, protocol) self.assertEqual(enable_extensions, os.path.exists(ehi.get_extension_events_dir()), "Events directory incorrectly set") handler_env_json = ehi.get_env_file() with open(handler_env_json, 'r') as env_json: env_data = json.load(env_json) self.assertEqual(enable_extensions, HandlerEnvironment.eventsFolder in env_data[0][ HandlerEnvironment.handlerEnvironment], "eventsFolder wrongfully set in HandlerEnvironment.json file") if enable_extensions: self.assertEqual(ehi.get_extension_events_dir(), env_data[0][HandlerEnvironment.handlerEnvironment][ HandlerEnvironment.eventsFolder], "Events directory dont match") # Clean the File System for the next test run if os.path.exists(tmp_lib_dir): shutil.rmtree(tmp_lib_dir, ignore_errors=True) def test_it_should_not_delete_extension_events_directory_on_extension_uninstall(self, *args): test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter with patch("azurelinuxagent.common.agent_supported_feature._ETPFeature.is_supported", True): exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") self._assert_ext_status(protocol.report_vm_status, "success", 0) ehi = ExtHandlerInstance(exthandlers_handler.ext_handlers[0], protocol) self.assertTrue(os.path.exists(ehi.get_extension_events_dir()), "Events directory should exist") # Uninstall extensions now test_data.set_extensions_config_state(ExtensionRequestedState.Uninstall) test_data.set_incarnation(2) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertTrue(os.path.exists(ehi.get_extension_events_dir()), "Events directory should still exist") def test_it_should_uninstall_unregistered_extensions_properly(self, *args): test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") # Update version and set it to uninstall. That is how it would be propagated by CRP if a version 1.0.0 is # unregistered in PIR and a new version 1.0.1 is published. test_data.set_extensions_config_state(ExtensionRequestedState.Uninstall) test_data.set_extensions_config_version("1.0.1") # Since the installed version is not in PIR anymore, we need to also remove it from manifest file test_data.manifest = test_data.manifest.replace("1.0.0", "9.9.9") test_data.set_incarnation(2) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() args, _ = protocol.report_vm_status.call_args vm_status = args[0] self.assertEqual(0, len(vm_status.vmAgent.extensionHandlers), "The extension should not be reported as it is uninstalled") @patch('azurelinuxagent.common.errorstate.ErrorState.is_triggered') @patch('azurelinuxagent.ga.exthandlers.add_event') def test_ext_handler_report_status_permanent(self, mock_add_event, mock_error_state, *args): test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter protocol.report_vm_status = Mock(side_effect=ProtocolError) mock_error_state.return_value = True exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() args, kw = mock_add_event.call_args self.assertEqual(False, kw['is_success']) self.assertTrue("Failed to report vm agent status" in kw['message']) self.assertEqual("ReportStatusExtended", kw['op']) @patch('azurelinuxagent.ga.exthandlers.add_event') def test_ext_handler_report_status_resource_gone(self, mock_add_event, *args): test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter protocol.report_vm_status = Mock(side_effect=ResourceGoneError) exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() args, kw = mock_add_event.call_args self.assertEqual(False, kw['is_success']) self.assertTrue("ResourceGoneError" in kw['message']) self.assertEqual("ReportStatus", kw['op']) @patch('azurelinuxagent.common.errorstate.ErrorState.is_triggered') @patch('azurelinuxagent.ga.exthandlers.add_event') def test_ext_handler_download_failure_permanent_ProtocolError(self, mock_add_event, mock_error_state, *args): test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter protocol.get_goal_state().fetch_extension_manifest = Mock(side_effect=ProtocolError) mock_error_state.return_value = True exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() event_occurrences = [kw for _, kw in mock_add_event.call_args_list if "[ExtensionError] Failed to get ext handler pkgs" in kw['message']] self.assertEqual(1, len(event_occurrences)) self.assertFalse(event_occurrences[0]['is_success']) self.assertTrue("Failed to get ext handler pkgs" in event_occurrences[0]['message']) self.assertTrue("ProtocolError" in event_occurrences[0]['message']) @patch('azurelinuxagent.ga.exthandlers.fileutil') def test_ext_handler_io_error(self, mock_fileutil, *args): test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=unused-variable,no-value-for-parameter mock_fileutil.write_file.return_value = IOError("Mock IO Error") exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() def _assert_ext_status(self, vm_agent_status, expected_status, expected_seq_no, expected_handler_name="OSTCExtensions.ExampleHandlerLinux", expected_msg=None): self.assertTrue(vm_agent_status.called) args, _ = vm_agent_status.call_args vm_status = args[0] ext_status = next(handler_status.extension_status for handler_status in vm_status.vmAgent.extensionHandlers if handler_status.name == expected_handler_name) self.assertEqual(expected_status, ext_status.status) self.assertEqual(expected_seq_no, ext_status.sequenceNumber) if expected_msg is not None: self.assertIn(expected_msg, ext_status.message) def test_it_should_initialise_and_use_command_execution_log_for_extensions(self, mock_get, mock_crypt_util, *args): test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE) exthandlers_handler, protocol = self._create_mock(test_data, mock_get, mock_crypt_util, *args) exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") command_execution_log = os.path.join(conf.get_ext_log_dir(), "OSTCExtensions.ExampleHandlerLinux", "CommandExecution.log") self.assertTrue(os.path.exists(command_execution_log), "CommandExecution.log file not found") self.assertGreater(os.path.getsize(command_execution_log), 0, "The file should not be empty") def test_ext_handler_no_reporting_status(self, *args): test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") # Remove status file and re-run collecting extension status status_file = os.path.join(self.tmp_dir, "OSTCExtensions.ExampleHandlerLinux-1.0.0", "status", "0.status") self.assertTrue(os.path.isfile(status_file)) os.remove(status_file) exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") self._assert_ext_status(protocol.report_vm_status, ExtensionStatusValue.transitioning, 0, expected_msg="This status is being reported by the Guest Agent since no status " "file was reported by extension OSTCExtensions.ExampleHandlerLinux") def test_wait_for_handler_completion_no_status(self, mock_http_get, mock_crypt_util, *args): """ Testing depends-on scenario when there is no status file reported by the extension. Expected to retry and eventually report failure for all dependent extensions. """ exthandlers_handler, protocol = self._create_mock( mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_EXT_SEQUENCING), mock_http_get, mock_crypt_util, *args) original_popen = subprocess.Popen def mock_popen(cmd, *args, **kwargs): # For the purpose of this test, deleting the placeholder status file created by the agent if "sample.py" in cmd: status_path = os.path.join(kwargs['env'][ExtCommandEnvVariable.ExtensionPath], "status", "{0}.status".format(kwargs['env'][ExtCommandEnvVariable.ExtensionSeqNumber])) mock_popen.deleted_status_file = status_path if os.path.exists(status_path): os.remove(status_path) return original_popen(["echo", "Yes"], *args, **kwargs) with patch('azurelinuxagent.common.cgroupapi.subprocess.Popen', side_effect=mock_popen): with patch('azurelinuxagent.ga.exthandlers._DEFAULT_EXT_TIMEOUT_MINUTES', 0.01): exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() # The Handler Status for the base extension should be ready as it was executed successfully by the agent self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0", expected_handler_name="OSTCExtensions.OtherExampleHandlerLinux") # The extension status reported by the Handler should be transitioning since no status file was found self._assert_ext_status(protocol.report_vm_status, ExtensionStatusValue.transitioning, 0, expected_handler_name="OSTCExtensions.OtherExampleHandlerLinux", expected_msg="This status is being reported by the Guest Agent since no status " "file was reported by extension OSTCExtensions.OtherExampleHandlerLinux") # The Handler Status for the dependent extension should be NotReady as it was not executed at all # And since it was not executed, it should not report any extension status either self._assert_handler_status(protocol.report_vm_status, "NotReady", 0, "1.0.0", expected_msg="Dependent Extension OSTCExtensions.OtherExampleHandlerLinux did not reach a terminal state within the allowed timeout. Last status was {0}".format( ExtensionStatusValue.warning)) def test_it_should_not_create_placeholder_for_single_config_extensions(self, mock_http_get, mock_crypt_util, *args): original_popen = subprocess.Popen def mock_popen(cmd, *_, **kwargs): if 'env' in kwargs: if ExtensionCommandNames.ENABLE not in cmd: # To force the test extension to not create a status file on Install, changing command return original_popen(["echo", "not-enable"], *_, **kwargs) seq_no = kwargs['env'][ExtCommandEnvVariable.ExtensionSeqNumber] ext_path = kwargs['env'][ExtCommandEnvVariable.ExtensionPath] status_file_name = "{0}.status".format(seq_no) status_file = os.path.join(ext_path, "status", status_file_name) self.assertFalse(os.path.exists(status_file), "Placeholder file should not be created for single config extensions") return original_popen(cmd, *_, **kwargs) aks_test_mock = DATA_FILE.copy() aks_test_mock["ext_conf"] = "wire/ext_conf_aks_extension.xml" exthandlers_handler, protocol = self._create_mock(mockwiredata.WireProtocolData(aks_test_mock), mock_http_get, mock_crypt_util, *args) with patch('azurelinuxagent.common.cgroupapi.subprocess.Popen', side_effect=mock_popen): exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0", expected_handler_name="OSTCExtensions.ExampleHandlerLinux") self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0", expected_handler_name="Microsoft.AKS.Compute.AKS.Linux.AKSNode") self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0", expected_handler_name="Microsoft.AKS.Compute.AKS-Engine.Linux.Billing") # Extension without settings self._assert_handler_status(protocol.report_vm_status, "Ready", 0, "1.0.0", expected_handler_name="Microsoft.AKS.Compute.AKS.Linux.Billing") self._assert_ext_status(protocol.report_vm_status, ExtensionStatusValue.success, 0, expected_handler_name="OSTCExtensions.ExampleHandlerLinux", expected_msg="Enabling non-AKS") self._assert_ext_status(protocol.report_vm_status, ExtensionStatusValue.success, 0, expected_handler_name="Microsoft.AKS.Compute.AKS.Linux.AKSNode", expected_msg="Enabling AKSNode") self._assert_ext_status(protocol.report_vm_status, ExtensionStatusValue.success, 0, expected_handler_name="Microsoft.AKS.Compute.AKS-Engine.Linux.Billing", expected_msg="Enabling AKSBilling") def test_it_should_include_part_of_status_in_ext_handler_message(self, mock_http_get, mock_crypt_util, *args): """ Testing scenario when the status file is invalid, The extension status reported by the Handler should contain a fragment of status file for debugging. """ exthandlers_handler, protocol = self._create_mock( mockwiredata.WireProtocolData(mockwiredata.DATA_FILE), mock_http_get, mock_crypt_util, *args) original_popen = subprocess.Popen def mock_popen(cmd, *args, **kwargs): # For the purpose of this test, replacing the status file with file that could not be parsed if "sample.py" in cmd: status_path = os.path.join(kwargs['env'][ExtCommandEnvVariable.ExtensionPath], "status", "{0}.status".format(kwargs['env'][ExtCommandEnvVariable.ExtensionSeqNumber])) invalid_json_path = os.path.join(data_dir, "ext", "sample-status-invalid-json-format.json") if 'enable' in cmd: invalid_json = fileutil.read_file(invalid_json_path) fileutil.write_file(status_path,invalid_json) return original_popen(["echo", "Yes"], *args, **kwargs) with patch('azurelinuxagent.common.cgroupapi.subprocess.Popen', side_effect=mock_popen): exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() # The Handler Status for the base extension should be ready as it was executed successfully by the agent self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0", expected_handler_name="OSTCExtensions.ExampleHandlerLinux") # The extension status reported by the Handler should contain a fragment of status file for # debugging. The uniqueMachineId tag comes from status file self._assert_ext_status(protocol.report_vm_status, ExtensionStatusValue.error, 0, expected_handler_name="OSTCExtensions.ExampleHandlerLinux", expected_msg="\"uniqueMachineId\": \"e5e5602b-48a6-4c35-9f96-752043777af1\"") def test_wait_for_handler_completion_success_status(self, mock_http_get, mock_crypt_util, *args): """ Testing depends-on scenario on a successful case. Expected to report the status for both extensions properly. """ exthandlers_handler, protocol = self._create_mock( mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_EXT_SEQUENCING), mock_http_get, mock_crypt_util, *args) exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0", expected_handler_name="OSTCExtensions.OtherExampleHandlerLinux", expected_msg='Plugin enabled') # The extension status reported by the Handler should be an error since no status file was found self._assert_ext_status(protocol.report_vm_status, ExtensionStatusValue.success, 0, expected_handler_name="OSTCExtensions.OtherExampleHandlerLinux") # The Handler Status for the dependent extension should be NotReady as it was not executed at all # And since it was not executed, it should not report any extension status either self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0", expected_msg='Plugin enabled') self._assert_ext_status(protocol.report_vm_status, ExtensionStatusValue.success, 0) def test_wait_for_handler_completion_error_status(self, mock_http_get, mock_crypt_util, *args): """ Testing wait_for_handler_completion() when there is error status. Expected to return False. """ exthandlers_handler, protocol = self._create_mock( mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_EXT_SEQUENCING), mock_http_get, mock_crypt_util, *args) original_popen = subprocess.Popen def mock_popen(cmd, *args, **kwargs): # For the purpose of this test, deleting the placeholder status file created by the agent if "sample.py" in cmd: return original_popen(["/fail/this/command"], *args, **kwargs) return original_popen(cmd, *args, **kwargs) with patch('azurelinuxagent.common.cgroupapi.subprocess.Popen', side_effect=mock_popen): exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() # The Handler Status for the base extension should be NotReady as it failed self._assert_handler_status(protocol.report_vm_status, "NotReady", 0, "1.0.0", expected_handler_name="OSTCExtensions.OtherExampleHandlerLinux") # The Handler Status for the dependent extension should be NotReady as it was not executed at all # And since it was not executed, it should not report any extension status either self._assert_handler_status(protocol.report_vm_status, "NotReady", 0, "1.0.0", expected_msg='Skipping processing of extensions since execution of dependent extension OSTCExtensions.OtherExampleHandlerLinux failed') def test_get_ext_handling_status(self, *args): """ Testing get_ext_handling_status() function with various cases and verifying against the expected values """ test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=unused-variable,no-value-for-parameter handler_name = "Handler" exthandler = Extension(name=handler_name) extension = ExtensionSettings(name=handler_name) exthandler.settings.append(extension) # In the following list of test cases, the first element corresponds to seq_no. # the second element is the status file name, the third element indicates if the status file exits or not. # The fourth element is the expected value from get_ext_handling_status() test_cases = [ [-5, None, False, None], [-1, None, False, None], [0, None, False, None], [0, "filename", False, "warning"], [0, "filename", True, ExtensionStatus(status="success")], [5, "filename", False, "warning"], [5, "filename", True, ExtensionStatus(status="success")] ] orig_state = os.path.exists for case in test_cases: ext_handler_i = ExtHandlerInstance(exthandler, protocol) ext_handler_i.get_status_file_path = MagicMock(return_value=(case[0], case[1])) os.path.exists = MagicMock(return_value=case[2]) if case[2]: # when the status file exists, it is expected return the value from collect_ext_status() ext_handler_i.collect_ext_status = MagicMock(return_value=case[3]) status = ext_handler_i.get_ext_handling_status(extension) if case[2]: self.assertEqual(status, case[3].status) else: self.assertEqual(status, case[3]) os.path.exists = orig_state def test_is_ext_handling_complete(self, *args): """ Testing is_ext_handling_complete() with various input and verifying against the expected output values. """ test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=unused-variable,no-value-for-parameter handler_name = "Handler" exthandler = Extension(name=handler_name) extension = ExtensionSettings(name=handler_name) exthandler.settings.append(extension) ext_handler_i = ExtHandlerInstance(exthandler, protocol) # Testing no status case ext_handler_i.get_ext_handling_status = MagicMock(return_value=None) completed, status = ext_handler_i.is_ext_handling_complete(extension) self.assertTrue(completed) self.assertEqual(status, None) # Here the key represents the possible input value to is_ext_handling_complete() # the value represents the output tuple from is_ext_handling_complete() expected_results = { "error": (True, "error"), "success": (True, "success"), "warning": (False, "warning"), "transitioning": (False, "transitioning") } for key in expected_results.keys(): ext_handler_i.get_ext_handling_status = MagicMock(return_value=key) completed, status = ext_handler_i.is_ext_handling_complete(extension) self.assertEqual(completed, expected_results[key][0]) self.assertEqual(status, expected_results[key][1]) def test_ext_handler_version_decide_autoupgrade_internalversion(self, *args): for internal in [False, True]: for autoupgrade in [False, True]: if internal: config_version = '1.3.0' decision_version = '1.3.0' if autoupgrade: datafile = mockwiredata.DATA_FILE_EXT_AUTOUPGRADE_INTERNALVERSION else: datafile = mockwiredata.DATA_FILE_EXT_INTERNALVERSION else: config_version = '1.0.0' decision_version = '1.0.0' if autoupgrade: datafile = mockwiredata.DATA_FILE_EXT_AUTOUPGRADE else: datafile = mockwiredata.DATA_FILE _, protocol = self._create_mock(mockwiredata.WireProtocolData(datafile), *args) # pylint: disable=no-value-for-parameter ext_handlers = protocol.get_goal_state().extensions_goal_state.extensions self.assertEqual(1, len(ext_handlers)) ext_handler = ext_handlers[0] self.assertEqual('OSTCExtensions.ExampleHandlerLinux', ext_handler.name) self.assertEqual(config_version, ext_handler.version, "config version.") ExtHandlerInstance(ext_handler, protocol).decide_version() self.assertEqual(decision_version, ext_handler.version, "decision version.") def test_ext_handler_version_decide_between_minor_versions(self, *args): """ Using v2.x~v4.x for unit testing Available versions via manifest XML (I stands for internal): 2.0.0, 2.1.0, 2.1.1, 2.2.0, 2.3.0(I), 2.4.0(I), 3.0, 3.1, 4.0.0.0, 4.0.0.1, 4.1.0.0 See tests/data/wire/manifest.xml for possible versions """ # (installed_version, config_version, exptected_version, autoupgrade_expected_version) cases = [ (None, '2.0', '2.0.0'), (None, '2.0.0', '2.0.0'), ('1.0', '1.0.0', '1.0.0'), (None, '2.1.0', '2.1.0'), (None, '2.1.1', '2.1.1'), (None, '2.2.0', '2.2.0'), (None, '2.3.0', '2.3.0'), (None, '2.4.0', '2.4.0'), (None, '3.0', '3.0'), (None, '3.1', '3.1'), (None, '4.0', '4.0.0.1'), (None, '4.1', '4.1.0.0'), ] _, protocol = self._create_mock(mockwiredata.WireProtocolData(mockwiredata.DATA_FILE), *args) # pylint: disable=no-value-for-parameter version_uri = 'http://mock-goal-state/Microsoft.OSTCExtensions_ExampleHandlerLinux_asiaeast_manifest.xml' for (installed_version, config_version, expected_version) in cases: ext_handler = Mock() ext_handler.properties = Mock() ext_handler.name = 'OSTCExtensions.ExampleHandlerLinux' ext_handler.manifest_uris = [version_uri] ext_handler.version = config_version ext_handler_instance = ExtHandlerInstance(ext_handler, protocol) ext_handler_instance.get_installed_version = Mock(return_value=installed_version) ext_handler_instance.decide_version() self.assertEqual(expected_version, ext_handler.version) @patch('azurelinuxagent.common.conf.get_extensions_enabled', return_value=False) def test_extensions_disabled(self, _, *args): # test status is reported for no extensions test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_NO_EXT) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_no_handler_status(protocol.report_vm_status) # test status is reported, but extensions are not processed test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_no_handler_status(protocol.report_vm_status) def test_extensions_deleted(self, *args): # Ensure initial enable is successful test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_EXT_DELETION) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") self._assert_ext_status(protocol.report_vm_status, "success", 0) # Update incarnation, simulate new extension version and old one deleted test_data.set_incarnation(2) test_data.set_extensions_config_version("1.0.1") test_data.set_manifest_version('1.0.1') protocol.client.update_goal_state() # Ensure new extension can be enabled exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.1") self._assert_ext_status(protocol.report_vm_status, "success", 0) @patch('azurelinuxagent.ga.exthandlers.ExtHandlerInstance.install', side_effect=ExtHandlerInstance.install, autospec=True) @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_install_command') def test_install_failure(self, patch_get_install_command, patch_install, *args): """ When extension install fails, the operation should not be retried. """ test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_EXT_SINGLE) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter # Ensure initial install is unsuccessful patch_get_install_command.return_value = "exit.sh 1" exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(1, patch_install.call_count) self.assertEqual(1, protocol.report_vm_status.call_count) self._assert_handler_status(protocol.report_vm_status, "NotReady", expected_ext_count=0, version="1.0.0") @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_install_command') def test_install_failure_check_exception_handling(self, patch_get_install_command, *args): """ When extension install fails, the operation should be reported to our telemetry service. """ test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_EXT_SINGLE) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter # Ensure install is unsuccessful patch_get_install_command.return_value = "exit.sh 1" exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(1, protocol.report_vm_status.call_count) self._assert_handler_status(protocol.report_vm_status, expected_status="NotReady", expected_ext_count=0, version="1.0.0") @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_enable_command') def test_enable_failure_check_exception_handling(self, patch_get_enable_command, *args): """ When extension enable fails, the operation should be reported. """ test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_EXT_SINGLE) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter # Ensure initial install is successful, but enable fails patch_get_enable_command.call_count = 0 patch_get_enable_command.return_value = "exit.sh 1" exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(1, patch_get_enable_command.call_count) self.assertEqual(1, protocol.report_vm_status.call_count) self._assert_handler_status(protocol.report_vm_status, "NotReady", expected_ext_count=1, version="1.0.0") @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_disable_command') def test_disable_failure_with_exception_handling(self, patch_get_disable_command, *args): """ When extension disable fails, the operation should be reported. """ # Ensure initial install and enable is successful, but disable fails test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_EXT_SINGLE) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter patch_get_disable_command.call_count = 0 patch_get_disable_command.return_value = "exit 1" exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(0, patch_get_disable_command.call_count) self.assertEqual(1, protocol.report_vm_status.call_count) self._assert_handler_status(protocol.report_vm_status, "Ready", expected_ext_count=1, version="1.0.0") self._assert_ext_status(protocol.report_vm_status, "success", 0) # Next incarnation, disable extension test_data.set_incarnation(2) test_data.set_extensions_config_state(ExtensionRequestedState.Disabled) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(1, patch_get_disable_command.call_count) self.assertEqual(2, protocol.report_vm_status.call_count) self._assert_handler_status(protocol.report_vm_status, "NotReady", expected_ext_count=1, version="1.0.0") @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_uninstall_command') def test_uninstall_failure(self, patch_get_uninstall_command, *args): """ When extension uninstall fails, the operation should not be retried. """ # Ensure initial install and enable is successful, but uninstall fails test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_EXT_SINGLE) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter patch_get_uninstall_command.call_count = 0 patch_get_uninstall_command.return_value = "exit 1" exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(0, patch_get_uninstall_command.call_count) self.assertEqual(1, protocol.report_vm_status.call_count) self._assert_handler_status(protocol.report_vm_status, "Ready", expected_ext_count=1, version="1.0.0") self._assert_ext_status(protocol.report_vm_status, "success", 0) # Next incarnation, disable extension test_data.set_incarnation(2) test_data.set_extensions_config_state(ExtensionRequestedState.Uninstall) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(1, patch_get_uninstall_command.call_count) self.assertEqual(2, protocol.report_vm_status.call_count) self.assertEqual("Ready", protocol.report_vm_status.call_args[0][0].vmAgent.status) self._assert_no_handler_status(protocol.report_vm_status) # Ensure there are no further retries exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(1, patch_get_uninstall_command.call_count) self.assertEqual(3, protocol.report_vm_status.call_count) self.assertEqual("Ready", protocol.report_vm_status.call_args[0][0].vmAgent.status) self._assert_no_handler_status(protocol.report_vm_status) @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_update_command') def test_extension_upgrade_failure_when_new_version_update_fails(self, patch_get_update_command, *args): """ When the update command of the new extension fails, it should result in the new extension failed and the old extension disabled. On the next goal state, the entire upgrade scenario should be retried (once), meaning the download, initialize and update are called on the new extension. Note: we don't re-download the zip since it wasn't cleaned up in the previous goal state (we only clean up NotInstalled handlers), so we just re-use the existing zip of the new extension. """ test_data, exthandlers_handler, protocol = self._set_up_update_test_and_update_gs(patch_get_update_command, *args) extension_name = exthandlers_handler.ext_handlers[0].name extension_calls = [] original_popen = subprocess.Popen def mock_popen(*args, **kwargs): # Maintain an internal list of invoked commands of the test extension to assert on later if extension_name in args[0]: extension_calls.append(args[0]) return original_popen(*args, **kwargs) with patch('azurelinuxagent.common.cgroupapi.subprocess.Popen', side_effect=mock_popen): exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() update_command_count = len([extension_call for extension_call in extension_calls if patch_get_update_command.return_value in extension_call]) enable_command_count = len([extension_call for extension_call in extension_calls if "-enable" in extension_call]) self.assertEqual(1, update_command_count) self.assertEqual(0, enable_command_count) # We report the failure of the new extension version self._assert_handler_status(protocol.report_vm_status, "NotReady", expected_ext_count=1, version="1.0.1") # If the incarnation number changes (there's a new goal state), ensure we go through the entire upgrade # process again. test_data.set_incarnation(3) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() update_command_count = len([extension_call for extension_call in extension_calls if patch_get_update_command.return_value in extension_call]) enable_command_count = len([extension_call for extension_call in extension_calls if "-enable" in extension_call]) self.assertEqual(2, update_command_count) self.assertEqual(0, enable_command_count) # We report the failure of the new extension version self._assert_handler_status(protocol.report_vm_status, "NotReady", expected_ext_count=1, version="1.0.1") @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_disable_command') def test_extension_upgrade_failure_when_prev_version_disable_fails(self, patch_get_disable_command, *args): test_data, exthandlers_handler, protocol = self._set_up_update_test_and_update_gs(patch_get_disable_command, *args) # pylint: disable=unused-variable with patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_enable_command') as patch_get_enable_command: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() # When the previous version's disable fails, we expect the upgrade scenario to fail, so the enable # for the new version is not called and the new version handler's status is reported as not ready. self.assertEqual(1, patch_get_disable_command.call_count) self.assertEqual(0, patch_get_enable_command.call_count) self._assert_handler_status(protocol.report_vm_status, "NotReady", expected_ext_count=0, version="1.0.1") @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_disable_command') def test_extension_upgrade_failure_when_prev_version_disable_fails_and_recovers_on_next_incarnation(self, patch_get_disable_command, *args): test_data, exthandlers_handler, protocol = self._set_up_update_test_and_update_gs(patch_get_disable_command, *args) with patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_enable_command') as patch_get_enable_command: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() # When the previous version's disable fails, we expect the upgrade scenario to fail, so the enable # for the new version is not called and the new version handler's status is reported as not ready. self.assertEqual(1, patch_get_disable_command.call_count) self.assertEqual(0, patch_get_enable_command.call_count) self._assert_handler_status(protocol.report_vm_status, "NotReady", expected_ext_count=0, version="1.0.1") # Force a new goal state incarnation, only then will we attempt the upgrade again test_data.set_incarnation(3) protocol.client.update_goal_state() # Ensure disable won't fail by making launch_command a no-op with patch('azurelinuxagent.ga.exthandlers.ExtHandlerInstance.launch_command') as patch_launch_command: # pylint: disable=unused-variable exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(2, patch_get_disable_command.call_count) self.assertEqual(1, patch_get_enable_command.call_count) self._assert_handler_status(protocol.report_vm_status, "Ready", expected_ext_count=1, version="1.0.1") @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_disable_command') def test_extension_upgrade_failure_when_prev_version_disable_fails_incorrect_zip(self, patch_get_disable_command, *args): test_data, exthandlers_handler, protocol = self._set_up_update_test_and_update_gs(patch_get_disable_command, # pylint: disable=unused-variable *args) # The download logic has retry logic that sleeps before each try - make sleep a no-op. with patch("time.sleep"): with patch("zipfile.ZipFile.extractall") as patch_zipfile_extractall: with patch( 'azurelinuxagent.ga.exthandlers.HandlerManifest.get_enable_command') as patch_get_enable_command: patch_zipfile_extractall.side_effect = raise_ioerror # The zipfile was corrupt and the upgrade sequence failed exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() # We never called the disable of the old version due to the failure when unzipping the new version, # nor the enable of the new version self.assertEqual(0, patch_get_disable_command.call_count) self.assertEqual(0, patch_get_enable_command.call_count) # Ensure we are processing the same goal state only once loop_run = 5 for x in range(loop_run): # pylint: disable=unused-variable exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(0, patch_get_disable_command.call_count) self.assertEqual(0, patch_get_enable_command.call_count) @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_disable_command') def test_old_handler_reports_failure_on_disable_fail_on_update(self, patch_get_disable_command, *args): old_version, new_version = "1.0.0", "1.0.1" test_data, exthandlers_handler, protocol = self._set_up_update_test_and_update_gs(patch_get_disable_command, # pylint: disable=unused-variable *args) with patch.object(ExtHandlerInstance, "report_event", autospec=True) as patch_report_event: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(1, patch_get_disable_command.call_count) old_version_args, old_version_kwargs = patch_report_event.call_args new_version_args, new_version_kwargs = patch_report_event.call_args_list[0] self.assertEqual(new_version_args[0].ext_handler.version, new_version, "The first call to report event should be from the new version of the ext-handler " "to report download succeeded") self.assertEqual(new_version_kwargs['message'], "Download succeeded", "The message should be Download Succedded") self.assertEqual(old_version_args[0].ext_handler.version, old_version, "The last report event call should be from the old version ext-handler " "to report the event from the previous version") self.assertFalse(old_version_kwargs['is_success'], "The last call to report event should be for a failure") self.assertTrue('Error' in old_version_kwargs['message'], "No error reported") # This is ensuring that the error status is being written to the new version self._assert_handler_status(protocol.report_vm_status, "NotReady", expected_ext_count=0, version=new_version) @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_update_command') def test_upgrade_failure_with_exception_handling(self, patch_get_update_command, *args): """ Extension upgrade failure should not be retried """ test_data, exthandlers_handler, protocol = self._set_up_update_test_and_update_gs(patch_get_update_command, # pylint: disable=unused-variable *args) exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(1, patch_get_update_command.call_count) self._assert_handler_status(protocol.report_vm_status, "NotReady", expected_ext_count=1, version="1.0.1") @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_disable_command') def test_extension_upgrade_should_pass_when_continue_on_update_failure_is_true_and_prev_version_disable_fails( self, patch_get_disable_command, *args): test_data, exthandlers_handler, protocol = self._set_up_update_test_and_update_gs(patch_get_disable_command, # pylint: disable=unused-variable *args) with patch('azurelinuxagent.ga.exthandlers.HandlerManifest.is_continue_on_update_failure', return_value=True) \ as mock_continue_on_update_failure: # These are just testing the mocks have been called and asserting the test conditions have been met exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(1, patch_get_disable_command.call_count) self.assertEqual(2, mock_continue_on_update_failure.call_count, "This should be called twice, for both disable and uninstall") # Ensure the handler status and ext_status is successful self._assert_handler_status(protocol.report_vm_status, "Ready", expected_ext_count=1, version="1.0.1") self._assert_ext_status(protocol.report_vm_status, "success", 0) @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_uninstall_command') def test_extension_upgrade_should_pass_when_continue_on_update_failue_is_true_and_prev_version_uninstall_fails( self, patch_get_uninstall_command, *args): test_data, exthandlers_handler, protocol = self._set_up_update_test_and_update_gs(patch_get_uninstall_command, # pylint: disable=unused-variable *args) with patch('azurelinuxagent.ga.exthandlers.HandlerManifest.is_continue_on_update_failure', return_value=True) \ as mock_continue_on_update_failure: # These are just testing the mocks have been called and asserting the test conditions have been met exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(1, patch_get_uninstall_command.call_count) self.assertEqual(2, mock_continue_on_update_failure.call_count, "This should be called twice, for both disable and uninstall") # Ensure the handler status and ext_status is successful self._assert_handler_status(protocol.report_vm_status, "Ready", expected_ext_count=1, version="1.0.1") self._assert_ext_status(protocol.report_vm_status, "success", 0) @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_disable_command') def test_extension_upgrade_should_fail_when_continue_on_update_failure_is_false_and_prev_version_disable_fails( self, patch_get_disable_command, *args): test_data, exthandlers_handler, protocol = self._set_up_update_test_and_update_gs(patch_get_disable_command, # pylint: disable=unused-variable *args) with patch('azurelinuxagent.ga.exthandlers.HandlerManifest.is_continue_on_update_failure', return_value=False) \ as mock_continue_on_update_failure: # These are just testing the mocks have been called and asserting the test conditions have been met exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(1, patch_get_disable_command.call_count) self.assertEqual(1, mock_continue_on_update_failure.call_count, "The first call would raise an exception") # Assert test scenario self._assert_handler_status(protocol.report_vm_status, "NotReady", expected_ext_count=0, version="1.0.1") @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_uninstall_command') def test_extension_upgrade_should_fail_when_continue_on_update_failure_is_false_and_prev_version_uninstall_fails( self, patch_get_uninstall_command, *args): test_data, exthandlers_handler, protocol = self._set_up_update_test_and_update_gs(patch_get_uninstall_command, # pylint: disable=unused-variable *args) with patch('azurelinuxagent.ga.exthandlers.HandlerManifest.is_continue_on_update_failure', return_value=False) \ as mock_continue_on_update_failure: # These are just testing the mocks have been called and asserting the test conditions have been met exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(1, patch_get_uninstall_command.call_count) self.assertEqual(2, mock_continue_on_update_failure.call_count, "The second call would raise an exception") # Assert test scenario self._assert_handler_status(protocol.report_vm_status, "NotReady", expected_ext_count=0, version="1.0.1") @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_disable_command') def test_extension_upgrade_should_fail_when_continue_on_update_failure_is_true_and_old_disable_and_new_enable_fails( self, patch_get_disable_command, *args): test_data, exthandlers_handler, protocol = self._set_up_update_test_and_update_gs(patch_get_disable_command, # pylint: disable=unused-variable *args) with patch('azurelinuxagent.ga.exthandlers.HandlerManifest.is_continue_on_update_failure', return_value=True) \ as mock_continue_on_update_failure: with patch('azurelinuxagent.ga.exthandlers.HandlerManifest.get_enable_command', return_value="exit 1")\ as patch_get_enable: # These are just testing the mocks have been called and asserting the test conditions have been met exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(1, patch_get_disable_command.call_count) self.assertEqual(2, mock_continue_on_update_failure.call_count) self.assertEqual(1, patch_get_enable.call_count) # Assert test scenario self._assert_handler_status(protocol.report_vm_status, "NotReady", expected_ext_count=1, version="1.0.1") @patch('azurelinuxagent.ga.exthandlers.HandlerManifest.is_continue_on_update_failure', return_value=True) def test_uninstall_rc_env_var_should_report_not_run_for_non_update_calls_to_exthandler_run( self, patch_continue_on_update, *args): test_data, exthandlers_handler, protocol = self._set_up_update_test_and_update_gs(Mock(), *args) with patch.object(CGroupConfigurator.get_instance(), "start_extension_command", side_effect=[ExtensionError("Disable Failed"), "ok", ExtensionError("uninstall failed"), "ok", "ok", "New enable run ok"]) as patch_start_cmd: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() _, update_kwargs = patch_start_cmd.call_args_list[1] _, install_kwargs = patch_start_cmd.call_args_list[3] _, enable_kwargs = patch_start_cmd.call_args_list[4] # Ensure that the env variables were present in the first run when failures were thrown for update self.assertEqual(2, patch_continue_on_update.call_count) self.assertTrue( '-update' in update_kwargs['command'] and ExtCommandEnvVariable.DisableReturnCode in update_kwargs['env'], "The update command call should have Disable Failed in env variable") self.assertTrue( '-install' in install_kwargs['command'] and ExtCommandEnvVariable.DisableReturnCode not in install_kwargs[ 'env'], "The Disable Failed env variable should be removed from install command") self.assertTrue( '-install' in install_kwargs['command'] and ExtCommandEnvVariable.UninstallReturnCode in install_kwargs[ 'env'], "The install command call should have Uninstall Failed in env variable") self.assertTrue( '-enable' in enable_kwargs['command'] and ExtCommandEnvVariable.UninstallReturnCode in enable_kwargs['env'], "The enable command call should have Uninstall Failed in env variable") # Initiating another run which shouldn't have any failed env variables in it if no failures # Updating Incarnation test_data.set_incarnation(3) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() _, new_enable_kwargs = patch_start_cmd.call_args # Ensure the new run didn't have Disable Return Code env variable self.assertNotIn(ExtCommandEnvVariable.DisableReturnCode, new_enable_kwargs['env']) # Ensure the new run had Uninstall Return Code env variable == NOT_RUN self.assertIn(ExtCommandEnvVariable.UninstallReturnCode, new_enable_kwargs['env']) self.assertTrue( new_enable_kwargs['env'][ExtCommandEnvVariable.UninstallReturnCode] == NOT_RUN) # Ensure the handler status and ext_status is successful self._assert_handler_status(protocol.report_vm_status, "Ready", expected_ext_count=1, version="1.0.1") self._assert_ext_status(protocol.report_vm_status, "success", 0) def test_ext_path_and_version_env_variables_set_for_ever_operation(self, *args): test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_EXT_SINGLE) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter with patch.object(CGroupConfigurator.get_instance(), "start_extension_command") as patch_start_cmd: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() # Extension Path and Version should be set for all launch_command calls for args, kwargs in patch_start_cmd.call_args_list: self.assertIn(ExtCommandEnvVariable.ExtensionPath, kwargs['env']) self.assertIn('OSTCExtensions.ExampleHandlerLinux-1.0.0', kwargs['env'][ExtCommandEnvVariable.ExtensionPath]) self.assertIn(ExtCommandEnvVariable.ExtensionVersion, kwargs['env']) self.assertEqual("1.0.0", kwargs['env'][ExtCommandEnvVariable.ExtensionVersion]) self._assert_handler_status(protocol.report_vm_status, "Ready", expected_ext_count=1, version="1.0.0") @patch("azurelinuxagent.common.cgroupconfigurator.handle_process_completion", side_effect="Process Successful") def test_ext_sequence_no_should_be_set_for_every_command_call(self, _, *args): test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_MULTIPLE_EXT) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter with patch("subprocess.Popen") as patch_popen: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() for _, kwargs in patch_popen.call_args_list: self.assertIn(ExtCommandEnvVariable.ExtensionSeqNumber, kwargs['env']) self.assertEqual(kwargs['env'][ExtCommandEnvVariable.ExtensionSeqNumber], "0") self._assert_handler_status(protocol.report_vm_status, "Ready", expected_ext_count=1, version="1.0.0") # Next incarnation and seq for extensions, update version test_data.goal_state = test_data.goal_state.replace("1<", "2<") test_data.ext_conf = test_data.ext_conf.replace('version="1.0.0"', 'version="1.0.1"') test_data.ext_conf = test_data.ext_conf.replace('seqNo="0"', 'seqNo="1"') test_data.manifest = test_data.manifest.replace('1.0.0', '1.0.1') exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter with patch("subprocess.Popen") as patch_popen: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() for _, kwargs in patch_popen.call_args_list: self.assertIn(ExtCommandEnvVariable.ExtensionSeqNumber, kwargs['env']) self.assertEqual(kwargs['env'][ExtCommandEnvVariable.ExtensionSeqNumber], "1") self._assert_handler_status(protocol.report_vm_status, "Ready", expected_ext_count=1, version="1.0.1") def test_ext_sequence_no_should_be_set_from_within_extension(self, *args): test_file_name = "testfile.sh" handler_json = { "installCommand": test_file_name, "uninstallCommand": test_file_name, "updateCommand": test_file_name, "enableCommand": test_file_name, "disableCommand": test_file_name, "rebootAfterInstall": False, "reportHeartbeat": False, "continueOnUpdateFailure": False } manifest = HandlerManifest({'handlerManifest': handler_json}) # Script prints env variables passed to this process and prints all starting with ConfigSequenceNumber test_file = """ printenv | grep ConfigSequenceNumber """ base_dir = os.path.join(conf.get_lib_dir(), 'OSTCExtensions.ExampleHandlerLinux-1.0.0') if not os.path.exists(base_dir): os.mkdir(base_dir) self.create_script(os.path.join(base_dir, test_file_name), test_file) test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_EXT_SINGLE) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=unused-variable,no-value-for-parameter expected_seq_no = 0 with patch.object(ExtHandlerInstance, "load_manifest", return_value=manifest): with patch.object(ExtHandlerInstance, 'report_event') as mock_report_event: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() for _, kwargs in mock_report_event.call_args_list: # The output is of the format - 'Command: testfile.sh -{Operation} \n[stdout]ConfigSequenceNumber=N\n[stderr]' if ("Command: " + test_file_name) not in kwargs['message']: continue self.assertIn("{0}={1}".format(ExtCommandEnvVariable.ExtensionSeqNumber, expected_seq_no), kwargs['message']) # Update goal state, extension version and seq no test_data.goal_state = test_data.goal_state.replace("1<", "2<") test_data.ext_conf = test_data.ext_conf.replace('version="1.0.0"', 'version="1.0.1"') test_data.ext_conf = test_data.ext_conf.replace('seqNo="0"', 'seqNo="1"') test_data.manifest = test_data.manifest.replace('1.0.0', '1.0.1') expected_seq_no = 1 base_dir = os.path.join(conf.get_lib_dir(), 'OSTCExtensions.ExampleHandlerLinux-1.0.1') if not os.path.exists(base_dir): os.mkdir(base_dir) self.create_script(os.path.join(base_dir, test_file_name), test_file) with patch.object(ExtHandlerInstance, 'report_event') as mock_report_event: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() for _, kwargs in mock_report_event.call_args_list: # The output is of the format - 'testfile.sh\n[stdout]ConfigSequenceNumber=N\n[stderr]' if test_file_name not in kwargs['message']: continue self.assertIn("{0}={1}".format(ExtCommandEnvVariable.ExtensionSeqNumber, expected_seq_no), kwargs['message']) def test_correct_exit_code_should_be_set_on_uninstall_cmd_failure(self, *args): test_file_name = "testfile.sh" test_error_file_name = "error.sh" handler_json = { "installCommand": test_file_name + " -install", "uninstallCommand": test_error_file_name, "updateCommand": test_file_name + " -update", "enableCommand": test_file_name + " -enable", "disableCommand": test_error_file_name, "rebootAfterInstall": False, "reportHeartbeat": False, "continueOnUpdateFailure": True } manifest = HandlerManifest({'handlerManifest': handler_json}) # Script prints env variables passed to this process and prints all starting with ConfigSequenceNumber test_file = """ printenv | grep AZURE_ """ exit_code = 151 test_error_content = """ exit %s """ % exit_code error_dir = os.path.join(conf.get_lib_dir(), 'OSTCExtensions.ExampleHandlerLinux-1.0.0') if not os.path.exists(error_dir): os.mkdir(error_dir) self.create_script(os.path.join(error_dir, test_error_file_name), test_error_content) test_data, exthandlers_handler, protocol = self._set_up_update_test_and_update_gs(Mock(), *args) # pylint: disable=unused-variable base_dir = os.path.join(conf.get_lib_dir(), 'OSTCExtensions.ExampleHandlerLinux-1.0.1') if not os.path.exists(base_dir): os.mkdir(base_dir) self.create_script(os.path.join(base_dir, test_file_name), test_file) with patch("azurelinuxagent.ga.exthandlers.ExtHandlerInstance.load_manifest", return_value=manifest): with patch.object(ExtHandlerInstance, 'report_event') as mock_report_event: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() update_kwargs = next(kwargs for _, kwargs in mock_report_event.call_args_list if "Command: testfile.sh -update" in kwargs['message']) install_kwargs = next(kwargs for _, kwargs in mock_report_event.call_args_list if "Command: testfile.sh -install" in kwargs['message']) enable_kwargs = next(kwargs for _, kwargs in mock_report_event.call_args_list if "Command: testfile.sh -enable" in kwargs['message']) self.assertIn("%s=%s" % (ExtCommandEnvVariable.DisableReturnCode, exit_code), update_kwargs['message']) self.assertIn("%s=%s" % (ExtCommandEnvVariable.UninstallReturnCode, exit_code), install_kwargs['message']) self.assertIn("%s=%s" % (ExtCommandEnvVariable.UninstallReturnCode, exit_code), enable_kwargs['message']) def test_it_should_persist_goal_state_aggregate_status_until_new_incarnation(self, mock_get, mock_crypt_util, *args): test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE) exthandlers_handler, protocol = self._create_mock(test_data, mock_get, mock_crypt_util, *args) exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", expected_ext_count=1, version="1.0.0") args, _ = protocol.report_vm_status.call_args gs_aggregate_status = args[0].vmAgent.vm_artifacts_aggregate_status.goal_state_aggregate_status self.assertIsNotNone(gs_aggregate_status, "Goal State Aggregate status not reported") self.assertEqual(gs_aggregate_status.status, GoalStateStatus.Success, "Wrong status reported") self.assertEqual(gs_aggregate_status.in_svd_seq_no, "1", "Incorrect seq no") # Update incarnation and ensure the gs_aggregate_status is modified too test_data.set_incarnation(2) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", expected_ext_count=1, version="1.0.0") args, _ = protocol.report_vm_status.call_args new_gs_aggregate_status = args[0].vmAgent.vm_artifacts_aggregate_status.goal_state_aggregate_status self.assertIsNotNone(new_gs_aggregate_status, "New Goal State Aggregate status not reported") self.assertNotEqual(gs_aggregate_status, new_gs_aggregate_status, "The gs_aggregate_status should be different") self.assertEqual(new_gs_aggregate_status.status, GoalStateStatus.Success, "Wrong status reported") self.assertEqual(new_gs_aggregate_status.in_svd_seq_no, "2", "Incorrect seq no") def test_it_should_parse_required_features_properly(self, mock_get, mock_crypt_util, *args): test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_REQUIRED_FEATURES) _, protocol = self._create_mock(test_data, mock_get, mock_crypt_util, *args) required_features = protocol.get_goal_state().extensions_goal_state.required_features self.assertEqual(3, len(required_features), "Incorrect features parsed") for i, feature in enumerate(required_features): self.assertEqual(feature, "TestRequiredFeature{0}".format(i+1), "Name mismatch") def test_it_should_fail_goal_state_if_required_features_not_supported(self, mock_get, mock_crypt_util, *args): test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_REQUIRED_FEATURES) exthandlers_handler, protocol = self._create_mock(test_data, mock_get, mock_crypt_util, *args) exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() args, _ = protocol.report_vm_status.call_args gs_aggregate_status = args[0].vmAgent.vm_artifacts_aggregate_status.goal_state_aggregate_status self.assertEqual(0, len(args[0].vmAgent.extensionHandlers), "No extensions should be reported") self.assertIsNotNone(gs_aggregate_status, "GS Aggregagte status should be reported") self.assertEqual(gs_aggregate_status.status, GoalStateStatus.Failed, "GS should be failed") self.assertEqual(gs_aggregate_status.code, GoalStateAggregateStatusCodes.GoalStateUnsupportedRequiredFeatures, "Incorrect error code set properly for GS failure") self.assertEqual(gs_aggregate_status.in_svd_seq_no, "1", "Sequence Number is wrong") @patch("azurelinuxagent.common.protocol.wire.CryptUtil") @patch("azurelinuxagent.common.utils.restutil.http_get") class TestExtensionSequencing(AgentTestCase): def _create_mock(self, mock_http_get, MockCryptUtil): test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE) # Mock protocol to return test data mock_http_get.side_effect = test_data.mock_http_get MockCryptUtil.side_effect = test_data.mock_crypt_util protocol = WireProtocol(KNOWN_WIRESERVER_IP) protocol.detect() protocol.report_vm_status = MagicMock() handler = get_exthandlers_handler(protocol) return handler def _set_dependency_levels(self, dependency_levels, exthandlers_handler): """ Creates extensions with the given dependencyLevel """ handler_map = {} all_handlers = [] for handler_name, level in dependency_levels: if handler_map.get(handler_name) is None: handler = Extension(name=handler_name) extension = ExtensionSettings(name=handler_name) handler.state = ExtensionRequestedState.Enabled handler.settings.append(extension) handler_map[handler_name] = handler all_handlers.append(handler) handler = handler_map[handler_name] for ext in handler.settings: ext.dependencyLevel = level exthandlers_handler.protocol.get_goal_state().extensions_goal_state._extensions *= 0 exthandlers_handler.protocol.get_goal_state().extensions_goal_state.extensions.extend(all_handlers) def _validate_extension_sequence(self, expected_sequence, exthandlers_handler): installed_extensions = [a[0].ext_handler.name for a, _ in exthandlers_handler.handle_ext_handler.call_args_list] self.assertListEqual(expected_sequence, installed_extensions, "Expected and actual list of extensions are not equal") def _run_test(self, extensions_to_be_failed, expected_sequence, exthandlers_handler): """ Mocks get_ext_handling_status() to mimic error status for a given extension. Calls ExtHandlersHandler.run() Verifies if the ExtHandlersHandler.handle_ext_handler() was called with appropriate extensions in the expected order. """ def get_ext_handling_status(ext): status = "error" if ext.name in extensions_to_be_failed else "success" return status exthandlers_handler.handle_ext_handler = MagicMock() with patch.object(ExtHandlerInstance, "get_ext_handling_status", side_effect=get_ext_handling_status): with patch.object(ExtHandlerInstance, "get_handler_status", ExtHandlerStatus): with patch('azurelinuxagent.ga.exthandlers._DEFAULT_EXT_TIMEOUT_MINUTES', 0.01): exthandlers_handler.run() self._validate_extension_sequence(expected_sequence, exthandlers_handler) def test_handle_ext_handlers(self, *args): """ Tests extension sequencing among multiple extensions with dependencies. This test introduces failure in all possible levels and extensions. Verifies that the sequencing is in the expected order and a failure in one extension skips the rest of the extensions in the sequence. """ exthandlers_handler = self._create_mock(*args) # pylint: disable=no-value-for-parameter self._set_dependency_levels([("A", 3), ("B", 2), ("C", 2), ("D", 1), ("E", 1), ("F", 1), ("G", 1)], exthandlers_handler) extensions_to_be_failed = [] expected_sequence = ["D", "E", "F", "G", "B", "C", "A"] self._run_test(extensions_to_be_failed, expected_sequence, exthandlers_handler) extensions_to_be_failed = ["D"] expected_sequence = ["D"] self._run_test(extensions_to_be_failed, expected_sequence, exthandlers_handler) extensions_to_be_failed = ["E"] expected_sequence = ["D", "E"] self._run_test(extensions_to_be_failed, expected_sequence, exthandlers_handler) extensions_to_be_failed = ["F"] expected_sequence = ["D", "E", "F"] self._run_test(extensions_to_be_failed, expected_sequence, exthandlers_handler) extensions_to_be_failed = ["G"] expected_sequence = ["D", "E", "F", "G"] self._run_test(extensions_to_be_failed, expected_sequence, exthandlers_handler) extensions_to_be_failed = ["B"] expected_sequence = ["D", "E", "F", "G", "B"] self._run_test(extensions_to_be_failed, expected_sequence, exthandlers_handler) extensions_to_be_failed = ["C"] expected_sequence = ["D", "E", "F", "G", "B", "C"] self._run_test(extensions_to_be_failed, expected_sequence, exthandlers_handler) extensions_to_be_failed = ["A"] expected_sequence = ["D", "E", "F", "G", "B", "C", "A"] self._run_test(extensions_to_be_failed, expected_sequence, exthandlers_handler) def test_handle_ext_handlers_with_uninstallation(self, *args): """ Tests extension sequencing among multiple extensions with dependencies when some extension are to be uninstalled. Verifies that the sequencing is in the expected order and the uninstallation takes place prior to all the installation/enable. """ exthandlers_handler = self._create_mock(*args) # pylint: disable=no-value-for-parameter # "A", "D" and "F" are marked as to be uninstalled self._set_dependency_levels([("A", 0), ("B", 2), ("C", 2), ("D", 0), ("E", 1), ("F", 0), ("G", 1)], exthandlers_handler) extensions_to_be_failed = [] expected_sequence = ["A", "D", "F", "E", "G", "B", "C"] self._run_test(extensions_to_be_failed, expected_sequence, exthandlers_handler) def test_handle_ext_handlers_fallback(self, *args): """ This test makes sure that the extension sequencing is applied only when the user specifies dependency information in the extension. When there is no dependency specified, the agent is expected to assign dependencyLevel=0 to all extension. Also, it is expected to install all the extension no matter if there is any failure in any of the extensions. """ exthandlers_handler = self._create_mock(*args) # pylint: disable=no-value-for-parameter self._set_dependency_levels([("A", 1), ("B", 1), ("C", 1), ("D", 1), ("E", 1), ("F", 1), ("G", 1)], exthandlers_handler) # Expected sequence must contain all the extensions in the given order. # The following test cases verfy against this same expected sequence no matter if any extension failed expected_sequence = ["A", "B", "C", "D", "E", "F", "G"] # Make sure that failure in any extension does not prevent other extensions to be installed extensions_to_be_failed = [] self._run_test(extensions_to_be_failed, expected_sequence, exthandlers_handler) extensions_to_be_failed = ["A"] self._run_test(extensions_to_be_failed, expected_sequence, exthandlers_handler) extensions_to_be_failed = ["B"] self._run_test(extensions_to_be_failed, expected_sequence, exthandlers_handler) extensions_to_be_failed = ["C"] self._run_test(extensions_to_be_failed, expected_sequence, exthandlers_handler) extensions_to_be_failed = ["D"] self._run_test(extensions_to_be_failed, expected_sequence, exthandlers_handler) extensions_to_be_failed = ["E"] self._run_test(extensions_to_be_failed, expected_sequence, exthandlers_handler) extensions_to_be_failed = ["F"] self._run_test(extensions_to_be_failed, expected_sequence, exthandlers_handler) extensions_to_be_failed = ["G"] self._run_test(extensions_to_be_failed, expected_sequence, exthandlers_handler) class TestInVMArtifactsProfile(AgentTestCase): def test_it_should_parse_boolean_values(self): profile_json = '{ "onHold": true }' profile = InVMArtifactsProfile(profile_json) self.assertTrue(profile.is_on_hold(), "Failed to parse '{0}'".format(profile_json)) profile_json = '{ "onHold": false }' profile = InVMArtifactsProfile(profile_json) self.assertFalse(profile.is_on_hold(), "Failed to parse '{0}'".format(profile_json)) def test_it_should_parse_boolean_values_encoded_as_strings(self): profile_json = '{ "onHold": "true" }' profile = InVMArtifactsProfile(profile_json) self.assertTrue(profile.is_on_hold(), "Failed to parse '{0}'".format(profile_json)) profile_json = '{ "onHold": "false" }' profile = InVMArtifactsProfile(profile_json) self.assertFalse(profile.is_on_hold(), "Failed to parse '{0}'".format(profile_json)) profile_json = '{ "onHold": "TRUE" }' profile = InVMArtifactsProfile(profile_json) self.assertTrue(profile.is_on_hold(), "Failed to parse '{0}'".format(profile_json)) class TestExtensionUpdateOnFailure(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) self.mock_sleep = patch("time.sleep", lambda *_: mock_sleep(0.0001)) self.mock_sleep.start() def tearDown(self): self.mock_sleep.stop() AgentTestCase.tearDown(self) @staticmethod def _do_upgrade_scenario_and_get_order(first_ext, upgraded_ext): """ Given the provided ExtensionEmulator objects, installs the first and then attempts to update to the second. StatusBlobs and command invocations for each actor can be checked with {emulator}.status_blobs and {emulator}.actions[{command_name}] respectively. Note that this method assumes the first extension's install command should succeed. Don't use this method if your test is attempting to emulate a fresh install (i.e. not an upgrade) with a failing install. """ with mock_wire_protocol(DATA_FILE, http_put_handler=generate_put_handler(first_ext, upgraded_ext)) as protocol: exthandlers_handler = get_exthandlers_handler(protocol) with enable_invocations(first_ext, upgraded_ext) as invocation_record: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() invocation_record.compare( (first_ext, ExtensionCommandNames.INSTALL), # Note that if installCommand is supposed to fail, this will erroneously raise. (first_ext, ExtensionCommandNames.ENABLE) ) protocol.mock_wire_data.set_extensions_config_version(upgraded_ext.version) protocol.mock_wire_data.set_incarnation(2) protocol.client.update_goal_state() with enable_invocations(first_ext, upgraded_ext) as invocation_record: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() return invocation_record def test_non_enabled_ext_should_not_be_disabled_at_ver_update(self): _, enable_action = Actions.generate_unique_fail() first_ext = extension_emulator(enable_action=enable_action) second_ext = extension_emulator(version="1.1.0") invocation_record = TestExtensionUpdateOnFailure._do_upgrade_scenario_and_get_order(first_ext, second_ext) invocation_record.compare( (second_ext, ExtensionCommandNames.UPDATE), (first_ext, ExtensionCommandNames.UNINSTALL), (second_ext, ExtensionCommandNames.INSTALL), (second_ext, ExtensionCommandNames.ENABLE) ) def test_disable_failed_env_variable_should_be_set_for_update_cmd_when_continue_on_update_failure_is_true(self): exit_code, disable_action = Actions.generate_unique_fail() first_ext = extension_emulator(disable_action=disable_action) second_ext = extension_emulator(version="1.1.0", continue_on_update_failure=True) invocation_record = TestExtensionUpdateOnFailure._do_upgrade_scenario_and_get_order(first_ext, second_ext) invocation_record.compare( (first_ext, ExtensionCommandNames.DISABLE), (second_ext, ExtensionCommandNames.UPDATE), (first_ext, ExtensionCommandNames.UNINSTALL), (second_ext, ExtensionCommandNames.INSTALL), (second_ext, ExtensionCommandNames.ENABLE) ) _, kwargs = second_ext.actions[ExtensionCommandNames.UPDATE].call_args self.assertEqual(kwargs["env"][ExtCommandEnvVariable.DisableReturnCode], exit_code, "DisableAction's return code should be in updateAction's env.") def test_uninstall_failed_env_variable_should_set_for_install_when_continue_on_update_failure_is_true(self): exit_code, uninstall_action = Actions.generate_unique_fail() first_ext = extension_emulator(uninstall_action=uninstall_action) second_ext = extension_emulator(version="1.1.0", continue_on_update_failure=True) invocation_record = TestExtensionUpdateOnFailure._do_upgrade_scenario_and_get_order(first_ext, second_ext) invocation_record.compare( (first_ext, ExtensionCommandNames.DISABLE), (second_ext, ExtensionCommandNames.UPDATE), (first_ext, ExtensionCommandNames.UNINSTALL), (second_ext, ExtensionCommandNames.INSTALL), (second_ext, ExtensionCommandNames.ENABLE) ) _, kwargs = second_ext.actions[ExtensionCommandNames.INSTALL].call_args self.assertEqual(kwargs["env"][ExtCommandEnvVariable.UninstallReturnCode], exit_code, "UninstallAction's return code should be in updateAction's env.") def test_extension_error_should_be_raised_when_continue_on_update_failure_is_false_on_disable_failure(self): exit_code, disable_action = Actions.generate_unique_fail() first_ext = extension_emulator(disable_action=disable_action) second_ext = extension_emulator(version="1.1.0", continue_on_update_failure=False) invocation_record = TestExtensionUpdateOnFailure._do_upgrade_scenario_and_get_order(first_ext, second_ext) invocation_record.compare( (first_ext, ExtensionCommandNames.DISABLE) ) self.assertEqual(len(first_ext.status_blobs), 1, "The first extension should not have submitted a second status.") self.assertEqual(len(second_ext.status_blobs), 1, "The second extension should have a single submitted status.") self.assertTrue(exit_code in second_ext.status_blobs[0]["formattedMessage"]["message"], "DisableAction's error code should be propagated to the status blob.") def test_extension_error_should_be_raised_when_continue_on_update_failure_is_false_on_uninstall_failure(self): exit_code, uninstall_action = Actions.generate_unique_fail() first_ext = extension_emulator(uninstall_action=uninstall_action) second_ext = extension_emulator(version="1.1.0", continue_on_update_failure=False) invocation_record = TestExtensionUpdateOnFailure._do_upgrade_scenario_and_get_order(first_ext, second_ext) invocation_record.compare( (first_ext, ExtensionCommandNames.DISABLE), (second_ext, ExtensionCommandNames.UPDATE), (first_ext, ExtensionCommandNames.UNINSTALL) ) self.assertEqual(len(first_ext.status_blobs), 1, "The first extension should not have submitted a second status.") self.assertEqual(len(second_ext.status_blobs), 1, "The second extension should have a single submitted status.") self.assertTrue(exit_code in second_ext.status_blobs[0]["formattedMessage"]["message"], "UninstallAction's error code should be propagated to the status blob.") def test_extension_error_should_be_raised_when_continue_on_update_failure_is_true_on_disable_and_update_failure(self): exit_codes = { } exit_codes["disable"], disable_action = Actions.generate_unique_fail() exit_codes["update"], update_action = Actions.generate_unique_fail() first_ext = extension_emulator(disable_action=disable_action) second_ext = extension_emulator(version="1.1.0", update_action=update_action, continue_on_update_failure=True) invocation_record = TestExtensionUpdateOnFailure._do_upgrade_scenario_and_get_order(first_ext, second_ext) invocation_record.compare( (first_ext, ExtensionCommandNames.DISABLE), (second_ext, ExtensionCommandNames.UPDATE) ) self.assertEqual(len(first_ext.status_blobs), 1, "The first extension should not have submitted a second status.") self.assertEqual(len(second_ext.status_blobs), 1, "The second extension should have a single submitted status.") self.assertTrue(exit_codes["update"] in second_ext.status_blobs[0]["formattedMessage"]["message"], "UpdateAction's error code should be propagated to the status blob.") def test_extension_error_should_be_raised_when_continue_on_update_failure_is_true_on_uninstall_and_install_failure(self): exit_codes = { } exit_codes["install"], install_action = Actions.generate_unique_fail() exit_codes["uninstall"], uninstall_action = Actions.generate_unique_fail() first_ext = extension_emulator(uninstall_action=uninstall_action) second_ext = extension_emulator(version="1.1.0", install_action=install_action, continue_on_update_failure=True) invocation_record = TestExtensionUpdateOnFailure._do_upgrade_scenario_and_get_order(first_ext, second_ext) invocation_record.compare( (first_ext, ExtensionCommandNames.DISABLE), (second_ext, ExtensionCommandNames.UPDATE), (first_ext, ExtensionCommandNames.UNINSTALL), (second_ext, ExtensionCommandNames.INSTALL) ) self.assertEqual(len(first_ext.status_blobs), 1, "The first extension should not have submitted a second status.") self.assertEqual(len(second_ext.status_blobs), 1, "The second extension should have a single submitted status.") self.assertTrue(exit_codes["install"] in second_ext.status_blobs[0]["formattedMessage"]["message"], "InstallAction's error code should be propagated to the status blob.") def test_failed_env_variables_should_be_set_from_within_extension_commands(self): """ This test will test from the perspective of the extensions command weather the env variables are being set for those processes """ exit_codes = { } exit_codes["disable"], disable_action = Actions.generate_unique_fail() exit_codes["uninstall"], uninstall_action = Actions.generate_unique_fail() first_ext = extension_emulator(disable_action=disable_action, uninstall_action=uninstall_action) second_ext = extension_emulator(version="1.1.0", continue_on_update_failure=True) invocation_record = TestExtensionUpdateOnFailure._do_upgrade_scenario_and_get_order(first_ext, second_ext) invocation_record.compare( (first_ext, ExtensionCommandNames.DISABLE), (second_ext, ExtensionCommandNames.UPDATE), (first_ext, ExtensionCommandNames.UNINSTALL), (second_ext, ExtensionCommandNames.INSTALL), (second_ext, ExtensionCommandNames.ENABLE) ) _, update_kwargs = second_ext.actions[ExtensionCommandNames.UPDATE].call_args _, install_kwargs = second_ext.actions[ExtensionCommandNames.INSTALL].call_args second_extension_dir = os.path.join( conf.get_lib_dir(), "{0}-{1}".format(second_ext.name, second_ext.version) ) # Ensure we're checking variables for update scenario self.assertEqual(update_kwargs["env"][ExtCommandEnvVariable.DisableReturnCode], exit_codes["disable"], "DisableAction's return code should be present in updateAction's env.") self.assertTrue(ExtCommandEnvVariable.UninstallReturnCode not in update_kwargs["env"], "UninstallAction's return code should not be in updateAction's env.") self.assertEqual(update_kwargs["env"][ExtCommandEnvVariable.ExtensionPath], second_extension_dir, "The second extension's directory should be present in updateAction's env.") self.assertEqual(update_kwargs["env"][ExtCommandEnvVariable.ExtensionVersion], "1.1.0", "The second extension's version should be present in updateAction's env.") # Ensure we're checking variables for install scenario self.assertEqual(install_kwargs["env"][ExtCommandEnvVariable.UninstallReturnCode], exit_codes["uninstall"], "UninstallAction's return code should be present in installAction's env.") self.assertTrue(ExtCommandEnvVariable.DisableReturnCode not in install_kwargs["env"], "DisableAction's return code should not be in installAction's env.") self.assertEqual(install_kwargs["env"][ExtCommandEnvVariable.ExtensionPath], second_extension_dir, "The second extension's directory should be present in installAction's env.") self.assertEqual(install_kwargs["env"][ExtCommandEnvVariable.ExtensionVersion], "1.1.0", "The second extension's version should be present in installAction's env.") def test_correct_exit_code_should_set_on_disable_cmd_failure(self): exit_code, disable_action = Actions.generate_unique_fail() first_ext = extension_emulator(disable_action=disable_action) second_ext = extension_emulator(version="1.1.0", continue_on_update_failure=True, update_mode="UpdateWithoutInstall") invocation_record = TestExtensionUpdateOnFailure._do_upgrade_scenario_and_get_order(first_ext, second_ext) invocation_record.compare( (first_ext, ExtensionCommandNames.DISABLE), (second_ext, ExtensionCommandNames.UPDATE), (first_ext, ExtensionCommandNames.UNINSTALL), (second_ext, ExtensionCommandNames.ENABLE) ) _, update_kwargs = second_ext.actions[ExtensionCommandNames.UPDATE].call_args self.assertEqual(update_kwargs["env"][ExtCommandEnvVariable.DisableReturnCode], exit_code, "DisableAction's return code should be present in UpdateAction's env.") def test_timeout_code_should_set_on_cmd_timeout(self): # Return None to every poll, forcing a timeout after 900 seconds (actually very quick because sleep(*) is mocked) force_timeout = lambda *args, **kwargs: None first_ext = extension_emulator(disable_action=force_timeout, uninstall_action=force_timeout) second_ext = extension_emulator(version="1.1.0", continue_on_update_failure=True) with patch("os.killpg"): with patch("os.getpgid"): invocation_record = TestExtensionUpdateOnFailure._do_upgrade_scenario_and_get_order(first_ext, second_ext) invocation_record.compare( (first_ext, ExtensionCommandNames.DISABLE), (second_ext, ExtensionCommandNames.UPDATE), (first_ext, ExtensionCommandNames.UNINSTALL), (second_ext, ExtensionCommandNames.INSTALL), (second_ext, ExtensionCommandNames.ENABLE) ) _, update_kwargs = second_ext.actions[ExtensionCommandNames.UPDATE].call_args _, install_kwargs = second_ext.actions[ExtensionCommandNames.INSTALL].call_args # Verify both commands are reported as timeouts. self.assertEqual(update_kwargs["env"][ExtCommandEnvVariable.DisableReturnCode], str(ExtensionErrorCodes.PluginHandlerScriptTimedout), "DisableAction's return code should be marked as a timeout in UpdateAction's env.") self.assertEqual(install_kwargs["env"][ExtCommandEnvVariable.UninstallReturnCode], str(ExtensionErrorCodes.PluginHandlerScriptTimedout), "UninstallAction's return code should be marked as a timeout in installAction's env.") def test_success_code_should_set_in_env_variables_on_cmd_success(self): first_ext = extension_emulator() second_ext = extension_emulator(version="1.1.0") invocation_record = TestExtensionUpdateOnFailure._do_upgrade_scenario_and_get_order(first_ext, second_ext) invocation_record.compare( (first_ext, ExtensionCommandNames.DISABLE), (second_ext, ExtensionCommandNames.UPDATE), (first_ext, ExtensionCommandNames.UNINSTALL), (second_ext, ExtensionCommandNames.INSTALL), (second_ext, ExtensionCommandNames.ENABLE) ) _, update_kwargs = second_ext.actions[ExtensionCommandNames.UPDATE].call_args _, install_kwargs = second_ext.actions[ExtensionCommandNames.INSTALL].call_args self.assertEqual(update_kwargs["env"][ExtCommandEnvVariable.DisableReturnCode], "0", "DisableAction's return code in updateAction's env should be 0.") self.assertEqual(install_kwargs["env"][ExtCommandEnvVariable.UninstallReturnCode], "0", "UninstallAction's return code in installAction's env should be 0.") class TestCollectExtensionStatus(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) self.lib_dir = tempfile.mkdtemp() self.mock_sleep = patch("time.sleep", lambda *_: mock_sleep(0.001)) self.mock_sleep.start() def tearDown(self): self.mock_sleep.stop() AgentTestCase.tearDown(self) def _setup_extension_for_validating_collect_ext_status(self, mock_lib_dir, status_file=None): handler_name = "TestHandler" handler_version = "1.0.0" mock_lib_dir.return_value = self.lib_dir fileutil.mkdir(os.path.join(self.lib_dir, handler_name + "-" + handler_version, "config")) fileutil.mkdir(os.path.join(self.lib_dir, handler_name + "-" + handler_version, "status")) shutil.copy(tempfile.mkstemp(prefix="test-file")[1], os.path.join(self.lib_dir, handler_name + "-" + handler_version, "config", "0.settings")) if status_file is not None: shutil.copy(os.path.join(data_dir, "ext", status_file), os.path.join(self.lib_dir, handler_name + "-" + handler_version, "status", "0.status")) with mock_wire_protocol(DATA_FILE) as protocol: exthandler = Extension(name=handler_name) exthandler.version = handler_version extension = ExtensionSettings(name=handler_name, sequenceNumber=0) exthandler.settings.append(extension) return ExtHandlerInstance(exthandler, protocol), extension @patch("azurelinuxagent.common.conf.get_lib_dir") def test_collect_ext_status(self, mock_lib_dir): """ This test validates that collect_ext_status correctly picks up the status file (sample-status.json) and then parses it correctly. """ ext_handler_i, extension = self._setup_extension_for_validating_collect_ext_status(mock_lib_dir, "sample-status.json") ext_status = ext_handler_i.collect_ext_status(extension) self.assertEqual(ext_status.code, SUCCESS_CODE_FROM_STATUS_FILE) self.assertEqual(ext_status.configurationAppliedTime, None) self.assertEqual(ext_status.operation, "Enable") self.assertEqual(ext_status.sequenceNumber, 0) self.assertEqual(ext_status.message, "Aenean semper nunc nisl, vitae sollicitudin felis consequat at. In " "lobortis elementum sapien, non commodo odio semper ac.") self.assertEqual(ext_status.status, ExtensionStatusValue.success) self.assertEqual(len(ext_status.substatusList), 1) sub_status = ext_status.substatusList[0] self.assertEqual(sub_status.code, "0") self.assertEqual(sub_status.message, None) self.assertEqual(sub_status.status, ExtensionStatusValue.success) @patch("azurelinuxagent.common.conf.get_lib_dir") def test_collect_ext_status_for_invalid_json(self, mock_lib_dir): """ This test validates that collect_ext_status correctly picks up the status file (sample-status-invalid-json-format.json) and then since the Json cannot be parsed correctly it extension status message should include 2000 bytes of status file and the line number in which it failed to parse. The uniqueMachineId tag comes from status file. """ ext_handler_i, extension = self._setup_extension_for_validating_collect_ext_status(mock_lib_dir, "sample-status-invalid-json-format.json") ext_status = ext_handler_i.collect_ext_status(extension) self.assertEqual(ext_status.code, ExtensionErrorCodes.PluginSettingsStatusInvalid) self.assertEqual(ext_status.configurationAppliedTime, None) self.assertEqual(ext_status.operation, None) self.assertEqual(ext_status.sequenceNumber, 0) self.assertRegex(ext_status.message, r".*The status reported by the extension TestHandler-1.0.0\(Sequence number 0\), " r"was in an incorrect format and the agent could not parse it correctly." r" Failed due to.*") self.assertIn("\"uniqueMachineId\": \"e5e5602b-48a6-4c35-9f96-752043777af1\"", ext_status.message) self.assertEqual(ext_status.status, ExtensionStatusValue.error) self.assertEqual(len(ext_status.substatusList), 0) @patch("azurelinuxagent.common.conf.get_lib_dir") def test_it_should_collect_ext_status_even_when_config_dir_deleted(self, mock_lib_dir): ext_handler_i, extension = self._setup_extension_for_validating_collect_ext_status(mock_lib_dir, "sample-status.json") shutil.rmtree(ext_handler_i.get_conf_dir(), ignore_errors=True) ext_status = ext_handler_i.collect_ext_status(extension) self.assertEqual(ext_status.code, SUCCESS_CODE_FROM_STATUS_FILE) self.assertEqual(ext_status.configurationAppliedTime, None) self.assertEqual(ext_status.operation, "Enable") self.assertEqual(ext_status.sequenceNumber, 0) self.assertEqual(ext_status.message, "Aenean semper nunc nisl, vitae sollicitudin felis consequat at. In " "lobortis elementum sapien, non commodo odio semper ac.") self.assertEqual(ext_status.status, ExtensionStatusValue.success) @patch("azurelinuxagent.common.conf.get_lib_dir") def test_collect_ext_status_very_large_status_message(self, mock_lib_dir): """ Testing collect_ext_status() with a very large status file (>128K) to see if it correctly parses the status without generating a really large message. """ ext_handler_i, extension = self._setup_extension_for_validating_collect_ext_status(mock_lib_dir, "sample-status-very-large.json") ext_status = ext_handler_i.collect_ext_status(extension) self.assertEqual(ext_status.code, SUCCESS_CODE_FROM_STATUS_FILE) self.assertEqual(ext_status.configurationAppliedTime, None) self.assertEqual(ext_status.operation, "Enable") self.assertEqual(ext_status.sequenceNumber, 0) # [TRUNCATED] comes from azurelinuxagent.ga.exthandlers._TRUNCATED_SUFFIX self.assertRegex(ext_status.message, r"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non " r"lacinia urna, sit .*\[TRUNCATED\]") self.maxDiff = None self.assertEqual(ext_status.status, ExtensionStatusValue.success) self.assertEqual(len(ext_status.substatusList), 1) # NUM OF SUBSTATUS PARSED for sub_status in ext_status.substatusList: self.assertRegex(sub_status.name, r'\[\{"status"\: \{"status": "success", "code": "1", "snapshotInfo": ' r'\[\{"snapshotUri":.*') self.assertEqual(0, sub_status.code) self.assertRegex(sub_status.message, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum " "non lacinia urna, sit amet venenatis orci.*") self.assertEqual(sub_status.status, ExtensionStatusValue.success) @patch("azurelinuxagent.common.conf.get_lib_dir") def test_collect_ext_status_very_large_status_file_with_multiple_substatus_nodes(self, mock_lib_dir): """ Testing collect_ext_status() with a very large status file (>128K) to see if it correctly parses the status without generating a really large message. This checks if the multiple substatus messages are correctly parsed and truncated. """ ext_handler_i, extension = self._setup_extension_for_validating_collect_ext_status( mock_lib_dir, "sample-status-very-large-multiple-substatuses.json") # ~470K bytes. ext_status = ext_handler_i.collect_ext_status(extension) self.assertEqual(ext_status.code, SUCCESS_CODE_FROM_STATUS_FILE) self.assertEqual(ext_status.configurationAppliedTime, None) self.assertEqual(ext_status.operation, "Enable") self.assertEqual(ext_status.sequenceNumber, 0) self.assertRegex(ext_status.message, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " "Vestibulum non lacinia urna, sit .*") self.assertEqual(ext_status.status, ExtensionStatusValue.success) self.assertEqual(len(ext_status.substatusList), 12) # The original file has 41 substatus nodes. for sub_status in ext_status.substatusList: self.assertRegex(sub_status.name, r'\[\{"status"\: \{"status": "success", "code": "1", "snapshotInfo": ' r'\[\{"snapshotUri":.*') self.assertEqual(0, sub_status.code) self.assertRegex(sub_status.message, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum " "non lacinia urna, sit amet venenatis orci.*") self.assertEqual(ExtensionStatusValue.success, sub_status.status) @patch("azurelinuxagent.common.conf.get_lib_dir") def test_collect_ext_status_read_file_read_exceptions(self, mock_lib_dir): """ Testing collect_ext_status to validate the readfile exceptions. """ ext_handler_i, extension = self._setup_extension_for_validating_collect_ext_status(mock_lib_dir, "sample-status.json") original_read_file = read_file def mock_read_file(file_, *args, **kwargs): expected_status_file_path = os.path.join(self.lib_dir, ext_handler_i.ext_handler.name + "-" + ext_handler_i.ext_handler.version, "status", "0.status") if file_ == expected_status_file_path: raise IOError("No such file or directory: {0}".format(expected_status_file_path)) else: original_read_file(file_, *args, **kwargs) with patch('azurelinuxagent.common.utils.fileutil.read_file', mock_read_file): ext_status = ext_handler_i.collect_ext_status(extension) self.assertEqual(ext_status.code, ExtensionErrorCodes.PluginUnknownFailure) self.assertEqual(ext_status.configurationAppliedTime, None) self.assertEqual(ext_status.operation, None) self.assertEqual(ext_status.sequenceNumber, 0) self.assertRegex(ext_status.message, r".*We couldn't read any status for {0}-{1} extension, for the " r"sequence number {2}. It failed due to". format("TestHandler", "1.0.0", 0)) self.assertEqual(ext_status.status, ExtensionStatusValue.error) self.assertEqual(len(ext_status.substatusList), 0) @patch("azurelinuxagent.common.conf.get_lib_dir") def test_collect_ext_status_json_exceptions(self, mock_lib_dir): """ Testing collect_ext_status() with a malformed json status file. """ ext_handler_i, extension = self._setup_extension_for_validating_collect_ext_status(mock_lib_dir, "sample-status-invalid-format-emptykey-line7.json") ext_status = ext_handler_i.collect_ext_status(extension) self.assertEqual(ext_status.code, ExtensionErrorCodes.PluginSettingsStatusInvalid) self.assertEqual(ext_status.configurationAppliedTime, None) self.assertEqual(ext_status.operation, None) self.assertEqual(ext_status.sequenceNumber, 0) self.assertRegex(ext_status.message, r".*The status reported by the extension {0}-{1}\(Sequence number {2}\), " "was in an incorrect format and the agent could not parse it correctly." " Failed due to.*". format("TestHandler", "1.0.0", 0)) self.assertEqual(ext_status.status, ExtensionStatusValue.error) self.assertEqual(len(ext_status.substatusList), 0) @patch("azurelinuxagent.common.conf.get_lib_dir") def test_collect_ext_status_parse_ext_status_exceptions(self, mock_lib_dir): """ Testing collect_ext_status() with a malformed json status file. """ ext_handler_i, extension = self._setup_extension_for_validating_collect_ext_status(mock_lib_dir, "sample-status-invalid-status-no-status-status-key.json") ext_status = ext_handler_i.collect_ext_status(extension) self.assertEqual(ext_status.code, ExtensionErrorCodes.PluginSettingsStatusInvalid) self.assertEqual(ext_status.configurationAppliedTime, None) self.assertEqual(ext_status.operation, None) self.assertEqual(ext_status.sequenceNumber, 0) self.assertRegex(ext_status.message, "Could not get a valid status from the extension {0}-{1}. " "Encountered the following error".format("TestHandler", "1.0.0")) self.assertEqual(ext_status.status, ExtensionStatusValue.error) self.assertEqual(len(ext_status.substatusList), 0) @patch("azurelinuxagent.common.conf.get_lib_dir") def test_it_should_report_transitioning_if_status_file_not_found(self, mock_lib_dir): """ Testing collect_ext_status() with a missing status file. """ ext_handler_i, extension = self._setup_extension_for_validating_collect_ext_status(mock_lib_dir) ext_status = ext_handler_i.collect_ext_status(extension) self.assertEqual(ext_status.code, ExtensionErrorCodes.PluginSuccess) self.assertEqual(ext_status.configurationAppliedTime, None) self.assertEqual(ext_status.operation, None) self.assertEqual(ext_status.sequenceNumber, 0) self.assertIn("This status is being reported by the Guest Agent since no status file was reported by extension {0}". format("TestHandler"), ext_status.message) self.assertEqual(ext_status.status, ExtensionStatusValue.transitioning) self.assertEqual(len(ext_status.substatusList), 0) class TestAdditionalLocationsExtensions(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) self.test_data = DATA_FILE_EXT_ADDITIONAL_LOCATIONS.copy() def tearDown(self): AgentTestCase.tearDown(self) @patch('time.sleep') def test_additional_locations_node_is_consumed(self, _): location_uri_pattern = r'https?://mock-goal-state/(?P{0})/(?P\d)/manifest.xml'\ .format(r'(location)|(failoverlocation)|(additionalLocation)') location_uri_regex = re.compile(location_uri_pattern) manifests_used = [ ('location', '1'), ('failoverlocation', '2'), ('additionalLocation', '3'), ('additionalLocation', '4') ] def manifest_location_handler(url, **kwargs): url_match = location_uri_regex.match(url) if not url_match: if "extensionArtifact" in url: wrapped_url = kwargs.get("headers", {}).get("x-ms-artifact-location") if wrapped_url and location_uri_regex.match(wrapped_url): return Exception("Ignoring host plugin requests for testing purposes.") return None location_type, manifest_num = url_match.group("location_type", "manifest_num") try: manifests_used.remove((location_type, manifest_num)) except ValueError: raise AssertionError("URI '{0}' used multiple times".format(url)) if manifests_used: # Still locations to try in the list; throw a fake # error to make sure all of the locations get called. return Exception("Failing manifest fetch from uri '{0}' for testing purposes.".format(url)) return None with mock_wire_protocol(self.test_data, http_get_handler=manifest_location_handler) as protocol: exthandlers_handler = get_exthandlers_handler(protocol) exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() def test_fetch_manifest_timeout_is_respected(self): location_uri_pattern = r'https?://mock-goal-state/(?P{0})/(?P\d)/manifest.xml'\ .format(r'(location)|(failoverlocation)|(additionalLocation)') location_uri_regex = re.compile(location_uri_pattern) def manifest_location_handler(url, **kwargs): url_match = location_uri_regex.match(url) if not url_match: if "extensionArtifact" in url: wrapped_url = kwargs.get("headers", {}).get("x-ms-artifact-location") if wrapped_url and location_uri_regex.match(wrapped_url): return Exception("Ignoring host plugin requests for testing purposes.") return None if manifest_location_handler.num_times_called == 0: time.sleep(.3) manifest_location_handler.num_times_called += 1 return Exception("Failing manifest fetch from uri '{0}' for testing purposes.".format(url)) return None manifest_location_handler.num_times_called = 0 with mock_wire_protocol(self.test_data, http_get_handler=manifest_location_handler) as protocol: ext_handlers = protocol.get_goal_state().extensions_goal_state.extensions download_timeout = wire._DOWNLOAD_TIMEOUT wire._DOWNLOAD_TIMEOUT = datetime.timedelta(minutes=0) try: with self.assertRaises(ExtensionDownloadError): protocol.client.fetch_manifest(ext_handlers[0].manifest_uris, use_verify_header=False) finally: wire._DOWNLOAD_TIMEOUT = download_timeout # New test cases should be added here.This class uses mock_wire_protocol class TestExtension(TestExtensionBase, HttpRequestPredicates): @classmethod def setUpClass(cls): AgentTestCase.setUpClass() cls.mock_sleep = patch('time.sleep', side_effect=lambda _: mock_sleep(0.001)) cls.mock_sleep.start() @classmethod def tearDownClass(cls): cls.mock_sleep.stop() def setUp(self): AgentTestCase.setUp(self) def tearDown(self): AgentTestCase.tearDown(self) @patch('time.gmtime', MagicMock(return_value=time.gmtime(0))) def test_ext_handler_reporting_status_file(self): with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: def mock_http_put(url, *args, **_): if HttpRequestPredicates.is_host_plugin_status_request(url): # Skip reading the HostGA request data as its encoded return MockHttpResponse(status=500) protocol.aggregate_status = json.loads(args[0]) return MockHttpResponse(status=201) protocol.aggregate_status = None protocol.set_http_handlers(http_put_handler=mock_http_put) exthandlers_handler = get_exthandlers_handler(protocol) # creating supported features list that is sent to crp supported_features = [] for _, feature in get_agent_supported_features_list_for_crp().items(): supported_features.append( { "Key": feature.name, "Value": feature.version } ) expected_status = { "__comment__": "The __status__ property is the actual status reported to CRP", "__status__": { "version": "1.1", "timestampUTC": "1970-01-01T00:00:00Z", "aggregateStatus": { "guestAgentStatus": { "version": AGENT_VERSION, "status": "Ready", "formattedMessage": { "lang": "en-US", "message": "Guest Agent is running" } }, "handlerAggregateStatus": [ { "handlerVersion": "1.0.0", "handlerName": "OSTCExtensions.ExampleHandlerLinux", "status": "Ready", "code": 0, "useExactVersion": True, "formattedMessage": { "lang": "en-US", "message": "Plugin enabled" }, "runtimeSettingsStatus": { "settingsStatus": { "status": { "name": "OSTCExtensions.ExampleHandlerLinux", "configurationAppliedTime": None, "operation": None, "status": "success", "code": 0, "formattedMessage": { "lang": "en-US", "message": None } }, "version": 1.0, "timestampUTC": "1970-01-01T00:00:00Z" }, "sequenceNumber": 0 } } ], "vmArtifactsAggregateStatus": { "goalStateAggregateStatus": { "formattedMessage": { "lang": "en-US", "message": "GoalState executed successfully" }, "timestampUTC": "1970-01-01T00:00:00Z", "inSvdSeqNo": "1", "status": "Success", "code": 0 } } }, "guestOSInfo": None, "supportedFeatures": supported_features }, "__debug__": { "agentName": AGENT_NAME, "daemonVersion": "0.0.0.0", "pythonVersion": "Python: {0}.{1}.{2}".format(PY_VERSION_MAJOR, PY_VERSION_MINOR, PY_VERSION_MICRO), "extensionSupportedFeatures": [name for name, _ in get_agent_supported_features_list_for_extensions().items()], "supportsMultiConfig": { "OSTCExtensions.ExampleHandlerLinux": False } } } exthandlers_handler.run() vm_status = exthandlers_handler.report_ext_handlers_status() actual_status_json = json.loads(exthandlers_handler.get_ext_handlers_status_debug_info(vm_status)) # Don't compare the guestOSInfo status_property = actual_status_json.get("__status__") self.assertIsNotNone(status_property, "The status file is missing the __status__ property") self.assertIsNotNone(status_property.get("guestOSInfo"), "The status file is missing the guestOSInfo property") status_property["guestOSInfo"] = None actual_status_json.pop('guestOSInfo', None) self.assertEqual(expected_status, actual_status_json) def test_it_should_process_extensions_only_if_allowed(self): def assert_extensions_called(exthandlers_handler, expected_call_count=0): extension_name = 'OSTCExtensions.ExampleHandlerLinux' extension_calls = [] original_popen = subprocess.Popen def mock_popen(*args, **kwargs): if extension_name in args[0]: extension_calls.append(args[0]) return original_popen(*args, **kwargs) with patch('subprocess.Popen', side_effect=mock_popen): exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(expected_call_count, len(extension_calls), "Call counts dont match") with patch('time.sleep', side_effect=lambda _: mock_sleep(0.001)): def http_get_handler(url, *_, **kwargs): if self.is_in_vm_artifacts_profile_request(url) or self.is_host_plugin_in_vm_artifacts_profile_request(url, kwargs): return mock_in_vm_artifacts_profile_response return None mock_in_vm_artifacts_profile_response = MockHttpResponse(200, body='{ "onHold": false }'.encode('utf-8')) with mock_wire_protocol(mockwiredata.DATA_FILE_IN_VM_ARTIFACTS_PROFILE, http_get_handler=http_get_handler) as protocol: protocol.report_vm_status = MagicMock() exthandlers_handler = get_exthandlers_handler(protocol) # Extension called once for Install and once for Enable assert_extensions_called(exthandlers_handler, expected_call_count=2) self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") # Update GoalState protocol.mock_wire_data.set_incarnation(2) protocol.client.update_goal_state() with patch.object(conf, 'get_extensions_enabled', return_value=False): assert_extensions_called(exthandlers_handler, expected_call_count=0) # Disabled over-provisioning in configuration # In this case we should process GoalState as incarnation changed with patch.object(conf, 'get_extensions_enabled', return_value=True): with patch.object(conf, 'get_enable_overprovisioning', return_value=False): # 1 expected call count for Enable command assert_extensions_called(exthandlers_handler, expected_call_count=1) # Enabled on_hold property in artifact_blob mock_in_vm_artifacts_profile_response = MockHttpResponse(200, body='{ "onHold": true }'.encode('utf-8')) protocol.client.reset_goal_state() with patch.object(conf, 'get_extensions_enabled', return_value=True): with patch.object(conf, "get_enable_overprovisioning", return_value=True): assert_extensions_called(exthandlers_handler, expected_call_count=0) # Disabled on_hold property in artifact_blob mock_in_vm_artifacts_profile_response = MockHttpResponse(200, body='{ "onHold": false }'.encode('utf-8')) protocol.client.reset_goal_state() with patch.object(conf, 'get_extensions_enabled', return_value=True): with patch.object(conf, "get_enable_overprovisioning", return_value=True): # 1 expected call count for Enable command assert_extensions_called(exthandlers_handler, expected_call_count=1) def test_it_should_process_extensions_appropriately_on_artifact_hold(self): with patch('time.sleep', side_effect=lambda _: mock_sleep(0.001)): with patch("azurelinuxagent.common.conf.get_enable_overprovisioning", return_value=True): with mock_wire_protocol(mockwiredata.DATA_FILE_IN_VM_ARTIFACTS_PROFILE) as protocol: protocol.report_vm_status = MagicMock() exthandlers_handler = get_exthandlers_handler(protocol) # # The test data sets onHold to True; extensions should not be processed # exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() vm_agent_status = protocol.report_vm_status.call_args[0][0].vmAgent self.assertEqual(vm_agent_status.status, "Ready", "Agent should report ready") self.assertEqual(0, len(vm_agent_status.extensionHandlers), "No extensions should be reported as on_hold is True") self.assertIsNone(vm_agent_status.vm_artifacts_aggregate_status.goal_state_aggregate_status, "No GS Aggregate status should be reported") # # Now force onHold to False; extensions should be processed # def http_get_handler(url, *_, **kwargs): if self.is_in_vm_artifacts_profile_request(url) or self.is_host_plugin_in_vm_artifacts_profile_request(url, kwargs): return MockHttpResponse(200, body='{ "onHold": false }'.encode('utf-8')) return None protocol.set_http_handlers(http_get_handler=http_get_handler) protocol.client.reset_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") self.assertEqual("1", protocol.report_vm_status.call_args[0][0].vmAgent.vm_artifacts_aggregate_status.goal_state_aggregate_status.in_svd_seq_no, "SVD sequence number mismatch") if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/ga/test_exthandlers.py000066400000000000000000001010461446033677600216750ustar00rootroot00000000000000# Copyright 2020 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import json import os import subprocess import time import uuid from azurelinuxagent.common.agent_supported_feature import AgentSupportedFeature from azurelinuxagent.common.event import AGENT_EVENT_FILE_EXTENSION, WALAEventOperation from azurelinuxagent.common.exception import ExtensionError, ExtensionErrorCodes from azurelinuxagent.common.protocol.restapi import ExtensionStatus, ExtensionSettings, Extension from azurelinuxagent.common.protocol.util import ProtocolUtil from azurelinuxagent.common.protocol.wire import WireProtocol from azurelinuxagent.common.utils import fileutil from azurelinuxagent.common.utils.extensionprocessutil import TELEMETRY_MESSAGE_MAX_LEN, format_stdout_stderr, \ read_output from azurelinuxagent.ga.exthandlers import parse_ext_status, ExtHandlerInstance, ExtCommandEnvVariable, \ ExtensionStatusError, _DEFAULT_SEQ_NO, get_exthandlers_handler, ExtHandlerState from tests.protocol.mocks import mock_wire_protocol, mockwiredata from tests.tools import AgentTestCase, patch, mock_sleep, clear_singleton_instances class TestExtHandlers(AgentTestCase): def setUp(self): super(TestExtHandlers, self).setUp() # Since ProtocolUtil is a singleton per thread, we need to clear it to ensure that the test cases do not # reuse a previous state clear_singleton_instances(ProtocolUtil) def test_parse_ext_status_should_raise_on_non_array(self): status = json.loads(''' {{ "status": {{ "status": "transitioning", "operation": "Enabling Handler", "code": 0, "name": "Microsoft.Azure.RecoveryServices.SiteRecovery.Linux" }}, "version": 1.0, "timestampUTC": "2020-01-14T15:04:43Z", "longText": "{0}" }}'''.format("*" * 5 * 1024)) with self.assertRaises(ExtensionStatusError) as context_manager: parse_ext_status(ExtensionStatus(seq_no=0), status) error_message = str(context_manager.exception) self.assertIn("The extension status must be an array", error_message) self.assertTrue(0 < len(error_message) - 64 < 4096, "The error message should not be much longer than 4K characters: [{0}]".format(error_message)) def test_parse_extension_status00(self): """ Parse a status report for a successful execution of an extension. """ s = '''[{ "status": { "status": "success", "formattedMessage": { "lang": "en-US", "message": "Command is finished." }, "operation": "Daemon", "code": "0", "name": "Microsoft.OSTCExtensions.CustomScriptForLinux" }, "version": "1.0", "timestampUTC": "2018-04-20T21:20:24Z" } ]''' ext_status = ExtensionStatus(seq_no=0) parse_ext_status(ext_status, json.loads(s)) self.assertEqual('0', ext_status.code) self.assertEqual(None, ext_status.configurationAppliedTime) self.assertEqual('Command is finished.', ext_status.message) self.assertEqual('Daemon', ext_status.operation) self.assertEqual('success', ext_status.status) self.assertEqual(0, ext_status.sequenceNumber) self.assertEqual(0, len(ext_status.substatusList)) def test_parse_extension_status01(self): """ Parse a status report for a failed execution of an extension. The extension returned a bad status/status of failed. The agent should handle this gracefully, and convert all unknown status/status values into an error. """ s = '''[{ "status": { "status": "failed", "formattedMessage": { "lang": "en-US", "message": "Enable failed: Failed with error: commandToExecute is empty or invalid ..." }, "operation": "Enable", "code": "0", "name": "Microsoft.OSTCExtensions.CustomScriptForLinux" }, "version": "1.0", "timestampUTC": "2018-04-20T20:50:22Z" }]''' ext_status = ExtensionStatus(seq_no=0) parse_ext_status(ext_status, json.loads(s)) self.assertEqual('0', ext_status.code) self.assertEqual(None, ext_status.configurationAppliedTime) self.assertEqual('Enable failed: Failed with error: commandToExecute is empty or invalid ...', ext_status.message) self.assertEqual('Enable', ext_status.operation) self.assertEqual('error', ext_status.status) self.assertEqual(0, ext_status.sequenceNumber) self.assertEqual(0, len(ext_status.substatusList)) def test_parse_ext_status_should_parse_missing_substatus_as_empty(self): status = '''[{ "status": { "status": "success", "formattedMessage": { "lang": "en-US", "message": "Command is finished." }, "operation": "Enable", "code": "0", "name": "Microsoft.OSTCExtensions.CustomScriptForLinux" }, "version": "1.0", "timestampUTC": "2018-04-20T21:20:24Z" } ]''' extension_status = ExtensionStatus(seq_no=0) parse_ext_status(extension_status, json.loads(status)) self.assertTrue(isinstance(extension_status.substatusList, list), 'substatus was not parsed correctly') self.assertEqual(0, len(extension_status.substatusList)) def test_parse_ext_status_should_parse_null_substatus_as_empty(self): status = '''[{ "status": { "status": "success", "formattedMessage": { "lang": "en-US", "message": "Command is finished." }, "operation": "Enable", "code": "0", "name": "Microsoft.OSTCExtensions.CustomScriptForLinux", "substatus": null }, "version": "1.0", "timestampUTC": "2018-04-20T21:20:24Z" } ]''' extension_status = ExtensionStatus(seq_no=0) parse_ext_status(extension_status, json.loads(status)) self.assertTrue(isinstance(extension_status.substatusList, list), 'substatus was not parsed correctly') self.assertEqual(0, len(extension_status.substatusList)) def test_parse_extension_status_with_empty_status(self): """ Parse a status report for a successful execution of an extension. """ # Validating empty status case s = '''[]''' ext_status = ExtensionStatus(seq_no=0) parse_ext_status(ext_status, json.loads(s)) self.assertEqual(None, ext_status.code) self.assertEqual(None, ext_status.configurationAppliedTime) self.assertEqual(None, ext_status.message) self.assertEqual(None, ext_status.operation) self.assertEqual(None, ext_status.status) self.assertEqual(0, ext_status.sequenceNumber) self.assertEqual(0, len(ext_status.substatusList)) # Validating None case ext_status = ExtensionStatus(seq_no=0) parse_ext_status(ext_status, None) self.assertEqual(None, ext_status.code) self.assertEqual(None, ext_status.configurationAppliedTime) self.assertEqual(None, ext_status.message) self.assertEqual(None, ext_status.operation) self.assertEqual(None, ext_status.status) self.assertEqual(0, ext_status.sequenceNumber) self.assertEqual(0, len(ext_status.substatusList)) @patch('azurelinuxagent.ga.exthandlers.ExtHandlerInstance._get_last_modified_seq_no_from_config_files') def assert_extension_sequence_number(self, patch_get_largest_seq=None, goal_state_sequence_number=None, disk_sequence_number=None, expected_sequence_number=None): ext = ExtensionSettings() ext.sequenceNumber = goal_state_sequence_number patch_get_largest_seq.return_value = disk_sequence_number ext_handler = Extension(name='foo') ext_handler.version = "1.2.3" instance = ExtHandlerInstance(ext_handler=ext_handler, protocol=None) seq, path = instance.get_status_file_path(ext) self.assertEqual(expected_sequence_number, seq) if seq > -1: self.assertTrue(path.endswith('/foo-1.2.3/status/{0}.status'.format(expected_sequence_number))) else: self.assertIsNone(path) def test_extension_sequence_number(self): self.assert_extension_sequence_number(goal_state_sequence_number="12", disk_sequence_number=366, expected_sequence_number=12) self.assert_extension_sequence_number(goal_state_sequence_number=" 12 ", disk_sequence_number=366, expected_sequence_number=12) self.assert_extension_sequence_number(goal_state_sequence_number=" foo", disk_sequence_number=3, expected_sequence_number=3) self.assert_extension_sequence_number(goal_state_sequence_number="-1", disk_sequence_number=3, expected_sequence_number=-1) def test_it_should_report_error_if_plugin_settings_version_mismatch(self): with mock_wire_protocol(mockwiredata.DATA_FILE_PLUGIN_SETTINGS_MISMATCH) as protocol: with patch("azurelinuxagent.common.protocol.extensions_goal_state_from_extensions_config.add_event") as mock_add_event: # Forcing update of GoalState to allow the ExtConfig to report an event protocol.mock_wire_data.set_incarnation(2) protocol.client.update_goal_state() plugin_setting_mismatch_calls = [kw for _, kw in mock_add_event.call_args_list if kw['op'] == WALAEventOperation.PluginSettingsVersionMismatch] self.assertEqual(1, len(plugin_setting_mismatch_calls), "PluginSettingsMismatch event should be reported once") self.assertIn('Extension PluginSettings Version Mismatch! Expected PluginSettings version: 1.0.0 for Extension: OSTCExtensions.ExampleHandlerLinux' , plugin_setting_mismatch_calls[0]['message'], "Invalid error message with incomplete data detected for PluginSettingsVersionMismatch") self.assertTrue("1.0.2" in plugin_setting_mismatch_calls[0]['message'] and "1.0.1" in plugin_setting_mismatch_calls[0]['message'], "Error message should contain the incorrect versions") self.assertFalse(plugin_setting_mismatch_calls[0]['is_success'], "The event should be false") @patch("azurelinuxagent.common.conf.get_ext_log_dir") def test_command_extension_log_truncates_correctly(self, mock_log_dir): log_dir_path = os.path.join(self.tmp_dir, "log_directory") mock_log_dir.return_value = log_dir_path ext_handler = Extension(name='foo') ext_handler.version = "1.2.3" first_line = "This is the first line!" second_line = "This is the second line." old_logfile_contents = "{first_line}\n{second_line}\n".format(first_line=first_line, second_line=second_line) log_file_path = os.path.join(log_dir_path, "foo", "CommandExecution.log") fileutil.mkdir(os.path.join(log_dir_path, "foo"), mode=0o755) with open(log_file_path, "a") as log_file: log_file.write(old_logfile_contents) _ = ExtHandlerInstance(ext_handler=ext_handler, protocol=None, execution_log_max_size=(len(first_line)+len(second_line)//2)) with open(log_file_path) as truncated_log_file: self.assertEqual(truncated_log_file.read(), "{second_line}\n".format(second_line=second_line)) def test_it_should_report_the_message_in_the_hearbeat(self): def heartbeat_with_message(): return {'code': 0, 'formattedMessage': {'lang': 'en-US', 'message': 'This is a heartbeat message'}, 'status': 'ready'} with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: with patch("azurelinuxagent.common.protocol.wire.WireProtocol.report_vm_status", return_value=None): with patch("azurelinuxagent.ga.exthandlers.ExtHandlerInstance.collect_heartbeat", side_effect=heartbeat_with_message): with patch("azurelinuxagent.ga.exthandlers.ExtHandlerInstance.get_handler_state", return_value=ExtHandlerState.Enabled): with patch("azurelinuxagent.ga.exthandlers.ExtHandlerInstance.collect_ext_status", return_value=None): exthandlers_handler = get_exthandlers_handler(protocol) exthandlers_handler.run() vm_status = exthandlers_handler.report_ext_handlers_status() ext_handler = vm_status.vmAgent.extensionHandlers[0] self.assertEqual(ext_handler.message, heartbeat_with_message().get('formattedMessage').get('message'), "Extension handler messages don't match") self.assertEqual(ext_handler.status, heartbeat_with_message().get('status'), "Extension handler statuses don't match") class LaunchCommandTestCase(AgentTestCase): """ Test cases for launch_command """ def setUp(self): AgentTestCase.setUp(self) self.ext_handler = Extension(name='foo') self.ext_handler.version = "1.2.3" self.ext_handler_instance = ExtHandlerInstance(ext_handler=self.ext_handler, protocol=WireProtocol("1.2.3.4")) self.mock_get_base_dir = patch("azurelinuxagent.ga.exthandlers.ExtHandlerInstance.get_base_dir", lambda *_: self.tmp_dir) self.mock_get_base_dir.start() self.log_dir = os.path.join(self.tmp_dir, "log") self.mock_get_log_dir = patch("azurelinuxagent.ga.exthandlers.ExtHandlerInstance.get_log_dir", lambda *_: self.log_dir) self.mock_get_log_dir.start() self.mock_sleep = patch("time.sleep", lambda *_: mock_sleep(0.01)) self.mock_sleep.start() def tearDown(self): self.mock_get_log_dir.stop() self.mock_get_base_dir.stop() self.mock_sleep.stop() AgentTestCase.tearDown(self) @staticmethod def _output_regex(stdout, stderr): return r"\[stdout\]\s+{0}\s+\[stderr\]\s+{1}".format(stdout, stderr) @staticmethod def _find_process(command): for pid in [pid for pid in os.listdir('/proc') if pid.isdigit()]: try: with open(os.path.join('/proc', pid, 'cmdline'), 'r') as cmdline: for line in cmdline.readlines(): if command in line: return True except IOError: # proc has already terminated continue return False def test_it_should_capture_the_output_of_the_command(self): stdout = "stdout" * 5 stderr = "stderr" * 5 command = "produce_output.py" self.create_script(os.path.join(self.ext_handler_instance.get_base_dir(), command), ''' import sys sys.stdout.write("{0}") sys.stderr.write("{1}") '''.format(stdout, stderr)) def list_directory(): base_dir = self.ext_handler_instance.get_base_dir() return [i for i in os.listdir(base_dir) if not i.endswith(AGENT_EVENT_FILE_EXTENSION)] # ignore telemetry files files_before = list_directory() output = self.ext_handler_instance.launch_command(command) files_after = list_directory() self.assertRegex(output, LaunchCommandTestCase._output_regex(stdout, stderr)) self.assertListEqual(files_before, files_after, "Not all temporary files were deleted. File list: {0}".format(files_after)) def test_it_should_raise_an_exception_when_the_command_times_out(self): extension_error_code = ExtensionErrorCodes.PluginHandlerScriptTimedout stdout = "stdout" * 7 stderr = "stderr" * 7 # the signal file is used by the test command to indicate it has produced output signal_file = os.path.join(self.tmp_dir, "signal_file.txt") # the test command produces some output then goes into an infinite loop command = "produce_output_then_hang.py" self.create_script(os.path.join(self.ext_handler_instance.get_base_dir(), command), ''' import sys import time sys.stdout.write("{0}") sys.stdout.flush() sys.stderr.write("{1}") sys.stderr.flush() with open("{2}", "w") as file: while True: file.write(".") time.sleep(1) '''.format(stdout, stderr, signal_file)) # mock time.sleep to wait for the signal file (launch_command implements the time out using polling and sleep) def sleep(seconds): if not os.path.exists(signal_file): sleep.original_sleep(seconds) sleep.original_sleep = time.sleep timeout = 60 start_time = time.time() with patch("time.sleep", side_effect=sleep, autospec=True) as mock_sleep: # pylint: disable=redefined-outer-name with self.assertRaises(ExtensionError) as context_manager: self.ext_handler_instance.launch_command(command, timeout=timeout, extension_error_code=extension_error_code) # the command name and its output should be part of the message message = str(context_manager.exception) command_full_path = os.path.join(self.tmp_dir, command.lstrip(os.path.sep)) self.assertRegex(message, r"Timeout\(\d+\):\s+{0}\s+{1}".format(command_full_path, LaunchCommandTestCase._output_regex(stdout, stderr))) # the exception code should be as specified in the call to launch_command self.assertEqual(context_manager.exception.code, extension_error_code) # the timeout period should have elapsed self.assertGreaterEqual(mock_sleep.call_count, timeout) # The command should have been terminated. # The /proc file system may still include the process when we do this check so we try a few times after a short delay; note that we # are mocking sleep, so we need to use the original implementation. terminated = False i = 0 while not terminated and i < 4: if not LaunchCommandTestCase._find_process(command): terminated = True else: sleep.original_sleep(0.25) i += 1 self.assertTrue(terminated, "The command was not terminated") # as a check for the test itself, verify it completed in just a few seconds self.assertLessEqual(time.time() - start_time, 5) def test_it_should_raise_an_exception_when_the_command_fails(self): extension_error_code = 2345 stdout = "stdout" * 3 stderr = "stderr" * 3 exit_code = 99 command = "fail.py" self.create_script(os.path.join(self.ext_handler_instance.get_base_dir(), command), ''' import sys sys.stdout.write("{0}") sys.stderr.write("{1}") exit({2}) '''.format(stdout, stderr, exit_code)) # the output is captured as part of the exception message with self.assertRaises(ExtensionError) as context_manager: self.ext_handler_instance.launch_command(command, extension_error_code=extension_error_code) message = str(context_manager.exception) self.assertRegex(message, r"Non-zero exit code: {0}.+{1}\s+{2}".format(exit_code, command, LaunchCommandTestCase._output_regex(stdout, stderr))) self.assertEqual(context_manager.exception.code, extension_error_code) def test_it_should_not_wait_for_child_process(self): stdout = "stdout" stderr = "stderr" command = "start_child_process.py" self.create_script(os.path.join(self.ext_handler_instance.get_base_dir(), command), ''' import os import sys import time pid = os.fork() if pid == 0: time.sleep(60) else: sys.stdout.write("{0}") sys.stderr.write("{1}") '''.format(stdout, stderr)) start_time = time.time() output = self.ext_handler_instance.launch_command(command) self.assertLessEqual(time.time() - start_time, 5) # Also check that we capture the parent's output self.assertRegex(output, LaunchCommandTestCase._output_regex(stdout, stderr)) def test_it_should_capture_the_output_of_child_process(self): parent_stdout = "PARENT STDOUT" parent_stderr = "PARENT STDERR" child_stdout = "CHILD STDOUT" child_stderr = "CHILD STDERR" more_parent_stdout = "MORE PARENT STDOUT" more_parent_stderr = "MORE PARENT STDERR" # the child process uses the signal file to indicate it has produced output signal_file = os.path.join(self.tmp_dir, "signal_file.txt") command = "start_child_with_output.py" self.create_script(os.path.join(self.ext_handler_instance.get_base_dir(), command), ''' import os import sys import time sys.stdout.write("{0}") sys.stderr.write("{1}") pid = os.fork() if pid == 0: sys.stdout.write("{2}") sys.stderr.write("{3}") open("{6}", "w").close() else: sys.stdout.write("{4}") sys.stderr.write("{5}") while not os.path.exists("{6}"): time.sleep(0.5) '''.format(parent_stdout, parent_stderr, child_stdout, child_stderr, more_parent_stdout, more_parent_stderr, signal_file)) output = self.ext_handler_instance.launch_command(command) self.assertIn(parent_stdout, output) self.assertIn(parent_stderr, output) self.assertIn(child_stdout, output) self.assertIn(child_stderr, output) self.assertIn(more_parent_stdout, output) self.assertIn(more_parent_stderr, output) def test_it_should_capture_the_output_of_child_process_that_fails_to_start(self): parent_stdout = "PARENT STDOUT" parent_stderr = "PARENT STDERR" child_stdout = "CHILD STDOUT" child_stderr = "CHILD STDERR" command = "start_child_that_fails.py" self.create_script(os.path.join(self.ext_handler_instance.get_base_dir(), command), ''' import os import sys import time pid = os.fork() if pid == 0: sys.stdout.write("{0}") sys.stderr.write("{1}") exit(1) else: sys.stdout.write("{2}") sys.stderr.write("{3}") '''.format(child_stdout, child_stderr, parent_stdout, parent_stderr)) output = self.ext_handler_instance.launch_command(command) self.assertIn(parent_stdout, output) self.assertIn(parent_stderr, output) self.assertIn(child_stdout, output) self.assertIn(child_stderr, output) def test_it_should_execute_commands_with_no_output(self): # file used to verify the command completed successfully signal_file = os.path.join(self.tmp_dir, "signal_file.txt") command = "create_file.py" self.create_script(os.path.join(self.ext_handler_instance.get_base_dir(), command), ''' open("{0}", "w").close() '''.format(signal_file)) output = self.ext_handler_instance.launch_command(command) self.assertTrue(os.path.exists(signal_file)) self.assertRegex(output, LaunchCommandTestCase._output_regex('', '')) def test_it_should_not_capture_the_output_of_commands_that_do_their_own_redirection(self): # the test script redirects its output to this file command_output_file = os.path.join(self.tmp_dir, "command_output.txt") stdout = "STDOUT" stderr = "STDERR" # the test script mimics the redirection done by the Custom Script extension command = "produce_output" self.create_script(os.path.join(self.ext_handler_instance.get_base_dir(), command), ''' exec &> {0} echo {1} >&2 echo {2} '''.format(command_output_file, stdout, stderr)) output = self.ext_handler_instance.launch_command(command) self.assertRegex(output, LaunchCommandTestCase._output_regex('', '')) with open(command_output_file, "r") as command_output: output = command_output.read() self.assertEqual(output, "{0}\n{1}\n".format(stdout, stderr)) def test_it_should_truncate_the_command_output(self): stdout = "STDOUT" stderr = "STDERR" command = "produce_long_output.py" self.create_script(os.path.join(self.ext_handler_instance.get_base_dir(), command), ''' import sys sys.stdout.write( "{0}" * {1}) sys.stderr.write( "{2}" * {3}) '''.format(stdout, int(TELEMETRY_MESSAGE_MAX_LEN / len(stdout)), stderr, int(TELEMETRY_MESSAGE_MAX_LEN / len(stderr)))) output = self.ext_handler_instance.launch_command(command) self.assertLessEqual(len(output), TELEMETRY_MESSAGE_MAX_LEN) self.assertIn(stdout, output) self.assertIn(stderr, output) def test_it_should_read_only_the_head_of_large_outputs(self): command = "produce_long_output.py" self.create_script(os.path.join(self.ext_handler_instance.get_base_dir(), command), ''' import sys sys.stdout.write("O" * 5 * 1024 * 1024) sys.stderr.write("E" * 5 * 1024 * 1024) ''') # Mocking the call to file.read() is difficult, so instead we mock the call to format_stdout_stderr, which takes the # return value of the calls to file.read(). The intention of the test is to verify we never read (and load in memory) # more than a few KB of data from the files used to capture stdout/stderr with patch('azurelinuxagent.common.utils.extensionprocessutil.format_stdout_stderr', side_effect=format_stdout_stderr) as mock_format: output = self.ext_handler_instance.launch_command(command) self.assertGreaterEqual(len(output), 1024) self.assertLessEqual(len(output), TELEMETRY_MESSAGE_MAX_LEN) mock_format.assert_called_once() args, kwargs = mock_format.call_args # pylint: disable=unused-variable stdout, stderr = args self.assertGreaterEqual(len(stdout), 1024) self.assertLessEqual(len(stdout), TELEMETRY_MESSAGE_MAX_LEN) self.assertGreaterEqual(len(stderr), 1024) self.assertLessEqual(len(stderr), TELEMETRY_MESSAGE_MAX_LEN) def test_it_should_handle_errors_while_reading_the_command_output(self): command = "produce_output.py" self.create_script(os.path.join(self.ext_handler_instance.get_base_dir(), command), ''' import sys sys.stdout.write("STDOUT") sys.stderr.write("STDERR") ''') # Mocking the call to file.read() is difficult, so instead we mock the call to_capture_process_output, # which will call file.read() and we force stdout/stderr to be None; this will produce an exception when # trying to use these files. original_capture_process_output = read_output def capture_process_output(stdout_file, stderr_file): # pylint: disable=unused-argument return original_capture_process_output(None, None) with patch('azurelinuxagent.common.utils.extensionprocessutil.read_output', side_effect=capture_process_output): output = self.ext_handler_instance.launch_command(command) self.assertIn("[stderr]\nCannot read stdout/stderr:", output) def test_it_should_contain_all_helper_environment_variables(self): wire_ip = str(uuid.uuid4()) ext_handler_instance = ExtHandlerInstance(ext_handler=self.ext_handler, protocol=WireProtocol(wire_ip)) helper_env_vars = {ExtCommandEnvVariable.ExtensionSeqNumber: _DEFAULT_SEQ_NO, ExtCommandEnvVariable.ExtensionPath: self.tmp_dir, ExtCommandEnvVariable.ExtensionVersion: ext_handler_instance.ext_handler.version, ExtCommandEnvVariable.WireProtocolAddress: wire_ip} command = """ printenv | grep -E '(%s)' """ % '|'.join(helper_env_vars.keys()) test_file = 'printHelperEnvironments.sh' self.create_script(os.path.join(self.ext_handler_instance.get_base_dir(), test_file), command) with patch("subprocess.Popen", wraps=subprocess.Popen) as patch_popen: # Returning empty list for get_agent_supported_features_list_for_extensions as we have a separate test for it with patch("azurelinuxagent.ga.exthandlers.get_agent_supported_features_list_for_extensions", return_value={}): output = ext_handler_instance.launch_command(test_file) args, kwagrs = patch_popen.call_args # pylint: disable=unused-variable without_os_env = dict((k, v) for (k, v) in kwagrs['env'].items() if k not in os.environ) # This check will fail if any helper environment variables are added/removed later on self.assertEqual(helper_env_vars, without_os_env) # This check is checking if the expected values are set for the extension commands for helper_var in helper_env_vars: self.assertIn("%s=%s" % (helper_var, helper_env_vars[helper_var]), output) def test_it_should_pass_supported_features_list_as_environment_variables(self): class TestFeature(AgentSupportedFeature): def __init__(self, name, version, supported): super(TestFeature, self).__init__(name=name, version=version, supported=supported) test_name = str(uuid.uuid4()) test_version = str(uuid.uuid4()) command = "check_env.py" self.create_script(os.path.join(self.ext_handler_instance.get_base_dir(), command), ''' import os import json import sys features = os.getenv("{0}") if not features: print("{0} not found in environment") sys.exit(0) l = json.loads(features) found = False for feature in l: if feature['Key'] == "{1}" and feature['Value'] == "{2}": found = True break print("Found Feature %s: %s" % ("{1}", found)) '''.format(ExtCommandEnvVariable.ExtensionSupportedFeatures, test_name, test_version)) # It should include all supported features and pass it as Environment Variable to extensions test_supported_features = {test_name: TestFeature(name=test_name, version=test_version, supported=True)} with patch("azurelinuxagent.common.agent_supported_feature.__EXTENSION_ADVERTISED_FEATURES", test_supported_features): output = self.ext_handler_instance.launch_command(command) self.assertIn("[stdout]\nFound Feature {0}: True".format(test_name), output, "Feature not found") # It should not include the feature if feature not supported test_supported_features = { test_name: TestFeature(name=test_name, version=test_version, supported=False), "testFeature": TestFeature(name="testFeature", version="1.2.1", supported=True) } with patch("azurelinuxagent.common.agent_supported_feature.__EXTENSION_ADVERTISED_FEATURES", test_supported_features): output = self.ext_handler_instance.launch_command(command) self.assertIn("[stdout]\nFound Feature {0}: False".format(test_name), output, "Feature wrongfully found") # It should not include the SupportedFeatures Key in Environment variables if no features supported test_supported_features = {test_name: TestFeature(name=test_name, version=test_version, supported=False)} with patch("azurelinuxagent.common.agent_supported_feature.__EXTENSION_ADVERTISED_FEATURES", test_supported_features): output = self.ext_handler_instance.launch_command(command) self.assertIn( "[stdout]\n{0} not found in environment".format(ExtCommandEnvVariable.ExtensionSupportedFeatures), output, "Environment variable should not be found") WALinuxAgent-2.9.1.1/tests/ga/test_exthandlers_download_extension.py000066400000000000000000000310421446033677600256560ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the Apache License. import contextlib import os import time import zipfile from azurelinuxagent.common.exception import ExtensionDownloadError, ExtensionErrorCodes from azurelinuxagent.common.protocol.restapi import Extension, ExtHandlerPackage from azurelinuxagent.common.protocol.wire import WireProtocol from azurelinuxagent.ga.exthandlers import ExtHandlerInstance, ExtHandlerState from tests.protocol import mockwiredata from tests.protocol.mocks import mock_wire_protocol from tests.tools import AgentTestCase, patch, Mock class DownloadExtensionTestCase(AgentTestCase): """ Test cases for launch_command """ @classmethod def setUpClass(cls): AgentTestCase.setUpClass() cls.mock_cgroups = patch("azurelinuxagent.ga.exthandlers.CGroupConfigurator") cls.mock_cgroups.start() @classmethod def tearDownClass(cls): cls.mock_cgroups.stop() AgentTestCase.tearDownClass() def setUp(self): AgentTestCase.setUp(self) ext_handler = Extension(name='Microsoft.CPlat.Core.RunCommandLinux') ext_handler.version = "1.0.0" protocol = WireProtocol("http://Microsoft.CPlat.Core.RunCommandLinux/foo-bar") protocol.client.get_host_plugin = Mock() protocol.client.get_artifact_request = Mock(return_value=(None, None)) # create a dummy goal state, since downloads are done via the GoalState class with mock_wire_protocol(mockwiredata.DATA_FILE) as p: goal_state = p.get_goal_state() goal_state._wire_client = protocol.client protocol.client._goal_state = goal_state self.pkg = ExtHandlerPackage() self.pkg.uris = [ 'https://zrdfepirv2cy4prdstr00a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712-foobar/Microsoft.CPlat.Core__RunCommandLinux__1.0.0', 'https://zrdfepirv2cy4prdstr01a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712-foobar/Microsoft.CPlat.Core__RunCommandLinux__1.0.0', 'https://zrdfepirv2cy4prdstr02a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712-foobar/Microsoft.CPlat.Core__RunCommandLinux__1.0.0', 'https://zrdfepirv2cy4prdstr03a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712-foobar/Microsoft.CPlat.Core__RunCommandLinux__1.0.0', 'https://zrdfepirv2cy4prdstr04a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712-foobar/Microsoft.CPlat.Core__RunCommandLinux__1.0.0' ] self.ext_handler_instance = ExtHandlerInstance(ext_handler=ext_handler, protocol=protocol) self.ext_handler_instance.pkg = self.pkg self.extension_dir = os.path.join(self.tmp_dir, "Microsoft.CPlat.Core.RunCommandLinux-1.0.0") self.mock_get_base_dir = patch("azurelinuxagent.ga.exthandlers.ExtHandlerInstance.get_base_dir", return_value=self.extension_dir) self.mock_get_base_dir.start() self.mock_get_log_dir = patch("azurelinuxagent.ga.exthandlers.ExtHandlerInstance.get_log_dir", return_value=self.tmp_dir) self.mock_get_log_dir.start() self.agent_dir = self.tmp_dir self.mock_get_lib_dir = patch("azurelinuxagent.ga.exthandlers.conf.get_lib_dir", return_value=self.agent_dir) self.mock_get_lib_dir.start() def tearDown(self): self.mock_get_lib_dir.stop() self.mock_get_log_dir.stop() self.mock_get_base_dir.stop() AgentTestCase.tearDown(self) _extension_command = "RunCommandLinux.sh" @staticmethod def _create_zip_file(filename): file = None # pylint: disable=redefined-builtin try: file = zipfile.ZipFile(filename, "w") info = zipfile.ZipInfo(DownloadExtensionTestCase._extension_command) info.date_time = time.localtime(time.time())[:6] info.compress_type = zipfile.ZIP_DEFLATED file.writestr(info, "#!/bin/sh\necho 'RunCommandLinux executed successfully'\n") finally: if file is not None: file.close() @staticmethod def _create_invalid_zip_file(filename): with open(filename, "w") as file: # pylint: disable=redefined-builtin file.write("An invalid ZIP file\n") def _get_extension_base_dir(self): return self.extension_dir def _get_extension_package_file(self): return os.path.join(self.agent_dir, self.ext_handler_instance.get_extension_package_zipfile_name()) def _get_extension_command_file(self): return os.path.join(self.extension_dir, DownloadExtensionTestCase._extension_command) def _assert_download_and_expand_succeeded(self): self.assertTrue(os.path.exists(self._get_extension_base_dir()), "The extension package was not downloaded to the expected location") self.assertTrue(os.path.exists(self._get_extension_command_file()), "The extension package was not expanded to the expected location") @staticmethod @contextlib.contextmanager def create_mock_stream(stream_function): with patch("azurelinuxagent.common.protocol.wire.WireClient.stream", side_effect=stream_function) as mock_stream: mock_stream.download_failures = 0 with patch('time.sleep'): # don't sleep in-between retries yield mock_stream def test_it_should_download_and_expand_extension_package(self): def stream(_, destination, **__): DownloadExtensionTestCase._create_zip_file(destination) return True with DownloadExtensionTestCase.create_mock_stream(stream) as mock_stream: with patch("azurelinuxagent.ga.exthandlers.ExtHandlerInstance.report_event") as mock_report_event: self.ext_handler_instance.download() # first download attempt should succeed mock_stream.assert_called_once() mock_report_event.assert_called_once() self._assert_download_and_expand_succeeded() def test_it_should_use_existing_extension_package_when_already_downloaded(self): DownloadExtensionTestCase._create_zip_file(self._get_extension_package_file()) with DownloadExtensionTestCase.create_mock_stream(lambda: None) as mock_stream: with patch("azurelinuxagent.ga.exthandlers.ExtHandlerInstance.report_event") as mock_report_event: self.ext_handler_instance.download() mock_stream.assert_not_called() mock_report_event.assert_not_called() self.assertTrue(os.path.exists(self._get_extension_command_file()), "The extension package was not expanded to the expected location") def test_it_should_ignore_existing_extension_package_when_it_is_invalid(self): def stream(_, destination, **__): DownloadExtensionTestCase._create_zip_file(destination) return True DownloadExtensionTestCase._create_invalid_zip_file(self._get_extension_package_file()) with DownloadExtensionTestCase.create_mock_stream(stream) as mock_stream: self.ext_handler_instance.download() mock_stream.assert_called_once() self._assert_download_and_expand_succeeded() def test_it_should_maintain_extension_handler_state_when_good_zip_exists(self): DownloadExtensionTestCase._create_zip_file(self._get_extension_package_file()) self.ext_handler_instance.set_handler_state(ExtHandlerState.NotInstalled) self.ext_handler_instance.download() self._assert_download_and_expand_succeeded() self.assertTrue(os.path.exists(os.path.join(self.ext_handler_instance.get_conf_dir(), "HandlerState")), "Ensure that the HandlerState file exists on disk") self.assertEqual(self.ext_handler_instance.get_handler_state(), ExtHandlerState.NotInstalled, "Ensure that the state is maintained for extension HandlerState") def test_it_should_maintain_extension_handler_state_when_bad_zip_exists_and_recovers_with_good_zip(self): def stream(_, destination, **__): DownloadExtensionTestCase._create_zip_file(destination) return True DownloadExtensionTestCase._create_invalid_zip_file(self._get_extension_package_file()) self.ext_handler_instance.set_handler_state(ExtHandlerState.NotInstalled) with DownloadExtensionTestCase.create_mock_stream(stream) as mock_stream: self.ext_handler_instance.download() mock_stream.assert_called_once() self._assert_download_and_expand_succeeded() self.assertEqual(self.ext_handler_instance.get_handler_state(), ExtHandlerState.NotInstalled, "Ensure that the state is maintained for extension HandlerState") def test_it_should_maintain_extension_handler_state_when_it_downloads_bad_zips(self): def stream(_, destination, **__): DownloadExtensionTestCase._create_invalid_zip_file(destination) return True self.ext_handler_instance.set_handler_state(ExtHandlerState.NotInstalled) with DownloadExtensionTestCase.create_mock_stream(stream): with self.assertRaises(ExtensionDownloadError): self.ext_handler_instance.download() self.assertFalse(os.path.exists(self._get_extension_package_file()), "The bad zip extension package should not be downloaded to the expected location") self.assertFalse(os.path.exists(self._get_extension_command_file()), "The extension package should not expanded be to the expected location due to bad zip") self.assertEqual(self.ext_handler_instance.get_handler_state(), ExtHandlerState.NotInstalled, "Ensure that the state is maintained for extension HandlerState") def test_it_should_use_alternate_uris_when_download_fails(self): def stream(_, destination, **__): # fail a few times, then succeed if mock_stream.download_failures < 3: mock_stream.download_failures += 1 return None DownloadExtensionTestCase._create_zip_file(destination) return True with DownloadExtensionTestCase.create_mock_stream(stream) as mock_stream: self.ext_handler_instance.download() self.assertEqual(mock_stream.call_count, mock_stream.download_failures + 1) self._assert_download_and_expand_succeeded() def test_it_should_use_alternate_uris_when_download_raises_an_exception(self): def stream(_, destination, **__): # fail a few times, then succeed if mock_stream.download_failures < 3: mock_stream.download_failures += 1 raise Exception("Download failed") DownloadExtensionTestCase._create_zip_file(destination) return True with DownloadExtensionTestCase.create_mock_stream(stream) as mock_stream: self.ext_handler_instance.download() self.assertEqual(mock_stream.call_count, mock_stream.download_failures + 1) self._assert_download_and_expand_succeeded() def test_it_should_use_alternate_uris_when_it_downloads_an_invalid_package(self): def stream(_, destination, **__): # fail a few times, then succeed if mock_stream.download_failures < 3: mock_stream.download_failures += 1 DownloadExtensionTestCase._create_invalid_zip_file(destination) else: DownloadExtensionTestCase._create_zip_file(destination) return True with DownloadExtensionTestCase.create_mock_stream(stream) as mock_stream: self.ext_handler_instance.download() self.assertEqual(mock_stream.call_count, mock_stream.download_failures + 1) self._assert_download_and_expand_succeeded() def test_it_should_raise_an_exception_when_all_downloads_fail(self): def stream(_, target_file, **___): stream.target_file = target_file DownloadExtensionTestCase._create_invalid_zip_file(target_file) return True stream.target_file = None with DownloadExtensionTestCase.create_mock_stream(stream) as mock_stream: with self.assertRaises(ExtensionDownloadError) as context_manager: self.ext_handler_instance.download() self.assertEqual(mock_stream.call_count, len(self.pkg.uris)) self.assertRegex(str(context_manager.exception), "Failed to download extension") self.assertEqual(context_manager.exception.code, ExtensionErrorCodes.PluginManifestDownloadError) self.assertFalse(os.path.exists(self.extension_dir), "The extension directory was not removed") self.assertFalse(os.path.exists(stream.target_file), "The extension package was not removed") WALinuxAgent-2.9.1.1/tests/ga/test_exthandlers_exthandlerinstance.py000066400000000000000000000142601446033677600256410ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the Apache License. import os import shutil import sys from azurelinuxagent.common.protocol.restapi import Extension, ExtHandlerPackage from azurelinuxagent.ga.exthandlers import ExtHandlerInstance from tests.tools import AgentTestCase, patch class ExtHandlerInstanceTestCase(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) ext_handler = Extension(name='foo') ext_handler.version = "1.2.3" self.ext_handler_instance = ExtHandlerInstance(ext_handler=ext_handler, protocol=None) pkg_uri = "http://bar/foo__1.2.3" self.ext_handler_instance.pkg = ExtHandlerPackage(ext_handler.version) self.ext_handler_instance.pkg.uris.append(pkg_uri) self.base_dir = self.tmp_dir self.extension_directory = os.path.join(self.tmp_dir, "extension_directory") self.mock_get_base_dir = patch.object(self.ext_handler_instance, "get_base_dir", return_value=self.extension_directory) self.mock_get_base_dir.start() def tearDown(self): self.mock_get_base_dir.stop() super(ExtHandlerInstanceTestCase, self).tearDown() def test_rm_ext_handler_dir_should_remove_the_extension_packages(self): os.mkdir(self.extension_directory) open(os.path.join(self.extension_directory, "extension_file1"), 'w').close() open(os.path.join(self.extension_directory, "extension_file2"), 'w').close() open(os.path.join(self.extension_directory, "extension_file3"), 'w').close() open(os.path.join(self.base_dir, "foo__1.2.3.zip"), 'w').close() self.ext_handler_instance.remove_ext_handler() self.assertFalse(os.path.exists(self.extension_directory)) self.assertFalse(os.path.exists(os.path.join(self.base_dir, "foo__1.2.3.zip"))) def test_rm_ext_handler_dir_should_remove_the_extension_directory(self): os.mkdir(self.extension_directory) os.mknod(os.path.join(self.extension_directory, "extension_file1")) os.mknod(os.path.join(self.extension_directory, "extension_file2")) os.mknod(os.path.join(self.extension_directory, "extension_file3")) self.ext_handler_instance.remove_ext_handler() self.assertFalse(os.path.exists(self.extension_directory)) def test_rm_ext_handler_dir_should_not_report_an_event_if_the_extension_directory_does_not_exist(self): if os.path.exists(self.extension_directory): os.rmdir(self.extension_directory) with patch.object(self.ext_handler_instance, "report_event") as mock_report_event: self.ext_handler_instance.remove_ext_handler() mock_report_event.assert_not_called() def test_rm_ext_handler_dir_should_not_report_an_event_if_a_child_is_removed_asynchronously_while_deleting_the_extension_directory(self): os.mkdir(self.extension_directory) os.mknod(os.path.join(self.extension_directory, "extension_file1")) os.mknod(os.path.join(self.extension_directory, "extension_file2")) os.mknod(os.path.join(self.extension_directory, "extension_file3")) # # Some extensions uninstall asynchronously and the files we are trying to remove may be removed # while shutil.rmtree is traversing the extension's directory. Mock this by deleting a file # twice (the second call will produce "[Errno 2] No such file or directory", which should not be # reported as a telemetry event. # In order to mock this, we need to know that remove_ext_handler invokes Pyhon's shutil.rmtree, # which in turn invokes os.unlink (Python 3) or os.remove (Python 2) # remove_api_name = "unlink" if sys.version_info >= (3, 0) else "remove" original_remove_api = getattr(shutil.os, remove_api_name) extension_directory = self.extension_directory def mock_remove(path, dir_fd=None): if dir_fd is not None: # path is relative, make it absolute path = os.path.join(extension_directory, path) if path.endswith("extension_file2"): original_remove_api(path) mock_remove.file_deleted_asynchronously = True original_remove_api(path) mock_remove.file_deleted_asynchronously = False with patch.object(shutil.os, remove_api_name, mock_remove): with patch.object(self.ext_handler_instance, "report_event") as mock_report_event: self.ext_handler_instance.remove_ext_handler() mock_report_event.assert_not_called() # The next 2 asserts are checks on the mock itself, in case the implementation of remove_ext_handler changes (mocks may need to be updated then) self.assertTrue(mock_remove.file_deleted_asynchronously) # verify the mock was actually called self.assertFalse(os.path.exists(self.extension_directory)) # verify the error produced by the mock did not prevent the deletion def test_rm_ext_handler_dir_should_report_an_event_if_an_error_occurs_while_deleting_the_extension_directory(self): os.mkdir(self.extension_directory) os.mknod(os.path.join(self.extension_directory, "extension_file1")) os.mknod(os.path.join(self.extension_directory, "extension_file2")) os.mknod(os.path.join(self.extension_directory, "extension_file3")) # The mock below relies on the knowledge that remove_ext_handler invokes Pyhon's shutil.rmtree, # which in turn invokes os.unlink (Python 3) or os.remove (Python 2) remove_api_name = "unlink" if sys.version_info >= (3, 0) else "remove" original_remove_api = getattr(shutil.os, remove_api_name) def mock_remove(path, dir_fd=None): # pylint: disable=unused-argument if path.endswith("extension_file2"): raise IOError("A mocked error") original_remove_api(path) with patch.object(shutil.os, remove_api_name, mock_remove): with patch.object(self.ext_handler_instance, "report_event") as mock_report_event: self.ext_handler_instance.remove_ext_handler() args, kwargs = mock_report_event.call_args # pylint: disable=unused-variable self.assertTrue("A mocked error" in kwargs["message"]) WALinuxAgent-2.9.1.1/tests/ga/test_monitor.py000066400000000000000000000346601446033677600210520ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import contextlib import os import random import string from azurelinuxagent.common import event, logger from azurelinuxagent.common.cgroup import CpuCgroup, MemoryCgroup, MetricValue, _REPORT_EVERY_HOUR from azurelinuxagent.common.cgroupstelemetry import CGroupsTelemetry from azurelinuxagent.common.event import EVENTS_DIRECTORY from azurelinuxagent.common.protocol.healthservice import HealthService from azurelinuxagent.common.protocol.util import ProtocolUtil from azurelinuxagent.common.protocol.wire import WireProtocol from azurelinuxagent.ga.monitor import get_monitor_handler, PeriodicOperation, SendImdsHeartbeat, \ ResetPeriodicLogMessages, SendHostPluginHeartbeat, PollResourceUsage, \ ReportNetworkErrors, ReportNetworkConfigurationChanges, PollSystemWideResourceUsage from tests.protocol.mocks import mock_wire_protocol, MockHttpResponse from tests.protocol.HttpRequestPredicates import HttpRequestPredicates from tests.protocol.mockwiredata import DATA_FILE from tests.tools import Mock, MagicMock, patch, AgentTestCase, clear_singleton_instances def random_generator(size=6, chars=string.ascii_uppercase + string.digits + string.ascii_lowercase): return ''.join(random.choice(chars) for x in range(size)) @contextlib.contextmanager def _mock_wire_protocol(): # Since ProtocolUtil is a singleton per thread, we need to clear it to ensure that the test cases do not # reuse a previous state clear_singleton_instances(ProtocolUtil) with mock_wire_protocol(DATA_FILE) as protocol: protocol_util = MagicMock() protocol_util.get_protocol = Mock(return_value=protocol) with patch("azurelinuxagent.ga.monitor.get_protocol_util", return_value=protocol_util): yield protocol class MonitorHandlerTestCase(AgentTestCase): def test_it_should_invoke_all_periodic_operations(self): def periodic_operation_run(self): invoked_operations.append(self.__class__.__name__) with _mock_wire_protocol(): with patch("azurelinuxagent.ga.monitor.MonitorHandler.stopped", side_effect=[False, True, False, True]): with patch("time.sleep"): with patch.object(PeriodicOperation, "run", side_effect=periodic_operation_run, autospec=True): with patch("azurelinuxagent.common.conf.get_monitor_network_configuration_changes") as monitor_network_changes: for network_changes in [True, False]: monitor_network_changes.return_value = network_changes invoked_operations = [] monitor_handler = get_monitor_handler() monitor_handler.run() monitor_handler.join() expected_operations = [ PollResourceUsage.__name__, PollSystemWideResourceUsage.__name__, ReportNetworkErrors.__name__, ResetPeriodicLogMessages.__name__, SendHostPluginHeartbeat.__name__, SendImdsHeartbeat.__name__, ] if network_changes: expected_operations.append(ReportNetworkConfigurationChanges.__name__) invoked_operations.sort() expected_operations.sort() self.assertEqual(invoked_operations, expected_operations, "The monitor thread did not invoke the expected operations") class SendHostPluginHeartbeatOperationTestCase(AgentTestCase, HttpRequestPredicates): def test_it_should_report_host_ga_health(self): with _mock_wire_protocol() as protocol: def http_post_handler(url, _, **__): if self.is_health_service_request(url): http_post_handler.health_service_posted = True return MockHttpResponse(status=200) return None http_post_handler.health_service_posted = False protocol.set_http_handlers(http_post_handler=http_post_handler) health_service = HealthService(protocol.get_endpoint()) SendHostPluginHeartbeat(protocol, health_service).run() self.assertTrue(http_post_handler.health_service_posted, "The monitor thread did not report host ga plugin health") def test_it_should_report_a_telemetry_event_when_host_plugin_is_not_healthy(self): with _mock_wire_protocol() as protocol: # the error triggers only after ERROR_STATE_DELTA_DEFAULT with patch('azurelinuxagent.common.errorstate.ErrorState.is_triggered', return_value=True): with patch('azurelinuxagent.common.event.EventLogger.add_event') as add_event_patcher: def http_get_handler(url, *_, **__): if self.is_host_plugin_health_request(url): return MockHttpResponse(status=503) return None protocol.set_http_handlers(http_get_handler=http_get_handler) health_service = HealthService(protocol.get_endpoint()) SendHostPluginHeartbeat(protocol, health_service).run() heartbeat_events = [kwargs for _, kwargs in add_event_patcher.call_args_list if kwargs['op'] == 'HostPluginHeartbeatExtended'] self.assertTrue(len(heartbeat_events) == 1, "The monitor thread should have reported exactly 1 telemetry event for an unhealthy host ga plugin") self.assertFalse(heartbeat_events[0]['is_success'], 'The reported event should indicate failure') def test_it_should_not_send_a_health_signal_when_the_hearbeat_fails(self): with _mock_wire_protocol() as protocol: with patch('azurelinuxagent.common.event.EventLogger.add_event') as add_event_patcher: health_service_post_requests = [] def http_get_handler(url, *_, **__): if self.is_host_plugin_health_request(url): del health_service_post_requests[:] # clear the requests; after this error there should be no more POSTs raise IOError('A CLIENT ERROR') return None def http_post_handler(url, _, **__): if self.is_health_service_request(url): health_service_post_requests.append(url) return MockHttpResponse(status=200) return None protocol.set_http_handlers(http_get_handler=http_get_handler, http_post_handler=http_post_handler) health_service = HealthService(protocol.get_endpoint()) SendHostPluginHeartbeat(protocol, health_service).run() self.assertEqual(0, len(health_service_post_requests), "No health signals should have been posted: {0}".format(health_service_post_requests)) heartbeat_events = [kwargs for _, kwargs in add_event_patcher.call_args_list if kwargs['op'] == 'HostPluginHeartbeat'] self.assertTrue(len(heartbeat_events) == 1, "The monitor thread should have reported exactly 1 telemetry event for an unhealthy host ga plugin") self.assertFalse(heartbeat_events[0]['is_success'], 'The reported event should indicate failure') self.assertIn('A CLIENT ERROR', heartbeat_events[0]['message'], 'The failure does not include the expected message') class ResetPeriodicLogMessagesOperationTestCase(AgentTestCase, HttpRequestPredicates): def test_it_should_clear_periodic_log_messages(self): logger.reset_periodic() # Adding 100 different messages expected = 100 for i in range(expected): logger.periodic_info(logger.EVERY_DAY, "Test {0}".format(i)) actual = len(logger.DEFAULT_LOGGER.periodic_messages) if actual != expected: raise Exception('Test setup error: the periodic messages were not added. Got: {0} Expected: {1}'.format(actual, expected)) ResetPeriodicLogMessages().run() self.assertEqual(0, len(logger.DEFAULT_LOGGER.periodic_messages), "The monitor thread did not reset the periodic log messages") @patch('azurelinuxagent.common.osutil.get_osutil') @patch('azurelinuxagent.common.protocol.util.get_protocol_util') @patch("azurelinuxagent.common.protocol.healthservice.HealthService._report") @patch("azurelinuxagent.common.utils.restutil.http_get") class TestExtensionMetricsDataTelemetry(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) event.init_event_logger(os.path.join(self.tmp_dir, EVENTS_DIRECTORY)) CGroupsTelemetry.reset() clear_singleton_instances(ProtocolUtil) protocol = WireProtocol('endpoint') protocol.client.update_goal_state = MagicMock() self.get_protocol = patch('azurelinuxagent.common.protocol.util.ProtocolUtil.get_protocol', return_value=protocol) self.get_protocol.start() def tearDown(self): AgentTestCase.tearDown(self) CGroupsTelemetry.reset() self.get_protocol.stop() @patch('azurelinuxagent.common.event.EventLogger.add_metric') @patch("azurelinuxagent.common.cgroupstelemetry.CGroupsTelemetry.poll_all_tracked") def test_send_extension_metrics_telemetry(self, patch_poll_all_tracked, # pylint: disable=unused-argument patch_add_metric, *args): patch_poll_all_tracked.return_value = [MetricValue("Process", "% Processor Time", "service", 1), MetricValue("Memory", "Total Memory Usage", "service", 1), MetricValue("Memory", "Max Memory Usage", "service", 1, _REPORT_EVERY_HOUR), MetricValue("Memory", "Swap Memory Usage", "service", 1, _REPORT_EVERY_HOUR) ] PollResourceUsage().run() self.assertEqual(1, patch_poll_all_tracked.call_count) self.assertEqual(4, patch_add_metric.call_count) # Four metrics being sent. @patch('azurelinuxagent.common.event.EventLogger.add_metric') @patch("azurelinuxagent.common.cgroupstelemetry.CGroupsTelemetry.poll_all_tracked") def test_send_extension_metrics_telemetry_for_empty_cgroup(self, patch_poll_all_tracked, # pylint: disable=unused-argument patch_add_metric, *args): patch_poll_all_tracked.return_value = [] PollResourceUsage().run() self.assertEqual(1, patch_poll_all_tracked.call_count) self.assertEqual(0, patch_add_metric.call_count) @patch('azurelinuxagent.common.event.EventLogger.add_metric') @patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_memory_usage") @patch('azurelinuxagent.common.logger.Logger.periodic_warn') def test_send_extension_metrics_telemetry_handling_memory_cgroup_exceptions_errno2(self, patch_periodic_warn, # pylint: disable=unused-argument patch_get_memory_usage, patch_add_metric, *args): ioerror = IOError() ioerror.errno = 2 patch_get_memory_usage.side_effect = ioerror CGroupsTelemetry._tracked["/test/path"] = MemoryCgroup("cgroup_name", "/test/path") PollResourceUsage().run() self.assertEqual(0, patch_periodic_warn.call_count) self.assertEqual(0, patch_add_metric.call_count) # No metrics should be sent. @patch('azurelinuxagent.common.event.EventLogger.add_metric') @patch("azurelinuxagent.common.cgroup.CpuCgroup.get_cpu_usage") @patch('azurelinuxagent.common.logger.Logger.periodic_warn') def test_send_extension_metrics_telemetry_handling_cpu_cgroup_exceptions_errno2(self, patch_periodic_warn, # pylint: disable=unused-argument patch_cpu_usage, patch_add_metric, *args): ioerror = IOError() ioerror.errno = 2 patch_cpu_usage.side_effect = ioerror CGroupsTelemetry._tracked["/test/path"] = CpuCgroup("cgroup_name", "/test/path") PollResourceUsage().run() self.assertEqual(0, patch_periodic_warn.call_count) self.assertEqual(0, patch_add_metric.call_count) # No metrics should be sent. class TestPollSystemWideResourceUsage(AgentTestCase): @patch('azurelinuxagent.common.event.EventLogger.add_metric') @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.get_used_and_available_system_memory") def test_send_system_memory_metrics(self, path_get_system_memory, patch_add_metric, *args): # pylint: disable=unused-argument path_get_system_memory.return_value = (234.45, 123.45) PollSystemWideResourceUsage().run() self.assertEqual(1, path_get_system_memory.call_count) self.assertEqual(2, patch_add_metric.call_count) # 2 metrics being sent. @patch('azurelinuxagent.common.event.EventLogger.add_metric') @patch("azurelinuxagent.ga.monitor.PollSystemWideResourceUsage.poll_system_memory_metrics") def test_send_system_memory_metrics_empty(self, path_poll_system_memory_metrics, patch_add_metric, # pylint: disable=unused-argument *args): path_poll_system_memory_metrics.return_value = [] PollSystemWideResourceUsage().run() self.assertEqual(1, path_poll_system_memory_metrics.call_count) self.assertEqual(0, patch_add_metric.call_count) # Zero metrics being sent.WALinuxAgent-2.9.1.1/tests/ga/test_multi_config_extension.py000066400000000000000000002400671446033677600241360ustar00rootroot00000000000000import contextlib import json import os.path import re import subprocess import uuid from azurelinuxagent.common import conf from azurelinuxagent.common.event import WALAEventOperation from azurelinuxagent.common.exception import GoalStateAggregateStatusCodes from azurelinuxagent.common.future import ustr from azurelinuxagent.common.protocol.restapi import ExtensionRequestedState, ExtensionState from azurelinuxagent.common.utils import fileutil from azurelinuxagent.ga.exthandlers import get_exthandlers_handler, ExtensionStatusValue, ExtCommandEnvVariable, \ GoalStateStatus, ExtHandlerInstance from tests.ga.extension_emulator import enable_invocations, extension_emulator, ExtensionCommandNames, Actions, \ extract_extension_info_from_command from tests.protocol.mocks import mock_wire_protocol, MockHttpResponse from tests.protocol.HttpRequestPredicates import HttpRequestPredicates from tests.protocol.mockwiredata import DATA_FILE, WireProtocolData from tests.tools import AgentTestCase, mock_sleep, patch class TestMultiConfigExtensionsConfigParsing(AgentTestCase): _MULTI_CONFIG_TEST_DATA = os.path.join("wire", "multi-config") def setUp(self): AgentTestCase.setUp(self) self.mock_sleep = patch("time.sleep", lambda *_: mock_sleep(0.0001)) self.mock_sleep.start() self.test_data = DATA_FILE.copy() def tearDown(self): self.mock_sleep.stop() AgentTestCase.tearDown(self) class _TestExtHandlerObject: def __init__(self, name, version, state="enabled"): self.name = name self.version = version self.state = state self.is_invalid_setting = False self.settings = dict() class _TestExtensionObject: def __init__(self, name, seq_no, dependency_level="0", state="enabled"): self.name = name self.seq_no = seq_no self.dependency_level = int(dependency_level) self.state = state def _mock_and_assert_ext_handlers(self, expected_handlers): with mock_wire_protocol(self.test_data) as protocol: ext_handlers = protocol.get_goal_state().extensions_goal_state.extensions for ext_handler in ext_handlers: if ext_handler.name not in expected_handlers: continue expected_handler = expected_handlers.pop(ext_handler.name) self.assertEqual(expected_handler.state, ext_handler.state) self.assertEqual(expected_handler.version, ext_handler.version) self.assertEqual(expected_handler.is_invalid_setting, ext_handler.is_invalid_setting) self.assertEqual(len(expected_handler.settings), len(ext_handler.settings)) for extension in ext_handler.settings: self.assertIn(extension.name, expected_handler.settings) expected_extension = expected_handler.settings.pop(extension.name) self.assertEqual(expected_extension.seq_no, extension.sequenceNumber) self.assertEqual(expected_extension.state, extension.state) self.assertEqual(expected_extension.dependency_level, extension.dependencyLevel) self.assertEqual(0, len(expected_handler.settings), "All extensions not verified for handler") self.assertEqual(0, len(expected_handlers), "All handlers not verified") def _get_mock_expected_handler_data(self, rc_extensions, vmaccess_extensions, geneva_extensions): # Set expected handler data run_command_test_handler = self._TestExtHandlerObject("Microsoft.CPlat.Core.RunCommandHandlerWindows", "2.3.0") run_command_test_handler.settings.update(rc_extensions) vm_access_test_handler = self._TestExtHandlerObject("Microsoft.Compute.VMAccessAgent", "2.4.7") vm_access_test_handler.settings.update(vmaccess_extensions) geneva_test_handler = self._TestExtHandlerObject("Microsoft.Azure.Geneva.GenevaMonitoring", "2.20.0.1") geneva_test_handler.settings.update(geneva_extensions) expected_handlers = { run_command_test_handler.name: run_command_test_handler, vm_access_test_handler.name: vm_access_test_handler, geneva_test_handler.name: geneva_test_handler } return expected_handlers def test_it_should_parse_multi_config_settings_properly(self): self.test_data['ext_conf'] = os.path.join(self._MULTI_CONFIG_TEST_DATA, "ext_conf_with_multi_config.xml") rc_extensions = dict() rc_extensions["firstRunCommand"] = self._TestExtensionObject(name="firstRunCommand", seq_no=2) rc_extensions["secondRunCommand"] = self._TestExtensionObject(name="secondRunCommand", seq_no=2, dependency_level="3") rc_extensions["thirdRunCommand"] = self._TestExtensionObject(name="thirdRunCommand", seq_no=1, dependency_level="4") vmaccess_extensions = { "Microsoft.Compute.VMAccessAgent": self._TestExtensionObject(name="Microsoft.Compute.VMAccessAgent", seq_no=1, dependency_level=2)} geneva_extensions = {"Microsoft.Azure.Geneva.GenevaMonitoring": self._TestExtensionObject( name="Microsoft.Azure.Geneva.GenevaMonitoring", seq_no=1)} expected_handlers = self._get_mock_expected_handler_data(rc_extensions, vmaccess_extensions, geneva_extensions) self._mock_and_assert_ext_handlers(expected_handlers) def test_it_should_parse_multi_config_with_disable_state_properly(self): self.test_data['ext_conf'] = os.path.join(self._MULTI_CONFIG_TEST_DATA, "ext_conf_with_disabled_multi_config.xml") rc_extensions = dict() rc_extensions["firstRunCommand"] = self._TestExtensionObject(name="firstRunCommand", seq_no=3) rc_extensions["secondRunCommand"] = self._TestExtensionObject(name="secondRunCommand", seq_no=3, dependency_level="1") rc_extensions["thirdRunCommand"] = self._TestExtensionObject(name="thirdRunCommand", seq_no=1, dependency_level="4", state="disabled") vmaccess_extensions = { "Microsoft.Compute.VMAccessAgent": self._TestExtensionObject(name="Microsoft.Compute.VMAccessAgent", seq_no=2, dependency_level="2")} geneva_extensions = {"Microsoft.Azure.Geneva.GenevaMonitoring": self._TestExtensionObject( name="Microsoft.Azure.Geneva.GenevaMonitoring", seq_no=2)} expected_handlers = self._get_mock_expected_handler_data(rc_extensions, vmaccess_extensions, geneva_extensions) self._mock_and_assert_ext_handlers(expected_handlers) class _MultiConfigBaseTestClass(AgentTestCase): _MULTI_CONFIG_TEST_DATA = os.path.join("wire", "multi-config") def setUp(self): AgentTestCase.setUp(self) self.mock_sleep = patch("time.sleep", lambda *_: mock_sleep(0.01)) self.mock_sleep.start() self.test_data = DATA_FILE.copy() def tearDown(self): self.mock_sleep.stop() AgentTestCase.tearDown(self) @contextlib.contextmanager def _setup_test_env(self, mock_manifest=False): with mock_wire_protocol(self.test_data) as protocol: def mock_http_put(url, *args, **_): if HttpRequestPredicates.is_host_plugin_status_request(url): # Skip reading the HostGA request data as its encoded return MockHttpResponse(status=500) protocol.aggregate_status = json.loads(args[0]) return MockHttpResponse(status=201) with patch("azurelinuxagent.common.agent_supported_feature._MultiConfigFeature.is_supported", True): protocol.aggregate_status = None protocol.set_http_handlers(http_put_handler=mock_http_put) exthandlers_handler = get_exthandlers_handler(protocol) no_of_extensions = protocol.mock_wire_data.get_no_of_extensions_in_config() if mock_manifest: with patch('azurelinuxagent.ga.exthandlers.HandlerManifest.supports_multiple_extensions', return_value=True): yield exthandlers_handler, protocol, no_of_extensions else: yield exthandlers_handler, protocol, no_of_extensions def _assert_and_get_handler_status(self, aggregate_status, handler_name="OSTCExtensions.ExampleHandlerLinux", handler_version="1.0.0", status="Ready", expected_count=1, message=None): self.assertIsNotNone(aggregate_status['aggregateStatus'], "No aggregate status found") handlers = [handler for handler in aggregate_status['aggregateStatus']['handlerAggregateStatus'] if handler_name == handler['handlerName'] and handler_version == handler['handlerVersion']] self.assertEqual(expected_count, len(handlers), "Unexpected extension count") self.assertTrue(all(handler['status'] == status for handler in handlers), "Unexpected Status reported for handler {0}".format(handler_name)) if message is not None: self.assertTrue(all(message in handler['formattedMessage']['message'] for handler in handlers), "Status Message mismatch") return handlers def _assert_extension_status(self, handler_statuses, expected_ext_status, multi_config=False): for ext_name, settings_status in expected_ext_status.items(): ext_status = next(handler for handler in handler_statuses if handler['runtimeSettingsStatus']['settingsStatus']['status']['name'] == ext_name) ext_runtime_status = ext_status['runtimeSettingsStatus'] self.assertIsNotNone(ext_runtime_status, "Extension not found") self.assertEqual(settings_status['seq_no'], ext_runtime_status['sequenceNumber'], "Sequence no mismatch") self.assertEqual(settings_status['status'], ext_runtime_status['settingsStatus']['status']['status'], "status mismatch") if 'message' in settings_status and settings_status['message'] is not None: self.assertIn(settings_status['message'], ext_runtime_status['settingsStatus']['status']['formattedMessage']['message'], "message mismatch") if multi_config: self.assertEqual(ext_name, ext_runtime_status['extensionName'], "ext name mismatch") else: self.assertNotIn('extensionName', ext_runtime_status, "Extension name should not be reported for SC") handler_statuses.remove(ext_status) self.assertEqual(0, len(handler_statuses), "Unexpected extensions left for handler") class TestMultiConfigExtensions(_MultiConfigBaseTestClass): def __assert_extension_not_present(self, handlers, extensions): for ext_name in extensions: self.assertFalse(all( 'runtimeSettingsStatus' in handler and 'extensionName' in handler['runtimeSettingsStatus'] and handler['runtimeSettingsStatus']['extensionName'] == ext_name for handler in handlers), "Extension status found") def __run_and_assert_generic_case(self, exthandlers_handler, protocol, no_of_extensions, with_message=True): def get_message(msg): return msg if with_message else None exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(no_of_extensions, len(protocol.aggregate_status['aggregateStatus']['handlerAggregateStatus']), "incorrect extensions reported") mc_handlers = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="OSTCExtensions.ExampleHandlerLinux", expected_count=3) expected_extensions = { "firstExtension": {"status": ExtensionStatusValue.success, "seq_no": 1, "message": get_message("Enabling firstExtension")}, "secondExtension": {"status": ExtensionStatusValue.success, "seq_no": 2, "message": get_message("Enabling secondExtension")}, "thirdExtension": {"status": ExtensionStatusValue.success, "seq_no": 3, "message": get_message("Enabling thirdExtension")}, } self._assert_extension_status(mc_handlers[:], expected_extensions, multi_config=True) sc_handler = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="Microsoft.Powershell.ExampleExtension") expected_extensions = { "Microsoft.Powershell.ExampleExtension": {"status": ExtensionStatusValue.success, "seq_no": 9, "message": get_message("Enabling SingleConfig extension")} } self._assert_extension_status(sc_handler[:], expected_extensions) return mc_handlers, sc_handler def __setup_and_assert_disable_scenario(self, exthandlers_handler, protocol): self.test_data['ext_conf'] = os.path.join(self._MULTI_CONFIG_TEST_DATA, 'ext_conf_mc_disabled_extensions.xml') protocol.mock_wire_data = WireProtocolData(self.test_data) protocol.mock_wire_data.set_incarnation(2) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() mc_handlers = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="OSTCExtensions.ExampleHandlerLinux", status="Ready", expected_count=2) expected_extensions = { "thirdExtension": {"status": ExtensionStatusValue.success, "seq_no": 99, "message": None}, "fourthExtension": {"status": ExtensionStatusValue.success, "seq_no": 101, "message": None}, } self.__assert_extension_not_present(mc_handlers[:], ["firstExtension", "secondExtension"]) self._assert_extension_status(mc_handlers[:], expected_extensions, multi_config=True) sc_handler = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="Microsoft.Powershell.ExampleExtension", status="Ready") expected_extensions = { "Microsoft.Powershell.ExampleExtension": {"status": ExtensionStatusValue.success, "seq_no": 10, "message": None} } self._assert_extension_status(sc_handler[:], expected_extensions) return mc_handlers, sc_handler @contextlib.contextmanager def __setup_generic_test_env(self): self.test_data['ext_conf'] = os.path.join(self._MULTI_CONFIG_TEST_DATA, "ext_conf_multi_config_no_dependencies.xml") first_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.firstExtension") second_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.secondExtension") third_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.thirdExtension") fourth_ext = extension_emulator(name="Microsoft.Powershell.ExampleExtension") with self._setup_test_env(mock_manifest=True) as (exthandlers_handler, protocol, no_of_extensions): with enable_invocations(first_ext, second_ext, third_ext, fourth_ext) as invocation_record: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(no_of_extensions, len(protocol.aggregate_status['aggregateStatus']['handlerAggregateStatus']), "incorrect extensions reported") invocation_record.compare( (first_ext, ExtensionCommandNames.INSTALL), (first_ext, ExtensionCommandNames.ENABLE), (second_ext, ExtensionCommandNames.ENABLE), (third_ext, ExtensionCommandNames.ENABLE), (fourth_ext, ExtensionCommandNames.INSTALL), (fourth_ext, ExtensionCommandNames.ENABLE) ) self.__run_and_assert_generic_case(exthandlers_handler, protocol, no_of_extensions, with_message=False) yield exthandlers_handler, protocol, [first_ext, second_ext, third_ext, fourth_ext] def test_it_should_execute_and_report_multi_config_extensions_properly(self): self.test_data['ext_conf'] = os.path.join(self._MULTI_CONFIG_TEST_DATA, "ext_conf_multi_config_no_dependencies.xml") with self._setup_test_env(mock_manifest=True) as (exthandlers_handler, protocol, no_of_extensions): # Case 1: Install and enable Single and MultiConfig extensions self.__run_and_assert_generic_case(exthandlers_handler, protocol, no_of_extensions) # Case 2: Disable 2 multi-config extensions and add another for enable self.__setup_and_assert_disable_scenario(exthandlers_handler, protocol) # Case 3: Uninstall Multi-config handler (with enabled extensions) and single config extension protocol.mock_wire_data.set_incarnation(3) protocol.mock_wire_data.set_extensions_config_state(ExtensionRequestedState.Uninstall) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(0, len(protocol.aggregate_status['aggregateStatus']['handlerAggregateStatus']), "No handler/extension status should be reported") def test_it_should_report_unregistered_version_error_per_extension(self): self.test_data['ext_conf'] = os.path.join(self._MULTI_CONFIG_TEST_DATA, "ext_conf_multi_config_no_dependencies.xml") with self._setup_test_env() as (exthandlers_handler, protocol, no_of_extensions): # Set a random failing extension failing_version = "19.12.1221" protocol.mock_wire_data.set_extensions_config_version(failing_version) protocol.mock_wire_data.set_incarnation(2) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(no_of_extensions, len(protocol.aggregate_status['aggregateStatus']['handlerAggregateStatus']), "incorrect extensions reported") error_msg_format = '[ExtensionError] Unable to find version {0} in manifest for extension {1}' mc_handlers = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="OSTCExtensions.ExampleHandlerLinux", handler_version=failing_version, status="NotReady", expected_count=3, message=error_msg_format.format(failing_version, "OSTCExtensions.ExampleHandlerLinux")) self.assertTrue(all( handler['runtimeSettingsStatus']['settingsStatus']['status']['operation'] == WALAEventOperation.Download and handler['runtimeSettingsStatus']['settingsStatus']['status']['status'] == ExtensionStatusValue.error for handler in mc_handlers), "Incorrect data reported") sc_handlers = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="Microsoft.Powershell.ExampleExtension", handler_version=failing_version, status="NotReady", message=error_msg_format.format(failing_version, "Microsoft.Powershell.ExampleExtension")) self.assertFalse(all("runtimeSettingsStatus" in handler for handler in sc_handlers), "Incorrect status") def test_it_should_not_install_handler_again_if_installed(self): with self.__setup_generic_test_env() as (_, _, _): # Everything is already asserted in the context manager pass def test_it_should_retry_handler_installation_per_extension_if_failed(self): self.test_data['ext_conf'] = os.path.join(self._MULTI_CONFIG_TEST_DATA, "ext_conf_multi_config_no_dependencies.xml") with self._setup_test_env() as (exthandlers_handler, protocol, no_of_extensions): fail_code, fail_action = Actions.generate_unique_fail() first_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.firstExtension", install_action=fail_action, supports_multiple_extensions=True) second_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.secondExtension", supports_multiple_extensions=True) third_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.thirdExtension", supports_multiple_extensions=True) sc_ext = extension_emulator(name="Microsoft.Powershell.ExampleExtension", install_action=fail_action) with enable_invocations(first_ext, second_ext, third_ext, sc_ext) as invocation_record: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(no_of_extensions, len(protocol.aggregate_status['aggregateStatus']['handlerAggregateStatus']), "incorrect extensions reported") invocation_record.compare( (first_ext, ExtensionCommandNames.INSTALL), # Should try installation again if first time failed (second_ext, ExtensionCommandNames.INSTALL), (second_ext, ExtensionCommandNames.ENABLE), (third_ext, ExtensionCommandNames.ENABLE), (sc_ext, ExtensionCommandNames.INSTALL) ) mc_handlers = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="OSTCExtensions.ExampleHandlerLinux", expected_count=3, status="Ready") expected_extensions = { "firstExtension": {"status": ExtensionStatusValue.error, "seq_no": 1, "message": fail_code}, "secondExtension": {"status": ExtensionStatusValue.success, "seq_no": 2, "message": None}, "thirdExtension": {"status": ExtensionStatusValue.success, "seq_no": 3, "message": None}, } self._assert_extension_status(mc_handlers, expected_extensions, multi_config=True) sc_handlers = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="Microsoft.Powershell.ExampleExtension", status="NotReady", message=fail_code) self.assertFalse(all("runtimeSettingsStatus" in handler for handler in sc_handlers), "Incorrect status") def test_it_should_only_disable_enabled_extensions_on_update(self): with self.__setup_generic_test_env() as (exthandlers_handler, protocol, old_exts): # Update extensions self.test_data['ext_conf'] = os.path.join(self._MULTI_CONFIG_TEST_DATA, 'ext_conf_mc_update_extensions.xml') protocol.mock_wire_data = WireProtocolData(self.test_data) protocol.mock_wire_data.set_incarnation(2) protocol.client.update_goal_state() new_version = "1.1.0" new_first_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.firstExtension", version=new_version, supports_multiple_extensions=True) new_second_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.secondExtension", version=new_version, supports_multiple_extensions=True) new_third_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.thirdExtension", version=new_version, supports_multiple_extensions=True) new_sc_ext = extension_emulator(name="Microsoft.Powershell.ExampleExtension", version=new_version) with enable_invocations(new_first_ext, new_second_ext, new_third_ext, new_sc_ext, *old_exts) as invocation_record: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() old_first, old_second, old_third, old_fourth = old_exts invocation_record.compare( # Disable all enabled commands for MC before updating the Handler (old_first, ExtensionCommandNames.DISABLE), (old_second, ExtensionCommandNames.DISABLE), (old_third, ExtensionCommandNames.DISABLE), (new_first_ext, ExtensionCommandNames.UPDATE), (old_first, ExtensionCommandNames.UNINSTALL), (new_first_ext, ExtensionCommandNames.INSTALL), # No enable for First and Second extension as their state is Disabled in GoalState, # only enabled the ThirdExtension (new_third_ext, ExtensionCommandNames.ENABLE), # Follow the normal update pattern for Single config handlers (old_fourth, ExtensionCommandNames.DISABLE), (new_sc_ext, ExtensionCommandNames.UPDATE), (old_fourth, ExtensionCommandNames.UNINSTALL), (new_sc_ext, ExtensionCommandNames.INSTALL), (new_sc_ext, ExtensionCommandNames.ENABLE) ) mc_handlers = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="OSTCExtensions.ExampleHandlerLinux", expected_count=1, handler_version=new_version) expected_extensions = { "thirdExtension": {"status": ExtensionStatusValue.success, "seq_no": 99, "message": None} } self._assert_extension_status(mc_handlers, expected_extensions, multi_config=True) self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_version=new_version, handler_name="Microsoft.Powershell.ExampleExtension") def test_it_should_retry_update_sequence_per_extension_if_previous_failed(self): with self.__setup_generic_test_env() as (exthandlers_handler, protocol, old_exts): # Update extensions self.test_data['ext_conf'] = os.path.join(self._MULTI_CONFIG_TEST_DATA, 'ext_conf_mc_update_extensions.xml') protocol.mock_wire_data = WireProtocolData(self.test_data) protocol.mock_wire_data.set_incarnation(2) protocol.client.update_goal_state() new_version = "1.1.0" _, fail_action = Actions.generate_unique_fail() # Fail Uninstall of the secondExtension old_exts[1] = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.secondExtension", uninstall_action=fail_action, supports_multiple_extensions=True) # Fail update of the first extension new_first_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.firstExtension", version=new_version, update_action=fail_action, supports_multiple_extensions=True) new_second_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.secondExtension", version=new_version, supports_multiple_extensions=True) new_third_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.thirdExtension", version=new_version, supports_multiple_extensions=True) new_sc_ext = extension_emulator(name="Microsoft.Powershell.ExampleExtension", version=new_version) with enable_invocations(new_first_ext, new_second_ext, new_third_ext, new_sc_ext, *old_exts) as invocation_record: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() old_first, old_second, old_third, old_fourth = old_exts invocation_record.compare( # Disable all enabled commands for MC before updating the Handler (old_first, ExtensionCommandNames.DISABLE), (old_second, ExtensionCommandNames.DISABLE), (old_third, ExtensionCommandNames.DISABLE), (new_first_ext, ExtensionCommandNames.UPDATE), # Since the extensions have been disabled before, we won't disable them again for Update scenario (new_second_ext, ExtensionCommandNames.UPDATE), # This will fail too as per the mock above (old_second, ExtensionCommandNames.UNINSTALL), (new_third_ext, ExtensionCommandNames.UPDATE), (old_third, ExtensionCommandNames.UNINSTALL), (new_third_ext, ExtensionCommandNames.INSTALL), # No enable for First and Second extension as their state is Disabled in GoalState, # only enabled the ThirdExtension (new_third_ext, ExtensionCommandNames.ENABLE), # Follow the normal update pattern for Single config handlers (old_fourth, ExtensionCommandNames.DISABLE), (new_sc_ext, ExtensionCommandNames.UPDATE), (old_fourth, ExtensionCommandNames.UNINSTALL), (new_sc_ext, ExtensionCommandNames.INSTALL), (new_sc_ext, ExtensionCommandNames.ENABLE) ) # Since firstExtension and secondExtension are Disabled, we won't report their status mc_handlers = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="OSTCExtensions.ExampleHandlerLinux", expected_count=1, handler_version=new_version) expected_extensions = { "thirdExtension": {"status": ExtensionStatusValue.success, "seq_no": 99, "message": None} } self._assert_extension_status(mc_handlers, expected_extensions, multi_config=True) sc_handler = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_version=new_version, handler_name="Microsoft.Powershell.ExampleExtension") expected_extensions = { "Microsoft.Powershell.ExampleExtension": {"status": ExtensionStatusValue.success, "seq_no": 10, "message": None} } self._assert_extension_status(sc_handler, expected_extensions) def test_it_should_report_disabled_extension_errors_if_update_failed(self): with self.__setup_generic_test_env() as (exthandlers_handler, protocol, old_exts): # Update extensions self.test_data['ext_conf'] = os.path.join(self._MULTI_CONFIG_TEST_DATA, 'ext_conf_mc_update_extensions.xml') protocol.mock_wire_data = WireProtocolData(self.test_data) protocol.mock_wire_data.set_incarnation(2) protocol.client.update_goal_state() new_version = "1.1.0" fail_code, fail_action = Actions.generate_unique_fail() # Fail Disable of the firstExtension old_exts[0] = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.firstExtension", disable_action=fail_action, supports_multiple_extensions=True) new_first_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.firstExtension", version=new_version, supports_multiple_extensions=True) new_second_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.secondExtension", version=new_version, supports_multiple_extensions=True) new_third_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.thirdExtension", version=new_version, supports_multiple_extensions=True) new_fourth_ext = extension_emulator(name="Microsoft.Powershell.ExampleExtension", version=new_version) with enable_invocations(new_first_ext, new_second_ext, new_third_ext, new_fourth_ext, *old_exts) as invocation_record: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() old_first, _, _, old_fourth = old_exts invocation_record.compare( # Disable for firstExtension should fail 3 times, i.e., once per extension which tries to update the Handler (old_first, ExtensionCommandNames.DISABLE), (old_first, ExtensionCommandNames.DISABLE), (old_first, ExtensionCommandNames.DISABLE), # Since Disable fails for the firstExtension and continueOnUpdate = False, Update should not go through # Follow the normal update pattern for Single config handlers (old_fourth, ExtensionCommandNames.DISABLE), (new_fourth_ext, ExtensionCommandNames.UPDATE), (old_fourth, ExtensionCommandNames.UNINSTALL), (new_fourth_ext, ExtensionCommandNames.INSTALL), (new_fourth_ext, ExtensionCommandNames.ENABLE) ) # Since firstExtension and secondExtension are Disabled, we won't report their status mc_handlers = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="OSTCExtensions.ExampleHandlerLinux", expected_count=1, handler_version=new_version, status="NotReady", message=fail_code) expected_extensions = { "thirdExtension": {"status": ExtensionStatusValue.error, "seq_no": 99, "message": fail_code} } self._assert_extension_status(mc_handlers, expected_extensions, multi_config=True) sc_handler = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_version=new_version, handler_name="Microsoft.Powershell.ExampleExtension") expected_extensions = { "Microsoft.Powershell.ExampleExtension": {"status": ExtensionStatusValue.success, "seq_no": 10, "message": None} } self._assert_extension_status(sc_handler, expected_extensions) def test_it_should_report_extension_status_properly(self): self.test_data['ext_conf'] = os.path.join(self._MULTI_CONFIG_TEST_DATA, "ext_conf_multi_config_no_dependencies.xml") with self._setup_test_env(mock_manifest=True) as (exthandlers_handler, protocol, no_of_extensions): self.__run_and_assert_generic_case(exthandlers_handler, protocol, no_of_extensions) def test_it_should_handle_and_report_enable_errors_properly(self): self.test_data['ext_conf'] = os.path.join(self._MULTI_CONFIG_TEST_DATA, "ext_conf_multi_config_no_dependencies.xml") with self._setup_test_env() as (exthandlers_handler, protocol, no_of_extensions): fail_code, fail_action = Actions.generate_unique_fail() first_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.firstExtension", supports_multiple_extensions=True) second_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.secondExtension", supports_multiple_extensions=True) third_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.thirdExtension", enable_action=fail_action, supports_multiple_extensions=True) fourth_ext = extension_emulator(name="Microsoft.Powershell.ExampleExtension", enable_action=fail_action) with enable_invocations(first_ext, second_ext, third_ext, fourth_ext) as invocation_record: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(no_of_extensions, len(protocol.aggregate_status['aggregateStatus']['handlerAggregateStatus']), "incorrect extensions reported") invocation_record.compare( (first_ext, ExtensionCommandNames.INSTALL), (first_ext, ExtensionCommandNames.ENABLE), (second_ext, ExtensionCommandNames.ENABLE), (third_ext, ExtensionCommandNames.ENABLE), (fourth_ext, ExtensionCommandNames.INSTALL), (fourth_ext, ExtensionCommandNames.ENABLE) ) mc_handlers = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="OSTCExtensions.ExampleHandlerLinux", expected_count=3, status="Ready") expected_extensions = { "firstExtension": {"status": ExtensionStatusValue.success, "seq_no": 1, "message": None}, "secondExtension": {"status": ExtensionStatusValue.success, "seq_no": 2, "message": None}, "thirdExtension": {"status": ExtensionStatusValue.error, "seq_no": 3, "message": fail_code}, } self._assert_extension_status(mc_handlers, expected_extensions, multi_config=True) sc_handler = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="Microsoft.Powershell.ExampleExtension", status="NotReady", message=fail_code) expected_extensions = { "Microsoft.Powershell.ExampleExtension": {"status": ExtensionStatusValue.error, "seq_no": 9, "message": fail_code} } self._assert_extension_status(sc_handler, expected_extensions) def test_it_should_cleanup_extension_state_on_disable(self): def __assert_state_file(handler_name, handler_version, extensions, state, not_present=None): config_path = os.path.join(self.tmp_dir, "{0}-{1}".format(handler_name, handler_version), "config") config_files = os.listdir(config_path) for ext_name in extensions: self.assertIn("{0}.settings".format(ext_name), config_files, "settings not found") self.assertEqual( fileutil.read_file(os.path.join(config_path, "{0}.HandlerState".format(ext_name.split(".")[0]))), state, "Invalid state") if not_present is not None: for ext_name in not_present: self.assertNotIn("{0}.HandlerState".format(ext_name), config_files, "Wrongful state found") with self.__setup_generic_test_env() as (ext_handler, protocol, _): __assert_state_file("OSTCExtensions.ExampleHandlerLinux", "1.0.0", ["firstExtension.1", "secondExtension.2", "thirdExtension.3"], ExtensionState.Enabled) self.test_data['ext_conf'] = os.path.join(self._MULTI_CONFIG_TEST_DATA, 'ext_conf_mc_disabled_extensions.xml') protocol.mock_wire_data = WireProtocolData(self.test_data) protocol.mock_wire_data.set_incarnation(2) protocol.client.update_goal_state() ext_handler.run() ext_handler.report_ext_handlers_status() mc_handlers = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="OSTCExtensions.ExampleHandlerLinux", expected_count=2, status="Ready") expected_extensions = { "thirdExtension": {"status": ExtensionStatusValue.success, "seq_no": 99, "message": "Enabling thirdExtension"}, "fourthExtension": {"status": ExtensionStatusValue.success, "seq_no": 101, "message": "Enabling fourthExtension"}, } self._assert_extension_status(mc_handlers, expected_extensions, multi_config=True) __assert_state_file("OSTCExtensions.ExampleHandlerLinux", "1.0.0", ["thirdExtension.99", "fourthExtension.101"], ExtensionState.Enabled, not_present=["firstExtension", "secondExtension"]) sc_handler = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="Microsoft.Powershell.ExampleExtension") expected_extensions = { "Microsoft.Powershell.ExampleExtension": {"status": ExtensionStatusValue.success, "seq_no": 10, "message": "Enabling SingleConfig Extension"} } self._assert_extension_status(sc_handler, expected_extensions) def test_it_should_create_command_execution_log_per_extension(self): with self.__setup_generic_test_env() as (_, _, _): sc_handler_path = os.path.join(conf.get_ext_log_dir(), "Microsoft.Powershell.ExampleExtension") mc_handler_path = os.path.join(conf.get_ext_log_dir(), "OSTCExtensions.ExampleHandlerLinux") self.assertIn("CommandExecution_firstExtension.log", os.listdir(mc_handler_path), "Command Execution file not found") self.assertGreater(os.path.getsize(os.path.join(mc_handler_path, "CommandExecution_firstExtension.log")), 0, "Log file not being used") self.assertIn("CommandExecution_secondExtension.log", os.listdir(mc_handler_path), "Command Execution file not found") self.assertGreater(os.path.getsize(os.path.join(mc_handler_path, "CommandExecution_secondExtension.log")), 0, "Log file not being used") self.assertIn("CommandExecution_thirdExtension.log", os.listdir(mc_handler_path), "Command Execution file not found") self.assertGreater(os.path.getsize(os.path.join(mc_handler_path, "CommandExecution_thirdExtension.log")), 0, "Log file not being used") self.assertIn("CommandExecution.log", os.listdir(sc_handler_path), "Command Execution file not found") self.assertGreater(os.path.getsize(os.path.join(sc_handler_path, "CommandExecution.log")), 0, "Log file not being used") def test_it_should_set_relevant_environment_variables_for_mc(self): original_popen = subprocess.Popen handler_envs = {} def __assert_env_variables(handler_name, handler_version="1.0.0", seq_no="1", ext_name=None, expected_vars=None, not_expected=None): original_env_vars = { ExtCommandEnvVariable.ExtensionPath: os.path.join(self.tmp_dir, "{0}-{1}".format(handler_name, handler_version)), ExtCommandEnvVariable.ExtensionVersion: handler_version, ExtCommandEnvVariable.ExtensionSeqNumber: ustr(seq_no), ExtCommandEnvVariable.WireProtocolAddress: '168.63.129.16', ExtCommandEnvVariable.ExtensionSupportedFeatures: json.dumps([{"Key": "ExtensionTelemetryPipeline", "Value": "1.0"}]) } full_name = handler_name if ext_name is not None: original_env_vars[ExtCommandEnvVariable.ExtensionName] = ext_name full_name = "{0}.{1}".format(handler_name, ext_name) self.assertIn(full_name, handler_envs, "Handler/ext combo not called") for commands in handler_envs[full_name]: expected_environment_variables = original_env_vars.copy() if expected_vars is not None and commands['command'] in expected_vars: for name, val in expected_vars[commands['command']].items(): expected_environment_variables[name] = val self.assertTrue(all( env_var in commands['data'] and env_val == commands['data'][env_var] for env_var, env_val in expected_environment_variables.items()), "Incorrect data for environment variable for {0}-{1}, incorrect: {2}".format( full_name, commands['command'], [(env_var, env_val) for env_var, env_val in expected_environment_variables.items() if env_var not in commands['data'] or env_val != commands['data'][env_var]])) if not_expected is not None and commands['command'] in not_expected: self.assertFalse(any(env_var in commands['data'] for env_var in not_expected), "Unwanted env variable found") def mock_popen(cmd, *_, **kwargs): # This cgroupsapi Popen mocking all other popen calls which breaking the extension emulator logic. # The emulator should be used only on extension commands and not on other commands even env flag set. # So, added ExtensionVersion check to avoid using extension emulator on non extension operations. if 'env' in kwargs and ExtCommandEnvVariable.ExtensionVersion in kwargs['env']: handler_name, __, command = extract_extension_info_from_command(cmd) name = handler_name if ExtCommandEnvVariable.ExtensionName in kwargs['env']: name = "{0}.{1}".format(handler_name, kwargs['env'][ExtCommandEnvVariable.ExtensionName]) data = { "command": command, "data": kwargs['env'] } if name in handler_envs: handler_envs[name].append(data) else: handler_envs[name] = [data] return original_popen(cmd, *_, **kwargs) self.test_data['ext_conf'] = os.path.join(self._MULTI_CONFIG_TEST_DATA, "ext_conf_multi_config_no_dependencies.xml") with self._setup_test_env(mock_manifest=True) as (exthandlers_handler, protocol, no_of_extensions): with patch('azurelinuxagent.common.cgroupapi.subprocess.Popen', side_effect=mock_popen): # Case 1: Check normal scenario - Install/Enable mc_handlers, sc_handler = self.__run_and_assert_generic_case(exthandlers_handler, protocol, no_of_extensions) for handler in mc_handlers: __assert_env_variables(handler['handlerName'], ext_name=handler['runtimeSettingsStatus']['extensionName'], seq_no=handler['runtimeSettingsStatus']['sequenceNumber']) for handler in sc_handler: __assert_env_variables(handler['handlerName'], seq_no=handler['runtimeSettingsStatus']['sequenceNumber']) # Case 2: Check Update Scenario # Clear old test case state handler_envs = {} self.test_data['ext_conf'] = os.path.join(self._MULTI_CONFIG_TEST_DATA, 'ext_conf_mc_update_extensions.xml') protocol.mock_wire_data = WireProtocolData(self.test_data) protocol.mock_wire_data.set_incarnation(2) protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() mc_handlers = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="OSTCExtensions.ExampleHandlerLinux", expected_count=1, handler_version="1.1.0") expected_extensions = { "thirdExtension": {"status": ExtensionStatusValue.success, "seq_no": 99, "message": "Enabling thirdExtension"}, } self._assert_extension_status(mc_handlers[:], expected_extensions, multi_config=True) sc_handler = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="Microsoft.Powershell.ExampleExtension", handler_version="1.1.0") expected_extensions = { "Microsoft.Powershell.ExampleExtension": {"status": ExtensionStatusValue.success, "seq_no": 10, "message": "Enabling SingleConfig extension"} } self._assert_extension_status(sc_handler[:], expected_extensions) for handler in mc_handlers: __assert_env_variables(handler['handlerName'], handler_version="1.1.0", ext_name=handler['runtimeSettingsStatus']['extensionName'], seq_no=handler['runtimeSettingsStatus']['sequenceNumber'], expected_vars={ "disable": { ExtCommandEnvVariable.ExtensionPath: os.path.join(self.tmp_dir, "{0}-{1}".format(handler['handlerName'], "1.0.0")), ExtCommandEnvVariable.ExtensionVersion: '1.0.0' }}) # Assert the environment variables were present even for disabled/uninstalled commands first_ext_expected_vars = { "disable": { ExtCommandEnvVariable.ExtensionPath: os.path.join(self.tmp_dir, "{0}-{1}".format(handler['handlerName'], "1.0.0")), ExtCommandEnvVariable.ExtensionVersion: '1.0.0' }, "uninstall": { ExtCommandEnvVariable.ExtensionPath: os.path.join(self.tmp_dir, "{0}-{1}".format(handler['handlerName'], "1.0.0")), ExtCommandEnvVariable.ExtensionVersion: '1.0.0' }, "update": { ExtCommandEnvVariable.UpdatingFromVersion: "1.0.0", ExtCommandEnvVariable.DisableReturnCodeMultipleExtensions: json.dumps([ {"extensionName": "firstExtension", "exitCode": "0"}, {"extensionName": "secondExtension", "exitCode": "0"}, {"extensionName": "thirdExtension", "exitCode": "0"} ]) } } __assert_env_variables(handler['handlerName'], ext_name="firstExtension", expected_vars=first_ext_expected_vars, handler_version="1.1.0", seq_no="1", not_expected={ "update": [ExtCommandEnvVariable.DisableReturnCode] }) __assert_env_variables(handler['handlerName'], ext_name="secondExtension", seq_no="2") for handler in sc_handler: sc_expected_vars = { "disable": { ExtCommandEnvVariable.ExtensionPath: os.path.join(self.tmp_dir, "{0}-{1}".format(handler['handlerName'], "1.0.0")), ExtCommandEnvVariable.ExtensionVersion: '1.0.0' }, "uninstall": { ExtCommandEnvVariable.ExtensionPath: os.path.join(self.tmp_dir, "{0}-{1}".format(handler['handlerName'], "1.0.0")), ExtCommandEnvVariable.ExtensionVersion: '1.0.0' }, "update": { ExtCommandEnvVariable.UpdatingFromVersion: "1.0.0", ExtCommandEnvVariable.DisableReturnCode: "0" } } __assert_env_variables(handler['handlerName'], handler_version="1.1.0", seq_no=handler['runtimeSettingsStatus']['sequenceNumber'], expected_vars=sc_expected_vars, not_expected={ "update": [ExtCommandEnvVariable.DisableReturnCodeMultipleExtensions] }) def test_it_should_ignore_disable_errors_for_multi_config_extensions(self): fail_code, fail_action = Actions.generate_unique_fail() with self.__setup_generic_test_env() as (exthandlers_handler, protocol, exts): # Fail disable of 1st and 2nd extension exts[0] = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.firstExtension", disable_action=fail_action) exts[1] = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.secondExtension", disable_action=fail_action) fourth_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.fourthExtension") with patch.object(ExtHandlerInstance, "report_event", autospec=True) as patch_report_event: with enable_invocations(fourth_ext, *exts) as invocation_record: # Assert even though 2 extensions are failing, we clean their state up properly and enable the # remaining extensions self.__setup_and_assert_disable_scenario(exthandlers_handler, protocol) first_ext, second_ext, third_ext, sc_ext = exts invocation_record.compare( (first_ext, ExtensionCommandNames.DISABLE), (second_ext, ExtensionCommandNames.DISABLE), (third_ext, ExtensionCommandNames.ENABLE), (fourth_ext, ExtensionCommandNames.ENABLE), (sc_ext, ExtensionCommandNames.ENABLE) ) reported_events = [kwargs for _, kwargs in patch_report_event.call_args_list if re.search("Executing command: (.+) with environment variables: ", kwargs['message']) is None] self.assertTrue(all( fail_code in kwargs['message'] for kwargs in reported_events if kwargs['name'] == first_ext.name), "Error not reported") self.assertTrue(all( fail_code in kwargs['message'] for kwargs in reported_events if kwargs['name'] == second_ext.name), "Error not reported") # Make sure fail code is not reported for any other extension self.assertFalse(all( fail_code in kwargs['message'] for kwargs in reported_events if kwargs['name'] == third_ext.name), "Error not reported") def test_it_should_report_transitioning_if_status_file_not_found(self): original_popen = subprocess.Popen def mock_popen(cmd, *_, **kwargs): if 'env' in kwargs: handler_name, handler_version, __ = extract_extension_info_from_command(cmd) ext_name = None if ExtCommandEnvVariable.ExtensionName in kwargs['env']: ext_name = kwargs['env'][ExtCommandEnvVariable.ExtensionName] seq_no = kwargs['env'][ExtCommandEnvVariable.ExtensionSeqNumber] status_file_name = "{0}.status".format(seq_no) status_file_name = "{0}.{1}".format(ext_name, status_file_name) if ext_name is not None else status_file_name status_file = os.path.join(self.tmp_dir, "{0}-{1}".format(handler_name, handler_version), "status", status_file_name) if os.path.exists(status_file): os.remove(status_file) return original_popen("echo " + cmd, *_, **kwargs) self.test_data['ext_conf'] = os.path.join(self._MULTI_CONFIG_TEST_DATA, "ext_conf_multi_config_no_dependencies.xml") with self._setup_test_env(mock_manifest=True) as (exthandlers_handler, protocol, no_of_extensions): with patch('azurelinuxagent.common.cgroupapi.subprocess.Popen', side_effect=mock_popen): exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(no_of_extensions, len(protocol.aggregate_status['aggregateStatus']['handlerAggregateStatus']), "incorrect extensions reported") mc_handlers = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="OSTCExtensions.ExampleHandlerLinux", expected_count=3) agent_status_message = "This status is being reported by the Guest Agent since no status file was " \ "reported by extension {0}: " \ "[ExtensionStatusError] Status file" expected_extensions = { "firstExtension": {"status": ExtensionStatusValue.transitioning, "seq_no": 1, "message": agent_status_message.format("OSTCExtensions.ExampleHandlerLinux.firstExtension")}, "secondExtension": {"status": ExtensionStatusValue.transitioning, "seq_no": 2, "message": agent_status_message.format("OSTCExtensions.ExampleHandlerLinux.secondExtension")}, "thirdExtension": {"status": ExtensionStatusValue.transitioning, "seq_no": 3, "message": agent_status_message.format("OSTCExtensions.ExampleHandlerLinux.thirdExtension")}, } self._assert_extension_status(mc_handlers[:], expected_extensions, multi_config=True) sc_handler = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="Microsoft.Powershell.ExampleExtension") expected_extensions = { "Microsoft.Powershell.ExampleExtension": {"status": ExtensionStatusValue.transitioning, "seq_no": 9, "message": agent_status_message.format("Microsoft.Powershell.ExampleExtension")} } self._assert_extension_status(sc_handler[:], expected_extensions) def test_it_should_report_status_correctly_for_unsupported_goal_state(self): with self.__setup_generic_test_env() as (exthandlers_handler, protocol, _): # Update GS with an ExtensionConfig with 3 Required features to force GA to mark it as unsupported self.test_data['ext_conf'] = "wire/ext_conf_required_features.xml" protocol.mock_wire_data = WireProtocolData(self.test_data) protocol.mock_wire_data.set_incarnation(2) protocol.client.update_goal_state() # Assert the extension status is the same as we reported for Incarnation 1. self.__run_and_assert_generic_case(exthandlers_handler, protocol, no_of_extensions=4, with_message=False) # Assert the GS was reported as unsupported gs_aggregate_status = protocol.aggregate_status['aggregateStatus']['vmArtifactsAggregateStatus'][ 'goalStateAggregateStatus'] self.assertEqual(gs_aggregate_status['status'], GoalStateStatus.Failed, "Incorrect status") self.assertEqual(gs_aggregate_status['code'], GoalStateAggregateStatusCodes.GoalStateUnsupportedRequiredFeatures, "Incorrect code") self.assertEqual(gs_aggregate_status['inSvdSeqNo'], '2', "Incorrect incarnation reported") self.assertEqual(gs_aggregate_status['formattedMessage']['message'], 'Failing GS incarnation_2 as Unsupported features found: TestRequiredFeature1, TestRequiredFeature2, TestRequiredFeature3', "Incorrect error message reported") def test_it_should_fail_handler_if_handler_does_not_support_mc(self): self.test_data['ext_conf'] = os.path.join(self._MULTI_CONFIG_TEST_DATA, "ext_conf_multi_config_no_dependencies.xml") first_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.firstExtension") second_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.secondExtension") third_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.thirdExtension") fourth_ext = extension_emulator(name="Microsoft.Powershell.ExampleExtension") with self._setup_test_env() as (exthandlers_handler, protocol, no_of_extensions): with enable_invocations(first_ext, second_ext, third_ext, fourth_ext) as invocation_record: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(no_of_extensions, len(protocol.aggregate_status['aggregateStatus']['handlerAggregateStatus']), "incorrect extensions reported") invocation_record.compare( # Since we raise a ConfigError, we shouldn't process any of the MC extensions at all (fourth_ext, ExtensionCommandNames.INSTALL), (fourth_ext, ExtensionCommandNames.ENABLE) ) err_msg = 'Handler OSTCExtensions.ExampleHandlerLinux does not support MultiConfig but CRP expects it, failing due to inconsistent data' mc_handlers = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="OSTCExtensions.ExampleHandlerLinux", expected_count=3, status="NotReady", message=err_msg) expected_extensions = { "firstExtension": {"status": ExtensionStatusValue.error, "seq_no": 1, "message": err_msg}, "secondExtension": {"status": ExtensionStatusValue.error, "seq_no": 2, "message": err_msg}, "thirdExtension": {"status": ExtensionStatusValue.error, "seq_no": 3, "message": err_msg}, } self._assert_extension_status(mc_handlers[:], expected_extensions, multi_config=True) sc_handler = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="Microsoft.Powershell.ExampleExtension") expected_extensions = { "Microsoft.Powershell.ExampleExtension": {"status": ExtensionStatusValue.success, "seq_no": 9} } self._assert_extension_status(sc_handler[:], expected_extensions) def test_it_should_check_every_time_if_handler_supports_mc(self): with self.__setup_generic_test_env() as (exthandlers_handler, protocol, old_exts): protocol.mock_wire_data.set_incarnation(2) protocol.client.update_goal_state() # Mock manifest to not support multiple extensions with patch('azurelinuxagent.ga.exthandlers.HandlerManifest.supports_multiple_extensions', return_value=False): with enable_invocations(*old_exts) as invocation_record: (_, _, _, fourth_ext) = old_exts exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(4, len(protocol.aggregate_status['aggregateStatus']['handlerAggregateStatus']), "incorrect extensions reported") invocation_record.compare( # Since we raise a ConfigError, we shouldn't process any of the MC extensions at all (fourth_ext, ExtensionCommandNames.ENABLE) ) err_msg = 'Handler OSTCExtensions.ExampleHandlerLinux does not support MultiConfig but CRP expects it, failing due to inconsistent data' mc_handlers = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="OSTCExtensions.ExampleHandlerLinux", expected_count=3, status="NotReady", message=err_msg) # Since the extensions were not even executed, their status file should reflect the last status # (Handler status above should always report the error though) expected_extensions = { "firstExtension": {"status": ExtensionStatusValue.success, "seq_no": 1}, "secondExtension": {"status": ExtensionStatusValue.success, "seq_no": 2}, "thirdExtension": {"status": ExtensionStatusValue.success, "seq_no": 3}, } self._assert_extension_status(mc_handlers[:], expected_extensions, multi_config=True) sc_handler = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="Microsoft.Powershell.ExampleExtension") expected_extensions = { "Microsoft.Powershell.ExampleExtension": {"status": ExtensionStatusValue.success, "seq_no": 9} } self._assert_extension_status(sc_handler[:], expected_extensions) class TestMultiConfigExtensionSequencing(_MultiConfigBaseTestClass): @contextlib.contextmanager def __setup_test_and_get_exts(self): self.test_data['ext_conf'] = os.path.join(self._MULTI_CONFIG_TEST_DATA, "ext_conf_with_multi_config_dependencies.xml") first_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.firstExtension", supports_multiple_extensions=True) second_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.secondExtension", supports_multiple_extensions=True) third_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.thirdExtension", supports_multiple_extensions=True) dependent_sc_ext = extension_emulator(name="Microsoft.Powershell.ExampleExtension") independent_sc_ext = extension_emulator(name="Microsoft.Azure.Geneva.GenevaMonitoring", version="1.1.0") with self._setup_test_env() as (exthandlers_handler, protocol, no_of_extensions): yield exthandlers_handler, protocol, no_of_extensions, first_ext, second_ext, third_ext, dependent_sc_ext, independent_sc_ext def test_it_should_process_dependency_chain_extensions_properly(self): with self.__setup_test_and_get_exts() as ( exthandlers_handler, protocol, no_of_extensions, first_ext, second_ext, third_ext, dependent_sc_ext, independent_sc_ext): with enable_invocations(first_ext, second_ext, third_ext, dependent_sc_ext, independent_sc_ext) as invocation_record: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(no_of_extensions, len(protocol.aggregate_status['aggregateStatus']['handlerAggregateStatus']), "incorrect extensions reported") invocation_record.compare( (first_ext, ExtensionCommandNames.INSTALL), (first_ext, ExtensionCommandNames.ENABLE), (independent_sc_ext, ExtensionCommandNames.INSTALL), (independent_sc_ext, ExtensionCommandNames.ENABLE), (dependent_sc_ext, ExtensionCommandNames.INSTALL), (dependent_sc_ext, ExtensionCommandNames.ENABLE), (second_ext, ExtensionCommandNames.ENABLE), (third_ext, ExtensionCommandNames.ENABLE) ) mc_handlers = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="OSTCExtensions.ExampleHandlerLinux", expected_count=3) expected_extensions = { "firstExtension": {"status": ExtensionStatusValue.success, "seq_no": 2}, "secondExtension": {"status": ExtensionStatusValue.success, "seq_no": 2}, "thirdExtension": {"status": ExtensionStatusValue.success, "seq_no": 1}, } self._assert_extension_status(mc_handlers[:], expected_extensions, multi_config=True) sc_dependent_handler = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="Microsoft.Powershell.ExampleExtension") expected_extensions = { "Microsoft.Powershell.ExampleExtension": {"status": ExtensionStatusValue.success, "seq_no": 2} } self._assert_extension_status(sc_dependent_handler[:], expected_extensions) sc_independent_handler = self._assert_and_get_handler_status( aggregate_status=protocol.aggregate_status, handler_name="Microsoft.Azure.Geneva.GenevaMonitoring", handler_version="1.1.0") expected_extensions = { "Microsoft.Azure.Geneva.GenevaMonitoring": {"status": ExtensionStatusValue.success, "seq_no": 1} } self._assert_extension_status(sc_independent_handler[:], expected_extensions) def __assert_invalid_status_scenario(self, protocol, fail_code, mc_status="NotReady", mc_message="Plugin installed but not enabled", err_msg=None): mc_handlers = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="OSTCExtensions.ExampleHandlerLinux", expected_count=3, status=mc_status, message=mc_message) expected_extensions = { "firstExtension": {"status": ExtensionStatusValue.error, "seq_no": 2, "message": fail_code}, "secondExtension": {"status": ExtensionStatusValue.error, "seq_no": 2, "message": err_msg}, "thirdExtension": {"status": ExtensionStatusValue.error, "seq_no": 1, "message": err_msg}, } self._assert_extension_status(mc_handlers[:], expected_extensions, multi_config=True) sc_dependent_handler = self._assert_and_get_handler_status(aggregate_status=protocol.aggregate_status, handler_name="Microsoft.Powershell.ExampleExtension", status="NotReady", message=err_msg) self.assertTrue(all('runtimeSettingsStatus' not in handler for handler in sc_dependent_handler)) sc_independent_handler = self._assert_and_get_handler_status( aggregate_status=protocol.aggregate_status, handler_name="Microsoft.Azure.Geneva.GenevaMonitoring", handler_version="1.1.0", status="NotReady", message=err_msg) self.assertTrue(all('runtimeSettingsStatus' not in handler for handler in sc_independent_handler)) def test_it_should_report_extension_status_failures_for_all_dependent_extensions(self): with self.__setup_test_and_get_exts() as ( exthandlers_handler, protocol, no_of_extensions, first_ext, second_ext, third_ext, dependent_sc_ext, independent_sc_ext): # Fail the enable for firstExtension. fail_code, fail_action = Actions.generate_unique_fail() first_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.firstExtension", enable_action=fail_action, supports_multiple_extensions=True) with enable_invocations(first_ext, second_ext, third_ext, dependent_sc_ext, independent_sc_ext) as invocation_record: exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(no_of_extensions, len(protocol.aggregate_status['aggregateStatus']['handlerAggregateStatus']), "incorrect extensions reported") # Since firstExtension is high up on the dependency chain, no other extensions should be executed invocation_record.compare( (first_ext, ExtensionCommandNames.INSTALL), (first_ext, ExtensionCommandNames.ENABLE) ) err_msg = 'Skipping processing of extensions since execution of dependent extension OSTCExtensions.ExampleHandlerLinux.firstExtension failed' self.__assert_invalid_status_scenario(protocol, fail_code, err_msg=err_msg) def test_it_should_stop_execution_if_status_file_contains_errors(self): # This test tests the scenario where the extensions exit with a success exit code but fail subsequently with an # error in the status file self.test_data['ext_conf'] = os.path.join(self._MULTI_CONFIG_TEST_DATA, "ext_conf_with_multi_config_dependencies.xml") original_popen = subprocess.Popen invocation_records = [] fail_code = str(uuid.uuid4()) def mock_popen(cmd, *_, **kwargs): try: handler_name, handler_version, command_name = extract_extension_info_from_command(cmd) except ValueError: return original_popen(cmd, *_, **kwargs) if 'env' in kwargs: env = kwargs['env'] if ExtCommandEnvVariable.ExtensionName in env: full_name = "{0}.{1}".format(handler_name, env[ExtCommandEnvVariable.ExtensionName]) status_file = "{0}.{1}.status".format(env[ExtCommandEnvVariable.ExtensionName], env[ExtCommandEnvVariable.ExtensionSeqNumber]) status_contents = [{"status": {"status": ExtensionStatusValue.error, "code": fail_code, "formattedMessage": {"message": fail_code, "lang": "en-US"}}}] fileutil.write_file(os.path.join(env[ExtCommandEnvVariable.ExtensionPath], "status", status_file), json.dumps(status_contents)) invocation_records.append((full_name, handler_version, command_name)) # The return code is 0 but the status file should have the error, this it to test the scenario # where the extensions return a success code but fail later. return original_popen(['echo', "works"], *_, **kwargs) invocation_records.append((handler_name, handler_version, command_name)) return original_popen(cmd, *_, **kwargs) with self._setup_test_env(mock_manifest=True) as (exthandlers_handler, protocol, no_of_extensions): with patch('azurelinuxagent.common.cgroupapi.subprocess.Popen', side_effect=mock_popen): exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(no_of_extensions, len(protocol.aggregate_status['aggregateStatus']['handlerAggregateStatus']), "incorrect extensions reported") # Since we're writing error status for firstExtension, only the firstExtension should be invoked and # everything else should be skipped expected_invocations = [ ('OSTCExtensions.ExampleHandlerLinux.firstExtension', '1.0.0', ExtensionCommandNames.INSTALL), ('OSTCExtensions.ExampleHandlerLinux.firstExtension', '1.0.0', ExtensionCommandNames.ENABLE)] self.assertEqual(invocation_records, expected_invocations, "Invalid invocations found") err_msg = 'Dependent Extension OSTCExtensions.ExampleHandlerLinux.firstExtension did not succeed. Status was error' self.__assert_invalid_status_scenario(protocol, fail_code, mc_status="Ready", mc_message="Plugin enabled", err_msg=err_msg) WALinuxAgent-2.9.1.1/tests/ga/test_periodic_operation.py000066400000000000000000000155741446033677600232440ustar00rootroot00000000000000# Copyright 2020 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import datetime import time from azurelinuxagent.ga.monitor import PeriodicOperation from tests.tools import AgentTestCase, patch, PropertyMock class TestPeriodicOperation(AgentTestCase): class SaveRunTimestamp(PeriodicOperation): def __init__(self, period): super(TestPeriodicOperation.SaveRunTimestamp, self).__init__(period) self.run_time = None def _operation(self): self.run_time = datetime.datetime.utcnow() def test_it_should_take_a_timedelta_as_period(self): op = TestPeriodicOperation.SaveRunTimestamp(datetime.timedelta(hours=1)) op.run() expected = op.run_time + datetime.timedelta(hours=1) difference = op.next_run_time() - expected self.assertTrue(difference < datetime.timedelta(seconds=1), "The next run time exceeds the expected value by more than 1 second: {0} vs {1}".format(op.next_run_time(), expected)) def test_it_should_take_a_number_of_seconds_as_period(self): op = TestPeriodicOperation.SaveRunTimestamp(3600) op.run() expected = op.run_time + datetime.timedelta(hours=1) difference = op.next_run_time() - expected self.assertTrue(difference < datetime.timedelta(seconds=1), "The next run time exceeds the expected value by more than 1 second: {0} vs {1}".format(op.next_run_time(), expected)) class CountInvocations(PeriodicOperation): def __init__(self, period): super(TestPeriodicOperation.CountInvocations, self).__init__(period) self.invoke_count = 0 def _operation(self): self.invoke_count += 1 def test_it_should_be_invoked_when_run_is_called_first_time(self): op = TestPeriodicOperation.CountInvocations(datetime.timedelta(hours=1)) op.run() self.assertTrue(op.invoke_count > 0, "The operation was not invoked") def test_it_should_not_be_invoked_if_the_period_has_not_elapsed(self): pop = TestPeriodicOperation.CountInvocations(datetime.timedelta(hours=1)) for _ in range(5): pop.run() # the first run() invoked the operation, so the count is 1 self.assertEqual(pop.invoke_count, 1, "The operation was invoked before the period elapsed") def test_it_should_be_invoked_if_the_period_has_elapsed(self): pop = TestPeriodicOperation.CountInvocations(datetime.timedelta(milliseconds=1)) for _ in range(5): pop.run() time.sleep(0.001) self.assertEqual(pop.invoke_count, 5, "The operation was not invoked after the period elapsed") class RaiseException(PeriodicOperation): def _operation(self): raise Exception("A test exception") @staticmethod def _get_number_of_warnings(warn_patcher, message="A test exception"): return len([args for args, _ in warn_patcher.call_args_list if any(message in a for a in args)]) def test_it_should_log_a_warning_if_the_operation_fails(self): with patch("azurelinuxagent.common.logger.warn") as warn_patcher: TestPeriodicOperation.RaiseException(datetime.timedelta(hours=1)).run() self.assertEqual(self._get_number_of_warnings(warn_patcher), 1, "The error in the operation was should have been reported exactly once") def test_it_should_not_log_multiple_warnings_when_the_period_has_not_elapsed(self): with patch("azurelinuxagent.common.logger.warn") as warn_patcher: pop = TestPeriodicOperation.RaiseException(datetime.timedelta(hours=1)) for _ in range(5): pop.run() self.assertEqual(self._get_number_of_warnings(warn_patcher), 1, "The error in the operation was should have been reported exactly once") def test_it_should_not_log_multiple_warnings_when_the_period_has_elapsed(self): with patch("azurelinuxagent.common.logger.warn") as warn_patcher: with patch("azurelinuxagent.ga.periodic_operation.PeriodicOperation._LOG_WARNING_PERIOD", new_callable=PropertyMock, return_value=datetime.timedelta(milliseconds=1)): pop = TestPeriodicOperation.RaiseException(datetime.timedelta(milliseconds=1)) for _ in range(5): pop.run() time.sleep(0.001) self.assertEqual(self._get_number_of_warnings(warn_patcher), 5, "The error in the operation was not reported the expected number of times") class RaiseTwoExceptions(PeriodicOperation): def __init__(self, period): super(TestPeriodicOperation.RaiseTwoExceptions, self).__init__(period) self._count = 0 def _operation(self): message = "WARNING {0}".format(self._count) if self._count == 0: self._count += 1 raise Exception(message) def test_it_should_log_warnings_if_they_are_different(self): with patch("azurelinuxagent.common.logger.warn") as warn_patcher: pop = TestPeriodicOperation.RaiseTwoExceptions(0) for _ in range(5): pop.run() self.assertEqual(self._get_number_of_warnings(warn_patcher, "WARNING 0"), 1, "The first error should have been reported exactly 1 time") self.assertEqual(self._get_number_of_warnings(warn_patcher, "WARNING 1"), 1, "The second error should have been reported exactly 1 time") class NoOp(PeriodicOperation): def _operation(self): pass def test_sleep_until_next_operation_should_wait_for_the_closest_operation(self): operations = [ TestPeriodicOperation.NoOp(datetime.timedelta(seconds=60)), TestPeriodicOperation.NoOp(datetime.timedelta(hours=1)), TestPeriodicOperation.NoOp(datetime.timedelta(seconds=10)), # closest operation TestPeriodicOperation.NoOp(datetime.timedelta(minutes=11)), TestPeriodicOperation.NoOp(datetime.timedelta(days=1)) ] for op in operations: op.run() def mock_sleep(seconds): mock_sleep.seconds = seconds mock_sleep.seconds = 0 with patch("azurelinuxagent.ga.periodic_operation.time.sleep", side_effect=mock_sleep): PeriodicOperation.sleep_until_next_operation(operations) self.assertAlmostEqual(mock_sleep.seconds, 10, 0, "did not sleep for the expected time") WALinuxAgent-2.9.1.1/tests/ga/test_remoteaccess.py000066400000000000000000000123661446033677600220370ustar00rootroot00000000000000# Copyright Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import xml from azurelinuxagent.common.protocol.goal_state import GoalState, RemoteAccess # pylint: disable=unused-import from tests.tools import AgentTestCase, load_data, patch, Mock # pylint: disable=unused-import from tests.protocol import mockwiredata from tests.protocol.mocks import mock_wire_protocol class TestRemoteAccess(AgentTestCase): def test_parse_remote_access(self): data_str = load_data('wire/remote_access_single_account.xml') remote_access = RemoteAccess(data_str) self.assertNotEqual(None, remote_access) self.assertEqual("1", remote_access.incarnation) self.assertEqual(1, len(remote_access.user_list.users), "User count does not match.") self.assertEqual("testAccount", remote_access.user_list.users[0].name, "Account name does not match") self.assertEqual("encryptedPasswordString", remote_access.user_list.users[0].encrypted_password, "Encrypted password does not match.") self.assertEqual("2019-01-01", remote_access.user_list.users[0].expiration, "Expiration does not match.") def test_goal_state_with_no_remote_access(self): with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: self.assertIsNone(protocol.client.get_remote_access()) def test_parse_two_remote_access_accounts(self): data_str = load_data('wire/remote_access_two_accounts.xml') remote_access = RemoteAccess(data_str) self.assertNotEqual(None, remote_access) self.assertEqual("1", remote_access.incarnation) self.assertEqual(2, len(remote_access.user_list.users), "User count does not match.") self.assertEqual("testAccount1", remote_access.user_list.users[0].name, "Account name does not match") self.assertEqual("encryptedPasswordString", remote_access.user_list.users[0].encrypted_password, "Encrypted password does not match.") self.assertEqual("2019-01-01", remote_access.user_list.users[0].expiration, "Expiration does not match.") self.assertEqual("testAccount2", remote_access.user_list.users[1].name, "Account name does not match") self.assertEqual("encryptedPasswordString", remote_access.user_list.users[1].encrypted_password, "Encrypted password does not match.") self.assertEqual("2019-01-01", remote_access.user_list.users[1].expiration, "Expiration does not match.") def test_parse_ten_remote_access_accounts(self): data_str = load_data('wire/remote_access_10_accounts.xml') remote_access = RemoteAccess(data_str) self.assertNotEqual(None, remote_access) self.assertEqual(10, len(remote_access.user_list.users), "User count does not match.") def test_parse_duplicate_remote_access_accounts(self): data_str = load_data('wire/remote_access_duplicate_accounts.xml') remote_access = RemoteAccess(data_str) self.assertNotEqual(None, remote_access) self.assertEqual(2, len(remote_access.user_list.users), "User count does not match.") self.assertEqual("testAccount", remote_access.user_list.users[0].name, "Account name does not match") self.assertEqual("encryptedPasswordString", remote_access.user_list.users[0].encrypted_password, "Encrypted password does not match.") self.assertEqual("2019-01-01", remote_access.user_list.users[0].expiration, "Expiration does not match.") self.assertEqual("testAccount", remote_access.user_list.users[1].name, "Account name does not match") self.assertEqual("encryptedPasswordString", remote_access.user_list.users[1].encrypted_password, "Encrypted password does not match.") self.assertEqual("2019-01-01", remote_access.user_list.users[1].expiration, "Expiration does not match.") def test_parse_zero_remote_access_accounts(self): data_str = load_data('wire/remote_access_no_accounts.xml') remote_access = RemoteAccess(data_str) self.assertNotEqual(None, remote_access) self.assertEqual(0, len(remote_access.user_list.users), "User count does not match.") def test_update_remote_access_conf_remote_access(self): with mock_wire_protocol(mockwiredata.DATA_FILE_REMOTE_ACCESS) as protocol: self.assertIsNotNone(protocol.client.get_remote_access()) self.assertEqual(1, len(protocol.client.get_remote_access().user_list.users)) self.assertEqual('testAccount', protocol.client.get_remote_access().user_list.users[0].name) self.assertEqual('encryptedPasswordString', protocol.client.get_remote_access().user_list.users[0].encrypted_password) def test_parse_bad_remote_access_data(self): data = "foobar" self.assertRaises(xml.parsers.expat.ExpatError, RemoteAccess, data)WALinuxAgent-2.9.1.1/tests/ga/test_remoteaccess_handler.py000066400000000000000000000656451446033677600235440ustar00rootroot00000000000000# Copyright Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # from datetime import timedelta, datetime from mock import Mock, MagicMock from azurelinuxagent.common.osutil.default import DefaultOSUtil from azurelinuxagent.common.protocol.goal_state import RemoteAccess from azurelinuxagent.common.protocol.util import ProtocolUtil from azurelinuxagent.common.protocol.wire import WireProtocol from azurelinuxagent.ga.remoteaccess import RemoteAccessHandler from tests.tools import AgentTestCase, load_data, patch, clear_singleton_instances from tests.protocol.mocks import mock_wire_protocol from tests.protocol.mockwiredata import DATA_FILE, DATA_FILE_REMOTE_ACCESS class MockOSUtil(DefaultOSUtil): def __init__(self): # pylint: disable=super-init-not-called self.all_users = {} self.sudo_users = set() self.jit_enabled = True def useradd(self, username, expiration=None, comment=None): if username == "": raise Exception("test exception for bad username") if username in self.all_users: raise Exception("test exception, user already exists") self.all_users[username] = (username, None, None, None, comment, None, None, expiration) def conf_sudoer(self, username, nopasswd=False, remove=False): if not remove: self.sudo_users.add(username) else: self.sudo_users.remove(username) def chpasswd(self, username, password, crypt_id=6, salt_len=10): if password == "": raise Exception("test exception for bad password") user = self.all_users[username] self.all_users[username] = (user[0], password, user[2], user[3], user[4], user[5], user[6], user[7]) def del_account(self, username): if username == "": raise Exception("test exception, bad data") if username not in self.all_users: raise Exception("test exception, user does not exist to delete") self.all_users.pop(username) def get_users(self): return self.all_users.values() def get_user_dictionary(users): user_dictionary = {} for user in users: user_dictionary[user[0]] = user return user_dictionary def mock_add_event(name, op, is_success, version, message): TestRemoteAccessHandler.eventing_data = (name, op, is_success, version, message) class TestRemoteAccessHandler(AgentTestCase): eventing_data = [()] def setUp(self): super(TestRemoteAccessHandler, self).setUp() # Since ProtocolUtil is a singleton per thread, we need to clear it to ensure that the test cases do not # reuse a previous state clear_singleton_instances(ProtocolUtil) for data in TestRemoteAccessHandler.eventing_data: del data # add_user tests @patch('azurelinuxagent.common.utils.cryptutil.CryptUtil.decrypt_secret', return_value="]aPPEv}uNg1FPnl?") def test_add_user(self, *_): with patch("azurelinuxagent.ga.remoteaccess.get_osutil", return_value=MockOSUtil()): rah = RemoteAccessHandler(Mock()) tstpassword = "]aPPEv}uNg1FPnl?" tstuser = "foobar" expiration_date = datetime.utcnow() + timedelta(days=1) pwd = tstpassword rah._add_user(tstuser, pwd, expiration_date) users = get_user_dictionary(rah._os_util.get_users()) self.assertTrue(tstuser in users, "{0} missing from users".format(tstuser)) actual_user = users[tstuser] expected_expiration = (expiration_date + timedelta(days=1)).strftime("%Y-%m-%d") self.assertEqual(actual_user[7], expected_expiration) self.assertEqual(actual_user[4], "JIT_Account") @patch('azurelinuxagent.common.utils.cryptutil.CryptUtil.decrypt_secret', return_value="]aPPEv}uNg1FPnl?") def test_add_user_bad_creation_data(self, *_): with patch("azurelinuxagent.ga.remoteaccess.get_osutil", return_value=MockOSUtil()): rah = RemoteAccessHandler(Mock()) tstpassword = "]aPPEv}uNg1FPnl?" tstuser = "" expiration = datetime.utcnow() + timedelta(days=1) pwd = tstpassword error = "test exception for bad username" self.assertRaisesRegex(Exception, error, rah._add_user, tstuser, pwd, expiration) self.assertEqual(0, len(rah._os_util.get_users())) @patch('azurelinuxagent.common.utils.cryptutil.CryptUtil.decrypt_secret', return_value="") def test_add_user_bad_password_data(self, *_): with patch("azurelinuxagent.ga.remoteaccess.get_osutil", return_value=MockOSUtil()): rah = RemoteAccessHandler(Mock()) tstpassword = "" tstuser = "foobar" expiration = datetime.utcnow() + timedelta(days=1) pwd = tstpassword error = "test exception for bad password" self.assertRaisesRegex(Exception, error, rah._add_user, tstuser, pwd, expiration) self.assertEqual(0, len(rah._os_util.get_users())) @patch('azurelinuxagent.common.utils.cryptutil.CryptUtil.decrypt_secret', return_value="]aPPEv}uNg1FPnl?") def test_add_user_already_existing(self, _): with patch("azurelinuxagent.ga.remoteaccess.get_osutil", return_value=MockOSUtil()): rah = RemoteAccessHandler(Mock()) tstpassword = "]aPPEv}uNg1FPnl?" tstuser = "foobar" expiration_date = datetime.utcnow() + timedelta(days=1) pwd = tstpassword rah._add_user(tstuser, pwd, expiration_date) users = get_user_dictionary(rah._os_util.get_users()) self.assertTrue(tstuser in users, "{0} missing from users".format(tstuser)) self.assertEqual(1, len(users.keys())) actual_user = users[tstuser] self.assertEqual(actual_user[7], (expiration_date + timedelta(days=1)).strftime("%Y-%m-%d")) # add the new duplicate user, ensure it's not created and does not overwrite the existing user. # this does not test the user add function as that's mocked, it tests processing skips the remaining # calls after the initial failure new_user_expiration = datetime.utcnow() + timedelta(days=5) self.assertRaises(Exception, rah._add_user, tstuser, pwd, new_user_expiration) # refresh users users = get_user_dictionary(rah._os_util.get_users()) self.assertTrue(tstuser in users, "{0} missing from users after dup user attempted".format(tstuser)) self.assertEqual(1, len(users.keys())) actual_user = users[tstuser] self.assertEqual(actual_user[7], (expiration_date + timedelta(days=1)).strftime("%Y-%m-%d")) # delete_user tests @patch('azurelinuxagent.common.utils.cryptutil.CryptUtil.decrypt_secret', return_value="]aPPEv}uNg1FPnl?") def test_delete_user(self, *_): with patch("azurelinuxagent.ga.remoteaccess.get_osutil", return_value=MockOSUtil()): rah = RemoteAccessHandler(Mock()) tstpassword = "]aPPEv}uNg1FPnl?" tstuser = "foobar" expiration_date = datetime.utcnow() + timedelta(days=1) expected_expiration = (expiration_date + timedelta(days=1)).strftime("%Y-%m-%d") # pylint: disable=unused-variable pwd = tstpassword rah._add_user(tstuser, pwd, expiration_date) users = get_user_dictionary(rah._os_util.get_users()) self.assertTrue(tstuser in users, "{0} missing from users".format(tstuser)) rah._remove_user(tstuser) # refresh users users = get_user_dictionary(rah._os_util.get_users()) self.assertFalse(tstuser in users) @patch('azurelinuxagent.common.utils.cryptutil.CryptUtil.decrypt_secret', return_value="]aPPEv}uNg1FPnl?") def test_handle_new_user(self, _): with patch("azurelinuxagent.ga.remoteaccess.get_osutil", return_value=MockOSUtil()): rah = RemoteAccessHandler(Mock()) data_str = load_data('wire/remote_access_single_account.xml') remote_access = RemoteAccess(data_str) tstuser = remote_access.user_list.users[0].name expiration_date = datetime.utcnow() + timedelta(days=1) expiration = expiration_date.strftime("%a, %d %b %Y %H:%M:%S ") + "UTC" remote_access.user_list.users[0].expiration = expiration rah._remote_access = remote_access rah._handle_remote_access() users = get_user_dictionary(rah._os_util.get_users()) self.assertTrue(tstuser in users, "{0} missing from users".format(tstuser)) actual_user = users[tstuser] expected_expiration = (expiration_date + timedelta(days=1)).strftime("%Y-%m-%d") self.assertEqual(actual_user[7], expected_expiration) self.assertEqual(actual_user[4], "JIT_Account") def test_do_not_add_expired_user(self): with patch("azurelinuxagent.ga.remoteaccess.get_osutil", return_value=MockOSUtil()): rah = RemoteAccessHandler(Mock()) data_str = load_data('wire/remote_access_single_account.xml') remote_access = RemoteAccess(data_str) expiration = (datetime.utcnow() - timedelta(days=2)).strftime("%a, %d %b %Y %H:%M:%S ") + "UTC" remote_access.user_list.users[0].expiration = expiration rah._remote_access = remote_access rah._handle_remote_access() users = get_user_dictionary(rah._os_util.get_users()) self.assertFalse("testAccount" in users) def test_error_add_user(self): with patch("azurelinuxagent.ga.remoteaccess.get_osutil", return_value=MockOSUtil()): rah = RemoteAccessHandler(Mock()) tstuser = "foobar" expiration = datetime.utcnow() + timedelta(days=1) pwd = "bad password" error = r"\[CryptError\] Error decoding secret\nInner error: Incorrect padding" self.assertRaisesRegex(Exception, error, rah._add_user, tstuser, pwd, expiration) users = get_user_dictionary(rah._os_util.get_users()) self.assertEqual(0, len(users)) def test_handle_remote_access_no_users(self): with patch("azurelinuxagent.ga.remoteaccess.get_osutil", return_value=MockOSUtil()): rah = RemoteAccessHandler(Mock()) data_str = load_data('wire/remote_access_no_accounts.xml') remote_access = RemoteAccess(data_str) rah._remote_access = remote_access rah._handle_remote_access() users = get_user_dictionary(rah._os_util.get_users()) self.assertEqual(0, len(users.keys())) def test_handle_remote_access_validate_jit_user_valid(self): rah = RemoteAccessHandler(Mock()) comment = "JIT_Account" result = rah._is_jit_user(comment) self.assertTrue(result, "Did not identify '{0}' as a JIT_Account".format(comment)) def test_handle_remote_access_validate_jit_user_invalid(self): rah = RemoteAccessHandler(Mock()) test_users = ["John Doe", None, "", " "] failed_results = "" for user in test_users: if rah._is_jit_user(user): failed_results += "incorrectly identified '{0} as a JIT_Account'. ".format(user) if len(failed_results) > 0: self.fail(failed_results) @patch('azurelinuxagent.common.utils.cryptutil.CryptUtil.decrypt_secret', return_value="]aPPEv}uNg1FPnl?") def test_handle_remote_access_multiple_users(self, _): with patch("azurelinuxagent.ga.remoteaccess.get_osutil", return_value=MockOSUtil()): rah = RemoteAccessHandler(Mock()) data_str = load_data('wire/remote_access_two_accounts.xml') remote_access = RemoteAccess(data_str) testusers = [] count = 0 while count < 2: user = remote_access.user_list.users[count].name expiration_date = datetime.utcnow() + timedelta(days=count + 1) expiration = expiration_date.strftime("%a, %d %b %Y %H:%M:%S ") + "UTC" remote_access.user_list.users[count].expiration = expiration testusers.append(user) count += 1 rah._remote_access = remote_access rah._handle_remote_access() users = get_user_dictionary(rah._os_util.get_users()) self.assertTrue(testusers[0] in users, "{0} missing from users".format(testusers[0])) self.assertTrue(testusers[1] in users, "{0} missing from users".format(testusers[1])) @patch('azurelinuxagent.common.utils.cryptutil.CryptUtil.decrypt_secret', return_value="]aPPEv}uNg1FPnl?") # max fabric supports in the Goal State def test_handle_remote_access_ten_users(self, _): with patch("azurelinuxagent.ga.remoteaccess.get_osutil", return_value=MockOSUtil()): rah = RemoteAccessHandler(Mock()) data_str = load_data('wire/remote_access_10_accounts.xml') remote_access = RemoteAccess(data_str) count = 0 for user in remote_access.user_list.users: count += 1 user.name = "tstuser{0}".format(count) expiration_date = datetime.utcnow() + timedelta(days=count) user.expiration = expiration_date.strftime("%a, %d %b %Y %H:%M:%S ") + "UTC" rah._remote_access = remote_access rah._handle_remote_access() users = get_user_dictionary(rah._os_util.get_users()) self.assertEqual(10, len(users.keys())) @patch('azurelinuxagent.common.utils.cryptutil.CryptUtil.decrypt_secret', return_value="]aPPEv}uNg1FPnl?") def test_handle_remote_access_user_removed(self, _): with patch("azurelinuxagent.ga.remoteaccess.get_osutil", return_value=MockOSUtil()): rah = RemoteAccessHandler(Mock()) data_str = load_data('wire/remote_access_10_accounts.xml') remote_access = RemoteAccess(data_str) count = 0 for user in remote_access.user_list.users: count += 1 user.name = "tstuser{0}".format(count) expiration_date = datetime.utcnow() + timedelta(days=count) user.expiration = expiration_date.strftime("%a, %d %b %Y %H:%M:%S ") + "UTC" rah._remote_access = remote_access rah._handle_remote_access() users = get_user_dictionary(rah._os_util.get_users()) self.assertEqual(10, len(users.keys())) del rah._remote_access.user_list.users[:] self.assertEqual(10, len(users.keys())) @patch('azurelinuxagent.common.utils.cryptutil.CryptUtil.decrypt_secret', return_value="]aPPEv}uNg1FPnl?") def test_handle_remote_access_bad_data_and_good_data(self, _): with patch("azurelinuxagent.ga.remoteaccess.get_osutil", return_value=MockOSUtil()): rah = RemoteAccessHandler(Mock()) data_str = load_data('wire/remote_access_10_accounts.xml') remote_access = RemoteAccess(data_str) count = 0 for user in remote_access.user_list.users: count += 1 user.name = "tstuser{0}".format(count) if count == 2: user.name = "" expiration_date = datetime.utcnow() + timedelta(days=count) user.expiration = expiration_date.strftime("%a, %d %b %Y %H:%M:%S ") + "UTC" rah._remote_access = remote_access rah._handle_remote_access() users = get_user_dictionary(rah._os_util.get_users()) self.assertEqual(9, len(users.keys())) @patch('azurelinuxagent.common.utils.cryptutil.CryptUtil.decrypt_secret', return_value="]aPPEv}uNg1FPnl?") def test_handle_remote_access_deleted_user_readded(self, _): with patch("azurelinuxagent.ga.remoteaccess.get_osutil", return_value=MockOSUtil()): rah = RemoteAccessHandler(Mock()) data_str = load_data('wire/remote_access_single_account.xml') remote_access = RemoteAccess(data_str) tstuser = remote_access.user_list.users[0].name expiration_date = datetime.utcnow() + timedelta(days=1) expiration = expiration_date.strftime("%a, %d %b %Y %H:%M:%S ") + "UTC" remote_access.user_list.users[0].expiration = expiration rah._remote_access = remote_access rah._handle_remote_access() users = get_user_dictionary(rah._os_util.get_users()) self.assertTrue(tstuser in users, "{0} missing from users".format(tstuser)) os_util = rah._os_util os_util.__class__ = MockOSUtil os_util.all_users.clear() # pylint: disable=no-member # refresh users users = get_user_dictionary(rah._os_util.get_users()) self.assertTrue(tstuser not in users) rah._handle_remote_access() # refresh users users = get_user_dictionary(rah._os_util.get_users()) self.assertTrue(tstuser in users, "{0} missing from users".format(tstuser)) @patch('azurelinuxagent.common.utils.cryptutil.CryptUtil.decrypt_secret', return_value="]aPPEv}uNg1FPnl?") @patch('azurelinuxagent.common.osutil.get_osutil', return_value=MockOSUtil()) @patch('azurelinuxagent.common.protocol.util.ProtocolUtil.get_protocol', return_value=WireProtocol("12.34.56.78")) @patch('azurelinuxagent.common.protocol.wire.WireClient.get_remote_access', return_value="asdf") def test_remote_access_handler_run_bad_data(self, _1, _2, _3, _4): with patch("azurelinuxagent.ga.remoteaccess.get_osutil", return_value=MockOSUtil()): rah = RemoteAccessHandler(Mock()) tstpassword = "]aPPEv}uNg1FPnl?" tstuser = "foobar" expiration_date = datetime.utcnow() + timedelta(days=1) pwd = tstpassword rah._add_user(tstuser, pwd, expiration_date) users = get_user_dictionary(rah._os_util.get_users()) self.assertTrue(tstuser in users, "{0} missing from users".format(tstuser)) rah.run() self.assertTrue(tstuser in users, "{0} missing from users".format(tstuser)) @patch('azurelinuxagent.common.utils.cryptutil.CryptUtil.decrypt_secret', return_value="]aPPEv}uNg1FPnl?") def test_handle_remote_access_multiple_users_one_removed(self, _): with patch("azurelinuxagent.ga.remoteaccess.get_osutil", return_value=MockOSUtil()): rah = RemoteAccessHandler(Mock()) data_str = load_data('wire/remote_access_10_accounts.xml') remote_access = RemoteAccess(data_str) count = 0 for user in remote_access.user_list.users: count += 1 user.name = "tstuser{0}".format(count) expiration_date = datetime.utcnow() + timedelta(days=count) user.expiration = expiration_date.strftime("%a, %d %b %Y %H:%M:%S ") + "UTC" rah._remote_access = remote_access rah._handle_remote_access() users = rah._os_util.get_users() self.assertEqual(10, len(users)) # now remove the user from RemoteAccess deleted_user = rah._remote_access.user_list.users[3] del rah._remote_access.user_list.users[3] rah._handle_remote_access() users = rah._os_util.get_users() self.assertTrue(deleted_user not in users, "{0} still in users".format(deleted_user)) self.assertEqual(9, len(users)) @patch('azurelinuxagent.common.utils.cryptutil.CryptUtil.decrypt_secret', return_value="]aPPEv}uNg1FPnl?") def test_handle_remote_access_multiple_users_null_remote_access(self, _): with patch("azurelinuxagent.ga.remoteaccess.get_osutil", return_value=MockOSUtil()): rah = RemoteAccessHandler(Mock()) data_str = load_data('wire/remote_access_10_accounts.xml') remote_access = RemoteAccess(data_str) count = 0 for user in remote_access.user_list.users: count += 1 user.name = "tstuser{0}".format(count) expiration_date = datetime.utcnow() + timedelta(days=count) user.expiration = expiration_date.strftime("%a, %d %b %Y %H:%M:%S ") + "UTC" rah._remote_access = remote_access rah._handle_remote_access() users = rah._os_util.get_users() self.assertEqual(10, len(users)) # now remove the user from RemoteAccess rah._remote_access = None rah._handle_remote_access() users = rah._os_util.get_users() self.assertEqual(0, len(users)) @patch('azurelinuxagent.common.utils.cryptutil.CryptUtil.decrypt_secret', return_value="]aPPEv}uNg1FPnl?") def test_handle_remote_access_multiple_users_error_with_null_remote_access(self, _): with patch("azurelinuxagent.ga.remoteaccess.get_osutil", return_value=MockOSUtil()): rah = RemoteAccessHandler(Mock()) data_str = load_data('wire/remote_access_10_accounts.xml') remote_access = RemoteAccess(data_str) count = 0 for user in remote_access.user_list.users: count += 1 user.name = "tstuser{0}".format(count) expiration_date = datetime.utcnow() + timedelta(days=count) user.expiration = expiration_date.strftime("%a, %d %b %Y %H:%M:%S ") + "UTC" rah._remote_access = remote_access rah._handle_remote_access() users = rah._os_util.get_users() self.assertEqual(10, len(users)) # now remove the user from RemoteAccess rah._remote_access = None rah._handle_remote_access() users = rah._os_util.get_users() self.assertEqual(0, len(users)) def test_remove_user_error(self): with patch("azurelinuxagent.ga.remoteaccess.get_osutil", return_value=MockOSUtil()): rah = RemoteAccessHandler(Mock()) error = "test exception, bad data" self.assertRaisesRegex(Exception, error, rah._remove_user, "") def test_remove_user_not_exists(self): with patch("azurelinuxagent.ga.remoteaccess.get_osutil", return_value=MockOSUtil()): rah = RemoteAccessHandler(Mock()) user = "bob" error = "test exception, user does not exist to delete" self.assertRaisesRegex(Exception, error, rah._remove_user, user) @patch('azurelinuxagent.common.utils.cryptutil.CryptUtil.decrypt_secret', return_value="]aPPEv}uNg1FPnl?") def test_handle_remote_access_remove_and_add(self, _): with patch("azurelinuxagent.ga.remoteaccess.get_osutil", return_value=MockOSUtil()): rah = RemoteAccessHandler(Mock()) data_str = load_data('wire/remote_access_10_accounts.xml') remote_access = RemoteAccess(data_str) count = 0 for user in remote_access.user_list.users: count += 1 user.name = "tstuser{0}".format(count) expiration_date = datetime.utcnow() + timedelta(days=count) user.expiration = expiration_date.strftime("%a, %d %b %Y %H:%M:%S ") + "UTC" rah._remote_access = remote_access rah._handle_remote_access() users = rah._os_util.get_users() self.assertEqual(10, len(users)) # now remove the user from RemoteAccess new_user = "tstuser11" deleted_user = rah._remote_access.user_list.users[3] rah._remote_access.user_list.users[3].name = new_user rah._handle_remote_access() users = rah._os_util.get_users() self.assertTrue(deleted_user not in users, "{0} still in users".format(deleted_user)) self.assertTrue(new_user in [u[0] for u in users], "user {0} not in users".format(new_user)) self.assertEqual(10, len(users)) @patch('azurelinuxagent.ga.remoteaccess.add_event', side_effect=mock_add_event) def test_remote_access_handler_run_error(self, _): with patch("azurelinuxagent.ga.remoteaccess.get_osutil", return_value=MockOSUtil()): mock_protocol = WireProtocol("foo.bar") mock_protocol.client.get_remote_access = MagicMock(side_effect=Exception("foobar!")) rah = RemoteAccessHandler(mock_protocol) rah.run() print(TestRemoteAccessHandler.eventing_data) check_message = "foobar!" self.assertTrue(check_message in TestRemoteAccessHandler.eventing_data[4], "expected message {0} not found in {1}" .format(check_message, TestRemoteAccessHandler.eventing_data[4])) self.assertEqual(False, TestRemoteAccessHandler.eventing_data[2], "is_success is true") def test_remote_access_handler_should_retrieve_users_when_it_is_invoked_the_first_time(self): mock_os_util = MagicMock() with patch("azurelinuxagent.ga.remoteaccess.get_osutil", return_value=mock_os_util): with mock_wire_protocol(DATA_FILE) as mock_protocol: rah = RemoteAccessHandler(mock_protocol) rah.run() self.assertTrue(len(mock_os_util.get_users.call_args_list) == 1, "The first invocation of remote access should have retrieved the current users") def test_remote_access_handler_should_retrieve_users_when_goal_state_contains_jit_users(self): mock_os_util = MagicMock() with patch("azurelinuxagent.ga.remoteaccess.get_osutil", return_value=mock_os_util): with mock_wire_protocol(DATA_FILE_REMOTE_ACCESS) as mock_protocol: rah = RemoteAccessHandler(mock_protocol) rah.run() self.assertTrue(len(mock_os_util.get_users.call_args_list) > 0, "A goal state with jit users did not retrieve the current users") def test_remote_access_handler_should_not_retrieve_users_when_goal_state_does_not_contain_jit_users(self): mock_os_util = MagicMock() with patch("azurelinuxagent.ga.remoteaccess.get_osutil", return_value=mock_os_util): with mock_wire_protocol(DATA_FILE) as mock_protocol: rah = RemoteAccessHandler(mock_protocol) rah.run() # this will trigger one call to retrieve the users mock_protocol.mock_wire_data.set_incarnation(123) # mock a new goal state; the data file does not include any jit users rah.run() self.assertTrue(len(mock_os_util.get_users.call_args_list) == 1, "A goal state without jit users retrieved the current users") WALinuxAgent-2.9.1.1/tests/ga/test_report_status.py000066400000000000000000000156541446033677600223030ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the Apache License. import json from azurelinuxagent.ga.exthandlers import ExtHandlersHandler from azurelinuxagent.ga.update import get_update_handler from tests.ga.mocks import mock_update_handler from tests.protocol.mocks import mock_wire_protocol, MockHttpResponse from tests.tools import AgentTestCase, patch from tests.protocol import mockwiredata from tests.protocol.HttpRequestPredicates import HttpRequestPredicates class ReportStatusTestCase(AgentTestCase): """ Tests for UpdateHandler._report_status() """ def test_update_handler_should_report_status_when_fetch_goal_state_fails(self): # The test executes the main loop of UpdateHandler.run() twice, failing requests for the goal state # on the second iteration. We expect the 2 iterations to report status, despite the goal state failure. fail_goal_state_request = [False] def http_get_handler(url, *_, **__): if HttpRequestPredicates.is_goal_state_request(url) and fail_goal_state_request[0]: return MockHttpResponse(status=410) return None def on_new_iteration(iteration): fail_goal_state_request[0] = iteration == 2 with mock_wire_protocol(mockwiredata.DATA_FILE, http_get_handler=http_get_handler) as protocol: exthandlers_handler = ExtHandlersHandler(protocol) with patch.object(exthandlers_handler, "run", wraps=exthandlers_handler.run) as exthandlers_handler_run: with mock_update_handler(protocol, iterations=2, on_new_iteration=on_new_iteration, exthandlers_handler=exthandlers_handler) as update_handler: update_handler.run(debug=True) self.assertEqual(1, exthandlers_handler_run.call_count, "Extensions should have been executed only once.") self.assertEqual(2, len(protocol.mock_wire_data.status_blobs), "Status should have been reported for the 2 iterations.") # # Verify that we reported status for the extension in the test data # first_status = json.loads(protocol.mock_wire_data.status_blobs[0]) handler_aggregate_status = first_status.get('aggregateStatus', {}).get("handlerAggregateStatus") self.assertIsNotNone(handler_aggregate_status, "Could not find the handlerAggregateStatus") self.assertEqual(1, len(handler_aggregate_status), "Expected 1 extension status. Got: {0}".format(handler_aggregate_status)) extension_status = handler_aggregate_status[0] self.assertEqual("OSTCExtensions.ExampleHandlerLinux", extension_status["handlerName"], "The status does not correspond to the test data") # # Verify that we reported the same status (minus timestamps) in the 2 iterations # second_status = json.loads(protocol.mock_wire_data.status_blobs[1]) def remove_timestamps(x): if isinstance(x, list): for v in x: remove_timestamps(v) elif isinstance(x, dict): for k, v in x.items(): if k == "timestampUTC": x[k] = '' else: remove_timestamps(v) remove_timestamps(first_status) remove_timestamps(second_status) self.assertEqual(first_status, second_status) def test_report_status_should_log_errors_only_once_per_goal_state(self): with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: with patch("azurelinuxagent.common.conf.get_autoupdate_enabled", return_value=False): # skip agent update with patch("azurelinuxagent.ga.update.logger.warn") as logger_warn: update_handler = get_update_handler() update_handler._goal_state = protocol.get_goal_state() # these tests skip the initialization of the goal state. so do that here exthandlers_handler = ExtHandlersHandler(protocol) update_handler._report_status(exthandlers_handler) self.assertEqual(0, logger_warn.call_count, "UpdateHandler._report_status() should not report WARNINGS when there are no errors") with patch("azurelinuxagent.ga.update.ExtensionsSummary.__init__", side_effect=Exception("TEST EXCEPTION")): # simulate an error during _report_status() get_warnings = lambda: [args[0] for args, _ in logger_warn.call_args_list if "TEST EXCEPTION" in args[0]] update_handler._report_status(exthandlers_handler) update_handler._report_status(exthandlers_handler) update_handler._report_status(exthandlers_handler) self.assertEqual(1, len(get_warnings()), "UpdateHandler._report_status() should report only 1 WARNING when there are multiple errors within the same goal state") exthandlers_handler.protocol.mock_wire_data.set_incarnation(999) update_handler._try_update_goal_state(exthandlers_handler.protocol) update_handler._report_status(exthandlers_handler) self.assertEqual(2, len(get_warnings()), "UpdateHandler._report_status() should continue reporting errors after a new goal state") def test_update_handler_should_add_fast_track_to_supported_features_when_it_is_supported(self): with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS) as protocol: self._test_supported_features_includes_fast_track(protocol, True) def test_update_handler_should_not_add_fast_track_to_supported_features_when_it_is_not_supported(self): def http_get_handler(url, *_, **__): if HttpRequestPredicates.is_host_plugin_vm_settings_request(url): return MockHttpResponse(status=404) return None with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS, http_get_handler=http_get_handler) as protocol: self._test_supported_features_includes_fast_track(protocol, False) def _test_supported_features_includes_fast_track(self, protocol, expected): with mock_update_handler(protocol) as update_handler: update_handler.run(debug=True) status = json.loads(protocol.mock_wire_data.status_blobs[0]) supported_features = status['supportedFeatures'] includes_fast_track = any(f['Key'] == 'FastTrack' for f in supported_features) self.assertEqual(expected, includes_fast_track, "supportedFeatures should {0}include FastTrack. Got: {1}".format("" if expected else "not ", supported_features)) WALinuxAgent-2.9.1.1/tests/ga/test_send_telemetry_events.py000066400000000000000000000562671446033677600240010ustar00rootroot00000000000000# Copyright 2020 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import contextlib import json import os import platform import re import tempfile import time import uuid from datetime import datetime, timedelta from mock import MagicMock, Mock, patch, PropertyMock from azurelinuxagent.common import logger from azurelinuxagent.common.datacontract import get_properties from azurelinuxagent.common.event import WALAEventOperation, EVENTS_DIRECTORY from azurelinuxagent.common.exception import HttpError, ServiceStoppedError from azurelinuxagent.common.future import ustr from azurelinuxagent.common.osutil.factory import get_osutil from azurelinuxagent.common.protocol.util import ProtocolUtil from azurelinuxagent.common.protocol.wire import event_to_v1_encoded from azurelinuxagent.common.telemetryevent import TelemetryEvent, TelemetryEventParam, \ GuestAgentExtensionEventsSchema from azurelinuxagent.common.utils import restutil, fileutil from azurelinuxagent.common.version import CURRENT_VERSION, DISTRO_NAME, DISTRO_VERSION, AGENT_VERSION, CURRENT_AGENT, \ DISTRO_CODE_NAME from azurelinuxagent.ga.collect_telemetry_events import _CollectAndEnqueueEvents from azurelinuxagent.ga.send_telemetry_events import get_send_telemetry_events_handler from tests.ga.test_monitor import random_generator from tests.protocol.mocks import MockHttpResponse, mock_wire_protocol from tests.protocol.HttpRequestPredicates import HttpRequestPredicates from tests.protocol.mockwiredata import DATA_FILE from tests.tools import AgentTestCase, clear_singleton_instances, mock_sleep from tests.utils.event_logger_tools import EventLoggerTools class TestSendTelemetryEventsHandler(AgentTestCase, HttpRequestPredicates): def setUp(self): AgentTestCase.setUp(self) clear_singleton_instances(ProtocolUtil) self.lib_dir = tempfile.mkdtemp() self.event_dir = os.path.join(self.lib_dir, EVENTS_DIRECTORY) EventLoggerTools.initialize_event_logger(self.event_dir) def tearDown(self): AgentTestCase.tearDown(self) fileutil.rm_dirs(self.lib_dir) _TEST_EVENT_PROVIDER_ID = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" _TEST_EVENT_OPERATION = "TEST_EVENT_OPERATION" @contextlib.contextmanager def _create_send_telemetry_events_handler(self, timeout=0.5, start_thread=True, batching_queue_limit=1): def http_post_handler(url, body, **__): if self.is_telemetry_request(url): send_telemetry_events_handler.event_calls.append((datetime.now(), body)) return MockHttpResponse(status=200) return None with mock_wire_protocol(DATA_FILE, http_post_handler=http_post_handler) as protocol: protocol_util = MagicMock() protocol_util.get_protocol = Mock(return_value=protocol) send_telemetry_events_handler = get_send_telemetry_events_handler(protocol_util) send_telemetry_events_handler.event_calls = [] with patch("azurelinuxagent.ga.send_telemetry_events.SendTelemetryEventsHandler._MIN_EVENTS_TO_BATCH", batching_queue_limit): with patch("azurelinuxagent.ga.send_telemetry_events.SendTelemetryEventsHandler._MAX_TIMEOUT", timeout): send_telemetry_events_handler.get_mock_wire_protocol = lambda: protocol if start_thread: send_telemetry_events_handler.start() self.assertTrue(send_telemetry_events_handler.is_alive(), "Thread didn't start properly!") yield send_telemetry_events_handler @staticmethod def _stop_handler(telemetry_handler, timeout=0.001): # Giving it some grace time to finish execution and then stopping thread time.sleep(timeout) telemetry_handler.stop() def _assert_test_data_in_event_body(self, telemetry_handler, test_events): # Stop the thread and Wait for the queue and thread to join TestSendTelemetryEventsHandler._stop_handler(telemetry_handler) for telemetry_event in test_events: event_str = event_to_v1_encoded(telemetry_event) found = False for _, event_body in telemetry_handler.event_calls: if event_str in event_body: found = True break self.assertTrue(found, "Event {0} not found in any telemetry calls".format(event_str)) def _assert_error_event_reported(self, mock_add_event, expected_msg, operation=WALAEventOperation.ReportEventErrors): found_msg = False for call_args in mock_add_event.call_args_list: _, kwargs = call_args if expected_msg in kwargs['message'] and kwargs['op'] == operation: found_msg = True break self.assertTrue(found_msg, "Error msg: {0} not reported".format(expected_msg)) def _setup_and_assert_bad_request_scenarios(self, http_post_handler, expected_msgs): with self._create_send_telemetry_events_handler() as telemetry_handler: telemetry_handler.get_mock_wire_protocol().set_http_handlers(http_post_handler=http_post_handler) with patch("azurelinuxagent.common.event.add_event") as mock_add_event: telemetry_handler.enqueue_event(TelemetryEvent()) TestSendTelemetryEventsHandler._stop_handler(telemetry_handler) for msg in expected_msgs: self._assert_error_event_reported(mock_add_event, msg) def test_it_should_send_events_properly(self): events = [TelemetryEvent(eventId=ustr(uuid.uuid4())), TelemetryEvent(eventId=ustr(uuid.uuid4()))] with self._create_send_telemetry_events_handler() as telemetry_handler: for test_event in events: telemetry_handler.enqueue_event(test_event) self._assert_test_data_in_event_body(telemetry_handler, events) def test_it_should_send_as_soon_as_events_available_in_queue_with_minimal_batching_limits(self): events = [TelemetryEvent(eventId=ustr(uuid.uuid4())), TelemetryEvent(eventId=ustr(uuid.uuid4()))] with self._create_send_telemetry_events_handler() as telemetry_handler: test_start_time = datetime.now() for test_event in events: telemetry_handler.enqueue_event(test_event) self._assert_test_data_in_event_body(telemetry_handler, events) # Ensure that we send out the data as soon as we enqueue the events for event_time, _ in telemetry_handler.event_calls: elapsed = event_time - test_start_time self.assertLessEqual(elapsed, timedelta(seconds=2), "Request was not sent as soon as possible") def test_thread_should_wait_for_events_to_get_in_queue_before_processing(self): events = [TelemetryEvent(eventId=ustr(uuid.uuid4())), TelemetryEvent(eventId=ustr(uuid.uuid4()))] with self._create_send_telemetry_events_handler(timeout=0.1) as telemetry_handler: # Do nothing for some time time.sleep(0.3) # Ensure that no events were transmitted by the telemetry handler during this time, i.e. telemetry thread was idle self.assertEqual(0, len(telemetry_handler.event_calls), "Unwanted calls to telemetry") # Now enqueue data and verify send_telemetry_events sends them asap for test_event in events: telemetry_handler.enqueue_event(test_event) self._assert_test_data_in_event_body(telemetry_handler, events) def test_it_should_honor_batch_time_limits_before_sending_telemetry(self): events = [TelemetryEvent(eventId=ustr(uuid.uuid4())), TelemetryEvent(eventId=ustr(uuid.uuid4()))] wait_time = timedelta(seconds=10) orig_sleep = time.sleep with patch("time.sleep", lambda *_: orig_sleep(0.01)): with patch("azurelinuxagent.ga.send_telemetry_events.SendTelemetryEventsHandler._MIN_BATCH_WAIT_TIME", wait_time): with self._create_send_telemetry_events_handler(batching_queue_limit=5) as telemetry_handler: for test_event in events: telemetry_handler.enqueue_event(test_event) self.assertEqual(0, len(telemetry_handler.event_calls), "No events should have been logged") TestSendTelemetryEventsHandler._stop_handler(telemetry_handler, timeout=0.01) wait_time = timedelta(seconds=0.2) with patch("time.sleep", lambda *_: orig_sleep(0.05)): with patch("azurelinuxagent.ga.send_telemetry_events.SendTelemetryEventsHandler._MIN_BATCH_WAIT_TIME", wait_time): with self._create_send_telemetry_events_handler(batching_queue_limit=5) as telemetry_handler: test_start_time = datetime.now() for test_event in events: telemetry_handler.enqueue_event(test_event) while not telemetry_handler.event_calls and (test_start_time + timedelta(seconds=1)) > datetime.now(): # Wait for event calls to be made, wait a max of 1 secs orig_sleep(0.1) self.assertGreater(len(telemetry_handler.event_calls), 0, "No event calls made at all!") self._assert_test_data_in_event_body(telemetry_handler, events) for event_time, _ in telemetry_handler.event_calls: elapsed = event_time - test_start_time # Technically we should send out data after 0.2 secs, but keeping a buffer of 1sec while testing self.assertLessEqual(elapsed, timedelta(seconds=1), "Request was not sent properly") def test_it_should_clear_queue_before_stopping(self): events = [TelemetryEvent(eventId=ustr(uuid.uuid4())), TelemetryEvent(eventId=ustr(uuid.uuid4()))] wait_time = timedelta(seconds=10) with patch("time.sleep", lambda *_: mock_sleep(0.01)): with patch("azurelinuxagent.ga.send_telemetry_events.SendTelemetryEventsHandler._MIN_BATCH_WAIT_TIME", wait_time): with self._create_send_telemetry_events_handler(batching_queue_limit=5) as telemetry_handler: for test_event in events: telemetry_handler.enqueue_event(test_event) self.assertEqual(0, len(telemetry_handler.event_calls), "No events should have been logged") TestSendTelemetryEventsHandler._stop_handler(telemetry_handler, timeout=0.01) # After the service is asked to stop, we should send all data in the queue self._assert_test_data_in_event_body(telemetry_handler, events) def test_it_should_honor_batch_queue_limits_before_sending_telemetry(self): batch_limit = 5 with self._create_send_telemetry_events_handler(batching_queue_limit=batch_limit) as telemetry_handler: events = [] for _ in range(batch_limit-1): test_event = TelemetryEvent(eventId=ustr(uuid.uuid4())) events.append(test_event) telemetry_handler.enqueue_event(test_event) self.assertEqual(0, len(telemetry_handler.event_calls), "No events should have been logged") for _ in range(batch_limit): test_event = TelemetryEvent(eventId=ustr(uuid.uuid4())) events.append(test_event) telemetry_handler.enqueue_event(test_event) self._assert_test_data_in_event_body(telemetry_handler, events) def test_it_should_raise_on_enqueue_if_service_stopped(self): with self._create_send_telemetry_events_handler(start_thread=False) as telemetry_handler: # Ensure the thread is stopped telemetry_handler.stop() with self.assertRaises(ServiceStoppedError) as context_manager: telemetry_handler.enqueue_event(TelemetryEvent(eventId=ustr(uuid.uuid4()))) exception = context_manager.exception self.assertIn("{0} is stopped, not accepting anymore events".format(telemetry_handler.get_thread_name()), str(exception)) def test_it_should_honour_the_incoming_order_of_events(self): with self._create_send_telemetry_events_handler(timeout=0.3, start_thread=False) as telemetry_handler: for index in range(5): telemetry_handler.enqueue_event(TelemetryEvent(eventId=index)) telemetry_handler.start() self.assertTrue(telemetry_handler.is_alive(), "Thread not alive") TestSendTelemetryEventsHandler._stop_handler(telemetry_handler) _, event_body = telemetry_handler.event_calls[0] event_orders = re.findall(r'', event_body.decode('utf-8')) self.assertEqual(sorted(event_orders), event_orders, "Events not ordered correctly") def test_send_telemetry_events_should_report_event_if_wireserver_returns_http_error(self): test_str = "A test exception, Guid: {0}".format(str(uuid.uuid4())) def http_post_handler(url, _, **__): if self.is_telemetry_request(url): return HttpError(test_str) return None self._setup_and_assert_bad_request_scenarios(http_post_handler, [test_str]) def test_send_telemetry_events_should_report_event_when_http_post_returning_503(self): def http_post_handler(url, _, **__): if self.is_telemetry_request(url): return MockHttpResponse(restutil.httpclient.SERVICE_UNAVAILABLE) return None expected_msgs = ["[ProtocolError] [Wireserver Exception] [ProtocolError] [Wireserver Failed]", "[HTTP Failed] Status Code 503"] self._setup_and_assert_bad_request_scenarios(http_post_handler, expected_msgs) def test_send_telemetry_events_should_add_event_on_unexpected_errors(self): with self._create_send_telemetry_events_handler(timeout=0.1) as telemetry_handler: with patch("azurelinuxagent.ga.send_telemetry_events.add_event") as mock_add_event: with patch("azurelinuxagent.common.protocol.wire.WireClient.report_event") as patch_report_event: test_str = "Test exception, Guid: {0}".format(str(uuid.uuid4())) patch_report_event.side_effect = Exception(test_str) telemetry_handler.enqueue_event(TelemetryEvent()) TestSendTelemetryEventsHandler._stop_handler(telemetry_handler, timeout=0.01) self._assert_error_event_reported(mock_add_event, test_str, operation=WALAEventOperation.UnhandledError) def _create_extension_event(self, size=0, name="DummyExtension", message="DummyMessage"): event_data = self._get_event_data(name=size if size != 0 else name, message=random_generator(size) if size != 0 else message) event_file = os.path.join(self.event_dir, "{0}.tld".format(int(time.time() * 1000000))) with open(event_file, 'wb+') as file_descriptor: file_descriptor.write(event_data.encode('utf-8')) @staticmethod def _get_event_data(message, name): event = TelemetryEvent(1, TestSendTelemetryEventsHandler._TEST_EVENT_PROVIDER_ID) event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.Name, name)) event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.Version, str(CURRENT_VERSION))) event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.Operation, TestSendTelemetryEventsHandler._TEST_EVENT_OPERATION)) event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.OperationSuccess, True)) event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.Message, message)) event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.Duration, 0)) data = get_properties(event) return json.dumps(data) @patch("azurelinuxagent.common.event.TELEMETRY_EVENT_PROVIDER_ID", _TEST_EVENT_PROVIDER_ID) @patch("azurelinuxagent.common.conf.get_lib_dir") def test_it_should_enqueue_and_send_events_properly(self, mock_lib_dir, *_): mock_lib_dir.return_value = self.lib_dir with self._create_send_telemetry_events_handler() as telemetry_handler: monitor_handler = _CollectAndEnqueueEvents(telemetry_handler) self._create_extension_event(message="Message-Test") test_mtime = 1000 # epoch time, in ms test_opcodename = datetime.fromtimestamp(test_mtime).strftime(logger.Logger.LogTimeFormatInUTC) test_eventtid = 42 test_eventpid = 24 test_taskname = "TEST_TaskName" with patch("os.path.getmtime", return_value=test_mtime): with patch('os.getpid', return_value=test_eventpid): with patch("threading.Thread.ident", new_callable=PropertyMock(return_value=test_eventtid)): with patch("threading.Thread.getName", return_value=test_taskname): monitor_handler.run() TestSendTelemetryEventsHandler._stop_handler(telemetry_handler) # Validating the crafted message by the collect_and_send_events call. extension_events = self._get_extension_events(telemetry_handler) self.assertEqual(1, len(extension_events), "Only 1 event should be sent") collected_event = extension_events[0] # Some of those expected values come from the mock protocol and imds client set up during test initialization osutil = get_osutil() osversion = u"{0}:{1}-{2}-{3}:{4}".format(platform.system(), DISTRO_NAME, DISTRO_VERSION, DISTRO_CODE_NAME, platform.release()) sample_message = '' \ '' \ '' \ '' \ '' \ '' \ '' \ '' \ '' \ '' \ '' \ '' \ '' \ '' \ '' \ '' \ '' \ '' \ '' \ '' \ '' \ '' \ '' \ '' \ '' \ '' \ '' \ ']]>'.format(AGENT_VERSION, TestSendTelemetryEventsHandler._TEST_EVENT_OPERATION, CURRENT_AGENT, test_opcodename, test_eventtid, test_eventpid, test_taskname, osversion, int(osutil.get_total_mem()), osutil.get_processor_cores()).encode('utf-8') self.assertIn(sample_message, collected_event) @patch("azurelinuxagent.common.conf.get_lib_dir") def test_collect_and_send_events_with_small_events(self, mock_lib_dir): mock_lib_dir.return_value = self.lib_dir with self._create_send_telemetry_events_handler() as telemetry_handler: sizes = [15, 15, 15, 15] # get the powers of 2 - 2**16 is the limit for power in sizes: size = 2 ** power self._create_extension_event(size) _CollectAndEnqueueEvents(telemetry_handler).run() # The send_event call would be called each time, as we are filling up the buffer up to the brim for each call. TestSendTelemetryEventsHandler._stop_handler(telemetry_handler) self.assertEqual(4, len(self._get_extension_events(telemetry_handler))) @patch("azurelinuxagent.common.conf.get_lib_dir") def test_collect_and_send_events_with_large_events(self, mock_lib_dir): mock_lib_dir.return_value = self.lib_dir with self._create_send_telemetry_events_handler() as telemetry_handler: sizes = [17, 17, 17] # get the powers of 2 for power in sizes: size = 2 ** power self._create_extension_event(size) with patch("azurelinuxagent.common.logger.periodic_warn") as patch_periodic_warn: _CollectAndEnqueueEvents(telemetry_handler).run() TestSendTelemetryEventsHandler._stop_handler(telemetry_handler) self.assertEqual(3, patch_periodic_warn.call_count) # The send_event call should never be called as the events are larger than 2**16. self.assertEqual(0, len(self._get_extension_events(telemetry_handler))) @staticmethod def _get_extension_events(telemetry_handler): return [event_xml for _, event_xml in telemetry_handler.event_calls if TestSendTelemetryEventsHandler._TEST_EVENT_OPERATION in event_xml.decode()] WALinuxAgent-2.9.1.1/tests/ga/test_update.py000066400000000000000000004625731446033677600206550ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the Apache License. from __future__ import print_function import contextlib import glob import json import os import re import shutil import stat import subprocess import sys import tempfile import time import unittest import uuid import zipfile from datetime import datetime, timedelta from threading import current_thread from tests.common.osutil.test_default import TestOSUtil import azurelinuxagent.common.osutil.default as osutil _ORIGINAL_POPEN = subprocess.Popen from azurelinuxagent.common import conf from azurelinuxagent.common.event import EVENTS_DIRECTORY, WALAEventOperation from azurelinuxagent.common.exception import ProtocolError, UpdateError, HttpError, \ ExitException, AgentMemoryExceededException from azurelinuxagent.common.future import ustr, httpclient from azurelinuxagent.common.persist_firewall_rules import PersistFirewallRulesHandler from azurelinuxagent.common.protocol.hostplugin import HostPluginProtocol from azurelinuxagent.common.protocol.restapi import VMAgentFamily, \ ExtHandlerPackage, ExtHandlerPackageList, Extension, VMStatus, ExtHandlerStatus, ExtensionStatus, \ VMAgentUpdateStatuses from azurelinuxagent.common.protocol.util import ProtocolUtil from azurelinuxagent.common.utils import fileutil, textutil, timeutil from azurelinuxagent.common.utils.archive import ARCHIVE_DIRECTORY_NAME, AGENT_STATUS_FILE from azurelinuxagent.common.utils.flexible_version import FlexibleVersion from azurelinuxagent.common.utils.networkutil import FirewallCmdDirectCommands, AddFirewallRules from azurelinuxagent.common.version import AGENT_PKG_GLOB, AGENT_DIR_GLOB, AGENT_NAME, AGENT_DIR_PATTERN, \ AGENT_VERSION, CURRENT_AGENT, CURRENT_VERSION, set_daemon_version, \ __DAEMON_VERSION_ENV_VARIABLE as DAEMON_VERSION_ENV_VARIABLE from azurelinuxagent.ga.exthandlers import ExtHandlersHandler, ExtHandlerInstance, HandlerEnvironment, ExtensionStatusValue from azurelinuxagent.ga.update import GuestAgent, GuestAgentError, MAX_FAILURE, AGENT_MANIFEST_FILE, \ get_update_handler, ORPHAN_POLL_INTERVAL, AGENT_PARTITION_FILE, AGENT_ERROR_FILE, ORPHAN_WAIT_INTERVAL, \ CHILD_LAUNCH_RESTART_MAX, CHILD_HEALTH_INTERVAL, GOAL_STATE_PERIOD_EXTENSIONS_DISABLED, UpdateHandler, \ READONLY_FILE_GLOBS, ExtensionsSummary, AgentUpgradeType from tests.ga.mocks import mock_update_handler from tests.protocol.mocks import mock_wire_protocol, MockHttpResponse from tests.protocol.mockwiredata import DATA_FILE, DATA_FILE_MULTIPLE_EXT, DATA_FILE_VM_SETTINGS from tests.tools import AgentTestCase, AgentTestCaseWithGetVmSizeMock, data_dir, DEFAULT, patch, load_bin_data, Mock, MagicMock, \ clear_singleton_instances, is_python_version_26_or_34, skip_if_predicate_true from tests.protocol import mockwiredata from tests.protocol.HttpRequestPredicates import HttpRequestPredicates NO_ERROR = { "last_failure": 0.0, "failure_count": 0, "was_fatal": False, "reason": '' } FATAL_ERROR = { "last_failure": 42.42, "failure_count": 2, "was_fatal": True, "reason": "Test failure" } WITH_ERROR = { "last_failure": 42.42, "failure_count": 2, "was_fatal": False, "reason": "Test failure" } EMPTY_MANIFEST = { "name": "WALinuxAgent", "version": 1.0, "handlerManifest": { "installCommand": "", "uninstallCommand": "", "updateCommand": "", "enableCommand": "", "disableCommand": "", "rebootAfterInstall": False, "reportHeartbeat": False } } def faux_logger(): print("STDOUT message") print("STDERR message", file=sys.stderr) return DEFAULT @contextlib.contextmanager def _get_update_handler(iterations=1, test_data=None, protocol=None): """ This function returns a mocked version of the UpdateHandler object to be used for testing. It will only run the main loop [iterations] no of times. """ test_data = DATA_FILE if test_data is None else test_data with patch.object(HostPluginProtocol, "is_default_channel", False): if protocol is None: with mock_wire_protocol(test_data) as mock_protocol: with mock_update_handler(mock_protocol, iterations=iterations, autoupdate_enabled=True) as update_handler: yield update_handler, mock_protocol else: with mock_update_handler(protocol, iterations=iterations, autoupdate_enabled=True) as update_handler: yield update_handler, protocol class UpdateTestCase(AgentTestCaseWithGetVmSizeMock): _test_suite_tmp_dir = None _agent_zip_dir = None @classmethod def setUpClass(cls): super(UpdateTestCase, cls).setUpClass() # copy data_dir/ga/WALinuxAgent-0.0.0.0.zip to _test_suite_tmp_dir/waagent-zip/WALinuxAgent-.zip sample_agent_zip = "WALinuxAgent-0.0.0.0.zip" test_agent_zip = sample_agent_zip.replace("0.0.0.0", AGENT_VERSION) UpdateTestCase._test_suite_tmp_dir = tempfile.mkdtemp() UpdateTestCase._agent_zip_dir = os.path.join(UpdateTestCase._test_suite_tmp_dir, "waagent-zip") os.mkdir(UpdateTestCase._agent_zip_dir) source = os.path.join(data_dir, "ga", sample_agent_zip) target = os.path.join(UpdateTestCase._agent_zip_dir, test_agent_zip) shutil.copyfile(source, target) @classmethod def tearDownClass(cls): super(UpdateTestCase, cls).tearDownClass() shutil.rmtree(UpdateTestCase._test_suite_tmp_dir) @staticmethod def _get_agent_pkgs(in_dir=None): if in_dir is None: in_dir = UpdateTestCase._agent_zip_dir path = os.path.join(in_dir, AGENT_PKG_GLOB) return glob.glob(path) @staticmethod def _get_agents(in_dir=None): if in_dir is None: in_dir = UpdateTestCase._agent_zip_dir path = os.path.join(in_dir, AGENT_DIR_GLOB) return [a for a in glob.glob(path) if os.path.isdir(a)] @staticmethod def _get_agent_file_path(): return UpdateTestCase._get_agent_pkgs()[0] @staticmethod def _get_agent_file_name(): return os.path.basename(UpdateTestCase._get_agent_file_path()) @staticmethod def _get_agent_path(): return fileutil.trim_ext(UpdateTestCase._get_agent_file_path(), "zip") @staticmethod def _get_agent_name(): return os.path.basename(UpdateTestCase._get_agent_path()) @staticmethod def _get_agent_version(): return FlexibleVersion(UpdateTestCase._get_agent_name().split("-")[1]) @staticmethod def _add_write_permission_to_goal_state_files(): # UpdateHandler.run() marks some of the files from the goal state as read-only. Those files are overwritten when # a new goal state is fetched. This is not a problem for the agent, since it runs as root, but tests need # to make those files writtable before fetching a new goal state. Note that UpdateHandler.run() fetches a new # goal state, so tests that make multiple calls to that method need to call this function in-between calls. for gb in READONLY_FILE_GLOBS: for path in glob.iglob(os.path.join(conf.get_lib_dir(), gb)): fileutil.chmod(path, stat.S_IRUSR | stat.S_IWUSR) def agent_bin(self, version, suffix): return "bin/{0}-{1}{2}.egg".format(AGENT_NAME, version, suffix) def rename_agent_bin(self, path, dst_v): src_bin = glob.glob(os.path.join(path, self.agent_bin("*.*.*.*", '*')))[0] dst_bin = os.path.join(path, self.agent_bin(dst_v, '')) shutil.move(src_bin, dst_bin) def agents(self): return [GuestAgent.from_installed_agent(path) for path in self.agent_dirs()] def agent_count(self): return len(self.agent_dirs()) def agent_dirs(self): return self._get_agents(in_dir=self.tmp_dir) def agent_dir(self, version): return os.path.join(self.tmp_dir, "{0}-{1}".format(AGENT_NAME, version)) def agent_paths(self): paths = glob.glob(os.path.join(self.tmp_dir, "*")) paths.sort() return paths def agent_pkgs(self): return self._get_agent_pkgs(in_dir=self.tmp_dir) def agent_versions(self): v = [FlexibleVersion(AGENT_DIR_PATTERN.match(a).group(1)) for a in self.agent_dirs()] v.sort(reverse=True) return v @contextlib.contextmanager def get_error_file(self, error_data=None): if error_data is None: error_data = NO_ERROR with tempfile.NamedTemporaryFile(mode="w") as fp: json.dump(error_data if error_data is not None else NO_ERROR, fp) fp.seek(0) yield fp def create_error(self, error_data=None): if error_data is None: error_data = NO_ERROR with self.get_error_file(error_data) as path: err = GuestAgentError(path.name) err.load() return err def copy_agents(self, *agents): if len(agents) <= 0: agents = self._get_agent_pkgs() for agent in agents: shutil.copy(agent, self.tmp_dir) return def expand_agents(self): for agent in self.agent_pkgs(): path = os.path.join(self.tmp_dir, fileutil.trim_ext(agent, "zip")) zipfile.ZipFile(agent).extractall(path) def prepare_agent(self, version): """ Create a download for the current agent version, copied from test data """ self.copy_agents(self._get_agent_pkgs()[0]) self.expand_agents() versions = self.agent_versions() src_v = FlexibleVersion(str(versions[0])) from_path = self.agent_dir(src_v) dst_v = FlexibleVersion(str(version)) to_path = self.agent_dir(dst_v) if from_path != to_path: shutil.move(from_path + ".zip", to_path + ".zip") shutil.move(from_path, to_path) self.rename_agent_bin(to_path, dst_v) return def prepare_agents(self, count=20, is_available=True): # Ensure the test data is copied over agent_count = self.agent_count() if agent_count <= 0: self.copy_agents(self._get_agent_pkgs()[0]) self.expand_agents() count -= 1 # Determine the most recent agent version versions = self.agent_versions() src_v = FlexibleVersion(str(versions[0])) # Create agent packages and directories return self.replicate_agents( src_v=src_v, count=count - agent_count, is_available=is_available) def remove_agents(self): for agent in self.agent_paths(): try: if os.path.isfile(agent): os.remove(agent) else: shutil.rmtree(agent) except: # pylint: disable=bare-except pass return def replicate_agents(self, count=5, src_v=AGENT_VERSION, is_available=True, increment=1): from_path = self.agent_dir(src_v) dst_v = FlexibleVersion(str(src_v)) for i in range(0, count): # pylint: disable=unused-variable dst_v += increment to_path = self.agent_dir(dst_v) shutil.copyfile(from_path + ".zip", to_path + ".zip") shutil.copytree(from_path, to_path) self.rename_agent_bin(to_path, dst_v) if not is_available: GuestAgent.from_installed_agent(to_path).mark_failure(is_fatal=True) return dst_v class TestGuestAgentError(UpdateTestCase): def test_creation(self): self.assertRaises(TypeError, GuestAgentError) self.assertRaises(UpdateError, GuestAgentError, None) with self.get_error_file(error_data=WITH_ERROR) as path: err = GuestAgentError(path.name) err.load() self.assertEqual(path.name, err.path) self.assertNotEqual(None, err) self.assertEqual(WITH_ERROR["last_failure"], err.last_failure) self.assertEqual(WITH_ERROR["failure_count"], err.failure_count) self.assertEqual(WITH_ERROR["was_fatal"], err.was_fatal) return def test_clear(self): with self.get_error_file(error_data=WITH_ERROR) as path: err = GuestAgentError(path.name) err.load() self.assertEqual(path.name, err.path) self.assertNotEqual(None, err) err.clear() self.assertEqual(NO_ERROR["last_failure"], err.last_failure) self.assertEqual(NO_ERROR["failure_count"], err.failure_count) self.assertEqual(NO_ERROR["was_fatal"], err.was_fatal) return def test_save(self): err1 = self.create_error() err1.mark_failure() err1.mark_failure(is_fatal=True) err2 = self.create_error(err1.to_json()) self.assertEqual(err1.last_failure, err2.last_failure) self.assertEqual(err1.failure_count, err2.failure_count) self.assertEqual(err1.was_fatal, err2.was_fatal) def test_mark_failure(self): err = self.create_error() self.assertFalse(err.is_blacklisted) for i in range(0, MAX_FAILURE): # pylint: disable=unused-variable err.mark_failure() # Agent failed >= MAX_FAILURE, it should be blacklisted self.assertTrue(err.is_blacklisted) self.assertEqual(MAX_FAILURE, err.failure_count) return def test_mark_failure_permanent(self): err = self.create_error() self.assertFalse(err.is_blacklisted) # Fatal errors immediately blacklist err.mark_failure(is_fatal=True) self.assertTrue(err.is_blacklisted) self.assertTrue(err.failure_count < MAX_FAILURE) return def test_str(self): err = self.create_error(error_data=NO_ERROR) s = "Last Failure: {0}, Total Failures: {1}, Fatal: {2}, Reason: {3}".format( NO_ERROR["last_failure"], NO_ERROR["failure_count"], NO_ERROR["was_fatal"], NO_ERROR["reason"]) self.assertEqual(s, str(err)) err = self.create_error(error_data=WITH_ERROR) s = "Last Failure: {0}, Total Failures: {1}, Fatal: {2}, Reason: {3}".format( WITH_ERROR["last_failure"], WITH_ERROR["failure_count"], WITH_ERROR["was_fatal"], WITH_ERROR["reason"]) self.assertEqual(s, str(err)) return class TestGuestAgent(UpdateTestCase): def setUp(self): UpdateTestCase.setUp(self) self.copy_agents(self._get_agent_file_path()) self.agent_path = os.path.join(self.tmp_dir, self._get_agent_name()) def test_creation(self): with self.assertRaises(UpdateError): GuestAgent.from_installed_agent("A very bad file name") with self.assertRaises(UpdateError): GuestAgent.from_installed_agent("{0}-a.bad.version".format(AGENT_NAME)) self.expand_agents() agent = GuestAgent.from_installed_agent(self.agent_path) self.assertNotEqual(None, agent) self.assertEqual(self._get_agent_name(), agent.name) self.assertEqual(self._get_agent_version(), agent.version) self.assertEqual(self.agent_path, agent.get_agent_dir()) path = os.path.join(self.agent_path, AGENT_MANIFEST_FILE) self.assertEqual(path, agent.get_agent_manifest_path()) self.assertEqual( os.path.join(self.agent_path, AGENT_ERROR_FILE), agent.get_agent_error_file()) path = ".".join((os.path.join(conf.get_lib_dir(), self._get_agent_name()), "zip")) self.assertEqual(path, agent.get_agent_pkg_path()) self.assertTrue(agent.is_downloaded) self.assertFalse(agent.is_blacklisted) self.assertTrue(agent.is_available) def test_clear_error(self): self.expand_agents() agent = GuestAgent.from_installed_agent(self.agent_path) agent.mark_failure(is_fatal=True) self.assertTrue(agent.error.last_failure > 0.0) self.assertEqual(1, agent.error.failure_count) self.assertTrue(agent.is_blacklisted) self.assertEqual(agent.is_blacklisted, agent.error.is_blacklisted) agent.clear_error() self.assertEqual(0.0, agent.error.last_failure) self.assertEqual(0, agent.error.failure_count) self.assertFalse(agent.is_blacklisted) self.assertEqual(agent.is_blacklisted, agent.error.is_blacklisted) def test_is_available(self): self.expand_agents() agent = GuestAgent.from_installed_agent(self.agent_path) self.assertTrue(agent.is_available) agent.mark_failure(is_fatal=True) self.assertFalse(agent.is_available) def test_is_blacklisted(self): self.expand_agents() agent = GuestAgent.from_installed_agent(self.agent_path) self.assertFalse(agent.is_blacklisted) self.assertEqual(agent.is_blacklisted, agent.error.is_blacklisted) agent.mark_failure(is_fatal=True) self.assertTrue(agent.is_blacklisted) self.assertEqual(agent.is_blacklisted, agent.error.is_blacklisted) def test_is_downloaded(self): self.expand_agents() agent = GuestAgent.from_installed_agent(self.agent_path) self.assertTrue(agent.is_downloaded) def test_mark_failure(self): agent = GuestAgent.from_installed_agent(self.agent_path) agent.mark_failure() self.assertEqual(1, agent.error.failure_count) agent.mark_failure(is_fatal=True) self.assertEqual(2, agent.error.failure_count) self.assertTrue(agent.is_blacklisted) def test_load_manifest(self): self.expand_agents() agent = GuestAgent.from_installed_agent(self.agent_path) agent._load_manifest() self.assertEqual(agent.manifest.get_enable_command(), agent.get_agent_cmd()) def test_load_manifest_missing(self): self.expand_agents() agent = GuestAgent.from_installed_agent(self.agent_path) os.remove(agent.get_agent_manifest_path()) self.assertRaises(UpdateError, agent._load_manifest) def test_load_manifest_is_empty(self): self.expand_agents() agent = GuestAgent.from_installed_agent(self.agent_path) self.assertTrue(os.path.isfile(agent.get_agent_manifest_path())) with open(agent.get_agent_manifest_path(), "w") as file: # pylint: disable=redefined-builtin json.dump(EMPTY_MANIFEST, file) self.assertRaises(UpdateError, agent._load_manifest) def test_load_manifest_is_malformed(self): self.expand_agents() agent = GuestAgent.from_installed_agent(self.agent_path) self.assertTrue(os.path.isfile(agent.get_agent_manifest_path())) with open(agent.get_agent_manifest_path(), "w") as file: # pylint: disable=redefined-builtin file.write("This is not JSON data") self.assertRaises(UpdateError, agent._load_manifest) def test_load_error(self): agent = GuestAgent.from_installed_agent(self.agent_path) agent.error = None agent._load_error() self.assertTrue(agent.error is not None) def test_download(self): self.remove_agents() self.assertFalse(os.path.isdir(self.agent_path)) agent_uri = 'https://foo.blob.core.windows.net/bar/OSTCExtensions.WALinuxAgent__1.0.0' def http_get_handler(uri, *_, **__): if uri == agent_uri: response = load_bin_data(self._get_agent_file_name(), self._agent_zip_dir) return MockHttpResponse(status=httpclient.OK, body=response) return None pkg = ExtHandlerPackage(version=str(self._get_agent_version())) pkg.uris.append(agent_uri) with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: protocol.set_http_handlers(http_get_handler=http_get_handler) agent = GuestAgent.from_agent_package(pkg, protocol, False) self.assertTrue(os.path.isdir(agent.get_agent_dir())) self.assertTrue(agent.is_downloaded) def test_download_fail(self): self.remove_agents() self.assertFalse(os.path.isdir(self.agent_path)) agent_uri = 'https://foo.blob.core.windows.net/bar/OSTCExtensions.WALinuxAgent__1.0.0' def http_get_handler(uri, *_, **__): if uri in (agent_uri, 'http://168.63.129.16:32526/extensionArtifact'): return MockHttpResponse(status=httpclient.SERVICE_UNAVAILABLE) return None agent_version = self._get_agent_version() pkg = ExtHandlerPackage(version=str(agent_version)) pkg.uris.append(agent_uri) with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: protocol.set_http_handlers(http_get_handler=http_get_handler) with patch("azurelinuxagent.ga.update.add_event") as add_event: agent = GuestAgent.from_agent_package(pkg, protocol, False) self.assertFalse(os.path.isfile(self.agent_path)) messages = [kwargs['message'] for _, kwargs in add_event.call_args_list if kwargs['op'] == 'Install' and kwargs['is_success'] == False] self.assertEqual(1, len(messages), "Expected exactly 1 install error/ Got: {0}".format(add_event.call_args_list)) self.assertIn(str.format('[UpdateError] Unable to download Agent WALinuxAgent-{0}', agent_version), messages[0], "The install error does not include the expected message") self.assertFalse(agent.is_blacklisted, "Download failures should not blacklist the Agent") def test_invalid_agent_package_does_not_blacklist_the_agent(self): agent_uri = 'https://foo.blob.core.windows.net/bar/OSTCExtensions.WALinuxAgent__9.9.9.9' def http_get_handler(uri, *_, **__): if uri in (agent_uri, 'http://168.63.129.16:32526/extensionArtifact'): response = load_bin_data("ga/WALinuxAgent-9.9.9.9-no_manifest.zip") return MockHttpResponse(status=httpclient.OK, body=response) return None pkg = ExtHandlerPackage(version="9.9.9.9") pkg.uris.append(agent_uri) with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: protocol.set_http_handlers(http_get_handler=http_get_handler) agent = GuestAgent.from_agent_package(pkg, protocol, False) self.assertFalse(agent.is_blacklisted, "The agent should not be blacklisted if unable to unpack/download") self.assertFalse(os.path.exists(agent.get_agent_dir()), "Agent directory should be cleaned up") @patch("azurelinuxagent.ga.update.GuestAgent._download") def test_ensure_download_skips_blacklisted(self, mock_download): agent = GuestAgent.from_installed_agent(self.agent_path) self.assertEqual(0, mock_download.call_count) agent.clear_error() agent.mark_failure(is_fatal=True) self.assertTrue(agent.is_blacklisted) pkg = ExtHandlerPackage(version=str(self._get_agent_version())) pkg.uris.append(None) # _download is mocked so there will be no http request; passing a None protocol agent = GuestAgent.from_agent_package(pkg, None, False) self.assertEqual(1, agent.error.failure_count) self.assertTrue(agent.error.was_fatal) self.assertTrue(agent.is_blacklisted) self.assertEqual(0, mock_download.call_count) class TestUpdate(UpdateTestCase): def setUp(self): UpdateTestCase.setUp(self) self.event_patch = patch('azurelinuxagent.common.event.add_event') self.update_handler = get_update_handler() protocol = Mock() self.update_handler.protocol_util = Mock() self.update_handler.protocol_util.get_protocol = Mock(return_value=protocol) self.update_handler._goal_state = Mock() self.update_handler._goal_state.extensions_goal_state = Mock() self.update_handler._goal_state.extensions_goal_state.source = "Fabric" # Since ProtocolUtil is a singleton per thread, we need to clear it to ensure that the test cases do not reuse # a previous state clear_singleton_instances(ProtocolUtil) def test_creation(self): self.assertEqual(None, self.update_handler.last_attempt_time) self.assertEqual(0, len(self.update_handler.agents)) self.assertEqual(None, self.update_handler.child_agent) self.assertEqual(None, self.update_handler.child_launch_time) self.assertEqual(0, self.update_handler.child_launch_attempts) self.assertEqual(None, self.update_handler.child_process) self.assertEqual(None, self.update_handler.signal_handler) def test_emit_restart_event_emits_event_if_not_clean_start(self): try: mock_event = self.event_patch.start() self.update_handler._set_sentinel() self.update_handler._emit_restart_event() self.assertEqual(1, mock_event.call_count) except Exception as e: # pylint: disable=unused-variable pass self.event_patch.stop() def _create_protocol(self, count=20, versions=None): latest_version = self.prepare_agents(count=count) if versions is None or len(versions) <= 0: versions = [latest_version] return ProtocolMock(versions=versions) def _test_ensure_no_orphans(self, invocations=3, interval=ORPHAN_WAIT_INTERVAL, pid_count=0): with patch.object(self.update_handler, 'osutil') as mock_util: # Note: # - Python only allows mutations of objects to which a function has # a reference. Incrementing an integer directly changes the # reference. Incrementing an item of a list changes an item to # which the code has a reference. # See http://stackoverflow.com/questions/26408941/python-nested-functions-and-variable-scope iterations = [0] def iterator(*args, **kwargs): # pylint: disable=unused-argument iterations[0] += 1 return iterations[0] < invocations mock_util.check_pid_alive = Mock(side_effect=iterator) pid_files = self.update_handler._get_pid_files() self.assertEqual(pid_count, len(pid_files)) with patch('os.getpid', return_value=42): with patch('time.sleep', return_value=None) as mock_sleep: # pylint: disable=redefined-outer-name self.update_handler._ensure_no_orphans(orphan_wait_interval=interval) for pid_file in pid_files: self.assertFalse(os.path.exists(pid_file)) return mock_util.check_pid_alive.call_count, mock_sleep.call_count def test_ensure_no_orphans(self): fileutil.write_file(os.path.join(self.tmp_dir, "0_waagent.pid"), ustr(41)) calls, sleeps = self._test_ensure_no_orphans(invocations=3, pid_count=1) self.assertEqual(3, calls) self.assertEqual(2, sleeps) def test_ensure_no_orphans_skips_if_no_orphans(self): calls, sleeps = self._test_ensure_no_orphans(invocations=3) self.assertEqual(0, calls) self.assertEqual(0, sleeps) def test_ensure_no_orphans_ignores_exceptions(self): with patch('azurelinuxagent.common.utils.fileutil.read_file', side_effect=Exception): calls, sleeps = self._test_ensure_no_orphans(invocations=3) self.assertEqual(0, calls) self.assertEqual(0, sleeps) def test_ensure_no_orphans_kills_after_interval(self): fileutil.write_file(os.path.join(self.tmp_dir, "0_waagent.pid"), ustr(41)) with patch('os.kill') as mock_kill: calls, sleeps = self._test_ensure_no_orphans( invocations=4, interval=3 * ORPHAN_POLL_INTERVAL, pid_count=1) self.assertEqual(3, calls) self.assertEqual(2, sleeps) self.assertEqual(1, mock_kill.call_count) @patch('azurelinuxagent.ga.update.datetime') def test_ensure_partition_assigned(self, mock_time): path = os.path.join(conf.get_lib_dir(), AGENT_PARTITION_FILE) mock_time.utcnow = Mock() self.assertFalse(os.path.exists(path)) for n in range(0, 99): mock_time.utcnow.return_value = Mock(microsecond=n * 10000) self.update_handler._ensure_partition_assigned() self.assertTrue(os.path.exists(path)) s = fileutil.read_file(path) self.assertEqual(n, int(s)) os.remove(path) def test_ensure_readonly_sets_readonly(self): test_files = [ os.path.join(conf.get_lib_dir(), "faux_certificate.crt"), os.path.join(conf.get_lib_dir(), "faux_certificate.p7m"), os.path.join(conf.get_lib_dir(), "faux_certificate.pem"), os.path.join(conf.get_lib_dir(), "faux_certificate.prv"), os.path.join(conf.get_lib_dir(), "ovf-env.xml") ] for path in test_files: fileutil.write_file(path, "Faux content") os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH) self.update_handler._ensure_readonly_files() for path in test_files: mode = os.stat(path).st_mode mode &= (stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) self.assertEqual(0, mode ^ stat.S_IRUSR) def test_ensure_readonly_leaves_unmodified(self): test_files = [ os.path.join(conf.get_lib_dir(), "faux.xml"), os.path.join(conf.get_lib_dir(), "faux.json"), os.path.join(conf.get_lib_dir(), "faux.txt"), os.path.join(conf.get_lib_dir(), "faux") ] for path in test_files: fileutil.write_file(path, "Faux content") os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH) self.update_handler._ensure_readonly_files() for path in test_files: mode = os.stat(path).st_mode mode &= (stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) self.assertEqual( stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH, mode) def _test_evaluate_agent_health(self, child_agent_index=0): self.prepare_agents() latest_agent = self.update_handler.get_latest_agent_greater_than_daemon() self.assertTrue(latest_agent.is_available) self.assertFalse(latest_agent.is_blacklisted) self.assertTrue(len(self.update_handler.agents) > 1) child_agent = self.update_handler.agents[child_agent_index] self.assertTrue(child_agent.is_available) self.assertFalse(child_agent.is_blacklisted) self.update_handler.child_agent = child_agent self.update_handler._evaluate_agent_health(latest_agent) def test_evaluate_agent_health_ignores_installed_agent(self): self.update_handler._evaluate_agent_health(None) def test_evaluate_agent_health_raises_exception_for_restarting_agent(self): self.update_handler.child_launch_time = time.time() - (4 * 60) self.update_handler.child_launch_attempts = CHILD_LAUNCH_RESTART_MAX - 1 self.assertRaises(Exception, self._test_evaluate_agent_health) def test_evaluate_agent_health_will_not_raise_exception_for_long_restarts(self): self.update_handler.child_launch_time = time.time() - 24 * 60 self.update_handler.child_launch_attempts = CHILD_LAUNCH_RESTART_MAX self._test_evaluate_agent_health() def test_evaluate_agent_health_will_not_raise_exception_too_few_restarts(self): self.update_handler.child_launch_time = time.time() self.update_handler.child_launch_attempts = CHILD_LAUNCH_RESTART_MAX - 2 self._test_evaluate_agent_health() def test_evaluate_agent_health_resets_with_new_agent(self): self.update_handler.child_launch_time = time.time() - (4 * 60) self.update_handler.child_launch_attempts = CHILD_LAUNCH_RESTART_MAX - 1 self._test_evaluate_agent_health(child_agent_index=1) self.assertEqual(1, self.update_handler.child_launch_attempts) def test_filter_blacklisted_agents(self): self.prepare_agents() self.update_handler._set_and_sort_agents([GuestAgent.from_installed_agent(path) for path in self.agent_dirs()]) self.assertEqual(len(self.agent_dirs()), len(self.update_handler.agents)) kept_agents = self.update_handler.agents[::2] blacklisted_agents = self.update_handler.agents[1::2] for agent in blacklisted_agents: agent.mark_failure(is_fatal=True) self.update_handler._filter_blacklisted_agents() self.assertEqual(kept_agents, self.update_handler.agents) def test_find_agents(self): self.prepare_agents() self.assertTrue(0 <= len(self.update_handler.agents)) self.update_handler._find_agents() self.assertEqual(len(self._get_agents(self.tmp_dir)), len(self.update_handler.agents)) def test_find_agents_does_reload(self): self.prepare_agents() self.update_handler._find_agents() agents = self.update_handler.agents self.update_handler._find_agents() self.assertNotEqual(agents, self.update_handler.agents) def test_find_agents_sorts(self): self.prepare_agents() self.update_handler._find_agents() v = FlexibleVersion("100000") for a in self.update_handler.agents: self.assertTrue(v > a.version) v = a.version def test_get_latest_agent(self): latest_version = self.prepare_agents() latest_agent = self.update_handler.get_latest_agent_greater_than_daemon() self.assertEqual(len(self._get_agents(self.tmp_dir)), len(self.update_handler.agents)) self.assertEqual(latest_version, latest_agent.version) def test_get_latest_agent_excluded(self): self.prepare_agent(AGENT_VERSION) self.assertFalse(self._test_upgrade_available( versions=self.agent_versions(), count=1)) self.assertEqual(None, self.update_handler.get_latest_agent_greater_than_daemon()) def test_get_latest_agent_no_updates(self): self.assertEqual(None, self.update_handler.get_latest_agent_greater_than_daemon()) def test_get_latest_agent_skip_updates(self): conf.get_autoupdate_enabled = Mock(return_value=False) self.assertEqual(None, self.update_handler.get_latest_agent_greater_than_daemon()) def test_get_latest_agent_skips_unavailable(self): self.prepare_agents() prior_agent = self.update_handler.get_latest_agent_greater_than_daemon() latest_version = self.prepare_agents(count=self.agent_count() + 1, is_available=False) latest_path = os.path.join(self.tmp_dir, "{0}-{1}".format(AGENT_NAME, latest_version)) self.assertFalse(GuestAgent.from_installed_agent(latest_path).is_available) latest_agent = self.update_handler.get_latest_agent_greater_than_daemon() self.assertTrue(latest_agent.version < latest_version) self.assertEqual(latest_agent.version, prior_agent.version) def test_get_pid_files(self): pid_files = self.update_handler._get_pid_files() self.assertEqual(0, len(pid_files)) def test_get_pid_files_returns_previous(self): for n in range(1250): fileutil.write_file(os.path.join(self.tmp_dir, str(n) + "_waagent.pid"), ustr(n + 1)) pid_files = self.update_handler._get_pid_files() self.assertEqual(1250, len(pid_files)) pid_dir, pid_name, pid_re = self.update_handler._get_pid_parts() # pylint: disable=unused-variable for p in pid_files: self.assertTrue(pid_re.match(os.path.basename(p))) def test_is_clean_start_returns_true_when_no_sentinel(self): self.assertFalse(os.path.isfile(self.update_handler._sentinel_file_path())) self.assertTrue(self.update_handler._is_clean_start) def test_is_clean_start_returns_false_when_sentinel_exists(self): self.update_handler._set_sentinel(agent=CURRENT_AGENT) self.assertFalse(self.update_handler._is_clean_start) def test_is_clean_start_returns_false_for_exceptions(self): self.update_handler._set_sentinel() with patch("azurelinuxagent.common.utils.fileutil.read_file", side_effect=Exception): self.assertFalse(self.update_handler._is_clean_start) def test_is_orphaned_returns_false_if_parent_exists(self): fileutil.write_file(conf.get_agent_pid_file_path(), ustr(42)) with patch('os.getppid', return_value=42): self.assertFalse(self.update_handler._is_orphaned) def test_is_orphaned_returns_true_if_parent_is_init(self): with patch('os.getppid', return_value=1): self.assertTrue(self.update_handler._is_orphaned) def test_is_orphaned_returns_true_if_parent_does_not_exist(self): fileutil.write_file(conf.get_agent_pid_file_path(), ustr(24)) with patch('os.getppid', return_value=42): self.assertTrue(self.update_handler._is_orphaned) def test_purge_agents(self): self.prepare_agents() self.update_handler._find_agents() # Ensure at least three agents initially exist self.assertTrue(2 < len(self.update_handler.agents)) # Purge every other agent. Don't add the current version to agents_to_keep explicitly; # the current version is never purged agents_to_keep = [] kept_agents = [] purged_agents = [] for i in range(0, len(self.update_handler.agents)): if self.update_handler.agents[i].version == CURRENT_VERSION: kept_agents.append(self.update_handler.agents[i]) else: if i % 2 == 0: agents_to_keep.append(self.update_handler.agents[i]) kept_agents.append(self.update_handler.agents[i]) else: purged_agents.append(self.update_handler.agents[i]) # Reload and assert only the kept agents remain on disk self.update_handler.agents = agents_to_keep self.update_handler._purge_agents() self.update_handler._find_agents() self.assertEqual( [agent.version for agent in kept_agents], [agent.version for agent in self.update_handler.agents]) # Ensure both directories and packages are removed for agent in purged_agents: agent_path = os.path.join(self.tmp_dir, "{0}-{1}".format(AGENT_NAME, agent.version)) self.assertFalse(os.path.exists(agent_path)) self.assertFalse(os.path.exists(agent_path + ".zip")) # Ensure kept agent directories and packages remain for agent in kept_agents: agent_path = os.path.join(self.tmp_dir, "{0}-{1}".format(AGENT_NAME, agent.version)) self.assertTrue(os.path.exists(agent_path)) self.assertTrue(os.path.exists(agent_path + ".zip")) def _test_run_latest(self, mock_child=None, mock_time=None, child_args=None): if mock_child is None: mock_child = ChildMock() if mock_time is None: mock_time = TimeMock() with patch('azurelinuxagent.ga.update.subprocess.Popen', return_value=mock_child) as mock_popen: with patch('time.time', side_effect=mock_time.time): with patch('time.sleep', side_effect=mock_time.sleep): self.update_handler.run_latest(child_args=child_args) agent_calls = [args[0] for (args, _) in mock_popen.call_args_list if "run-exthandlers" in ''.join(args[0])] self.assertEqual(1, len(agent_calls), "Expected a single call to the latest agent; got: {0}. All mocked calls: {1}".format( agent_calls, mock_popen.call_args_list)) return mock_popen.call_args def test_run_latest(self): self.prepare_agents() agent = self.update_handler.get_latest_agent_greater_than_daemon() args, kwargs = self._test_run_latest() args = args[0] cmds = textutil.safe_shlex_split(agent.get_agent_cmd()) if cmds[0].lower() == "python": cmds[0] = sys.executable self.assertEqual(args, cmds) self.assertTrue(len(args) > 1) self.assertRegex(args[0], r"^(/.*/python[\d.]*)$", "The command doesn't contain full python path") self.assertEqual("-run-exthandlers", args[len(args) - 1]) self.assertEqual(True, 'cwd' in kwargs) self.assertEqual(agent.get_agent_dir(), kwargs['cwd']) self.assertEqual(False, '\x00' in cmds[0]) def test_run_latest_passes_child_args(self): self.prepare_agents() self.update_handler.get_latest_agent_greater_than_daemon() args, _ = self._test_run_latest(child_args="AnArgument") args = args[0] self.assertTrue(len(args) > 1) self.assertRegex(args[0], r"^(/.*/python[\d.]*)$", "The command doesn't contain full python path") self.assertEqual("AnArgument", args[len(args) - 1]) def test_run_latest_polls_and_waits_for_success(self): mock_child = ChildMock(return_value=None) mock_time = TimeMock(time_increment=CHILD_HEALTH_INTERVAL / 3) self._test_run_latest(mock_child=mock_child, mock_time=mock_time) self.assertEqual(2, mock_child.poll.call_count) self.assertEqual(1, mock_child.wait.call_count) def test_run_latest_polling_stops_at_success(self): mock_child = ChildMock(return_value=0) mock_time = TimeMock(time_increment=CHILD_HEALTH_INTERVAL / 3) self._test_run_latest(mock_child=mock_child, mock_time=mock_time) self.assertEqual(1, mock_child.poll.call_count) self.assertEqual(0, mock_child.wait.call_count) def test_run_latest_polling_stops_at_failure(self): mock_child = ChildMock(return_value=42) mock_time = TimeMock() self._test_run_latest(mock_child=mock_child, mock_time=mock_time) self.assertEqual(1, mock_child.poll.call_count) self.assertEqual(0, mock_child.wait.call_count) def test_run_latest_polls_frequently_if_installed_is_latest(self): mock_child = ChildMock(return_value=0) # pylint: disable=unused-variable mock_time = TimeMock(time_increment=CHILD_HEALTH_INTERVAL / 2) self._test_run_latest(mock_time=mock_time) self.assertEqual(1, mock_time.sleep_interval) def test_run_latest_polls_every_second_if_installed_not_latest(self): self.prepare_agents() mock_time = TimeMock(time_increment=CHILD_HEALTH_INTERVAL / 2) self._test_run_latest(mock_time=mock_time) self.assertEqual(1, mock_time.sleep_interval) def test_run_latest_defaults_to_current(self): self.assertEqual(None, self.update_handler.get_latest_agent_greater_than_daemon()) args, kwargs = self._test_run_latest() self.assertEqual(args[0], [sys.executable, "-u", sys.argv[0], "-run-exthandlers"]) self.assertEqual(True, 'cwd' in kwargs) self.assertEqual(os.getcwd(), kwargs['cwd']) def test_run_latest_forwards_output(self): try: tempdir = tempfile.mkdtemp() stdout_path = os.path.join(tempdir, "stdout") stderr_path = os.path.join(tempdir, "stderr") with open(stdout_path, "w") as stdout: with open(stderr_path, "w") as stderr: saved_stdout, sys.stdout = sys.stdout, stdout saved_stderr, sys.stderr = sys.stderr, stderr try: self._test_run_latest(mock_child=ChildMock(side_effect=faux_logger)) finally: sys.stdout = saved_stdout sys.stderr = saved_stderr with open(stdout_path, "r") as stdout: self.assertEqual(1, len(stdout.readlines())) with open(stderr_path, "r") as stderr: self.assertEqual(1, len(stderr.readlines())) finally: shutil.rmtree(tempdir, True) def test_run_latest_nonzero_code_does_not_mark_failure(self): self.prepare_agents() latest_agent = self.update_handler.get_latest_agent_greater_than_daemon() self.assertTrue(latest_agent.is_available) self.assertEqual(0.0, latest_agent.error.last_failure) self.assertEqual(0, latest_agent.error.failure_count) with patch('azurelinuxagent.ga.update.UpdateHandler.get_latest_agent_greater_than_daemon', return_value=latest_agent): self._test_run_latest(mock_child=ChildMock(return_value=1)) self.assertFalse(latest_agent.is_blacklisted, "Agent should not be blacklisted") def test_run_latest_exception_blacklists(self): self.prepare_agents() latest_agent = self.update_handler.get_latest_agent_greater_than_daemon() self.assertTrue(latest_agent.is_available) self.assertEqual(0.0, latest_agent.error.last_failure) self.assertEqual(0, latest_agent.error.failure_count) verify_string = "Force blacklisting: {0}".format(str(uuid.uuid4())) with patch('azurelinuxagent.ga.update.UpdateHandler.get_latest_agent_greater_than_daemon', return_value=latest_agent): self._test_run_latest(mock_child=ChildMock(side_effect=Exception(verify_string))) self.assertFalse(latest_agent.is_available) self.assertTrue(latest_agent.error.is_blacklisted) self.assertNotEqual(0.0, latest_agent.error.last_failure) self.assertEqual(1, latest_agent.error.failure_count) self.assertIn(verify_string, latest_agent.error.reason, "Error reason not found while blacklisting") def test_run_latest_exception_does_not_blacklist_if_terminating(self): self.prepare_agents() latest_agent = self.update_handler.get_latest_agent_greater_than_daemon() self.assertTrue(latest_agent.is_available) self.assertEqual(0.0, latest_agent.error.last_failure) self.assertEqual(0, latest_agent.error.failure_count) with patch('azurelinuxagent.ga.update.UpdateHandler.get_latest_agent_greater_than_daemon', return_value=latest_agent): self.update_handler.is_running = False self._test_run_latest(mock_child=ChildMock(side_effect=Exception("Attempt blacklisting"))) self.assertTrue(latest_agent.is_available) self.assertFalse(latest_agent.error.is_blacklisted) self.assertEqual(0.0, latest_agent.error.last_failure) self.assertEqual(0, latest_agent.error.failure_count) @patch('signal.signal') def test_run_latest_captures_signals(self, mock_signal): self._test_run_latest() self.assertEqual(1, mock_signal.call_count) @patch('signal.signal') def test_run_latest_creates_only_one_signal_handler(self, mock_signal): self.update_handler.signal_handler = "Not None" self._test_run_latest() self.assertEqual(0, mock_signal.call_count) def test_get_latest_agent_should_return_latest_agent_even_on_bad_error_json(self): dst_ver = self.prepare_agents() # Add a malformed error.json file in all existing agents for agent_dir in self.agent_dirs(): error_file_path = os.path.join(agent_dir, AGENT_ERROR_FILE) with open(error_file_path, 'w') as f: f.write("") latest_agent = self.update_handler.get_latest_agent_greater_than_daemon() self.assertEqual(latest_agent.version, dst_ver, "Latest agent version is invalid") def test_set_agents_sets_agents(self): self.prepare_agents() self.update_handler._set_and_sort_agents([GuestAgent.from_installed_agent(path) for path in self.agent_dirs()]) self.assertTrue(len(self.update_handler.agents) > 0) self.assertEqual(len(self.agent_dirs()), len(self.update_handler.agents)) def test_set_agents_sorts_agents(self): self.prepare_agents() self.update_handler._set_and_sort_agents([GuestAgent.from_installed_agent(path) for path in self.agent_dirs()]) v = FlexibleVersion("100000") for a in self.update_handler.agents: self.assertTrue(v > a.version) v = a.version def test_set_sentinel(self): self.assertFalse(os.path.isfile(self.update_handler._sentinel_file_path())) self.update_handler._set_sentinel() self.assertTrue(os.path.isfile(self.update_handler._sentinel_file_path())) def test_set_sentinel_writes_current_agent(self): self.update_handler._set_sentinel() self.assertTrue( fileutil.read_file(self.update_handler._sentinel_file_path()), CURRENT_AGENT) def test_shutdown(self): self.update_handler._set_sentinel() self.update_handler._shutdown() self.assertFalse(self.update_handler.is_running) self.assertFalse(os.path.isfile(self.update_handler._sentinel_file_path())) def test_shutdown_ignores_missing_sentinel_file(self): self.assertFalse(os.path.isfile(self.update_handler._sentinel_file_path())) self.update_handler._shutdown() self.assertFalse(self.update_handler.is_running) self.assertFalse(os.path.isfile(self.update_handler._sentinel_file_path())) def test_shutdown_ignores_exceptions(self): self.update_handler._set_sentinel() try: with patch("os.remove", side_effect=Exception): self.update_handler._shutdown() except Exception as e: # pylint: disable=unused-variable self.assertTrue(False, "Unexpected exception") # pylint: disable=redundant-unittest-assert def _test_upgrade_available( self, base_version=FlexibleVersion(AGENT_VERSION), protocol=None, versions=None, count=20): if protocol is None: protocol = self._create_protocol(count=count, versions=versions) self.update_handler.protocol_util = protocol self.update_handler._goal_state = protocol.get_goal_state() self.update_handler._goal_state.extensions_goal_state.is_outdated = False conf.get_autoupdate_gafamily = Mock(return_value=protocol.family) return self.update_handler._download_agent_if_upgrade_available(protocol, base_version=base_version) def test_upgrade_available_returns_true_on_first_use(self): self.assertTrue(self._test_upgrade_available()) def test_upgrade_available_handles_missing_family(self): data_file = mockwiredata.DATA_FILE.copy() data_file["ext_conf"] = "wire/ext_conf_missing_family.xml" with mock_wire_protocol(data_file) as protocol: self.update_handler.protocol_util = protocol with patch('azurelinuxagent.common.logger.warn') as mock_logger: with patch('azurelinuxagent.common.protocol.goal_state.GoalState.fetch_agent_manifest', side_effect=ProtocolError): self.assertFalse(self.update_handler._download_agent_if_upgrade_available(protocol, base_version=CURRENT_VERSION)) self.assertEqual(0, mock_logger.call_count) def test_upgrade_available_includes_old_agents(self): self.prepare_agents() old_version = self.agent_versions()[-1] old_count = old_version.version[-1] self.replicate_agents(src_v=old_version, count=old_count, increment=-1) all_count = len(self.agent_versions()) self.assertTrue(self._test_upgrade_available(versions=self.agent_versions())) self.assertEqual(all_count, len(self.update_handler.agents)) def test_upgrade_available_purges_old_agents(self): self.prepare_agents() agent_count = self.agent_count() self.assertEqual(20, agent_count) agent_versions = self.agent_versions()[:3] self.assertTrue(self._test_upgrade_available(versions=agent_versions)) self.assertEqual(len(agent_versions), len(self.update_handler.agents)) # Purging always keeps the running agent if CURRENT_VERSION not in agent_versions: agent_versions.append(CURRENT_VERSION) self.assertEqual(agent_versions, self.agent_versions()) def test_upgrade_available_skips_if_too_frequent(self): conf.get_autoupdate_frequency = Mock(return_value=10000) self.update_handler.last_attempt_time = time.time() self.assertFalse(self._test_upgrade_available()) def test_upgrade_available_skips_when_no_new_versions(self): self.prepare_agents() base_version = self.agent_versions()[0] + 1 self.assertFalse(self._test_upgrade_available(base_version=base_version)) def test_upgrade_available_skips_when_no_versions(self): self.assertFalse(self._test_upgrade_available(protocol=ProtocolMock())) def test_upgrade_available_sorts(self): self.prepare_agents() self._test_upgrade_available() v = FlexibleVersion("100000") for a in self.update_handler.agents: self.assertTrue(v > a.version) v = a.version def test_write_pid_file(self): for n in range(1112): fileutil.write_file(os.path.join(self.tmp_dir, str(n) + "_waagent.pid"), ustr(n + 1)) with patch('os.getpid', return_value=1112): pid_files, pid_file = self.update_handler._write_pid_file() self.assertEqual(1112, len(pid_files)) self.assertEqual("1111_waagent.pid", os.path.basename(pid_files[-1])) self.assertEqual("1112_waagent.pid", os.path.basename(pid_file)) self.assertEqual(fileutil.read_file(pid_file), ustr(1112)) def test_write_pid_file_ignores_exceptions(self): with patch('azurelinuxagent.common.utils.fileutil.write_file', side_effect=Exception): with patch('os.getpid', return_value=42): pid_files, pid_file = self.update_handler._write_pid_file() self.assertEqual(0, len(pid_files)) self.assertEqual(None, pid_file) def test_update_happens_when_extensions_disabled(self): """ Although the extension enabled config will not get checked before an update is found, this test attempts to ensure that behavior never changes. """ with patch('azurelinuxagent.common.conf.get_extensions_enabled', return_value=False): with patch('azurelinuxagent.ga.update.UpdateHandler._download_agent_if_upgrade_available', return_value=True) as download_agent: with mock_wire_protocol(DATA_FILE) as protocol: with mock_update_handler(protocol, autoupdate_enabled=True) as update_handler: update_handler.run() self.assertEqual(1, download_agent.call_count, "Agent update did not execute (no attempts to download the agent") @staticmethod def _get_test_ext_handler_instance(protocol, name="OSTCExtensions.ExampleHandlerLinux", version="1.0.0"): eh = Extension(name=name) eh.version = version return ExtHandlerInstance(eh, protocol) def test_update_handler_recovers_from_error_with_no_certs(self): data = DATA_FILE.copy() data['goal_state'] = 'wire/goal_state_no_certs.xml' def fail_gs_fetch(url, *_, **__): if HttpRequestPredicates.is_goal_state_request(url): return MockHttpResponse(status=500) return None with mock_wire_protocol(data) as protocol: def fail_fetch_on_second_iter(iteration): if iteration == 2: protocol.set_http_handlers(http_get_handler=fail_gs_fetch) if iteration > 2: # Zero out the fail handler for subsequent iterations. protocol.set_http_handlers(http_get_handler=None) with mock_update_handler(protocol, 3, on_new_iteration=fail_fetch_on_second_iter) as update_handler: with patch("azurelinuxagent.ga.update.logger.error") as patched_error: with patch("azurelinuxagent.ga.update.logger.info") as patched_info: def match_unexpected_errors(): unexpected_msg_fragment = "Error fetching the goal state:" matching_errors = [] for (args, _) in filter(lambda a: len(a) > 0, patched_error.call_args_list): if unexpected_msg_fragment in args[0]: matching_errors.append(args[0]) if len(matching_errors) > 1: self.fail("Guest Agent did not recover, with new error(s): {}"\ .format(matching_errors[1:])) def match_expected_info(): expected_msg_fragment = "Fetching the goal state recovered from previous errors" for (call_args, _) in filter(lambda a: len(a) > 0, patched_info.call_args_list): if expected_msg_fragment in call_args[0]: break else: self.fail("Expected the guest agent to recover with '{}', but it didn't"\ .format(expected_msg_fragment)) update_handler.run(debug=True) match_unexpected_errors() # Match on errors first, they can provide more info. match_expected_info() def test_it_should_recreate_handler_env_on_service_startup(self): iterations = 5 with _get_update_handler(iterations) as (update_handler, protocol): update_handler.run(debug=True) expected_handler = self._get_test_ext_handler_instance(protocol) handler_env_file = expected_handler.get_env_file() self.assertTrue(os.path.exists(expected_handler.get_base_dir()), "Extension not found") # First iteration should install the extension handler and # subsequent iterations should not recreate the HandlerEnvironment file last_modification_time = os.path.getmtime(handler_env_file) self.assertEqual(os.path.getctime(handler_env_file), last_modification_time, "The creation time and last modified time of the HandlerEnvironment file dont match") # Simulate a service restart by getting a new instance of the update handler and protocol and # re-runnning the update handler. Then,ensure that the HandlerEnvironment file is recreated with eventsFolder # flag in HandlerEnvironment.json file. self._add_write_permission_to_goal_state_files() with _get_update_handler(iterations=1) as (update_handler, protocol): with patch("azurelinuxagent.common.agent_supported_feature._ETPFeature.is_supported", True): update_handler.run(debug=True) self.assertGreater(os.path.getmtime(handler_env_file), last_modification_time, "HandlerEnvironment file didn't get overwritten") with open(handler_env_file, 'r') as handler_env_content_file: content = json.load(handler_env_content_file) self.assertIn(HandlerEnvironment.eventsFolder, content[0][HandlerEnvironment.handlerEnvironment], "{0} not found in HandlerEnv file".format(HandlerEnvironment.eventsFolder)) def test_it_should_not_setup_persistent_firewall_rules_if_EnableFirewall_is_disabled(self): executed_firewall_commands = [] def _mock_popen(cmd, *args, **kwargs): if 'firewall-cmd' in cmd: executed_firewall_commands.append(cmd) cmd = ["echo", "running"] return _ORIGINAL_POPEN(cmd, *args, **kwargs) with _get_update_handler(iterations=1) as (update_handler, _): with patch("azurelinuxagent.common.logger.info") as patch_info: with patch("azurelinuxagent.common.utils.shellutil.subprocess.Popen", side_effect=_mock_popen): with patch('azurelinuxagent.common.conf.enable_firewall', return_value=False): with patch("azurelinuxagent.common.logger.warn") as patch_warn: update_handler.run(debug=True) self.assertEqual(0, update_handler.get_exit_code(), "Exit code should be 0; List of all warnings logged by the agent: {0}".format( patch_warn.call_args_list)) self.assertEqual(0, len(executed_firewall_commands), "firewall-cmd should not be called at all") self.assertTrue(any( "Not setting up persistent firewall rules as OS.EnableFirewall=False" == args[0] for (args, _) in patch_info.call_args_list), "Info not logged properly, got: {0}".format(patch_info.call_args_list)) @skip_if_predicate_true(is_python_version_26_or_34, "Disabled on Python 2.6 and 3.4 for now. Need to revisit to fix it") def test_it_should_setup_persistent_firewall_rules_on_startup(self): iterations = 1 executed_commands = [] def _mock_popen(cmd, *args, **kwargs): if 'firewall-cmd' in cmd: executed_commands.append(cmd) cmd = ["echo", "running"] return _ORIGINAL_POPEN(cmd, *args, **kwargs) with _get_update_handler(iterations) as (update_handler, _): with patch("azurelinuxagent.common.utils.shellutil.subprocess.Popen", side_effect=_mock_popen) as mock_popen: with patch('azurelinuxagent.common.conf.enable_firewall', return_value=True): with patch('azurelinuxagent.common.osutil.systemd.is_systemd', return_value=True): with patch("azurelinuxagent.common.logger.warn") as patch_warn: update_handler.run(debug=True) self.assertEqual(0, update_handler.get_exit_code(), "Exit code should be 0; List of all warnings logged by the agent: {0}".format( patch_warn.call_args_list)) # Firewall-cmd should only be called 4 times - 1st to check if running, 2nd, 3rd and 4th for the QueryPassThrough cmd self.assertEqual(4, len(executed_commands), "The number of times firewall-cmd should be called is only 4; Executed firewall commands: {0}; All popen calls: {1}".format( executed_commands, mock_popen.call_args_list)) self.assertEqual(PersistFirewallRulesHandler._FIREWALLD_RUNNING_CMD, executed_commands.pop(0), "First command should be to check if firewalld is running") self.assertTrue([FirewallCmdDirectCommands.QueryPassThrough in cmd for cmd in executed_commands], "The remaining commands should only be for querying the firewall commands") def test_it_should_set_dns_tcp_iptable_if_drop_available_accept_unavailable(self): with TestOSUtil._mock_iptables() as mock_iptables: with _get_update_handler(test_data=DATA_FILE) as (update_handler, _): with patch('azurelinuxagent.common.conf.enable_firewall', return_value=True): with patch.object(osutil, '_enable_firewall', True): # drop rule is present mock_iptables.set_command( AddFirewallRules.get_wire_non_root_drop_rule(AddFirewallRules.CHECK_COMMAND, mock_iptables.destination, wait=mock_iptables.wait), exit_code=0) # non root tcp iptable rule is absent mock_iptables.set_command(AddFirewallRules.get_accept_tcp_rule(AddFirewallRules.CHECK_COMMAND, mock_iptables.destination, wait=mock_iptables.wait), exit_code=1) update_handler._add_accept_tcp_firewall_rule_if_not_enabled(mock_iptables.destination) drop_check_command = TestOSUtil._command_to_string( AddFirewallRules.get_wire_non_root_drop_rule(AddFirewallRules.CHECK_COMMAND, mock_iptables.destination, wait=mock_iptables.wait)) accept_tcp_check_rule = TestOSUtil._command_to_string( AddFirewallRules.get_accept_tcp_rule(AddFirewallRules.CHECK_COMMAND, mock_iptables.destination, wait=mock_iptables.wait)) accept_tcp_insert_rule = TestOSUtil._command_to_string( AddFirewallRules.get_accept_tcp_rule(AddFirewallRules.INSERT_COMMAND, mock_iptables.destination, wait=mock_iptables.wait)) # Filtering the mock iptable command calls with only the ones related to this test. filtered_mock_iptable_calls = [cmd for cmd in mock_iptables.command_calls if cmd in [drop_check_command, accept_tcp_check_rule, accept_tcp_insert_rule]] self.assertEqual(len(filtered_mock_iptable_calls), 3, "Incorrect number of calls to iptables: [{0}]".format( mock_iptables.command_calls)) self.assertEqual(filtered_mock_iptable_calls[0], drop_check_command, "The first command should check the drop rule") self.assertEqual(filtered_mock_iptable_calls[1], accept_tcp_check_rule, "The second command should check the accept rule") self.assertEqual(filtered_mock_iptable_calls[2], accept_tcp_insert_rule, "The third command should add the accept rule") def test_it_should_not_set_dns_tcp_iptable_if_drop_unavailable(self): with TestOSUtil._mock_iptables() as mock_iptables: with _get_update_handler(test_data=DATA_FILE) as (update_handler, _): with patch('azurelinuxagent.common.conf.enable_firewall', return_value=True): with patch.object(osutil, '_enable_firewall', True): # drop rule is not available mock_iptables.set_command( AddFirewallRules.get_wire_non_root_drop_rule(AddFirewallRules.CHECK_COMMAND, mock_iptables.destination, wait=mock_iptables.wait), exit_code=1) update_handler._add_accept_tcp_firewall_rule_if_not_enabled(mock_iptables.destination) drop_check_command = TestOSUtil._command_to_string( AddFirewallRules.get_wire_non_root_drop_rule(AddFirewallRules.CHECK_COMMAND, mock_iptables.destination, wait=mock_iptables.wait)) accept_tcp_check_rule = TestOSUtil._command_to_string( AddFirewallRules.get_accept_tcp_rule(AddFirewallRules.CHECK_COMMAND, mock_iptables.destination, wait=mock_iptables.wait)) accept_tcp_insert_rule = TestOSUtil._command_to_string( AddFirewallRules.get_accept_tcp_rule(AddFirewallRules.INSERT_COMMAND, mock_iptables.destination, wait=mock_iptables.wait)) # Filtering the mock iptable command calls with only the ones related to this test. filtered_mock_iptable_calls = [cmd for cmd in mock_iptables.command_calls if cmd in [drop_check_command, accept_tcp_check_rule, accept_tcp_insert_rule]] self.assertEqual(len(filtered_mock_iptable_calls), 1, "Incorrect number of calls to iptables: [{0}]".format( mock_iptables.command_calls)) self.assertEqual(filtered_mock_iptable_calls[0], drop_check_command, "The first command should check the drop rule") def test_it_should_not_set_dns_tcp_iptable_if_drop_and_accept_available(self): with TestOSUtil._mock_iptables() as mock_iptables: with _get_update_handler(test_data=DATA_FILE) as (update_handler, _): with patch('azurelinuxagent.common.conf.enable_firewall', return_value=True): with patch.object(osutil, '_enable_firewall', True): # drop rule is available mock_iptables.set_command( AddFirewallRules.get_wire_non_root_drop_rule(AddFirewallRules.CHECK_COMMAND, mock_iptables.destination, wait=mock_iptables.wait), exit_code=0) # non root tcp iptable rule is available mock_iptables.set_command(AddFirewallRules.get_accept_tcp_rule(AddFirewallRules.CHECK_COMMAND, mock_iptables.destination, wait=mock_iptables.wait), exit_code=0) update_handler._add_accept_tcp_firewall_rule_if_not_enabled(mock_iptables.destination) drop_check_command = TestOSUtil._command_to_string( AddFirewallRules.get_wire_non_root_drop_rule(AddFirewallRules.CHECK_COMMAND, mock_iptables.destination, wait=mock_iptables.wait)) accept_tcp_check_rule = TestOSUtil._command_to_string( AddFirewallRules.get_accept_tcp_rule(AddFirewallRules.CHECK_COMMAND, mock_iptables.destination, wait=mock_iptables.wait)) accept_tcp_insert_rule = TestOSUtil._command_to_string( AddFirewallRules.get_accept_tcp_rule(AddFirewallRules.INSERT_COMMAND, mock_iptables.destination, wait=mock_iptables.wait)) # Filtering the mock iptable command calls with only the ones related to this test. filtered_mock_iptable_calls = [cmd for cmd in mock_iptables.command_calls if cmd in [drop_check_command, accept_tcp_check_rule, accept_tcp_insert_rule]] self.assertEqual(len(filtered_mock_iptable_calls), 2, "Incorrect number of calls to iptables: [{0}]".format( mock_iptables.command_calls)) self.assertEqual(filtered_mock_iptable_calls[0], drop_check_command, "The first command should check the drop rule") self.assertEqual(filtered_mock_iptable_calls[1], accept_tcp_check_rule, "The second command should check the accept rule") @contextlib.contextmanager def _setup_test_for_ext_event_dirs_retention(self): try: with _get_update_handler(test_data=DATA_FILE_MULTIPLE_EXT) as (update_handler, protocol): with patch("azurelinuxagent.common.agent_supported_feature._ETPFeature.is_supported", True): update_handler.run(debug=True) expected_events_dirs = glob.glob(os.path.join(conf.get_ext_log_dir(), "*", EVENTS_DIRECTORY)) no_of_extensions = protocol.mock_wire_data.get_no_of_plugins_in_extension_config() # Ensure extensions installed and events directory created self.assertEqual(len(expected_events_dirs), no_of_extensions, "Extension events directories dont match") for ext_dir in expected_events_dirs: self.assertTrue(os.path.exists(ext_dir), "Extension directory {0} not created!".format(ext_dir)) yield update_handler, expected_events_dirs finally: # The TestUpdate.setUp() initializes the self.tmp_dir to be used as a placeholder # for everything (event logger, status logger, conf.get_lib_dir() and more). # Since we add more data to the dir for this test, ensuring its completely clean before exiting the test. shutil.rmtree(self.tmp_dir, ignore_errors=True) self.tmp_dir = None def test_it_should_delete_extension_events_directory_if_extension_telemetry_pipeline_disabled(self): # Disable extension telemetry pipeline and ensure events directory got deleted with self._setup_test_for_ext_event_dirs_retention() as (update_handler, expected_events_dirs): with patch("azurelinuxagent.common.agent_supported_feature._ETPFeature.is_supported", False): self._add_write_permission_to_goal_state_files() update_handler.run(debug=True) for ext_dir in expected_events_dirs: self.assertFalse(os.path.exists(ext_dir), "Extension directory {0} still exists!".format(ext_dir)) def test_it_should_retain_extension_events_directories_if_extension_telemetry_pipeline_enabled(self): # Rerun update handler again with extension telemetry pipeline enabled to ensure we dont delete events directories with self._setup_test_for_ext_event_dirs_retention() as (update_handler, expected_events_dirs): self._add_write_permission_to_goal_state_files() update_handler.run(debug=True) for ext_dir in expected_events_dirs: self.assertTrue(os.path.exists(ext_dir), "Extension directory {0} should exist!".format(ext_dir)) def test_it_should_recreate_extension_event_directories_for_existing_extensions_if_extension_telemetry_pipeline_enabled(self): with self._setup_test_for_ext_event_dirs_retention() as (update_handler, expected_events_dirs): # Delete existing events directory for ext_dir in expected_events_dirs: shutil.rmtree(ext_dir, ignore_errors=True) self.assertFalse(os.path.exists(ext_dir), "Extension directory not deleted") with patch("azurelinuxagent.common.agent_supported_feature._ETPFeature.is_supported", True): self._add_write_permission_to_goal_state_files() update_handler.run(debug=True) for ext_dir in expected_events_dirs: self.assertTrue(os.path.exists(ext_dir), "Extension directory {0} should exist!".format(ext_dir)) def test_it_should_report_update_status_in_status_blob(self): with mock_wire_protocol(DATA_FILE) as protocol: with patch.object(conf, "get_enable_ga_versioning", return_value=True): with patch.object(conf, "get_autoupdate_gafamily", return_value="Prod"): with patch("azurelinuxagent.common.logger.warn") as patch_warn: protocol.aggregate_status = None protocol.incarnation = 1 def mock_http_put(url, *args, **_): if HttpRequestPredicates.is_host_plugin_status_request(url): # Skip reading the HostGA request data as its encoded return MockHttpResponse(status=500) protocol.aggregate_status = json.loads(args[0]) return MockHttpResponse(status=201) def update_goal_state_and_run_handler(): protocol.incarnation += 1 protocol.mock_wire_data.set_incarnation(protocol.incarnation) self._add_write_permission_to_goal_state_files() with _get_update_handler(iterations=1, protocol=protocol) as (update_handler, _): update_handler.run(debug=True) self.assertEqual(0, update_handler.get_exit_code(), "Exit code should be 0; List of all warnings logged by the agent: {0}".format( patch_warn.call_args_list)) protocol.set_http_handlers(http_put_handler=mock_http_put) # Case 1: No requested version in GS; updateStatus should not be reported update_goal_state_and_run_handler() self.assertFalse("updateStatus" in protocol.aggregate_status['aggregateStatus']['guestAgentStatus'], "updateStatus should not be reported if not asked in GS") # Case 2: Requested version in GS != Current Version; updateStatus should be error protocol.mock_wire_data.set_extension_config("wire/ext_conf_requested_version.xml") update_goal_state_and_run_handler() self.assertTrue("updateStatus" in protocol.aggregate_status['aggregateStatus']['guestAgentStatus'], "updateStatus should be in status blob. Warns: {0}".format(patch_warn.call_args_list)) update_status = protocol.aggregate_status['aggregateStatus']['guestAgentStatus']["updateStatus"] self.assertEqual(VMAgentUpdateStatuses.Error, update_status['status'], "Status should be an error") self.assertEqual(update_status['expectedVersion'], "9.9.9.10", "incorrect version reported") self.assertEqual(update_status['code'], 1, "incorrect code reported") # Case 3: Requested version in GS == Current Version; updateStatus should be Success protocol.mock_wire_data.set_extension_config_requested_version(str(CURRENT_VERSION)) update_goal_state_and_run_handler() self.assertTrue("updateStatus" in protocol.aggregate_status['aggregateStatus']['guestAgentStatus'], "updateStatus should be reported if asked in GS") update_status = protocol.aggregate_status['aggregateStatus']['guestAgentStatus']["updateStatus"] self.assertEqual(VMAgentUpdateStatuses.Success, update_status['status'], "Status should be successful") self.assertEqual(update_status['expectedVersion'], str(CURRENT_VERSION), "incorrect version reported") self.assertEqual(update_status['code'], 0, "incorrect code reported") # Case 4: Requested version removed in GS; no updateStatus should be reported protocol.mock_wire_data.reload() update_goal_state_and_run_handler() self.assertFalse("updateStatus" in protocol.aggregate_status['aggregateStatus']['guestAgentStatus'], "updateStatus should not be reported if not asked in GS") def test_it_should_wait_to_fetch_first_goal_state(self): with _get_update_handler() as (update_handler, protocol): with patch("azurelinuxagent.common.logger.error") as patch_error: with patch("azurelinuxagent.common.logger.info") as patch_info: # Fail GS fetching for the 1st 5 times the agent asks for it update_handler._fail_gs_count = 5 def get_handler(url, **kwargs): if HttpRequestPredicates.is_goal_state_request(url) and update_handler._fail_gs_count > 0: update_handler._fail_gs_count -= 1 return MockHttpResponse(status=500) return protocol.mock_wire_data.mock_http_get(url, **kwargs) protocol.set_http_handlers(http_get_handler=get_handler) update_handler.run(debug=True) self.assertEqual(0, update_handler.get_exit_code(), "Exit code should be 0; List of all errors logged by the agent: {0}".format( patch_error.call_args_list)) error_msgs = [args[0] for (args, _) in patch_error.call_args_list if "Error fetching the goal state" in args[0]] self.assertTrue(len(error_msgs) > 0, "Error should've been reported when failed to retrieve GS") info_msgs = [args[0] for (args, _) in patch_info.call_args_list if "Fetching the goal state recovered from previous errors." in args[0]] self.assertTrue(len(info_msgs) > 0, "Agent should've logged a message when recovered from GS errors") def test_it_should_reset_legacy_blacklisted_agents_on_process_start(self): # Add some good agents self.prepare_agents(count=10) good_agents = [agent.name for agent in self.agents()] # Add a set of blacklisted agents self.prepare_agents(count=20, is_available=False) for agent in self.agents(): # Assert the test environment is correctly set if agent.name not in good_agents: self.assertTrue(agent.is_blacklisted, "Agent {0} should be blacklisted".format(agent.name)) else: self.assertFalse(agent.is_blacklisted, "Agent {0} should not be blacklisted".format(agent.name)) with _get_update_handler() as (update_handler, _): update_handler.run(debug=True) self.assertEqual(20, self.agent_count(), "All agents should be available on disk") # Ensure none of the agents are blacklisted for agent in self.agents(): self.assertFalse(agent.is_blacklisted, "Legacy Agent should not be blacklisted") class UpdateHandlerRunTestCase(AgentTestCase): def _test_run(self, autoupdate_enabled=False, check_daemon_running=False, expected_exit_code=0, emit_restart_event=None): fileutil.write_file(conf.get_agent_pid_file_path(), ustr(42)) with patch('azurelinuxagent.ga.update.get_monitor_handler') as mock_monitor: with patch('azurelinuxagent.ga.remoteaccess.get_remote_access_handler') as mock_ra_handler: with patch('azurelinuxagent.ga.update.get_env_handler') as mock_env: with patch('azurelinuxagent.ga.update.get_collect_logs_handler') as mock_collect_logs: with patch('azurelinuxagent.ga.update.get_send_telemetry_events_handler') as mock_telemetry_send_events: with patch('azurelinuxagent.ga.update.get_collect_telemetry_events_handler') as mock_event_collector: with patch('azurelinuxagent.ga.update.initialize_event_logger_vminfo_common_parameters'): with patch('azurelinuxagent.ga.update.is_log_collection_allowed', return_value=True): with mock_wire_protocol(DATA_FILE) as protocol: mock_exthandlers_handler = Mock() with mock_update_handler( protocol, exthandlers_handler=mock_exthandlers_handler, remote_access_handler=mock_ra_handler, autoupdate_enabled=autoupdate_enabled, check_daemon_running=check_daemon_running ) as update_handler: if emit_restart_event is not None: update_handler._emit_restart_event = emit_restart_event if isinstance(os.getppid, MagicMock): update_handler.run() else: with patch('os.getppid', return_value=42): update_handler.run() self.assertEqual(1, mock_monitor.call_count) self.assertEqual(1, mock_env.call_count) self.assertEqual(1, mock_collect_logs.call_count) self.assertEqual(1, mock_telemetry_send_events.call_count) self.assertEqual(1, mock_event_collector.call_count) self.assertEqual(expected_exit_code, update_handler.get_exit_code()) if update_handler.get_iterations_completed() > 0: # some test cases exit before executing extensions or remote access self.assertEqual(1, mock_exthandlers_handler.run.call_count) self.assertEqual(1, mock_ra_handler.run.call_count) return update_handler def test_run(self): self._test_run() def test_run_stops_if_update_available(self): with patch('azurelinuxagent.ga.update.UpdateHandler._download_agent_if_upgrade_available', return_value=True): update_handler = self._test_run(autoupdate_enabled=True) self.assertEqual(0, update_handler.get_iterations_completed()) def test_run_stops_if_orphaned(self): with patch('os.getppid', return_value=1): update_handler = self._test_run(check_daemon_running=True) self.assertEqual(0, update_handler.get_iterations_completed()) def test_run_clears_sentinel_on_successful_exit(self): update_handler = self._test_run() self.assertFalse(os.path.isfile(update_handler._sentinel_file_path())) def test_run_leaves_sentinel_on_unsuccessful_exit(self): with patch('azurelinuxagent.ga.update.UpdateHandler._download_agent_if_upgrade_available', side_effect=Exception): update_handler = self._test_run(autoupdate_enabled=True,expected_exit_code=1) self.assertTrue(os.path.isfile(update_handler._sentinel_file_path())) def test_run_emits_restart_event(self): update_handler = self._test_run(emit_restart_event=Mock()) self.assertEqual(1, update_handler._emit_restart_event.call_count) class TestAgentUpgrade(UpdateTestCase): @contextlib.contextmanager def create_conf_mocks(self, hotfix_frequency, normal_frequency): # Disabling extension processing to speed up tests as this class deals with testing agent upgrades with patch("azurelinuxagent.common.conf.get_extensions_enabled", return_value=False): with patch("azurelinuxagent.common.conf.get_autoupdate_frequency", return_value=0.001): with patch("azurelinuxagent.common.conf.get_hotfix_upgrade_frequency", return_value=hotfix_frequency): with patch("azurelinuxagent.common.conf.get_normal_upgrade_frequency", return_value=normal_frequency): with patch("azurelinuxagent.common.conf.get_autoupdate_gafamily", return_value="Prod"): yield @contextlib.contextmanager def __get_update_handler(self, iterations=1, test_data=None, hotfix_frequency=1.0, normal_frequency=2.0, reload_conf=None): test_data = DATA_FILE if test_data is None else test_data with _get_update_handler(iterations, test_data) as (update_handler, protocol): protocol.aggregate_status = None def get_handler(url, **kwargs): if reload_conf is not None: reload_conf(url, protocol) if HttpRequestPredicates.is_agent_package_request(url): agent_pkg = load_bin_data(self._get_agent_file_name(), self._agent_zip_dir) protocol.mock_wire_data.call_counts['agentArtifact'] += 1 return MockHttpResponse(status=httpclient.OK, body=agent_pkg) return protocol.mock_wire_data.mock_http_get(url, **kwargs) def put_handler(url, *args, **_): if HttpRequestPredicates.is_host_plugin_status_request(url): # Skip reading the HostGA request data as its encoded return MockHttpResponse(status=500) protocol.aggregate_status = json.loads(args[0]) return MockHttpResponse(status=201) protocol.set_http_handlers(http_get_handler=get_handler, http_put_handler=put_handler) with self.create_conf_mocks(hotfix_frequency, normal_frequency): with patch("azurelinuxagent.ga.update.add_event") as mock_telemetry: update_handler._protocol = protocol yield update_handler, mock_telemetry def __assert_exit_code_successful(self, update_handler): self.assertEqual(0, update_handler.get_exit_code(), "Exit code should be 0") def __assert_upgrade_telemetry_emitted_for_requested_version(self, mock_telemetry, upgrade=True, version="99999.0.0.0"): upgrade_event_msgs = [kwarg['message'] for _, kwarg in mock_telemetry.call_args_list if 'Exiting current process to {0} to the request Agent version {1}'.format( "upgrade" if upgrade else "downgrade", version) in kwarg['message'] and kwarg[ 'op'] == WALAEventOperation.AgentUpgrade] self.assertEqual(1, len(upgrade_event_msgs), "Did not find the event indicating that the agent was upgraded. Got: {0}".format( mock_telemetry.call_args_list)) def __assert_upgrade_telemetry_emitted(self, mock_telemetry, upgrade_type=AgentUpgradeType.Normal): upgrade_event_msgs = [kwarg['message'] for _, kwarg in mock_telemetry.call_args_list if '{0} Agent upgrade discovered, updating to WALinuxAgent-99999.0.0.0 -- exiting'.format( upgrade_type) in kwarg['message'] and kwarg[ 'op'] == WALAEventOperation.AgentUpgrade] self.assertEqual(1, len(upgrade_event_msgs), "Did not find the event indicating that the agent was upgraded. Got: {0}".format( mock_telemetry.call_args_list)) def __assert_agent_directories_available(self, versions): for version in versions: self.assertTrue(os.path.exists(self.agent_dir(version)), "Agent directory {0} not found".format(version)) def __assert_agent_directories_exist_and_others_dont_exist(self, versions): self.__assert_agent_directories_available(versions=versions) other_agents = [agent_dir for agent_dir in self.agent_dirs() if agent_dir not in [self.agent_dir(version) for version in versions]] self.assertFalse(any(other_agents), "All other agents should be purged from agent dir: {0}".format(other_agents)) def __assert_no_agent_upgrade_telemetry(self, mock_telemetry): self.assertEqual(0, len([kwarg['message'] for _, kwarg in mock_telemetry.call_args_list if "Agent upgrade discovered, updating to" in kwarg['message'] and kwarg[ 'op'] == WALAEventOperation.AgentUpgrade]), "Unwanted upgrade") def __assert_ga_version_in_status(self, aggregate_status, version=str(CURRENT_VERSION)): self.assertIsNotNone(aggregate_status, "Status should be reported") self.assertEqual(aggregate_status['aggregateStatus']['guestAgentStatus']['version'], version, "Status should be reported from the Current version") self.assertEqual(aggregate_status['aggregateStatus']['guestAgentStatus']['status'], 'Ready', "Guest Agent should be reported as Ready") def test_it_should_upgrade_agent_on_process_start_if_auto_upgrade_enabled(self): with self.__get_update_handler(iterations=10) as (update_handler, mock_telemetry): update_handler.run(debug=True) self.__assert_exit_code_successful(update_handler) self.assertEqual(1, update_handler.get_iterations(), "Update handler should've exited after the first run") self.__assert_agent_directories_available(versions=["99999.0.0.0"]) self.__assert_upgrade_telemetry_emitted(mock_telemetry) def test_it_should_download_new_agents_and_not_auto_upgrade_if_not_permitted(self): no_of_iterations = 10 data_file = DATA_FILE.copy() data_file['ga_manifest'] = "wire/ga_manifest_no_upgrade.xml" def reload_conf(url, protocol): mock_wire_data = protocol.mock_wire_data # This function reloads the conf mid-run to mimic an actual customer scenario if HttpRequestPredicates.is_ga_manifest_request(url) and mock_wire_data.call_counts["manifest_of_ga.xml"] >= no_of_iterations/2: reload_conf.call_count += 1 # Ensure the first set of versions were downloaded as part of the first manifest self.__assert_agent_directories_available(versions=["1.0.0", "1.1.0", "1.2.0"]) # As per our current agent upgrade model, we don't rely on an incarnation update to upgrade the agent. Mocking the same mock_wire_data.data_files["ga_manifest"] = "wire/ga_manifest.xml" mock_wire_data.reload() reload_conf.call_count = 0 with self.__get_update_handler(iterations=no_of_iterations, test_data=data_file, hotfix_frequency=10, normal_frequency=10, reload_conf=reload_conf) as (update_handler, mock_telemetry): update_handler.run(debug=True) self.assertGreater(reload_conf.call_count, 0, "Ensure the conf reload was called") self.__assert_exit_code_successful(update_handler) self.assertEqual(no_of_iterations, update_handler.get_iterations(), "Update handler should've run its course") # Ensure the new agent versions were also downloaded once the manifest was updated self.__assert_agent_directories_available(versions=["2.0.0", "2.1.0", "99999.0.0.0"]) self.__assert_no_agent_upgrade_telemetry(mock_telemetry) def test_it_should_upgrade_agent_in_given_time_window_if_permitted(self): data_file = DATA_FILE.copy() data_file['ga_manifest'] = "wire/ga_manifest_no_upgrade.xml" def reload_conf(url, protocol): mock_wire_data = protocol.mock_wire_data # This function reloads the conf mid-run to mimic an actual customer scenario if HttpRequestPredicates.is_ga_manifest_request(url) and mock_wire_data.call_counts["manifest_of_ga.xml"] >= 2: reload_conf.call_count += 1 # Ensure no new agent available so far self.assertFalse(os.path.exists(self.agent_dir("99999.0.0.0")), "New agent directory should not be found") # As per our current agent upgrade model, we don't rely on an incarnation update to upgrade the agent. Mocking the same mock_wire_data.data_files["ga_manifest"] = "wire/ga_manifest.xml" mock_wire_data.reload() reload_conf.call_count = 0 test_normal_frequency = 0.1 with self.__get_update_handler(iterations=50, test_data=data_file, reload_conf=reload_conf, normal_frequency=test_normal_frequency) as (update_handler, mock_telemetry): start_time = time.time() update_handler.run(debug=True) diff = time.time() - start_time self.assertGreater(reload_conf.call_count, 0, "Ensure the conf reload was called") self.__assert_exit_code_successful(update_handler) self.assertGreaterEqual(update_handler.get_iterations(), 3, "Update handler should've run at least until the new GA was available") # A bare-bone check to ensure that the agent waited for the new agent at least for the preset frequency time self.assertGreater(diff, test_normal_frequency, "The test run should be at least greater than the set frequency") self.__assert_agent_directories_available(versions=["99999.0.0.0"]) self.__assert_upgrade_telemetry_emitted(mock_telemetry) def test_it_should_not_auto_upgrade_if_auto_update_disabled(self): with self.__get_update_handler(iterations=10) as (update_handler, mock_telemetry): with patch("azurelinuxagent.common.conf.get_autoupdate_enabled", return_value=False): update_handler.run(debug=True) self.__assert_exit_code_successful(update_handler) self.assertGreaterEqual(update_handler.get_iterations(), 10, "Update handler should've run 10 times") self.__assert_no_agent_upgrade_telemetry(mock_telemetry) self.assertFalse(os.path.exists(self.agent_dir("99999.0.0.0")), "New agent directory should not be found") def test_it_should_not_auto_upgrade_if_corresponding_time_not_elapsed(self): # On Normal upgrade, should not upgrade if Hotfix time elapsed no_of_iterations = 10 data_file = DATA_FILE.copy() data_file['ga_manifest'] = "wire/ga_manifest_no_upgrade.xml" def reload_conf(url, protocol): mock_wire_data = protocol.mock_wire_data # This function reloads the conf mid-run to mimic an actual customer scenario if HttpRequestPredicates.is_ga_manifest_request(url) and mock_wire_data.call_counts["manifest_of_ga.xml"] >= no_of_iterations / 2: reload_conf.call_count += 1 # As per our current agent upgrade model, we don't rely on an incarnation update to upgrade the agent. Mocking the same mock_wire_data.data_files["ga_manifest"] = "wire/ga_manifest.xml" mock_wire_data.reload() reload_conf.call_count = 0 with self.__get_update_handler(iterations=no_of_iterations, test_data=data_file, hotfix_frequency=0.01, normal_frequency=10, reload_conf=reload_conf) as (update_handler, mock_telemetry): update_handler.run(debug=True) self.assertGreater(reload_conf.call_count, 0, "Ensure the conf reload was called") self.__assert_exit_code_successful(update_handler) self.assertEqual(no_of_iterations, update_handler.get_iterations(), "Update handler didn't run completely") self.__assert_no_agent_upgrade_telemetry(mock_telemetry) upgrade_event_msgs = [kwarg['message'] for _, kwarg in mock_telemetry.call_args_list if kwarg['op'] == WALAEventOperation.AgentUpgrade] self.assertGreater(len([msg for msg in upgrade_event_msgs if 'Discovered new {0} upgrade WALinuxAgent-99999.0.0.0; Will upgrade on or after'.format( AgentUpgradeType.Normal) in msg]), 0, "Error message not propagated properly") def test_it_should_download_only_requested_version_if_available(self): data_file = mockwiredata.DATA_FILE.copy() data_file["ext_conf"] = "wire/ext_conf_requested_version.xml" with self.__get_update_handler(test_data=data_file) as (update_handler, mock_telemetry): with patch.object(conf, "get_enable_ga_versioning", return_value=True): update_handler.run(debug=True) self.__assert_exit_code_successful(update_handler) self.__assert_upgrade_telemetry_emitted_for_requested_version(mock_telemetry, version="9.9.9.10") self.__assert_agent_directories_exist_and_others_dont_exist(versions=["9.9.9.10"]) def test_it_should_cleanup_all_agents_except_requested_version_and_current_version(self): data_file = mockwiredata.DATA_FILE.copy() data_file["ext_conf"] = "wire/ext_conf_requested_version.xml" # Set the test environment by adding 20 random agents to the agent directory self.prepare_agents() self.assertEqual(20, self.agent_count(), "Agent directories not set properly") with self.__get_update_handler(test_data=data_file) as (update_handler, mock_telemetry): with patch.object(conf, "get_enable_ga_versioning", return_value=True): update_handler.run(debug=True) self.__assert_exit_code_successful(update_handler) self.__assert_upgrade_telemetry_emitted_for_requested_version(mock_telemetry, version="9.9.9.10") self.__assert_agent_directories_exist_and_others_dont_exist(versions=["9.9.9.10", str(CURRENT_VERSION)]) def test_it_should_not_update_if_requested_version_not_found_in_manifest(self): data_file = mockwiredata.DATA_FILE.copy() data_file["ext_conf"] = "wire/ext_conf_missing_requested_version.xml" with self.__get_update_handler(test_data=data_file) as (update_handler, mock_telemetry): with patch.object(conf, "get_enable_ga_versioning", return_value=True): update_handler.run(debug=True) self.__assert_exit_code_successful(update_handler) self.__assert_no_agent_upgrade_telemetry(mock_telemetry) agent_msgs = [kwarg for _, kwarg in mock_telemetry.call_args_list if kwarg['op'] in (WALAEventOperation.AgentUpgrade, WALAEventOperation.Download)] # This will throw if corresponding message not found so not asserting on that requested_version_found = next(kwarg for kwarg in agent_msgs if "Found requested version in manifest: 5.2.1.0 for goal state incarnation_1" in kwarg['message']) self.assertTrue(requested_version_found['is_success'], "The requested version found op should be reported as a success") skipping_update = next(kwarg for kwarg in agent_msgs if "No matching package found in the agent manifest for requested version: 5.2.1.0 in goal state incarnation_1, skipping agent update" in kwarg['message']) self.assertEqual(skipping_update['version'], FlexibleVersion("5.2.1.0"), "The not found message should be reported from requested agent version") self.assertFalse(skipping_update['is_success'], "The not found op should be reported as a failure") def test_it_should_only_try_downloading_requested_version_on_new_incarnation(self): no_of_iterations = 1000 # Set the test environment by adding 20 random agents to the agent directory self.prepare_agents() self.assertEqual(20, self.agent_count(), "Agent directories not set properly") def reload_conf(url, protocol): mock_wire_data = protocol.mock_wire_data # This function reloads the conf mid-run to mimic an actual customer scenario if HttpRequestPredicates.is_goal_state_request(url) and mock_wire_data.call_counts[ "goalstate"] >= 10 and mock_wire_data.call_counts["goalstate"] < 15: # Ensure we didn't try to download any agents except during the incarnation change self.__assert_agent_directories_exist_and_others_dont_exist(versions=[str(CURRENT_VERSION)]) # Update the requested version to "99999.0.0.0" update_handler._protocol.mock_wire_data.set_extension_config_requested_version("99999.0.0.0") reload_conf.call_count += 1 self._add_write_permission_to_goal_state_files() reload_conf.incarnation += 1 mock_wire_data.set_incarnation(reload_conf.incarnation) reload_conf.call_count = 0 reload_conf.incarnation = 2 data_file = mockwiredata.DATA_FILE.copy() data_file["ext_conf"] = "wire/ext_conf_requested_version.xml" with self.__get_update_handler(iterations=no_of_iterations, test_data=data_file, reload_conf=reload_conf, normal_frequency=0.01, hotfix_frequency=0.01) as (update_handler, mock_telemetry): with patch.object(conf, "get_enable_ga_versioning", return_value=True): update_handler._protocol.mock_wire_data.set_extension_config_requested_version(str(CURRENT_VERSION)) update_handler._protocol.mock_wire_data.set_incarnation(2) update_handler.run(debug=True) self.assertGreaterEqual(reload_conf.call_count, 1, "Reload conf not updated as expected") self.__assert_exit_code_successful(update_handler) self.__assert_upgrade_telemetry_emitted_for_requested_version(mock_telemetry) self.__assert_agent_directories_exist_and_others_dont_exist(versions=["99999.0.0.0", str(CURRENT_VERSION)]) self.assertEqual(update_handler._protocol.mock_wire_data.call_counts['agentArtifact'], 1, "only 1 agent should've been downloaded - 1 per incarnation") self.assertEqual(update_handler._protocol.mock_wire_data.call_counts["manifest_of_ga.xml"], 1, "only 1 agent manifest call should've been made - 1 per incarnation") def test_it_should_fallback_to_old_update_logic_if_requested_version_not_available(self): no_of_iterations = 100 # Set the test environment by adding 20 random agents to the agent directory self.prepare_agents() self.assertEqual(20, self.agent_count(), "Agent directories not set properly") def reload_conf(url, protocol): mock_wire_data = protocol.mock_wire_data # This function reloads the conf mid-run to mimic an actual customer scenario if HttpRequestPredicates.is_goal_state_request(url) and mock_wire_data.call_counts[ "goalstate"] >= 5: reload_conf.call_count += 1 # By this point, the GS with requested version should've been executed. Verify that self.__assert_agent_directories_exist_and_others_dont_exist(versions=[str(CURRENT_VERSION)]) # Update the ext-conf and incarnation and remove requested versions from GS, # this should download all versions requested in config mock_wire_data.data_files["ext_conf"] = "wire/ext_conf.xml" mock_wire_data.reload() self._add_write_permission_to_goal_state_files() reload_conf.incarnation += 1 mock_wire_data.set_incarnation(reload_conf.incarnation) reload_conf.call_count = 0 reload_conf.incarnation = 2 data_file = mockwiredata.DATA_FILE.copy() data_file["ext_conf"] = "wire/ext_conf_requested_version.xml" with self.__get_update_handler(iterations=no_of_iterations, test_data=data_file, reload_conf=reload_conf, normal_frequency=0.001) as (update_handler, mock_telemetry): with patch.object(conf, "get_enable_ga_versioning", return_value=True): update_handler._protocol.mock_wire_data.set_extension_config_requested_version(str(CURRENT_VERSION)) update_handler._protocol.mock_wire_data.set_incarnation(2) update_handler.run(debug=True) self.assertGreater(reload_conf.call_count, 0, "Reload conf not updated") self.__assert_exit_code_successful(update_handler) self.__assert_upgrade_telemetry_emitted(mock_telemetry) self.__assert_agent_directories_exist_and_others_dont_exist( versions=["1.0.0", "1.1.0", "1.2.0", "2.0.0", "2.1.0", "9.9.9.10", "99999.0.0.0", str(CURRENT_VERSION)]) def test_it_should_not_download_anything_if_requested_version_is_current_version_and_delete_all_agents(self): data_file = mockwiredata.DATA_FILE.copy() data_file["ext_conf"] = "wire/ext_conf_requested_version.xml" # Set the test environment by adding 20 random agents to the agent directory self.prepare_agents() self.assertEqual(20, self.agent_count(), "Agent directories not set properly") with self.__get_update_handler(test_data=data_file) as (update_handler, mock_telemetry): with patch.object(conf, "get_enable_ga_versioning", return_value=True): update_handler._protocol.mock_wire_data.set_extension_config_requested_version(str(CURRENT_VERSION)) update_handler._protocol.mock_wire_data.set_incarnation(2) update_handler.run(debug=True) self.__assert_exit_code_successful(update_handler) self.__assert_no_agent_upgrade_telemetry(mock_telemetry) self.__assert_agent_directories_exist_and_others_dont_exist(versions=[str(CURRENT_VERSION)]) def test_it_should_skip_wait_to_update_if_requested_version_available(self): no_of_iterations = 100 def reload_conf(url, protocol): mock_wire_data = protocol.mock_wire_data # This function reloads the conf mid-run to mimic an actual customer scenario if HttpRequestPredicates.is_goal_state_request(url) and mock_wire_data.call_counts["goalstate"] >= 5: reload_conf.call_count += 1 # Assert GA version from status to ensure agent is running fine from the current version self.__assert_ga_version_in_status(protocol.aggregate_status) # Update the ext-conf and incarnation and add requested version from GS mock_wire_data.data_files["ext_conf"] = "wire/ext_conf_requested_version.xml" data_file['ga_manifest'] = "wire/ga_manifest.xml" mock_wire_data.reload() self._add_write_permission_to_goal_state_files() mock_wire_data.set_incarnation(2) reload_conf.call_count = 0 data_file = mockwiredata.DATA_FILE.copy() data_file['ga_manifest'] = "wire/ga_manifest_no_upgrade.xml" with self.__get_update_handler(iterations=no_of_iterations, test_data=data_file, reload_conf=reload_conf, normal_frequency=10, hotfix_frequency=10) as (update_handler, mock_telemetry): with patch.object(conf, "get_enable_ga_versioning", return_value=True): update_handler.run(debug=True) self.assertGreater(reload_conf.call_count, 0, "Reload conf not updated") self.assertLess(update_handler.get_iterations(), no_of_iterations, "The code should've exited as soon as requested version was found") self.__assert_exit_code_successful(update_handler) self.__assert_upgrade_telemetry_emitted_for_requested_version(mock_telemetry, version="9.9.9.10") def test_it_should_blacklist_current_agent_on_downgrade(self): # Create Agent directory for current agent self.prepare_agents(count=1) self.assertTrue(os.path.exists(self.agent_dir(CURRENT_VERSION))) self.assertFalse(next(agent for agent in self.agents() if agent.version == CURRENT_VERSION).is_blacklisted, "The current agent should not be blacklisted") downgraded_version = "1.2.0" data_file = mockwiredata.DATA_FILE.copy() data_file["ext_conf"] = "wire/ext_conf_requested_version.xml" with self.__get_update_handler(test_data=data_file) as (update_handler, mock_telemetry): with patch.object(conf, "get_enable_ga_versioning", return_value=True): update_handler._protocol.mock_wire_data.set_extension_config_requested_version(downgraded_version) update_handler._protocol.mock_wire_data.set_incarnation(2) try: set_daemon_version("1.0.0.0") update_handler.run(debug=True) finally: os.environ.pop(DAEMON_VERSION_ENV_VARIABLE) self.__assert_exit_code_successful(update_handler) self.__assert_upgrade_telemetry_emitted_for_requested_version(mock_telemetry, upgrade=False, version=downgraded_version) current_agent = next(agent for agent in self.agents() if agent.version == CURRENT_VERSION) self.assertTrue(current_agent.is_blacklisted, "The current agent should be blacklisted") self.assertEqual(current_agent.error.reason, "Blacklisting the agent {0} since a downgrade was requested in the GoalState, " "suggesting that we really don't want to execute any extensions using this version".format(CURRENT_VERSION), "Invalid reason specified for blacklisting agent") def test_it_should_not_downgrade_below_daemon_version(self): data_file = mockwiredata.DATA_FILE.copy() data_file["ext_conf"] = "wire/ext_conf_requested_version.xml" with self.__get_update_handler(test_data=data_file) as (update_handler, mock_telemetry): with patch.object(conf, "get_enable_ga_versioning", return_value=True): update_handler._protocol.mock_wire_data.set_extension_config_requested_version("1.0.0.0") update_handler._protocol.mock_wire_data.set_incarnation(2) try: set_daemon_version("1.2.3.4") update_handler.run(debug=True) finally: os.environ.pop(DAEMON_VERSION_ENV_VARIABLE) self.__assert_exit_code_successful(update_handler) upgrade_msgs = [kwarg for _, kwarg in mock_telemetry.call_args_list if kwarg['op'] == WALAEventOperation.AgentUpgrade] # This will throw if corresponding message not found so not asserting on that requested_version_found = next(kwarg for kwarg in upgrade_msgs if "Found requested version in manifest: 1.0.0.0 for goal state incarnation_2" in kwarg[ 'message']) self.assertTrue(requested_version_found['is_success'], "The requested version found op should be reported as a success") skipping_update = next(kwarg for kwarg in upgrade_msgs if "Can't process the upgrade as the requested version: 1.0.0.0 is < current daemon version: 1.2.3.4" in kwarg['message']) self.assertFalse(skipping_update['is_success'], "Failed Event should be reported as a failure") self.__assert_ga_version_in_status(update_handler._protocol.aggregate_status) @patch('azurelinuxagent.ga.update.get_collect_telemetry_events_handler') @patch('azurelinuxagent.ga.update.get_send_telemetry_events_handler') @patch('azurelinuxagent.ga.update.get_collect_logs_handler') @patch('azurelinuxagent.ga.update.get_monitor_handler') @patch('azurelinuxagent.ga.update.get_env_handler') class MonitorThreadTest(AgentTestCaseWithGetVmSizeMock): def setUp(self): super(MonitorThreadTest, self).setUp() self.event_patch = patch('azurelinuxagent.common.event.add_event') current_thread().name = "ExtHandler" protocol = Mock() self.update_handler = get_update_handler() self.update_handler.protocol_util = Mock() self.update_handler.protocol_util.get_protocol = Mock(return_value=protocol) clear_singleton_instances(ProtocolUtil) def _test_run(self, invocations=1): def iterator(*_, **__): iterator.count += 1 if iterator.count <= invocations: return True return False iterator.count = 0 with patch('os.getpid', return_value=42): with patch.object(UpdateHandler, '_is_orphaned') as mock_is_orphaned: mock_is_orphaned.__get__ = Mock(return_value=False) with patch.object(UpdateHandler, 'is_running') as mock_is_running: mock_is_running.__get__ = Mock(side_effect=iterator) with patch('azurelinuxagent.ga.exthandlers.get_exthandlers_handler'): with patch('azurelinuxagent.ga.remoteaccess.get_remote_access_handler'): with patch('azurelinuxagent.ga.update.initialize_event_logger_vminfo_common_parameters'): with patch('azurelinuxagent.common.cgroupapi.CGroupsApi.cgroups_supported', return_value=False): # skip all cgroup stuff with patch('azurelinuxagent.ga.update.is_log_collection_allowed', return_value=True): with patch('time.sleep'): with patch('sys.exit'): self.update_handler.run() def _setup_mock_thread_and_start_test_run(self, mock_thread, is_alive=True, invocations=0): thread = MagicMock() thread.run = MagicMock() thread.is_alive = MagicMock(return_value=is_alive) thread.start = MagicMock() mock_thread.return_value = thread self._test_run(invocations=invocations) return thread def test_start_threads(self, mock_env, mock_monitor, mock_collect_logs, mock_telemetry_send_events, mock_telemetry_collector): def _get_mock_thread(): thread = MagicMock() thread.run = MagicMock() return thread all_threads = [mock_telemetry_send_events, mock_telemetry_collector, mock_env, mock_monitor, mock_collect_logs] for thread in all_threads: thread.return_value = _get_mock_thread() self._test_run(invocations=0) for thread in all_threads: self.assertEqual(1, thread.call_count) self.assertEqual(1, thread().run.call_count) def test_check_if_monitor_thread_is_alive(self, _, mock_monitor, *args): # pylint: disable=unused-argument mock_monitor_thread = self._setup_mock_thread_and_start_test_run(mock_monitor, is_alive=True, invocations=1) self.assertEqual(1, mock_monitor.call_count) self.assertEqual(1, mock_monitor_thread.run.call_count) self.assertEqual(1, mock_monitor_thread.is_alive.call_count) self.assertEqual(0, mock_monitor_thread.start.call_count) def test_check_if_env_thread_is_alive(self, mock_env, *args): # pylint: disable=unused-argument mock_env_thread = self._setup_mock_thread_and_start_test_run(mock_env, is_alive=True, invocations=1) self.assertEqual(1, mock_env.call_count) self.assertEqual(1, mock_env_thread.run.call_count) self.assertEqual(1, mock_env_thread.is_alive.call_count) self.assertEqual(0, mock_env_thread.start.call_count) def test_restart_monitor_thread_if_not_alive(self, _, mock_monitor, *args): # pylint: disable=unused-argument mock_monitor_thread = self._setup_mock_thread_and_start_test_run(mock_monitor, is_alive=False, invocations=1) self.assertEqual(1, mock_monitor.call_count) self.assertEqual(1, mock_monitor_thread.run.call_count) self.assertEqual(1, mock_monitor_thread.is_alive.call_count) self.assertEqual(1, mock_monitor_thread.start.call_count) def test_restart_env_thread_if_not_alive(self, mock_env, *args): # pylint: disable=unused-argument mock_env_thread = self._setup_mock_thread_and_start_test_run(mock_env, is_alive=False, invocations=1) self.assertEqual(1, mock_env.call_count) self.assertEqual(1, mock_env_thread.run.call_count) self.assertEqual(1, mock_env_thread.is_alive.call_count) self.assertEqual(1, mock_env_thread.start.call_count) def test_restart_monitor_thread(self, _, mock_monitor, *args): # pylint: disable=unused-argument mock_monitor_thread = self._setup_mock_thread_and_start_test_run(mock_monitor, is_alive=False, invocations=1) self.assertEqual(True, mock_monitor.called) self.assertEqual(True, mock_monitor_thread.run.called) self.assertEqual(True, mock_monitor_thread.is_alive.called) self.assertEqual(True, mock_monitor_thread.start.called) def test_restart_env_thread(self, mock_env, *args): # pylint: disable=unused-argument mock_env_thread = self._setup_mock_thread_and_start_test_run(mock_env, is_alive=False, invocations=1) self.assertEqual(True, mock_env.called) self.assertEqual(True, mock_env_thread.run.called) self.assertEqual(True, mock_env_thread.is_alive.called) self.assertEqual(True, mock_env_thread.start.called) class ChildMock(Mock): def __init__(self, return_value=0, side_effect=None): Mock.__init__(self, return_value=return_value, side_effect=side_effect) self.poll = Mock(return_value=return_value, side_effect=side_effect) self.wait = Mock(return_value=return_value, side_effect=side_effect) class GoalStateMock(object): def __init__(self, incarnation, family, versions): if versions is None: versions = [] self.incarnation = incarnation self.extensions_goal_state = Mock() self.extensions_goal_state.id = incarnation self.extensions_goal_state.agent_families = GoalStateMock._create_agent_families(family, versions) agent_manifest = Mock() agent_manifest.pkg_list = GoalStateMock._create_packages(versions) self.fetch_agent_manifest = Mock(return_value=agent_manifest) @staticmethod def _create_agent_families(family, versions): families = [] if len(versions) > 0 and family is not None: manifest = VMAgentFamily(name=family) for i in range(0, 10): manifest.uris.append("https://nowhere.msft/agent/{0}".format(i)) families.append(manifest) return families @staticmethod def _create_packages(versions): packages = ExtHandlerPackageList() for version in versions: package = ExtHandlerPackage(str(version)) for i in range(0, 5): package_uri = "https://nowhere.msft/agent_pkg/{0}".format(i) package.uris.append(package_uri) packages.versions.append(package) return packages class ProtocolMock(object): def __init__(self, family="TestAgent", etag=42, versions=None, client=None): self.family = family self.client = client self.call_counts = { "update_goal_state": 0 } self._goal_state = GoalStateMock(etag, family, versions) self.goal_state_is_stale = False self.etag = etag self.versions = versions if versions is not None else [] def emulate_stale_goal_state(self): self.goal_state_is_stale = True def get_protocol(self): return self def get_goal_state(self): return self._goal_state def update_goal_state(self): self.call_counts["update_goal_state"] += 1 class TimeMock(Mock): def __init__(self, time_increment=1): Mock.__init__(self) self.next_time = time.time() self.time_call_count = 0 self.time_increment = time_increment self.sleep_interval = None def sleep(self, n): self.sleep_interval = n def time(self): self.time_call_count += 1 current_time = self.next_time self.next_time += self.time_increment return current_time class TryUpdateGoalStateTestCase(HttpRequestPredicates, AgentTestCase): """ Tests for UpdateHandler._try_update_goal_state() """ def test_it_should_return_true_on_success(self): update_handler = get_update_handler() with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: self.assertTrue(update_handler._try_update_goal_state(protocol), "try_update_goal_state should have succeeded") def test_it_should_return_false_on_failure(self): with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: def http_get_handler(url, *_, **__): if self.is_goal_state_request(url): return HttpError('Exception to fake an error retrieving the goal state') return None protocol.set_http_handlers(http_get_handler=http_get_handler) update_handler = get_update_handler() self.assertFalse(update_handler._try_update_goal_state(protocol), "try_update_goal_state should have failed") def test_it_should_update_the_goal_state(self): update_handler = get_update_handler() with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: protocol.mock_wire_data.set_incarnation(12345) # the first goal state should produce an update update_handler._try_update_goal_state(protocol) self.assertEqual(update_handler._goal_state.incarnation, '12345', "The goal state was not updated (received unexpected incarnation)") # no changes in the goal state should not produce an update update_handler._try_update_goal_state(protocol) self.assertEqual(update_handler._goal_state.incarnation, '12345', "The goal state should not be updated (received unexpected incarnation)") # a new goal state should produce an update protocol.mock_wire_data.set_incarnation(6789) update_handler._try_update_goal_state(protocol) self.assertEqual(update_handler._goal_state.incarnation, '6789', "The goal state was not updated (received unexpected incarnation)") def test_it_should_log_errors_only_when_the_error_state_changes(self): with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: def http_get_handler(url, *_, **__): if self.is_goal_state_request(url): if fail_goal_state_request: return HttpError('Exception to fake an error retrieving the goal state') return None protocol.set_http_handlers(http_get_handler=http_get_handler) @contextlib.contextmanager def create_log_and_telemetry_mocks(): with patch("azurelinuxagent.ga.update.logger", autospec=True) as logger_patcher: with patch("azurelinuxagent.ga.update.add_event") as add_event_patcher: yield logger_patcher, add_event_patcher calls_to_strings = lambda calls: (str(c) for c in calls) filter_calls = lambda calls, regex=None: (c for c in calls_to_strings(calls) if regex is None or re.match(regex, c)) logger_calls = lambda regex=None: [m for m in filter_calls(logger.method_calls, regex)] # pylint: disable=used-before-assignment,unnecessary-comprehension errors = lambda: logger_calls(r'call.error\(.*Error fetching the goal state.*') periodic_errors = lambda: logger_calls(r'call.error\(.*Fetching the goal state is still failing*') success_messages = lambda: logger_calls(r'call.info\(.*Fetching the goal state recovered from previous errors.*') telemetry_calls = lambda regex=None: [m for m in filter_calls(add_event.mock_calls, regex)] # pylint: disable=used-before-assignment,unnecessary-comprehension goal_state_events = lambda: telemetry_calls(r".*op='FetchGoalState'.*") # # Initially calls to retrieve the goal state are successful... # update_handler = get_update_handler() fail_goal_state_request = False with create_log_and_telemetry_mocks() as (logger, add_event): update_handler._try_update_goal_state(protocol) lc = logger_calls() self.assertTrue(len(lc) == 0, "A successful call should not produce any log messages: [{0}]".format(lc)) tc = telemetry_calls() self.assertTrue(len(tc) == 0, "A successful call should not produce any telemetry events: [{0}]".format(tc)) # # ... then an error happens... # fail_goal_state_request = True with create_log_and_telemetry_mocks() as (logger, add_event): update_handler._try_update_goal_state(protocol) e = errors() self.assertEqual(1, len(e), "A failure should have produced an error: [{0}]".format(e)) gs = goal_state_events() self.assertTrue(len(gs) == 1 and 'is_success=False' in gs[0], "A failure should produce a telemetry event (success=false): [{0}]".format(gs)) # # ... and errors continue happening... # with create_log_and_telemetry_mocks() as (logger, add_event): for _ in range(5): update_handler._update_goal_state_last_error_report = datetime.now() + timedelta(days=1) update_handler._try_update_goal_state(protocol) e = errors() pe = periodic_errors() self.assertEqual(2, len(e), "Two additional errors should have been reported: [{0}]".format(e)) self.assertEqual(len(pe), 3, "Subsequent failures should produce periodic errors: [{0}]".format(pe)) tc = telemetry_calls() self.assertTrue(len(tc) == 5, "The failures should have produced telemetry events. Got: [{0}]".format(tc)) # # ... until we finally succeed # fail_goal_state_request = False with create_log_and_telemetry_mocks() as (logger, add_event): update_handler._try_update_goal_state(protocol) s = success_messages() e = errors() pe = periodic_errors() self.assertEqual(len(s), 1, "Recovering after failures should have produced an info message: [{0}]".format(s)) self.assertTrue(len(e) == 0 and len(pe) == 0, "Recovering after failures should have not produced any errors: [{0}] [{1}]".format(e, pe)) gs = goal_state_events() self.assertTrue(len(gs) == 1 and 'is_success=True' in gs[0], "Recovering after failures should produce a telemetry event (success=true): [{0}]".format(gs)) def _create_update_handler(): """ Creates an UpdateHandler in which agent updates are mocked as a no-op. """ update_handler = get_update_handler() update_handler._download_agent_if_upgrade_available = Mock(return_value=False) return update_handler @contextlib.contextmanager def _mock_exthandlers_handler(extension_statuses=None): """ Creates an ExtHandlersHandler that doesn't actually handle any extensions, but that returns status for 1 extension. The returned ExtHandlersHandler uses a mock WireProtocol, and both the run() and report_ext_handlers_status() are mocked. The mock run() is a no-op. If a list of extension_statuses is given, successive calls to the mock report_ext_handlers_status() returns a single extension with each of the statuses in the list. If extension_statuses is omitted all calls to report_ext_handlers_status() return a single extension with a success status. """ def create_vm_status(extension_status): vm_status = VMStatus(status="Ready", message="Ready") vm_status.vmAgent.extensionHandlers = [ExtHandlerStatus()] vm_status.vmAgent.extensionHandlers[0].extension_status = ExtensionStatus(name="TestExtension") vm_status.vmAgent.extensionHandlers[0].extension_status.status = extension_status return vm_status with mock_wire_protocol(DATA_FILE) as protocol: exthandlers_handler = ExtHandlersHandler(protocol) exthandlers_handler.run = Mock() if extension_statuses is None: exthandlers_handler.report_ext_handlers_status = Mock(return_value=create_vm_status(ExtensionStatusValue.success)) else: exthandlers_handler.report_ext_handlers_status = Mock(side_effect=[create_vm_status(s) for s in extension_statuses]) exthandlers_handler.get_ext_handlers_status_debug_info = Mock(return_value='') yield exthandlers_handler class ProcessGoalStateTestCase(AgentTestCase): """ Tests for UpdateHandler._process_goal_state() """ def test_it_should_process_goal_state_only_on_new_goal_state(self): with _mock_exthandlers_handler() as exthandlers_handler: update_handler = _create_update_handler() remote_access_handler = Mock() remote_access_handler.run = Mock() # process a goal state update_handler._process_goal_state(exthandlers_handler, remote_access_handler) self.assertEqual(1, exthandlers_handler.run.call_count, "exthandlers_handler.run() should have been called on the first goal state") self.assertEqual(1, exthandlers_handler.report_ext_handlers_status.call_count, "exthandlers_handler.report_ext_handlers_status() should have been called on the first goal state") self.assertEqual(1, remote_access_handler.run.call_count, "remote_access_handler.run() should have been called on the first goal state") # process the same goal state update_handler._process_goal_state(exthandlers_handler, remote_access_handler) self.assertEqual(1, exthandlers_handler.run.call_count, "exthandlers_handler.run() should have not been called on the same goal state") self.assertEqual(2, exthandlers_handler.report_ext_handlers_status.call_count, "exthandlers_handler.report_ext_handlers_status() should have been called on the same goal state") self.assertEqual(1, remote_access_handler.run.call_count, "remote_access_handler.run() should not have been called on the same goal state") # process a new goal state exthandlers_handler.protocol.mock_wire_data.set_incarnation(999) exthandlers_handler.protocol.client.update_goal_state() update_handler._process_goal_state(exthandlers_handler, remote_access_handler) self.assertEqual(2, exthandlers_handler.run.call_count, "exthandlers_handler.run() should have been called on a new goal state") self.assertEqual(3, exthandlers_handler.report_ext_handlers_status.call_count, "exthandlers_handler.report_ext_handlers_status() should have been called on a new goal state") self.assertEqual(2, remote_access_handler.run.call_count, "remote_access_handler.run() should have been called on a new goal state") def test_it_should_write_the_agent_status_to_the_history_folder(self): with _mock_exthandlers_handler() as exthandlers_handler: update_handler = _create_update_handler() remote_access_handler = Mock() remote_access_handler.run = Mock() update_handler._process_goal_state(exthandlers_handler, remote_access_handler) incarnation = exthandlers_handler.protocol.get_goal_state().incarnation matches = glob.glob(os.path.join(conf.get_lib_dir(), ARCHIVE_DIRECTORY_NAME, "*_{0}".format(incarnation))) self.assertTrue(len(matches) == 1, "Could not find the history directory for the goal state. Got: {0}".format(matches)) status_file = os.path.join(matches[0], AGENT_STATUS_FILE) self.assertTrue(os.path.exists(status_file), "Could not find {0}".format(status_file)) @staticmethod def _prepare_fast_track_goal_state(): """ Creates a set of mock wire data where the most recent goal state is a FastTrack goal state; also invokes HostPluginProtocol.fetch_vm_settings() to save the Fast Track status to disk """ # Do a query for the vmSettings; this would retrieve a FastTrack goal state and keep track of its timestamp mock_wire_data_file = mockwiredata.DATA_FILE_VM_SETTINGS.copy() with mock_wire_protocol(mock_wire_data_file) as protocol: protocol.mock_wire_data.set_etag("0123456789") _ = protocol.client.get_host_plugin().fetch_vm_settings() return mock_wire_data_file def test_it_should_mark_outdated_goal_states_on_service_restart_when_fast_track_is_disabled(self): data_file = self._prepare_fast_track_goal_state() with patch("azurelinuxagent.common.conf.get_enable_fast_track", return_value=False): with mock_wire_protocol(data_file) as protocol: with mock_update_handler(protocol) as update_handler: update_handler.run() self.assertTrue(protocol.client.get_goal_state().extensions_goal_state.is_outdated) @staticmethod def _http_get_vm_settings_handler_not_found(url, *_, **__): if HttpRequestPredicates.is_host_plugin_vm_settings_request(url): return MockHttpResponse(httpclient.NOT_FOUND) # HostGAPlugin returns 404 if the API is not supported return None def test_it_should_mark_outdated_goal_states_on_service_restart_when_host_ga_plugin_stops_supporting_vm_settings(self): data_file = self._prepare_fast_track_goal_state() with mock_wire_protocol(data_file, http_get_handler=self._http_get_vm_settings_handler_not_found) as protocol: with mock_update_handler(protocol) as update_handler: update_handler.run() self.assertTrue(protocol.client.get_goal_state().extensions_goal_state.is_outdated) def test_it_should_clear_the_timestamp_for_the_most_recent_fast_track_goal_state(self): data_file = self._prepare_fast_track_goal_state() if HostPluginProtocol.get_fast_track_timestamp() == timeutil.create_timestamp(datetime.min): raise Exception("The test setup did not save the Fast Track state") with patch("azurelinuxagent.common.conf.get_enable_fast_track", return_value=False): with mock_wire_protocol(data_file) as protocol: with mock_update_handler(protocol) as update_handler: update_handler.run() self.assertEqual(HostPluginProtocol.get_fast_track_timestamp(), timeutil.create_timestamp(datetime.min), "The Fast Track state was not cleared") def test_it_should_default_fast_track_timestamp_to_datetime_min(self): data = DATA_FILE_VM_SETTINGS.copy() # TODO: Currently, there's a limitation in the mocks where bumping the incarnation but the goal # state will cause the agent to error out while trying to write the certificates to disk. These # files have no dependencies on certs, so using them does not present that issue. # # Note that the scenario this test is representing does not depend on certificates at all, and # can be changed to use the default files when the above limitation is addressed. data["vm_settings"] = "hostgaplugin/vm_settings-fabric-no_thumbprints.json" data['goal_state'] = 'wire/goal_state_no_certs.xml' def vm_settings_no_change(url, *_, **__): if HttpRequestPredicates.is_host_plugin_vm_settings_request(url): return MockHttpResponse(httpclient.NOT_MODIFIED) return None def vm_settings_not_supported(url, *_, **__): if HttpRequestPredicates.is_host_plugin_vm_settings_request(url): return MockHttpResponse(404) return None with mock_wire_protocol(data) as protocol: def mock_live_migration(iteration): if iteration == 1: protocol.mock_wire_data.set_incarnation(2) protocol.set_http_handlers(http_get_handler=vm_settings_no_change) elif iteration == 2: protocol.mock_wire_data.set_incarnation(3) protocol.set_http_handlers(http_get_handler=vm_settings_not_supported) with mock_update_handler(protocol, 3, on_new_iteration=mock_live_migration) as update_handler: with patch("azurelinuxagent.ga.update.logger.error") as patched_error: def check_for_errors(): msg_fragment = "Error fetching the goal state:" for (args, _) in filter(lambda a: len(a) > 0, patched_error.call_args_list): if msg_fragment in args[0]: self.fail("Found error: {}".format(args[0])) update_handler.run(debug=True) check_for_errors() timestamp = protocol.client.get_host_plugin()._fast_track_timestamp self.assertEqual(timestamp, timeutil.create_timestamp(datetime.min), "Expected fast track time stamp to be set to {0}, got {1}".format(datetime.min, timestamp)) class HeartbeatTestCase(AgentTestCase): @patch("azurelinuxagent.common.logger.info") @patch("azurelinuxagent.ga.update.add_event") def test_telemetry_heartbeat_creates_event(self, patch_add_event, patch_info, *_): with mock_wire_protocol(mockwiredata.DATA_FILE) as mock_protocol: update_handler = get_update_handler() update_handler.last_telemetry_heartbeat = datetime.utcnow() - timedelta(hours=1) update_handler._send_heartbeat_telemetry(mock_protocol) self.assertEqual(1, patch_add_event.call_count) self.assertTrue(any(call_args[0] == "[HEARTBEAT] Agent {0} is running as the goal state agent {1}" for call_args in patch_info.call_args), "The heartbeat was not written to the agent's log") class AgentMemoryCheckTestCase(AgentTestCase): @patch("azurelinuxagent.common.logger.info") @patch("azurelinuxagent.ga.update.add_event") def test_check_agent_memory_usage_raises_exit_exception(self, patch_add_event, patch_info, *_): with patch("azurelinuxagent.common.cgroupconfigurator.CGroupConfigurator._Impl.check_agent_memory_usage", side_effect=AgentMemoryExceededException()): with patch('azurelinuxagent.common.conf.get_enable_agent_memory_usage_check', return_value=True): with self.assertRaises(ExitException) as context_manager: update_handler = get_update_handler() update_handler._check_agent_memory_usage() self.assertEqual(1, patch_add_event.call_count) self.assertTrue(any("Check on agent memory usage" in call_args[0] for call_args in patch_info.call_args), "The memory check was not written to the agent's log") self.assertIn("Agent {0} is reached memory limit -- exiting".format(CURRENT_AGENT), ustr(context_manager.exception), "An incorrect exception was raised") @patch("azurelinuxagent.common.logger.warn") @patch("azurelinuxagent.ga.update.add_event") def test_check_agent_memory_usage_fails(self, patch_add_event, patch_warn, *_): with patch("azurelinuxagent.common.cgroupconfigurator.CGroupConfigurator._Impl.check_agent_memory_usage", side_effect=Exception()): with patch('azurelinuxagent.common.conf.get_enable_agent_memory_usage_check', return_value=True): update_handler = get_update_handler() update_handler._check_agent_memory_usage() self.assertTrue(any("Error checking the agent's memory usage" in call_args[0] for call_args in patch_warn.call_args), "The memory check was not written to the agent's log") self.assertEqual(1, patch_add_event.call_count) add_events = [kwargs for _, kwargs in patch_add_event.call_args_list if kwargs["op"] == WALAEventOperation.AgentMemory] self.assertTrue( len(add_events) == 1, "Exactly 1 event should have been emitted when memory usage check fails. Got: {0}".format(add_events)) self.assertIn( "Error checking the agent's memory usage", add_events[0]["message"], "The error message is not correct when memory usage check failed") class GoalStateIntervalTestCase(AgentTestCase): def test_initial_goal_state_period_should_default_to_goal_state_period(self): configuration_provider = conf.ConfigurationProvider() test_file = os.path.join(self.tmp_dir, "waagent.conf") with open(test_file, "w") as file_: file_.write("Extensions.GoalStatePeriod=987654321\n") conf.load_conf_from_file(test_file, configuration_provider) self.assertEqual(987654321, conf.get_initial_goal_state_period(conf=configuration_provider)) def test_update_handler_should_use_the_default_goal_state_period(self): update_handler = get_update_handler() default = conf.get_int_default_value("Extensions.GoalStatePeriod") self.assertEqual(default, update_handler._goal_state_period, "The UpdateHanlder is not using the default goal state period") def test_update_handler_should_not_use_the_default_goal_state_period_when_extensions_are_disabled(self): with patch('azurelinuxagent.common.conf.get_extensions_enabled', return_value=False): update_handler = get_update_handler() self.assertEqual(GOAL_STATE_PERIOD_EXTENSIONS_DISABLED, update_handler._goal_state_period, "Incorrect goal state period when extensions are disabled") def test_the_default_goal_state_period_and_initial_goal_state_period_should_be_the_same(self): update_handler = get_update_handler() default = conf.get_int_default_value("Extensions.GoalStatePeriod") self.assertEqual(default, update_handler._goal_state_period, "The UpdateHanlder is not using the default goal state period") def test_update_handler_should_use_the_initial_goal_state_period_when_it_is_different_to_the_goal_state_period(self): with patch('azurelinuxagent.common.conf.get_initial_goal_state_period', return_value=99999): update_handler = get_update_handler() self.assertEqual(99999, update_handler._goal_state_period, "Expected the initial goal state period") def test_update_handler_should_use_the_initial_goal_state_period_until_the_goal_state_converges(self): initial_goal_state_period, goal_state_period = 11111, 22222 with patch('azurelinuxagent.common.conf.get_initial_goal_state_period', return_value=initial_goal_state_period): with patch('azurelinuxagent.common.conf.get_goal_state_period', return_value=goal_state_period): with _mock_exthandlers_handler([ExtensionStatusValue.transitioning, ExtensionStatusValue.success]) as exthandlers_handler: remote_access_handler = Mock() update_handler = _create_update_handler() self.assertEqual(initial_goal_state_period, update_handler._goal_state_period, "Expected the initial goal state period") # the extension is transisioning, so we should still be using the initial goal state period update_handler._process_goal_state(exthandlers_handler, remote_access_handler) self.assertEqual(initial_goal_state_period, update_handler._goal_state_period, "Expected the initial goal state period when the extension is transitioning") # the goal state converged (the extension succeeded), so we should switch to the regular goal state period update_handler._process_goal_state(exthandlers_handler, remote_access_handler) self.assertEqual(goal_state_period, update_handler._goal_state_period, "Expected the regular goal state period after the goal state converged") def test_update_handler_should_switch_to_the_regular_goal_state_period_when_the_goal_state_does_not_converges(self): initial_goal_state_period, goal_state_period = 11111, 22222 with patch('azurelinuxagent.common.conf.get_initial_goal_state_period', return_value=initial_goal_state_period): with patch('azurelinuxagent.common.conf.get_goal_state_period', return_value=goal_state_period): with _mock_exthandlers_handler([ExtensionStatusValue.transitioning, ExtensionStatusValue.transitioning]) as exthandlers_handler: remote_access_handler = Mock() update_handler = _create_update_handler() self.assertEqual(initial_goal_state_period, update_handler._goal_state_period, "Expected the initial goal state period") # the extension is transisioning, so we should still be using the initial goal state period update_handler._process_goal_state(exthandlers_handler, remote_access_handler) self.assertEqual(initial_goal_state_period, update_handler._goal_state_period, "Expected the initial goal state period when the extension is transitioning") # a new goal state arrives before the current goal state converged (the extension is transitioning), so we should switch to the regular goal state period exthandlers_handler.protocol.mock_wire_data.set_incarnation(100) update_handler._process_goal_state(exthandlers_handler, remote_access_handler) self.assertEqual(goal_state_period, update_handler._goal_state_period, "Expected the regular goal state period when the goal state does not converge") class ExtensionsSummaryTestCase(AgentTestCase): @staticmethod def _create_extensions_summary(extension_statuses): """ Creates an ExtensionsSummary from an array of (extension name, extension status) tuples """ vm_status = VMStatus(status="Ready", message="Ready") vm_status.vmAgent.extensionHandlers = [ExtHandlerStatus()] * len(extension_statuses) for i in range(len(extension_statuses)): vm_status.vmAgent.extensionHandlers[i].extension_status = ExtensionStatus(name=extension_statuses[i][0]) vm_status.vmAgent.extensionHandlers[0].extension_status.status = extension_statuses[i][1] return ExtensionsSummary(vm_status) def test_equality_operator_should_return_true_on_items_with_the_same_value(self): summary1 = ExtensionsSummaryTestCase._create_extensions_summary([("Extension 1", ExtensionStatusValue.success), ("Extension 2", ExtensionStatusValue.transitioning)]) summary2 = ExtensionsSummaryTestCase._create_extensions_summary([("Extension 1", ExtensionStatusValue.success), ("Extension 2", ExtensionStatusValue.transitioning)]) self.assertTrue(summary1 == summary2, "{0} == {1} should be True".format(summary1, summary2)) def test_equality_operator_should_return_false_on_items_with_different_values(self): summary1 = ExtensionsSummaryTestCase._create_extensions_summary([("Extension 1", ExtensionStatusValue.success), ("Extension 2", ExtensionStatusValue.transitioning)]) summary2 = ExtensionsSummaryTestCase._create_extensions_summary([("Extension 1", ExtensionStatusValue.success), ("Extension 2", ExtensionStatusValue.success)]) self.assertFalse(summary1 == summary2, "{0} == {1} should be False") summary1 = ExtensionsSummaryTestCase._create_extensions_summary([("Extension 1", ExtensionStatusValue.success)]) summary2 = ExtensionsSummaryTestCase._create_extensions_summary([("Extension 1", ExtensionStatusValue.success), ("Extension 2", ExtensionStatusValue.success)]) self.assertFalse(summary1 == summary2, "{0} == {1} should be False") def test_inequality_operator_should_return_true_on_items_with_different_values(self): summary1 = ExtensionsSummaryTestCase._create_extensions_summary([("Extension 1", ExtensionStatusValue.success), ("Extension 2", ExtensionStatusValue.transitioning)]) summary2 = ExtensionsSummaryTestCase._create_extensions_summary([("Extension 1", ExtensionStatusValue.success), ("Extension 2", ExtensionStatusValue.success)]) self.assertTrue(summary1 != summary2, "{0} != {1} should be True".format(summary1, summary2)) summary1 = ExtensionsSummaryTestCase._create_extensions_summary([("Extension 1", ExtensionStatusValue.success)]) summary2 = ExtensionsSummaryTestCase._create_extensions_summary([("Extension 1", ExtensionStatusValue.success), ("Extension 2", ExtensionStatusValue.success)]) self.assertTrue(summary1 != summary2, "{0} != {1} should be True") def test_inequality_operator_should_return_false_on_items_with_same_value(self): summary1 = ExtensionsSummaryTestCase._create_extensions_summary([("Extension 1", ExtensionStatusValue.success), ("Extension 2", ExtensionStatusValue.transitioning)]) summary2 = ExtensionsSummaryTestCase._create_extensions_summary([("Extension 1", ExtensionStatusValue.success), ("Extension 2", ExtensionStatusValue.transitioning)]) self.assertFalse(summary1 != summary2, "{0} != {1} should be False".format(summary1, summary2)) if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/pa/000077500000000000000000000000001446033677600157525ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/pa/__init__.py000066400000000000000000000011651446033677600200660ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # WALinuxAgent-2.9.1.1/tests/pa/test_deprovision.py000066400000000000000000000127361446033677600217350ustar00rootroot00000000000000# Copyright 2016 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import tempfile import unittest import azurelinuxagent.common.utils.fileutil as fileutil from azurelinuxagent.pa.deprovision import get_deprovision_handler from azurelinuxagent.pa.deprovision.default import DeprovisionHandler from tests.tools import AgentTestCase, distros, Mock, patch class TestDeprovision(AgentTestCase): @patch('signal.signal') @patch('azurelinuxagent.common.osutil.get_osutil') @patch('azurelinuxagent.common.protocol.util.get_protocol_util') @patch('azurelinuxagent.pa.deprovision.default.read_input') def test_confirmation(self, mock_read, mock_protocol, mock_util, mock_signal): # pylint: disable=unused-argument dh = DeprovisionHandler() dh.setup = Mock() dh.setup.return_value = ([], []) dh.do_actions = Mock() # Do actions if confirmed mock_read.return_value = "y" dh.run() self.assertEqual(1, dh.do_actions.call_count) # Skip actions if not confirmed mock_read.return_value = "n" dh.run() self.assertEqual(1, dh.do_actions.call_count) # Do actions if forced mock_read.return_value = "n" dh.run(force=True) self.assertEqual(2, dh.do_actions.call_count) @distros("ubuntu") @patch('azurelinuxagent.common.conf.get_lib_dir') def test_del_lib_dir_files(self, distro_name, distro_version, distro_full_name, mock_conf): dirs = [ 'WALinuxAgent-2.2.26/config', 'Microsoft.Azure.Extensions.CustomScript-2.0.6/config', 'Microsoft.Azure.Extensions.CustomScript-2.0.6/status' ] files = [ 'HostingEnvironmentConfig.xml', 'Incarnation', 'Protocol', 'SharedConfig.xml', 'WireServerEndpoint', 'Extensions.1.xml', 'ExtensionsConfig.1.xml', 'GoalState.1.xml', 'Extensions.2.xml', 'ExtensionsConfig.2.xml', 'GoalState.2.xml', 'Microsoft.Azure.Extensions.CustomScript-2.0.6/config/42.settings', 'Microsoft.Azure.Extensions.CustomScript-2.0.6/config/HandlerStatus', 'Microsoft.Azure.Extensions.CustomScript-2.0.6/config/HandlerState', 'Microsoft.Azure.Extensions.CustomScript-2.0.6/status/12.notstatus', 'Microsoft.Azure.Extensions.CustomScript-2.0.6/mrseq', 'WALinuxAgent-2.2.26/config/0.settings' ] tmp = tempfile.mkdtemp() mock_conf.return_value = tmp for d in dirs: fileutil.mkdir(os.path.join(tmp, d)) for f in files: fileutil.write_file(os.path.join(tmp, f), "Value") deprovision_handler = get_deprovision_handler(distro_name, distro_version, distro_full_name) warnings = [] actions = [] deprovision_handler.del_lib_dir_files(warnings, actions) deprovision_handler.del_ext_handler_files(warnings, actions) self.assertTrue(len(warnings) == 0) self.assertTrue(len(actions) == 2) self.assertEqual(fileutil.rm_files, actions[0].func) self.assertEqual(fileutil.rm_files, actions[1].func) self.assertEqual(11, len(actions[0].args)) self.assertEqual(3, len(actions[1].args)) for f in actions[0].args: self.assertTrue(os.path.basename(f) in files) for f in actions[1].args: self.assertTrue(f[len(tmp)+1:] in files) @distros("redhat") def test_deprovision(self, distro_name, distro_version, distro_full_name): deprovision_handler = get_deprovision_handler(distro_name, distro_version, distro_full_name) warnings, actions = deprovision_handler.setup(deluser=False) # pylint: disable=unused-variable assert any("/etc/resolv.conf" in w for w in warnings) @distros("ubuntu") def test_deprovision_ubuntu(self, distro_name, distro_version, distro_full_name): deprovision_handler = get_deprovision_handler(distro_name, distro_version, distro_full_name) with patch("os.path.realpath", return_value="/run/resolvconf/resolv.conf"): warnings, actions = deprovision_handler.setup(deluser=False) # pylint: disable=unused-variable assert any("/etc/resolvconf/resolv.conf.d/tail" in w for w in warnings) if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/pa/test_provision.py000066400000000000000000000415361446033677600214240ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import re import tempfile import unittest import azurelinuxagent.common.conf as conf from azurelinuxagent.common.exception import ProvisionError from azurelinuxagent.common.osutil.default import DefaultOSUtil from azurelinuxagent.common.protocol.util import OVF_FILE_NAME from azurelinuxagent.pa.provision import get_provision_handler from azurelinuxagent.pa.provision.cloudinit import CloudInitProvisionHandler from azurelinuxagent.pa.provision.default import ProvisionHandler from azurelinuxagent.common.utils import fileutil from tests.tools import AgentTestCase, distros, load_data, MagicMock, Mock, patch class TestProvision(AgentTestCase): @distros("redhat") @patch('azurelinuxagent.common.osutil.default.DefaultOSUtil.get_instance_id', return_value='B9F3C233-9913-9F42-8EB3-BA656DF32502') def test_provision(self, mock_util, distro_name, distro_version, distro_full_name): # pylint: disable=unused-argument provision_handler = get_provision_handler(distro_name, distro_version, distro_full_name) mock_osutil = MagicMock() mock_osutil.decode_customdata = Mock(return_value="") provision_handler.osutil = mock_osutil provision_handler.protocol_util.osutil = mock_osutil provision_handler.protocol_util.get_protocol = MagicMock() conf.get_dvd_mount_point = Mock(return_value=self.tmp_dir) ovfenv_file = os.path.join(self.tmp_dir, OVF_FILE_NAME) ovfenv_data = load_data("ovf-env.xml") fileutil.write_file(ovfenv_file, ovfenv_data) provision_handler.run() def test_customdata(self): base64data = 'Q3VzdG9tRGF0YQ==' data = DefaultOSUtil().decode_customdata(base64data) fileutil.write_file(tempfile.mktemp(), data) @patch('azurelinuxagent.common.conf.get_provision_enabled', return_value=False) def test_provisioning_is_skipped_when_not_enabled(self, mock_conf): # pylint: disable=unused-argument ph = ProvisionHandler() ph.osutil = DefaultOSUtil() ph.osutil.get_instance_id = Mock( return_value='B9F3C233-9913-9F42-8EB3-BA656DF32502') ph.check_provisioned_file = Mock() ph.report_ready = Mock() ph.write_provisioned = Mock() ph.run() self.assertEqual(0, ph.check_provisioned_file.call_count) self.assertEqual(1, ph.report_ready.call_count) self.assertEqual(1, ph.write_provisioned.call_count) @patch('os.path.isfile', return_value=False) def test_check_provisioned_file_not_provisioned(self, mock_isfile): # pylint: disable=unused-argument ph = ProvisionHandler() self.assertFalse(ph.check_provisioned_file()) @patch('os.path.isfile', return_value=True) @patch('azurelinuxagent.common.utils.fileutil.read_file', return_value="B9F3C233-9913-9F42-8EB3-BA656DF32502") @patch('azurelinuxagent.pa.deprovision.get_deprovision_handler') def test_check_provisioned_file_is_provisioned(self, mock_deprovision, mock_read, mock_isfile): # pylint: disable=unused-argument ph = ProvisionHandler() ph.osutil = Mock() ph.osutil.is_current_instance_id = Mock(return_value=True) ph.write_provisioned = Mock() deprovision_handler = Mock() mock_deprovision.return_value = deprovision_handler self.assertTrue(ph.check_provisioned_file()) self.assertEqual(1, ph.osutil.is_current_instance_id.call_count) self.assertEqual(0, deprovision_handler.run_changed_unique_id.call_count) @patch('os.path.isfile', return_value=True) @patch('azurelinuxagent.common.utils.fileutil.read_file', return_value="B9F3C233-9913-9F42-8EB3-BA656DF32502") @patch('azurelinuxagent.pa.deprovision.get_deprovision_handler') def test_check_provisioned_file_not_deprovisioned(self, mock_deprovision, mock_read, mock_isfile): # pylint: disable=unused-argument ph = ProvisionHandler() ph.osutil = Mock() ph.osutil.is_current_instance_id = Mock(return_value=False) ph.report_ready = Mock() ph.write_provisioned = Mock() deprovision_handler = Mock() mock_deprovision.return_value = deprovision_handler self.assertTrue(ph.check_provisioned_file()) self.assertEqual(1, ph.osutil.is_current_instance_id.call_count) self.assertEqual(1, deprovision_handler.run_changed_unique_id.call_count) @distros() @patch('azurelinuxagent.common.conf.get_provisioning_agent', return_value='waagent') def test_provision_telemetry_pga_false(self, distro_name, distro_version, distro_full_name, _): """ ProvisionGuestAgent flag is 'false' """ self._provision_test(distro_name, # pylint: disable=no-value-for-parameter distro_version, distro_full_name, OVF_FILE_NAME, 'false', True) @distros() @patch('azurelinuxagent.common.conf.get_provisioning_agent', return_value='waagent') def test_provision_telemetry_pga_true(self, distro_name, distro_version, distro_full_name, _): """ ProvisionGuestAgent flag is 'true' """ self._provision_test(distro_name, # pylint: disable=no-value-for-parameter distro_version, distro_full_name, 'ovf-env-2.xml', 'true', True) @distros() @patch('azurelinuxagent.common.conf.get_provisioning_agent', return_value='waagent') def test_provision_telemetry_pga_empty(self, distro_name, distro_version, distro_full_name, _): """ ProvisionGuestAgent flag is '' """ self._provision_test(distro_name, # pylint: disable=no-value-for-parameter distro_version, distro_full_name, 'ovf-env-3.xml', 'true', False) @distros() @patch('azurelinuxagent.common.conf.get_provisioning_agent', return_value='waagent') def test_provision_telemetry_pga_bad(self, distro_name, distro_version, distro_full_name, _): """ ProvisionGuestAgent flag is 'bad data' """ self._provision_test(distro_name, # pylint: disable=no-value-for-parameter distro_version, distro_full_name, 'ovf-env-4.xml', 'bad data', True) @patch('azurelinuxagent.common.osutil.default.DefaultOSUtil.get_instance_id', return_value='B9F3C233-9913-9F42-8EB3-BA656DF32502') @patch('azurelinuxagent.pa.provision.default.ProvisionHandler.write_agent_disabled') @patch('azurelinuxagent.pa.provision.default.cloud_init_is_enabled', return_value=False) def _provision_test(self, distro_name, distro_version, distro_full_name, ovf_file, provisionMessage, expect_success, # pylint:disable=unused-argument patch_cloud_init_is_enabled, patch_write_agent_disabled, patch_get_instance_id): """ Assert that the agent issues two telemetry messages as part of a successful provisioning. 1. Provision 2. GuestState """ ph = get_provision_handler(distro_name, distro_version, distro_full_name) ph.report_event = MagicMock() ph.reg_ssh_host_key = MagicMock(return_value='--thumprint--') mock_osutil = MagicMock() mock_osutil.decode_customdata = Mock(return_value="") ph.osutil = mock_osutil ph.protocol_util.osutil = mock_osutil ph.protocol_util.get_protocol = MagicMock() conf.get_dvd_mount_point = Mock(return_value=self.tmp_dir) ovfenv_file = os.path.join(self.tmp_dir, OVF_FILE_NAME) ovfenv_data = load_data(ovf_file) fileutil.write_file(ovfenv_file, ovfenv_data) ph.run() if expect_success: self.assertEqual(2, ph.report_event.call_count) positional_args, kw_args = ph.report_event.call_args_list[0] # [call('Provisioning succeeded (146473.68s)', duration=65, is_success=True)] self.assertTrue(re.match(r'Provisioning succeeded \(\d+\.\d+s\)', positional_args[0]) is not None) self.assertTrue(isinstance(kw_args['duration'], int)) self.assertTrue(kw_args['is_success']) positional_args, kw_args = ph.report_event.call_args_list[1] self.assertTrue(kw_args['operation'] == 'ProvisionGuestAgent') self.assertTrue(kw_args['message'] == provisionMessage) self.assertTrue(kw_args['is_success']) expected_disabled = True if provisionMessage == 'false' else False self.assertTrue(patch_write_agent_disabled.call_count == expected_disabled) else: self.assertEqual(1, ph.report_event.call_count) positional_args, kw_args = ph.report_event.call_args_list[0] # [call(u'[ProtocolError] Failed to validate OVF: ProvisionGuestAgent not found')] self.assertTrue('Failed to validate OVF: ProvisionGuestAgent not found' in positional_args[0]) self.assertFalse(kw_args['is_success']) @distros() @patch( 'azurelinuxagent.common.osutil.default.DefaultOSUtil.get_instance_id', return_value='B9F3C233-9913-9F42-8EB3-BA656DF32502') @patch('azurelinuxagent.common.conf.get_provisioning_agent', return_value='waagent') @patch('azurelinuxagent.pa.provision.default.cloud_init_is_enabled', return_value=False) def test_provision_telemetry_fail(self, distro_name, distro_version, distro_full_name, # pylint:disable=unused-argument patch_cloud_init_is_enabled, patch_get_provisioning_agent, mock_util): """ Assert that the agent issues one telemetry message as part of a failed provisioning. 1. Provision """ ph = get_provision_handler(distro_name, distro_version, distro_full_name) ph.report_event = MagicMock() ph.reg_ssh_host_key = MagicMock(side_effect=ProvisionError( "--unit-test--")) mock_osutil = MagicMock() mock_osutil.decode_customdata = Mock(return_value="") ph.osutil = mock_osutil ph.protocol_util.osutil = mock_osutil ph.protocol_util.get_protocol = MagicMock() conf.get_dvd_mount_point = Mock(return_value=self.tmp_dir) ovfenv_file = os.path.join(self.tmp_dir, OVF_FILE_NAME) ovfenv_data = load_data("ovf-env.xml") fileutil.write_file(ovfenv_file, ovfenv_data) ph.run() positional_args, kw_args = ph.report_event.call_args_list[0] # pylint: disable=unused-variable self.assertTrue(re.match(r'Provisioning failed: \[ProvisionError\] --unit-test-- \(\d+\.\d+s\)', positional_args[0]) is not None) @patch('azurelinuxagent.pa.provision.default.ProvisionHandler.write_agent_disabled') @distros() def test_handle_provision_guest_agent(self, patch_write_agent_disabled, distro_name, distro_version, distro_full_name): ph = get_provision_handler(distro_name, distro_version, distro_full_name) patch_write_agent_disabled.call_count = 0 ph.handle_provision_guest_agent(provision_guest_agent='false') self.assertEqual(1, patch_write_agent_disabled.call_count) ph.handle_provision_guest_agent(provision_guest_agent='False') self.assertEqual(2, patch_write_agent_disabled.call_count) ph.handle_provision_guest_agent(provision_guest_agent='FALSE') self.assertEqual(3, patch_write_agent_disabled.call_count) ph.handle_provision_guest_agent(provision_guest_agent='') self.assertEqual(3, patch_write_agent_disabled.call_count) ph.handle_provision_guest_agent(provision_guest_agent=' ') self.assertEqual(3, patch_write_agent_disabled.call_count) ph.handle_provision_guest_agent(provision_guest_agent=None) self.assertEqual(3, patch_write_agent_disabled.call_count) ph.handle_provision_guest_agent(provision_guest_agent='true') self.assertEqual(3, patch_write_agent_disabled.call_count) ph.handle_provision_guest_agent(provision_guest_agent='True') self.assertEqual(3, patch_write_agent_disabled.call_count) ph.handle_provision_guest_agent(provision_guest_agent='TRUE') self.assertEqual(3, patch_write_agent_disabled.call_count) @patch( 'azurelinuxagent.common.conf.get_provisioning_agent', return_value='auto' ) @patch( 'azurelinuxagent.pa.provision.factory.cloud_init_is_enabled', return_value=False ) def test_get_provision_handler_config_auto_no_cloudinit( self, patch_cloud_init_is_enabled, # pylint: disable=unused-argument patch_get_provisioning_agent): # pylint: disable=unused-argument provisioning_handler = get_provision_handler() self.assertIsInstance(provisioning_handler, ProvisionHandler, 'Auto provisioning handler should be waagent if cloud-init is not enabled') @patch( 'azurelinuxagent.common.conf.get_provisioning_agent', return_value='waagent' ) @patch( 'azurelinuxagent.pa.provision.factory.cloud_init_is_enabled', return_value=True ) def test_get_provision_handler_config_waagent( self, patch_cloud_init_is_enabled, # pylint: disable=unused-argument patch_get_provisioning_agent): # pylint: disable=unused-argument provisioning_handler = get_provision_handler() self.assertIsInstance(provisioning_handler, ProvisionHandler, 'Provisioning handler should be waagent if agent is set to waagent') @patch( 'azurelinuxagent.common.conf.get_provisioning_agent', return_value='auto' ) @patch( 'azurelinuxagent.pa.provision.factory.cloud_init_is_enabled', return_value=True ) def test_get_provision_handler_config_auto_cloudinit( self, patch_cloud_init_is_enabled, # pylint: disable=unused-argument patch_get_provisioning_agent): # pylint: disable=unused-argument provisioning_handler = get_provision_handler() self.assertIsInstance(provisioning_handler, CloudInitProvisionHandler, 'Auto provisioning handler should be cloud-init if cloud-init is enabled') @patch( 'azurelinuxagent.common.conf.get_provisioning_agent', return_value='cloud-init' ) def test_get_provision_handler_config_cloudinit( self, patch_get_provisioning_agent): # pylint: disable=unused-argument provisioning_handler = get_provision_handler() self.assertIsInstance(provisioning_handler, CloudInitProvisionHandler, 'Provisioning handler should be cloud-init if agent is set to cloud-init') if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/protocol/000077500000000000000000000000001446033677600172135ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/protocol/HttpRequestPredicates.py000066400000000000000000000112361446033677600240640ustar00rootroot00000000000000import re from azurelinuxagent.common.utils import restutil class HttpRequestPredicates(object): """ Utility functions to check the urls used by tests """ @staticmethod def is_goal_state_request(url): return url.lower() == 'http://{0}/machine/?comp=goalstate'.format(restutil.KNOWN_WIRESERVER_IP) @staticmethod def is_certificates_request(url): return re.match(r'http://{0}(:80)?/machine/.*?comp=certificates'.format(restutil.KNOWN_WIRESERVER_IP), url, re.IGNORECASE) @staticmethod def is_extensions_config_request(url): return re.match(r'http://{0}(:80)?/machine/.*?comp=config&type=extensionsConfig'.format(restutil.KNOWN_WIRESERVER_IP), url, re.IGNORECASE) @staticmethod def is_hosting_environment_config_request(url): return re.match(r'http://{0}(:80)?/machine/.*?comp=config&type=hostingEnvironmentConfig'.format(restutil.KNOWN_WIRESERVER_IP), url, re.IGNORECASE) @staticmethod def is_shared_config_request(url): return re.match(r'http://{0}(:80)?/machine/.*?comp=config&type=sharedConfig'.format(restutil.KNOWN_WIRESERVER_IP), url, re.IGNORECASE) @staticmethod def is_telemetry_request(url): return url.lower() == 'http://{0}/machine?comp=telemetrydata'.format(restutil.KNOWN_WIRESERVER_IP) @staticmethod def is_health_service_request(url): return url.lower() == 'http://{0}:80/healthservice'.format(restutil.KNOWN_WIRESERVER_IP) @staticmethod def is_in_vm_artifacts_profile_request(url): return re.match(r'https://.+\.blob\.core\.windows\.net/\$system/.+\.(vmSettings|settings)\?.+', url) is not None @staticmethod def _get_host_plugin_request_artifact_location(url, request_kwargs): if 'headers' not in request_kwargs: raise ValueError('Host plugin request is missing HTTP headers ({0})'.format(url)) headers = request_kwargs['headers'] if 'x-ms-artifact-location' not in headers: raise ValueError('Host plugin request is missing the x-ms-artifact-location header ({0})'.format(url)) return headers['x-ms-artifact-location'] @staticmethod def is_host_plugin_vm_settings_request(url): return url.lower() == 'http://{0}:{1}/vmsettings'.format(restutil.KNOWN_WIRESERVER_IP, restutil.HOST_PLUGIN_PORT) @staticmethod def is_host_plugin_health_request(url): return url.lower() == 'http://{0}:{1}/health'.format(restutil.KNOWN_WIRESERVER_IP, restutil.HOST_PLUGIN_PORT) @staticmethod def is_host_plugin_extension_artifact_request(url): return url.lower() == 'http://{0}:{1}/extensionartifact'.format(restutil.KNOWN_WIRESERVER_IP, restutil.HOST_PLUGIN_PORT) @staticmethod def is_status_request(url): return HttpRequestPredicates.is_storage_status_request(url) or HttpRequestPredicates.is_host_plugin_status_request(url) @staticmethod def is_storage_status_request(url): # e.g. 'https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo' return re.match(r'^https://.+/.*\.status\?[^/]+$', url, re.IGNORECASE) @staticmethod def is_host_plugin_status_request(url): return url.lower() == 'http://{0}:{1}/status'.format(restutil.KNOWN_WIRESERVER_IP, restutil.HOST_PLUGIN_PORT) @staticmethod def is_host_plugin_extension_request(request_url, request_kwargs, extension_url): if not HttpRequestPredicates.is_host_plugin_extension_artifact_request(request_url): return False artifact_location = HttpRequestPredicates._get_host_plugin_request_artifact_location(request_url, request_kwargs) return artifact_location == extension_url @staticmethod def is_host_plugin_in_vm_artifacts_profile_request(url, request_kwargs): if not HttpRequestPredicates.is_host_plugin_extension_artifact_request(url): return False artifact_location = HttpRequestPredicates._get_host_plugin_request_artifact_location(url, request_kwargs) return HttpRequestPredicates.is_in_vm_artifacts_profile_request(artifact_location) @staticmethod def is_host_plugin_put_logs_request(url): return url.lower() == 'http://{0}:{1}/vmagentlog'.format(restutil.KNOWN_WIRESERVER_IP, restutil.HOST_PLUGIN_PORT) @staticmethod def is_agent_package_request(url): return re.match(r"^http://mock-goal-state/ga-manifests/OSTCExtensions.WALinuxAgent__([\d.]+)$", url) is not None @staticmethod def is_ga_manifest_request(url): return "manifest_of_ga.xml" in url WALinuxAgent-2.9.1.1/tests/protocol/__init__.py000066400000000000000000000011651446033677600213270ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # WALinuxAgent-2.9.1.1/tests/protocol/mocks.py000066400000000000000000000161641446033677600207110ustar00rootroot00000000000000# Copyright 2020 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import contextlib from azurelinuxagent.common.protocol.wire import WireProtocol from azurelinuxagent.common.utils import restutil from tests.tools import patch from tests.protocol import mockwiredata @contextlib.contextmanager def mock_wire_protocol(mock_wire_data_file, http_get_handler=None, http_post_handler=None, http_put_handler=None, do_not_mock=lambda method, url: False, fail_on_unknown_request=True): """ Creates a WireProtocol object that handles requests to the WireServer, the Host GA Plugin, and some requests to storage (requests that provide mock data in mockwiredata.py). The data returned by those requests is read from the files specified by 'mock_wire_data_file' (which must follow the structure of the data files defined in tests/protocol/mockwiredata.py). The caller can also provide handler functions for specific HTTP methods using the http_*_handler arguments. The return value of the handler function is interpreted similarly to the "return_value" argument of patch(): if it is an exception the exception is raised or, if it is any object other than None, the value is returned by the mock. If the handler function returns None the call is handled using the mock wireserver data or passed to the original to restutil.http_request. The 'do_not_mock' lambda can be used to skip the mocks for specific requests; if the lambda returns True, the mocks won't be applied and the original common.utils.restutil.http_request will be invoked instead. The returned protocol object maintains a list of "tracked" urls. When a handler function returns a value than is not None the url for the request is automatically added to the tracked list. The handler function can add other items to this list using the track_url() method on the mock. The return value of this function is an instance of WireProtocol augmented with these properties/methods: * mock_wire_data - the WireProtocolData constructed from the mock_wire_data_file parameter. * start() - starts the patchers for http_request and CryptUtil * stop() - stops the patchers * track_url(url) - adds the given item to the list of tracked urls. * get_tracked_urls() - returns the list of tracked urls. NOTE: This function patches common.utils.restutil.http_request and common.protocol.wire.CryptUtil; you need to be aware of this if your tests patch those methods or others in the call stack (e.g. restutil.get, resutil._http_request, etc) """ tracked_urls = [] # use a helper function to keep the HTTP handlers (they need to be modified by set_http_handlers() and # Python 2.* does not support nonlocal declarations) def http_handlers(get, post, put): http_handlers.get = get http_handlers.post = post http_handlers.put = put del tracked_urls[:] http_handlers(get=http_get_handler, post=http_post_handler, put=http_put_handler) # # function used to patch restutil.http_request # original_http_request = restutil.http_request def http_request(method, url, data, timeout, **kwargs): # call the original resutil.http_request if the request should be mocked if protocol.do_not_mock(method, url): return original_http_request(method, url, data, timeout, **kwargs) # if there is a handler for the request, use it handler = None if method == 'GET': handler = http_handlers.get elif method == 'POST': handler = http_handlers.post elif method == 'PUT': handler = http_handlers.put if handler is not None: if method == 'GET': return_value = handler(url, **kwargs) else: return_value = handler(url, data, **kwargs) if return_value is not None: tracked_urls.append(url) if isinstance(return_value, Exception): raise return_value return return_value # if the request was not handled try to use the mock wireserver data try: if method == 'GET': return protocol.mock_wire_data.mock_http_get(url, **kwargs) if method == 'POST': return protocol.mock_wire_data.mock_http_post(url, data, **kwargs) if method == 'PUT': return protocol.mock_wire_data.mock_http_put(url, data, **kwargs) except NotImplementedError: pass # if there was not a response for the request then fail it or call the original resutil.http_request if fail_on_unknown_request: raise ValueError('Unknown HTTP request: {0} [{1}]'.format(url, method)) return original_http_request(method, url, data, timeout, **kwargs) # # functions to start/stop the mocks # def start(): patched = patch("azurelinuxagent.common.utils.restutil.http_request", side_effect=http_request) patched.start() start.http_request_patch = patched patched = patch("azurelinuxagent.common.protocol.wire.CryptUtil", side_effect=protocol.mock_wire_data.mock_crypt_util) patched.start() start.crypt_util_patch = patched start.http_request_patch = None start.crypt_util_patch = None def stop(): if start.crypt_util_patch is not None: start.crypt_util_patch.stop() if start.http_request_patch is not None: start.http_request_patch.stop() # # create the protocol object # protocol = WireProtocol(restutil.KNOWN_WIRESERVER_IP) protocol.mock_wire_data = mockwiredata.WireProtocolData(mock_wire_data_file) protocol.start = start protocol.stop = stop protocol.track_url = lambda url: tracked_urls.append(url) # pylint: disable=unnecessary-lambda protocol.get_tracked_urls = lambda: tracked_urls protocol.set_http_handlers = lambda http_get_handler=None, http_post_handler=None, http_put_handler=None:\ http_handlers(get=http_get_handler, post=http_post_handler, put=http_put_handler) protocol.do_not_mock = do_not_mock # go do it try: protocol.start() protocol.detect() yield protocol finally: protocol.stop() class MockHttpResponse: def __init__(self, status, body=b'', headers=None, reason=None): self.body = body self.status = status self.headers = [] if headers is None else headers self.reason = reason def read(self, *_): return self.body def getheaders(self): return self.headers WALinuxAgent-2.9.1.1/tests/protocol/mockwiredata.py000066400000000000000000000475641446033677600222570ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import base64 import datetime import json import re from azurelinuxagent.common.utils import timeutil from azurelinuxagent.common.utils.textutil import parse_doc, find, findall from tests.protocol.HttpRequestPredicates import HttpRequestPredicates from tests.tools import load_bin_data, load_data, MagicMock, Mock from azurelinuxagent.common.protocol.imds import IMDS_ENDPOINT from azurelinuxagent.common.exception import HttpError, ResourceGoneError from azurelinuxagent.common.future import httpclient from azurelinuxagent.common.utils.cryptutil import CryptUtil DATA_FILE = { "version_info": "wire/version_info.xml", "goal_state": "wire/goal_state.xml", "hosting_env": "wire/hosting_env.xml", "shared_config": "wire/shared_config.xml", "certs": "wire/certs.xml", "ext_conf": "wire/ext_conf.xml", "manifest": "wire/manifest.xml", "ga_manifest": "wire/ga_manifest.xml", "trans_prv": "wire/trans_prv", "trans_cert": "wire/trans_cert", "test_ext": "ext/sample_ext-1.3.0.zip", "imds_info": "imds/valid.json", "remote_access": None, "in_vm_artifacts_profile": None, "vm_settings": None, "ETag": None } DATA_FILE_IN_VM_ARTIFACTS_PROFILE = DATA_FILE.copy() DATA_FILE_IN_VM_ARTIFACTS_PROFILE["ext_conf"] = "wire/ext_conf_in_vm_artifacts_profile.xml" DATA_FILE_IN_VM_ARTIFACTS_PROFILE["in_vm_artifacts_profile"] = "wire/in_vm_artifacts_profile.json" DATA_FILE_IN_VM_META_DATA = DATA_FILE.copy() DATA_FILE_IN_VM_META_DATA["ext_conf"] = "wire/ext_conf_in_vm_metadata.xml" DATA_FILE_INVALID_VM_META_DATA = DATA_FILE.copy() DATA_FILE_INVALID_VM_META_DATA["ext_conf"] = "wire/ext_conf_invalid_vm_metadata.xml" DATA_FILE_NO_EXT = DATA_FILE.copy() DATA_FILE_NO_EXT["ext_conf"] = "wire/ext_conf_no_extensions-block_blob.xml" DATA_FILE_NOOP_GS = DATA_FILE.copy() DATA_FILE_NOOP_GS["goal_state"] = "wire/goal_state_noop.xml" DATA_FILE_NOOP_GS["ext_conf"] = None DATA_FILE_EXT_NO_SETTINGS = DATA_FILE.copy() DATA_FILE_EXT_NO_SETTINGS["ext_conf"] = "wire/ext_conf_no_settings.xml" DATA_FILE_EXT_NO_PUBLIC = DATA_FILE.copy() DATA_FILE_EXT_NO_PUBLIC["ext_conf"] = "wire/ext_conf_no_public.xml" DATA_FILE_EXT_AUTOUPGRADE = DATA_FILE.copy() DATA_FILE_EXT_AUTOUPGRADE["ext_conf"] = "wire/ext_conf_autoupgrade.xml" DATA_FILE_EXT_INTERNALVERSION = DATA_FILE.copy() DATA_FILE_EXT_INTERNALVERSION["ext_conf"] = "wire/ext_conf_internalversion.xml" DATA_FILE_EXT_AUTOUPGRADE_INTERNALVERSION = DATA_FILE.copy() DATA_FILE_EXT_AUTOUPGRADE_INTERNALVERSION["ext_conf"] = "wire/ext_conf_autoupgrade_internalversion.xml" DATA_FILE_EXT_ROLLINGUPGRADE = DATA_FILE.copy() DATA_FILE_EXT_ROLLINGUPGRADE["ext_conf"] = "wire/ext_conf_upgradeguid.xml" DATA_FILE_EXT_SEQUENCING = DATA_FILE.copy() DATA_FILE_EXT_SEQUENCING["ext_conf"] = "wire/ext_conf_sequencing.xml" DATA_FILE_EXT_ADDITIONAL_LOCATIONS = DATA_FILE.copy() DATA_FILE_EXT_ADDITIONAL_LOCATIONS["ext_conf"] = "wire/ext_conf_additional_locations.xml" DATA_FILE_EXT_DELETION = DATA_FILE.copy() DATA_FILE_EXT_DELETION["manifest"] = "wire/manifest_deletion.xml" DATA_FILE_EXT_SINGLE = DATA_FILE.copy() DATA_FILE_EXT_SINGLE["manifest"] = "wire/manifest_deletion.xml" DATA_FILE_MULTIPLE_EXT = DATA_FILE.copy() DATA_FILE_MULTIPLE_EXT["ext_conf"] = "wire/ext_conf_multiple_extensions.xml" DATA_FILE_CASE_MISMATCH_EXT = DATA_FILE.copy() DATA_FILE_CASE_MISMATCH_EXT["ext_conf"] = "wire/ext_conf_settings_case_mismatch.xml" DATA_FILE_NO_CERT_FORMAT = DATA_FILE.copy() DATA_FILE_NO_CERT_FORMAT["certs"] = "wire/certs_no_format_specified.xml" DATA_FILE_CERT_FORMAT_NOT_PFX = DATA_FILE.copy() DATA_FILE_CERT_FORMAT_NOT_PFX["certs"] = "wire/certs_format_not_pfx.xml" DATA_FILE_REMOTE_ACCESS = DATA_FILE.copy() DATA_FILE_REMOTE_ACCESS["goal_state"] = "wire/goal_state_remote_access.xml" DATA_FILE_REMOTE_ACCESS["remote_access"] = "wire/remote_access_single_account.xml" DATA_FILE_PLUGIN_SETTINGS_MISMATCH = DATA_FILE.copy() DATA_FILE_PLUGIN_SETTINGS_MISMATCH["ext_conf"] = "wire/invalid_config/ext_conf_plugin_settings_version_mismatch.xml" DATA_FILE_REQUIRED_FEATURES = DATA_FILE.copy() DATA_FILE_REQUIRED_FEATURES["ext_conf"] = "wire/ext_conf_required_features.xml" DATA_FILE_VM_SETTINGS = DATA_FILE.copy() DATA_FILE_VM_SETTINGS["vm_settings"] = "hostgaplugin/vm_settings.json" DATA_FILE_VM_SETTINGS["ETag"] = "1" DATA_FILE_VM_SETTINGS["ext_conf"] = "hostgaplugin/ext_conf.xml" DATA_FILE_VM_SETTINGS["in_vm_artifacts_profile"] = "hostgaplugin/in_vm_artifacts_profile.json" class WireProtocolData(object): def __init__(self, data_files=None): if data_files is None: data_files = DATA_FILE self.emulate_stale_goal_state = False self.call_counts = { "comp=versions": 0, "/versions": 0, "/health": 0, "/HealthService": 0, "/vmAgentLog": 0, "goalstate": 0, "hostingEnvironmentConfig": 0, "sharedConfig": 0, "certificates": 0, "extensionsConfig": 0, "remoteaccessinfouri": 0, "extensionArtifact": 0, "agentArtifact": 0, "manifest.xml": 0, "manifest_of_ga.xml": 0, "ExampleHandlerLinux": 0, "in_vm_artifacts_profile": 0, "vm_settings": 0 } self.status_blobs = [] self.data_files = data_files self.version_info = None self.goal_state = None self.hosting_env = None self.shared_config = None self.certs = None self.ext_conf = None self.manifest = None self.ga_manifest = None self.trans_prv = None self.trans_cert = None self.ext = None self.remote_access = None self.in_vm_artifacts_profile = None self.vm_settings = None self.etag = None self.prev_etag = None self.imds_info = None self.reload() def reload(self): self.version_info = load_data(self.data_files.get("version_info")) self.goal_state = load_data(self.data_files.get("goal_state")) self.hosting_env = load_data(self.data_files.get("hosting_env")) self.shared_config = load_data(self.data_files.get("shared_config")) self.certs = load_data(self.data_files.get("certs")) self.ext_conf = self.data_files.get("ext_conf") if self.ext_conf is not None: self.ext_conf = load_data(self.ext_conf) self.manifest = load_data(self.data_files.get("manifest")) self.ga_manifest = load_data(self.data_files.get("ga_manifest")) self.trans_prv = load_data(self.data_files.get("trans_prv")) self.trans_cert = load_data(self.data_files.get("trans_cert")) self.imds_info = json.loads(load_data(self.data_files.get("imds_info"))) self.ext = load_bin_data(self.data_files.get("test_ext")) vm_settings = self.data_files.get("vm_settings") if vm_settings is not None: self.vm_settings = load_data(self.data_files.get("vm_settings")) self.etag = self.data_files.get("ETag") remote_access_data_file = self.data_files.get("remote_access") if remote_access_data_file is not None: self.remote_access = load_data(remote_access_data_file) in_vm_artifacts_profile_file = self.data_files.get("in_vm_artifacts_profile") if in_vm_artifacts_profile_file is not None: self.in_vm_artifacts_profile = load_data(in_vm_artifacts_profile_file) def reset_call_counts(self): for counter in self.call_counts: self.call_counts[counter] = 0 def mock_http_get(self, url, *_, **kwargs): content = '' response_headers = [] resp = MagicMock() resp.status = httpclient.OK if "comp=versions" in url: # wire server versions content = self.version_info self.call_counts["comp=versions"] += 1 elif "/versions" in url: # HostPlugin versions content = '["2015-09-01"]' self.call_counts["/versions"] += 1 elif url.endswith("/health"): # HostPlugin health content = '' self.call_counts["/health"] += 1 elif "goalstate" in url: content = self.goal_state self.call_counts["goalstate"] += 1 elif HttpRequestPredicates.is_hosting_environment_config_request(url): content = self.hosting_env self.call_counts["hostingEnvironmentConfig"] += 1 elif HttpRequestPredicates.is_shared_config_request(url): content = self.shared_config self.call_counts["sharedConfig"] += 1 elif HttpRequestPredicates.is_certificates_request(url): content = self.certs self.call_counts["certificates"] += 1 elif HttpRequestPredicates.is_extensions_config_request(url): content = self.ext_conf self.call_counts["extensionsConfig"] += 1 elif "remoteaccessinfouri" in url: content = self.remote_access self.call_counts["remoteaccessinfouri"] += 1 elif ".vmSettings" in url or ".settings" in url: content = self.in_vm_artifacts_profile self.call_counts["in_vm_artifacts_profile"] += 1 elif "/vmSettings" in url: if self.vm_settings is None: resp.status = httpclient.NOT_FOUND elif self.call_counts["vm_settings"] > 0 and self.prev_etag == self.etag: resp.status = httpclient.NOT_MODIFIED else: content = self.vm_settings response_headers = [('ETag', self.etag)] self.prev_etag = self.etag self.call_counts["vm_settings"] += 1 elif '{0}/metadata/compute'.format(IMDS_ENDPOINT) in url: content = json.dumps(self.imds_info.get("compute", "{}")) else: # A stale GoalState results in a 400 from the HostPlugin # for which the HTTP handler in restutil raises ResourceGoneError if self.emulate_stale_goal_state: if "extensionArtifact" in url: self.emulate_stale_goal_state = False self.call_counts["extensionArtifact"] += 1 raise ResourceGoneError() else: raise HttpError() # For HostPlugin requests, replace the URL with that passed # via the x-ms-artifact-location header if "extensionArtifact" in url: self.call_counts["extensionArtifact"] += 1 if "headers" not in kwargs: raise ValueError("HostPlugin request is missing the HTTP headers: {0}", kwargs) # pylint: disable=raising-format-tuple if "x-ms-artifact-location" not in kwargs["headers"]: raise ValueError("HostPlugin request is missing the x-ms-artifact-location header: {0}", kwargs) # pylint: disable=raising-format-tuple url = kwargs["headers"]["x-ms-artifact-location"] if "manifest.xml" in url: content = self.manifest self.call_counts["manifest.xml"] += 1 elif HttpRequestPredicates.is_ga_manifest_request(url): content = self.ga_manifest self.call_counts["manifest_of_ga.xml"] += 1 elif "ExampleHandlerLinux" in url: content = self.ext self.call_counts["ExampleHandlerLinux"] += 1 resp.read = Mock(return_value=content) return resp elif ".vmSettings" in url or ".settings" in url: content = self.in_vm_artifacts_profile self.call_counts["in_vm_artifacts_profile"] += 1 else: raise NotImplementedError(url) resp.read = Mock(return_value=content.encode("utf-8")) resp.getheaders = Mock(return_value=response_headers) return resp def mock_http_post(self, url, *_, **__): content = None resp = MagicMock() resp.status = httpclient.OK if url.endswith('/HealthService'): self.call_counts['/HealthService'] += 1 content = '' else: raise NotImplementedError(url) resp.read = Mock(return_value=content.encode("utf-8")) return resp def mock_http_put(self, url, data, **_): content = '' resp = MagicMock() resp.status = httpclient.OK if url.endswith('/vmAgentLog'): self.call_counts['/vmAgentLog'] += 1 elif HttpRequestPredicates.is_storage_status_request(url): self.status_blobs.append(data) elif HttpRequestPredicates.is_host_plugin_status_request(url): self.status_blobs.append(WireProtocolData.get_status_blob_from_hostgaplugin_put_status_request(content)) else: raise NotImplementedError(url) resp.read = Mock(return_value=content.encode("utf-8")) return resp def mock_crypt_util(self, *args, **kw): # Partially patch instance method of class CryptUtil cryptutil = CryptUtil(*args, **kw) cryptutil.gen_transport_cert = Mock(side_effect=self.mock_gen_trans_cert) return cryptutil def mock_gen_trans_cert(self, trans_prv_file, trans_cert_file): with open(trans_prv_file, 'w+') as prv_file: prv_file.write(self.trans_prv) with open(trans_cert_file, 'w+') as cert_file: cert_file.write(self.trans_cert) @staticmethod def get_status_blob_from_hostgaplugin_put_status_request(data): status_object = json.loads(data) content = status_object["content"] return base64.b64decode(content) def get_no_of_plugins_in_extension_config(self): if self.ext_conf is None: return 0 ext_config_doc = parse_doc(self.ext_conf) plugins_list = find(ext_config_doc, "Plugins") return len(findall(plugins_list, "Plugin")) def get_no_of_extensions_in_config(self): if self.ext_conf is None: return 0 ext_config_doc = parse_doc(self.ext_conf) plugin_settings = find(ext_config_doc, "PluginSettings") return len(findall(plugin_settings, "ExtensionRuntimeSettings")) + len( findall(plugin_settings, "RuntimeSettings")) # # Having trouble reading the regular expressions below? you are not alone! # # For the use of "(?<=" "(?=" see 7.2.1 in https://docs.python.org/3.1/library/re.html # For the use of "\g<1>" see backreferences in https://docs.python.org/3.1/library/re.html#re.sub # # Note that these regular expressions are not enough to parse all valid XML documents (e.g. they do # not account for metacharacters like < or > in the values) but they are good enough for the test # data. There are some basic checks, but the functions may not match valid XML or produce invalid # XML if their input is too complex. # @staticmethod def replace_xml_element_value(xml_document, element_name, element_value): element_regex = r'(?<=<{0}>).+(?=)'.format(element_name) if not re.search(element_regex, xml_document): raise Exception("Can't find XML element '{0}' in {1}".format(element_name, xml_document)) return re.sub(element_regex, element_value, xml_document) @staticmethod def replace_xml_attribute_value(xml_document, element_name, attribute_name, attribute_value): attribute_regex = r'(?<=<{0} )(.*{1}=")[^"]+(?="[^>]*>)'.format(element_name, attribute_name) if not re.search(attribute_regex, xml_document): raise Exception("Can't find attribute {0} in XML element '{1}'. Document: {2}".format(attribute_name, element_name, xml_document)) return re.sub(attribute_regex, r'\g<1>{0}'.format(attribute_value), xml_document) def set_etag(self, etag, timestamp=None): """ Sets the ETag for the mock response. This function is used to mock a new goal state, and it also updates the timestamp (extensionsLastModifiedTickCount) in vmSettings. """ if timestamp is None: timestamp = datetime.datetime.utcnow() self.etag = etag try: vm_settings = json.loads(self.vm_settings) vm_settings["extensionsLastModifiedTickCount"] = timeutil.datetime_to_ticks(timestamp) self.vm_settings = json.dumps(vm_settings) except ValueError: # some test data include syntax errors; ignore those pass def set_vm_settings_source(self, source): """ Sets the "extensionGoalStatesSource" for the mock vm_settings data """ vm_settings = json.loads(self.vm_settings) vm_settings["extensionGoalStatesSource"] = source self.vm_settings = json.dumps(vm_settings) def set_incarnation(self, incarnation, timestamp=None): """ Sets the incarnation in the goal state, but not on its subcomponents (e.g. hosting env, shared config). This function is used to mock a new goal state, and it also updates the timestamp (createdOnTicks) in ExtensionsConfig. """ self.goal_state = WireProtocolData.replace_xml_element_value(self.goal_state, "Incarnation", str(incarnation)) if self.ext_conf is not None: if timestamp is None: timestamp = datetime.datetime.utcnow() self.ext_conf = WireProtocolData.replace_xml_attribute_value(self.ext_conf, "InVMGoalStateMetaData", "createdOnTicks", timeutil.datetime_to_ticks(timestamp)) def set_container_id(self, container_id): self.goal_state = WireProtocolData.replace_xml_element_value(self.goal_state, "ContainerId", container_id) def set_role_config_name(self, role_config_name): self.goal_state = WireProtocolData.replace_xml_element_value(self.goal_state, "ConfigName", role_config_name) def set_hosting_env_deployment_name(self, deployment_name): self.hosting_env = WireProtocolData.replace_xml_attribute_value(self.hosting_env, "Deployment", "name", deployment_name) def set_shared_config_deployment_name(self, deployment_name): self.shared_config = WireProtocolData.replace_xml_attribute_value(self.shared_config, "Deployment", "name", deployment_name) def set_extensions_config_sequence_number(self, sequence_number): ''' Sets the sequence number for *all* extensions ''' self.ext_conf = WireProtocolData.replace_xml_attribute_value(self.ext_conf, "RuntimeSettings", "seqNo", str(sequence_number)) def set_extensions_config_version(self, version): ''' Sets the version for *all* extensions ''' self.ext_conf = WireProtocolData.replace_xml_attribute_value(self.ext_conf, "Plugin", "version", version) def set_extensions_config_state(self, state): ''' Sets the state for *all* extensions ''' self.ext_conf = WireProtocolData.replace_xml_attribute_value(self.ext_conf, "Plugin", "state", state) def set_manifest_version(self, version): ''' Sets the version of the extension manifest ''' self.manifest = WireProtocolData.replace_xml_element_value(self.manifest, "Version", version) def set_extension_config(self, ext_conf_file): self.ext_conf = load_data(ext_conf_file) def set_extension_config_requested_version(self, version): self.ext_conf = WireProtocolData.replace_xml_element_value(self.ext_conf, "Version", version) WALinuxAgent-2.9.1.1/tests/protocol/test_datacontract.py000066400000000000000000000027031446033677600232750ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import unittest from azurelinuxagent.common.datacontract import get_properties, set_properties, DataContract, DataContractList class SampleDataContract(DataContract): def __init__(self): self.foo = None self.bar = DataContractList(int) class TestDataContract(unittest.TestCase): def test_get_properties(self): obj = SampleDataContract() obj.foo = "foo" obj.bar.append(1) data = get_properties(obj) self.assertEqual("foo", data["foo"]) self.assertEqual(list, type(data["bar"])) def test_set_properties(self): obj = SampleDataContract() data = { 'foo' : 1, 'baz': 'a' } set_properties('sample', obj, data) self.assertFalse(hasattr(obj, 'baz')) if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/protocol/test_extensions_goal_state_from_extensions_config.py000066400000000000000000000103511446033677600320540ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the Apache License. from azurelinuxagent.common.AgentGlobals import AgentGlobals from azurelinuxagent.common.protocol.extensions_goal_state import GoalStateChannel from tests.protocol.mocks import mockwiredata, mock_wire_protocol from tests.tools import AgentTestCase class ExtensionsGoalStateFromExtensionsConfigTestCase(AgentTestCase): def test_it_should_parse_in_vm_metadata(self): with mock_wire_protocol(mockwiredata.DATA_FILE_IN_VM_META_DATA) as protocol: extensions_goal_state = protocol.get_goal_state().extensions_goal_state self.assertEqual("555e551c-600e-4fb4-90ba-8ab8ec28eccc", extensions_goal_state.activity_id, "Incorrect activity Id") self.assertEqual("400de90b-522e-491f-9d89-ec944661f531", extensions_goal_state.correlation_id, "Incorrect correlation Id") self.assertEqual('2020-11-09T17:48:50.412125Z', extensions_goal_state.created_on_timestamp, "Incorrect GS Creation time") def test_it_should_use_default_values_when_in_vm_metadata_is_missing(self): data_file = mockwiredata.DATA_FILE.copy() data_file["ext_conf"] = "wire/ext_conf-no_gs_metadata.xml" with mock_wire_protocol(data_file) as protocol: extensions_goal_state = protocol.get_goal_state().extensions_goal_state self.assertEqual(AgentGlobals.GUID_ZERO, extensions_goal_state.activity_id, "Incorrect activity Id") self.assertEqual(AgentGlobals.GUID_ZERO, extensions_goal_state.correlation_id, "Incorrect correlation Id") self.assertEqual('1900-01-01T00:00:00.000000Z', extensions_goal_state.created_on_timestamp, "Incorrect GS Creation time") def test_it_should_use_default_values_when_in_vm_metadata_is_invalid(self): with mock_wire_protocol(mockwiredata.DATA_FILE_INVALID_VM_META_DATA) as protocol: extensions_goal_state = protocol.get_goal_state().extensions_goal_state self.assertEqual(AgentGlobals.GUID_ZERO, extensions_goal_state.activity_id, "Incorrect activity Id") self.assertEqual(AgentGlobals.GUID_ZERO, extensions_goal_state.correlation_id, "Incorrect correlation Id") self.assertEqual('1900-01-01T00:00:00.000000Z', extensions_goal_state.created_on_timestamp, "Incorrect GS Creation time") def test_it_should_parse_missing_status_upload_blob_as_none(self): data_file = mockwiredata.DATA_FILE.copy() data_file["ext_conf"] = "hostgaplugin/ext_conf-no_status_upload_blob.xml" with mock_wire_protocol(data_file) as protocol: extensions_goal_state = protocol.get_goal_state().extensions_goal_state self.assertIsNone(extensions_goal_state.status_upload_blob, "Expected status upload blob to be None") self.assertEqual("BlockBlob", extensions_goal_state.status_upload_blob_type, "Expected status upload blob to be Block") def test_it_should_default_to_block_blob_when_the_status_blob_type_is_not_valid(self): data_file = mockwiredata.DATA_FILE.copy() data_file["ext_conf"] = "hostgaplugin/ext_conf-invalid_blob_type.xml" with mock_wire_protocol(data_file) as protocol: extensions_goal_state = protocol.get_goal_state().extensions_goal_state self.assertEqual("BlockBlob", extensions_goal_state.status_upload_blob_type, 'Expected BlockBlob for an invalid statusBlobType') def test_it_should_parse_empty_depends_on_as_dependency_level_0(self): data_file = mockwiredata.DATA_FILE_VM_SETTINGS.copy() data_file["vm_settings"] = "hostgaplugin/vm_settings-empty_depends_on.json" data_file["ext_conf"] = "hostgaplugin/ext_conf-empty_depends_on.xml" with mock_wire_protocol(data_file) as protocol: extensions = protocol.get_goal_state().extensions_goal_state.extensions self.assertEqual(0, extensions[0].settings[0].dependencyLevel, "Incorrect dependencyLevel") def test_its_source_channel_should_be_wire_server(self): with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: extensions_goal_state = protocol.get_goal_state().extensions_goal_state self.assertEqual(GoalStateChannel.WireServer, extensions_goal_state.channel, "The channel is incorrect") WALinuxAgent-2.9.1.1/tests/protocol/test_extensions_goal_state_from_vm_settings.py000066400000000000000000000240751446033677600307020ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the Apache License. import json from azurelinuxagent.common.protocol.goal_state import GoalState from azurelinuxagent.common.protocol.extensions_goal_state import GoalStateChannel from azurelinuxagent.common.protocol.extensions_goal_state_from_vm_settings import _CaseFoldedDict from tests.protocol.mocks import mockwiredata, mock_wire_protocol from tests.tools import AgentTestCase class ExtensionsGoalStateFromVmSettingsTestCase(AgentTestCase): def test_it_should_parse_vm_settings(self): with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS) as protocol: extensions_goal_state = protocol.get_goal_state().extensions_goal_state def assert_property(name, value): self.assertEqual(value, getattr(extensions_goal_state, name), '{0} was not parsed correctly'.format(name)) assert_property("activity_id", "a33f6f53-43d6-4625-b322-1a39651a00c9") assert_property("correlation_id", "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e") assert_property("created_on_timestamp", "2021-11-16T13:22:50.620529Z") assert_property("status_upload_blob", "https://dcrcl3a0xs.blob.core.windows.net/$system/edp0plkw2b.86f4ae0a-61f8-48ae-9199-40f402d56864.status?sv=2018-03-28&sr=b&sk=system-1&sig=KNWgC2%3d&se=9999-01-01T00%3a00%3a00Z&sp=w") assert_property("status_upload_blob_type", "BlockBlob") assert_property("required_features", ["MultipleExtensionsPerHandler"]) assert_property("on_hold", True) # # for the rest of the attributes, we check only 1 item in each container (but check the length of the container) # # agent families self.assertEqual(2, len(extensions_goal_state.agent_families), "Incorrect number of agent families. Got: {0}".format(extensions_goal_state.agent_families)) self.assertEqual("Prod", extensions_goal_state.agent_families[0].name, "Incorrect agent family.") self.assertEqual(2, len(extensions_goal_state.agent_families[0].uris), "Incorrect number of uris. Got: {0}".format(extensions_goal_state.agent_families[0].uris)) expected = "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml" self.assertEqual(expected, extensions_goal_state.agent_families[0].uris[0], "Unexpected URI for the agent manifest.") # extensions self.assertEqual(5, len(extensions_goal_state.extensions), "Incorrect number of extensions. Got: {0}".format(extensions_goal_state.extensions)) self.assertEqual('Microsoft.Azure.Monitor.AzureMonitorLinuxAgent', extensions_goal_state.extensions[0].name, "Incorrect extension name") self.assertEqual(1, len(extensions_goal_state.extensions[0].settings[0].publicSettings), "Incorrect number of public settings") self.assertEqual(True, extensions_goal_state.extensions[0].settings[0].publicSettings["GCS_AUTO_CONFIG"], "Incorrect public settings") # dependency level (single-config) self.assertEqual(1, extensions_goal_state.extensions[2].settings[0].dependencyLevel, "Incorrect dependency level (single-config)") # dependency level (multi-config) self.assertEqual(1, extensions_goal_state.extensions[3].settings[1].dependencyLevel, "Incorrect dependency level (multi-config)") def test_it_should_parse_requested_version_properly(self): with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS) as protocol: goal_state = GoalState(protocol.client) families = goal_state.extensions_goal_state.agent_families for family in families: self.assertEqual(family.requested_version_string, "0.0.0.0", "Version should be None") data_file = mockwiredata.DATA_FILE_VM_SETTINGS.copy() data_file["vm_settings"] = "hostgaplugin/vm_settings-requested_version.json" with mock_wire_protocol(data_file) as protocol: protocol.mock_wire_data.set_etag(888) goal_state = GoalState(protocol.client) families = goal_state.extensions_goal_state.agent_families for family in families: self.assertEqual(family.requested_version_string, "9.9.9.9", "Version should be 9.9.9.9") def test_it_should_parse_missing_status_upload_blob_as_none(self): data_file = mockwiredata.DATA_FILE_VM_SETTINGS.copy() data_file["vm_settings"] = "hostgaplugin/vm_settings-no_status_upload_blob.json" with mock_wire_protocol(data_file) as protocol: extensions_goal_state = protocol.get_goal_state().extensions_goal_state self.assertIsNone(extensions_goal_state.status_upload_blob, "Expected status upload blob to be None") self.assertEqual("BlockBlob", extensions_goal_state.status_upload_blob_type, "Expected status upload blob to be Block") def test_it_should_parse_missing_agent_manifests_as_empty(self): data_file = mockwiredata.DATA_FILE_VM_SETTINGS.copy() data_file["vm_settings"] = "hostgaplugin/vm_settings-no_manifests.json" with mock_wire_protocol(data_file) as protocol: extensions_goal_state = protocol.get_goal_state().extensions_goal_state self.assertEqual(1, len(extensions_goal_state.agent_families), "Expected exactly one agent manifest. Got: {0}".format(extensions_goal_state.agent_families)) self.assertListEqual([], extensions_goal_state.agent_families[0].uris, "Expected an empty list of agent manifests") def test_it_should_parse_missing_extension_manifests_as_empty(self): data_file = mockwiredata.DATA_FILE_VM_SETTINGS.copy() data_file["vm_settings"] = "hostgaplugin/vm_settings-no_manifests.json" with mock_wire_protocol(data_file) as protocol: extensions_goal_state = protocol.get_goal_state().extensions_goal_state self.assertEqual(3, len(extensions_goal_state.extensions), "Incorrect number of extensions. Got: {0}".format(extensions_goal_state.extensions)) self.assertEqual([], extensions_goal_state.extensions[0].manifest_uris, "Expected an empty list of manifests for {0}".format(extensions_goal_state.extensions[0])) self.assertEqual([], extensions_goal_state.extensions[1].manifest_uris, "Expected an empty list of manifests for {0}".format(extensions_goal_state.extensions[1])) self.assertEqual( [ "https://umsakzkwhng2ft0jjptl.blob.core.windows.net/deeb2df6-c025-e6fb-b015-449ed6a676bc/deeb2df6-c025-e6fb-b015-449ed6a676bc_manifest.xml", "https://umsafmqfbv4hgrd1hqff.blob.core.windows.net/deeb2df6-c025-e6fb-b015-449ed6a676bc/deeb2df6-c025-e6fb-b015-449ed6a676bc_manifest.xml", ], extensions_goal_state.extensions[2].manifest_uris, "Incorrect list of manifests for {0}".format(extensions_goal_state.extensions[2])) def test_it_should_default_to_block_blob_when_the_status_blob_type_is_not_valid(self): data_file = mockwiredata.DATA_FILE_VM_SETTINGS.copy() data_file["vm_settings"] = "hostgaplugin/vm_settings-invalid_blob_type.json" with mock_wire_protocol(data_file) as protocol: extensions_goal_state = protocol.get_goal_state().extensions_goal_state self.assertEqual("BlockBlob", extensions_goal_state.status_upload_blob_type, 'Expected BlockBlob for an invalid statusBlobType') def test_its_source_channel_should_be_host_ga_plugin(self): with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS) as protocol: extensions_goal_state = protocol.get_goal_state().extensions_goal_state self.assertEqual(GoalStateChannel.HostGAPlugin, extensions_goal_state.channel, "The channel is incorrect") class CaseFoldedDictionaryTestCase(AgentTestCase): def test_it_should_retrieve_items_ignoring_case(self): dictionary = json.loads('''{ "activityId": "2e7f8b5d-f637-4721-b757-cb190d49b4e9", "StatusUploadBlob": { "statusBlobType": "BlockBlob", "value": "https://dcrcqabsr1.blob.core.windows.net/$system/edpxmal5j1.058b176d-445b-4e75-bd97-4911511b7d96.status" }, "gaFamilies": [ { "Name": "Prod", "Version": "2.5.0.2", "Uris": [ "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml", "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml" ] } ] }''') case_folded = _CaseFoldedDict.from_dict(dictionary) def test_retrieve_item(key, expected_value): """ Test for operators [] and in, and methods get() and has_key() """ try: self.assertEqual(expected_value, case_folded[key], "Operator [] retrieved incorrect value for '{0}'".format(key)) except KeyError: self.fail("Operator [] failed to retrieve '{0}'".format(key)) self.assertTrue(case_folded.has_key(key), "Method has_key() did not find '{0}'".format(key)) self.assertEqual(expected_value, case_folded.get(key), "Method get() retrieved incorrect value for '{0}'".format(key)) self.assertTrue(key in case_folded, "Operator in did not find key '{0}'".format(key)) test_retrieve_item("activityId", "2e7f8b5d-f637-4721-b757-cb190d49b4e9") test_retrieve_item("activityid", "2e7f8b5d-f637-4721-b757-cb190d49b4e9") test_retrieve_item("ACTIVITYID", "2e7f8b5d-f637-4721-b757-cb190d49b4e9") self.assertEqual("BlockBlob", case_folded["statusuploadblob"]["statusblobtype"], "Failed to retrieve item in nested dictionary") self.assertEqual("Prod", case_folded["gafamilies"][0]["name"], "Failed to retrieve item in nested array") WALinuxAgent-2.9.1.1/tests/protocol/test_goal_state.py000066400000000000000000000704671446033677600227640ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the Apache License. import contextlib import datetime import glob import os import re import time from azurelinuxagent.common import conf from azurelinuxagent.common.future import httpclient from azurelinuxagent.common.protocol.extensions_goal_state import GoalStateSource, GoalStateChannel from azurelinuxagent.common.protocol.extensions_goal_state_from_extensions_config import ExtensionsGoalStateFromExtensionsConfig from azurelinuxagent.common.protocol.extensions_goal_state_from_vm_settings import ExtensionsGoalStateFromVmSettings from azurelinuxagent.common.protocol import hostplugin from azurelinuxagent.common.protocol.goal_state import GoalState, GoalStateInconsistentError, \ _GET_GOAL_STATE_MAX_ATTEMPTS, GoalStateProperties from azurelinuxagent.common.exception import ProtocolError from azurelinuxagent.common.utils import fileutil from azurelinuxagent.common.utils.archive import ARCHIVE_DIRECTORY_NAME from tests.protocol.mocks import mock_wire_protocol, MockHttpResponse from tests.protocol import mockwiredata from tests.protocol.HttpRequestPredicates import HttpRequestPredicates from tests.tools import AgentTestCase, patch, load_data class GoalStateTestCase(AgentTestCase, HttpRequestPredicates): def test_it_should_use_vm_settings_by_default(self): with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS) as protocol: protocol.mock_wire_data.set_etag(888) extensions_goal_state = GoalState(protocol.client).extensions_goal_state self.assertTrue( isinstance(extensions_goal_state, ExtensionsGoalStateFromVmSettings), 'The extensions goal state should have been created from the vmSettings (got: {0})'.format(type(extensions_goal_state))) def _assert_is_extensions_goal_state_from_extensions_config(self, extensions_goal_state): self.assertTrue( isinstance(extensions_goal_state, ExtensionsGoalStateFromExtensionsConfig), 'The extensions goal state should have been created from the extensionsConfig (got: {0})'.format(type(extensions_goal_state))) def test_it_should_use_extensions_config_when_fast_track_is_disabled(self): with patch("azurelinuxagent.common.conf.get_enable_fast_track", return_value=False): with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS) as protocol: self._assert_is_extensions_goal_state_from_extensions_config(GoalState(protocol.client).extensions_goal_state) def test_it_should_use_extensions_config_when_fast_track_is_not_supported(self): def http_get_handler(url, *_, **__): if self.is_host_plugin_vm_settings_request(url): return MockHttpResponse(httpclient.NOT_FOUND) return None with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS, http_get_handler=http_get_handler) as protocol: self._assert_is_extensions_goal_state_from_extensions_config(GoalState(protocol.client).extensions_goal_state) def test_it_should_use_extensions_config_when_the_host_ga_plugin_version_is_not_supported(self): data_file = mockwiredata.DATA_FILE_VM_SETTINGS.copy() data_file["vm_settings"] = "hostgaplugin/vm_settings-unsupported_version.json" with mock_wire_protocol(data_file) as protocol: self._assert_is_extensions_goal_state_from_extensions_config(GoalState(protocol.client).extensions_goal_state) def test_it_should_retry_get_vm_settings_on_resource_gone_error(self): # Requests to the hostgaplugin incude the Container ID and the RoleConfigName as headers; when the hostgaplugin returns GONE (HTTP status 410) the agent # needs to get a new goal state and retry the request with updated values for the Container ID and RoleConfigName headers. with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS) as protocol: # Do not mock the vmSettings request at the level of azurelinuxagent.common.utils.restutil.http_request. The GONE status is handled # in the internal _http_request, which we mock below. protocol.do_not_mock = lambda method, url: method == "GET" and self.is_host_plugin_vm_settings_request(url) request_headers = [] # we expect a retry with new headers and use this array to persist the headers of each request def http_get_vm_settings(_method, _host, _relative_url, _timeout, **kwargs): request_headers.append(kwargs["headers"]) if len(request_headers) == 1: # Fail the first request with status GONE and update the mock data to return the new Container ID and RoleConfigName that should be # used in the headers of the retry request. protocol.mock_wire_data.set_container_id("GET_VM_SETTINGS_TEST_CONTAINER_ID") protocol.mock_wire_data.set_role_config_name("GET_VM_SETTINGS_TEST_ROLE_CONFIG_NAME") return MockHttpResponse(status=httpclient.GONE) # For this test we are interested only on the retry logic, so the second request (the retry) is not important; we use NOT_MODIFIED (304) for simplicity. return MockHttpResponse(status=httpclient.NOT_MODIFIED) with patch("azurelinuxagent.common.utils.restutil._http_request", side_effect=http_get_vm_settings): protocol.client.update_goal_state() self.assertEqual(2, len(request_headers), "We expected 2 requests for vmSettings: the original request and the retry request") self.assertEqual("GET_VM_SETTINGS_TEST_CONTAINER_ID", request_headers[1][hostplugin._HEADER_CONTAINER_ID], "The retry request did not include the expected header for the ContainerId") self.assertEqual("GET_VM_SETTINGS_TEST_ROLE_CONFIG_NAME", request_headers[1][hostplugin._HEADER_HOST_CONFIG_NAME], "The retry request did not include the expected header for the RoleConfigName") def test_fetch_goal_state_should_raise_on_incomplete_goal_state(self): with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: protocol.mock_wire_data.data_files = mockwiredata.DATA_FILE_NOOP_GS protocol.mock_wire_data.reload() protocol.mock_wire_data.set_incarnation(2) with patch('time.sleep') as mock_sleep: with self.assertRaises(ProtocolError): GoalState(protocol.client) self.assertEqual(_GET_GOAL_STATE_MAX_ATTEMPTS, mock_sleep.call_count, "Unexpected number of retries") def test_fetching_the_goal_state_should_save_the_shared_config(self): # SharedConfig.xml is used by other components (Azsec and Singularity/HPC Infiniband); verify that we do not delete it with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS) as protocol: _ = GoalState(protocol.client) shared_config = os.path.join(conf.get_lib_dir(), 'SharedConfig.xml') self.assertTrue(os.path.exists(shared_config), "{0} should have been created".format(shared_config)) def test_fetching_the_goal_state_should_save_the_goal_state_to_the_history_directory(self): with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS) as protocol: protocol.mock_wire_data.set_incarnation(999) protocol.mock_wire_data.set_etag(888) _ = GoalState(protocol.client) self._assert_directory_contents( self._find_history_subdirectory("999-888"), ["GoalState.xml", "ExtensionsConfig.xml", "VmSettings.json", "Certificates.json", "SharedConfig.xml", "HostingEnvironmentConfig.xml"]) def _find_history_subdirectory(self, tag): matches = glob.glob(os.path.join(self.tmp_dir, ARCHIVE_DIRECTORY_NAME, "*_{0}".format(tag))) self.assertTrue(len(matches) == 1, "Expected one history directory for tag {0}. Got: {1}".format(tag, matches)) return matches[0] def _assert_directory_contents(self, directory, expected_files): actual_files = os.listdir(directory) expected_files.sort() actual_files.sort() self.assertEqual(expected_files, actual_files, "The expected files were not saved to {0}".format(directory)) def test_update_should_create_new_history_subdirectories(self): with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS) as protocol: protocol.mock_wire_data.set_incarnation(123) protocol.mock_wire_data.set_etag(654) goal_state = GoalState(protocol.client) self._assert_directory_contents( self._find_history_subdirectory("123-654"), ["GoalState.xml", "ExtensionsConfig.xml", "VmSettings.json", "Certificates.json", "SharedConfig.xml", "HostingEnvironmentConfig.xml"]) def http_get_handler(url, *_, **__): if HttpRequestPredicates.is_host_plugin_vm_settings_request(url): return MockHttpResponse(status=httpclient.NOT_MODIFIED) return None protocol.mock_wire_data.set_incarnation(234) protocol.set_http_handlers(http_get_handler=http_get_handler) goal_state.update() self._assert_directory_contents( self._find_history_subdirectory("234-654"), ["GoalState.xml", "ExtensionsConfig.xml", "Certificates.json", "SharedConfig.xml", "HostingEnvironmentConfig.xml"]) protocol.mock_wire_data.set_etag(987) protocol.set_http_handlers(http_get_handler=None) goal_state.update() self._assert_directory_contents( self._find_history_subdirectory("234-987"), ["VmSettings.json", "Certificates.json"]) def test_it_should_redact_the_protected_settings_when_saving_to_the_history_directory(self): with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS) as protocol: protocol.mock_wire_data.set_incarnation(888) protocol.mock_wire_data.set_etag(888) goal_state = GoalState(protocol.client) extensions_goal_state = goal_state.extensions_goal_state protected_settings = [] for ext_handler in extensions_goal_state.extensions: for extension in ext_handler.settings: if extension.protectedSettings is not None: protected_settings.append(extension.protectedSettings) if len(protected_settings) == 0: raise Exception("The test goal state does not include any protected settings") history_directory = self._find_history_subdirectory("888-888") extensions_config_file = os.path.join(history_directory, "ExtensionsConfig.xml") vm_settings_file = os.path.join(history_directory, "VmSettings.json") for file_name in extensions_config_file, vm_settings_file: with open(file_name, "r") as stream: file_contents = stream.read() for settings in protected_settings: self.assertNotIn( settings, file_contents, "The protectedSettings should not have been saved to {0}".format(file_name)) matches = re.findall(r'"protectedSettings"\s*:\s*"\*\*\* REDACTED \*\*\*"', file_contents) self.assertEqual( len(matches), len(protected_settings), "Could not find the expected number of redacted settings in {0}.\nExpected {1}.\n{2}".format(file_name, len(protected_settings), file_contents)) def test_it_should_save_vm_settings_on_parse_errors(self): with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS) as protocol: invalid_vm_settings_file = "hostgaplugin/vm_settings-parse_error.json" data_file = mockwiredata.DATA_FILE_VM_SETTINGS.copy() data_file["vm_settings"] = invalid_vm_settings_file protocol.mock_wire_data = mockwiredata.WireProtocolData(data_file) with self.assertRaises(ProtocolError): # the parsing error will cause an exception _ = GoalState(protocol.client) # Do an extra call to update the goal state; this should save the vmsettings to the history directory # only once (self._find_history_subdirectory asserts 1 single match) time.sleep(0.1) # add a short delay to ensure that a new timestamp would be saved in the history folder protocol.mock_wire_data.set_etag(888) with self.assertRaises(ProtocolError): _ = GoalState(protocol.client) history_directory = self._find_history_subdirectory("888") vm_settings_file = os.path.join(history_directory, "VmSettings.json") self.assertTrue(os.path.exists(vm_settings_file), "{0} was not saved".format(vm_settings_file)) expected = load_data(invalid_vm_settings_file) actual = fileutil.read_file(vm_settings_file) self.assertEqual(expected, actual, "The vmSettings were not saved correctly") @staticmethod @contextlib.contextmanager def _create_protocol_ws_and_hgap_in_sync(): """ Creates a mock protocol in which the HostGAPlugin and the WireServer are in sync, both of them returning the same Fabric goal state. """ data_file = mockwiredata.DATA_FILE_VM_SETTINGS.copy() with mock_wire_protocol(data_file) as protocol: timestamp = datetime.datetime.utcnow() incarnation = '111' etag = '111111' protocol.mock_wire_data.set_incarnation(incarnation, timestamp=timestamp) protocol.mock_wire_data.set_etag(etag, timestamp=timestamp) protocol.mock_wire_data.set_vm_settings_source(GoalStateSource.Fabric) # Do a few checks on the mock data to ensure we catch changes in internal implementations # that may invalidate this setup. vm_settings, _ = protocol.client.get_host_plugin().fetch_vm_settings() if vm_settings.etag != etag: raise Exception("The HostGAPlugin is not in sync. Expected ETag {0}. Got {1}".format(etag, vm_settings.etag)) if vm_settings.source != GoalStateSource.Fabric: raise Exception("The HostGAPlugin should be returning a Fabric goal state. Got {0}".format(vm_settings.source)) goal_state = GoalState(protocol.client) if goal_state.incarnation != incarnation: raise Exception("The WireServer is not in sync. Expected incarnation {0}. Got {1}".format(incarnation, goal_state.incarnation)) if goal_state.extensions_goal_state.correlation_id != vm_settings.correlation_id: raise Exception( "The correlation ID in the WireServer and HostGAPlugin are not in sync. WS: {0} HGAP: {1}".format( goal_state.extensions_goal_state.correlation_id, vm_settings.correlation_id)) yield protocol def _assert_goal_state(self, goal_state, goal_state_id, channel=None, source=None): self.assertIn(goal_state_id, goal_state.extensions_goal_state.id, "Incorrect Goal State ID") if channel is not None: self.assertEqual(channel, goal_state.extensions_goal_state.channel, "Incorrect Goal State channel") if source is not None: self.assertEqual(source, goal_state.extensions_goal_state.source, "Incorrect Goal State source") def test_it_should_ignore_fabric_goal_states_from_the_host_ga_plugin(self): with GoalStateTestCase._create_protocol_ws_and_hgap_in_sync() as protocol: # # Verify __init__() # expected_incarnation = '111' # test setup initializes to this value timestamp = datetime.datetime.utcnow() + datetime.timedelta(seconds=15) protocol.mock_wire_data.set_etag('22222', timestamp) goal_state = GoalState(protocol.client) self._assert_goal_state(goal_state, expected_incarnation, channel=GoalStateChannel.WireServer) # # Verify update() # timestamp += datetime.timedelta(seconds=15) protocol.mock_wire_data.set_etag('333333', timestamp) goal_state.update() self._assert_goal_state(goal_state, expected_incarnation, channel=GoalStateChannel.WireServer) def test_it_should_use_fast_track_goal_states_from_the_host_ga_plugin(self): with GoalStateTestCase._create_protocol_ws_and_hgap_in_sync() as protocol: protocol.mock_wire_data.set_vm_settings_source(GoalStateSource.FastTrack) # # Verify __init__() # expected_etag = '22222' timestamp = datetime.datetime.utcnow() + datetime.timedelta(seconds=15) protocol.mock_wire_data.set_etag(expected_etag, timestamp) goal_state = GoalState(protocol.client) self._assert_goal_state(goal_state, expected_etag, channel=GoalStateChannel.HostGAPlugin) # # Verify update() # expected_etag = '333333' timestamp += datetime.timedelta(seconds=15) protocol.mock_wire_data.set_etag(expected_etag, timestamp) goal_state.update() self._assert_goal_state(goal_state, expected_etag, channel=GoalStateChannel.HostGAPlugin) def test_it_should_use_the_most_recent_goal_state(self): with GoalStateTestCase._create_protocol_ws_and_hgap_in_sync() as protocol: goal_state = GoalState(protocol.client) # The most recent goal state is FastTrack timestamp = datetime.datetime.utcnow() + datetime.timedelta(seconds=15) protocol.mock_wire_data.set_vm_settings_source(GoalStateSource.FastTrack) protocol.mock_wire_data.set_etag('222222', timestamp) goal_state.update() self._assert_goal_state(goal_state, '222222', channel=GoalStateChannel.HostGAPlugin, source=GoalStateSource.FastTrack) # The most recent goal state is Fabric timestamp += datetime.timedelta(seconds=15) protocol.mock_wire_data.set_incarnation('222', timestamp) goal_state.update() self._assert_goal_state(goal_state, '222', channel=GoalStateChannel.WireServer, source=GoalStateSource.Fabric) # The most recent goal state is Fabric, but it is coming from the HostGAPlugin (should be ignored) timestamp += datetime.timedelta(seconds=15) protocol.mock_wire_data.set_vm_settings_source(GoalStateSource.Fabric) protocol.mock_wire_data.set_etag('333333', timestamp) goal_state.update() self._assert_goal_state(goal_state, '222', channel=GoalStateChannel.WireServer, source=GoalStateSource.Fabric) def test_it_should_mark_outdated_goal_states(self): with GoalStateTestCase._create_protocol_ws_and_hgap_in_sync() as protocol: goal_state = GoalState(protocol.client) initial_incarnation = goal_state.incarnation initial_timestamp = goal_state.extensions_goal_state.created_on_timestamp # Make the most recent goal state FastTrack timestamp = datetime.datetime.utcnow() + datetime.timedelta(seconds=15) protocol.mock_wire_data.set_vm_settings_source(GoalStateSource.FastTrack) protocol.mock_wire_data.set_etag('444444', timestamp) goal_state.update() # Update the goal state after the HGAP plugin stops supporting vmSettings def http_get_handler(url, *_, **__): if self.is_host_plugin_vm_settings_request(url): return MockHttpResponse(httpclient.NOT_FOUND) return None protocol.set_http_handlers(http_get_handler=http_get_handler) goal_state.update() self._assert_goal_state(goal_state, initial_incarnation, channel=GoalStateChannel.WireServer, source=GoalStateSource.Fabric) self.assertEqual(initial_timestamp, goal_state.extensions_goal_state.created_on_timestamp, "The timestamp of the updated goal state is incorrect") self.assertTrue(goal_state.extensions_goal_state.is_outdated, "The updated goal state should be marked as outdated") def test_it_should_raise_when_the_tenant_certificate_is_missing(self): data_file = mockwiredata.DATA_FILE_VM_SETTINGS.copy() with mock_wire_protocol(data_file) as protocol: data_file["vm_settings"] = "hostgaplugin/vm_settings-missing_cert.json" protocol.mock_wire_data.reload() protocol.mock_wire_data.set_etag(888) with self.assertRaises(GoalStateInconsistentError) as context: _ = GoalState(protocol.client) expected_message = "Certificate 59A10F50FFE2A0408D3F03FE336C8FD5716CF25C needed by Microsoft.OSTCExtensions.VMAccessForLinux is missing from the goal state" self.assertIn(expected_message, str(context.exception)) def test_it_should_download_certs_on_a_new_fast_track_goal_state(self): data_file = mockwiredata.DATA_FILE_VM_SETTINGS.copy() with mock_wire_protocol(data_file) as protocol: goal_state = GoalState(protocol.client) cert = "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F" crt_path = os.path.join(self.tmp_dir, cert + ".crt") prv_path = os.path.join(self.tmp_dir, cert + ".prv") # Check that crt and prv files are downloaded after processing goal state self.assertTrue(os.path.isfile(crt_path)) self.assertTrue(os.path.isfile(prv_path)) # Remove .crt file os.remove(crt_path) if os.path.isfile(crt_path): raise Exception("{0}.crt was not removed.".format(cert)) # Update goal state and check that .crt was downloaded protocol.mock_wire_data.set_etag(888) goal_state.update() self.assertTrue(os.path.isfile(crt_path)) def test_it_should_download_certs_on_a_new_fabric_goal_state(self): data_file = mockwiredata.DATA_FILE_VM_SETTINGS.copy() with mock_wire_protocol(data_file) as protocol: protocol.mock_wire_data.set_vm_settings_source(GoalStateSource.Fabric) goal_state = GoalState(protocol.client) cert = "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F" crt_path = os.path.join(self.tmp_dir, cert + ".crt") prv_path = os.path.join(self.tmp_dir, cert + ".prv") # Check that crt and prv files are downloaded after processing goal state self.assertTrue(os.path.isfile(crt_path)) self.assertTrue(os.path.isfile(prv_path)) # Remove .crt file os.remove(crt_path) if os.path.isfile(crt_path): raise Exception("{0}.crt was not removed.".format(cert)) # Update goal state and check that .crt was downloaded protocol.mock_wire_data.set_incarnation(999) goal_state.update() self.assertTrue(os.path.isfile(crt_path)) def test_it_should_refresh_the_goal_state_when_it_is_inconsistent(self): # # Some scenarios can produce inconsistent goal states. For example, during hibernation/resume, the Fabric goal state changes (the # tenant certificate is re-generated when the VM is restarted) *without* the incarnation changing. If a Fast Track goal state # comes after that, the extensions will need the new certificate. This test simulates that scenario by mocking the certificates # request and returning first a set of certificates (certs-2.xml) that do not match those needed by the extensions, and then a # set (certs.xml) that does match. The test then ensures that the goal state was refreshed and the correct certificates were # fetched. # data_files = [ "wire/certs-2.xml", "wire/certs.xml" ] def http_get_handler(url, *_, **__): if HttpRequestPredicates.is_certificates_request(url): http_get_handler.certificate_requests += 1 if http_get_handler.certificate_requests < len(data_files): data = load_data(data_files[http_get_handler.certificate_requests - 1]) return MockHttpResponse(status=200, body=data.encode('utf-8')) return None http_get_handler.certificate_requests = 0 with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS) as protocol: protocol.set_http_handlers(http_get_handler=http_get_handler) protocol.mock_wire_data.reset_call_counts() goal_state = GoalState(protocol.client) self.assertEqual(2, protocol.mock_wire_data.call_counts['goalstate'], "There should have been exactly 2 requests for the goal state (original + refresh)") self.assertEqual(4, http_get_handler.certificate_requests, "There should have been exactly 4 requests for the goal state certificates (2x original + 2x refresh)") thumbprints = [c.thumbprint for c in goal_state.certs.cert_list.certificates] for extension in goal_state.extensions_goal_state.extensions: for settings in extension.settings: if settings.protectedSettings is not None: self.assertIn(settings.certificateThumbprint, thumbprints, "Certificate is missing from the goal state.") def test_it_should_raise_when_goal_state_properties_not_initialized(self): with GoalStateTestCase._create_protocol_ws_and_hgap_in_sync() as protocol: goal_state = GoalState( protocol.client, goal_state_properties=~GoalStateProperties.All) goal_state.update() with self.assertRaises(ProtocolError) as context: _ = goal_state.container_id expected_message = "ContainerId is not in goal state properties" self.assertIn(expected_message, str(context.exception)) with self.assertRaises(ProtocolError) as context: _ = goal_state.role_config_name expected_message = "RoleConfig is not in goal state properties" self.assertIn(expected_message, str(context.exception)) with self.assertRaises(ProtocolError) as context: _ = goal_state.role_instance_id expected_message = "RoleInstanceId is not in goal state properties" self.assertIn(expected_message, str(context.exception)) with self.assertRaises(ProtocolError) as context: _ = goal_state.extensions_goal_state expected_message = "ExtensionsGoalState is not in goal state properties" self.assertIn(expected_message, str(context.exception)) with self.assertRaises(ProtocolError) as context: _ = goal_state.hosting_env expected_message = "HostingEnvironment is not in goal state properties" self.assertIn(expected_message, str(context.exception)) with self.assertRaises(ProtocolError) as context: _ = goal_state.certs expected_message = "Certificates is not in goal state properties" self.assertIn(expected_message, str(context.exception)) with self.assertRaises(ProtocolError) as context: _ = goal_state.shared_conf expected_message = "SharedConfig is not in goal state properties" self.assertIn(expected_message, str(context.exception)) with self.assertRaises(ProtocolError) as context: _ = goal_state.remote_access expected_message = "RemoteAccessInfo is not in goal state properties" self.assertIn(expected_message, str(context.exception)) goal_state = GoalState( protocol.client, goal_state_properties=GoalStateProperties.All & ~GoalStateProperties.HostingEnv) goal_state.update() _ = goal_state.container_id, goal_state.role_instance_id, goal_state.role_config_name, \ goal_state.extensions_goal_state, goal_state.certs, goal_state.shared_conf, goal_state.remote_access with self.assertRaises(ProtocolError) as context: _ = goal_state.hosting_env expected_message = "HostingEnvironment is not in goal state properties" self.assertIn(expected_message, str(context.exception)) WALinuxAgent-2.9.1.1/tests/protocol/test_healthservice.py000066400000000000000000000261111446033677600234530ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ import json from azurelinuxagent.common.exception import HttpError from azurelinuxagent.common.protocol.healthservice import Observation, HealthService from azurelinuxagent.common.utils import restutil from tests.protocol.test_hostplugin import MockResponse from tests.tools import AgentTestCase, patch class TestHealthService(AgentTestCase): def assert_status_code(self, status_code, expected_healthy): response = MockResponse('response', status_code) is_healthy = not restutil.request_failed_at_hostplugin(response) self.assertEqual(expected_healthy, is_healthy) def assert_observation(self, call_args, name, is_healthy, value, description): endpoint = call_args[0][0] content = call_args[0][1] jo = json.loads(content) api = jo['Api'] source = jo['Source'] version = jo['Version'] obs = jo['Observations'] fo = obs[0] obs_name = fo['ObservationName'] obs_healthy = fo['IsHealthy'] obs_value = fo['Value'] obs_description = fo['Description'] self.assertEqual('application/json', call_args[1]['headers']['Content-Type']) self.assertEqual('http://endpoint:80/HealthService', endpoint) self.assertEqual('reporttargethealth', api) self.assertEqual('WALinuxAgent', source) self.assertEqual('1.0', version) self.assertEqual(name, obs_name) self.assertEqual(value, obs_value) self.assertEqual(is_healthy, obs_healthy) self.assertEqual(description, obs_description) def assert_telemetry(self, call_args, response=''): args, kw_args = call_args # pylint: disable=unused-variable self.assertFalse(kw_args['is_success']) self.assertEqual('HealthObservation', kw_args['op']) obs = json.loads(kw_args['message']) self.assertEqual(obs['Value'], response) def test_observation_validity(self): try: Observation(name=None, is_healthy=True) self.fail('Empty observation name should raise ValueError') except ValueError: pass try: Observation(name='Name', is_healthy=None) self.fail('Empty measurement should raise ValueError') except ValueError: pass o = Observation(name='Name', is_healthy=True, value=None, description=None) self.assertEqual('', o.value) self.assertEqual('', o.description) long_str = 's' * 200 o = Observation(name=long_str, is_healthy=True, value=long_str, description=long_str) self.assertEqual(200, len(o.name)) self.assertEqual(200, len(o.value)) self.assertEqual(200, len(o.description)) self.assertEqual(64, len(o.as_obj['ObservationName'])) self.assertEqual(128, len(o.as_obj['Value'])) self.assertEqual(128, len(o.as_obj['Description'])) def test_observation_json(self): health_service = HealthService('endpoint') health_service.observations.append(Observation(name='name', is_healthy=True, value='value', description='description')) expected_json = '{"Source": "WALinuxAgent", ' \ '"Api": "reporttargethealth", ' \ '"Version": "1.0", ' \ '"Observations": [{' \ '"Value": "value", ' \ '"ObservationName": "name", ' \ '"Description": "description", ' \ '"IsHealthy": true' \ '}]}' expected = sorted(json.loads(expected_json).items()) actual = sorted(json.loads(health_service.as_json).items()) self.assertEqual(expected, actual) @patch('azurelinuxagent.common.event.add_event') @patch("azurelinuxagent.common.utils.restutil.http_post") def test_reporting(self, patch_post, patch_add_event): health_service = HealthService('endpoint') health_service.report_host_plugin_status(is_healthy=True, response='response') self.assertEqual(1, patch_post.call_count) self.assertEqual(0, patch_add_event.call_count) self.assert_observation(call_args=patch_post.call_args, name=HealthService.HOST_PLUGIN_STATUS_OBSERVATION_NAME, is_healthy=True, value='response', description='') self.assertEqual(0, len(health_service.observations)) health_service.report_host_plugin_status(is_healthy=False, response='error') self.assertEqual(2, patch_post.call_count) self.assertEqual(1, patch_add_event.call_count) self.assert_telemetry(call_args=patch_add_event.call_args, response='error') self.assert_observation(call_args=patch_post.call_args, name=HealthService.HOST_PLUGIN_STATUS_OBSERVATION_NAME, is_healthy=False, value='error', description='') self.assertEqual(0, len(health_service.observations)) health_service.report_host_plugin_extension_artifact(is_healthy=True, source='source', response='response') self.assertEqual(3, patch_post.call_count) self.assertEqual(1, patch_add_event.call_count) self.assert_observation(call_args=patch_post.call_args, name=HealthService.HOST_PLUGIN_ARTIFACT_OBSERVATION_NAME, is_healthy=True, value='response', description='source') self.assertEqual(0, len(health_service.observations)) health_service.report_host_plugin_extension_artifact(is_healthy=False, source='source', response='response') self.assertEqual(4, patch_post.call_count) self.assertEqual(2, patch_add_event.call_count) self.assert_telemetry(call_args=patch_add_event.call_args, response='response') self.assert_observation(call_args=patch_post.call_args, name=HealthService.HOST_PLUGIN_ARTIFACT_OBSERVATION_NAME, is_healthy=False, value='response', description='source') self.assertEqual(0, len(health_service.observations)) health_service.report_host_plugin_heartbeat(is_healthy=True) self.assertEqual(5, patch_post.call_count) self.assertEqual(2, patch_add_event.call_count) self.assert_observation(call_args=patch_post.call_args, name=HealthService.HOST_PLUGIN_HEARTBEAT_OBSERVATION_NAME, is_healthy=True, value='', description='') self.assertEqual(0, len(health_service.observations)) health_service.report_host_plugin_heartbeat(is_healthy=False) self.assertEqual(3, patch_add_event.call_count) self.assert_telemetry(call_args=patch_add_event.call_args) self.assertEqual(6, patch_post.call_count) self.assert_observation(call_args=patch_post.call_args, name=HealthService.HOST_PLUGIN_HEARTBEAT_OBSERVATION_NAME, is_healthy=False, value='', description='') self.assertEqual(0, len(health_service.observations)) health_service.report_host_plugin_versions(is_healthy=True, response='response') self.assertEqual(7, patch_post.call_count) self.assertEqual(3, patch_add_event.call_count) self.assert_observation(call_args=patch_post.call_args, name=HealthService.HOST_PLUGIN_VERSIONS_OBSERVATION_NAME, is_healthy=True, value='response', description='') self.assertEqual(0, len(health_service.observations)) health_service.report_host_plugin_versions(is_healthy=False, response='response') self.assertEqual(8, patch_post.call_count) self.assertEqual(4, patch_add_event.call_count) self.assert_telemetry(call_args=patch_add_event.call_args, response='response') self.assert_observation(call_args=patch_post.call_args, name=HealthService.HOST_PLUGIN_VERSIONS_OBSERVATION_NAME, is_healthy=False, value='response', description='') self.assertEqual(0, len(health_service.observations)) patch_post.side_effect = HttpError() health_service.report_host_plugin_versions(is_healthy=True, response='') self.assertEqual(9, patch_post.call_count) self.assertEqual(4, patch_add_event.call_count) self.assertEqual(0, len(health_service.observations)) def test_observation_length(self): health_service = HealthService('endpoint') # make 100 observations for i in range(0, 100): health_service._observe(is_healthy=True, name='{0}'.format(i)) # ensure we keep only 10 self.assertEqual(10, len(health_service.observations)) # ensure we keep the most recent 10 self.assertEqual('90', health_service.observations[0].name) self.assertEqual('99', health_service.observations[9].name) def test_status_codes(self): # healthy self.assert_status_code(status_code=200, expected_healthy=True) self.assert_status_code(status_code=201, expected_healthy=True) self.assert_status_code(status_code=302, expected_healthy=True) self.assert_status_code(status_code=400, expected_healthy=True) self.assert_status_code(status_code=416, expected_healthy=True) self.assert_status_code(status_code=419, expected_healthy=True) self.assert_status_code(status_code=429, expected_healthy=True) self.assert_status_code(status_code=502, expected_healthy=True) # unhealthy self.assert_status_code(status_code=500, expected_healthy=False) self.assert_status_code(status_code=501, expected_healthy=False) self.assert_status_code(status_code=503, expected_healthy=False) self.assert_status_code(status_code=504, expected_healthy=False) WALinuxAgent-2.9.1.1/tests/protocol/test_hostplugin.py000066400000000000000000001437161446033677600230340ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import base64 import contextlib import datetime import json import os.path import sys import unittest from azurelinuxagent.common.protocol import hostplugin, restapi, wire from azurelinuxagent.common import conf from azurelinuxagent.common.errorstate import ErrorState from azurelinuxagent.common.exception import HttpError, ResourceGoneError, ProtocolError from azurelinuxagent.common.future import ustr, httpclient from azurelinuxagent.common.osutil.default import UUID_PATTERN from azurelinuxagent.common.protocol.hostplugin import API_VERSION, _VmSettingsErrorReporter, VmSettingsNotSupported, VmSettingsSupportStopped from azurelinuxagent.common.protocol.extensions_goal_state import GoalStateSource from azurelinuxagent.common.protocol.goal_state import GoalState from azurelinuxagent.common.utils import restutil from azurelinuxagent.common.version import AGENT_VERSION, AGENT_NAME from tests.protocol.mocks import mock_wire_protocol, mockwiredata, MockHttpResponse from tests.protocol.HttpRequestPredicates import HttpRequestPredicates from tests.protocol.mockwiredata import DATA_FILE, DATA_FILE_NO_EXT from tests.tools import AgentTestCase, PY_VERSION_MAJOR, Mock, patch hostplugin_status_url = "http://168.63.129.16:32526/status" hostplugin_versions_url = "http://168.63.129.16:32526/versions" health_service_url = 'http://168.63.129.16:80/HealthService' hostplugin_logs_url = "http://168.63.129.16:32526/vmAgentLog" sas_url = "http://sas_url" wireserver_url = "168.63.129.16" block_blob_type = 'BlockBlob' page_blob_type = 'PageBlob' api_versions = '["2015-09-01"]' storage_version = "2014-02-14" faux_status = "{ 'dummy' : 'data' }" faux_status_b64 = base64.b64encode(bytes(bytearray(faux_status, encoding='utf-8'))) if PY_VERSION_MAJOR > 2: faux_status_b64 = faux_status_b64.decode('utf-8') class TestHostPlugin(HttpRequestPredicates, AgentTestCase): def _init_host(self): with mock_wire_protocol(DATA_FILE) as protocol: host_plugin = wire.HostPluginProtocol(wireserver_url) GoalState.update_host_plugin_headers(protocol.client) self.assertTrue(host_plugin.health_service is not None) return host_plugin def _init_status_blob(self): wire_protocol_client = wire.WireProtocol(wireserver_url).client status_blob = wire_protocol_client.status_blob status_blob.data = faux_status status_blob.vm_status = restapi.VMStatus(message="Ready", status="Ready") return status_blob def _relax_timestamp(self, headers): new_headers = [] for header in headers: header_value = header['headerValue'] if header['headerName'] == 'x-ms-date': timestamp = header['headerValue'] header_value = timestamp[:timestamp.rfind(":")] new_header = {header['headerName']: header_value} new_headers.append(new_header) return new_headers def _compare_data(self, actual, expected): # Remove seconds from the timestamps for testing purposes, that level or granularity introduces test flakiness actual['headers'] = self._relax_timestamp(actual['headers']) expected['headers'] = self._relax_timestamp(expected['headers']) for k in iter(expected.keys()): if k == 'content' or k == 'requestUri': if actual[k] != expected[k]: print("Mismatch: Actual '{0}'='{1}', " "Expected '{0}'='{2}'".format(k, actual[k], expected[k])) return False elif k == 'headers': for h in expected['headers']: if not (h in actual['headers']): print("Missing Header: '{0}'".format(h)) return False else: print("Unexpected Key: '{0}'".format(k)) return False return True def _hostplugin_data(self, blob_headers, content=None): headers = [] for name in iter(blob_headers.keys()): headers.append({ 'headerName': name, 'headerValue': blob_headers[name] }) data = { 'requestUri': sas_url, 'headers': headers } if not content is None: s = base64.b64encode(bytes(content)) if PY_VERSION_MAJOR > 2: s = s.decode('utf-8') data['content'] = s return data def _hostplugin_headers(self, goal_state): return { 'x-ms-version': '2015-09-01', 'Content-type': 'application/json', 'x-ms-containerid': goal_state.container_id, 'x-ms-host-config-name': goal_state.role_config_name } def _validate_hostplugin_args(self, args, goal_state, exp_method, exp_url, exp_data): args, kwargs = args self.assertEqual(exp_method, args[0]) self.assertEqual(exp_url, args[1]) self.assertTrue(self._compare_data(json.loads(args[2]), exp_data)) headers = kwargs['headers'] self.assertEqual(headers['x-ms-containerid'], goal_state.container_id) self.assertEqual(headers['x-ms-host-config-name'], goal_state.role_config_name) @staticmethod @contextlib.contextmanager def create_mock_protocol(): data_file = DATA_FILE_NO_EXT.copy() data_file["ext_conf"] = "wire/ext_conf_no_extensions-page_blob.xml" with mock_wire_protocol(data_file) as protocol: status = restapi.VMStatus(status="Ready", message="Guest Agent is running") protocol.client.status_blob.set_vm_status(status) # Also, they mock WireClient.update_goal_state() to verify how it is called protocol.client.update_goal_state = Mock() yield protocol @patch("azurelinuxagent.common.protocol.healthservice.HealthService.report_host_plugin_versions") @patch("azurelinuxagent.common.protocol.hostplugin.restutil.http_get") @patch("azurelinuxagent.common.protocol.hostplugin.add_event") def assert_ensure_initialized(self, patch_event, patch_http_get, patch_report_health, response_body, response_status_code, should_initialize, should_report_healthy): host = hostplugin.HostPluginProtocol(endpoint='ws') host.is_initialized = False patch_http_get.return_value = MockResponse(body=response_body, reason='reason', status_code=response_status_code) return_value = host.ensure_initialized() self.assertEqual(return_value, host.is_available) self.assertEqual(should_initialize, host.is_initialized) init_events = [kwargs for _, kwargs in patch_event.call_args_list if kwargs['op'] == 'InitializeHostPlugin'] self.assertEqual(1, len(init_events), 'Expected exactly 1 InitializeHostPlugin event') self.assertEqual(should_initialize, init_events[0]['is_success']) self.assertEqual(1, patch_report_health.call_count) self.assertEqual(should_report_healthy, patch_report_health.call_args[1]['is_healthy']) actual_response = patch_report_health.call_args[1]['response'] if should_initialize: self.assertEqual('', actual_response) else: self.assertTrue('HTTP Failed' in actual_response) self.assertTrue(response_body in actual_response) self.assertTrue(ustr(response_status_code) in actual_response) def test_ensure_initialized(self): """ Test calls to ensure_initialized """ self.assert_ensure_initialized(response_body=api_versions, # pylint: disable=no-value-for-parameter response_status_code=200, should_initialize=True, should_report_healthy=True) self.assert_ensure_initialized(response_body='invalid ip', # pylint: disable=no-value-for-parameter response_status_code=400, should_initialize=False, should_report_healthy=True) self.assert_ensure_initialized(response_body='generic bad request', # pylint: disable=no-value-for-parameter response_status_code=400, should_initialize=False, should_report_healthy=True) self.assert_ensure_initialized(response_body='resource gone', # pylint: disable=no-value-for-parameter response_status_code=410, should_initialize=False, should_report_healthy=True) self.assert_ensure_initialized(response_body='generic error', # pylint: disable=no-value-for-parameter response_status_code=500, should_initialize=False, should_report_healthy=False) self.assert_ensure_initialized(response_body='upstream error', # pylint: disable=no-value-for-parameter response_status_code=502, should_initialize=False, should_report_healthy=True) @patch("azurelinuxagent.common.protocol.hostplugin.HostPluginProtocol.ensure_initialized", return_value=True) @patch("azurelinuxagent.common.protocol.wire.StatusBlob.upload", return_value=False) @patch("azurelinuxagent.common.protocol.hostplugin.HostPluginProtocol._put_page_blob_status") def test_default_channel(self, patch_put, patch_upload, _): """ Status now defaults to HostPlugin. Validate that any errors on the public channel are ignored. Validate that the default channel is never changed as part of status upload. """ with self.create_mock_protocol() as wire_protocol: wire.HostPluginProtocol.is_default_channel = False wire_protocol.client.update_goal_state() # act wire_protocol.client.upload_status_blob() # assert direct route is not called self.assertEqual(0, patch_upload.call_count, "Direct channel was used") # assert host plugin route is called self.assertEqual(1, patch_put.call_count, "Host plugin was not used") # assert update goal state is only called once self.assertEqual(1, wire_protocol.client.update_goal_state.call_count, "Unexpected call count") # ensure the correct url is used self.assertEqual(sas_url, patch_put.call_args[0][0]) # ensure host plugin is not set as default self.assertFalse(wire.HostPluginProtocol.is_default_channel) @patch("azurelinuxagent.common.protocol.hostplugin.HostPluginProtocol.ensure_initialized", return_value=True) @patch("azurelinuxagent.common.protocol.wire.StatusBlob.upload", return_value=True) @patch("azurelinuxagent.common.protocol.hostplugin.HostPluginProtocol._put_page_blob_status", side_effect=HttpError("503")) def test_fallback_channel_503(self, patch_put, patch_upload, _): """ When host plugin returns a 503, we should fall back to the direct channel """ with self.create_mock_protocol() as wire_protocol: wire.HostPluginProtocol.is_default_channel = False wire_protocol.client.update_goal_state() # act wire_protocol.client.upload_status_blob() # assert direct route is called self.assertEqual(1, patch_upload.call_count, "Direct channel was not used") # assert host plugin route is called self.assertEqual(1, patch_put.call_count, "Host plugin was not used") # assert update goal state is only called once self.assertEqual(1, wire_protocol.client.update_goal_state.call_count, "Update goal state unexpected call count") # ensure the correct url is used self.assertEqual(sas_url, patch_put.call_args[0][0]) # ensure host plugin is not set as default self.assertFalse(wire.HostPluginProtocol.is_default_channel) @patch("azurelinuxagent.common.protocol.hostplugin.HostPluginProtocol.ensure_initialized", return_value=True) @patch("azurelinuxagent.common.protocol.wire.StatusBlob.upload", return_value=True) @patch("azurelinuxagent.common.protocol.hostplugin.HostPluginProtocol._put_page_blob_status", side_effect=ResourceGoneError("410")) @patch("azurelinuxagent.common.protocol.wire.WireClient.update_host_plugin_from_goal_state") def test_fallback_channel_410(self, patch_refresh_host_plugin, patch_put, patch_upload, _): """ When host plugin returns a 410, we should force the goal state update and return """ with self.create_mock_protocol() as wire_protocol: wire.HostPluginProtocol.is_default_channel = False wire_protocol.client.update_goal_state() # act wire_protocol.client.upload_status_blob() # assert direct route is not called self.assertEqual(0, patch_upload.call_count, "Direct channel was used") # assert host plugin route is called self.assertEqual(1, patch_put.call_count, "Host plugin was not used") # assert update goal state is called, then update_host_plugin_from_goal_state is called self.assertEqual(1, wire_protocol.client.update_goal_state.call_count, "Update goal state unexpected call count") self.assertEqual(1, patch_refresh_host_plugin.call_count, "Refresh host plugin unexpected call count") # ensure the correct url is used self.assertEqual(sas_url, patch_put.call_args[0][0]) # ensure host plugin is not set as default self.assertFalse(wire.HostPluginProtocol.is_default_channel) @patch("azurelinuxagent.common.protocol.hostplugin.HostPluginProtocol.ensure_initialized", return_value=True) @patch("azurelinuxagent.common.protocol.wire.StatusBlob.upload", return_value=False) @patch("azurelinuxagent.common.protocol.hostplugin.HostPluginProtocol._put_page_blob_status", side_effect=HttpError("500")) def test_fallback_channel_failure(self, patch_put, patch_upload, _): """ When host plugin returns a 500, and direct fails, we should raise a ProtocolError """ with self.create_mock_protocol() as wire_protocol: wire.HostPluginProtocol.is_default_channel = False wire_protocol.client.update_goal_state() # act self.assertRaises(wire.ProtocolError, wire_protocol.client.upload_status_blob) # assert direct route is not called self.assertEqual(1, patch_upload.call_count, "Direct channel was not used") # assert host plugin route is called self.assertEqual(1, patch_put.call_count, "Host plugin was not used") # assert update goal state is called twice self.assertEqual(1, wire_protocol.client.update_goal_state.call_count, "Update goal state unexpected call count") # ensure the correct url is used self.assertEqual(sas_url, patch_put.call_args[0][0]) # ensure host plugin is not set as default self.assertFalse(wire.HostPluginProtocol.is_default_channel) @patch("azurelinuxagent.common.event.add_event") def test_put_status_error_reporting(self, patch_add_event): """ Validate the telemetry when uploading status fails """ wire.HostPluginProtocol.is_default_channel = False with patch.object(wire.StatusBlob, "upload", return_value=False): with self.create_mock_protocol() as wire_protocol: wire_protocol_client = wire_protocol.client put_error = wire.HttpError("put status http error") with patch.object(restutil, "http_put", side_effect=put_error): with patch.object(wire.HostPluginProtocol, "ensure_initialized", return_value=True): self.assertRaises(wire.ProtocolError, wire_protocol_client.upload_status_blob) # The agent tries to upload via HostPlugin and that fails due to # http_put having a side effect of "put_error" # # The agent tries to upload using a direct connection, and that succeeds. self.assertEqual(1, wire_protocol_client.status_blob.upload.call_count) # pylint: disable=no-member # The agent never touches the default protocol is this code path, so no change. self.assertFalse(wire.HostPluginProtocol.is_default_channel) # The agent never logs telemetry event for direct fallback self.assertEqual(1, patch_add_event.call_count) self.assertEqual('ReportStatus', patch_add_event.call_args[1]['op']) self.assertTrue('Falling back to direct' in patch_add_event.call_args[1]['message']) self.assertEqual(True, patch_add_event.call_args[1]['is_success']) def test_validate_http_request_when_uploading_status(self): """Validate correct set of data is sent to HostGAPlugin when reporting VM status""" with mock_wire_protocol(DATA_FILE) as protocol: test_goal_state = protocol.client._goal_state plugin = protocol.client.get_host_plugin() status_blob = protocol.client.status_blob status_blob.data = faux_status status_blob.vm_status = restapi.VMStatus(message="Ready", status="Ready") exp_method = 'PUT' exp_url = hostplugin_status_url exp_data = self._hostplugin_data( status_blob.get_block_blob_headers(len(faux_status)), bytearray(faux_status, encoding='utf-8')) with patch.object(restutil, "http_request") as patch_http: patch_http.return_value = Mock(status=httpclient.OK) with patch.object(plugin, 'get_api_versions') as patch_api: patch_api.return_value = API_VERSION plugin.put_vm_status(status_blob, sas_url, block_blob_type) self.assertTrue(patch_http.call_count == 2) # first call is to host plugin self._validate_hostplugin_args( patch_http.call_args_list[0], test_goal_state, exp_method, exp_url, exp_data) # second call is to health service self.assertEqual('POST', patch_http.call_args_list[1][0][0]) self.assertEqual(health_service_url, patch_http.call_args_list[1][0][1]) def test_validate_block_blob(self): with mock_wire_protocol(DATA_FILE) as protocol: host_client = protocol.client.get_host_plugin() self.assertFalse(host_client.is_initialized) self.assertTrue(host_client.api_versions is None) self.assertTrue(host_client.health_service is not None) status_blob = protocol.client.status_blob status_blob.data = faux_status status_blob.type = block_blob_type status_blob.vm_status = restapi.VMStatus(message="Ready", status="Ready") exp_method = 'PUT' exp_url = hostplugin_status_url exp_data = self._hostplugin_data( status_blob.get_block_blob_headers(len(faux_status)), bytearray(faux_status, encoding='utf-8')) with patch.object(restutil, "http_request") as patch_http: patch_http.return_value = Mock(status=httpclient.OK) with patch.object(wire.HostPluginProtocol, "get_api_versions") as patch_get: patch_get.return_value = api_versions host_client.put_vm_status(status_blob, sas_url) self.assertTrue(patch_http.call_count == 2) # first call is to host plugin self._validate_hostplugin_args( patch_http.call_args_list[0], protocol.get_goal_state(), exp_method, exp_url, exp_data) # second call is to health service self.assertEqual('POST', patch_http.call_args_list[1][0][0]) self.assertEqual(health_service_url, patch_http.call_args_list[1][0][1]) def test_validate_page_blobs(self): """Validate correct set of data is sent for page blobs""" with mock_wire_protocol(DATA_FILE) as protocol: test_goal_state = protocol.get_goal_state() host_client = protocol.client.get_host_plugin() self.assertFalse(host_client.is_initialized) self.assertTrue(host_client.api_versions is None) status_blob = protocol.client.status_blob status_blob.data = faux_status status_blob.type = page_blob_type status_blob.vm_status = restapi.VMStatus(message="Ready", status="Ready") exp_method = 'PUT' exp_url = hostplugin_status_url page_status = bytearray(status_blob.data, encoding='utf-8') page_size = int((len(page_status) + 511) / 512) * 512 page_status = bytearray(status_blob.data.ljust(page_size), encoding='utf-8') page = bytearray(page_size) page[0: page_size] = page_status[0: len(page_status)] mock_response = MockResponse('', httpclient.OK) with patch.object(restutil, "http_request", return_value=mock_response) as patch_http: with patch.object(wire.HostPluginProtocol, "get_api_versions") as patch_get: patch_get.return_value = api_versions host_client.put_vm_status(status_blob, sas_url) self.assertTrue(patch_http.call_count == 3) # first call is to host plugin exp_data = self._hostplugin_data( status_blob.get_page_blob_create_headers( page_size)) self._validate_hostplugin_args( patch_http.call_args_list[0], test_goal_state, exp_method, exp_url, exp_data) # second call is to health service self.assertEqual('POST', patch_http.call_args_list[1][0][0]) self.assertEqual(health_service_url, patch_http.call_args_list[1][0][1]) # last call is to host plugin exp_data = self._hostplugin_data( status_blob.get_page_blob_page_headers( 0, page_size), page) exp_data['requestUri'] += "?comp=page" self._validate_hostplugin_args( patch_http.call_args_list[2], test_goal_state, exp_method, exp_url, exp_data) def test_validate_http_request_for_put_vm_log(self): def http_put_handler(url, *args, **kwargs): # pylint: disable=inconsistent-return-statements if self.is_host_plugin_put_logs_request(url): http_put_handler.args, http_put_handler.kwargs = args, kwargs return MockResponse(body=b'', status_code=200) http_put_handler.args, http_put_handler.kwargs = [], {} with mock_wire_protocol(DATA_FILE, http_put_handler=http_put_handler) as protocol: test_goal_state = protocol.get_goal_state() expected_url = hostplugin.URI_FORMAT_PUT_LOG.format(wireserver_url, hostplugin.HOST_PLUGIN_PORT) expected_headers = {'x-ms-version': '2015-09-01', "x-ms-containerid": test_goal_state.container_id, "x-ms-vmagentlog-deploymentid": test_goal_state.role_config_name.split(".")[0], "x-ms-client-name": AGENT_NAME, "x-ms-client-version": AGENT_VERSION} host_client = protocol.client.get_host_plugin() self.assertFalse(host_client.is_initialized, "Host plugin should not be initialized!") content = b"test" host_client.put_vm_log(content) self.assertTrue(host_client.is_initialized, "Host plugin is not initialized!") urls = protocol.get_tracked_urls() self.assertEqual(expected_url, urls[0], "Unexpected request URL!") self.assertEqual(content, http_put_handler.args[0], "Unexpected content for HTTP PUT request!") headers = http_put_handler.kwargs['headers'] for k in expected_headers: self.assertTrue(k in headers, "Header {0} not found in headers!".format(k)) self.assertEqual(expected_headers[k], headers[k], "Request headers don't match!") # Special check for correlation id header value, check for pattern, not exact value self.assertTrue("x-ms-client-correlationid" in headers.keys(), "Correlation id not found in headers!") self.assertTrue(UUID_PATTERN.match(headers["x-ms-client-correlationid"]), "Correlation id is not in GUID form!") def test_put_vm_log_should_raise_an_exception_when_request_fails(self): def http_put_handler(url, *args, **kwargs): # pylint: disable=inconsistent-return-statements if self.is_host_plugin_put_logs_request(url): http_put_handler.args, http_put_handler.kwargs = args, kwargs return MockResponse(body=ustr('Gone'), status_code=410) http_put_handler.args, http_put_handler.kwargs = [], {} with mock_wire_protocol(DATA_FILE, http_put_handler=http_put_handler) as protocol: host_client = wire.HostPluginProtocol(wireserver_url) GoalState.update_host_plugin_headers(protocol.client) self.assertFalse(host_client.is_initialized, "Host plugin should not be initialized!") with self.assertRaises(HttpError) as context_manager: content = b"test" host_client.put_vm_log(content) self.assertIsInstance(context_manager.exception, HttpError) self.assertIn("410", ustr(context_manager.exception)) self.assertIn("Gone", ustr(context_manager.exception)) def test_validate_get_extension_artifacts(self): with mock_wire_protocol(DATA_FILE) as protocol: test_goal_state = protocol.get_goal_state() expected_url = hostplugin.URI_FORMAT_GET_EXTENSION_ARTIFACT.format(wireserver_url, hostplugin.HOST_PLUGIN_PORT) expected_headers = {'x-ms-version': '2015-09-01', "x-ms-containerid": test_goal_state.container_id, "x-ms-host-config-name": test_goal_state.role_config_name, "x-ms-artifact-location": sas_url} host_client = protocol.client.get_host_plugin() self.assertFalse(host_client.is_initialized) self.assertTrue(host_client.api_versions is None) self.assertTrue(host_client.health_service is not None) with patch.object(wire.HostPluginProtocol, "get_api_versions", return_value=api_versions) as patch_get: # pylint: disable=unused-variable actual_url, actual_headers = host_client.get_artifact_request(sas_url, use_verify_header=False) self.assertTrue(host_client.is_initialized) self.assertFalse(host_client.api_versions is None) self.assertEqual(expected_url, actual_url) for k in expected_headers: self.assertTrue(k in actual_headers) self.assertEqual(expected_headers[k], actual_headers[k]) def test_health(self): host_plugin = self._init_host() with patch("azurelinuxagent.common.utils.restutil.http_get") as patch_http_get: patch_http_get.return_value = MockResponse('', 200) result = host_plugin.get_health() self.assertEqual(1, patch_http_get.call_count) self.assertTrue(result) patch_http_get.return_value = MockResponse('', 500) result = host_plugin.get_health() self.assertFalse(result) patch_http_get.side_effect = IOError('client IO error') try: host_plugin.get_health() self.fail('IO error expected to be raised') except IOError: # expected pass def test_ensure_health_service_called(self): host_plugin = self._init_host() with patch("azurelinuxagent.common.utils.restutil.http_get", return_value=MockHttpResponse(200)) as patch_http_get: with patch("azurelinuxagent.common.protocol.healthservice.HealthService.report_host_plugin_versions") as patch_report_versions: host_plugin.get_api_versions() self.assertEqual(1, patch_http_get.call_count) self.assertEqual(1, patch_report_versions.call_count) def test_put_status_healthy_signal(self): host_plugin = self._init_host() with patch("azurelinuxagent.common.utils.restutil.http_get") as patch_http_get: with patch("azurelinuxagent.common.utils.restutil.http_post") as patch_http_post: with patch("azurelinuxagent.common.utils.restutil.http_put") as patch_http_put: status_blob = self._init_status_blob() # get_api_versions patch_http_get.return_value = MockResponse(api_versions, 200) # put status blob patch_http_put.return_value = MockResponse(None, 201) host_plugin.put_vm_status(status_blob=status_blob, sas_url=sas_url) get_versions = [args for args in patch_http_get.call_args_list if args[0][0] == hostplugin_versions_url] self.assertEqual(1, len(get_versions), "Expected exactly 1 GET on {0}".format(hostplugin_versions_url)) self.assertEqual(2, patch_http_put.call_count) self.assertEqual(hostplugin_status_url, patch_http_put.call_args_list[0][0][0]) self.assertEqual(hostplugin_status_url, patch_http_put.call_args_list[1][0][0]) self.assertEqual(2, patch_http_post.call_count) # signal for /versions self.assertEqual(health_service_url, patch_http_post.call_args_list[0][0][0]) jstr = patch_http_post.call_args_list[0][0][1] obj = json.loads(jstr) self.assertEqual(1, len(obj['Observations'])) self.assertTrue(obj['Observations'][0]['IsHealthy']) self.assertEqual('GuestAgentPluginVersions', obj['Observations'][0]['ObservationName']) # signal for /status self.assertEqual(health_service_url, patch_http_post.call_args_list[1][0][0]) jstr = patch_http_post.call_args_list[1][0][1] obj = json.loads(jstr) self.assertEqual(1, len(obj['Observations'])) self.assertTrue(obj['Observations'][0]['IsHealthy']) self.assertEqual('GuestAgentPluginStatus', obj['Observations'][0]['ObservationName']) def test_put_status_unhealthy_signal_transient(self): host_plugin = self._init_host() with patch("azurelinuxagent.common.utils.restutil.http_get") as patch_http_get: with patch("azurelinuxagent.common.utils.restutil.http_post") as patch_http_post: with patch("azurelinuxagent.common.utils.restutil.http_put") as patch_http_put: status_blob = self._init_status_blob() # get_api_versions patch_http_get.return_value = MockResponse(api_versions, 200) # put status blob patch_http_put.return_value = MockResponse(None, 500) with self.assertRaises(HttpError): host_plugin.put_vm_status(status_blob=status_blob, sas_url=sas_url) get_versions = [args for args in patch_http_get.call_args_list if args[0][0] == hostplugin_versions_url] self.assertEqual(1, len(get_versions), "Expected exactly 1 GET on {0}".format(hostplugin_versions_url)) self.assertEqual(1, patch_http_put.call_count) self.assertEqual(hostplugin_status_url, patch_http_put.call_args[0][0]) self.assertEqual(2, patch_http_post.call_count) # signal for /versions self.assertEqual(health_service_url, patch_http_post.call_args_list[0][0][0]) jstr = patch_http_post.call_args_list[0][0][1] obj = json.loads(jstr) self.assertEqual(1, len(obj['Observations'])) self.assertTrue(obj['Observations'][0]['IsHealthy']) self.assertEqual('GuestAgentPluginVersions', obj['Observations'][0]['ObservationName']) # signal for /status self.assertEqual(health_service_url, patch_http_post.call_args_list[1][0][0]) jstr = patch_http_post.call_args_list[1][0][1] obj = json.loads(jstr) self.assertEqual(1, len(obj['Observations'])) self.assertTrue(obj['Observations'][0]['IsHealthy']) self.assertEqual('GuestAgentPluginStatus', obj['Observations'][0]['ObservationName']) def test_put_status_unhealthy_signal_permanent(self): host_plugin = self._init_host() with patch("azurelinuxagent.common.utils.restutil.http_get") as patch_http_get: with patch("azurelinuxagent.common.utils.restutil.http_post") as patch_http_post: with patch("azurelinuxagent.common.utils.restutil.http_put") as patch_http_put: status_blob = self._init_status_blob() # get_api_versions patch_http_get.return_value = MockResponse(api_versions, 200) # put status blob patch_http_put.return_value = MockResponse(None, 500) host_plugin.status_error_state.is_triggered = Mock(return_value=True) with self.assertRaises(HttpError): host_plugin.put_vm_status(status_blob=status_blob, sas_url=sas_url) get_versions = [args for args in patch_http_get.call_args_list if args[0][0] == hostplugin_versions_url] self.assertEqual(1, len(get_versions), "Expected exactly 1 GET on {0}".format(hostplugin_versions_url)) self.assertEqual(1, patch_http_put.call_count) self.assertEqual(hostplugin_status_url, patch_http_put.call_args[0][0]) self.assertEqual(2, patch_http_post.call_count) # signal for /versions self.assertEqual(health_service_url, patch_http_post.call_args_list[0][0][0]) jstr = patch_http_post.call_args_list[0][0][1] obj = json.loads(jstr) self.assertEqual(1, len(obj['Observations'])) self.assertTrue(obj['Observations'][0]['IsHealthy']) self.assertEqual('GuestAgentPluginVersions', obj['Observations'][0]['ObservationName']) # signal for /status self.assertEqual(health_service_url, patch_http_post.call_args_list[1][0][0]) jstr = patch_http_post.call_args_list[1][0][1] obj = json.loads(jstr) self.assertEqual(1, len(obj['Observations'])) self.assertFalse(obj['Observations'][0]['IsHealthy']) self.assertEqual('GuestAgentPluginStatus', obj['Observations'][0]['ObservationName']) @patch("azurelinuxagent.common.protocol.hostplugin.HostPluginProtocol.should_report", return_value=True) @patch("azurelinuxagent.common.protocol.healthservice.HealthService.report_host_plugin_extension_artifact") def test_report_fetch_health(self, patch_report_artifact, patch_should_report): host_plugin = self._init_host() host_plugin.report_fetch_health(uri='', is_healthy=True) self.assertEqual(0, patch_should_report.call_count) host_plugin.report_fetch_health(uri='http://169.254.169.254/extensionArtifact', is_healthy=True) self.assertEqual(0, patch_should_report.call_count) host_plugin.report_fetch_health(uri='http://168.63.129.16:32526/status', is_healthy=True) self.assertEqual(0, patch_should_report.call_count) self.assertEqual(None, host_plugin.fetch_last_timestamp) host_plugin.report_fetch_health(uri='http://168.63.129.16:32526/extensionArtifact', is_healthy=True) self.assertNotEqual(None, host_plugin.fetch_last_timestamp) self.assertEqual(1, patch_should_report.call_count) self.assertEqual(1, patch_report_artifact.call_count) @patch("azurelinuxagent.common.protocol.hostplugin.HostPluginProtocol.should_report", return_value=True) @patch("azurelinuxagent.common.protocol.healthservice.HealthService.report_host_plugin_status") def test_report_status_health(self, patch_report_status, patch_should_report): host_plugin = self._init_host() self.assertEqual(None, host_plugin.status_last_timestamp) host_plugin.report_status_health(is_healthy=True) self.assertNotEqual(None, host_plugin.status_last_timestamp) self.assertEqual(1, patch_should_report.call_count) self.assertEqual(1, patch_report_status.call_count) def test_should_report(self): host_plugin = self._init_host() error_state = ErrorState(min_timedelta=datetime.timedelta(minutes=5)) period = datetime.timedelta(minutes=1) last_timestamp = None # first measurement at 0s, should report is_healthy = True actual = host_plugin.should_report(is_healthy, error_state, last_timestamp, period) self.assertEqual(True, actual) # second measurement at 30s, should not report last_timestamp = datetime.datetime.utcnow() - datetime.timedelta(seconds=30) actual = host_plugin.should_report(is_healthy, error_state, last_timestamp, period) self.assertEqual(False, actual) # third measurement at 60s, should report last_timestamp = datetime.datetime.utcnow() - datetime.timedelta(seconds=60) actual = host_plugin.should_report(is_healthy, error_state, last_timestamp, period) self.assertEqual(True, actual) # fourth measurement unhealthy, should report and increment counter is_healthy = False self.assertEqual(0, error_state.count) actual = host_plugin.should_report(is_healthy, error_state, last_timestamp, period) self.assertEqual(1, error_state.count) self.assertEqual(True, actual) # fifth measurement, should not report and reset counter is_healthy = True last_timestamp = datetime.datetime.utcnow() - datetime.timedelta(seconds=30) self.assertEqual(1, error_state.count) actual = host_plugin.should_report(is_healthy, error_state, last_timestamp, period) self.assertEqual(0, error_state.count) self.assertEqual(False, actual) class TestHostPluginVmSettings(HttpRequestPredicates, AgentTestCase): def test_it_should_raise_protocol_error_when_the_vm_settings_request_fails(self): def http_get_handler(url, *_, **__): if self.is_host_plugin_vm_settings_request(url): return MockHttpResponse(httpclient.INTERNAL_SERVER_ERROR, body="TEST ERROR") return None with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS) as protocol: protocol.set_http_handlers(http_get_handler=http_get_handler) with self.assertRaisesRegexCM(ProtocolError, r'GET vmSettings \[correlation ID: .* eTag: .*\]: \[HTTP Failed\] \[500: None].*TEST ERROR.*'): protocol.client.get_host_plugin().fetch_vm_settings() @staticmethod def _fetch_vm_settings_ignoring_errors(protocol): try: protocol.client.get_host_plugin().fetch_vm_settings() except (ProtocolError, VmSettingsNotSupported): pass def test_it_should_keep_track_of_errors_in_vm_settings_requests(self): mock_response = None def http_get_handler(url, *_, **__): if self.is_host_plugin_vm_settings_request(url): if isinstance(mock_response, Exception): # E0702: Raising NoneType while only classes or instances are allowed (raising-bad-type) - Disabled: we never raise None raise mock_response # pylint: disable=raising-bad-type return mock_response return None with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS, http_get_handler=http_get_handler) as protocol: mock_response = MockHttpResponse(httpclient.INTERNAL_SERVER_ERROR) self._fetch_vm_settings_ignoring_errors(protocol) mock_response = MockHttpResponse(httpclient.BAD_REQUEST) self._fetch_vm_settings_ignoring_errors(protocol) self._fetch_vm_settings_ignoring_errors(protocol) mock_response = IOError("timed out") self._fetch_vm_settings_ignoring_errors(protocol) mock_response = httpclient.HTTPException() self._fetch_vm_settings_ignoring_errors(protocol) self._fetch_vm_settings_ignoring_errors(protocol) # force the summary by resetting its period and calling update_goal_state with patch("azurelinuxagent.common.protocol.hostplugin.add_event") as add_event: mock_response = None # stop producing errors protocol.client._host_plugin._vm_settings_error_reporter._next_period = datetime.datetime.now() self._fetch_vm_settings_ignoring_errors(protocol) summary_text = [kwargs["message"] for _, kwargs in add_event.call_args_list if kwargs["op"] == "VmSettingsSummary"] self.assertEqual(1, len(summary_text), "Exactly 1 summary should have been produced. Got: {0} ".format(summary_text)) summary = json.loads(summary_text[0]) expected = { "requests": 6 + 2, # two extra calls to update_goal_state (when creating the mock protocol and when forcing the summary) "errors": 6, "serverErrors": 1, "clientErrors": 2, "timeouts": 1, "failedRequests": 2 } self.assertEqual(expected, summary, "The count of errors is incorrect") def test_it_should_limit_the_number_of_errors_it_reports(self): with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS) as protocol: def http_get_handler(url, *_, **__): if self.is_host_plugin_vm_settings_request(url): return MockHttpResponse(httpclient.BAD_GATEWAY) # HostGAPlugin returns 502 for internal errors return None protocol.set_http_handlers(http_get_handler=http_get_handler) def get_telemetry_messages(): return [kwargs["message"] for _, kwargs in add_event.call_args_list if kwargs["op"] == "VmSettings"] with patch("azurelinuxagent.common.protocol.hostplugin.add_event") as add_event: for _ in range(_VmSettingsErrorReporter._MaxErrors + 3): self._fetch_vm_settings_ignoring_errors(protocol) telemetry_messages = get_telemetry_messages() self.assertEqual(_VmSettingsErrorReporter._MaxErrors, len(telemetry_messages), "The number of errors reported to telemetry is not the max allowed (got: {0})".format(telemetry_messages)) # Reset the error reporter and verify that additional errors are reported protocol.client.get_host_plugin()._vm_settings_error_reporter._next_period = datetime.datetime.now() self._fetch_vm_settings_ignoring_errors(protocol) # this triggers the reset with patch("azurelinuxagent.common.protocol.hostplugin.add_event") as add_event: self._fetch_vm_settings_ignoring_errors(protocol) telemetry_messages = get_telemetry_messages() self.assertEqual(1, len(telemetry_messages), "Expected additional errors to be reported to telemetry in the next period (got: {0})".format(telemetry_messages)) def test_it_should_stop_issuing_vm_settings_requests_when_api_is_not_supported(self): with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS) as protocol: def http_get_handler(url, *_, **__): if self.is_host_plugin_vm_settings_request(url): return MockHttpResponse(httpclient.NOT_FOUND) # HostGAPlugin returns 404 if the API is not supported return None protocol.set_http_handlers(http_get_handler=http_get_handler) def get_vm_settings_call_count(): return len([url for url in protocol.get_tracked_urls() if "vmSettings" in url]) self._fetch_vm_settings_ignoring_errors(protocol) self.assertEqual(1, get_vm_settings_call_count(), "There should have been an initial call to vmSettings.") self._fetch_vm_settings_ignoring_errors(protocol) self._fetch_vm_settings_ignoring_errors(protocol) self.assertEqual(1, get_vm_settings_call_count(), "Additional calls to update_goal_state should not have produced extra calls to vmSettings.") # reset the vmSettings check period; this should restart the calls to the API protocol.client._host_plugin._supports_vm_settings_next_check = datetime.datetime.now() protocol.client.update_goal_state() self.assertEqual(2, get_vm_settings_call_count(), "A second call to vmSettings was expecting after the check period has elapsed.") def test_it_should_raise_when_the_vm_settings_api_stops_being_supported(self): def http_get_handler(url, *_, **__): if self.is_host_plugin_vm_settings_request(url): return MockHttpResponse(httpclient.NOT_FOUND) # HostGAPlugin returns 404 if the API is not supported return None with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS) as protocol: host_ga_plugin = protocol.client.get_host_plugin() # Do an initial call to ensure the API is supported vm_settings, _ = host_ga_plugin.fetch_vm_settings() # Now return NOT_FOUND to indicate the API is not supported protocol.set_http_handlers(http_get_handler=http_get_handler) with self.assertRaises(VmSettingsSupportStopped) as cm: host_ga_plugin.fetch_vm_settings() self.assertEqual(vm_settings.created_on_timestamp, cm.exception.timestamp) def test_it_should_save_the_timestamp_of_the_most_recent_fast_track_goal_state(self): with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS) as protocol: host_ga_plugin = protocol.client.get_host_plugin() vm_settings, _ = host_ga_plugin.fetch_vm_settings() state_file = os.path.join(conf.get_lib_dir(), "fast_track.json") self.assertTrue(os.path.exists(state_file), "The timestamp was not saved (can't find {0})".format(state_file)) with open(state_file, "r") as state_file_: state = json.load(state_file_) self.assertEqual(vm_settings.created_on_timestamp, state["timestamp"], "{0} does not contain the expected timestamp".format(state_file)) # A fabric goal state should remove the state file protocol.mock_wire_data.set_vm_settings_source(GoalStateSource.Fabric) protocol.mock_wire_data.set_etag(888) _ = host_ga_plugin.fetch_vm_settings() self.assertFalse(os.path.exists(state_file), "{0} was not removed by a Fabric goal state".format(state_file)) class MockResponse: def __init__(self, body, status_code, reason=''): self.body = body self.status = status_code self.reason = reason def read(self): return self.body if sys.version_info[0] == 2 else bytes(self.body, encoding='utf-8') if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/protocol/test_image_info_matcher.py000066400000000000000000000114421446033677600244260ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ import unittest from azurelinuxagent.common.protocol.imds import ImageInfoMatcher class TestImageInfoMatcher(unittest.TestCase): def test_image_does_not_exist(self): doc = '{}' test_subject = ImageInfoMatcher(doc) self.assertFalse(test_subject.is_match("Red Hat", "RHEL", "6.3", "")) def test_image_exists_by_sku(self): doc = '''{ "CANONICAL": { "UBUNTUSERVER": { "16.04-LTS": { "Match": ".*" } } } }''' test_subject = ImageInfoMatcher(doc) self.assertTrue(test_subject.is_match("Canonical", "UbuntuServer", "16.04-LTS", "")) self.assertTrue(test_subject.is_match("Canonical", "UbuntuServer", "16.04-LTS", "16.04.201805090")) self.assertFalse(test_subject.is_match("Canonical", "UbuntuServer", "14.04.0-LTS", "16.04.201805090")) def test_image_exists_by_version(self): doc = '''{ "REDHAT": { "RHEL": { "Minimum": "6.3" } } }''' test_subject = ImageInfoMatcher(doc) self.assertFalse(test_subject.is_match("RedHat", "RHEL", "6.1", "")) self.assertFalse(test_subject.is_match("RedHat", "RHEL", "6.2", "")) self.assertTrue(test_subject.is_match("RedHat", "RHEL", "6.3", "")) self.assertTrue(test_subject.is_match("RedHat", "RHEL", "6.4", "")) self.assertTrue(test_subject.is_match("RedHat", "RHEL", "6.5", "")) self.assertTrue(test_subject.is_match("RedHat", "RHEL", "7.0", "")) self.assertTrue(test_subject.is_match("RedHat", "RHEL", "7.1", "")) def test_image_exists_by_version01(self): """ Test case to ensure the matcher exhaustively searches all cases. REDHAT/RHEL have a SKU >= 6.3 is less precise than REDHAT/RHEL/7-LVM have a any version. Both should return a successful match. """ doc = '''{ "REDHAT": { "RHEL": { "Minimum": "6.3", "7-LVM": { "Match": ".*" } } } }''' test_subject = ImageInfoMatcher(doc) self.assertTrue(test_subject.is_match("RedHat", "RHEL", "6.3", "")) self.assertTrue(test_subject.is_match("RedHat", "RHEL", "7-LVM", "")) def test_ignores_case(self): doc = '''{ "CANONICAL": { "UBUNTUSERVER": { "16.04-LTS": { "Match": ".*" } } } }''' test_subject = ImageInfoMatcher(doc) self.assertTrue(test_subject.is_match("canonical", "ubuntuserver", "16.04-lts", "")) self.assertFalse(test_subject.is_match("canonical", "ubuntuserver", "14.04.0-lts", "16.04.201805090")) def test_list_operator(self): doc = '''{ "CANONICAL": { "UBUNTUSERVER": { "List": [ "14.04.0-LTS", "14.04.1-LTS" ] } } }''' test_subject = ImageInfoMatcher(doc) self.assertTrue(test_subject.is_match("Canonical", "UbuntuServer", "14.04.0-LTS", "")) self.assertTrue(test_subject.is_match("Canonical", "UbuntuServer", "14.04.1-LTS", "")) self.assertFalse(test_subject.is_match("Canonical", "UbuntuServer", "22.04-LTS", "")) def test_invalid_version(self): doc = '''{ "REDHAT": { "RHEL": { "Minimum": "6.3" } } }''' test_subject = ImageInfoMatcher(doc) self.assertFalse(test_subject.is_match("RedHat", "RHEL", "16.04-LTS", "")) # This is *expected* behavior as opposed to desirable. The specification is # controlled by the agent, so there is no reason to use these values, but if # one does this is expected behavior. # # FlexibleVersion chops off all leading zeros. self.assertTrue(test_subject.is_match("RedHat", "RHEL", "6.04", "")) # FlexibleVersion coerces everything to a string self.assertTrue(test_subject.is_match("RedHat", "RHEL", 6.04, "")) if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/protocol/test_imds.py000066400000000000000000000736401446033677600215720ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ import json import os import unittest from azurelinuxagent.common.protocol import imds from azurelinuxagent.common.datacontract import set_properties from azurelinuxagent.common.exception import HttpError, ResourceGoneError from azurelinuxagent.common.future import ustr, httpclient from azurelinuxagent.common.utils import restutil from tests.protocol.mocks import MockHttpResponse from tests.tools import AgentTestCase, data_dir, MagicMock, Mock, patch def get_mock_compute_response(): return MockHttpResponse(status=httpclient.OK, body='''{ "location": "westcentralus", "name": "unit_test", "offer": "UnitOffer", "osType": "Linux", "placementGroupId": "", "platformFaultDomain": "0", "platformUpdateDomain": "0", "publisher": "UnitPublisher", "resourceGroupName": "UnitResourceGroupName", "sku": "UnitSku", "subscriptionId": "e4402c6c-2804-4a0a-9dee-d61918fc4d28", "tags": "Key1:Value1;Key2:Value2", "vmId": "f62f23fb-69e2-4df0-a20b-cb5c201a3e7a", "version": "UnitVersion", "vmSize": "Standard_D1_v2" }'''.encode('utf-8')) class TestImds(AgentTestCase): @patch("azurelinuxagent.common.protocol.imds.restutil.http_get") def test_get(self, mock_http_get): mock_http_get.return_value = get_mock_compute_response() test_subject = imds.ImdsClient(restutil.KNOWN_WIRESERVER_IP) test_subject.get_compute() self.assertEqual(1, mock_http_get.call_count) positional_args, kw_args = mock_http_get.call_args self.assertEqual('http://169.254.169.254/metadata/instance/compute?api-version=2018-02-01', positional_args[0]) self.assertTrue('User-Agent' in kw_args['headers']) self.assertTrue('Metadata' in kw_args['headers']) self.assertEqual(True, kw_args['headers']['Metadata']) @patch("azurelinuxagent.common.protocol.imds.restutil.http_get") def test_get_bad_request(self, mock_http_get): mock_http_get.return_value = MockHttpResponse(status=restutil.httpclient.BAD_REQUEST) test_subject = imds.ImdsClient(restutil.KNOWN_WIRESERVER_IP) self.assertRaises(HttpError, test_subject.get_compute) @patch("azurelinuxagent.common.protocol.imds.restutil.http_get") def test_get_internal_service_error(self, mock_http_get): mock_http_get.return_value = MockHttpResponse(status=restutil.httpclient.INTERNAL_SERVER_ERROR) test_subject = imds.ImdsClient(restutil.KNOWN_WIRESERVER_IP) self.assertRaises(HttpError, test_subject.get_compute) @patch("azurelinuxagent.common.protocol.imds.restutil.http_get") def test_get_empty_response(self, mock_http_get): mock_http_get.return_value = MockHttpResponse(status=httpclient.OK, body=''.encode('utf-8')) test_subject = imds.ImdsClient(restutil.KNOWN_WIRESERVER_IP) self.assertRaises(ValueError, test_subject.get_compute) def test_deserialize_ComputeInfo(self): s = '''{ "location": "westcentralus", "name": "unit_test", "offer": "UnitOffer", "osType": "Linux", "placementGroupId": "", "platformFaultDomain": "0", "platformUpdateDomain": "0", "publisher": "UnitPublisher", "resourceGroupName": "UnitResourceGroupName", "sku": "UnitSku", "subscriptionId": "e4402c6c-2804-4a0a-9dee-d61918fc4d28", "tags": "Key1:Value1;Key2:Value2", "vmId": "f62f23fb-69e2-4df0-a20b-cb5c201a3e7a", "version": "UnitVersion", "vmSize": "Standard_D1_v2", "vmScaleSetName": "MyScaleSet", "zone": "In" }''' data = json.loads(s) compute_info = imds.ComputeInfo() set_properties("compute", compute_info, data) self.assertEqual('westcentralus', compute_info.location) self.assertEqual('unit_test', compute_info.name) self.assertEqual('UnitOffer', compute_info.offer) self.assertEqual('Linux', compute_info.osType) self.assertEqual('', compute_info.placementGroupId) self.assertEqual('0', compute_info.platformFaultDomain) self.assertEqual('0', compute_info.platformUpdateDomain) self.assertEqual('UnitPublisher', compute_info.publisher) self.assertEqual('UnitResourceGroupName', compute_info.resourceGroupName) self.assertEqual('UnitSku', compute_info.sku) self.assertEqual('e4402c6c-2804-4a0a-9dee-d61918fc4d28', compute_info.subscriptionId) self.assertEqual('Key1:Value1;Key2:Value2', compute_info.tags) self.assertEqual('f62f23fb-69e2-4df0-a20b-cb5c201a3e7a', compute_info.vmId) self.assertEqual('UnitVersion', compute_info.version) self.assertEqual('Standard_D1_v2', compute_info.vmSize) self.assertEqual('MyScaleSet', compute_info.vmScaleSetName) self.assertEqual('In', compute_info.zone) self.assertEqual('UnitPublisher:UnitOffer:UnitSku:UnitVersion', compute_info.image_info) def test_is_custom_image(self): image_origin = self._setup_image_origin_assert("", "", "", "") self.assertEqual(imds.IMDS_IMAGE_ORIGIN_CUSTOM, image_origin) def test_is_endorsed_CentOS(self): self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("OpenLogic", "CentOS", "6.3", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("OpenLogic", "CentOS", "6.4", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("OpenLogic", "CentOS", "6.5", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("OpenLogic", "CentOS", "6.6", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("OpenLogic", "CentOS", "6.7", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("OpenLogic", "CentOS", "6.8", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("OpenLogic", "CentOS", "6.9", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("OpenLogic", "CentOS", "7.0", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("OpenLogic", "CentOS", "7.1", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("OpenLogic", "CentOS", "7.2", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("OpenLogic", "CentOS", "7.3", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("OpenLogic", "CentOS", "7.4", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("OpenLogic", "CentOS", "7-LVM", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("OpenLogic", "CentOS", "7-RAW", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("OpenLogic", "CentOS-HPC", "6.5", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("OpenLogic", "CentOS-HPC", "6.8", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("OpenLogic", "CentOS-HPC", "7.1", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("OpenLogic", "CentOS-HPC", "7.3", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("OpenLogic", "CentOS-HPC", "7.4", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_PLATFORM, self._setup_image_origin_assert("OpenLogic", "CentOS", "6.2", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_PLATFORM, self._setup_image_origin_assert("OpenLogic", "CentOS", "6.1", "")) def test_is_endorsed_CoreOS(self): self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("CoreOS", "CoreOS", "stable", "494.4.0")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("CoreOS", "CoreOS", "stable", "899.17.0")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("CoreOS", "CoreOS", "stable", "1688.5.3")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_PLATFORM, self._setup_image_origin_assert("CoreOS", "CoreOS", "stable", "494.3.0")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_PLATFORM, self._setup_image_origin_assert("CoreOS", "CoreOS", "alpha", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_PLATFORM, self._setup_image_origin_assert("CoreOS", "CoreOS", "beta", "")) def test_is_endorsed_Debian(self): self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("credativ", "Debian", "7", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("credativ", "Debian", "8", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("credativ", "Debian", "9", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_PLATFORM, self._setup_image_origin_assert("credativ", "Debian", "9-DAILY", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_PLATFORM, self._setup_image_origin_assert("credativ", "Debian", "10-DAILY", "")) def test_is_endorsed_Rhel(self): self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("RedHat", "RHEL", "6.7", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("RedHat", "RHEL", "6.8", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("RedHat", "RHEL", "6.9", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("RedHat", "RHEL", "7.0", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("RedHat", "RHEL", "7.1", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("RedHat", "RHEL", "7.2", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("RedHat", "RHEL", "7.3", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("RedHat", "RHEL", "7.4", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("RedHat", "RHEL", "7-LVM", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("RedHat", "RHEL", "7-RAW", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("RedHat", "RHEL-SAP-HANA", "7.2", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("RedHat", "RHEL-SAP-HANA", "7.3", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("RedHat", "RHEL-SAP-HANA", "7.4", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("RedHat", "RHEL-SAP", "7.2", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("RedHat", "RHEL-SAP", "7.3", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("RedHat", "RHEL-SAP", "7.4", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("RedHat", "RHEL-SAP-APPS", "7.2", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("RedHat", "RHEL-SAP-APPS", "7.3", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("RedHat", "RHEL-SAP-APPS", "7.4", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_PLATFORM, self._setup_image_origin_assert("RedHat", "RHEL", "6.6", "")) def test_is_endorsed_SuSE(self): self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("SuSE", "SLES", "11-SP4", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("SuSE", "SLES-BYOS", "11-SP4", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("SuSE", "SLES", "12-SP1", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("SuSE", "SLES", "12-SP2", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("SuSE", "SLES", "12-SP3", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("SuSE", "SLES", "12-SP4", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("SuSE", "SLES", "12-SP5", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("SuSE", "SLES-BYOS", "12-SP1", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("SuSE", "SLES-BYOS", "12-SP2", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("SuSE", "SLES-BYOS", "12-SP3", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("SuSE", "SLES-BYOS", "12-SP4", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("SuSE", "SLES-BYOS", "12-SP5", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("SuSE", "SLES-SAP", "12-SP1", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("SuSE", "SLES-SAP", "12-SP2", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("SuSE", "SLES-SAP", "12-SP3", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("SuSE", "SLES-SAP", "12-SP4", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("SuSE", "SLES-SAP", "12-SP5", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_PLATFORM, self._setup_image_origin_assert("SuSE", "SLES", "11-SP3", "")) def test_is_endorsed_UbuntuServer(self): self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("Canonical", "UbuntuServer", "14.04.0-LTS", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("Canonical", "UbuntuServer", "14.04.1-LTS", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("Canonical", "UbuntuServer", "14.04.2-LTS", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("Canonical", "UbuntuServer", "14.04.3-LTS", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("Canonical", "UbuntuServer", "14.04.4-LTS", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("Canonical", "UbuntuServer", "14.04.5-LTS", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("Canonical", "UbuntuServer", "14.04.6-LTS", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("Canonical", "UbuntuServer", "14.04.7-LTS", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("Canonical", "UbuntuServer", "14.04.8-LTS", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("Canonical", "UbuntuServer", "16.04-LTS", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("Canonical", "UbuntuServer", "18.04-LTS", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("Canonical", "UbuntuServer", "20.04-LTS", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_ENDORSED, self._setup_image_origin_assert("Canonical", "UbuntuServer", "22.04-LTS", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_PLATFORM, self._setup_image_origin_assert("Canonical", "UbuntuServer", "12.04-LTS", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_PLATFORM, self._setup_image_origin_assert("Canonical", "UbuntuServer", "17.10", "")) self.assertEqual(imds.IMDS_IMAGE_ORIGIN_PLATFORM, self._setup_image_origin_assert("Canonical", "UbuntuServer", "18.04-DAILY-LTS", "")) @staticmethod def _setup_image_origin_assert(publisher, offer, sku, version): s = '''{{ "publisher": "{0}", "offer": "{1}", "sku": "{2}", "version": "{3}" }}'''.format(publisher, offer, sku, version) data = json.loads(s) compute_info = imds.ComputeInfo() set_properties("compute", compute_info, data) return compute_info.image_origin def test_response_validation(self): # invalid json or empty response self._assert_validation(http_status_code=200, http_response='', expected_valid=False, expected_response='JSON parsing failed') self._assert_validation(http_status_code=200, http_response=None, expected_valid=False, expected_response='JSON parsing failed') self._assert_validation(http_status_code=200, http_response='{ bad json ', expected_valid=False, expected_response='JSON parsing failed') # 500 response self._assert_validation(http_status_code=500, http_response='error response', expected_valid=False, expected_response='IMDS error in /metadata/instance: [HTTP Failed] [500: reason] error response') # 429 response - throttling does not mean service is unhealthy self._assert_validation(http_status_code=429, http_response='server busy', expected_valid=True, expected_response='[HTTP Failed] [429: reason] server busy') # 404 response - error responses do not mean service is unhealthy self._assert_validation(http_status_code=404, http_response='not found', expected_valid=True, expected_response='[HTTP Failed] [404: reason] not found') # valid json self._assert_validation(http_status_code=200, http_response=self._imds_response('valid'), expected_valid=True, expected_response='') # unicode self._assert_validation(http_status_code=200, http_response=self._imds_response('unicode'), expected_valid=True, expected_response='') def test_field_validation(self): # TODO: compute fields (#1249) self._assert_field('network', 'interface', 'ipv4', 'ipAddress', 'privateIpAddress') self._assert_field('network', 'interface', 'ipv4', 'ipAddress') self._assert_field('network', 'interface', 'ipv4') self._assert_field('network', 'interface', 'macAddress') self._assert_field('network') def _assert_field(self, *fields): response = self._imds_response('valid') response_obj = json.loads(ustr(response, encoding="utf-8")) # assert empty value self._update_field(response_obj, fields, '') altered_response = json.dumps(response_obj).encode() self._assert_validation(http_status_code=200, http_response=altered_response, expected_valid=False, expected_response='Empty field: [{0}]'.format(fields[-1])) # assert missing value self._update_field(response_obj, fields, None) altered_response = json.dumps(response_obj).encode() self._assert_validation(http_status_code=200, http_response=altered_response, expected_valid=False, expected_response='Missing field: [{0}]'.format(fields[-1])) def _update_field(self, obj, fields, val): if isinstance(obj, list): self._update_field(obj[0], fields, val) else: f = fields[0] if len(fields) == 1: if val is None: del obj[f] else: obj[f] = val else: self._update_field(obj[f], fields[1:], val) @staticmethod def _imds_response(f): path = os.path.join(data_dir, "imds", "{0}.json".format(f)) with open(path, "rb") as fh: return fh.read() def _assert_validation(self, http_status_code, http_response, expected_valid, expected_response): test_subject = imds.ImdsClient(restutil.KNOWN_WIRESERVER_IP) with patch("azurelinuxagent.common.utils.restutil.http_get") as mock_http_get: mock_http_get.return_value = MockHttpResponse(status=http_status_code, reason='reason', body=http_response) validate_response = test_subject.validate() self.assertEqual(1, mock_http_get.call_count) positional_args, kw_args = mock_http_get.call_args self.assertTrue('User-Agent' in kw_args['headers']) self.assertEqual(restutil.HTTP_USER_AGENT_HEALTH, kw_args['headers']['User-Agent']) self.assertTrue('Metadata' in kw_args['headers']) self.assertEqual(True, kw_args['headers']['Metadata']) self.assertEqual('http://169.254.169.254/metadata/instance?api-version=2018-02-01', positional_args[0]) self.assertEqual(expected_valid, validate_response[0]) self.assertTrue(expected_response in validate_response[1], "Expected: '{0}', Actual: '{1}'" .format(expected_response, validate_response[1])) def test_endpoint_fallback(self): # http error status codes are tested in test_response_validation, none of which # should trigger a fallback. This is confirmed as _assert_validation will count # http GET calls and enforces a single GET call (fallback would cause 2) and # checks the url called. test_subject = imds.ImdsClient("foo.bar") # ensure user-agent gets set correctly for is_health, expected_useragent in [(False, restutil.HTTP_USER_AGENT), (True, restutil.HTTP_USER_AGENT_HEALTH)]: # set a different resource path for health query to make debugging unit test easier resource_path = 'something/health' if is_health else 'something' for has_primary_ioerror in (False, True): # secondary endpoint unreachable test_subject._http_get = Mock(side_effect=self._mock_http_get) self._mock_imds_setup(primary_ioerror=has_primary_ioerror, secondary_ioerror=True) result = test_subject.get_metadata(resource_path=resource_path, is_health=is_health) self.assertFalse(result.success) if has_primary_ioerror else self.assertTrue(result.success) # pylint: disable=expression-not-assigned self.assertFalse(result.service_error) if has_primary_ioerror: self.assertEqual('IMDS error in /metadata/{0}: Unable to connect to endpoint'.format(resource_path), result.response) else: self.assertEqual('Mock success response', result.response) for _, kwargs in test_subject._http_get.call_args_list: self.assertTrue('User-Agent' in kwargs['headers']) self.assertEqual(expected_useragent, kwargs['headers']['User-Agent']) self.assertEqual(2 if has_primary_ioerror else 1, test_subject._http_get.call_count) # IMDS success test_subject._http_get = Mock(side_effect=self._mock_http_get) self._mock_imds_setup(primary_ioerror=has_primary_ioerror) result = test_subject.get_metadata(resource_path=resource_path, is_health=is_health) self.assertTrue(result.success) self.assertFalse(result.service_error) self.assertEqual('Mock success response', result.response) for _, kwargs in test_subject._http_get.call_args_list: self.assertTrue('User-Agent' in kwargs['headers']) self.assertEqual(expected_useragent, kwargs['headers']['User-Agent']) self.assertEqual(2 if has_primary_ioerror else 1, test_subject._http_get.call_count) # IMDS throttled test_subject._http_get = Mock(side_effect=self._mock_http_get) self._mock_imds_setup(primary_ioerror=has_primary_ioerror, throttled=True) result = test_subject.get_metadata(resource_path=resource_path, is_health=is_health) self.assertFalse(result.success) self.assertFalse(result.service_error) self.assertEqual('IMDS error in /metadata/{0}: Throttled'.format(resource_path), result.response) for _, kwargs in test_subject._http_get.call_args_list: self.assertTrue('User-Agent' in kwargs['headers']) self.assertEqual(expected_useragent, kwargs['headers']['User-Agent']) self.assertEqual(2 if has_primary_ioerror else 1, test_subject._http_get.call_count) # IMDS gone error test_subject._http_get = Mock(side_effect=self._mock_http_get) self._mock_imds_setup(primary_ioerror=has_primary_ioerror, gone_error=True) result = test_subject.get_metadata(resource_path=resource_path, is_health=is_health) self.assertFalse(result.success) self.assertTrue(result.service_error) self.assertEqual('IMDS error in /metadata/{0}: HTTP Failed with Status Code 410: Gone'.format(resource_path), result.response) for _, kwargs in test_subject._http_get.call_args_list: self.assertTrue('User-Agent' in kwargs['headers']) self.assertEqual(expected_useragent, kwargs['headers']['User-Agent']) self.assertEqual(2 if has_primary_ioerror else 1, test_subject._http_get.call_count) # IMDS bad request test_subject._http_get = Mock(side_effect=self._mock_http_get) self._mock_imds_setup(primary_ioerror=has_primary_ioerror, bad_request=True) result = test_subject.get_metadata(resource_path=resource_path, is_health=is_health) self.assertFalse(result.success) self.assertFalse(result.service_error) self.assertEqual('IMDS error in /metadata/{0}: [HTTP Failed] [404: reason] Mock not found'.format(resource_path), result.response) for _, kwargs in test_subject._http_get.call_args_list: self.assertTrue('User-Agent' in kwargs['headers']) self.assertEqual(expected_useragent, kwargs['headers']['User-Agent']) self.assertEqual(2 if has_primary_ioerror else 1, test_subject._http_get.call_count) def _mock_imds_setup(self, primary_ioerror=False, secondary_ioerror=False, gone_error=False, throttled=False, bad_request=False): self._mock_imds_expect_fallback = primary_ioerror # pylint: disable=attribute-defined-outside-init self._mock_imds_primary_ioerror = primary_ioerror # pylint: disable=attribute-defined-outside-init self._mock_imds_secondary_ioerror = secondary_ioerror # pylint: disable=attribute-defined-outside-init self._mock_imds_gone_error = gone_error # pylint: disable=attribute-defined-outside-init self._mock_imds_throttled = throttled # pylint: disable=attribute-defined-outside-init self._mock_imds_bad_request = bad_request # pylint: disable=attribute-defined-outside-init def _mock_http_get(self, *_, **kwargs): if "foo.bar" == kwargs['endpoint'] and not self._mock_imds_expect_fallback: raise Exception("Unexpected endpoint called") if self._mock_imds_primary_ioerror and "169.254.169.254" == kwargs['endpoint']: raise HttpError("[HTTP Failed] GET http://{0}/metadata/{1} -- IOError timed out -- 6 attempts made" .format(kwargs['endpoint'], kwargs['resource_path'])) if self._mock_imds_secondary_ioerror and "foo.bar" == kwargs['endpoint']: raise HttpError("[HTTP Failed] GET http://{0}/metadata/{1} -- IOError timed out -- 6 attempts made" .format(kwargs['endpoint'], kwargs['resource_path'])) if self._mock_imds_gone_error: raise ResourceGoneError("Resource is gone") if self._mock_imds_throttled: raise HttpError("[HTTP Retry] GET http://{0}/metadata/{1} -- Status Code 429 -- 25 attempts made" .format(kwargs['endpoint'], kwargs['resource_path'])) resp = MagicMock() resp.reason = 'reason' if self._mock_imds_bad_request: resp.status = httpclient.NOT_FOUND resp.read.return_value = 'Mock not found' else: resp.status = httpclient.OK resp.read.return_value = 'Mock success response' return resp if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/protocol/test_metadata_server_migration_util.py000066400000000000000000000153241446033677600271050ustar00rootroot00000000000000# Copyright 2020 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import tempfile import unittest import azurelinuxagent.common.osutil.default as osutil # pylint: disable=unused-import import azurelinuxagent.common.protocol.metadata_server_migration_util as migration_util from azurelinuxagent.common.protocol.metadata_server_migration_util import _LEGACY_METADATA_SERVER_TRANSPORT_PRV_FILE_NAME, \ _LEGACY_METADATA_SERVER_TRANSPORT_CERT_FILE_NAME, \ _LEGACY_METADATA_SERVER_P7B_FILE_NAME, \ _KNOWN_METADATASERVER_IP from azurelinuxagent.common.utils.restutil import KNOWN_WIRESERVER_IP from tests.tools import AgentTestCase, patch, MagicMock class TestMetadataServerMigrationUtil(AgentTestCase): @patch('azurelinuxagent.common.conf.get_lib_dir') def test_is_metadata_server_artifact_present(self, mock_get_lib_dir): dir = tempfile.gettempdir() # pylint: disable=redefined-builtin metadata_server_transport_cert_file = os.path.join(dir, _LEGACY_METADATA_SERVER_TRANSPORT_CERT_FILE_NAME) open(metadata_server_transport_cert_file, 'w').close() mock_get_lib_dir.return_value = dir self.assertTrue(migration_util.is_metadata_server_artifact_present()) @patch('azurelinuxagent.common.conf.get_lib_dir') def test_is_metadata_server_artifact_not_present(self, mock_get_lib_dir): mock_get_lib_dir.return_value = tempfile.gettempdir() self.assertFalse(migration_util.is_metadata_server_artifact_present()) @patch('azurelinuxagent.common.conf.enable_firewall') @patch('azurelinuxagent.common.conf.get_lib_dir') def test_cleanup_metadata_server_artifacts_does_not_throw_with_no_metadata_certs(self, mock_get_lib_dir, mock_enable_firewall): mock_get_lib_dir.return_value = tempfile.gettempdir() mock_enable_firewall.return_value = False osutil = MagicMock() # pylint: disable=redefined-outer-name migration_util.cleanup_metadata_server_artifacts(osutil) @patch('azurelinuxagent.common.conf.enable_firewall') @patch('azurelinuxagent.common.conf.get_lib_dir') @patch('os.getuid') def test_cleanup_metadata_server_artifacts_firewall_enabled(self, mock_os_getuid, mock_get_lib_dir, mock_enable_firewall): # Setup Certificate Files dir = tempfile.gettempdir() # pylint: disable=redefined-builtin metadata_server_transport_prv_file = os.path.join(dir, _LEGACY_METADATA_SERVER_TRANSPORT_PRV_FILE_NAME) metadata_server_transport_cert_file = os.path.join(dir, _LEGACY_METADATA_SERVER_TRANSPORT_CERT_FILE_NAME) metadata_server_p7b_file = os.path.join(dir, _LEGACY_METADATA_SERVER_P7B_FILE_NAME) open(metadata_server_transport_prv_file, 'w').close() open(metadata_server_transport_cert_file, 'w').close() open(metadata_server_p7b_file, 'w').close() # Setup Mocks mock_get_lib_dir.return_value = dir mock_enable_firewall.return_value = True fixed_uid = 0 mock_os_getuid.return_value = fixed_uid osutil = MagicMock() # pylint: disable=redefined-outer-name osutil.enable_firewall.return_value = (MagicMock(), MagicMock()) # Run migration_util.cleanup_metadata_server_artifacts(osutil) # Assert files deleted self.assertFalse(os.path.exists(metadata_server_transport_prv_file)) self.assertFalse(os.path.exists(metadata_server_transport_cert_file)) self.assertFalse(os.path.exists(metadata_server_p7b_file)) # Assert Firewall rule calls osutil.remove_firewall.assert_called_once_with(dst_ip=_KNOWN_METADATASERVER_IP, uid=fixed_uid, wait=osutil.get_firewall_will_wait()) osutil.enable_firewall.assert_called_once_with(dst_ip=KNOWN_WIRESERVER_IP, uid=fixed_uid) @patch('azurelinuxagent.common.conf.enable_firewall') @patch('azurelinuxagent.common.conf.get_lib_dir') @patch('os.getuid') def test_cleanup_metadata_server_artifacts_firewall_disabled(self, mock_os_getuid, mock_get_lib_dir, mock_enable_firewall): # Setup Certificate Files dir = tempfile.gettempdir() # pylint: disable=redefined-builtin metadata_server_transport_prv_file = os.path.join(dir, _LEGACY_METADATA_SERVER_TRANSPORT_PRV_FILE_NAME) metadata_server_transport_cert_file = os.path.join(dir, _LEGACY_METADATA_SERVER_TRANSPORT_CERT_FILE_NAME) metadata_server_p7b_file = os.path.join(dir, _LEGACY_METADATA_SERVER_P7B_FILE_NAME) open(metadata_server_transport_prv_file, 'w').close() open(metadata_server_transport_cert_file, 'w').close() open(metadata_server_p7b_file, 'w').close() # Setup Mocks mock_get_lib_dir.return_value = dir mock_enable_firewall.return_value = False fixed_uid = 0 mock_os_getuid.return_value = fixed_uid osutil = MagicMock() # pylint: disable=redefined-outer-name # Run migration_util.cleanup_metadata_server_artifacts(osutil) # Assert files deleted self.assertFalse(os.path.exists(metadata_server_transport_prv_file)) self.assertFalse(os.path.exists(metadata_server_transport_cert_file)) self.assertFalse(os.path.exists(metadata_server_p7b_file)) # Assert Firewall rule calls osutil.remove_firewall.assert_called_once_with(dst_ip=_KNOWN_METADATASERVER_IP, uid=fixed_uid, wait=osutil.get_firewall_will_wait()) osutil.enable_firewall.assert_not_called() # Cleanup certificate files def tearDown(self): # pylint: disable=redefined-builtin dir = tempfile.gettempdir() for file in [_LEGACY_METADATA_SERVER_TRANSPORT_PRV_FILE_NAME, \ _LEGACY_METADATA_SERVER_TRANSPORT_CERT_FILE_NAME, \ _LEGACY_METADATA_SERVER_P7B_FILE_NAME]: path = os.path.join(dir, file) if os.path.exists(path): os.remove(path) # pylint: enable=redefined-builtin super(TestMetadataServerMigrationUtil, self).tearDown() if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/protocol/test_protocol_util.py000066400000000000000000000367471446033677600235430ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import tempfile import unittest from errno import ENOENT from threading import Thread from azurelinuxagent.common.exception import ProtocolError, DhcpError, OSUtilError from azurelinuxagent.common.protocol.goal_state import TRANSPORT_CERT_FILE_NAME, TRANSPORT_PRV_FILE_NAME from azurelinuxagent.common.protocol.metadata_server_migration_util import _METADATA_PROTOCOL_NAME, \ _LEGACY_METADATA_SERVER_TRANSPORT_PRV_FILE_NAME, \ _LEGACY_METADATA_SERVER_TRANSPORT_CERT_FILE_NAME, \ _LEGACY_METADATA_SERVER_P7B_FILE_NAME from azurelinuxagent.common.protocol.util import get_protocol_util, ProtocolUtil, PROTOCOL_FILE_NAME, \ WIRE_PROTOCOL_NAME, ENDPOINT_FILE_NAME from azurelinuxagent.common.utils.restutil import KNOWN_WIRESERVER_IP from tests.tools import AgentTestCase, MagicMock, Mock, patch, clear_singleton_instances @patch("time.sleep") class TestProtocolUtil(AgentTestCase): MDS_CERTIFICATES = [_LEGACY_METADATA_SERVER_TRANSPORT_PRV_FILE_NAME, \ _LEGACY_METADATA_SERVER_TRANSPORT_CERT_FILE_NAME, \ _LEGACY_METADATA_SERVER_P7B_FILE_NAME] WIRESERVER_CERTIFICATES = [TRANSPORT_CERT_FILE_NAME, TRANSPORT_PRV_FILE_NAME] def setUp(self): super(TestProtocolUtil, self).setUp() # Since ProtocolUtil is a singleton per thread, we need to clear it to ensure that the test cases do not # reuse a previous state clear_singleton_instances(ProtocolUtil) # Cleanup certificate files, protocol file, and endpoint files def tearDown(self): dir = tempfile.gettempdir() # pylint: disable=redefined-builtin for path in [os.path.join(dir, mds_cert) for mds_cert in TestProtocolUtil.MDS_CERTIFICATES]: if os.path.exists(path): os.remove(path) for path in [os.path.join(dir, ws_cert) for ws_cert in TestProtocolUtil.WIRESERVER_CERTIFICATES]: if os.path.exists(path): os.remove(path) protocol_path = os.path.join(dir, PROTOCOL_FILE_NAME) if os.path.exists(protocol_path): os.remove(protocol_path) endpoint_path = os.path.join(dir, ENDPOINT_FILE_NAME) if os.path.exists(endpoint_path): os.remove(endpoint_path) super(TestProtocolUtil, self).tearDown() def test_get_protocol_util_should_return_same_object_for_same_thread(self, _): protocol_util1 = get_protocol_util() protocol_util2 = get_protocol_util() self.assertEqual(protocol_util1, protocol_util2) def test_get_protocol_util_should_return_different_object_for_different_thread(self, _): protocol_util_instances = [] errors = [] def get_protocol_util_instance(): try: protocol_util_instances.append(get_protocol_util()) except Exception as e: errors.append(e) t1 = Thread(target=get_protocol_util_instance) t2 = Thread(target=get_protocol_util_instance) t1.start() t2.start() t1.join() t2.join() self.assertEqual(len(protocol_util_instances), 2, "Could not create the expected number of protocols. Errors: [{0}]".format(errors)) self.assertNotEqual(protocol_util_instances[0], protocol_util_instances[1], "The instances created by different threads should be different") @patch("azurelinuxagent.common.protocol.util.WireProtocol") def test_detect_protocol(self, WireProtocol, _): WireProtocol.return_value = MagicMock() protocol_util = get_protocol_util() protocol_util.dhcp_handler = MagicMock() protocol_util.dhcp_handler.endpoint = "foo.bar" # Test wire protocol is available protocol = protocol_util.get_protocol() self.assertEqual(WireProtocol.return_value, protocol) # Test wire protocol is not available protocol_util.clear_protocol() WireProtocol.return_value.detect.side_effect = ProtocolError() self.assertRaises(ProtocolError, protocol_util.get_protocol) @patch("azurelinuxagent.common.conf.get_lib_dir") @patch("azurelinuxagent.common.protocol.util.WireProtocol") def test_detect_protocol_no_dhcp(self, WireProtocol, mock_get_lib_dir, _): WireProtocol.return_value.detect = Mock() mock_get_lib_dir.return_value = self.tmp_dir protocol_util = get_protocol_util() protocol_util.osutil = MagicMock() protocol_util.osutil.is_dhcp_available.return_value = False protocol_util.dhcp_handler = MagicMock() protocol_util.dhcp_handler.endpoint = None protocol_util.dhcp_handler.run = Mock() endpoint_file = protocol_util._get_wireserver_endpoint_file_path() # pylint: disable=unused-variable # Test wire protocol when no endpoint file has been written protocol_util._detect_protocol() self.assertEqual(KNOWN_WIRESERVER_IP, protocol_util.get_wireserver_endpoint()) # Test wire protocol on dhcp failure protocol_util.osutil.is_dhcp_available.return_value = True protocol_util.dhcp_handler.run.side_effect = DhcpError() self.assertRaises(ProtocolError, protocol_util._detect_protocol) @patch("azurelinuxagent.common.protocol.util.WireProtocol") def test_get_protocol(self, WireProtocol, _): WireProtocol.return_value = MagicMock() protocol_util = get_protocol_util() protocol_util.get_wireserver_endpoint = Mock() protocol_util._detect_protocol = MagicMock() protocol_util._save_protocol("WireProtocol") protocol = protocol_util.get_protocol() self.assertEqual(WireProtocol.return_value, protocol) protocol_util.get_wireserver_endpoint.assert_any_call() @patch('azurelinuxagent.common.conf.get_lib_dir') @patch('azurelinuxagent.common.conf.enable_firewall') def test_get_protocol_wireserver_to_wireserver_update_removes_metadataserver_artifacts(self, mock_enable_firewall, mock_get_lib_dir, _): """ This is for testing that agent upgrade from WireServer to WireServer protocol will clean up leftover MDS Certificates (from a previous Metadata Server to Wireserver update, intermediate updated agent does not clean up MDS certificates) and reset firewall rules. We don't test that WireServer certificates, protocol file, or endpoint file were created because we already expect them to be created since we are updating from a WireServer agent. """ # Setup Protocol file with WireProtocol dir = tempfile.gettempdir() # pylint: disable=redefined-builtin filename = os.path.join(dir, PROTOCOL_FILE_NAME) with open(filename, "w") as f: f.write(WIRE_PROTOCOL_NAME) # Setup MDS Certificates mds_cert_paths = [os.path.join(dir, mds_cert) for mds_cert in TestProtocolUtil.MDS_CERTIFICATES] for mds_cert_path in mds_cert_paths: open(mds_cert_path, "w").close() # Setup mocks mock_get_lib_dir.return_value = dir mock_enable_firewall.return_value = True protocol_util = get_protocol_util() protocol_util.osutil = MagicMock() protocol_util.osutil.enable_firewall.return_value = (MagicMock(), MagicMock()) protocol_util.dhcp_handler = MagicMock() protocol_util.dhcp_handler.endpoint = KNOWN_WIRESERVER_IP # Run protocol_util.get_protocol() # Check MDS Certs do not exist for mds_cert_path in mds_cert_paths: self.assertFalse(os.path.exists(mds_cert_path)) # Check firewall rules was reset protocol_util.osutil.remove_firewall.assert_called_once() protocol_util.osutil.enable_firewall.assert_called_once() @patch('azurelinuxagent.common.conf.get_lib_dir') @patch('azurelinuxagent.common.conf.enable_firewall') @patch('azurelinuxagent.common.protocol.wire.WireClient') def test_get_protocol_metadataserver_to_wireserver_update_removes_metadataserver_artifacts(self, mock_wire_client, mock_enable_firewall, mock_get_lib_dir, _): """ This is for testing that agent upgrade from MetadataServer to WireServer protocol will clean up leftover MDS Certificates and reset firewall rules. Also check that WireServer certificates are present, and protocol/endpoint files are written to appropriately. """ # Setup Protocol file with MetadataProtocol dir = tempfile.gettempdir() # pylint: disable=redefined-builtin protocol_filename = os.path.join(dir, PROTOCOL_FILE_NAME) with open(protocol_filename, "w") as f: f.write(_METADATA_PROTOCOL_NAME) # Setup MDS Certificates mds_cert_paths = [os.path.join(dir, mds_cert) for mds_cert in TestProtocolUtil.MDS_CERTIFICATES] for mds_cert_path in mds_cert_paths: open(mds_cert_path, "w").close() # Setup mocks mock_get_lib_dir.return_value = dir mock_enable_firewall.return_value = True protocol_util = get_protocol_util() protocol_util.osutil = MagicMock() protocol_util.osutil.enable_firewall.return_value = (MagicMock(), MagicMock()) mock_wire_client.return_value = MagicMock() protocol_util.dhcp_handler = MagicMock() protocol_util.dhcp_handler.endpoint = KNOWN_WIRESERVER_IP # Run protocol_util.get_protocol() # Check MDS Certs do not exist for mds_cert_path in mds_cert_paths: self.assertFalse(os.path.exists(mds_cert_path)) # Check that WireServer Certs exist ws_cert_paths = [os.path.join(dir, ws_cert) for ws_cert in TestProtocolUtil.WIRESERVER_CERTIFICATES] for ws_cert_path in ws_cert_paths: self.assertTrue(os.path.isfile(ws_cert_path)) # Check firewall rules was reset protocol_util.osutil.remove_firewall.assert_called_once() protocol_util.osutil.enable_firewall.assert_called_once() # Check Protocol File is updated to WireProtocol with open(os.path.join(dir, PROTOCOL_FILE_NAME), "r") as f: self.assertEqual(f.read(), WIRE_PROTOCOL_NAME) # Check Endpoint file is updated to WireServer IP with open(os.path.join(dir, ENDPOINT_FILE_NAME), 'r') as f: self.assertEqual(f.read(), KNOWN_WIRESERVER_IP) @patch('azurelinuxagent.common.conf.get_lib_dir') @patch('azurelinuxagent.common.conf.enable_firewall') @patch('azurelinuxagent.common.protocol.wire.WireClient') def test_get_protocol_new_wireserver_agent_generates_certificates(self, mock_wire_client, mock_enable_firewall, mock_get_lib_dir, _): """ This is for testing that a new WireServer Linux Agent generates appropriate certificates, protocol file, and endpoint file. """ # Setup mocks dir = tempfile.gettempdir() # pylint: disable=redefined-builtin mock_get_lib_dir.return_value = dir mock_enable_firewall.return_value = True protocol_util = get_protocol_util() protocol_util.osutil = MagicMock() mock_wire_client.return_value = MagicMock() protocol_util.dhcp_handler = MagicMock() protocol_util.dhcp_handler.endpoint = KNOWN_WIRESERVER_IP # Run protocol_util.get_protocol() # Check that WireServer Certs exist ws_cert_paths = [os.path.join(dir, ws_cert) for ws_cert in TestProtocolUtil.WIRESERVER_CERTIFICATES] for ws_cert_path in ws_cert_paths: self.assertTrue(os.path.isfile(ws_cert_path)) # Check firewall rules were not reset protocol_util.osutil.remove_firewall.assert_not_called() protocol_util.osutil.enable_firewall.assert_not_called() # Check Protocol File is updated to WireProtocol with open(os.path.join(dir, PROTOCOL_FILE_NAME), "r") as f: self.assertEqual(f.read(), WIRE_PROTOCOL_NAME) # Check Endpoint file is updated to WireServer IP with open(os.path.join(dir, ENDPOINT_FILE_NAME), 'r') as f: self.assertEqual(f.read(), KNOWN_WIRESERVER_IP) @patch("azurelinuxagent.common.protocol.util.fileutil") @patch("azurelinuxagent.common.conf.get_lib_dir") def test_endpoint_file_states(self, mock_get_lib_dir, mock_fileutil, _): mock_get_lib_dir.return_value = self.tmp_dir protocol_util = get_protocol_util() endpoint_file = protocol_util._get_wireserver_endpoint_file_path() # Test get endpoint for io error mock_fileutil.read_file.side_effect = IOError() ep = protocol_util.get_wireserver_endpoint() self.assertEqual(ep, KNOWN_WIRESERVER_IP) # Test get endpoint when file not found mock_fileutil.read_file.side_effect = IOError(ENOENT, 'File not found') ep = protocol_util.get_wireserver_endpoint() self.assertEqual(ep, KNOWN_WIRESERVER_IP) # Test get endpoint for empty file mock_fileutil.read_file.return_value = "" ep = protocol_util.get_wireserver_endpoint() self.assertEqual(ep, KNOWN_WIRESERVER_IP) # Test set endpoint for io error mock_fileutil.write_file.side_effect = IOError() ep = protocol_util.get_wireserver_endpoint() self.assertRaises(OSUtilError, protocol_util._set_wireserver_endpoint, 'abc') # Test clear endpoint for io error with open(endpoint_file, "w+") as ep_fd: ep_fd.write("") with patch('os.remove') as mock_remove: protocol_util._clear_wireserver_endpoint() self.assertEqual(1, mock_remove.call_count) self.assertEqual(endpoint_file, mock_remove.call_args_list[0][0][0]) # Test clear endpoint when file not found with patch('os.remove') as mock_remove: mock_remove = Mock(side_effect=IOError(ENOENT, 'File not found')) protocol_util._clear_wireserver_endpoint() mock_remove.assert_not_called() def test_protocol_file_states(self, _): protocol_util = get_protocol_util() protocol_util._clear_wireserver_endpoint = Mock() protocol_file = protocol_util._get_protocol_file_path() # Test clear protocol for io error with open(protocol_file, "w+") as proto_fd: proto_fd.write("") with patch('os.remove') as mock_remove: protocol_util.clear_protocol() self.assertEqual(1, protocol_util._clear_wireserver_endpoint.call_count) self.assertEqual(1, mock_remove.call_count) self.assertEqual(protocol_file, mock_remove.call_args_list[0][0][0]) # Test clear protocol when file not found protocol_util._clear_wireserver_endpoint.reset_mock() with patch('os.remove') as mock_remove: protocol_util.clear_protocol() self.assertEqual(1, protocol_util._clear_wireserver_endpoint.call_count) self.assertEqual(1, mock_remove.call_count) self.assertEqual(protocol_file, mock_remove.call_args_list[0][0][0]) if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/protocol/test_wire.py000066400000000000000000002012201446033677600215670ustar00rootroot00000000000000# -*- encoding: utf-8 -*- # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import contextlib import json import os import socket import time import unittest import uuid from azurelinuxagent.common.agent_supported_feature import SupportedFeatureNames, get_supported_feature_by_name, \ get_agent_supported_features_list_for_crp from azurelinuxagent.common.event import WALAEventOperation from azurelinuxagent.common.exception import ResourceGoneError, ProtocolError, \ ExtensionDownloadError, HttpError from azurelinuxagent.common.protocol.extensions_goal_state_from_extensions_config import ExtensionsGoalStateFromExtensionsConfig from azurelinuxagent.common.protocol.goal_state import GoalStateProperties from azurelinuxagent.common.protocol.hostplugin import HostPluginProtocol from azurelinuxagent.common.protocol.wire import WireProtocol, WireClient, \ StatusBlob, VMStatus from azurelinuxagent.common.telemetryevent import GuestAgentExtensionEventsSchema, \ TelemetryEventParam, TelemetryEvent from azurelinuxagent.common.utils import restutil from azurelinuxagent.common.version import CURRENT_VERSION, DISTRO_NAME, DISTRO_VERSION from azurelinuxagent.ga.exthandlers import get_exthandlers_handler from tests.ga.test_monitor import random_generator from tests.protocol import mockwiredata from tests.protocol.mocks import mock_wire_protocol, MockHttpResponse from tests.protocol.HttpRequestPredicates import HttpRequestPredicates from tests.protocol.mockwiredata import DATA_FILE_NO_EXT, DATA_FILE from tests.protocol.mockwiredata import WireProtocolData from tests.tools import patch, AgentTestCase, load_bin_data data_with_bom = b'\xef\xbb\xbfhehe' testurl = 'http://foo' testtype = 'BlockBlob' WIRESERVER_URL = '168.63.129.16' def get_event(message, duration=30000, evt_type="", is_internal=False, is_success=True, name="", op="Unknown", version=CURRENT_VERSION, eventId=1): event = TelemetryEvent(eventId, "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX") event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.Name, name)) event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.Version, str(version))) event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.IsInternal, is_internal)) event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.Operation, op)) event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.OperationSuccess, is_success)) event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.Message, message)) event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.Duration, duration)) event.parameters.append(TelemetryEventParam(GuestAgentExtensionEventsSchema.ExtensionType, evt_type)) return event @contextlib.contextmanager def create_mock_protocol(): with mock_wire_protocol(DATA_FILE_NO_EXT) as protocol: yield protocol @patch("time.sleep") @patch("azurelinuxagent.common.protocol.wire.CryptUtil") @patch("azurelinuxagent.common.protocol.healthservice.HealthService._report") class TestWireProtocol(AgentTestCase, HttpRequestPredicates): def setUp(self): super(TestWireProtocol, self).setUp() HostPluginProtocol.is_default_channel = False def _test_getters(self, test_data, certsMustBePresent, __, MockCryptUtil, _): MockCryptUtil.side_effect = test_data.mock_crypt_util with patch.object(restutil, 'http_get', test_data.mock_http_get): protocol = WireProtocol(WIRESERVER_URL) protocol.detect() protocol.get_vminfo() protocol.get_certs() ext_handlers = protocol.get_goal_state().extensions_goal_state.extensions for ext_handler in ext_handlers: protocol.get_goal_state().fetch_extension_manifest(ext_handler.name, ext_handler.manifest_uris) crt1 = os.path.join(self.tmp_dir, '38B85D88F03D1A8E1C671EB169274C09BC4D4703.crt') crt2 = os.path.join(self.tmp_dir, 'BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F.crt') prv2 = os.path.join(self.tmp_dir, 'BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F.prv') if certsMustBePresent: self.assertTrue(os.path.isfile(crt1)) self.assertTrue(os.path.isfile(crt2)) self.assertTrue(os.path.isfile(prv2)) else: self.assertFalse(os.path.isfile(crt1)) self.assertFalse(os.path.isfile(crt2)) self.assertFalse(os.path.isfile(prv2)) self.assertEqual("1", protocol.get_goal_state().incarnation) @staticmethod def _get_telemetry_events_generator(event_list): def _yield_events(): for telemetry_event in event_list: yield telemetry_event return _yield_events() def test_getters(self, *args): """Normal case""" test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE) self._test_getters(test_data, True, *args) def test_getters_no_ext(self, *args): """Provision with agent is not checked""" test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_NO_EXT) self._test_getters(test_data, True, *args) def test_getters_ext_no_settings(self, *args): """Extensions without any settings""" test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_EXT_NO_SETTINGS) self._test_getters(test_data, True, *args) def test_getters_ext_no_public(self, *args): """Extensions without any public settings""" test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_EXT_NO_PUBLIC) self._test_getters(test_data, True, *args) def test_getters_ext_no_cert_format(self, *args): """Certificate format not specified""" test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_NO_CERT_FORMAT) self._test_getters(test_data, True, *args) def test_getters_ext_cert_format_not_pfx(self, *args): """Certificate format is not Pkcs7BlobWithPfxContents specified""" test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_CERT_FORMAT_NOT_PFX) self._test_getters(test_data, False, *args) @patch("azurelinuxagent.common.protocol.healthservice.HealthService.report_host_plugin_extension_artifact") def test_getters_with_stale_goal_state(self, patch_report, *args): test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE) test_data.emulate_stale_goal_state = True self._test_getters(test_data, True, *args) # Ensure HostPlugin was invoked self.assertEqual(1, test_data.call_counts["/versions"]) self.assertEqual(2, test_data.call_counts["extensionArtifact"]) # Ensure the expected number of HTTP calls were made # -- Tracking calls to retrieve GoalState is problematic since it is # fetched often; however, the dependent documents, such as the # HostingEnvironmentConfig, will be retrieved the expected number self.assertEqual(1, test_data.call_counts["hostingEnvironmentConfig"]) self.assertEqual(1, patch_report.call_count) def test_call_storage_kwargs(self, *args): # pylint: disable=unused-argument with patch.object(restutil, 'http_get') as http_patch: http_req = restutil.http_get url = testurl headers = {} # no kwargs -- Default to True WireClient.call_storage_service(http_req) # kwargs, no use_proxy -- Default to True WireClient.call_storage_service(http_req, url, headers) # kwargs, use_proxy None -- Default to True WireClient.call_storage_service(http_req, url, headers, use_proxy=None) # kwargs, use_proxy False -- Keep False WireClient.call_storage_service(http_req, url, headers, use_proxy=False) # kwargs, use_proxy True -- Keep True WireClient.call_storage_service(http_req, url, headers, use_proxy=True) # assert self.assertTrue(http_patch.call_count == 5) for i in range(0, 5): c = http_patch.call_args_list[i][-1]['use_proxy'] self.assertTrue(c == (True if i != 3 else False)) def test_status_blob_parsing(self, *args): # pylint: disable=unused-argument with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: extensions_goal_state = protocol.get_goal_state().extensions_goal_state self.assertIsInstance(extensions_goal_state, ExtensionsGoalStateFromExtensionsConfig) self.assertEqual(extensions_goal_state.status_upload_blob, 'https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?' 'sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&' 'sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo') self.assertEqual(protocol.get_goal_state().extensions_goal_state.status_upload_blob_type, u'BlockBlob') def test_get_host_ga_plugin(self, *args): # pylint: disable=unused-argument with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: host_plugin = protocol.client.get_host_plugin() goal_state = protocol.client.get_goal_state() self.assertEqual(goal_state.container_id, host_plugin.container_id) self.assertEqual(goal_state.role_config_name, host_plugin.role_config_name) def test_upload_status_blob_should_use_the_host_channel_by_default(self, *_): def http_put_handler(url, *_, **__): # pylint: disable=inconsistent-return-statements if protocol.get_endpoint() in url and url.endswith('/status'): return MockHttpResponse(200) with mock_wire_protocol(mockwiredata.DATA_FILE, http_put_handler=http_put_handler) as protocol: HostPluginProtocol.is_default_channel = False protocol.client.status_blob.vm_status = VMStatus(message="Ready", status="Ready") protocol.client.upload_status_blob() urls = protocol.get_tracked_urls() self.assertEqual(len(urls), 1, 'Expected one post request to the host: [{0}]'.format(urls)) def test_upload_status_blob_host_ga_plugin(self, *_): with create_mock_protocol() as protocol: protocol.client.status_blob.vm_status = VMStatus(message="Ready", status="Ready") with patch.object(HostPluginProtocol, "ensure_initialized", return_value=True): with patch.object(StatusBlob, "upload", return_value=False) as patch_default_upload: with patch.object(HostPluginProtocol, "_put_block_blob_status") as patch_http: HostPluginProtocol.is_default_channel = False protocol.client.upload_status_blob() patch_default_upload.assert_not_called() patch_http.assert_called_once_with(testurl, protocol.client.status_blob) self.assertFalse(HostPluginProtocol.is_default_channel) def test_upload_status_blob_reports_prepare_error(self, *_): with create_mock_protocol() as protocol: protocol.client.status_blob.vm_status = VMStatus(message="Ready", status="Ready") with patch.object(StatusBlob, "prepare", side_effect=Exception) as mock_prepare: self.assertRaises(ProtocolError, protocol.client.upload_status_blob) self.assertEqual(1, mock_prepare.call_count) def test_get_in_vm_artifacts_profile_blob_not_available(self, *_): data_file = mockwiredata.DATA_FILE.copy() data_file["ext_conf"] = "wire/ext_conf_in_vm_empty_artifacts_profile.xml" with mock_wire_protocol(data_file) as protocol: self.assertFalse(protocol.get_goal_state().extensions_goal_state.on_hold) def test_it_should_set_on_hold_to_false_when_the_in_vm_artifacts_profile_is_not_valid(self, *_): with mock_wire_protocol(mockwiredata.DATA_FILE_IN_VM_ARTIFACTS_PROFILE) as protocol: extensions_on_hold = protocol.get_goal_state().extensions_goal_state.on_hold self.assertTrue(extensions_on_hold, "Extensions should be on hold in the test data") def http_get_handler(url, *_, **kwargs): if self.is_in_vm_artifacts_profile_request(url) or self.is_host_plugin_in_vm_artifacts_profile_request(url, kwargs): return mock_response return None protocol.set_http_handlers(http_get_handler=http_get_handler) mock_response = MockHttpResponse(200, body=None) protocol.client.reset_goal_state() extensions_on_hold = protocol.get_goal_state().extensions_goal_state.on_hold self.assertFalse(extensions_on_hold, "Extensions should not be on hold when the in-vm artifacts profile response body is None") mock_response = MockHttpResponse(200, ' '.encode('utf-8')) protocol.client.reset_goal_state() extensions_on_hold = protocol.get_goal_state().extensions_goal_state.on_hold self.assertFalse(extensions_on_hold, "Extensions should not be on hold when the in-vm artifacts profile response is an empty string") mock_response = MockHttpResponse(200, '{ }'.encode('utf-8')) protocol.client.reset_goal_state() extensions_on_hold = protocol.get_goal_state().extensions_goal_state.on_hold self.assertFalse(extensions_on_hold, "Extensions should not be on hold when the in-vm artifacts profile response is an empty json object") with patch("azurelinuxagent.common.protocol.extensions_goal_state_from_extensions_config.add_event") as add_event: mock_response = MockHttpResponse(200, 'invalid json'.encode('utf-8')) protocol.client.reset_goal_state() extensions_on_hold = protocol.get_goal_state().extensions_goal_state.on_hold self.assertFalse(extensions_on_hold, "Extensions should not be on hold when the in-vm artifacts profile response is not valid json") events = [kwargs for _, kwargs in add_event.call_args_list if kwargs['op'] == WALAEventOperation.ArtifactsProfileBlob] self.assertEqual(1, len(events), "Expected 1 event for operation ArtifactsProfileBlob. Got: {0}".format(events)) self.assertFalse(events[0]['is_success'], "Expected ArtifactsProfileBlob's success to be False") self.assertTrue("Can't parse the artifacts profile blob" in events[0]['message'], "Expected 'Can't parse the artifacts profile blob as the reason for the operation failure. Got: {0}".format(events[0]['message'])) @patch("socket.gethostname", return_value="hostname") @patch("time.gmtime", return_value=time.localtime(1485543256)) def test_report_vm_status(self, *args): # pylint: disable=unused-argument status = 'status' message = 'message' client = WireProtocol(WIRESERVER_URL).client actual = StatusBlob(client=client) actual.set_vm_status(VMStatus(status=status, message=message)) timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) formatted_msg = { 'lang': 'en-US', 'message': message } v1_ga_status = { 'version': str(CURRENT_VERSION), 'status': status, 'formattedMessage': formatted_msg } v1_ga_guest_info = { 'computerName': socket.gethostname(), 'osName': DISTRO_NAME, 'osVersion': DISTRO_VERSION, 'version': str(CURRENT_VERSION), } v1_agg_status = { 'guestAgentStatus': v1_ga_status, 'handlerAggregateStatus': [] } supported_features = [] for _, feature in get_agent_supported_features_list_for_crp().items(): supported_features.append( { "Key": feature.name, "Value": feature.version } ) v1_vm_status = { 'version': '1.1', 'timestampUTC': timestamp, 'aggregateStatus': v1_agg_status, 'guestOSInfo': v1_ga_guest_info, 'supportedFeatures': supported_features } self.assertEqual(json.dumps(v1_vm_status), actual.to_json()) def test_it_should_report_supported_features_in_status_blob_if_supported(self, *_): with mock_wire_protocol(DATA_FILE) as protocol: def mock_http_put(url, *args, **__): if not HttpRequestPredicates.is_host_plugin_status_request(url): # Skip reading the HostGA request data as its encoded protocol.aggregate_status = json.loads(args[0]) protocol.aggregate_status = {} protocol.set_http_handlers(http_put_handler=mock_http_put) exthandlers_handler = get_exthandlers_handler(protocol) with patch("azurelinuxagent.common.agent_supported_feature._MultiConfigFeature.is_supported", True): exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertIsNotNone(protocol.aggregate_status, "Aggregate status should not be None") self.assertIn("supportedFeatures", protocol.aggregate_status, "supported features not reported") multi_config_feature = get_supported_feature_by_name(SupportedFeatureNames.MultiConfig) found = False for feature in protocol.aggregate_status['supportedFeatures']: if feature['Key'] == multi_config_feature.name and feature['Value'] == multi_config_feature.version: found = True break self.assertTrue(found, "Multi-config name should be present in supportedFeatures") # Feature should not be reported if not present with patch("azurelinuxagent.common.agent_supported_feature._MultiConfigFeature.is_supported", False): exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertIsNotNone(protocol.aggregate_status, "Aggregate status should not be None") if "supportedFeatures" not in protocol.aggregate_status: # In the case Multi-config was the only feature available, 'supportedFeatures' should not be # reported in the status blob as its not supported as of now. # Asserting no other feature was available to report back to crp self.assertEqual(0, len(get_agent_supported_features_list_for_crp()), "supportedFeatures should be available if there are more features") return # If there are other features available, confirm MultiConfig was not reported multi_config_feature = get_supported_feature_by_name(SupportedFeatureNames.MultiConfig) found = False for feature in protocol.aggregate_status['supportedFeatures']: if feature['Key'] == multi_config_feature.name and feature['Value'] == multi_config_feature.version: found = True break self.assertFalse(found, "Multi-config name should be present in supportedFeatures") @patch("azurelinuxagent.common.utils.restutil.http_request") def test_send_encoded_event(self, mock_http_request, *args): mock_http_request.return_value = MockHttpResponse(200) event_str = u'a test string' client = WireProtocol(WIRESERVER_URL).client client.send_encoded_event("foo", event_str.encode('utf-8')) first_call = mock_http_request.call_args_list[0] args, kwargs = first_call method, url, body_received, timeout = args # pylint: disable=unused-variable headers = kwargs['headers'] # the headers should include utf-8 encoding... self.assertTrue("utf-8" in headers['Content-Type']) # the body is encoded, decode and check for equality self.assertIn(event_str, body_received.decode('utf-8')) @patch("azurelinuxagent.common.protocol.wire.WireClient.send_encoded_event") def test_report_event_small_event(self, patch_send_event, *args): # pylint: disable=unused-argument event_list = [] client = WireProtocol(WIRESERVER_URL).client event_str = random_generator(10) event_list.append(get_event(message=event_str)) event_str = random_generator(100) event_list.append(get_event(message=event_str)) event_str = random_generator(1000) event_list.append(get_event(message=event_str)) event_str = random_generator(10000) event_list.append(get_event(message=event_str)) client.report_event(self._get_telemetry_events_generator(event_list)) # It merges the messages into one message self.assertEqual(patch_send_event.call_count, 1) @patch("azurelinuxagent.common.protocol.wire.WireClient.send_encoded_event") def test_report_event_multiple_events_to_fill_buffer(self, patch_send_event, *args): # pylint: disable=unused-argument event_list = [] client = WireProtocol(WIRESERVER_URL).client event_str = random_generator(2 ** 15) event_list.append(get_event(message=event_str)) event_list.append(get_event(message=event_str)) client.report_event(self._get_telemetry_events_generator(event_list)) # It merges the messages into one message self.assertEqual(patch_send_event.call_count, 2) @patch("azurelinuxagent.common.protocol.wire.WireClient.send_encoded_event") def test_report_event_large_event(self, patch_send_event, *args): # pylint: disable=unused-argument event_list = [] event_str = random_generator(2 ** 18) event_list.append(get_event(message=event_str)) client = WireProtocol(WIRESERVER_URL).client client.report_event(self._get_telemetry_events_generator(event_list)) self.assertEqual(patch_send_event.call_count, 0) class TestWireClient(HttpRequestPredicates, AgentTestCase): def test_get_ext_conf_without_extensions_should_retrieve_vmagent_manifests_info(self, *args): # pylint: disable=unused-argument # Basic test for extensions_goal_state when extensions are not present in the config. The test verifies that # extensions_goal_state fetches the correct data by comparing the returned data with the test data provided the # mock_wire_protocol. with mock_wire_protocol(mockwiredata.DATA_FILE_NO_EXT) as protocol: extensions_goal_state = protocol.get_goal_state().extensions_goal_state ext_handlers_names = [ext_handler.name for ext_handler in extensions_goal_state.extensions] self.assertEqual(0, len(extensions_goal_state.extensions), "Unexpected number of extension handlers in the extension config: [{0}]".format(ext_handlers_names)) vmagent_families = [manifest.name for manifest in extensions_goal_state.agent_families] self.assertEqual(0, len(extensions_goal_state.agent_families), "Unexpected number of vmagent manifests in the extension config: [{0}]".format(vmagent_families)) self.assertFalse(extensions_goal_state.on_hold, "Extensions On Hold is expected to be False") def test_get_ext_conf_with_extensions_should_retrieve_ext_handlers_and_vmagent_manifests_info(self): # Basic test for extensions_goal_state when extensions are present in the config. The test verifies that extensions_goal_state # fetches the correct data by comparing the returned data with the test data provided the mock_wire_protocol. with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: extensions_goal_state = protocol.get_goal_state().extensions_goal_state ext_handlers_names = [ext_handler.name for ext_handler in extensions_goal_state.extensions] self.assertEqual(1, len(extensions_goal_state.extensions), "Unexpected number of extension handlers in the extension config: [{0}]".format(ext_handlers_names)) vmagent_families = [manifest.name for manifest in extensions_goal_state.agent_families] self.assertEqual(2, len(extensions_goal_state.agent_families), "Unexpected number of vmagent manifests in the extension config: [{0}]".format(vmagent_families)) self.assertEqual("https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw" "&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo", extensions_goal_state.status_upload_blob, "Unexpected value for status upload blob URI") self.assertEqual("BlockBlob", extensions_goal_state.status_upload_blob_type, "Unexpected status upload blob type in the extension config") self.assertFalse(extensions_goal_state.on_hold, "Extensions On Hold is expected to be False") def test_download_zip_package_should_expand_and_delete_the_package(self): extension_url = 'https://fake_host/fake_extension.zip' target_file = os.path.join(self.tmp_dir, 'fake_extension.zip') target_directory = os.path.join(self.tmp_dir, "fake_extension") def http_get_handler(url, *_, **__): if url == extension_url or self.is_host_plugin_extension_artifact_request(url): return MockHttpResponse(200, body=load_bin_data("ga/fake_extension.zip")) return None with mock_wire_protocol(mockwiredata.DATA_FILE, http_get_handler=http_get_handler) as protocol: protocol.client.download_zip_package("extension package", [extension_url], target_file, target_directory, use_verify_header=False) self.assertTrue(os.path.exists(target_directory), "The extension package was not downloaded") self.assertFalse(os.path.exists(target_file), "The extension package was not deleted") def test_download_zip_package_should_not_invoke_host_channel_when_direct_channel_succeeds(self): extension_url = 'https://fake_host/fake_extension.zip' target_file = os.path.join(self.tmp_dir, 'fake_extension.zip') target_directory = os.path.join(self.tmp_dir, "fake_extension") def http_get_handler(url, *_, **__): if url == extension_url: return MockHttpResponse(200, body=load_bin_data("ga/fake_extension.zip")) if self.is_host_plugin_extension_artifact_request(url): self.fail('The host channel should not have been used') return None with mock_wire_protocol(mockwiredata.DATA_FILE, http_get_handler=http_get_handler) as protocol: HostPluginProtocol.is_default_channel = False protocol.client.download_zip_package("extension package", [extension_url], target_file, target_directory, use_verify_header=False) urls = protocol.get_tracked_urls() self.assertEqual(len(urls), 1, "Unexpected number of HTTP requests: [{0}]".format(urls)) self.assertEqual(urls[0], extension_url, "The extension should have been downloaded over the direct channel") self.assertTrue(os.path.exists(target_directory), "The extension package was not downloaded") self.assertFalse(HostPluginProtocol.is_default_channel, "The host channel should not have been set as the default") def test_download_zip_package_should_use_host_channel_when_direct_channel_fails_and_set_host_as_default(self): extension_url = 'https://fake_host/fake_extension.zip' target_file = os.path.join(self.tmp_dir, 'fake_extension.zip') target_directory = os.path.join(self.tmp_dir, "fake_extension") def http_get_handler(url, *_, **kwargs): if url == extension_url: return HttpError("Exception to fake an error on the direct channel") if self.is_host_plugin_extension_request(url, kwargs, extension_url): return MockHttpResponse(200, body=load_bin_data("ga/fake_extension.zip")) return None with mock_wire_protocol(mockwiredata.DATA_FILE, http_get_handler=http_get_handler) as protocol: HostPluginProtocol.is_default_channel = False protocol.client.download_zip_package("extension package", [extension_url], target_file, target_directory, use_verify_header=False) urls = protocol.get_tracked_urls() self.assertEqual(len(urls), 2, "Unexpected number of HTTP requests: [{0}]".format(urls)) self.assertEqual(urls[0], extension_url, "The first attempt should have been over the direct channel") self.assertTrue(self.is_host_plugin_extension_artifact_request(urls[1]), "The retry attempt should have been over the host channel") self.assertTrue(os.path.exists(target_directory), 'The extension package was not downloaded') self.assertTrue(HostPluginProtocol.is_default_channel, "The host channel should have been set as the default") def test_download_zip_package_should_retry_the_host_channel_after_refreshing_host_plugin(self): extension_url = 'https://fake_host/fake_extension.zip' target_file = os.path.join(self.tmp_dir, 'fake_extension.zip') target_directory = os.path.join(self.tmp_dir, "fake_extension") def http_get_handler(url, *_, **kwargs): if url == extension_url: return HttpError("Exception to fake an error on the direct channel") if self.is_host_plugin_extension_request(url, kwargs, extension_url): # fake a stale goal state then succeed once the goal state has been refreshed if http_get_handler.goal_state_requests == 0: http_get_handler.goal_state_requests += 1 return ResourceGoneError("Exception to fake a stale goal") return MockHttpResponse(200, body=load_bin_data("ga/fake_extension.zip")) if self.is_goal_state_request(url): protocol.track_url(url) # track requests for the goal state return None http_get_handler.goal_state_requests = 0 with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: HostPluginProtocol.is_default_channel = False try: # initialization of the host plugin triggers a request for the goal state; do it here before we start tracking those requests. protocol.client.get_host_plugin() protocol.set_http_handlers(http_get_handler=http_get_handler) protocol.client.download_zip_package("extension package", [extension_url], target_file, target_directory, use_verify_header=False) urls = protocol.get_tracked_urls() self.assertEqual(len(urls), 4, "Unexpected number of HTTP requests: [{0}]".format(urls)) self.assertEqual(urls[0], extension_url, "The first attempt should have been over the direct channel") self.assertTrue(self.is_host_plugin_extension_artifact_request(urls[1]), "The second attempt should have been over the host channel") self.assertTrue(self.is_goal_state_request(urls[2]), "The host channel should have been refreshed the goal state") self.assertTrue(self.is_host_plugin_extension_artifact_request(urls[3]), "The third attempt should have been over the host channel") self.assertTrue(os.path.exists(target_directory), 'The extension package was not downloaded') self.assertTrue(HostPluginProtocol.is_default_channel, "The host channel should have been set as the default") finally: HostPluginProtocol.is_default_channel = False def test_download_zip_package_should_not_change_default_channel_when_all_channels_fail(self): extension_url = 'https://fake_host/fake_extension.zip' target_file = os.path.join(self.tmp_dir, "fake_extension.zip") target_directory = os.path.join(self.tmp_dir, "fake_extension") def http_get_handler(url, *_, **kwargs): if url == extension_url or self.is_host_plugin_extension_request(url, kwargs, extension_url): return MockHttpResponse(status=404, body=b"content not found", reason="Not Found") if self.is_goal_state_request(url): protocol.track_url(url) # keep track of goal state requests return None with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: HostPluginProtocol.is_default_channel = False # initialization of the host plugin triggers a request for the goal state; do it here before we start tracking those requests. protocol.client.get_host_plugin() protocol.set_http_handlers(http_get_handler=http_get_handler) with self.assertRaises(ExtensionDownloadError): protocol.client.download_zip_package("extension package", [extension_url], target_file, target_directory, use_verify_header=False) urls = protocol.get_tracked_urls() self.assertEqual(len(urls), 2, "Unexpected number of HTTP requests: [{0}]".format(urls)) self.assertEqual(urls[0], extension_url, "The first attempt should have been over the direct channel") self.assertTrue(self.is_host_plugin_extension_artifact_request(urls[1]), "The second attempt should have been over the host channel") self.assertFalse(os.path.exists(target_file), "The extension package was downloaded and it shouldn't have") self.assertFalse(HostPluginProtocol.is_default_channel, "The host channel should not have been set as the default") def test_invalid_zip_should_raise_an_error(self): extension_url = 'https://fake_host/fake_extension.zip' target_file = os.path.join(self.tmp_dir, "fake_extension.zip") target_directory = os.path.join(self.tmp_dir, "fake_extension") def http_get_handler(url, *_, **kwargs): if url == extension_url or self.is_host_plugin_extension_request(url, kwargs, extension_url): return MockHttpResponse(status=200, body=b"NOT A ZIP") return None with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: protocol.set_http_handlers(http_get_handler=http_get_handler) with self.assertRaises(ExtensionDownloadError): protocol.client.download_zip_package("extension package", [extension_url], target_file, target_directory, use_verify_header=False) self.assertFalse(os.path.exists(target_file), "The extension package should have been deleted") self.assertFalse(os.path.exists(target_directory), "The extension directory should not have been created") def test_fetch_manifest_should_not_invoke_host_channel_when_direct_channel_succeeds(self): manifest_url = 'https://fake_host/fake_manifest.xml' manifest_xml = '' def http_get_handler(url, *_, **__): if url == manifest_url: return MockHttpResponse(200, manifest_xml.encode('utf-8')) if url.endswith('/extensionArtifact'): self.fail('The Host GA Plugin should not have been invoked') return None with mock_wire_protocol(mockwiredata.DATA_FILE, http_get_handler=http_get_handler) as protocol: HostPluginProtocol.is_default_channel = False manifest = protocol.client.fetch_manifest([manifest_url], use_verify_header=False) urls = protocol.get_tracked_urls() self.assertEqual(manifest, manifest_xml, 'The expected manifest was not downloaded') self.assertEqual(len(urls), 1, "Unexpected number of HTTP requests: [{0}]".format(urls)) self.assertEqual(urls[0], manifest_url, "The manifest should have been downloaded over the direct channel") self.assertFalse(HostPluginProtocol.is_default_channel, "The default channel should not have changed") def test_fetch_manifest_should_use_host_channel_when_direct_channel_fails_and_set_it_to_default(self): manifest_url = 'https://fake_host/fake_manifest.xml' manifest_xml = '' def http_get_handler(url, *_, **kwargs): if url == manifest_url: return ResourceGoneError("Exception to fake an error on the direct channel") if self.is_host_plugin_extension_request(url, kwargs, manifest_url): return MockHttpResponse(200, body=manifest_xml.encode('utf-8')) return None with mock_wire_protocol(mockwiredata.DATA_FILE, http_get_handler=http_get_handler) as protocol: HostPluginProtocol.is_default_channel = False try: manifest = protocol.client.fetch_manifest([manifest_url], use_verify_header=False) urls = protocol.get_tracked_urls() self.assertEqual(manifest, manifest_xml, 'The expected manifest was not downloaded') self.assertEqual(len(urls), 2, "Unexpected number of HTTP requests: [{0}]".format(urls)) self.assertEqual(urls[0], manifest_url, "The first attempt should have been over the direct channel") self.assertTrue(self.is_host_plugin_extension_artifact_request(urls[1]), "The retry should have been over the host channel") self.assertTrue(HostPluginProtocol.is_default_channel, "The host should have been set as the default channel") finally: HostPluginProtocol.is_default_channel = False # Reset default channel def test_fetch_manifest_should_retry_the_host_channel_after_refreshing_the_host_plugin_and_set_the_host_as_default(self): manifest_url = 'https://fake_host/fake_manifest.xml' manifest_xml = '' def http_get_handler(url, *_, **kwargs): if url == manifest_url: return HttpError("Exception to fake an error on the direct channel") if self.is_host_plugin_extension_request(url, kwargs, manifest_url): # fake a stale goal state then succeed once the goal state has been refreshed if http_get_handler.goal_state_requests == 0: http_get_handler.goal_state_requests += 1 return ResourceGoneError("Exception to fake a stale goal state") return MockHttpResponse(200, manifest_xml.encode('utf-8')) elif self.is_goal_state_request(url): protocol.track_url(url) # keep track of goal state requests return None http_get_handler.goal_state_requests = 0 with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: HostPluginProtocol.is_default_channel = False try: # initialization of the host plugin triggers a request for the goal state; do it here before we start tracking those requests. protocol.client.get_host_plugin() protocol.set_http_handlers(http_get_handler=http_get_handler) manifest = protocol.client.fetch_manifest([manifest_url], use_verify_header=False) urls = protocol.get_tracked_urls() self.assertEqual(manifest, manifest_xml) self.assertEqual(len(urls), 4, "Unexpected number of HTTP requests: [{0}]".format(urls)) self.assertEqual(urls[0], manifest_url, "The first attempt should have been over the direct channel") self.assertTrue(self.is_host_plugin_extension_artifact_request(urls[1]), "The second attempt should have been over the host channel") self.assertTrue(self.is_goal_state_request(urls[2]), "The host channel should have been refreshed the goal state") self.assertTrue(self.is_host_plugin_extension_artifact_request(urls[3]), "The third attempt should have been over the host channel") self.assertTrue(HostPluginProtocol.is_default_channel, "The host should have been set as the default channel") finally: HostPluginProtocol.is_default_channel = False # Reset default channel def test_fetch_manifest_should_update_goal_state_and_not_change_default_channel_if_host_fails(self): manifest_url = 'https://fake_host/fake_manifest.xml' def http_get_handler(url, *_, **kwargs): if url == manifest_url or self.is_host_plugin_extension_request(url, kwargs, manifest_url): return ResourceGoneError("Exception to fake an error on either channel") elif self.is_goal_state_request(url): protocol.track_url(url) # keep track of goal state requests return None # Everything fails. Goal state should have been updated and host channel should not have been set as default. with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: HostPluginProtocol.is_default_channel = False # initialization of the host plugin triggers a request for the goal state; do it here before we start # tracking those requests. protocol.client.get_host_plugin() protocol.set_http_handlers(http_get_handler=http_get_handler) with self.assertRaises(ExtensionDownloadError): protocol.client.fetch_manifest([manifest_url], use_verify_header=False) urls = protocol.get_tracked_urls() self.assertEqual(len(urls), 4, "Unexpected number of HTTP requests: [{0}]".format(urls)) self.assertEqual(urls[0], manifest_url, "The first attempt should have been over the direct channel") self.assertTrue(self.is_host_plugin_extension_artifact_request(urls[1]), "The second attempt should have been over the host channel") self.assertTrue(self.is_goal_state_request(urls[2]), "The host channel should have been refreshed the goal state") self.assertTrue(self.is_host_plugin_extension_artifact_request(urls[3]), "The third attempt should have been over the host channel") self.assertFalse(HostPluginProtocol.is_default_channel, "The host should not have been set as the default channel") self.assertEqual(HostPluginProtocol.is_default_channel, False) def test_get_artifacts_profile_should_not_invoke_host_channel_when_direct_channel_succeeds(self): def http_get_handler(url, *_, **__): if self.is_in_vm_artifacts_profile_request(url): protocol.track_url(url) return None with mock_wire_protocol(mockwiredata.DATA_FILE_IN_VM_ARTIFACTS_PROFILE) as protocol: protocol.set_http_handlers(http_get_handler=http_get_handler) HostPluginProtocol.is_default_channel = False protocol.client.reset_goal_state() urls = protocol.get_tracked_urls() self.assertEqual(len(urls), 1, "Unexpected HTTP requests: [{0}]".format(urls)) self.assertFalse(HostPluginProtocol.is_default_channel, "The host should not have been set as the default channel") def test_get_artifacts_profile_should_use_host_channel_when_direct_channel_fails(self): def http_get_handler(url, *_, **kwargs): if self.is_in_vm_artifacts_profile_request(url): return HttpError("Exception to fake an error on the direct channel") if self.is_host_plugin_in_vm_artifacts_profile_request(url, kwargs): protocol.track_url(url) return None with mock_wire_protocol(mockwiredata.DATA_FILE_IN_VM_ARTIFACTS_PROFILE) as protocol: protocol.set_http_handlers(http_get_handler=http_get_handler) HostPluginProtocol.is_default_channel = False try: protocol.client.reset_goal_state() urls = protocol.get_tracked_urls() self.assertEqual(len(urls), 2, "Invalid number of requests: [{0}]".format(urls)) self.assertTrue(self.is_in_vm_artifacts_profile_request(urls[0]), "The first request should have been over the direct channel") self.assertTrue(self.is_host_plugin_extension_artifact_request(urls[1]), "The second request should have been over the host channel") self.assertTrue(HostPluginProtocol.is_default_channel, "The default channel should have changed to the host") finally: HostPluginProtocol.is_default_channel = False def test_get_artifacts_profile_should_retry_the_host_channel_after_refreshing_the_host_plugin(self): def http_get_handler(url, *_, **kwargs): if self.is_in_vm_artifacts_profile_request(url): return HttpError("Exception to fake an error on the direct channel") if self.is_host_plugin_in_vm_artifacts_profile_request(url, kwargs): if http_get_handler.host_plugin_calls == 0: http_get_handler.host_plugin_calls += 1 return ResourceGoneError("Exception to fake a stale goal state") protocol.track_url(url) if self.is_goal_state_request(url) and http_get_handler.host_plugin_calls == 1: protocol.track_url(url) return None http_get_handler.host_plugin_calls = 0 with mock_wire_protocol(mockwiredata.DATA_FILE_IN_VM_ARTIFACTS_PROFILE) as protocol: HostPluginProtocol.is_default_channel = False try: # initialization of the host plugin triggers a request for the goal state; do it here before we start tracking those requests. protocol.client.get_host_plugin() protocol.set_http_handlers(http_get_handler=http_get_handler) protocol.client.reset_goal_state() urls = protocol.get_tracked_urls() self.assertEqual(len(urls), 4, "Invalid number of requests: [{0}]".format(urls)) self.assertTrue(self.is_in_vm_artifacts_profile_request(urls[0]), "The first request should have been over the direct channel") self.assertTrue(self.is_host_plugin_extension_artifact_request(urls[1]), "The second request should have been over the host channel") self.assertTrue(self.is_goal_state_request(urls[2]), "The goal state should have been refreshed before retrying the host channel") self.assertTrue(self.is_host_plugin_extension_artifact_request(urls[3]), "The retry request should have been over the host channel") self.assertTrue(HostPluginProtocol.is_default_channel, "The default channel should have changed to the host") finally: HostPluginProtocol.is_default_channel = False def test_get_artifacts_profile_should_refresh_the_host_plugin_and_not_change_default_channel_if_host_plugin_fails(self): def http_get_handler(url, *_, **kwargs): if self.is_in_vm_artifacts_profile_request(url): return HttpError("Exception to fake an error on the direct channel") if self.is_host_plugin_in_vm_artifacts_profile_request(url, kwargs): http_get_handler.host_plugin_calls += 1 return ResourceGoneError("Exception to fake a stale goal state") if self.is_goal_state_request(url) and http_get_handler.host_plugin_calls == 1: protocol.track_url(url) return None http_get_handler.host_plugin_calls = 0 with mock_wire_protocol(mockwiredata.DATA_FILE_IN_VM_ARTIFACTS_PROFILE) as protocol: HostPluginProtocol.is_default_channel = False # initialization of the host plugin triggers a request for the goal state; do it here before we start tracking those requests. protocol.client.get_host_plugin() protocol.set_http_handlers(http_get_handler=http_get_handler) protocol.client.reset_goal_state() urls = protocol.get_tracked_urls() self.assertEqual(len(urls), 4, "Invalid number of requests: [{0}]".format(urls)) self.assertTrue(self.is_in_vm_artifacts_profile_request(urls[0]), "The first request should have been over the direct channel") self.assertTrue(self.is_host_plugin_extension_artifact_request(urls[1]), "The second request should have been over the host channel") self.assertTrue(self.is_goal_state_request(urls[2]), "The goal state should have been refreshed before retrying the host channel") self.assertTrue(self.is_host_plugin_extension_artifact_request(urls[3]), "The retry request should have been over the host channel") self.assertFalse(HostPluginProtocol.is_default_channel, "The default channel should not have changed") @staticmethod def _set_and_fail_helper_channel_functions(fail_direct=False, fail_host=False): def direct_func(*_): direct_func.counter += 1 if direct_func.fail: raise Exception("Direct channel failed") return "direct" def host_func(*_): host_func.counter += 1 if host_func.fail: raise Exception("Host channel failed") return "host" direct_func.counter = 0 direct_func.fail = fail_direct host_func.counter = 0 host_func.fail = fail_host return direct_func, host_func def test_download_using_appropriate_channel_should_not_invoke_secondary_when_primary_channel_succeeds(self): with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: # Scenario #1: Direct channel default HostPluginProtocol.is_default_channel = False direct_func, host_func = self._set_and_fail_helper_channel_functions() # Assert we're only calling the primary channel (direct) and that it succeeds. for iteration in range(5): ret = protocol.client._download_using_appropriate_channel(direct_func, host_func) self.assertEqual("direct", ret) self.assertEqual(iteration + 1, direct_func.counter) self.assertEqual(0, host_func.counter) self.assertFalse(HostPluginProtocol.is_default_channel) # Scenario #2: Host channel default HostPluginProtocol.is_default_channel = True direct_func, host_func = self._set_and_fail_helper_channel_functions() # Assert we're only calling the primary channel (host) and that it succeeds. for iteration in range(5): ret = protocol.client._download_using_appropriate_channel(direct_func, host_func) self.assertEqual("host", ret) self.assertEqual(0, direct_func.counter) self.assertEqual(iteration + 1, host_func.counter) self.assertTrue(HostPluginProtocol.is_default_channel) def test_download_using_appropriate_channel_should_not_change_default_channel_if_none_succeeds(self): with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: # Scenario #1: Direct channel is default HostPluginProtocol.is_default_channel = False direct_func, host_func = self._set_and_fail_helper_channel_functions(fail_direct=True, fail_host=True) # Assert we keep trying both channels, but the default channel doesn't change for iteration in range(5): with self.assertRaises(HttpError): protocol.client._download_using_appropriate_channel(direct_func, host_func) self.assertEqual(iteration + 1, direct_func.counter) self.assertEqual(iteration + 1, host_func.counter) self.assertFalse(HostPluginProtocol.is_default_channel) # Scenario #2: Host channel is default HostPluginProtocol.is_default_channel = True direct_func, host_func = self._set_and_fail_helper_channel_functions(fail_direct=True, fail_host=True) # Assert we keep trying both channels, but the default channel doesn't change for iteration in range(5): with self.assertRaises(HttpError): protocol.client._download_using_appropriate_channel(direct_func, host_func) self.assertEqual(iteration + 1, direct_func.counter) self.assertEqual(iteration + 1, host_func.counter) self.assertTrue(HostPluginProtocol.is_default_channel) def test_download_using_appropriate_channel_should_change_default_channel_when_secondary_succeeds(self): with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: # Scenario #1: Direct channel is default HostPluginProtocol.is_default_channel = False direct_func, host_func = self._set_and_fail_helper_channel_functions(fail_direct=True, fail_host=False) # Assert we've called both channels and the default channel changed ret = protocol.client._download_using_appropriate_channel(direct_func, host_func) self.assertEqual("host", ret) self.assertEqual(1, direct_func.counter) self.assertEqual(1, host_func.counter) self.assertTrue(HostPluginProtocol.is_default_channel) # If host keeps succeeding, assert we keep calling only that channel and not changing the default. for iteration in range(5): ret = protocol.client._download_using_appropriate_channel(direct_func, host_func) self.assertEqual("host", ret) self.assertEqual(1, direct_func.counter) self.assertEqual(1 + iteration + 1, host_func.counter) self.assertTrue(HostPluginProtocol.is_default_channel) # Scenario #2: Host channel is default HostPluginProtocol.is_default_channel = True direct_func, host_func = self._set_and_fail_helper_channel_functions(fail_direct=False, fail_host=True) # Assert we've called both channels and the default channel changed ret = protocol.client._download_using_appropriate_channel(direct_func, host_func) self.assertEqual("direct", ret) self.assertEqual(1, direct_func.counter) self.assertEqual(1, host_func.counter) self.assertFalse(HostPluginProtocol.is_default_channel) # If direct keeps succeeding, assert we keep calling only that channel and not changing the default. for iteration in range(5): ret = protocol.client._download_using_appropriate_channel(direct_func, host_func) self.assertEqual("direct", ret) self.assertEqual(1 + iteration + 1, direct_func.counter) self.assertEqual(1, host_func.counter) self.assertFalse(HostPluginProtocol.is_default_channel) class UpdateGoalStateTestCase(HttpRequestPredicates, AgentTestCase): """ Tests for WireClient.update_goal_state() and WireClient.reset_goal_state() """ def test_it_should_update_the_goal_state_and_the_host_plugin_when_the_incarnation_changes(self): with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: protocol.client.get_host_plugin() # if the incarnation changes the behavior is the same for forced and non-forced updates for forced in [True, False]: protocol.mock_wire_data.reload() # start each iteration of the test with fresh mock data # # Update the mock data with random values; include at least one field from each of the components # in the goal state to ensure the entire state was updated. Note that numeric entities, e.g. incarnation, are # actually represented as strings in the goal state. # # Note that the shared config is not parsed by the agent, so we modify the XML data directly. Also, the # certificates are encrypted and it is hard to update a single field; instead, we update the entire list with # empty. # new_incarnation = str(uuid.uuid4()) new_container_id = str(uuid.uuid4()) new_role_config_name = str(uuid.uuid4()) new_hosting_env_deployment_name = str(uuid.uuid4()) new_shared_conf = WireProtocolData.replace_xml_attribute_value(protocol.mock_wire_data.shared_config, "Deployment", "name", str(uuid.uuid4())) new_sequence_number = 12345 if 'Pkcs7BlobWithPfxContents' not in protocol.mock_wire_data.certs: raise Exception('This test requires a non-empty certificate list') protocol.mock_wire_data.set_incarnation(new_incarnation) protocol.mock_wire_data.set_container_id(new_container_id) protocol.mock_wire_data.set_role_config_name(new_role_config_name) protocol.mock_wire_data.set_hosting_env_deployment_name(new_hosting_env_deployment_name) protocol.mock_wire_data.shared_config = new_shared_conf protocol.mock_wire_data.set_extensions_config_sequence_number(new_sequence_number) protocol.mock_wire_data.certs = r''' 2012-11-30 12 CertificatesNonPfxPackage NotPFXData ''' if forced: protocol.client.reset_goal_state() else: protocol.client.update_goal_state() sequence_number = protocol.get_goal_state().extensions_goal_state.extensions[0].settings[0].sequenceNumber self.assertEqual(protocol.client.get_goal_state().incarnation, new_incarnation) self.assertEqual(protocol.client.get_hosting_env().deployment_name, new_hosting_env_deployment_name) self.assertEqual(protocol.client.get_shared_conf().xml_text, new_shared_conf) self.assertEqual(sequence_number, new_sequence_number) self.assertEqual(len(protocol.client.get_certs().cert_list.certificates), 0) self.assertEqual(protocol.client.get_host_plugin().container_id, new_container_id) self.assertEqual(protocol.client.get_host_plugin().role_config_name, new_role_config_name) def test_non_forced_update_should_not_update_the_goal_state_but_should_update_the_host_plugin_when_the_incarnation_does_not_change(self): with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: protocol.client.get_host_plugin() # The container id, role config name and shared config can change without the incarnation changing; capture the initial # goal state and then change those fields. container_id = protocol.client.get_goal_state().container_id role_config_name = protocol.client.get_goal_state().role_config_name new_container_id = str(uuid.uuid4()) new_role_config_name = str(uuid.uuid4()) protocol.mock_wire_data.set_container_id(new_container_id) protocol.mock_wire_data.set_role_config_name(new_role_config_name) protocol.mock_wire_data.shared_config = WireProtocolData.replace_xml_attribute_value( protocol.mock_wire_data.shared_config, "Deployment", "name", str(uuid.uuid4())) protocol.client.update_goal_state() self.assertEqual(protocol.client.get_goal_state().container_id, container_id) self.assertEqual(protocol.client.get_goal_state().role_config_name, role_config_name) self.assertEqual(protocol.client.get_host_plugin().container_id, new_container_id) self.assertEqual(protocol.client.get_host_plugin().role_config_name, new_role_config_name) def test_forced_update_should_update_the_goal_state_and_the_host_plugin_when_the_incarnation_does_not_change(self): with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: protocol.client.get_host_plugin() # The container id, role config name and shared config can change without the incarnation changing incarnation = protocol.client.get_goal_state().incarnation new_container_id = str(uuid.uuid4()) new_role_config_name = str(uuid.uuid4()) new_shared_conf = WireProtocolData.replace_xml_attribute_value( protocol.mock_wire_data.shared_config, "Deployment", "name", str(uuid.uuid4())) protocol.mock_wire_data.set_container_id(new_container_id) protocol.mock_wire_data.set_role_config_name(new_role_config_name) protocol.mock_wire_data.shared_config = new_shared_conf protocol.client.reset_goal_state() self.assertEqual(protocol.client.get_goal_state().incarnation, incarnation) self.assertEqual(protocol.client.get_shared_conf().xml_text, new_shared_conf) self.assertEqual(protocol.client.get_host_plugin().container_id, new_container_id) self.assertEqual(protocol.client.get_host_plugin().role_config_name, new_role_config_name) def test_reset_should_init_provided_goal_state_properties(self): with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: protocol.client.reset_goal_state(goal_state_properties=GoalStateProperties.All & ~GoalStateProperties.Certificates) with self.assertRaises(ProtocolError) as context: _ = protocol.client.get_certs() expected_message = "Certificates is not in goal state properties" self.assertIn(expected_message, str(context.exception)) def test_reset_should_init_the_goal_state(self): with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: new_container_id = str(uuid.uuid4()) new_role_config_name = str(uuid.uuid4()) protocol.mock_wire_data.set_container_id(new_container_id) protocol.mock_wire_data.set_role_config_name(new_role_config_name) protocol.client.reset_goal_state() self.assertEqual(protocol.client.get_goal_state().container_id, new_container_id) self.assertEqual(protocol.client.get_goal_state().role_config_name, new_role_config_name) class UpdateHostPluginFromGoalStateTestCase(AgentTestCase): """ Tests for WireClient.update_host_plugin_from_goal_state() """ def test_it_should_update_the_host_plugin_with_or_without_incarnation_changes(self): with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: protocol.client.get_host_plugin() # the behavior should be the same whether the incarnation changes or not for incarnation_change in [True, False]: protocol.mock_wire_data.reload() # start each iteration of the test with fresh mock data new_container_id = str(uuid.uuid4()) new_role_config_name = str(uuid.uuid4()) if incarnation_change: protocol.mock_wire_data.set_incarnation(str(uuid.uuid4())) protocol.mock_wire_data.set_container_id(new_container_id) protocol.mock_wire_data.set_role_config_name(new_role_config_name) protocol.mock_wire_data.shared_config = WireProtocolData.replace_xml_attribute_value( protocol.mock_wire_data.shared_config, "Deployment", "name", str(uuid.uuid4())) protocol.client.update_host_plugin_from_goal_state() self.assertEqual(protocol.client.get_host_plugin().container_id, new_container_id) self.assertEqual(protocol.client.get_host_plugin().role_config_name, new_role_config_name) if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/test_agent.py000066400000000000000000000325611446033677600200700ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os.path from azurelinuxagent.agent import parse_args, Agent, usage, AgentCommands from azurelinuxagent.common import cgroupconfigurator, conf, logcollector from azurelinuxagent.common.cgroupapi import SystemdCgroupsApi from azurelinuxagent.common.utils import fileutil from azurelinuxagent.ga.collect_logs import CollectLogsHandler from tests.tools import AgentTestCase, data_dir, Mock, patch EXPECTED_CONFIGURATION = \ """AutoUpdate.Enabled = True AutoUpdate.GAFamily = Prod Autoupdate.Frequency = 3600 DVD.MountPoint = /mnt/cdrom/secure Debug.AgentCpuQuota = 50 Debug.AgentCpuThrottledTimeThreshold = 120 Debug.AgentMemoryQuota = 31457280 Debug.AutoUpdateHotfixFrequency = 14400 Debug.AutoUpdateNormalFrequency = 86400 Debug.CgroupCheckPeriod = 300 Debug.CgroupDisableOnProcessCheckFailure = True Debug.CgroupDisableOnQuotaCheckFailure = True Debug.CgroupLogMetrics = False Debug.CgroupMonitorExpiryTime = 2022-03-31 Debug.CgroupMonitorExtensionName = Microsoft.Azure.Monitor.AzureMonitorLinuxAgent Debug.EnableAgentMemoryUsageCheck = False Debug.EnableFastTrack = True Debug.EnableGAVersioning = False Debug.EtpCollectionPeriod = 300 Debug.FirewallRulesLogPeriod = 86400 DetectScvmmEnv = False EnableOverProvisioning = True Extension.LogDir = /var/log/azure Extensions.Enabled = True Extensions.GoalStatePeriod = 6 Extensions.InitialGoalStatePeriod = 6 HttpProxy.Host = None HttpProxy.Port = None Lib.Dir = /var/lib/waagent Logs.Collect = True Logs.CollectPeriod = 3600 Logs.Console = True Logs.Verbose = False OS.AllowHTTP = False OS.CheckRdmaDriver = False OS.EnableFIPS = True OS.EnableFirewall = False OS.EnableFirewallPeriod = 300 OS.EnableRDMA = False OS.HomeDir = /home OS.MonitorDhcpClientRestartPeriod = 30 OS.OpensslPath = /usr/bin/openssl OS.PasswordPath = /etc/shadow OS.RemovePersistentNetRulesPeriod = 30 OS.RootDeviceScsiTimeout = 300 OS.RootDeviceScsiTimeoutPeriod = 30 OS.SshClientAliveInterval = 42 OS.SshDir = /notareal/path OS.SudoersDir = /etc/sudoers.d OS.UpdateRdmaDriver = False Pid.File = /var/run/waagent.pid Provisioning.Agent = auto Provisioning.AllowResetSysUser = False Provisioning.DecodeCustomData = False Provisioning.DeleteRootPassword = True Provisioning.ExecuteCustomData = False Provisioning.MonitorHostName = True Provisioning.MonitorHostNamePeriod = 30 Provisioning.PasswordCryptId = 6 Provisioning.PasswordCryptSaltLength = 10 Provisioning.RegenerateSshHostKeyPair = True Provisioning.SshHostKeyPairType = rsa ResourceDisk.EnableSwap = False ResourceDisk.EnableSwapEncryption = False ResourceDisk.Filesystem = ext4 ResourceDisk.Format = True ResourceDisk.MountOptions = None ResourceDisk.MountPoint = /mnt/resource ResourceDisk.SwapSizeMB = 0""".split('\n') class TestAgent(AgentTestCase): def test_accepts_configuration_path(self): conf_path = os.path.join(data_dir, "test_waagent.conf") c, f, v, d, cfp, lcm, _ = parse_args(["-configuration-path:" + conf_path]) # pylint: disable=unused-variable self.assertEqual(cfp, conf_path) @patch("os.path.exists", return_value=True) def test_checks_configuration_path(self, mock_exists): conf_path = "/foo/bar-baz/something.conf" c, f, v, d, cfp, lcm, _ = parse_args(["-configuration-path:"+conf_path]) # pylint: disable=unused-variable self.assertEqual(cfp, conf_path) self.assertEqual(mock_exists.call_count, 1) @patch("sys.stderr") @patch("os.path.exists", return_value=False) @patch("sys.exit", side_effect=Exception) def test_rejects_missing_configuration_path(self, mock_exit, mock_exists, mock_stderr): # pylint: disable=unused-argument try: c, f, v, d, cfp, lcm, _ = parse_args(["-configuration-path:/foo/bar.conf"]) # pylint: disable=unused-variable except Exception: self.assertEqual(mock_exit.call_count, 1) def test_configuration_path_defaults_to_none(self): c, f, v, d, cfp, lcm, _ = parse_args([]) # pylint: disable=unused-variable self.assertEqual(cfp, None) def test_agent_accepts_configuration_path(self): Agent(False, conf_file_path=os.path.join(data_dir, "test_waagent.conf")) self.assertTrue(conf.get_fips_enabled()) @patch("azurelinuxagent.common.conf.load_conf_from_file") def test_agent_uses_default_configuration_path(self, mock_load): Agent(False) mock_load.assert_called_once_with("/etc/waagent.conf") @patch("azurelinuxagent.daemon.get_daemon_handler") @patch("azurelinuxagent.common.conf.load_conf_from_file") def test_agent_does_not_pass_configuration_path(self, mock_load, mock_handler): mock_daemon = Mock() mock_daemon.run = Mock() mock_handler.return_value = mock_daemon agent = Agent(False) agent.daemon() mock_daemon.run.assert_called_once_with(child_args=None) self.assertEqual(1, mock_load.call_count) @patch("azurelinuxagent.daemon.get_daemon_handler") @patch("azurelinuxagent.common.conf.load_conf_from_file") def test_agent_passes_configuration_path(self, mock_load, mock_handler): mock_daemon = Mock() mock_daemon.run = Mock() mock_handler.return_value = mock_daemon agent = Agent(False, conf_file_path="/foo/bar.conf") agent.daemon() mock_daemon.run.assert_called_once_with(child_args="-configuration-path:/foo/bar.conf") self.assertEqual(1, mock_load.call_count) @patch("azurelinuxagent.common.conf.get_ext_log_dir") def test_agent_ensures_extension_log_directory(self, mock_dir): ext_log_dir = os.path.join(self.tmp_dir, "FauxLogDir") mock_dir.return_value = ext_log_dir self.assertFalse(os.path.isdir(ext_log_dir)) agent = Agent(False, # pylint: disable=unused-variable conf_file_path=os.path.join(data_dir, "test_waagent.conf")) self.assertTrue(os.path.isdir(ext_log_dir)) @patch("azurelinuxagent.common.logger.error") @patch("azurelinuxagent.common.conf.get_ext_log_dir") def test_agent_logs_if_extension_log_directory_is_a_file(self, mock_dir, mock_log): ext_log_dir = os.path.join(self.tmp_dir, "FauxLogDir") mock_dir.return_value = ext_log_dir fileutil.write_file(ext_log_dir, "Foo") self.assertTrue(os.path.isfile(ext_log_dir)) self.assertFalse(os.path.isdir(ext_log_dir)) agent = Agent(False, # pylint: disable=unused-variable conf_file_path=os.path.join(data_dir, "test_waagent.conf")) self.assertTrue(os.path.isfile(ext_log_dir)) self.assertFalse(os.path.isdir(ext_log_dir)) self.assertEqual(1, mock_log.call_count) def test_agent_get_configuration(self): Agent(False, conf_file_path=os.path.join(data_dir, "test_waagent.conf")) actual_configuration = [] configuration = conf.get_configuration() for k in sorted(configuration.keys()): actual_configuration.append("{0} = {1}".format(k, configuration[k])) self.assertListEqual(EXPECTED_CONFIGURATION, actual_configuration) def test_checks_log_collector_mode(self): # Specify full mode c, f, v, d, cfp, lcm, _ = parse_args(["-collect-logs", "-full"]) # pylint: disable=unused-variable self.assertEqual(c, "collect-logs") self.assertEqual(lcm, True) # Defaults to None if mode not specified c, f, v, d, cfp, lcm, _ = parse_args(["-collect-logs"]) # pylint: disable=unused-variable self.assertEqual(c, "collect-logs") self.assertEqual(lcm, False) @patch("sys.stderr") @patch("sys.exit", side_effect=Exception) def test_rejects_invalid_log_collector_mode(self, mock_exit, mock_stderr): # pylint: disable=unused-argument try: c, f, v, d, cfp, lcm, _ = parse_args(["-collect-logs", "-notvalid"]) # pylint: disable=unused-variable except Exception: self.assertEqual(mock_exit.call_count, 1) @patch("os.path.exists", return_value=True) @patch("azurelinuxagent.agent.LogCollector") def test_calls_collect_logs_with_proper_mode(self, mock_log_collector, *args): # pylint: disable=unused-argument agent = Agent(False, conf_file_path=os.path.join(data_dir, "test_waagent.conf")) mock_log_collector.run = Mock() agent.collect_logs(is_full_mode=True) full_mode = mock_log_collector.call_args_list[0][0][0] self.assertTrue(full_mode) agent.collect_logs(is_full_mode=False) full_mode = mock_log_collector.call_args_list[1][0][0] self.assertFalse(full_mode) @patch("azurelinuxagent.agent.LogCollector") def test_calls_collect_logs_on_valid_cgroups(self, mock_log_collector): try: CollectLogsHandler.enable_cgroups_validation() mock_log_collector.run = Mock() def mock_cgroup_paths(*args, **kwargs): if args and args[0] == "self": relative_path = "{0}/{1}".format(cgroupconfigurator.LOGCOLLECTOR_SLICE, logcollector.CGROUPS_UNIT) return (cgroupconfigurator.LOGCOLLECTOR_SLICE, relative_path) return SystemdCgroupsApi.get_process_cgroup_relative_paths(*args, **kwargs) with patch("azurelinuxagent.agent.SystemdCgroupsApi.get_process_cgroup_paths", side_effect=mock_cgroup_paths): agent = Agent(False, conf_file_path=os.path.join(data_dir, "test_waagent.conf")) agent.collect_logs(is_full_mode=True) mock_log_collector.assert_called_once() finally: CollectLogsHandler.disable_cgroups_validation() @patch("azurelinuxagent.agent.LogCollector") def test_doesnt_call_collect_logs_on_invalid_cgroups(self, mock_log_collector): try: CollectLogsHandler.enable_cgroups_validation() mock_log_collector.run = Mock() def mock_cgroup_paths(*args, **kwargs): if args and args[0] == "self": return ("NOT_THE_CORRECT_PATH", "NOT_THE_CORRECT_PATH") return SystemdCgroupsApi.get_process_cgroup_relative_paths(*args, **kwargs) with patch("azurelinuxagent.agent.SystemdCgroupsApi.get_process_cgroup_paths", side_effect=mock_cgroup_paths): agent = Agent(False, conf_file_path=os.path.join(data_dir, "test_waagent.conf")) exit_error = RuntimeError("Exiting") with patch("sys.exit", return_value=exit_error) as mock_exit: try: agent.collect_logs(is_full_mode=True) except RuntimeError as re: mock_exit.assert_called_once_with(logcollector.INVALID_CGROUPS_ERRCODE) self.assertEqual(exit_error, re) finally: CollectLogsHandler.disable_cgroups_validation() def test_it_should_parse_setup_firewall_properly(self): test_firewall_meta = { "dst_ip": "1.2.3.4", "uid": "9999", "wait": "-w" } cmd, _, _, _, _, _, firewall_metadata = parse_args( ["-{0}".format(AgentCommands.SetupFirewall), "-dst_ip=1.2.3.4", "-uid=9999", "-w"]) self.assertEqual(cmd, AgentCommands.SetupFirewall) self.assertEqual(firewall_metadata, test_firewall_meta) # Defaults to None if command is different test_firewall_meta = { "dst_ip": None, "uid": None, "wait": "" } cmd, _, _, _, _, _, firewall_metadata = parse_args(["-{0}".format(AgentCommands.Help)]) self.assertEqual(cmd, AgentCommands.Help) self.assertEqual(test_firewall_meta, firewall_metadata) def test_it_should_ignore_empty_arguments(self): test_firewall_meta = { "dst_ip": "1.2.3.4", "uid": "9999", "wait": "" } cmd, _, _, _, _, _, firewall_metadata = parse_args( ["-{0}".format(AgentCommands.SetupFirewall), "-dst_ip=1.2.3.4", "-uid=9999", ""]) self.assertEqual(cmd, AgentCommands.SetupFirewall) self.assertEqual(firewall_metadata, test_firewall_meta) def test_agent_usage_message(self): message = usage() # Python 2.6 does not have assertIn() self.assertTrue("-verbose" in message) self.assertTrue("-force" in message) self.assertTrue("-help" in message) self.assertTrue("-configuration-path" in message) self.assertTrue("-deprovision" in message) self.assertTrue("-register-service" in message) self.assertTrue("-version" in message) self.assertTrue("-daemon" in message) self.assertTrue("-start" in message) self.assertTrue("-run-exthandlers" in message) self.assertTrue("-show-configuration" in message) self.assertTrue("-collect-logs" in message) # sanity check self.assertFalse("-not-a-valid-option" in message) WALinuxAgent-2.9.1.1/tests/tools.py000066400000000000000000000510761446033677600170750ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # """ Define util functions for unit test """ import difflib import os import pprint import re import shutil import stat import sys import tempfile import time import unittest from functools import wraps from threading import currentThread import azurelinuxagent.common.conf as conf import azurelinuxagent.common.event as event import azurelinuxagent.common.logger as logger from azurelinuxagent.common.future import range # pylint: disable=redefined-builtin from azurelinuxagent.common.utils import fileutil from azurelinuxagent.common.version import PY_VERSION_MAJOR try: from unittest.mock import Mock, patch, MagicMock, ANY, DEFAULT, call, PropertyMock # pylint: disable=unused-import # Import mock module for Python2 and Python3 from bin.waagent2 import Agent # pylint: disable=unused-import except ImportError: from mock import Mock, patch, MagicMock, ANY, DEFAULT, call, PropertyMock test_dir = os.path.dirname(os.path.abspath(__file__)) data_dir = os.path.join(test_dir, "data") debug = False if os.environ.get('DEBUG') == '1': debug = True # Enable verbose logger to stdout if debug: logger.add_logger_appender(logger.AppenderType.STDOUT, logger.LogLevel.VERBOSE) _MAX_LENGTH = 120 _MAX_LENGTH_SAFE_REPR = 80 # Mock sleep to reduce test execution time _SLEEP = time.sleep def mock_sleep(sec=0.01): """ Mocks the time.sleep method to reduce unit test time :param sec: Time to replace the sleep call with, default = 0.01sec """ _SLEEP(sec) def safe_repr(obj, short=False): try: result = repr(obj) except Exception: result = object.__repr__(obj) if not short or len(result) < _MAX_LENGTH: return result return result[:_MAX_LENGTH_SAFE_REPR] + ' [truncated]...' def skip_if_predicate_false(predicate, message): if not predicate(): if hasattr(unittest, "skip"): return unittest.skip(message) return lambda func: None return lambda func: func def skip_if_predicate_true(predicate, message): if predicate(): if hasattr(unittest, "skip"): return unittest.skip(message) return lambda func: None return lambda func: func def _safe_repr(obj, short=False): """ Copied from Python 3.x """ try: result = repr(obj) except Exception: result = object.__repr__(obj) if not short or len(result) < _MAX_LENGTH: return result return result[:_MAX_LENGTH] + ' [truncated]...' def i_am_root(): return os.geteuid() == 0 def is_python_version_26(): return sys.version_info[0] == 2 and sys.version_info[1] == 6 def is_python_version_34(): return sys.version_info[0] == 3 and sys.version_info[1] == 4 def is_python_version_26_or_34(): return is_python_version_26() or is_python_version_34() class AgentTestCase(unittest.TestCase): @classmethod def setUpClass(cls): # Setup newer unittest assertions missing in prior versions of Python if not hasattr(cls, "assertRegex"): cls.assertRegex = cls.assertRegexpMatches if hasattr(cls, "assertRegexpMatches") else cls.emulate_assertRegexpMatches if not hasattr(cls, "assertNotRegex"): cls.assertNotRegex = cls.assertNotRegexpMatches if hasattr(cls, "assertNotRegexpMatches") else cls.emulate_assertNotRegexpMatches # pylint: disable=no-member if not hasattr(cls, "assertIn"): cls.assertIn = cls.emulate_assertIn if not hasattr(cls, "assertNotIn"): cls.assertNotIn = cls.emulate_assertNotIn if not hasattr(cls, "assertGreater"): cls.assertGreater = cls.emulate_assertGreater if not hasattr(cls, "assertGreaterEqual"): cls.assertGreaterEqual = cls.emulate_assertGreaterEqual if not hasattr(cls, "assertLess"): cls.assertLess = cls.emulate_assertLess if not hasattr(cls, "assertLessEqual"): cls.assertLessEqual = cls.emulate_assertLessEqual if not hasattr(cls, "assertIsNone"): cls.assertIsNone = cls.emulate_assertIsNone if not hasattr(cls, "assertIsNotNone"): cls.assertIsNotNone = cls.emulate_assertIsNotNone if hasattr(cls, "assertRaisesRegexp"): cls.assertRaisesRegex = cls.assertRaisesRegexp if not hasattr(cls, "assertRaisesRegex"): cls.assertRaisesRegex = cls.emulate_raises_regex if not hasattr(cls, "assertListEqual"): cls.assertListEqual = cls.emulate_assertListEqual if not hasattr(cls, "assertIsInstance"): cls.assertIsInstance = cls.emulate_assertIsInstance if sys.version_info < (2, 7): # assertRaises does not implement a context manager in 2.6; override it with emulate_assertRaises but # keep a pointer to the original implementation to use when a context manager is not requested. cls.original_assertRaises = unittest.TestCase.assertRaises cls.assertRaises = cls.emulate_assertRaises cls.assertDictEqual = cls.emulate_assertDictEqual @classmethod def tearDownClass(cls): pass def setUp(self): prefix = "{0}_".format(self.__class__.__name__) self.tmp_dir = tempfile.mkdtemp(prefix=prefix) self.test_file = 'test_file' conf.get_autoupdate_enabled = Mock(return_value=True) conf.get_lib_dir = Mock(return_value=self.tmp_dir) ext_log_dir = os.path.join(self.tmp_dir, "azure") conf.get_ext_log_dir = Mock(return_value=ext_log_dir) conf.get_agent_pid_file_path = Mock(return_value=os.path.join(self.tmp_dir, "waagent.pid")) event.init_event_status(self.tmp_dir) event.init_event_logger(self.tmp_dir) def tearDown(self): if not debug and self.tmp_dir is not None: shutil.rmtree(self.tmp_dir) def emulate_assertIn(self, a, b, msg=None): if a not in b: msg = msg if msg is not None else "{0} not found in {1}".format(_safe_repr(a), _safe_repr(b)) self.fail(msg) def emulate_assertNotIn(self, a, b, msg=None): if a in b: msg = msg if msg is not None else "{0} unexpectedly found in {1}".format(_safe_repr(a), _safe_repr(b)) self.fail(msg) def emulate_assertGreater(self, a, b, msg=None): if not a > b: msg = msg if msg is not None else '{0} not greater than {1}'.format(_safe_repr(a), _safe_repr(b)) self.fail(msg) def emulate_assertGreaterEqual(self, a, b, msg=None): if not a >= b: msg = msg if msg is not None else '{0} not greater or equal to {1}'.format(_safe_repr(a), _safe_repr(b)) self.fail(msg) def emulate_assertLess(self, a, b, msg=None): if not a < b: msg = msg if msg is not None else '{0} not less than {1}'.format(_safe_repr(a), _safe_repr(b)) self.fail(msg) def emulate_assertLessEqual(self, a, b, msg=None): if not a <= b: msg = msg if msg is not None else '{0} not less or equal to {1}'.format(_safe_repr(a), _safe_repr(b)) self.fail(msg) def emulate_assertIsNone(self, x, msg=None): if x is not None: msg = msg if msg is not None else '{0} is not None'.format(_safe_repr(x)) self.fail(msg) def emulate_assertIsNotNone(self, x, msg=None): if x is None: msg = msg if msg is not None else '{0} is None'.format(_safe_repr(x)) self.fail(msg) def emulate_assertRegexpMatches(self, text, regexp, msg=None): if re.search(regexp, text) is not None: return msg = msg if msg is not None else "'{0}' does not match '{1}'.".format(text, regexp) self.fail(msg) def emulate_assertNotRegexpMatches(self, text, regexp, msg=None): if re.search(regexp, text, flags=1) is None: return msg = msg if msg is not None else "'{0}' should not match '{1}'.".format(text, regexp) self.fail(msg) class _AssertRaisesContextManager(object): def __init__(self, expected_exception_type, test_case, match_regex=None, regex_flags=0): self._expected_exception_type = expected_exception_type self._test_case = test_case self._match_regex = match_regex self._regex_flags = regex_flags self.exception = None def __enter__(self): return self @staticmethod def _get_type_name(t): return t.__name__ if hasattr(t, "__name__") else str(t) def __exit__(self, exception_type, exception, *_): if exception_type is None: expected = AgentTestCase._AssertRaisesContextManager._get_type_name(self._expected_exception_type) self._test_case.fail("Did not raise an exception; expected '{0}'".format(expected)) if not issubclass(exception_type, self._expected_exception_type): raised = AgentTestCase._AssertRaisesContextManager._get_type_name(exception_type) expected = AgentTestCase._AssertRaisesContextManager._get_type_name(self._expected_exception_type) self._test_case.fail("Raised '{0}', but expected '{1}'".format(raised, expected)) if self._match_regex is not None: exception_text = str(exception) if re.search(self._match_regex, exception_text, flags=self._regex_flags) is None: self._test_case.fail("The exception did not match the expected pattern. Expected: r'{0}' Got: '{1}'".format(self._match_regex, exception_text)) self.exception = exception return True def emulate_assertRaises(self, exception_type, function=None, *args, **kwargs): # pylint: disable=keyword-arg-before-vararg # return a context manager only when function is not provided; otherwise use the original assertRaises if function is None: return AgentTestCase._AssertRaisesContextManager(exception_type, self) self.original_assertRaises(exception_type, function, *args, **kwargs) return None def emulate_raises_regex(self, exception_type, regex, function, *args, **kwargs): try: function(*args, **kwargs) except Exception as e: if re.search(regex, str(e), flags=1) is not None: return else: self.fail("Expected exception {0} matching {1}. Actual: {2}".format( exception_type, regex, str(e))) self.fail("No exception was thrown. Expected exception {0} matching {1}".format(exception_type, regex)) def assertRaisesRegexCM(self, exception_type, regex, flags=0): """ Similar to assertRaisesRegex, but returns a context manager (mostly needed for Python 2.*, which does not have a assertRaisesRegex) """ return AgentTestCase._AssertRaisesContextManager(exception_type, self, match_regex=regex, regex_flags=flags) def emulate_assertDictEqual(self, first, second, msg=None): def fail(message): self.fail(self._formatMessage(msg, message)) for k in first.keys(): if k not in second: fail("'{0}' is missing from second".format(k)) if first[k] != second[k]: fail("'{0}' != '{1}' (key: {2})".format(first[k], second[k], k)) for k in second.keys(): if k not in first: fail("'{0}' is missing from first".format(k)) def emulate_assertListEqual(self, seq1, seq2, msg=None, seq_type=None): """An equality assertion for ordered sequences (like lists and tuples). For the purposes of this function, a valid ordered sequence type is one which can be indexed, has a length, and has an equality operator. Args: seq1: The first sequence to compare. seq2: The second sequence to compare. seq_type: The expected datatype of the sequences, or None if no datatype should be enforced. msg: Optional message to use on failure instead of a list of differences. """ if seq_type is not None: seq_type_name = seq_type.__name__ if not isinstance(seq1, seq_type): raise self.failureException('First sequence is not a %s: %s' % (seq_type_name, safe_repr(seq1))) if not isinstance(seq2, seq_type): raise self.failureException('Second sequence is not a %s: %s' % (seq_type_name, safe_repr(seq2))) else: seq_type_name = "sequence" differing = None try: len1 = len(seq1) except (TypeError, NotImplementedError): differing = 'First %s has no length. Non-sequence?' % ( seq_type_name) if differing is None: try: len2 = len(seq2) except (TypeError, NotImplementedError): differing = 'Second %s has no length. Non-sequence?' % ( seq_type_name) if differing is None: if seq1 == seq2: return seq1_repr = safe_repr(seq1) seq2_repr = safe_repr(seq2) if len(seq1_repr) > 30: seq1_repr = seq1_repr[:30] + '...' if len(seq2_repr) > 30: seq2_repr = seq2_repr[:30] + '...' elements = (seq_type_name.capitalize(), seq1_repr, seq2_repr) differing = '%ss differ: %s != %s\n' % elements for i in range(min(len1, len2)): try: item1 = seq1[i] except (TypeError, IndexError, NotImplementedError): differing += ('\nUnable to index element %d of first %s\n' % (i, seq_type_name)) break try: item2 = seq2[i] except (TypeError, IndexError, NotImplementedError): differing += ('\nUnable to index element %d of second %s\n' % (i, seq_type_name)) break if item1 != item2: differing += ('\nFirst differing element %d:\n%s\n%s\n' % (i, safe_repr(item1), safe_repr(item2))) break else: if (len1 == len2 and seq_type is None and type(seq1) != type(seq2)): # The sequences are the same, but have differing types. return if len1 > len2: differing += ('\nFirst %s contains %d additional ' 'elements.\n' % (seq_type_name, len1 - len2)) try: differing += ('First extra element %d:\n%s\n' % (len2, safe_repr(seq1[len2]))) except (TypeError, IndexError, NotImplementedError): differing += ('Unable to index element %d ' 'of first %s\n' % (len2, seq_type_name)) elif len1 < len2: differing += ('\nSecond %s contains %d additional ' 'elements.\n' % (seq_type_name, len2 - len1)) try: differing += ('First extra element %d:\n%s\n' % (len1, safe_repr(seq2[len1]))) except (TypeError, IndexError, NotImplementedError): differing += ('Unable to index element %d ' 'of second %s\n' % (len1, seq_type_name)) standardMsg = differing diffMsg = '\n' + '\n'.join( difflib.ndiff(pprint.pformat(seq1).splitlines(), pprint.pformat(seq2).splitlines())) standardMsg = self._truncateMessage(standardMsg, diffMsg) msg = self._formatMessage(msg, standardMsg) self.fail(msg) def emulate_assertIsInstance(self, obj, object_type, msg=None): if not isinstance(obj, object_type): msg = msg if msg is not None else '{0} is not an instance of {1}'.format(_safe_repr(obj), _safe_repr(object_type)) self.fail(msg) @staticmethod def _create_files(tmp_dir, prefix, suffix, count, with_sleep=0): for i in range(count): f = os.path.join(tmp_dir, '.'.join((prefix, str(i), suffix))) fileutil.write_file(f, "faux content") time.sleep(with_sleep) @staticmethod def create_script(script_file, contents): """ Creates an executable script with the given contents. If file ends with ".py", it creates a Python3 script, otherwise it creates a bash script. """ with open(script_file, "w") as script: if script_file.endswith(".py"): script.write("#!/usr/bin/env python3\n") else: script.write("#!/usr/bin/env bash\n") script.write(contents) os.chmod(script_file, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) class AgentTestCaseWithGetVmSizeMock(AgentTestCase): def setUp(self): self._get_vm_size_patch = patch('azurelinuxagent.ga.update.UpdateHandler._get_vm_size', return_value="unknown") self._get_vm_size_patch.start() super(AgentTestCaseWithGetVmSizeMock, self).setUp() def tearDown(self): if self._get_vm_size_patch: self._get_vm_size_patch.stop() super(AgentTestCaseWithGetVmSizeMock, self).tearDown() def load_data(name): """Load test data""" path = os.path.join(data_dir, name) with open(path, "r") as data_file: return data_file.read() def load_bin_data(name, directory=None): """Load test bin data""" if directory is None: directory = data_dir path = os.path.join(directory, name) with open(path, "rb") as data_file: return data_file.read() supported_distro = [ ["ubuntu", "12.04", ""], ["ubuntu", "14.04", ""], ["ubuntu", "14.10", ""], ["ubuntu", "15.10", ""], ["ubuntu", "15.10", "Snappy Ubuntu Core"], ["coreos", "", ""], ["flatcar", "", ""], ["suse", "12", "SUSE Linux Enterprise Server"], ["suse", "13.2", "openSUSE"], ["suse", "11", "SUSE Linux Enterprise Server"], ["suse", "13.1", "openSUSE"], ["debian", "6.0", ""], ["redhat", "6.5", ""], ["redhat", "7.0", ""], ] def open_patch(): open_name = '__builtin__.open' if PY_VERSION_MAJOR == 3: open_name = 'builtins.open' return open_name def patch_builtin(target, *args, **kwargs): prefix = 'builtins' if PY_VERSION_MAJOR >= 3 else '__builtin__' return patch("{0}.{1}".format(prefix, target), *args, **kwargs) def distros(distro_name=".*", distro_version=".*", distro_full_name=".*"): """Run test on multiple distros""" def decorator(test_method): @wraps(test_method) def wrapper(self, *args, **kwargs): for distro in supported_distro: if re.match(distro_name, distro[0]) and \ re.match(distro_version, distro[1]) and \ re.match(distro_full_name, distro[2]): if debug: logger.info("Run {0} on {1}", test_method.__name__, distro) new_args = [] new_args.extend(args) new_args.extend(distro) test_method(self, *new_args, **kwargs) # Call tearDown and setUp to create separated environment # for distro testing self.tearDown() self.setUp() return wrapper return decorator def clear_singleton_instances(cls): # Adding this lock to avoid any race conditions with cls._lock: obj_name = "%s__%s" % (cls.__name__, currentThread().getName()) # Object Name = className__threadName if obj_name in cls._instances: del cls._instances[obj_name] WALinuxAgent-2.9.1.1/tests/utils/000077500000000000000000000000001446033677600165125ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests/utils/__init__.py000066400000000000000000000011651446033677600206260ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # WALinuxAgent-2.9.1.1/tests/utils/cgroups_tools.py000066400000000000000000000043721446033677600217740ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os from azurelinuxagent.common.utils import fileutil class CGroupsTools(object): @staticmethod def create_legacy_agent_cgroup(cgroups_file_system_root, controller, daemon_pid): """ Previous versions of the daemon (2.2.31-2.2.40) wrote their PID to /sys/fs/cgroup/{cpu,memory}/WALinuxAgent/WALinuxAgent; starting from version 2.2.41 we track the agent service in walinuxagent.service instead of WALinuxAgent/WALinuxAgent. This method creates a mock cgroup using the legacy path and adds the given PID to it. """ legacy_cgroup = os.path.join(cgroups_file_system_root, controller, "WALinuxAgent", "WALinuxAgent") if not os.path.exists(legacy_cgroup): os.makedirs(legacy_cgroup) fileutil.append_file(os.path.join(legacy_cgroup, "cgroup.procs"), daemon_pid + "\n") return legacy_cgroup @staticmethod def create_agent_cgroup(cgroups_file_system_root, controller, extension_handler_pid): """ Previous versions of the daemon (2.2.31-2.2.40) wrote their PID to /sys/fs/cgroup/{cpu,memory}/WALinuxAgent/WALinuxAgent; starting from version 2.2.41 we track the agent service in walinuxagent.service instead of WALinuxAgent/WALinuxAgent. This method creates a mock cgroup using the newer path and adds the given PID to it. """ new_cgroup = os.path.join(cgroups_file_system_root, controller, "walinuxagent.service") if not os.path.exists(new_cgroup): os.makedirs(new_cgroup) fileutil.append_file(os.path.join(new_cgroup, "cgroup.procs"), extension_handler_pid + "\n") return new_cgroup WALinuxAgent-2.9.1.1/tests/utils/event_logger_tools.py000066400000000000000000000052371446033677600227730ustar00rootroot00000000000000# Copyright 2020 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import platform import azurelinuxagent.common.event as event from azurelinuxagent.common.version import DISTRO_NAME, DISTRO_VERSION, DISTRO_CODE_NAME import tests.tools as tools from tests.protocol import mockwiredata from tests.protocol.mocks import mock_wire_protocol class EventLoggerTools(object): mock_imds_data = { 'location': 'uswest', 'subscriptionId': 'AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE', 'resourceGroupName': 'test-rg', 'vmId': '99999999-8888-7777-6666-555555555555', 'image_origin': 2468 } @staticmethod def initialize_event_logger(event_dir): """ Initializes the event logger using mock data for the common parameters; the goal state fields are taken from mockwiredata.DATA_FILE and the IMDS fields from mock_imds_data. """ if not os.path.exists(event_dir): os.mkdir(event_dir) event.init_event_logger(event_dir) mock_imds_info = tools.Mock() mock_imds_info.location = EventLoggerTools.mock_imds_data['location'] mock_imds_info.subscriptionId = EventLoggerTools.mock_imds_data['subscriptionId'] mock_imds_info.resourceGroupName = EventLoggerTools.mock_imds_data['resourceGroupName'] mock_imds_info.vmId = EventLoggerTools.mock_imds_data['vmId'] mock_imds_info.image_origin = EventLoggerTools.mock_imds_data['image_origin'] mock_imds_client = tools.Mock() mock_imds_client.get_compute = tools.Mock(return_value=mock_imds_info) with mock_wire_protocol(mockwiredata.DATA_FILE) as mock_protocol: with tools.patch("azurelinuxagent.common.event.get_imds_client", return_value=mock_imds_client): event.initialize_event_logger_vminfo_common_parameters(mock_protocol) @staticmethod def get_expected_os_version(): """ Returns the expected value for the OS Version in telemetry events """ return u"{0}:{1}-{2}-{3}:{4}".format(platform.system(), DISTRO_NAME, DISTRO_VERSION, DISTRO_CODE_NAME, platform.release()) WALinuxAgent-2.9.1.1/tests/utils/miscellaneous_tools.py000066400000000000000000000040161446033677600231500ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # # # Utility functions for the unit tests. # # This module is meant for simple, small tools that don't fit elsewhere. # import datetime import os import time from azurelinuxagent.common.future import ustr def format_processes(pid_list): """ Formats the given PIDs as a sequence of PIDs and their command lines """ def get_command_line(pid): try: cmdline = '/proc/{0}/cmdline'.format(pid) if os.path.exists(cmdline): with open(cmdline, "r") as cmdline_file: return "[PID: {0}] {1}".format(pid, cmdline_file.read()) except Exception: pass return "[PID: {0}] UNKNOWN".format(pid) return ustr([get_command_line(pid) for pid in pid_list]) def wait_for(predicate, timeout=10, frequency=0.01): """ Waits until the given predicate is true or the given timeout elapses. Returns the last evaluation of the predicate. Both the timeout and frequency are in seconds; the latter indicates how often the predicate is evaluated. """ def to_seconds(time_delta): return (time_delta.microseconds + (time_delta.seconds + time_delta.days * 24 * 3600) * 10 ** 6) / 10 ** 6 start_time = datetime.datetime.now() while to_seconds(datetime.datetime.now() - start_time) < timeout: if predicate(): return True time.sleep(frequency) return False WALinuxAgent-2.9.1.1/tests/utils/test_archive.py000066400000000000000000000200641446033677600215460ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the Apache License. import os import tempfile import zipfile from datetime import datetime, timedelta import azurelinuxagent.common.logger as logger from azurelinuxagent.common import conf from azurelinuxagent.common.utils import fileutil, timeutil from azurelinuxagent.common.utils.archive import GoalStateHistory, StateArchiver, _MAX_ARCHIVED_STATES, ARCHIVE_DIRECTORY_NAME from tests.tools import AgentTestCase, patch debug = False if os.environ.get('DEBUG') == '1': debug = True # Enable verbose logger to stdout if debug: logger.add_logger_appender(logger.AppenderType.STDOUT, logger.LogLevel.VERBOSE) class TestArchive(AgentTestCase): def setUp(self): super(TestArchive, self).setUp() prefix = "{0}_".format(self.__class__.__name__) self.tmp_dir = tempfile.mkdtemp(prefix=prefix) def _write_file(self, filename, contents=None): full_name = os.path.join(conf.get_lib_dir(), filename) fileutil.mkdir(os.path.dirname(full_name)) with open(full_name, 'w') as file_handler: data = contents if contents is not None else filename file_handler.write(data) return full_name @property def history_dir(self): return os.path.join(conf.get_lib_dir(), ARCHIVE_DIRECTORY_NAME) @staticmethod def _parse_archive_name(name): # Name can be a directory or a zip # '0000-00-00T00:00:00.000000_incarnation_0' # '0000-00-00T00:00:00.000000_incarnation_0.zip' timestamp_str, incarnation_ext = name.split("_incarnation_") incarnation_no_ext = os.path.splitext(incarnation_ext)[0] return timestamp_str, incarnation_no_ext def test_archive_should_zip_all_but_the_latest_goal_state_in_the_history_folder(self): test_files = [ 'GoalState.xml', 'Prod.manifest.xml', 'Prod.agentsManifest', 'Microsoft.Azure.Extensions.CustomScript.xml' ] # these directories match the pattern that StateArchiver.archive() searches for test_directories = [] for i in range(0, 3): timestamp = (datetime.utcnow() + timedelta(minutes=i)).isoformat() directory = os.path.join(self.history_dir, "{0}_incarnation_{1}".format(timestamp, i)) for current_file in test_files: self._write_file(os.path.join(directory, current_file)) test_directories.append(directory) test_subject = StateArchiver(conf.get_lib_dir()) # NOTE: StateArchiver sorts the state directories by creation time, but the test files are created too fast and the # time resolution is too coarse, so instead we mock getctime to simply return the path of the file with patch("azurelinuxagent.common.utils.archive.os.path.getctime", side_effect=lambda path: path): test_subject.archive() for directory in test_directories[0:2]: zip_file = directory + ".zip" self.assertTrue(os.path.exists(zip_file), "{0} was not archived (could not find {1})".format(directory, zip_file)) missing_file = self.assert_zip_contains(zip_file, test_files) self.assertEqual(None, missing_file, missing_file) self.assertFalse(os.path.exists(directory), "{0} was not removed after being archived ".format(directory)) self.assertTrue(os.path.exists(test_directories[2]), "{0}, the latest goal state, should not have being removed".format(test_directories[2])) def test_goal_state_history_init_should_purge_old_items(self): """ GoalStateHistory.__init__ should _purge the MAX_ARCHIVED_STATES oldest files or directories. The oldest timestamps are purged first. This test case creates a mixture of archive files and directories. It creates 5 more values than MAX_ARCHIVED_STATES to ensure that 5 archives are cleaned up. It asserts that the files and directories are properly deleted from the disk. """ count = 6 total = _MAX_ARCHIVED_STATES + count start = datetime.now() timestamps = [] for i in range(0, total): timestamp = start + timedelta(seconds=i) timestamps.append(timestamp) if i % 2 == 0: filename = os.path.join('history', "{0}_0".format(timestamp.isoformat()), 'Prod.manifest.xml') else: filename = os.path.join('history', "{0}_0.zip".format(timestamp.isoformat())) self._write_file(filename) self.assertEqual(total, len(os.listdir(self.history_dir))) # NOTE: The purge method sorts the items by creation time, but the test files are created too fast and the # time resolution is too coarse, so instead we mock getctime to simply return the path of the file with patch("azurelinuxagent.common.utils.archive.os.path.getctime", side_effect=lambda path: path): GoalStateHistory(datetime.utcnow(), 'test') archived_entries = os.listdir(self.history_dir) self.assertEqual(_MAX_ARCHIVED_STATES, len(archived_entries)) archived_entries.sort() for i in range(0, _MAX_ARCHIVED_STATES): timestamp = timestamps[i + count].isoformat() if i % 2 == 0: filename = "{0}_0".format(timestamp) else: filename = "{0}_0.zip".format(timestamp) self.assertTrue(filename in archived_entries, "'{0}' is not in the list of unpurged entires".format(filename)) def test_purge_legacy_goal_state_history(self): with patch("azurelinuxagent.common.conf.get_lib_dir", return_value=self.tmp_dir): # SharedConfig.xml is used by other components (Azsec and Singularity/HPC Infiniband); verify that we do not delete it shared_config = os.path.join(self.tmp_dir, 'SharedConfig.xml') legacy_files = [ 'GoalState.2.xml', 'VmSettings.2.json', 'Prod.2.manifest.xml', 'ExtensionsConfig.2.xml', 'Microsoft.Azure.Extensions.CustomScript.1.xml', 'HostingEnvironmentConfig.xml', 'RemoteAccess.xml', 'waagent_status.1.json' ] legacy_files = [os.path.join(self.tmp_dir, f) for f in legacy_files] self._write_file(shared_config) for f in legacy_files: self._write_file(f) StateArchiver.purge_legacy_goal_state_history() self.assertTrue(os.path.exists(shared_config), "{0} should not have been removed".format(shared_config)) for f in legacy_files: self.assertFalse(os.path.exists(f), "Legacy file {0} was not removed".format(f)) @staticmethod def parse_isoformat(timestamp_str): return datetime.strptime(timestamp_str, '%Y-%m-%dT%H:%M:%S.%f') @staticmethod def assert_is_iso8601(timestamp_str): try: TestArchive.parse_isoformat(timestamp_str) except: raise AssertionError("the value '{0}' is not an ISO8601 formatted timestamp".format(timestamp_str)) def assert_datetime_close_to(self, time1, time2, within): if time1 <= time2: diff = time2 - time1 else: diff = time1 - time2 secs = timeutil.total_seconds(within - diff) if secs < 0: self.fail("the timestamps are outside of the tolerance of by {0} seconds".format(secs)) @staticmethod def assert_zip_contains(zip_filename, files): ziph = None try: # contextmanager for zipfile.ZipFile doesn't exist for py2.6, manually closing it ziph = zipfile.ZipFile(zip_filename, 'r') zip_files = [x.filename for x in ziph.filelist] for current_file in files: if current_file not in zip_files: return "'{0}' was not found in {1}".format(current_file, zip_filename) return None finally: if ziph is not None: ziph.close()WALinuxAgent-2.9.1.1/tests/utils/test_crypt_util.py000066400000000000000000000064371446033677600223330ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import unittest import azurelinuxagent.common.conf as conf from azurelinuxagent.common.exception import CryptError from azurelinuxagent.common.utils.cryptutil import CryptUtil from tests.tools import AgentTestCase, data_dir, load_data, is_python_version_26, skip_if_predicate_true class TestCryptoUtilOperations(AgentTestCase): def test_decrypt_encrypted_text(self): encrypted_string = load_data("wire/encrypted.enc") prv_key = os.path.join(self.tmp_dir, "TransportPrivate.pem") with open(prv_key, 'w+') as c: c.write(load_data("wire/sample.pem")) secret = ']aPPEv}uNg1FPnl?' crypto = CryptUtil(conf.get_openssl_cmd()) decrypted_string = crypto.decrypt_secret(encrypted_string, prv_key) self.assertEqual(secret, decrypted_string, "decrypted string does not match expected") def test_decrypt_encrypted_text_missing_private_key(self): encrypted_string = load_data("wire/encrypted.enc") prv_key = os.path.join(self.tmp_dir, "TransportPrivate.pem") crypto = CryptUtil(conf.get_openssl_cmd()) self.assertRaises(CryptError, crypto.decrypt_secret, encrypted_string, "abc" + prv_key) @skip_if_predicate_true(is_python_version_26, "Disabled on Python 2.6") def test_decrypt_encrypted_text_wrong_private_key(self): encrypted_string = load_data("wire/encrypted.enc") prv_key = os.path.join(self.tmp_dir, "wrong.pem") with open(prv_key, 'w+') as c: c.write(load_data("wire/trans_prv")) crypto = CryptUtil(conf.get_openssl_cmd()) self.assertRaises(CryptError, crypto.decrypt_secret, encrypted_string, prv_key) def test_decrypt_encrypted_text_text_not_encrypted(self): encrypted_string = "abc@123" prv_key = os.path.join(self.tmp_dir, "TransportPrivate.pem") with open(prv_key, 'w+') as c: c.write(load_data("wire/sample.pem")) crypto = CryptUtil(conf.get_openssl_cmd()) self.assertRaises(CryptError, crypto.decrypt_secret, encrypted_string, prv_key) def test_get_pubkey_from_crt(self): crypto = CryptUtil(conf.get_openssl_cmd()) prv_key = os.path.join(data_dir, "wire", "trans_prv") expected_pub_key = os.path.join(data_dir, "wire", "trans_pub") with open(expected_pub_key) as fh: self.assertEqual(fh.read(), crypto.get_pubkey_from_prv(prv_key)) def test_get_pubkey_from_crt_invalid_file(self): crypto = CryptUtil(conf.get_openssl_cmd()) prv_key = os.path.join(data_dir, "wire", "trans_prv_does_not_exist") self.assertRaises(IOError, crypto.get_pubkey_from_prv, prv_key) if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/utils/test_extension_process_util.py000066400000000000000000000370741446033677600247450ustar00rootroot00000000000000# Copyright Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import shutil import subprocess import tempfile from azurelinuxagent.common.cgroup import CpuCgroup from azurelinuxagent.common.exception import ExtensionError, ExtensionErrorCodes from azurelinuxagent.common.future import ustr from azurelinuxagent.common.utils.extensionprocessutil import format_stdout_stderr, read_output, \ wait_for_process_completion_or_timeout, handle_process_completion from tests.tools import AgentTestCase, patch, data_dir class TestProcessUtils(AgentTestCase): def setUp(self): AgentTestCase.setUp(self) self.tmp_dir = tempfile.mkdtemp() self.stdout = tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") self.stderr = tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") self.stdout.write("The quick brown fox jumps over the lazy dog.".encode("utf-8")) self.stderr.write("The five boxing wizards jump quickly.".encode("utf-8")) def tearDown(self): self.stderr.close() self.stdout.close() super(TestProcessUtils, self).tearDown() def test_wait_for_process_completion_or_timeout_should_terminate_cleanly(self): process = subprocess.Popen( "date", shell=True, cwd=self.tmp_dir, env={}, stdout=subprocess.PIPE, stderr=subprocess.PIPE) timed_out, ret, _ = wait_for_process_completion_or_timeout(process=process, timeout=5, cpu_cgroup=None) self.assertEqual(timed_out, False) self.assertEqual(ret, 0) def test_wait_for_process_completion_or_timeout_should_kill_process_on_timeout(self): timeout = 5 process = subprocess.Popen( # pylint: disable=subprocess-popen-preexec-fn "sleep 1m", shell=True, cwd=self.tmp_dir, env={}, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=os.setsid) # We don't actually mock the kill, just wrap it so we can assert its call count with patch('azurelinuxagent.common.utils.extensionprocessutil.os.killpg', wraps=os.killpg) as patch_kill: with patch('time.sleep') as mock_sleep: timed_out, ret, _ = wait_for_process_completion_or_timeout(process=process, timeout=timeout, cpu_cgroup=None) # We're mocking sleep to avoid prolonging the test execution time, but we still want to make sure # we're "waiting" the correct amount of time before killing the process self.assertEqual(mock_sleep.call_count, timeout) self.assertEqual(patch_kill.call_count, 1) self.assertEqual(timed_out, True) self.assertEqual(ret, None) def test_handle_process_completion_should_return_nonzero_when_process_fails(self): process = subprocess.Popen( "ls folder_does_not_exist", shell=True, cwd=self.tmp_dir, env={}, stdout=subprocess.PIPE, stderr=subprocess.PIPE) timed_out, ret, _ = wait_for_process_completion_or_timeout(process=process, timeout=5, cpu_cgroup=None) self.assertEqual(timed_out, False) self.assertEqual(ret, 2) def test_handle_process_completion_should_return_process_output(self): command = "echo 'dummy stdout' && 1>&2 echo 'dummy stderr'" with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stdout: with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stderr: process = subprocess.Popen(command, # pylint: disable=subprocess-popen-preexec-fn shell=True, cwd=self.tmp_dir, env={}, stdout=stdout, stderr=stderr, preexec_fn=os.setsid) process_output = handle_process_completion(process=process, command=command, timeout=5, stdout=stdout, stderr=stderr, error_code=42) expected_output = "[stdout]\ndummy stdout\n\n\n[stderr]\ndummy stderr\n" self.assertEqual(process_output, expected_output) def test_handle_process_completion_should_raise_on_timeout(self): command = "sleep 1m" timeout = 20 with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stdout: with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stderr: with patch('time.sleep') as mock_sleep: with self.assertRaises(ExtensionError) as context_manager: process = subprocess.Popen(command, # pylint: disable=subprocess-popen-preexec-fn shell=True, cwd=self.tmp_dir, env={}, stdout=stdout, stderr=stderr, preexec_fn=os.setsid) handle_process_completion(process=process, command=command, timeout=timeout, stdout=stdout, stderr=stderr, error_code=42) # We're mocking sleep to avoid prolonging the test execution time, but we still want to make sure # we're "waiting" the correct amount of time before killing the process and raising an exception # Due to an extra call to sleep at some point in the call stack which only happens sometimes, # we are relaxing this assertion to allow +/- 2 sleep calls. self.assertTrue(abs(mock_sleep.call_count - timeout) <= 2) self.assertEqual(context_manager.exception.code, ExtensionErrorCodes.PluginHandlerScriptTimedout) self.assertIn("Timeout({0})".format(timeout), ustr(context_manager.exception)) self.assertNotIn("CPUThrottledTime({0}secs)".format(timeout), ustr(context_manager.exception)) #Extension not started in cpuCgroup def test_handle_process_completion_should_log_throttled_time_on_timeout(self): command = "sleep 1m" timeout = 20 with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stdout: with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stderr: with patch('time.sleep') as mock_sleep: with self.assertRaises(ExtensionError) as context_manager: test_file = os.path.join(self.tmp_dir, "cpu.stat") shutil.copyfile(os.path.join(data_dir, "cgroups", "cpu.stat_t0"), test_file) # throttled_time = 50 cgroup = CpuCgroup("test", self.tmp_dir) process = subprocess.Popen(command, # pylint: disable=subprocess-popen-preexec-fn shell=True, cwd=self.tmp_dir, env={}, stdout=stdout, stderr=stderr, preexec_fn=os.setsid) handle_process_completion(process=process, command=command, timeout=timeout, stdout=stdout, stderr=stderr, error_code=42, cpu_cgroup=cgroup) # We're mocking sleep to avoid prolonging the test execution time, but we still want to make sure # we're "waiting" the correct amount of time before killing the process and raising an exception # Due to an extra call to sleep at some point in the call stack which only happens sometimes, # we are relaxing this assertion to allow +/- 2 sleep calls. self.assertTrue(abs(mock_sleep.call_count - timeout) <= 2) self.assertEqual(context_manager.exception.code, ExtensionErrorCodes.PluginHandlerScriptTimedout) self.assertIn("Timeout({0})".format(timeout), ustr(context_manager.exception)) throttled_time = float(50 / 1E9) self.assertIn("CPUThrottledTime({0}secs)".format(throttled_time), ustr(context_manager.exception)) def test_handle_process_completion_should_raise_on_nonzero_exit_code(self): command = "ls folder_does_not_exist" error_code = 42 with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stdout: with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stderr: with self.assertRaises(ExtensionError) as context_manager: process = subprocess.Popen(command, # pylint: disable=subprocess-popen-preexec-fn shell=True, cwd=self.tmp_dir, env={}, stdout=stdout, stderr=stderr, preexec_fn=os.setsid) handle_process_completion(process=process, command=command, timeout=4, stdout=stdout, stderr=stderr, error_code=error_code) self.assertEqual(context_manager.exception.code, error_code) self.assertIn("Non-zero exit code:", ustr(context_manager.exception)) def test_read_output_should_return_no_content(self): with patch('azurelinuxagent.common.utils.extensionprocessutil.TELEMETRY_MESSAGE_MAX_LEN', 0): expected = "" actual = read_output(self.stdout, self.stderr) self.assertEqual(expected, actual) def test_read_output_should_truncate_the_content(self): with patch('azurelinuxagent.common.utils.extensionprocessutil.TELEMETRY_MESSAGE_MAX_LEN', 50): expected = "[stdout]\nr the lazy dog.\n\n" \ "[stderr]\ns jump quickly." actual = read_output(self.stdout, self.stderr) self.assertEqual(expected, actual) def test_read_output_should_not_truncate_the_content(self): with patch('azurelinuxagent.common.utils.extensionprocessutil.TELEMETRY_MESSAGE_MAX_LEN', 90): expected = "[stdout]\nThe quick brown fox jumps over the lazy dog.\n\n" \ "[stderr]\nThe five boxing wizards jump quickly." actual = read_output(self.stdout, self.stderr) self.assertEqual(expected, actual) def test_format_stdout_stderr00(self): """ If stdout and stderr are both smaller than the max length, the full representation should be displayed. """ stdout = "The quick brown fox jumps over the lazy dog." stderr = "The five boxing wizards jump quickly." expected = "[stdout]\n{0}\n\n[stderr]\n{1}".format(stdout, stderr) with patch('azurelinuxagent.common.utils.extensionprocessutil.TELEMETRY_MESSAGE_MAX_LEN', 1000): actual = format_stdout_stderr(stdout, stderr) self.assertEqual(expected, actual) def test_format_stdout_stderr01(self): """ If stdout and stderr both exceed the max length, then both stdout and stderr are trimmed equally. """ stdout = "The quick brown fox jumps over the lazy dog." stderr = "The five boxing wizards jump quickly." # noinspection SpellCheckingInspection expected = '[stdout]\ns over the lazy dog.\n\n[stderr]\nizards jump quickly.' with patch('azurelinuxagent.common.utils.extensionprocessutil.TELEMETRY_MESSAGE_MAX_LEN', 60): actual = format_stdout_stderr(stdout, stderr) self.assertEqual(expected, actual) self.assertEqual(60, len(actual)) def test_format_stdout_stderr02(self): """ If stderr is much larger than stdout, stderr is allowed to borrow space from stdout's quota. """ stdout = "empty" stderr = "The five boxing wizards jump quickly." expected = '[stdout]\nempty\n\n[stderr]\ns jump quickly.' with patch('azurelinuxagent.common.utils.extensionprocessutil.TELEMETRY_MESSAGE_MAX_LEN', 40): actual = format_stdout_stderr(stdout, stderr) self.assertEqual(expected, actual) self.assertEqual(40, len(actual)) def test_format_stdout_stderr03(self): """ If stdout is much larger than stderr, stdout is allowed to borrow space from stderr's quota. """ stdout = "The quick brown fox jumps over the lazy dog." stderr = "empty" expected = '[stdout]\nr the lazy dog.\n\n[stderr]\nempty' with patch('azurelinuxagent.common.utils.extensionprocessutil.TELEMETRY_MESSAGE_MAX_LEN', 40): actual = format_stdout_stderr(stdout, stderr) self.assertEqual(expected, actual) self.assertEqual(40, len(actual)) def test_format_stdout_stderr04(self): """ If the max length is not sufficient to even hold the stdout and stderr markers an empty string is returned. """ stdout = "The quick brown fox jumps over the lazy dog." stderr = "The five boxing wizards jump quickly." expected = '' with patch('azurelinuxagent.common.utils.extensionprocessutil.TELEMETRY_MESSAGE_MAX_LEN', 4): actual = format_stdout_stderr(stdout, stderr) self.assertEqual(expected, actual) self.assertEqual(0, len(actual)) def test_format_stdout_stderr05(self): """ If stdout and stderr are empty, an empty template is returned. """ expected = '[stdout]\n\n\n[stderr]\n' with patch('azurelinuxagent.common.utils.extensionprocessutil.TELEMETRY_MESSAGE_MAX_LEN', 1000): actual = format_stdout_stderr('', '') self.assertEqual(expected, actual) WALinuxAgent-2.9.1.1/tests/utils/test_file_util.py000066400000000000000000000254741446033677600221130ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import errno import glob import os import random import shutil import string import tempfile import unittest import uuid import azurelinuxagent.common.utils.fileutil as fileutil from azurelinuxagent.common.future import ustr from tests.tools import AgentTestCase, patch class TestFileOperations(AgentTestCase): def test_read_write_file(self): test_file=os.path.join(self.tmp_dir, self.test_file) content = ustr(uuid.uuid4()) fileutil.write_file(test_file, content) content_read = fileutil.read_file(test_file) self.assertEqual(content, content_read) os.remove(test_file) def test_write_file_content_is_None(self): """ write_file throws when content is None. No file is created. """ try: test_file=os.path.join(self.tmp_dir, self.test_file) fileutil.write_file(test_file, None) self.fail("expected write_file to throw an exception") except: # pylint: disable=bare-except self.assertEqual(False, os.path.exists(test_file)) def test_rw_utf8_file(self): test_file=os.path.join(self.tmp_dir, self.test_file) content = u"\u6211" fileutil.write_file(test_file, content, encoding="utf-8") content_read = fileutil.read_file(test_file) self.assertEqual(content, content_read) os.remove(test_file) def test_remove_bom(self): test_file=os.path.join(self.tmp_dir, self.test_file) data = b'\xef\xbb\xbfhehe' fileutil.write_file(test_file, data, asbin=True) data = fileutil.read_file(test_file, remove_bom=True) self.assertNotEqual(0xbb, ord(data[0])) def test_append_file(self): test_file=os.path.join(self.tmp_dir, self.test_file) content = ustr(uuid.uuid4()) fileutil.append_file(test_file, content) content_read = fileutil.read_file(test_file) self.assertEqual(content, content_read) os.remove(test_file) def test_findre_in_file(self): fp = tempfile.mktemp() with open(fp, 'w') as f: f.write( ''' First line Second line Third line with more words ''' ) self.assertNotEqual( None, fileutil.findre_in_file(fp, ".*rst line$")) self.assertNotEqual( None, fileutil.findre_in_file(fp, ".*ond line$")) self.assertNotEqual( None, fileutil.findre_in_file(fp, ".*with more.*")) self.assertNotEqual( None, fileutil.findre_in_file(fp, "^Third.*")) self.assertEqual( None, fileutil.findre_in_file(fp, "^Do not match.*")) def test_findstr_in_file(self): fp = tempfile.mktemp() with open(fp, 'w') as f: f.write( ''' First line Second line Third line with more words ''' ) self.assertTrue(fileutil.findstr_in_file(fp, "First line")) self.assertTrue(fileutil.findstr_in_file(fp, "Second line")) self.assertTrue( fileutil.findstr_in_file(fp, "Third line with more words")) self.assertFalse(fileutil.findstr_in_file(fp, "Not a line")) def test_get_last_path_element(self): filepath = '/tmp/abc.def' filename = fileutil.base_name(filepath) self.assertEqual('abc.def', filename) filepath = '/tmp/abc' filename = fileutil.base_name(filepath) self.assertEqual('abc', filename) def test_remove_files(self): random_word = lambda : ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(5)) #Create 10 test files test_file = os.path.join(self.tmp_dir, self.test_file) test_file2 = os.path.join(self.tmp_dir, 'another_file') test_files = [test_file + random_word() for _ in range(5)] + \ [test_file2 + random_word() for _ in range(5)] for file in test_files: # pylint: disable=redefined-builtin open(file, 'a').close() #Remove files using fileutil.rm_files test_file_pattern = test_file + '*' test_file_pattern2 = test_file2 + '*' fileutil.rm_files(test_file_pattern, test_file_pattern2) self.assertEqual(0, len(glob.glob(os.path.join(self.tmp_dir, test_file_pattern)))) self.assertEqual(0, len(glob.glob(os.path.join(self.tmp_dir, test_file_pattern2)))) def test_remove_dirs(self): dirs = [] for n in range(0,5): dirs.append(tempfile.mkdtemp()) for d in dirs: for n in range(0, random.choice(range(0,10))): fileutil.write_file(os.path.join(d, "test"+str(n)), "content") for n in range(0, random.choice(range(0,10))): dd = os.path.join(d, "testd"+str(n)) os.mkdir(dd) for nn in range(0, random.choice(range(0,10))): os.symlink(dd, os.path.join(dd, "sym"+str(nn))) for n in range(0, random.choice(range(0,10))): os.symlink(d, os.path.join(d, "sym"+str(n))) fileutil.rm_dirs(*dirs) for d in dirs: self.assertEqual(len(os.listdir(d)), 0) def test_get_all_files(self): random_word = lambda: ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(5)) # Create 10 test files at the root dir and 10 other in the sub dir test_file = os.path.join(self.tmp_dir, self.test_file) test_file2 = os.path.join(self.tmp_dir, 'another_file') expected_files = [test_file + random_word() for _ in range(5)] + \ [test_file2 + random_word() for _ in range(5)] test_subdir = os.path.join(self.tmp_dir, 'test_dir') os.mkdir(test_subdir) test_file_in_subdir = os.path.join(test_subdir, self.test_file) test_file_in_subdir2 = os.path.join(test_subdir, 'another_file') expected_files.extend([test_file_in_subdir + random_word() for _ in range(5)] + \ [test_file_in_subdir2 + random_word() for _ in range(5)]) for file in expected_files: # pylint: disable=redefined-builtin open(file, 'a').close() # Get All files using fileutil.get_all_files actual_files = fileutil.get_all_files(self.tmp_dir) self.assertEqual(set(expected_files), set(actual_files)) @patch('os.path.isfile') def test_update_conf_file(self, _): new_file = "\ DEVICE=eth0\n\ ONBOOT=yes\n\ BOOTPROTO=dhcp\n\ TYPE=Ethernet\n\ USERCTL=no\n\ PEERDNS=yes\n\ IPV6INIT=no\n\ NM_CONTROLLED=yes\n" existing_file = "\ DEVICE=eth0\n\ ONBOOT=yes\n\ BOOTPROTO=dhcp\n\ TYPE=Ethernet\n\ DHCP_HOSTNAME=existing\n\ USERCTL=no\n\ PEERDNS=yes\n\ IPV6INIT=no\n\ NM_CONTROLLED=yes\n" bad_file = "\ DEVICE=eth0\n\ ONBOOT=yes\n\ BOOTPROTO=dhcp\n\ TYPE=Ethernet\n\ USERCTL=no\n\ PEERDNS=yes\n\ IPV6INIT=no\n\ NM_CONTROLLED=yes\n\ DHCP_HOSTNAME=no_new_line" updated_file = "\ DEVICE=eth0\n\ ONBOOT=yes\n\ BOOTPROTO=dhcp\n\ TYPE=Ethernet\n\ USERCTL=no\n\ PEERDNS=yes\n\ IPV6INIT=no\n\ NM_CONTROLLED=yes\n\ DHCP_HOSTNAME=test\n" path = 'path' with patch.object(fileutil, 'write_file') as patch_write: with patch.object(fileutil, 'read_file', return_value=new_file): fileutil.update_conf_file(path, 'DHCP_HOSTNAME', 'DHCP_HOSTNAME=test') patch_write.assert_called_once_with(path, updated_file) with patch.object(fileutil, 'write_file') as patch_write: with patch.object(fileutil, 'read_file', return_value=existing_file): fileutil.update_conf_file(path, 'DHCP_HOSTNAME', 'DHCP_HOSTNAME=test') patch_write.assert_called_once_with(path, updated_file) with patch.object(fileutil, 'write_file') as patch_write: with patch.object(fileutil, 'read_file', return_value=bad_file): fileutil.update_conf_file(path, 'DHCP_HOSTNAME', 'DHCP_HOSTNAME=test') patch_write.assert_called_once_with(path, updated_file) def test_clean_ioerror_ignores_missing(self): e = IOError() e.errno = errno.ENOSPC # Send no paths fileutil.clean_ioerror(e) # Send missing file(s) / directories fileutil.clean_ioerror(e, paths=['/foo/not/here', None, '/bar/not/there']) def test_clean_ioerror_ignores_unless_ioerror(self): try: d = tempfile.mkdtemp() fd, f = tempfile.mkstemp() os.close(fd) fileutil.write_file(f, 'Not empty') # Send non-IOError exception e = Exception() fileutil.clean_ioerror(e, paths=[d, f]) self.assertTrue(os.path.isdir(d)) self.assertTrue(os.path.isfile(f)) # Send unrecognized IOError e = IOError() e.errno = errno.EFAULT self.assertFalse(e.errno in fileutil.KNOWN_IOERRORS) fileutil.clean_ioerror(e, paths=[d, f]) self.assertTrue(os.path.isdir(d)) self.assertTrue(os.path.isfile(f)) finally: shutil.rmtree(d) os.remove(f) def test_clean_ioerror_removes_files(self): fd, f = tempfile.mkstemp() os.close(fd) fileutil.write_file(f, 'Not empty') e = IOError() e.errno = errno.ENOSPC fileutil.clean_ioerror(e, paths=[f]) self.assertFalse(os.path.isdir(f)) self.assertFalse(os.path.isfile(f)) def test_clean_ioerror_removes_directories(self): d1 = tempfile.mkdtemp() d2 = tempfile.mkdtemp() for n in ['foo', 'bar']: fileutil.write_file(os.path.join(d2, n), 'Not empty') e = IOError() e.errno = errno.ENOSPC fileutil.clean_ioerror(e, paths=[d1, d2]) self.assertFalse(os.path.isdir(d1)) self.assertFalse(os.path.isfile(d1)) self.assertFalse(os.path.isdir(d2)) self.assertFalse(os.path.isfile(d2)) def test_clean_ioerror_handles_a_range_of_errors(self): for err in fileutil.KNOWN_IOERRORS: e = IOError() e.errno = err d = tempfile.mkdtemp() fileutil.clean_ioerror(e, paths=[d]) self.assertFalse(os.path.isdir(d)) self.assertFalse(os.path.isfile(d)) if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/utils/test_flexible_version.py000066400000000000000000000372621446033677600234740ustar00rootroot00000000000000import random # pylint: disable=unused-import import re import unittest from azurelinuxagent.common.utils.flexible_version import FlexibleVersion class TestFlexibleVersion(unittest.TestCase): def setUp(self): self.v = FlexibleVersion() def test_compile_separator(self): tests = [ '.', '', '-' ] for t in tests: t_escaped = re.escape(t) t_re = re.compile(t_escaped) self.assertEqual((t_escaped, t_re), self.v._compile_separator(t)) self.assertEqual(('', re.compile('')), self.v._compile_separator(None)) return def test_compile_pattern(self): self.v._compile_pattern() tests = { '1': True, '1.2': True, '1.2.3': True, '1.2.3.4': True, '1.2.3.4.5': True, '1alpha': True, '1.alpha': True, '1-alpha': True, '1alpha0': True, '1.alpha0': True, '1-alpha0': True, '1.2alpha': True, '1.2.alpha': True, '1.2-alpha': True, '1.2alpha0': True, '1.2.alpha0': True, '1.2-alpha0': True, '1beta': True, '1.beta': True, '1-beta': True, '1beta0': True, '1.beta0': True, '1-beta0': True, '1.2beta': True, '1.2.beta': True, '1.2-beta': True, '1.2beta0': True, '1.2.beta0': True, '1.2-beta0': True, '1rc': True, '1.rc': True, '1-rc': True, '1rc0': True, '1.rc0': True, '1-rc0': True, '1.2rc': True, '1.2.rc': True, '1.2-rc': True, '1.2rc0': True, '1.2.rc0': True, '1.2-rc0': True, '1.2.3.4alpha5': True, ' 1': False, 'beta': False, '1delta0': False, '': False } for test in iter(tests): expectation = tests[test] self.assertEqual( expectation, self.v.version_re.match(test) is not None, "test: {0} expected: {1} ".format(test, expectation)) return def test_compile_pattern_sep(self): self.v.sep = '-' self.v._compile_pattern() tests = { '1': True, '1-2': True, '1-2-3': True, '1-2-3-4': True, '1-2-3-4-5': True, '1alpha': True, '1-alpha': True, '1alpha0': True, '1-alpha0': True, '1-2alpha': True, '1-2.alpha': True, '1-2-alpha': True, '1-2alpha0': True, '1-2.alpha0': True, '1-2-alpha0': True, '1beta': True, '1-beta': True, '1beta0': True, '1-beta0': True, '1-2beta': True, '1-2.beta': True, '1-2-beta': True, '1-2beta0': True, '1-2.beta0': True, '1-2-beta0': True, '1rc': True, '1-rc': True, '1rc0': True, '1-rc0': True, '1-2rc': True, '1-2.rc': True, '1-2-rc': True, '1-2rc0': True, '1-2.rc0': True, '1-2-rc0': True, '1-2-3-4alpha5': True, ' 1': False, 'beta': False, '1delta0': False, '': False } for test in iter(tests): expectation = tests[test] self.assertEqual( expectation, self.v.version_re.match(test) is not None, "test: {0} expected: {1} ".format(test, expectation)) return def test_compile_pattern_prerel(self): self.v.prerel_tags = ('a', 'b', 'c') self.v._compile_pattern() tests = { '1': True, '1.2': True, '1.2.3': True, '1.2.3.4': True, '1.2.3.4.5': True, '1a': True, '1.a': True, '1-a': True, '1a0': True, '1.a0': True, '1-a0': True, '1.2a': True, '1.2.a': True, '1.2-a': True, '1.2a0': True, '1.2.a0': True, '1.2-a0': True, '1b': True, '1.b': True, '1-b': True, '1b0': True, '1.b0': True, '1-b0': True, '1.2b': True, '1.2.b': True, '1.2-b': True, '1.2b0': True, '1.2.b0': True, '1.2-b0': True, '1c': True, '1.c': True, '1-c': True, '1c0': True, '1.c0': True, '1-c0': True, '1.2c': True, '1.2.c': True, '1.2-c': True, '1.2c0': True, '1.2.c0': True, '1.2-c0': True, '1.2.3.4a5': True, ' 1': False, '1.2.3.4alpha5': False, 'beta': False, '1delta0': False, '': False } for test in iter(tests): expectation = tests[test] self.assertEqual( expectation, self.v.version_re.match(test) is not None, "test: {0} expected: {1} ".format(test, expectation)) return def test_ensure_compatible_separators(self): v1 = FlexibleVersion('1.2.3') v2 = FlexibleVersion('1-2-3', sep='-') try: v1 == v2 # pylint: disable=pointless-statement self.assertTrue(False, "Incompatible separators failed to raise an exception") # pylint: disable=redundant-unittest-assert except ValueError: pass except Exception as e: t = e.__class__.__name__ # pylint: disable=redundant-unittest-assert self.assertTrue(False, "Incompatible separators raised an unexpected exception: {0}" \ .format(t)) # pylint: enable=redundant-unittest-assert return def test_ensure_compatible_prerel(self): v1 = FlexibleVersion('1.2.3', prerel_tags=('alpha', 'beta', 'rc')) v2 = FlexibleVersion('1.2.3', prerel_tags=('a', 'b', 'c')) try: v1 == v2 # pylint: disable=pointless-statement self.assertTrue(False, "Incompatible prerel_tags failed to raise an exception") # pylint: disable=redundant-unittest-assert except ValueError: pass except Exception as e: t = e.__class__.__name__ # pylint: disable=redundant-unittest-assert self.assertTrue(False, "Incompatible prerel_tags raised an unexpected exception: {0}" \ .format(t)) # pylint: enable=redundant-unittest-assert return def test_ensure_compatible_prerel_length(self): v1 = FlexibleVersion('1.2.3', prerel_tags=('a', 'b', 'c')) v2 = FlexibleVersion('1.2.3', prerel_tags=('a', 'b')) try: v1 == v2 # pylint: disable=pointless-statement self.assertTrue(False, "Incompatible prerel_tags failed to raise an exception") # pylint: disable=redundant-unittest-assert except ValueError: pass except Exception as e: t = e.__class__.__name__ # pylint: disable=redundant-unittest-assert self.assertTrue(False, "Incompatible prerel_tags raised an unexpected exception: {0}" \ .format(t)) # pylint: enable=redundant-unittest-assert return def test_ensure_compatible_prerel_order(self): v1 = FlexibleVersion('1.2.3', prerel_tags=('a', 'b')) v2 = FlexibleVersion('1.2.3', prerel_tags=('b', 'a')) try: v1 == v2 # pylint: disable=pointless-statement self.assertTrue(False, "Incompatible prerel_tags failed to raise an exception") # pylint: disable=redundant-unittest-assert except ValueError: pass except Exception as e: t = e.__class__.__name__ # pylint: disable=redundant-unittest-assert self.assertTrue(False, "Incompatible prerel_tags raised an unexpected exception: {0}" \ .format(t)) # pylint: enable=redundant-unittest-assert return def test_major(self): tests = { '1' : 1, '1.2' : 1, '1.2.3' : 1, '1.2.3.4' : 1 } for test in iter(tests): expectation = tests[test] self.assertEqual( expectation, FlexibleVersion(test).major) return def test_minor(self): tests = { '1' : 0, '1.2' : 2, '1.2.3' : 2, '1.2.3.4' : 2 } for test in iter(tests): expectation = tests[test] self.assertEqual( expectation, FlexibleVersion(test).minor) return def test_patch(self): tests = { '1' : 0, '1.2' : 0, '1.2.3' : 3, '1.2.3.4' : 3 } for test in iter(tests): expectation = tests[test] self.assertEqual( expectation, FlexibleVersion(test).patch) return def test_parse(self): tests = { "1.2.3.4": ((1, 2, 3, 4), None), "1.2.3.4alpha5": ((1, 2, 3, 4), ('alpha', 5)), "1.2.3.4-alpha5": ((1, 2, 3, 4), ('alpha', 5)), "1.2.3.4.alpha5": ((1, 2, 3, 4), ('alpha', 5)) } for test in iter(tests): expectation = tests[test] self.v._parse(test) self.assertEqual(expectation, (self.v.version, self.v.prerelease)) return def test_decrement(self): src_v = FlexibleVersion('1.0.0.0.10') dst_v = FlexibleVersion(str(src_v)) for i in range(1,10): dst_v -= 1 self.assertEqual(i, src_v.version[-1] - dst_v.version[-1]) return def test_decrement_disallows_below_zero(self): try: FlexibleVersion('1.0') - 1 # pylint: disable=expression-not-assigned self.assertTrue(False, "Decrement failed to raise an exception") # pylint: disable=redundant-unittest-assert except ArithmeticError: pass except Exception as e: t = e.__class__.__name__ self.assertTrue(False, "Decrement raised an unexpected exception: {0}".format(t)) # pylint: disable=redundant-unittest-assert return def test_increment(self): src_v = FlexibleVersion('1.0.0.0.0') dst_v = FlexibleVersion(str(src_v)) for i in range(1,10): dst_v += 1 self.assertEqual(i, dst_v.version[-1] - src_v.version[-1]) return def test_str(self): tests = [ '1', '1.2', '1.2.3', '1.2.3.4', '1.2.3.4.5', '1alpha', '1.alpha', '1-alpha', '1alpha0', '1.alpha0', '1-alpha0', '1.2alpha', '1.2.alpha', '1.2-alpha', '1.2alpha0', '1.2.alpha0', '1.2-alpha0', '1beta', '1.beta', '1-beta', '1beta0', '1.beta0', '1-beta0', '1.2beta', '1.2.beta', '1.2-beta', '1.2beta0', '1.2.beta0', '1.2-beta0', '1rc', '1.rc', '1-rc', '1rc0', '1.rc0', '1-rc0', '1.2rc', '1.2.rc', '1.2-rc', '1.2rc0', '1.2.rc0', '1.2-rc0', '1.2.3.4alpha5', ] for test in tests: self.assertEqual(test, str(FlexibleVersion(test))) return def test_creation_from_flexible_version(self): tests = [ '1', '1.2', '1.2.3', '1.2.3.4', '1.2.3.4.5', '1alpha', '1.alpha', '1-alpha', '1alpha0', '1.alpha0', '1-alpha0', '1.2alpha', '1.2.alpha', '1.2-alpha', '1.2alpha0', '1.2.alpha0', '1.2-alpha0', '1beta', '1.beta', '1-beta', '1beta0', '1.beta0', '1-beta0', '1.2beta', '1.2.beta', '1.2-beta', '1.2beta0', '1.2.beta0', '1.2-beta0', '1rc', '1.rc', '1-rc', '1rc0', '1.rc0', '1-rc0', '1.2rc', '1.2.rc', '1.2-rc', '1.2rc0', '1.2.rc0', '1.2-rc0', '1.2.3.4alpha5', ] for test in tests: v = FlexibleVersion(test) self.assertEqual(test, str(FlexibleVersion(v))) return def test_repr(self): v = FlexibleVersion('1,2,3rc4', ',', ['lol', 'rc']) expected = "FlexibleVersion ('1,2,3rc4', ',', ('lol', 'rc'))" self.assertEqual(expected, repr(v)) def test_order(self): test0 = ["1.7.0", "1.7.0rc0", "1.11.0"] expected0 = ['1.7.0rc0', '1.7.0', '1.11.0'] self.assertEqual(expected0, list(map(str, sorted([FlexibleVersion(v) for v in test0])))) test1 = [ '2.0.2rc2', '2.2.0beta3', '2.0.10', '2.1.0alpha42', '2.0.2beta4', '2.1.1', '2.0.1', '2.0.2rc3', '2.2.0', '2.0.0', '3.0.1', '2.1.0rc1' ] expected1 = [ '2.0.0', '2.0.1', '2.0.2beta4', '2.0.2rc2', '2.0.2rc3', '2.0.10', '2.1.0alpha42', '2.1.0rc1', '2.1.1', '2.2.0beta3', '2.2.0', '3.0.1' ] self.assertEqual(expected1, list(map(str, sorted([FlexibleVersion(v) for v in test1])))) self.assertEqual(FlexibleVersion("1.0.0.0.0.0.0.0"), FlexibleVersion("1")) self.assertFalse(FlexibleVersion("1.0") > FlexibleVersion("1.0")) self.assertFalse(FlexibleVersion("1.0") < FlexibleVersion("1.0")) self.assertTrue(FlexibleVersion("1.0") < FlexibleVersion("1.1")) self.assertTrue(FlexibleVersion("1.9") < FlexibleVersion("1.10")) self.assertTrue(FlexibleVersion("1.9.9") < FlexibleVersion("1.10.0")) self.assertTrue(FlexibleVersion("1.0.0.0") < FlexibleVersion("1.2.0.0")) self.assertTrue(FlexibleVersion("1.1") > FlexibleVersion("1.0")) self.assertTrue(FlexibleVersion("1.10") > FlexibleVersion("1.9")) self.assertTrue(FlexibleVersion("1.10.0") > FlexibleVersion("1.9.9")) self.assertTrue(FlexibleVersion("1.2.0.0") > FlexibleVersion("1.0.0.0")) self.assertTrue(FlexibleVersion("1.0") <= FlexibleVersion("1.1")) self.assertTrue(FlexibleVersion("1.1") > FlexibleVersion("1.0")) self.assertTrue(FlexibleVersion("1.1") >= FlexibleVersion("1.0")) self.assertTrue(FlexibleVersion("1.0") == FlexibleVersion("1.0")) self.assertTrue(FlexibleVersion("1.0") >= FlexibleVersion("1.0")) self.assertTrue(FlexibleVersion("1.0") <= FlexibleVersion("1.0")) self.assertFalse(FlexibleVersion("1.0") != FlexibleVersion("1.0")) self.assertTrue(FlexibleVersion("1.1") != FlexibleVersion("1.0")) return if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/utils/test_network_util.py000066400000000000000000000104531446033677600226540ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import subprocess from mock.mock import patch import azurelinuxagent.common.utils.networkutil as networkutil from tests.tools import AgentTestCase class TestNetworkOperations(AgentTestCase): def test_route_entry(self): interface = "eth0" mask = "C0FFFFFF" # 255.255.255.192 destination = "C0BB910A" # gateway = "C1BB910A" flags = "1" metric = "0" expected = 'Iface: eth0\tDestination: 10.145.187.192\tGateway: 10.145.187.193\tMask: 255.255.255.192\tFlags: 0x0001\tMetric: 0' expected_json = '{"Iface": "eth0", "Destination": "10.145.187.192", "Gateway": "10.145.187.193", "Mask": "255.255.255.192", "Flags": "0x0001", "Metric": "0"}' entry = networkutil.RouteEntry(interface, destination, gateway, mask, flags, metric) self.assertEqual(str(entry), expected) self.assertEqual(entry.to_json(), expected_json) def test_nic_link_only(self): nic = networkutil.NetworkInterfaceCard("test0", "link info") self.assertEqual(str(nic), '{ "name": "test0", "link": "link info" }') def test_nic_ipv4(self): nic = networkutil.NetworkInterfaceCard("test0", "link info") nic.add_ipv4("ipv4-1") self.assertEqual(str(nic), '{ "name": "test0", "link": "link info", "ipv4": ["ipv4-1"] }') nic.add_ipv4("ipv4-2") self.assertEqual(str(nic), '{ "name": "test0", "link": "link info", "ipv4": ["ipv4-1","ipv4-2"] }') def test_nic_ipv6(self): nic = networkutil.NetworkInterfaceCard("test0", "link info") nic.add_ipv6("ipv6-1") self.assertEqual(str(nic), '{ "name": "test0", "link": "link info", "ipv6": ["ipv6-1"] }') nic.add_ipv6("ipv6-2") self.assertEqual(str(nic), '{ "name": "test0", "link": "link info", "ipv6": ["ipv6-1","ipv6-2"] }') def test_nic_ordinary(self): nic = networkutil.NetworkInterfaceCard("test0", "link INFO") nic.add_ipv6("ipv6-1") nic.add_ipv4("ipv4-1") self.assertEqual(str(nic), '{ "name": "test0", "link": "link INFO", "ipv4": ["ipv4-1"], "ipv6": ["ipv6-1"] }') class TestAddFirewallRules(AgentTestCase): def test_it_should_add_firewall_rules(self): test_dst_ip = "1.2.3.4" test_uid = 9999 test_wait = "-w" original_popen = subprocess.Popen commands_called = [] def mock_popen(command, *args, **kwargs): if "iptables" in command: commands_called.append(command) command = ["echo", "iptables"] return original_popen(command, *args, **kwargs) with patch("azurelinuxagent.common.utils.shellutil.subprocess.Popen", side_effect=mock_popen): networkutil.AddFirewallRules.add_iptables_rules(test_wait, test_dst_ip, test_uid) self.assertTrue(all(test_dst_ip in cmd for cmd in commands_called), "Dest IP was not set correctly in iptables") self.assertTrue(all(test_wait in cmd for cmd in commands_called), "The wait was not set properly") self.assertTrue(all(str(test_uid) in cmd for cmd in commands_called if ("ACCEPT" in cmd and "--uid-owner" in cmd)), "The UID is not set for the accept command") def test_it_should_raise_if_invalid_data(self): with self.assertRaises(Exception) as context_manager: networkutil.AddFirewallRules.add_iptables_rules(wait="", dst_ip="", uid=9999) self.assertIn("Destination IP should not be empty", str(context_manager.exception)) with self.assertRaises(Exception) as context_manager: networkutil.AddFirewallRules.add_iptables_rules(wait="", dst_ip="1.2.3.4", uid="") self.assertIn("User ID should not be empty", str(context_manager.exception)) WALinuxAgent-2.9.1.1/tests/utils/test_passwords.txt000066400000000000000000000000401446033677600223310ustar00rootroot00000000000000김치 करी hamburger caféWALinuxAgent-2.9.1.1/tests/utils/test_rest_util.py000066400000000000000000001075371446033677600221520ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import unittest from azurelinuxagent.common.exception import HttpError, ResourceGoneError, InvalidContainerError import azurelinuxagent.common.utils.restutil as restutil from azurelinuxagent.common.utils.restutil import HTTP_USER_AGENT from azurelinuxagent.common.future import httpclient, ustr from tests.tools import AgentTestCase, call, Mock, MagicMock, patch class TestIOErrorCounter(AgentTestCase): def test_increment_hostplugin(self): restutil.IOErrorCounter.reset() restutil.IOErrorCounter.set_protocol_endpoint() restutil.IOErrorCounter.increment( restutil.KNOWN_WIRESERVER_IP, restutil.HOST_PLUGIN_PORT) counts = restutil.IOErrorCounter.get_and_reset() self.assertEqual(1, counts["hostplugin"]) self.assertEqual(0, counts["protocol"]) self.assertEqual(0, counts["other"]) def test_increment_protocol(self): restutil.IOErrorCounter.reset() restutil.IOErrorCounter.set_protocol_endpoint() restutil.IOErrorCounter.increment( restutil.KNOWN_WIRESERVER_IP, 80) counts = restutil.IOErrorCounter.get_and_reset() self.assertEqual(0, counts["hostplugin"]) self.assertEqual(1, counts["protocol"]) self.assertEqual(0, counts["other"]) def test_increment_other(self): restutil.IOErrorCounter.reset() restutil.IOErrorCounter.set_protocol_endpoint() restutil.IOErrorCounter.increment( '169.254.169.254', 80) counts = restutil.IOErrorCounter.get_and_reset() self.assertEqual(0, counts["hostplugin"]) self.assertEqual(0, counts["protocol"]) self.assertEqual(1, counts["other"]) def test_get_and_reset(self): restutil.IOErrorCounter.reset() restutil.IOErrorCounter.set_protocol_endpoint() restutil.IOErrorCounter.increment( restutil.KNOWN_WIRESERVER_IP, restutil.HOST_PLUGIN_PORT) restutil.IOErrorCounter.increment( restutil.KNOWN_WIRESERVER_IP, restutil.HOST_PLUGIN_PORT) restutil.IOErrorCounter.increment( restutil.KNOWN_WIRESERVER_IP, 80) restutil.IOErrorCounter.increment( '169.254.169.254', 80) restutil.IOErrorCounter.increment( '169.254.169.254', 80) counts = restutil.IOErrorCounter.get_and_reset() self.assertEqual(2, counts.get("hostplugin")) self.assertEqual(1, counts.get("protocol")) self.assertEqual(2, counts.get("other")) self.assertEqual( {"hostplugin":0, "protocol":0, "other":0}, restutil.IOErrorCounter._counts) class TestHttpOperations(AgentTestCase): def test_parse_url(self): test_uri = "http://abc.def/ghi#hash?jkl=mn" host, port, secure, rel_uri = restutil._parse_url(test_uri) # pylint: disable=unused-variable self.assertEqual("abc.def", host) self.assertEqual("/ghi#hash?jkl=mn", rel_uri) test_uri = "http://abc.def/" host, port, secure, rel_uri = restutil._parse_url(test_uri) self.assertEqual("abc.def", host) self.assertEqual("/", rel_uri) self.assertEqual(False, secure) test_uri = "https://abc.def/ghi?jkl=mn" host, port, secure, rel_uri = restutil._parse_url(test_uri) self.assertEqual(True, secure) test_uri = "http://abc.def:80/" host, port, secure, rel_uri = restutil._parse_url(test_uri) self.assertEqual("abc.def", host) host, port, secure, rel_uri = restutil._parse_url("") self.assertEqual(None, host) self.assertEqual(rel_uri, "") host, port, secure, rel_uri = restutil._parse_url("None") self.assertEqual(None, host) self.assertEqual(rel_uri, "None") def test_trim_url_parameters(self): test_uri = "http://abc.def/ghi#hash?jkl=mn" rel_uri = restutil._trim_url_parameters(test_uri) self.assertEqual("http://abc.def/ghi", rel_uri) test_uri = "https://abc.def/ghi?jkl=mn" rel_uri = restutil._trim_url_parameters(test_uri) self.assertEqual("https://abc.def/ghi", rel_uri) test_uri = "https://abc.def:8443/ghi?jkl=mn" rel_uri = restutil._trim_url_parameters(test_uri) self.assertEqual("https://abc.def:8443/ghi", rel_uri) rel_uri = restutil._trim_url_parameters("") self.assertEqual(rel_uri, "") rel_uri = restutil._trim_url_parameters("None") self.assertEqual(rel_uri, "None") def test_cleanup_sas_tokens_from_urls_for_normal_cases(self): test_url = "http://abc.def/ghi#hash?jkl=mn" filtered_url = restutil.redact_sas_tokens_in_urls(test_url) self.assertEqual(test_url, filtered_url) test_url = "http://abc.def:80/" filtered_url = restutil.redact_sas_tokens_in_urls(test_url) self.assertEqual(test_url, filtered_url) test_url = "http://abc.def/" filtered_url = restutil.redact_sas_tokens_in_urls(test_url) self.assertEqual(test_url, filtered_url) test_url = "https://abc.def/ghi?jkl=mn" filtered_url = restutil.redact_sas_tokens_in_urls(test_url) self.assertEqual(test_url, filtered_url) def test_cleanup_sas_tokens_from_urls_containing_sas_tokens(self): # Contains pair of URLs (RawURL, RedactedURL) urls_tuples = [("https://abc.def.xyz.123.net/functiontest/yokawasa.png?sig" "=sXBjML1Fpk9UnTBtajo05ZTFSk0LWFGvARZ6WlVcAog%3D&srt=o&ss=b&" "spr=https&sp=rl&sv=2016-05-31&se=2017-07-01T00%3A21%3A38Z&" "st=2017-07-01T23%3A16%3A38Z", "https://abc.def.xyz.123.net/functiontest/yokawasa.png?sig" "=" + restutil.REDACTED_TEXT + "&srt=o&ss=b&spr=https&sp=rl&sv=2016-05-31&se=2017-07-01T00" "%3A21%3A38Z&st=2017-07-01T23%3A16%3A38Z"), ("https://abc.def.xyz.123.net/?sv=2017-11-09&ss=b&srt=o&sp=r&se=2018-07" "-26T02:20:44Z&st=2018-07-25T18:20:44Z&spr=https," "http&sig=DavQgRtl99DsEPv9Xeb63GnLXCuaLYw5ay%2BE1cFckQY%3D", "https://abc.def.xyz.123.net/?sv=2017-11-09&ss=b&srt=o&sp=r&se" "=2018-07-26T02:20:44Z&st=2018-07-25T18:20:44Z&spr=https," "http&sig=" + restutil.REDACTED_TEXT), ("https://abc.def.xyz.123.net/?sv=2017-11-09&ss=b&srt=o&sp=r&se=2018-07" "-26T02:20:44Z&st=2018-07-25T18:20:44Z&spr=https," "http&sig=ttSCKmyjiDEeIzT9q7HtYYgbCRIXuesFSOhNEab52NM%3D", "https://abc.def.xyz.123.net/?sv=2017-11-09&ss=b&srt=o&sp=r&se" "=2018-07-26T02:20:44Z&st=2018-07-25T18:20:44Z&spr=https," "http&sig=" + restutil.REDACTED_TEXT), ("https://abc.def.xyz.123.net/?sv=2017-11-09&ss=b&srt=o&sp=r&se=2018-07" "-26T02:20:42Z&st=2018-07-25T18:20:44Z&spr=https," "http&sig=X0imGmcj5KcBPFcqlfYjIZakzGrzONGbRv5JMOnGrwc%3D", "https://abc.def.xyz.123.net/?sv=2017-11-09&ss=b&srt=o&sp=r&se" "=2018-07-26T02:20:42Z&st=2018-07-25T18:20:44Z&spr=https," "http&sig=" + restutil.REDACTED_TEXT), ("https://abc.def.xyz.123.net/?sv=2017-11-09&ss=b&srt=o&sp=r&se=2018-07" "-26T02:20:42Z&st=2018-07-25T18:20:44Z&spr=https," "http&sig=9hfxYvaZzrMahtGO1OgMUiFGnDOtZXulZ3skkv1eVBg%3D", "https://abc.def.xyz.123.net/?sv=2017-11-09&ss=b&srt=o&sp=r&se" "=2018-07-26T02:20:42Z&st=2018-07-25T18:20:44Z&spr=https," "http&sig=" + restutil.REDACTED_TEXT), ("https://abc.def.xyz.123.net/?sv=2017-11-09&ss=b&srt=o&sp=r&se=2018-07" "-26T02:20:42Z&st=2018-07-25T18:20:44Z&spr=https" "&sig=cmluQEHnOGsVK9NDm83ruuPdPWNQcerfjOAbkspNZXU%3D", "https://abc.def.xyz.123.net/?sv=2017-11-09&ss=b&srt=o&sp=r&se" "=2018-07-26T02:20:42Z&st=2018-07-25T18:20:44Z&spr=https&sig" "=" + restutil.REDACTED_TEXT) ] for x in urls_tuples: self.assertEqual(restutil.redact_sas_tokens_in_urls(x[0]), x[1]) @patch('azurelinuxagent.common.conf.get_httpproxy_port') @patch('azurelinuxagent.common.conf.get_httpproxy_host') def test_get_http_proxy_none_is_default(self, mock_host, mock_port): mock_host.return_value = None mock_port.return_value = None h, p = restutil._get_http_proxy() self.assertEqual(None, h) self.assertEqual(None, p) @patch('azurelinuxagent.common.conf.get_httpproxy_port') @patch('azurelinuxagent.common.conf.get_httpproxy_host') def test_get_http_proxy_configuration_overrides_env(self, mock_host, mock_port): mock_host.return_value = "host" mock_port.return_value = None h, p = restutil._get_http_proxy() self.assertEqual("host", h) self.assertEqual(None, p) self.assertEqual(1, mock_host.call_count) self.assertEqual(1, mock_port.call_count) @patch('azurelinuxagent.common.conf.get_httpproxy_port') @patch('azurelinuxagent.common.conf.get_httpproxy_host') def test_get_http_proxy_configuration_requires_host(self, mock_host, mock_port): mock_host.return_value = None mock_port.return_value = None h, p = restutil._get_http_proxy() self.assertEqual(None, h) self.assertEqual(None, p) self.assertEqual(1, mock_host.call_count) self.assertEqual(0, mock_port.call_count) @patch('azurelinuxagent.common.conf.get_httpproxy_host') def test_get_http_proxy_http_uses_httpproxy(self, mock_host): mock_host.return_value = None with patch.dict(os.environ, { 'http_proxy' : 'http://foo.com:80', 'https_proxy' : 'https://bar.com:443' }): h, p = restutil._get_http_proxy() self.assertEqual("foo.com", h) self.assertEqual(80, p) @patch('azurelinuxagent.common.conf.get_httpproxy_host') def test_get_http_proxy_https_uses_httpsproxy(self, mock_host): mock_host.return_value = None with patch.dict(os.environ, { 'http_proxy' : 'http://foo.com:80', 'https_proxy' : 'https://bar.com:443' }): h, p = restutil._get_http_proxy(secure=True) self.assertEqual("bar.com", h) self.assertEqual(443, p) @patch('azurelinuxagent.common.conf.get_httpproxy_host') def test_get_http_proxy_ignores_user_in_httpproxy(self, mock_host): mock_host.return_value = None with patch.dict(os.environ, { 'http_proxy' : 'http://user:pw@foo.com:80' }): h, p = restutil._get_http_proxy() self.assertEqual("foo.com", h) self.assertEqual(80, p) def test_get_no_proxy_with_values_set(self): no_proxy_list = ["foo.com", "www.google.com"] with patch.dict(os.environ, { 'no_proxy': ",".join(no_proxy_list) }): no_proxy_from_environment = restutil.get_no_proxy() self.assertEqual(len(no_proxy_list), len(no_proxy_from_environment)) for i, j in zip(no_proxy_from_environment, no_proxy_list): self.assertEqual(i, j) def test_get_no_proxy_with_incorrect_variable_set(self): no_proxy_list = ["foo.com", "www.google.com", "", ""] no_proxy_list_cleaned = [entry for entry in no_proxy_list if entry] with patch.dict(os.environ, { 'no_proxy': ",".join(no_proxy_list) }): no_proxy_from_environment = restutil.get_no_proxy() self.assertEqual(len(no_proxy_list_cleaned), len(no_proxy_from_environment)) for i, j in zip(no_proxy_from_environment, no_proxy_list_cleaned): print(i, j) self.assertEqual(i, j) def test_get_no_proxy_with_ip_addresses_set(self): no_proxy_var = "10.0.0.1,10.0.0.2,10.0.0.3,10.0.0.4,10.0.0.5,10.0.0.6,10.0.0.7,10.0.0.8,10.0.0.9,10.0.0.10," no_proxy_list = ['10.0.0.1', '10.0.0.2', '10.0.0.3', '10.0.0.4', '10.0.0.5', '10.0.0.6', '10.0.0.7', '10.0.0.8', '10.0.0.9', '10.0.0.10'] with patch.dict(os.environ, { 'no_proxy': no_proxy_var }): no_proxy_from_environment = restutil.get_no_proxy() self.assertEqual(len(no_proxy_list), len(no_proxy_from_environment)) for i, j in zip(no_proxy_from_environment, no_proxy_list): self.assertEqual(i, j) def test_get_no_proxy_default(self): no_proxy_generator = restutil.get_no_proxy() self.assertIsNone(no_proxy_generator) def test_is_ipv4_address(self): self.assertTrue(restutil.is_ipv4_address('8.8.8.8')) self.assertFalse(restutil.is_ipv4_address('localhost.localdomain')) self.assertFalse(restutil.is_ipv4_address('2001:4860:4860::8888')) # ipv6 tests def test_is_valid_cidr(self): self.assertTrue(restutil.is_valid_cidr('192.168.1.0/24')) self.assertFalse(restutil.is_valid_cidr('8.8.8.8')) self.assertFalse(restutil.is_valid_cidr('192.168.1.0/a')) self.assertFalse(restutil.is_valid_cidr('192.168.1.0/128')) self.assertFalse(restutil.is_valid_cidr('192.168.1.0/-1')) self.assertFalse(restutil.is_valid_cidr('192.168.1.999/24')) def test_address_in_network(self): self.assertTrue(restutil.address_in_network('192.168.1.1', '192.168.1.0/24')) self.assertFalse(restutil.address_in_network('172.16.0.1', '192.168.1.0/24')) def test_dotted_netmask(self): self.assertEqual(restutil.dotted_netmask(0), '0.0.0.0') self.assertEqual(restutil.dotted_netmask(8), '255.0.0.0') self.assertEqual(restutil.dotted_netmask(16), '255.255.0.0') self.assertEqual(restutil.dotted_netmask(24), '255.255.255.0') self.assertEqual(restutil.dotted_netmask(32), '255.255.255.255') self.assertRaises(ValueError, restutil.dotted_netmask, 33) def test_bypass_proxy(self): no_proxy_list = ["foo.com", "www.google.com", "168.63.129.16", "Microsoft.com"] with patch.dict(os.environ, { 'no_proxy': ",".join(no_proxy_list) }): self.assertFalse(restutil.bypass_proxy("http://bar.com")) self.assertTrue(restutil.bypass_proxy("http://foo.com")) self.assertTrue(restutil.bypass_proxy("http://168.63.129.16")) self.assertFalse(restutil.bypass_proxy("http://baz.com")) self.assertFalse(restutil.bypass_proxy("http://10.1.1.1")) self.assertTrue(restutil.bypass_proxy("http://www.microsoft.com")) @patch("azurelinuxagent.common.future.httpclient.HTTPSConnection") @patch("azurelinuxagent.common.future.httpclient.HTTPConnection") def test_http_request_direct(self, HTTPConnection, HTTPSConnection): mock_conn = \ MagicMock(getresponse=\ Mock(return_value=\ Mock(read=Mock(return_value="TheResults")))) HTTPConnection.return_value = mock_conn resp = restutil._http_request("GET", "foo", "/bar", 10) HTTPConnection.assert_has_calls([ call("foo", 80, timeout=10) ]) HTTPSConnection.assert_not_called() mock_conn.request.assert_has_calls([ call(method="GET", url="/bar", body=None, headers={'User-Agent': HTTP_USER_AGENT, 'Connection': 'close'}) ]) self.assertEqual(1, mock_conn.getresponse.call_count) self.assertNotEqual(None, resp) self.assertEqual("TheResults", resp.read()) @patch("azurelinuxagent.common.future.httpclient.HTTPSConnection") @patch("azurelinuxagent.common.future.httpclient.HTTPConnection") def test_http_request_direct_secure(self, HTTPConnection, HTTPSConnection): mock_conn = \ MagicMock(getresponse=\ Mock(return_value=\ Mock(read=Mock(return_value="TheResults")))) HTTPSConnection.return_value = mock_conn resp = restutil._http_request("GET", "foo", "/bar", 10, secure=True) HTTPConnection.assert_not_called() HTTPSConnection.assert_has_calls([ call("foo", 443, timeout=10) ]) mock_conn.request.assert_has_calls([ call(method="GET", url="/bar", body=None, headers={'User-Agent': HTTP_USER_AGENT, 'Connection': 'close'}) ]) self.assertEqual(1, mock_conn.getresponse.call_count) self.assertNotEqual(None, resp) self.assertEqual("TheResults", resp.read()) @patch("azurelinuxagent.common.future.httpclient.HTTPSConnection") @patch("azurelinuxagent.common.future.httpclient.HTTPConnection") def test_http_request_proxy(self, HTTPConnection, HTTPSConnection): mock_conn = \ MagicMock(getresponse=\ Mock(return_value=\ Mock(read=Mock(return_value="TheResults")))) HTTPConnection.return_value = mock_conn resp = restutil._http_request("GET", "foo", "/bar", 10, proxy_host="foo.bar", proxy_port=23333) HTTPConnection.assert_has_calls([ call("foo.bar", 23333, timeout=10) ]) HTTPSConnection.assert_not_called() mock_conn.request.assert_has_calls([ call(method="GET", url="http://foo:80/bar", body=None, headers={'User-Agent': HTTP_USER_AGENT, 'Connection': 'close'}) ]) self.assertEqual(1, mock_conn.getresponse.call_count) self.assertNotEqual(None, resp) self.assertEqual("TheResults", resp.read()) @patch("azurelinuxagent.common.utils.restutil._get_http_proxy") @patch("time.sleep") @patch("azurelinuxagent.common.utils.restutil._http_request") def test_http_request_proxy_with_no_proxy_check(self, _http_request, sleep, mock_get_http_proxy): # pylint: disable=unused-argument mock_http_resp = MagicMock() mock_http_resp.read = Mock(return_value="hehe") _http_request.return_value = mock_http_resp mock_get_http_proxy.return_value = "host", 1234 # Return a host/port combination no_proxy_list = ["foo.com", "www.google.com", "168.63.129.16"] with patch.dict(os.environ, { 'no_proxy': ",".join(no_proxy_list) }): # Test http get resp = restutil.http_get("http://foo.com", use_proxy=True) self.assertEqual("hehe", resp.read()) self.assertEqual(0, mock_get_http_proxy.call_count) # Test http get resp = restutil.http_get("http://bar.com", use_proxy=True) self.assertEqual("hehe", resp.read()) self.assertEqual(1, mock_get_http_proxy.call_count) def test_proxy_conditions_with_no_proxy(self): should_use_proxy = True should_not_use_proxy = False use_proxy = True no_proxy_list = ["foo.com", "www.google.com", "168.63.129.16"] with patch.dict(os.environ, { 'no_proxy': ",".join(no_proxy_list) }): host = "10.0.0.1" self.assertEqual(should_use_proxy, use_proxy and not restutil.bypass_proxy(host)) host = "foo.com" self.assertEqual(should_not_use_proxy, use_proxy and not restutil.bypass_proxy(host)) host = "www.google.com" self.assertEqual(should_not_use_proxy, use_proxy and not restutil.bypass_proxy(host)) host = "168.63.129.16" self.assertEqual(should_not_use_proxy, use_proxy and not restutil.bypass_proxy(host)) host = "www.bar.com" self.assertEqual(should_use_proxy, use_proxy and not restutil.bypass_proxy(host)) no_proxy_list = ["10.0.0.1/24"] with patch.dict(os.environ, { 'no_proxy': ",".join(no_proxy_list) }): host = "www.bar.com" self.assertEqual(should_use_proxy, use_proxy and not restutil.bypass_proxy(host)) host = "10.0.0.1" self.assertEqual(should_not_use_proxy, use_proxy and not restutil.bypass_proxy(host)) host = "10.0.1.1" self.assertEqual(should_use_proxy, use_proxy and not restutil.bypass_proxy(host)) # When No_proxy is empty with patch.dict(os.environ, { 'no_proxy': "" }): host = "10.0.0.1" self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) host = "foo.com" self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) host = "www.google.com" self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) host = "168.63.129.16" self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) host = "www.bar.com" self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) host = "10.0.0.1" self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) host = "10.0.1.1" self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) # When os.environ is empty - No global variables defined. with patch.dict(os.environ, {}): host = "10.0.0.1" self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) host = "foo.com" self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) host = "www.google.com" self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) host = "168.63.129.16" self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) host = "www.bar.com" self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) host = "10.0.0.1" self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) host = "10.0.1.1" self.assertTrue(use_proxy and not restutil.bypass_proxy(host)) @patch("azurelinuxagent.common.future.httpclient.HTTPSConnection") @patch("azurelinuxagent.common.future.httpclient.HTTPConnection") def test_http_request_proxy_secure(self, HTTPConnection, HTTPSConnection): mock_conn = \ MagicMock(getresponse=\ Mock(return_value=\ Mock(read=Mock(return_value="TheResults")))) HTTPSConnection.return_value = mock_conn resp = restutil._http_request("GET", "foo", "/bar", 10, proxy_host="foo.bar", proxy_port=23333, secure=True) HTTPConnection.assert_not_called() HTTPSConnection.assert_has_calls([ call("foo.bar", 23333, timeout=10) ]) mock_conn.request.assert_has_calls([ call(method="GET", url="https://foo:443/bar", body=None, headers={'User-Agent': HTTP_USER_AGENT, 'Connection': 'close'}) ]) self.assertEqual(1, mock_conn.getresponse.call_count) self.assertNotEqual(None, resp) self.assertEqual("TheResults", resp.read()) @patch("time.sleep") @patch("azurelinuxagent.common.utils.restutil._http_request") def test_http_request_with_retry(self, _http_request, sleep): # pylint: disable=unused-argument mock_http_resp = MagicMock() mock_http_resp.read = Mock(return_value="hehe") _http_request.return_value = mock_http_resp # Test http get resp = restutil.http_get("http://foo.bar") self.assertEqual("hehe", resp.read()) # Test https get resp = restutil.http_get("https://foo.bar") self.assertEqual("hehe", resp.read()) # Test http failure _http_request.side_effect = httpclient.HTTPException("Http failure") self.assertRaises(restutil.HttpError, restutil.http_get, "http://foo.bar") # Test http failure _http_request.side_effect = IOError("IO failure") self.assertRaises(restutil.HttpError, restutil.http_get, "http://foo.bar") @patch("time.sleep") @patch("azurelinuxagent.common.utils.restutil._http_request") def test_http_request_retries_status_codes(self, _http_request, _sleep): _http_request.side_effect = [ Mock(status=httpclient.SERVICE_UNAVAILABLE), Mock(status=httpclient.OK) ] restutil.http_get("https://foo.bar") self.assertEqual(2, _http_request.call_count) self.assertEqual(1, _sleep.call_count) @patch("time.sleep") @patch("azurelinuxagent.common.utils.restutil._http_request") def test_http_request_retries_passed_status_codes(self, _http_request, _sleep): # Ensure the code is not part of the standard set self.assertFalse(httpclient.UNAUTHORIZED in restutil.RETRY_CODES) _http_request.side_effect = [ Mock(status=httpclient.UNAUTHORIZED), Mock(status=httpclient.OK) ] restutil.http_get("https://foo.bar", retry_codes=[httpclient.UNAUTHORIZED]) self.assertEqual(2, _http_request.call_count) self.assertEqual(1, _sleep.call_count) @patch("time.sleep") @patch("azurelinuxagent.common.utils.restutil._http_request") def test_http_request_retries_with_fibonacci_delay(self, _http_request, _sleep): # Ensure the code is not a throttle code self.assertFalse(httpclient.BAD_GATEWAY in restutil.THROTTLE_CODES) _http_request.side_effect = [ Mock(status=httpclient.BAD_GATEWAY) for i in range(restutil.DEFAULT_RETRIES) ] + [Mock(status=httpclient.OK)] restutil.http_get("https://foo.bar", max_retry=restutil.DEFAULT_RETRIES+1) self.assertEqual(restutil.DEFAULT_RETRIES+1, _http_request.call_count) self.assertEqual(restutil.DEFAULT_RETRIES, _sleep.call_count) self.assertEqual( [ call(restutil._compute_delay(i+1, restutil.DELAY_IN_SECONDS)) for i in range(restutil.DEFAULT_RETRIES)], _sleep.call_args_list) @patch("time.sleep") @patch("azurelinuxagent.common.utils.restutil._http_request") def test_http_request_retries_with_constant_delay_when_throttled(self, _http_request, _sleep): # Ensure the code is a throttle code self.assertTrue(httpclient.SERVICE_UNAVAILABLE in restutil.THROTTLE_CODES) _http_request.side_effect = [ Mock(status=httpclient.SERVICE_UNAVAILABLE) for i in range(restutil.DEFAULT_RETRIES) # pylint: disable=unused-variable ] + [Mock(status=httpclient.OK)] restutil.http_get("https://foo.bar", max_retry=restutil.DEFAULT_RETRIES+1) self.assertEqual(restutil.DEFAULT_RETRIES+1, _http_request.call_count) self.assertEqual(restutil.DEFAULT_RETRIES, _sleep.call_count) self.assertEqual( [call(1) for i in range(restutil.DEFAULT_RETRIES)], _sleep.call_args_list) @patch("time.sleep") @patch("azurelinuxagent.common.utils.restutil._http_request") def test_http_request_retries_for_safe_minimum_number_when_throttled(self, _http_request, _sleep): # Ensure the code is a throttle code self.assertTrue(httpclient.SERVICE_UNAVAILABLE in restutil.THROTTLE_CODES) _http_request.side_effect = [ Mock(status=httpclient.SERVICE_UNAVAILABLE) for i in range(restutil.THROTTLE_RETRIES-1) # pylint: disable=unused-variable ] + [Mock(status=httpclient.OK)] restutil.http_get("https://foo.bar", max_retry=1) self.assertEqual(restutil.THROTTLE_RETRIES, _http_request.call_count) self.assertEqual(restutil.THROTTLE_RETRIES-1, _sleep.call_count) self.assertEqual( [call(1) for i in range(restutil.THROTTLE_RETRIES-1)], _sleep.call_args_list) @patch("time.sleep") @patch("azurelinuxagent.common.utils.restutil._http_request") def test_http_request_raises_for_resource_gone(self, _http_request, _sleep): _http_request.side_effect = [ Mock(status=httpclient.GONE) ] self.assertRaises(ResourceGoneError, restutil.http_get, "https://foo.bar") self.assertEqual(1, _http_request.call_count) @patch("time.sleep") @patch("azurelinuxagent.common.utils.restutil._http_request") def test_http_request_raises_for_invalid_container_configuration(self, _http_request, _sleep): def read(): return b'{ "errorCode": "InvalidContainerConfiguration", "message": "Invalid request." }' _http_request.side_effect = [ Mock(status=httpclient.BAD_REQUEST, reason='Bad Request', read=read) ] self.assertRaises(InvalidContainerError, restutil.http_get, "https://foo.bar") self.assertEqual(1, _http_request.call_count) @patch("time.sleep") @patch("azurelinuxagent.common.utils.restutil._http_request") def test_http_request_raises_for_invalid_role_configuration(self, _http_request, _sleep): def read(): return b'{ "errorCode": "RequestRoleConfigFileNotFound", "message": "Invalid request." }' _http_request.side_effect = [ Mock(status=httpclient.GONE, reason='Resource Gone', read=read) ] self.assertRaises(ResourceGoneError, restutil.http_get, "https://foo.bar") self.assertEqual(1, _http_request.call_count) @patch("time.sleep") @patch("azurelinuxagent.common.utils.restutil._http_request") def test_http_request_retries_exceptions(self, _http_request, _sleep): # Testing each exception is difficult because they have varying # signatures; for now, test one and ensure the set is unchanged recognized_exceptions = [ httpclient.NotConnected, httpclient.IncompleteRead, httpclient.ImproperConnectionState, httpclient.BadStatusLine ] self.assertEqual(recognized_exceptions, restutil.RETRY_EXCEPTIONS) _http_request.side_effect = [ httpclient.IncompleteRead(''), Mock(status=httpclient.OK) ] restutil.http_get("https://foo.bar") self.assertEqual(2, _http_request.call_count) self.assertEqual(1, _sleep.call_count) @patch("time.sleep") @patch("azurelinuxagent.common.utils.restutil._http_request") def test_http_request_retries_ioerrors(self, _http_request, _sleep): ioerror = IOError() ioerror.errno = 42 _http_request.side_effect = [ ioerror, Mock(status=httpclient.OK) ] restutil.http_get("https://foo.bar") self.assertEqual(2, _http_request.call_count) self.assertEqual(1, _sleep.call_count) def test_request_failed(self): self.assertTrue(restutil.request_failed(None)) resp = Mock() for status in restutil.OK_CODES: resp.status = status self.assertFalse(restutil.request_failed(resp)) self.assertFalse(httpclient.BAD_REQUEST in restutil.OK_CODES) resp.status = httpclient.BAD_REQUEST self.assertTrue(restutil.request_failed(resp)) self.assertFalse( restutil.request_failed( resp, ok_codes=[httpclient.BAD_REQUEST])) def test_request_succeeded(self): self.assertFalse(restutil.request_succeeded(None)) resp = Mock() for status in restutil.OK_CODES: resp.status = status self.assertTrue(restutil.request_succeeded(resp)) self.assertFalse(httpclient.BAD_REQUEST in restutil.OK_CODES) resp.status = httpclient.BAD_REQUEST self.assertFalse(restutil.request_succeeded(resp)) self.assertTrue( restutil.request_succeeded( resp, ok_codes=[httpclient.BAD_REQUEST])) def test_read_response_error(self): """ Validate the read_response_error method handles encoding correctly """ responses = ['message', b'message', '\x80message\x80'] response = MagicMock() response.status = 'status' response.reason = 'reason' with patch.object(response, 'read') as patch_response: for s in responses: patch_response.return_value = s result = restutil.read_response_error(response) print("RESPONSE: {0}".format(s)) print("RESULT: {0}".format(result)) print("PRESENT: {0}".format('[status: reason]' in result)) self.assertTrue('[status: reason]' in result) self.assertTrue('message' in result) def test_read_response_bytes(self): response_bytes = '7b:0a:20:20:20:20:22:65:72:72:6f:72:43:6f:64:65:22:' \ '3a:20:22:54:68:65:20:62:6c:6f:62:20:74:79:70:65:20:' \ '69:73:20:69:6e:76:61:6c:69:64:20:66:6f:72:20:74:68:' \ '69:73:20:6f:70:65:72:61:74:69:6f:6e:2e:22:2c:0a:20:' \ '20:20:20:22:6d:65:73:73:61:67:65:22:3a:20:22:c3:af:' \ 'c2:bb:c2:bf:3c:3f:78:6d:6c:20:76:65:72:73:69:6f:6e:' \ '3d:22:31:2e:30:22:20:65:6e:63:6f:64:69:6e:67:3d:22:' \ '75:74:66:2d:38:22:3f:3e:3c:45:72:72:6f:72:3e:3c:43:' \ '6f:64:65:3e:49:6e:76:61:6c:69:64:42:6c:6f:62:54:79:' \ '70:65:3c:2f:43:6f:64:65:3e:3c:4d:65:73:73:61:67:65:' \ '3e:54:68:65:20:62:6c:6f:62:20:74:79:70:65:20:69:73:' \ '20:69:6e:76:61:6c:69:64:20:66:6f:72:20:74:68:69:73:' \ '20:6f:70:65:72:61:74:69:6f:6e:2e:0a:52:65:71:75:65:' \ '73:74:49:64:3a:63:37:34:32:39:30:63:62:2d:30:30:30:' \ '31:2d:30:30:62:35:2d:30:36:64:61:2d:64:64:36:36:36:' \ '61:30:30:30:22:2c:0a:20:20:20:20:22:64:65:74:61:69:' \ '6c:73:22:3a:20:22:22:0a:7d'.split(':') expected_response = '[HTTP Failed] [status: reason] {\n "errorCode": "The blob ' \ 'type is invalid for this operation.",\n ' \ '"message": "' \ 'InvalidBlobTypeThe ' \ 'blob type is invalid for this operation.\n' \ 'RequestId:c74290cb-0001-00b5-06da-dd666a000",' \ '\n "details": ""\n}' response_string = ''.join(chr(int(b, 16)) for b in response_bytes) response = MagicMock() response.status = 'status' response.reason = 'reason' with patch.object(response, 'read') as patch_response: patch_response.return_value = response_string result = restutil.read_response_error(response) self.assertEqual(result, expected_response) try: raise HttpError("{0}".format(result)) except HttpError as e: self.assertTrue(result in ustr(e)) if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/utils/test_shell_util.py000066400000000000000000000552451446033677600223020ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import os import signal import subprocess import tempfile import threading import unittest from azurelinuxagent.common.future import ustr import azurelinuxagent.common.utils.shellutil as shellutil from tests.tools import AgentTestCase, patch from tests.utils.miscellaneous_tools import wait_for, format_processes class ShellQuoteTestCase(AgentTestCase): def test_shellquote(self): self.assertEqual("\'foo\'", shellutil.quote("foo")) self.assertEqual("\'foo bar\'", shellutil.quote("foo bar")) self.assertEqual("'foo'\\''bar'", shellutil.quote("foo\'bar")) class RunTestCase(AgentTestCase): def test_it_should_return_the_exit_code_of_the_command(self): exit_code = shellutil.run("exit 123") self.assertEqual(123, exit_code) def test_it_should_be_a_pass_thru_to_run_get_output(self): with patch.object(shellutil, "run_get_output", return_value=(0, "")) as mock_run_get_output: shellutil.run("echo hello word!", chk_err=False, expected_errors=[1, 2, 3]) self.assertEqual(mock_run_get_output.call_count, 1) args, kwargs = mock_run_get_output.call_args self.assertEqual(args[0], "echo hello word!") self.assertEqual(kwargs["chk_err"], False) self.assertEqual(kwargs["expected_errors"], [1, 2, 3]) class RunGetOutputTestCase(AgentTestCase): def test_run_get_output(self): output = shellutil.run_get_output(u"ls /") self.assertNotEqual(None, output) self.assertEqual(0, output[0]) err = shellutil.run_get_output(u"ls /not-exists") self.assertNotEqual(0, err[0]) err = shellutil.run_get_output(u"ls 我") self.assertNotEqual(0, err[0]) def test_it_should_log_the_command(self): command = "echo hello world!" with patch("azurelinuxagent.common.utils.shellutil.logger", autospec=True) as mock_logger: shellutil.run_get_output(command) self.assertEqual(mock_logger.verbose.call_count, 1) args, kwargs = mock_logger.verbose.call_args # pylint: disable=unused-variable command_in_message = args[1] self.assertEqual(command_in_message, command) def test_it_should_log_command_failures_as_errors(self): return_code = 99 command = "exit {0}".format(return_code) with patch("azurelinuxagent.common.utils.shellutil.logger", autospec=True) as mock_logger: shellutil.run_get_output(command, log_cmd=False) self.assertEqual(mock_logger.error.call_count, 1) args, _ = mock_logger.error.call_args message = args[0] # message is similar to "Command: [exit 99], return code: [99], result: []" self.assertIn("[{0}]".format(command), message) self.assertIn("[{0}]".format(return_code), message) self.assertEqual(mock_logger.info.call_count, 0, "Did not expect any info messages. Got: {0}".format(mock_logger.info.call_args_list)) self.assertEqual(mock_logger.warn.call_count, 0, "Did not expect any warnings. Got: {0}".format(mock_logger.warn.call_args_list)) def test_it_should_log_expected_errors_as_info(self): return_code = 99 command = "exit {0}".format(return_code) with patch("azurelinuxagent.common.utils.shellutil.logger", autospec=True) as mock_logger: shellutil.run_get_output(command, log_cmd=False, expected_errors=[return_code]) self.assertEqual(mock_logger.info.call_count, 1) args, _ = mock_logger.info.call_args message = args[0] # message is similar to "Command: [exit 99], return code: [99], result: []" self.assertIn("[{0}]".format(command), message) self.assertIn("[{0}]".format(return_code), message) self.assertEqual(mock_logger.warn.call_count, 0, "Did not expect any warnings. Got: {0}".format(mock_logger.warn.call_args_list)) self.assertEqual(mock_logger.error.call_count, 0, "Did not expect any errors. Got: {0}".format(mock_logger.error.call_args_list)) def test_it_should_log_unexpected_errors_as_errors(self): return_code = 99 command = "exit {0}".format(return_code) with patch("azurelinuxagent.common.utils.shellutil.logger", autospec=True) as mock_logger: shellutil.run_get_output(command, log_cmd=False, expected_errors=[return_code + 1]) self.assertEqual(mock_logger.error.call_count, 1) args, _ = mock_logger.error.call_args message = args[0] # message is similar to "Command: [exit 99], return code: [99], result: []" self.assertIn("[{0}]".format(command), message) self.assertIn("[{0}]".format(return_code), message) self.assertEqual(mock_logger.info.call_count, 0, "Did not expect any info messages. Got: {0}".format(mock_logger.info.call_args_list)) self.assertEqual(mock_logger.warn.call_count, 0, "Did not expect any warnings. Got: {0}".format(mock_logger.warn.call_args_list)) class RunCommandTestCase(AgentTestCase): """ Tests for shellutil.run_command/run_pipe """ def __create_tee_script(self, return_code=0): """ Creates a Python script that tees its stdin to stdout and stderr """ tee_script = os.path.join(self.tmp_dir, "tee.py") AgentTestCase.create_script(tee_script, """ import sys for line in sys.stdin: sys.stdout.write(line) sys.stderr.write(line) exit({0}) """.format(return_code)) return tee_script def test_run_command_should_execute_the_command(self): command = ["echo", "-n", "A TEST STRING"] ret = shellutil.run_command(command) self.assertEqual(ret, "A TEST STRING") def test_run_command_should_use_popen_arg_list(self): with patch("azurelinuxagent.common.utils.shellutil.subprocess.Popen", wraps=subprocess.Popen) as popen_patch: command = ["echo", "-n", "A TEST STRING"] ret = shellutil.run_command(command) self.assertEqual(ret, "A TEST STRING") self.assertEqual(popen_patch.call_count, 1) args, kwargs = popen_patch.call_args self.assertTrue(any(arg for arg in args[0] if "A TEST STRING" in arg), "command not being used") self.assertEqual(kwargs['env'].get(shellutil.PARENT_PROCESS_NAME), shellutil.AZURE_GUEST_AGENT, "Env flag not being used") def test_run_pipe_should_execute_a_pipe_with_two_commands(self): # Output the same string 3 times and then remove duplicates test_string = "A TEST STRING\n" pipe = [["echo", "-n", "-e", test_string * 3], ["uniq"]] output = shellutil.run_pipe(pipe) self.assertEqual(output, test_string) def test_run_pipe_should_execute_a_pipe_with_more_than_two_commands(self): # # The test pipe splits the output of "ls" in lines and then greps for "." # # Sample output of "ls -d .": # drwxrwxr-x 13 nam nam 4096 Nov 13 16:54 . # pipe = [["ls", "-ld", "."], ["sed", "-r", "s/\\s+/\\n/g"], ["grep", "\\."]] output = shellutil.run_pipe(pipe) self.assertEqual(".\n", output, "The pipe did not produce the expected output. Got: {0}".format(output)) def __it_should_raise_an_exception_when_the_command_fails(self, action): with self.assertRaises(shellutil.CommandError) as context_manager: action() exception = context_manager.exception self.assertIn("tee.py", str(exception), "The CommandError does not include the expected command") self.assertEqual(1, exception.returncode, "Unexpected return value from the test pipe") self.assertEqual("TEST_STRING\n", exception.stdout, "Unexpected stdout from the test pipe") self.assertEqual("TEST_STRING\n", exception.stderr, "Unexpected stderr from the test pipe") def test_run_command_should_raise_an_exception_when_the_command_fails(self): tee_script = self.__create_tee_script(return_code=1) self.__it_should_raise_an_exception_when_the_command_fails( lambda: shellutil.run_command(tee_script, input="TEST_STRING\n")) def test_run_pipe_should_raise_an_exception_when_the_last_command_fails(self): tee_script = self.__create_tee_script(return_code=1) self.__it_should_raise_an_exception_when_the_command_fails( lambda: shellutil.run_pipe([["echo", "-n", "TEST_STRING\n"], [tee_script]])) def __it_should_raise_an_exception_when_it_cannot_execute_the_command(self, action): with self.assertRaises(Exception) as context_manager: action() exception = context_manager.exception self.assertIn("No such file or directory", str(exception)) def test_run_command_should_raise_an_exception_when_it_cannot_execute_the_command(self): self.__it_should_raise_an_exception_when_it_cannot_execute_the_command( lambda: shellutil.run_command("nonexistent_command")) def test_run_pipe_should_raise_an_exception_when_it_cannot_execute_the_pipe(self): self.__it_should_raise_an_exception_when_it_cannot_execute_the_command( lambda: shellutil.run_pipe([["ls", "-ld", "."], ["nonexistent_command"], ["wc", "-l"]])) def __it_should_not_log_by_default(self, action): with patch("azurelinuxagent.common.utils.shellutil.logger", autospec=True) as mock_logger: try: action() except Exception: pass self.assertEqual(mock_logger.warn.call_count, 0, "Did not expect any WARNINGS; Got: {0}".format(mock_logger.warn.call_args)) self.assertEqual(mock_logger.error.call_count, 0, "Did not expect any ERRORS; Got: {0}".format(mock_logger.error.call_args)) def test_run_command_it_should_not_log_by_default(self): self.__it_should_not_log_by_default( lambda: shellutil.run_command(["ls", "nonexistent_file"])) # Raises a CommandError self.__it_should_not_log_by_default( lambda: shellutil.run_command("nonexistent_command")) # Raises an OSError def test_run_pipe_it_should_not_log_by_default(self): self.__it_should_not_log_by_default( lambda: shellutil.run_pipe([["date"], [self.__create_tee_script(return_code=1)]])) # Raises a CommandError self.__it_should_not_log_by_default( lambda: shellutil.run_pipe([["date"], ["nonexistent_command"]])) # Raises an OSError def __it_should_log_an_error_when_log_error_is_set(self, action, command): with patch("azurelinuxagent.common.utils.shellutil.logger.error") as mock_log_error: try: action() except Exception: pass self.assertEqual(mock_log_error.call_count, 1) args, _ = mock_log_error.call_args self.assertTrue(any(command in str(a) for a in args), "The command was not logged") self.assertTrue(any("2" in str(a) for a in args), "The command's return code was not logged") # errno 2: No such file or directory def test_run_command_should_log_an_error_when_log_error_is_set(self): self.__it_should_log_an_error_when_log_error_is_set( lambda: shellutil.run_command(["ls", "file-does-not-exist"], log_error=True), # Raises a CommandError command="ls") self.__it_should_log_an_error_when_log_error_is_set( lambda: shellutil.run_command("command-does-not-exist", log_error=True), # Raises a CommandError command="command-does-not-exist") def test_run_command_should_raise_when_both_the_input_and_stdin_parameters_are_specified(self): with tempfile.TemporaryFile() as input_file: with self.assertRaises(ValueError): shellutil.run_command(["cat"], input='0123456789ABCDEF', stdin=input_file) def test_run_command_should_read_the_command_input_from_the_input_parameter_when_it_is_a_string(self): command_input = 'TEST STRING' output = shellutil.run_command(["cat"], input=command_input) self.assertEqual(output, command_input, "The command did not process its input correctly; the output should match the input") def test_run_command_should_read_stdin_from_the_input_parameter_when_it_is_a_sequence_of_bytes(self): command_input = 'TEST BYTES' output = shellutil.run_command(["cat"], input=command_input) self.assertEqual(output, command_input, "The command did not process its input correctly; the output should match the input") def __it_should_read_the_command_input_from_the_stdin_parameter(self, action): command_input = 'TEST STRING\n' with tempfile.TemporaryFile() as input_file: input_file.write(command_input.encode()) input_file.seek(0) output = action(stdin=input_file) self.assertEqual(output, command_input, "The command did not process its input correctly; the output should match the input") def test_run_command_should_read_the_command_input_from_the_stdin_parameter(self): self.__it_should_read_the_command_input_from_the_stdin_parameter( lambda stdin: shellutil.run_command(["cat"], stdin=stdin)) def test_run_pipe_should_read_the_command_input_from_the_stdin_parameter(self): self.__it_should_read_the_command_input_from_the_stdin_parameter( lambda stdin: shellutil.run_pipe([["cat"], ["sort"]], stdin=stdin)) def __it_should_write_the_command_output_to_the_stdout_parameter(self, action): with tempfile.TemporaryFile() as output_file: captured_output = action(stdout=output_file) output_file.seek(0) command_output = ustr(output_file.read(), encoding='utf-8', errors='backslashreplace') self.assertEqual(command_output, "TEST STRING\n", "The command did not produce the correct output; the output should match the input") self.assertEqual("", captured_output, "No output should have been captured since it was redirected to a file. Output: [{0}]".format(captured_output)) def test_run_command_should_write_the_command_output_to_the_stdout_parameter(self): self.__it_should_write_the_command_output_to_the_stdout_parameter( lambda stdout: shellutil.run_command(["echo", "TEST STRING"], stdout=stdout)) def test_run_pipe_should_write_the_command_output_to_the_stdout_parameter(self): self.__it_should_write_the_command_output_to_the_stdout_parameter( lambda stdout: shellutil.run_pipe([["echo", "TEST STRING"], ["sort"]], stdout=stdout)) def __it_should_write_the_command_error_output_to_the_stderr_parameter(self, action): with tempfile.TemporaryFile() as output_file: action(stderr=output_file) output_file.seek(0) command_error_output = ustr(output_file.read(), encoding='utf-8', errors="backslashreplace") self.assertEqual("TEST STRING\n", command_error_output, "stderr was not redirected to the output file correctly") def test_run_command_should_write_the_command_error_output_to_the_stderr_parameter(self): self.__it_should_write_the_command_error_output_to_the_stderr_parameter( lambda stderr: shellutil.run_command(self.__create_tee_script(), input="TEST STRING\n", stderr=stderr)) def test_run_pipe_should_write_the_command_error_output_to_the_stderr_parameter(self): self.__it_should_write_the_command_error_output_to_the_stderr_parameter( lambda stderr: shellutil.run_pipe([["echo", "TEST STRING"], [self.__create_tee_script()]], stderr=stderr)) def test_run_pipe_should_capture_the_stderr_of_all_the_commands_in_the_pipe(self): with self.assertRaises(shellutil.CommandError) as context_manager: shellutil.run_pipe([ ["echo", "TEST STRING"], [self.__create_tee_script()], [self.__create_tee_script()], [self.__create_tee_script(return_code=1)]]) self.assertEqual("TEST STRING\n" * 3, context_manager.exception.stderr, "Expected 3 copies of the test string since there are 3 commands in the pipe") def test_run_command_should_return_a_string_by_default(self): output = shellutil.run_command(self.__create_tee_script(), input="TEST STRING") self.assertTrue(isinstance(output, ustr), "The return value should be a string. Got: '{0}'".format(type(output))) def test_run_pipe_should_return_a_string_by_default(self): output = shellutil.run_pipe([["echo", "TEST STRING"], [self.__create_tee_script()]]) self.assertTrue(isinstance(output, ustr), "The return value should be a string. Got: '{0}'".format(type(output))) def test_run_command_should_return_a_bytes_object_when_encode_output_is_false(self): output = shellutil.run_command(self.__create_tee_script(), input="TEST STRING", encode_output=False) self.assertTrue(isinstance(output, bytes), "The return value should be a bytes object. Got: '{0}'".format(type(output))) def test_run_pipe_should_return_a_bytes_object_when_encode_output_is_false(self): output = shellutil.run_pipe([["echo", "TEST STRING"], [self.__create_tee_script()]], encode_output=False) self.assertTrue(isinstance(output, bytes), "The return value should be a bytes object. Got: '{0}'".format(type(output))) def test_run_command_run_pipe_run_get_output_should_keep_track_of_the_running_commands(self): # The children processes run this script, which creates a file with the PIDs of the script and its parent and then sleeps for a long time child_script = os.path.join(self.tmp_dir, "write_pids.py") AgentTestCase.create_script(child_script, """ import os import sys import time with open(sys.argv[1], "w") as pid_file: pid_file.write("{0} {1}".format(os.getpid(), os.getppid())) time.sleep(120) """) threads = [] try: child_processes = [] parent_processes = [] try: # each of these files will contain the PIDs of the command that created it and its parent pid_files = [os.path.join(self.tmp_dir, "pids.txt.{0}".format(i)) for i in range(4)] # we test these functions in shellutil commands_to_execute = [ # run_get_output must be the first in this list; see the code to fetch the PIDs a few lines below lambda: shellutil.run_get_output("{0} {1}".format(child_script, pid_files[0])), lambda: shellutil.run_command([child_script, pid_files[1]]), lambda: shellutil.run_pipe([[child_script, pid_files[2]], [child_script, pid_files[3]]]), ] # start each command on a separate thread (since we need to examine the processes running the commands while they are running) def invoke(command): try: command() except shellutil.CommandError as command_error: if command_error.returncode != -9: # test cleanup terminates the commands, so this is expected raise for cmd in commands_to_execute: thread = threading.Thread(target=invoke, args=(cmd,)) thread.start() threads.append(thread) # now fetch the PIDs in the files created by the commands, but wait until they are created if not wait_for(lambda: all(os.path.exists(file) and os.path.getsize(file) > 0 for file in pid_files)): raise Exception("The child processes did not start within the allowed timeout") for sig_file in pid_files: with open(sig_file, "r") as read_handle: pids = read_handle.read().split() child_processes.append(int(pids[0])) parent_processes.append(int(pids[1])) # the first item to in the PIDs we fetched corresponds to run_get_output, which invokes the command using the # shell, so in that case we need to use the parent's pid (i.e. the shell that we started) started_commands = parent_processes[0:1] + child_processes[1:] # wait for all the commands to start def all_commands_running(): all_commands_running.running_commands = shellutil.get_running_commands() return len(all_commands_running.running_commands) >= len(commands_to_execute) + 1 # +1 because run_pipe starts 2 commands all_commands_running.running_commands = [] if not wait_for(all_commands_running): self.fail("shellutil.get_running_commands() did not report the expected number of commands after the allowed timeout.\nExpected: {0}\nGot: {1}".format( format_processes(started_commands), format_processes(all_commands_running.running_commands))) started_commands.sort() all_commands_running.running_commands.sort() self.assertEqual( started_commands, all_commands_running.running_commands, "shellutil.get_running_commands() did not return the expected commands.\nExpected: {0}\nGot: {1}".format( format_processes(started_commands), format_processes(all_commands_running.running_commands))) finally: # terminate the child processes, since they are blocked for pid in child_processes: os.kill(pid, signal.SIGKILL) # once the processes complete, their PIDs should go away def no_commands_running(): no_commands_running.running_commands = shellutil.get_running_commands() return len(no_commands_running.running_commands) == 0 no_commands_running.running_commands = [] if not wait_for(no_commands_running): self.fail("shellutil.get_running_commands() should return empty after the commands complete. Got: {0}".format( format_processes(no_commands_running.running_commands))) finally: for thread in threads: thread.join(timeout=5) if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests/utils/test_text_util.py000066400000000000000000000214041446033677600221450ustar00rootroot00000000000000# Copyright 2018 Microsoft Corporation # # 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. # # Requires Python 2.6+ and Openssl 1.0+ # import hashlib import os import unittest from distutils.version import LooseVersion as Version # pylint: disable=no-name-in-module,import-error import azurelinuxagent.common.utils.textutil as textutil from azurelinuxagent.common.future import ustr from tests.tools import AgentTestCase class TestTextUtil(AgentTestCase): def test_get_password_hash(self): with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'test_passwords.txt'), 'rb') as in_file: for data in in_file: # Remove bom on bytes data before it is converted into string. data = textutil.remove_bom(data) data = ustr(data, encoding='utf-8') password_hash = textutil.gen_password_hash(data, 6, 10) self.assertNotEqual(None, password_hash) def test_replace_non_ascii(self): data = ustr(b'\xef\xbb\xbfhehe', encoding='utf-8') self.assertEqual('hehe', textutil.replace_non_ascii(data)) data = "abcd\xa0e\xf0fghijk\xbblm" self.assertEqual("abcdefghijklm", textutil.replace_non_ascii(data)) data = "abcd\xa0e\xf0fghijk\xbblm" self.assertEqual("abcdXeXfghijkXlm", textutil.replace_non_ascii(data, replace_char='X')) self.assertEqual('', textutil.replace_non_ascii(None)) def test_remove_bom(self): #Test bom could be removed data = ustr(b'\xef\xbb\xbfhehe', encoding='utf-8') data = textutil.remove_bom(data) self.assertNotEqual(0xbb, data[0]) #bom is comprised of a sequence of three bytes and ff length of the input is shorter # than three bytes, remove_bom should not do anything data = u"\xa7" data = textutil.remove_bom(data) self.assertEqual(data, data[0]) data = u"\xa7\xef" data = textutil.remove_bom(data) self.assertEqual(u"\xa7", data[0]) self.assertEqual(u"\xef", data[1]) #Test string without BOM is not affected data = u"hehe" data = textutil.remove_bom(data) self.assertEqual(u"h", data[0]) data = u"" data = textutil.remove_bom(data) self.assertEqual(u"", data) data = u" " data = textutil.remove_bom(data) self.assertEqual(u" ", data) def test_version_compare(self): self.assertTrue(Version("1.0") < Version("1.1")) self.assertTrue(Version("1.9") < Version("1.10")) self.assertTrue(Version("1.9.9") < Version("1.10.0")) self.assertTrue(Version("1.0.0.0") < Version("1.2.0.0")) self.assertTrue(Version("1.0") <= Version("1.1")) self.assertTrue(Version("1.1") > Version("1.0")) self.assertTrue(Version("1.1") >= Version("1.0")) self.assertTrue(Version("1.0") == Version("1.0")) self.assertTrue(Version("1.0") >= Version("1.0")) self.assertTrue(Version("1.0") <= Version("1.0")) self.assertTrue(Version("1.9") < "1.10") self.assertTrue("1.9" < Version("1.10")) def test_get_bytes_from_pem(self): content = ("-----BEGIN CERTIFICATE-----\n" "certificate\n" "-----END CERTIFICATE----\n") base64_bytes = textutil.get_bytes_from_pem(content) self.assertEqual("certificate", base64_bytes) content = ("-----BEGIN PRIVATE KEY-----\n" "private key\n" "-----END PRIVATE Key-----\n") base64_bytes = textutil.get_bytes_from_pem(content) self.assertEqual("private key", base64_bytes) def test_swap_hexstring(self): data = [ ['12', 1, '21'], ['12', 2, '12'], ['12', 3, '012'], ['12', 4, '0012'], ['123', 1, '321'], ['123', 2, '2301'], ['123', 3, '123'], ['123', 4, '0123'], ['1234', 1, '4321'], ['1234', 2, '3412'], ['1234', 3, '234001'], ['1234', 4, '1234'], ['abcdef12', 1, '21fedcba'], ['abcdef12', 2, '12efcdab'], ['abcdef12', 3, 'f12cde0ab'], ['abcdef12', 4, 'ef12abcd'], ['aBcdEf12', 1, '21fEdcBa'], ['aBcdEf12', 2, '12EfcdaB'], ['aBcdEf12', 3, 'f12cdE0aB'], ['aBcdEf12', 4, 'Ef12aBcd'] ] for t in data: self.assertEqual(t[2], textutil.swap_hexstring(t[0], width=t[1])) def test_compress(self): result = textutil.compress('[stdout]\nHello World\n\n[stderr]\n\n') self.assertEqual('eJyLLi5JyS8tieXySM3JyVcIzy/KSeHiigaKphYVxXJxAQDAYQr2', result) def test_hash_empty_list(self): result = textutil.hash_strings([]) self.assertEqual(b'\xda9\xa3\xee^kK\r2U\xbf\xef\x95`\x18\x90\xaf\xd8\x07\t', result) def test_hash_list(self): test_list = ["abc", "123"] result_from_list = textutil.hash_strings(test_list) test_string = "".join(test_list) hash_from_string = hashlib.sha1() hash_from_string.update(test_string.encode()) self.assertEqual(result_from_list, hash_from_string.digest()) self.assertEqual(hash_from_string.hexdigest(), '6367c48dd193d56ea7b0baad25b19455e529f5ee') def test_empty_strings(self): self.assertTrue(textutil.is_str_none_or_whitespace(None)) self.assertTrue(textutil.is_str_none_or_whitespace(' ')) self.assertTrue(textutil.is_str_none_or_whitespace('\t')) self.assertTrue(textutil.is_str_none_or_whitespace('\n')) self.assertTrue(textutil.is_str_none_or_whitespace(' \t')) self.assertTrue(textutil.is_str_none_or_whitespace(' \r\n')) self.assertTrue(textutil.is_str_empty(None)) self.assertTrue(textutil.is_str_empty(' ')) self.assertTrue(textutil.is_str_empty('\t')) self.assertTrue(textutil.is_str_empty('\n')) self.assertTrue(textutil.is_str_empty(' \t')) self.assertTrue(textutil.is_str_empty(' \r\n')) self.assertFalse(textutil.is_str_none_or_whitespace(u' \x01 ')) self.assertFalse(textutil.is_str_none_or_whitespace(u'foo')) self.assertFalse(textutil.is_str_none_or_whitespace('bar')) self.assertFalse(textutil.is_str_empty(u' \x01 ')) self.assertFalse(textutil.is_str_empty(u'foo')) self.assertFalse(textutil.is_str_empty('bar')) hex_null_1 = u'\x00' hex_null_2 = u' \x00 ' self.assertFalse(textutil.is_str_none_or_whitespace(hex_null_1)) self.assertFalse(textutil.is_str_none_or_whitespace(hex_null_2)) self.assertTrue(textutil.is_str_empty(hex_null_1)) self.assertTrue(textutil.is_str_empty(hex_null_2)) self.assertNotEqual(textutil.is_str_none_or_whitespace(hex_null_1), textutil.is_str_empty(hex_null_1)) self.assertNotEqual(textutil.is_str_none_or_whitespace(hex_null_2), textutil.is_str_empty(hex_null_2)) def test_format_memory_value(self): """ Test formatting of memory amounts into human-readable units """ self.assertEqual(2048, textutil.format_memory_value('kilobytes', 2)) self.assertEqual(0, textutil.format_memory_value('kilobytes', 0)) self.assertEqual(2048000, textutil.format_memory_value('kilobytes', 2000)) self.assertEqual(2048 * 1024, textutil.format_memory_value('megabytes', 2)) self.assertEqual((1024 + 512) * 1024 * 1024, textutil.format_memory_value('gigabytes', 1.5)) self.assertRaises(ValueError, textutil.format_memory_value, 'KiloBytes', 1) self.assertRaises(TypeError, textutil.format_memory_value, 'bytes', None) def test_format_exception(self): """ Test formatting of exception into human-readable format """ def raise_exception(count=3): if count <= 1: raise Exception("Test Exception") raise_exception(count - 1) msg = "" try: raise_exception() except Exception as e: msg = textutil.format_exception(e) self.assertIn("Test Exception", msg) # Raise exception at count 1 after two nested calls since count starts at 3 self.assertEqual(2, msg.count("raise_exception(count - 1)")) if __name__ == '__main__': unittest.main() WALinuxAgent-2.9.1.1/tests_e2e/000077500000000000000000000000001446033677600161055ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests_e2e/orchestrator/000077500000000000000000000000001446033677600206245ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests_e2e/orchestrator/docker/000077500000000000000000000000001446033677600220735ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests_e2e/orchestrator/docker/Dockerfile000066400000000000000000000152371446033677600240750ustar00rootroot00000000000000# # * Sample command to build the image: # # docker build -t waagenttests . # # * Sample command to execute a container interactively: # # docker run --rm -it -v /home/nam/src/WALinuxAgent:/home/waagent/WALinuxAgent waagenttests bash --login # FROM ubuntu:latest LABEL description="Test environment for WALinuxAgent" SHELL ["/bin/bash", "-c"] # # Install the required packages as root # USER root RUN \ apt-get update && \ \ # \ # Install basic dependencies \ # \ apt-get install -y git python3.10 python3.10-dev wget bzip2 && \ ln /usr/bin/python3.10 /usr/bin/python3 && \ \ # \ # Install LISA dependencies \ # \ apt-get install -y git gcc libgirepository1.0-dev libcairo2-dev qemu-utils libvirt-dev \ python3-pip python3-venv && \ \ # \ # Install test dependencies \ # \ apt-get install -y zip && \ \ # \ # Create user waagent, which is used to execute the tests \ # \ groupadd waagent && \ useradd --shell /bin/bash --create-home -g waagent waagent && \ : # # Do the Poetry and LISA setup as waagent # USER waagent RUN \ export PATH="$HOME/.local/bin:$PATH" && \ \ # \ # Install LISA \ # \ cd $HOME && \ git clone https://github.com/microsoft/lisa.git && \ cd lisa && \ \ python3 -m pip install --upgrade pip && \ python3 -m pip install --editable .[azure,libvirt] --config-settings editable_mode=compat && \ \ # \ # Install additional test dependencies \ # \ python3 -m pip install distro msrestazure && \ python3 -m pip install azure-mgmt-compute --upgrade && \ \ # \ # Download Pypy to a known location, from which it will be installed to the test VMs. \ # \ mkdir $HOME/bin && \ wget https://downloads.python.org/pypy/pypy3.7-v7.3.5-linux64.tar.bz2 -O /tmp/pypy3.7-x64.tar.bz2 && \ wget https://downloads.python.org/pypy/pypy3.7-v7.3.5-aarch64.tar.bz2 -O /tmp/pypy3.7-arm64.tar.bz2 && \ \ # \ # The setup for the tests depends on a few paths; add those to the profile \ # \ echo 'export PYTHONPATH="$HOME/WALinuxAgent"' >> $HOME/.bash_profile && \ echo 'export PATH="$HOME/.local/bin:$PATH"' >> $HOME/.bash_profile && \ echo 'cd $HOME' >> $HOME/.bash_profile && \ : WALinuxAgent-2.9.1.1/tests_e2e/orchestrator/lib/000077500000000000000000000000001446033677600213725ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests_e2e/orchestrator/lib/agent_junit.py000066400000000000000000000053361446033677600242620ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. from typing import Type # # Disable those warnings, since 'lisa' is an external, non-standard, dependency # E0401: Unable to import 'dataclasses_json' (import-error) # E0401: Unable to import 'lisa.notifiers.junit' (import-error) # E0401: Unable to import 'lisa' (import-error) # E0401: Unable to import 'lisa.messages' (import-error) from dataclasses import dataclass # pylint: disable=E0401 from dataclasses_json import dataclass_json # pylint: disable=E0401 from lisa.notifiers.junit import JUnit # pylint: disable=E0401 from lisa import schema # pylint: disable=E0401 from lisa.messages import ( # pylint: disable=E0401 MessageBase, TestResultMessage, ) @dataclass_json() @dataclass class AgentJUnitSchema(schema.Notifier): path: str = "agent.junit.xml" class AgentJUnit(JUnit): @classmethod def type_name(cls) -> str: return "agent.junit" @classmethod def type_schema(cls) -> Type[schema.TypedSchema]: return AgentJUnitSchema def _received_message(self, message: MessageBase) -> None: # The Agent sends its own TestResultMessage and marks them as "AgentTestResultMessage"; for the # test results sent by LISA itself, we change the suite name to "_Runbook_" in order to separate them # from actual test results. if isinstance(message, TestResultMessage) and message.type != "AgentTestResultMessage": if "Unexpected error in AgentTestSuite" in message.message: # Ignore these errors, they are already reported as AgentTestResultMessages return message.suite_full_name = "_Runbook_" message.suite_name = message.suite_full_name image = message.information.get('image') if image is not None: # NOTE: message.information['environment'] is similar to "[generated_2]" and can be correlated # with the main LISA log to find the specific VM for the message. message.full_name = f"{image} [{message.information['environment']}]" message.name = message.full_name super()._received_message(message) WALinuxAgent-2.9.1.1/tests_e2e/orchestrator/lib/agent_test_loader.py000066400000000000000000000264561446033677600254440ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # import importlib.util # E0401: Unable to import 'yaml' (import-error) import yaml # pylint: disable=E0401 from pathlib import Path from typing import Any, Dict, List, Type import tests_e2e from tests_e2e.tests.lib.agent_test import AgentTest class TestSuiteInfo(object): """ Description of a test suite """ # The name of the test suite name: str # The tests that comprise the suite tests: List[Type[AgentTest]] # Images or image sets (as defined in images.yml) on which the suite must run. images: List[str] # The location (region) on which the suite must run; if empty, the suite can run on any location location: str # Whether this suite must run on its own test VM owns_vm: bool def __str__(self): return self.name class VmImageInfo(object): # The URN of the image (publisher, offer, version separated by spaces) urn: str # Indicates that the image is available only on those locations. If empty, the image should be available in all locations locations: List[str] # Indicates that the image is available only for those VM sizes. If empty, the image should be available for all VM sizes vm_sizes: List[str] def __str__(self): return self.urn class AgentTestLoader(object): """ Loads a given set of test suites from the YAML configuration files. """ def __init__(self, test_suites: str): """ Loads the specified 'test_suites', which are given as a string of comma-separated suite names or a YAML description of a single test_suite. When given as a comma-separated list, each item must correspond to the name of the YAML files describing s suite (those files are located under the .../WALinuxAgent/tests_e2e/test_suites directory). For example, if test_suites == "agent_bvt, fast_track" then this method will load files agent_bvt.yml and fast_track.yml. When given as a YAML string, the value must correspond to the description a single test suite, for example name: "AgentBvt" tests: - "bvts/extension_operations.py" - "bvts/run_command.py" - "bvts/vm_access.py" """ self.__test_suites: List[TestSuiteInfo] = self._load_test_suites(test_suites) self.__images: Dict[str, List[VmImageInfo]] = self._load_images() self._validate() _SOURCE_CODE_ROOT: Path = Path(tests_e2e.__path__[0]) @property def test_suites(self) -> List[TestSuiteInfo]: return self.__test_suites @property def images(self) -> Dict[str, List[VmImageInfo]]: """ A dictionary where, for each item, the key is the name of an image or image set and the value is a list of VmImageInfos for the corresponding images. """ return self.__images def _validate(self): """ Performs some basic validations on the data loaded from the YAML description files """ for suite in self.test_suites: # Validate that the images the suite must run on are in images.yml for image in suite.images: if image not in self.images: raise Exception(f"Invalid image reference in test suite {suite.name}: Can't find {image} in images.yml") # If the suite specifies a location, validate that the images it uses are available in that location if suite.location != '': for suite_image in suite.images: for image in self.images[suite_image]: if len(image.locations) > 0: if suite.location not in image.locations: raise Exception(f"Test suite {suite.name} must be executed in {suite.location}, but <{image.urn}> is not available in that location") @staticmethod def _load_test_suites(test_suites: str) -> List[TestSuiteInfo]: # # Attempt to parse 'test_suites' as the YML description of a single suite # parsed = yaml.safe_load(test_suites) # # A comma-separated list (e.g. "foo", "foo, bar", etc.) is valid YAML, but it is parsed as a string. An actual test suite would # be parsed as a dictionary. If it is a dict, take is as the YML description of a single test suite # if isinstance(parsed, dict): return [AgentTestLoader._load_test_suite(parsed)] # # If test_suites is not YML, then it should be a comma-separated list of description files # description_files: List[Path] = [AgentTestLoader._SOURCE_CODE_ROOT/"test_suites"/f"{t.strip()}.yml" for t in test_suites.split(',')] return [AgentTestLoader._load_test_suite(f) for f in description_files] @staticmethod def _load_test_suite(description_file: Path) -> TestSuiteInfo: """ Loads the description of a TestSuite from its YAML file. A test suite has 5 properties: name, tests, images, location, and owns-vm. For example: name: "AgentBvt" tests: - "bvts/extension_operations.py" - "bvts/run_command.py" - "bvts/vm_access.py" images: "endorsed" location: "eastuseaup" owns-vm: true * name - A string used to identify the test suite * tests - A list of the tests in the suite. Each test is specified by the path for its source code relative to WALinuxAgent/tests_e2e/tests. * images - A string, or a list of strings, specifying the images on which the test suite must be executed. Each value can be the name of a single image (e.g."ubuntu_2004"), or the name of an image set (e.g. "endorsed"). The names for images and image sets are defined in WALinuxAgent/tests_e2e/tests_suites/images.yml. * location - [Optional; string] If given, the test suite must be executed on that location. If not specified, or set to an empty string, the test suite will be executed in the default location. This is useful for test suites that exercise a feature that is enabled only in certain regions. * owns-vm - [Optional; boolean] By default all suites in a test run are executed on the same test VMs; if this value is set to True, new test VMs will be created and will be used exclusively for this test suite. This is useful for suites that modify the test VMs in such a way that the setup may cause problems in other test suites (for example, some tests targeted to the HGAP block internet access in order to force the agent to use the HGAP). """ test_suite: Dict[str, Any] = AgentTestLoader._load_file(description_file) if any([test_suite.get(p) is None for p in ["name", "tests", "images"]]): raise Exception(f"Invalid test suite: {description_file}. 'name', 'tests', and 'images' are required properties") test_suite_info = TestSuiteInfo() test_suite_info.name = test_suite["name"] test_suite_info.tests = [] source_files = [AgentTestLoader._SOURCE_CODE_ROOT/"tests"/t for t in test_suite["tests"]] for f in source_files: test_suite_info.tests.extend(AgentTestLoader._load_test_classes(f)) images = test_suite["images"] if isinstance(images, str): test_suite_info.images = [images] else: test_suite_info.images = images test_suite_info.location = test_suite.get("location") if test_suite_info.location is None: test_suite_info.location = "" test_suite_info.owns_vm = "owns-vm" in test_suite and test_suite["owns-vm"] return test_suite_info @staticmethod def _load_test_classes(source_file: Path) -> List[Type[AgentTest]]: """ Takes a 'source_file', which must be a Python module, and returns a list of all the classes derived from AgentTest. """ spec = importlib.util.spec_from_file_location(f"tests_e2e.tests.{source_file.name}", str(source_file)) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) # return all the classes in the module that are subclasses of AgentTest but are not AgentTest itself. return [v for v in module.__dict__.values() if isinstance(v, type) and issubclass(v, AgentTest) and v != AgentTest] @staticmethod def _load_images() -> Dict[str, List[VmImageInfo]]: """ Loads images.yml into a dictionary where, for each item, the key is an image or image set and the value is a list of VmImageInfos for the corresponding images. See the comments in image.yml for a description of the structure of each item. """ image_descriptions = AgentTestLoader._load_file(AgentTestLoader._SOURCE_CODE_ROOT/"test_suites"/"images.yml") if "images" not in image_descriptions: raise Exception("images.yml is missing the 'images' item") images = {} # first load the images as 1-item lists for name, description in image_descriptions["images"].items(): i = VmImageInfo() if isinstance(description, str): i.urn = description i.locations = [] i.vm_sizes = [] else: if "urn" not in description: raise Exception(f"Image {name} is missing the 'urn' property: {description}") i.urn = description["urn"] i.locations = description["locations"] if "locations" in description else [] i.vm_sizes = description["vm_sizes"] if "vm_sizes" in description else [] images[name] = [i] # now load the image-sets, mapping them to the images that we just computed for image_set_name, image_list in image_descriptions["image-sets"].items(): # the same name cannot denote an image and an image-set if image_set_name in images: raise Exception(f"Invalid image-set in images.yml: {image_set_name}. The name is used by an existing image") images_in_set = [] for i in image_list: if i not in images: raise Exception(f"Can't find image {i} (referenced by image-set {image_set_name}) in images.yml") images_in_set.extend(images[i]) images[image_set_name] = images_in_set return images @staticmethod def _load_file(file: Path) -> Dict[str, Any]: """Helper to load a YML file""" try: with file.open() as f: return yaml.safe_load(f) except Exception as e: raise Exception(f"Can't load {file}: {e}") WALinuxAgent-2.9.1.1/tests_e2e/orchestrator/lib/agent_test_suite.py000066400000000000000000000722711446033677600253230ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # import contextlib import datetime import json import logging import traceback import uuid from pathlib import Path from threading import current_thread, RLock from typing import Any, Dict, List # Disable those warnings, since 'lisa' is an external, non-standard, dependency # E0401: Unable to import 'lisa' (import-error) # etc from lisa import ( # pylint: disable=E0401 Environment, Logger, Node, notifier, simple_requirement, TestCaseMetadata, TestSuite as LisaTestSuite, TestSuiteMetadata, ) from lisa.environment import EnvironmentStatus # pylint: disable=E0401 from lisa.messages import TestStatus, TestResultMessage # pylint: disable=E0401 from lisa.sut_orchestrator import AZURE # pylint: disable=E0401 from lisa.sut_orchestrator.azure.common import get_node_context, AzureNodeSchema # pylint: disable=E0401 import makepkg from azurelinuxagent.common.version import AGENT_VERSION from tests_e2e.orchestrator.lib.agent_test_loader import TestSuiteInfo from tests_e2e.tests.lib.agent_log import AgentLog from tests_e2e.tests.lib.agent_test import TestSkipped from tests_e2e.tests.lib.agent_test_context import AgentTestContext from tests_e2e.tests.lib.identifiers import VmIdentifier from tests_e2e.tests.lib.logging import log from tests_e2e.tests.lib.logging import set_current_thread_log from tests_e2e.tests.lib.agent_log import AgentLogRecord from tests_e2e.tests.lib.shell import run_command from tests_e2e.tests.lib.ssh_client import SshClient def _initialize_lisa_logger(): """ Customizes the LISA logger. The default behavior of this logger is too verbose, which makes reading the logs difficult. We set up a more succinct formatter and decrease the log level to INFO (the default is VERBOSE). In the future we may consider making this customization settable at runtime in case we need to debug LISA issues. """ logger: Logger = logging.getLogger("lisa") logger.setLevel(logging.INFO) formatter = logging.Formatter('%(asctime)s.%(msecs)03d [%(levelname)s] [%(threadName)s] %(message)s', datefmt="%Y-%m-%dT%H:%M:%SZ") for handler in logger.handlers: handler.setFormatter(formatter) # # We want to customize the LISA logger as early as possible, so we do it when this module is first imported. That will # happen early in the LISA workflow, when it loads the test suites to execute. # _initialize_lisa_logger() # # Helper to change the current thread name temporarily # @contextlib.contextmanager def _set_thread_name(name: str): initial_name = current_thread().name current_thread().name = name try: yield finally: current_thread().name = initial_name # # Possible values for the collect_logs parameter # class CollectLogs(object): Always = 'always' # Always collect logs Failed = 'failed' # Collect logs only on test failures No = 'no' # Never collect logs @TestSuiteMetadata(area="waagent", category="", description="") class AgentTestSuite(LisaTestSuite): """ Manages the setup of test VMs and execution of Agent test suites. This class acts as the interface with the LISA framework, which will invoke the execute() method when a runbook is executed. """ class _Context(AgentTestContext): def __init__(self, vm: VmIdentifier, paths: AgentTestContext.Paths, connection: AgentTestContext.Connection): super().__init__(vm=vm, paths=paths, connection=connection) # These are initialized by AgentTestSuite._set_context(). self.log_path: Path = None self.lisa_log: Logger = None self.node: Node = None self.runbook_name: str = None self.environment_name: str = None self.is_vhd: bool = None self.test_suites: List[AgentTestSuite] = None self.collect_logs: str = None self.skip_setup: bool = None self.ssh_client: SshClient = None def __init__(self, metadata: TestSuiteMetadata) -> None: super().__init__(metadata) # The context is initialized by _set_context() via the call to execute() self.__context: AgentTestSuite._Context = None def _initialize(self, node: Node, variables: Dict[str, Any], lisa_working_path: str, lisa_log_path: str, lisa_log: Logger): connection_info = node.connection_info node_context = get_node_context(node) runbook = node.capability.get_extended_runbook(AzureNodeSchema, AZURE) self.__context = self._Context( vm=VmIdentifier( location=runbook.location, subscription=node.features._platform.subscription_id, resource_group=node_context.resource_group_name, name=node_context.vm_name), paths=AgentTestContext.Paths( working_directory=self._get_working_directory(lisa_working_path), remote_working_directory=Path('/home')/connection_info['username']), connection=AgentTestContext.Connection( ip_address=connection_info['address'], username=connection_info['username'], private_key_file=connection_info['private_key_file'], ssh_port=connection_info['port'])) self.__context.log_path = self._get_log_path(variables, lisa_log_path) self.__context.lisa_log = lisa_log self.__context.node = node self.__context.is_vhd = self._get_optional_parameter(variables, "c_vhd") != "" self.__context.environment_name = f"{node.os.name}-vhd" if self.__context.is_vhd else self._get_required_parameter(variables, "c_env_name") self.__context.test_suites = self._get_required_parameter(variables, "c_test_suites") self.__context.collect_logs = self._get_required_parameter(variables, "collect_logs") self.__context.skip_setup = self._get_required_parameter(variables, "skip_setup") self.__context.ssh_client = SshClient(ip_address=self.__context.vm_ip_address, username=self.__context.username, private_key_file=self.__context.private_key_file) @staticmethod def _get_required_parameter(variables: Dict[str, Any], name: str) -> Any: value = variables.get(name) if value is None: raise Exception(f"The runbook is missing required parameter '{name}'") return value @staticmethod def _get_optional_parameter(variables: Dict[str, Any], name: str, default_value: Any = "") -> Any: value = variables.get(name) if value is None: return default_value return value @staticmethod def _get_log_path(variables: Dict[str, Any], lisa_log_path: str) -> Path: # NOTE: If "log_path" is not given as argument to the runbook, use a path derived from LISA's log for the test suite. # That path is derived from LISA's "--log_path" command line argument and has a value similar to # "<--log_path>/20230217/20230217-040022-342/tests/20230217-040119-288-agent_test_suite"; use the directory # 2 levels up. log_path = variables.get("log_path") if log_path is not None and len(log_path) > 0: return Path(log_path) return Path(lisa_log_path).parent.parent @staticmethod def _get_working_directory(lisa_working_path: str) -> Path: # LISA's "working_path" has a value similar to # "<--working_path>/20230322/20230322-194430-287/tests/20230322-194451-333-agent_test_suite # where "<--working_path>" is the value given to the --working_path command line argument. Create the working for # the AgentTestSuite as # "<--working_path>/20230322/20230322-194430-287/waagent # This directory will be unique for each execution of the runbook ("20230322-194430" is the timestamp and "287" is a # unique ID per execution) return Path(lisa_working_path).parent.parent / "waagent" @property def context(self): if self.__context is None: raise Exception("The context for the AgentTestSuite has not been initialized") return self.__context # # Test suites within the same runbook may be executed concurrently, and setup needs to be done only once. # We use this lock to allow only 1 thread to do the setup. Setup completion is marked using the 'completed' # file: the thread doing the setup creates the file and threads that find that the file already exists # simply skip setup. # _setup_lock = RLock() def _setup(self) -> None: """ Prepares the test suite for execution (currently, it just builds the agent package) Returns the path to the agent package. """ self._setup_lock.acquire() try: log.info("") log.info("**************************************** [Build] ****************************************") log.info("") completed: Path = self.context.working_directory/"completed" if completed.exists(): log.info("Found %s. Build has already been done, skipping.", completed) return self.context.lisa_log.info("Building test agent") log.info("Creating working directory: %s", self.context.working_directory) self.context.working_directory.mkdir(parents=True) self._build_agent_package() log.info("Completed setup, creating %s", completed) completed.touch() finally: self._setup_lock.release() def _build_agent_package(self) -> None: """ Builds the agent package and returns the path to the package. """ log.info("Building agent package to %s", self.context.working_directory) makepkg.run(agent_family="Test", output_directory=str(self.context.working_directory), log=log) package_path: Path = self._get_agent_package_path() if not package_path.exists(): raise Exception(f"Can't find the agent package at {package_path}") log.info("Built agent package as %s", package_path) def _get_agent_package_path(self) -> Path: """ Returns the path to the agent package. """ return self.context.working_directory/"eggs"/f"WALinuxAgent-{AGENT_VERSION}.zip" def _clean_up(self) -> None: """ Cleans up any leftovers from the test suite run. Currently just an empty placeholder for future use. """ def _setup_node(self) -> None: """ Prepares the remote node for executing the test suite (installs tools and the test agent, etc) """ self.context.lisa_log.info("Setting up test node") log.info("") log.info("************************************** [Node Setup] **************************************") log.info("") log.info("Test Node: %s", self.context.vm.name) log.info("IP Address: %s", self.context.vm_ip_address) log.info("Resource Group: %s", self.context.vm.resource_group) log.info("") # # Ensure that the correct version (x84 vs ARM64) Pypy has been downloaded; it is pre-downloaded to /tmp on the container image # used for Azure Pipelines runs, but for developer runs it may need to be downloaded. # if self.context.ssh_client.get_architecture() == "aarch64": pypy_path = Path("/tmp/pypy3.7-arm64.tar.bz2") pypy_download = "https://downloads.python.org/pypy/pypy3.7-v7.3.5-aarch64.tar.bz2" else: pypy_path = Path("/tmp/pypy3.7-x64.tar.bz2") pypy_download = "https://downloads.python.org/pypy/pypy3.7-v7.3.5-linux64.tar.bz2" if pypy_path.exists(): log.info("Found Pypy at %s", pypy_path) else: log.info("Downloading %s to %s", pypy_download, pypy_path) run_command(["wget", pypy_download, "-O", pypy_path]) # # Create a tarball with the files we need to copy to the test node. The tarball includes two directories: # # * bin - Executables file (Bash and Python scripts) # * lib - Library files (Python modules) # # After extracting the tarball on the test node, 'bin' will be added to PATH and PYTHONPATH will be set to 'lib'. # # Note that executables are placed directly under 'bin', while the path for Python modules is preserved under 'lib. # tarball_path: Path = Path("/tmp/waagent.tar") log.info("Creating %s with the files need on the test node", tarball_path) log.info("Adding orchestrator/scripts") run_command(['tar', 'cvf', str(tarball_path), '--transform=s,.*/,bin/,', '-C', str(self.context.test_source_directory/"orchestrator"/"scripts"), '.']) # log.info("Adding tests/scripts") # run_command(['tar', 'rvf', str(tarball_path), '--transform=s,.*/,bin/,', '-C', str(self.context.test_source_directory/"tests"/"scripts"), '.']) log.info("Adding tests/lib") run_command(['tar', 'rvf', str(tarball_path), '--transform=s,^,lib/,', '-C', str(self.context.test_source_directory.parent), '--exclude=__pycache__', 'tests_e2e/tests/lib']) log.info("Contents of %s:\n\n%s", tarball_path, run_command(['tar', 'tvf', str(tarball_path)])) # # Cleanup the test node (useful for developer runs) # log.info('Preparing the test node for setup') # Note that removing lib requires sudo, since a Python cache may have been created by tests using sudo self.context.ssh_client.run_command("rm -rvf ~/{bin,lib,tmp}", use_sudo=True) # # Copy the tarball, Pypy and the test Agent to the test node # target_path = Path("~")/"tmp" self.context.ssh_client.run_command(f"mkdir {target_path}") log.info("Copying %s to %s:%s", tarball_path, self.context.node.name, target_path) self.context.ssh_client.copy_to_node(tarball_path, target_path) log.info("Copying %s to %s:%s", pypy_path, self.context.node.name, target_path) self.context.ssh_client.copy_to_node(pypy_path, target_path) agent_package_path: Path = self._get_agent_package_path() log.info("Copying %s to %s:%s", agent_package_path, self.context.node.name, target_path) self.context.ssh_client.copy_to_node(agent_package_path, target_path) # # Extract the tarball and execute the install scripts # log.info('Installing tools on the test node') command = f"tar xf {target_path/tarball_path.name} && ~/bin/install-tools" log.info("%s\n%s", command, self.context.ssh_client.run_command(command)) if self.context.is_vhd: log.info("Using a VHD; will not install the Test Agent.") else: log.info("Installing the Test Agent on the test node") command = f"install-agent --package ~/tmp/{agent_package_path.name} --version {AGENT_VERSION}" log.info("%s\n%s", command, self.context.ssh_client.run_command(command, use_sudo=True)) log.info("Completed test node setup") def _collect_node_logs(self) -> None: """ Collects the test logs from the remote machine and copies them to the local machine """ try: # Collect the logs on the test machine into a compressed tarball self.context.lisa_log.info("Collecting logs on test node") log.info("Collecting logs on test node") stdout = self.context.ssh_client.run_command("collect-logs", use_sudo=True) log.info(stdout) # Copy the tarball to the local logs directory remote_path = "/tmp/waagent-logs.tgz" local_path = self.context.log_path/'{0}.tgz'.format(self.context.environment_name) log.info("Copying %s:%s to %s", self.context.node.name, remote_path, local_path) self.context.ssh_client.copy_from_node(remote_path, local_path) except: # pylint: disable=bare-except log.exception("Failed to collect logs from the test machine") # NOTES: # # * environment_status=EnvironmentStatus.Deployed skips most of LISA's initialization of the test node, which is not needed # for agent tests. # # * We need to take the LISA Logger using a parameter named 'log'; this parameter hides tests_e2e.tests.lib.logging.log. # Be aware then, that within this method 'log' refers to the LISA log, and elsewhere it refers to tests_e2e.tests.lib.logging.log. # # W0621: Redefining name 'log' from outer scope (line 53) (redefined-outer-name) @TestCaseMetadata(description="", priority=0, requirement=simple_requirement(environment_status=EnvironmentStatus.Deployed)) def main(self, node: Node, environment: Environment, variables: Dict[str, Any], working_path: str, log_path: str, log: Logger): # pylint: disable=redefined-outer-name """ Entry point from LISA """ self._initialize(node, variables, working_path, log_path, log) self._execute(environment, variables) def _execute(self, environment: Environment, variables: Dict[str, Any]): """ Executes each of the AgentTests included in the "c_test_suites" variable (which is generated by the AgentTestSuitesCombinator). """ # Set the thread name to the name of the environment. The thread name is added to each item in LISA's log. with _set_thread_name(self.context.environment_name): log_path: Path = self.context.log_path/f"env-{self.context.environment_name}.log" with set_current_thread_log(log_path): start_time: datetime.datetime = datetime.datetime.now() success = True try: # Log the environment's name and the variables received from the runbook (note that we need to expand the names of the test suites) log.info("LISA Environment (for correlation with the LISA log): %s", environment.name) log.info("Runbook variables:") for name, value in variables.items(): log.info(" %s: %s", name, value if name != 'c_test_suites' else [t.name for t in value]) test_suite_success = True try: if not self.context.skip_setup: self._setup() if not self.context.skip_setup: self._setup_node() # pylint seems to think self.context.test_suites is not iterable. Suppressing warning, since its type is List[AgentTestSuite] # E1133: Non-iterable value self.context.test_suites is used in an iterating context (not-an-iterable) for suite in self.context.test_suites: # pylint: disable=E1133 log.info("Executing test suite %s", suite.name) self.context.lisa_log.info("Executing Test Suite %s", suite.name) test_suite_success = self._execute_test_suite(suite) and test_suite_success test_suite_success = self._check_agent_log() and test_suite_success finally: collect = self.context.collect_logs if collect == CollectLogs.Always or collect == CollectLogs.Failed and not test_suite_success: self._collect_node_logs() except Exception as e: # pylint: disable=bare-except # Report the error and raise an exception to let LISA know that the test errored out. success = False log.exception("UNEXPECTED ERROR.") self._report_test_result( self.context.environment_name, "Unexpected Error", TestStatus.FAILED, start_time, message="UNEXPECTED ERROR.", add_exception_stack_trace=True) raise Exception(f"[{self.context.environment_name}] Unexpected error in AgentTestSuite: {e}") finally: self._clean_up() if not success: self._mark_log_as_failed() def _execute_test_suite(self, suite: TestSuiteInfo) -> bool: """ Executes the given test suite and returns True if all the tests in the suite succeeded. """ suite_name = suite.name suite_full_name = f"{suite_name}-{self.context.environment_name}" suite_start_time: datetime.datetime = datetime.datetime.now() success: bool = True # True if all the tests succeed with _set_thread_name(suite_full_name): # The thread name is added to the LISA log log_path: Path = self.context.log_path/f"{suite_full_name}.log" with set_current_thread_log(log_path): try: log.info("") log.info("**************************************** %s ****************************************", suite_name) log.info("") summary: List[str] = [] for test in suite.tests: test_name = test.__name__ test_full_name = f"{suite_name}-{test_name}" test_start_time: datetime.datetime = datetime.datetime.now() log.info("******** Executing %s", test_name) self.context.lisa_log.info("Executing test %s", test_full_name) try: test(self.context).run() summary.append(f"[Passed] {test_name}") log.info("******** [Passed] %s", test_name) self.context.lisa_log.info("[Passed] %s", test_full_name) self._report_test_result( suite_full_name, test_name, TestStatus.PASSED, test_start_time) except TestSkipped as e: summary.append(f"[Skipped] {test_name}") log.info("******** [Skipped] %s: %s", test_name, e) self.context.lisa_log.info("******** [Skipped] %s", test_full_name) self._report_test_result( suite_full_name, test_name, TestStatus.SKIPPED, test_start_time, message=str(e)) except AssertionError as e: success = False summary.append(f"[Failed] {test_name}") log.error("******** [Failed] %s: %s", test_name, e) self.context.lisa_log.error("******** [Failed] %s", test_full_name) self._report_test_result( suite_full_name, test_name, TestStatus.FAILED, test_start_time, message=str(e)) except: # pylint: disable=bare-except success = False summary.append(f"[Error] {test_name}") log.exception("UNHANDLED EXCEPTION IN %s", test_name) self.context.lisa_log.exception("UNHANDLED EXCEPTION IN %s", test_full_name) self._report_test_result( suite_full_name, test_name, TestStatus.FAILED, test_start_time, message="Unhandled exception.", add_exception_stack_trace=True) log.info("") log.info("********* [Test Results]") log.info("") for r in summary: log.info("\t%s", r) log.info("") except: # pylint: disable=bare-except success = False self._report_test_result( suite_full_name, suite_name, TestStatus.FAILED, suite_start_time, message=f"Unhandled exception while executing test suite {suite_name}.", add_exception_stack_trace=True) finally: if not success: self._mark_log_as_failed() return success def _check_agent_log(self) -> bool: """ Checks the agent log for errors; returns true on success (no errors int the log) """ start_time: datetime.datetime = datetime.datetime.now() try: self.context.lisa_log.info("Checking agent log on the test node") log.info("Checking agent log on the test node") output = self.context.ssh_client.run_command("check-agent-log.py -j") errors = json.loads(output, object_hook=AgentLogRecord.from_dictionary) # Individual tests may have rules to ignore known errors; filter those out ignore_error_rules = [] # pylint seems to think self.context.test_suites is not iterable. Suppressing warning, since its type is List[AgentTestSuite] # E1133: Non-iterable value self.context.test_suites is used in an iterating context (not-an-iterable) for suite in self.context.test_suites: # pylint: disable=E1133 for test in suite.tests: ignore_error_rules.extend(test(self.context).get_ignore_error_rules()) if len(ignore_error_rules) > 0: new = [] for e in errors: if not AgentLog.matches_ignore_rule(e, ignore_error_rules): new.append(e) errors = new if len(errors) == 0: # If no errors, we are done; don't create a log or test result. log.info("There are no errors in the agent log") return True message = f"Detected {len(errors)} error(s) in the agent log" self.context.lisa_log.error(message) log.error("%s:\n\n%s\n", message, '\n'.join(['\t\t' + e.text.replace('\n', '\n\t\t') for e in errors])) self._mark_log_as_failed() self._report_test_result( self.context.environment_name, "CheckAgentLog", TestStatus.FAILED, start_time, message=message + ' - First few errors:\n' + '\n'.join([e.text for e in errors[0:3]])) except: # pylint: disable=bare-except log.exception("Error checking agent log") self._report_test_result( self.context.environment_name, "CheckAgentLog", TestStatus.FAILED, start_time, "Error checking agent log", add_exception_stack_trace=True) return False @staticmethod def _mark_log_as_failed(): """ Adds a message to indicate the log contains errors. """ log.info("MARKER-LOG-WITH-ERRORS") @staticmethod def _report_test_result( suite_name: str, test_name: str, status: TestStatus, start_time: datetime.datetime, message: str = "", add_exception_stack_trace: bool = False ) -> None: """ Reports a test result to the junit notifier """ # The junit notifier requires an initial RUNNING message in order to register the test in its internal cache. msg: TestResultMessage = TestResultMessage() msg.type = "AgentTestResultMessage" msg.id_ = str(uuid.uuid4()) msg.status = TestStatus.RUNNING msg.suite_full_name = suite_name msg.suite_name = msg.suite_full_name msg.full_name = test_name msg.name = msg.full_name msg.elapsed = 0 notifier.notify(msg) # Now send the actual result. The notifier pipeline makes a deep copy of the message so it is OK to re-use the # same object and just update a few fields. If using a different object, be sure that the "id_" is the same. msg.status = status msg.message = message if add_exception_stack_trace: msg.stacktrace = traceback.format_exc() msg.elapsed = (datetime.datetime.now() - start_time).total_seconds() notifier.notify(msg) WALinuxAgent-2.9.1.1/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py000066400000000000000000000253401446033677600275330ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT license. import logging import re import urllib.parse from dataclasses import dataclass, field from typing import Any, Dict, List, Optional, Type # E0401: Unable to import 'dataclasses_json' (import-error) from dataclasses_json import dataclass_json # pylint: disable=E0401 # Disable those warnings, since 'lisa' is an external, non-standard, dependency # E0401: Unable to import 'lisa' (import-error) # etc from lisa import schema # pylint: disable=E0401 from lisa.combinator import Combinator # pylint: disable=E0401 from lisa.util import field_metadata # pylint: disable=E0401 from tests_e2e.orchestrator.lib.agent_test_loader import AgentTestLoader, VmImageInfo @dataclass_json() @dataclass class AgentTestSuitesCombinatorSchema(schema.Combinator): test_suites: str = field( default_factory=str, metadata=field_metadata(required=True) ) cloud: str = field( default_factory=str, metadata=field_metadata(required=True) ) location: str = field( default_factory=str, metadata=field_metadata(required=True) ) image: str = field( default_factory=str, metadata=field_metadata(required=False) ) vm_size: str = field( default_factory=str, metadata=field_metadata(required=False) ) vm_name: str = field( default_factory=str, metadata=field_metadata(required=False) ) class AgentTestSuitesCombinator(Combinator): """ The "agent_test_suites" combinator returns a list of variables that specify the environments (i.e. test VMs) that the agent test suites must be executed on: * c_env_name: Unique name for the environment, e.g. "0001-com-ubuntu-server-focal-20_04-lts-westus2" * c_marketplace_image: e.g. "Canonical UbuntuServer 18.04-LTS latest", * c_location: e.g. "westus2", * c_vm_size: e.g. "Standard_D2pls_v5" * c_vhd: e.g "https://rhel.blob.core.windows.net/images/RHEL_8_Standard-8.3.202006170423.vhd?se=..." * c_test_suites: e.g. [AgentBvt, FastTrack] (c_marketplace_image, c_location, c_vm_size) and vhd are mutually exclusive and define the environment (i.e. the test VM) in which the test will be executed. c_test_suites defines the test suites that should be executed in that environment. The 'vm_name' runbook parameter can be used to execute the test suites on an existing VM. In that case, the combinator generates a single item with these variables: * c_env_name: Name for the environment, same as vm_name * c_vm_name: Name of the test VM * c_location: Location of the test VM e.g. "westus2", * c_test_suites: e.g. [AgentBvt, FastTrack] """ def __init__(self, runbook: AgentTestSuitesCombinatorSchema) -> None: super().__init__(runbook) if self.runbook.cloud not in self._DEFAULT_LOCATIONS: raise Exception(f"Invalid cloud: {self.runbook.cloud}") if self.runbook.vm_name != '' and (self.runbook.image != '' or self.runbook.vm_size != ''): raise Exception("Invalid runbook parameters: When 'vm_name' is specified, 'image' and 'vm_size' should not be specified.") if self.runbook.vm_name != '': self._environments = self.create_environment_for_existing_vm() else: self._environments = self.create_environment_list() self._index = 0 @classmethod def type_name(cls) -> str: return "agent_test_suites" @classmethod def type_schema(cls) -> Type[schema.TypedSchema]: return AgentTestSuitesCombinatorSchema def _next(self) -> Optional[Dict[str, Any]]: result: Optional[Dict[str, Any]] = None if self._index < len(self._environments): result = self._environments[self._index] self._index += 1 return result _DEFAULT_LOCATIONS = { "china": "china north 2", "government": "usgovarizona", "public": "westus2" } def create_environment_for_existing_vm(self) -> List[Dict[str, Any]]: loader = AgentTestLoader(self.runbook.test_suites) environment: List[Dict[str, Any]] = [ { "c_env_name": self.runbook.vm_name, "c_vm_name": self.runbook.vm_name, "c_location": self.runbook.location, "c_test_suites": loader.test_suites, } ] log: logging.Logger = logging.getLogger("lisa") log.info("******** Environment for existing VMs *****") log.info( "{ c_env_name: '%s', c_vm_name: '%s', c_location: '%s', c_test_suites: '%s' }", environment[0]['c_env_name'], environment[0]['c_vm_name'], environment[0]['c_location'], [s.name for s in environment[0]['c_test_suites']]) log.info("***************************") return environment def create_environment_list(self) -> List[Dict[str, Any]]: loader = AgentTestLoader(self.runbook.test_suites) # # If the runbook provides any of 'image', 'location', or 'vm_size', those values # override any configuration values on the test suite. # # Check 'images' first and add them to 'runbook_images', if any # if self.runbook.image == "": runbook_images = [] else: runbook_images = loader.images.get(self.runbook.image) if runbook_images is None: if not self._is_urn(self.runbook.image) and not self._is_vhd(self.runbook.image): raise Exception(f"The 'image' parameter must be an image or image set name, a urn, or a vhd: {self.runbook.image}") i = VmImageInfo() i.urn = self.runbook.image # Note that this could be a URN or the URI for a VHD i.locations = [] i.vm_sizes = [] runbook_images = [i] # # Now walk through all the test_suites and create a list of the environments (test VMs) that need to be created. # environment_list: List[Dict[str, Any]] = [] shared_environments: Dict[str, Dict[str, Any]] = {} for suite_info in loader.test_suites: if len(runbook_images) > 0: images_info = runbook_images else: # The test suite may be referencing multiple image sets, and sets can intersect, so we need to ensure # we eliminate any duplicates. unique_images: Dict[str, str] = {} for image in suite_info.images: for i in loader.images[image]: unique_images[i] = i images_info = unique_images.values() for image in images_info: # The URN can actually point to a VHD if the runbook provided a VHD in the 'images' parameter if self._is_vhd(image.urn): marketplace_image = "" vhd = image.urn name = "vhd" else: marketplace_image = image.urn vhd = "" match = AgentTestSuitesCombinator._URN.match(image.urn) if match is None: raise Exception(f"Invalid URN: {image.urn}") name = f"{match.group('offer')}-{match.group('sku')}" # If the runbook specified a location, use it. Then try the suite location, if any. Otherwise, check if the image specifies # a list of locations and use any of them. If no location is specified so far, use the default. if self.runbook.location != "": location = self.runbook.location elif suite_info.location != '': location = suite_info.location elif len(image.locations) > 0: location = image.locations[0] else: location = AgentTestSuitesCombinator._DEFAULT_LOCATIONS[self.runbook.cloud] # If the runbook specified a VM size, use it. Else if the image specifies a list of VM sizes, use any of them. Otherwise, # set the size to empty and let LISA choose it. if self.runbook.vm_size != '': vm_size = self.runbook.vm_size elif len(image.vm_sizes) > 0: vm_size = image.vm_sizes[0] else: vm_size = "" if suite_info.owns_vm: # create an environment for exclusive use by this suite environment_list.append({ "c_marketplace_image": marketplace_image, "c_location": location, "c_vm_size": vm_size, "c_vhd": vhd, "c_test_suites": [suite_info], "c_env_name": f"{name}-{suite_info.name}" }) else: # add this suite to the shared environments key: str = f"{name}-{location}" if key in shared_environments: shared_environments[key]["c_test_suites"].append(suite_info) else: shared_environments[key] = { "c_marketplace_image": marketplace_image, "c_location": location, "c_vm_size": vm_size, "c_vhd": vhd, "c_test_suites": [suite_info], "c_env_name": key } environment_list.extend(shared_environments.values()) log: logging.Logger = logging.getLogger("lisa") log.info("******** Environments *****") for e in environment_list: log.info( "{ c_marketplace_image: '%s', c_location: '%s', c_vm_size: '%s', c_vhd: '%s', c_test_suites: '%s', c_env_name: '%s' }", e['c_marketplace_image'], e['c_location'], e['c_vm_size'], e['c_vhd'], [s.name for s in e['c_test_suites']], e['c_env_name']) log.info("***************************") return environment_list _URN = re.compile(r"(?P[^\s:]+)[\s:](?P[^\s:]+)[\s:](?P[^\s:]+)[\s:](?P[^\s:]+)") @staticmethod def _is_urn(urn: str) -> bool: # URNs can be given as ' ' or ':::' return AgentTestSuitesCombinator._URN.match(urn) is not None @staticmethod def _is_vhd(vhd: str) -> bool: # VHDs are given as URIs to storage; do some basic validation, not intending to be exhaustive. parsed = urllib.parse.urlparse(vhd) return parsed.scheme == 'https' and parsed.netloc != "" and parsed.path != "" WALinuxAgent-2.9.1.1/tests_e2e/orchestrator/runbook.yml000066400000000000000000000063661446033677600230410ustar00rootroot00000000000000name: WALinuxAgent testcase: - criteria: area: waagent extension: - "./lib" variable: # # These variables define parameters handled by LISA. # - name: subscription_id value: "" - name: user value: "waagent" - name: identity_file value: "" is_secret: true - name: admin_password value: "" is_secret: true - name: keep_environment value: "no" # # These variables define parameters for the AgentTestSuite; see the test wiki for details. # # NOTE: c_test_suites, generated by the AgentTestSuitesCombinator, is also a parameter # for the AgentTestSuite # # Root directory for log files (optional) - name: log_path value: "" is_case_visible: true # Whether to collect logs from the test VM - name: collect_logs value: "failed" is_case_visible: true # Whether to skip setup of the test VM - name: skip_setup value: false is_case_visible: true # # These variables are parameters for the AgentTestSuitesCombinator # # The test suites to execute - name: test_suites value: "agent_bvt" - name: cloud value: "public" - name: image value: "" - name: location value: "" - name: vm_size value: "" # # The values for these variables are generated by the AgentTestSuitesCombinator combinator. They are # prefixed with "c_" to distinguish them from the rest of the variables, whose value can be set from # the command line. # # c_marketplace_image, c_vm_size, c_location, and c_vhd are handled by LISA and define # the set of test VMs that need to be created, while c_test_suites and c_env_name are parameters # for the AgentTestSuite; the former defines the test suites that must be executed on each # of those test VMs and the latter is the name of the environment, which is used for logging # purposes (NOTE: the AgentTestSuite also uses c_vhd). # - name: c_env_name value: "" is_case_visible: true - name: c_marketplace_image value: "" - name: c_vm_size value: "" - name: c_location value: "" - name: c_vhd value: "" is_case_visible: true - name: c_test_suites value: [] is_case_visible: true # # Set these variables to use an SSH proxy when executing the runbook # - name: proxy value: False - name: proxy_host value: "" - name: proxy_user value: "foo" - name: proxy_identity_file value: "" is_secret: true platform: - type: azure admin_username: $(user) admin_private_key_file: $(identity_file) admin_password: $(admin_password) keep_environment: $(keep_environment) azure: deploy: True subscription_id: $(subscription_id) wait_delete: false requirement: core_count: min: 2 azure: marketplace: $(c_marketplace_image) vhd: $(c_vhd) location: $(c_location) vm_size: $(c_vm_size) combinator: type: agent_test_suites test_suites: $(test_suites) cloud: $(cloud) image: $(image) location: $(location) vm_size: $(vm_size) concurrency: 16 notifier: - type: agent.junit dev: enabled: $(proxy) mock_tcp_ping: $(proxy) jump_boxes: - private_key_file: $(proxy_identity_file) address: $(proxy_host) username: $(proxy_user) password: "dummy" WALinuxAgent-2.9.1.1/tests_e2e/orchestrator/sample_runbooks/000077500000000000000000000000001446033677600240275ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests_e2e/orchestrator/sample_runbooks/existing_vm.yml000066400000000000000000000066041446033677600271140ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # # Executes the test suites on an existing VM # name: ExistingVM testcase: - criteria: area: waagent extension: - "../lib" variable: # # These variables identify the existing VM, and the user for SSH connections # - name: cloud value: "public" - name: subscription_id value: "" - name: resource_group_name value: "" - name: vm_name value: "" - name: location value: "" - name: user value: "" - name: identity_file value: "" is_secret: true # # The test suites to execute # - name: test_suites value: "agent_bvt" # # These variables define parameters for the AgentTestSuite; see the test wiki for details. # # NOTE: c_test_suites, generated by the AgentTestSuitesCombinator, is also a parameter # for the AgentTestSuite # # Root directory for log files (optional) - name: log_path value: "" is_case_visible: true # Whether to collect logs from the test VM - name: collect_logs value: "failed" is_case_visible: true # Whether to skip setup of the test VM - name: skip_setup value: false is_case_visible: true # # The values for these variables are generated by the AgentTestSuitesCombinator combinator. They are # prefixed with "c_" to distinguish them from the rest of the variables, whose value can be set from # the command line. # # c_marketplace_image, c_vm_size, c_location, and c_vhd are handled by LISA and define # the set of test VMs that need to be created, while c_test_suites is a parameter # for the AgentTestSuite and defines the test suites that must be executed on each # of those test VMs (the AgentTestSuite also uses c_vhd) # - name: c_env_name value: "" is_case_visible: true - name: c_vm_name value: "" - name: c_location value: "" - name: c_test_suites value: [] is_case_visible: true # # Set these variables to use an SSH proxy when executing the runbook # - name: proxy value: False - name: proxy_host value: "" - name: proxy_user value: "foo" - name: proxy_identity_file value: "" is_secret: true platform: - type: azure admin_username: $(user) admin_private_key_file: $(identity_file) azure: resource_group_name: $(resource_group_name) deploy: false subscription_id: $(subscription_id) requirement: azure: name: $(c_vm_name) location: $(c_location) combinator: type: agent_test_suites test_suites: $(test_suites) cloud: $(cloud) location: $(location) vm_name: $(vm_name) notifier: - type: env_stats - type: agent.junit dev: enabled: $(proxy) mock_tcp_ping: $(proxy) jump_boxes: - private_key_file: $(proxy_identity_file) address: $(proxy_host) username: $(proxy_user) password: "dummy" WALinuxAgent-2.9.1.1/tests_e2e/orchestrator/sample_runbooks/local_machine/000077500000000000000000000000001446033677600266055ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests_e2e/orchestrator/sample_runbooks/local_machine/hello_world.py000066400000000000000000000020131446033677600314650ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # E0401: Unable to import 'lisa' (import-error) from lisa import ( # pylint: disable=E0401 Logger, Node, TestCaseMetadata, TestSuite, TestSuiteMetadata, ) @TestSuiteMetadata(area="sample", category="", description="") class HelloWorld(TestSuite): @TestCaseMetadata(description="") def main(self, node: Node, log: Logger) -> None: log.info(f"Hello world from {node.os.name}!") WALinuxAgent-2.9.1.1/tests_e2e/orchestrator/sample_runbooks/local_machine/local.yml000066400000000000000000000014731446033677600304270ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # # Executes the test suites on the local machine # extension: - "." environment: environments: - nodes: - type: local notifier: - type: console testcase: - criteria: area: sample WALinuxAgent-2.9.1.1/tests_e2e/orchestrator/scripts/000077500000000000000000000000001446033677600223135ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests_e2e/orchestrator/scripts/check-agent-log.py000077500000000000000000000026641446033677600256300ustar00rootroot00000000000000#!/usr/bin/env pypy3 # Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # import argparse import json import sys from pathlib import Path from tests_e2e.tests.lib.agent_log import AgentLog try: parser = argparse.ArgumentParser() parser.add_argument('path', nargs='?', help='Path of the log file', default='/var/log/waagent.log') parser.add_argument('-j', '--json', action='store_true', help='Produce a JSON report') parser.set_defaults(json=False) args = parser.parse_args() error_list = AgentLog(Path(args.path)).get_errors() if args.json: print(json.dumps(error_list, default=lambda o: o.__dict__)) else: if len(error_list) == 0: print("No errors were found.") else: for e in error_list: print(e.text) except Exception as e: print(f"{e}", file=sys.stderr) sys.exit(1) sys.exit(0) WALinuxAgent-2.9.1.1/tests_e2e/orchestrator/scripts/collect-logs000077500000000000000000000016521446033677600246340ustar00rootroot00000000000000#!/usr/bin/env bash # # Collects the logs needed to debug agent issues into a compressed tarball. # # Note that we do "set -euxo pipefail" only after executing "tar". That command exits with code 1 on warnings # and we do not want to consider those as failures. logs_file_name="/tmp/waagent-logs.tgz" echo "Collecting logs to $logs_file_name ..." tar --exclude='journal/*' --exclude='omsbundle' --exclude='omsagent' --exclude='mdsd' --exclude='scx*' \ --exclude='*.so' --exclude='*__LinuxDiagnostic__*' --exclude='*.zip' --exclude='*.deb' --exclude='*.rpm' \ --warning=no-file-changed \ -czf "$logs_file_name" \ /var/log \ /var/lib/waagent/ \ /etc/waagent.conf set -euxo pipefail # Ignore warnings (exit code 1) exit_code=$? if [ "$exit_code" == "1" ]; then echo "WARNING: tar exit code is 1" elif [ "$exit_code" != "0" ]; then exit $exit_code fi chmod a+r "$logs_file_name" ls -l "$logs_file_name" WALinuxAgent-2.9.1.1/tests_e2e/orchestrator/scripts/get-agent-bin-path000077500000000000000000000026671446033677600256270ustar00rootroot00000000000000#!/usr/bin/env bash # Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # # Returns the path for the 'waagent' command. # set -euo pipefail # On most distros, 'waagent' is in PATH if which waagent 2> /dev/null; then exit 0 fi # if the agent is running, get the path from 'cmdline' in the /proc file system if test -e /run/waagent.pid; then cmdline="/proc/$(cat /run/waagent.pid)/cmdline" if test -e "$cmdline"; then # cmdline is a sequence of null-terminated strings; break into lines and look for waagent if tr '\0' '\n' < "$cmdline" | grep waagent; then exit 0 fi fi fi # try some well-known locations declare -a known_locations=( "/usr/sbin/waagent" "/usr/share/oem/bin/waagent" ) for path in "${known_locations[@]}" do if [[ -e $path ]]; then echo "$path" exit 0 fi done echo "Can't find the path for the 'waagent' command" >&2 exit 1 WALinuxAgent-2.9.1.1/tests_e2e/orchestrator/scripts/get-agent-modules-path000077500000000000000000000020621446033677600265140ustar00rootroot00000000000000#!/usr/bin/env bash # Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # # Returns the PYTHONPATH on which the azurelinuxagent and associated modules are located. # # To do this, the script walks the site packages for the Python used to execute the agent, # looking for the directory that contains "azurelinuxagent". # set -euo pipefail $(get-agent-python) -c ' import site import os for dir in site.getsitepackages(): if os.path.isdir(dir + "/azurelinuxagent"): print(dir) exit(0) exit(1) ' WALinuxAgent-2.9.1.1/tests_e2e/orchestrator/scripts/get-agent-python000077500000000000000000000031141446033677600254320ustar00rootroot00000000000000#!/usr/bin/env bash # Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # # Returns the path of the Python executable used to start the Agent. # set -euo pipefail # if the agent is running, get the python command from 'exe' in the /proc file system if test -e /run/waagent.pid; then exe="/proc/$(cat /run/waagent.pid)/exe" if test -e "$exe"; then # exe is a symbolic link; return its target readlink -f "$exe" exit 0 fi fi # try all the instances of 'python' and 'python3' in $PATH for path in $(echo "$PATH" | tr ':' '\n'); do if [[ -e $path ]]; then for python in $(find "$path" -maxdepth 1 -name python3 -or -name python); do if $python -c 'import azurelinuxagent' 2> /dev/null; then echo "$python" exit 0 fi done fi done # try some well-known locations declare -a known_locations=( "/usr/share/oem/python/bin/python" ) for python in "${known_locations[@]}" do if $python -c 'import azurelinuxagent' 2> /dev/null; then echo "$python" exit 0 fi done exit 1 WALinuxAgent-2.9.1.1/tests_e2e/orchestrator/scripts/install-agent000077500000000000000000000064401446033677600250070ustar00rootroot00000000000000#!/usr/bin/env bash # Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # set -euo pipefail usage() ( echo "Usage: install-agent -p|--package -v|--version " exit 1 ) while [[ $# -gt 0 ]]; do case $1 in -p|--package) shift if [ "$#" -lt 1 ]; then usage fi package=$1 shift ;; -v|--version) shift if [ "$#" -lt 1 ]; then usage fi version=$1 shift ;; *) usage esac done if [ "$#" -ne 0 ] || [ -z ${package+x} ] || [ -z ${version+x} ]; then usage fi # # Find the command to manage services # if command -v systemctl &> /dev/null; then service-status() { systemctl --no-pager -l status $1; } service-stop() { systemctl stop $1; } service-start() { systemctl start $1; } else service-status() { service $1 status; } service-stop() { service $1 stop; } service-start() { service $1 start; } fi # # Find the service name (walinuxagent in Ubuntu and waagent elsewhere) # if service-status walinuxagent > /dev/null 2>&1;then service_name="walinuxagent" else service_name="waagent" fi echo "Service name: $service_name" # # Output the initial version of the agent # python=$(get-agent-python) waagent=$(get-agent-bin-path) echo "Agent's path: $waagent" $python "$waagent" --version printf "\n" # # Install the package # echo "Installing $package as version $version..." unzip.py "$package" "/var/lib/waagent/WALinuxAgent-$version" # Ensure that AutoUpdate is enabled. some distros, e.g. Flatcar, don't have a waagent.conf # but AutoUpdate defaults to True so there is no need to do anything in that case. if [[ -e /etc/waagent.conf ]]; then sed -i 's/AutoUpdate.Enabled=n/AutoUpdate.Enabled=y/g' /etc/waagent.conf fi # # Restart the service # echo "Restarting service..." service-stop $service_name # Rename the previous log to ensure the new log starts with the agent we just installed mv /var/log/waagent.log /var/log/waagent."$(date --iso-8601=seconds)".log service-start $service_name # # Verify that the new agent is running and output its status. # Note that the extension handler may take some time to start so give 1 minute. # echo "Verifying agent installation..." check-version() { for i in {0..5} do if $python "$waagent" --version | grep -E "Goal state agent:\s+$version" > /dev/null; then return 0 fi sleep 10 done return 1 } if check-version "$version"; then printf "\nThe agent was installed successfully\n" exit_code=0 else printf "\nFailed to install agent.\n" exit_code=1 fi $python "$waagent" --version printf "\n" service-status $service_name exit $exit_code WALinuxAgent-2.9.1.1/tests_e2e/orchestrator/scripts/install-tools000077500000000000000000000114221446033677600250450ustar00rootroot00000000000000#!/usr/bin/env bash # Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # # Installs the tools in ~/bin/scripts/* to ~/bin, as well as Pypy. # # It also makes Pypy the default python for the current user. # set -euo pipefail PATH="$HOME/bin:$PATH" python=$(get-agent-python) echo "Python executable: $python" echo "Python version: $($python --version)" # # Install Pypy as ~/bin/pypy3 # # Note that bzip2/lbzip2 (used by tar to uncompress *.bz2 files) are not available by default in some distros; # use Python to uncompress the Pypy tarball. # echo "Installing Pypy 3.7" $python ~/bin/uncompress.py ~/tmp/pypy3.7-*.tar.bz2 ~/tmp/pypy3.7.tar tar xf ~/tmp/pypy3.7.tar -C ~/bin echo "Pypy was installed in $(ls -d ~/bin/pypy*)" ln -s ~/bin/pypy*/bin/pypy3.7 ~/bin/pypy3 echo "Creating symbolic link to Pypy: ~/bin/pypy3" # # The 'distro' and 'platform' modules in Pypy have small differences with the ones in the system's Python. # This can create problems in tests that use the get_distro() method in the Agent's 'version.py' module. # To work around this, we copy the system's 'distro' module to Pypy. # # In the case of 'platform', the 'linux_distribution' method was removed on Python 3.7 so we check the # system's module and, if the method does not exist, we also remove it from Pypy. Ubuntu 16 and 18 are # special cases in that the 'platform' module in Pypy identifies the distro as 'debian'; in this case we # copy the system's 'platform' module to Pypy. # distro_path=$($python -c ' try: import distro except: exit(0) print(distro.__file__.replace("__init__.py", "distro.py")) exit(0) ') if [[ "$distro_path" != "" ]]; then echo "Copying the system's distro module to Pypy" cp -v "$distro_path" ~/bin/pypy*/site-packages else echo "The distro module is not is not installing on the system; skipping." fi has_linux_distribution=$($python -c 'import platform; print(hasattr(platform, "linux_distribution"))') if [[ "$has_linux_distribution" == "False" ]]; then echo "Python does not have platform.linux_distribution; removing it from Pypy" sed -i 's/def linux_distribution(/def __linux_distribution__(/' ~/bin/pypy*/lib-python/3/platform.py else echo "Python has platform.linux_distribution" uname=$(uname -v) if [[ "$uname" == *~18*-Ubuntu* || "$uname" == *~16*-Ubuntu* ]]; then echo "Copying the system's platform module to Pypy" pypy_platform=$(pypy3 -c 'import platform; print(platform.__file__)') python_platform=$($python -c 'import platform; print(platform.__file__)') cp -v "$python_platform" "$pypy_platform" fi fi # # Now install the test Agent as a module package in Pypy. # echo "Installing Agent modules to Pypy" unzip.py ~/tmp/WALinuxAgent-*.zip ~/tmp/WALinuxAgent unzip.py ~/tmp/WALinuxAgent/bin/WALinuxAgent-*.egg ~/tmp/WALinuxAgent/bin/WALinuxAgent.egg mv ~/tmp/WALinuxAgent/bin/WALinuxAgent.egg/azurelinuxagent ~/bin/pypy*/site-packages # # Log the results of get_distro() in Pypy and Python. # pypy_get_distro=$(pypy3 -c 'from azurelinuxagent.common.version import get_distro; print(get_distro())') python_get_distro=$($python -c 'from azurelinuxagent.common.version import get_distro; print(get_distro())') echo "Pypy get_distro(): $pypy_get_distro" echo "Python get_distro(): $python_get_distro" # # Create ~/bin/set-agent-env to set PATH and PYTHONPATH. # # We append $HOME/bin to PATH and set PYTHONPATH to $HOME/lib (bin contains the scripts used by tests, while # lib contains the Python libraries used by tests). # echo "Creating ~/bin/set-agent-env to set PATH and PYTHONPATH" echo " if [[ \$PATH != *\"$HOME/bin\"* ]]; then PATH=\"$HOME/bin:\$PATH:\" fi export PYTHONPATH=\"$HOME/lib\" " > ~/bin/set-agent-env chmod u+x ~/bin/set-agent-env # # Add ~/bin/set-agent-env to .bash_profile to simplify interactive debugging sessions # # Note that in some distros .bash_profile is a symbolic link to a read-only file. Make a copy in that case. # echo "Adding ~/bin/set-agent-env to ~/.bash_profile" if test -e ~/.bash_profile && ls -l .bash_profile | grep '\->'; then cp ~/.bash_profile ~/.bash_profile-bk rm ~/.bash_profile mv ~/.bash_profile-bk ~/.bash_profile fi if ! test -e ~/.bash_profile || ! grep '~/bin/set-agent-env' ~/.bash_profile > /dev/null; then echo 'source ~/bin/set-agent-env ' >> ~/.bash_profile fi WALinuxAgent-2.9.1.1/tests_e2e/orchestrator/scripts/uncompress.py000077500000000000000000000017401446033677600250700ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # # Un-compresses a bz2 file # import argparse import bz2 import shutil parser = argparse.ArgumentParser() parser.add_argument('source', help='File to uncompress') parser.add_argument('target', help='Output file') args = parser.parse_args() with bz2.BZ2File(args.source, 'rb') as f_in: with open(args.target, 'wb') as f_out: shutil.copyfileobj(f_in, f_out) WALinuxAgent-2.9.1.1/tests_e2e/orchestrator/scripts/unzip.py000077500000000000000000000020121446033677600240300ustar00rootroot00000000000000#!/usr/bin/env pypy3 # Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # import argparse import sys import zipfile try: parser = argparse.ArgumentParser() parser.add_argument('source', help='ZIP package to expand') parser.add_argument('target', help='Destination directory') args = parser.parse_args() zipfile.ZipFile(args.source).extractall(args.target) except Exception as e: print(f"{e}", file=sys.stderr) sys.exit(1) sys.exit(0) WALinuxAgent-2.9.1.1/tests_e2e/pipeline/000077500000000000000000000000001446033677600177125ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests_e2e/pipeline/pipeline-cleanup.yml000066400000000000000000000032721446033677600236730ustar00rootroot00000000000000# # Pipeline for cleaning up any remaining Resource Groups generated by the Azure.WALinuxAgent pipeline. # # Deletes any resource groups that are more than a day old and contain string "lisa-WALinuxAgent-" # schedules: - cron: "0 */12 * * *" # Run twice a day (every 12 hours) displayName: cleanup build branches: include: - develop always: true trigger: - develop pr: none pool: vmImage: ubuntu-latest variables: - name: azureConnection value: 'azuremanagement' - name: rgPrefix value: 'lisa-WALinuxAgent-' steps: - task: AzureKeyVault@2 displayName: "Fetch secrets from KV" inputs: azureSubscription: '$(azureConnection)' KeyVaultName: 'dcrV2SPs' SecretsFilter: '*' RunAsPreJob: true - task: AzureCLI@2 inputs: azureSubscription: '$(azureConnection)' scriptType: 'bash' scriptLocation: 'inlineScript' inlineScript: | set -euxo pipefail date=`date --utc +%Y-%m-%d'T'%H:%M:%S.%N'Z' -d "1 day ago"` # Using the Azure REST GET resourceGroups API call as we can add the createdTime to the results. # This feature is not available via the az-cli commands directly so we have to use the Azure REST APIs az rest --method GET \ --url "https://management.azure.com/subscriptions/$(SUBSCRIPTION-ID)/resourcegroups" \ --url-parameters api-version=2021-04-01 \$expand=createdTime \ --output json \ --query value \ | jq --arg date "$date" '.[] | select (.createdTime < $date).name' \ | grep "$(rgPrefix)" \ | xargs -l -t -r az group delete --no-wait -y -n \ || echo "No resource groups found to delete" WALinuxAgent-2.9.1.1/tests_e2e/pipeline/pipeline.yml000066400000000000000000000066741446033677600222570ustar00rootroot00000000000000# variables: # # NOTE: When creating the pipeline, "connection_info" must be added as a variable pointing to the # corresponding key vault; see wiki for details. # parameters: # See the test wiki for a description of the parameters - name: test_suites displayName: Test Suites type: string default: agent_bvt # NOTES: # * 'image', 'location' and 'vm_size' override any values in the test suites/images definition # files. Those parameters are useful for 1-off tests, like testing a VHD or checking if # an image is supported in a particular location. # * Azure Pipelines do not allow empty string for the parameter value, using "-" instead. # - name: image displayName: Image (image/image set name, URN, or VHD) type: string default: "-" - name: location displayName: Location (region) type: string default: "-" - name: vm_size displayName: VM size type: string default: "-" - name: collect_logs displayName: Collect logs from test VMs type: string default: failed values: - always - failed - no - name: keep_environment displayName: Keep the test VMs (do not delete them) type: string default: no values: - always - failed - no trigger: - develop pr: none pool: vmImage: ubuntu-latest jobs: - job: "ExecuteTests" steps: - task: UsePythonVersion@0 displayName: "Set Python Version" inputs: versionSpec: '3.10' addToPath: true architecture: 'x64' # Extract the Azure cloud from the "connection_info" variable and store it in the "cloud" variable. # The cloud name is used as a suffix of the value for "connection_info" and comes after the last '-'. - bash: echo "##vso[task.setvariable variable=cloud]$(echo $CONNECTION_INFO | sed 's/^.*-//')" displayName: "Set Cloud type" - task: DownloadSecureFile@1 name: downloadSshKey displayName: "Download SSH key" inputs: secureFile: 'id_rsa' - task: AzureKeyVault@2 displayName: "Fetch connection info" inputs: azureSubscription: 'azuremanagement' KeyVaultName: '$(connection_info)' SecretsFilter: '*' - bash: $(Build.SourcesDirectory)/tests_e2e/pipeline/scripts/execute_tests.sh displayName: "Execute tests" continueOnError: true env: SUBSCRIPTION_ID: $(SUBSCRIPTION-ID) AZURE_CLIENT_ID: $(AZURE-CLIENT-ID) AZURE_CLIENT_SECRET: $(AZURE-CLIENT-SECRET) AZURE_TENANT_ID: $(AZURE-TENANT-ID) CR_USER: $(CR-USER) CR_SECRET: $(CR-SECRET) CLOUD: ${{ variables.cloud }} COLLECT_LOGS: ${{ parameters.collect_logs }} IMAGE: ${{ parameters.image }} KEEP_ENVIRONMENT: ${{ parameters.keep_environment }} LOCATION: ${{ parameters.location }} TEST_SUITES: ${{ parameters.test_suites }} VM_SIZE: ${{ parameters.vm_size }} - publish: $(Build.ArtifactStagingDirectory) artifact: 'artifacts' displayName: 'Publish test artifacts' - task: PublishTestResults@2 displayName: 'Publish test results' inputs: testResultsFormat: 'JUnit' testResultsFiles: 'runbook_logs/agent.junit.xml' searchFolder: $(Build.ArtifactStagingDirectory) failTaskOnFailedTests: true WALinuxAgent-2.9.1.1/tests_e2e/pipeline/scripts/000077500000000000000000000000001446033677600214015ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests_e2e/pipeline/scripts/execute_tests.sh000077500000000000000000000101611446033677600246230ustar00rootroot00000000000000#!/usr/bin/env bash set -euxo pipefail # # UID of 'waagent' in the Docker container # WAAGENT_UID=1000 # # Set the correct mode and owner for the private SSH key and generate the public key. # cd "$HOME" mkdir ssh cp "$DOWNLOADSSHKEY_SECUREFILEPATH" ssh chmod 700 ssh/id_rsa ssh-keygen -y -f ssh/id_rsa > ssh/id_rsa.pub sudo find ssh -exec chown "$WAAGENT_UID" {} \; # # Allow write access to the sources directory. This is needed because building the agent package (within the Docker # container) writes the egg info to that directory. # chmod a+w "$BUILD_SOURCESDIRECTORY" # # Create the directory where the Docker container will create the test logs and give ownership to 'waagent' # LOGS_DIRECTORY="$HOME/logs" mkdir "$LOGS_DIRECTORY" sudo chown "$WAAGENT_UID" "$LOGS_DIRECTORY" # # Pull the container image used to execute the tests # az acr login --name waagenttests --username "$CR_USER" --password "$CR_SECRET" docker pull waagenttests.azurecr.io/waagenttests:latest # Azure Pipelines does not allow an empty string as the value for a pipeline parameter; instead we use "-" to indicate # an empty value. Change "-" to "" for the variables that capture the parameter values. if [[ $IMAGE == "-" ]]; then IMAGE="" fi if [[ $LOCATION == "-" ]]; then LOCATION="" fi if [[ $VM_SIZE == "-" ]]; then VM_SIZE="" fi # A test failure will cause automation to exit with an error code and we don't want this script to stop so we force the command # to succeed and capture the exit code to return it at the end of the script. echo "exit 0" > /tmp/exit.sh docker run --rm \ --volume "$BUILD_SOURCESDIRECTORY:/home/waagent/WALinuxAgent" \ --volume "$HOME"/ssh:/home/waagent/.ssh \ --volume "$LOGS_DIRECTORY":/home/waagent/logs \ --env AZURE_CLIENT_ID \ --env AZURE_CLIENT_SECRET \ --env AZURE_TENANT_ID \ waagenttests.azurecr.io/waagenttests \ bash --login -c \ "lisa \ --runbook \$HOME/WALinuxAgent/tests_e2e/orchestrator/runbook.yml \ --log_path \$HOME/logs/lisa \ --working_path \$HOME/tmp \ -v cloud:$CLOUD \ -v subscription_id:$SUBSCRIPTION_ID \ -v identity_file:\$HOME/.ssh/id_rsa \ -v test_suites:\"$TEST_SUITES\" \ -v log_path:\$HOME/logs \ -v collect_logs:\"$COLLECT_LOGS\" \ -v keep_environment:\"$KEEP_ENVIRONMENT\" \ -v image:\"$IMAGE\" \ -v location:\"$LOCATION\" \ -v vm_size:\"$VM_SIZE\"" \ || echo "exit $?" > /tmp/exit.sh # # Re-take ownership of the logs directory # sudo find "$LOGS_DIRECTORY" -exec chown "$USER" {} \; # # Move the relevant logs to the staging directory # # Move the logs for failed tests to a temporary location mkdir "$BUILD_ARTIFACTSTAGINGDIRECTORY"/tmp for log in $(grep -l MARKER-LOG-WITH-ERRORS "$LOGS_DIRECTORY"/*.log); do mv "$log" "$BUILD_ARTIFACTSTAGINGDIRECTORY"/tmp done # Move the environment logs to "environment_logs" if ls "$LOGS_DIRECTORY"/env-*.log > /dev/null 2>&1; then mkdir "$BUILD_ARTIFACTSTAGINGDIRECTORY"/environment_logs mv "$LOGS_DIRECTORY"/env-*.log "$BUILD_ARTIFACTSTAGINGDIRECTORY"/environment_logs fi # Move the rest of the logs to "test_logs" if ls "$LOGS_DIRECTORY"/*.log > /dev/null 2>&1; then mkdir "$BUILD_ARTIFACTSTAGINGDIRECTORY"/test_logs mv "$LOGS_DIRECTORY"/*.log "$BUILD_ARTIFACTSTAGINGDIRECTORY"/test_logs fi # Move the logs for failed tests to the main directory if ls "$BUILD_ARTIFACTSTAGINGDIRECTORY"/tmp/*.log > /dev/null 2>&1; then mv "$BUILD_ARTIFACTSTAGINGDIRECTORY"/tmp/*.log "$BUILD_ARTIFACTSTAGINGDIRECTORY" fi rmdir "$BUILD_ARTIFACTSTAGINGDIRECTORY"/tmp # Move the logs collected from the test VMs to vm_logs if ls "$LOGS_DIRECTORY"/*.tgz > /dev/null 2>&1; then mkdir "$BUILD_ARTIFACTSTAGINGDIRECTORY"/vm_logs mv "$LOGS_DIRECTORY"/*.tgz "$BUILD_ARTIFACTSTAGINGDIRECTORY"/vm_logs fi # Files created by LISA are under .../lisa//" mkdir "$BUILD_ARTIFACTSTAGINGDIRECTORY"/runbook_logs mv "$LOGS_DIRECTORY"/lisa/*/*/lisa-*.log "$BUILD_ARTIFACTSTAGINGDIRECTORY"/runbook_logs mv "$LOGS_DIRECTORY"/lisa/*/*/agent.junit.xml "$BUILD_ARTIFACTSTAGINGDIRECTORY"/runbook_logs cat /tmp/exit.sh bash /tmp/exit.sh WALinuxAgent-2.9.1.1/tests_e2e/test_suites/000077500000000000000000000000001446033677600204605ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests_e2e/test_suites/agent_bvt.yml000066400000000000000000000002311446033677600231500ustar00rootroot00000000000000name: "AgentBvt" tests: - "bvts/extension_operations.py" - "bvts/run_command.py" - "bvts/vm_access.py" images: - "endorsed" - "endorsed-arm64" WALinuxAgent-2.9.1.1/tests_e2e/test_suites/fail.yml000066400000000000000000000001211446033677600221100ustar00rootroot00000000000000name: "Fail" tests: - "fail_test.py" - "error_test.py" images: "ubuntu_1804" WALinuxAgent-2.9.1.1/tests_e2e/test_suites/images.yml000066400000000000000000000060341446033677600224530ustar00rootroot00000000000000# # Image sets are used to group images # image-sets: # Endorsed distros that are tested on the daily runs endorsed: # # TODO: Add CentOS 6.10 and Debian 8 # # - "centos_610" # - "debian_8" # - "alma_9" - "centos_79" - "debian_9" - "debian_10" - "debian_11" - "flatcar" - "suse_12" - "mariner_1" - "mariner_2" - "suse_15" - "rhel_79" - "rhel_82" - "rhel_90" - "rocky_9" - "ubuntu_1604" - "ubuntu_1804" - "ubuntu_2004" - "ubuntu_2204" # Endorsed distros (ARM64) that are tested on the daily runs endorsed-arm64: - "debian_11_arm64" - "flatcar_arm64" - "mariner_2_arm64" - "rhel_90_arm64" - "ubuntu_2204_arm64" # # An image can be specified by a string giving its urn, as in # # ubuntu_2004: "Canonical 0001-com-ubuntu-server-focal 20_04-lts latest" # # or by an object with 3 properties: urn, locations and vm_sizes, as in # # mariner_2_arm64: # urn: "microsoftcblmariner cbl-mariner cbl-mariner-2-arm64 latest" # locations: # - "eastus" # vm_sizes: # - "Standard_D2pls_v5" # # 'urn' is required, while 'locations' and 'vm_sizes' are optional. The latter # two properties can be used to specify that the image is available only in # some locations, or that it can be used only on some VM sizes. # # URNs follow the format ' ' or # ':::' # images: alma_9: "almalinux almalinux 9-gen2 latest" centos_610: "OpenLogic CentOS 6.10 latest" centos_79: "OpenLogic CentOS 7_9 latest" debian_8: "credativ Debian 8 latest" debian_9: "credativ Debian 9 latest" debian_10: "Debian debian-10 10 latest" debian_11: "Debian debian-11 11 latest" debian_11_arm64: "Debian debian-11 11-backports-arm64 latest" flatcar: "kinvolk flatcar-container-linux-free stable latest" flatcar_arm64: urn: "kinvolk flatcar-container-linux-corevm stable latest" vm_sizes: - "Standard_D2pls_v5" mariner_1: "microsoftcblmariner cbl-mariner cbl-mariner-1 latest" mariner_2: "microsoftcblmariner cbl-mariner cbl-mariner-2 latest" mariner_2_arm64: urn: "microsoftcblmariner cbl-mariner cbl-mariner-2-arm64 latest" locations: - "eastus" vm_sizes: - "Standard_D2pls_v5" rocky_9: "erockyenterprisesoftwarefoundationinc1653071250513 rockylinux-9 rockylinux-9 latest" suse_12: "SUSE sles-12-sp5-basic gen1 latest" suse_15: "SUSE sles-15-sp2-basic gen2 latest" rhel_79: "RedHat RHEL 7_9 latest" rhel_82: "RedHat RHEL 8.2 latest" rhel_90: "RedHat RHEL 9_0 latest" rhel_90_arm64: "RedHat rhel-arm64 9_0-arm64 latest" ubuntu_1604: "Canonical UbuntuServer 16.04-LTS latest" ubuntu_1804: "Canonical UbuntuServer 18.04-LTS latest" ubuntu_2004: "Canonical 0001-com-ubuntu-server-focal 20_04-lts latest" ubuntu_2204: "Canonical 0001-com-ubuntu-server-jammy 22_04-lts latest" ubuntu_2204_arm64: "Canonical 0001-com-ubuntu-server-jammy 22_04-lts-arm64 latest" WALinuxAgent-2.9.1.1/tests_e2e/test_suites/pass.yml000066400000000000000000000000751446033677600221530ustar00rootroot00000000000000name: "Pass" tests: - "pass_test.py" images: "ubuntu_2004" WALinuxAgent-2.9.1.1/tests_e2e/tests/000077500000000000000000000000001446033677600172475ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests_e2e/tests/__init__.py000066400000000000000000000000001446033677600213460ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests_e2e/tests/bvts/000077500000000000000000000000001446033677600202255ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests_e2e/tests/bvts/__init__.py000066400000000000000000000000001446033677600223240ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests_e2e/tests/bvts/extension_operations.py000077500000000000000000000066761446033677600251000ustar00rootroot00000000000000#!/usr/bin/env python3 # Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # # BVT for extension operations (Install/Enable/Update/Uninstall). # # The test executes an older version of an extension, then updates it to a newer version, and lastly # it removes it. The actual extension is irrelevant, but the test uses CustomScript for simplicity, # since it's invocation is trivial and the entire extension workflow can be tested end-to-end by # checking the message in the status produced by the extension. # import uuid from assertpy import assert_that from azure.core.exceptions import ResourceNotFoundError from tests_e2e.tests.lib.agent_test import AgentTest from tests_e2e.tests.lib.identifiers import VmExtensionIds, VmExtensionIdentifier from tests_e2e.tests.lib.logging import log from tests_e2e.tests.lib.ssh_client import SshClient from tests_e2e.tests.lib.vm_extension import VmExtension class ExtensionOperationsBvt(AgentTest): def run(self): ssh_client: SshClient = SshClient( ip_address=self._context.vm_ip_address, username=self._context.username, private_key_file=self._context.private_key_file) is_arm64: bool = ssh_client.get_architecture() == "aarch64" custom_script_2_0 = VmExtension( self._context.vm, VmExtensionIds.CustomScript, resource_name="CustomScript") if is_arm64: log.info("Will skip the update scenario, since currently there is only 1 version of CSE on ARM64") else: log.info("Installing %s", custom_script_2_0) message = f"Hello {uuid.uuid4()}!" custom_script_2_0.enable( settings={ 'commandToExecute': f"echo \'{message}\'" }, auto_upgrade_minor_version=False ) custom_script_2_0.assert_instance_view(expected_version="2.0", expected_message=message) custom_script_2_1 = VmExtension( self._context.vm, VmExtensionIdentifier(VmExtensionIds.CustomScript.publisher, VmExtensionIds.CustomScript.type, "2.1"), resource_name="CustomScript") if is_arm64: log.info("Installing %s", custom_script_2_1) else: log.info("Updating %s to %s", custom_script_2_0, custom_script_2_1) message = f"Hello {uuid.uuid4()}!" custom_script_2_1.enable( settings={ 'commandToExecute': f"echo \'{message}\'" } ) custom_script_2_1.assert_instance_view(expected_version="2.1", expected_message=message) custom_script_2_1.delete() assert_that(custom_script_2_1.get_instance_view).\ described_as("Fetching the instance view should fail after removing the extension").\ raises(ResourceNotFoundError) if __name__ == "__main__": ExtensionOperationsBvt.run_from_command_line() WALinuxAgent-2.9.1.1/tests_e2e/tests/bvts/run_command.py000077500000000000000000000071111446033677600231040ustar00rootroot00000000000000#!/usr/bin/env python3 # Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # # BVT for RunCommand. # # Note that there are two incarnations of RunCommand (which are actually two different extensions): # Microsoft.CPlat.Core.RunCommandHandlerLinux and Microsoft.CPlat.Core.RunCommandLinux. This test # exercises both using the same strategy: execute the extension to create a file on the test VM, # then fetch the contents of the file over SSH and compare against the known value. # import base64 import uuid from assertpy import assert_that, soft_assertions from typing import Callable, Dict from tests_e2e.tests.lib.agent_test import AgentTest from tests_e2e.tests.lib.identifiers import VmExtensionIds from tests_e2e.tests.lib.logging import log from tests_e2e.tests.lib.ssh_client import SshClient from tests_e2e.tests.lib.vm_extension import VmExtension class RunCommandBvt(AgentTest): class TestCase: def __init__(self, extension: VmExtension, get_settings: Callable[[str], Dict[str, str]]): self.extension = extension self.get_settings = get_settings def run(self): ssh_client = SshClient( ip_address=self._context.vm_ip_address, username=self._context.username, private_key_file=self._context.private_key_file) test_cases = [ RunCommandBvt.TestCase( VmExtension(self._context.vm, VmExtensionIds.RunCommand, resource_name="RunCommand"), lambda s: { "script": base64.standard_b64encode(bytearray(s, 'utf-8')).decode('utf-8') }) ] if ssh_client.get_architecture() == "aarch64": log.info("Skipping test case for %s, since it has not been published on ARM64", VmExtensionIds.RunCommandHandler) else: test_cases.append( RunCommandBvt.TestCase( VmExtension(self._context.vm, VmExtensionIds.RunCommandHandler, resource_name="RunCommandHandler"), lambda s: { "source": { "script": s } })) with soft_assertions(): for t in test_cases: log.info("Test case: %s", t.extension) unique = str(uuid.uuid4()) test_file = f"/tmp/waagent-test.{unique}" script = f"echo '{unique}' > {test_file}" log.info("Script to execute: %s", script) t.extension.enable(settings=t.get_settings(script)) t.extension.assert_instance_view() log.info("Verifying contents of the file created by the extension") contents = ssh_client.run_command(f"cat {test_file}").rstrip() # remove the \n assert_that(contents).\ described_as("Contents of the file created by the extension").\ is_equal_to(unique) log.info("The contents match") if __name__ == "__main__": RunCommandBvt.run_from_command_line() WALinuxAgent-2.9.1.1/tests_e2e/tests/bvts/vm_access.py000077500000000000000000000061031446033677600225450ustar00rootroot00000000000000#!/usr/bin/env python3 # Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # # BVT for the VmAccess extension # # The test executes VmAccess to add a user and then verifies that an SSH connection to the VM can # be established with that user's identity. # import uuid from assertpy import assert_that from pathlib import Path from tests_e2e.tests.lib.agent_test import AgentTest, TestSkipped from tests_e2e.tests.lib.identifiers import VmExtensionIds from tests_e2e.tests.lib.logging import log from tests_e2e.tests.lib.ssh_client import SshClient from tests_e2e.tests.lib.vm_extension import VmExtension class VmAccessBvt(AgentTest): def run(self): ssh: SshClient = SshClient(ip_address=self._context.vm_ip_address, username=self._context.username, private_key_file=self._context.private_key_file) if "-flatcar" in ssh.run_command("uname -a"): raise TestSkipped("Currently VMAccess is not supported on Flatcar") # Try to use a unique username for each test run (note that we truncate to 32 chars to # comply with the rules for usernames) log.info("Generating a new username and SSH key") username: str = f"test-{uuid.uuid4()}"[0:32] log.info("Username: %s", username) # Create an SSH key for the user and fetch the public key private_key_file: Path = self._context.working_directory/f"{username}_rsa" public_key_file: Path = self._context.working_directory/f"{username}_rsa.pub" log.info("Generating SSH key as %s", private_key_file) ssh = SshClient(ip_address=self._context.vm_ip_address, username=username, private_key_file=private_key_file) ssh.generate_ssh_key(private_key_file) with public_key_file.open() as f: public_key = f.read() # Invoke the extension vm_access = VmExtension(self._context.vm, VmExtensionIds.VmAccess, resource_name="VmAccess") vm_access.enable( protected_settings={ 'username': username, 'ssh_key': public_key, 'reset_ssh': 'false' } ) vm_access.assert_instance_view() # Verify the user was added correctly by starting an SSH session to the VM log.info("Verifying SSH connection to the test VM") stdout = ssh.run_command("echo -n $USER") assert_that(stdout).described_as("Output from SSH command").is_equal_to(username) log.info("SSH command output ($USER): %s", stdout) if __name__ == "__main__": VmAccessBvt.run_from_command_line() WALinuxAgent-2.9.1.1/tests_e2e/tests/error_test.py000077500000000000000000000016051446033677600220160ustar00rootroot00000000000000#!/usr/bin/env python3 # Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # from tests_e2e.tests.lib.agent_test import AgentTest class ErrorTest(AgentTest): """ A trivial test that errors out """ def run(self): raise Exception("* ERROR *") if __name__ == "__main__": ErrorTest.run_from_command_line() WALinuxAgent-2.9.1.1/tests_e2e/tests/fail_test.py000077500000000000000000000016161446033677600216020ustar00rootroot00000000000000#!/usr/bin/env python3 # Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # from assertpy import fail from tests_e2e.tests.lib.agent_test import AgentTest class FailTest(AgentTest): """ A trivial test that fails """ def run(self): fail("* FAILED *") if __name__ == "__main__": FailTest.run_from_command_line() WALinuxAgent-2.9.1.1/tests_e2e/tests/lib/000077500000000000000000000000001446033677600200155ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests_e2e/tests/lib/__init__.py000066400000000000000000000000001446033677600221140ustar00rootroot00000000000000WALinuxAgent-2.9.1.1/tests_e2e/tests/lib/agent_log.py000066400000000000000000000661261446033677600223410ustar00rootroot00000000000000# # Copyright 2018 Microsoft Corporation # # 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. # import os import re from datetime import datetime from pathlib import Path from typing import Any, AnyStr, Dict, Iterable, List, Match from azurelinuxagent.common.version import DISTRO_NAME, DISTRO_VERSION class AgentLogRecord: """ Represents an entry in the Agent's log (note that entries can span multiple lines in the log) Sample message: 2023-03-13T15:44:04.906673Z INFO ExtHandler ExtHandler Azure Linux Agent (Goal State Agent version 9.9.9.9) """ text: str # Full text of the record when: str # Timestamp (as text) level: str # Level (INFO, ERROR, etc) thread: str # Thread name (e.g. 'Daemon', 'ExtHandler') prefix: str # Prefix (e.g. 'Daemon', 'ExtHandler', ) message: str # Message @staticmethod def from_match(match: Match[AnyStr]): """Builds a record from a regex match""" record = AgentLogRecord() record.text = match.string record.when = match.group("when") record.level = match.group("level") record.thread = match.group("thread") record.prefix = match.group("prefix") record.message = match.group("message") return record @staticmethod def from_dictionary(dictionary: Dict[str, str]): """Deserializes from a dict""" record = AgentLogRecord() record.text = dictionary["text"] record.when = dictionary["when"] record.level = dictionary["level"] record.thread = dictionary["thread"] record.prefix = dictionary["prefix"] record.message = dictionary["message"] return record @property def timestamp(self) -> datetime: return datetime.strptime(self.when, u'%Y-%m-%dT%H:%M:%S.%fZ') class AgentLog(object): """ Provides facilities to parse and/or extract errors from the agent's log. """ def __init__(self, path: Path = Path('/var/log/waagent.log')): self._path: Path = path self._counter_table: Dict[str, int] = {} def get_errors(self) -> List[AgentLogRecord]: """ Returns any ERRORs or WARNINGs in the agent log. The function filters out known/uninteresting errors, which are kept in the 'ignore_list' variable. """ # # Items in this list are known errors and they are ignored. # # * 'message' - A regular expression matched using re.search; be sure to escape any regex metacharacters. A positive match indicates # that the error should be ignored # * 'if' - A lambda that takes as parameter an AgentLogRecord representing an error and returns true if the error should be ignored # ignore_rules = [ # # NOTE: This list was taken from the older agent tests and needs to be cleaned up. Feel free to un-comment rules as new tests are added. # # # This warning is expected on CentOS/RedHat 7.4, 7.8 and Redhat 7.6 # { # 'message': r"Move rules file 70-persistent-net.rules to /var/lib/waagent/70-persistent-net.rules", # 'if': lambda r: # re.match(r"(((centos|redhat)7\.[48])|(redhat7\.6)|(redhat8\.2))\D*", DISTRO_NAME, flags=re.IGNORECASE) is not None # and r.level == "WARNING" # and r.prefix == "ExtHandler" and r.thread in ("", "EnvHandler") # }, # # This warning is expected on SUSE 12 # { # 'message': r"WARNING EnvHandler ExtHandler Move rules file 75-persistent-net-generator.rules to /var/lib/waagent/75-persistent-net-generator.rules", # 'if': lambda _: re.match(r"((sles15\.2)|suse12)\D*", DISTRO_NAME, flags=re.IGNORECASE) is not None # }, # # The following message is expected to log an error if systemd is not enabled on it # { # 'message': r"Did not detect Systemd, unable to set wa(|linux)agent-network-setup.service", # 'if': lambda _: not self._is_systemd() # }, # # # # Journalctl in Debian 8.11 does not have the --utc option by default. # # Ignoring this error for Deb 8 as its not a blocker and since Deb 8 is old and not widely used # { # 'message': r"journalctl: unrecognized option '--utc'", # 'if': lambda r: re.match(r"(debian8\.11)\D*", DISTRO_NAME, flags=re.IGNORECASE) is not None and r.level == "WARNING" # }, # # Sometimes it takes the Daemon some time to identify primary interface and the route to Wireserver, # # ignoring those errors if they come from the Daemon. # { # 'message': r"(No route exists to \d+\.\d+\.\d+\.\d+|" # r"Could not determine primary interface, please ensure \/proc\/net\/route is correct|" # r"Contents of \/proc\/net\/route:|Primary interface examination will retry silently)", # 'if': lambda r: r.prefix == "Daemon" # }, # # # This happens in CENTOS and RHEL when waagent attempt to format and mount the error while cloud init is already doing it # # 2021-09-20T06:45:57.253801Z WARNING Daemon Daemon Could not mount resource disk: mount: /dev/sdb1 is already mounted or /mnt/resource busy # # /dev/sdb1 is already mounted on /mnt/resource # { # 'message': r"Could not mount resource disk: mount: \/dev\/sdb1 is already mounted or \/mnt\/resource busy", # 'if': lambda r: # re.match(r"((centos7\.8)|(redhat7\.8)|(redhat7\.6)|(redhat8\.2))\D*", DISTRO_NAME, flags=re.IGNORECASE) # and r.level == "WARNING" # and r.prefix == "Daemon" # }, # # # # 2021-09-20T06:45:57.246593Z ERROR Daemon Daemon Command: [mkfs.ext4 -F /dev/sdb1], return code: [1], result: [mke2fs 1.42.9 (28-Dec-2013) # # /dev/sdb1 is mounted; will not make a filesystem here! # { # 'message': r"Command: \[mkfs.ext4 -F \/dev\/sdb1\], return code: \[1\]", # 'if': lambda r: # re.match(r"((centos7\.8)|(redhat7\.8)|(redhat7\.6)|(redhat8\.2))\D*", DISTRO_NAME, flags=re.IGNORECASE) # and r.level == "ERROR" # and r.prefix == "Daemon" # }, # # # # 2022-01-20T06:52:21.515447Z WARNING Daemon Daemon Fetch failed: [HttpError] [HTTP Failed] GET https://dcrgajhx62.blob.core.windows.net/$system/edprpwqbj6.5c2ddb5b-d6c3-4d73-9468-54419ca87a97.vmSettings -- IOError timed out -- 6 attempts made # # # # The daemon does not need the artifacts profile blob, but the request is done as part of protocol initialization. This timeout can be ignored, if the issue persist the log would include additional instances. # # # { # 'message': r"\[HTTP Failed\] GET https://.*\.vmSettings -- IOError timed out", # 'if': lambda r: r.level == "WARNING" and r.prefix == "Daemon" # }, # # Probably the agent should log this as INFO, but for now it is a warning # e.g. # 2021-07-29T04:40:17.190879Z WARNING EnvHandler ExtHandler Dhcp client is not running. # old agents logs don't have a prefix of thread and/or logger names. { 'message': r"Dhcp client is not running.", 'if': lambda r: r.level == "WARNING" }, # Known bug fixed in the current agent, but still present in older daemons # { 'message': r"\[CGroupsException\].*Error: join\(\) argument must be str, bytes, or os.PathLike object, not 'NoneType'", 'if': lambda r: r.level == "WARNING" and r.prefix == "Daemon" }, # This warning is expected on when WireServer gives us the incomplete goalstate without roleinstance data { 'message': r"\[ProtocolError\] Fetched goal state without a RoleInstance", }, # # Download warnings (manifest and zips). # # Examples: # 2021-03-31T03:48:35.216494Z WARNING ExtHandler ExtHandler Fetch failed: [HttpError] [HTTP Failed] GET https://zrdfepirv2cbn04prdstr01a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712/Microsoft.CPlat.Core_NullSeqA_useast2euap_manifest.xml -- IOError ('The read operation timed out',) -- 1 attempts made # 2021-03-31T06:54:29.655861Z WARNING ExtHandler ExtHandler Fetch failed: [HttpError] [HTTP Retry] GET http://168.63.129.16:32526/extensionArtifact -- Status Code 502 -- 1 attempts made # 2021-03-31T06:43:17.806663Z WARNING ExtHandler ExtHandler Download failed, switching to host plugin { 'message': r"(Fetch failed: \[HttpError\] .+ GET .+ -- [0-9]+ attempts made)|(Download failed, switching to host plugin)", 'if': lambda r: r.level == "WARNING" and r.prefix == "ExtHandler" and r.thread == "ExtHandler" }, # 2021-07-09T01:46:53.307959Z INFO MonitorHandler ExtHandler [CGW] Disabling resource usage monitoring. Reason: Check on cgroups failed: # [CGroupsException] The agent's cgroup includes unexpected processes: ['[PID: 2367] UNKNOWN'] { 'message': r"The agent's cgroup includes unexpected processes: \[('\[PID:\s?\d+\]\s*UNKNOWN'(,\s*)?)+\]" }, # 2021-12-20T07:46:23.020197Z INFO ExtHandler ExtHandler [CGW] The agent's process is not within a memory cgroup # Ignoring this since memory cgroup(MemoryAccounting) not enabled. { 'message': r"The agent's process is not within a memory cgroup", 'if': lambda r: re.match(r"(((centos|redhat)7\.[48])|(redhat7\.6)|(redhat8\.2))\D*", DISTRO_NAME, flags=re.IGNORECASE) }, # # Ubuntu 22 uses cgroups v2, so we need to ignore these: # # 2023-03-15T20:47:56.684849Z INFO ExtHandler ExtHandler [CGW] The CPU cgroup controller is not mounted # 2023-03-15T20:47:56.685392Z INFO ExtHandler ExtHandler [CGW] The memory cgroup controller is not mounted # 2023-03-15T20:47:56.688576Z INFO ExtHandler ExtHandler [CGW] The agent's process is not within a CPU cgroup # 2023-03-15T20:47:56.688981Z INFO ExtHandler ExtHandler [CGW] The agent's process is not within a memory cgroup # { 'message': r"\[CGW\]\s*(The (CPU|memory) cgroup controller is not mounted)|(The agent's process is not within a (CPU|memory) cgroup)", 'if': lambda r: DISTRO_NAME == 'ubuntu' and DISTRO_VERSION >= '22.00' }, # # 2022-02-09T04:50:37.384810Z ERROR ExtHandler ExtHandler Error fetching the goal state: [ProtocolError] GET vmSettings [correlation ID: 2bed9b62-188e-4668-b1a8-87c35cfa4927 eTag: 7031887032544600793]: [Internal error in HostGAPlugin] [HTTP Failed] [502: Bad Gateway] b'{ "errorCode": "VMArtifactsProfileBlobContentNotFound", "message": "VM artifacts profile blob has no content in it.", "details": ""}' # # Fetching the goal state may catch the HostGAPlugin in the process of computing the vmSettings. This can be ignored, if the issue persist the log would include other errors as well. # { 'message': r"\[ProtocolError\] GET vmSettings.*VMArtifactsProfileBlobContentNotFound", 'if': lambda r: r.level == "ERROR" }, # # 2022-11-01T02:45:55.513692Z ERROR ExtHandler ExtHandler Error fetching the goal state: [ProtocolError] GET vmSettings [correlation ID: 616873cc-be87-41b6-83b7-ef3a76370628 eTag: 3693655388249891516]: [Internal error in HostGAPlugin] [HTTP Failed] [502: Bad Gateway] { "errorCode": "InternalError", "message": "The server encountered an internal error. Please retry the request.", "details": ""} # # Fetching the goal state may catch the HostGAPlugin in the process of computing the vmSettings. This can be ignored, if the issue persist the log would include other errors as well. # { 'message': r"\[ProtocolError\] GET vmSettings.*Please retry the request", 'if': lambda r: r.level == "ERROR" }, # # 2022-08-16T01:50:10.759502Z ERROR ExtHandler ExtHandler Error fetching the goal state: [ProtocolError] GET vmSettings [correlation ID: e162f7c3-8d0c-4a9b-a987-8f9ec0699dae eTag: 9757461589808963322]: Timeout # # Fetching the goal state may hit timeouts in the HostGAPlugin's vmSettings. This can be ignored, if the issue persist the log would include other errors as well. # { 'message': r"\[ProtocolError\] GET vmSettings.*Timeout", 'if': lambda r: r.level == "ERROR" }, # # 2021-12-29T06:50:49.904601Z ERROR ExtHandler ExtHandler Error fetching the goal state: [ProtocolError] Error fetching goal state Inner error: [ResourceGoneError] [HTTP Failed] [410: Gone] The page you requested was removed. # 2022-03-21T02:44:03.770017Z ERROR ExtHandler ExtHandler Error fetching the goal state: [ProtocolError] Error fetching goal state Inner error: [ResourceGoneError] Resource is gone # 2022-02-16T04:46:50.477315Z WARNING Daemon Daemon Fetching the goal state failed: [ResourceGoneError] [HTTP Failed] [410: Gone] b'\n\n ResourceNotAvailable\n The resource requested is no longer available. Please refresh your cache.\n

\n' # # ResourceGone can happen if we are fetching one of the URIs in the goal state and a new goal state arrives { 'message': r"(?s)(Fetching the goal state failed|Error fetching goal state|Error fetching the goal state).*(\[ResourceGoneError\]|\[410: Gone\]|Resource is gone)", 'if': lambda r: r.level in ("WARNING", "ERROR") }, # # 2022-12-02T05:45:51.771876Z ERROR ExtHandler ExtHandler Error fetching the goal state: [ProtocolError] [Wireserver Exception] [HttpError] [HTTP Failed] GET http://168.63.129.16/machine/ -- IOError [Errno 104] Connection reset by peer -- 6 attempts made # { 'message': r"\[HttpError\] \[HTTP Failed\] GET http://168.63.129.16/machine/ -- IOError \[Errno 104\] Connection reset by peer", 'if': lambda r: r.level in ("WARNING", "ERROR") }, # # 2022-03-08T03:03:23.036161Z WARNING ExtHandler ExtHandler Fetch failed from [http://168.63.129.16:32526/extensionArtifact]: [HTTP Failed] [400: Bad Request] b'' # 2022-03-08T03:03:23.042008Z WARNING ExtHandler ExtHandler Fetch failed: [ProtocolError] Fetch failed from [http://168.63.129.16:32526/extensionArtifact]: [HTTP Failed] [400: Bad Request] b'' # # Warning downloading extension manifest. If the issue persists, this would cause errors elsewhere so safe to ignore { 'message': r"\[http://168.63.129.16:32526/extensionArtifact\]: \[HTTP Failed\] \[400: Bad Request\]", 'if': lambda r: r.level == "WARNING" }, # # 2022-03-29T05:52:10.089958Z WARNING ExtHandler ExtHandler An error occurred while retrieving the goal state: [ProtocolError] GET vmSettings [correlation ID: da106cf5-83a0-44ec-9484-d0e9223847ab eTag: 9856274988128027586]: Timeout # # Ignore warnings about timeouts in vmSettings; if the condition persists, an error will occur elsewhere. # { 'message': r"GET vmSettings \[[^]]+\]: Timeout", 'if': lambda r: r.level == "WARNING" }, # # 2022-09-30T02:48:33.134649Z WARNING MonitorHandler ExtHandler Error in SendHostPluginHeartbeat: [HttpError] [HTTP Failed] GET http://168.63.129.16:32526/health -- IOError timed out -- 1 attempts made --- [NOTE: Will not log the same error for the next hour] # # Ignore timeouts in the HGAP's health API... those are tracked in the HGAP dashboard so no need to worry about them on test runs # { 'message': r"SendHostPluginHeartbeat:.*GET http://168.63.129.16:32526/health.*timed out", 'if': lambda r: r.level == "WARNING" }, # # 2022-09-30T03:09:25.013398Z WARNING MonitorHandler ExtHandler Error in SendHostPluginHeartbeat: [ResourceGoneError] [HTTP Failed] [410: Gone] # # ResourceGone should not happen very often, since the monitor thread already refreshes the goal state before sending the HostGAPlugin heartbeat. Errors can still happen, though, since the goal state # can change in-between the time at which the monitor thread refreshes and the time at which it sends the heartbeat. Ignore these warnings unless there are 2 or more of them. # { 'message': r"SendHostPluginHeartbeat:.*ResourceGoneError.*410", 'if': lambda r: r.level == "WARNING" and self._increment_counter("SendHostPluginHeartbeat-ResourceGoneError-410") < 2 # ignore unless there are 2 or more instances }, # 2023-01-18T02:58:25.589492Z ERROR SendTelemetryHandler ExtHandler Event: name=WALinuxAgent, op=ReportEventErrors, message=DroppedEventsCount: 1 # Reasons (first 5 errors): [ProtocolError] [Wireserver Exception] [ProtocolError] [Wireserver Failed] URI http://168.63.129.16/machine?comp=telemetrydata [HTTP Failed] Status Code 400: Traceback (most recent call last): # { 'message': r"(?s)SendTelemetryHandler.*http://168.63.129.16/machine\?comp=telemetrydata.*Status Code 400", 'if': lambda _: self._increment_counter("SendTelemetryHandler-telemetrydata-Status Code 400") < 2 # ignore unless there are 2 or more instances }, # # Ignore these errors in flatcar: # # 1) 2023-03-16T14:30:33.091427Z ERROR Daemon Daemon Failed to mount resource disk [ResourceDiskError] unable to detect disk topology # 2) 2023-03-16T14:30:33.091708Z ERROR Daemon Daemon Event: name=WALinuxAgent, op=ActivateResourceDisk, message=[ResourceDiskError] unable to detect disk topology, duration=0 # 3) 2023-03-16T14:30:34.660976Z WARNING ExtHandler ExtHandler Fetch failed: [HttpError] HTTPS is unavailable and required # 4) 2023-03-16T14:30:34.800112Z ERROR ExtHandler ExtHandler Unable to setup the persistent firewall rules: [Errno 30] Read-only file system: '/lib/systemd/system/waagent-network-setup.service' # # 1, 2) under investigation # 3) There seems to be a configuration issue in flatcar that prevents python from using HTTPS when trying to reach storage. This does not produce any actual errors, since the agent fallbacks to the HGAP. # 4) Remove this when bug 17523033 is fixed. # { 'message': r"(Failed to mount resource disk)|(unable to detect disk topology)", 'if': lambda r: r.prefix == 'Daemon' and DISTRO_NAME == 'flatcar' }, { 'message': r"(HTTPS is unavailable and required)|(Unable to setup the persistent firewall rules.*Read-only file system)", 'if': lambda r: DISTRO_NAME == 'flatcar' }, # # AzureSecurityLinuxAgent fails to install on a few distros (e.g. Debian 11) # # 2023-03-16T14:29:48.798415Z ERROR ExtHandler ExtHandler Event: name=Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent, op=Install, message=[ExtensionOperationError] Non-zero exit code: 56, /var/lib/waagent/Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent-2.21.115/handler.sh install # { 'message': r"Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent.*op=Install.*Non-zero exit code: 56,", }, ] def is_error(r: AgentLogRecord) -> bool: return r.level in ('ERROR', 'WARNING') or any(err in r.text for err in ['Exception', 'Traceback', '[CGW]']) errors = [] primary_interface_error = None provisioning_complete = False for record in self.read(): if is_error(record) and not self.matches_ignore_rule(record, ignore_rules): # Handle "/proc/net/route contains no routes" and "/proc/net/route is missing headers" as a special case # since it can take time for the primary interface to come up, and we don't want to report transient # errors as actual errors. The last of these errors in the log will be reported if "/proc/net/route contains no routes" in record.text or "/proc/net/route is missing headers" in record.text and record.prefix == "Daemon": primary_interface_error = record provisioning_complete = False else: errors.append(record) if "Provisioning complete" in record.text and record.prefix == "Daemon": provisioning_complete = True # Keep the "no routes found" as a genuine error message if it was never corrected if primary_interface_error is not None and not provisioning_complete: errors.append(primary_interface_error) return errors @staticmethod def _is_systemd(): # Taken from azurelinuxagent/common/osutil/systemd.py; repeated here because it is available only on agents >= 2.3 return os.path.exists("/run/systemd/system/") def _increment_counter(self, counter_name) -> int: """ Keeps a table of counters indexed by the given 'counter_name'. Each call to the function increments the value of that counter and returns the new value. """ count = self._counter_table.get(counter_name) count = 1 if count is None else count + 1 self._counter_table[counter_name] = count return count @staticmethod def matches_ignore_rule(record: AgentLogRecord, ignore_rules: List[Dict[str, Any]]) -> bool: """ Returns True if the given 'record' matches any of the 'ignore_rules' """ return any(re.search(rule['message'], record.message) is not None and ('if' not in rule or rule['if'](record)) for rule in ignore_rules) # The format of the log has changed over time and the current log may include records from different sources. Most records are single-line, but some of them # can span across multiple lines. We will assume records always start with a line similar to the examples below; any other lines will be assumed to be part # of the record that is being currently parsed. # # Newer Agent: 2019-11-27T22:22:48.123985Z VERBOSE ExtHandler ExtHandler Report vm agent status # 2021-03-30T19:45:33.793213Z INFO ExtHandler [Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent-2.14.64] Target handler state: enabled [incarnation 3] # # 2.2.46: the date time was changed to ISO-8601 format but the thread name was not added. # 2021-05-28T01:17:40.683072Z INFO ExtHandler Wire server endpoint:168.63.129.16 # 2021-05-28T01:17:40.683823Z WARNING ExtHandler Move rules file 70-persistent-net.rules to /var/lib/waagent/70-persistent-net.rules # 2021-05-28T01:17:40.767600Z INFO ExtHandler Successfully added Azure fabric firewall rules # # Older Agent: 2021/03/30 19:35:35.971742 INFO Daemon Azure Linux Agent Version:2.2.45 # # Extension: 2021/03/30 19:45:31 Azure Monitoring Agent for Linux started to handle. # 2021/03/30 19:45:31 [Microsoft.Azure.Monitor.AzureMonitorLinuxAgent-1.7.0] cwd is /var/lib/waagent/Microsoft.Azure.Monitor.AzureMonitorLinuxAgent-1.7.0 # _NEWER_AGENT_RECORD = re.compile(r'(?P[\d-]+T[\d:.]+Z)\s(?PVERBOSE|INFO|WARNING|ERROR)\s(?P\S+)\s(?P(Daemon)|(ExtHandler)|(\[\S+\]))\s(?P.*)') _2_2_46_AGENT_RECORD = re.compile(r'(?P[\d-]+T[\d:.]+Z)\s(?PVERBOSE|INFO|WARNING|ERROR)\s(?P)(?PDaemon|ExtHandler|\[\S+\])\s(?P.*)') _OLDER_AGENT_RECORD = re.compile(r'(?P[\d/]+\s[\d:.]+)\s(?PVERBOSE|INFO|WARNING|ERROR)\s(?P)(?P\S*)\s(?P.*)') _EXTENSION_RECORD = re.compile(r'(?P[\d/]+\s[\d:.]+)\s(?P)(?P)((?P\[[^\]]+\])\s)?(?P.*)') def read(self) -> Iterable[AgentLogRecord]: """ Generator function that returns each of the entries in the agent log parsed as AgentLogRecords. The function can be used following this pattern: for record in read_agent_log(): ... do something... """ if not self._path.exists(): raise IOError('{0} does not exist'.format(self._path)) def match_record(): for regex in [self._NEWER_AGENT_RECORD, self._2_2_46_AGENT_RECORD, self._OLDER_AGENT_RECORD]: m = regex.match(line) if m is not None: return m # The extension regex also matches the old agent records, so it needs to be last return self._EXTENSION_RECORD.match(line) def complete_record(): record.text = record.text.rstrip() # the text includes \n if extra_lines != "": record.text = record.text + "\n" + extra_lines.rstrip() record.message = record.message + "\n" + extra_lines.rstrip() return record with self._path.open() as file_: record = None extra_lines = "" line = file_.readline() while line != "": # while not EOF match = match_record() if match is not None: if record is not None: yield complete_record() record = AgentLogRecord.from_match(match) extra_lines = "" else: extra_lines = extra_lines + line line = file_.readline() if record is not None: yield complete_record() WALinuxAgent-2.9.1.1/tests_e2e/tests/lib/agent_test.py000066400000000000000000000040611446033677600225250ustar00rootroot00000000000000#!/usr/bin/env python3 # Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # import sys from abc import ABC, abstractmethod from typing import Any, Dict, List from tests_e2e.tests.lib.agent_test_context import AgentTestContext from tests_e2e.tests.lib.logging import log class TestSkipped(Exception): """ Tests can raise this exception to indicate they should not be executed (for example, if trying to execute them on an unsupported distro """ class AgentTest(ABC): """ Defines the interface for agent tests, which are simply constructed from an AgentTestContext and expose a single method, run(), to execute the test. """ def __init__(self, context: AgentTestContext): self._context = context @abstractmethod def run(self): pass def get_ignore_error_rules(self) -> List[Dict[str, Any]]: # Tests can override this method to return a list with rules to ignore errors in the agent log (see agent_log.py for sample rules). return [] @classmethod def run_from_command_line(cls): """ Convenience method to execute the test when it is being invoked directly from the command line (as opposed as being invoked from a test framework or library. """ try: cls(AgentTestContext.from_args()).run() except SystemExit: # Bad arguments pass except: # pylint: disable=bare-except log.exception("Test failed") sys.exit(1) sys.exit(0) WALinuxAgent-2.9.1.1/tests_e2e/tests/lib/agent_test_context.py000066400000000000000000000136421446033677600242760ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # import argparse import os from pathlib import Path import tests_e2e from tests_e2e.tests.lib.identifiers import VmIdentifier class AgentTestContext: """ Execution context for agent tests. Defines the test VM, working directories and connection info for the tests. NOTE: The context is shared by all tests in the same runbook execution. Tests within the same test suite are executed sequentially, but multiple test suites may be executed concurrently depending on the concurrency level of the runbook. """ class Paths: DEFAULT_TEST_SOURCE_DIRECTORY = Path(tests_e2e.__path__[0]) def __init__( self, working_directory: Path, remote_working_directory: Path, test_source_directory: Path = DEFAULT_TEST_SOURCE_DIRECTORY ): self._test_source_directory: Path = test_source_directory self._working_directory: Path = working_directory self._remote_working_directory: Path = remote_working_directory class Connection: DEFAULT_SSH_PORT = 22 def __init__( self, ip_address: str, username: str, private_key_file: Path, ssh_port: int = DEFAULT_SSH_PORT ): self._ip_address: str = ip_address self._username: str = username self._private_key_file: Path = private_key_file self._ssh_port: int = ssh_port def __init__(self, vm: VmIdentifier, paths: Paths, connection: Connection): self._vm: VmIdentifier = vm self._paths = paths self._connection = connection @property def vm(self) -> VmIdentifier: """ The test VM (the VM on which the tested Agent is running) """ return self._vm @property def vm_ip_address(self) -> str: """ The IP address of the test VM """ return self._connection._ip_address @property def test_source_directory(self) -> Path: """ Root directory for the source code of the tests. Used to build paths to specific scripts. """ return self._paths._test_source_directory @property def working_directory(self) -> Path: """ Tests can create temporary files under this directory. """ return self._paths._working_directory @property def remote_working_directory(self) -> Path: """ Tests can create temporary files under this directory on the test VM. """ return self._paths._remote_working_directory @property def username(self) -> str: """ The username to use for SSH connections """ return self._connection._username @property def private_key_file(self) -> Path: """ The file containing the private SSH key for the username """ return self._connection._private_key_file @property def ssh_port(self) -> int: """ Port for SSH connections """ return self._connection._ssh_port @staticmethod def from_args(): """ Creates an AgentTestContext from the command line arguments. """ parser = argparse.ArgumentParser() parser.add_argument('-g', '--group', required=True) parser.add_argument('-l', '--location', required=True) parser.add_argument('-s', '--subscription', required=True) parser.add_argument('-vm', '--vm', required=True) parser.add_argument('-rw', '--remote-working-directory', dest="remote_working_directory", required=False, default=str(Path('/home')/os.getenv("USER"))) parser.add_argument('-t', '--test-source-directory', dest="test_source_directory", required=False, default=str(AgentTestContext.Paths.DEFAULT_TEST_SOURCE_DIRECTORY)) parser.add_argument('-w', '--working-directory', dest="working_directory", required=False, default=str(Path().home()/"tmp")) parser.add_argument('-a', '--ip-address', dest="ip_address", required=False) # Use the vm name as default parser.add_argument('-u', '--username', required=False, default=os.getenv("USER")) parser.add_argument('-k', '--private-key-file', dest="private_key_file", required=False, default=Path.home()/".ssh"/"id_rsa") parser.add_argument('-p', '--ssh-port', dest="ssh_port", required=False, default=AgentTestContext.Connection.DEFAULT_SSH_PORT) args = parser.parse_args() working_directory = Path(args.working_directory) if not working_directory.exists(): working_directory.mkdir(exist_ok=True) return AgentTestContext( vm=VmIdentifier( location=args.location, subscription=args.subscription, resource_group=args.group, name=args.vm), paths=AgentTestContext.Paths( working_directory=working_directory, remote_working_directory=Path(args.remote_working_directory), test_source_directory=Path(args.test_source_directory)), connection=AgentTestContext.Connection( ip_address=args.ip_address if args.ip_address is not None else args.vm, username=args.username, private_key_file=Path(args.private_key_file), ssh_port=args.ssh_port)) WALinuxAgent-2.9.1.1/tests_e2e/tests/lib/identifiers.py000066400000000000000000000051561446033677600227030ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # class VmIdentifier(object): def __init__(self, location, subscription, resource_group, name): """ Represents the information that identifies a VM to the ARM APIs """ self.location = location self.subscription: str = subscription self.resource_group: str = resource_group self.name: str = name def __str__(self): return f"{self.resource_group}:{self.name}" class VmExtensionIdentifier(object): def __init__(self, publisher, ext_type, version): """ Represents the information that identifies an extension to the ARM APIs publisher - e.g. Microsoft.Azure.Extensions type - e.g. CustomScript version - e.g. 2.1, 2.* name - arbitrary name for the extension ARM resource """ self.publisher: str = publisher self.type: str = ext_type self.version: str = version def __str__(self): return f"{self.publisher}.{self.type}" class VmExtensionIds(object): """ A set of extensions used by the tests, listed here for convenience (easy to reference them by name). Only the major version is specified, and the minor version is set to 0 (set autoUpgradeMinorVersion to True in the call to enable to use the latest version) """ CustomScript: VmExtensionIdentifier = VmExtensionIdentifier(publisher='Microsoft.Azure.Extensions', ext_type='CustomScript', version="2.0") # Older run command extension, still used by the Portal as of Dec 2022 RunCommand: VmExtensionIdentifier = VmExtensionIdentifier(publisher='Microsoft.CPlat.Core', ext_type='RunCommandLinux', version="1.0") # New run command extension, with support for multi-config RunCommandHandler: VmExtensionIdentifier = VmExtensionIdentifier(publisher='Microsoft.CPlat.Core', ext_type='RunCommandHandlerLinux', version="1.0") VmAccess: VmExtensionIdentifier = VmExtensionIdentifier(publisher='Microsoft.OSTCExtensions', ext_type='VMAccessForLinux', version="1.0") WALinuxAgent-2.9.1.1/tests_e2e/tests/lib/logging.py000066400000000000000000000137761446033677600220330ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # # This module defines a single object, 'log', of type AgentLogger, which the end-to-end tests and libraries use # for logging. # import contextlib from logging import FileHandler, Formatter, Handler, Logger, StreamHandler, INFO from pathlib import Path from threading import current_thread from typing import Dict, Callable class _AgentLoggingHandler(Handler): """ AgentLoggingHandler is a helper class for AgentLogger. This handler simply redirects logging to other handlers. It maintains a set of FileHandlers associated to specific threads. When a thread emits a log record, the AgentLoggingHandler passes through the call to the FileHandlers associated with that thread, or to a StreamHandler that outputs to stdout if there is not a FileHandler for that thread. Thread can set a FileHandler for themselves using _AgentLoggingHandler.set_current_thread_log() and remove that handler using _AgentLoggingHandler.close_current_thread_log(). The _AgentLoggingHandler simply passes through calls to setLevel, setFormatter, flush, and close to the handlers it maintains. AgentLoggingHandler is meant to be primarily used in multithreaded scenarios and is thread-safe. """ def __init__(self): super().__init__() self.formatter: Formatter = Formatter('%(asctime)s.%(msecs)03d [%(levelname)s] %(message)s', datefmt="%Y-%m-%dT%H:%M:%SZ") self.default_handler = StreamHandler() self.default_handler.setFormatter(self.formatter) self.per_thread_handlers: Dict[int, FileHandler] = {} def set_thread_log(self, thread_ident: int, log_file: Path) -> None: self.close_current_thread_log() handler: FileHandler = FileHandler(str(log_file)) handler.setFormatter(self.formatter) self.per_thread_handlers[thread_ident] = handler def get_thread_log(self, thread_ident: int) -> Path: handler = self.per_thread_handlers.get(thread_ident) if handler is None: return None return Path(handler.baseFilename) def close_thread_log(self, thread_ident: int) -> None: handler = self.per_thread_handlers.pop(thread_ident, None) if handler is not None: handler.close() def set_current_thread_log(self, log_file: Path) -> None: self.set_thread_log(current_thread().ident, log_file) def get_current_thread_log(self) -> Path: return self.get_thread_log(current_thread().ident) def close_current_thread_log(self) -> None: self.close_thread_log(current_thread().ident) def emit(self, record) -> None: handler = self.per_thread_handlers.get(current_thread().ident) if handler is None: handler = self.default_handler handler.emit(record) def setLevel(self, level) -> None: self._for_each_handler(lambda h: h.setLevel(level)) def setFormatter(self, fmt) -> None: self._for_each_handler(lambda h: h.setFormatter(fmt)) def flush(self) -> None: self._for_each_handler(lambda h: h.flush()) def close(self) -> None: self._for_each_handler(lambda h: h.close()) def _for_each_handler(self, op: Callable[[Handler], None]) -> None: op(self.default_handler) # copy of the values into a new list in case the dictionary changes while we are iterating for handler in list(self.per_thread_handlers.values()): op(handler) class AgentLogger(Logger): """ AgentLogger is a Logger customized for agent test scenarios. When tests are executed from the command line (for example, during development) the AgentLogger can be used with its default configuration, which simply outputs to stdout. When tests are executed from the test framework, typically there are multiple test suites executed concurrently on different threads, and each test suite must have its own log file; in that case, each thread can call AgentLogger.set_current_thread_log() to send all the logging from that thread to a particular file. """ def __init__(self): super().__init__(name="waagent", level=INFO) self._handler: _AgentLoggingHandler = _AgentLoggingHandler() self.addHandler(self._handler) def set_thread_log(self, thread_ident: int, log_file: Path) -> None: self._handler.set_thread_log(thread_ident, log_file) def get_thread_log_file(self, thread_ident: int) -> Path: """ Returns the Path of the log file for the current thread, or None if a log has not been set """ return self._handler.get_thread_log(thread_ident) def close_thread_log(self, thread_ident: int) -> None: self._handler.close_thread_log(thread_ident) def set_current_thread_log(self, log_file: Path) -> None: self._handler.set_current_thread_log(log_file) def get_current_thread_log(self) -> Path: return self._handler.get_current_thread_log() def close_current_thread_log(self) -> None: self._handler.close_current_thread_log() log: AgentLogger = AgentLogger() @contextlib.contextmanager def set_current_thread_log(log_file: Path): """ Context Manager to set the log file for the current thread temporarily """ initial_value = log.get_current_thread_log() log.set_current_thread_log(log_file) try: yield finally: log.close_current_thread_log() if initial_value is not None: log.set_current_thread_log(initial_value) WALinuxAgent-2.9.1.1/tests_e2e/tests/lib/retry.py000066400000000000000000000043001446033677600215310ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # import time from typing import Callable, Any from tests_e2e.tests.lib.logging import log from tests_e2e.tests.lib.shell import CommandError def execute_with_retry(operation: Callable[[], Any]) -> Any: """ Some Azure errors (e.g. throttling) are retryable; this method attempts the given operation retrying a few times (after a short delay) if the error includes the string "RetryableError" """ attempts = 3 while attempts > 0: attempts -= 1 try: return operation() except Exception as e: # TODO: Do we need to retry on msrestazure.azure_exceptions.CloudError? if "RetryableError" not in str(e) or attempts == 0: raise log.warning("The operation failed with a RetryableError, retrying in 30 secs. Error: %s", e) time.sleep(30) def retry_ssh_run(operation: Callable[[], Any]) -> Any: """ This method attempts to retry ssh run command a few times if operation failed with connection time out """ attempts = 3 while attempts > 0: attempts -= 1 try: return operation() except Exception as e: # We raise CommandError on !=0 exit codes in the called method if isinstance(e, CommandError): # Instance of 'Exception' has no 'exit_code' member (no-member) - Disabled: e is actually an CommandError if e.exit_code != 255 or attempts == 0: # pylint: disable=no-member raise log.warning("The operation failed with %s, retrying in 30 secs.", e) time.sleep(30) WALinuxAgent-2.9.1.1/tests_e2e/tests/lib/shell.py000066400000000000000000000037751446033677600215120ustar00rootroot00000000000000#!/usr/bin/env python3 # Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # from subprocess import Popen, PIPE from typing import Any class CommandError(Exception): """ Exception raised by run_command when the command returns an error """ def __init__(self, command: Any, exit_code: int, stdout: str, stderr: str): super().__init__(f"'{command}' failed (exit code: {exit_code}): {stderr}") self.command: Any = command self.exit_code: int = exit_code self.stdout: str = stdout self.stderr: str = stderr def __str__(self): return f"'{self.command}' failed (exit code: {self.exit_code})\nstdout:\n{self.stdout}\nstderr:\n{self.stderr}\n" def run_command(command: Any, shell=False) -> str: """ This function is a thin wrapper around Popen/communicate in the subprocess module. It executes the given command and returns its stdout. If the command returns a non-zero exit code, the function raises a RunCommandException. Similarly to Popen, the 'command' can be a string or a list of strings, and 'shell' indicates whether to execute the command through the shell. NOTE: The command's stdout and stderr are read as text streams. """ process = Popen(command, stdout=PIPE, stderr=PIPE, shell=shell, text=True) stdout, stderr = process.communicate() if process.returncode != 0: raise CommandError(command, process.returncode, stdout, stderr) return stdout WALinuxAgent-2.9.1.1/tests_e2e/tests/lib/ssh_client.py000066400000000000000000000067511446033677600225330ustar00rootroot00000000000000#!/usr/bin/env python3 # Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # import re from pathlib import Path from tests_e2e.tests.lib import shell from tests_e2e.tests.lib.retry import retry_ssh_run class SshClient(object): def __init__(self, ip_address: str, username: str, private_key_file: Path, port: int = 22): self._ip_address: str = ip_address self._username: str = username self._private_key_file: Path = private_key_file self._port: int = port def run_command(self, command: str, use_sudo: bool = False) -> str: """ Executes the given command over SSH and returns its stdout. If the command returns a non-zero exit code, the function raises a RunCommandException. """ if re.match(r"^\s*sudo\s*", command): raise Exception("Do not include 'sudo' in the 'command' argument, use the 'use_sudo' parameter instead") destination = f"ssh://{self._username}@{self._ip_address}:{self._port}" # Note that we add ~/bin to the remote PATH, since Python (Pypy) and other test tools are installed there. # Note, too, that when using sudo we need to carry over the value of PATH to the sudo session sudo = "sudo env PATH=$PATH PYTHONPATH=$PYTHONPATH" if use_sudo else '' return retry_ssh_run(lambda: shell.run_command([ "ssh", "-o", "StrictHostKeyChecking=no", "-i", self._private_key_file, destination, f"if [[ -e ~/bin/set-agent-env ]]; then source ~/bin/set-agent-env; fi; {sudo} {command}"])) @staticmethod def generate_ssh_key(private_key_file: Path): """ Generates an SSH key on the given Path """ shell.run_command( ["ssh-keygen", "-m", "PEM", "-t", "rsa", "-b", "4096", "-q", "-N", "", "-f", str(private_key_file)]) def get_architecture(self): return self.run_command("uname -m").rstrip() def copy_to_node(self, local_path: Path, remote_path: Path, recursive: bool = False) -> None: """ File copy to a remote node """ self._copy(local_path, remote_path, remote_source=False, remote_target=True, recursive=recursive) def copy_from_node(self, remote_path: Path, local_path: Path, recursive: bool = False) -> None: """ File copy from a remote node """ self._copy(remote_path, local_path, remote_source=True, remote_target=False, recursive=recursive) def _copy(self, source: Path, target: Path, remote_source: bool, remote_target: bool, recursive: bool) -> None: if remote_source: source = f"{self._username}@{self._ip_address}:{source}" if remote_target: target = f"{self._username}@{self._ip_address}:{target}" command = ["scp", "-o", "StrictHostKeyChecking=no", "-i", self._private_key_file] if recursive: command.append("-r") command.extend([str(source), str(target)]) shell.run_command(command) WALinuxAgent-2.9.1.1/tests_e2e/tests/lib/virtual_machine.py000066400000000000000000000143551446033677600235510ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # # This module includes facilities to execute some operations on virtual machines and scale sets (list extensions, restart, etc). # from abc import ABC, abstractmethod from builtins import TimeoutError from typing import Any, List from azure.core.polling import LROPoller from azure.identity import DefaultAzureCredential from azure.mgmt.compute import ComputeManagementClient from azure.mgmt.compute.models import VirtualMachineExtension, VirtualMachineScaleSetExtension, VirtualMachineInstanceView, VirtualMachineScaleSetInstanceView from azure.mgmt.resource import ResourceManagementClient from tests_e2e.tests.lib.identifiers import VmIdentifier from tests_e2e.tests.lib.logging import log from tests_e2e.tests.lib.retry import execute_with_retry class VirtualMachineBaseClass(ABC): """ Abstract base class for VirtualMachine and VmScaleSet. Defines the interface common to both classes and provides the implementation of some methods in that interface. """ def __init__(self, vm: VmIdentifier): super().__init__() self._identifier: VmIdentifier = vm self._compute_client = ComputeManagementClient(credential=DefaultAzureCredential(), subscription_id=vm.subscription) self._resource_client = ResourceManagementClient(credential=DefaultAzureCredential(), subscription_id=vm.subscription) @abstractmethod def get_instance_view(self) -> Any: # Returns VirtualMachineInstanceView or VirtualMachineScaleSetInstanceView """ Retrieves the instance view of the virtual machine or scale set """ @abstractmethod def get_extensions(self) -> Any: # Returns List[VirtualMachineExtension] or List[VirtualMachineScaleSetExtension] """ Retrieves the extensions installed on the virtual machine or scale set """ def restart(self, timeout=5 * 60) -> None: """ Restarts the virtual machine or scale set """ log.info("Initiating restart of %s", self._identifier) poller: LROPoller = execute_with_retry(self._begin_restart) poller.wait(timeout=timeout) if not poller.done(): raise TimeoutError(f"Failed to restart {self._identifier.name} after {timeout} seconds") log.info("Restarted %s", self._identifier.name) @abstractmethod def _begin_restart(self) -> LROPoller: """ Derived classes must provide the implementation for this method using their corresponding begin_restart() implementation """ def __str__(self): return f"{self._identifier}" class VirtualMachine(VirtualMachineBaseClass): def get_instance_view(self) -> VirtualMachineInstanceView: log.info("Retrieving instance view for %s", self._identifier) return execute_with_retry(lambda: self._compute_client.virtual_machines.get( resource_group_name=self._identifier.resource_group, vm_name=self._identifier.name, expand="instanceView" ).instance_view) def get_extensions(self) -> List[VirtualMachineExtension]: log.info("Retrieving extensions for %s", self._identifier) return execute_with_retry(lambda: self._compute_client.virtual_machine_extensions.list( resource_group_name=self._identifier.resource_group, vm_name=self._identifier.name)) def _begin_restart(self) -> LROPoller: return self._compute_client.virtual_machines.begin_restart( resource_group_name=self._identifier.resource_group, vm_name=self._identifier.name) class VmScaleSet(VirtualMachineBaseClass): def get_instance_view(self) -> VirtualMachineScaleSetInstanceView: log.info("Retrieving instance view for %s", self._identifier) # TODO: Revisit this implementation. Currently this method returns the instance view of the first VM instance available. # For the instance view of the complete VMSS, use the compute_client.virtual_machine_scale_sets function # https://docs.microsoft.com/en-us/python/api/azure-mgmt-compute/azure.mgmt.compute.v2019_12_01.operations.virtualmachinescalesetsoperations?view=azure-python for vm in execute_with_retry(lambda: self._compute_client.virtual_machine_scale_set_vms.list(self._identifier.resource_group, self._identifier.name)): try: return execute_with_retry(lambda: self._compute_client.virtual_machine_scale_set_vms.get_instance_view( resource_group_name=self._identifier.resource_group, vm_scale_set_name=self._identifier.name, instance_id=vm.instance_id)) except Exception as e: log.warning("Unable to retrieve instance view for scale set instance %s. Trying out other instances.\nError: %s", vm, e) raise Exception(f"Unable to retrieve instance view of any instances for scale set {self._identifier}") @property def vm_func(self): return self._compute_client.virtual_machine_scale_set_vms @property def extension_func(self): return self._compute_client.virtual_machine_scale_set_extensions def get_extensions(self) -> List[VirtualMachineScaleSetExtension]: log.info("Retrieving extensions for %s", self._identifier) return execute_with_retry(lambda: self._compute_client.virtual_machine_scale_set_extensions.list( resource_group_name=self._identifier.resource_group, vm_scale_set_name=self._identifier.name)) def _begin_restart(self) -> LROPoller: return self._compute_client.virtual_machine_scale_sets.begin_restart( resource_group_name=self._identifier.resource_group, vm_scale_set_name=self._identifier.name) WALinuxAgent-2.9.1.1/tests_e2e/tests/lib/vm_extension.py000066400000000000000000000231261446033677600231110ustar00rootroot00000000000000# Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # # # This module includes facilities to execute VM extension operations (enable, remove, etc) on single virtual machines (using # class VmExtension) or virtual machine scale sets (using class VmssExtension). # import uuid from abc import ABC, abstractmethod from assertpy import assert_that, soft_assertions from typing import Any, Callable, Dict, Type from azure.core.polling import LROPoller from azure.mgmt.compute import ComputeManagementClient from azure.mgmt.compute.models import VirtualMachineExtension, VirtualMachineScaleSetExtension, VirtualMachineExtensionInstanceView from azure.identity import DefaultAzureCredential from tests_e2e.tests.lib.identifiers import VmIdentifier, VmExtensionIdentifier from tests_e2e.tests.lib.logging import log from tests_e2e.tests.lib.retry import execute_with_retry _TIMEOUT = 5 * 60 # Timeout for extension operations (in seconds) class _VmExtensionBaseClass(ABC): """ Abstract base class for VmExtension and VmssExtension. Implements the operations that are common to virtual machines and scale sets. Derived classes must provide the specific types and methods for the virtual machine or scale set. """ def __init__(self, vm: VmIdentifier, extension: VmExtensionIdentifier, resource_name: str): super().__init__() self._vm: VmIdentifier = vm self._identifier = extension self._resource_name = resource_name self._compute_client: ComputeManagementClient = ComputeManagementClient(credential=DefaultAzureCredential(), subscription_id=vm.subscription) def enable( self, settings: Dict[str, Any] = None, protected_settings: Dict[str, Any] = None, auto_upgrade_minor_version: bool = True, force_update: bool = False, force_update_tag: str = None ) -> None: """ Performs an enable operation on the extension. NOTE: 'force_update' is not a parameter of the actual ARM API. It is provided for convenience: If set to True, the 'force_update_tag' can be left unspecified and this method will generate a random tag. """ if force_update_tag is not None and not force_update: raise ValueError("If force_update_tag is provided then force_update must be set to true") if force_update and force_update_tag is None: force_update_tag = str(uuid.uuid4()) extension_parameters = self._ExtensionType( publisher=self._identifier.publisher, location=self._vm.location, type_properties_type=self._identifier.type, type_handler_version=self._identifier.version, auto_upgrade_minor_version=auto_upgrade_minor_version, settings=settings, protected_settings=protected_settings, force_update_tag=force_update_tag) # Hide the protected settings from logging if protected_settings is not None: extension_parameters.protected_settings = "*****[REDACTED]*****" log.info("Enabling %s", self._identifier) log.info("%s", extension_parameters) # Now set the actual protected settings before invoking the extension extension_parameters.protected_settings = protected_settings result: VirtualMachineExtension = execute_with_retry( lambda: self._begin_create_or_update( self._vm.resource_group, self._vm.name, self._resource_name, extension_parameters ).result(timeout=_TIMEOUT)) if result.provisioning_state not in ('Succeeded', 'Updating'): raise Exception(f"Enable {self._identifier} failed. Provisioning state: {result.provisioning_state}") log.info("Enable completed (provisioning state: %s).", result.provisioning_state) def get_instance_view(self) -> VirtualMachineExtensionInstanceView: # TODO: Check type for scale sets """ Retrieves the instance view of the extension """ log.info("Retrieving instance view for %s...", self._identifier) return execute_with_retry(lambda: self._get( resource_group_name=self._vm.resource_group, vm_name=self._vm.name, vm_extension_name=self._resource_name, expand="instanceView" ).instance_view) def assert_instance_view( self, expected_status_code: str = "ProvisioningState/succeeded", expected_version: str = None, expected_message: str = None, assert_function: Callable[[VirtualMachineExtensionInstanceView], None] = None ) -> None: """ Asserts that the extension's instance view matches the given expected values. If 'expected_version' and/or 'expected_message' are omitted, they are not validated. If 'assert_function' is provided, it is invoked passing as parameter the instance view. This function can be used to perform additional validations. """ instance_view = self.get_instance_view() with soft_assertions(): if expected_version is not None: # Compare only the major and minor versions (i.e. the first 2 items in the result of split()) installed_version = instance_view.type_handler_version assert_that(expected_version.split(".")[0:2]).described_as("Unexpected extension version").is_equal_to(installed_version.split(".")[0:2]) assert_that(instance_view.statuses).described_as(f"Expected 1 status, got: {instance_view.statuses}").is_length(1) status = instance_view.statuses[0] if expected_message is not None: assert_that(expected_message in status.message).described_as(f"{expected_message} should be in the InstanceView message ({status.message})").is_true() assert_that(status.code).described_as("InstanceView status code").is_equal_to(expected_status_code) if assert_function is not None: assert_function(instance_view) log.info("The instance view matches the expected values") @abstractmethod def delete(self) -> None: """ Performs a delete operation on the extension """ @property @abstractmethod def _ExtensionType(self) -> Type: """ Type of the extension object for the virtual machine or scale set (i.e. VirtualMachineExtension or VirtualMachineScaleSetExtension) """ @property @abstractmethod def _begin_create_or_update(self) -> Callable[[str, str, str, Any], LROPoller[Any]]: # "Any" can be VirtualMachineExtension or VirtualMachineScaleSetExtension """ The begin_create_or_update method for the virtual machine or scale set extension """ @property @abstractmethod def _get(self) -> Any: # VirtualMachineExtension or VirtualMachineScaleSetExtension """ The get method for the virtual machine or scale set extension """ def __str__(self): return f"{self._identifier}" class VmExtension(_VmExtensionBaseClass): """ Extension operations on a single virtual machine. """ @property def _ExtensionType(self) -> Type: return VirtualMachineExtension @property def _begin_create_or_update(self) -> Callable[[str, str, str, VirtualMachineExtension], LROPoller[VirtualMachineExtension]]: return self._compute_client.virtual_machine_extensions.begin_create_or_update @property def _get(self) -> VirtualMachineExtension: return self._compute_client.virtual_machine_extensions.get def delete(self) -> None: log.info("Deleting %s", self._identifier) execute_with_retry(lambda: self._compute_client.virtual_machine_extensions.begin_delete( self._vm.resource_group, self._vm.name, self._resource_name ).wait(timeout=_TIMEOUT)) class VmssExtension(_VmExtensionBaseClass): """ Extension operations on virtual machine scale sets. """ @property def _ExtensionType(self) -> Type: return VirtualMachineScaleSetExtension @property def _begin_create_or_update(self) -> Callable[[str, str, str, VirtualMachineScaleSetExtension], LROPoller[VirtualMachineScaleSetExtension]]: return self._compute_client.virtual_machine_scale_set_extensions.begin_create_or_update @property def _get(self) -> VirtualMachineScaleSetExtension: return self._compute_client.virtual_machine_scale_set_extensions.get def delete(self) -> None: # TODO: Implement this method raise NotImplementedError() def delete_from_instance(self, instance_id: str) -> None: log.info("Deleting %s from scale set instance %s", self._identifier, instance_id) execute_with_retry(lambda: self._compute_client.virtual_machine_scale_set_vm_extensions.begin_delete( resource_group_name=self._vm.resource_group, vm_scale_set_name=self._vm.name, vm_extension_name=self._resource_name, instance_id=instance_id ).wait(timeout=_TIMEOUT)) WALinuxAgent-2.9.1.1/tests_e2e/tests/pass_test.py000077500000000000000000000016461446033677600216400ustar00rootroot00000000000000#!/usr/bin/env python3 # Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation # # 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. # from tests_e2e.tests.lib.agent_test import AgentTest from tests_e2e.tests.lib.logging import log class PassTest(AgentTest): """ A trivial test that passes. """ def run(self): log.info("* PASSED *") if __name__ == "__main__": PassTest.run_from_command_line()