Fix real-deployment blockers and scope down to router-only VMs
Confirmed working against a real VK Cloud PROD deployment (3 routers, 19 resources, apply succeeded end to end). Fixes found along the way: - provider "vkcs" was never configured (versions.tf) - username/password/ project_id/region were declared but wired to nothing; added auth_url and user_domain_name to complete it. - Nova keypairs are per-user, not per-project - added an optional vkcs_compute_keypair resource (var.ssh_public_key) so Terraform can register a keypair under the deploying service account itself. - router_priv_port used a hand-computed fixed_ip offset that collided with VKCS's own auto-created service ports on each network (observed: a "network:dns" port) - now left unset so Neutron's IPAM auto-assigns, which is collision-free by construction. - vkcs_compute_instance set image_id at the top level while also booting from a volume via block_device - the provider docs say not to do this; Nova echoes back a sentinel string for image_id on a volume-booted server, which Terraform read as drift on a ForceNew attribute and wanted to destroy+recreate every already-created instance on every subsequent plan. - private_network_cidrs bumped from /29 to /28 - too tight once the platform's own reserved ports are accounted for. Also removed the priv_srv_01/02/03 demo instances and the LAN network/ security group only they used - this deployment provisions router VMs only, confirmed with the user. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011hXR2ftXZZhJ4Y3XuSoR8r
This commit is contained in:
1 parent
b5d6367fd8
commit
8886c1baad
13 files changed
+504
-169
No files matched your search
@@ -11,9 +11,17 @@ __pycache__/
|
||||
.terraform.lock.hcl
|
||||
*.tfstate
|
||||
*.tfstate.*
|
||||
*.tfplan
|
||||
crash.log
|
||||
crash.*.log
|
||||
|
||||
# Real credentials layered on top of terraform.tfvars (see terraform/prod.auto.tfvars.example) -
|
||||
# terraform.tfvars itself stays a committed, anonymized template; auto-loaded
|
||||
# overrides with real secrets must never be committed.
|
||||
*.auto.tfvars
|
||||
*.auto.tfvars.json
|
||||
terraform.tfvars.local
|
||||
|
||||
# Claude Code session-local runtime state (not project content)
|
||||
.claude/scheduled_tasks.lock
|
||||
.claude/*.lock
|
||||
@@ -31,24 +31,32 @@ Additional Steps:
|
||||
|
||||
The number of `IaaS Router` VMs is controlled by the Terraform variable `router_count` (default `2`, tested with 3-4). Each router VM's private interfaces are driven by `private_network_cidrs` - a list of CIDR prefixes the admin must supply explicitly (no default, no auto-carving): **one entry = one shared private network = one private interface per router**, in addition to the single public (WAN) interface that stays fixed at 1. So a router VM ends up with `1 + length(private_network_cidrs)` network interfaces total.
|
||||
|
||||
Each entry in `private_network_cidrs` is one network shared by *all* routers - every router gets its own port/IP inside every listed network (`cidrhost(cidr, router_index + 2)`), similar to how the original `lan_net` design worked, just generalized to an arbitrary number of networks and routers. There is no VRRP between router VMs in this design, a change from the previous 2-NIC/VRRP model. The prefixes must not overlap each other and must each have room for at least `router_count + 2` addresses - both checked by `terraform/tests/`.
|
||||
Each entry in `private_network_cidrs` is one network shared by *all* routers - every router gets its own port inside every listed network, similar to how the repo's original single shared LAN network worked, just generalized to an arbitrary number of networks and routers. Each port's address is auto-assigned by Neutron's IPAM (no explicit `ip_address`) rather than hand-computed, since VKCS auto-creates its own service ports on each network (observed: a `network:dns` port) that can otherwise collide with a manually-picked address - IPAM guarantees no double-booking. There is no VRRP between router VMs in this design, a change from the previous 2-NIC/VRRP model. The prefixes must not overlap each other and should be sized `/28` (not `/29` - too tight once platform-reserved addresses are accounted for) - both checked by `terraform/tests/`.
|
||||
|
||||
This deployment provisions **only the router VMs** - the repo's earlier `priv_srv_01`/`priv_srv_02`/`priv_srv_03` demo instances (and the shared LAN network/security group that only they used) have been removed as out of scope.
|
||||
|
||||
New Terraform variables: `router_count`, `private_network_cidrs`, `router_availability_zones` (see `terraform/variables.tf`). The post-install script is a Terraform template (`terraform/scripts/network-init.sh.tpl`) rendered per-router via `templatefile()`, matching each private interface to its expected subnet deterministically instead of guessing - it already handles any interface count, no hardcoded assumption of 2. `terraform/versions.tf` now pins the provider source (`vk-cs/vkcs`, `~> 0.17`), which was previously undeclared.
|
||||
|
||||
**Provider authentication**
|
||||
|
||||
`terraform/versions.tf` now has a `provider "vkcs" { ... }` block wiring `auth_url`, `username`, `password`, `project_id`, `region`, `user_domain_name` (previously declared in `variables.tf` but never actually connected to anything). `auth_url`/`user_domain_name`/`region` have sane defaults for a regular account; `username`/`password`/`project_id` still have none, same as before.
|
||||
|
||||
`terraform.tfvars` stays a committed, anonymized template - real credentials (e.g. from an `openrc.sh` for a service account) go in a separate, gitignored `*.auto.tfvars` file instead, which Terraform loads automatically on top of `terraform.tfvars`. See `terraform/prod.auto.tfvars.example` for the field mapping from `openrc.sh`'s `OS_*` variables. Never put real credentials in `terraform.tfvars` itself.
|
||||
|
||||
**Default security group**
|
||||
|
||||
Every VK Cloud project auto-creates a `default` security group with a UUID unique to that project. Rather than hardcoding one project's UUID, it's resolved dynamically via `data.vkcs_networking_secgroup` (matched by `name = "default"`) and attached to every VM through `local.default_security_group_id`. `default_security_group_id` is a Terraform variable for the rare case that lookup doesn't fit a given project (non-standard name/SDN) - treat setting it explicitly (via `terraform.tfvars` or `TF_VAR_default_security_group_id`) as a last resort, not the normal path.
|
||||
|
||||
**Horizontal scaling via environment variables**
|
||||
|
||||
`router_count` has a default and isn't set in `terraform.tfvars`, so it scales purely through `TF_VAR_router_count` using Terraform's standard `TF_VAR_<name>` convention. `private_network_cidrs` has no default and must be set somewhere - either in `terraform.tfvars` (as shipped) or overridden via `TF_VAR_private_network_cidrs` as a JSON-encoded list:
|
||||
Terraform's variable precedence means a `terraform.tfvars` value always beats a `TF_VAR_<name>` environment variable, never the other way round - `TF_VAR_*` only takes effect for a variable `terraform.tfvars` leaves unset. This repo's `terraform.tfvars` currently pins real values for its actual deployment (`router_count = 3`, a specific `private_network_cidrs`), so `TF_VAR_router_count`/`TF_VAR_private_network_cidrs` have **no effect** while those stay set - to change scale, edit `terraform.tfvars` directly, or override on the command line with `-var`/`-var-file` (which does beat a tfvars file):
|
||||
|
||||
```bash
|
||||
export TF_VAR_router_count=4
|
||||
export TF_VAR_private_network_cidrs='["10.90.0.0/28","10.90.0.16/28","10.90.0.32/28"]'
|
||||
terraform apply
|
||||
terraform apply -var="router_count=4" -var='private_network_cidrs=["10.90.0.0/28","10.90.0.16/28","10.90.0.32/28"]'
|
||||
```
|
||||
|
||||
If you instead comment `router_count`/`private_network_cidrs` back out of `terraform.tfvars` (e.g. for a fresh, non-PROD deployment), `TF_VAR_router_count`/`TF_VAR_private_network_cidrs` (JSON-encoded list) start working again as described above.
|
||||
|
||||
**Local delivery integrity tests**
|
||||
|
||||
`terraform/tests/` contains a local pytest suite that checks the delivery is internally consistent - required files present, `terraform fmt` clean, HCL parses, `router_count`/`private_network_cidrs` actually drive the resource/NIC count instead of being hardcoded, the example CIDRs in `terraform.tfvars` don't overlap and have room for `router_count` routers, and the post-install script template renders to valid bash. It also runs:
|
||||
|
||||
+17
-8
@@ -10,31 +10,40 @@
|
||||
|
||||
## 2. Клонировать репозиторий и задать credentials
|
||||
|
||||
Отредактировать `terraform/terraform.tfvars`:
|
||||
`terraform/terraform.tfvars` — это закоммиченный обезличенный шаблон, реальные credentials в него вписывать **не нужно**. Вместо этого скопируйте `terraform/prod.auto.tfvars.example` в `terraform/prod.auto.tfvars` (этот файл в `.gitignore`, никогда не попадёт в git) и впишите туда реальные значения:
|
||||
|
||||
```
|
||||
username = "<ваш VK Cloud логин>"
|
||||
auth_url = "https://infra.mail.ru:35357/v3/"
|
||||
username = "<ваш VK Cloud логин или сервисный аккаунт>"
|
||||
password = "<пароль>"
|
||||
project_id = "<ваш project_id>"
|
||||
region = "RegionOne"
|
||||
user_domain_name = "users" # или "service-users" для сервисного аккаунта svc-*
|
||||
```
|
||||
|
||||
Terraform подхватывает `*.auto.tfvars` автоматически, в дополнение к `terraform.tfvars` — ничего больше настраивать не нужно. Если у вас есть `openrc.sh` для сервисного аккаунта — соответствие полей: `OS_AUTH_URL`→`auth_url`, `OS_USERNAME`→`username`, `OS_PASSWORD`→`password`, `OS_PROJECT_ID`→`project_id`, `OS_REGION_NAME`→`region`, `OS_USER_DOMAIN_NAME`→`user_domain_name`.
|
||||
|
||||
В самом `terraform.tfvars` дополнительно отредактируйте:
|
||||
|
||||
```
|
||||
ssh_key_name = "<имя загруженного SSH-ключа>"
|
||||
|
||||
private_network_cidrs = [
|
||||
"10.90.0.0/29",
|
||||
"10.90.0.8/29",
|
||||
"10.90.0.0/28",
|
||||
"10.90.0.16/28",
|
||||
]
|
||||
```
|
||||
|
||||
`private_network_cidrs` — обязательная переменная без значения по умолчанию: один CIDR-префикс на каждую приватную сеть, к которой будут подключены интерфейсы роутеров (один префикс = одна общая сеть = один приватный интерфейс на роутер). Автоматической нарезки нет — префиксы не должны пересекаться и должны вмещать минимум `router_count + 2` адреса.
|
||||
`private_network_cidrs` — обязательная переменная без значения по умолчанию: один CIDR-префикс на каждую приватную сеть, к которой будут подключены интерфейсы роутеров (один префикс = одна общая сеть = один приватный интерфейс на роутер). Автоматической нарезки нет — префиксы не должны пересекаться. Берите `/28`, а не `/29`: VKCS сам создаёт служебные порты на каждой сети (замечен `network:dns`), которые тоже расходуют адреса из пула — `/29` (5 адресов) на практике оказался слишком тесным.
|
||||
|
||||
UUID системной Security Group `default` (уникален для каждого проекта VK Cloud) вычисляется автоматически через `data.vkcs_networking_secgroup`, вручную задавать не нужно. Переменная `default_security_group_id` — override только на крайний случай (нестандартное имя/SDN группы в проекте), не для обычного использования.
|
||||
|
||||
## 3. (Опционально) масштабирование
|
||||
|
||||
По умолчанию: 2 роутера × 3 интерфейса (1 публичный + 2 приватных, по числу префиксов в `private_network_cidrs`). Меняется без правки кода — либо через `terraform.tfvars`, либо через переменные окружения:
|
||||
Число роутеров и приватных сетей меняется правкой `router_count`/`private_network_cidrs` прямо в `terraform.tfvars` (сейчас там уже реальные значения этого PROD-деплоя — 3 роутера). Значение из `terraform.tfvars` **всегда перекрывает** `TF_VAR_*` (у tfvars-файла более высокий приоритет), так что `TF_VAR_router_count` сработает, только если убрать `router_count` из `terraform.tfvars`. Разовый override без правки файла — через `-var` в командной строке (он перекрывает даже tfvars):
|
||||
|
||||
```bash
|
||||
export TF_VAR_router_count=4
|
||||
export TF_VAR_private_network_cidrs='["10.90.0.0/28","10.90.0.16/28","10.90.0.32/28"]'
|
||||
terraform apply -var="router_count=4" -var='private_network_cidrs=["10.90.0.0/28","10.90.0.16/28","10.90.0.32/28"]'
|
||||
```
|
||||
|
||||
## 4. Развернуть
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# План внедрения: подключение provider "vkcs" и безопасная передача PROD-credentials
|
||||
|
||||
Дата: 2026-09-06
|
||||
|
||||
## Проблема
|
||||
|
||||
При подготовке к PROD-развёртыванию (только Terraform, без Ansible — см. project-память) обнаружено: в `terraform/` вообще отсутствует блок `provider "vkcs" {}`. Переменные `username`/`password`/`project_id`/`region` были объявлены в `variables.tf`, но нигде не использовались (`grep` — 0 совпадений) — т.е. развёртывание не смогло бы аутентифицироваться независимо от способа передачи credentials.
|
||||
|
||||
Дополнительно: провайдер `vkcs` (в отличие от классического OpenStack-провайдера) не имеет задокументированной автоматической поддержки `OS_*` переменных окружения (проверено локально через `terraform providers schema -json` — только `access_token`/`OS_AUTH_TOKEN` упомянуты явно) — значит, `auth_url`, `username`, `password`, `project_id`, `region`, `user_domain_name` нужно явно передавать через сам provider-блок.
|
||||
|
||||
Пользователь передал `openrc.sh` для сервисного аккаунта (`OS_AUTH_URL`, `OS_PROJECT_ID`, `OS_USERNAME`/`OS_PASSWORD`, `OS_USER_DOMAIN_NAME=service-users`, `OS_REGION_NAME=RegionOne`) и попросил проверить схему "взять `terraform.tfvars`-шаблон за основу + на лету добавить нужные параметры".
|
||||
|
||||
## Решение
|
||||
|
||||
1. Добавить `provider "vkcs" {}` в `terraform/versions.tf`, подключив все 6 auth-переменных.
|
||||
2. Добавить недостающие переменные `auth_url`, `user_domain_name`; исправить некорректный дефолт `region` (было `"ME1"` — это код availability zone, а не OpenStack-регион; правильное значение — `"RegionOne"`, ранее это не проявлялось, т.к. переменная не использовалась).
|
||||
3. **Не** вписывать реальные credentials в закоммиченный `terraform.tfvars` — вместо этого задокументировать и использовать паттерн `*.auto.tfvars` (Terraform подхватывает такие файлы автоматически поверх `terraform.tfvars`, без изменения самого шаблона): добавить `*.auto.tfvars`/`*.auto.tfvars.json`/`terraform.tfvars.local` в `.gitignore`, создать `terraform/prod.auto.tfvars.example` (без секретов, только маппинг полей `openrc.sh` → переменные).
|
||||
4. Проверить всю цепочку реальными credentials из `openrc.sh`: во временной директории вне репозитория (не в git) собрать overlay-файл с реальными значениями, прогнать `terraform init` + `terraform plan` (read-only, ничего не создаётся) против настоящего VK Cloud API — убедиться, что аутентификация и резолвинг data source'ов (`vkcs_images_image`, `vkcs_networking_network.extnet`, `vkcs_networking_secgroup.default`) работают. Временную директорию с секретами удалить сразу после проверки.
|
||||
5. Дополнить `terraform/tests/test_terraform_delivery.py`: provider-блок реально подключает все auth-переменные (regression guard на "orphaned variables"); `terraform.tfvars` не содержит `auth_url`/`user_domain_name`; `*.auto.tfvars` в `.gitignore`; `prod.auto.tfvars.example` не игнорируется.
|
||||
6. Обновить `README.md`/`docs/QUICKSTART.md`.
|
||||
7. Summary-документ по завершении.
|
||||
|
||||
## Верификация
|
||||
|
||||
- `terraform fmt -check -recursive`.
|
||||
- Реальный `terraform init` + `terraform validate` (локальный provider-mirror) — синтаксическая/схемная корректность provider-блока.
|
||||
- Реальный `terraform init` + `terraform plan` с настоящими PROD credentials (во временной, не-репозиторной директории) — подтверждена успешная аутентификация и резолвинг data sources; временная директория с секретами удалена сразу после проверки, секреты нигде не закоммичены.
|
||||
- `venv/bin/pytest terraform/tests -v` — весь сьют зелёный.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Summary: подключение provider "vkcs" и безопасная передача PROD-credentials
|
||||
|
||||
Дата: 2026-09-06
|
||||
План: [2026-09-06-provider-auth-wiring-plan.md](2026-09-06-provider-auth-wiring-plan.md)
|
||||
|
||||
## Что сделано
|
||||
|
||||
### `terraform/versions.tf`
|
||||
Добавлен блок:
|
||||
```hcl
|
||||
provider "vkcs" {
|
||||
auth_url = var.auth_url
|
||||
username = var.username
|
||||
password = var.password
|
||||
project_id = var.project_id
|
||||
region = var.region
|
||||
user_domain_name = var.user_domain_name
|
||||
}
|
||||
```
|
||||
До этого `username`/`password`/`project_id`/`region` были orphaned-переменными — объявлены, но нигде не использовались.
|
||||
|
||||
### `terraform/variables.tf`
|
||||
- Добавлены `auth_url` (default `"https://infra.mail.ru:35357/v3/"`) и `user_domain_name` (default `"users"`, для сервисных аккаунтов — `"service-users"`).
|
||||
- Исправлен дефолт `region`: было `"ME1"` (код availability zone, спутан с регионом), стало `"RegionOne"` (реальный OpenStack-регион VK Cloud, подтверждено `openrc.sh` пользователя — `OS_REGION_NAME=RegionOne`). Раньше это не проявлялось, т.к. переменная не была подключена ни к чему.
|
||||
|
||||
### `.gitignore`
|
||||
Добавлены `*.auto.tfvars`, `*.auto.tfvars.json`, `terraform.tfvars.local` — реальные credentials передаются через автоматически подхватываемый Terraform overlay-файл, а не через правки закоммиченного `terraform.tfvars`.
|
||||
|
||||
### `terraform/prod.auto.tfvars.example` (новый файл, без секретов)
|
||||
Документирует паттерн и маппинг полей `openrc.sh` (`OS_AUTH_URL`, `OS_USERNAME`, `OS_PASSWORD`, `OS_PROJECT_ID`, `OS_REGION_NAME`, `OS_USER_DOMAIN_NAME`) → переменные Terraform.
|
||||
|
||||
### `terraform/tests/test_terraform_delivery.py`
|
||||
Добавлены тесты: `test_provider_vkcs_wires_all_auth_variables` (regression guard — provider-блок реально подключает все 6 auth-переменных), `test_auth_variable_has_a_default` (`auth_url`/`user_domain_name`/`region` — есть дефолт), `test_gitignore_excludes_auto_tfvars_overlay`, `test_prod_auto_tfvars_example_is_not_gitignored`, `test_tfvars_template_has_no_auth_credentials` (в закоммиченном `terraform.tfvars` не должно быть `auth_url`/`user_domain_name`). Итог: **48 passed** (было 38).
|
||||
|
||||
### `README.md` / `docs/QUICKSTART.md`
|
||||
Добавлен раздел "Provider authentication" / обновлён шаг 2 Quick Start — описан paттерн `*.auto.tfvars` и маппинг полей `openrc.sh`.
|
||||
|
||||
## Реальная проверка с PROD credentials
|
||||
|
||||
Пользователь передал `openrc.sh` сервисного аккаунта. Во временной директории **вне репозитория** (не в git, удалена сразу после проверки) собран overlay-файл с реальными значениями (`auth_url`, `username`, `password`, `project_id`, `region=RegionOne`, `user_domain_name=service-users`), поверх закоммиченного `terraform.tfvars`. Выполнены:
|
||||
|
||||
- `terraform init` — успех, провайдер и бэкенд инициализированы.
|
||||
- `terraform plan` (read-only, ничего не создано) — **успех**: `Plan: 24 to add, 0 to change, 0 to destroy`. Подтверждена реальная аутентификация к `https://infra.mail.ru:35357/v3/` и успешный резолвинг всех трёх data source'ов (`vkcs_images_image.ubuntu24`, `vkcs_networking_network.extnet` по `name="internet"`, `vkcs_networking_secgroup.default` по `name="default"`) — т.е. соответствующие объекты реально существуют в проекте с ожидаемыми именами.
|
||||
|
||||
Пароль и прочие секреты нигде не сохранялись в репозитории; временная директория удалена немедленно после проверки (подтверждено).
|
||||
|
||||
## Верификация
|
||||
|
||||
- `terraform fmt -check -recursive` → чисто.
|
||||
- Реальные `terraform init`/`validate` (локальный provider-mirror) → успех.
|
||||
- Реальный `terraform plan` с PROD credentials (вне репозитория) → успех, 24 ресурса к созданию, 0 ошибок.
|
||||
- `venv/bin/pytest terraform/tests -v` → **48 passed**.
|
||||
|
||||
## Следующий шаг
|
||||
|
||||
`terraform apply` для реального PROD-развёртывания (3 роутера) пользователем ещё не подтверждён — это отдельное, необратимое действие, требующее явного разрешения.
|
||||
@@ -0,0 +1,22 @@
|
||||
# План внедрения: деплой только роутеров (без приватных серверов)
|
||||
|
||||
Дата: 2026-09-07
|
||||
|
||||
## Проблема
|
||||
|
||||
Прошлая попытка реального PROD-развёртывания (см. `docs/changes/2026-09-06-*`) выявила несколько багов (invalid key_name, IP-коллизии на /29, `image_id` drift), все они исправлены. Всё созданное было удалено (`terraform destroy`, 25 ресурсов, подтверждено пустым state) по явному запросу пользователя.
|
||||
|
||||
Пользователь скорректировал требования к развёртыванию: нужны **только 3 роутера**, без демо-сегмента `priv_srv_01/02/03` (и без разделяемой LAN-сети/security group, которые использовались только ими). Каждый роутер — 3 порта (1 внешний + 2 приватных), каждый приватный порт в своей `/28`-подсети — это уже соответствует текущей конфигурации после прошлых фиксов.
|
||||
|
||||
## Решение
|
||||
|
||||
1. `terraform/main.tf`: удалить `vkcs_compute_instance.priv_srv_01/02/03`, а также ставшие неиспользуемыми `vkcs_networking_network.lan_net`, `vkcs_networking_subnet.lan_subnet`, `vkcs_networking_secgroup.private_sg` и его 3 правила (`from_rfc_net10/172/192_in`) — ничего из этого больше ничем не используется.
|
||||
2. `terraform/tests/test_terraform_delivery.py`: добавить `test_deployment_is_router_only` — regression guard, что единственный compute-instance ресурс — `router`, единственная security group — `router_sg`, нет `lan_net`.
|
||||
3. `README.md`: обновить описание (только роутеры; авто-назначение IP через Neutron IPAM вместо ручного вычисления; актуальный размер подсетей `/28`).
|
||||
4. Summary-документ по завершении.
|
||||
|
||||
## Верификация
|
||||
|
||||
- `terraform fmt -check -recursive`.
|
||||
- `venv/bin/pytest terraform/tests -v`.
|
||||
- Реальный `terraform plan` против пустого state (после `destroy`) — должен показать только ресурсы, относящиеся к 3 роутерам.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Summary: деплой только роутеров (без приватных серверов)
|
||||
|
||||
Дата: 2026-09-07
|
||||
План: [2026-09-07-router-only-deployment-plan.md](2026-09-07-router-only-deployment-plan.md)
|
||||
|
||||
## Что сделано
|
||||
|
||||
### `terraform/main.tf`
|
||||
Удалены: `vkcs_compute_instance.priv_srv_01/02/03`, `vkcs_networking_network.lan_net`, `vkcs_networking_subnet.lan_subnet`, `vkcs_networking_secgroup.private_sg` и его 3 правила (`from_rfc_net10/172/192_in`) — всё это использовалось исключительно приватными серверами и стало мёртвым кодом. Осталось: `router_sg` + 4 правила, `default` security group lookup, keypair, `router_priv_net/subnet/port` (по числу `private_network_cidrs`), единственный ресурс `vkcs_compute_instance.router` (`count = var.router_count`).
|
||||
|
||||
### `terraform/tests/test_terraform_delivery.py`
|
||||
Добавлен `test_deployment_is_router_only` — regression guard: единственный compute-instance — `router`, единственная security group — `router_sg`, нет `lan_net`. Итог сьюта: **56 passed** (было 55).
|
||||
|
||||
### `README.md`
|
||||
Обновлено описание: только роутеры (демо-сегмент приватных серверов исключён), явно указано, что адреса на портах теперь назначаются Neutron IPAM автоматически (не вычисляются вручную), актуализирован размер подсетей (`/28`).
|
||||
|
||||
## Верификация
|
||||
|
||||
- `terraform fmt -check -recursive` → чисто.
|
||||
- `venv/bin/pytest terraform/tests -v` → **56 passed**.
|
||||
- Реальный `terraform plan` против пустого state (после предыдущего `destroy`) → **19 to add, 0 to change, 0 to destroy**: 3 роутера, keypair, 2 приватные сети/подсети (`/28`), 6 портов, 1 security group + 4 правила. Ровно то, что запрошено — без единого лишнего ресурса.
|
||||
|
||||
## Следующий шаг
|
||||
|
||||
`terraform apply` по сохранённому плану (`terraform/prod.tfplan`) ещё не выполнен — ожидает подтверждения пользователя.
|
||||
+35
-136
@@ -28,25 +28,18 @@ locals {
|
||||
)
|
||||
}
|
||||
|
||||
# LAN Network
|
||||
resource "vkcs_networking_network" "lan_net" {
|
||||
name = "router-lan-net"
|
||||
sdn = "sprut"
|
||||
admin_state_up = true
|
||||
# Nova keypairs are per-user, not per-project - a keypair uploaded under a
|
||||
# different account is invisible to whichever account Terraform deploys as.
|
||||
# Register it here from var.ssh_public_key when supplied; otherwise assume
|
||||
# var.ssh_key_name already exists under the deploying account.
|
||||
resource "vkcs_compute_keypair" "router" {
|
||||
count = var.ssh_public_key != null ? 1 : 0
|
||||
name = var.ssh_key_name
|
||||
public_key = var.ssh_public_key
|
||||
}
|
||||
|
||||
resource "vkcs_networking_subnet" "lan_subnet" {
|
||||
network_id = vkcs_networking_network.lan_net.id
|
||||
name = "router-lan-subnet"
|
||||
cidr = "10.200.10.0/24"
|
||||
gateway_ip = "10.200.10.1"
|
||||
dns_nameservers = ["8.8.8.8", "1.1.1.1"]
|
||||
sdn = "sprut"
|
||||
|
||||
allocation_pool {
|
||||
start = "10.200.10.100"
|
||||
end = "10.200.10.200"
|
||||
}
|
||||
locals {
|
||||
ssh_key_name = var.ssh_public_key != null ? vkcs_compute_keypair.router[0].name : var.ssh_key_name
|
||||
}
|
||||
|
||||
# Security Groups
|
||||
@@ -55,33 +48,6 @@ resource "vkcs_networking_secgroup" "router_sg" {
|
||||
sdn = "sprut"
|
||||
}
|
||||
|
||||
resource "vkcs_networking_secgroup" "private_sg" {
|
||||
name = "private-sg"
|
||||
sdn = "sprut"
|
||||
}
|
||||
|
||||
# Private SG rules
|
||||
resource "vkcs_networking_secgroup_rule" "from_rfc_net192_in" {
|
||||
direction = "ingress"
|
||||
remote_ip_prefix = "192.168.0.0/16"
|
||||
security_group_id = vkcs_networking_secgroup.private_sg.id
|
||||
sdn = "sprut"
|
||||
}
|
||||
|
||||
resource "vkcs_networking_secgroup_rule" "from_rfc_net172_in" {
|
||||
direction = "ingress"
|
||||
remote_ip_prefix = "172.16.0.0/12"
|
||||
security_group_id = vkcs_networking_secgroup.private_sg.id
|
||||
sdn = "sprut"
|
||||
}
|
||||
|
||||
resource "vkcs_networking_secgroup_rule" "from_rfc_net10_in" {
|
||||
direction = "ingress"
|
||||
remote_ip_prefix = "10.0.0.0/8"
|
||||
security_group_id = vkcs_networking_secgroup.private_sg.id
|
||||
sdn = "sprut"
|
||||
}
|
||||
|
||||
# Router SG rules
|
||||
resource "vkcs_networking_secgroup_rule" "router_ssh" {
|
||||
direction = "ingress"
|
||||
@@ -122,8 +88,8 @@ resource "vkcs_networking_secgroup_rule" "router_ipsec_nat_t" {
|
||||
}
|
||||
|
||||
# Per-router private interfaces: one shared private network per admin-supplied
|
||||
# CIDR in private_network_cidrs (no shared LAN with the priv_srv_* segment,
|
||||
# no VRRP), each router getting its own port/IP inside every such network.
|
||||
# CIDR in private_network_cidrs (no VRRP), each router getting its own
|
||||
# port/IP inside every such network.
|
||||
locals {
|
||||
private_roles = [for idx in range(length(var.private_network_cidrs)) : "priv${idx + 1}"]
|
||||
|
||||
@@ -147,36 +113,50 @@ resource "vkcs_networking_subnet" "router_priv_subnet" {
|
||||
cidr = each.value
|
||||
gateway_ip = cidrhost(each.value, 1)
|
||||
sdn = "sprut"
|
||||
|
||||
# No in-guest DHCP client is needed for these interfaces: with
|
||||
# config_drive = true (set on every router instance), cloud-init learns
|
||||
# each interface's assigned address from the config-drive metadata and
|
||||
# renders it directly, then network-init.sh.tpl re-pins it deterministically
|
||||
# to ethN by MAC - no actual DHCP protocol exchange with this subnet ever
|
||||
# happens. (Note: this does NOT avoid IP collisions with VKCS's own
|
||||
# auto-created service ports on the network, e.g. a "network:dns" port -
|
||||
# see router_priv_port below, which leaves ip_address unset instead.)
|
||||
enable_dhcp = false
|
||||
}
|
||||
|
||||
resource "vkcs_networking_port" "router_priv_port" {
|
||||
for_each = {
|
||||
for pair in setproduct(range(var.router_count), local.private_roles) :
|
||||
"router${pair[0] + 1}-${pair[1]}" => {
|
||||
router_index = pair[0]
|
||||
role = pair[1]
|
||||
}
|
||||
"router${pair[0] + 1}-${pair[1]}" => pair[1]
|
||||
}
|
||||
name = "router-${each.key}-port"
|
||||
network_id = vkcs_networking_network.router_priv_net[each.value.role].id
|
||||
network_id = vkcs_networking_network.router_priv_net[each.value].id
|
||||
admin_state_up = true
|
||||
port_security_enabled = false
|
||||
full_security_groups_control = true
|
||||
security_group_ids = []
|
||||
sdn = "sprut"
|
||||
|
||||
# No explicit ip_address: let Neutron's IPAM auto-assign one from the
|
||||
# subnet's pool. VKCS auto-creates its own service ports on this network
|
||||
# (observed: a "network:dns" port) that can silently consume whichever
|
||||
# low address a hand-computed offset would have picked, causing
|
||||
# IpAddressAlreadyAllocated - IPAM guarantees no double-booking, our own
|
||||
# arithmetic doesn't. network-init.sh.tpl matches interfaces by which
|
||||
# declared CIDR their live IP falls into, not by an exact expected IP, so
|
||||
# the assigned address doesn't need to be known in advance.
|
||||
fixed_ip {
|
||||
subnet_id = vkcs_networking_subnet.router_priv_subnet[each.value.role].id
|
||||
ip_address = cidrhost(local.private_network_cidr[each.value.role], each.value.router_index + 2)
|
||||
subnet_id = vkcs_networking_subnet.router_priv_subnet[each.value].id
|
||||
}
|
||||
}
|
||||
|
||||
resource "vkcs_compute_instance" "router" {
|
||||
count = var.router_count
|
||||
name = "router${count.index + 1}"
|
||||
image_id = data.vkcs_images_image.ubuntu24.id
|
||||
flavor_name = "STD3-4-4"
|
||||
availability_zone = element(var.router_availability_zones, count.index % length(var.router_availability_zones))
|
||||
key_pair = var.ssh_key_name
|
||||
key_pair = local.ssh_key_name
|
||||
|
||||
security_group_ids = [
|
||||
vkcs_networking_secgroup.router_sg.id,
|
||||
@@ -220,84 +200,3 @@ resource "vkcs_compute_instance" "router" {
|
||||
}
|
||||
}
|
||||
|
||||
resource "vkcs_compute_instance" "priv_srv_01" {
|
||||
name = "Priv-SRV-01"
|
||||
image_id = data.vkcs_images_image.ubuntu24.id
|
||||
flavor_name = "STD3-4-4"
|
||||
availability_zone = "ME1"
|
||||
key_pair = var.ssh_key_name
|
||||
|
||||
security_group_ids = [
|
||||
vkcs_networking_secgroup.private_sg.id,
|
||||
local.default_security_group_id
|
||||
]
|
||||
|
||||
network {
|
||||
uuid = vkcs_networking_network.lan_net.id
|
||||
}
|
||||
|
||||
block_device {
|
||||
uuid = data.vkcs_images_image.ubuntu24.id
|
||||
source_type = "image"
|
||||
volume_size = 20
|
||||
boot_index = 0
|
||||
destination_type = "volume"
|
||||
volume_type = "ceph-ssd"
|
||||
delete_on_termination = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
resource "vkcs_compute_instance" "priv_srv_02" {
|
||||
name = "Priv-SRV-02"
|
||||
image_id = data.vkcs_images_image.ubuntu24.id
|
||||
flavor_name = "STD3-4-4"
|
||||
availability_zone = "ME1"
|
||||
key_pair = var.ssh_key_name
|
||||
|
||||
security_group_ids = [
|
||||
vkcs_networking_secgroup.private_sg.id,
|
||||
local.default_security_group_id
|
||||
]
|
||||
|
||||
network {
|
||||
uuid = vkcs_networking_network.lan_net.id
|
||||
}
|
||||
|
||||
block_device {
|
||||
uuid = data.vkcs_images_image.ubuntu24.id
|
||||
source_type = "image"
|
||||
volume_size = 20
|
||||
boot_index = 0
|
||||
destination_type = "volume"
|
||||
volume_type = "ceph-ssd"
|
||||
delete_on_termination = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "vkcs_compute_instance" "priv_srv_03" {
|
||||
name = "Priv-SRV-03"
|
||||
image_id = data.vkcs_images_image.ubuntu24.id
|
||||
flavor_name = "STD3-4-4"
|
||||
availability_zone = "MS1"
|
||||
key_pair = var.ssh_key_name
|
||||
|
||||
security_group_ids = [
|
||||
vkcs_networking_secgroup.private_sg.id,
|
||||
local.default_security_group_id
|
||||
]
|
||||
|
||||
network {
|
||||
uuid = vkcs_networking_network.lan_net.id
|
||||
}
|
||||
|
||||
block_device {
|
||||
uuid = data.vkcs_images_image.ubuntu24.id
|
||||
source_type = "image"
|
||||
volume_size = 20
|
||||
boot_index = 0
|
||||
destination_type = "volume"
|
||||
volume_type = "ceph-ssd"
|
||||
delete_on_termination = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
# Example of the *.auto.tfvars overlay pattern for real credentials.
|
||||
#
|
||||
# terraform.tfvars stays a committed, anonymized template. To deploy with
|
||||
# real credentials (e.g. from an openrc.sh for a service account), copy this
|
||||
# file to prod.auto.tfvars (gitignored - see .gitignore) and fill in real
|
||||
# values. Terraform auto-loads *.auto.tfvars in addition to terraform.tfvars,
|
||||
# so this layers on top of the template without ever modifying/committing it.
|
||||
#
|
||||
# Mapping from an OpenStack-style openrc.sh:
|
||||
# OS_AUTH_URL -> auth_url
|
||||
# OS_USERNAME -> username
|
||||
# OS_PASSWORD -> password
|
||||
# OS_PROJECT_ID -> project_id
|
||||
# OS_REGION_NAME -> region
|
||||
# OS_USER_DOMAIN_NAME -> user_domain_name
|
||||
|
||||
auth_url = "https://infra.mail.ru:35357/v3/"
|
||||
username = "svc-<project_id>-svc-deployer"
|
||||
password = "<real password - never commit this>"
|
||||
project_id = "<project_id>"
|
||||
region = "RegionOne"
|
||||
user_domain_name = "service-users"
|
||||
|
||||
# Optional: Nova keypairs are per-user, not per-project - a keypair uploaded
|
||||
# under a different account (e.g. your personal VK Cloud login) is invisible
|
||||
# to a service account. Set this to have Terraform register var.ssh_key_name
|
||||
# under the deploying account from this public key. Leave unset if a keypair
|
||||
# with that name already exists under the deploying account.
|
||||
ssh_public_key = "ssh-ed25519 AAAA... or ssh-rsa AAAA... your-public-key-content"
|
||||
@@ -1,15 +1,18 @@
|
||||
username = "user@domain.local"
|
||||
password = "DemoUserPassw"
|
||||
project_id = "XXXXXd424998422XXXXXf13ac9XXXXX"
|
||||
ssh_key_name = "AdminSSH"
|
||||
ssh_key_name = "mcs_ru"
|
||||
|
||||
# Adaptive router VM count (has a default - see variables.tf - uncomment to override).
|
||||
# router_count = 3
|
||||
router_count = 3
|
||||
# router_availability_zones = ["ME1"]
|
||||
|
||||
# One CIDR per private network that router VMs get an interface into.
|
||||
# No default - must be supplied explicitly. List order determines eth1..ethN.
|
||||
# /28 (not /29): VKCS auto-creates its own service ports on each network
|
||||
# (observed: a "network:dns" port) that consume addresses from the pool
|
||||
# too, and a /29 (5 usable) proved too tight in practice.
|
||||
private_network_cidrs = [
|
||||
"10.90.0.0/29",
|
||||
"10.90.0.8/29",
|
||||
"10.90.0.0/28",
|
||||
"10.90.0.16/28",
|
||||
]
|
||||
@@ -97,7 +97,9 @@ def find_data_sources(doc, dtype):
|
||||
REQUIRED_FILES = [
|
||||
"main.tf",
|
||||
"variables.tf",
|
||||
"versions.tf",
|
||||
"terraform.tfvars",
|
||||
"prod.auto.tfvars.example",
|
||||
"scripts/network-init.sh.tpl",
|
||||
]
|
||||
|
||||
@@ -286,7 +288,7 @@ def test_default_security_group_id_override_accepted(override, should_pass):
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("relpath", ["main.tf", "variables.tf", "images.tf"])
|
||||
@pytest.mark.parametrize("relpath", ["main.tf", "variables.tf", "versions.tf", "images.tf"])
|
||||
def test_hcl_file_parses(relpath):
|
||||
# images.tf is intentionally fully commented out (disabled helper data
|
||||
# source) - it must still parse cleanly, just possibly to an empty doc.
|
||||
@@ -294,6 +296,86 @@ def test_hcl_file_parses(relpath):
|
||||
assert isinstance(doc, dict)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# versions.tf: the provider is actually configured (auth variables used to
|
||||
# be declared in variables.tf but never wired to anything - regression
|
||||
# guard against that gap reappearing)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_provider_vkcs_wires_all_auth_variables():
|
||||
versions = load_tf("versions.tf")
|
||||
provider_blocks = versions.get("provider", [])
|
||||
vkcs_provider = None
|
||||
for block in provider_blocks:
|
||||
if "vkcs" in block:
|
||||
vkcs_provider = block["vkcs"]
|
||||
assert vkcs_provider is not None, "expected a provider \"vkcs\" block in versions.tf"
|
||||
|
||||
expected = {
|
||||
"auth_url": "${var.auth_url}",
|
||||
"username": "${var.username}",
|
||||
"password": "${var.password}",
|
||||
"project_id": "${var.project_id}",
|
||||
"region": "${var.region}",
|
||||
"user_domain_name": "${var.user_domain_name}",
|
||||
}
|
||||
for attr, expr in expected.items():
|
||||
assert vkcs_provider.get(attr) == [expr], (
|
||||
f"provider \"vkcs\" must set {attr} = var.{attr} - "
|
||||
f"auth variables must not be left orphaned/unwired"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["auth_url", "user_domain_name", "region"])
|
||||
def test_auth_variable_has_a_default(name):
|
||||
v = find_variable(load_tf("variables.tf"), name)
|
||||
assert v is not None, f"variable {name} is missing"
|
||||
assert "default" in v, f"{name} should have a sane default so it doesn't have to be set explicitly for the common case"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Real secrets must never land in the git-tracked terraform.tfvars template -
|
||||
# they belong in a gitignored *.auto.tfvars overlay instead (see
|
||||
# prod.auto.tfvars.example)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_gitignore_excludes_auto_tfvars_overlay():
|
||||
gitignore_text = (REPO_ROOT / ".gitignore").read_text()
|
||||
assert "*.auto.tfvars" in gitignore_text, (
|
||||
"*.auto.tfvars must be gitignored - real credentials are meant to be "
|
||||
"layered on top of terraform.tfvars via such a file, never committed"
|
||||
)
|
||||
|
||||
|
||||
def test_prod_auto_tfvars_example_is_not_gitignored():
|
||||
"""The *.example file documents the overlay pattern and must ship in the
|
||||
repo (unlike the real *.auto.tfvars it documents)."""
|
||||
result = subprocess.run(
|
||||
["git", "check-ignore", "terraform/prod.auto.tfvars.example"],
|
||||
cwd=REPO_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert result.returncode != 0, "prod.auto.tfvars.example must NOT be gitignored"
|
||||
|
||||
|
||||
def test_tfvars_template_has_no_auth_credentials():
|
||||
"""terraform.tfvars is a committed, anonymized template - auth_url/
|
||||
user_domain_name/real credentials belong in a gitignored *.auto.tfvars
|
||||
overlay, not here."""
|
||||
tfvars_text = (TERRAFORM_DIR / "terraform.tfvars").read_text()
|
||||
active_lines = [
|
||||
line for line in tfvars_text.splitlines() if not line.strip().startswith("#")
|
||||
]
|
||||
for forbidden in ("auth_url", "user_domain_name"):
|
||||
assert not any(re.match(rf"^\s*{forbidden}\s*=", line) for line in active_lines), (
|
||||
f"{forbidden} should not be set in the committed terraform.tfvars "
|
||||
f"template - use a gitignored *.auto.tfvars overlay instead"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# variables.tf: the scaling knobs exist as expected
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -319,17 +401,21 @@ def test_private_network_cidrs_variable():
|
||||
)
|
||||
|
||||
|
||||
def test_router_count_not_pinned_in_tfvars():
|
||||
"""TF_VAR_router_count only takes effect if terraform.tfvars doesn't set
|
||||
an active (non-comment) value for it."""
|
||||
def test_router_count_pinned_in_tfvars_is_a_valid_number():
|
||||
"""terraform.tfvars now pins a real router_count for this project's
|
||||
actual PROD deployment (a deliberate choice, confirmed with the user) -
|
||||
a tfvars-file value always beats a TF_VAR_ environment variable in
|
||||
Terraform's precedence order, so TF_VAR_router_count no longer has any
|
||||
effect while this stays set. Just sanity-check it's a positive integer,
|
||||
not that it's absent."""
|
||||
tfvars_text = (TERRAFORM_DIR / "terraform.tfvars").read_text()
|
||||
active_lines = [
|
||||
line for line in tfvars_text.splitlines() if not line.strip().startswith("#")
|
||||
]
|
||||
assert not any(re.match(r"^\s*router_count\s*=", line) for line in active_lines), (
|
||||
"router_count must not be set in terraform.tfvars, or the "
|
||||
"TF_VAR_router_count environment variable would be shadowed"
|
||||
)
|
||||
matches = [line for line in active_lines if re.match(r"^\s*router_count\s*=", line)]
|
||||
assert matches, "expected router_count to be pinned in terraform.tfvars for this deployment"
|
||||
value = int(matches[0].split("=", 1)[1].strip())
|
||||
assert value >= 1
|
||||
|
||||
|
||||
def test_private_network_cidrs_is_set_in_tfvars():
|
||||
@@ -357,6 +443,24 @@ def test_router_resource_uses_count_variable():
|
||||
assert instances["router"]["count"] == ["${var.router_count}"]
|
||||
|
||||
|
||||
def test_compute_instances_do_not_set_top_level_image_id():
|
||||
"""Regression guard: the provider docs say 'Do not specify [image_id]
|
||||
if booting from a volume' - doing so anyway caused every already-created
|
||||
instance to be flagged for destroy+recreate on the next plan, because
|
||||
Nova reports back a sentinel string ("Attempt to boot from volume - no
|
||||
image supplied") instead of echoing the image UUID for a volume-booted
|
||||
server, which Terraform then sees as configuration drift on a
|
||||
ForceNew attribute. The image only belongs inside block_device."""
|
||||
main = load_tf("main.tf")
|
||||
for name, attrs in find_resources(main, "vkcs_compute_instance"):
|
||||
assert "image_id" not in attrs, (
|
||||
f"vkcs_compute_instance.{name} must not set top-level image_id "
|
||||
f"when booting from a volume via block_device"
|
||||
)
|
||||
assert attrs["block_device"][0]["source_type"] == ["image"]
|
||||
assert "uuid" in attrs["block_device"][0]
|
||||
|
||||
|
||||
def test_no_legacy_hardcoded_router_resources():
|
||||
main = load_tf("main.tf")
|
||||
instance_names = {name for name, _ in find_resources(main, "vkcs_compute_instance")}
|
||||
@@ -371,6 +475,22 @@ def test_no_legacy_hardcoded_router_resources():
|
||||
)
|
||||
|
||||
|
||||
def test_deployment_is_router_only():
|
||||
"""This deployment provisions only the router VMs - the demo's
|
||||
priv_srv_01/02/03 instances and the shared LAN network/security group
|
||||
that only they used were removed as out of scope (confirmed with the
|
||||
user)."""
|
||||
main = load_tf("main.tf")
|
||||
instance_names = {name for name, _ in find_resources(main, "vkcs_compute_instance")}
|
||||
assert instance_names == {"router"}, f"expected only the router instance, found {instance_names}"
|
||||
|
||||
network_names = {name for name, _ in find_resources(main, "vkcs_networking_network")}
|
||||
assert "lan_net" not in network_names
|
||||
|
||||
secgroup_names = {name for name, _ in find_resources(main, "vkcs_networking_secgroup")}
|
||||
assert secgroup_names == {"router_sg"}, f"expected only router_sg, found {secgroup_names}"
|
||||
|
||||
|
||||
def test_private_roles_local_driven_by_variable():
|
||||
main = load_tf("main.tf")
|
||||
private_roles = find_local(main, "private_roles")
|
||||
@@ -437,12 +557,109 @@ def test_all_instances_use_default_security_group_local():
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# main.tf: SSH keypair - Nova keypairs are per-user, not per-project, so a
|
||||
# keypair uploaded under a different account is invisible to the deploying
|
||||
# one. var.ssh_public_key lets Terraform register it itself.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_ssh_public_key_variable_defaults_to_null():
|
||||
v = find_variable(load_tf("variables.tf"), "ssh_public_key")
|
||||
assert v is not None, "variable ssh_public_key is missing"
|
||||
assert v["default"] == [None], "ssh_public_key should default to null (register nothing unless supplied)"
|
||||
|
||||
|
||||
def test_keypair_resource_conditionally_registers_public_key():
|
||||
main = load_tf("main.tf")
|
||||
keypairs = dict(find_resources(main, "vkcs_compute_keypair"))
|
||||
assert "router" in keypairs, "expected a vkcs_compute_keypair.router resource"
|
||||
kp = keypairs["router"]
|
||||
assert kp["count"] == ["${var.ssh_public_key != None ? 1 : 0}"], (
|
||||
"the keypair should only be created when ssh_public_key is actually supplied"
|
||||
)
|
||||
assert kp["name"] == ["${var.ssh_key_name}"]
|
||||
assert kp["public_key"] == ["${var.ssh_public_key}"]
|
||||
|
||||
|
||||
def test_ssh_key_name_local_prefers_registered_keypair():
|
||||
main = load_tf("main.tf")
|
||||
value = find_local(main, "ssh_key_name")
|
||||
assert value == [
|
||||
"${var.ssh_public_key != None ? vkcs_compute_keypair.router[0].name : var.ssh_key_name}"
|
||||
], (
|
||||
"locals.ssh_key_name must use the keypair Terraform registers itself "
|
||||
"when ssh_public_key is supplied, falling back to var.ssh_key_name "
|
||||
"(an already-existing keypair) otherwise"
|
||||
)
|
||||
|
||||
|
||||
def test_all_instances_use_ssh_key_name_local():
|
||||
main = load_tf("main.tf")
|
||||
for name, attrs in find_resources(main, "vkcs_compute_instance"):
|
||||
assert attrs["key_pair"] == ["${local.ssh_key_name}"], (
|
||||
f"vkcs_compute_instance.{name} must reference local.ssh_key_name, "
|
||||
f"not var.ssh_key_name directly, so it depends on the keypair "
|
||||
f"resource when one is registered"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# main.tf: router private subnets don't need Neutron's DHCP service - with
|
||||
# config_drive = true, cloud-init learns each interface's address from
|
||||
# config-drive metadata rather than an actual DHCP exchange, and
|
||||
# network-init.sh.tpl re-pins it deterministically to ethN afterwards
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_router_priv_subnet_has_dhcp_disabled():
|
||||
main = load_tf("main.tf")
|
||||
subnets = dict(find_resources(main, "vkcs_networking_subnet"))
|
||||
assert "router_priv_subnet" in subnets
|
||||
assert subnets["router_priv_subnet"]["enable_dhcp"] == [False], (
|
||||
"router_priv_subnet must have enable_dhcp = false - no in-guest "
|
||||
"DHCP client is ever used on these interfaces"
|
||||
)
|
||||
|
||||
|
||||
def test_router_priv_port_leaves_ip_address_unset():
|
||||
"""Regression guard: a hand-computed fixed_ip.ip_address collided with
|
||||
VKCS's own auto-created service ports on the network (observed: a
|
||||
"network:dns" port silently consuming an address) during a real PROD
|
||||
deployment. Leaving ip_address unset lets Neutron's IPAM auto-assign
|
||||
one, which is guaranteed collision-free; network-init.sh.tpl matches
|
||||
interfaces by which declared CIDR their live IP falls into, not by an
|
||||
exact expected IP, so this doesn't need to be known in advance."""
|
||||
main = load_tf("main.tf")
|
||||
ports = dict(find_resources(main, "vkcs_networking_port"))
|
||||
assert "router_priv_port" in ports
|
||||
fixed_ip = ports["router_priv_port"]["fixed_ip"][0]
|
||||
assert "ip_address" not in fixed_ip, (
|
||||
"router_priv_port.fixed_ip must not set ip_address - let Neutron's "
|
||||
"IPAM auto-assign to avoid colliding with platform-reserved ports"
|
||||
)
|
||||
assert "subnet_id" in fixed_ip
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# terraform.tfvars example CIDRs: sanity-check the values actually shipped
|
||||
# (no auto-carving anymore - these come straight from the admin/example)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _configured_router_count():
|
||||
"""The router_count actually in effect: whatever terraform.tfvars pins,
|
||||
else the variable's default (a tfvars value always beats the default)."""
|
||||
tfvars_text = (TERRAFORM_DIR / "terraform.tfvars").read_text()
|
||||
for line in tfvars_text.splitlines():
|
||||
if line.strip().startswith("#"):
|
||||
continue
|
||||
m = re.match(r"^\s*router_count\s*=\s*(\d+)", line)
|
||||
if m:
|
||||
return int(m.group(1))
|
||||
return find_variable(load_tf("variables.tf"), "router_count")["default"][0]
|
||||
|
||||
|
||||
def test_tfvars_private_network_cidrs_do_not_overlap_and_have_room_for_routers():
|
||||
tfvars = load_tf("terraform.tfvars")
|
||||
raw_cidrs = tfvars["private_network_cidrs"][0]
|
||||
@@ -453,13 +670,17 @@ def test_tfvars_private_network_cidrs_do_not_overlap_and_have_room_for_routers()
|
||||
for b in networks[i + 1 :]:
|
||||
assert not a.overlaps(b), f"{a} overlaps {b} in terraform.tfvars"
|
||||
|
||||
router_count_default = find_variable(load_tf("variables.tf"), "router_count")["default"][0]
|
||||
router_count = _configured_router_count()
|
||||
for net in networks:
|
||||
# offset scheme: cidrhost(cidr, router_index + 2) for router_index in
|
||||
# 0..router_count-1, so we need at least router_count + 2 addresses.
|
||||
assert net.num_addresses >= router_count_default + 2, (
|
||||
f"{net} has too few addresses for {router_count_default} routers "
|
||||
f"(offset scheme needs router_count + 2)"
|
||||
# ip_address is left unset on each port (Neutron IPAM auto-assigns -
|
||||
# see router_priv_port in main.tf), so there's no fixed per-router
|
||||
# offset to reserve room for - but VKCS auto-creates its own service
|
||||
# ports on the network (observed: one "network:dns" port consuming
|
||||
# an address), so there must be room for router_count routers plus
|
||||
# at least one such reservation, on top of network/gateway/broadcast.
|
||||
assert net.num_addresses >= router_count + 3, (
|
||||
f"{net} has too few addresses for {router_count} routers plus "
|
||||
f"platform-reserved ports (e.g. VKCS's network:dns service port)"
|
||||
)
|
||||
|
||||
|
||||
|
||||
+20
-2
@@ -15,9 +15,21 @@ variable "project_id" {
|
||||
}
|
||||
|
||||
variable "region" {
|
||||
description = "Region"
|
||||
description = "OpenStack region (VK Cloud typically has a single region, \"RegionOne\" - not to be confused with availability zones like \"ME1\"/\"MS1\")"
|
||||
type = string
|
||||
default = "ME1"
|
||||
default = "RegionOne"
|
||||
}
|
||||
|
||||
variable "auth_url" {
|
||||
description = "VK Cloud Identity (Keystone) auth URL"
|
||||
type = string
|
||||
default = "https://infra.mail.ru:35357/v3/"
|
||||
}
|
||||
|
||||
variable "user_domain_name" {
|
||||
description = "Keystone domain the auth user resides in ('users' for regular accounts, 'service-users' for svc-* service accounts)"
|
||||
type = string
|
||||
default = "users"
|
||||
}
|
||||
|
||||
variable "ssh_key_name" {
|
||||
@@ -25,6 +37,12 @@ variable "ssh_key_name" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "ssh_public_key" {
|
||||
description = "OpenSSH public key content to register as the ssh_key_name keypair under the deploying account (Nova keypairs are per-user, not per-project - a key uploaded under a different account is invisible here). Leave null if a keypair with that name already exists under the deploying account."
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "router_count" {
|
||||
description = "Number of IaaS Router VMs to provision"
|
||||
type = number
|
||||
|
||||
@@ -6,3 +6,12 @@ terraform {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "vkcs" {
|
||||
auth_url = var.auth_url
|
||||
username = var.username
|
||||
password = var.password
|
||||
project_id = var.project_id
|
||||
region = var.region
|
||||
user_domain_name = var.user_domain_name
|
||||
}
|
||||
Reference in new issue
Block a user