Pull to refresh

SMF — управление сервисами в Solaris

Reading time 13 min
Views 19K
Прочитав недавно статью «Собираем пакет для Solaris из сорцов» я понял, что функционал SMF вообще никак не освещен на Хабре.
Давайте исправим эту ситуацию и посмотрим что из себя представляет SMF и какие преимущества он дает администраторам.

Введение


Service Management Facility (SMF) — система управления сервисами, которая появилась в Solaris 10. SMF позволяет более гибко управлять процессами, назначать им зависимости и перезапускать при необходимости. В дополнение к этому SMF позволяет делегировать права управления сервисами обычным (non-root) пользователям.
Для управления SMF достаточно «всего» трех команд:
  • svcs — проверяет состояние сервисов,
  • svcadm — управление состоянием сервисов,
  • svccfg — настройка параметров сервиса.

Попробуем разобраться с управлением SMF на примере добавления собственного сервиса.
Недавно мне понадобился nginx под Solaris, пришлось собрать пакет и интергрироваться в общую систему сервисов — на его примере и подсмотрим как может быть оформлен сервис для управления через SMF.

Добавление сервиса


Чтобы интегрировать сервис в SMF для него нужно написать манифест — XML файл с описанием зависимостей, методов запуска и прочих параметров. Для элементарных сервисов этого достаточно, для более сложных нужен еще скрипт запуска (аналог /etc/init.d/service).

Содержание манифеста SMF

<?xml version="1.0"?> 
<!DOCTYPE service_bundle SYSTEM "/usr/share/lib/xml/dtd/service_bundle.dtd.1"> 

<service_bundle type='manifest' name='nginx'> 

<service 
        name='network/nginx' 
        type='service' 
        version='1'> 

        <create_default_instance enabled='false' /> 
        <single_instance /> 

        <dependency name='loopback' 
            grouping='require_all' 
            restart_on='error' 
            type='service'> 
                <service_fmri value='svc:/network/loopback:default'/> 
        </dependency> 

        <dependency name='physical' 
            grouping='optional_all' 
            restart_on='error' 
            type='service'> 
                <service_fmri value='svc:/network/physical:default'/> 
        </dependency> 

        <dependency name='multiuser-server' 
            grouping='require_all' 
            restart_on='error' 
            type='service'> 
                <service_fmri value='svc:/milestone/multi-user-server:default'/> 
        </dependency> 

        <exec_method 
            type='method' 
            name='start' 
            exec='/opt/nginx/svc/nginx start' 
            timeout_seconds='60' /> 

        <exec_method 
            type='method' 
            name='stop' 
            exec=':kill -QUIT' 
            timeout_seconds='60' /> 

        <exec_method 
            type='method' 
            name='refresh' 
            exec='/opt/nginx/svc/nginx refresh' 
            timeout_seconds='60' /> 

        <property_group name='nginx' type='application'> 
                <propval name='config' type='astring' 
                    value='/opt/nginx/etc/nginx.conf'/> 
                <propval name='pid' type='astring' 
                    value='/opt/nginx/var/run/nginx.pid'/> 
        </property_group> 

        <property_group name='startd' type='framework'> 
                <!-- sub-process core dumps shouldn't restart 
                     session --> 
                <propval name='ignore_error' type='astring' 
                         value='core,signal' /> 
        </property_group> 

        <template> 
                <common_name> 
                        <loctext xml:lang='C'> 
                                Nginx HTTP server 
                        </loctext> 
                </common_name> 
                <documentation> 
                        <manpage title='nginx' section='1M' /> 
                        <doc_link name='nginx.org' 
                                uri='http://www.nginx.org/' /> 
                </documentation> 
        </template> 
</service> 
</service_bundle>

Разберем по-порядку:
  • service_bundle — показывает как данный файл должен обрабатываться SMF. Возможные значения для type «arhive», «manifest», «profile». В нашем случае рассматривается только «manifest». В атрибут name заносится имя сервиса;
  • service — содержит набор инстансов сервиса (каждый сервис может иметь несколько инстансов различающихся конфигурацией), зависимости, методы управления и конфигурационные параметры. Атрибуты name, version и type содержат соответственно имя, версию и тип сервиса. Тип может быть один из «service», «restarter», «milestone».
    Для именования сервиса (атрибут name) существует ряд соглашений. Они не обязательны к выполнению, но облегчают общее восприятие. Есть некоторое количество стандартных категорий (system, application, network, etc.), которые дописываются к имени через слеш (network/nginx). Также допустимо несколько категорий, например, для разделения типов (application/database/mysql);
  • create_default_instance и single_instance говорят нам о том что сервис имеет только один инстанс и его необходимо создать в выключенном состоянии (enabled='false');
  • dependency — описывает зависимости сервиса. Все зависимости группируются по типу (атрибут groupping). Сервисы из группы «require_all» обязаны все быть online чтобы сервис стартовал. Группа «require_any» требует любой сервис из описанных, «exclude_all» исключает все указанные сервисы, а «optional_all» просто (как я понял) требует чтобы сервис запускался после всех optional_all зависимостей, так как все равно ждет чтобы они загрузились или вышли с ошибкой, или былы выключены.
    Тип перезагрузки сервиса от зависимости опрделеяется атрибутом restart_on и включает следующие значения: «error» — перезапустить если зависимость перезагрузилась из-за аппаратной ошибки, «restart» — перезапустить если зависимость перезагрузили по любой причине (в том числе и аппаратной ошибки), «refresh» — перезапустить если зависимость перезагрузили или обновили. Значение «none» запрещает перезагрузку сервиса не смотря на состояние зависимости;
  • exec_method — различные методы для управления сервисом. Методы start/stop вызываются при включении (enable), выключении (disable) и перезагрузке (restart) сервиса. Метод refresh вызвается при обновлении сервиса (refresh);
  • property_group — определяет набор конфигурационных параметров для сервиса. Тип «framework» отвечает за конфигурацию параметров SMF, а «application» за параметры самого сервиса. В нашем случае передается параметр startd/ignore_error который объясняет рестартеру (англ. вариант restarter) что сервис нужно перезагружать только если все его процессы вышли и игнорировать «кору» и «смертельные сигналы». Параметры в группе «application» используются для настройки инстанса;
  • template — опциональный тег который содержит метаинформацию про сервис.

Как видно сам манифест довольно подробно описывает сервис а также ссылается на внешний скрипт /opt/nginx/svc/nginx который и отвечает, собственно, за запуск сервиса. Разберем теперь его:
#!/sbin/sh
#

. /lib/svc/share/smf_include.sh

# SMF_FMRI is the name of the target service. This allows multiple instances
# to use the same script.

if [ -z $SMF_FMRI ]; then
echo "SMF framework variables are not initialized."
exit $SMF_EXIT_ERR
fi

getproparg() {
val=`svcprop -p $1 $SMF_FMRI`
[ -n "$val" ] && echo $val
}

NGINX_HOME=/opt/nginx
HTTPD="${NGINX_HOME}/sbin/nginx"
CONF_FILE=`getproparg nginx/config`
PIDFILE=`getproparg nginx/pid`

if [ -z $CONF_FILE ]; then
echo "nginx/config property is not set"
exit $SMF_EXIT_ERR_CONFIG
fi

if [ -z $PIDFILE ]; then
echo "nginx/pid property is not set"
exit $SMF_EXIT_ERR_CONFIG
fi

if [ ! -f ${CONF_FILE} ]; then
echo "nginx/config: could not find config file"
exit $SMF_EXIT_ERR_CONFIG
fi

case "$1" in
start)
        $HTTPD -t -c ${CONF_FILE} 2>&1
        if [ $? -ne 0 ]; then
                exit $SMF_EXIT_ERR_CONFIG
        fi
        $HTTPD -c ${CONF_FILE} 2>&1
        ;;
refresh)
        if [ -f "$PIDFILE" ]; then
                /usr/bin/kill -HUP `/usr/bin/cat $PIDFILE`
        fi
        ;;
stop)
        if [ -f "$PIDFILE" ]; then
                /usr/bin/kill -KILL `/usr/bin/cat $PIDFILE`
        fi
        ;;
*)
        echo "Usage: $0 {start|stop|refresh}"
        exit 1
        ;;
esac

exit $SMF_EXIT_OK

Это обычный init.d скрипт с некоторыми вспомогательными функциями. В самом начале включается файл с системными переменными, которые помогут нам в дальнейшем. Одна из них SMF_FRMI — в ней содержится полное имя сервиса. SMF_FRMI используется чтобы получить конфигурационные параметры из манифеста (вспомогательная функция getproparg). Плюс такого подхода будет очевиден позже, когда мы рассмотрим разные инстансы одного сервиса.

Теперь, когда мы имеем полный комплект файлов, сделаем их видимыми системе:
# svccfg -v import nginx.xml
svccfg: Taking "initial" snapshot for svc:/network/nginx:default.
svccfg: Taking "last-import" snapshot for svc:/network/nginx:default.
svccfg: Refreshed svc:/network/nginx:default.
svccfg: Successful import.

У нас появился сервис network/nginx с одним инстансом default, так как в манифесте было прописано «создать инстанс по-умолчанию». Также этот инстанс должен быть создаться в выключенном состоянии, проверим:
# svcs nginx
STATE          STIME    FMRI
disabled       19:11:07 svc:/network/nginx:default

Командам управления не обязательно передавать полный FRMI (англ. Fault Managed Resource Identifier) сервиса, достаточно только имени (например nginx), но если существует несколько сервисов с одинаковым именем, но в разных категориях — то нужно указать полное имя. Также нужно указывать инстанс (nginx:default) если у сервиса более одного инстанса (команда svcs при неполном FRMI выведет статус всех сервисов, которые попадают под сравнение).

Сервис в состоянии disabled не будет подниматься при загрузке ОС. Поэтому его нужно включить:
# svcadm -v enable nginx
svc:/network/nginx:default enabled.

Проверим что сервис запущен:
# svcs nginx
STATE          STIME    FMRI
maintenance    19:26:28 svc:/network/nginx:default

Нас ожидает разочарование — вместо статуса online мы имеем maintenance. Состояние maintenance соответствует некой ошибке в сервисе. SMF переводит сервис в это состояние если стартовый метод возвращает значение отличное от OK или попытка остановить сервис завершилась неудачей три раза подряд. Посмотрим что стало причиной в нашем случае. Для этого посмотрим расширенный статус сервиса:
# svcs -x nginx
svc:/network/nginx:default (Nginx HTTP server)
 State: maintenance since Thu Mar 24 19:26:28 2011
Reason: Start method exited with $SMF_EXIT_ERR_CONFIG.
   See: http://sun.com/msg/SMF-8000-KS
   See: nginx(1M)
   See: /var/svc/log/network-nginx:default.log
Impact: This service is not running.

Как видно, в расширенном статусе также присутствует описание из template манифеста. Нам показывают что стартовый метод завершился с ошибкой и указывают лог где можно посмотреть детальнее. В логе видно следующее:
[ Mar 24 19:26:28 Enabled. ]
[ Mar 24 19:26:28 Executing start method ("/opt/nginx/svc/nginx start") ]
nginx/config: could not find config file
[ Mar 24 19:26:28 Method "start" exited with status 96 ]

Наш стартовый скрипт говорит что не может найти конфигурационный файл. Да, я банально забыл создать его. Попробуем перезапустить сервис после создания файла. Следует отметить что состояние maintenance никак не влияет на enabled/disabled сервиса — это временная остановка. Поэтому нам всего лишь нужно «почистить» сервис, сказать системе что мы его починили:
# svcadm clear nginx
# svcs -x nginx
svc:/network/nginx:default (Nginx HTTP server)
 State: online since Thu Mar 24 19:40:03 2011
   See: nginx(1M)
   See: /var/svc/log/network-nginx:default.log
Impact: None.

# ps -fe | grep nginx
    root  5864     1   0 19:40:04 ?           0:00 /opt/nginx/sbin/nginx -c /opt/nginx/etc/nginx.conf
  nobody  5865  5864   0 19:40:04 ?           0:00 /opt/nginx/sbin/nginx -c /opt/nginx/etc/nginx.conf

Сервис запустился и успешно выполняет свои функции. В логе при этом будет следующее:
[ Mar 24 19:40:03 Leaving maintenance because clear requested. ]
[ Mar 24 19:40:03 Enabled. ]
[ Mar 24 19:40:03 Executing start method ("/opt/nginx/svc/nginx start") ]
the configuration file /opt/nginx/etc/nginx.conf syntax is ok
configuration file /opt/nginx/etc/nginx.conf test is successful
[ Mar 24 19:40:03 Method "start" exited with status 0 ]

Теперь, если сервис неожиданно умрет, SMF автоматически его перезапустит (согласно параметрам startd/ignore_error). Создадим такую ситуацию с помощью kill -9 и посмотрим логи:
[ Mar 24 19:42:25 Stopping because all processes in service exited. ]
[ Mar 24 19:42:25 Executing stop method (:kill) ]
[ Mar 24 19:42:25 Executing start method ("/opt/nginx/svc/nginx start") ]
the configuration file /opt/nginx/etc/nginx.conf syntax is ok
configuration file /opt/nginx/etc/nginx.conf test is successful
[ Mar 24 19:42:25 Method "start" exited with status 0 ]


Дополнительные возможности

Итак, у нас есть сервис, работоспособность которого контролируется на уровне ОС. Но если нам нужно два или три одинаковых сервиса (например несколько postgresql серверов или nginx с разными задачами) то что, писать кучу манифестов? В чем же тогда выгода?
Тут нам на выручку прийдет возможность создавать несколько инстансов одного сервиса. Для этого нам в манифесте нужно убрать теги create_default_instance и single_service, явно создать свой инстанс и перенести туда уникальные параметры:
<instance name='default' enabled='false'>
  <property_group name='nginx' type='application'> 
     <propval name='config' type='astring' 
              value='/opt/nginx/etc/nginx.conf'/> 
     <propval name='pid' type='astring' 
              value='/opt/nginx/var/run/nginx.pid'/> 
  </property_group> 

  <property_group name='startd' type='framework'> 
     <!-- sub-process core dumps shouldn't restart 
          session --> 
     <propval name='ignore_error' type='astring' 
              value='core,signal' /> 
  </property_group> 
</instance>

Нужно перезалить манифест через импорт. SMF определит что поменялось:
# svccfg -v import nginx.xml 
svccfg: Taking "previous" snapshot for svc:/network/nginx:default.
svccfg: Upgrading properties of svc:/network/nginx according to instance "default".
svccfg: svc:/network/nginx: Deleting property group "nginx".
svccfg: svc:/network/nginx: Deleting property group "general".
svccfg: svc:/network/nginx: Deleting property group "startd".
svccfg: Taking "last-import" snapshot for svc:/network/nginx:default.
svccfg: Refreshed svc:/network/nginx:default.
svccfg: Successful import.

В итоге мы получили такое же описание сервиса, только теперь с возможностью настройки нескольких инстансов. Можно было указать прямо в манифесте дополнительный инстанс:
<instance name='monitoring' enabled='false'>
  <property_group name='nginx' type='application'> 
     <propval name='config' type='astring' 
              value='/opt/nginx/etc/nginx-munin.conf'/> 
     <propval name='pid' type='astring' 
              value='/opt/nginx/var/run/nginx-munin.pid'/> 
  </property_group> 

  <property_group name='startd' type='framework'> 
     <!-- sub-process core dumps shouldn't restart 
          session --> 
     <propval name='ignore_error' type='astring' 
              value='core,signal' /> 
  </property_group> 
</instance>

И импортировать:
# svccfg -v import nginx.xml 
svccfg: Taking "previous" snapshot for svc:/network/nginx:default.
svccfg: Taking "previous" snapshot for new service svc:/network/nginx:monitoring.
svccfg: Upgrading properties of svc:/network/nginx according to instance "default".
svccfg: Taking "initial" snapshot for svc:/network/nginx:monitoring.
svccfg: Taking "last-import" snapshot for svc:/network/nginx:monitoring.
svccfg: Taking "last-import" snapshot for svc:/network/nginx:default.
svccfg: Refreshed svc:/network/nginx:monitoring.
svccfg: Refreshed svc:/network/nginx:default.
svccfg: Successful import.

Теперь у нас два инстанса сервиса (заметьте, что default инстанс остался включенным):
# svcs nginx
STATE          STIME    FMRI
disabled       20:16:31 svc:/network/nginx:monitoring
online         20:16:31 svc:/network/nginx:default

Теперь для запуска сервиса прийдется указывать инстанс явно, иначе система нас предупредит:
# svcadm enable nginx
svcadm: Pattern 'nginx' matches multiple instances:
	svc:/network/nginx:monitoring
	svc:/network/nginx:default

# svcadm -v enable nginx:monitoring
svc:/network/nginx:monitoring enabled.

Также инстанс можно добавить без правки манифеста (но манифест уже должен быть настроен для нескольких инстансов и содержать default) с помощью команды svccfg которая служит для изменения параметров сервиса. Грубо говоря, после импорта манифеста исходный файл уже не играет никакой роли, так как он импортируется в базу SMF. Чтобы получить манифест с текущими настройками сервиса можно использовать команду svccfg export. Добавление инстансов «на лету» позволяет автоматизировать процесс:
# svccfg -s nginx add phpfpm
# svccfg -s nginx:phpfpm addpg nginx application
# svccfg -s nginx:phpfpm setprop nginx/config = astring: /opt/nginx/etc/fpm.conf
# svccfg -s nginx:phpfpm setprop nginx/pid = astring: /opt/nginx/run/fpm.pid
# svcadm disable nginx:phpfpm # This will add system properties automatically
# svcs nginx
STATE          STIME    FMRI
disabled       20:37:30 svc:/network/nginx:phpfpm
online         20:16:31 svc:/network/nginx:default
online         20:21:09 svc:/network/nginx:monitoring

Если нужно запускать сервис не от пользователя root — то и тут все возможно. Достаточно добавить
<method_context><method_credential user='munin' group='munin'/></method_context>
в описание инстанса или отдельного метода (если запускать нужно только метод от другого пользователя):
# ps -fe | grep nginx
  munin  6254  1    0 21:10:52 ?   0:00 /opt/nginx/sbin/nginx -c /opt/nginx/etc/nginx-munin.conf
  munin  6255  6254 0 21:10:52 ?   0:00 /opt/nginx/sbin/nginx -c /opt/nginx/etc/nginx-munin.conf 
  root   5884  1    0 19:42:25 ?   0:00 /opt/nginx/sbin/nginx -c /opt/nginx/etc/nginx.conf
  nobody 6015  5884 0 21:05:04 ?   0:00 /opt/nginx/sbin/nginx -c /opt/nginx/etc/nginx.conf

Похожими средствами можно запускать сервисы в отдельных проектах (projects — ограничение по ресурсам). А если к тому же хочется чтобы не только root мог управлять сервисами — то SMF интегрируется с Solaris RBAC!
Пользователю можно назначить как глобальные роли для изменения любых методов, зависимостей, параметров в группах «application»/«framework», так и разрешения на конкретные группы. Для каждой группы можно назначить особые атрибуты (property value) modify_authorization, value_authorization, action_authorization, в которые прописать необходимую «авторизацию» для выполнения операции.
  • action_authorization — я встречал использование только в группе «general» инстанса. В группе хранится атрибут enabled и эта «авторизация» позволяет производить действия с сервисом не записывая никаких данных в манифест. Например refresh, restart, set/clear maintenance
  • value_authorization — позволяет изменять значения атрибутов в группе, но не добавлять/удалять атрибуты. Если добавить эту авторизацию в группу general, то это позволить пользователю изменять атрибут enabled, таким образом он будет иметь возможность делать enable/disable сервису. Если добавить эту авторизацию в группу, скажем, nginx, то это позволит пользователю менять путь к конфигурационному файлу
  • modify_authorization — позволяет изменять, добавлять и удалять атрибуты в группе.
    Для примера покажу использование action/value авторизации для управления сервисом

Добавим сначала пользователю какую-нибудь «авторизацию», я возьму короткую строку, но есть соглашение, по которому авторизация должна называться осмысленно (например solaris.smf.manage.nginx/monitoring). (Также существует некоторое количество предопределенных авторизаций в /etc/security/auth_attr, но RBAC это тема отдельной большой статьи):
# echo "solaris.munin:::Munin authrization::" >> /etc/security/auth_attr
# usermod -A solaris.munin munin

Пока сервис мы никак не конфигурировали и пользователь не может ничего сделать:
munin@sol2 $ /usr/sbin/svcadm restart nginx:monitoring
svcadm: svc:/network/nginx:monitoring: Permission denied.
munin@sol2 $ /usr/sbin/svcadm disable nginx:monitoring
svcadm: svc:/network/nginx:monitoring: Permission denied.

Добавим возможность перезагружать сервис для «авторизации» solaris.munin:
# svccfg -s nginx:monitoring setprop general/action_authorization = astring: solaris.munin
# svcadm refresh nginx:monitoring

(После любых изменений в манифесте следует делать refresh сервису, чтобы SMF перечитал конфигурацию для текущего сервиса)

Проверяем:
munin@sol2 $ /usr/sbin/svcadm -v restart nginx:monitoring
Action restart set for svc:/network/nginx:monitoring.
munin@sol2 $ /usr/sbin/svcadm -v disable nginx:monitoring
svcadm: svc:/network/nginx:monitoring: Couldn't modify "general" property group (permission denied).

Видно, что перезагружать сервис пользователь может, но изменить свойство в базе SMF — нет. Такой подход позволяет пользователю перезагружать «свои» сервисы при изменении конфигурационного файла. Если пользователю нужно дать возможность перманентно останавливать сервис, то дадим ему возможность modify «general» property group:
# svccfg -s nginx:monitoring setprop general/value_authorization = astring: solaris.munin
# svcadm refresh nginx:monitoring

Проверяем:
munin@sol2 $ /usr/sbin/svcadm -v disable nginx:monitoring
svc:/network/nginx:monitoring disabled.
munin@sol2 $ /usr/sbin/svcadm -v enable nginx:monitoring
svc:/network/nginx:monitoring enabled.


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

Надеюсь что это статья поможет кому-то лучше понять что представляет из себя SMF, а то и подвигнет к более глубокому изучению SMF, RBAC и системы Solaris.
Tags:
Hubs:
+22
Comments 5
Comments Comments 5

Articles