fabric 命令行操作自动化例子

fabric 使用ssh 远程执行shell 命令的一个第三方库。他底层用了 Invoke(执行命令) Paramiko (提供SSH支持)这2个库

例子

from fabric import Connection
from fabric import task
from invoke.watchers import Responder ,StreamWatcher
from invoke.exceptions import UnexpectedExit
import logging
import time
import paramiko
import traceback
import io
import os
from datetime import date
from datetime import datetime
from plot_p2p import plot
import traceback
import re


class ActionResponder(StreamWatcher):

    def __init__(self, pattern_response_list):
        #todo 数据检测
        self.pattern_response_list = pattern_response_list
        self.current_action_id=0
        #结束动作
        self.end_action_id=len(pattern_response_list)
        self.pattern=pattern_response_list[0][0]
        self.response=pattern_response_list[0][1]
        self.index = 0


    def pattern_matches(self, stream, pattern, index_attr):
        index = getattr(self, index_attr)
        new_ = stream[index:]
        # Search, across lines if necessary
        matches = re.findall(pattern, new_, re.S)
        # Update seek index if we've matched
        if matches:
            setattr(self, index_attr, index + len(new_))
        return matches

    def submit(self, stream):
        # Iterate over findall() response in case >1 match occurred.
        #for没考虑去掉的影响
        for _ in self.pattern_matches(stream, self.pattern, "index"):
            yield self.response
            #匹配成功

            #高级点可以用行为树
            #
            if self.current_action_id<self.end_action_id:
                self.current_action_id+=1
                self.pattern=self.pattern_response_list[self.current_action_id][0]
                self.response=self.pattern_response_list[self.current_action_id][1]

            

   

# from plot_p2p import plot

work_path=os.path.abspath("./")+os.sep




today=str(date.today())

if not os.path.exists(work_path+"{}".format(today)):
    os.mkdir(today)

date_dir_path=work_path+os.sep+today+os.sep

#设置日志
logger = logging.getLogger()
logger.setLevel(logging.INFO)  
fh = logging.FileHandler(date_dir_path+"autop2p.log", mode='w')
fh.setLevel(logging.DEBUG)  # 输出到file的log等级的开关
formatter = logging.Formatter("%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s")
fh.setFormatter(formatter)
logger.addHandler(fh)



logger.info("程序初始化成功")



c=Connection(host='10.233.95.75',user="root",connect_kwargs={"password":"Sd!n@$#2"})

@task
def task1(c):
    try :
        in_en0 = ActionResponder([
        [r"root@192.172.0.1's password:","zte123\n",],
        [r"\[root@ne0_m ~\]#","telnet 0 10000\n",],
        [r"login:","zte\n",],
        [r"password:","zte\n",],
        [r"Successfully login into ushell!","exit\n",],
        [r"\[root@ne0_m ~\]#","exit\n",],
        ])

        re=c.run("ssh 192.172.0.1",hide=False,pty=True,watchers=[in_en0])
    except UnexpectedExit as e:
        # traceback.print_exc()
        pass


task1(c)


源码解析

fabric 的Connection 的run 使我们命令运行开始的地方。我们从这个地方开始看

    @opens  #开启ssh 链接的装饰器
    def run(self, command, **kwargs):
        """
        Execute a shell command on the remote end of this connection.

        This method wraps an SSH-capable implementation of
        `invoke.runners.Runner.run`; see its documentation for details.

        .. warning::
            There are a few spots where Fabric departs from Invoke's default
            settings/behaviors; they are documented under
            `.Config.global_defaults`.

        .. versionadded:: 2.0
        """
        return self._run(self._remote_runner(), command, **kwargs) 

这个_run 是其父类的方法。也就是fabric 引用的底层库Invoke的Context._run 方法

    # NOTE: broken out of run() to allow for runner class injection in
    # Fabric/etc, which needs to juggle multiple runner class types (local and
    # remote).
    def _run(self, runner, command, **kwargs):
        command = self._prefix_commands(command)
        return runner.run(command, **kwargs)

Context._run 方法会直接调用传进来的对象的run方法

所以真正的流程就是Connection._run -> Connection._remote_runner-> Connection.config.runners.remote
而Connection.config是在Connection._init_ 初始化的

   """
        # NOTE: parent __init__ sets self._config; for now we simply overwrite
        # that below. If it's somehow problematic we would want to break parent
        # __init__ up in a manner that is more cleanly overrideable.
        super(Connection, self).__init__(config=config)

        #: The .Config object referenced when handling default values (for e.g.
        #: user or port, when not explicitly given) or deciding how to behave.
        if config is None:
            config = Config() #fabric 的Config 类
        # Handle 'vanilla' Invoke config objects, which need cloning 'into' one
        # of our own Configs (which grants the new defaults, etc, while not
        # squashing them if the Invoke-level config already accounted for them)
        elif not isinstance(config, Config):
            config = config.clone(into=Config)
        self._set(_config=config)
        # TODO: when/how to run load_files, merge, load_shell_env, etc?
        # TODO: i.e. what is the lib use case here (and honestly in invoke too)

在顺着看abric 的Config 类初始化


    # Technically an implementation detail - do not expose in public API.
        # Stores merged configs and is accessed via DataProxy.
        self._set(_config={})

        # Config file suffixes to search, in preference order.
        self._set(_file_suffixes=("yaml", "yml", "json", "py"))

        # Default configuration values, typically a copy of `global_defaults`.
        if defaults is None:
            defaults = copy_dict(self.global_defaults())# Remote的初始化地方
        self._set(_defaults=defaults)
        。。。。

我们的 Connection.config.runners.remote 是在Config.global_defaults中初始化的

 @staticmethod
    def global_defaults():
        """
        Default configuration values and behavior toggles.

        Fabric only extends this method in order to make minor adjustments and
        additions to Invoke's `~invoke.config.Config.global_defaults`; see its
        documentation for the base values, such as the config subtrees
        controlling behavior of ``run`` or how ``tasks`` behave.

        For Fabric-specific modifications and additions to the Invoke-level
        defaults, see our own config docs at :ref:`default-values`.

        .. versionadded:: 2.0
        """
        # TODO: hrm should the run-related things actually be derived from the
        # runner_class? E.g. Local defines local stuff, Remote defines remote
        # stuff? Doesn't help with the final config tree tho...
        # TODO: as to that, this is a core problem, Fabric wants split
        # local/remote stuff, eg replace_env wants to be False for local and
        # True remotely; shell wants to differ depending on target (and either
        # way, does not want to use local interrogation for remote)
        # TODO: is it worth moving all of our 'new' settings to a discrete
        # namespace for cleanliness' sake? e.g. ssh.port, ssh.user etc.
        # It wouldn't actually simplify this code any, but it would make it
        # easier for users to determine what came from which library/repo.
        defaults = InvokeConfig.global_defaults() #其他的初始化用允原来默认的
        ours = {
            # New settings
            "connect_kwargs": {},
            "forward_agent": False,
            "gateway": None,
            # TODO 3.0: change to True and update all docs accordingly.
            "inline_ssh_env": False,
            "load_ssh_configs": True,
            "port": 22,
            "run": {"replace_env": True},
            "runners": {"remote": Remote}, #我们自己定义的Remote
            "ssh_config_path": None,
            "tasks": {"collection_name": "fabfile"},
            # TODO: this becomes an override/extend once Invoke grows execution
            # timeouts (which should be timeouts.execute)
            "timeouts": {"connect": None},
            "user": get_local_user(),
        }
        merge_dicts(defaults, ours)
        return defaults

Invoke.run 和 Fabirc.Remote

  • Invoke库可以帮我们自动话的话命令行做交互。而核心逻辑就在Invoke.Runner.run方法上
  • Fabirc.Remote 就是我们自己定义的ssh 命令行交互类。他必须继承Invoke.Runner
  • 所以和命令行交互的相关代码都是通过重载和实现Invoke.Runner的方法实现的

Invoke.Runner

Runner 中有很多需要被子类实现的方法。大家可以搜索 NotImplementedError 来查找。
Runner 的核心源码在run。run有巨长无比的说明。有兴趣的可以自己去看看,这里就不贴出来了。

        try:
            return self._run_body(command, **kwargs)
        finally:
            if not (self._asynchronous or self._disowned):
                self._stop_everything()

很简单的逻辑。运行然后做停止操作,我们接着看_run_body


    def _run_body(self, command, **kwargs):
        # Prepare all the bits n bobs.
        self._setup(command, kwargs)
        # If dry-run, stop here.
        if self.opts["dry"]:
            return self.generate_result(
                **dict(self.result_kwargs, stdout="", stderr="", exited=0)
            )
        # Start executing the actual command (runs in background)
        self.start(command, self.opts["shell"], self.env) #子类需要实现的关键方法之一
        # If disowned, we just stop here - no threads, no timer, no error
        # checking, nada.
        if self._disowned:
            return
        # Stand up & kick off IO, timer threads
        self.start_timer(self.opts["timeout"])
        self.threads, self.stdout, self.stderr = self.create_io_threads()  #这里实现自动化交互的地方
        for thread in self.threads.values():
            thread.start()   #创建了好多线程来运行
        # Wrap up or promise that we will, depending
        return self.make_promise() if self._asynchronous else self._finish()
        

有2处重点 self.start 和self.create_io_threads() 。他们一个负责如何发送command,一个负责如何自动化出来响应。
我们先看后者为啥创建很多线程来运行。

    def create_io_threads(self):
        """
        Create and return a dictionary of IO thread worker objects.

        Caller is expected to handle persisting and/or starting the wrapped
        threads.
        """
        stdout, stderr = [], []
        # Set up IO thread parameters (format - body_func: {kwargs})
        thread_args = {
            self.handle_stdout: {
                "buffer_": stdout,
                "hide": "stdout" in self.opts["hide"],
                "output": self.streams["out"],
            }
        }
        # After opt processing above, in_stream will be a real stream obj or
        # False, so we can truth-test it. We don't even create a stdin-handling
        # thread if it's False, meaning user indicated stdin is nonexistent or
        # problematic.
        if self.streams["in"]:
            thread_args[self.handle_stdin] = {
                "input_": self.streams["in"],
                "output": self.streams["out"],
                "echo": self.opts["echo_stdin"],
            }
        if not self.using_pty:
            thread_args[self.handle_stderr] = {
                "buffer_": stderr,
                "hide": "stderr" in self.opts["hide"],
                "output": self.streams["err"],
            }
        # Kick off IO threads
        threads = {}
        for target, kwargs in six.iteritems(thread_args):
            t = ExceptionHandlingThread(target=target, kwargs=kwargs)  #自定义的Thead
            threads[target] = t
        return threads, stdout, stderr

我们可以看到一个 ExceptionHandlingThread类。他是Invoke 库自己实现的Thead,看名字就知道可以处理异常的Thead。而Thead中的run 会去调用我们传递给他的 target 方法。
所以create_io_threads 可以理解为。最多启动三个线程 来运行 handle_stdout , handle_stdin 。 handle_stderr 方法。我们深入handle_stdout看看,看看下面是什么,

    def handle_stdout(self, buffer_, hide, output):
        """
        Read process' stdout, storing into a buffer & printing/parsing.

        Intended for use as a thread target. Only terminates when all stdout
        from the subprocess has been read.

        :param buffer_: The capture buffer shared with the main thread.
        :param bool hide: Whether or not to replay data into ``output``.
        :param output:
            Output stream (file-like object) to write data into when not
            hiding.

        :returns: ``None``.

        .. versionadded:: 1.0
        """
        self._handle_output(
            buffer_, hide, output, reader=self.read_proc_stdout
        )

这有一个非常的方法read_proc_stdout 他是需要子类实现。reader参数就是一块空地。而read_proc_stdout就快递柜。我们在空地上安装什么样的快递柜。快递柜就能吐出来什么样的商品

    def _handle_output(self, buffer_, hide, output, reader):
        # TODO: store un-decoded/raw bytes somewhere as well...
        for data in self.read_proc_output(reader):
            # Echo to local stdout if necessary
            # TODO: should we rephrase this as "if you want to hide, give me a
            # dummy output stream, e.g. something like /dev/null"? Otherwise, a
            # combo of 'hide=stdout' + 'here is an explicit out_stream' means
            # out_stream is never written to, and that seems...odd.
            if not hide:
                self.write_our_output(stream=output, string=data)
            # Store in shared buffer so main thread can do things with the
            # result after execution completes.
            # NOTE: this is threadsafe insofar as no reading occurs until after
            # the thread is join()'d.
            buffer_.append(data)
            # Run our specific buffer through the autoresponder framework
            self.respond(buffer_)

看名字我们可以知道,就是一个读然后响应的过程。我们看看如何读取的。

  def read_proc_output(self, reader):
        """
        Iteratively read & decode bytes from a subprocess' out/err stream.

        :param reader:
            A literal reader function/partial, wrapping the actual stream
            object in question, which takes a number of bytes to read, and
            returns that many bytes (or ``None``).

            ``reader`` should be a reference to either `read_proc_stdout` or
            `read_proc_stderr`, which perform the actual, platform/library
            specific read calls.

        :returns:
            A generator yielding Unicode strings (`unicode` on Python 2; `str`
            on Python 3).

            Specifically, each resulting string is the result of decoding
            `read_chunk_size` bytes read from the subprocess' out/err stream.

        .. versionadded:: 1.0
        """
        # NOTE: Typically, reading from any stdout/err (local, remote or
        # otherwise) can be thought of as "read until you get nothing back".
        # This is preferable over "wait until an out-of-band signal claims the
        # process is done running" because sometimes that signal will appear
        # before we've actually read all the data in the stream (i.e.: a race
        # condition).
        while True:
            data = reader(self.read_chunk_size)
            if not data:
                break
            yield self.decode(data)

一个非常简单的循环读取。使用到了python 生成器。这里的read是上面read_proc_stdout 返回。而read_proc_stdout 函数需要子类实现的。所以这个也就看到头了。之后去fabric 看看他是如何实现read_proc_stdout方法的

现在我们接着看看respond 如何响应的


    def respond(self, buffer_):
        """
        Write to the program's stdin in response to patterns in ``buffer_``.

        The patterns and responses are driven by the `.StreamWatcher` instances
        from the ``watchers`` kwarg of `run` - see :doc:`/concepts/watchers`
        for a conceptual overview.

        :param buffer:
            The capture buffer for this thread's particular IO stream.

        :returns: ``None``.

        .. versionadded:: 1.0
        """
        # Join buffer contents into a single string; without this,
        # StreamWatcher subclasses can't do things like iteratively scan for
        # pattern matches.
        # NOTE: using string.join should be "efficient enough" for now, re:
        # speed and memory use. Should that become false, consider using
        # StringIO or cStringIO (tho the latter doesn't do Unicode well?) which
        # is apparently even more efficient.
        stream = u"".join(buffer_)
        for watcher in self.watchers:
            for response in watcher.submit(stream):
                self.write_proc_stdin(response)

可以看到也是比较简单的。 就是调用每一个watcher.submit(stream) 然后把返回值往标准输入里面塞(可以想象成另一个快递柜)。而watcher 也是需要我们在子类里面定义的。所以响应我们也分析到头了。

至此。我们吧handle_stdout 方法分析完毕了。其他2个handle_stdin 。 handle_stderr 也是如此。

fabric.Runner

来看fabric.Runner 的实现可以很好的总结我们上面看的源代码

from invoke import Runner, pty_size, Result as InvokeResult


class Remote(Runner):
    """
    Run a shell command over an SSH connection.

    This class subclasses `invoke.runners.Runner`; please see its documentation
    for most public API details.

    .. note::
        `.Remote`'s ``__init__`` method expects a `.Connection` (or subclass)
        instance for its ``context`` argument.

    .. versionadded:: 2.0
    """

    def __init__(self, *args, **kwargs):
        """
        Thin wrapper for superclass' ``__init__``; please see it for details.

        Additional keyword arguments defined here are listed below.

        :param bool inline_env:
            Whether to 'inline' shell env vars as prefixed parameters, instead
            of trying to submit them via `.Channel.update_environment`.
            Default:: ``False``.

        .. versionchanged:: 2.3
            Added the ``inline_env`` parameter.
        """
        self.inline_env = kwargs.pop("inline_env", None)
        super(Remote, self).__init__(*args, **kwargs)

    def start(self, command, shell, env, timeout=None):
    	# start 就是用来指定如何发送command 命令的。
    	#因为在运行start 方法之前 。其他的所有配置已经配置好了。比如各种输入输出都已绑定ok。响应监听也添加完毕。
    	#命令的发送和接受会经过我们绑定的各种输入输出,在通过绑定的响应监听处理
    	#所以start没什么可看的就是通过channel 把命令发出去
        self.channel = self.context.create_session()

        if self.using_pty:
            rows, cols = pty_size()
            self.channel.get_pty(width=rows, height=cols)
        if env:
            # TODO: honor SendEnv from ssh_config (but if we do, _should_ we
            # honor it even when prefixing? That would depart from OpenSSH
            # somewhat (albeit as a "what we can do that it cannot" feature...)
            if self.inline_env:
                # TODO: escaping, if we can find a FOOLPROOF THIRD PARTY METHOD
                # for doing so!
                # TODO: switch to using a higher-level generic command
                # prefixing functionality, when implemented.
                parameters = " ".join(
                    ["{}={}".format(k, v) for k, v in sorted(env.items())]
                )
                # NOTE: we can assume 'export' and '&&' relatively safely, as
                # sshd always brings some shell into play, even if it's just
                # /bin/sh.
                command = "export {} && {}".format(parameters, command)
            else:
                self.channel.update_environment(env)
        self.channel.exec_command(command)

	# 还记得我们上面分析的invoke.Runner 源码吗。invoke 给了我们很多空地放加快递柜,
	#而下面这些命令就是返回各种快递柜,他们回呗invoke 添加到对应的空地上。然后快递柜就会在需要的时候自己取存商品
    def read_proc_stdout(self, num_bytes):
        return self.channel.recv(num_bytes)

    def read_proc_stderr(self, num_bytes):
        return self.channel.recv_stderr(num_bytes)

    def _write_proc_stdin(self, data):
        return self.channel.sendall(data)

    def close_proc_stdin(self):
        return self.channel.shutdown_write()

    @property
    def process_is_finished(self):
        return self.channel.exit_status_ready()
	
	#打断命令
    def send_interrupt(self, interrupt):
        # NOTE: in v1, we just reraised the KeyboardInterrupt unless a PTY was
        # present; this seems to have been because without a PTY, the
        # below escape sequence is ignored, so all we can do is immediately
        # terminate on our end.
        # NOTE: also in v1, the raising of the KeyboardInterrupt completely
        # skipped all thread joining & cleanup; presumably regular interpreter
        # shutdown suffices to tie everything off well enough.
        if self.using_pty:
            # Submit hex ASCII character 3, aka ETX, which most Unix PTYs
            # interpret as a foreground SIGINT.
            # TODO: is there anything else we can do here to be more portable?
            self.channel.send(u"\x03")
        else:
            raise interrupt

    def returncode(self):
        return self.channel.recv_exit_status()

    def generate_result(self, **kwargs):
        kwargs["connection"] = self.context
        return Result(**kwargs)
	
    def stop(self):
        if hasattr(self, "channel"):
            self.channel.close()

    def kill(self):
        # Just close the channel immediately, which is about as close as we can
        # get to a local SIGKILL unfortunately.
        # TODO: consider _also_ calling .send_interrupt() and only doing this
        # after another few seconds; but A) kinda fragile/complex and B) would
        # belong in invoke.Runner anyways?
        self.channel.close()

    # TODO: shit that is in fab 1 run() but could apply to invoke.Local too:
    # * see rest of stuff in _run_command/_execute in operations.py...there is
    # a bunch that applies generally like optional exit codes, etc

    # TODO: general shit not done yet
    # * stdin; Local relies on local process management to ensure stdin is
    # hooked up; we cannot do that.
    # * output prefixing
    # * agent forwarding
    # * reading at 4096 bytes/time instead of whatever inv defaults to (also,
    # document why we are doing that, iirc it changed recentlyish via ticket)
    # * TODO: oh god so much more, go look it up

    # TODO: shit that has no Local equivalent that we probs need to backfill
    # into Runner, probably just as a "finish()" or "stop()" (to mirror
    # start()):
    # * channel close()
    # * agent-forward close()


class Result(InvokeResult):
    """
    An `invoke.runners.Result` exposing which `.Connection` was run against.

    Exposes all attributes from its superclass, then adds a ``.connection``,
    which is simply a reference to the `.Connection` whose method yielded this
    result.

    .. versionadded:: 2.0
    """

    def __init__(self, **kwargs):
        connection = kwargs.pop("connection")
        super(Result, self).__init__(**kwargs)
        self.connection = connection

    # TODO: have useful str/repr differentiation from invoke.Result,
    # transfer.Result etc.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值