From feb5b705a4a8c316b95e1756d7c92bd8268bc0bf Mon Sep 17 00:00:00 2001 From: Mikhail <98700194+ez4gotit@users.noreply.github.com> Date: Thu, 6 Feb 2025 04:30:22 +0300 Subject: [PATCH 01/11] Update README.md --- README.md | 66 ++++--------------------------------------------------- 1 file changed, 4 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index d475641123..79e14d5ecf 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,4 @@ -# DevOps Engineering Labs - -## Introduction - -Welcome to the DevOps Engineering course labs! These hands-on labs are designed to guide you through various aspects of DevOps practices and principles. As you progress through the labs, you'll gain practical experience in application development, containerization, testing, infrastructure setup, CI/CD processes, and more. - -## Architecture - -This repository has a master branch containing an introduction. Each new lab assignment will be added as a markdown file with a lab number. - -## Rules - -To successfully complete the labs and pass the course, follow these rules: - -1. **Lab Dependency:** Complete the labs in order; each lab builds upon the previous one. -2. **Submission and Grading:** Submit your solutions as pull requests (PRs) to the master branch of this repository. You need at least 6/10 points for each lab to pass. -3. **Fork Repository:** Fork this repository to your workspace to create your own version for solving the labs. -4. **Recommended Workflow:** Build your solutions incrementally. Complete lab N based on lab N-1. -5. **PR Creation:** Create a PR from your fork to the master branch of this repository and from your fork's branch to your fork's master branch. -6. **Wait for Grade:** Once your PR is created, wait for your lab to be reviewed and graded. - -### Example for the first lab - -1. Fork this repository. -2. Checkout to the lab1 branch. -3. Complete the lab1 tasks. -4. Push the code to your repository. -5. Create a PR to the master branch of this repository from your fork's lab1 branch. -6. Create a PR to the master branch of your repository from your lab1 branch. -7. Wait for your grade. - -## Grading and Grades Distribution - -Your final grade will be determined based on labs and a final exam: - -- Labs: 70% of your final grade. -- Final Exam: 30% of your final grade. - -Grade ranges: - -- [90-100] - A -- [75-90) - B -- [60-75) - C -- [0-60) - D - -### Labs Grading - -Each lab is worth 10 points. Completing main tasks correctly earns you 10 points. Completing bonus tasks correctly adds 2.5 points. You can earn a maximum of 12.5 points per lab by completing all main and bonus tasks. - -Finishing all bonus tasks lets you skip the exam and grants you 5 extra points. Incomplete bonus tasks require you to take the exam, which could save you from failing it. - ->The labs account for 70% of your final grade. With 14 labs in total, each lab contributes 5% to your final grade. Completing all main tasks in a lab earns you the maximum 10 points, which corresponds to 5% of your final grade. ->If you successfully complete all bonus tasks, you'll earn an additional 2.5 points, totaling 12.5 points for that lab, or 6.25% of your final grade. Over the course of all 14 labs, the cumulative points from bonus tasks add up to 87.5% of your final grade. ->Additionally, a 5% bonus is granted for successfully finishing all bonus tasks, ensuring that if you successfully complete everything, your final grade will be 92.5%, which corresponds to an A grade. - -## Deadlines and Labs Distribution - -Each week, two new labs will be available. You'll have one week to submit your solutions. Refer to Moodle for presentation slides and deadlines. - -## Submission Policy - -Submitting your lab results on time is crucial for your grading. Late submissions receive a maximum score of 6 points for the corresponding lab. Remember, completing all labs is necessary to successfully pass the course. +## Unit Tests +Run tests: +```bash +pytest tests/ From b4a8c13899391c3bdbfa66470508458acd326e12 Mon Sep 17 00:00:00 2001 From: Mikhail <98700194+ez4gotit@users.noreply.github.com> Date: Thu, 6 Feb 2025 04:41:02 +0300 Subject: [PATCH 02/11] Create app.py --- app/app.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 app/app.py diff --git a/app/app.py b/app/app.py new file mode 100644 index 0000000000..dc070999aa --- /dev/null +++ b/app/app.py @@ -0,0 +1,14 @@ +class Calculator: + def add(self, a, b): + return a + b +https://github.com/ez4gotit/S25-core-course-labs/tree/master + def subtract(self, a, b): + return a - b + + def multiply(self, a, b): + return a * b + + def divide(self, a, b): + if b == 0: + raise ValueError("Cannot divide by zero") + return a / b From f29a9c1d291718b2e297908431727ad31f903c45 Mon Sep 17 00:00:00 2001 From: Mikhail <98700194+ez4gotit@users.noreply.github.com> Date: Thu, 6 Feb 2025 04:41:42 +0300 Subject: [PATCH 03/11] Create tests_app.py --- tests/tests_app.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tests/tests_app.py diff --git a/tests/tests_app.py b/tests/tests_app.py new file mode 100644 index 0000000000..10e352d329 --- /dev/null +++ b/tests/tests_app.py @@ -0,0 +1,23 @@ +import unittest +from app.app import Calculator + +class TestCalculator(unittest.TestCase): + def setUp(self): + self.calc = Calculator() + + def test_add(self): + self.assertEqual(self.calc.add(2, 3), 5) + + def test_subtract(self): + self.assertEqual(self.calc.subtract(5, 3), 2) + + def test_multiply(self): + self.assertEqual(self.calc.multiply(2, 3), 6) + + def test_divide(self): + self.assertEqual(self.calc.divide(6, 2), 3) + with self.assertRaises(ValueError): + self.calc.divide(6, 0) + +if __name__ == '__main__': + unittest.main() From 42e78880ff1ac56b1e5ad0ab596b8f3ae859cf5e Mon Sep 17 00:00:00 2001 From: Mikhail <98700194+ez4gotit@users.noreply.github.com> Date: Mon, 17 Feb 2025 00:05:11 +0300 Subject: [PATCH 04/11] Init --- .gitignore | 32 + ansible/ANSIBLE.md | 185 +++++ ansible/ansible.cfg | 3 + ansible/inventory/default_aws_ec2.yml | 7 + ansible/playbooks/dev/main.yaml | 14 + ansible/roles/docker/README.md | 20 + ansible/roles/docker/defaults/main.yml | 1 + ansible/roles/docker/handlers/main.yml | 4 + ansible/roles/docker/meta/main.yml | 17 + .../roles/docker/tasks/install_compose.yml | 13 + ansible/roles/docker/tasks/install_docker.yml | 39 ++ ansible/roles/docker/tasks/main.yml | 5 + ansible/roles/web_app/README.md | 32 + ansible/roles/web_app/defaults/main.yml | 11 + ansible/roles/web_app/handlers/main.yml | 8 + ansible/roles/web_app/meta/main.yml | 36 + ansible/roles/web_app/tasks/0-wipe.yml | 17 + ansible/roles/web_app/tasks/main.yml | 40 ++ .../web_app/templates/docker-compose.yml.j2 | 14 + ansible/roles/web_app/tests/inventory | 3 + ansible/roles/web_app/tests/test.yml | 6 + ansible/roles/web_app/vars/main.yml | 3 + app/app.py | 14 - app_python/.dockerignore | 11 + app_python/.gitignore | 32 + app_python/DOCKER.md | 27 + app_python/Dockerfile | 13 + app_python/PYTHON.md | 16 + app_python/README.md | 51 ++ app_python/requirements.txt | 22 + app_python/static/script.js | 9 + app_python/static/style.css | 21 + app_python/templates/index.html | 17 + app_python/unit_test.py | 24 + app_python/web.py | 44 ++ terraform/.gitignore | 32 + terraform/TF.md | 648 ++++++++++++++++++ terraform/docker/.gitignore | 32 + terraform/docker/main.tf | 23 + terraform/docker/outputs.tf | 14 + terraform/docker/variables.tf | 9 + terraform/github/.gitignore | 32 + terraform/github/main.tf | 39 ++ terraform/github/variables.tf | 28 + terraform/yandex/.gitignore | 32 + terraform/yandex/main.tf | 52 ++ terraform/yandex/variables.tf | 44 ++ tests/tests_app.py | 23 - 48 files changed, 1782 insertions(+), 37 deletions(-) create mode 100644 .gitignore create mode 100644 ansible/ANSIBLE.md create mode 100644 ansible/ansible.cfg create mode 100644 ansible/inventory/default_aws_ec2.yml create mode 100644 ansible/playbooks/dev/main.yaml create mode 100644 ansible/roles/docker/README.md create mode 100644 ansible/roles/docker/defaults/main.yml create mode 100644 ansible/roles/docker/handlers/main.yml create mode 100644 ansible/roles/docker/meta/main.yml create mode 100644 ansible/roles/docker/tasks/install_compose.yml create mode 100644 ansible/roles/docker/tasks/install_docker.yml create mode 100644 ansible/roles/docker/tasks/main.yml create mode 100644 ansible/roles/web_app/README.md create mode 100644 ansible/roles/web_app/defaults/main.yml create mode 100644 ansible/roles/web_app/handlers/main.yml create mode 100644 ansible/roles/web_app/meta/main.yml create mode 100644 ansible/roles/web_app/tasks/0-wipe.yml create mode 100644 ansible/roles/web_app/tasks/main.yml create mode 100644 ansible/roles/web_app/templates/docker-compose.yml.j2 create mode 100644 ansible/roles/web_app/tests/inventory create mode 100644 ansible/roles/web_app/tests/test.yml create mode 100644 ansible/roles/web_app/vars/main.yml delete mode 100644 app/app.py create mode 100644 app_python/.dockerignore create mode 100644 app_python/.gitignore create mode 100644 app_python/DOCKER.md create mode 100644 app_python/Dockerfile create mode 100644 app_python/PYTHON.md create mode 100644 app_python/README.md create mode 100644 app_python/requirements.txt create mode 100644 app_python/static/script.js create mode 100644 app_python/static/style.css create mode 100644 app_python/templates/index.html create mode 100644 app_python/unit_test.py create mode 100644 app_python/web.py create mode 100644 terraform/.gitignore create mode 100644 terraform/TF.md create mode 100644 terraform/docker/.gitignore create mode 100644 terraform/docker/main.tf create mode 100644 terraform/docker/outputs.tf create mode 100644 terraform/docker/variables.tf create mode 100644 terraform/github/.gitignore create mode 100644 terraform/github/main.tf create mode 100644 terraform/github/variables.tf create mode 100644 terraform/yandex/.gitignore create mode 100644 terraform/yandex/main.tf create mode 100644 terraform/yandex/variables.tf delete mode 100644 tests/tests_app.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..8b63d63bcf --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +env/ +venv/ +*.env +instance/ +*.log +*.pot +*.pyc +.vscode/ +.idea/ +.DS_Store +Thumbs.db +.coverage +nosetests.xml +*.cover +*.hypothesis/ +*.tox/ +*.nox/ +*.coverage +venv/ +.env/ +**/.terraform/ +**/.terraform.lock.hcl +**/terraform.tfstate +**/terraform.tfstate.backup +yandex/key.json +**/terraform.tfstate.*.backup + diff --git a/ansible/ANSIBLE.md b/ansible/ANSIBLE.md new file mode 100644 index 0000000000..857d81c4e5 --- /dev/null +++ b/ansible/ANSIBLE.md @@ -0,0 +1,185 @@ +# Ansible Docs + +## Check connection to server(s) +To check is connection to server for deploy successful, let's run following command +```sh +ansible -i ansible/inventory/default_aws_ec2.yml all -m ping +``` +Output: +``` +vm | SUCCESS => { + "changed": false, + "ping": "pong" +} +``` + +--- + +## Running the Playbook +To execute the playbook, run: +```sh +ansible-playbook -i ansible/inventory/default_aws_ec2.yml ansible/playbooks/dev/main.yaml -K +``` + +``` +PLAY [Deploy Docker] ************************************************************************** + +TASK [Gathering Facts] ************************************************************************ +ok: [vm] + +TASK [docker : Include installation tasks] **************************************************** +included: /home/usr/hw/DevOps/S25-core-course-labs/ansible/roles/docker/tasks/install_docker.yml for vm + +TASK [docker : Install required system packages] ********************************************** +ok: [vm] + +TASK [docker : Add Docker official GPG key] *************************************************** +ok: [vm] + +TASK [docker : Set up the Docker stable repository] ******************************************* +ok: [vm] + +TASK [docker : Install Docker CE, CLI, and containerd] **************************************** +ok: [vm] + +TASK [docker : Start and enable Docker service] *********************************************** +ok: [vm] + +TASK [docker : Add user to Docker group] ****************************************************** +ok: [vm] + +TASK [docker : Include Docker Compose tasks] ************************************************** +included: /home/usr/hw/DevOps/S25-core-course-labs/ansible/roles/docker/tasks/install_compose.yml for vm + +TASK [docker : Download Docker Compose] ******************************************************* +changed: [vm] + +TASK [docker : Verify Docker Compose installation] ******************************************** +ok: [vm] + +TASK [docker : debug] ************************************************************************* +ok: [vm] => { + "msg": "Docker Compose version: Docker Compose version v2.33.0" +} + +PLAY RECAP ************************************************************************************ +vm : ok=12 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 + +``` + +## Output `nsible-inventory --list` +``` +{ + "_meta": { + "hostvars": { + "vm": { + "ansible_host": "89.169.154.202", + "ansible_python_interpreter": "/usr/bin/python3", + "ansible_ssh_private_key_file": "~/.ssh/id_ed25519", + "ansible_user": "ubuntu" + } + } + }, + "all": { + "children": [ + "ungrouped" + ] + }, + "ungrouped": { + "hosts": [ + "vm" + ] + } +} +``` + +## Output of `ansible-inventory --graph` +``` +@all: + |--@ungrouped: + | |--vm +``` + +## Running the Playbook with web-app +```sh +ansible-playbook -i ansible/inventory/default_aws_ec2.yml ansible/playbooks/dev/main.yaml -K +``` + +``` +PLAY [Deploy Docker] ************************************************************************** + +TASK [Gathering Facts] ************************************************************************ +ok: [vm] + +TASK [docker : Include installation tasks] **************************************************** +included: /home/usr/hw/DevOps/S25-core-course-labs/ansible/roles/docker/tasks/install_docker.yml for vm + +TASK [docker : Install required system packages] ********************************************** +ok: [vm] + +TASK [docker : Add Docker official GPG key] *************************************************** +ok: [vm] + +TASK [docker : Set up the Docker stable repository] ******************************************* +ok: [vm] + +TASK [docker : Install Docker CE, CLI, and containerd] **************************************** +ok: [vm] + +TASK [docker : Start and enable Docker service] *********************************************** +ok: [vm] + +TASK [docker : Add user to Docker group] ****************************************************** +ok: [vm] + +TASK [docker : Include Docker Compose tasks] ************************************************** +included: /home/usr/hw/DevOps/S25-core-course-labs/ansible/roles/docker/tasks/install_compose.yml for vm + +TASK [docker : Download Docker Compose] ******************************************************* +ok: [vm] + +TASK [docker : Verify Docker Compose installation] ******************************************** +ok: [vm] + +TASK [docker : debug] ************************************************************************* +ok: [vm] => { + "msg": "Docker Compose version: Docker Compose version v2.33.0" +} + +TASK [web_app : Create Docker network] ******************************************************** +ok: [vm] + +TASK [web_app : Deploy application using Docker Compose] ************************************** +ok: [vm] + +TASK [web_app : Start application with Docker Compose] **************************************** +changed: [vm] + +PLAY RECAP ************************************************************************************ +vm : ok=15 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +## `ansible-playbook ansible/playbooks/dev/main.yaml --tags wipe` +``` +PLAY [Deploy Web Application] ***************************************************************** + +TASK [Gathering Facts] ************************************************************************ +ok: [vm] + +PLAY [Wipe Web Application] ******************************************************************* + +TASK [Gathering Facts] ************************************************************************ +ok: [vm] + +TASK [Remove Docker container] **************************************************************** +skipping: [vm] + +TASK [Remove Docker image] ******************************************************************** +skipping: [vm] + +TASK [Remove Docker volumes] ****************************************************************** +skipping: [vm] + +PLAY RECAP ************************************************************************************ +vm : ok=2 changed=0 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0 +``` diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000000..17ba3c88d3 --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,3 @@ +[defaults] +roles_path = ./roles +inventory = ./inventory/default_aws_ec2.yml \ No newline at end of file diff --git a/ansible/inventory/default_aws_ec2.yml b/ansible/inventory/default_aws_ec2.yml new file mode 100644 index 0000000000..6576d21545 --- /dev/null +++ b/ansible/inventory/default_aws_ec2.yml @@ -0,0 +1,7 @@ +all: + hosts: + vm: + ansible_host: 89.169.24.212 + ansible_user: ubuntu + ansible_ssh_private_key_file: ~/.ssh/id_ed25519 + ansible_python_interpreter: /usr/bin/python3 \ No newline at end of file diff --git a/ansible/playbooks/dev/main.yaml b/ansible/playbooks/dev/main.yaml new file mode 100644 index 0000000000..955a7f2aeb --- /dev/null +++ b/ansible/playbooks/dev/main.yaml @@ -0,0 +1,14 @@ +- name: Deploy Web Application + hosts: all + become: yes + roles: + - docker + - web_app + +- name: Wipe Web Application + hosts: all + become: yes + tasks: + - import_tasks: ../../roles/web_app/tasks/0-wipe.yml + tags: + - wipe \ No newline at end of file diff --git a/ansible/roles/docker/README.md b/ansible/roles/docker/README.md new file mode 100644 index 0000000000..8816b0e9f1 --- /dev/null +++ b/ansible/roles/docker/README.md @@ -0,0 +1,20 @@ +# Docker Role + +This role installs and configures Docker and Docker Compose. + +## Requirements + +- Ansible 2.9+ +- Ubuntu 22.04 + +## Role Variables + +- `docker_version`: The version of Docker to install (default: `latest`). +- `docker_compose_version`: The version of Docker Compose to install (default: `1.29.2`). + +## Example Playbook + +```yaml +- hosts: all + roles: + - role: docker \ No newline at end of file diff --git a/ansible/roles/docker/defaults/main.yml b/ansible/roles/docker/defaults/main.yml new file mode 100644 index 0000000000..73b314ff7c --- /dev/null +++ b/ansible/roles/docker/defaults/main.yml @@ -0,0 +1 @@ +--- \ No newline at end of file diff --git a/ansible/roles/docker/handlers/main.yml b/ansible/roles/docker/handlers/main.yml new file mode 100644 index 0000000000..def71c66cf --- /dev/null +++ b/ansible/roles/docker/handlers/main.yml @@ -0,0 +1,4 @@ +- name: Restart Docker + systemd: + name: docker + state: restarted \ No newline at end of file diff --git a/ansible/roles/docker/meta/main.yml b/ansible/roles/docker/meta/main.yml new file mode 100644 index 0000000000..ceed84023c --- /dev/null +++ b/ansible/roles/docker/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + author: Ivan Sannikov + description: Install Docker + license: MIT + company: Some Test compony + min_ansible_version: "2.9" + platforms: + - name: Ubuntu + versions: + - focal + - jammy + galaxy_tags: + - docker + - container + +dependencies: [] \ No newline at end of file diff --git a/ansible/roles/docker/tasks/install_compose.yml b/ansible/roles/docker/tasks/install_compose.yml new file mode 100644 index 0000000000..d9e1067bd9 --- /dev/null +++ b/ansible/roles/docker/tasks/install_compose.yml @@ -0,0 +1,13 @@ +- name: Download Docker Compose + get_url: + url: "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64" + dest: /usr/local/bin/docker-compose + mode: '0755' + +- name: Verify Docker Compose installation + command: docker-compose --version + register: docker_compose_version + changed_when: false + +- debug: + msg: "Docker Compose version: {{ docker_compose_version.stdout }}" \ No newline at end of file diff --git a/ansible/roles/docker/tasks/install_docker.yml b/ansible/roles/docker/tasks/install_docker.yml new file mode 100644 index 0000000000..e2a0443ef4 --- /dev/null +++ b/ansible/roles/docker/tasks/install_docker.yml @@ -0,0 +1,39 @@ +- name: Install required system packages + apt: + name: + - apt-transport-https + - ca-certificates + - curl + - software-properties-common + state: present + update_cache: yes + +- name: Add Docker official GPG key + apt_key: + url: https://download.docker.com/linux/ubuntu/gpg + state: present + +- name: Set up the Docker stable repository + apt_repository: + repo: "deb https://download.docker.com/linux/ubuntu focal stable" + state: present + +- name: Install Docker CE, CLI, and containerd + apt: + name: + - docker-ce + - docker-ce-cli + - containerd.io + state: latest + +- name: Start and enable Docker service + systemd: + name: docker + enabled: true + state: started + +- name: Add user to Docker group + user: + name: venom + groups: docker + append: yes \ No newline at end of file diff --git a/ansible/roles/docker/tasks/main.yml b/ansible/roles/docker/tasks/main.yml new file mode 100644 index 0000000000..e948f34913 --- /dev/null +++ b/ansible/roles/docker/tasks/main.yml @@ -0,0 +1,5 @@ +- name: Include installation tasks + include_tasks: install_docker.yml + +- name: Include Docker Compose tasks + include_tasks: install_compose.yml \ No newline at end of file diff --git a/ansible/roles/web_app/README.md b/ansible/roles/web_app/README.md new file mode 100644 index 0000000000..770a685c50 --- /dev/null +++ b/ansible/roles/web_app/README.md @@ -0,0 +1,32 @@ +# web_app Ansible Role + +## Description + +This Ansible role deploys a web application container using Docker and Docker Compose. It pulls the Docker image from the specified registry, creates the required network, and deploys the application in a container. + +## Requirements + +- Docker +- Docker Compose +- Ansible >= 2.9 + +## Role Variables + +- `docker_image`: Docker image for the application (e.g., `ez4gotit/flask_app:latest`) +- `container_name`: Name of the Docker container (e.g., `flask_app`) +- `host_port`: Port on the host to map to the app port (default: `8080`) +- `app_port`: Port the app will listen on inside the container (default: `8080`) +- `network_name`: Name of the Docker network (default: `web_app_network`) +- `web_app_full_wipe`: Boolean to enable/disable full wipe (default: `false`) + +## Dependencies + +This role depends on the `docker` role to ensure Docker is installed and configured before deploying the web app. + +## Usage + +To deploy the web app: + +```sh +ansible-playbook ansible/playbooks/dev/main.yaml +``` \ No newline at end of file diff --git a/ansible/roles/web_app/defaults/main.yml b/ansible/roles/web_app/defaults/main.yml new file mode 100644 index 0000000000..6588f43285 --- /dev/null +++ b/ansible/roles/web_app/defaults/main.yml @@ -0,0 +1,11 @@ +#SPDX-License-Identifier: MIT-0 +--- +# defaults file for ansible/roles/web_app + +app_name: my_web_app +docker_image: "ez4gotit/flask_app:latest" +container_name: "flask_app" +app_port: 8080 +host_port: 8080 +network_name: "web_app_network" +web_app_full_wipe: false diff --git a/ansible/roles/web_app/handlers/main.yml b/ansible/roles/web_app/handlers/main.yml new file mode 100644 index 0000000000..0e1eccedcc --- /dev/null +++ b/ansible/roles/web_app/handlers/main.yml @@ -0,0 +1,8 @@ +#SPDX-License-Identifier: MIT-0 +--- +# handlers file for ansible/roles/web_app + +- name: Restart Application + command: + cmd: docker-compose restart + chdir: /home/ubuntu/ \ No newline at end of file diff --git a/ansible/roles/web_app/meta/main.yml b/ansible/roles/web_app/meta/main.yml new file mode 100644 index 0000000000..0caf3910e5 --- /dev/null +++ b/ansible/roles/web_app/meta/main.yml @@ -0,0 +1,36 @@ +#SPDX-License-Identifier: MIT-0 +galaxy_info: + author: Mikhail Dudinov + description: test + company: test + + # If the issue tracker for your role is not on github, uncomment the + # next line and provide a value + # issue_tracker_url: http://example.com/issue/tracker + + # Choose a valid license ID from https://spdx.org - some suggested licenses: + # - BSD-3-Clause (default) + # - MIT + # - GPL-2.0-or-later + # - GPL-3.0-only + # - Apache-2.0 + # - CC-BY-4.0 + license: license (GPL-2.0-or-later, MIT, etc) + + min_ansible_version: 2.0 + + # If this a Container Enabled role, provide the minimum Ansible Container version. + # min_ansible_container_version: + + galaxy_tags: [] + # List tags for your role here, one per line. A tag is a keyword that describes + # and categorizes the role. Users find roles by searching for tags. Be sure to + # remove the '[]' above, if you add tags to this list. + # + # NOTE: A tag is limited to a single word comprised of alphanumeric characters. + # Maximum 20 tags per role. + +dependencies: + - { role: docker } + # List your role dependencies here, one per line. Be sure to remove the '[]' above, + # if you add dependencies to this list. diff --git a/ansible/roles/web_app/tasks/0-wipe.yml b/ansible/roles/web_app/tasks/0-wipe.yml new file mode 100644 index 0000000000..49b59a6737 --- /dev/null +++ b/ansible/roles/web_app/tasks/0-wipe.yml @@ -0,0 +1,17 @@ +- name: Remove Docker container + docker_container: + name: "{{ container_name }}" + state: absent + when: web_app_full_wipe | default(false) + +- name: Remove Docker image + docker_image: + name: "{{ docker_image }}" + state: absent + when: web_app_full_wipe | default(false) + +- name: Remove Docker volumes + docker_volume: + name: "{{ container_name }}" + state: absent + when: web_app_full_wipe | default(false) \ No newline at end of file diff --git a/ansible/roles/web_app/tasks/main.yml b/ansible/roles/web_app/tasks/main.yml new file mode 100644 index 0000000000..4334c008e1 --- /dev/null +++ b/ansible/roles/web_app/tasks/main.yml @@ -0,0 +1,40 @@ +#SPDX-License-Identifier: MIT-0 +--- +# tasks file for ansible/roles/web_app + +# ansible/roles/web_app/tasks/main.yml +- name: Setup Docker Environment + block: + - name: Install Docker + apt: + name: docker.io + state: present + + - name: Start Docker Service + service: + name: docker + state: started + enabled: yes + tags: + - setup + +- name: Create Docker Compose file + template: + src: docker-compose.yml.j2 + dest: "/home/ubuntu/docker-compose.yml" + notify: Restart Application + +- name: Pull Docker image + docker_image: + name: "{{ docker_image }}" + source: pull + tags: + - docker + +- name: Start application with Docker Compose + command: + cmd: docker-compose up -d + chdir: /home/ubuntu/ + tags: + - docker + diff --git a/ansible/roles/web_app/templates/docker-compose.yml.j2 b/ansible/roles/web_app/templates/docker-compose.yml.j2 new file mode 100644 index 0000000000..0cd106a6a6 --- /dev/null +++ b/ansible/roles/web_app/templates/docker-compose.yml.j2 @@ -0,0 +1,14 @@ +version: '3' +services: + {{ app_name }}: + image: "{{ docker_image }}" + container_name: "{{ container_name }}" + ports: + - "{{ host_port }}:{{ app_port }}" + restart: always + networks: + - "{{ network_name }}" + +networks: + {{ network_name }}: + driver: bridge \ No newline at end of file diff --git a/ansible/roles/web_app/tests/inventory b/ansible/roles/web_app/tests/inventory new file mode 100644 index 0000000000..03ca42fd17 --- /dev/null +++ b/ansible/roles/web_app/tests/inventory @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +localhost + diff --git a/ansible/roles/web_app/tests/test.yml b/ansible/roles/web_app/tests/test.yml new file mode 100644 index 0000000000..fde722cf0d --- /dev/null +++ b/ansible/roles/web_app/tests/test.yml @@ -0,0 +1,6 @@ +#SPDX-License-Identifier: MIT-0 +--- +- hosts: localhost + remote_user: root + roles: + - ansible/roles/web_app diff --git a/ansible/roles/web_app/vars/main.yml b/ansible/roles/web_app/vars/main.yml new file mode 100644 index 0000000000..2c15087b5e --- /dev/null +++ b/ansible/roles/web_app/vars/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# vars file for ansible/roles/web_app diff --git a/app/app.py b/app/app.py deleted file mode 100644 index dc070999aa..0000000000 --- a/app/app.py +++ /dev/null @@ -1,14 +0,0 @@ -class Calculator: - def add(self, a, b): - return a + b -https://github.com/ez4gotit/S25-core-course-labs/tree/master - def subtract(self, a, b): - return a - b - - def multiply(self, a, b): - return a * b - - def divide(self, a, b): - if b == 0: - raise ValueError("Cannot divide by zero") - return a / b diff --git a/app_python/.dockerignore b/app_python/.dockerignore new file mode 100644 index 0000000000..5b59df5e9d --- /dev/null +++ b/app_python/.dockerignore @@ -0,0 +1,11 @@ +__pycache__ +*.pyc +*.pyo +*.pyd +venv/ +.git +.gitignore +Dockerfile +README.md +tests/ +*.log \ No newline at end of file diff --git a/app_python/.gitignore b/app_python/.gitignore new file mode 100644 index 0000000000..8b63d63bcf --- /dev/null +++ b/app_python/.gitignore @@ -0,0 +1,32 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +env/ +venv/ +*.env +instance/ +*.log +*.pot +*.pyc +.vscode/ +.idea/ +.DS_Store +Thumbs.db +.coverage +nosetests.xml +*.cover +*.hypothesis/ +*.tox/ +*.nox/ +*.coverage +venv/ +.env/ +**/.terraform/ +**/.terraform.lock.hcl +**/terraform.tfstate +**/terraform.tfstate.backup +yandex/key.json +**/terraform.tfstate.*.backup + diff --git a/app_python/DOCKER.md b/app_python/DOCKER.md new file mode 100644 index 0000000000..d3f0dfac45 --- /dev/null +++ b/app_python/DOCKER.md @@ -0,0 +1,27 @@ +# Docker Best Practices for Flask App + +## 1. Rootless Container +We ensure that the container runs as **non-root user**: `flask_app_user`\ +This is critical for security purposes, as running as the root user can expose the container to unnecessary risks + +## 2. Layer sanity +To minimize image size and optimize build time, we combine related commands, such as **installing dependencies** and **copying files**, into fewer layers\ +This allows Docker to cache layers **efficiently, reducing the time needed for future builds** + +## 3. Selective Copy +We selectively copy only the files that are necessary for the application to run: +* `requirements.txt` +* `web.py` +* `templates/` +* `static/` + +This reduces the final image size and prevents unnecessary files from being included in the Docker image + +## 4. Precise Version of Base Image +We use a specific version of the Python base image (`python:3.9-alpine3.15`) to ensure that the image is consistent and not affected by any future breaking changes in newer versions of the base image + +## 5. `.dockerignore`-file +We use a `.dockerignore` file to exclude unnecessary files from the Docker image, such as local python bytecode files, git-related files etc. + +## 6. Env Variables +We set the `FLASK_APP` and `FLASK_ENV` env variables to ensure the application runs in production mode, **improving performance and security** diff --git a/app_python/Dockerfile b/app_python/Dockerfile new file mode 100644 index 0000000000..a990558dca --- /dev/null +++ b/app_python/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.9-alpine3.15 +WORKDIR /app_python +RUN adduser -D flask_app_user +USER flask_app_user +COPY requirements.txt /app_python/ +RUN pip install --no-cache-dir -r requirements.txt +COPY web.py /app_python/ +COPY templates/ /app_python/templates/ +COPY static/ /app_python/static/ +EXPOSE 5000 +ENV FLASK_APP=web.py +ENV FLASK_ENV=production +CMD ["python", "web.py"] \ No newline at end of file diff --git a/app_python/PYTHON.md b/app_python/PYTHON.md new file mode 100644 index 0000000000..27761428b0 --- /dev/null +++ b/app_python/PYTHON.md @@ -0,0 +1,16 @@ +# Python Web Application. Current time in MSK + +## ✅ Code Structure & Best Practices +- **Separation of Concerns**: Flask follows MVC-like architecture: **HTML** in `templates/`, **styles** in `static/` +- **Modular Design**: The app is structured with separation between logic, templates, and static files +- **Environment Variables**: The `web.py` uses host `"0.0.0.0"` and port `5000` +- **PEP 8 Compliance**: Code follows PEP8 + +## ✅ Timezone Setup +- `pytz` to accurately fetch **Moscow Time** +- Ensures that the displayed time updates dinamicaly + +## ✅ Code Quality and Testing +- **Linting**: Used `flake8` and `pylint` for code cleanliness and finding potential errors +- **Logging**: Can be enhanced using Python’s `logging` module for debugging +- **Unit Testing**: Unit tests can be added with `pytest`. Unit test is implemented in `unit_test.py` file diff --git a/app_python/README.md b/app_python/README.md new file mode 100644 index 0000000000..5087ddf4d3 --- /dev/null +++ b/app_python/README.md @@ -0,0 +1,51 @@ +# ⏰ Flask App: Moscow Time + +### 📌 Overview +✔ This is a simple **Flask web application** that displays the **current time in Moscow** \ +✔ The time updates dynamically + +--- + +## 🚀 Features +✔ Displays **current Moscow time** using `pytz` \ +✔ Lightweight **Flask-based** server \ +✔ Unit tests \ +✔ Styled with **HTML and CSS** \ +✔ Good code style + +--- + +## 🐳 Run via Docker image from Docker Hub +```sh +docker pull petrel312/flask_app:latest +docker run -p 5000:5000 petrel312/flask_app:latest +``` + +--- + +## 🛠 Local Installation and Run +```sh +git clone https://github.com/Petrel321/S25-core-course-labs.git +cd S25-core-course-labs +python3 -m venv venv +source venv/bin/activate # For Windows: venv\Scripts\activate +pip install -r requirements.txt +cd app_python +python web.py +``` + +--- + +## 🛠 Local Installation and Build the Docker Image + +```sh +git clone https://github.com/Petrel321/S25-core-course-labs.git +cd S25-core-course-labs/app_python +docker build -t any_docker_image_name . +``` + +## Unit Test +There is a unit test that can check the availability and success of the application launch. + +### test_index_page +Check status code. If it's 200 - excelent diff --git a/app_python/requirements.txt b/app_python/requirements.txt new file mode 100644 index 0000000000..d98a9624fb --- /dev/null +++ b/app_python/requirements.txt @@ -0,0 +1,22 @@ +astroid==3.3.8 +blinker==1.9.0 +click==8.1.8 +dill==0.3.9 +flake8==7.1.1 +Flask==3.1.0 +iniconfig==2.0.0 +isort==6.0.0 +itsdangerous==2.2.0 +Jinja2==3.1.5 +MarkupSafe==3.0.2 +mccabe==0.7.0 +packaging==24.2 +platformdirs==4.3.6 +pluggy==1.5.0 +pycodestyle==2.12.1 +pyflakes==3.2.0 +pylint==3.3.4 +pytest==8.3.4 +pytz==2024.2 +tomlkit==0.13.2 +Werkzeug==3.1.3 diff --git a/app_python/static/script.js b/app_python/static/script.js new file mode 100644 index 0000000000..ee73c32041 --- /dev/null +++ b/app_python/static/script.js @@ -0,0 +1,9 @@ +function updateTime() { + fetch('/ct') + .then(response => response.json()) + .then(data => { + document.getElementById('ct').textContent = data.ct; + }); +} + +setInterval(updateTime, 1000); diff --git a/app_python/static/style.css b/app_python/static/style.css new file mode 100644 index 0000000000..14f7b173cc --- /dev/null +++ b/app_python/static/style.css @@ -0,0 +1,21 @@ +body { + font-family: Arial, sans-serif; + text-align: center; + background-color: #f4f4f4; + margin: 0; + padding: 0; +} + +.container { + margin-top: 50px; +} + +h1 { + color: #333; +} + +p { + font-size: 24px; + font-weight: bold; + color: #007BFF; +} diff --git a/app_python/templates/index.html b/app_python/templates/index.html new file mode 100644 index 0000000000..35d3c2d3d4 --- /dev/null +++ b/app_python/templates/index.html @@ -0,0 +1,17 @@ + + +
+ + +{{ time }}
+3O%=i3bLEtxr^l(+2$A1=_N>T=m zTU!s<7r+aezR&L{SSw_inL{Q2%ab!Zsr}=D1bhCUdUEgoqbC>tzj$&*f&cG3Id|JW z5KnM_!|P_h%dcwHdlxVM1IQz-sKFo )N>Wm+-iLy`YxE{P){e&vf9G>fn 5%jEjr5((1USICI|Pi~p%Fvvf+@f }2=L3ee-Zvzv+3w% zaCRR2ebw>)!QP)<0J~3ywfTD-Lq2ICFGd@8LV@c4fJ){F$@3BVXV_&pY!iY~r|g+u zkTL3utxi&(3lHCeHpXaJKz~nfDvf{oVbXv6Faq)TCtq$^4G4`s0sAv>zS&zAeLH~o zKv^A`>f)7P@=@!D4gWXshjvu@H|PY)?pitaT^Hh^i~2E^Et4z6AjjYODB>~b!`+#W zTILd|4ShW>S2zcFBLZa_+uJ((5MUPdGSfj>0&yHpNiA;KNJ!f &2+*O#>@^R`kCDvt!r$FN~rFI+k6azKhu%-0fUzJ*UNA}w*L;veJ z37~&HCjmylZdJ|k3|a8yXn1T#$`5>{+5eMrRZDOr*AQm}K?x*BZH|n3ryTzA%hwir z;G4uonr^GNW_-rq#by-_yRayOy9!nAEG>Vs@0JcPK{_q&u??CfJL eS0J#oDc)xR^nPam4zMii-335 zCb`wI*sH!q@@56YD`%);m2PdHEY@9HU(s5)d8L%esR`y?Lm7GTA(c04F1T5z%BwE= zxqcJtn o=V|!q)s*2T+6fZjB;%p`!zqt`?hT0I}1)!X@GvH2dA{d^Y}E(Cw)C zOud0Bh2-lNgoYbmBJx?O{5 NVmupu6dv#Qk5k2M-|a&wsI-Kq$G=`J-843*iz&NU)Um+Bxy3;2Jb<$s zTVc|;1A^pI!lKg;JDE%f%$o&~<8IP!^2>t|f60Cr!slY4qvWPMo21kC!+y8X3H@ iW%okp2Y;b2N{y|DIpSA^s-W&Zs|UkfF7tn0DRf z&8vZJuY0|vK5f{qm`@SyIf> FoScZBt^!ln|tC>s;HOAolMfBToB3e?X)ybIv%;>7O&h<%M5|8C7Kw7UGrqzAF zU3ZU5DVf}fVyw^P{R|QqAS0n<`t1KJ?LDKK{I<1k6a}QJAc8cpffPXyLXV1ybSZ*# z5fJIULn0s|QX q~g2gElSTPRg0NN^ZJ>e+!SIetI;}b4KP3Le2NWa{JihlSwnW< z@&@!FUUEudzXRK@Pp5VyB7Bq)NktX `|P0x2hd9hQMu (DnL{G0+Eb$Y z!eVq`0fD(s3FK{-zP~C2S9sC*%2`lUO^(QX+`(pNn2c!O9WNb{kkgE~b~TKX>XXf- zPZf@46G9~H26uqkk2tHtc`0miO7jlCm(x9}*K%-hO|M%h a(m}B+gfDa&0OM>l`e*TLGojR-_Zpvl~SsnnYn8_J!xbFPLan z;LDzy@f%~` an2+`uuvg(=v|W{GX8)cU+bIFLLGaohEUj#IouW7a z;gIZ=?XbsJn3DR%K>BI|z-^3e-~=)2fXIwTUw=EbcddOpdWZ{i#f2Mx-g*UgQ<9OJ z()4}#6zFa{!T3-DjI-Pcy?iX9lsNjiQeN(l zoeddT=_%j@X|I+|kvfS3-w(|}{%&O$w^ETKJIJ|RR^>u&7r{B+HACBo&QwU&Mdlwo z@V%N1c`t4QX!6hq$K46T<$=9BSnV)v1yzAc@blG)YjA+ds}Zr3a7f20w=Z6RU*Oim z7Q{w&gA8nZ9DdEsVPDSY1?u`sNg#puVlCq3vk4)cLfgf$c7w5o{Zqyix)KcPf|(pt zY_hQus2F8jG^#w2G%cm*KXZDb?G(rL$h>%K2hTWs3r4G32Nu=X(C)pb;#NC7ZD(gN zs%=2KBrswdw(0u*V{34BEt8muLJXcF?ntm8?h=QpL;|l *tz3|FjWlHB|BPd3g!pHJ2>ak}JFV5dTuS{iZZ(stgZ* z;N$St(ygtK{O%q*%z!Bl@U-G<_pcI#ia#o~O@XLH9#mF e;noF zWFM`fpE@S;2)!*W0=h6sj<5rpnBgvTukr#s#1wsWMEzu!u7LBlh`i9uJcD?D=99j! zog8Tup~p##7uk$T4>>=tS7ojk5k@~5^Kao)KhvLp(3R0L+FHiM6x^wPx4Xb*J7#q9 zUfhLpqqP|UTO*EiYSo5-l9g>w*-Bx+bE}})F9F88d$U`f#<4zT)6Bwo*Q@OM4?7_Y zT}GjtAL{WBEY6+ey2YY|Og~fK_&t6^GI1Huuj1Etc8no{oDAyi0WkN?%n@oOrCxm) zc41Oj+Bbi?Da` bY25xv_JeYriVt@lYH`{({FAn7q6tifC8gY8GTa|_Q1 13}2vM(7)GxvH$3r znIn!vqYhjl29{UG*VcqV#UGgQSz3$1R3JaDj_WCTS7=jd7i9Zkmuyl$f0s|^*^O9A z60;p<;3C}pl)F0T%>Rsb^HIn_l1qFz4xVi~{uI0CuDb(V7%p|`Xib^-PEDUY83X*r zue;F;STiK)rd8sVAk3;*u9-J746Zu01Te`sp2VVI(iDQ?lW@yW^8?I3euL=-sn7w% zt32B?4r^L^j*CHg0Dxne=;76TzguVH4iz${YzdS2&{%7jM*g{g(Zh;-5XN+Bi;nr0 zrB5;Sy9WXGatPo(V9iLz{<*`}g;41jBK2IXmlNj&p5o}faXEBxo)BIRhh}v($$Xp9 zSKo?Rzu9CrE1S#DXnP^w>GB;O;Bu7sB&0SV)y?05odl^RHF)>F9ZzOD1(-PehQfB+ zbYSX(a7W~g;{iwL7RTRw8mJxUKYQ2Y(v9KDy9!R)O$LPcN$V!kt`ZM31NL&p3m5yh zn}L9J@++@>O+ccN@Q|5x#OzqOKAaQ#{jK7V!#&-sM=tP`o{c4mD)-Zx^_5Pl`U0Q4 z<%B`zN)CvOt*7?{Z9iSSIjSq;kAp}1vgFvjWRf}&CuXJCQn-1?X&!@t*Ri7N?WeBa z(Up<&qe^cLnOZlIsdDt^PCM$hiwr(VF%rC#g1N;H9YO?a!o|FH9t>HR;rToZc2VO@ z+%()4xZF6&XQ_4Y4%gF8U>*g*rErsFf|u)vYnb*L5^C~oquNFE6{3DYj@#IeI40L* z_F5&Gr+qmwN%bC_KQpa2V91l53&8E6>F@0KM8unudxAtUuGNTxa=T3vx}(wP 8)i-0E_RRgkhIe0kHf4sAUAk>7N8Tv@}#Z3 zE#+!#GIH`@NiC@`S^y$nDg2giTr%so0OGD53YIR|T7` z?qpj@qE#0;cMxm9jSIx-glDK77vIMfqIj5q3YB-j^7rtyiDA&fm?myO+weMiaKq-k z6}4GNt9zaXr$$`g&MEmVTAy$@*8W@hu>ZMl7BiU1r@F;V;8&!$YuC?SU)Fb#c`B2s zd4~VtsJK;*(?`SNxkv@x(V3_`-?b?D>x%*(x}MQ&9tb&UTQv-rvC$qsF}XE-s1Y>J z-(n$m($f-m%SfWlmm5D|UN>bTWny~2^;{hz{qvMeX6onqjHv~=`njvLO`mv1)_yE8 zh{ Huddh%de{@Z`+CBxpr9 zg(EZs1zXzA8ry#8A?;Gv;O;Z?X#)q0bk)UvHzcK9EI%+pSmx8uK4Ht~4%0zhlR5i* zTaiL<0p}xb8=2 biU^~06^}O#&2%KKTD=bTe_0^%nN@3eK)Pg zNsxt$jJTYT8TkWax%;f`8Vtr<6@G*_$lLgKKW%IOkC`LKz>gs)N+}Y?*Al1pYbhiz zsJA!s_}%T__`YVJEz6n-OSkqQT)E}H*tuxyz7M>-d9-lgIVgLn#d@)G(}0vc0V2mZ zBu17}4@a`_ub;&p@gT4?kaijmuZ6uT 7cj$v_=8jEmHUndUR1|BQ?AzrL#a3P*1BWL$9W!jivs)coNDRhQ;-#y ziqUHAfh#>pu~zek&z}GC=%wq2+FqGTa;*qwbOFfqJTT*6lZpE-z%$k=3BEBTb!7;@ zr6nrhGbj$WE}V${z7X&-@v`p`djRcf8>gVo8SUiCW7Y7h5gRUFqG3JtQ_q#MwY}jB z=QQQ~^E}Aw$M+@eb%h^0 )^YdiT?zeX&5ettr>$o3%GTxs?%9qgn}Jo8P^CGMrA zF;CB`y|1h>mZ3PIQP2tOh^Tz}l@0Mwr^f&gR{g<>*-&;BD!s7bFICJ?2>KrsH&F!0 zp8F&vfu5h%(=&$!pxk%M=oYEmI*Zg7J#nmlopq-TmxO8L*}pQegm^3+igniNCm$LG z-xp}|O`O=dQ8#2O@A;BhT?<*7Bbp)Ith(vGrk2he!b}^ownt&&=9D6&=M=%Hk=t9v zzUm7w1JRDcuUh0+Hj6eViP;e%7lo0?mhv~fr%R{$=9X8*pVN^CF
eeT3%C{yWiO z339JMeIi)5kNc+^5f0+gola5a$4EHat6%MAj_~rj?D3!LY7ZmZh&&)=>#G&%6N7hG zAJ8BeTvSVgKbM &nQ&o^1@g#peetmdF4cdl`{&%E)Hj>Jkh2TMCaWv>AHB@)+b&mY6y;b{ z_^gH r@numVUbRHSQl~kbzezSqId3sB~QnPp9?pb0k+49XpwH-KsaZ zxa?*IA`6KN*DOgl$-R}T>Hnf#j{JWsj`RObO8jfmtE1>& 6>5*wl=NkJJ^1ymCgElq)>;ymKLVa*bP| znnR-6Ib<5zzWI?Y G`=gDHcK$$I5teU# zeEjY~HAgkqZo*R%!>1ZfHY0ZJkX K>s%;tEvpp{(?Z Ed(qXG zsykYp7G_g4XlPX%{f?p(-t$T%vkUGQ7)KG4StEW=%Em+s32B&06II`3v^Ax>0ORB) z&aVWs%zZU{X%s6g1m3{Ip3{k;l@fOqYyTP7TcaRA!jBz*+Yti;QPB-B%XW=9$9IMc z+~O&;EgzUD!2V(88GXAL`6ei!{r-gNW`fjAJ-n-aEeu2U{nzZx0O &OKJ9+L62qia1E|$# pp3XsMudiFL!hwKp-cDGj~BDmD)&J z=RXzZ5u851uEl(BEAQLqr?zZtt95U`*}O8`vn^{o&Rxp2fSH 3h1- @TuF uq>XmdSIOnZQK2ai!eB&oDQ69>fgM553je@v)&^UH^KCSDI_^x)rb|T zAS&M3*xuR9Ma+LM+$0%P8h_C}#%$MMA(k8MD+4~zhS5ZK${cRvLN^m{;?;zI;E3bv z6nS* H84$VU^kHEB zvSa>%ndi^*^XHLLbHcH?kXY#Ocbw7q8E4!_ *0p^%)7O|B-V&`|E-K*AO(`n}V6b%YksoH3-iMVd?48$rX#$5u+PH`tAlrfvb3O zD8lVHrI+|l2+IVO$E^OKi=S;G)2?bEc?QZA1ZcZoa8>~&KXqTEztRAjk@X0l%qV@U zo|o?$THl!dAJ`>Qe|^F28dCEe9JU1NE0*sHH^bF>JpS0E(4JN+NAY f$J zFx1d;hUncN60ptVRzA(GX;efSoT&3@7*+x;p0k^rU~QCgO?lWn%56n77~@;h8--@d zKeoJlKd1Zh0W#oYmb|(?nV%gp@8t4(2CLle-Zc(#uLZsG3t}z~i-lo%4aSeW{yR;J zpCr}F#t@}pXfNKS6dHBt^#=L&MhPH%xxOPJqC{JvmG+ho*0Ijb44iRW5MA&0Dmn1= z#p<6dtVA@y68R1T(*7OH(;P|}$^z0OggbulP_81qUU9!adDicMb$eZ&JMErz7pDGL zV~R8;4UNNwkaY`nU}XL%Q4vBCF~`+Rhk (gIsU*5g;$jF?70?{w{qUj2q z&Q(i~kfIL$Qohz$YU}X|G09<5kk6%-Xp Df7BC3`N~D{K4a2t#wv&+npAiUQ5I z+B0K8-Ws#_K|}jGRq%)}{7;On>^;-cn7{=l<*VzS$STEId;*^y@I^slA^2Z;0%qT} z40!2tj1W})dcX5LWO}Iud1CM=CPQzTegHYtDa4|nto;Y1%`5FIRhr#N%>(!O;Z2$` zj#jYO$$FVhnOd2T)PcyqURyb4hC6MgMPEXX8EKiX5mZVo=WT3_EWv G;j$2iW53M}fdi_W#mHS?^&;?m|rYkL)^s%xd%V6o?y04Kg{PS@Dq zosokY9NSD$zn)a|R|rBNn{!wm^N}>frdD~{mYVrjI(b%M;q(8A*FZS*tuePL0~B=I zG#q5%zOM0Yy#n6 _&2OJ>v~p>FP(H__)Q= zC4qs%b)a&zxdWcU+d?F9=VD?lz#8=?BknLM3i^osC32&KSow_TlfNcmTgjD!D$$v` z2T=H$xKGZP@@<#|gDt)uMl8+!FrwO5-umi48V{epDs%4oVtq)X-nDwUZnbrb23kgS z<_UR=bgS#pm!IYVrU35)$b vT_3UvTYqQ!5Pc-QE1uzi;GQ{ip`)EbDqI$#ngy zm(!aIz4!62&Vh|&tw@qbXOxP8E`~4B^?aYSpK)8dWYYFIey5$I5uL1UPF22KhUMAw ztI_D2RlYmYdFyNG!NXP8SY>tjYe~gOFg}^jtWv^lQQ2SikuAVIB?6q1fzjqPS}^)1 zne0&Nv4??}uZw#n4qRQ~KnRJ>0HyyU*yLc4lTk31+`tuQc3@-?XfMO;HPS{bDF-Q7 z{(FxDbSBHhj~B0h#&djx7DfY0jG9zXq(uy30sT2&w%r*_QJmNRR^#^3+vr>g4Q6m3 zft}>5o}Fl5>AL9GL?ZHrHKaJpJz6;jQK!PyiHhalu~5L;pQwjJQ;Ix2Ke33{$K>^E zYOcD)J!9jr+|depq4chZ$)#shx!L-INDX@$sAX{QeDN|5GfEDB??y_uk{J}@t1z_c zUXEw&0tVnV-9zqgHei$d5D?Lr(sL2O&(%I?11-*R*-!&tLh_iZNMh8{LVdrI?S+#8 zre!g~{zo%_2%*h62+V70jJ&IrGtzoCuTnvwFg04=1>nHON!oBV1 !)li`0j}X^#^&`hD!Ro_t%$?DoqMw>yoJZ?#dvnP z90;<#9a;M1@I3Ci#*rh9`w%l)M=ip6rQV4xRdC?8sM_%Ndd{x6OYiH^iV4t!S2eXf zTJlnnVpcOM_*Y!u-n1{^6hra?jz4U3vZjG(-x!li-bXa5!bVSapQh%9OlkW>MR`Un z%HI(8Jn5x4eEukLLYL;PKgCZ-cZizu2iAgqoJos;klY|ntcfU|I *tM1`9~c_ z*D8 ?VS930zVI|9yT}LDBKL~#dAh{A-^( pbE@9T>Ez_7s<2u5vkUvw!4?96u~#R=}n!O zSTmkb=};^ZGkWRj)1Q=yJPdO=8@PDoXo_Ai#VYS y`cb^}bT`L)%3+RvU?tSPjGd?QzZjVRG)Qn;|CyjfSHZH_BEtpE zdMw>hRrrO?qDRt!I~0Uy{sGUDT3wdH`>b~8%%}Lug*ybg 0K2t!CloIsBQswY+PKKpX&O}bxV;U85{(5a 3{Df6T)8dBVMzN94 zkxa8AkH$A!LfLpbnLmtAEw+~hSS0((EgyrUN=(#C^i90x6c2Rx*yJ;Lc!DjK zt}TF|%dgmsJlQo|c`}9PIKSb$T5ntT+S!3a309HCEpYI9V3@5z@CVpw#|xE(4DjIV zvkK#Tl=W$o@f=cp^)&QdM%0emGMT#&V-hUY(vz|Ad2?gursZMG9kW%z&6G2eeZGCx zWdS3Ac4g2((0I50zNMZ|dFl%t*^=jPgNqkjj$`G(;EI;gL`Q^hJGDp5zPNYr#O(z* zpHPH|D#0(SatrJrug*KDS`iS^b{=z5VO=_WTi+($Y+`a~>flo4C4SF2J=t4bmb8Af z`70@C^`%w}86_6&4F5!lLen4}LAzlVCI#zvw=)4e#MA_n#U+F{my9aiE+ofD6gi ziNj(kL8UgQ`rhv5ClmqLb2ke$_+ZbZWhEp}q?k8HX>d!;wynoNz&aPtAq?Bhyex2F z=n*Ez@I^h9!*gB*;}9?Qv>BpmyV($#>!s(BihYRjd*OAJyP65Jze!|Y<}VUC%v;^) zaXJ|Dw14;X{_#^AaH}$e^t75w9 MHfghYzVk73URU z!}(S! 128 zBD8%#5uF2xTp7*CD6a}gG2xPe!n4Su!Ieryo5-`t*6jvJBP<(O{%CHIa5~SEroq#x zSwlDK*Rw(5OP#x8-4ET{LMKO*{iI>uj5q51y=`fIYw2s$M~k=nT_M~4IQ`Vw&uF8u zocZj%Ezu_5_uHeFV07;nKIxSnzipF8=79#wn_%oD&rMjTOCP$>3NLTF>uoh_K4AZ@ z5c@sj#?)+uJXo56Y*%w8`H}<37M7{h38W=@6Hx$CMNL_nZz|~>*}ur5+& q~t*O6bKa1+cMB>XOKn19P>$cJo% zh%cJ6@oN5!+~7g=@;N-7{JzmI)U#TvNu1eAfysLF&XGNmg4o_c_}2i>VNZCIdoisc z`dwII-opKCe{;T8RL(4c$8y@iEK6 LPl6aKx&@Ht1O#!{jTfqVac)C&+@FEYFPv|+8i3xQjl2wEsO)=y2iiu4kGSn3*H zd{S~SY^fc=391{uw1n4!F *~BozEV%oUZT~97BFf zG?f1UR4?6}%Xd!H7zu059b`}c1T*2yD8BCzsXP1V`Q=Am^~~AbO5Qc0Gua92DPKng zy)!l6c~ocSnbuioo~fn%Di$dyGe5xgc}v58*0Z6>@rx$aYTgnD{8{qP6u<*_DfVt` zqFLLT?plPx^-nAY@H?LVpP$8kK!jG$gcr;9AiZgnKnM4sdj*)x2eM2RB1Uh0;{my1 zv#TQXrvq|sU9|P%YjKX9n^k&JapjUg;B32;OCBymvs7Bfg~D^G`OUB`_I0m0wY`+R zU-tfu4MfluMsOieCJCX(FC*vOINJ^!gH=)eB$Zv-K1$LR1lB4Ma&|{ $7 z d4xc)TMz+3?xic$+J03aPbhWP|wF3M)6P6Kz zw#owcwjWuEZ;%XcJ?xu(D76{ve>|l0T%*o^gvCjWia1UgWO`A )-(cyyIyJ2Y|U)IPxkeBrVqdPxao{&GLxnZz&J>s4{cEP_i< z1=BDsQzvHxca|%NH{tNUe{(m{pWN-XLh7AcLbKLy&wTO97|4EY+EXePcR5R9GMky# zAMiuCjqz=tCogOh&XdvhDh(k=a7R&&FT*V_DOn~X dTMxKNvTg+6(-F~_H 40Q}n6V`)qmp a?M zPybbFNACr$eM}dNFf4l%?FDVU<&k!e^nTBrF1dYsx9jT5SeqBE%=?vrO>w-pLHHvm z=k>z{;ecv|sFc72u;}ZS!*w=21* tbR}|;gZ;Pe2aLR+f`!6cQ*~!METhF* zcz{lA4P#GxXhcT0RU^q^24ZljKk0ZL8)D*6OZ*BBkxJKil&~<)?3Gx%iztjGB>aUp zz4M`G7miCk#L U zaZUP942jVV2>t=FGL%Q_3v_-ZxqUiT3d7vdh8PCuJxYz;FUA;$ytv&_7$dmO=D_Xy zg5WK{ZmC0V5ZvP>58eFnxneNRo5sYLRUgU4!EKQ;z0ecVB)VUInINd(CL<6%Z}v`r z)?u^6q(fA;(s`oy{QY+v|BZ|0bOA{oeHnXyT8GHvKC?dlb*s?-PX~q=y81CHplc)R zOi&-ZoEU??y}vHZd~eDM@Zb*=RWKqy(?RY%@C`8}e0V_aTrdM>jd>^^guDIZpDEsL zQ(hwPD$P9Y{wVkh-o$Myz+7Gbpm@Yr$Yb| 1MkK z9`hfP$8)}2)4sr|+aV0UU8zUW{Ku?4n#hMnvE>@>fg82b70%3SO_or4fx$2n3fCUu zk<{Aq2zK;9bFY%))Ax*+GwT6NG+35#)1?$#=jegaD $a3qEJS17R0Pl6uImVV+GtN;Iu~@Z1Ol zhsEd3>xZhTTV%pDv;P3k!hCfayTw7ZqT)_Z&+K42-K!A*Lrqm 7O%|9KQ)zc#N#KwG!?GN%ssia@|dK6A9F`XiOTmh z@!9ea`SCT{ka3?q@uMal^BWdd*}(UU+rn4=16J*u8KdGqb=+XdM~&SgD>w$@Ej*Cz zn+TzqE3)H|q=$bNQBjZ`5DnNrIpH67|6XhV-dBLcd!q}R?>WeI2mXWlFVpK^wYdLf z=x?Gb=KBv(g?=c? t&Md)2mR)zPjZsz8x3r zzWv7)j_Vb+Nxf}KmhMdQpm1>~;ci0#)z8LRhQiS%8d|R2J2giYZUIRZqf$d(9Mqz7 zH$@OaCVzglW0_q*K9J&HHQsn*A~>JxqLo;Pe09ax9!uBRot(Q|qWvmUeuF Sa^84z*t0 zf*0T845y!gE;`Sh3u>+T W-!oO_#~ivw zp*hvuu{*;o`>yQH!CK}4FH@Hmh*frmNOs&1EeyFo%$kV5{qVD)lbjt1)?$t7Rk%1V zKv@0jd&xTrG3a@Q;X;NNyr6HnH9ttN!nqRrY?F+8EB+VU>y%*n&7R-=SbjBxvklLh z4AV@Yt8ir(h;*+m8}k1#kAy-tb5Q942NLm!@7)LXGZ;CU?S!#QF(8VOR$}puNwPoD z%&-LNU%vl0&+bjf=<76evSBQkXnaki(opWO7yILT1w}@F`8eud#y7?`w9Oa22a4DG zH &i9uil?ICacWeSZYc(`{F1U-~`^% zR9M}06t>#Tzh_q6(#M)_0eL2rG)x}W{VV(8ZzAc>9&oH~#7rkmRXl Cf(%x#v$1Q3d4Rbm@MKDY`=@~Uo$(#YCL zZ|ey?UxgJ >;(zZ>WoRp%G$;E)h`-oe8t#!$0nFfO-%H~7^;2b+M3$q2%c8C zbX}}>n~SF_!>Z6;V&={N3$UJqHusS&IcFS&5#~Yx<2bIK!GHRwU;gUQB)x}9HD`U1 zai=}Lu6gV*qlQQF)fwU~H>$UNDqf;T>Hom|v!NfuNDLSMhr3F*X7TL^;4%K$w-F9H zmFxfN^0Xn3M14D?6-H0GudDUSA{2H1?L=-!w<9Y>a>KoUya|12az8I79P6+5+BW=# zXbQju8D9An$cw~&nXQUSGjANd mQ9m;+L8tb&WdR*^;0h&SAf!1caRZ8h zlkl1p#Diim8c5m$; 45m=ktQP?jQYBozN)n#Ajq`R((X?C?6wW}xk-(y_L7 z-{ZB`%?}3H$dxrh#+Ujo6ws^6kz3>-n#boyvgbr(ZO~V+TIDq8)+bV*1I{BPi4;J= zMB#HD5fz<>QYKGP3OJhy^P+1bUT~_fx@pPEuKE&Q!~4sowcvJ0I AM$9g5Q3-QC@#pdc`Sw6w&4bax3z!@$tp-QDqR&ikFU z&U&7+&L1va%rLX>``-Jyesvk5ASaHFLWqKZfPgM3@mUE0;khdU0@C6OWbhMe9~$rj zgqIEyn$8Fa82=vsAjUCb5Iw%zMO4GZ#@^i0*w97;fsL2_-NIO8G JW=;$!FCkN z)D)se1^=8tw>?%K+CXw~%CkmOS0}mq&oOLeb(~6W*M+t@WKW~Xy&w}q z&J)y`{mAcF(z@S%nk<|MMUp0`CUPvK@H+QSMC>-JV*bK<@$|{p4{u-n*I%uIA-8s@ z|Cbm3=iLEtEHT~x>o0;I)&~DSUsRm8MSS|Cbrb*8tG>SS^z6(Cw?QBEKiAP3OA+%m zHB}G>vx;RHo5K|cYoYq4sQ$KAoNe>0vYHFkxcI*oK1<$jwz; D6qOi>u}LkLv$*LHke4Dn!K@!40@TY5T(n-Ut>nZ;SPx%lW$az%26kzJ2Qd zSp=41{eRs#hl^KyT3 nIYe_!v5rw{T+heM0Y%TDU1KYL7~(b`HMuZ5BlETGl=1wTK( z+FVO?Fqz-0FK0X4<*qYsr(Sou@iPB;A7;b;jBPhC2Yk~iYAzC+`;NM9%$3aRv3Y3A zsQk7T@9K>8AtwD{xuw=`rBgSib!H~4j&~}XiWUnO7v=95qdIT0e*~p?Bz$z#M}5`F z(Q&DR%qMd551m}9Q2W9Cp*SCZmAhzs4+W&&9pByEeY{Xl(&Y2gCBux`+S;Mv;kJ3- z=aY5bthTnc-@ZLPSzU=ds?fjfn!{B;r2m+wQf{;Ln$-Ig3r!5k|N2B9!Q hYTtTv71cp&e~>~@$LSFe|Alc`|!o<|GeSKeREQE z0 Q5oNv+c#(M|U_}{SbrS3=cUWDT{i@3XUmoy*IU>0h2)U=$T&wHF~ zxT^%sbi@+4lKE{S3h{dUijR-atM6bow6~WxFhEc*)@fR~NaN;CF7aEFc5!nlx9n59 zud_o!&`V2W(ro-$WvzC!Bs_7nIQ_o1u5RA(>S?-=2|EEQ9&E!oQaGZZ0Ci{F-e6?C zNXcYBin7bYV2?1(d!JyuSa%?p>^>$i7y~>^`>QlopFPGD0na4&`MbGNWBaBnWNBR1 zwuQmS1bt80&oBP-2nO&+$j}%MY^(a5!n?Xn*hzg1n2ES;(_-~h4j*D5r<`fN*OCEZ zFPsQNl9IIMZ)R>%2Kc! #Ls=kRaymLptoMgEx2FR&?-FIOo4Xbk{3DsQYbxySg0$mDa7V4) z-(2i3JIf2T#}%kwowxX4>+6$&%Uh#wDp{m)b8{288{L?!wL~o{N{_fx5(;ORf5s3O z3J&GZRDO5({JdjL2bNFgi }2il*T7}n7yG 7gy7$L^wJ5)y!dtjddSn-N zocIZ YpJqEw=jD@Nrm6Ga)?zE#Yb)2&4SxUGb~ql z!Vkm+>P04ZL9!PqAMt+Y=M#cD*Vf*Ku>Zqw0&Hhifd?jkzXN-lse0dk&a<;RlCqL% zPMS~CNkl{fdL-mm*3|V@X6pjj20^`9OK9;*&gydV+>1+iiIFDe>N*hHn~)aYTQMjw z;O*b!oO;p(i|z<}fr+_t;eR>uXXCTDC>w_xGNHTs_4Vm&tz#fItvq~W1nG+sl#0kGX65{BZu@ni}pN$%@0% 9v>gm>W()_NJx~* zo}K@lejgi`FfuyY-+QBWxY5%1{O|3RbI1$K=YLJu!3J)HN+;B6=seM=cI@t!pk?BG z3RM!7wnjA{Tz9;yr#4k~alzr8(rn+a0pFICtlil}`j(CDYa&OEvHigZUtdA-$hFKd zD_aI(E;ky-vN9fC-r?EVw!D$tyb^nSxBH~Af}x$W_UpEzUa}nzZ|~=lk`+H7FS_RE zzk;(2TVGSw*0y%tx44T2wJ=iPcBr&mK8 ^w?xAyQ0COdOPFH<-k~b8^%e zxS8L-3GYlTE@nYNL8;SG<0=&R;(6lq1M*{$_25o6RLH^Iosy9Q+umL}UBhLG0+gnw zGz-Q1xN&iDH603Oea?AfR-hpnZXF0etd$^iuQ{#vC(2q#ywI+;qvqnmRmZ~vN9FHE zx_^Xj!-c~Nu5#Mc)KpYzoT%Ntqm`BQ%aDMb_Auvcc^Dk?AC| zxp`17o}x|%7Vzxk@Z@ae`fP`o&*$^Dy4ir#zp1Ij+1ZuhE32xc*jTE7CxN9E%5XTm z=c-Ml=gP=J|IY-Gg|&57VPi_P-=|a=nw1b*(v|M0tkH437#PVO$$TgT^gpF F!Ey)u@Ey-7;JN%ig7$;R>hf^aGOCM-F5z P zmA!#GM)gw9oof0-bHu`Cf9go#^U^WDCBZx2)-P$kJ2|chE^cne7nnju6Z=id6sUho z7W|$=rA$Zaf*i}!=hd?I4FdOX{2vzGaDBDu85zY{60iwhKapuNbRS5%$t%n!9s7-X z7{}_{%HY#dQerP<`r5~>)@$rsW% ZY8j%e+Tx<=EsA2A6D zG~}fQ+H`Sov5E{_ 5;LwXRX&GX)>lG-;YgcB_G+` zC0^)0vJ}l7nNqPw+Sqd8yS^?Q1h;2-G>-$|87zE!3GjJ*x~y;1i+`r1d@Cuz jz*@p$(yxY~6LDvbxk+w#{Vy5-FpiFn&gSlJC4)87RTrO- zu@{ v^o);j^0I@8?rGRIrTQ`3S3(Fb&J0V-O^%x7yrd!b+obTUPnC@_a zj+rQIPpb8J0P<&si}#UZ!bb@WC e1ECu<`Ju4L%?M;Qgs!FSDj5L64sw;riNZ zYp?0-E<;CG_h+A}icVbs+# kqXSIbP2^dYfn#d;+1$x!TW2#x^Ly(a{ zuG6Fy5*UcYVJ1_v-GUg9`wTvnVQPQmSKrX^0;8HI9AFwiMVL}+jJsGWb#?!wrXHQ0 zQ8T~){`U*QIoajYiYXaS&u0Kgs)^0P=a)X4%E(JYl?zP45!yWS&CZ3EnH(7vyl6k_ z44vuOg}7jl@_j=i(9}#ECXJM=c%ApB*2wgm5<^@+Ok%@Hlc38f(%9a ={_I~T%tXy7(`E4Vlv6IfEW+*N_}Iit z2F>&8s A^Trat4_RM z5fQP6*(|HHP*7m<87eCrQTCSk)473xf&kSogI`Tx(G@FlGXoEsih5vqcS-=3!rOOs zG7jqod*s{G=QAcl&wg<+zo4b1Wx>T88Odtz>Pp_+v;dzced4OGuOBP*OjdTF{j`@o z5e<~sJlo;%LamNQPu^esX5dXHOTiESEZAS;zxvDFeE$RnljG#BIV`C0|D>%=)Y{tW z15mi(JA99exrLF@SqK0<`ggqv9xJ)R4W_(e#&&XC`u (hWTjdBZeaHB^!Oyo5Hl_)P?FMasi%1;rToXlA9 z8k@>^OF|}{G_cj+*D#ZKUpz}rZf 1%X!T=5uL z?do#30R~)H`d~eM@A=^)81g>QlS?aybO_GPshnP%fdY65Yw;uN@}o^lDU?a`UeBX{ zza2j>Mzm_nv5kT6Qe!vcC{N__9RJ JQAerw;r3Wv zr`bl)#napS wqSYe(TYh=rGb(T=5f`sE($>1+Sw0WXS)ffi7sR`Jme%&>gK54>*`c|)&rjCZf5U4> zrt;SMrz%ubRTq3B6ju?YQ8=6QA_JQQ!JYUeBPJy!1@NVuoEt7EcNSH;DMG$M`P +A7YO+T5-g?k=fgTej{jD@eqo5`i{h$<_O)Sd}m zk}$+cWl~TAO$ngD6$`s!9JZy+O$oJi;wMN*Z1`xgdi7MI(cnn})cO(=vwa>G0h43i zfW1+0?BLoU!o)-eg T=(*nbL$++@auHs0FjKs z!UDaHFqx3ob-QwuCe44JVAZK^tSXtyJ_8Dkn{U>0sn>H6 U&$=xzcvf9>fN^Ct zI_|;#R0YQi6tp%QE=67C_MVtrDrg^eq;T&Rhj~#7bYI_-Z0!Aeu=T>H)H^ xqoli(eNLgrqL`X!=xfb*JImTykarCfhQtA7$2z@4& zD2gP+ w0$9Ui=dMCA6+iRH~_9 z%q_})pZLhpQB6$O-roN8@(QodDjnm2Hp?C9tHsEQ3JxM}`_|vwL(|i}q6bE1uoQCM z2FPe}5W7-?Ql5&5x#??SVo0mO@5Nis#zp~48;k7PY?V;XYXHTh;g|qG4+=jp%~T)! z#678DXF->3-&$Fb6%*qFqE M$!( z+=rEvlo0TTqvLXciyq~13@^%_ry2u-U$JhSOFug$%ZP #qk%;K@|2i z?q>5<8Nu<3!^zjv(<`ih5URFa_^7OWQtRCNbJ1XV*@)M7o$2wss}*=Y_&-GSn3P-g z6YkUr2yy~cO9-BNLIN!+9@B?Bl~SKXQ@a+sJ7fw V2}<8dR11sURa z$8Ux_EVBK!uEU8uyu6&wtf=Yfdtzf_wW{9W@7pLUDM?8=y|yd11JBY{UHw5L8(Q3) z)|)T&DA|Fwqf_S IP`fve`2M; zsg$$Zf9b+U0vJ|hb#>;%PMzN!AHmkv7i#HHxIrcRU!-VCI06YtYWzX_#6+*f_}rLa zW?{U|v-l&?k_lyp``dRbmFXp{V26X-@_T1*4QN}dYYVd#4vns}2w;B|>bVNzvCQ`y zv5nVOy6suXr3uJ>_P|$ER7_!I%5HA<6FK9UZ!~gf3XhFz1t5*Lg5!GQf*n9-s^^DC z+opTdV>oa0-U ob>l_PT=%ao;0aJ)fz0p<)FZ}tRwO&I=aCc5&vOsbb_p#j z%PO+6jQ<@qR#n-GEDW2>Y*^U#!QqH?Iv*V!Uy5Z##m5Wf=NG_h8<23bmWC{bN{3#- zFnjmeJ#_33Hu2+0Ys+rR-o2`32UXc|9%)v<_n8UB!abI;XYmAJzunmA)owM-GKY zgi YLsLg|Dt{R2W^cllq@x`CSAHm%AKM+#f|^ zI=Z`?&o|cDOxA{Uk$J~PiY3m-#CL?@IxVmE_&uj!*45Pool4DDLRIyBq`>)8aFte7 zgyyv7YDHQ- 1Ux6W!i(8I%bJ-<=oJ Db&}ZVM-JRaUBAd|)-JNbJpL08Q4G zdpjstSyS~aPpWEpZLNp(;ilyh6cLZJD(=Ixqa|_HyI0=c{C+1h?ueK3?k%6Rv`~Z} zI$E4p#7F>yj*c#ZR>dvcu9NgKG63~ zAzGgcYwybdsEtPJx?Z^%vk{qe=46WW@Pl_*|QBv zd0A+MvUQch0IYZQ@+fANRQRKU!du>{sngR_H0{NJjvE))?i4Y9ytMBpr2Bin{?z18 z>CN(d`ZN`vN#oR20OKT>o}PJuj!w?Z94C)Knq5=`%trH4fLew0YW#lDT7ho&{(b!E z3Mwuv_s`}N?~DB|a4_WM H5fQ=1hpEkc4;K3bTokuEMPdN&fWwj!5rF|z zA{bcs +7!%^pR6g1OUoK%2#(h*)hE47&Gn~)qHfUC2uJxW_$k-;o>qt&1*z+N)Hvh>b)oZ7vJ*f zVuF%n0!thyp7RF#tfQkt)$3l{*Bvmx6dup=mU%Ocv6;4KCq F$mPn(l0k5ic-pe&^ gg*|{060^0NojF&l8W99BQK 0R=Gnr{-bQ+<8?BW4C|AZuCbSz20}38th!&-e|G%jOG=a|}+d_nHL9 zr`JMKQqq )u;ip9K#fImM- vwD6=S+W@b z^c@q+8P=D;Ix;{1(MUu9fct3db`OYq6M#b-$fAy$XI@Fk$wx=$@u2T#s4&3MuY$C- zn8daafh-Kp@}sH(OoZVPrCg>rHA;D4$x@nFHd_f|fD}K^YW)!$9P|uzJwqw)PhQ^I zWk}5F`FW-SU680IAniY+eVqGny}iBH2b23U+Jl1$Oq&`TEv)R~WB(v<+s(dpTbD67 z^)sfUtSSHv<`7toU9eQJr08ffEW+1P#-CPxm_q&)L8L#G(WNIiACJ mA2Jn!qwCoA03W$C55taA~B z=I8Oz#m>oizUY !c~17={Q0Mo~2E{c81Q+mzsOhZ$VXl-ml|HQ)n09Zsx zKYlm>^-D@xn$gyF?^|%NChvUYB?c`yr3SGkI`T>_mPfp8SU+%&qf=rUo0=30#-iCv z8*-CJ%wfYo&24BP0 sU$5i)*0QsyPI^Ah#Eygaa&k^{3H2u$PpO&RM34+$0>e>~ zckQm6uiR!zDKs>c$7AyyQ0r56nQ&0PucmO%^I Uo-m#j>o*}GMyI!S{3sn_*h z)d!P+H`l1fL$veKGfqOw0AYKS+raSx>T6h!Nn2|xeBBFCdixUS0FUPiT$|=Qe<9a1 z*V$UdM<0&cXn{-bhP2mYTq1shSjcVN#59~Q4Kt70V0j_BXwSK_8alECWJm)wqbHtM z8xGFS)SOf%8rkOvhK3n`RNSJYOfPHg#I>xpf0YBSwaQL_90;|oudN-Pn+wm)rNzKF z$3I0b7;QtTGx}xR{uy+LB_qc&mE7kTK5d^cg+A;bVFOPm#vgX D%dZpIaR%H@(=H}*Zcf-{I5+deJ>ht#ZO)uVWNU*z0n^ETG z=f^I^Cr %6?8rAYs4RD`Ok36C4@gTt5v|9-NEQ#?@4fa;S% zc6Ujul$6C7IQ$HqYaSQ(jpNp_DMeem`1@@xZtfFUL%hWN&V+rb^#rLI>=%YcqZpa* zs-fTIa3u6ja>4WB0;u_qJna59;ljbiNn~OKh rCHqg;ydIa)TkUoovDJa<-RjV_^{rmU1!gzyadYY*-1t7mdP2ZN) zF~8GEHZ>-FId^q#0LN{nZT8T$RaKcqDFE#?(o%Q@G!9BaSM22#dV1zK7%L-VSKmm? zzi&?qP8bXF=}1j@BErI NLG`e-v8j|yD=xOPTv9F` zi~jZN!>3PytK>e-r9Un&c}A_wr)T&<0N|07@9V3n!Ini)p)HKXI^^`0@v&~4Z~u}# z_kNxypl#yh ismRryljqH5Bi*|fGC@Dn ?nMo@r350xCAyXvzo>E+_zxL^l+YU0V7gPU^c1O-1(W(KDyL z=>fiSC@CN?fh0ws_(M0Zqp_4?fZF3!UcuwMj;2+F3uL0*{={8jhYz7nyw2-PBRS8o zut^5$0+L}OfL*_S{d%(6x1o}b;2jG$7nc+Uxt8rj>FDq3?!@?bDQD*jobbu9v4OQd zQ<=y*0$-!P8+<8m@8GfdsWH_tr3Ij7irdZr1nJ@7QR+RYCz Vln6R3jGr=8 z4u=9ULBWv~7k}Pxv0J6}Eq@H=bAPpw-Ewp71^ojlD^{n&p~E>g3?kFDXT=EYOv_l? z@pAP!Y!x3J(@d#i(P#OTJF>L1^GQX8YHyD_i?&VV6I)_GP`^H9DEv%~>!`H+sFo<% z8SP(qgGtTH8?qk1{xVn7_cSWF=zhM|l7q$SeMCfrho@(m;|MO0&QCT5D4k(8z&eoe z{UjI?@)CE&Mj0EBDai^m%`;5Y^}$EXcYTe_Nf u3BW zV^0cnG6I60=DQo}Ky5&k!IEfIXdl%SY8t+zN6RHSKR vA)VDJ`jHk hY{pu|#9&obRSb}irlvG? z>YU!x$*>H3eU5K0w*{WhSEO_aECgf0l_56k!i{M1WEGbYk3AfwlTqWkE-05*bLmg0 zbi9Nq D7Cj~! zLBUjzd;xtFn-o{-lQCs5CRrThDX@e3)44Ifi^y496M3KSlHfA)zkclT-;kyK^;sAx z`51b8|L_m Y$t2@`0}kE8SNTG}bbjvOl$2xw7T}#qYI^blo6tCdmBU;nx2Z|j z$>v~pcl9)pqd%EH0Tk8l)pExPNA|=Ns5I^feBLlFt~LH%b0Vu^y1v_jQ!d|6ELk1G zX|GU_>geif3h-xpITb{m6^ZXl{x)IH@0>9=EdB~ad)5OsB*jkag-?n7gqB}2j5jn^ zu1uPtA=QKM>G&QKqn&DC=aoVJ{GlTzCg$;3?fyfMx4g1UM^7I&-*@Opf&xo7pPz+? zlK>b5I&3^e &rVkb4Wp7tE;O6Go-1hsoNxGM!E(Bhrm%Wwl|M#HF!%Y zh+Y*&h#4MKJ n}8Ny|0LqYKhLzoUT5s@1LDd`#_kR#4NGGV 2*F$w!%%_H_hk21myMY+D>C!Yh z!L6XAB_$_^n=6-2P<^0c-I8hXC1>3XB(n%!0x^TJ9sPr`@<=Hf+9$l9pxFor2uS>h zjPX;>Q{myCo0X=frY5ME>Yx8;{0}g#MDB^# K|J`}h^(6$E+AK+aZqw{ep}6^0_w+o4pR}gDO>UYkU5vP*O4Lm3l1Z1xj%rc zOaA@KB(W?Iay?$%AWev1#F3J@Q!D^a;PU$NaFa4pK}}5z{yi+($*9o_fQ6^ekcWq@ zv~j34GMyUg>$97OJ37kER~BE;eaY#h5fCUu{{%D}oHuVYp=>qJwATO`%gCG3iWn!y z%B*gT%(%Sti23#Ni`YtpPVrxu95uaim(|Sv{=O2!3Zj>n*N{~e2qDv$jFL>N*EKX~ zijH6!NbB=!0xJl_y+C#Zo;Bjg#VYR Y s7ax-EWz z0Vf-pQ!Uh=fnWrJw5@YS*KKu&Xu+a@Ap4kAIh#rIp>2RESz8Vy=*?wtt?)&N{H2EWY zVh4(bzS0z#pKq1(?Ag&KC8nabH#+bmV`3;tQc0+2XiDww#MakMu(7e3!>vI;62J}` zP?KL^V3@7>UX)p1koLUbC;Wtt{^rdaK#P{v*ZDHBY`$nmIer1W5*!foU=vq-TPC`a zKL%UT)Wh}s0!;l@5D4VQTN)a|Dy=#F;0ToqbL4=inRNhEyJ6)fG(7`?a$-TI4`ZXT zCZVkHAaVN7moZ*hSJzlo7zABGSov4 KVcO?R?@iBd2|A1KFBq%8OD> mS!0-m-$A2qycj L5eLES&T2P?>Y#(XCp` zW3xMQc?u94kQI(k2C-hhma&pTDJm*zhwvd`WP4xCjY>l}i)*SQg!V75uYs;b`HV fDeZ!)h#Urldb{+ z=&=P&$YpZUIGoA^xxH-73Z7}QC_aP9yD547b^}fg0vE*SnCR?1>71mDpgCaW{1Cod zD{=q#8&yGFeX#QP64oC)R@{>qtWz>1paOTkCn}$xWjH%K8@+hW#>2~ZZ2!RLwYrnK z2x#jCu$> |lHxq7X!zcRo9aCHXvmUmY|(D)wESTS zdbfxV=Ye><-dvb5J^vaGZq52{w!FkIXj(i=me82!fGy&2oM#RU?d3%Cv}L$B>H2qj zA8U~td~Mt!T~J?#;qX=@>j~i1ZEeAU)K>_(M8Z 8b)AURdJ7* OmWSe}7BX z8bk&Lh94mz8NU|=fO7JQ;>=Dq2jq2i>U|P{PG0Ua#SYqb&=}?9{eEXYxArhbq^O7p zv8(Hu(rKIyjd{ e#O{hwD>KQvY2=LPA0wm&s}zK7jrIeHPaig-5QT z^YZ9A0W$#QdmDpmF*CqyvNAC8g(SxxMqTk2Y^V1?|H=hx2L$>X+=7bN5?4q_XqY#v zY7bss05TqIE$}Eve->1oIQ-~(x<^4(wIi{@d~0U}Q(1>uv++kJDqgn5KzTVkKvO~E z<7$=*1~G6>rs0n99du;M^Ye4HiAjD9@u=2nA6Ftq@rEV@_}G%q9&L5YI7Iy$xv z4q}2}sab#N?nhyRr;ZK|KQsSyMlZy#C@Mx}L!sr)8~a!nwM3EU=Mjbz1ZY8qzL|0p zfjV{}+3z>ead1+=@*68#OpHVVV(s3suwX!yBsDaCsS*qdUoZhH^mNnZa$4J4W@cu_ zm@|nc&d(kI;MJi6H#(HHA^X+wPH2XTni{&aJz!(+gnsKC!JJ(Gv_4z!B+b9O9{^L<4^(h)@|ZeKTm_D+qz>h@t1hFlG?d{-|1Ac4UU zNU-CJ7NMopVD=v5@IPk374-E1fClO2_`koue7xWu%Bab%u(7cCH#ZPYz>yPaJ$iO} zn(eG`w+%qp_&A`IymkzCA-yl+Ip+z(gHjvZF>ma|h*0fj|6~E*!g3^FbAP{ceqQf7 zC@?VhG4lynXr0@D(V=#o)AzA6%BlfCU3(^9y(WgB5vZz;nV6b70YwKm1Hdn+t^ET% zH#ax$3SH|1CKNu~iHg1c!Y|_~3&^TJbxcq8g0z$rl+6!_9e{Z64DyVKgTUn2*n7~| zG;|L@X64wlkH>M@#2>}ly1r7k-Nbd9J1ydUGbHZWph-Q!5yv>6T%OAC^t6 }juwzXJW{JCIkPhk`?KA&5}`?Q7L8W+Yr +}~avef=EcCjE2Xc}`-Q7wQ@11)h<)ZkR&%ZlCa`1^&3 z6fd4l__b-bTw?0oE?wl=TPy-K`RUWA5a%xKm0@TfkfPq%L %i`u z`xmnNMQ@)*zradYX)rk9-h2^gxN&`T6&(}Pp)LFn$xpNJ#qgp^U2pxaWg+p!93t#Z z%Eo;hBS;xg2{6U_U#(97fsy2Bw!YrW?=IQ>Pku4E5P8HIQRf2tiW;B;aTKoV#Tr7U znqCLUHO$SPe@jsx3;hbe$yY~KkoocGLaRsMci%Ugu6(OjqQ$#khbdHw3=-~c=+j8@ zZ}G!HjQ-I#5_p)c;Ng$lpDR)0KLKI7qluO#;iW(D23bwbXB=j7HP^PCr}$pp-gS;y z5j)&=pzncD3xS)hloUHKAbEYSA)Msr7bgv-njj&+wYO(_{nLBkmr!bNuhf=3!2D5J zU6;cNEn>EN@73n)ug<|^aJrg|GW|r*D6e8|%_x>78OnzomXO NsL1ASeKCa8`wK*p!W= zun*%T#o%X!JQWZ~P;glasJwJ0X#D~vc%E@{a~l~m-%w+?(foqQ=<3b^rz_s~<^tr4 zgfow})fVOqBGP$Ua|X17?)V*-M=$Y^K+Z?a_!^9XfERke42mK@zp{o)>$XpLL&NC< zNM(W{2H=Y%?|3t5X=^*Wxpmrf<~Ik!c!7@$hWFk9B>Io`U${0V(4=h^%8 ed48RpbdaAiu zyK|!IM$D7nzGn!)LI4q85TJ;S6=5l6Y2O!Anz6IA{8*%>O+XF+GgwGG1?H(&VD?)? z=^ICqjN6P#;8fno{jF|xD?i;jtE_QH