本文主要講述如何利用Python的Fabric模組編寫一個指令碼用於配置多個主機間SSH互信以及如何將管理員自己的公鑰批量新增到多個主機中。

指令碼說明

該指令碼只提供如題所述的少量功能,用於幫助熟悉Python的Fabric和SSH幾項簡單的基本配置,原本的目的是想通過Python和Fabric實現對主機進行一些批量操作,如完成主機的初始化等。因為SSH的配置具有通用性和必要性,所以便有了此文,希望對Linux運維和使用Python、Fabric自動化部署感興趣的人有所幫助。

該指令碼將繼續維護,直至使用Python和Fabric完成主機的初始化這個指令碼在生產環境可用,可以關注GitHub上LinuxBashShellScriptForOps專案的更新資訊

說明:LinuxBashShellScriptForOps是一個Linux運維指令碼倉庫,對在Linux運維工作所能用到的Shell指令碼和Python指令碼的歸納和總結。 絕大部分的原始碼均出自生產系統並經過比較嚴謹的測試,可以通過閱讀該專案的README獲得該專案的更多資訊。

涉及的主要知識

其中涉及的知識:

  1. Python程式設計知識

  2. Fabric模組配置和使用相關知識

    1. 定義主機角色、使用者名稱和密碼、sudo使用者名稱和密碼、SSH配置和連線配置等

    2. 使用設定上下文 (with setting),用於一些需要Fabric特殊設定的執行過程,如在執行命令出錯時是否該彈出警告並不退出當前任務

    3. sudo執行命令

    4. 獲取遠端命令的返回結果(標準輸出和標準錯誤輸出,但不包括返回結果值0,1)

    5. 終端多顏色顯示

    6. 特殊操作需要使用者確認(confirm函式)

    7. runs_once裝飾器,注意runs_once裝飾器不能與定義的角色中有多個主機的情況下聯用

  3. 日誌模組的配置已經隱含在指令碼中,非常有用但沒有使用,原因在本文第二段已經說明

  4. 使用Python判斷當前系統是Windows還是Linux,是Debian系還是RHEL系

  5. 使用Python判斷IP是否合法

功能說明

此指令碼以Ubuntu為例,主要有三個功能:

  1. 重置主機自動生成的SSH Key

  2. 將管理員自己的SSH 公鑰注入到root使用者的SSH認證檔案,從而可以通過key和以root身份登入到主機

  3. 將其他(同一網段或者能訪問到)主機的SSH 公鑰新增到root使用者的SSH認證檔案

注:是否要重置主機自動生成的SSH Key取決於當前環境下主機是如何安裝作業系統的,如果是通過模板的方式批量部署的虛擬機器通常擁有相同的SSH配置,這就意味著它們的SSH配置完全相同,包括公鑰、私鑰、SSH配置和使用者SSH配置等。如果這是公有云環境,可能導致安全問題,就像通過模板安裝Windows需要sysprep安裝刪除唯一性資訊一樣,Linux中SSH也必須重置。這些key通常位於/etc/ssh目錄下:

/etc/ssh/ssh_host_dsa_key    
/etc/ssh/ssh_host_dsa_key.pub      
/etc/ssh/ssh_host_ecdsa_key      
/etc/ssh/ssh_host_ecdsa_key.pub      
/etc/ssh/ssh_host_ed25519_key      
/etc/ssh/ssh_host_ed25519_key.pub      
/etc/ssh/ssh_host_rsa_key      
/etc/ssh/ssh_host_rsa_key.pub

指令碼內容

指令碼內容如下,也可以從GitHub獲取:

#!/usr/bin/python
# encoding: utf-8
# -*- coding: utf8 -*-
"""
Created by PyCharm.
File:               LinuxBashShellScriptForOps:pyLinuxHostsSSHKeyInitialization.py
User:               Guodong
Create Date:        2017/3/9
Create Time:        23:05
 """
import time
import os
import logging
import logging.handlers
import sys
from fabric.api import *
from fabric.colors import red, green, yellow, blue, magenta, cyan, white
from fabric.context_managers import *
from fabric.contrib.console import confirm

env.roledefs = {
    `base`: [`ubuntu@192.168.1.101:22`, ],
    "bigData": [`ubuntu@192.168.100.122:22`, `ubuntu@192.168.100.123:22`, `ubuntu@192.168.100.124:22`, ],
    "coreServices": [`ubuntu@192.168.100.127:22`, `ubuntu@192.168.100.128:22`, `ubuntu@192.168.100.129:22`,
                     `ubuntu@192.168.100.130:22`, ],
    "webAppFrontend": [`ubuntu@192.168.100.125:22`, ],
    "webAppBackend": [`ubuntu@192.168.100.126:22`, ],
    `all`: [`192.168.1.101`, `192.168.100.122`, `192.168.100.123`, `192.168.100.124`, `192.168.100.125`,
            `192.168.100.126`, `192.168.100.127`, `192.168.100.128`, `192.168.100.129`, `192.168.100.130`, ],
    `db`: [`ubuntu@192.168.100.127:22`, ],
    `nginx`: [`ubuntu@192.168.100.128:22`, ],
}

env.hosts = [`192.168.1.101`, `192.168.100.122`, `192.168.100.123`, `192.168.100.124`, `192.168.100.125`,
             `192.168.100.126`, `192.168.100.127`, `192.168.100.128`, `192.168.100.129`, `192.168.100.130`, ]
env.port = `22`
env.user = "ubuntu"
env.password = "ubuntu"
env.sudo_user = "root"  # fixed setting, it must be `root`
env.sudo_password = "ubuntu"
env.disable_known_hosts = True
env.warn_only = False
env.command_timeout = 15
env.connection_attempts = 2


def initLoggerWithRotate(logPath="/var/log", logName=None, singleLogFile=True):
    current_time = time.strftime("%Y%m%d%H")
    if logName is not None and not singleLogFile:
        logPath = os.path.join(logPath, logName)
        logFilename = logName + "_" + current_time + ".log"
    elif logName is not None and singleLogFile:
        logPath = os.path.join(logPath, logName)
        logFilename = logName + ".log"
    else:
        logName = "default"
        logFilename = logName + ".log"

    if not os.path.exists(logPath):
        os.makedirs(logPath)
        logFilename = os.path.join(logPath, logFilename)
    else:
        logFilename = os.path.join(logPath, logFilename)

    logger = logging.getLogger(logName)
    log_formatter = logging.Formatter("%(asctime)s %(filename)s:%(lineno)d %(name)s %(levelname)s: %(message)s",
                                      "%Y-%m-%d %H:%M:%S")
    file_handler = logging.handlers.RotatingFileHandler(logFilename, maxBytes=104857600, backupCount=5)
    file_handler.setFormatter(log_formatter)
    stream_handler = logging.StreamHandler(sys.stderr)
    logger.addHandler(file_handler)
    logger.addHandler(stream_handler)
    logger.setLevel(logging.DEBUG)
    return logger


mswindows = (sys.platform == "win32")  # learning from `subprocess` module
linux = (sys.platform == "linux2")


def _win_or_linux():
    # os.name ->(sames to) sys.builtin_module_names
    if `posix` in sys.builtin_module_names:
        os_type = `Linux`
    elif `nt` in sys.builtin_module_names:
        os_type = `Windows`
    return os_type


def is_windows():
    if "windows" in _win_or_linux().lower():
        return True
    else:
        return False


def is_linux():
    if "linux" in _win_or_linux().lower():
        return True
    else:
        return False


def is_debian_family():
    import platform
    # http://stackoverflow.com/questions/2988017/string-comparison-in-python-is-vs
    # http://stackoverflow.com/questions/1504717/why-does-comparing-strings-in-python-using-either-or-is-sometimes-produce
    if platform.system() == "Linux":
        distname = platform.linux_distribution()
        if "Ubuntu" in distname or "Debian" in distname:
            return True
        else:
            return False
    else:
        return False


def is_rhel_family():
    import platform
    if platform.system() == "Linux":
        distname = platform.linux_distribution()
        if "CentOS" in distname or "Debian" in distname:
            return True
        else:
            return False
    else:
        return False


# log_path = "/var/log" if os.path.exists("/var/log") or os.makedirs("/var/log") else "/var/log"
log_path = "/var/log"
log_name = "." + os.path.splitext(os.path.basename(__file__))[0]

log = initLoggerWithRotate(logPath="/var/log", logName=log_name, singleLogFile=True)
log.setLevel(logging.INFO)


def is_valid_ipv4(ip, version=4):
    from IPy import IP
    try:
        result = IP(ip, ipversion=version)
    except ValueError:
        return False
    if result is not None and result != "":
        return True


@roles(`all`)
def reset_ssh_public_host_key():
    """
    First job to run
    Reset ssh public host key after clone from one same virtual machine template
    Repeat do this will disable ssh connect between different hosts which ssh key has been registered!
    :return:
    """
    with settings(warn_only=False):
        out = sudo("test -f /etc/ssh/unique.lck && cat /etc/ssh/unique.lck", combine_stderr=False, warn_only=True)
        print yellow(
            "Repeat do this will disable ssh connect between different hosts which ssh key has been registered!")

        if "1" not in out:
            if confirm("Are you really want to reset ssh public key on this host? "):
                blue("Reconfigure openssh-server with dpkg")
                sudo("rm /etc/ssh/ssh_host_* && dpkg-reconfigure openssh-server && echo 1 >/etc/ssh/unique.lck")
            else:
                print green("Brilliant, user canceled this dangerous operation.")
        else:
            print blue("If you see a `Warning` in red color here, do not panic, this is normal when first time to run.")
            print green("ssh public host key is ok.")


@roles(`all`)
def inject_admin_ssh_public_key():
    """
    Second job to run
    Inject Admin user`s ssh key to each host
    :return:
    """
    with settings(warn_only=False):
        sudo(`yes | ssh-keygen -N "" -f /root/.ssh/id_rsa`)
        content_ssh_public_key = """ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCawuOgQup3Qc1OILytyH+u3S9te85ctEKTvzPtRjHfnEEOjpRS6v6/PsuDHplHO1PAm8cKbEZmqR9tg4mWSweosBYW7blUUB4yWfBu6cHAnJOZ7ADNWHHJHAYi8QFZd4SLAAKbf9J12Xrkw2qZkdUyTBVbm+Y8Ay9bHqGX7KKLhjt0FIqQHRizcvncBFHXbCTJWsAduj2i7GQ5vJ507+MgFl2ZTKD2BGX5m0Jq9z3NTJD7fEb2J6RxC9PypYjayXyQBhgACxaBrPXRdYVXmy3f3zRQ4/OmJvkgoSodB7fYL8tcUZWSoXFa33vdPlVlBYx91uuA6onvOXDnryo3frN1
ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAumQ2srRwd9slaeYTdr/dGd0H4NzJ3uQdBQABTe/nhJsUFWVG3titj7JiOYjCb54dmpHoi4rAYIElwrolQttZSCDKTVjamnzXfbV8HvJapLLLJTdKraSXhiUkdS4D004uleMpaqhmgNxCLu7onesCCWQzsNw9Hgpx5Hicpko6Xh0=
"""
        sudo("echo -e `%s` >/root/.ssh/authorized_keys" % content_ssh_public_key)


@roles(`all`)
def scan_host_ssh_public_key():
    """
    Third and last job to run
    scan all host`s public key, then inject to /root/.ssh/authorized_keys
    :return:
    """
    with settings(warn_only=False):
        for host in env.hosts:
            if is_valid_ipv4(host):
                sudo(
                    r"""ssh-keyscan -t rsa %s |& awk -F `[ ]+` `!/^#/ {print $2" "$3}` >>/root/.ssh/authorized_keys"""
                    % host)


@roles(`all`)
def config_ssh_connection():
    reset_ssh_public_host_key()
    inject_admin_ssh_public_key()
    scan_host_ssh_public_key()


def terminal_debug_win32(func):
    command = "fab -i c:UsersGuodong.sshexportedkey201310171355
                -f %s 
                %s" % (__file__, func)
    os.system(command)


def terminal_debug_posix(func):
    command = "fab -i /etc/ssh/ssh_host_rsa_key
                -f %s 
                %s" % (__file__, func)
    os.system(command)


if __name__ == `__main__`:
    import re

    if len(sys.argv) == 1:
        if is_windows():
            terminal_debug_win32("config_ssh_connection")
            sys.exit(0)
        if is_linux():
            terminal_debug_posix("config_ssh_connection")
            sys.exit(0)

    sys.argv[0] = re.sub(r`(-script.pyw|.exe)?$`, ``, sys.argv[0])
    print red("Please use `fab -f %s`" % " ".join(str(x) for x in sys.argv[0:]))
    sys.exit(1)

參考資訊

一些有用的連結和參考資訊:

  1. Fabric中文文件

  2. Fabric是什麼

  3. 關於雲伺服器相同系統映象模板中OpenSSH金鑰相同的處理方法

tag:SSH互信,SSH公鑰,Fabric

–end–