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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值