Remote Execution - SaltStack远程执行模块使用指南

文章目录

Remote Execution - Salt远程执行模块

在远程主机上运行预定义或任意的命令(也称为远程执行)是Salt的核心功能。 以下内容将探索modules和returners,这是远程执行的两个关键组件。

Salt Execution Modules

远程执行系统调用Salt执行模块来执行各种任务。 这些模块提供诸如安装包、重新启动服务、运行远程命令以及传输文件等功能。

执行模块的一个完整列表,包括了随Salt一同发布的核心执行模块。

写一个执行模块,关于怎样写一个执行模块的指南

Remote execution tutorial - 远程执行模块使用教程

在继续之前,请按照安装说明配置说明确保您已安装Salt。

遇到问题了?
有很多方法可以从Salt社区获得帮助,包括我们的邮件列表和我们的IRC频道#salt。

开始管理你的minions节点吧

现在你有一个master和至少一个minion可能彼此沟通,你可以通过salt命令对minion执行管理命令。 Salt命令调用参数由三部分组成:

salt '<target>' <function> [arguments]

参见:salt manpage

target

目标,允许您过滤哪些minions应运行以下功能。 默认过滤器是minion id上的glob通配符匹配。 例如:

salt '*' test.version
salt '*.example.org' test.version

目标可以基于使用Grains系统的minion系统信息进行过滤:

salt -G 'os:Ubuntu' test.version

参见:Grains system

可以通过正则表达式过滤目标:

salt -E 'virtmach[0-9]' test.version

可以在列表中明确指定目标:

salt -L 'foo,bar,baz,quo' test.version
``
或者可以在一个命令中组合使用多个目标类型:
```bash
salt -C 'G@os:Ubuntu and webser* or E@database.*' test.version

function

函数是由模块提供的一些功能。 Salt附带了大量功能函数。 列出你的minion支持的所有可用功能函数:

salt '*' sys.doc

下面是一些使用示例:

显示当前所有可用的 minions:

salt '*' test.version

运行任意一个 shell 命令:

salt '*' cmd.run 'uname -a'

参见:the full list of modules

arguments

以空格分隔的函数的参数:

salt '*' cmd.exec_code python 'import sys; print sys.version'

而且,关键字参数也是支持的:

salt '*' pip.install salt timeout=5 upgrade=True

他们总是以 kwarg=argument 出现。

Running Commands on Salt Minions - 在Salt Minions上运行管理命令

Salt master服务器上的root用户可以通过命令行客户端执行Salt管理命令。 Salt命令行客户端使用Salt客户端API与Salt master服务器通信。 Salt客户端简单易用。

使用Salt客户端命令可以轻松地给minions发送管理命令。

这些命令中的每一个都支持显式的使用–config选项指定master或minion配置文件。 如果未提供此选项且缺省配置文件不存在,那么Salt将回退到使用环境变量SALT_MASTER_CONFIG和SALT_MINION_CONFIG。

参见:Configuration

Using the Salt Command

Salt命令需要一些组件来向Salt minions发送信息。 需要定义目标minions、调用函数和函数需要的任何参数。

Defining the Target Minions

传递给salt的第一个参数定义了目标minions,通过其主机名访问目标minions。 默认目标类型是glob通配符:

salt '*foo.com' sys.doc

Salt 支持使用正则过滤目标minions:

salt -E '.*' cmd.run 'ls -l | grep foo'

或者是提供一个主机的列表, salt 可以使用主机列表进行过滤:

salt -L foo.bar.baz,quo.qux cmd.run 'ps aux | grep foo'

More Powerful Targets

参见: Targeting

Calling the Function

调用指定目标的函数是放在目标之后。

New in version 0.9.8.

函数也可以接受以空格分隔的参数:

salt '*' cmd.exec_code python 'import sys; print sys.version'

同样地,也可以使用关键字参数:

salt '*' pip.install salt timeout=5 upgrade=True

通常都是以 kwarg=argument 的形式出现。

以YAML格式提供的函数参数:

salt '*' cmd.run 'echo "Hello: $FIRST_NAME"' saltenv='{FIRST_NAME: "Joe"}'

注意:字典必须有大括号(如上面的saltenv关键字参数)。 这在0.15.1中已更改:在上面的示例中,第一个参数曾被解析为字典{‘echo“Hello’:’$ FIRST_NAME’’}。 这通常不是预期的行为。

如果要测试实际传递给模块的参数,请使用test.arg_repr命令:

salt '*' test.arg_repr 'echo "Hello: $FIRST_NAME"' saltenv='{FIRST_NAME: "Joe"}'

查找目标minions可用的功能函数有哪些:

Salt函数是自我描述的,所有函数文档都可以通过sys.doc()函数从minions获取:

salt '*' sys.doc

Compound Command Execution - 复合命令的执行方法

如果需要将一组命令发送到单个目标,可以在单个发布中发送这些命令。 这可以更快地收集信息组,并降低网络交互与传输的压力。

复合命令执行的工作原理是发送函数和参数的列表,而不是发送单个函数和参数。 这些函数按照它们在命令行中定义的顺序在minion上依次执行,然后所有命令中的数据都在一个字典中返回。 这意味着以可预测的方式调用命令集,并且可以容易地解释返回的数据。

如通过传递以逗号分隔的函数列表,后跟逗号分隔的参数列表来执行复合命令:

salt '*' cmd.run,test.ping,test.echo 'cat /proc/cpuinfo',,foo

在这里要注意的诀窍是,如果函数没有传递任何参数,那么需要有一个占位符来替代缺少的参数。 这就是为什么在上面的例子中,有两个逗号紧挨着。 test.ping不带参数,所以我们需要添加另一个逗号,否则Salt会尝试将“foo”传递给test.ping。

如果需要传递包含逗号的参数,请确保在分隔参数的逗号周围添加空格。 例如:

salt '*' cmd.run,test.ping,test.echo 'echo "1,2,3"' , , foo

您可以使用–args-separator选项更改参数分隔符:

salt --args-separator=:: '*' some.fun,test.echo params with , comma :: foo

CLI Completion

可以在pkg Salt源代码目录中找到Salt CLI的服务脚本。

Writing Execution Modules - 开发执行模块

Salt执行模块就是那些可以由salt命令调用执行的功能函数。

Modules Are Easy to Write! - 开发Salt模块很容易

编写Salt执行模块非常简单。

Salt执行模块是放置在Salt文件服务器根目录下名为_modules/的目录中的Python或Cython模块。 使用默认的后端文件服务器(即 roots)时,除非在file_roots配置选项中另外定义了环境参数,否则_modules/目录将位于大多数系统上的/srv/salt/_modules中。

当调用以下任何Salt函数时,放置在_modules/中的模块将会同步到minions:

请注意,模块的默认名称是其文件名(即foo.py成为模块foo),但可以使用__virtual__函数为其自定义名称。

如果Salt模块有错误且无法导入,则Salt minion将继续加载而不会出现问题,并且将简单地省略带有错误的模块。

如果添加一个Cython模块,该文件必须命名为<modulename>.pyx,以便加载器知道该模块需要作为Cython模块导入。 Cython模块的编译是自动的,并且在minion启动时发生,因此只需要*.pyx文件。

Zip Archives as Modules - 使用zip压缩文件作为salt模块

Python 2.3及更高版本允许开发人员直接导入包含Python代码的zip文件。 通过在minion配置中将enable_zip_modules设置为True,Salt加载器将能够以这种方式导入.zip文件。 这允许Salt模块开发人员将依赖关系与其模块打包在一起,以便于部署、隔离等。

对于用户,使用Zip Archive格式的模块时的行为与其他模块类似。 当从作为文件my_module.zip提供的模块执行函数时,用户可以将该模块中的函数调用写为my_module.<function>

Creating a Zip Archive Module

Zip Archive模块的结构与简单的Python包类似。 .zip文件包含一个与模块同名的目录。 传统上在<module_name>.py中的模块代码位于<module_name>/__ init__.py中。 依赖包是<module_name>/的子目录。

下面是lumberjack模块的示例目录结构,它包含两个库依赖项(sleep和work)。

modules $ ls -R lumberjack
__init__.py     sleep           work

lumberjack/sleep:
__init__.py

lumberjack/work:
__init__.py

lumberjack/__init__.py 展示了如何导入和使用这些依赖类。

# Libraries included in lumberjack.zip
from lumberjack import sleep, work


def is_ok(person):
    ''' Checks whether a person is really a lumberjack '''
    return sleep.all_night(person) and work.all_day(person)

接下来,打包生成 zip 文件:

modules $ zip -r lumberjack lumberjack
  adding: lumberjack/ (stored 0%)
  adding: lumberjack/__init__.py (deflated 39%)
  adding: lumberjack/sleep/ (stored 0%)
  adding: lumberjack/sleep/__init__.py (deflated 7%)
  adding: lumberjack/work/ (stored 0%)
  adding: lumberjack/work/__init__.py (deflated 7%)
modules $ unzip -l lumberjack.zip
Archive:  lumberjack.zip
  Length     Date   Time    Name
 --------    ----   ----    ----
        0  08-21-15 20:08   lumberjack/
      348  08-21-15 20:08   lumberjack/__init__.py
        0  08-21-15 19:53   lumberjack/sleep/
       83  08-21-15 19:53   lumberjack/sleep/__init__.py
        0  08-21-15 19:53   lumberjack/work/
       81  08-21-15 19:21   lumberjack/work/__init__.py
 --------                   -------
      512                   6 files

一旦部署到 file_roots 指定的路径下, Salt 用户就可以像其它模块一样分发和调用 lumberjack.zip 模块了。

$ sudo salt minion1 saltutil.sync_modules
minion1:
  - modules.lumberjack
$ sudo salt minion1 lumberjack.is_ok 'Michael Palin'
minion1:
  True

Cross Calling Execution Modules - Salt执行模块的交叉调用

所有Salt执行模块都可以互相调用,模块可以调用其他执行模块中可用的功能函数。

变量__salt__在加载到Salt minion后被打包到模块中。

__salt__变量是一个包含所有Salt函数的Python字典。 字典键是表示模块名称的字符串,值是函数本身。

可以通过访问__salt__ dict中的值来交叉调用Salt模块:

def foo(bar):
    return __salt__['cmd.run'](bar)

此代码将调用cmd模块中的run函数并将参数传递给它。

Calling Execution Modules on the Salt Master - 在salt master上运行执行模块

New in version 2016.11.0.

现在也可以使用salt runner通过salt-run命令调用执行模块。

Preloaded Execution Module Data - 预加载模块数据

当经常与执行模块交互时,能够动态地读取关于minion的信息或加载模块的配置参数是很有帮助的。

Salt允许将不同类型的数据由minion加载到模块中。

Grains Data

由minion上的Salt Grains检测到的值可以在名为__grains__的Python字典中找到,并且可以从Python模块中的可调用对象中访问。

要查看部署中给定系统的grain字典的内容,请运行grains.items()函数:

salt 'hostname' grains.items --output=pprint

grain字典中的任何值都可以像任何其他Python字典一样访问。 例如,表示minion ID的颗粒存储在id键中,在执行模块中,该值将存储在__grains __ ['id']中。

Module Configuration

由于可能需要使用到用于配置模块的参数,因此Salt允许来自minion配置文件的配置信息被传递到执行模块。

由于minion配置文件是YAML文档,因此可以在模块读取的minion配置中传递任意配置数据。 因此,强烈建议配置文件中传递的值与模块名称匹配。 例如,用于test执行模块的值应命名为test.<value>

test执行模块包含模块配置信息的使用方法以及minion的默认配置文件,包含用于将数据传递给模块的信息和格式。 salt.modules.testconf/minion

init Function

如果您希望模块具有基于minion配置的不同执行模式,则可以使用__init __(opts)功能执行初始模块设置。 参数opts是完整的minion配置,这可以在__opts__ dict中找到。

'''
Cheese module initialization example
'''
def __init__(opts):
    '''
    Allow foreign imports if configured to do so
    '''
    if opts.get('cheese.allow_foreign', False):
        _enable_foreign_products()

Strings and Unicode

执行模块的作者应始终假设送到模块的字符串已经从字符串解码为Unicode。 在Python 2中,这些将是’Unicode’类型,而在Python 3中它们将是str类型。 从状态调用到其他Salt子系统,应该传递Unicode(如果传递二进制数据,则传递字节)。 在极少数情况下,状态需要直接写入磁盘,Unicode应该在写入磁盘之前立即编码为字符串。 作者可以使用__salt_system_encoding__来了解系统的编码类型。 例如,'my_string'.encode(__ salt_system_encoding__')

Outputter Configuration

由于执行模块功能可以返回不同的数据,并且数据的打印方式可以极大地改变显示效果,Salt允许在逐个功能的基础上设置特定的输出器。

这样做是在模块的全局范围内声明__outputter__字典。 __outputter__字典包含函数名称到Salt输出器的映射。

__outputter__ = {
    'run': 'txt'
}

这将确保txt输出器用于显示运行功能的输出。

Virtual Modules - 模拟模块

虚拟模块允许您覆盖模块的名称,以便使用相同的名称来引用几个类似模块中的一个。根据当前平台或环境选择加载与虚拟名称对应的实际特定模块。

例如,我们可以使用pkg模块跨平台管理包。 pkg就是一个虚拟模块的名称,它是在特定系统上加载的特定软件包管理器模块的别名(例如,RHEL/CentOS系统上的yumpkg和Ubuntu上的aptpkg)。

使用__virtual__函数和virtual name设置虚拟模块的名称。

__virtual__函数

__virtual__函数在结果为True或False可以返回一个字符串,或者是在结果为False时返回一个错误字符串。如果返回一个字符串,则使用字符串的名称作为虚拟名称来加载模块。如果返回True,则使用当前模块名称加载模块。如果返回False,则不加载模块。 False允许模块执行系统检查,并在不满足依赖性时阻止加载。

由于__virtual__是在加载模块之前就被调用,所以__salt__将是不可靠的,因为此时并非所有模块都可用。然而,__pillar____grains__等“dunder”词典是可用的。

注意:在从__virtual__返回一个已经由Salt附带的模块使用了的字符串的模块时,这将会原始的模块。

Returning Error Information from __virtual__ - 从__virtual__函数返回错误信息

可选地,诸如执行、状态、返回者、信标等模块的Salt插件模块可另外返回包含无法加载模块的原因的错误信息字符串。 例如,一个名为cheese的执行模块和一个名为cheese的相应状态模块,两者都依赖于一个名为enzyme的实用程序,它应该具有__virtual__函数,用于处理依赖项不可用时的情况。

'''
Cheese execution (or returner/beacon/etc.) module
'''
try:
    import enzymes
    HAS_ENZYMES = True
except ImportError:
    HAS_ENZYMES = False


def __virtual__():
    '''
    only load cheese if enzymes are available
    '''
    if HAS_ENZYMES:
        return 'cheese'
    else:
        return False, 'The cheese execution module cannot be loaded: enzymes unavailable.'

def slice():
    pass
'''
Cheese state module. Note that this works in state modules because it is
guaranteed that execution modules are loaded first
'''

def __virtual__():
    '''
    only load cheese if enzymes are available
    '''
    # predicate loading of the cheese state on the corresponding execution module
    if 'cheese.slice' in __salt__:
        return 'cheese'
    else:
        return False, 'The cheese state module cannot be loaded: enzymes unavailable.'

示例

包管理器模块是使用__virtual__函数的最佳示例。 可在此处找到所有虚拟pkg模块的表。

Overriding Virtual Module Providers - 覆盖虚拟模块提供程序

Salt通常使用OS grains(os,osrelease,os_family等)来确定应该将哪个模块作为pkgservice等的虚拟模块加载。有时这个OS检测不能完整覆盖全部的发行版本,比如当新的发行版发布或现有的发行版更改init默认设置时,下面是一个容易受此影响的虚拟模块的列表:

如果Salt使用其中一个错误的模块,首先,请在问题跟踪器上报告,以便在将来的版本中解决此问题。 为了便于排除故障,请同时提供grain.items输出,注意避免携带任何敏感信息。

然后,在等待SaltStack开发团队解决问题的同时,Salt可以使用minion配置文件中的providers选项设置使用正确的模块:

providers:
  service: systemd
  pkg: aptpkg

上面的例子将强制minion使用systemd模块来提供服务管理,并使用aptpkg模块来提供包管理。

Logging Restrictions

通常,在加载之前,不应在Salt模块中的任何位置进行日志记录。 此规则适用于在__virtual __()函数之前运行的所有代码,以及__virtual __()函数本身中的代码。

如果在virtual函数确定是否应加载模块之前进行日志记录语句,则将重复调用这些日志记录语句。 这会不必要地给日志文件造成混乱。

对于trace级别的日志记录,可以考虑例外。 但是,最好通过其他方式提供必要的信息。 一种方法是在__virtual __()函数中返回错误信息。

__virtualname__

__virtualname__是文档构建系统用于在不调用__virtual__函数的情况下知道模块的虚拟名称的变量。 从__virtual__函数返回字符串的模块也必须设置__virtualname__变量。

要避免将虚拟名称字符串设置两次,可以使用类似于以下的模式实现让__virtual__返回__virtualname__的值:

# Define the module's virtual name
__virtualname__ = 'pkg'


def __virtual__():
    '''
    Confine this module to Mac OS with Homebrew.
    '''

    if salt.utils.path.which('brew') and __grains__['os'] == 'MacOS':
        return __virtualname__
    return False

__virtual __()函数可以返回TrueFalse布尔值,元组或字符串。 如果它返回True值,则可以设置此__virtualname__模块级属性,如上例所示。 这是模块应该被称为的字符串。

__virtual __()返回一个元组时,第一个项应该是布尔值,第二个项应该是一个字符串。 这通常在模块未正常加载时发生。 元组的第一个值为False,第二个值是为了显示模块未加载的原因而显示的错误消息。

例如:

def __virtual__():
    '''
    Only load if git exists on the system
    '''
    if salt.utils.path.which('git') is None:
        return (False,
                'The git execution module cannot be loaded: git unavailable.')
    else:
        return True

Documentation - 模块文档

Salt执行模块有完善的文档说明。 sys.doc()函数将返回所有可用模块的文档:

salt '*' sys.doc

sys.doc函数只打印出模块中的docstrings; 在编写Salt执行模块时,请遵循docstrings的格式约定,因为它们会出现在其他模块中。

Adding Documentation to Salt Modules - 在Salt模块中添加文档说明

强烈建议所有Salt模块都添加文档。

要添加文档,请在函数中添加Python docstring

def spam(eggs):
    '''
    A function to make some spam with eggs!

    CLI Example::

        salt '*' test.spam eggs
    '''
    return eggs

现在,当执行sys.doc调用时,docstring将被直接地返回给调用终端。

在执行模块的docstring中添加的文档将自动添加到基于Web的在线文档中。

Add Execution Module Metadata - 添加执行模块的元数据

为执行模块编写Python docstring时,使用以下字段列表添加有关该模块的元数据信息:

:maintainer:    Thomas Hatch <thatch@saltstack.com, Seth House <shouse@saltstack.com>
:maturity:      new
:depends:       python-mysqldb
:platform:      all

maintainer字段是以逗号分隔的开发人员列表,这些开发人员帮助维护此模块。

maturity字段表示该模块的质量水平和测试。 用于做标准管理的标签。

depends字段是此模块所依赖的以逗号分隔的模块列表。

platform字段是以逗号分隔的平台列表,已知此模块可在其上运行。

Log Output - 日志输出

您可以从自定义模块调用logger以将消息写入minion日志。 以下代码段演示了怎样编写日志消息:

import logging

log = logging.getLogger(__name__)

log.info('Here is Some Information')
log.warning('You Should Not Do That')
log.error('It Is Busted')

Aliasing Functions - 别名功能

有时候,人们希望使用一个会干扰到内置python函数的名称。 一个常见的例子是set()。 要支持此功能,请在函数定义中附加下划线,def set_():,并使用__func_alias__功能为函数提供别名。

__func_alias__是一个字典,其中每个键是模块中函数的名称,每个值都是表示该函数别名的字符串。 从不同的执行模块,状态模块或cli调用别名函数时,应使用别名。

__func_alias__ = {
    'set_': 'set',
    'list_': 'list',
}

Private Functions - 私有函数

在Salt中,执行模块中包含的Python可调用对象可供Salt minion使用。 此规则的唯一例外是可调用对象,其名称以下划线_开头时。

Objects Loaded Into the Salt Minion

def foo(bar):
    return bar

Objects NOT Loaded into the Salt Minion

def _foobar(baz): # Preceded with an _
    return baz

cheese = {} # Not a callable Python object

Useful Decorators for Modules - 模块装饰器的用处

Depends Decorator - 使用装饰器管理依赖

在编写执行模块时,很多时候某些模块将在所有主机上运行,但某些功能具有外部依赖性,例如需要安装的服务或需要在系统上存在的二进制文件。

可以使用装饰器,而不是尝试将大部分代码包装在大型try/except块中。

如果传递给装饰器的依赖项不存在,那么salt minion将从该主机上的模块中删除这些函数。

如果定义了fallback_function,它将替换该函数而不是删除它。

import logging

from salt.utils.decorators import depends

log = logging.getLogger(__name__)

try:
    import dependency_that_sometimes_exists
except ImportError as e:
    log.trace('Failed to import dependency_that_sometimes_exists: {0}'.format(e))

@depends('dependency_that_sometimes_exists')
def foo():
    '''
    Function with a dependency on the "dependency_that_sometimes_exists" module,
    if the "dependency_that_sometimes_exists" is missing this function will not exist
    '''
    return True

def _fallback():
    '''
    Fallback function for the depends decorator to replace a function with
    '''
    return '"dependency_that_sometimes_exists" needs to be installed for this function to exist'

@depends('dependency_that_sometimes_exists', fallback_function=_fallback)
def foo():
    '''
    Function with a dependency on the "dependency_that_sometimes_exists" module.
    If the "dependency_that_sometimes_exists" is missing this function will be
    replaced with "_fallback"
    '''
    return True

除了全局依赖之外,depends装饰器还支持原始布尔值。

from salt.utils.decorators import depends

HAS_DEP = False
try:
    import dependency_that_sometimes_exists
    HAS_DEP = True
except ImportError:
    pass

@depends(HAS_DEP)
def foo():
    return True

Executors - 执行器程序

minion使用Executors来执行模块功能。 Executors可用于修改函数行为,执行任何执行前步骤或以特定方式执行,如sudo执行程序。

Executors可以作为列表传递,它们将在流程中被逐个使用。 如果执行程序返回None,则将调用下一个Executors。 如果执行程序返non-None,则终止执行序列,并将返回的值用作结果。 这是一种Executors执行器可以控制模块执行作为过滤器的方式。 请注意,Executors执行程序实际上可能无法执行该函数,只是执行其他操作并返回None,就像splay executor一样。 在这种情况下,必须将一些其他Executors执行程序用作实际执行该函数的最终执行程序。 见下面的例子。

Executors执行者列表可以通过以下方式由minion配置文件传递:

module_executors:
  - splay
  - direct_call
splaytime: 30

在命令行上做相同的调用:

salt -t 40 --module-executors='[splay, direct_call]' --executor-opts='{splaytime: 30}' '*' test.version

使用netapi 调用相同的管理命令时:

curl -sSk https://localhost:8000 \
    -H 'Accept: application/x-yaml' \
    -H 'X-Auth-Token: 697adbdc8fe971d09ae4c2a3add7248859c87079' \
    -H 'Content-type: application/json' \
    -d '[{
        "client": "local",
        "tgt": "*",
        "fun": "test.version",
        "module_executors": ["splay", "direct_call"],
        "executor_opts": {"splaytime": 10}
        }]'

参见:The full list of executors

Writing Salt Executors - 开发一个Executor执行程序

一个Salt Executor执行程序以类似于Salt执行模块的方式进行编写。 Executor是一个python模块,放在executors文件夹中,包含带有以下签名的execute函数:

def execute(opts, data, func, args, kwargs)

args 参数的含义是:

  • opts: 包含minion配置选项的字典
  • data: 包含load数据的字典,包括通过命令行/API传递的executor opts
  • func, args, kwargs:要执行的执行模块函数及其参数。 例如,最简单的direct_call执行程序只是将其作为func(* args,** kwargs)运行。
  • Returns:如果必须使用下一个Executor执行程序继续执行序列,则为 None。 如果作业完成且必须停止执行,则返回错误字符串或执行结果。

可以通过minion configexecutor_opts参数将特定选项传递给executor 执行程序。 例如,访问由minion设置的splaytime选项配置执行程序访问opts.get('splaytime')。 要访问由命令行或API设置的选项,应使用 data.get(‘executor_opts’,{}).get(‘splaytime’)。 因此,如果选项是安全的并且必须可由用户执行程序访问,则应在两个位置进行检查,但如果选项不安全,则应从唯一的配置中读取,忽略传递的请求数据。

还有一个名为all_missing_func的函数,它传递了func的名称,可用于验证命令是否仍应运行,即使它未在minion_mods中加载。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值