diff --git a/README.md b/README.md deleted file mode 100644 index c2300ca..0000000 --- a/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# CloudPractice -Практическое задание для студентов SPBSTU. Работа с terraform и облаком selectel - -В данной практике необходимо создать инфраструктуру в проекте Selectel с помощью terraform и сконфигурировать веб-сервисы с помощью ansible -![img.png](assets/schema.png) - -В качестве конечного приложения можно выбрать либо [2 лабу](https://github.com/antonaleks/DockerPractice) либо контейнер с todo и БД из [docker tutorial](https://www.docker.com/101-tutorial/) - -В первом случаем необходимо создать n=3 виртуальные машины в том же принципе (две разные подсети, одна виртуальная машина как шлюз) - -Во втором случае проще - нужно создать n=2 виртуальные машины, одну для todo app, другую для mysql DB -## Terraform -1. В провайдере openstack заполнить своими данными поля из excel таблицы -- domain_name = -- tenant_id = -- user_name = <логин из экселя> -- password = "пароль из экселя" -- region = "ru-7" -2. Необходимо развернуть n количество ВМ с атрибутом прерываемый -2. Развернуть необходимые подсети -3. Создать публичный ssh ключ и приатачить к ВМ -4. Создать загружаемый диск с ubuntu 20.04 -5. Создать flavor 1CPU 2 gb RAM, Диск объем 10гб на каждую вм (базовый hdd) -6. Для каждой ВМ зафиксировать публичный ip адрес -7. В output зафиксировать вывод ip адрес и команду ssh для полключения -8. Создать файл inventory.ini для ansible, где описаны айпи адреса созданных ВМ - -В качестве примеров использовать [vpc-example](vpc-example), заменив на прерываемую ВМ либо [preempible_sever](preemptible_server) с модулями - -## Ansible -1. Использовать сгенерированный terraform'ом inventory файл -2. Написать плейбуки для развертывания ваших приложений в docker-compose файлах. [Пример](https://github.com/antonaleks/ya-praktikum-infra/tree/main/ansible) - -## Итог -В итоге по запуску команд -```bash -terraform apply -``` -должна сгенерироваться инфраструктура и inventory.ini - -по запуску команд -```bash -ansible-playbook your-playbook.yml -``` -должно установиться ваше приложение на инфрастуктуру автоматически. Критерием выполнения задания является доступность веб интерфейса в публичном ip адресе (grafana в 1 случае и todo-app во втором) - -## Раздел по безопасности -Ни в коем случае нельзя хранить в репозитории чувствительные данные! Работа не принимается, если в репозитории будут лежать логины, пароли от ВМ! - -# Источники -- ресурсы опенстека в [terraform registry](https://terraform-eap.website.yandexcloud.net/docs/providers/openstack/index.html) -- откуда брать id проекта -![img.png](assets/id_project.png) -- установка terraform через зеркало [яндекса](https://cloud.yandex.ru/docs/tutorials/infrastructure-management/terraform-quickstart) -- документация [selectel](https://docs.selectel.ru/cloud/servers/tools/terraform/) -- [примеры selectel](https://github.com/selectel/terraform-examples) (terraform selectel provider использовать не нужно, только openstack) -- пример связки [terraform+ansible](https://github.com/antonaleks/ya-praktikum-infra). смотреть папку [terraform](https://github.com/antonaleks/ya-praktikum-infra/blob/main/terraform/sausage-store/main.tf) и [ansible](https://github.com/antonaleks/ya-praktikum-infra/tree/main/ansible) diff --git a/assets/id_project.png b/assets/id_project.png deleted file mode 100644 index 964089d..0000000 Binary files a/assets/id_project.png and /dev/null differ diff --git a/assets/schema.png b/assets/schema.png deleted file mode 100644 index ab32fa8..0000000 Binary files a/assets/schema.png and /dev/null differ diff --git a/img/ansible_playbook.png b/img/ansible_playbook.png new file mode 100644 index 0000000..99a5af5 Binary files /dev/null and b/img/ansible_playbook.png differ diff --git a/img/ansible_playbook_ok.png b/img/ansible_playbook_ok.png new file mode 100644 index 0000000..59101cb Binary files /dev/null and b/img/ansible_playbook_ok.png differ diff --git a/img/inventory_ini.png b/img/inventory_ini.png new file mode 100644 index 0000000..f7620e2 Binary files /dev/null and b/img/inventory_ini.png differ diff --git a/img/private_networks.png b/img/private_networks.png new file mode 100644 index 0000000..b560a03 Binary files /dev/null and b/img/private_networks.png differ diff --git a/img/selectel_servers.png b/img/selectel_servers.png new file mode 100644 index 0000000..7eca375 Binary files /dev/null and b/img/selectel_servers.png differ diff --git a/img/tf_apply.png b/img/tf_apply.png new file mode 100644 index 0000000..cd2d122 Binary files /dev/null and b/img/tf_apply.png differ diff --git a/img/tf_init.png b/img/tf_init.png new file mode 100644 index 0000000..3e3283c Binary files /dev/null and b/img/tf_init.png differ diff --git a/img/tf_outputs.png b/img/tf_outputs.png new file mode 100644 index 0000000..a88aae3 Binary files /dev/null and b/img/tf_outputs.png differ diff --git a/img/to_do_list.png b/img/to_do_list.png new file mode 100644 index 0000000..228e665 Binary files /dev/null and b/img/to_do_list.png differ diff --git a/preemptible_server/.terraform.lock.hcl b/preemptible_server/.terraform.lock.hcl new file mode 100644 index 0000000..62c177e --- /dev/null +++ b/preemptible_server/.terraform.lock.hcl @@ -0,0 +1,62 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/local" { + version = "2.5.1" + hashes = [ + "h1:Np4kERf9SMrqUi7DJ1rK3soMK14k49nfgE7l/ipQ5xw=", + "zh:0af29ce2b7b5712319bf6424cb58d13b852bf9a777011a545fac99c7fdcdf561", + "zh:126063ea0d79dad1f68fa4e4d556793c0108ce278034f101d1dbbb2463924561", + "zh:196bfb49086f22fd4db46033e01655b0e5e036a5582d250412cc690fa7995de5", + "zh:37c92ec084d059d37d6cffdb683ccf68e3a5f8d2eb69dd73c8e43ad003ef8d24", + "zh:4269f01a98513651ad66763c16b268f4c2da76cc892ccfd54b401fff6cc11667", + "zh:51904350b9c728f963eef0c28f1d43e73d010333133eb7f30999a8fb6a0cc3d8", + "zh:73a66611359b83d0c3fcba2984610273f7954002febb8a57242bbb86d967b635", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7ae387993a92bcc379063229b3cce8af7eaf082dd9306598fcd42352994d2de0", + "zh:9e0f365f807b088646db6e4a8d4b188129d9ebdbcf2568c8ab33bddd1b82c867", + "zh:b5263acbd8ae51c9cbffa79743fbcadcb7908057c87eb22fd9048268056efbc4", + "zh:dfcd88ac5f13c0d04e24be00b686d069b4879cc4add1b7b1a8ae545783d97520", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.6.1" + hashes = [ + "h1:Xx3UvdKXObNTjfd4yYHDcFalYZujg7NBY/VpZISiTb4=", + "zh:2a0ec154e39911f19c8214acd6241e469157489fc56b6c739f45fbed5896a176", + "zh:57f4e553224a5e849c99131f5e5294be3a7adcabe2d867d8a4fef8d0976e0e52", + "zh:58f09948c608e601bd9d0a9e47dcb78e2b2c13b4bda4d8f097d09152ea9e91c5", + "zh:5c2a297146ed6fb3fe934c800e78380f700f49ff24dbb5fb5463134948e3a65f", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7ce41e26f0603e31cdac849085fc99e5cd5b3b73414c6c6d955c0ceb249b593f", + "zh:8c9e8d30c4ef08ee8bcc4294dbf3c2115cd7d9049c6ba21422bd3471d92faf8a", + "zh:93e91be717a7ffbd6410120eb925ebb8658cc8f563de35a8b53804d33c51c8b0", + "zh:982542e921970d727ce10ed64795bf36c4dec77a5db0741d4665230d12250a0d", + "zh:b9d1873f14d6033e216510ef541c891f44d249464f13cc07d3f782d09c7d18de", + "zh:cfe27faa0bc9556391c8803ade135a5856c34a3fe85b9ae3bdd515013c0c87c1", + "zh:e4aabf3184bbb556b89e4b195eab1514c86a2914dd01c23ad9813ec17e863a8a", + ] +} + +provider "registry.terraform.io/terraform-provider-openstack/openstack" { + version = "1.49.0" + constraints = "~> 1.49.0" + hashes = [ + "h1:EJImbAXNraVwSO3lo+EIom8qoMd1cVvMoiZ3B1lL9Jw=", + "zh:18b0a5d528fe3eb30060cf478db5a5efaed9d9837f4afb35ba58f0196ba6a51c", + "zh:3cd7f28730ed216740a7bc62169a0d630f95ecdaee1162952aab67011fcf8831", + "zh:60a827813523fd77e75d0145cd066cb4c2a89453083a5bd9e0712a8423bdc14a", + "zh:70a4e4af076ce946943d36cb81c47569d8b30722f2b89768006565a4f512fdbb", + "zh:83563688ec5a5435649191f3e80fc96b93571b7333eee12e2d448149f21ba7c5", + "zh:9067cba5ef3f89e81f01b7e68989ffce2460c239a697ece2cd08c77c73afaf87", + "zh:92589eec7fd057ad5bb00a5c5968eb93d2a4b07380c5be794410349f0188787f", + "zh:951db60626bedcd4538b88d284f9b70ef41166dfbdc568ae781518e24be0b077", + "zh:9d7340122ae1a7ea5b0e5c469e89ffc43c24f4391fc870b27efe4dba461f8b84", + "zh:9f31056e278e8bd0a4b0fbfe5b02a625ee9d072177c36148cde3295adbd4a9d1", + "zh:bf7a0beb72d9214fe2a61db76401057462f1133a48f8ce0a666756660d27b2b5", + "zh:e44dcadcc0680e7b7af94a8a4dd1e421835497178976604455182dd98d6ffe96", + "zh:f5d03f5ada85d41cb94bd7a2b956ca2eb9d7d6cb6d5382bf78a5e641be3eadb0", + "zh:fa7134711a60f8518b82c0246f5a72efd24d23e074f3aec4eda90c013c0d23b5", + ] +} diff --git a/preemptible_server/README.RU.md b/preemptible_server/README.RU.md index 7f578f5..1a3f688 100644 --- a/preemptible_server/README.RU.md +++ b/preemptible_server/README.RU.md @@ -1,4 +1,4 @@ -# Пример создания прерываемого сервера с сетевым загрузочным диском +# Пример создания прерываемого сервера с сетевым загрузочным диском Используется два terraform провайдера: - Selectel - для создания проекта, пользователя и роли. diff --git a/preemptible_server/main.tf b/preemptible_server/main.tf index d50bcdc..9c8488a 100644 --- a/preemptible_server/main.tf +++ b/preemptible_server/main.tf @@ -1,4 +1,4 @@ -# Создание ключевой пары для доступа к ВМ +# Создание ключевой пары для доступа к ВМ module "keypair" { source = "./modules/keypair" keypair_name = "keypair-tf" @@ -39,9 +39,9 @@ module "preemptible_server" { resource "local_file" "ansible_inventory" { content = templatefile("./resources/inventory.tmpl", { - webapp_vm_ip_public = module.preemptible_server.0.floating_ip, - database_vm_ip_public = module.preemptible_server.1.floating_ip, - webapp_vm_ip_nat = module.preemptible_server.1.nat_ip.0 + webapp_vm_ip_public = module.preemptible_server.0.floating_ip, + database_vm_ip_public = module.preemptible_server.1.floating_ip, + webapp_vm_ip_nat = module.preemptible_server.1.nat_ip.0 } ) filename = "../ansible/inventory.ini" diff --git a/preemptible_server/provider.tf b/preemptible_server/provider.tf index d9597ab..51b884f 100644 --- a/preemptible_server/provider.tf +++ b/preemptible_server/provider.tf @@ -1,4 +1,4 @@ -# Инициализация провайдера OpenStack +# Инициализация провайдера OpenStack provider "openstack" { auth_url = "https://api.selvpc.ru/identity/v3" domain_name = var.domain_name diff --git a/preemptible_server/resources/inventory.tmpl b/preemptible_server/resources/inventory.tmpl new file mode 100644 index 0000000..2bc1461 --- /dev/null +++ b/preemptible_server/resources/inventory.tmpl @@ -0,0 +1,4 @@ +[webapp] +${webapp_vm_ip_public} private_database_ip=${webapp_vm_ip_nat} +[database] +${database_vm_ip_public} \ No newline at end of file diff --git a/preemptible_server/vars.tf b/preemptible_server/vars.tf index 16840e5..3ec7fa8 100644 --- a/preemptible_server/vars.tf +++ b/preemptible_server/vars.tf @@ -7,7 +7,7 @@ variable "os_auth_url" { } variable "server_name" { - default = "tf_server" + default = "my_server" } variable "server_vcpus" { diff --git a/preemptible_server/versions.tf b/preemptible_server/versions.tf index b184cf6..10b2b7c 100644 --- a/preemptible_server/versions.tf +++ b/preemptible_server/versions.tf @@ -7,12 +7,12 @@ terraform { version = "~> 1.49.0" } } - backend "s3" { - bucket = "/students" - endpoint = "s3.storage.selcloud.ru" - key = "thirdtask.tfstate" - region = "ru-1" - skip_region_validation = true - skip_credentials_validation = true - } + # backend "s3" { + # bucket = "/students" + # endpoint = "s3.storage.selcloud.ru" + # key = "thirdtask.tfstate" + # region = "ru-1" + # skip_region_validation = true + # skip_credentials_validation = true + # } } \ No newline at end of file diff --git a/report.md b/report.md new file mode 100644 index 0000000..a35607e --- /dev/null +++ b/report.md @@ -0,0 +1,152 @@ +## Отчет по работе №3 "Infrastructure as Code" + +### Шаг 1 - создание инфраструктуры на серверах selectel используя *Terraform* + +Файл main.tf выглядит следующим образом: +``` +# Создание ключевой пары для доступа к ВМ +module "keypair" { + source = "./modules/keypair" + keypair_name = "keypair-tf" + keypair_public_key = file("~/.ssh/id_rsa.pub") + region = var.region +} + +# Создание приватной сети для ВМ +module "nat" { + source = "./modules/nat" +} + +# Создание прерываемого сервера. +module "preemptible_server" { + count = 2 + source = "./modules/server_remote_root_disk" + + server_name = var.server_name + server_zone = var.server_zone + server_vcpus = var.server_vcpus + server_ram_mb = var.server_ram_mb + server_root_disk_gb = var.server_root_disk_gb + server_volume_type = var.server_volume_type + server_image_name = var.server_image_name + server_ssh_key = module.keypair.keypair_name + region = var.region + network_id = module.nat.network_id + subnet_id = module.nat.subnet_id + + # Для смены прерываемого сервера на обычный используйте + # переменную server_no_preemptible_tag: + # server_preemptible_tag = var.server_no_preemptible_tag + server_preemptible_tag = var.server_preemptible_tag + # server_ssh_key_user = "" +} + +# Создание inventory файла для ansible +resource "local_file" "ansible_inventory" { + content = templatefile("./resources/inventory.tmpl", + { + webapp_vm_ip_public = module.preemptible_server.0.floating_ip, + database_vm_ip_public = module.preemptible_server.1.floating_ip, + webapp_vm_ip_nat = module.preemptible_server.1.nat_ip.0 + } + ) + filename = "../ansible/inventory.ini" +} +``` +Провайдер OpenStack: + +``` +# Инициализация провайдера OpenStack +provider "openstack" { + auth_url = "https://api.selvpc.ru/identity/v3" + domain_name = var.domain_name + tenant_id = var.tenant_id + user_name = var.user_name + password = var.password + region = var.region +} +``` +Где `domain_name`, `tenant_id`,`user_name`,`password`,`region` - чувствительные данные, хранящиеся в файле **terraform.tfvars**, игнорируемым git-ом. + +Для конфигурации необходимо из папки проекта выполнить следующие команды: +1. `terraform init` - инициализация рабочего каталога (установка необходимых модулей) +1. `terraform plan` - шаг планирвания изменений +1. `terraform applay` - применение изменений + + +

+ +terraforn init +

+ + +

+ +terraforn apply +

+ +После чего в терминале отобразятся (из конфигурации **output.tf**) добавленные ресурсы и данные для ssh подключений. + +

+ +outputs.tf +

+ + +Содержимое файла **output.tf**: +``` +output "server_outputs" { + value = [ + "ssh root@${module.preemptible_server.0.floating_ip}", + "ssh root@${module.preemptible_server.1.floating_ip}" + ] +} +``` + +

+ +Отображение созданных серверов через облачеую панель Selectel +

+ +

+ +Приватные сети +

+ +Для взаимодействия terraform и ansible необходимо создать файл **inventoyry.tmpl** в папке `preemptible_server\resources`. +```tmpl +[webapp] +${webapp_vm_ip_public} private_database_ip=${webapp_vm_ip_nat} +[database] +${database_vm_ip_public} +``` + +После запуска terraform необходимо настроить ansible-playbook + +1.docker-install для установки docker и docker-compose на всех хостах +1.docker-service для загрузки необходимых контейнеров и их запуска + +Запуск плейбука производится через терминал из директории ansible командой: +`ansible-playbook playbook.yml` + +После чего начнется запуск всех задач: + + +

+ +Запуск плейбука +

+ +

+ +Завершение выполнения плейбука +

+ + +

+ +Результат работы +

+ + + diff --git a/simple-example/main.tf b/simple-example/main.tf deleted file mode 100644 index 047dbc4..0000000 --- a/simple-example/main.tf +++ /dev/null @@ -1,47 +0,0 @@ -# Инициализация Terraform и хранения Terraform State -terraform { -required_version = ">= 0.14.0" - required_providers { - openstack = { - source = "terraform-provider-openstack/openstack" - version = "~> 1.49.0" - } - } -} - -# Инициализация провайдера OpenStack -provider "openstack" { - auth_url = "https://api.selvpc.ru/identity/v3" - domain_name = "" - tenant_id = "" - user_name = "" - password = "" - region = "ru-3" -} - -# Создание сети -resource "openstack_networking_network_v2" "network_tf" { - name = "network_tf" -} - -# Создание подсети -resource "openstack_networking_subnet_v2" "subnet_tf" { - name = "network_tf" - network_id = openstack_networking_network_v2.network_tf.id - cidr = "10.0.0.0/24" -} - - -# Создание сетевых портов -resource "openstack_networking_port_v2" "private_port" { - name = "test-eth0" - network_id = openstack_networking_network_v2.network_tf.id - fixed_ip { - ip_address = "10.0.0.10" - subnet_id = openstack_networking_subnet_v2.subnet_tf.id - } -} - -output "tf_output" { - value = openstack_networking_network_v2.network_tf.id -} \ No newline at end of file diff --git a/vpc-example/main.tf b/vpc-example/main.tf deleted file mode 100644 index 9c2c435..0000000 --- a/vpc-example/main.tf +++ /dev/null @@ -1,109 +0,0 @@ -# Создание SSH-ключа -resource "openstack_compute_keypair_v2" "key_tf" { - name = "key_tf" - region = var.region - public_key = var.public_key -} - -# Запрос ID внешней сети по имени -data "openstack_networking_network_v2" "external_net" { - name = "external-network" -} - -# Создание роутера -resource "openstack_networking_router_v2" "router_tf" { - name = "router_tf" - external_network_id = data.openstack_networking_network_v2.external_net.id -} - -# Создание сети -resource "openstack_networking_network_v2" "network_tf" { - name = "network_tf" -} - -# Создание подсети -resource "openstack_networking_subnet_v2" "subnet_tf" { - network_id = openstack_networking_network_v2.network_tf.id - name = "subnet_tf" - cidr = var.subnet_cidr -} - -# Подключение роутера к подсети -resource "openstack_networking_router_interface_v2" "router_interface_tf" { - router_id = openstack_networking_router_v2.router_tf.id - subnet_id = openstack_networking_subnet_v2.subnet_tf.id -} - -# Поиск ID образа (из которого будет создан сервер) по его имени -data "openstack_images_image_v2" "ubuntu_image" { - most_recent = true - visibility = "public" - name = "Ubuntu 20.04 LTS 64-bit" -} - -# Создание уникального имени флейвора -resource "random_string" "random_name_server" { - length = 16 - special = false -} - -# Создание конфигурации сервера с 1 ГБ RAM и 1 vCPU -# Параметр disk = 0 делает сетевой диск загрузочным -resource "openstack_compute_flavor_v2" "flavor_server" { - name = "server-${random_string.random_name_server.result}" - ram = "1024" - vcpus = "1" - disk = "0" - is_public = "false" -} - -# Создание сетевого загрузочного диска размером 5 ГБ из образа -resource "openstack_blockstorage_volume_v3" "volume_server" { - name = "volume-for-server1" - size = "10" - image_id = data.openstack_images_image_v2.ubuntu_image.id - volume_type = var.volume_type - availability_zone = var.az_zone - enable_online_resize = true - lifecycle { - ignore_changes = [image_id] - } -} - -# Создание сервера -resource "openstack_compute_instance_v2" "server_tf" { - name = "server_tf" - flavor_id = openstack_compute_flavor_v2.flavor_server.id - key_pair = openstack_compute_keypair_v2.key_tf.id - availability_zone = var.az_zone - network { - uuid = openstack_networking_network_v2.network_tf.id - } - block_device { - uuid = openstack_blockstorage_volume_v3.volume_server.id - source_type = "volume" - destination_type = "volume" - boot_index = 0 - } - vendor_options { - ignore_resize_confirmation = true - } - lifecycle { - ignore_changes = [image_id] - } -} - -# Создание публичного IP-адреса -resource "openstack_networking_floatingip_v2" "fip_tf" { - pool = "external-network" -} - -# Привязка публичного IP-адреса к серверу -resource "openstack_compute_floatingip_associate_v2" "fip_tf" { - floating_ip = openstack_networking_floatingip_v2.fip_tf.address - instance_id = openstack_compute_instance_v2.server_tf.id -} - -output "server_outputs" { - value = "ssh root@${openstack_networking_floatingip_v2.fip_tf.address}" -} \ No newline at end of file diff --git a/vpc-example/provider.tf b/vpc-example/provider.tf deleted file mode 100644 index d9597ab..0000000 --- a/vpc-example/provider.tf +++ /dev/null @@ -1,9 +0,0 @@ -# Инициализация провайдера OpenStack -provider "openstack" { - auth_url = "https://api.selvpc.ru/identity/v3" - domain_name = var.domain_name - tenant_id = var.tenant_id - user_name = var.user_name - password = var.password - region = var.region -} \ No newline at end of file diff --git a/vpc-example/vars.tf b/vpc-example/vars.tf deleted file mode 100644 index df3ec17..0000000 --- a/vpc-example/vars.tf +++ /dev/null @@ -1,41 +0,0 @@ -# Пул Облачной платформы -variable "region" { - default = "ru-7" -} - -# Значение SSH-ключа для доступа к облачному серверу -variable "public_key" { - default = "" -} - -# Сегмент пула -variable "az_zone" { - default = "ru-7a" -} - -# Тип сетевого диска, из которого создается сервер -variable "volume_type" { - default = "basic.ru-7a" -} - -# CIDR подсети -variable "subnet_cidr" { - default = "10.10.0.0/24" -} - -# Sensitive block -variable "domain_name" { - sensitive = true -} - -variable "tenant_id" { - sensitive = true -} - -variable "user_name" { - sensitive = true -} - -variable "password" { - sensitive = true -} \ No newline at end of file diff --git a/vpc-example/versions.tf b/vpc-example/versions.tf deleted file mode 100644 index 6ba0e4f..0000000 --- a/vpc-example/versions.tf +++ /dev/null @@ -1,18 +0,0 @@ -# Инициализация Terraform и хранения Terraform State -terraform { - required_version = ">= 0.14.0" - required_providers { - openstack = { - source = "terraform-provider-openstack/openstack" - version = "~> 1.49.0" - } - } - backend "s3" { - bucket = "/students" - endpoint = "s3.storage.selcloud.ru" - key = ".tfstate" - region = "ru-1" - skip_region_validation = true - skip_credentials_validation = true - } -} \ No newline at end of file