1
0

Compare commits

..

4 Commits

22 changed files with 304 additions and 399 deletions

View File

@ -9,6 +9,9 @@ indent_size = 4
[*.yml] [*.yml]
indent_size = 2 indent_size = 2
[*.yml.j2]
indent_size = 2
[Vagrantfile] [Vagrantfile]
indent_size = 2 indent_size = 2

View File

@ -3,12 +3,11 @@
Настройки виртуального сервера для домашних проектов. Настройки виртуального сервера для домашних проектов.
> В этом проекте не самые оптимальные решения. > В этом проекте не самые оптимальные решения.
> Но они помогают мне поддерживать сервер для моих личных проектов уже семь лет. > Но они помогают мне поддерживать сервер для моих личных проектов уже много лет.
## Требования ## Требования
- [ansible](https://docs.ansible.com/ansible/latest/getting_started/index.html) - [ansible](https://docs.ansible.com/ansible/latest/getting_started/index.html)
- [invoke](https://www.pyinvoke.org/)
- [task](https://taskfile.dev/) - [task](https://taskfile.dev/)
- [yq](https://github.com/mikefarah/yq) - [yq](https://github.com/mikefarah/yq)
@ -21,7 +20,7 @@ $ ansible-galaxy install --role-file requirements.yml
## Структура ## Структура
- Для каждого приложения создается свой пользователь. - Для каждого приложения создается свой пользователь (опционально).
- Для доступа используется ssh-ключ. - Для доступа используется ssh-ключ.
- Докер используется для запуска и изоляции приложений. Для загрузки образов настраивается Yandex Docker Registry. - Докер используется для запуска и изоляции приложений. Для загрузки образов настраивается Yandex Docker Registry.
- Выход во внешнюю сеть через proxy server [Caddy](https://caddyserver.com/). - Выход во внешнюю сеть через proxy server [Caddy](https://caddyserver.com/).
@ -32,30 +31,10 @@ $ ansible-galaxy install --role-file requirements.yml
В организации Яндекс: https://admin.yandex.ru/domains/vakhrushev.me?action=set_dns&uid=46045840 В организации Яндекс: https://admin.yandex.ru/domains/vakhrushev.me?action=set_dns&uid=46045840
## Частые команды
Конфигурация приложений (если нужно добавить новое приложение):
```bash
$ task configure-apps
```
Конфигурация мониторинга (если нужно обновить netdata):
```bash
$ task configure-monitoring
```
## Деплой приложений ## Деплой приложений
Доступные для деплоя приложения: Деплой всех приложений через ansible:
```bash ```bash
invoke --list ansible-playbook -i production.yml --diff playbook-gitea.yml
```
Выполнить команду деплоя, например:
```bash
invoke deploy:gitea
``` ```

28
Vagrantfile vendored
View File

@ -1,28 +0,0 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Этот файл предназначен для запуска тестовой виртуальной машины,
# на которой можно обкатать роли для настройки сервера.
ENV["LC_ALL"] = "en_US.UTF-8"
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/bionic64"
config.vm.provider "virtualbox" do |v|
v.memory = 2048
v.cpus = 2
end
config.vm.network "private_network", ip: "192.168.50.10"
# Приватный ключ для доступа к машине
config.vm.provision "shell" do |s|
ssh_pub_key = File.readlines("#{Dir.home}/.ssh/id_rsa.pub").first.strip
s.inline = <<-SHELL
echo #{ssh_pub_key} >> /home/vagrant/.ssh/authorized_keys
echo #{ssh_pub_key} >> /root/.ssh/authorized_keys
SHELL
end
end

View File

@ -1,5 +0,0 @@
WEB_SERVER_PORT=9595
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=password
USER_UID=1000
USER_GID=1000

View File

@ -1 +0,0 @@
data/

View File

@ -1,16 +0,0 @@
# Images: https://quay.io/repository/keycloak/keycloak?tab=tags&tag=latest
# Configuration: https://www.keycloak.org/server/all-config
services:
keycloak:
image: quay.io/keycloak/keycloak:24.0.4
command: ["start-dev"]
restart: unless-stopped
environment:
KEYCLOAK_ADMIN: "${KEYCLOAK_ADMIN}"
KEYCLOAK_ADMIN_PASSWORD: "${KEYCLOAK_ADMIN_PASSWORD}"
ports:
- "${WEB_SERVER_PORT}:8080"
volumes:
- "./data:/opt/keycloak/data"

View File

@ -1,60 +0,0 @@
services:
outline-app:
image: outlinewiki/outline:0.81.1
restart: unless-stopped
ports:
- "${WEB_SERVER_PORT}:3000"
depends_on:
- postgres
- redis
environment:
NODE_ENV: '${NODE_ENV}'
SECRET_KEY: '${SECRET_KEY}'
UTILS_SECRET: '${UTILS_SECRET}'
DATABASE_URL: '${DATABASE_URL}'
PGSSLMODE: '${PGSSLMODE}'
REDIS_URL: '${REDIS_URL}'
URL: '${URL}'
FILE_STORAGE: '${FILE_STORAGE}'
FILE_STORAGE_UPLOAD_MAX_SIZE: '262144000'
AWS_ACCESS_KEY_ID: '${AWS_ACCESS_KEY_ID}'
AWS_SECRET_ACCESS_KEY: '${AWS_SECRET_ACCESS_KEY}'
AWS_REGION: '${AWS_REGION}'
AWS_S3_ACCELERATE_URL: '${AWS_S3_ACCELERATE_URL}'
AWS_S3_UPLOAD_BUCKET_URL: '${AWS_S3_UPLOAD_BUCKET_URL}'
AWS_S3_UPLOAD_BUCKET_NAME: '${AWS_S3_UPLOAD_BUCKET_NAME}'
AWS_S3_FORCE_PATH_STYLE: '${AWS_S3_FORCE_PATH_STYLE}'
AWS_S3_ACL: '${AWS_S3_ACL}'
OIDC_CLIENT_ID: '${OIDC_CLIENT_ID}'
OIDC_CLIENT_SECRET: '${OIDC_CLIENT_SECRET}'
OIDC_AUTH_URI: '${OIDC_AUTH_URI}'
OIDC_TOKEN_URI: '${OIDC_TOKEN_URI}'
OIDC_USERINFO_URI: '${OIDC_USERINFO_URI}'
OIDC_LOGOUT_URI: '${OIDC_LOGOUT_URI}'
OIDC_USERNAME_CLAIM: '${OIDC_USERNAME_CLAIM}'
OIDC_DISPLAY_NAME: '${OIDC_DISPLAY_NAME}'
redis:
image: redis:7.2-bookworm
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- ./redis.conf:/redis.conf
command: ["redis-server", "/redis.conf"]
postgres:
image: postgres:16.3-bookworm
restart: unless-stopped
ports:
- "5432:5432"
volumes:
- ./data/postgres:/var/lib/postgresql/data
environment:
POSTGRES_USER: '${POSTGRES_USER}'
POSTGRES_PASSWORD: '${POSTGRES_PASSWORD}'
POSTGRES_DB: '${POSTGRES_DB}'
volumes:
database-data:

View File

@ -1,3 +0,0 @@
WEB_SERVER_PORT=9494
USER_UID=1000
USER_GID=1000

View File

@ -1 +0,0 @@
data/

View File

@ -1,11 +1,11 @@
services: services:
gitea_web_app: gitea_app:
image: gitea/gitea:1.23.7 image: gitea/gitea:1.23.7
restart: unless-stopped restart: unless-stopped
container_name: gitea_web_app container_name: gitea_app
ports: ports:
- "${WEB_SERVER_PORT}:3000" - "127.0.0.1:{{ gitea_port }}:3000"
- "2222:22" - "2222:22"
volumes: volumes:
- ./data:/data - ./data:/data

View File

@ -5,7 +5,7 @@ set -o pipefail
echo "Gitea: backup data with gitea dump" echo "Gitea: backup data with gitea dump"
(cd {{ base_dir }} && docker compose exec -u "{{ user_create_result.uid }}:{{ user_create_result.group }}" -w /backups gitea_web_app gitea dump -c /data/gitea/conf/app.ini) (cd {{ base_dir }} && docker compose exec -u "{{ user_create_result.uid }}:{{ user_create_result.group }}" -w /backups gitea_app gitea dump -c /data/gitea/conf/app.ini)
echo "Gitea: remove old backups" echo "Gitea: remove old backups"

View File

@ -1,6 +1,7 @@
services: services:
homepage-web: homepage_app:
image: "${WEB_SERVICE_IMAGE}" image: "${WEB_SERVICE_IMAGE}"
container_name: homepage_app
ports: ports:
- "127.0.0.1:${WEB_SERVICE_PORT}:80" - "127.0.0.1:${WEB_SERVICE_PORT}:80"
restart: unless-stopped restart: unless-stopped

View File

@ -5,10 +5,13 @@ import argparse
def main(): def main():
parser = argparse.ArgumentParser(description='Retain specified number of files in a directory sorted by name, delete others.') parser = argparse.ArgumentParser(
parser.add_argument('directory', type=str, help='Path to target directory') description="Retain specified number of files in a directory sorted by name, delete others."
parser.add_argument('--keep', type=int, default=2, )
help='Number of files to retain (default: 2)') parser.add_argument("directory", type=str, help="Path to target directory")
parser.add_argument(
"--keep", type=int, default=2, help="Number of files to retain (default: 2)"
)
args = parser.parse_args() args = parser.parse_args()
# Validate arguments # Validate arguments
@ -29,7 +32,7 @@ def main():
sorted_files = sorted(files) sorted_files = sorted(files)
# Identify files to delete # Identify files to delete
to_delete = sorted_files[:-args.keep] if args.keep > 0 else sorted_files.copy() to_delete = sorted_files[: -args.keep] if args.keep > 0 else sorted_files.copy()
# Delete files and print results # Delete files and print results
for filename in to_delete: for filename in to_delete:

View File

@ -7,16 +7,17 @@
services: services:
keycloak: keycloak_app:
image: quay.io/keycloak/keycloak:24.0.4 image: quay.io/keycloak/keycloak:24.0.4
container_name: keycloak_app
command: ["start-dev"] command: ["start-dev"]
restart: unless-stopped restart: unless-stopped
environment: environment:
KEYCLOAK_ADMIN: "${KEYCLOAK_ADMIN}" KEYCLOAK_ADMIN: "{{ keycloak_admin_login }}"
KEYCLOAK_ADMIN_PASSWORD: "${KEYCLOAK_ADMIN_PASSWORD}" KEYCLOAK_ADMIN_PASSWORD: "{{ keycloak_admin_password }}"
KC_HOSTNAME_URL: "https://kk.vakhrushev.me" KC_HOSTNAME_URL: "https://kk.vakhrushev.me"
KC_HOSTNAME_ADMIN_URL: "https://kk.vakhrushev.me" KC_HOSTNAME_ADMIN_URL: "https://kk.vakhrushev.me"
ports: ports:
- "${WEB_SERVER_PORT}:8080" - "127.0.0.1:{{ keycloak_port }}:8080"
volumes: volumes:
- "./data:/opt/keycloak/data" - "./data:/opt/keycloak/data"

View File

@ -0,0 +1,60 @@
services:
# See sample https://github.com/outline/outline/blob/main/.env.sample
outline_app:
image: outlinewiki/outline:0.81.1
container_name: outline_app
restart: unless-stopped
ports:
- "127.0.0.1:{{ outline_port }}:3000"
depends_on:
- outline_postgres
- outline_redis
environment:
NODE_ENV: 'production'
URL: 'https://outline.vakhrushev.me'
SECRET_KEY: '{{ outline_secret_key }}'
UTILS_SECRET: '{{ outline_utils_secret }}'
DATABASE_URL: 'postgres://{{ outline_postgres_user }}:{{ outline_postgres_password }}@outline_postgres:5432/{{ outline_postgres_database }}'
PGSSLMODE: 'disable'
REDIS_URL: 'redis://outline_redis:6379'
FILE_STORAGE: 's3'
FILE_STORAGE_UPLOAD_MAX_SIZE: '262144000'
AWS_ACCESS_KEY_ID: '{{ outline_s3_access_key }}'
AWS_SECRET_ACCESS_KEY: '{{ outline_s3_secret_key }}'
AWS_REGION: '{{ outline_s3_region }}'
AWS_S3_ACCELERATE_URL: ''
AWS_S3_UPLOAD_BUCKET_URL: '{{ outline_s3_url }}'
AWS_S3_UPLOAD_BUCKET_NAME: '{{ outline_s3_bucket }}'
AWS_S3_FORCE_PATH_STYLE: 'true'
AWS_S3_ACL: 'private'
OIDC_CLIENT_ID: '{{ outline_oidc_client_id }}'
OIDC_CLIENT_SECRET: '{{ outline_oidc_client_secret }}'
OIDC_AUTH_URI: 'https://kk.vakhrushev.me/realms/outline/protocol/openid-connect/auth'
OIDC_TOKEN_URI: 'https://kk.vakhrushev.me/realms/outline/protocol/openid-connect/token'
OIDC_USERINFO_URI: 'https://kk.vakhrushev.me/realms/outline/protocol/openid-connect/userinfo'
OIDC_LOGOUT_URI: 'https://kk.vakhrushev.me/realms/outline/protocol/openid-connect/logout'
OIDC_USERNAME_CLAIM: 'email'
OIDC_DISPLAY_NAME: 'KK'
outline_redis:
image: redis:7.2-bookworm
container_name: outline_redis
restart: unless-stopped
outline_postgres:
image: postgres:16.3-bookworm
container_name: outline_postgres
restart: unless-stopped
volumes:
- ./data/postgres:/var/lib/postgresql/data
environment:
POSTGRES_USER: '{{ outline_postgres_user }}'
POSTGRES_PASSWORD: '{{ outline_postgres_password }}'
POSTGRES_DB: '{{ outline_postgres_database }}'
volumes:
database-data:

View File

@ -1,5 +1,6 @@
#!/usr/bin/env sh #!/usr/bin/env sh
# Must be executed for every user
# See https://cloud.yandex.ru/docs/container-registry/tutorials/run-docker-on-vm#run # See https://cloud.yandex.ru/docs/container-registry/tutorials/run-docker-on-vm#run
set -eu set -eu

View File

@ -1,79 +0,0 @@
---
- hosts: all
vars_files:
- vars/ports.yml
- vars/vars.yml
tasks:
# Applications
- ansible.builtin.import_role:
name: docker-app
vars:
username: keycloak
extra_groups:
- docker
ssh_keys:
- '{{ lookup("file", "files/av_id_rsa.pub") }}'
env:
PROJECT_NAME: keycloak
DOCKER_PREFIX: keycloak
IMAGE_PREFIX: keycloak
CONTAINER_PREFIX: keycloak
WEB_SERVER_PORT: "127.0.0.1:{{ keycloak_port }}"
KEYCLOAK_ADMIN: "{{ keycloak.admin_login }}"
KEYCLOAK_ADMIN_PASSWORD: "{{ keycloak.admin_password }}"
USER_UID: "{{ uc_result.uid }}"
USER_GID: "{{ uc_result.group }}"
tags:
- apps
- ansible.builtin.import_role:
name: docker-app
vars:
username: outline
extra_groups:
- docker
ssh_keys:
- '{{ lookup("file", "files/av_id_rsa.pub") }}'
env:
PROJECT_NAME: outline
DOCKER_PREFIX: outline
IMAGE_PREFIX: outline
CONTAINER_PREFIX: outline
WEB_SERVER_PORT: "127.0.0.1:{{ outline_port }}"
USER_UID: "{{ uc_result.uid }}"
USER_GID: "{{ uc_result.group }}"
# Postgres
POSTGRES_USER: "{{ outline.postgres_user }}"
POSTGRES_PASSWORD: "{{ outline.postgres_password }}"
POSTGRES_DB: "outline"
# See sample https://github.com/outline/outline/blob/main/.env.sample
NODE_ENV: "production"
SECRET_KEY: "{{ outline.secret_key }}"
UTILS_SECRET: "{{ outline.utils_secret }}"
DATABASE_URL: "postgres://{{ outline.postgres_user }}:{{ outline.postgres_password }}@postgres:5432/outline"
PGSSLMODE: "disable"
REDIS_URL: "redis://redis:6379"
URL: "https://outline.vakhrushev.me"
FILE_STORAGE: "s3"
AWS_ACCESS_KEY_ID: "{{ outline.s3_access_key }}"
AWS_SECRET_ACCESS_KEY: "{{ outline.s3_secret_key }}"
AWS_REGION: "ru-central1"
AWS_S3_ACCELERATE_URL: ""
AWS_S3_UPLOAD_BUCKET_URL: "https://storage.yandexcloud.net"
AWS_S3_UPLOAD_BUCKET_NAME: "av-outline-wiki"
AWS_S3_FORCE_PATH_STYLE: "true"
AWS_S3_ACL: "private"
OIDC_CLIENT_ID: "{{ outline.oidc_client_id }}"
OIDC_CLIENT_SECRET: "{{ outline.oidc_client_secret }}"
OIDC_AUTH_URI: "https://kk.vakhrushev.me/realms/outline/protocol/openid-connect/auth"
OIDC_TOKEN_URI: "https://kk.vakhrushev.me/realms/outline/protocol/openid-connect/token"
OIDC_USERINFO_URI: "https://kk.vakhrushev.me/realms/outline/protocol/openid-connect/userinfo"
OIDC_LOGOUT_URI: "https://kk.vakhrushev.me/realms/outline/protocol/openid-connect/logout"
OIDC_USERNAME_CLAIM: "email"
OIDC_DISPLAY_NAME: "KK"
tags:
- apps

View File

@ -27,7 +27,6 @@
DOCKER_PREFIX: "{{ app_name }}" DOCKER_PREFIX: "{{ app_name }}"
IMAGE_PREFIX: "{{ app_name }}" IMAGE_PREFIX: "{{ app_name }}"
CONTAINER_PREFIX: "{{ app_name }}" CONTAINER_PREFIX: "{{ app_name }}"
WEB_SERVER_PORT: "127.0.0.1:{{ gitea_port }}"
USER_UID: "{{ user_create_result.uid }}" USER_UID: "{{ user_create_result.uid }}"
USER_GID: "{{ user_create_result.group }}" USER_GID: "{{ user_create_result.group }}"

52
playbook-keycloak.yml Normal file
View File

@ -0,0 +1,52 @@
---
- name: "Configure keycloak application"
hosts: all
vars_files:
- vars/ports.yml
- vars/vars.yml
vars:
app_name: "keycloak"
app_user: "{{ app_name }}"
base_dir: "/home/{{ app_name }}"
tasks:
- name: "Create user and environment"
ansible.builtin.import_role:
name: owner
vars:
owner_name: "{{ app_user }}"
owner_extra_groups:
- "docker"
owner_ssh_keys:
- "{{ lookup('file', 'files/av_id_rsa.pub') }}"
owner_env:
PROJECT_NAME: "{{ app_name }}"
DOCKER_PREFIX: "{{ app_name }}"
IMAGE_PREFIX: "{{ app_name }}"
CONTAINER_PREFIX: "{{ app_name }}"
USER_UID: "{{ user_create_result.uid }}"
USER_GID: "{{ user_create_result.group }}"
- name: "Create application data directory"
ansible.builtin.file:
path: "{{ (base_dir, 'data') | path_join }}"
state: "directory"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0777"
- name: "Copy docker compose file"
ansible.builtin.template:
src: "./files/{{ app_name }}/docker-compose.yml.j2"
dest: "{{ base_dir }}/docker-compose.yml"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0644"
- name: "Run application with docker compose"
community.docker.docker_compose_v2:
project_src: "{{ base_dir }}"
state: "present"
remove_orphans: true

46
playbook-outline.yml Normal file
View File

@ -0,0 +1,46 @@
---
- name: "Configure outline application"
hosts: all
vars_files:
- vars/ports.yml
- vars/vars.yml
vars:
app_name: "outline"
app_user: "{{ app_name }}"
base_dir: "/home/{{ app_name }}"
data_dir: "{{ (base_dir, 'data') | path_join }}"
backups_dir: "{{ (base_dir, 'backups') | path_join }}"
tasks:
- name: "Create user and environment"
ansible.builtin.import_role:
name: owner
vars:
owner_name: "{{ app_user }}"
owner_extra_groups:
- "docker"
owner_ssh_keys:
- "{{ lookup('file', 'files/av_id_rsa.pub') }}"
owner_env:
PROJECT_NAME: "{{ app_name }}"
DOCKER_PREFIX: "{{ app_name }}"
IMAGE_PREFIX: "{{ app_name }}"
CONTAINER_PREFIX: "{{ app_name }}"
USER_UID: "{{ user_create_result.uid }}"
USER_GID: "{{ user_create_result.group }}"
- name: "Copy docker compose file"
ansible.builtin.template:
src: "./files/{{ app_name }}/docker-compose.yml.j2"
dest: "{{ base_dir }}/docker-compose.yml"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0644"
- name: "Run application with docker compose"
community.docker.docker_compose_v2:
project_src: "{{ base_dir }}"
state: "present"
remove_orphans: true

View File

@ -1,57 +0,0 @@
import os
import shlex
import fabric
from invoke import task
SERVER_HOST_FILE = "hosts_prod"
DOKER_REGISTRY = "cr.yandex/crplfk0168i4o8kd7ade"
@task(name="deploy:gitea")
def deploy_gitea(context):
deploy("gitea", dirs=["data"])
@task(name="deploy:keycloak")
def deploy_keykloak(context):
deploy("keycloak", compose_file="docker-compose.prod.yml", dirs=["data"])
@task(name="deploy:outline")
def deploy_outline(context):
deploy("outline", compose_file="docker-compose.prod.yml", dirs=["data/postgres"])
def read_host():
with open(SERVER_HOST_FILE) as f:
return f.read().strip()
def ssh_host(app_name):
return f"{app_name}@{read_host()}"
def deploy(app_name: str, compose_file="docker-compose.yml", dirs=None):
docker_compose = os.path.join("app", app_name, compose_file)
assert os.path.exists(docker_compose)
conn_str = ssh_host(app_name)
dirs = dirs or []
print("Deploy app from", docker_compose)
print("Start setup remote host", conn_str)
with fabric.Connection(conn_str) as c:
print("Copy docker compose file to remote host")
c.put(
local=docker_compose,
remote=f"/home/{app_name}/docker-compose.yml",
)
print("Copy environment file")
c.run("cp .env .env.prod")
for d in dirs:
print("Create remote directory", d)
c.run(f"mkdir -p {d}")
print("Up services")
c.run(
f"docker compose --project-name {shlex.quote(app_name)} --env-file=.env.prod up --detach --remove-orphans"
)
c.run(f"docker system prune --all --volumes --force")
print("Done.")

View File

@ -1,108 +1,118 @@
$ANSIBLE_VAULT;1.1;AES256 $ANSIBLE_VAULT;1.1;AES256
30306132313836613433323532313530343831393837306363663664393562666363663834396630 62356266326334623764386265376638616664653061653834633862326162366136343036383037
3762316165386338383336653036616139656561346533660a643963323365353464663766316237 3236326264353231306664343331313661383066653162640a643738303463613038616433363231
34633362396562643766383465333061313433373262353036303337326332666334626633623833 65333565313031303033633334313430613161343435376361363533626638306332343166383836
3035663161353330350a363233336437643431333831623537376431613036663434313764333532 3565393839663162650a366236373831653637376361383536636333333136386639643735373762
32363336306434373632643466333866336130303164613531303865386566386434656536643438 39333039323066383035643438656665623866343161643064383039316166366230646565346166
30333134663862643865346531306464336331326564383436343738386537653163626632336163 30343834333636333962333236636130646263396536653030333563363963623930656633656162
62353464376537353833333039626637396637663866316636396137383861653037376131656138 31626633326530653731636262326532383565633261353964653535313232333334653939613561
66626661343836303161666332363332626630396138303666306165336466316466363261663139 38333361383862333562353534376434313163303438653236613062336531653266616238613239
34656536643466636333353032633738373230343337393762666537383465323961383961353830 64623637633366336561356265386161646534303338376533643439633435613434613138633132
36306630623830306332303766323565353832346166613266313139393731633832643364393162 64366638636363666365646234333264353633393835326464366261363938363232393962646462
39366539666365316361343732343733303062643161633261656132666464636330323962363631 64623535356333373865633464613334356164353263346638313732396431333331656238616338
33386137363339636435336564636466313534343161646362626630656538626266393765616232 65613631336631346230626338363330353764616332356266356630346361346631343333326339
65386631306434636365306235616339656665356363383033356330623866333730313063343363 65343837363637326232323566323263323733613864386230623438353739336534316633363333
62363338623139666334383364633664366565343536666231396230346634393663653366353461 61356466393735306539353135616566373065616664656437343435653131313830373934646433
34393232616432616664666135343339663738666161323635656536313537383034393938356565 63343662636661323934643536306638306137336633346635663337363361346161386632373164
61636432303836303564356661393933353764336434656664363561663866643836663764626365 34396639323566613136386462373466636463616162386266366230323231363839663662376231
39383232343765333066633531313261623538373139343231336334653537623062623662653232 66643133313939373338633563656134313035303161363462333337333838623430663162626539
39623334306436383833626465316138383261653763303130306231333139313239346538386132 36383735393733346631386132333132336430333031636537363936323033336630616462616564
34313532663132336664376538663238343532306163366565636333663138316536656465666331 32326561373737383833653033333463383561386462386563333733373633323065386239333830
65663564636364316535623065366335656464616532613762306565623566383838336364303530 34623435306336343264303165333432616530343564663639306135356632643664336263363439
37343338356166653038376663636432633238346536323961353336336166346364633561633131 66376634363637343165383536373263656631313461373630666235656261656437616662653566
63626438336235613833343830653531303334326632316335326530313266323061393535363033 64356664343861316437383236346165613262356534613364376537383962396665323964313064
37303136346139393166326332633339396262646234656332636463613532373363393066643336 61373337313234633362323266623666343835343335366166343538633061613063626364366637
36346439633834326666333231346666353533383938646462336635666437626464363061633236 61626435366337366533666333303165653361376239333732356535303362313430313962386366
30366261333763306563383638356264326130633264653230373161653261623766643932633138 64396631616139386134306565343233343238363866356364636130613337396366303234393564
38633632646231313263326237326438366534346535396439653233323130313463336630343534 31353536663039396365363163663463613265613264393733656134306237323239623238343536
30333839613065383061643137353736623939323366663537626132393965613138333439333439 31323430383464303032323031383665316466653530353934623136333335303764376661613736
32653231373931643166313736326336373037343039313064646563393635356533393538363661 66356533663631613634626266303430666236346363333564633330636666343338303733363263
35343965666261303138646635366339646461383139336666333831633964356462393436356366 65333232383138616631316365616431656531623161306430636265343336356161666561396664
34633031653137313165343164623634663037316134333431306539386139663930326166613266 30353832663863663764636366333836326163303164656464393661663237363038626162636131
64353433353935386165363932346166333237636162373063343962653930313865313530336239 35636533663733643533643963316137643435313434323864653335623263356665323761326435
61623431333039653938336266626337356265373563363235623464323037396637656666353732 65663462383161373462616637303436326537636338393739313331306666643866373966653566
33383564306139383864336533313531313936376166363866373239643039323434383437663437 31313961613832643931336666303865633137613438326433373966323130313936633265393363
38373363626135623134633062646662333432343530613633326536383136623866383033366566 33613663306430633138643235343662666562656534663434643131626430306463316433323364
38323265363635353666383061383038346164386330336139316566613962303236663435323733 63313539396663613537646631623935643538373263613235313838646661336633326464653861
39376462363563636438366131386234653232376536393036363664306362313435313464353962 61663566396437363864306235383931326535323434633238343031653465633162396439393730
63306533373533633633663039313564376231643835316461363834346162343330396138613631 32303635633139613538346430343936316166633331323036316333663833343733396232306164
62353134653463633932326461653839613936653430653261343532313166363835383434336336 62616132623538643531643635303130383162303732303566643935333066353737353538386238
35616361363539393437393030356332323138373963306662353837363939366533303466306633 65636465613661383234613963656238613863393337373165323737333936356533363462623632
36373464396362616633393334613264323362343432373366613531353233363239316664343235 62643934323262646436383261656463316464333930356136363335646561343364663762343563
63633230363463626633323637353937313263386530393532663935326462356664653532346131 64663037353862396265393762303861633435356432356564366234356263353731643035323737
32393236633765666334336437663135393262393034343534326362383961616666383736663333 31333634336362326535626533636533613138343561316139333766356661656138646666363365
38663432633636326264393632356131313231643338323164663831303465353064613862313762 61373462396633383436383964373365613333303934633839323334303036616237646465653535
64663665383963373435356436393934346434326633356136336238373531386332353530303236 31643330313164363266386136383464343134663439356531373039313966316366396265616232
33336632623634356262373837666465313034396131653833646130303738346461653339643631 61396435386237313932303166386361376537663333313038356664333637633930333135623034
32663736646262333561643438306437626437373430363134613130303366613038356163343630 61656361663662306439326333333133666235623462326334303130613963313962313364303837
61303936373963373734396561353936346432636233643538346235643833653966363930346663 62653934353331356232666430633763616135613831383762326131366336616436313530346365
39623266353064393736616433346131383538373434623338326436636436323333326662616336 31393063663263376132326234663439396262323836386563346135613632353231343734376333
35623637663266386130623936626238623961333138346166646165383534336432323865386233 37636333333632336436633363346232323837633461336537373138363265306538646337333535
36376434323861383064303239326638333837343834303938336339383032393731633638663162 35626538663731343565393132363035633438653966313136313035613061303133383962383764
66666139383632333866353537333165376637373866633738303336613034323062383933346638 61303962303862396331626437663338656632333063313435646666376430626235353861353264
61633964616562373138663131613765353131326536383038656632646363323263323539663061 34396362663130653766663563363665303064646133376235623837393965303435626337633637
36666364653964656133303136306330613737326132343737353465333066376661393265643739 62313364313061363235633636323639353338656232316266653435616635643937386162643133
35333430633062616661343533633766353238313265313832626636373663376265386435653264 62653838323962363534633839336136393830396237653561613463643430646137313262343563
39313239383831383030366564313431643162653639656634623735313132663730303664643965 37343631623334626138633830353634613036373531313439633335313137613965326339646164
39663939383333663134653161393933353436633030313634316438643739306463396364356239 61376662616265623364336264373330393738313938383864343034663365303836313465323733
31386632663261366136303935323838633637316166326335313965646231373165623138643439 35313535313333636439323834323231353431383034363732616635303638303938393437663565
63643838316337376134636237393335656332643431646665303262386337616430306237353230 36346235623634356636323835343032613563623230303734363865343763633363306238336139
61306665333631303839383539346465313734306261393864323330323137323730306232643038 37313065666466323533633764653964366665363831393062666437316235396266636134633566
37313765326131613638383333623130373166343534326234383163323165353139326635383030 39623837623239316132333232646263346362333433663661653036373233323261653439626165
63616634356462303630613538633463376539333636323736666231313738396139643965633031 64376639333836353236386337303333333565376436633166353266363237346162313466663430
38643061643238306339383966343736356261343766333131366365333663363535653934303863 62306433316531653539336665346237616431643765643161653063663431633339313939633435
34306339396530303030643865396164373032663037656137616633373266663039663963393763 64306634326533303035653633653862633738386131336431663434663637653830373537643066
37653933353935616264633162333662396161616134376338616537643765306339643864393966 36633232646362393932306162346436666162343463306666306130613239303438623066346265
35663437643538356462613763356435656132393462646465343663623436663735613037346431 32343034663539353665323734343561303965666331383337326537646434396233323563353134
63316465346138613533356264363465653562626237623065653434663038333830373133383236 61313232323333623966343339313066363335383532623061643331393938643765643939653035
63353661666666306432346566636465313464313662346633656536303966663064396336633364 38626133333939643262326561353635326461376633313861376139633462306665373133346364
63643266616366663161326563353033396161383038663837616230376466616639313431626666 33613766316534643138383637303839336162643762363334373061666436366634653536663364
66396264336334333461616439663763306639633639646333663633666663393061333036396131 33323437333964373339643334666632636631623666386432626135353362653931383736366339
64326535663239353339306332363462373365653230346237353332373836386433393465323762 37633861333234386534323062626130383136306161363631346465306366333633326366653765
35386439666532333830616565613664386662633165313762353461313032313335393232393566 64313737343565396439303261383165643136396165393733643833616366646432663566396562
39346538306434646239376330366563366334343432396564393261383635643537613330306430 63356331353961663335643732663532393233343066363331346538626135323662393839646163
66333739393332336434373462316365396364313830633930653134653634333838376532623162 32393062326564353531623563663361643563366164623739326131643935326336393061373039
62656336363331303565313333626463623362373739613765623463303761316337613763353435 34393164326438346163346365313366613261353963373631373863306236313236653637653265
33616431643838386331346233373938343835323836313133623533653537356333323761626433 64663837383134376439633536633238316565353132333038386563613433303231313163623639
34343830356431376236393132653138656164653133383330303863383361313966393963663838 39303338333731323031353462333162666565396134393234633562376563653163323163653537
30653431313366653739366438653762633539303637653030613231303130623066633639376438 38643561353537343939346430666634316166666465653935393738346463316464613164366264
33353362353039653866626463653365353135636664343431323531313030303535323135306230 37613662383937656664623335313539323638646164613030633164623036623330623365303537
61613565633834613565333938353466356432613335616566323730373064383866336333366538 61623164333736346463373765626238353035656261656439336437326264333839643463353130
32643464373939376363303937353937333761303737363862326463366663613833376534376565 63616139393661646462626164613332366338653735363634663531363461633961363566386561
63636238616261346137356163653232393363323737383538356234313733653339626263613036 31353262306631653931623837643662313065633162633630313735626138363861646434366433
37623263356238343436383835363438323930666433666162386365646635633964323137623465 64663839353438643335623762636266653033623966623162343631633530613166346262396564
37343864323639333236333661616362313361353464313039623038376534323832323233626636 35333263373763353165333737356235303465666561663633616665396334326135633666663162
30396333313836663632363138303130633733623061653838633137303438616363626562386434 66666631623635663437613961366337663061396266346333633135613735313461363130316332
64313262363230643532616566333439656563366664653330623635303233363736396361326236 35303761653535373838366532626434303731333466626465636330656238636638376235366638
32366465636332333335316333393437326133666465356630646332613431326135346437643931 61393365666262373364623364653265643334613835366439623938393831306664326437333230
33636135656337306337383236353031313561626437313739373033386635356161633363363238 63616265376663366433653664326232373062363763313064366665623031616633323838353864
35626636623231396261386133663331373363633035653363326634656438303432653735383766 38363562333163303163373933636336303031303062383864353661303832613438336130666661
38353738623561626364326538323761376536633166383333656131356530303363613835396433 61323631643338663331353530623064396361303266383534373034663564376664663133306637
34633334386539383635383362373138633536393066356537653763383662343865393963643534 37313561376532633163613037353862336532363163323137313630363762373938393164663837
64646562313466633565653763376335623830346565326437633430346264663261353363616233 65383163313837383466623330373031303531653961663164626535353939663836626431316166
64643465643933336166303234386533313962633037616335623034303563346161373064323661 61653964363566636161306535626661623239623965653632343936656633306637366330333536
64326635643437316566303866386131626561366362303462353863373430346532373437313334 66363064626466363831643837306635356238376530396164373263363437633562633063366534
65353262323862636539386439336430383634303532306538623831373737343533306433373861 36313535643836643362353134393430333532343337373161306534343538643964346262613663
37316161636466393037343964646637653062373538643734646235316337363032373962333564 37323530613331343264363230656538353233323139653764656266303663303461376139613165
31383138643037636538363037643562613864323032366632653130613464613433353336323833 66633936303632666264336562363263346233356533653062366531633637626335613639303133
37626237396462353130633237656536383366356335656331396465626538333263366334393761 38393931626538313231376336353839623233306564633034393938643263343061623461646333
63376535353162383331366164366338633961653735353735313734343338336166373030343637 33323831396639636661373831383532303330663131323237396430396530663235316130363430
65613933373035393937376535336364636662383062396634613966343838336235336363663133 39323430363533343236386633643938303565666236343639343730333266346238616164353165
61343437303832393239386134366566383962393163353431363532643036613034353739643064 30333266623866316361356538373138643638616335373436353439336662323437303137303462
39316230393261326661336233666337616262633030636437363433393335333033663965323662 65393963336463623230393565306130313335373361646232386631343333653339303934333331
63323235633166373763376334356239623033353939366437613431323336616535363162366664 30396632376639316634663262333965356131383938313864323530326332633039363865363833
37663466313463353132333166646332653330613039376264386236626535626236303663393132 65356630343039376231346561326537323035316639316563633065313833626635396463343366
39663035633730626161373063313333623734303833313363323734656238326465636562373439 62626338393262363539306131386362616330356364373734363666333864366166306633373165
61316162386138323236653765616230393136333931353163373538373539356635306166396130 34656135316230626633346431313033373736666235376166373062323437363963643265313832
63383234376133346161626535393161616664353661663437383066303439393662633066616366 65373262653230376539316163626538326432303536323166623738633764353362366662633166
306263303961643465393830666262333830 30626463613264613065386361363065663165643938353961376363323239323663643232353332
37656134316637363139633938656263643366373461393561363731653237383935666136303366
66306638376130353238653563313133663933643431333835383961633538626365646165616232
33633663396337636365323835373166386536386231383632383038663037663265333966643737
34303233626337623062636139613231346363303235623362373634343865653538646663653731
37383937323739303431613237633437306531346438613235313934613361356533616465396533
38323430343831356562326239623137653366303738356239396161663762663738636139303134
32366638333335626539366530366563363538623865366665343037643239336238303030633733
61613461323837656463663630663630313162616133333632326432383635656631643663613464
65383863643435363437386435336166663261336561633231666536353537303461623837373162
62666234646137666663303336393732396365656138363536343032366338376331