python之堡壘機(第九天)


 本節作業:

       通過使用paramiko和sqlalchemy實現堡壘機功能

 主要功能實現:

       1、用戶登錄堡壘機后,無需知道密碼或密鑰可以SSH登錄遠端服務器;

       2、用戶對一個組內所有主機批量執行指定命令,獲取格式化輸出;

       3、針對遠端主機,可以進行上傳下載文件;

       4、用戶在遠端主機上執行的命令,均被記錄並入庫,實現審計功能;

 主要參考了alex的開源代碼jumpserver,並添加部分功能。

目前代碼仍不盡人如意,即在SSH的交互模式interactive下,將輸入緩沖區置為0,但無法還原,故交互結束后,有報錯;

故臨時解決方法:使用try和except方法,將OSError類的報錯,強制exit程序(使用pass,coutinue,break等重置、跳出while無法解決)

 

結果:經調試將stdin = os.fdopen(sys.stdin.fileno(), 'rb', 0,closefd=False),exit后,正常回到循環。BUG解決了。

一、堡壘機具體介紹:

1、堡壘機功能 :
        堡壘機,也稱為跳板機,多用於系統運維環境中。指的是在一個特定的網絡環境下,為了保障網絡和數據不受來自外部和內部用戶的入侵和破壞,而運用各種技術手段實時收集和監控網絡環境中每一個組成部分的系統狀態、安全事件、網絡活動,以便集中報警、及時處理及審計定責。
從功能上分析,它綜合了核心系統運維和安全審計管控兩大主要功能;

從技術實現上分析,它通過切斷終端計算機對網絡和服務器資源的直接訪問,而采用協議代理的方式,接管了終端計算機對網絡和服務器的訪問。


2、堡壘機服務器 :
IP :192.168.x.x
數據庫:mysql5.6
程序開發環境:python3 

3、權限說明 :
對訪問權限細分了用戶、帳號、服務器三塊,具體如下:
(1)、用戶:即登錄堡壘機的ssh 帳號,分配給開發、QA、運維;
(2)、帳號:即堡壘機訪問業務服務器的ssh帳號,例:root、ubuntu、只讀帳號等; 
(3)、遠程業務主機:通過綁定主機和主機組跟用戶相關聯;


    三者關系說明:
<1>.同類型業務的主機划分到同一主機組下;方便批量給同一開發小組用戶賦權;
<2>.多個用戶使用同一帳號來訪問同一台業務主機或同一個組下的多台主機;
<3>.一個用戶使用多個帳號來訪問同一台業務主機或同一個組下的多台主機
<4>.優先以主機組的方式賦權給用戶,單個主機賦權次之(無組的主機在登錄界面會歸於ungroupped hots)


4、安全審計說明:
(1)、通過對遠程業務主機進行源地址限制,可以對外攔截非法訪問和惡意攻擊;
(2)、對登錄服務器的操作命令和輸入字符進行記錄,可以對內部人員誤操作和非法操作進行審計監控,以便事后責任追蹤;
(3)、堡壘機登錄業務主機主要使用ssh公私鑰,避免了業務主機的密碼泄漏及內部員工離職的密碼更新;


5、流程安全管理:
想要正確可靠的發揮堡壘機的作用,只靠堡壘機本身是不夠的, 還需要對用戶進行安全上的限制,堡壘機部署后,要確保你的系統達到以下條件:

  • 所有人包括運維、開發等任何需要訪問業務系統的人員,只能通過堡壘機訪問業務系統
    • 回收所有對業務系統的訪問權限,做到除了堡壘機管理人員,沒有人知道業務系統任何機器的登錄密碼
    • 網絡上限制所有人員只能通過堡壘機的跳轉才能訪問業務系統 
  • 確保除了堡壘機管理員之外,所有其它人對堡壘機本身無任何操作權限,只有一個登錄跳轉功能
  • 確保用戶的操作紀錄不能被用戶自己以任何方式獲取到並篡改,達到安全審計的作用。 

二、堡壘機登錄操作流程:

       1、管理員為用戶在堡壘機服務器上創建賬號(使用用戶名密碼或公鑰);

       2、用戶登錄堡壘機,輸入堡壘機用戶名密碼,顯示當前用戶可管理的服務器得列表;

       3、用戶選擇主機組;

       4、用戶選擇具體的主機,並自動登錄;

       5、執行操作並同時將用戶操作記錄;

       6、用戶exit退出業務主機后,返回到堡壘機,程序結束;

       7、程序啟動命令:jump

      具體截圖如下:

      <1> . 選擇組:

<2> . 批量執行命令:

<3> . 選擇主機,輸入「t」,可以傳輸文件:

<4> . 輸入其他字符即正常登錄主機,退出及重啟程序:


業務主機自身的命令記錄:


數據庫審計的命令記錄:

 

 

三、代碼具體說明如下:

目錄結構:
1、bin (主程序目錄)
2、conf   (配置文件目錄)
3、modules  (具體模塊目錄)
4、share  (數據表目錄)

一、bin目錄下的litter_finger.py主程序:
1、添加當前目錄到環境變量;
2、從modules.actions文件里加載excute_from_command_line
3、執行該模塊excute_from_command_line(sys.argv)

二、conf目錄:
1、目錄下的settings.py:
    (1)、定義了BASE_DIR上一級路徑;
    (2)、定義了DB_CONN的連接變量;
    (3)、定義了WELCOME_MSG變量,顯示歡迎界面;
    (4)、定義了USER_GROUP_MSG變量,顯示主機組名稱;
    (5)、定義了GROUP_LIST變量,顯示主機組列表;
    (6)、定義了HOST_LIST變量,顯示主機列表;
    (7)、定義了SSH_SFTP_HELPINFO變量,顯示SFTP幫助信息;
   
2、action_registers.py:
    (1)、加載modules目錄下的views模塊;
    (2)、定義了actions字典,使用反射指定執行啟動、停止、同步、創建用戶,組,綁定主機;
               start_session,stop,syncdb,create_users,create_groups,create_hosts
        ,create_bindhosts,create_remoteusers;

三、modules目錄:
1、目錄下的actions.py:
    (1)、導入conf模塊下的settings和action_registers二個函數;
    (2)、導入modules模塊下的utils函數;
    (3)、定義help_msg函數,輸出執行的命令參數;
    (4)、定義excute_from_command_line(argvs)函數:
                如果參數長度小於2,則輸出幫助信息,並結束;
                第一個位置參數不在指定名稱中,則調用utils的錯誤輸出函數;
                執行指定的命令action_registers.actions[argvs[1]](argvs[1:]),
                例:createusers -f <the new users file>2、目錄下的utils.py:
    (1)、導入conf模塊下的settings;
    (2)、導入yaml模塊;
    (3)、定義print_err(msg,quit=False)函數,如果quit為真,則exit;否則print;
    (4)、定義yaml_parser(yml_filename)解析函數,文件為share目錄下的yml格式文件;

3、目錄下的db_conn.py:
    (1)、導入sqlalchemy和sqlalchemy.orm模塊;
    (2)、導入conf目錄下的setting配置文件的DB變量;
    (3)、創建數據庫連接engine和session;

4、目錄下的models.py:
    (1)、導入連接數據庫的各種模塊;
    (2)、導入密碼hash模塊werkzeug.security;
    (3)、生成一個ORM基類 Base = declarative_base();
    (3)、創建BindHost2Group、BindHost2UserProfile和Group2UserProfile
三個關聯表;
    (4)、定義用戶配置文件UserProfile表,並調用relationship定義關聯groups、bind_hosts和audit_logs;
    (5)、定義遠端用戶RemoteUser表,其中定義了ssh二種認證方法password和key;
    (6)、定義主機Host表,字段為主機名,IP和端口;
    (7)、定義組Group表,其中調用relationship定義關聯bind_hosts和user_profiles;
    (8)、定義綁定主機BindHost表,其中host_id外鍵與host表關聯;remoteuser_id外鍵與remoteuser表關聯;並調用relationship定義了groups、user_profiles進行關聯;
    (9)、定義審計AuditLog表,其中user_id和bind_host_id用外鍵關聯;

5、目錄下的common_filters.py:
    (1)、導入models,db_conn,utils三個模塊;
    (2)、定義函數bind_hosts_filter(vals),判斷一個主機是不是在綁定主機表中,在則返回,否則退出;
    (3)、定義函數user_profiles_filter(vals),判斷一個用戶是否在用戶配置表中,在則返回,否則退出;
    (4)、定義函數groups_filter(vals),判斷一個組是否在組配置表中,在則返回,否則退出;

6、目錄下的ssh_login.py:
    (1)、導入base64,getpass,socket,traceback,paramiko,datetime模塊;
    (2)、導入models模塊;
    (3)、導入interactive模塊;
    (4)、定義ssh_login(user_obj,bind_host_obj,mysql_engine,log_recording)函數
                   定義client = paramiko.SSHClient();
          允許連接不在know_hosts文件中的主機ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy());
          定義client.connect(),參數為bind_host表中的主機IP,端口,用戶名和密碼(如果使用key,則為key的創建密碼);
          定義cmd_caches變量為空列表;
          調用client的invoke_shell(指定終端的寬度和高度)函數;
                   cmd_caches列表追加AuditLog的記錄;
                  調用log_recording函數進行日志記錄
                  調用interactive里的interacitive_shell
                  關閉chan和client

7、目錄下的interactive.py:
     (1)、導入socket,os,sys,paramiko,datetime,select,fcntl,signal,struct等模塊;
     (2)、導入models模塊;
     (3)、使用try導入termios和tty模塊,因windows下沒有此模塊;
     (4)、定義ioctl_GWINSZ,getTerminalSize,resize_pty三個函數,主要目的:終端大小適應。paramiko.channel會創建一個pty(偽終端),有個默認的大小(width=80, height=24),所以登錄過去會發現能顯示的區域很小,並且是固定的。編
輯vim的時候尤其痛苦。channel中有resize_pty方法,但是需要獲取到當前終端的大小。經查找,當終端窗口發生變化時,系統會給前台進程組發送SIGWINCH信號,也就>
是當進程收到該信號時,獲取一下當前size,然后再同步到pty中,那pty中的進程等於也感受到了窗口變化,也會收到SIGWINCH信號。
     (5)、定義交互shell函數,如果是linux,則執行posix_shell;否則執行windows_shell函數;
     (6)、定義posix_shell(chan,user_obj,bind_host_obj,cmd_caches,log_recording)函數,進行輸入輸出操作交互;
     (7)、定義windows的交互模式,目前此功能沒有使用到

8、目錄下的ssh_cmd.py:
     (1)、導入traceback,paramiko模塊;
     (2)、定義ssh_cmd(bind_host_obj,cmd)函數
                  定義client = paramiko.SSHClient();
          允許連接不在know_hosts文件中的主機ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy());
          定義client.connect(),參數為bind_host表中的主機IP,端口,用戶名和密碼(如果使用key,則為key的創建密碼);
                  調用stdin, stdout, stderr = client.exec_command(cmd) ,並返回命令結果;

9、目錄下的ssh_sftp.py:
     (1)、導入traceback,paramiko,stat模塊;
     (2)、定義ssh_sftp(object)類
     (3)、定義類下的__init__(self, host_obj,timeout=30),定義遠端主機變量;
     (4)、顯示文件傳輸幫助信息help(self);
     (5)、定義get下載單個文件函數;
     (6)、定義put上傳單個文件函數;
     (7)、定義獲取遠端linux主機上指定目錄及其子目錄下的所有文件函數;
     (8)、定義下載遠端主機指定目錄到本地目錄函數 ;
     (9)、定義獲取本地指定目錄及其子目錄下的所有文件函數;
     (10)、定義上傳本地目錄下的文件到遠程主機指定目錄函數;


10、目錄下的主程序views.py:
     (1)、導入os,sys,getpass,time
     (2)、導入modules目錄下的models、common_filters、ssh_login等模塊;
     (3)、導入db_conn模塊中的變量engine和session;
     (4)、導入utils里的print_err和yaml_parser函數;
     (5)、定義自登錄login函數(獲取當前系統用戶名自登錄)
                      獲取當前登錄跳板機的用戶;
                      在數據庫的user_profile表中,根據此用戶進行查找匹配;
                      如果不存在,則調用auth認證函數;
                      正常返回user_obj;
     (6)、定義認證函數auth:
                循環3次,輸入用戶名和密碼;
                定義user_obj變量,即去數據庫取userprofile表中的用戶和密碼進行比對 ;
                或不為空,則成功返回;為空則繼續循環;
     (7)、定義框架函數,輸出登錄信息framework(user,group);
     (8)、定義顯示組函數,輸出主機組信息show_group(user);
     (9)、定義顯示主機函數,輸出主機信息show_host(user_obj,group_name,bind_hosts);
     (10)、定義日志記錄到數據庫的函數log_recording(user_obj,bind_host_obj,logs);
     (11)、定義調用多線程函數執行命令make_threading(func,tuple_args);
     (12)、定義文件上傳下載的try和except報錯函數try_exec(client_mode,first_file,second_file);
     (13)、定義調用文件傳輸函數ssh_transport(host_obj),主要有:ldir,rdir,put,get四種方式;
     (14)、定義啟動會話函數start_session(argvs);
                       調用登錄login及認證auth函數,獲取用戶信息實例;
                       進行第一層循環,顯示登錄信息框架;
                       顯示組列表信息,並手工輸入組編號,進行一系列判斷;
                       顯示主機列表進行第二層循環,並手工輸入主機編號,如果輸入e,則批量執行命令;
                       輸入正確主機編號后,輸入t,則進行文件傳輸,輸入其他則登錄主機,進行命令交互;
      (15)、定義創建本地登錄用戶函數create_users(argvs),讀取指定的yaml文件,將數據導入數據庫;
      (16)、定義創建主機組函數create_groups(argvs),讀取指定的yaml文件,將數據導入數據庫;
      (17)、定義創建遠程主機函數create_hosts(argvs),讀取指定的yaml文件,將數據導入數據庫;
      (18)、定義創建遠程主機的登錄信息函數create_remoteusers(argvs),讀取指定的yaml文件,將數據導入數據庫;
      (19)、定義創建綁定主機函數create_bindhosts(argvs),讀取指定的yaml文件,將數據導入數據庫;
      (20)、定義創建所有表結構函數syncdb(argvs);
                       
                      
                  

代碼介紹
代碼介紹

四、代碼BUG記錄

環境說明:

       1、服務器IP:192.168.4.208

        2、運行環境:python3

        3、代碼目錄:/usr/local/jumpserver

        4、程序運行方式:/usr/bin/python3 /usr/local/jumpserver/bin/jumpserver.py start_session

                                     或jump  (已定義別名:alias jump='/usr/local/jumpserver/bin/jumpserver.py start_session'5、調用了python下的ssh訪問模塊paramiko和交互模塊interactive,interactive功能通過讀取鍵盤輸入命令,並將結果返回到屏幕輸出,同時將輸入的字符記錄並導入到數據庫。

 

故障說明及解決方法:

1、在調用ssh_login和interactive.py里,ssh交互執行命令的功能已經實現,就是有點在對上下左右控制鍵上的小問題:1、在命令行按向上鍵查看歷史命令時,第一次按向上鍵沒有輸出顯示,但回車鍵可以正常執行命令   2、在vim編輯狀態下上下左右鍵的處理,有時候會回顯A,B,C,D

故障原因:

      本程序最大BUG:

       當讀取鍵盤輸入時,方向鍵會有問題,因為按一次方向鍵會產生3個字節數據,我的理解是按鍵一次會被select捕捉一次標准輸入有變化,但是我每次只處理1個字節的數據,其他的數據會存放在輸入緩沖區中,等待下次按鍵的時候一起發過去。這就導致了本來3個字節才能完整定義一個方向鍵的行為,但是我只發過去一個字節,所以終端並不知道我要干什么。所以沒有變化,當下次觸發按鍵,才會把上一次的信息完整發過去,看起來就是按一下方向鍵有延遲。多字節的粘貼也是一個原理。

故障解決:

      解決辦法是將輸入緩沖區置為0,這樣就沒有緩沖,有多少發過去多少,這樣就不會有那種顯示的延遲問題了。          

          stdin = os.fdopen(sys.stdin.fileno(), 'rb', 0)

          fd = stdin.fileno()

備注:解決方案見https://blog.csdn.net/zlucifer/article/details/70858491

2、Traceback (most recent call last):

  File "/usr/local/jumpserver/bin/jumpserver.py", line 12, in <module>

    excute_from_command_line(sys.argv)

  File "/usr/local/jumpserver/modules/actions.py", line 26, in excute_from_command_line

    action_registers.actions[argvs[1]](argvs[1:])

  File "/usr/local/jumpserver/modules/views.py", line 251, in start_session

    host_choice = input("[(b)back,(q)quit, select host to login]:").strip()

OSError: [Errno 9] Bad file descriptor

故障原因:

      在解決故障1(本程序最大BUG)上產生的一系列新BUG(包括以下全部BUG)

      (1)、 在設置了緩沖區為0后,導致在業務主機和堡壘機之間的標准輸入sys.stdin不一致,但嘗試了N多方法,也無法還原標准輸入;

                    在業務主機上輸入exit后,結束SSH登錄后,python程序無法識別新的stdin,程序報如上錯誤;

               經調試將stdin = os.fdopen(sys.stdin.fileno(), 'rb', 0,closefd=False),exit后,正常回到循環。

      (2)、 因python3環境下的標准輸入為bytes字節后,讀入內存需要decode為unicode編碼,保存到文件又需要encode為bytes編碼,針對普通字符和漢字產生N多編碼問題;

故障解決:

      (1)、臨時解決方案:使用try和except方法,將OSError類的報錯,強制exit程序(使用pass,coutinue,break等重置、跳出while無法解決)

      (2)、編碼解決方案見如下具體故障;

        

3、*** Caught exception: <class 'ValueError'>: can't have unbuffered text I/O

Traceback (most recent call last):

  File "/usr/local/jumpserver/modules/ssh_login.py", line 54, in ssh_login

    interactive.interactive_shell(chan,user_obj,bind_host_obj,cmd_caches,log_recording)

  File "/usr/local/jumpserver/modules/interactive.py", line 62, in interactive_shell

    posix_shell(chan,user_obj,bind_host_obj,cmd_caches,log_recording)

  File "/usr/local/jumpserver/modules/interactive.py", line 77, in posix_shell

    stdin = os.fdopen(sys.stdin.fileno(), 'r', 0)

  File "/usr/lib/python3.4/os.py", line 980, in fdopen

    return io.open(fd, *args, **kwargs)

ValueError: can't have unbuffered text I/O

 故障原因:

        因python3默認使用的是str類型對字符串編碼,默認使用bytes操作二進制數據流,故在標准輸入時,打開方式應為二進制;

故障解決:

                 stdin = os.fdopen(sys.stdin.fileno(), 'rb', 0)

       同時在讀取標准輸入時,應轉化為str編碼:

                 x = stdin.read(1).decode('utf-8',errors='ignore')

 

4、Caught exception: <class 'UnicodeDecodeError'>: 'utf-8' codec can't decode bytes in position 1022-1023: unexpected end of data

Traceback (most recent call last):

  File "/usr/local/jumpserver/modules/ssh_login.py", line 53, in ssh_login

    interactive.interactive_shell(chan,user_obj,bind_host_obj,cmd_caches,log_recording)

  File "/usr/local/jumpserver/modules/interactive.py", line 61, in interactive_shell

    posix_shell(chan,user_obj,bind_host_obj,cmd_caches,log_recording)

  File "/usr/local/jumpserver/modules/interactive.py", line 110, in posix_shell

    x = u(chan.recv(1024))

  File "/usr/local/lib/python3.4/dist-packages/paramiko/py3compat.py", line 143, in u

    return s.decode(encoding)

UnicodeDecodeError: 'utf-8' codec can't decode bytes in position 1022-1023: unexpected end of data

故障原因:

       python3環境下,因在interactive交互過程中,標准輸出需要轉化為str編碼,文件中的漢字無法decode;

故障解決:

       使用x = chan.recv(1024).decode('utf-8',errors='ignore')替換原有的 x = u(chan.recv(1024)),即在decode中,如有報錯,則忽略;

 

5、'utf-8' codec can't encode character '\udc9b' in position 0: surrogates not allowed

Traceback (most recent call last):

  File "/usr/lib/python3/dist-packages/CommandNotFound/util.py", line 24, in crash_guard

    callback()

  File "/usr/lib/command-not-found", line 90, in main

    if not cnf.advise(args[0], options.ignore_installed) and not options.no_failure_msg:

  File "/usr/lib/python3/dist-packages/CommandNotFound/CommandNotFound.py", line 265, in advise

    packages = self.getPackages(command)

  File "/usr/lib/python3/dist-packages/CommandNotFound/CommandNotFound.py", line 157, in getPackages

    result.update([(pkg, db.component) for pkg in db.lookup(command)])

  File "/usr/lib/python3/dist-packages/CommandNotFound/CommandNotFound.py", line 85, in lookup

    result = self.db.lookup(command)

  File "/usr/lib/python3/dist-packages/CommandNotFound/CommandNotFound.py", line 41, in lookup

    key = key.encode('utf-8')

UnicodeEncodeError: 'utf-8' codec can't encode character '\udc9b' in position 0: surrogates not allowed

故障原因:

       python3環境下,因在interactive交互過程中,標准輸入為bytes字節碼,需要轉化為str編碼,但一個漢字需要3個字節,故原有的x = u(stdin.read(1))會導致漢字無法獲取,故報錯;

故障解決:

      臨時解決方案:x = u(stdin.read(3))
6、Caught exception: <class 'UnicodeEncodeError'>: 'ascii' codec can't encode character '\u4f60' in position 0: ordinal not in range(128)

Traceback (most recent call last):

  File "/usr/local/jumpserver/modules/ssh_login.py", line 53, in ssh_login

    interactive.interactive_shell(chan,user_obj,bind_host_obj,cmd_caches,log_recording)

  File "/usr/local/jumpserver/modules/interactive.py", line 62, in interactive_shell

    posix_shell(chan,user_obj,bind_host_obj,cmd_caches,log_recording)

  File "/usr/local/jumpserver/modules/interactive.py", line 126, in posix_shell

    sys.stdout.write(x)

UnicodeEncodeError: 'ascii' codec can't encode character '\u4f60' in position 0: ordinal not in range(128)

故障原因:

     因mac下的ssh訪問終端工具iTerm2編碼不是utf-8,故有此報錯;

故障解決:

     將終端編碼更換為utf-8;

 

7、/usr/local/lib/python3.4/dist-packages/pymysql/cursors.py:166: Warning: (1265, "Data truncated for column 'cmd' at row 1")

                        result = self._query(query)

故障原因:

       因輸出獲取以1024字節,偶爾會出現超過1024字節的這類警告;

故障解決:

        因偶爾出現,暫不影響使用,故此warning暫無處理;

        將audit_log表中的cmd字段屬性改為text類型即可解決。
BUG

五、代碼如下:

目錄結構:

<1>、bin (主程序目錄)

<2>、conf   (配置文件目錄)

<3>、modules  (具體模塊目錄)

<4>、share  (數據表目錄)

1、bin目錄下主程序jumpserver.py代碼:

#!/usr/bin/python3
#_*_coding:utf-8_*_
__author__ = 'Kevin Wang'
import os,sys,readline

###定義上級目錄###
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

###將上級目錄添加到環境變量PATH中###
sys.path.append(BASE_DIR)

###主程序,調用actions下的函數excute_from_command_line###
if __name__ == '__main__':
    from modules.actions import excute_from_command_line
    excute_from_command_line(sys.argv)
jumpserver.py

 

2、conf目錄下settings.py代碼:

定義些顯示輸出的變量

#!/usr/bin/python3
#_*_coding:utf-8_*_
__author__ = 'Kevin Wang'

###MYSQL數據庫連接地址###
DB_CONN = "mysql+pymysql://user:password@192.168.4.x:3306/db_name?charset=utf8"


###菜單顯示主界面,會顯示登錄的用戶名和當前的時間###
WELCOME_MSG = '''\033[32;1m
********************************************************************************
*                                                                              *
*                                FDT JumpServer                                *
*                                                                              *
*{0}*
********************************************************************************
\033[0m'''

###顯示主前選擇的組名稱###
USER_GROUP_MSG = '''\033[35;1m 
--------------------------------------------------------------------------------
group : {0}
--------------------------------------------------------------------------------
\033[0m'''

###輸出主前的主機組列表###
GROUP_LIST = '''
================================================================================
group_id  group_name                              host_num       
'''

###輸出選擇主機組下的主機列表###
HOST_LIST = '''
================================================================================
host_id   host_user@host_name                     host_ipaddress         
'''

###SSH的FTP的幫助信息###
SSH_SFTP_HELPINFO = '''\033[35;1m 
--------------------------------------------------------------------------------
功能:可單個文件上傳、下載
操作命令如下:
    1、列出本地目錄: ldir 本地目錄
    2、列出遠端目錄: rdir 遠端目錄
    3、上傳文件:put 本地文件 遠程目錄
    4、下載文件:get 遠程文件 本地目錄
    5、退出輸入q or Q
舉例:
    1、ldir /home/fdt
    2、rdir /home/ubuntu
    3、put /tmp/test.txt /home/ubuntu
    4、get /home/ubuntu/xx.tar.gz /tmp
--------------------------------------------------------------------------------
\033[0m'''
settings.py

 

3、conf目錄下的action_registers.py代碼:

###定義actions字典,使用反射指定執行啟動、停止、同步、創建用戶,組,綁定主機操作###

#!/usr/bin/python3
#_*_coding:utf-8_*_
__author__ = 'Kevin Wang'

from modules import views

###定義actions字典,使用反射指定執行啟動、停止、同步、創建用戶,組,綁定主機操作###
actions = {
    'start_session': views.start_session,
    #'stop': views.stop_server,
    'syncdb': views.syncdb,
    'create_users': views.create_users,
    'create_groups': views.create_groups,
    'create_hosts': views.create_hosts,
    'create_bindhosts': views.create_bindhosts,
    'create_remoteusers': views.create_remoteusers,

}
action_registers.py

 

4、modules目錄下的actions.py代碼:

#!/usr/bin/python3
#_*_coding:utf-8_*_
__author__ = 'Kevin Wang'

from conf import settings
from conf import action_registers
from modules import utils


###定義幫助函數:提示命令格式###
def help_msg():
    '''
    print help msgs
    :return:
    '''
    print("\033[31;1mAvailable commands:\033[0m")
    for key in action_registers.actions:
        print("\t",key)

###定義excute_from_command_line(argvs)函數###
def excute_from_command_line(argvs):

    ###如果參數長度小於2,則輸出幫助信息,並結束###
    if len(argvs) < 2:
        help_msg()
        exit()
    
    ###第一個位置參數不在指定名稱中,則調用utils的錯誤輸出函數###
    if argvs[1] not in action_registers.actions:
        utils.print_err("Command [%s] does not exist!" % argvs[1], quit=True)

    ###執行指定的命令action_registers.actions,例:createusers -f <the new users file>###
    action_registers.actions[argvs[1]](argvs[1:])
actions.py

 

5、modules目錄下的utils.py代碼:

定義錯誤輸出函數和yaml配置文件解析函數

#!/usr/bin/python3
#_*_coding:utf-8_*_
__author__ = 'Kevin Wang'

import hashlib
from conf import settings
import yaml
try:
    from yaml import CLoader as Loader, CDumper as Dumper
except ImportError:
    from yaml import Loader, Dumper

###定義print_err(msg,quit=False)函數,如果quit為真,則exit;否則print###
def print_err(msg,quit=False):
    '''
    輸出錯誤信息 
    :param msg : 錯誤信息變量
    :return:
    '''
    output = "\033[31;1mError: %s\033[0m" % msg
    if quit:
        exit(output)
    else:
        print(output)

###定義yaml_parser(yml_filename)解析函數,文件為share目錄下的yml格式文件###
def yaml_parser(yml_filename):
    '''
    load yaml file and return
    :param yml_filename : yaml格式的配置文件
    :return:
    '''
    #yml_filename = "%s/%s.yml" % (settings.StateFileBaseDir,yml_filename)
    try:
        yaml_file = open(yml_filename,'r')
        data = yaml.load(yaml_file)
        return data
    except Exception as e:
        print_err(e)
utils.py

 

6、modules目錄下的db_conn.py代碼:

定義和創建數據庫連接的engine和session

#!/usr/bin/python3
#_*_coding:utf-8_*_
__author__ = 'Kevin Wang'

import os,sys

#BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

#sys.path.append(BASE_DIR)

###導入sqlalchemy和sqlalchemy.orm模塊###
from sqlalchemy import create_engine,Table
from sqlalchemy.orm import sessionmaker

from conf import settings

###創建數據庫連接engine和session###
engine = create_engine(settings.DB_CONN)
#engine = create_engine(settings.DB_CONN,echo=True)

###創建與數據庫的會話session class ,注意,這里返回給session的是個class,不是實例###
SessionCls = sessionmaker(bind=engine) 
session = SessionCls()
db_conn.py

 

7、modules目錄下的models.py代碼:

創建host,group,remoteuser,userprofile,bind_host等數據

#!/usr/bin/python3
#_*_coding:utf-8_*_
__author__ = 'Kevin Wang'

import os,sys

#BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

#sys.path.append(BASE_DIR)


from sqlalchemy import create_engine,Table
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String,ForeignKey,UniqueConstraint,UnicodeText,DateTime,Text
from sqlalchemy.orm import relationship
from sqlalchemy import or_,and_
from sqlalchemy import func
from sqlalchemy_utils import ChoiceType,PasswordType
from sqlalchemy.ext.hybrid import hybrid_property

###導入密碼hash模塊###
from werkzeug.security import generate_password_hash,check_password_hash


###生成一個ORM基類###
Base = declarative_base() #生成一個ORM 基類

###創建BindHost2Group關聯表###
BindHost2Group = Table('bindhost_2_group',Base.metadata,
    Column('bindhost_id',ForeignKey('bind_host.id'),primary_key=True),
    Column('group_id',ForeignKey('group.id'),primary_key=True),
)

###創建BindHost2UserProfile關聯表###
BindHost2UserProfile = Table('bindhost_2_userprofile',Base.metadata,
    Column('bindhost_id',ForeignKey('bind_host.id'),primary_key=True),
    Column('userprofile_id',ForeignKey('user_profile.id'),primary_key=True),
)

###創建Group2UserProfile關聯表###
Group2UserProfile = Table('group_2_userprofile',Base.metadata,
    Column('userprofile_id',ForeignKey('user_profile.id'),primary_key=True),
    Column('group_id',ForeignKey('group.id'),primary_key=True),
)


#class UserProfile(Base):
#    __tablename__ = 'user_profile'
#    id = Column(Integer,primary_key=True,autoincrement=True)
#    username = Column(String(32),unique=True,nullable=False)
#    password = Column(String(128),unique=True,nullable=False)
#    groups = relationship('Group',secondary=Group2UserProfile)
#    bind_hosts = relationship('BindHost',secondary=BindHost2UserProfile)
#    audit_logs = relationship('AuditLog')
#
#    def __repr__(self):
#        return "<UserProfile(id='%s',username='%s')>" % (self.id,self.username)

###定義用戶配置文件UserProfile表,並調用relationship定義關聯groups、bind_hosts和audit_logs###
class UserProfile(Base):
    __tablename__ = 'user_profile'
    id = Column(Integer,primary_key=True,autoincrement=True)
    username = Column(String(32),unique=True,nullable=False)
    password = Column(String(255),unique=True,nullable=False)
    groups = relationship('Group',secondary=Group2UserProfile)
    bind_hosts = relationship('BindHost',secondary=BindHost2UserProfile)
    audit_logs = relationship('AuditLog')
    
    #def __init__(self,name,passwd):
    #    self.username = name
    #    self.password = passwd


    @hybrid_property
    def passwd(self):
        return self.password

    ###通過調用hybrid屬性對密碼進行加密###
    @passwd.setter
    def passwd(self,plaintext):
        self.password = generate_password_hash(plaintext)

    ###定義密碼修改函數###
    def verify_passwd(self,plaintext):
        return check_password_hash(self.password,plaintext)

    def __repr__(self):
        #return "<UserProfile(id='%s',username='%s')>" % (self.id,self.username)
        return "<UserProfile(id='%s',username='%s',password='%s')>" % (self.id,self.username,self.password)

###定義遠端用戶RemoteUser表,其中定義了ssh二種認證方法password和key###
class RemoteUser(Base):
    __tablename__ = 'remote_user'
    AuthTypes = [
        (u'ssh-passwd',u'SSH/Password'),
        (u'ssh-key',u'SSH/KEY'),
    ]
    id = Column(Integer,primary_key=True,autoincrement=True)
    auth_type = Column(ChoiceType(AuthTypes))
    username = Column(String(64),nullable=False)
    password = Column(String(255))

    __table_args__ = (UniqueConstraint('auth_type', 'username','password', name='_user_passwd_uc'),)

    def __repr__(self):
        return "<RemoteUser(id='%s',auth_type='%s',user='%s')>" % (self.id,self.auth_type,self.username)

###定義主機Host表,字段為主機名,IP和端口###
class Host(Base):
    __tablename__ = 'host'
    id = Column(Integer,primary_key=True,autoincrement=True)
    hostname = Column(String(64),unique=True,nullable=False)
    ip_addr = Column(String(128),unique=True,nullable=False)
    port = Column(Integer,default=22)
    #bind_hosts = relationship("BindHost")
    def __repr__(self):
        return "<Host(id='%s',hostname='%s')>" % (self.id,self.hostname)

###定義組Group表,其中調用relationship定義關聯bind_hosts和user_profiles###
class Group(Base):
    __tablename__ = 'group'
    id = Column(Integer,primary_key=True)
    name = Column(String(64),nullable=False,unique=True)
    bind_hosts = relationship("BindHost",secondary=BindHost2Group, back_populates='groups' )
    user_profiles = relationship("UserProfile",secondary=Group2UserProfile )

    def __repr__(self):
        return "<HostGroup(id='%s',name='%s')>" % (self.id,self.name)

###定義綁定主機BindHost表,其中host_id外鍵與host表關聯;remoteuser_id外鍵與remoteuser表關聯;並調用relationship定義了groups、user_profiles進行關聯###
class BindHost(Base):
    '''Bind host with different remote user,
       eg. 192.168.1.1 mysql passAbc123
       eg. 10.5.1.6    mysql pass532Dr!
       eg. 10.5.1.8    mysql pass532Dr!
       eg. 192.168.1.1 root
    '''
    __tablename__ = 'bind_host'
    id = Column(Integer,primary_key=True,autoincrement=True)
    host_id = Column(Integer,ForeignKey('host.id'))
    remoteuser_id = Column(Integer,ForeignKey('remote_user.id'))

    host = relationship("Host")
    remoteuser = relationship("RemoteUser")
    groups = relationship("Group",secondary=BindHost2Group,back_populates='bind_hosts')
    user_profiles = relationship("UserProfile",secondary=BindHost2UserProfile)
    audit_logs = relationship('AuditLog')

    __table_args__ = (UniqueConstraint('host_id', 'remoteuser_id', name='_bindhost_and_user_uc'),)

    def __repr__(self):
        return "<BindHost(id='%s',name='%s',user='%s')>" % (self.id,
                                                           self.host.hostname,
                                                           self.remoteuser.username
                                                                      )
###定義審計AuditLog表,其中user_id和bind_host_id用外鍵關聯###
class AuditLog(Base):
    __tablename__ = 'audit_log'
    id = Column(Integer,primary_key=True)
    user_id = Column(Integer,ForeignKey('user_profile.id'))
    bind_host_id = Column(Integer,ForeignKey('bind_host.id'))
    action_choices = [
        (0,'CMD'),
        (1,'Login'),
        (2,'Logout'),
        (3,'GetFile'),
        (4,'SendFile'),
        (5,'Exception'),
    ]
    action_choices2 = [
        (u'cmd',u'CMD'),
        (u'login',u'Login'),
        (u'logout',u'Logout'),
        #(3,'GetFile'),
        #(4,'SendFile'),
        #(5,'Exception'),
    ]
    action_type = Column(ChoiceType(action_choices2))
    #action_type = Column(String(64))
    #cmd = Column(String(255))
    cmd = Column(Text)
    date = Column(DateTime)

    user_profile = relationship("UserProfile")
    bind_host = relationship("BindHost")
    '''
    def __repr__(self):
        return "<user=%s,host=%s,action=%s,cmd=%s,date=%s>" %(self.user_profile.username,
                                                      self.bind_host.host.hostname,
                                                              self.action_type,
                                                              self.date)
    '''
models.py

 

8、modules目錄下的common_filters.py代碼:

定義用戶、組、綁定主機的表過濾函數

#!/usr/bin/python3
#_*_coding:utf-8_*_
__author__ = 'Kevin Wang'

import os,sys

#BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

#sys.path.append(BASE_DIR)

from modules import models
from modules.db_conn import engine,session
from modules.utils import print_err

###定義函數bind_hosts_filter(vals),判斷一個主機是不是在綁定主機表中,在則返回,否則退出###
def bind_hosts_filter(vals):
    host = session.query(models.Host).filter(models.Host.hostname.in_(vals.get('bind_hosts'))).all()
    bind_hosts_id = [h.id for h in host]
    bind_hosts = session.query(models.BindHost).filter(models.BindHost.host_id.in_(bind_hosts_id)).all()
    if not bind_hosts:
        print_err("none of [%s] exist in bind_host table." % vals.get('bind_hosts'),quit=True)
    return bind_hosts

###定義函數user_profiles_filter(vals),判斷一個用戶是否在用戶配置表中,在則返回,否則退出###
def user_profiles_filter(vals):
    user_profiles = session.query(models.UserProfile).filter(models.UserProfile.username.in_(vals.get('user_profiles'))).all()
    if not user_profiles:
        print_err("none of [%s] exist in user_profile table." % vals.get('user_profiles'),quit=True)
    return  user_profiles

###定義函數groups_filter(vals),判斷一個組是否在組配置表中,在則返回,否則退出;
def groups_filter(vals):
    groups = session.query(models.Group).filter(models.Group.name.in_(vals.get('groups'))).all()
    if not groups:
        print_err("none of [%s] exist in group table." % val.get('groups'),quit=True) 
    return groups 
common_filters.py

 

9、modules目錄下的ssh_login.py代碼:

定義ssh交互函數

#_*_coding:utf-8_*_
__author__ = 'Kevin Wang'

import os,sys

#BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

#sys.path.append(BASE_DIR)

import base64,getpass,os,socket,sys,traceback,datetime
from paramiko.py3compat import input
from  modules import models

import paramiko
try:
    import interactive
except ImportError:
    from . import interactive

###定義ssh_login(user_obj,bind_host_obj,mysql_engine,log_recording)函數,連接指定主機###
def ssh_login(user_obj,bind_host_obj,mysql_engine,log_recording):
    '''
    now, connect and use paramiko Client to negotiate SSH2 across the connection
    :param: user_obj : 當前登錄的用戶實例obj
    :param: bind_host_obj : 當前登錄的主機實例obj
    :param: mysql_engine : mysql數據庫的實例 
    :param: log_recording : 審計命令函數名
    :return: 無返回值
    '''

    try:
        #default_path = os.path.join(os.environ['HOME'], '.ssh', 'id_rsa')
        #private_key = paramiko.RSAKey.from_private_key_file(default_path)
        client = paramiko.SSHClient()
        #client.load_system_host_keys()
        client.load_host_keys('/tmp/.ssh/known_hosts')
        client.save_host_keys('/tmp/.ssh/known_hosts')
      
        #client.set_missing_host_key_policy(paramiko.WarningPolicy())
        ###允許連接不在know_hosts文件中的主機###
        client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        print('***...Connecting...***')

        ###定義client.connect(),參數為bind_host表中的主機IP,端口,用戶名和密碼(如果使用key,則為key的創建密碼)###
        client.connect(bind_host_obj.host.ip_addr,
                       bind_host_obj.host.port,
                       bind_host_obj.remoteuser.username,
                       bind_host_obj.remoteuser.password,
                       #pkey=private_key,
                       timeout=30)

        ###定義cmd_caches變量為空列表###
        cmd_caches = []

        ###調用client的invoke_shell,指定終端的寬度和高度###
        chan = client.invoke_shell(term='vt100', width=149, height=39, width_pixels=0, height_pixels=0, environment=None)

        ###cmd_caches列表追加AuditLog的記錄###
        cmd_caches.append(models.AuditLog(user_id=user_obj.id,
                                          bind_host_id=bind_host_obj.id,
                                          action_type='login',
                                          date=datetime.datetime.now()
                                          ))
        ###調用log_recording函數進行日志記錄###
        log_recording(user_obj,bind_host_obj,cmd_caches)

        ###調用interactive里的interacitive_shell###
        interactive.interactive_shell(chan,user_obj,bind_host_obj,cmd_caches,log_recording)
        
        ###關閉chan和client###
        chan.close()
        client.close()
  
    ###異常處理###
    except Exception as e:
        print('*** Caught exception: %s: %s' % (e.__class__, e))
        #traceback.print_exc()
        try:
            client.close()
        except:
            pass
        sys.exit(1)
ssh_login.py

 

10、modules目錄下的interactive.py代碼:

#_*_coding:utf-8_*_
# Copyright (C) 2003-2007  Robey Pointer <robeypointer@gmail.com>
#
# This file is part of paramiko.
#
# Paramiko is free software; you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation; either version 2.1 of the License, or (at your option)
# any later version.
#
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Paramiko; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA.


import socket
import os,sys
from paramiko.py3compat import u
from  modules import models
import datetime

import select
import fcntl,signal,struct

# windows does not have termios...
try:
    import termios
    import tty
    has_termios = True
except ImportError:
    has_termios = False

now_channel = None

###終端大小適應。paramiko.channel會創建一個pty(偽終端),有個默認的大小(width=80, height=24),所以登錄過去會發現能顯示的區域很小,並且是固定的。編輯vim的時候尤其痛苦。channel中有resize_pty方法,但是需要獲取到當前終端的大小。經查找,當終端窗口發生變化時,系統會給前台進程組發送SIGWINCH信號,也就是當進程收到該信號時,獲取一下當前size,然后再同步到pty中,那pty中的進程等於也感受到了窗口變化,也會收到SIGWINCH信號。###
def ioctl_GWINSZ(fd):  
    try:  
        cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ,'aaaa'))  
    except:  
        return  
    return cr  
  
def getTerminalSize():  
    cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)  
    return int(cr[1]), int(cr[0])  
      
  
def resize_pty(signum=0, frame=0):  
    width, height = getTerminalSize()  
    if now_channel is not None:  
        now_channel.resize_pty(width=width, height=height)  

###定義交互shell函數,如果是linux,則執行posix_shell;否則執行windows_shell函數###
def interactive_shell(chan,user_obj,bind_host_obj,cmd_caches,log_recording):
    if has_termios:
        posix_shell(chan,user_obj,bind_host_obj,cmd_caches,log_recording)
    else:
        windows_shell(chan)

###定義posix_shell(chan,user_obj,bind_host_obj,cmd_caches,log_recording)函數,進行輸入輸出操作交互###
def posix_shell(chan,user_obj,bind_host_obj,cmd_caches,log_recording):
    '''
    調用termios和pty進行輸入、輸出操作交互
    :param chan : invoke_shell的實例obj
    :param user_obj : 當前登錄的用戶實例obj
    :param: bind_host_obj : 當前登錄的主機實例obj
    :cmd_caches : 命令列表
    :param: log_recording : 審計命令函數名
    :return: 無返回值
    '''

    ###定義全局變量now_channel,並將chan賦值###
    global now_channel
    now_channel = chan

    ###執行pty,對寬度和高度進行調整###
    resize_pty()

    ###當終端窗口發生變化時,系統會給前台進程組發送SIGWINCH信號###
    signal.signal(signal.SIGWINCH, resize_pty)

    ###將輸入緩沖區置為0###
    stdin = os.fdopen(sys.stdin.fileno(), 'rb', 0)
   
    ###定義新的文件描述符,為數字ID###
    fd = stdin.fileno()    
 

    oldtty = termios.tcgetattr(fd)
    newtty = termios.tcgetattr(fd)  
    newtty[3] = newtty[3] | termios.ICANON 
    try:
    ### 為tty設置新屬性
    ### 默認當前tty設備屬性:
    ### 輸入一行回車,執行
    ### CTRL+C 進程退出,遇到特殊字符,特殊處理。

    ### 這是為原始模式,不認識所有特殊符號
    ### 放置特殊字符應用在當前終端,如此設置,將所有的用戶輸入均發送到遠程服務器
        #termios.tcsetattr(fd, termios.TCSANOW, newtty)
        tty.setraw(fd)
        tty.setcbreak(fd)
        chan.settimeout(0.0)

        cmd = ''

        tab_key = False
        while True:
        ### 監視用戶輸入和服務器返回數據,阻塞,直到句柄可讀
        ### sys.stdin 處理用戶輸入
        ### chan 是之前創建的通道,用於接收服務器返回信息
        ### select模塊是IO多路復用模塊,功能強大,無處不在,你值得學習和記憶!
            try:
                r, w, e = select.select([chan,stdin], [], [])
            except:
                ### 解決SIGWINCH信號將休眠的select系統調用喚醒引發的系統中斷,忽略中斷重新調用解決。
                continue
            if chan in r:
                try:
                    x = chan.recv(1024).decode('utf-8',errors='ignore')
                    #x = u(chan.recv(1024))


                    ###在獲取到輸入為\t時,如能正常補齊並顯示的唯一字符串即賦值給x(例:/etc/iss然后tab,可以補齊為issue,則x的值為ue);
                    ###如補齊無值或有多個值時,x的值為二進制的'\x07';
                    ###如多次按tab鍵時,x的值會為'\r\n'
                    if tab_key:
                        if x not in ('\x07','\r\n'):
                            cmd += x
                        tab_key = False

                    if len(x) == 0:
                        sys.stdout.write('\r\n*** EOF\r\n')
                        break
                    sys.stdout.write(x)
                    sys.stdout.flush()
                except socket.timeout:
                    pass

            if stdin in r:
                ###因為打開的fd是二進制,所以輸入的字符需要轉換為unicode, 因漢字占3個字節,故一次接收值為3###
               
                #x = stdin.read(1).decode('utf-8',errors='ignore')
                x = u(stdin.read(3))

                if len(x) == 0:
                    break
                if x == '\t':
                    tab_key = True
                ###如果輸入字符為退格鍵,則命令cmd重新賦值###
                elif x == '\x08':
                    cmd = cmd[0:-1]
                elif x != '\r':
                    cmd += x
                else:
                    ###如果命令不為空或者並非以上下左右鍵或退格鍵開頭的輸入,則導入到數據庫中###
                    if len(cmd) >= 1 and not cmd.startswith(('\x07','\x08','\x1b[A','\x1b[B','\x1b[C','\x1b[D')):
                        log_item = models.AuditLog(user_id=user_obj.id,
                                              bind_host_id=bind_host_obj.id,
                                              action_type='cmd',
                                              cmd=cmd,
                                              date=datetime.datetime.now()
                                              )
                        cmd_caches.append(log_item)
                    cmd = ''

                    ###如果命令列表超過1,則調用日記審計函數###
                    if len(cmd_caches)>=1:
                        log_recording(user_obj,bind_host_obj,cmd_caches)
                        cmd_caches = []


                chan.send(x)

    finally:
        ###將標准輸入重新定為正常的模式###
        termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty)

    
###windows交互模式,目前此功能沒有使用到###
def windows_shell(chan):
    import threading

    sys.stdout.write("Line-buffered terminal emulation. Press F6 or ^Z to send EOF.\r\n\r\n")
        
    def writeall(sock):
        while True:
            data = sock.recv(256)
            if not data:
                sys.stdout.write('\r\n*** EOF ***\r\n\r\n')
                sys.stdout.flush()
                break
            sys.stdout.write(data)
            sys.stdout.flush()
        
    writer = threading.Thread(target=writeall, args=(chan,))
    writer.start()
        
    try:
        while True:
            d = sys.stdin.read(1)
            if not d:
                break
            chan.send(d)
    except EOFError:
        # user hit ^Z or F6
        pass
interactive.py

 

11、modules目錄下的ssh_cmd.py代碼:

定義ssh的批量命令執行函數

#_*_coding:utf-8_*_
__author__ = 'Kevin Wang'

import os,sys

#BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

#sys.path.append(BASE_DIR)

import traceback
import paramiko

###定義ssh_cmd(bind_host_obj,cmd)函數###
def ssh_cmd(bind_host_obj,cmd):
    '''
    now, connect and use paramiko Client to negotiate SSH2 across the connection
    :param: bind_host_obj : 當前登錄的主機實例obj
    :param: cmd : 需要執行的命令
    '''
  
    try:
        #default_path = os.path.join(os.environ['HOME'], '.ssh', 'id_rsa')
        #private_key = paramiko.RSAKey.from_private_key_file(default_path)
        client = paramiko.SSHClient()

        #client.load_system_host_keys()
        #client.set_missing_host_key_policy(paramiko.WarningPolicy())

        ###允許連接不在know_hosts文件中的主機###
        client.set_missing_host_key_policy(paramiko.AutoAddPolicy())

        ###定義client.connect(),參數為bind_host表中的主機IP,端口,用戶名和密碼(如果使用key,則為key的創建密碼)###
        client.connect(bind_host_obj.host.ip_addr,
                       bind_host_obj.host.port,
                       bind_host_obj.remoteuser.username,
                       bind_host_obj.remoteuser.password,
                       #pkey=private_key,
                       timeout=30)
        ###調用stdin, stdout, stderr = client.exec_command(cmd) ,並返回命令結果###
        stdin, stdout, stderr = client.exec_command(cmd)  # 執行命令,不可執行類似vim,top watch命令
        result = stdout.read().decode()
        print("=================================================================================")
        print("\033[32;1m%s(%s)【%s】 result is : \033[0m" % (bind_host_obj.host.hostname,bind_host_obj.host.ip_addr,cmd))
        print(result,stderr.read().decode())
        client.close()

    ###錯誤異常處理###
    except Exception as e:
        print('*** Caught exception: %s: %s' % (e.__class__, e))
        traceback.print_exc()
        try:
            client.close()
        except:
            pass
        sys.exit(1)
ssh_cmd.py

 

12、modules目錄下的ssh_sftp.py代碼:

定義ssh的ftp上傳下載文件函數

#_*_coding:utf-8_*_
__author__ = 'Kevin Wang'

import os,sys,subprocess

#BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
#sys.path.append(BASE_DIR)

import traceback
import paramiko
from conf import settings
from modules import utils

from stat import ST_MODE, S_ISREG, S_ISDIR, S_ISLNK

from progressbar import ETA,FileTransferSpeed,Percentage,ProgressBar,Bar


#def example(file_size):
#    class CrazyFileTransferSpeed(FileTransferSpeed):
#        """It's bigger between 45 and 80 percent."""
#        def update(self, pbar):
#            if 45 < pbar.percentage() < 80:
#                return 'Bigger Now ' + FileTransferSpeed.update(self,pbar)
#            else:
#                return FileTransferSpeed.update(self,pbar)
#
#    #widgets = [CrazyFileTransferSpeed(),' <<<', Bar(), '>>> ', Percentage(),' ', ETA()]
#    widgets = [Percentage(),' <<<', Bar(), '>>> ', ' ',ETA(),CrazyFileTransferSpeed()]
#    pbar = ProgressBar(widgets=widgets, maxval=file_size)
#    # maybe do something
#    pbar.start()
#    new_size = int(file_size / 5)
#    for i in range(new_size):
#        pbar.update(5*i+1)
#    pbar.finish()


###定義SSH下的FTP類,進行文件傳輸###
class ssh_sftp(object):
    '''
    定義SSH下的FTP類,進行文件傳輸
    '''

    def __init__(self, host_obj,timeout=30):
        self.host_obj = host_obj 
        self.timeout = timeout
        
        ###定義ssh密鑰路徑及文件###
        self.__path = os.path.join(os.environ['HOME'], '.ssh', 'id_rsa')
        self.__private_key = paramiko.RSAKey.from_private_key_file(self.__path,password='hkfdt.com')

    ###顯示文件傳輸幫助信息函數### 
    def help(self):
        print(settings.SSH_SFTP_HELPINFO,end='')

    ###get下載單個文件函數###
    def sftp_get(self, remotefile, localfile):
        ###調用paramiko.SFTPClient###
        t = paramiko.Transport(sock=(self.host_obj.host.ip_addr,self.host_obj.host.port))
        t.connect(username=self.host_obj.remoteuser.username,pkey=self.__private_key)
        sftp = paramiko.SFTPClient.from_transport(t) 

        ###下載遠端文件到本地目錄###
        print("filename  is %s, the file transferring is starting ..." % remotefile)
        sftp.get(remotefile, localfile)
        print("file transferring is end ...")
        t.close()

    ###put上傳單個文件函數###
    def sftp_put(self,localfile,remotefile):
        t = paramiko.Transport(sock=(self.host_obj.host.ip_addr,self.host_obj.host.port))
        t.connect(username=self.host_obj.remoteuser.username,pkey=self.__private_key)
        sftp = paramiko.SFTPClient.from_transport(t) 

        ###上傳本地文件到遠端主機目錄###
        print("filename  is %s, the file transferring is starting ..." % localfile)
        sftp.put(localfile, remotefile)
        print("file transferring is end ...")
        t.close()

    ###獲取遠端linux主機上指定目錄及其子目錄下的所有文件###
    def get_all_files_in_remote_dir(self,remote_dir):
        t = paramiko.Transport(sock=(self.host_obj.host.ip_addr,self.host_obj.host.port))
        t.connect(username=self.host_obj.remoteuser.username,pkey=self.__private_key)
        sftp = paramiko.SFTPClient.from_transport(t) 
        
        ###保存所有文件的列表###
        all_files = list()

        ###去掉路徑字符串最后的字符'/',如果有的話###
        if remote_dir[-1] == '/':
            remote_dir = remote_dir[0:-1]

        ###獲取當前指定目錄下的所有目錄及文件,包含屬性值###
        files = sftp.listdir_attr(remote_dir)
        for x in files:
            ###remote_dir目錄中每一個文件或目錄的完整路徑###
            print(x)
            filename = remote_dir + '/' + x.filename
            ###如果是目錄,則遞歸處理該目錄,這里用到了stat庫中的S_ISDIR方法,與linux中的宏的名字完全一致###
            #if S_ISDIR(x.st_mode):
            #    all_files.extend(self.get_all_files_in_remote_dir(filename))
            #else:
            #    all_files.append(filename)
            all_files.append(filename)
        return all_files
        t.close()


    ###下載遠端主機指定目錄到本地目錄###
    def sftp_get_dir(self, remote_dir, local_dir):
        t = paramiko.Transport(sock=(self.host_obj.host.ip_addr,self.host_obj.host.port))
        t.connect(username=self.host_obj.remoteuser.username,pkey=self.__private_key)
        sftp = paramiko.SFTPClient.from_transport(t) 
    
        ###獲取遠端linux主機上指定目錄及其子目錄下的所有文件###
        all_files = self.get_all_files_in_remote_dir(remote_dir)
        ###依次get每一個文件###
        print("the dir is %s, the dir transferring is starting ..." % remote_dir)
        for x in all_files:
            filename = x.split('/')[-1]
            local_filename = os.path.join(local_dir, filename)
            print('Get文件%s傳輸中...' % filename)
            sftp.get(x, local_filename)
        print("the dir transferring is end ...")
        t.close()

    ###獲取本地指定目錄及其子目錄下的所有文件###
    def get_all_files_in_local_dir(self, local_dir):
        ###保存所有文件的列表###
        all_files = list()

        ###輸出本地目錄文件信息###
        print(subprocess.getoutput("ls -al %s" % local_dir))
        ###獲取當前指定目錄下的所有目錄及文件,包含屬性值###
        files = os.listdir(local_dir)
        for x in files:
            ###local_dir目錄中每一個文件或目錄的完整路徑###
            filename = os.path.join(local_dir, x)
            ###如果是目錄,則遞歸處理該目錄###
            #if os.path.isdir(x):
            #    all_files.extend(self.get_all_files_in_local_dir(filename))
            #else:
            #    all_files.append(filename)
            all_files.append(filename)
        return all_files

    ###上傳本地目錄下的文件到遠程主機指定目錄###
    def sftp_put_dir(self, local_dir, remote_dir):
        t = paramiko.Transport(sock=(self.host_obj.host.ip_addr,self.host_obj.host.port))
        t.connect(username=self.host_obj.remoteuser.username,pkey=self.__private_key)
        sftp = paramiko.SFTPClient.from_transport(t) 
 
        ###去掉路徑字符穿最后的字符'/',如果有的話###
        if remote_dir[-1] == '/':
            remote_dir = remote_dir[0:-1]

        ###獲取本地指定目錄及其子目錄下的所有文件###
        all_files = self.get_all_files_in_local_dir(local_dir)
        ###依次put每一個文件###
        print("the dir is %s, the dir transferring is starting ..." % local_dir)
        for x in all_files:
            filename = os.path.split(x)[-1]
            remote_filename = remote_dir + '/' + filename
            print('Put文件%s傳輸中...' % filename)
            sftp.put(x, remote_filename)
        print("the dir transferring is end ...")

        t.close()
ssh_sftp.py

 

13、modules目錄下的主功能框架程序views.py代碼:

主功能程序,實現yaml文件數據插入表;SSH登錄,命令執行,SFTP文件上傳下載

#!/usr/bin/python3
#_*_coding:utf-8_*_
__author__ = 'Kevin Wang'

import os,sys,getpass,time
import termios,tty,threading
from datetime import date, datetime


###導入settings里的變量值###
from conf import settings

from modules import common_filters
from modules import ssh_login
from modules import ssh_cmd
from modules import ssh_sftp
from modules import models
from modules import utils
from modules.db_conn import engine,session
from modules.utils import print_err,yaml_parser

##################################################################
###定義自登錄函數(獲取當前系統用戶名自登錄)###
def login():
    '''
    獲取當前系統用戶名並免密碼登錄
    :return: user_obj : 返回當前登錄用戶的實例
    '''
    os.system('clear')
    ###獲取當前系統的用戶名###
    username = getpass.getuser()
    ###獲取當前用戶在數據庫表的信息###
    user_obj = session.query(models.UserProfile).filter(models.UserProfile.username==username).first()
    ###判斷獲取的用戶信息實例為假,則調用認證函數進行用戶名密碼判斷###
    #if not user_obj:
    #    user_obj = auth()
    ###返回用戶信息實例###
    return user_obj

##################################################################
###定義認證函數(需要輸入用戶名和密碼)###
def auth():
    '''
    定義認證函數(需要輸入用戶名和密碼)
    :return: user_obj : 返回當前登錄用戶的實例
    '''

    ###定義認證循環3次###
    count = 0
    while count <3:
        print("\033[31;1mwelcome to FDT jumpserver\033[0m")
        ###輸入用戶名###
        username = input("\033[32;1mUsername:\033[0m").strip()
        if len(username) ==0:
            continue
        ###輸入密碼,使用getpass隱藏###
        password = getpass.getpass("\033[32;1mPassword:\033[0m")
        if len(password) ==0:
            continue
        ###去數據庫表UserProfile進行用戶名和密碼驗證###
        #user_obj = session.query(models.UserProfile).filter(models.UserProfile.username==username,models.UserProfile.password==hash_str(password)).first()
        user_obj = session.query(models.UserProfile).filter(models.UserProfile.username==username).first()
        ###驗證成功則返回用戶信息實例###
        if user_obj and user_obj.verify_passwd(password):
            return user_obj
        else:
            print("wrong username or password, you have %s more chances." %(3-count-1))
            count +=1
    else:
        print_err("too many attempts.")


##################################################################
###框架函數,輸出登錄信息###
def framework(user,group):
    '''
    輸出框架信息 
    :param: user :  登錄用戶的實例
    :param: group : 當前選擇的組名稱
    
********************************************************************************
*                                                                              *
*                                FDT JumpServer                                *
*                                                                              *
* user : hkfdt                            today : 2018-01-27 18:08:16 Saturday *
********************************************************************************
 
--------------------------------------------------------------------------------
group : 
--------------------------------------------------------------------------------
    '''

    ###定義當前時間變量###
    today = datetime.now().strftime("%Y-%m-%d %H:%M:%S %A")
    ###定義用戶信息變量和日期信息變量###
    user_info = " user : {0}".format(user.username)
    date_info = "today : {0} ".format(today)
    ###定義二變量的長度###
    length = len(user_info) + len(date_info) 
    
    ###計算空格的長度###   
    fill_char_size = 78 - length if 78 >= length else 0
    
    ###定義輸出信息的變量###
    MSG = "%s%s%s" % (user_info,' ' * fill_char_size,date_info)

    ###調用conf/settings模塊下的二個變量名並輸出###
    print(settings.WELCOME_MSG.format(MSG),end='')
    print(settings.USER_GROUP_MSG.format(group),end='')

##################################################################
###顯示組函數,輸出主機組信息###
def show_group(user):
    '''
    輸出組列表信息 
    :param: user : 登錄用戶的實例
    :return: 當前選擇的組列表字典
    '''

    ###定義組列表變更,key為組編號,value為組名稱###
    glist = {}

    ###定義組字典(key為組名,value為組實例)和按組名排序的數組###
    group_dict = {}
    sorted_group_list = []

    ###調用conf/settings模塊下的組列表變量並輸出###
    print(settings.GROUP_LIST,end='')

    ###判斷用戶是否有綁定的非組主機並格式輸出###
    if user.bind_hosts:
        glist['h'] = 'ungroupped hosts'
        print('\033[32;1m{0}{1}{2}\033[0m'.format("h".ljust(10),"ungroupped hosts".ljust(40),str(len(user.bind_hosts))))

    ###遍歷用戶相關聯的組列表,並方便排序進行生成字典###
    for group_index in user.groups:
        group_dict[group_index.name] = group_index
 
    ###按照group_dict字典中的key組名稱進行排序,並生成新的排序數組###
    sorted_group_list = sorted(group_dict.items(),key=lambda g:g[0])
    
    ###對排序后的數組進行遍歷,並格式化輸出組列表###
    for index in range(len(sorted_group_list)):
        glist[index] = sorted_group_list[index][1]
        print('\033[32;1m{0}{1}{2}\033[0m'.format(str(index).ljust(10),sorted_group_list[index][0].ljust(40),str(len(sorted_group_list[index][1].bind_hosts))))
    
    ###返回組列表字典###
    return glist

##################################################################
###顯示主機函數,輸出主機信息###
def show_host(user_obj,group_name,bind_hosts):
    '''
    輸出主機列表信息
    :param : user_obj : 登錄用戶的實例
    :param : group_name : 選擇的組名稱
    :param : bind_hosts : 綁定主機列表,用作按IP排序
    '''
    os.system('clear')

    ###在選擇組后,刷新輸出當前的框架信息###
    framework(user_obj,group_name)
    print(settings.HOST_LIST,end='')

    ###定義綁定主機字典,key為綁定主機的實例,value為綁定主機的IP地址###
    bind_dict = {}

    ###遍歷綁定主機列表,並生成需要排序的主機字典###
    for bind_host in bind_hosts:
        bind_dict[bind_host] = bind_host.host.ip_addr
    
    ###定義排序的主機數組###
    sorted_host_list = []

    ###使用sorted函數對字典的key進行排序###
    sort_items = sorted(bind_dict.items(),key=lambda d:d[1]) 

    ###對排序后的數組進行遍歷,並生成排序后的數組及格式化輸出主機列表###
    for index in range(len(sort_items)):
        bind_host_index = sort_items[index][0]
        sorted_host_list.append(bind_host_index)
        bind_host_ip = sort_items[index][1]
        user_info = "{0}@{1}".format(bind_host_index.remoteuser.username,bind_host_index.host.hostname)
        print('\033[32;1m{0}{1}{2}\033[0m'.format(str(index).ljust(10),user_info.ljust(40),bind_host_ip))

    ###返回排序后的主機列表字典,值為綁定主機的實例###
    return sorted_host_list

##################################################################
###日志記錄函數,記錄登錄用戶的操作命令###
def log_recording(user_obj,bind_host_obj,logs):
    '''
    flush user operations on remote host into DB
    :param user_obj:
    :param bind_host_obj:
    :param logs: list format [logItem1,logItem2,...]
    :return:
    '''
    #print("\033[41;1m--logs:\033[0m",logs)

    session.add_all(logs)
    session.commit()

###調用多線程函數執行命令###
def make_threading(func,tuple_args):
    '''
    調用多線程函數 
    :param: func : 多線程要執行的函數名稱
    :param: tuple_args : 元組,執行操作的遠程主機obj信息,操作命令
    :return : 無返回值
    '''
    
    thread = threading.Thread(target=func,args=tuple_args,daemon=True)
    thread.start()

###定義文件上傳下載的try和except報錯函數###
def try_exec(client_mode,first_file,second_file):
    '''
    定義文件上傳下載的try和except報錯 
    :param: client_mode : 文件操作的方式get或put
    :param: first_file : 文件操作的源文件 
    :param: second_file : 文件操作的目的文件 
    :return : 無返回值
    '''
    try:
        client_mode(first_file,second_file)
        input("請輸入「enter」繼續...").strip()
    except PermissionError as e:
        utils.print_err("權限不足(%s)" % e)
    except FileNotFoundError as e:
        utils.print_err("文件不存在(%s)" % e)
    except OSError as e:
        utils.print_err("無法傳輸目錄(%s)" % e)
    except Exception as e:
        utils.print_err("異常,請重新輸入(%s)" % e)

###調用文件傳輸函數###
def ssh_transport(host_obj):
    '''
    文件傳輸執行 
    :param: host_obj : 文件傳輸的主機 
    :return : 無返回值
    '''

    ###初始化文件的列表,名稱###
    local_list = ()
    remote_list = ()
    local_file = ''
    local_dir = ''
    remote_file = ''
    remote_dir = ''
    put_filename = ''
    get_filename = ''

    ###調用ssh_sftp模塊的函數,並獲取返回值###
    client = ssh_sftp.ssh_sftp(host_obj)
    while True:
        ###輸出文件上傳下載幫助信息###
        client.help()
    
        ###獲取輸入的命令,命令一般分2-3塊,例:ldir /tmp或put a.txt /tmp###
        sftp_cmd = input("\033[36;1m請輸入文件傳輸的命令:\033[0m").strip()
        args = sftp_cmd.split()

        ###輸入Q,則退出本次循環;如果參數個數小於2,則重新輸入###       
        if sftp_cmd == 'q' or sftp_cmd == 'Q':
            break
        elif len(args) < 2:
            print("輸入有誤,請重新輸入!")
            continue 
        else:
            ###操作模式為ldir,查看本地目錄信息###
            if args[0] == 'ldir':
                if os.path.isdir(args[1]):

                    ###調用類中函數列出本地目錄信息###
                    client.get_all_files_in_local_dir(args[1]) 
                else:
                    utils.print_err("本地目錄不存在,請重新輸入") 
            ###操作模式為rdir,查看遠端目錄信息###
            elif args[0] == 'rdir':
                try:
                    ###調用類中函數列出遠端目錄信息###
                    client.get_all_files_in_remote_dir(args[1]) 
                except:
                    utils.print_err("遠端目錄不存在,請重新輸入")
            ###操作模式為put,並且參數個數大於3,則進行文件上傳操作###
            elif args[0] == 'put' and len(args) >= 3:
                local_file = args[1]

                ###判斷要上傳的文件是否存在###
                if os.path.isfile(local_file):
                    put_filename=args[1].split('/')[-1]
                    remote_dir = args[2]
                    if remote_dir.endswith('/'):
                        remote_file = remote_dir + put_filename 
                    else:
                        remote_file = remote_dir + '/' + put_filename 

                    ###調用try_exec函數進行文件操作,並進行錯誤信息提示###
                    try_exec(client.sftp_put,local_file,remote_file)

                    ###多線程操作文件上傳###
                    #make_threading(client.sftp_put,(local_file,remote_file))
                    #while threading.active_count() != 1:
                    #    #print(threading.active_count(),end='')
                    #    print("#",end='')
                    #    time.sleep(0.1)
                    #else:
                    #    input("請輸入「enter」繼續...").strip()
                elif os.path.isdir(local_file):
                    utils.print_err("無法傳輸目錄,請重新輸入")
                else:
                    utils.print_err("本地文件不存在,請重新輸入") 
            ###操作模式為get,並且參數個數大於3,則進行文件下載操作###
            elif args[0] == 'get' and len(args) >= 3:
                remote_file = args[1]
                get_filename = args[1].split('/')[-1]
                local_dir = args[2]
                ###判斷要下載到本地的目錄是否存在###
                if os.path.isdir(local_dir):
                    if local_dir.endswith('/'):
                        local_file = local_dir + get_filename
                    else:
                        local_file = local_dir + '/' + get_filename
   
                    ###調用try_exec函數進行文件操作,並進行錯誤信息提示###
                    try_exec(client.sftp_get,remote_file,local_file)
                else:
                    utils.print_err("本地目錄不存在,請重新輸入")

                ###多線程操作文件下載###
                #make_threading(client.sftp_get,(remote_file,local_file))
                #while threading.active_count() != 1:
                #    #print(threading.active_count(),end='')
                #    print("#",end='')
                #    time.sleep(0.1)
                #else:
                #    input("請輸入「enter」繼續...").strip()
            else:
                print("輸入格式有誤,請重新輸入!")
    
    
##################################################################
###啟動會話函數###
def start_session(argvs):
    '''
    程序運行主函數,啟動自登錄會話 
    :param: argvs : 暫未使用
    '''

    ###調用登錄及認證函數,獲取用戶信息實例###
    user = login()
    if user:
       
        ###用戶認證后進入第一輪顯示組列表循環###
        exit_flag = False
        while not exit_flag:
            os.system('clear')

            ###第一次顯示框架,組信息定義為空###
            group_name = ''
            framework(user,group_name)
            
            ###調用顯示組列表函數,並返回選擇的組編號###
            group_list = show_group(user)
            if 'h' in group_list.keys():
                group_len = len(group_list) - 2
            else:
                group_len = len(group_list) - 1

            print("================================================================================")
            ###輸出要選擇的組編號信息,如果為q,則退出程序###
            group_choice = input("[(q)退出,(h,0-%d)選擇組]: " % group_len).strip()


            ###如果組編號為空就重新循環,否則定義主機列表實例###
            if not group_choice: continue
            if group_choice == 'q': 
                sys.exit(0)
            elif group_choice == 'h':
                ###如果輸入h,即定義組名為ungroupped hosts,綁定主機列表實例###
                group_name = group_list[group_choice] 
                bind_hosts_obj = user.bind_hosts
            elif group_choice.isdigit() and int(group_choice) in group_list.keys(): 
                ###對組ID進行int,並定義組名和綁定主機列表實例###
                group_choice = int(group_choice)
                group_name = group_list[group_choice].name
                bind_hosts_obj = group_list[group_choice].bind_hosts
            else:
                continue 
           
            ###進行主機選擇,進入第二輪循環###
            while not exit_flag:

                ###格式化輸出主機列表,並獲取排序后的主機列表###
                sorted_hosts_obj = show_host(user,group_name,bind_hosts_obj)
                sorted_hosts_len = len(sorted_hosts_obj) - 1
                print("=================================================================================")
                #host_choice = input("[(b)back,(q)quit, select host to login]:").strip()
                #print("the choice is %s" % host_choice)
                try:
                    host_choice = input("[(b)返回,(q)退出,(e)批量執行命令,[0-%d]選擇主機]:" % sorted_hosts_len).strip()
                except OSError:
                    os.system('clear')
                    sys.exit(0)
                ###輸入字符為空,則重新輸入;輸入為b,則返回上層重新選擇組;輸入q,則退出程序;輸入正確編號,則執行ssh登錄###
                if len(host_choice)==0:
                    continue
                if host_choice == 'b':
                    break
                elif host_choice == 'q':
                    exit_flag=True
                elif host_choice == 'e':
                    exec_cmd = input("請輸入批量命令:").strip()
                    i = 0
                    while i < len(sorted_hosts_obj):
                        make_threading(ssh_cmd.ssh_cmd,(sorted_hosts_obj[i],exec_cmd)) 
                        i += 1
                    while threading.active_count() != 1:
                        time.sleep(0.1)
                    else:
                        input("請輸入「enter」繼續...").strip()
                if host_choice.isdigit():
                    host_choice = int(host_choice)
                    if host_choice < len(sorted_hosts_obj) :
                        exec_choice = input("\033[36;1m(t)傳輸文件,其他輸入登錄系統:\033[0m").strip()
                        if exec_choice == "t":
                            ssh_transport(sorted_hosts_obj[host_choice])
                        else:
                            ###調用ssh登錄指定主機,退出SSH遠程主機后,重新第二輪循環,進行重新選擇主機###
                            os.system('clear')
                            ssh_login.ssh_login(user,
                                            sorted_hosts_obj[host_choice],
                                            session,
                                            log_recording)
                        
##################################################################
###創建關閉會話函數###

def stop_server(argvs):
    pass

##################################################################
###創建本地登錄用戶函數###
def create_users(argvs):
    '''
    創建本地登錄用戶,並將用戶信息插入到數據庫表user_profile中
    :param: argvs : 指定本地登錄用戶的配置文件
    :return: 無返回值
    '''
    if '-f' in argvs:
        user_file  = argvs[argvs.index("-f") +1 ]
    else:
        print_err("invalid usage, should be:\ncreateusers -f <the new users file>",quit=True)

    ###讀取yaml格式的本地用戶配置文件,例:conf/new_user.yml###
    source = yaml_parser(user_file)
    if source:
        for key,val in source.items():
            #obj = models.UserProfile(username=key,password=hash_str(val.get('password')))
            obj = models.UserProfile(username=key,passwd=val.get('password'))
            
             
            ###如果配置文件中有groups信息,則判斷group是否存在###
            if val.get('groups'):
                groups = common_filters.groups_filter(val)
                ###綁定主機組存在,則賦值,並將相關數據插入到group_2_userprofile表中###
                obj.groups = groups
            ###如果配置文件中有bind_hosts信息,則判斷綁定主機是否存在###
            if val.get('bind_hosts'):
                bind_hosts = common_filters.bind_hosts_filter(val)
                ###綁定主機存在,則賦值,並將相關數據插入到bindhost_2_userprofile表中###
                obj.bind_hosts = bind_hosts
            #print(obj)
            session.add(obj)
        session.commit()

##################################################################
###創建主機組函數###
def create_groups(argvs):
    '''
    創建主機組,將組信息插入到數據庫表group中
    :param: argvs : 指定主機組的配置文件
    :return: 無返回值
    '''
    if '-f' in argvs:
        group_file  = argvs[argvs.index("-f") +1 ]
    else:
        print_err("invalid usage, should be:\ncreategroups -f <the new groups file>",quit=True)

    ###讀取yaml格式的主機組配置文件,例:conf/new_groups.yml###
    source = yaml_parser(group_file)
    if source:
        for key,val in source.items():
            print(key,val)
            obj = models.Group(name=key)
            ###如果配置文件中有bind_hosts信息,則判斷綁定主機是否存在###
            if val.get('bind_hosts'):
                bind_hosts = common_filters.bind_hosts_filter(val)
                ###綁定主機存在,則賦值,並將相關數據插入到bindhost_2_group表中###
                obj.bind_hosts = bind_hosts

            ###如果配置文件中有登錄用戶信息,則判斷該用戶是否存在###
            if val.get('user_profiles'):
                user_profiles = common_filters.user_profiles_filter(val)
                ###登錄用戶存在,則賦值,並將相關數據插入到group_2_userprofile表中###
                obj.user_profiles = user_profiles
            session.add(obj)
        session.commit()

##################################################################
###創建遠程主機函數###
def create_hosts(argvs):
    '''
    創建遠程主機,將主機信息插入到數據庫表host中
    :param: argvs : 指定遠程主機的配置文件
    :return: 無返回值
    '''
    if '-f' in argvs:
        hosts_file  = argvs[argvs.index("-f") +1 ]
    else:
        print_err("invalid usage, should be:\ncreate_hosts -f <the new hosts file>",quit=True)

    ###讀取yaml格式的遠程主機配置文件,例:conf/new_hosts.yml###
    source = yaml_parser(hosts_file)
    if source:
        for key,val in source.items():
            print(key,val)
            obj = models.Host(hostname=key,ip_addr=val.get('ip_addr'), port=val.get('port') or 22)
            session.add(obj)
:
    if source:
        for key,val in source.items():
            print(key,val)
            obj = models.Group(name=key)
            ###如果配置文件中有bind_hosts信息,則判斷綁定主機是否存在###
            if val.get('bind_hosts'):
                bind_hosts = common_filters.bind_hosts_filter(val)
                ###綁定主機存在,則賦值,並將相關數據插入到bindhost_2_group表中###
                obj.bind_hosts = bind_hosts

            ###如果配置文件中有登錄用戶信息,則判斷該用戶是否存在###
            if val.get('user_profiles'):
                user_profiles = common_filters.user_profiles_filter(val)
                ###登錄用戶存在,則賦值,並將相關數據插入到group_2_userprofile表中###
                obj.user_profiles = user_profiles
            session.add(obj)
        session.commit()

##################################################################
###創建遠程主機函數###
def create_hosts(argvs):
    '''
    創建遠程主機,將主機信息插入到數據庫表host中
    :param: argvs : 指定遠程主機的配置文件
    :return: 無返回值
    '''
    if '-f' in argvs:
        hosts_file  = argvs[argvs.index("-f") +1 ]
    else:
        print_err("invalid usage, should be:\ncreate_hosts -f <the new hosts file>",quit=True)

    ###讀取yaml格式的遠程主機配置文件,例:conf/new_hosts.yml###
    source = yaml_parser(hosts_file)
    if source:
        for key,val in source.items():
            print(key,val)
            obj = models.Host(hostname=key,ip_addr=val.get('ip_addr'), port=val.get('port') or 22)
            session.add(obj)
:
    if source:
        for key,val in source.items():
            print(key,val)
            obj = models.Group(name=key)
            ###如果配置文件中有bind_hosts信息,則判斷綁定主機是否存在###
            if val.get('bind_hosts'):
                bind_hosts = common_filters.bind_hosts_filter(val)
                ###綁定主機存在,則賦值,並將相關數據插入到bindhost_2_group表中###
                obj.bind_hosts = bind_hosts

            ###如果配置文件中有登錄用戶信息,則判斷該用戶是否存在###
            if val.get('user_profiles'):
                user_profiles = common_filters.user_profiles_filter(val)
                ###登錄用戶存在,則賦值,並將相關數據插入到group_2_userprofile表中###
                obj.user_profiles = user_profiles
            session.add(obj)
        session.commit()


##################################################################
###創建遠程主機函數###
def create_hosts(argvs):
    '''
    創建遠程主機,將主機信息插入到數據庫表host中
    :param: argvs : 指定遠程主機的配置文件
    :return: 無返回值
    '''
    if '-f' in argvs:
        hosts_file  = argvs[argvs.index("-f") +1 ]
    else:
        print_err("invalid usage, should be:\ncreate_hosts -f <the new hosts file>",quit=True)

    ###讀取yaml格式的遠程主機配置文件,例:conf/new_hosts.yml###
    source = yaml_parser(hosts_file)
    if source:
        for key,val in source.items():
            print(key,val)
            obj = models.Host(hostname=key,ip_addr=val.get('ip_addr'), port=val.get('port') or 22)
            session.add(obj)
        session.commit()

##################################################################
###創建遠程主機的登錄信息函數###
def create_remoteusers(argvs):
    '''
    創建遠程主機的登錄信息 
    :param: argvs : 指定遠程用戶帳號信息的配置文件
    :return: 無返回值
    '''
    if '-f' in argvs:
        remoteusers_file  = argvs[argvs.index("-f") +1 ]
    else:
        print_err("invalid usage, should be:\ncreate_remoteusers -f <the new remoteusers file>",quit=True)

    ###讀取yaml格式的遠程主機帳號信息配置文件,例:conf/new_remoteusers.yml###
    source = yaml_parser(remoteusers_file)
    if source:
        for key,val in source.items():
            print(key,val)
            obj = models.RemoteUser(username=val.get('username'),auth_type=val.get('auth_type'),password=val.get('password'))
            session.add(obj)
        session.commit()

##################################################################
###創建綁定主機函數###
def create_bindhosts(argvs):
    '''
    創建遠程綁定主機,並將相關聯的主機組和用戶數據插入到相關聯表中
    :param: argvs : 指定遠程綁定主機的配置文件
    :return: 無返回值
    '''
    if '-f' in argvs:
        bindhosts_file  = argvs[argvs.index("-f") +1 ]
    else:
        print_err("invalid usage, should be:\ncreate_hosts -f <the new bindhosts file>",quit=True)

    ###讀取yaml格式的遠程綁定主機配置文件,例:conf/new_bindhosts.yml###
    source = yaml_parser(bindhosts_file)
    if source:
        for key,val in source.items():
            ###根據配置文件中的主機名,並判斷主機表host是否有此記錄###
            host_obj = session.query(models.Host).filter(models.Host.hostname==val.get('hostname')).first()

            ###assert斷言是聲明其布爾值必須為真的判定,如果發生異常就說明表達示為假###
            assert host_obj
            for item in val['remote_users']:
                #print(item)
                assert item.get('auth_type')
                ###根據ssh的認證方式,來驗證配置文件中的remoteuser信息是否正確###
                if item.get('auth_type') == 'ssh-passwd':
                    remoteuser_obj = session.query(models.RemoteUser).filter(
                                                        models.RemoteUser.username==item.get('username'),
                                                        models.RemoteUser.password==item.get('password')
                                                    ).first()
                else:
                    remoteuser_obj = session.query(models.RemoteUser).filter(
                                                        models.RemoteUser.username==item.get('username'),
                                                        models.RemoteUser.auth_type==item.get('auth_type'),
                                                    ).first()
                if not remoteuser_obj:
                    print_err("RemoteUser obj %s does not exist." % item,quit=True )
                print(host_obj.id,remoteuser_obj.id)

                ###根據獲取到的主機ID和遠程用戶ID,插入數據到表bind_host###
                bindhost_obj = models.BindHost(host_id=host_obj.id,remoteuser_id=remoteuser_obj.id)
                session.add(bindhost_obj)

                ###如果配置文件中有groups信息,則判斷group是否存在###
                if source[key].get('groups'):
                    group_objs = session.query(models.Group).filter(models.Group.name.in_(source[key].get('groups'))).all()
                    assert group_objs
                    print('groups:', group_objs)
                    ###group存在,則賦值,並將相關數據插入到bindhost_2_group表中###
                    bindhost_obj.groups = group_objs
                ###如果配置文件中有登錄用戶信息,則判斷該用戶是否存在###
                if source[key].get('user_profiles'):
                    userprofile_objs = session.query(models.UserProfile).filter(models.UserProfile.username.in_(source[key].get('user_profiles'))).all()
                    assert userprofile_objs
                    print("userprofiles:",userprofile_objs)
                    ###登錄用戶存在,則賦值,並將相關數據插入到bindhost_2_userprofile表中###
                    bindhost_obj.user_profiles = userprofile_objs
        session.commit()

##################################################################
###創建所有表結構函數###
def syncdb(argvs):
    '''
     創建所有表結構
    :param: argvs : 未使用
    :return: 無返回值
    '''
    print("Syncing DB....")
    models.Base.metadata.create_all(engine) 
views.py

 

14、share目錄下的yaml格式文件:

<1> . host.yml:

HOST1:
  ip_addr: 192.168.4.135
  port: 22

<2> . user_profile.yml:

wang_kai:
  password: xxxxxx

<3> . group.yml:

GROUP1:
  user_profiles:
    - wang_kai
<4> . remoteuser.yml:

key_ubuntu:
  auth_type: ssh-key
  username: ubuntu
  password: 123456

passwd_ubuntu:
  auth_type: ssh-passwd
  username: ubuntu 
  password: 123456

<5> . bindhost.yml:

bind1:
  hostname: HOST1
  remote_users:
    - user1:
       username: ubuntu
       auth_type: ssh-key
       password: 123456 
  groups:
     - GROUP1


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM