воскресенье, 31 марта 2013 г.

Обработка конфигурации Flask

Перевод статьи: Configuration Handling

Новинка версии 0.3

Приложения требуют настройки. Здесь описаны различные настройки, которые можно менять в зависимости от окружения, в котором работает приложение: переключение режима отладки, настройки секретного ключа и т.п.

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

Вне зависимости от того, каким образом загружена конфигурация, существует объект конфигурации, содержащий загруженные параметры: атрибут config объекта Flask. Это место, где Flask содержит свои настройки, а также то место, куда расширения могут поместить собственные настройки. Но здесь можно размещать и конфигурацию вашего приложения.

Основы конфигурации

config на самом деле является подклассом словаря и может изменяться точно так же, как и любой словарь:
app = Flask(__name__)
app.config['DEBUG'] = True
Некоторые параметры конфигурации передаются в объект Flask, которые тоже можно читать и писать:
app.debug = True
Для обновления нескольких ключей за раз можно воспользоваться методом словаря update():
app.config.update(
    DEBUG=True,
    SECRET_KEY='...'
)
Встроенные параметры конфигурации

Сам Flask использует следующие параметры конфигурации:
DEBUG Включить/выключить режим отладки.
TESTING Включить/выключить режим тестирования.
PROPAGATE_EXCEPTIONSЯвное включение или отключение исключений. Если не задано или явным образом задано значение None, то подразумевается истина, если истиной является TESTING или DEBUG.
PRESERVE_CONTEXT_ON_EXCEPTIONПо умолчанию в режиме отладки при возникновении исключения контекст запроса не извлекается из стека, позволяя отладчику анализировать данные. Такое поведение можно отключить с помощью этого параметра. Также можно воспользоваться этим параметром для его принудительного включения, если это может помочь в отладке приложений в эксплуатации (однако, это не рекомендуется).
SECRET_KEYСекретный ключ.
SESSION_COOKIE_NAMEИмя переменной (cookie) браузера для хранения сеанса.
SESSION_COOKIE_DOMAINДомен переменной браузера, используемой для хранения сеанса. Если не задан, переменная браузера будет действительной для всех поддоменов SERVER_NAME.
SESSION_COOKIE_PATHПуть к переменной браузера, используемой для хранения сеанса. Если не задан, переменная браузера будет действительной для всех APPLICATION_ROOT, если не задано значение '/'.
SESSION_COOKIE_HTTPONLYУказывает, должен ли у переменной браузера устанавливаться флаг httponly (что защищает переменную от доступа со стороны скриптов, работающих внутри браузера - прим. перев.). По умолчанию - истина.
SESSION_COOKIE_SECUREУказывает, должен ли у переменной браузера устанавливаться флаг secure (что позволяет передавать переменную только по защищённому протоколу HTTPS - прим. перев.). По умолчанию - ложь.
PERMANENT_SESSION_LIFETIME Непрерывное время жизни сеанса, как объект datetime.timedelta. Начиная с Flask 0.8 этот параметр может быть задан в виде целого числа с количеством секунд.
USE_X_SENDFILE Включить/отключить x-sendfile. (При использовании этой возможности представление может вернуть специально сформированный ответ со ссылкой на статический файл. Получив такой ответ от представления, веб-сервер отдаёт клиенту вместо ответа представления сам статический файл, найдя его по ссылке в локальной файловой системе. Это позволяет перенести нагрузку по отдаче больших файлов на веб-сервер, если перед отдачей файла представление должно решить, можно ли отдавать этот файл клиенту и какой именно файл нужно отдать по этой ссылке - прим. перев.)
LOGGER_NAMEИмя средства журналирования.
SERVER_NAMEИмя и номер порта сервера. Необходимо для поддержки поддоменов (например: 'myapp.dev:5000') Отметим, что localhost не поддерживает поддомены, поэтому установка параметра в значение "localhost" не поможет. Настройка SERVER_NAME также по умолчанию включает генерацию URL'ов без контекста запроса, но с контекстом приложения.
APPLICATION_ROOTЕсли приложение не занимает целый домен или поддомен, с помощью этого параметра можно задать путь к настроенному приложению. Значение этого параметра используется в качестве пути к переменной браузера для хранения сеанса. Если используются домены, значением этого параметра должно быть None.
MAX_CONTENT_LENGTHЕсли задать значение в байтах, Flask будет отклонять входящие запросы, объём содержимого которых больше этого значения, возвращая код статуса 413.
SEND_FILE_MAX_AGE_DEFAULTПо умолчанию задаёт время кэширования файла для использования совместно с send_static_file() (обработчик статических файлов по умолчанию) и send_file(), в секундах. Заменить это значение для каждого файла индивидуально можно с помощью обработчика get_send_file_max_age() Flask или Blueprint. По умолчанию - 43200 (12 часов).
TRAP_HTTP_EXCEPTIONSЕсли истина, Flask не выполняет обработчиков ошибок исключений HTTP, но вместо этого трактует исключение как любое другое и передаёт исключение выше. Этот параметр полезен для отладки сложных случаев, когда нужно найти, где именно произошло исключение HTTP.
TRAP_BAD_REQUEST_ERRORSВнутренние структуры данных Werkzeug, работающие с данными запроса порождают ошибки с особым ключом, также являющимся исключением запроса. Также, многие операции могут неявно приводить к исключениям BadRequest в случае ошибок целостности. Поскольку для отладки важно знать, где именно произошла ошибка, этот флаг может использоваться для отладки в подобных случаях. Если этот параметр истинен, произойдёт обычная выдача результата трассировки.
PREFERRED_URL_SCHEMEСхема, которую нужно использовать для генерации URL'ов, если она не указана явно. По умолчанию - http.

Подробнее о SERVER_NAME

SERVER_NAME - это параметр, который используется для поддержки поддоменов. Flask не может догадаться о том, какая часть доменного имени является поддоменом, не зная имя сервера. Этот же параметр используется для настройки переменной браузера, в которой хранится сеанс.

Помните, что не только Flask не может узнать поддомен, ваш веб-браузер тоже не может. Большинство соверменных браузеров не разрешают междоменные переменные браузера, если в имени сервера нет точек. Поэтому если имя сервера 'localhost', вы не сможете задать переменную браузера для 'localhost' и каждого из его поддоменов. В этом случае выберите другое имя сервера, например 'myapplication.local' и добавьте это имя и поддомены, которые вы хотите использовать, в файл hosts или настройте локальный bind.

Новинка версии 0.4: LOGGER_NAME

Новинка версии 0.5: SERVER_NAME

Новинка версии 0.6: MAX_CONTENT_LENGTH

Новинки версии 0.7: PROPAGATE_EXCEPTIONS, PRESERVE_CONTEXT_ON_EXCEPTION

Новинки версии 0.8: TRAP_BAD_REQUEST_ERRORS, TRAP_HTTP_EXCEPTIONS, APPLICATION_ROOT, SESSION_COOKIE_DOMAIN, SESSION_COOKIE_PATH, SESSION_COOKIE_HTTPONLY, SESSION_COOKIE_SECURE

Новинка версии 0.9: PREFERRED_URL_SCHEME

Задание конфигурации с помощью файлов

Конфигурация становится более удобной, если разместить её в отдельном файле. Лучше, если он находится за пределами пакета с приложением. Это позволяет создавать пакеты и распространять приложения с помощью различных инструментов обработки пакетов (Развёртывание приложений при помощи distribute) и впоследствии - изменять конфигурацию.

Далее показан обычный пример:
app = Flask(__name__)
app.config.from_object('yourapplication.default_settings')
app.config.from_envvar('YOURAPPLICATION_SETTINGS')
Сначала грузится конфигурация из модуля yourapplication.default_settings, а затем её значения заменяет содержимое файла, указанного в переменной окружения YOURAPPLICATION_SETTINGS. Эта переменная окружения может быть задана в Linux или OS X при помощи команды export оболочки перед запуском сервера:
$ export YOURAPPLICATION_SETTINGS=/path/to/settings.cfg
$ python run-app.py
* Running on http://127.0.0.1:5000/
* Restarting with reloader...
В системах Windows воспользуйтесь встроенной командой set:
>set YOURAPPLICATION_SETTINGS=\path\to\settings.cfg
Файлы конфигурации являются обычными файлами Python. В объект конфигурации сохраняются только переменные с именами в верхнем регистре, так что убедитесь в том, что имена ваших параметров заданы в верхнем регистре.

Вот пример файла конфигурации:
# Пример конфигурации
DEBUG = False
SECRET_KEY = '?\xbf,\xb4\x8d\xa3"<\x9c\xb0@\x0f5\xab,w\xee\x8d$0\x13\x8b83'
Убедитесь в том, что файл загружается как можно раньше, чтобы расширения могли получить доступ к собственным настройкам при запуске. Существуют другие методы загрузки объекта конфигурации из отдельных файлов. За более полной информацией обратитесь к документации объекта Config.

Лучшие способы задания конфигурации

Недостаток описанного выше подхода заключается в усложнении тестирования. Нет стопроцентного способа решения этой проблемы, но вот несколько рекомендаций опытных пользователей:
  1. Создайте ваше приложение внутри функции и зарегистрируйте в ней blueprint'ы. Таким образом вы можете создать несколько экземпляров вашего приложения с разными конфигурациями, что значительно упростит модульное тестирование. Вы можете воспользоваться функцией, чтобы передать в неё необходимую конфигурацию.
  2. Не пишите код, которому требуется конфигурация при импорте. Если ограничиться чтением конфигурации по запросу, возможно будет переконфигурировать объект позже.
Режим разработки и режим эксплуатации

Большинству приложений нужно более одной конфигурации. По меньшей мере нужна отдельная конфигурация для рабочего сервера и ещё одна для разработки. Простейший способ управления ими - это создать конфигурацию по умолчанию, которая загружается всегда и которую можно поместить в систему управления версиями, и частичные конфигурации, которые заменяют необходимые значения следующим образом:
app = Flask(__name__)
app.config.from_object('yourapplication.default_settings')
app.config.from_envvar('YOURAPPLICATION_SETTINGS')
Теперь просто создайте отдельный файл config.py, выполните команду export YOURAPPLICATION_SETTINGS=/path/to/config.py и готово. Однако, существуют альтернативные способы. Например, можно воспользоваться импортом и подклассами.

В мире Django распространён следующий способ: сделать явный импорт конфигурации, добавив строку from yourapplication.default_settings import * в начале файла, а затем заменить значения вручную. Можно сделать также, затем взять из переменной окружения вида YOURAPPLICATION_MODE необходимый режим - production, development и т.п., а затем импортировать заранее определённые файлы, основываясь на этом значении.

Другой любопытный способ - воспользоваться классами и наследованием конфигурации:
class Config(object):
    DEBUG = False
    TESTING = False
    DATABASE_URI = 'sqlite://:memory:'

class ProductionConfig(Config):
    DATABASE_URI = 'mysql://user@localhost/foo'

class DevelopmentConfig(Config):
    DEBUG = True

class TestingConfig(Config):
    TESTING = True
Для включения такой конфигурации, вам просто нужно вызвать from_object():
app.config.from_object('configmodule.ProductionConfig')
Есть много разных способов, которые можно выбрать для управления файлами конфигурации. Вот список хороших советов:
  • Храните файл конфигурации по умолчанию в системе управления версиями. Заполните объект конфигурации значениями по умолчанию или импортируйте его в ваших собственных файлах конфигурации перед тем, как заменить значения.
  • Воспользуйтесь переменной окружения для переключения между конфигурациями. Это можно сделать вне интерпретатора Python и это позволит упростить разработку и развёртывание, потому что вы можете быстро и легко переключаться между разными конфигурациями, совсем не прикасаясь к коду. Если вы часто работаете над разными проектами, можно создать собственный скрипт, который будет определять текущий каталог проекта, активировать virtualenv и экспортировать конфигурацию режима разработки.
  • Используйте инструмент fabric для внесения изменений на сервер эксплуатации и раздельных конфигураций на серверы эксплуатации. За более подробным описанием того, как это сделать, обратитесь к главе Развёртывание приложений при помощи fabric.
Каталоги экземпляров

Новинка версии 0.8.

Во Flask 0.8 появились каталоги экземпляров. Flask долгое время позволял ссылаться на пути относительно каталога приложения (с помощью Flask.root_path). Поэтому многие разработчики хранили конфигурацию рядом с приложением. К несчастью, это возможно только если приложение не находится внутри пакета, так как в таком случае root_path указывает внутрь пакета.

Во Flask 0.8 был введён новый атрибут: Flask.instance_path. Он вводит новое понятие, которое называется каталогом экземпляра. Каталог экземпляра задуман как каталог, не управляемый системой контроля версий и относящийся к развёрнутому приложению. Это подходящее место для того, чтобы поместить в него файлы, изменяемые в процессе работы или файлы конфигурации.

Можно явным образом указать путь к каталогу экземпляра при создании приложения Flask или можно разрешить Flask'у самому выбрать каталог экземпляра. Чтобы задать его явным образом, воспользуйтесь параметром instance_path:
app = Flask(__name__, instance_path='/path/to/instance/folder')
Помните, что если этот путь указан, он должен быть абсолютным.

Если параметр instance_path не указан, по умолчанию используются следующие места:
  • Не установленный модуль:
    /myapp.py
    /instance
  • Не установленный пакет:
    /myapp
        /__init__.py
    /instance
  • Установленный модуль или пакет:
    $PREFIX/lib/python2.X/site-packages/myapp
    $PREFIX/var/myapp-instance
    $PREFIX - это префикс, с которым установлен Python. Это может быть каталог /usr или путь в каталоге с виртуальным окружением, созданном virtualenv. Можно вывести на экран значение sys.prefix, чтобы увидеть его действительное значение.
Как только стало возможным загружать объект конфигурации из файлов с относительными именами, мы добавили возможность загружать файлы с именами относительно каталога экземпляра. При помощи переключателя instance_relative_config в конструкторе приложения можно указать, должны ли интерпретироваться относительные пути файлов относительно корня приложения (по умолчанию) или относительно каталога экземпляра:
app = Flask(__name__, instance_relative_config=True)
Ниже представлен полный пример настройки Flask для предварительной загрузки конфигурации из модуля и последующей замены параметров значениями из файла в каталоге конфигурации, если он существует:
app = Flask(__name__, instance_relative_config=True)
app.config.from_object('yourapplication.default_settings')
app.config.from_pyfile('application.cfg', silent=True)
Путь к каталогу экземпляра может быть найден при помощи Flask.instance_path. Flask также предоставляет более короткий способ открытия файлов из каталога экземпляра при помощи Flask.open_instance_resource().

Вот пример для обоих способов:
filename = os.path.join(app.instance_path, 'application.cfg')
with open(filename) as f:
    config = f.read()
# или при помощи open_instance_resource:
with app.open_instance_resource('application.cfg') as f:
    config = f.read()
Примечания переводчика
В качестве перевода для термина cookie было использовано понятие "переменных бразуера".

Информация о флагах httponly и secure взята из статьи HTTP cookie.

Информация о x-sendfile взята из статьи Передача файлов с помощью XSendfile с помощью NGINX.

Примечания переводчика

Этот и другие переводы можно найти на сайте проекта перевода документации по Flask. Автор проекта - Виталий Кузьмин aka ferm32.

воскресенье, 24 марта 2013 г.

Проектные решения во Flask

Перевод статьи: Design Decisions in Flask

Если вам кажется странным, почему Flask делает некоторые вещи именно так, а не как-нибудь иначе, этот раздел для вас. После его прочтения у вас должно появиться понимание, почему некоторые проектные решения, на первый взгляд кажутся случайными и неожиданными, особенно по сравнению с другими фреймворками.

Явный объект приложения

Веб-приложения на Python, основанные на WSGI, должны обладать центральным вызываемым объектом, реализующим приложение. Во Flask это экземпляр класса Flask. Каждое приложение Flask должно само создать экземпляр этого класса и передать ему имя модуля, но почему Flask не может сделать это сам?

Если бы не было явного объекта приложения, то следующий код:
from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return 'Hello World!'
Выглядел бы так:
from hypothetical_flask import route

@route('/')
def index():
    return 'Hello World!'
Для этого есть три основных причины. Наиболее важная заключается в том, что неявный объект приложения может существовать одновременно только в одном экземпляре. Есть способы сымитировать множество приложений в одном объекте приложения, например, поддерживая пачку приложений, но это вызовет некоторые проблемы, в детали которых мы не станем вдаваться. Следующий вопрос: в каких случаях микрофреймворку может понадобиться создавать больше одного приложения одновременно? Хорошим примером является модульное тестирование. Когда вам нужно протестировать что-нибудь, может оказаться полезным создать минимальное приложение для тестирования определённого поведения. Когда объект приложения удалятся, занятые им ресурсы снова становятся свободными.

Другой возможной причиной, почему могут пригодиться явные объекты приложений, может быть необходимость обернуть объект в дополнительный код, что можно сделать создав подкласс базового класса (Flask) для изменения части его поведения. Это невозможно сделать без костылей, если объект был создан до этого, на основе класса, который вам не доступен.

Но существует и ещё одна очень важная причина, почему Flask зависит от явных экземпляров классов: имя пакета. Когда бы вы ни создавали экземпляр Flask, обычно вы передаёте ему __name__ в качестве имени пакета. Flask использует эту информацию для правильной загрузки ресурсов относящихся к модулю. При помощи выдающихся возможностей Python к рефлексии, вы можете получить доступ к пакету, чтобы узнать, где хранятся шаблоны и статические файлы (см. open_resource()). Очевидно, что фреймворкам не требуется какая-либо настройка и они по-прежнему смогут загружать шаблоны относительно модуля вашего приложения. Но они используют для этого текущий рабочий каталог, поэтому нет возможности надёжно узнать, где именно находится приложение. Текущий рабочий каталог одинаков в рамках процесса, поэтому если запустить несколько приложений в рамках одного процесса (а это может происходить в веб-сервере, даже если вы об этом не догадываетесь), путь может измениться. Даже хуже: многие веб-серверы устанавливают рабочим каталогом не каталог приложения, а корневой каталог документов, который обычно является другим каталогом.

Третья причина заключается в том, что "явное лучше неявного". Этот объект - ваше приложение WSGI, вам больше не нужно помнить о чём-то ещё. Если вам нужно воспользоваться промежуточным модулем WSGI, просто оберните его этим модулем и всё (хотя есть способы лучше, чтобы сделать это, если вы не хотите потерять ссылки на объект приложения wsgi_app()).

Кроме того, такой дизайн делает возможным использование фабричных методов для создания приложения, которое может оказаться полезным для модульного тестирования и других подобных вещей (Фабрики приложений).

Система маршрутизации

Flask использует систему маршрутизации Werkzeug, который был спроектирован для того, чтобы автоматически упорядочивать маршруты в зависимости от их сложности. Это означает, что вы можете объявлять маршруты в произвольном порядке и они всё равно будут работать ожидаемым образом. Это необходимо, если вы хотите, чтобы маршруты, основанные на декораторах, работали правильно, потому что декораторы могут активироваться в неопределённом порядке, если приложение разделено на несколько модулей.

Другое проектное решение системы маршрутизации Werkzeug заключается в том, что Werkzeug старается, чтобы URL'ы были уникальными. Werkzeug настолько последователен в этом, что автоматически выполнит переадресацию на канонический URL, если маршрут окажется неоднозначным.

Одна система шаблонизации

Flask остановился на одной системе шаблонизации: Jinja2. Почему Flask не имеет интерфейса для подключения систем шаблонизации? Очевидно, что вы можете использовать любые системы шаблонизации, но Flask всё равно настроит Jinja2. Хотя всегда настроенная Jinja2, возможно и не потребуется, скорее всего нет причин, чтобы не принять решение воспользоваться единой системой шаблонизации.

Системы шаблонизации похожи на языки программирования и каждая из них обладает собственным мировоззрением. Внешне они все работают одинаково: вы просите систему шаблонизации заполнить шаблон набором переменных и получаете его в виде строки.

Но на этом сходство заканчивается. Jinja2, например, обдает системой расширяемых фильтров, возможностью наследования шаблонов, поддержкой повторного использования блоков (макросов), которые можно использовать внутри шаблонов и из кода на Python для всех операций, использует Unicode, поддерживает поэтапную отрисовку шаблонов, настраиваемый синтаксис и многое другое. С другой стороны, системы шаблонизации вроде Genshi основываются на потоковом заполнении XML, наследовании шаблонов с помощью XPath и т.п. Mako интерпретирует шаблоны аналогично модулям Python.

Когда нужно соединить систему шаблонизации с приложением или фреймворком, требуется больше чем просто отрисовка шаблонов. Например, Flask широко использует поддержку экранирования из Jinja2. А ещё он позволяет получить доступ к макросам из шаблонов Jinja2.

Слой абстракции над системами шаблонизации, учитывающий уникальные особенности каждой из них - это почти наука, слишком сложная для поддержки в микрофреймворке вроде Flask.

Кроме того, расширения из-за этого могут оказаться слишком зависимыми от имеющейся системы шаблонизации. Вы можете воспользоваться собственным языком шаблонов, но расширения по-прежнему будут зависеть от Jinja.

Микро, но с зависимостями

Почему Flask называет себя микрофреймворком, если он зависит от двух библиотек (а именно - от Werkzeug и Jinja2)? Если посмотреть на Ruby со стороны веб-разработки, то и там имеется протокол очень похожий на WSGI. Только там он называется Rack, но всё же он больше похож на реализацию WSGI для Ruby. Но практически ни одно приложение для Ruby не работает с Rack напрямую, используя для этого одноимённую библиотеку. У библиотеки Rack в Python имеется два аналога: WebOb (прежде известная под именем Paste) и Werkzeug. Paste всё ещё существует, но насколько я понимаю, он уступил своё место в пользу WebOb. Разработчики WebOb и Werkzeug развивались параллельно, имея схожие цели: сделать хорошую реализацию WSGI для использования в других приложениях.

Фреймворк Flask использует преимущества уже реализованные в Werkzeug для правильного взаимодействия с WSGI (что иногда может оказаться сложной задачей). Благодаря недавним разработкам в инфраструктуре пакетов Python, пакеты с зависимостями больше не являются проблемой и осталось мало причин, чтобы воздерживаться от использования библиотек, зависящих от других библиотек.

Локальные объекты потоков

Flask использует объекты, локальные внутри потока (фактически - объекты локального контекста, по сути они поддерживают контексты гринлетов) для запроса, сеанса и для дополнительного объекта, в который можно положить собственные объекты (g). Почему сделано так и почему это не так уж плохо?

Да, обычно это не лучшая мысль, использовать локальные объекты потоков. Они могут вызвать проблемы с серверами, которые не основаны на понятии потоков и приводят к сложностям в сопровождении больших приложений. Однако, Flask просто не предназначен для больших приложений или асинхронных серверов. Flask стремится упростить и ускорить разработку традиционных веб-приложений.

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

Что есть Flask и чем он не является

Во Flask никогда не будет слоя для работы с базами данных. В нём нет библиотеки форм или чего-то подобного. Flask сам по себе является мостом к Werkzeug для реализации хорошего приложения WSGI и к Jinja2 для обработки шаблонов. Он также связан с несколькими стандартными библиотеками, например, с библиотекой журналирования logging. Всё остальное отдаётся на откуп расширениям.

Почему так? Потому что люди обладают разными предпочтениями и требованиями и Flask не сможет удовлетворить их, если всё это будет частью ядра. Большинству веб-приложений нужна какая-нибудь система шаблонизации. Однако не каждому приложению необходима база данных SQL.

Идея Flask заключается в создании хорошей основы для других приложений. Всё остальное должны сделать вы сами или расширения.

Примечания переводчика

В сети можно найти перевод первых глав документации по Flask - Flask - викиучебник.

Этот и другие переводы можно найти на сайте проекта перевода документации по Flask. Автор проекта - Виталий Кузьмин aka ferm32.

воскресенье, 3 марта 2013 г.

systemd. Часть 3. Безопасность и ограничение использования ресурсов

В этой заметке описываются средства и директивы, предоставляемые systemd, предназначенные для снижения риска повреждения системы со стороны сервисов и ограничения потребления системных ресурсов для сервисов.

1. Безопасность

1.1. Безопасность. chroot

Для запуска сервиса внутри chroot нужно вписать в раздел Service следующие параметры:

RootDirectory - изменённый корневой каталог (chroot),
ExecStartPre - команды, выполняемые до входа в chroot. Например, команды монтирования файловых систем /proc и /sys внутрь будущего корневого каталога сервиса, копирование внутрь chroot файлов, необходимых для работы сервиса.
RootDirectoryStartOnly - указывает, будут ли выполняться внутри chroot все команды (no), или только команда ExecStart (no).

Даже в случае запуска сервиса в chroot, сервисом можно продолжать управлять с помощью команды systemctl.

Стоит также помнить, что chroot не является наилучшим средством обеспечения безопасности, потому-что процесс, запущенный в chroot по прежнему имеет доступ ко всем идентификаторам процессов и имеет доступ ко всем системным вызовам системы, поэтому имеет возможность управлять процессами и даже может выйти за пределы chroot.

1.2. Безопасность. Пространства имён

Достойной альтернативой chroot может служить FSNS (Filesystem Namespace) - пространство имён файловых систем. С помощью FSNS можно сделать полностью недоступными или доступными только для чтения части файловой системы. Для этого можно воспользоваться следующими параметрами, которые нужно внести в раздел Service:

ReadOnlyDirectories - указанный каталог и все его подкаталоги будут доступны сервису только на чтение,
InaccessibleDirectories - указанный каталог и все его подкаталоги будут полностью недоступны сервису.

ПРЕДУПРЕЖДЕНИЕ: действие двух вышеописанных директив не распространяется на точки монтирования, попавшие внутрь указанных каталогов. Чтобы ограничить доступ сервиса к таким точкам монтирования, нужно задать дополнительный параметр с указанием точки монтирования.

Если сервису для выполнения его обязанностей не нужен доступ в сеть, можно запустить его в своей собственной изолированной сети, состоящей из одного интерфейса обратной петли, который будет изолирован от системного интерфейса обратной петли. Для этого достаточно указать в разделе Service всего один параметр:

PrivateNetwork - значение yes указывает необходимость запустить сервис в изолированной сети, а значение no позволит сервису использовать сетевые интерфейсы системы.

Если сервис не использует каталог /tmp для создания сокет-файлов, можно указать в разделе Service параметр, создающий для процесса изолированное пространство имён для каталога /tmp:

PrivateTmp - значение yes указывает на необходимость создать для сервиса изолированное пространство имён файловой системы с каталогом /tmp, а значение no позволит сервису пользоваться общесистемным каталогом /tmp.

Кроме вышеописанных настроек, работающих внутри service-файлов, в составе systemd имеется утилита systemd-nspawn, создающая полностью изолированное пространство имён. В нём автоматически будут смонтированы файловые системы /proc и /sys, будет создан изолированный интерфейс обратной петли и отдельное пространство имён для идентификаторов процессов. Внутри такого пространства имён можно даже запустить процесс с идентификатором 1, что позволяет запускать внутри него полноценную операционную систему, основанную на ядре Linux.

Например, воспользовавшись всего тремя командами можно запустить Debian поверх Fedora:
# yum install debootstrap
# debootstrap --arch=amd64 unstable debian-tree/
# systemd-nspawn -D debian-tree/ /sbin/init
В последней команде нужно указать процесс, который будет запущен внутри изолированного пространства имён. Если указать процесс init, как в примере выше, внутри запустится полный комплект сервисов. По умолчанию запускается интерпретатор shell.

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

1.3. Безопасность. cgroups

Имеется возможность ограничить доступ сервиса к файлам устройств системы средствами cgroups. Для этого нужно указать параметр в разделе Service:

DeviceAllow - задаёт имя файла устройства и права доступа к нему: r - чтение файла устройства, w - запись в файл устройства, m - создание нового файла устройства. Например, параметр DeviceAllow=/dev/null rw разрешит сервису работать только с файлом устройства /dev/null, разрешая операции чтения и записи. Доступ к остальным устройствам будет полностью заблокирован.

1.4. Безопасность. capabilities

Для задания списка доступных полномочий (capabilities) сервиса можно воспользоваться следующим параметром, который необходимо поместить в раздел Service:

CapabilityBoundingSet - задаёт список разделённых пробелами разрешённых полномочий. Доступ к не указанным здесь полномочиям будет заблокирован. При необходимости задать список запрещённых полномочий, после знака равно и перед списком нужно поставить знак тильды. Доступ к не указанным в списке полномочиям в этом случае будет разрешён.

Список активных полномочий процессов можно увидеть с помощью команды pscap из пакета libcap-ng-utils.

2. Ограничение использования ресурсов

2.1. Ограничение использования ресурсов. rlimit

Ещё одна функция, которой обладает systemd - это возможность настраивать ограничения на использование ресурсов. Можно настроить любое ограничение, описанное в setrlimit(2). Для установки ограничения RLIMIT_NPROC нужно указать в разделе Service параметр LimitNPROC, а названия других параметров строятся по такой же схеме - префикс RLIMIT_ заменяется на Limit.

Стоит отметить, что ограничения эффективны лишь в том случае, если процесс не имеет полномочия CAP_SYS_RESOURCE и не запущен от имени пользователя root.

2.2. Ограничение использования ресурсов. cgroups

systemd использует две группы возможностей, предоставляемых cgroups - это:
1. группировка процессов для определения их принадлежности определённому сервису или сеансу пользователя,
2. ограничения на использование ресурсов системы сервисами и сеансами пользователей.

Первая группа возможностей необходима для нормального функционирования systemd, без неё возможности systemd будут сильно ограничены. Без второй группы возможностей systemd прекрасно обходится и она не обязательна для использования. Именно её мы сейчас и рассмотрим.

В cgroups имеется три контроллера, ограничивающих использование системных ресурсов: cpu - ограничивает использование процессоров системы, memory - ограничивает использование оперативной памяти системы, blkio - ограничивает использование пропускной полосы к блочным устройствам (дискам). По умолчанию в systemd используется только первый контроллер, который делит процессорное время поровну между всеми сервисами. В отличие от systemd, классический SysV init делит процессорное время поровну между всеми процессами, работающими в системе, так что сервис, состоящий из большего количества процессов, получает и больше процессорного времени.

Для просмотра текущего использования ресурсов системы сервисами можно воспользоваться командой systemd-cgtop. Поскольку по умолчанию в systemd включен только контроллер cpu, то используется общий учёт использования памяти и пропускной полосы к блочным устройствам для всех сервисов. При указании ограничений по использованию памяти или пропускной полосы к блочным устройствам хотя-бы в одном из service-файлов, systemd автоматически начинает использовать этот контроллер. Включить же его принудительно можно с помощью директивы DefaultControllers в файле /etc/systemd/system.conf.

Стоит отметить, что ограничение ресурсов обходится не бесплатно и может сказаться на общей производительности системы. Особенно в этом замечен контроллер memory, однако в будущих версиях ядер Linux всё может стать лучше.

Рассмотрим ограничения, предоставляемые каждым из контроллеров:

2.2.1. Процессор

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

2.2.2. Память

MemoryLimit - жёсткое ограничение по использованию памяти сервисом. Превышать этот лимит сервис не может,
MemorySoftLimit - мягкое ограничение по использованию памяти сервисом. Допускается превышение этого лимита, если в системе есть свободная память.

Оба параметра поддерживают суффиксы K, M, G и T, обозначающие, соответственно, килобайты, мегабайты, гигабайты и терабайты (Будьте добры, дайте пару планочек по 4 терабайта. Благодарю Вас, Вы очень любезны).

2.2.3. Ввод-вывод

BlockIOWeight - количество долей пропускной полосы, выделенных сервису для работы с блочными устройствами. Этот параметр может принимать значения от 10 до 1000, а по умолчанию принимает значение 1000,
BlockIOReadBandwidth - объём данных, который сервису разрешается прочитать с блочных устройств за одну секунду,
BlockIOWriteBandwidth - объём данных, который сервису разрешается записать на блочные устройства за одну секунду.

Абсолютные значения также поддерживают суффиксы K, M, G и T, обозначающие, соответственно, скорости обмена данными в килобайтах в секунду, мегабайтах секунду, гигабайтах в секунду и терабайтах в секунду (Отстой! Наш провайдер уже год петабайтные тарифы предоставляет).

В любом из параметров можно указать конкретное блочное устройство, доступ к которому регулируется. При указании каталога или файла, systemd самостоятельно определит, на каком блочном устройстве он располагается и выставит ограничения на это блочное устройство.

Примеры использования:
BlockIOWeight=500
BlockIOWeight=/dev/disk/by-id/ata-SAMSUNG_MMCRE28G8MXP-0VBL1_DC06K01009SE009B5252 750
BlockIOWeight=/home/lennart 750
BlockIOReadBandwidth=/var/log 5M
2.2.4. Другие параметры cgroups

ControlGroupAttribute - позволяет задать значение произвольного атрибута контрольной группы, для которого в systemd нет специально предусмотренного параметра.

Существует также набор параметров, действующих на уровне процессов, но не использующих возможностей cgroups и потому не требующих много ресурсов для их применения IOSchedulingClass, IOSchedulingPriority, CPUSchedulingPolicy, CPUSchedulingPriority, CPUAffinity, LimitCPU.