Salt Proxy Minion 用於控制由於某種原因而無法運行標準Salt-minion的設備

Proxy minions是正在開發的Salt功能,可控制由於某種原因而無法運行標準Salt-minion的設備。示例包括具有API但運行專有OS系統的網絡設備、CPU或內存有限的設備,或可以運行一個minion程序但出於安全原因而不會運行的設備。

您也可以參考在Github上維護的一份相同的技術資料:Salt Proxy Minion

Proxy minions不是“開箱即用”功能。由於可能存在無限數量的可控設備,因此您很可能必須自己編寫接口。幸運的是,這僅與代理設備的實際接口一樣困難。具有現有Python模塊(例如PyUSB)的設備將相對易於接口。用於控制具有基於HTML REST的界面的設備的代碼應該很容易。

Salt Proxy minions提供了“管道連接”,可進行設備枚舉和發現、控制、狀態、遠程執行和狀態管理。

請參閱 Proxy Minion實戰演練,以瞭解基於REST的有效代理Minion的端到端演示。

有關一個SSH proxy minin是如何工作的,請參閱 Proxy Minion SSH實戰演練

請參閱 Proxyminion States 以在遠程Minion上配置和運行salt-proxy。指定所有master側proxy(pillar)配置,並使用此狀態遠程配置一個或多個minions上的代理。

請參閱Proxy minion Beacon,以幫助輕鬆配置和管理salt-proxy進程。

New in 2017.7.0

2016.3中引入的proxy_merge_grains_in_module配置變量已更改,默認爲True

默認情況下,當模塊實現alive功能並且proxy_keep_alive設置爲True時,與遠程設備的連接將保持活動狀態。 使用proxy_keep_alive_interval選項設置輪詢間隔,該選項默認爲1分鐘。

當設計足夠靈活的代理模塊以僅在需要時打開與遠程設備的連接時,開發人員還可以使用proxy_always_alive

New in 2016.11.0

Proxy minions現在支持名稱以’*.conf’結尾並放在/etc/salt/proxy.d中的配置文件。

現在可以在 /etc/salt/proxy 或 /etc/salt/proxy.d中配置Proxy minions ,而不僅僅是pillar。 配置格式與pillar中的配置格式相同。

New in 2016.3

不推薦使用的配置選項enumerate_proxy_minions已被刪除。

如先前文檔中所述,在此版本中,add_proxymodule_to_opts配置變量默認爲False。 這意味着,如果在__opts__ ['proxymodule']中查找代理模塊或其他代碼,則需要在/etc/salt/proxy文件中設置此變量,或者修改代碼以使用__proxy__注入的變量。

__proxyenabled__指令現在僅適用於grains和代理模塊本身。 不會阻止標準執行模塊和狀態模塊加載proxy minions。

Grains處理的功能增強使__proxyenabled__指令在動態grains代碼中有些多餘。 它仍然是必需的,但是grains文件中__virtual__函數的最佳做法已更改。 現在建議檢查__virtual__函數,以確保爲正確的proxy類型加載了它們,例如以下示例:

def __virtual__():
    '''
    Only work on proxy
    '''
    try:
        if salt.utils.platform.is_proxy() and \
           __opts__['proxy']['proxytype'] == 'ssh_sample':
            return __virtualname__
    except KeyError:
        pass

    return False

上面的try/except塊之所以存在,是因爲在proxy minion啓動過程中很早就處理了grains,有時早於__opts__字典中的proxy key密鑰被填充。

Grains在啓動時被加載得如此之早,以至於沒有需要使用的配置字典,因此__proxy____salt__等不可用。 現在,位於/srv/salt/_grains和salt install grains目錄中的自定義grains可以採用單個參數,proxy,與__proxy__相同。 這樣可以啓用類似下面的模式:

def get_ip(proxy):
    '''
    Ask the remote device what IP it has
    '''
    return {'ip':proxy['proxymodulename.get_ip']()}

然後,grain ip將包含在名爲proxymodulename的proxymodule中調用get_ip()函數的結果。

Proxy模塊現在受益於包含一個名爲initialized()的函數。 如果已成功調用代理的init()函數,則此函數應返回True。 這是使處理grains更容易的必要條件。

最後,如果代理模塊中有一個稱爲grains的函數,它將在代理minion啓動時執行,並且其內容將與代理的其餘grains合併。 由於較早的proxy-minions可能已使用其他方法來調用此函數並將其結果添加到grains中,因此這由稱爲proxy_merge_grains_in_module的新代理配置選項進行配置。 在2017.7.0版中此默認爲True

New in 2015.8.2

重要變更: 不建議將proxymodule變量添加到__opts__。 proxymodule變量已移至新的全局注入變量__proxy__。已爲此添加一個名爲add_proxymodule_to_opts的相關配置選項,默認爲True。在下一個主要版本2016.3.0中,此變量將默認爲False。

同時,在2015.8.0和.1下運行的proxy應該可以在2015.8.2下繼續工作。您應該儘快重構proxy代碼以使用__proxy__。

rest_sample示例代理服務器奴才已更新爲使用__proxy__。

進行此更改是因爲proxymodules是LazyLoader對象,但是LazyLoader無法序列化。 __opts__被序列化,因此saltutil.sync_all和state.highstate之類的東西將引發異常。

Salt的加載程序已添加支持,允許將自定義代理模塊放置在salt://_ proxy中。需要這些模塊的proxy minions需要重新啓動以獲取所有更改。已添加相應的實用程序函數saltutil.sync_proxymodules,以將這些模塊同步到minions。

另外,添加了一個名爲is_proxy()的salt.utils幫助函數,以使分辨運行中的minion何時是proxy minion更加容易。注意:對於2018.3.0版本,此功能已重命名爲salt.utils.platform.is_proxy()

New in 2015.8

從2015.8版本的Salt開始,proxy代理進程不再從minion進程派生出來。 取而代之的是,他們有自己的腳本salt-proxy,該腳本所接受的參數與標準Salt minion在添加–proxyid時所執行的參數相同。 這是salt proxy用來向master服務器標識自己的ID。 Proxy配置仍最好保留在Pillar中,其格式未更改。

此更改可實現更好的過程控制和日誌記錄。 現在可以使用標準流程管理實用程序(命令行中的ps)列出代理進程。 另外,託管代理的計算機上不再需要完整的Salt Minion(儘管仍然強烈建議使用)。

Getting Started

下圖可能有助於理解包含proxy-minions的Salt安裝的結構:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Yh5ftaYS-1580569514102)(./images/proxy_minions.png)]

要記住的關鍵是該圖的最左側部分。 Salt的本質是讓一個minion連接到一個master,然後master可以控制這個minion。 但是,對於proxy minions,目標設備無法運行一個minion。

在啓動proxy minion並啓動其與設備的連接後,它會重新連接到Salt-master,並且從所有管理意圖和目的來看,似乎就像Salt-master的另一個minion一樣。

要創建對proxy代理設備的支持,需要創建四件事:

Configuration parameters

Proxy minions 功能不需要在 /etc/salt/master 進行配置。

Salt的Pillar系統非常適合配置proxy-minions(儘管它們也可以在/etc/salt/proxy中進行配置)。 可以通過pillar_roots中的pillar文件或通過外部pillars來定義proxies代理。 外部pillars爲與配置管理系統、數據庫或其他可能已經包含代理目標的所有詳細信息的知識系統進行接口提供了機會。 要在pillar_roots中使用靜態文件,請根據以下示例對文件進行模式化的配置:

/srv/pillar/top.sls

base:
  net-device1:
    - net-device1
  net-device2:
    - net-device2
  net-device3:
    - net-device3
  i2c-device4:
    - i2c-device4
  i2c-device5:
    - i2c-device5
  433wireless-device6:
    - 433wireless-device6
  smsgate-device7:
    - device7

/srv/pillar/net-device1.sls

proxy:
  proxytype: networkswitch
  host: 172.23.23.5
  username: root
  passwd: letmein

/srv/pillar/net-device2.sls

proxy:
  proxytype: networkswitch
  host: 172.23.23.6
  username: root
  passwd: letmein

/srv/pillar/net-device3.sls

proxy:
  proxytype: networkswitch
  host: 172.23.23.7
  username: root
  passwd: letmein

/srv/pillar/i2c-device4.sls

proxy:
  proxytype: i2c_lightshow
  i2c_address: 1

/srv/pillar/i2c-device5.sls

proxy:
  proxytype: i2c_lightshow
  i2c_address: 2

/srv/pillar/433wireless-device6.sls

proxy:
  proxytype: 433mhz_wireless

/srv/pillar/smsgate-device7.sls

proxy:
  proxytype: sms_serial
  deventry: /dev/tty04

請注意,每個minioncontroller密鑰的內容可能會根據proxy-minion管理的設備類型而有很大差異。

在上面的例子中:

  • net-devices 1, 2, 3 是網絡交換機,使用一個指定的 IP 地址作爲可管理的接口。
  • i2c-devices 4 和 5 是非常底層的設備,通過 i2c bus總線控制。 在這個例子中,這些設備物理連接到 ‘minioncontroller2’ 設備, 可以通過 i2c bus 總線訪問到這些設備。
  • 433wireless-device6 是一個 433 MHz 無線轉換器, 同樣是通過物理連接到 minioncontroller2 設備。
  • smsgate-device7 是一個 SMS gateway 網關設備,通過一個串口物理連接到 minioncontroller3 設備。

由於pillar的工作方式,每一個從proxy minions派生出來的salt-proxy進程,將僅看到特定於將要處理的代理的密鑰。

從Salt的2016.11.0版本開始,可以在/etc/salt/proxy中配置代理,也可以在/etc/salt/proxy.d中配置文件。

另外,通常proxy-minions是輕量級的,因此,運行它們的機器可以控制大量設備。 要在一臺計算機上運行多個代理,只需啓動另一個代理進程,並將–proxyid設置爲您希望代理綁定到的ID。 如有必要,代理服務可能會分佈在許多計算機上,或者由於某些物理接口(例如上面的i2c和串行)而有意在需要控制設備的計算機上運行。 劃分代理服務的另一個原因可能是安全性。 在更安全的環境中,只有某些機器可能具有通往某些設備的網絡路徑。

Proxymodules

一個代理模塊封裝了與設備接口所需的所有代碼。代理模塊位於salt.proxy模塊內部,或者可以放置在file_roots的_proxy目錄中(默認值爲/srv/salt/_proxy。代理模塊對象至少必須實現以下功能:

__virtual __():此函數執行的功能與其他類型的Salt模塊相同。邏輯在這裏確定是否可以加載該模塊,並檢查proxy代理所依賴的Python模塊是否存在。返回False時將會阻止模塊加載。

init(opts):執行設備所需的任何初始化。這是建立與設備的持久連接或進行身份驗證以創建持久授權令牌的理想地方。

initialized():如果成功調用了init(),則返回True

shutdown():此處用於乾淨的關閉服務或關閉與受控設備連接的代碼。此函數必須存在,但如果不需要關閉邏輯,則只需要包含一個關鍵字pass

ping():雖然不是必需的,但強烈建議您在proxymodule中也定義此函數。用於ping的代碼應聯繫受控設備一方,並確保它確實可用。

alive(opts):一個可選功能,它與proxy_keep_alive選項一起使用(默認值:True)。此函數應返回與連接狀態相對應的布爾值。如果連接斷開,將嘗試重新啓動(先shutdown後再執行init)。使用proxy_keep_alive_interval選項以分鐘爲單位控制輪詢頻率。

grains():可以在此函數中計算並返回grains,而不是在 /srv/salt/_grains 或grains的標準安裝目錄中。如果在/etc/salt/proxy中將proxy_merge_grains_in_module設置爲True,則會自動調用此函數。在名爲2017.7.0的發行版中,此變量默認爲True

在2015.8之前,proxymodule還必須具有id()函數。 2015.8及之後的版本不使用此功能,因爲命令行上會提供proxy的id。

這是用於連接到非常簡單的REST服務器的示例proxymodule模塊。服務器的代碼位於salt-contrib GitHub存儲庫中。

該代理模塊啓用“service”的enumeration、starting、stopping、restarting和status; "package"安裝,以及一個ping操作。

# -*- coding: utf-8 -*-
'''
This is a simple proxy-minion designed to connect to and communicate with
the bottle-based web service contained in https://github.com/saltstack/salt-contrib/tree/master/proxyminion_rest_example
'''
from __future__ import absolute_import

# Import python libs
import logging
import salt.utils.http

HAS_REST_EXAMPLE = True

# This must be present or the Salt loader won't load this module
__proxyenabled__ = ['rest_sample']


# Variables are scoped to this module so we can have persistent data
# across calls to fns in here.
GRAINS_CACHE = {}
DETAILS = {}

# Want logging!
log = logging.getLogger(__file__)


# This does nothing, it's here just as an example and to provide a log
# entry when the module is loaded.
def __virtual__():
    '''
    Only return if all the modules are available
    '''
    log.debug('rest_sample proxy __virtual__() called...')
    return True


def _complicated_function_that_determines_if_alive():
    return True

# Every proxy module needs an 'init', though you can
# just put DETAILS['initialized'] = True here if nothing
# else needs to be done.

def init(opts):
    log.debug('rest_sample proxy init() called...')
    DETAILS['initialized'] = True

    # Save the REST URL
    DETAILS['url'] = opts['proxy']['url']

    # Make sure the REST URL ends with a '/'
    if not DETAILS['url'].endswith('/'):
        DETAILS['url'] += '/'

def alive(opts):
    '''
    This function returns a flag with the connection state.
    It is very useful when the proxy minion establishes the communication
    via a channel that requires a more elaborated keep-alive mechanism, e.g.
    NETCONF over SSH.
    '''
    log.debug('rest_sample proxy alive() called...')
    return _complicated_function_that_determines_if_alive()


def initialized():
    '''
    Since grains are loaded in many different places and some of those
    places occur before the proxy can be initialized, return whether
    our init() function has been called
    '''
    return DETAILS.get('initialized', False)


def grains():
    '''
    Get the grains from the proxied device
    '''
    if not DETAILS.get('grains_cache', {}):
        r = salt.utils.http.query(DETAILS['url']+'info', decode_type='json', decode=True)
        DETAILS['grains_cache'] = r['dict']
    return DETAILS['grains_cache']


def grains_refresh():
    '''
    Refresh the grains from the proxied device
    '''
    DETAILS['grains_cache'] = None
    return grains()


def fns():
    return {'details': 'This key is here because a function in '
                      'grains/rest_sample.py called fns() here in the proxymodule.'}


def service_start(name):
    '''
    Start a "service" on the REST server
    '''
    r = salt.utils.http.query(DETAILS['url']+'service/start/'+name, decode_type='json', decode=True)
    return r['dict']


def service_stop(name):
    '''
    Stop a "service" on the REST server
    '''
    r = salt.utils.http.query(DETAILS['url']+'service/stop/'+name, decode_type='json', decode=True)
    return r['dict']


def service_restart(name):
    '''
    Restart a "service" on the REST server
    '''
    r = salt.utils.http.query(DETAILS['url']+'service/restart/'+name, decode_type='json', decode=True)
    return r['dict']


def service_list():
    '''
    List "services" on the REST server
    '''
    r = salt.utils.http.query(DETAILS['url']+'service/list', decode_type='json', decode=True)
    return r['dict']


def service_status(name):
    '''
    Check if a service is running on the REST server
    '''
    r = salt.utils.http.query(DETAILS['url']+'service/status/'+name, decode_type='json', decode=True)
    return r['dict']


def package_list():
    '''
    List "packages" installed on the REST server
    '''
    r = salt.utils.http.query(DETAILS['url']+'package/list', decode_type='json', decode=True)
    return r['dict']


def package_install(name, **kwargs):
    '''
    Install a "package" on the REST server
    '''
    cmd = DETAILS['url']+'package/install/'+name
    if kwargs.get('version', False):
        cmd += '/'+kwargs['version']
    else:
        cmd += '/1.0'
    r = salt.utils.http.query(cmd, decode_type='json', decode=True)
    return r['dict']


def fix_outage():
    r = salt.utils.http.query(DETAILS['url']+'fix_outage')
    return r


def uptodate(name):

    '''
    Call the REST endpoint to see if the packages on the "server" are up to date.
    '''
    r = salt.utils.http.query(DETAILS['url']+'package/remove/'+name, decode_type='json', decode=True)
    return r['dict']


def package_remove(name):

    '''
    Remove a "package" on the REST server
    '''
    r = salt.utils.http.query(DETAILS['url']+'package/remove/'+name, decode_type='json', decode=True)
    return r['dict']


def package_status(name):
    '''
    Check the installation status of a package on the REST server
    '''
    r = salt.utils.http.query(DETAILS['url']+'package/status/'+name, decode_type='json', decode=True)
    return r['dict']


def ping():
    '''
    Is the REST server up?
    '''
    r = salt.utils.http.query(DETAILS['url']+'ping', decode_type='json', decode=True)
    try:
        return r['dict'].get('ret', False)
    except Exception:
        return False


def shutdown(opts):
    '''
    For this proxy shutdown is a no-op
    '''
    log.debug('rest_sample proxy shutdown() called...')

Grains是有關minions屬性信息的數據。與典型的Linux服務器相比,大多數代理設備這方面的數據量很少。默認情況下,proxy minion會從宿主身上獲取多個grains信息。 Salt核心代碼需要kernelosos_family的值,所有這些值都被強制用作proxy-minions的proxy

要將一個特定設備的屬性信息添加到它的proxy minion,請在salt/grains中創建一個名爲[proxytype].py的文件,並將其中需要運行的各種功能收集到您感興趣的數據中。以下是一個示例。請注意下面的函數proxy_functions。它演示了grains函數如何可以採用單個參數,該參數將設置爲__proxy__的值。在加載grains時,尚未將Dunder變量注入到Salt進程中,因此這使我們能夠獲取proxymodule模塊的句柄,因此我們可以交叉調用其中用於與受控設備通信的功能。

請注意,自2016.3起,也可以在proxymodule本身的名爲grains()的函數中計算grains值。如果代理模塊作者希望將代理接口的所有代碼都放在同一位置,而不是在代理目錄和grains目錄之間進行拆分,則這可能很有用。

僅在代理配置文件(默認爲/etc/salt/proxy)中將配置變量proxy_merge_grains_in_module設置爲True時,纔會自動調用此函數。在名爲2017.7.0的發行版中,此變量默認爲True

proxyenabled directive

關於__proxyenabled__指令
在先前版本的Salt中,__proxyenabled__指令控制proxies的所有Salt模塊(例如,grains、execution modules、state modules)的加載。從2016.3開始,繼續保留支持__proxyenabled__的模塊是grains和proxy模塊。需要告知這些模塊與它們一起使用的proxy是誰。

__proxyenabled__是一個列表,並且可以包含單個“*”以指示Grains模塊適用於所有代理。

一個示例 salt/grains/rest_sample.py:

# -*- coding: utf-8 -*-
'''
Generate baseline proxy minion grains
'''
from __future__ import absolute_import
import salt.utils

__proxyenabled__ = ['rest_sample']

__virtualname__ = 'rest_sample'

def __virtual__():
    try:
        if salt.utils.platform.is_proxy() and __opts__['proxy']['proxytype'] == 'rest_sample':
            return __virtualname__
    except KeyError:
        pass

    return False


SSH Proxymodules

有關編寫代理模塊的一般介紹,請參見上文。 適用於REST的所有準則對於SSH都是相同的。 本節專門討論SSH proxy模塊,並說明示例的代理模塊ssh_sample是怎樣工作的。

這是一個簡單的示例代理模塊,用於演示通過SSH連接到設備。 SSH shell的代碼位於salt-contrib GitHub存儲庫中。

下面的代理模塊啓用了“package”安裝功能。

# -*- coding: utf-8 -*-
'''
This is a simple proxy-minion designed to connect to and communicate with
a server that exposes functionality via SSH.
This can be used as an option when the device does not provide
an api over HTTP and doesn't have the python stack to run a minion.
'''
from __future__ import absolute_import

# Import python libs
import salt.utils.json
import logging

# Import Salt's libs
from salt.utils.vt_helper import SSHConnection
from salt.utils.vt import TerminalException

# This must be present or the Salt loader won't load this module
__proxyenabled__ = ['ssh_sample']

DETAILS = {}

# Want logging!
log = logging.getLogger(__file__)


# This does nothing, it's here just as an example and to provide a log
# entry when the module is loaded.
def __virtual__():
    '''
    Only return if all the modules are available
    '''
    log.info('ssh_sample proxy __virtual__() called...')

    return True


def init(opts):
    '''
    Required.
    Can be used to initialize the server connection.
    '''
    try:
        DETAILS['server'] = SSHConnection(host=__opts__['proxy']['host'],
                                          username=__opts__['proxy']['username'],
                                          password=__opts__['proxy']['password'])
        # connected to the SSH server
        out, err = DETAILS['server'].sendline('help')

    except TerminalException as e:
        log.error(e)
        return False


def shutdown(opts):
    '''
    Disconnect
    '''
    DETAILS['server'].close_connection()


def parse(out):
    '''
    Extract json from out.

    Parameter
        out: Type string. The data returned by the
        ssh command.
    '''
    jsonret = []
    in_json = False
    for ln_ in out.split('\n'):
        if '{' in ln_:
            in_json = True
        if in_json:
            jsonret.append(ln_)
        if '}' in ln_:
            in_json = False
    return salt.utils.json.loads('\n'.join(jsonret))


def package_list():
    '''
    List "packages" by executing a command via ssh
    This function is called in response to the salt command

    ..code-block::bash
        salt target_minion pkg.list_pkgs

    '''
    # Send the command to execute
    out, err = DETAILS['server'].sendline('pkg_list')

    # "scrape" the output and return the right fields as a dict
    return parse(out)


def package_install(name, **kwargs):
    '''
    Install a "package" on the REST server
    '''
    cmd = 'pkg_install ' + name
    if 'version' in kwargs:
        cmd += '/'+kwargs['version']
    else:
        cmd += '/1.0'

    # Send the command to execute
    out, err = DETAILS['server'].sendline(cmd)

    # "scrape" the output and return the right fields as a dict
    return parse(out)


def package_remove(name):
    '''
    Remove a "package" on the REST server
    '''
    cmd = 'pkg_remove ' + name

    # Send the command to execute
    out, err = DETAILS['server'].sendline(cmd)

    # "scrape" the output and return the right fields as a dict
    return parse(out)

Connection Setup

init()方法負責建立連接。 它使用在pillar數據中定義的host, usernamepassword配置變量。 如果您的SSH服務器prompt提示與示例提示(Cmd)不同,則可以將prompt kwarg傳遞給SSHConnection。 實例化SSHConnection類將建立到ssh服務器的SSH連接(使用Salt VT)。

Command execution

package_*方法使用SSH連接(在init()中建立)將命令發送到SSH服務器。 SSHConnection類的sendline()方法用於將命令發送到服務器。 在上面的示例中,我們發送了諸如pkg_listpkg_install之類的命令。 您可以通過此實用工具發送任何SSH命令。

Output parsing

sendline()返回的輸出是分別表示stdout和stderr的字符串元組。 在所示的示例中,我們只需抓取輸出並將其轉換爲python字典,如parse方法所示。 您可以定製此方法以匹配您的解析邏輯。

Connection teardown

shutdown方法負責調用SSHConnection類的close_connection()方法。 這將終止與服務器的SSH連接。

有關更多信息,請參考類SSHConnection

發佈了171 篇原創文章 · 獲贊 93 · 訪問量 50萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章