Goで軽量レベルのsshバッチ操作ツールを書く

13136 ワード

前言
これは車輪です.
Ansibleは非常に強力な自動化メンテナンスツールであり、非常に高いことはよく知られています.あまりにも大きくて、ローエンドでは運維が少し水土不服で、3点にあります.
  • AnsibleはPythonベースで、Pythonの下のインストールは依存しています.の笑うな!Winを利用する多くのユーザーにとって、Pythonを入れるだけでpipを入れるだけでポットを飲むことができます.
  • Ansibleのpaybookで使われているyaml構文はもちろん強力です.しかし、新人にとって、手に入れたばかりでは游ぶことができず、勉强する必要があります.Ansibleは他の自動化メンテナンスツールに比べて、学習曲線が非常に親しみやすいが、やはり学ばなければならないのではないだろうか.
  • Ansible自動化メンテナンスLinuxサーバは、Linux上のpythonのデフォルトのサポートのおかげで、非常に強力な機能を備えています.しかし、スイッチを走ると、通常python環境がないため、機能が大幅に割引されます.基本的には一連のコマンドの組合せを実行します.私达のこのような大きい园区のネットの伝统的な単位があって、运维の大頭は正式に交换机~
  • です
    この車輪を作る出発点は以下の考慮に基づいている.
  • はプラットフォームをまたいで、木に依存して、箱を開けてすぐ使います.Goで1つやっつけるとこのニーズをよく満たすことができます.Open-Falconのagent、ELKのbeatsを見て、すべてGoで実現することを選んだのは、このためです.
  • 簡単で頭がなく、勉強する必要はありません.スイッチを初期化するコマンドラインの組み合わせテンプレートのように、コマンドラインを直接積み上げてください.cliが遊べる限り、そのまま引っ越してくれればいいのに.
  • は、同時実行をサポートします.これはGoの強みですから、言うまでもありません.
  • 最後はもちろんGoを勉強しました.

  • ブラックAnsibleという意味は全くありません.私たちもAnsibleで自動化メンテナンスの仕事をしています.すべてのメンテナンスはAnsibleを勉強したほうがいいと思います.将来はいつも自動化の方向に行かなければなりません.このホイールの目的はAnsibleを学ぶ前に、まず簡単で頭のないツールで目の前のニーズを解決することです.
    sshセッションの確立
    Go自身はsshバッグを持っていません.彼のsshバッグは置いてあるhttps://godoc.org/golang.org/x/crypto/sshここです.import彼ならいい
    import "golang.org/x/crypto/ssh"
    

    まず、このようなsshセッションを確立する必要があります.
    func connect(user, password, host, key string, port int, cipherList []string) (*ssh.Session, error) {
        var (
            auth         []ssh.AuthMethod
            addr         string
            clientConfig *ssh.ClientConfig
            client       *ssh.Client
            config       ssh.Config
            session      *ssh.Session
            err          error
        )
        // get auth method
        auth = make([]ssh.AuthMethod, 0)
        if key == "" {
            auth = append(auth, ssh.Password(password))
        } else {
            pemBytes, err := ioutil.ReadFile(key)
            if err != nil {
                return nil, err
            }
    
            var signer ssh.Signer
            if password == "" {
                signer, err = ssh.ParsePrivateKey(pemBytes)
            } else {
                signer, err = ssh.ParsePrivateKeyWithPassphrase(pemBytes, []byte(password))
            }
            if err != nil {
                return nil, err
            }
            auth = append(auth, ssh.PublicKeys(signer))
        }
    
        if len(cipherList) == 0 {
            config = ssh.Config{
                Ciphers: []string{"aes128-ctr", "aes192-ctr", "aes256-ctr", "[email protected]", "arcfour256", "arcfour128", "aes128-cbc", "3des-cbc", "aes192-cbc", "aes256-cbc"},
            }
        } else {
            config = ssh.Config{
                Ciphers: cipherList,
            }
        }
    
        clientConfig = &ssh.ClientConfig{
            User:    user,
            Auth:    auth,
            Timeout: 30 * time.Second,
            Config:  config,
            HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
                return nil
            },
        }
    
        // connet to ssh
        addr = fmt.Sprintf("%s:%d", host, port)
    
        if client, err = ssh.Dial("tcp", addr, clientConfig); err != nil {
            return nil, err
        }
    
        // create session
        if session, err = client.NewSession(); err != nil {
            return nil, err
        }
    
        modes := ssh.TerminalModes{
            ssh.ECHO:          0,     // disable echoing
            ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
            ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
        }
    
        if err := session.RequestPty("xterm", 80, 40, modes); err != nil {
            return nil, err
        }
    
        return session, nil
    }
    
    ssh.AuthMethodにはsshの認証方式が格納されている.パスワード認証を使うと、ssh.Password()でパスワードをロードします.鍵認証を使用すると、ssh.ParsePrivateKey()またはssh.ParsePrivateKeyWithPassphrase()で鍵を読み出し、ssh.PublicKeys()でロードします.ssh.configこのstructにはsshの構成パラメータが格納されています.彼は次のいくつかの構成オプションを持っています.以下はGoDocから参照されます.
    type Config struct {
        // Rand provides the source of entropy for cryptographic
        // primitives. If Rand is nil, the cryptographic random reader
        // in package crypto/rand will be used.
        //        。    
        Rand io.Reader
    
        // The maximum number of bytes sent or received after which a
        // new key is negotiated. It must be at least 256. If
        // unspecified, a size suitable for the chosen cipher is used.
        //             ,    
        RekeyThreshold uint64
    
        // The allowed key exchanges algorithms. If unspecified then a
        // default set of algorithms is used.
        // 
        KeyExchanges []string
    
        // The allowed cipher algorithms. If unspecified then a sensible
        // default is used.
        //           
        Ciphers []string
    
        // The allowed MAC algorithms. If unspecified then a sensible default
        // is used.
        //       MAC (Message Authentication Code     )  ,    
        MACs []string
    }
    

    基本的に黙認すればいいです.ただし、Ciphersは変更が必要であり、デフォルト構成ではGoのSSHパケットが提供するCiphersには以下の暗号化方式が含まれている.
    aes128-ctr aes192-ctr aes256-ctr [email protected] arcfour256 arcfour128
    

    linuxでは通常問題はありませんが、多くのスイッチはデフォルトでaes128-cbc 3des-cbc aes192-cbc aes256-cbcしか提供されていません.だから私たちはもう少し加えたほうがいいです.
    ここには2つの場所があります.
  • clientConfigにこんな一節があります
  • HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
        return nil
    },
    

    これは、デフォルト鍵が信頼されていない場合、GoのsshパケットがHostKeyCallbackで接続を乾かすからです(1.8以降に追加すべきです).しかし、私たちがユーザー名パスワードを使って接続する場合、これは普通ではないでしょうか.return nilにすればいいのです.
  • NewSession()の後、modesおよびRequestPtyを定義した.これは、後にsession.Shell()を用いて端末をシミュレートする場合に確立される端末パラメータであるからである.不一致の場合、デフォルト値は一部の端末で実行に失敗する可能性があります.たとえば、H 3 Cのスイッチでは、接続が確立された後にデフォルトでプッシュされたCopyrightがssh接続異常を起こし、タイムアウトまたは直接切断する可能性があります.例えば、
  • ******************************************************************************
    * Copyright (c) 2004-2016 Hangzhou H3C Tech. Co., Ltd. All rights reserved.  *
    * Without the owner's prior written consent,                                 *
    * no decompiling or reverse-engineering shall be allowed.                    *
    ******************************************************************************
    

    構成されたパラメータはGoDocの例に従ってください.
    // Set up terminal modes
    modes := ssh.TerminalModes{
        ssh.ECHO:          0,     // disable echoing
        ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
        ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
    }
    // Request pseudo terminal
    if err := session.RequestPty("xterm", 40, 80, modes); err != nil {
        log.Fatal("request for pseudo terminal failed: ", err)
    }
    

    コマンドの実行
    セッションが確立されると、命令を実行するのは簡単で、session.Run()で私たちの命令を実行することができ、結果はsession.Studoutに戻ります.簡単なテストをします.
    const (
        username = "admin"
        password = "password"
        ip       = "192.168.15.101"
        port     = 22
        cmd      = "show clock"
    )
    
    func Test_SSH_run(t *testing.T) {
        ciphers := []string{}
        session, err := connect(username, password, ip, port, ciphers)
        if err != nil {
            t.Error(err)
            return
        }
        defer session.Close()
        var stdoutBuf bytes.Buffer
        session.Stdout = &stdoutBuf
        session.Run(cmd)
        t.Log(session.Stdout)
        return
    }
    

    ターゲットはスイッチです.テストしてください.
    === RUN   Test_SSH_run
    --- PASS: Test_SSH_run (0.69s)
        ssh_test.go:30: 07:55:52.598 UTC Wed Jan 17 2018
    PASS
    
    show clockのコマンドが正常に実行され、結果が返されたことがわかります.session.Run()は単一のコマンドを実行することを限定し、いくつかのコマンドの組み合わせを実行するにはsession.Shell()が必要です.意味ははっきりしていて、1つの端末をシミュレートして1つの実行命令を行って、そして結果を返します.私たちがShellを使うように、プロセス全体を印刷して出力すればいいです.session.StdinPipe()からコマンドを1つずつ入力し、session.Stdoutおよびsession.StderrからShellの出力を取得する.同じようにテストをします.
    const (
        username = "admin"
        password = "password"
        ip       = "192.168.15.101"
        port     = 22
        cmds     = "show clock;show env power;exit"
    )
    func Test_SSH(t *testing.T) {
        var cipherList []string
        session, err := connect(username, password, ip, key, port, cipherList)
        if err != nil {
            t.Error(err)
            return
        }
        defer session.Close()
    
        cmdlist := strings.Split(cmd, ";")
        stdinBuf, err := session.StdinPipe()
        if err != nil {
            t.Error(err)
            return
        }
    
        var outbt, errbt bytes.Buffer
        session.Stdout = &outbt
    
        session.Stderr = &errbt
        err = session.Shell()
        if err != nil {
            t.Error(err)
            return
        }
        for _, c := range cmdlist {
            c = c + "
    " stdinBuf.Write([]byte(c)) } session.Wait() t.Log((outbt.String() + errbt.String())) return }

    それともあのスイッチか、テストしてみましょう.
    === RUN   Test_SSH
    --- PASS: Test_SSH (0.69s)
        ssh_test.go:51: sw-1#show clock
            07:59:52.598 UTC Wed Jan 17 2018
            sw-1#show env power
            SW  PID                 Serial#     Status           Sys Pwr  PoE Pwr  Watts
            --  ------------------  ----------  ---------------  -------  -------  -----
             1  Built-in                                         Good
            
            sw-1#exit
    PASS
    

    両方のコマンドが実行され、exitが実行された後に接続が終了することがわかります.session.Run()との違いを比較すると、session.Shell()モードでは、出力されたコンテンツにホスト名、入力されたコマンドなどが含まれていることがわかります.これはtty実行の結果ですからね.命令を実行するだけなら構わないが、命令を実行した結果からいくつかの情報を読み取る必要がある場合は、これらの内容は少し肥大化しているように見えます.例えばubuntuで走ってみましょう
    === RUN   Test_SSH
    --- PASS: Test_SSH (0.98s)
            ssh_test.go:50: Welcome to Ubuntu 16.04.3 LTS (GNU/Linux 4.4.0-98-generic x86_64)
    
                     * Documentation:  https://help.ubuntu.com
                     * Management:     https://landscape.canonical.com
                     * Support:        https://ubuntu.com/advantage
    
                      System information as of Thu Jan 18 16:34:56 CST 2018
    
                      System load:  0.0                Processes:              335
                      Usage of /:   10.0% of 90.18GB   Users logged in:        0
                      Memory usage: 2%                 IP address for eth0:    192.168.80.131
                      Swap usage:   0%                 IP address for docker0: 172.17.0.1
    
                      Graph this data and manage this system at:
                        https://landscape.canonical.com/
    
                    16        。
                    16      。
    
                    New release '17.10' available.
                    Run 'do-release-upgrade' to upgrade to it.
    
                    You have new mail.
                    Last login: Thu Jan 18 16:31:41 2018 from 192.168.95.104
                    root@ubuntu-docker-node3:~# root@ubuntu-docker-node3:/opt# /opt
                    root@ubuntu-docker-node3:/opt#   
    

    少なくとも、上のSystem informationの山は必要ありません.スイッチは仕方がありません.Linuxで命令を通すことができますか.つまり、方法を考えます.Run()はコマンドの組み合わせを実行しますか?
    答えはいいですが、命令を&&でつなげればいいのでしょうか.LinnuxのShellは私たちを分解してそれぞれ実行します.例えば、上のこのコマンドはcd /opt&&pwd&&exitに統合できます.
    === RUN   Test_SSH_run
    --- PASS: Test_SSH_run (0.91s)
        ssh_test.go:76: /opt
    

    すぐに簡潔になりますよね?
    ホイール
    sshがコマンドを実行するのはそれほど悪くない.sshバッチ操作ツールになるには、同時実行、同時制限、タイムアウト制御、入力パラメータ解析、出力フォーマットなどを追加します.
    ここでは展開されず、最終的にはこのように作られた車輪が生えています.https://github.com/shanghai-edu/multissh
    コマンドとホストの区切り記号として;番または,番を使用して、直接コマンドラインで実行できます.
    # ./multissh -cmds "show clock" -hosts "192.168.31.21;192.168.15.102" -u admin -p password
    

    ホストグループとコマンドグループをテキストで格納し、改行で区切ることもできます.
    # ./multissh -cmdfile cmd1.txt.example -hostfile host.txt.example -u admin -p password
    

    特に、IP(-ipsまたは-ipfile)が入力されている場合、192.168.15.101-192.168.15.110のようなIPアドレスセグメント方式の入力を許可する.
    # ./multissh -cmds "show clock" -ips "192.168.15.101-192.168.15.110" -u admin -p password
    

    ssh鍵認証の使用をサポートします.この場合passwordを入力するとkeyとしてのパスワードになります.
    # ./multissh -hosts "192.168.80.131" -cmds "date;cd /opt;ls" -u root -k "server.key"
    

    linuxについては、linuxModeモードをサポートする、つまり、コマンドの組み合わせを&&で接続した後、se ssionを使用する.Run()運転.
    # ./multissh -hosts "192.168.80.131" -cmds "date;cd /opt;ls" -u root -k "server.key" -l
    

    ホストごとに異なる構成パラメータを定義し、json形式で構成をロードすることもできます.
    # ./multissh -c ssh.json.example
    

    出力はjson形式に打つことができて、プログラムの処理を便利にします.
    # ./multissh -c ssh.json.example -j
    

    また、構成バックアップなど、ホスト名で命名されたテキストに出力結果を保存することもできます.
    # ./multissh -c ssh.json.example -outTxt
    

    リファレンスドキュメント
    golang.org/x/crypto/ssh golang-ssh-how-to-run-multiple-commands-on-the-same-session golang-enter-ssh-sudo-password-on-prompt-or-exit
    以上
    転載許可
    CC BY-SA