Red Hat Insightsの説明 (Part 3)

第二部:Red Hat Insights の少し突っ込んだ話

Red Hat Enterprise Linux 8.1がリリースされて、更に注目が集まっており、様々な機能拡張とともにRed Hat Insightsの話題も取り上げられるようになってきています。前回は、Red Hat Insights のクライアント側の話をしました。今回は、Red Hat Insights のinsights-Engine 及びその他のコンポーネントに関しての説明を行います。

7. insights Engine (insights-coreフレームワーク)の説明

ホストの情報がinsights-clientによって収集されると、前章で紹介したInsightsのAPI(https://cert-api.access.redhat.com/r/insights)を通して、Red Hatの提供するinsights Engine(insights-core)に転送されます。insights Engineは、Red HatでホストされるSaaSアプリケーションです。insights Engineは、受信した情報を処理し、ユーザにわかりやすい形で結果をユーザインターフェイスに出力します。

insights Engine(以下insights-core)は、前章の例で示したような情報(tar.gzでアーカイブされているもの)を解凍・展開し、各情報を識別して処理を開始します。insights-coreでは、このアーカイブされていたデータのような、様々な書式の情報(コマンドからの出力や、色々な形式の設定ファイル等)を処理するために、APIを使用してデータをオブジェクトに変換します。
更に、Pluginを用いてデータを分析し、ユーザにわかりやすい形での結果を出力しています。

7.1 insights-coreのプラグイン

プラグインには次の3つの種類があります。

  1. Parser PluginsParser プラグインは、rawデータを分析し、Combiners Plugin / Rules Pluginで使用出来る形式にデータを変換します。 通常、各Parser プラグインは、特定のデータセットの解析を担当します。 たとえば、 Mount Parserプラグイン( insights.parsers.mount.Mount )はmountコマンドの出力を変換し、 FSTAb Parser プラグイン( insights.parsers.fstab.FSTab )は/ etc / fstab /ファイルの内容を解析して変換しています。
  2. Combiner PluginsCombiner プラグインではinsights-clientが取得してきた情報を集約します。わかりやすい例で言うと、Red Hat Enterprise Linuxのリリース番号(7.4や8.0等)は、/ etc / redhat_releaseの情報からも、”uname -a”の実行結果からも取得できます。redhat_release Combiner プラグイン( insights.combiners.redhat_release)では、両方のパーサー( insights.parsers.redhat_release.RedhatRelease 、およびinsights.parsers.uname.Uname )の結果からメジャーリリース番号とマイナーリリース番号を判別します。Combinerでは最初に優先される情報を使用し、最初の情報が利用できない場合に2番目の情報を使用します。上記のリリース番号の例で言うと、redhat_release Combinerプラグインでは
    ---snip---
           if uname and uname.redhat_release.major != -1:
                self.major = uname.redhat_release.major
                self.minor = uname.redhat_release.minor
                self.rhel = '{0}.{1}'.format(self.major, self.minor)
            elif rh_rel and rh_rel.is_rhel:
                self.major = rh_rel.major
                self.minor = rh_rel.minor
                self.rhel = rh_rel.version
    ---snip---
    

    となっており、unameの出力情報を/etc/redhat_releaseの情報よりも優先しています。

  3. Rule PluginsRule プラグインでは、ParserとCombinerによって利用可能になった情報の分析を行います。Ruleでは、システムになんらかの(障害などにつながる)状態が存在するか、また将来発生する可能性が高いか等を判断することができます。 たとえば、ファイル/ etc / ssh / sshd_configの特定の設定でRed Hat Enterprise Linux 7.1を使用しているときに特定のssh脆弱性が存在する場合、Ruleはシステムが7.1かどうかをRed Hat Release Combinerの情報を元に確認し、次に sshd_configファイルの該当する設定を確認します。これら両方の結果が当てはまった場合、ルールは結果のレポートと、脆弱性及び解決方法に関する情報を出力します。この結果はエンジンによって統合され、カスタマーインターフェイスに表示されます。

7.2 insights-coreチュートリアル

Fedora30でinsights-coreをチュートリアルに従ってインストール/開発を行い、大体の動きを掴んでみましょう。こちらの「Insights-core-tutorials」のドキュメントに従います。

チュートリアルに従って行うと全体像が見えてきますが、insights-coreでどのようにRuleを用いて処理を行っているかの概要だけを掴みたい場合には、ここを飛ばして7.3を確認してもいいでしょう。7.2の環境はチュートリアル専用の環境のため、7.3の環境とは、別環境になります。

7.2.1 環境構築

チュートリアルガイドに従って環境を構築していきます。ここではFedora30の/home/sios/work/以下を環境として構築していきます。

  1. git clone
  2. work/insights-core-tutorialsに移動します。
  3. setup_env.shファイルではPython 3.6を使用することになっていますが、Fedora 30ではPython 3.7になっていますので、setup_env.shファイルを修正し3.6に合わせます。
  4. ./setup_env.sh を実行します。この際、下記のようなエラーが出力されることがあります。
    ============================================================= ERRORS =============================================================
    _________________________________ ERROR collecting insights_examples/rules/tests/integration.py __________________________________
    insights_examples/rules/tests/integration.py:6: in pytest_generate_tests
        pattern = pytest.config.getoption("-k")
    lib/python3.6/site-packages/_pytest/main.py:187: in __getattr__
        warnings.warn(PYTEST_CONFIG_GLOBAL, stacklevel=2)
    E   pytest.PytestDeprecationWarning: the `pytest.config` global is deprecated.  Please use `request.config` or `pytest_configure` (if you're a pytest plugin) instead.
    ======================================================== warnings summary ========================================================
    lib64/python3.6/distutils/__init__.py:4
      /home/sios/work/insights-core-tutorials/lib64/python3.6/distutils/__init__.py:4: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses
        import imp
    
    -- Docs: https://docs.pytest.org/en/latest/warnings.html
    ==================================================== short test summary info =====================================================
    FAILED insights_examples/rules/tests/integration.py - pytest.PytestDeprecationWarning: the `pytest.config` global is deprecated...
    !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Interrupted: 1 errors during collection !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    ============================================== 1 warnings, 1 error in 0.35 seconds ===============================================
    

    この場合には、lib/python3.6/site-packages/_pytest/main.pyを編集し、下記の行を削除します。

    [sios@fc30 _pytest]$ diff -Nru main.py.org main.py
    --- main.py.org    2019-08-14 10:54:00.869161072 +0900
    +++ main.py    2019-08-14 10:54:15.922120389 +0900
    @@ -195,10 +195,6 @@
             return "{}({!r})".format(type(self).__name__, self._config)
     
     
    -def pytest_configure(config):
    -    __import__("pytest").config = _ConfigDeprecated(config)  # compatibility
    -
    -
     def wrap_session(config, doit):
         """Skeleton command line program"""
         session = Session(config)
    
  5. pytestまで成功したら、ほぼ準備は完了です。
    *** Running pytest ***
    ====================================================== test session starts =======================================================
    platform linux -- Python 3.6.8, pytest-3.6.0, py-1.8.0, pluggy-0.6.0
    rootdir: /home/sios/work/insights-core-tutorials, inifile: setup.cfg
    plugins: cov-2.4.0
    collected 11 items                                                                                                               
    
    insights_examples/combiners/tests/test_hostname_uh.py .                                                                    [  9%]
    insights_examples/parsers/tests/test_secure_shell.py ...                                                                   [ 36%]
    insights_examples/rules/tests/integration.py .......                                                                       [100%]
    
    =================================================== 11 passed in 0.33 seconds ====================================================
    
  6. “source ~/work/insights-core-tutorials/bin/activate”を実行して環境変数類を読み込みます。以降、下のようなプロンプトになります。
    (insights-core-tutorials) [sios@fc30 tests]$
    

7.2.2 カスタムParserプラグインの作成

まず最初にカスタムのParser プラグイン(以下Parser)を作成してみます。念の為に復習ですが、ParserはInsights-clientが収集してきたファイルやコマンド実行結果などを、Combined プラグインやRules プラグインで取り扱える形式にするための物です。

今回は、サンプルとして”SSHの設定”を扱うParserを作成します。

尚、サンプルのParserはwork/insights-core-tutorials/insights_examples以下にあります。今回は~/work/insights-core-tutorials/mycomponents/parsers以下にカスタムのParserを作成してみます。

ここでは、sshd_configとして下記のようなものをサンプルにしています。

#   $OpenBSD: sshd_config,v 1.93 2014/01/10 05:59:19 djm Exp $

Port 22
#AddressFamily any
ListenAddress 10.110.0.1
#ListenAddress ::

# The default requires explicit activation of protocol 1
#Protocol 2

このファイルをParseするParserを作成します。

    1. ~/work/insights-core-tutorials/mycomponents/parsers/secure_shell.pyとして、以下のようなParserのファイルを作成します。
from insights import Parser, parser
from insights.specs import Specs


@parser(Specs.sshd_config)
class SSHDConfig(Parser):

    def parse_content(self, content):
        pass
    1. ~/work/insights-core-tutorials/mycomponents/parsers/tests/test_secure_shell.pyとして、以下のようなParserのテストファイルを作成します。
from mycomponents.parsers.secure_shell import SSHDConfig

def test_sshd_config():
    pass
    1. ~/work/insights-core-tutorials ディレクトリでpytestを用いてテストします。
(insights-core-tutorials) [sios@fc30 tests]$ pytest -k secure_shell
====================================================== test session starts =======================================================
platform linux -- Python 3.6.8, pytest-3.6.0, py-1.8.0, pluggy-0.6.0
rootdir: /home/sios/work/insights-core-tutorials, inifile: setup.cfg
plugins: cov-2.4.0
collected 6 items / 2 deselected                                                                                                 

insights_examples/parsers/tests/test_secure_shell.py ...                                                                   [ 75%]
mycomponents/parsers/tests/test_secure_shell.py .                                                                          [100%]

============================================= 4 passed, 2 deselected in 0.29 seconds =============================================
(insights-core-tutorials) [sios@fc30 insights-core-tutorials]$
    1. 次に、Parserのテスト用のファイル(test_secure_shell.py)を以下のようにSSHD_CONFIGの設定ファイルを読み込ませる形で作成します。
from mycomponents.parsers.secure_shell import SSHDConfig
from insights.tests import context_wrap

SSHD_CONFIG_INPUT = """
#    $OpenBSD: sshd_config,v 1.93 2014/01/10 05:59:19 djm Exp $

Port 22
#AddressFamily any
ListenAddress 10.110.0.1
Port 22
ListenAddress 10.110.1.1
#ListenAddress ::

# The default requires explicit activation of protocol 1
#Protocol 2
Protocol 1
"""


def test_sshd_config():
    sshd_config = SSHDConfig(context_wrap(SSHD_CONFIG_INPUT))
    assert sshd_config is not None
    assert 'Port' in sshd_config
    assert 'PORT' in sshd_config
    assert sshd_config['port'] == ['22', '22']
    assert 'ListenAddress' in sshd_config
    assert sshd_config['ListenAddress'] == ['10.110.0.1', '10.110.1.1']
    assert sshd_config['Protocol'] == ['1']
    assert 'AddressFamily' not in sshd_config
    ports = [l for l in sshd_config if l.keyword == 'Port']
    assert len(ports) == 2
    assert ports[0].value == '22'

以下、このテストファイルの説明になります。

      1. まず最初の
        from mycomponents.parsers.secure_shell import SSHDConfig
        from insights.tests import context_wrap
        

        の所で、”context_wrap”を用いて、入力されたデータを”Context”オブジェクトに渡せるようにします。

      2. 次に、テスト用のデータ(sshd_configのサンプル)をSSHD_CONFIG_INPUTとして
        SSHD_CONFIG_INPUT = """
        #    $OpenBSD: sshd_config,v 1.93 2014/01/10 05:59:19 djm Exp $
        
        Port 22
        #AddressFamily any
        ListenAddress 10.110.0.1
        Port 22
        ListenAddress 10.110.1.1
        #ListenAddress ::
        
        # The default requires explicit activation of protocol 1
        #Protocol 2
        Protocol 1
        """
        

        のように入力します。

      3. 次に、テスト用のデータセットとして
        def test_sshd_config():
            sshd_config = SSHDConfig(context_wrap(SSHD_CONFIG_INPUT))
        

        を定義します。

      4. 最後に
           assert sshd_config is not None
            assert 'Port' in sshd_config
            assert 'PORT' in sshd_config
            assert sshd_config['port'] == ['22', '22']
            assert 'ListenAddress' in sshd_config
            assert sshd_config['ListenAddress'] == ['10.110.0.1', '10.110.0.1']
            assert sshd_config['Protocol'] == ['1']
            assert 'AddressFamily' not in sshd_config
            ports = [l for l in sshd_config if l.keyword == 'Port']
            assert len(ports) == 2
            assert ports[0].value == '22'
        

        としてキーワードでテストを行っています。

    1. 次に、このテストファイルを実行できるように、パーサファイル(parsers/secure_shell.py)の方を下記のように修正します。
from collections import namedtuple
 from insights import Parser, parser, get_active_lines
 from insights.core.spec_factory import SpecSet, simple_file
 import os

 class LocalSpecs(SpecSet):
     """ Datasources for collection from local host """
     conf_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'sshd_config')

     sshd_config = simple_file(conf_file)

 @parser(LocalSpecs.sshd_config)
 class SSHDConfig(Parser):

     KeyValue = namedtuple('KeyValue', ['keyword', 'value', 'kw_lower'])

     def parse_content(self, content):
         self.lines = []
         for line in get_active_lines(content):
             kw, val = line.split(None, 1)
             self.lines.append(self.KeyValue(kw.strip(), val.strip(), kw.lower().strip()))
         self.keywords = set([k.kw_lower for k in self.lines])

     def __contains__(self, keyword):
         return keyword.lower() in self.keywords

     def __iter__(self):
         for line in self.lines:
             yield line

     def __getitem__(self, keyword):
         kw = keyword.lower()
         if kw in self.keywords:
             return [kv.value for kv in self.lines if kv.kw_lower == kw]

それぞれの意味は下記のようになります。

      1. まず、Parserファイルの方で”get_active_lines()” と”namedtuples”が使えるようにします。”get_active_lines()”は、空白行とコメント行を削除して有効な行だけを抽出するものです。
        from collections import namedtuple
         from insights import Parser, parser, get_active_lines
         from insights.core.spec_factory import SpecSet, simple_file
         import os
        
      2. 通常/etc/ssh/sshd_configファイルにアクセスするためにはroot権限が必要になりますが、ここではローカルに作成したsshd_configファイルを呼び出すため、SpecSetクラスを呼び出してLocalSpecsとしてローカルの(テスト用の)sshd_configファイルを呼び出すことにします。
        class LocalSpecs(SpecSet):
             """ Datasources for collection from local host """
             conf_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'sshd_config')
        
             sshd_config = simple_file(conf_file)
        
      3. 上で書いたローカルの(テスト用の)sshd_configファイルをmycomponents/parserディレクトリにコピーしておきます。
        (insights-core-tutorials) [sios@fc30 insights-core-tutorials]$ cp ./insights_examples/parsers/sshd_config ./mycomponents/parsers/
        (insights-core-tutorials) [sios@fc30 insights-core-tutorials]$ ls mycomponents/parsers/
        __init__.py  __pycache__  secure_shell.py  sshd_config  tests
        
      4. namedtupleを用いて格納している情報に簡単にアクセスできるようにしておきます。
            KeyValue = namedtuple('KeyValue', ['keyword', 'value', 'kw_lower'])
        
      5. 今回のパーサでは、全ての行(self.line)をnamedtupleに格納しています。
         def parse_content(self, content):
                 self.lines = []
                 for line in get_active_lines(content):
                     kw, val = line.split(None, 1)
                     self.lines.append(self.KeyValue(kw.strip(), val.strip(), kw.lower().strip()))
                 self.keywords = set([k.kw_lower for k in self.lines])
        
      6. 最後に、いくつかの「dunder」メソッドを実装して、クラスの使用を簡素化します。 __contains__は、キーワードチェックのin演算子を有効にします。 __iter__は、self.linesの内容に対する反復を可能にします。 __getitem__を使用すると、キーワードのすべての値にアクセスできます。
         def __contains__(self, keyword):
                 return keyword.lower() in self.keywords
        
             def __iter__(self):
                 for line in self.lines:
                     yield line
        
             def __getitem__(self, keyword):
                 kw = keyword.lower()
                 if kw in self.keywords:
                     return [kv.value for kv in self.lines if kv.kw_lower == kw]
        
    1. 最終的な(コメントも入れた)パーサファイルは以下のようになります。
"""
 secure_shell - Files for configuration of `ssh`
 ===============================================

 The ``secure_shell`` module provides parsing for the ``sshd_config``
 file.  The ``SSHDConfig`` class implements the parsing and
 provides a ``list`` of all configuration lines present in
 the file.

 Sample content from the ``/etc/sshd/sshd_config`` file is::

     #       $OpenBSD: sshd_config,v 1.93 2014/01/10 05:59:19 djm Exp $

         Port 22
     #AddressFamily any
     ListenAddress 10.110.0.1
    Port 22
     ListenAddress 10.110.1.1
     #ListenAddress ::
    
     # The default requires explicit activation of protocol 1
     #Protocol 2
     Protocol 1

 Examples:
     >>> 'Port' in sshd_config
     True
     >>> 'PORT' in sshd_config  # items are stored case-insensitive
     True
     >>> 'AddressFamily' in sshd_config  # comments are ignored
     False
     >>> sshd_config['port']  # All value stored by keyword in lists
     ['22', '22']
     >>> sshd_config['Protocol']  # Single items have one list element
     ['1']
     >>> [line for line in sshd_config if line.keyword == 'Port']  # can be used as an iterator
     [KeyValue(keyword='Port', value='22', kw_lower='port'), KeyValue(keyword='Port', value='22', kw_lower='port')]
     >>> sshd_config.last('ListenAddress')  # Easy way of finding the current configuration for a single item
     '10.110.1.1'
 """
 from collections import namedtuple
 from insights import Parser, parser, get_active_lines
 from insights.specs import Specs
 from insights.core.spec_factory import SpecSet, simple_file
 import os

 class LocalSpecs(SpecSet):
     """ Datasources for collection from local host """
     conf_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'sshd_config')
     sshd_config = simple_file(conf_file)

 @parser(LocalSpecs.sshd_config)
 class SSHDConfig(Parser):
     """Parsing for ``sshd_config`` file.
    
     Attributes:
         lines (list): List of `KeyValue` namedtupules for each line in
             the configuration file.
         keywords (set): Set of keywords present in the configuration
             file, each keyword has been converted to lowercase.
     """
    
     KeyValue = namedtuple('KeyValue', ['keyword', 'value', 'kw_lower'])
     """namedtuple: Represent name value pair as a namedtuple with case ."""
    
     def parse_content(self, content):
         self.lines = []
         for line in get_active_lines(content):
             kw, val = (w.strip() for w in line.split(None, 1))
             self.lines.append(self.KeyValue(kw, val, kw.lower()))
         self.keywords = set([k.kw_lower for k in self.lines])
    
     def __contains__(self, keyword):
         return keyword.lower() in self.keywords
    
     def __iter__(self):
         for line in self.lines:
             yield line
    
     def __getitem__(self, keyword):
         kw = keyword.lower()
         if kw in self.keywords:
             return [kv.value for kv in self.lines if kv.kw_lower == kw]
    
     def last(self, keyword):
         """str: Returns the value of the last keyword found in config."""
         entries = self.__getitem__(keyword)
         if entries:
             return entries[-1]
    1. pytestコマンドでエラーが出力されなければ、作成は完了です。
(insights-core-tutorials) [sios@fc30 insights-core-tutorials]$ pytest -k secure_shell
====================================================== test session starts =======================================================
platform linux -- Python 3.7.4, pytest-3.6.0, py-1.8.0, pluggy-0.6.0
rootdir: /home/sios/work/insights-core-tutorials, inifile: setup.cfg
plugins: cov-2.4.0
collected 8 items / 2 deselected                                                                                            

insights_examples/parsers/tests/test_secure_shell.py ...                                                                   [ 50%]
mycomponents/parsers/tests/test_secure_shell.py ...                                                                        [100%]

============================================= 6 passed, 2 deselected in 0.28 seconds =============================================

7.2.3 カスタムCombinerプラグインの作成

次にカスタムのCombiner プラグイン(以下Combiner)を作成してみます。今回は、サンプルとして”HOSTNAMEを取得する” Combiner プラグインを作成します。

尚、サンプルのCombiner プラグインはwork/insights-core-tutorials/insights_examples以下にあります。今回は~/work/insights-core-tutorials/mycomponents/combiners以下にカスタムのCombiner プラグインを作成してみます。

    1. ~/work/insights-core-tutorials/mycomponents/combiners/hostname_uh.pyとして、以下のようなプラグインのファイルを作成します。最初にcombinerをインポートした後に、Hostname(/usr/bin/hostnameの結果)Uname(/usr/bin/uname -aの結果)をインポートします。
from insights.core.plugins import combiner
from insights.parsers.hostname import Hostname
from insights.parsers.uname import Uname


@combiner([Hostname, Uname])
class HostnameUH(object):

    def __init__(self, hostname, uname):
        pass
    1. ~/work/insights-core-tutorials/mycomponents/combiners/tests/test_hostname_uh.pyとして、以下のようなテスト用のファイルを作成します。
from mycomponents.combiners.hostname_uh import HostnameUH


def test_hostname_uh():
    pass
    1. ”pytest -k hostname_uh”を実行して確認します。
(insights-core-tutorials) [sios@fc30 insights-core-tutorials]$ pytest -k hostname_uh
====================================================== test session starts =======================================================
platform linux -- Python 3.7.4, pytest-3.6.0, py-1.8.0, pluggy-0.6.0
rootdir: /home/sios/work/insights-core-tutorials, inifile: setup.cfg
plugins: cov-2.4.0
collected 9 items / 7 deselected                                                                                                 

insights_examples/combiners/tests/test_hostname_uh.py .                                                                    [ 50%]
mycomponents/combiners/tests/test_hostname_uh.py .                                                                         [100%]

============================================= 2 passed, 7 deselected in 0.27 seconds =============================================
    1. では、Combinerのテスト用のファイル(test_hostname_uh.py)を以下のようにします。
from mycomponents.combiners.hostname_uh import HostnameUH
from insights.parsers.hostname import Hostname
from insights.parsers.uname import Uname
from insights.tests import context_wrap

HOSTNAME = "hostone_h.example.com"
UNAME = "Linux hostone_u.example.com 3.10.0-693.21.1.el7.x86_64 #1 SMP Fri Feb 23 18:54:16 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux"


def test_hostname_uh():
    hostname = Hostname(context_wrap(HOSTNAME))
    uname = Uname(context_wrap(UNAME))

    hostname_uh = HostnameUH(hostname, None)
    assert hostname_uh.hostname == HOSTNAME

    hostname_uh = HostnameUH(None, uname)
    assert hostname_uh.hostname == "hostone_u.example.com"

    hostname_uh = HostnameUH(hostname, uname)
    assert hostname_uh.hostname == HOSTNAME

それぞれの意味は下記のようになります。

      1. context_wrapをインポートし、入力されたデータを”Context”オブジェクトに渡せるようにします。
        from insights.combiners.hostname_uh import HostnameUH
         from insights.parsers.hostname import Hostname
         from insights.parsers.uname import Uname
         from insights.tests import context_wrap
        
      2. HOSTNAMEとUNAMEにサンプルで出力データを入力します。
        HOSTNAME = "hostone_h.example.com"
        UNAME = "Linux hostone_u.example.com 3.10.0-693.21.1.el7.x86_64 #1 SMP Fri Feb 23 18:54:16 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux"
        
      3. 必要なパーサクラスセットを生成するコードを追加します。
        def test_hostname_uh():
            hostname = Hostname(context_wrap(HOSTNAME))
            uname = Uname(context_wrap(UNAME))
        
      4. テスト部分は以下のようになります。
        hostname_uh = HostnameUH(hostname, None)
        assert hostname_uh.hostname == HOSTNAME
        
        hostname_uh = HostnameUH(None, uname)
        assert hostname_uh.hostname == "hostone_u.example.com"
        
        hostname_uh = HostnameUH(hostname, uname)
        assert hostname_uh.hostname == HOSTNAME
        
    1. 最終的なCombinerコード(hostname_uh.py)は以下のようになります。
from insights.core.plugins import combiner
from insights.parsers.hostname import Hostname
from insights.parsers.uname import Uname


@combiner([Hostname, Uname])
class HostnameUH(object):

    def __init__(self, hostname, uname):
        if hostname:
            self.hostname = hostname.fqdn
        else:
            self.hostname = uname.nodename
    1. pytestコマンドでエラーが出力されなければ、作成は完了です。
(insights-core-tutorials) [sios@fc30 insights-core-tutorials]$ pytest -k hostname_uh
====================================================== test session starts =======================================================
platform linux -- Python 3.7.4, pytest-3.6.0, py-1.8.0, pluggy-0.6.0
rootdir: /home/sios/work/insights-core-tutorials, inifile: setup.cfg
plugins: cov-2.4.0
collected 9 items / 7 deselected                                                                                                 

insights_examples/combiners/tests/test_hostname_uh.py .                                                                    [ 50%]
mycomponents/combiners/tests/test_hostname_uh.py .                                                                         [100%]

============================================= 2 passed, 7 deselected in 0.30 seconds =============================================

7.2.4 カスタムRulesプラグインの作成

最後にカスタムのRulesプラグイン(以下Rules)を作成してみます。今回は、今までに作ってきたsshd_configとホスト名からホストのsshdの情報を取得するRulesプラグインを作成します。

尚、サンプルのRules プラグインはwork/insights-core-tutorials/insights_examples以下にあります。今回は~/work/insights-core-tutorials/mycomponents/rules以下にカスタムのRules プラグインを作成してみます。

    1. ~/work/insights-core-tutorials/mycomponents/rules/sshd_secure.pyとして、以下のようなプラグインのファイルを作成します。
from insights.core.plugins import make_fail, rule
from mycomponents.parsers.secure_shell import SSHDConfig

ERROR_KEY = "SSHD_SECURE"


@rule(SSHDConfig)
def report(sshd_config):
    """
    1. Evaluate config file facts
    2. Evaluate version facts
    """
    if results_found:
        return make_fail(ERROR_KEY, results=the_results)

このファイルの説明は以下になります。

      1. 最初に、 make_fail(), ruleをインポートし、先のパーサで作成したSSHDConfigをインポートします。
        from insights.core.plugins import make_fail, rule
        from mycomponents.parsers.secure_shell import SSHDConfig
        
      2. 次に、ルールの実行時にinsights-coreによって収集され、実行結果で提供されるユニークな文字列ERROR_KEYとして”SSHD_SECURE”を設定します。
        ERROR_KEY = "SSHD_SECURE"
        
      3. @rule()の引数は、ルール処理に必要なパーサーとCombinerで構成されます。
        @rule(SSHDConfig)
        
    1. 最終的なRuleファイル(sshd_secure.py)は以下のようになります。
from insights.core.plugins import make_fail, rule
from insights.parsers.secure_shell import SSHDConfig
from insights.parsers.installed_rpms import InstalledRpms

ERROR_KEY = "SSHD_SECURE"

 # Jinga template displayed for make_fail results
 CONTENT =  ERROR_KEY + """
 :{
                 {% for key, value in errors.items() -%}
                     {{key}}: {{value}}
                 {% endfor -%} }
 OPEN_SSH_PACKAGE: {{openssh}}""".strip()


def check_auth_method(sshd_config, errors):
    auth_method = sshd_config.last('AuthenticationMethods')
    if auth_method:
        if auth_method.lower() != 'publickey':
            errors['AuthenticationMethods'] = auth_method
    else:
        errors['AuthenticationMethods'] = 'default'
    return errors


def check_log_level(sshd_config, errors):
    log_level = sshd_config.last('LogLevel')
    if log_level:
        if log_level.lower() != 'verbose':
            errors['LogLevel'] = log_level
    else:
        errors['LogLevel'] = 'default'
    return errors


def check_permit_root(sshd_config, errors):
    permit_root = sshd_config.last('PermitRootLogin')
    if permit_root:
        if permit_root.lower() != 'no':
            errors['PermitRootLogin'] = permit_root
    else:
        errors['PermitRootLogin'] = 'default'
    return errors


def check_protocol(sshd_config, errors):
    # Default Protocol is 2 if not specified
    protocol = sshd_config.last('Protocol')
    if protocol:
        if protocol.lower() != '2':
            errors['Protocol'] = protocol
    return errors


@rule(InstalledRpms, SSHDConfig)
def report(installed_rpms, sshd_config):
    errors = {}
    errors = check_auth_method(sshd_config, errors)
    errors = check_log_level(sshd_config, errors)
    errors = check_permit_root(sshd_config, errors)
    errors = check_protocol(sshd_config, errors)

    if errors:
        openssh_version = installed_rpms.get_max('openssh')
        return make_fail(ERROR_KEY, errors=errors, openssh=openssh_version.package)
    1. 対応するテストファイル(integration_test.py)は以下のようになります。
from mycomponents.rules import sshd_secure
from insights.tests import InputData, archive_provider, context_wrap
from insights.core.plugins import make_fail
from insights.specs import Specs
# The following imports are not necessary for integration tests
from mycomponents.parsers.secure_shell import SSHDConfig

OPENSSH_RPM = """
openssh-6.6.1p1-31.el7.x86_64
openssh-6.5.1p1-31.el7.x86_64
""".strip()

EXPECTED_OPENSSH = "openssh-6.6.1p1-31.el7"

GOOD_CONFIG = """
AuthenticationMethods publickey
LogLevel VERBOSE
PermitRootLogin No
# Protocol 2
""".strip()

BAD_CONFIG = """
AuthenticationMethods badkey
LogLevel normal
PermitRootLogin Yes
Protocol 1
""".strip()

DEFAULT_CONFIG = """
# All default config values
""".strip()



@archive_provider(sshd_secure.report)
def integration_tests():
    """
    InputData acts as the data source for the parsers
    so that they may execute and then be used as input
    to the rule.  So this is essentially an end-to-end
    test of the component chain.
    """
    input_data = InputData("GOOD_CONFIG")
    input_data.add(Specs.sshd_config, GOOD_CONFIG)
    input_data.add(Specs.installed_rpms, OPENSSH_RPM)
    yield input_data, None

    input_data = InputData("BAD_CONFIG")
    input_data.add(Specs.sshd_config, BAD_CONFIG)
    input_data.add(Specs.installed_rpms, OPENSSH_RPM)
    errors = {
        'AuthenticationMethods': 'badkey',
        'LogLevel': 'normal',
        'PermitRootLogin': 'Yes',
        'Protocol': '1'
    }
    expected = make_fail(sshd_secure.ERROR_KEY,
                             errors=errors,
                             openssh=EXPECTED_OPENSSH)
    yield input_data, expected

    input_data = InputData("DEFAULT_CONFIG")
    input_data.add(Specs.sshd_config, DEFAULT_CONFIG)
    input_data.add(Specs.installed_rpms, OPENSSH_RPM)
    errors = {
        'AuthenticationMethods': 'default',
        'LogLevel': 'default',
        'PermitRootLogin': 'default'
    }
    expected = make_fail(sshd_secure.ERROR_KEY,
                             errors=errors,
                             openssh=EXPECTED_OPENSSH)
    yield input_data, expected
    1. ルールファイルは”insights-run”コマンドで確認できます。”insights-run -p mycomponents/rules/sshd_secure.py”を実行した結果が以下になります。
(insights-core-tutorials) [sios@fc30 insights-core-tutorials]$ insights-run -p mycomponents/rules/sshd_secure.py
Install colorama if console colors are preferred.
---------
Progress:
---------
F

--------------
Rules Executed
--------------
[FAIL] mycomponents.rules.sshd_secure.report
--------------------------------------------
SSHD_SECURE:{
                 AuthenticationMethods: default
                 LogLevel: default
                 PermitRootLogin: default
                 Protocol: 1
                 }
OPEN_SSH_PACKAGE: openssh-8.0p1-5.fc30


----------------------
Rule Execution Summary
----------------------
Passed      : 0
Failed      : 1
Info        : 0
Missing Deps: 0
Fingerprint : 0
Metadata    : 0
Metadata Key: 0
Exceptions  : 0

7.3 insights-core rules単体のテスト

Fedora 30でinsights-coreを手順に従ってインストールし、テストしてみましょう。ドキュメントはこちら(https://insights-core.readthedocs.io/en/latest/)の「Quickstart Insights Development」に従っています。

(前章のチュートリアルの環境とは異なる環境のため、新たに作り直す必要があります。前章のチュートリアルを飛ばした方は、前章の環境は使いませんので、この小から新たに作成して下さい)。

7.3.1 環境構築

以下、~/project_dirを作業用ディレクトリとします。

    1. 事前準備として、Fedora30にdnfを用いて以下のパッケージをインストールします。
gcc, git, python3, python3-pip, unzip, pandoc, virtualenv
    1. insights-coreをgitを用いて取得してきます。
[sios@fc30 ~]$ cd project_dir/
[sios@fc30 project_dir]$ git clone http://github.com/RedHatInsights/insights-core
Cloning into 'insights-core'...

--省略--
Resolving deltas: 100% (143/143), done.
[sios@fc30 project_dir]$
    1. insights-coreディレクトリ下でPythonの仮想環境を構築するvirtualenvを使用します。
[sios@fc30 project_dir]$ cd insights-core/
[sios@fc30 insights-core]$ virtualenv -p python3 .
Already using interpreter /usr/bin/python3
Using base prefix '/usr'
New python executable in /home/sios/project_dir/insights-core/bin/python3
Also creating executable in /home/sios/project_dir/insights-core/bin/python
    Installing setuptools, pip, wheel...done.
    1. Pythonの仮想環境を確認します。
[sios@fc30 insights-core]$ source bin/activate
(insights-core) [sios@fc30 insights-core]$ python --version
Python 3.7.3
(insights-core) [sios@fc30 insights-core]$ which python
~/project_dir/insights-core/bin/python
(insights-core) [sios@fc30 insights-core]$
    1. insights-coreプロジェクトとパッケージをpipでダウンロードします。
(insights-core) [sios@fc30 insights-core]$ bin/pip install -e .[develop]
Obtaining file:///home/sios/project_dir/insights-core
Collecting defusedxml (from insights-core==3.0.8)
--省略--
Running setup.py develop for insights-core
Successfully installed MarkupSafe-1.1.1 Pygments-2.4.2 (省略) webencodings-0.5.1
(insights-core) [sios@fc30 insights-core]$
    1. insights-runコマンドが実行できることを”insights-run –help”で確認します。
(insights-core) [sios@fc30 insights-core]$ insights-run --help
usage: insights-run [-h] [-p PLUGINS] [-c CONFIG] [-i INVENTORY] [-v]
                    [-f FORMAT] [-s] [-D] [--context CONTEXT] [-m] [-t] [-d]
                    [-F]
                    [archive]
    --省略
  -F, --fail-only   Show FAIL results only. Conflict with '-m' or '-f',
                        will be dropped when using them together
(insights-core) [sios@fc30 insights-core]$
    1. 独自のruleを作成してテストするために、~/project_dir以下にmyrulesディレクトリを作成し、ディレクトリ内に空の__init__.pyファイルを作成します。これ(__init__.pyファイル)により、そのディレクトリがPythonパッケージとして扱われるため、insights-runコマンドを用いてruleを扱うことが出来ます。
(insights-core) [sios@fc30 project_dir]$ mkdir my_rules
(insights-core) [sios@fc30 project_dir]$ cd my_rules/
(insights-core) [sios@fc30 my_rules]$ pwd
/home/sios/project_dir/my_rules
(insights-core) [sios@fc30 my_rules]$ touch __init__.py
(insights-core) [sios@fc30 my_rules]$ ls
__init__.py

7.3.2 テスト用のruleの説明

myrules内にテスト用のrule(bash_version.py, hostname_rel.py)を作成して実行してみます。どちらのruleも参考ドキュメントに載っておりダウンロードリンクもあります。ここでは、簡単に次のようなテスト用のrule(openssh-server_version.py)を作成し、説明をしたいと思います。

#!/usr/bin/env python

from insights.core.plugins import make_pass, rule               ---------(1)
from insights.parsers.installed_rpms import InstalledRpms    ------(2)

KEY = "SSH-SERVER_VERSION"

CONTENT = "SSH-Server RPM Version: {{ ssh_server_version }}"

@rule(InstalledRpms)
def report(rpms):
    ssh_server_ver = rpms.get_max('openssh-server')                     ----(3)
    return make_pass(KEY, ssh_server_version=ssh_server_ver)

if __name__ == "__main__":                           -----(4)
    from insights import run
    run(report, print_summary=True)

解説)

  1. (1)(2)でPythonのクラスとオブジェクトを呼び出しています。これらinsightsで使用できるクラス/オブジェクトはAPIとして、このドキュメント(API Document)に使用方法までまとめられています。今回は、openssh-serverのバージョンをRPMパッケージから取得しています。そのため、(2)でインストールされているRPMの情報の取得にShared Parser CatalogクラスのInstalled.RPMsオブジェクトを使用しています。”rpm -qa”コマンドの出力は、行単位でパースされてInstalledRpmオブジェクトに格納されています。 その格納された内容に対して検索を掛けていくわけです。(3)で、検索を行っています。Installed.RPMsには検索用の式がいくつか準備されており、ドキュメントから見る事が可能です
    • get_max(package_name)
    • get_min(package_name)
    • __contains__(package_name)

    等があり、上述のサンプルではget_max(package_name)を用いて、インストールされているバージョンのうちの最大のパッケージを取得しています。package_nameには”openssh-server”等のパッケージ名(バージョン番号の前)を指定します。例えば”os-prober-1.74-8.fc30”を検索したい場合には、”os-prober”までを指定します。

    (4)の部分で、insights.run()を呼び出して結果を表示しています。この辺りの動作はinsights-coreのnotebooksにTutorialとして幾つかのドキュメントが用意されているので、そちらを参照して下さい。

  2. myrulesの下で上述のサンプルを実行すると、次のようなメッセージが出力され、インストールされているopenssh-serverのパッケージ情報を取得して表示していることがわかります。
    (insights-core) [sios@fc30 my_rules]$ python ./openssh-server_version.py
    ---------
    Progress:
    ---------
    P
            
    --------------
    Rules Executed
    --------------
    [PASS] __main__.report
    ----------------------
    SSH-Server RPM Version: 0:openssh-server-8.0p1-4.fc30
            
            
    ----------------------
    Rule Execution Summary
    ----------------------
    Passed      : 1
    Failed      : 0
    Info        : 0
    Missing Deps: 0
    Fingerprint : 0
    Metadata    : 0
    Metadata Key: 0
    Exceptions  : 0
    

    尚、insightsで使用できるAPI及びドキュメントは

    から辿れるようになっていますので、これらのドキュメントを参照にして動作を追うことが可能です。

Insights Customer Interface

Insights Customer Interfaceは、Red Hat InsightsのポータルサイトとしてRed Hatから提供されています。Red Hat Customerのサブスクリプションが必要となります。

まとめ

前半部分では通常のInsightsの使い方を、後半部分ではInsightsの中身をDeveloper用途のチュートリアルを参考にしてお見せしました。InsightsはOSの状況を簡単に知ることが出来るので、ユーザとして使用するにはとても手間が省ける良いツールだと思います。また、Insightsの中身を知ることで、Pythonのモジュールや、OSの内部に関しても詳しくなれるので、エンジニアとしての腕を磨くには良い材料だと思います。特に開発にも参加してコミットできるようになると、OSSとして皆さんでハッピーになれるので、是非チャレンジしてみて下さい。

Red Hat Insights 関連記事