Як запустити сценарій оболонки на хості з контейнера докера?


95

Як керувати хостом з контейнера докера?

Наприклад, як виконати скопійований в хост скрипт bash?


8
чи не буде це прямо протилежним ізоляції хоста від докера?
Маркус Мюллер,

31
Так. Але це іноді потрібно.
Олексій Ушаков


Не впевнений у "керуванні хостом", але нещодавно я був на доповіді вчених-дослідників даних, які використовують докер для запуску сценаріїв для обробки величезних робочих навантажень (з використанням графічних процесорів, встановлених AWS) і виведення результату на хост. Дуже цікавий варіант використання. По суті, сценарії упаковані в надійне середовище виконання завдяки
докеру

@KCD І чому вони віддають перевагу контейнеризації додатків через докер замість використання контейнерів на системному рівні (LXC)?
Олексій Ушаков

Відповіді:


28

Це ДІЙСНО залежить від того, що вам потрібно зробити для цього скрипта bash!

Наприклад, якщо сценарій bash просто повторює певний результат, ви можете просто зробити

docker run --rm -v $(pwd)/mybashscript.sh:/mybashscript.sh ubuntu bash /mybashscript.sh

Інша можливість полягає в тому, що ви хочете, щоб скрипт bash встановив деяке програмне забезпечення - скажімо, скрипт для встановлення docker-compose. ти міг би зробити щось подібне

docker run --rm -v /usr/bin:/usr/bin --privileged -v $(pwd)/mybashscript.sh:/mybashscript.sh ubuntu bash /mybashscript.sh

Але на цьому етапі ви справді починаєте глибоко знати, що робить скрипт, щоб дозволити конкретні дозволи, необхідні для вашого хосту, зсередини контейнера.


1
У мене була ідея зробити контейнер, який підключається до хоста і створює нові контейнери.
Олексій Ушаков

1
Здається, Докеру не подобається ваше відносне кріплення. Це має спрацюватиdocker run --rm -v $(pwd)/mybashscript.sh:/work/mybashscript.sh ubuntu /work/mybashscript.sh
KCD

5
Перший рядок запускає новий контейнер ubuntu і монтує сценарій, де його можна прочитати. Наприклад, це не дозволяє контейнеру доступ до файлової системи хоста. Другий рядок виставляє хост /usr/binдо контейнера. У жодному випадку контейнер не має повного доступу до хост-системи. Можливо, я помиляюся, але це здається поганою відповіддю на погане питання.
Пол

3
Досить справедливо - питання було досить розмитим. Питання не вимагало "повного доступу до хост-системи". Як описано, якщо скрипт bash призначений лише для повторення певних вихідних даних, він не потребуватиме доступу до файлової системи хоста. Для мого другого прикладу, який встановлював docker-compose, єдиним дозволом, який вам потрібен, є доступ до каталогу bin, де зберігається двійковий файл. Як я вже говорив на початку, для цього вам слід мати дуже конкретні уявлення про те, що робить сценарій, щоб дозволити правильні дозволи.
Пол Бекотт

1
Спробував це, сценарій виконується в контейнері, а не на хості
All2Pie

60

Рішення, яке я використовую, полягає в підключенні до хоста SSHта виконуванні команди, як це:

ssh -l ${USERNAME} ${HOSTNAME} "${SCRIPT}"

ОНОВЛЕННЯ

Оскільки ця відповідь продовжує набирати кількість голосів, я хотів би нагадати (і настійно рекомендую), що обліковий запис, який використовується для виклику сценарію, повинен бути обліковим записом без жодних дозволів, а лише виконувати цей сценарій як sudo(що може бути зроблено з sudoersфайлу).


Як ще одне обхідне рішення - контейнер може виводити набір команд, і хост може запускати їх після виходу контейнера: eval $ (запуск докера --rm -it скрипт_імена_на_виходу)
parity3

Мені потрібно запустити командний рядок на хості зсередини контейнера Docker, але коли я заходжу в контейнер, sshне знайдено. Чи є у вас інші пропозиції?
Рон Розенфельд,

@RonRosenfeld, яке зображення Docker ви використовуєте? в разі Debian / Ubuntu, запустіть це: apt update && apt install openssh-client.
Мохаммед Нурелдін,

Це буде все, що встановлено на моєму Synology NAS. Як я можу сказати?
Рон Розенфельд,

@RonRosenfeld, вибачте, я не розумію, що ви маєте на увазі
Мохаммед Нурелдін

52

Використовував іменовану трубу. На хост-OS створіть скрипт для циклу та читання команд, після чого ви викликаєте eval на цьому.

Нехай докер-контейнер прочитаний у цій іменованій трубі.

Щоб мати доступ до труби, вам потрібно змонтувати її через том.

Це схоже на механізм SSH (або подібний метод на основі сокета), але належним чином обмежує вас на хост-пристрої, що, ймовірно, краще. Крім того, вам не потрібно передавати інформацію про автентифікацію.

Моє єдине попередження - бути обережними щодо того, чому ви це робите. Це цілком щось робити, якщо ви хочете створити метод для самостійного оновлення за допомогою введення користувачем або що завгодно, але ви, мабуть, не хочете викликати команду, щоб отримати деякі дані конфігурації, оскільки правильним способом буде передавати це як аргументи / том у докер. Також будьте обережні щодо того, що ви оцінюєте, тому просто подумайте про модель дозволу.

Деякі з інших відповідей, таких як запуск скрипта. Під томом, не працюватимуть загалом, оскільки вони не матимуть доступу до повних системних ресурсів, але це може бути більш доречним залежно від вашого використання.


14
УВАГА: Це правильна / найкраща відповідь, і вона потребує трохи більше похвали. Кожна інша відповідь возиться із запитанням "що ти намагаєшся зробити" і робиш винятки для речей. У мене є дуже конкретний випадок використання, який вимагає від мене можливості робити це, і це єдина хороша відповідь imho. Наведений вище SSH вимагає зниження стандартів безпеки / брандмауера, а матеріали запуску докера просто невірно. Дякую за це. Я припускаю, що це не отримує стільки голосів, тому що це не проста відповідь копіювання / вставлення, але це відповідь. +100 балів від мене, якби міг
Фарлі

3
Для тих, хто шукає додаткову інформацію, ви можете скористатися таким сценарієм, що працює на хост-машині: unix.stackexchange.com/a/369465 Звичайно, вам доведеться запустити його за допомогою "nohup" і створити якусь обгортку супервізора для того, щоб підтримувати його в живих (можливо, використовувати роботу cron: P)
сукотронік

7
Це може бути гарною відповіддю. Однак було б набагато краще, якщо ви надасте більше деталей та ще кілька пояснень командного рядка. Чи можна це детально розробити?
Мохаммед Нурелдін,

5
Заголос, це працює! Створіть іменовану трубу, використовуючи 'mkfifo host_executor_queue', де встановлений том. Потім, щоб додати споживача, який виконує команди, які поміщаються в чергу як оболонка хоста, використовуйте 'tail -f host_executor_queue | sh & '. Значок & в кінці робить його запущеним у фоновому режимі. Нарешті, для проштовхування команд у чергу використовуйте 'echo touch foo> host_executor_queue' - цей тест створює тимчасовий файл foo у домашньому каталозі. Якщо ви хочете, щоб споживач запускав систему під час запуску, поставте '@reboot tail -f host_executor_queue | sh & 'у crontab. Просто додайте відносний шлях до host_executor_queue.
skybunk

1
хтось може відредагувати відповідь за допомогою прикладу коду?
Лукас Поттерський,

6

Якщо вас не турбує безпека, і ви просто хочете запустити контейнер докера на хості з іншого контейнера докера, такого як OP, ви можете поділитися сервером докера, що працює на хості, з контейнером докера, поділившись його сокетом прослуховування.

Будь ласка, перегляньте https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface і перевірте, чи дозволяє ваша особиста толерантність до ризику це для цього конкретного додатка.

Це можна зробити, додавши наступні аргументи обсягу до команди start

docker run -v /var/run/docker.sock:/var/run/docker.sock ...

або поділившись /var/run/docker.sock у вашому файлі складання докера таким чином:

version: '3'

services:
   ci:
      command: ...
      image: ...
      volumes
         - /var/run/docker.sock:/var/run/docker.sock

Коли ви запускаєте команду запуску докера в контейнері докера, сервер докера, запущений на вашому хості, побачить запит та надасть контейнер для братів і сестер.

кредит: http://jpetazzo.github.io/2015/09/03/do-not-use-docker-in-docker-for-ci/


1
Враховуйте, що докер повинен бути встановлений у контейнері, інакше вам також потрібно буде встановити том для двійкового файлу докера (наприклад /usr/bin/docker:/usr/bin/docker).
Gerry

1
Будьте обережні, встановлюючи сокет докера у вашому контейнері, це може бути серйозною проблемою безпеки: docs.docker.com/engine/security/security/…
DatGuyKaj

@DatGuyKaj, дякую, я відредагував свою відповідь, щоб відобразити проблеми, викладені у вашому ресурсі.
Метт Буччі,

Це не відповідає на запитання, яке стосується запуску сценарію на хості, а не в контейнері
Брендон

5

Ця відповідь - лише більш детальний варіант рішення Бредфорда Медейроса , який, як і для мене, виявився найкращою відповіддю, тому заслуга в ньому.

У своїй відповіді він пояснює, ЩО робити (з іменами труби ), але не точно ЯК це робити.

Треба визнати, що я не знав, як називаються труби, на той час, коли я читав його рішення. Тож я намагався його реалізувати (хоча насправді це дуже просто), але мені це вдалося, тому я радий допомогти, пояснивши, як я це зробив. Тож суть моєї відповіді полягає лише у деталізації команд, які потрібно виконати, щоб це працювало, але знову ж таки, заслуга в ньому.

ЧАСТИНА 1 - Тестування названої концепції труби без докера

На головному хості виберіть папку, куди ви хочете помістити свій файл із іменем pipe, наприклад, /path/to/pipe/і назву труби, наприклад mypipe, а потім запустіть:

mkfifo /path/to/pipe/mypipe

Створена труба. Тип

ls -l /path/to/pipe/mypipe 

І перевірте, що права доступу починаються з "p", наприклад

prw-r--r-- 1 root root 0 mypipe

Тепер запустіть:

tail -f /path/to/pipe/mypipe

Зараз термінал очікує надсилання даних у цей канал

Тепер відкрийте інше вікно терміналу.

А потім запустіть:

echo "hello world" > /path/to/pipe/mypipe

Перевірте перший термінал (той, у якого є tail -f), він повинен відображати "привіт світ"

ЧАСТИНА 2 - Запуск команд по трубі

На хост-контейнері, замість запуску, tail -fякий просто виводить все, що надіслано як вхід, запустіть цю команду, яка виконає її як команди:

eval "$(cat /path/to/pipe/mypipe)"

Потім з іншого терміналу спробуйте запустити:

echo "ls -l" > /path/to/pipe/mypipe

Поверніться до першого терміналу, і ви побачите результат ls -lкоманди.

ЧАСТИНА 3 - Нехай воно слухається вічно

Можливо, ви помітили, що в попередній частині, відразу після ls -lвідображення виводу, він перестає слухати команди.

Замість eval "$(cat /path/to/pipe/mypipe)", виконайте:

while true; do eval "$(cat /path/to/pipe/mypipe)"; done

(це не можна сказати)

Тепер ви можете відправляти необмежену кількість команд одну за одною, всі вони будуть виконані, а не лише перша.

ЧАСТИНА 4 - Зробіть так, щоб це працювало навіть тоді, коли відбувається перезавантаження

Єдине застереження - якщо хосту доведеться перезавантажитись, цикл "while" перестане працювати.

Щоб виконати перезавантаження, ось що я зробив:

Помістіть while true; do eval "$(cat /path/to/pipe/mypipe)"; doneфайл у файл execpipe.shіз #!/bin/bashзаголовком

Не забувайте про chmod +xце

Додайте його до crontab, запустивши

crontab -e

А потім додавання

@reboot /path/to/execpipe.sh

На цьому етапі протестуйте його: перезавантажте сервер, а коли він буде створений резервно, повторіть деякі команди в конвеєр і перевірте, чи вони виконуються. Звичайно, ви не можете бачити висновок команд, тому ls -lне допоможе, але touch somefileдопоможе.

Іншим варіантом є модифікація сценарію для розміщення вихідних даних у файлі, наприклад:

while true; do eval "$(cat /path/to/pipe/mypipe)" &> /somepath/output.txt; done

Тепер ви можете запустити, ls -lі результат (як stdout, так і stderr, що використовують &>bash) повинен бути у output.txt.

ЧАСТИНА 5 - Зробіть так, щоб це працювало з докером

Якщо ви використовуєте як docker compose, так і dockerfile, як я, ось що я зробив:

Припустимо, ви хочете змонтувати батьківську папку mypipe як /hostpipeу вашому контейнері

Додайте це:

VOLUME /hostpipe

у вашому докер-файлі, щоб створити точку монтування

Потім додайте це:

volumes:
   - /path/to/pipe:/hostpipe

у вашому файлі докера, щоб створити файл / шлях / до / труби як / hostpipe

Перезапустіть контейнери докера.

ЧАСТИНА 6 - Тестування

Виконайте в контейнері докера:

docker exec -it <container> bash

Зайдіть в папку монтування і перевірте, чи видно трубу:

cd /hostpipe && ls -l

Тепер спробуйте запустити команду з контейнера:

echo "touch this_file_was_created_on_main_host_from_a_container.txt" > /hostpipe/mypipe

І це має спрацювати!

ПОПЕРЕДЖЕННЯ: Якщо у вас є хост OSX (Mac OS) і контейнер Linux, це не буде працювати (пояснення тут https://stackoverflow.com/a/43474708/10018801 та видайте тут https://github.com/docker / for-mac / issues / 483 ), оскільки реалізація конвеєра неоднакова, тому те, що ви пишете в конвеєр з Linux, може читати лише Linux, а те, що ви пишете в конвеєр з Mac OS, може читати лише Mac OS (це речення може бути не дуже точним, але просто майте на увазі, що існує проблема з кількома платформами).

Наприклад, коли я запускаю налаштування докера в DEV з мого комп’ютера Mac OS, названий канал, як пояснено вище, не працює. Але в постановці та виробництві я маю контейнери Linux і Linux, і це чудово працює.

ЧАСТИНА 7 - Приклад із контейнера Node.JS

Ось як я відправляю команду з мого контейнера js node на основний хост і отримую вихідні дані:

const pipePath = "/hostpipe/mypipe"
const outputPath = "/hostpipe/output.txt"
const commandToRun = "pwd && ls-l"

console.log("delete previous output")
if (fs.existsSync(outputPath)) fs.unlinkSync(outputPath)

console.log("writing to pipe...")
const wstream = fs.createWriteStream(pipePath)
wstream.write(commandToRun)
wstream.close()

console.log("waiting for output.txt...") //there are better ways to do that than setInterval
let timeout = 10000 //stop waiting after 10 seconds (something might be wrong)
const timeoutStart = Date.now()
const myLoop = setInterval(function () {
    if (Date.now() - timeoutStart > timeout) {
        clearInterval(myLoop);
        console.log("timed out")
    } else {
        //if output.txt exists, read it
        if (fs.existsSync(outputPath)) {
            clearInterval(myLoop);
            const data = fs.readFileSync(outputPath).toString()
            if (fs.existsSync(outputPath)) fs.unlinkSync(outputPath) //delete the output file
            console.log(data) //log the output of the command
        }
    }
}, 300);

Це чудово працює. А як щодо безпеки? Я хочу використовувати це для запуску / зупинки контейнерів докера з запущеного контейнера? Чи просто я можу зробити користувача-докера без будь-яких привілеїв, крім запуску команд докера?
Kristof van Woensel

4

Напишіть простий серверний сервер python, який прослуховує порт (скажімо, 8080), прив’яжіть порт -p 8080: 8080 до контейнера, зробіть HTTP-запит до localhost: 8080, щоб попросити сервер python, який запускає сценарії оболонки з popen, запустити curl або написання коду для створення запиту HTTP curl -d '{"foo": "bar"}' localhost: 8080

#!/usr/bin/python
from BaseHTTPServer import BaseHTTPRequestHandler,HTTPServer
import subprocess
import json

PORT_NUMBER = 8080

# This class will handles any incoming request from
# the browser 
class myHandler(BaseHTTPRequestHandler):
        def do_POST(self):
                content_len = int(self.headers.getheader('content-length'))
                post_body = self.rfile.read(content_len)
                self.send_response(200)
                self.end_headers()
                data = json.loads(post_body)

                # Use the post data
                cmd = "your shell cmd"
                p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
                p_status = p.wait()
                (output, err) = p.communicate()
                print "Command output : ", output
                print "Command exit status/return code : ", p_status

                self.wfile.write(cmd + "\n")
                return
try:
        # Create a web server and define the handler to manage the
        # incoming request
        server = HTTPServer(('', PORT_NUMBER), myHandler)
        print 'Started httpserver on port ' , PORT_NUMBER

        # Wait forever for incoming http requests
        server.serve_forever()

except KeyboardInterrupt:
        print '^C received, shutting down the web server'
        server.socket.close()

ІМО це найкраща відповідь. Запуск довільних команд на хост-машині ПОВИНЕН виконуватися за допомогою якогось API (наприклад, REST). Це єдиний спосіб забезпечення безпеки та належного контролю запущених процесів (наприклад, вбивство, обробка stdin, stdout, код виходу тощо). Звичайно, було б непогано, якби цей API міг працювати в Docker, але особисто я не проти запустити його безпосередньо на хості.
barney765

2

Моя лінь привела мене до найпростішого рішення, яке тут не було опубліковане як відповідь.

Він заснований на велику статтю по LUC juggery .

Все, що вам потрібно зробити, щоб отримати повну оболонку для вашого хосту Linux з вашого контейнера докера, це:

docker run --privileged --pid=host -it alpine:3.8 \
nsenter -t 1 -m -u -n -i sh

Пояснення:

--privileged: надає додаткові дозволи контейнеру, це дозволяє контейнеру отримати доступ до пристроїв хоста (/ dev)

--pid = host: дозволяє контейнерам використовувати дерево процесів хоста Docker (віртуальної машини, в якій запущений демон Docker); утиліта nsenter: дозволяє запускати процес у існуючих просторах імен (будівельні блоки, які забезпечують ізоляцію контейнерів)

nsenter (-t 1 -m -u -n -i sh) дозволяє запускати процес sh в тому ж контексті ізоляції, що і процес з PID 1. Потім вся команда забезпечить інтерактивну оболонку sh у віртуальній машині

Ця установка має серйозні наслідки для безпеки, тому її слід застосовувати з обережністю (якщо така є).


1
docker run --detach-keys="ctrl-p" -it -v /:/mnt/rootdir --name testing busybox
# chroot /mnt/rootdir
# 

3
Хоча ця відповідь може вирішити питання OP, пропонується пояснити, як це працює та чому вирішує проблему. Це допомагає новим розробникам зрозуміти, що відбувається і як самостійно виправити цю та подібні проблеми. Дякуємо за внесок!
Калеб Клеветер

1

У мене простий підхід.

Крок 1: Змонтуйте /var/run/docker.sock:/var/run/docker.sock (Отже, ви зможете виконувати команди докера всередині вашого контейнера)

Крок 2: Виконайте це нижче всередині контейнера. Ключовою частиною тут є ( --network host, оскільки це буде виконуватися з контексту хоста)

docker run -i --rm --network host -v /opt/test.sh:/test.sh alpine: 3,7 sh /test.sh

test.sh повинен містити деякі команди (ifconfig, netstat тощо ...), що вам потрібно. Тепер ви зможете отримати вихідні дані контексту хосту.


2
Згідно з офіційною документацією докера щодо роботи в мережі за допомогою хост-мережі, "Однак усіма іншими способами, такими як сховище, простір імен процесів та простір імен користувачів, процес є ізольованим від хосту". Виїзд - docs.docker.com/network/network-tutorial-host
Пітер Мутіся

0

Як нагадує Маркус, Docker - це, в основному, ізоляція процесу. Починаючи з docker 1.8, ви можете копіювати файли в обох напрямках між хостом і контейнером, див. Документdocker cp

https://docs.docker.com/reference/commandline/cp/

Після копіювання файлу ви можете запустити його локально


1
Я це знаю. Як запустити цей сценарій з, іншими словами, всередині контейнера докера?
Олексій Ушаков


2
@AlexUshakov: ніяк. Це зробило б багато переваг докера. Не роби цього. Не намагайтеся. Перегляньте, що вам потрібно зробити.
Маркус Мюллер

Дивіться також форум
user2915097

1
Ви завжди можете на хості отримати значення якоїсь змінної у вашому контейнері, щось на зразок myvalue=$(docker run -it ubuntu echo $PATH)і регулярно тестувати її в оболонці сценарію (звичайно, ви будете використовувати щось інше, ніж $ PATH, це лише приклад), коли це - це якесь конкретне значення, ви запускаєте свій скрипт
user2915097

0

Ви можете використовувати концепцію pipe, але використовуйте файл на хості та fswatch, щоб досягти мети виконати сценарій на хост-машині з контейнера докера. Ось так (Використовуйте на власний ризик):

#! /bin/bash

touch .command_pipe
chmod +x .command_pipe

# Use fswatch to execute a command on the host machine and log result
fswatch -o --event Updated .command_pipe | \
            xargs -n1 -I "{}"  .command_pipe >> .command_pipe_log  &

 docker run -it --rm  \
   --name alpine  \
   -w /home/test \
   -v $PWD/.command_pipe:/dev/command_pipe \
   alpine:3.7 sh

rm -rf .command_pipe
kill %1

У цьому прикладі всередині контейнера надсилайте команди в / dev / command_pipe приблизно так:

/home/test # echo 'docker network create test2.network.com' > /dev/command_pipe

На хості ви можете перевірити, чи створена мережа:

$ docker network ls | grep test2
8e029ec83afe        test2.network.com                            bridge              local

-7

Для розширення user2915097 ігровий відповідь :

Ідея ізоляції полягає в тому, щоб мати можливість чітко обмежити те, що програма / процес / контейнер (незалежно від вашого кута нахилу) може робити для хост-системи дуже чітко. Отже, можливість копіювати та виконувати файл дійсно порушить цілу концепцію.

Так. Але це іноді потрібно.

Ні. Це не так, або Docker - не те, що потрібно використовувати. Що вам слід зробити, це оголосити чіткий інтерфейс для того, що ви хочете зробити (наприклад, оновити конфігурацію хосту), і написати мінімальний клієнт / сервер, щоб зробити саме це, і більше нічого. Однак загалом це, здається, не дуже бажано. У багатьох випадках вам слід просто переосмислити свій підхід і викорінити цю потребу. Докер з’явився тоді, коли в основному все було послугою, до якої можна було дістатись за допомогою якогось протоколу. Я не можу придумати жодного належного випадку використання контейнера Docker, який отримував би права виконувати довільні речі на хості.


У мене є варіант використання: у мене є докернізована служба A(src на github). У Aрепо я створюю належні хуки, які після команди 'git pull' створюють новий образ докера і запускають їх (і звичайно видаляють старий контейнер). Далі: github має веб-хуки, які дозволяють створювати запит POST до довільного посилання на кінцеву точку після натискання на master. Тому я не хочу створювати докеризований сервіс B, який буде цією кінцевою точкою і який буде запускати лише 'git pull' у репо A на машині HOST (важливо: команда 'git pull' повинна виконуватися в середовищі HOST - а не в середовищі B, оскільки B не може запустити новий контейнер A всередині B ...)
Каміль Kiełczewski

1
Проблема: я не хочу мати у HOST нічого, крім linux, git та docker. І я хочу мати службу dockerizet A та службу B (що насправді є обробником git-push, який виконує git pull на репо A після того, як хтось змушує git push на master). Тож автоматичне розгортання git є проблематичним варіантом використання
Kamil Kiełczewski,

@ KamilKiełczewski Я намагаюся зробити точно те саме, чи знайшов ти рішення?
user871784

1
Сказати: "Ні, це не так" є вузьким і передбачає, що ви знаєте всі випадки використання у світі. Наш варіант використання - це запущені тести. Їм потрібно працювати в контейнерах, щоб правильно протестувати середовище, але враховуючи характер тестів, їм також потрібно виконувати сценарії на хості.
Senica Gonzalez

1
Тільки для тих, хто задається питанням, чому я залишаю відповідь -7: а) це нормально, якщо бути помилковим. Я був неправий. Це нормально, що це зафіксовано тут. б) коментарі насправді сприяють підвищенню цінності; видалення відповіді також їх видалить. в) Це все ще надає точку зору, яку, можливо, було б доцільно розглянути (не порушуйте свою ізоляцію, якщо вам не доводиться. Іноді, однак, це потрібно).
Маркус Мюллер,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.