【简洁明了】xterm.js+paramiko+websocket实现Web终端,可ssh连接linux bash

项目要求,需要在web使用ssh或者控制台,就找有没有好的方法可以实现,找到了xterm.js+paramiko+websocket等实现交互,xtarm.js可以在前端展示一个终端,可以自定义,通过websocket和后端进行通信,后端可以自己写一个脚本程序,配置一个websocket的server端进行通信。
paramiko

python执行终端命令

在这里我本来想的是直接在python里调用终端命令执行,但是单独使用的时候可以,集成起来就不好用了,为什么不好用后面说,先说几种常用的终端命令的执行。

os.system

import os
os.system('ls')

会启动一个子进程调用command执行命令,在终端直接执行会显示结果,在程序中使用只会有返回值,结果不会返回。

os.popen

os.popen(command,mode)

import os
os.popen('cat /proc/cpuinfo')

直接与command进程通信的一个管道,返回一个文件对象,mode可以指定模式,如果是’r’,可以使用read()或者readlines()方法读取返回的执行结果。

commands

import commands
status=commands.getstatus('cat /proc/cpuinfo')
output=commands.getoutput('ls -l')
(status,output)=commands.getstatusoutput('ls -l')

会返回一个元组,包含执行结果和返回的状态。output中包含了控制台的输出信息和错误信息等。

subprocess

是python2.4出现的一个模块,可以代替上面的模块方法,还集中了多个关于进程的操作。call()完全替代了system()popen()Popen类进行实现和完善。

websocket

这个没什么好介绍的(就是我不想写,直接百度都是,而且主要是看实现的过程,概念这些东西估计也没多少人想看)。
主要就是,它是socket的更高级版本吧,然后呢,可以双方都使用websocket进行通信,会比较方便;也可以使用socket进行,如python中使用socket实现服务端,客户端使用websocket进行连接。
但是会非常麻烦,需要自己处理握手通信以及数据的编码解码等处理操作,虽然网上可以找到很多,但是我看了好些综合了好几个博客的代码,都没有实现,接收到的数据都是乱码,而且我传输的都是英文,没有中文,也不涉及编码混乱的问题,因此呢,手动实现就可以放弃了。
然后我找到由第三方库实现websocket的功能。

websockets

这是github
但是好像需要python3版本才行,我是2,就放弃了。

simple-websocket-server

这是github
这个我试了一下很好用,主要是很方便,把websocket的操作都封装好了,直接使用就行了,
官方示例如下,看代码也不用多解释,建立一个连接,然后新建一个类,有三个方法处理三个事件就好了。

from SimpleWebSocketServer import SimpleWebSocketServer, WebSocket

class SimpleEcho(WebSocket):

    def handleMessage(self):
        # echo message back to client
        self.sendMessage(self.data)

    def handleConnected(self):
        print(self.address, 'connected')

    def handleClose(self):
        print(self.address, 'closed')

server = SimpleWebSocketServer('', 8000, SimpleEcho)
server.serveforever()

问题来了,就是在这里,单独用脚本执行commands.getstatusoutput没有问题,但是放在这个框架里就有问题来了,所以在这里没法用。
然后呢,这个框架有点问题,如果你的程序在运行中突然断开连接了,或者刚连上就断开了,说明代码有问题,这个库代码出错是不会报错的,只会断开连接。善用print调试,定位出错位置。

paramiko

ssh是一个协议,OpenSSH是其中一个开源实现,paramiko是Python的一个库,实现了SSHv2协议(底层使用cryptography)。

有了Paramiko以后,我们就可以在Python代码中直接使用SSH协议对远程服务器执行操作,而不是通过ssh命令对远程服务器进行操作。
安装的话,直接pip就行了

pip install paramiko

这里只介绍SSHClient
基本使用如下

def sshConnection():
	ip    = '192.168.1.104'
	port  = 22
	user  = 'root'
	passwd= 'root'

	cmd = 'cd /home/kang/Desktop'
	# 设置记录日志
	log_file = 'kali_ssh.log'
	util.log_to_file(log_file)

	# 生成ssh客户端实例
	s = SSHClient()
	s.set_missing_host_key_policy(AutoAddPolicy())
	print ("[+] Start ssh into: "+ip)
	s.connect(ip, port, user, passwd)
	print ("[+] SSH established !")
	s.exec_command('ls -l')
	recv=s.recv(1024)
	print(recv)
	chan.close()
	s.close()

非交互式:ssh_client.exec_command

但是有一个小问题,这里如果使用exec_command,他是非交互式的,差不多就是每次只能执行一条,如果需要使用环境变量啥的,就不行了,得bash -ls 'ls -l’才行。如果需要执行多条语句,使用;分隔开。
而且如果出现比如进入python终端这种情况,exec_command就无法执行了,会卡住,可能是无法判断出后台终端的响应。

交互式:channel.invoke_shell

比较推荐的是交互式的方式,相当于创建一个shell终端,在其中运行命令,但是这个的问题是,如何判断命令是否执行完成,如果等的时间太短可能获取不到结果,如果等的时间太长用户体验不好。
这里通过返回的SP1输入提示符进行判断,在返回结果的最后都会返回一个输入的提示符,而每个界面或者系统的提示符不同,可能会有# 结尾的,也可能是$ 结尾的。因此可以把这些添加到一个list中,进行判断。
下面是一个示例

import paramiko
import time
 
hostname = '192.16.21.12'
port = 22
username = 'hadoop'
password = 'hadoop'
timeout = 10
 
 
def runCommand(chanT, command, endSymbol):
    chanT.send(command + '\n')  # 指令后加 '\n' 表示换行
    results = ''
    while True: 
        result = chanT.recv(1024).decode('utf-8')
        results += result
        if results[-2:] in endSymbol:  # 判断最后两个字符是否是我们定义的结束符
            break
    re = results.split('\n')[1:]  # 第一行是我们输入的指令,没用丢弃
    print('\n'.join(re), end='')
    return re[:-1]  # 最后一行是linux的SP1输入提示符,没用丢弃
 
 
if __name__ == "__main__":
    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    ssh.connect(hostname, port, username, password)
    chan = ssh.invoke_shell()  # 创建一个交互式的shell窗口
    chan.settimeout(1000)
    time.sleep(3)  # 刚进入linux服务器等待一会,否则直接通过chan.recv获取的信息不完整
    loginInfo = chan.recv(1024).decode('utf-8')  # Welcome to Ubuntu 16.04.6 LTS..等登录信息
    print(loginInfo, end='')
    endSymbol = ['$ ', '> ', '* ']  # 设置我们定义的结束符
    while True:
        command = input():  # 等待用户输入指令
        if command == 'quitshell'  # 当用户输入quitshell指令时退出程序
            print('Bye Bye!')
            exit(0)
        result = runCommand(chan, command, endSymbol)  
优化

可以使用多线程进一步优化性能。

xterm.js

这是github
xtarm.js的使用很简单,引入两个文件,xterm/css/xterm.css和xterm/lib/xterm.js


  <html>
    <head>
      <link rel="stylesheet" href="node_modules/xterm/css/xterm.css" />
      <script src="node_modules/xterm/lib/xterm.js">script>
    head>
    <body>
      <div id="terminal">div>
      <script>
        var term = new Terminal();
        term.open(document.getElementById('terminal'));
        term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
      script>
    body>
  html>

web terminal

现在放一个我差不多实现好的一个web terminal,后台连接是通过本地ssh连接的。

html代码

<div id="terminal">
	div>
    
        			
        			<script>
        				
        				//terminal
        				var ContainerID='{{ ContainerID }}';
        				var ContainerName='{{ ContainerName }}';
        				var UserName='{{ UserName }}';
        				var Password='{{ Password }}';
        				var ProjectName='{{ ProjectName }}';
        				var input='';
        				var prefix='\r\n~$ ';
        				var term = new Terminal({
							cursorStyle: 'underline', //光标样式
							cursorBlink: true, // 光标闪烁
							convertEol: true, //启用时,光标将设置为下一行的开头
							disableStdin: false, //是否应禁用输入。
							theme: {
								foreground: 'yellow', //字体
								background: '#060101', //背景色
								cursor: 'help',//设置光标
							}
						});
						term.open(document.getElementById('terminal'));
						function runFakeTerminal() {
							if (term._initialized) {
								return;
							}

							term._initialized = true;
							term.prompt = () => {
								term.write(prefix);
							};
							term.writeln('Welcome to terminal');
							prompt(term);
							term.onKey(e => {
								const printable = !e.domEvent.altKey && !e.domEvent.altGraphKey && !e.domEvent.ctrlKey && !e.domEvent.metaKey;

								if (e.domEvent.keyCode === 13) {
									//pressed  enter key
									//prompt(term);
									ws.send(input);
									input='';
								} else if (e.domEvent.keyCode === 8) {
									// Do not delete the prompt
									if (term._core.buffer.x > 2) {
										term.write('\b \b');
										input=input.slice(0,-1);
									}
								} else if (printable) {
									term.write(e.key);
									input+=e.key;
								}
								console.log(e.key);
							});
						}
						function prompt(term) {
							term.write(prefix);
						}
						runFakeTerminal();
						//websocket
        				
        				var ws=new WebSocket('ws://127.0.0.1:6789');
        				ws.onopen=function(){
        					//Command='!sep#;export OS_USERNAME=demo!sep#;export OS_PASSWORD=demo!sep#;export OS_PROJECT_NAME=demo!sep#;export OS_USER_DOMAIN_NAME=Default!sep#;export OS_PROJECT_DOMAIN_NAME=Default!sep#;export OS_AUTH_URL=http://controller:5000/v3!sep#;export OS_IDENTITY_API_VERSION=3';
        					//ws.send(Command);
							
        				}
        				ws.onmessage = function (evt) 
						{ 
							var RecvMsg = evt.data;
							//alert("数据已接收..."+RecvMsg);
							//var DataArray=RecvMsg.split('!sep#;');
							var DataArray=RecvMsg.split('\n');
							if (DataArray.length==1)
								prefix='\r\n'+DataArray[0];
							else{
								for (var i=0;i<DataArray.length;i++){
									if (i==0)
										term.write('\r\n'+DataArray[i]);
									else  if (i<DataArray.length-1)
										term.write(DataArray[i]+'\r\n');
									else if (i==DataArray.length-1){
										prefix='\r\n'+DataArray[i];
										//term.write('\r\n');
									}
									else
										term.write(DataArray[i]);
								
								}
							}
							prompt(term);
						};
        			script>

后端代码

有不少没用的提示输出,自己看着删除吧,print调试大法好。
需要注意的是,如果想在这里面再进去其他的终端,要在self.EndSymbol添加新的提示符,比如如果想进入docker,虽然执行docker exec -it c618 /bin/sh然后提示符是 # ,别看也是# ,但是他并不普通字符,而是[J,因此需要添加这个。
然后我发现,执行openstack appcontainer exec --interactive bbb /bin/sh也是一样,需要添加提示符[J
而且还需要屏蔽一下exit命令,不然访客就可以退出容器直接去你的终端了。
有一个小问题,我不知道为什么执行不了docker ps命令,明明已经获取成功了,但是就是返回数据的时候报错,我不知道为什么。

# -*- coding: utf-8 -*-
from __future__ import print_function
from SimpleWebSocketServer import SimpleWebSocketServer, WebSocket
import commands
import os
from paramiko import util
from paramiko import SSHClient
from paramiko import AutoAddPolicy
import paramiko

class SimpleEcho(WebSocket):
	def handleMessage(self):
		# echo message back to client
		print('[%s] - Get:%s' % (self.address,self.data))
		result=self.runCommand(self.data)
		print('main reply')
		print(result)
		self.sendMessage(result)
		print('[%s] - Send:%s' % (self.address,result))

	def handleConnected(self):
		print(self.address, 'connected')
		ip    = '192.168.1.104'
		port  = 22
		user  = 'root'
		passwd= 'root'

		# 设置记录日志
		log_file = 'centos_ssh.log'
		util.log_to_file(log_file)

		# 生成ssh客户端实例
		self.ssh = SSHClient()
		self.ssh.set_missing_host_key_policy(AutoAddPolicy())
		print('get ssh client')
		self.ssh.connect(ip,port,user,passwd)
		print('ssh login successfully')
		self.chan=self.ssh.invoke_shell()
		print('get invoke_shell')
		self.chan.settimeout(1000)
		# 刚进入linux服务器等待一会,否则直接通过chan.recv获取的信息不完整
		#
		second=1
		for a in range(second):
			for i in range(30):
				for j in range(1145):
					for k in range(1000):
						u=i*j
		#
		print('sleep 1s ')
		LoginInfo=self.chan.recv(2048)	 # Welcome to Ubuntu 16.04.6 LTS..等登录信息
		print(LoginInfo)
		
		#=====================================================================
		self.EndSymbol=['$ ','# ','> ','* ']	# 设置我们定义的结束符
		print('start transport')

	def handleClose(self):
		print(self.address, 'closed')
	#接收指令的方法
	def runCommand(self,Command):
		print('exec command:'+Command)
		self.chan.send(Command+'\n')		# 指令后加 '\n' 表示换行
		Result=''
		print('waiting for reply')
		while True:
			Temp=self.chan.recv(4096)
			Result+=Temp
			if Result[-2:] in self.EndSymbol:		# 判断最后两个字符是否是我们定义的结束符
				break
		Final=Result.split('\n')[1:]	# 第一行是我们输入的指令,没用丢弃
		print('开始输出结果')
		print('\n'.join(Final),end='')
		print('输出结果完成')
		#return Final[:-1]
		return '\n'.join(Final)	# 最后一行是linux的SP1输入提示符,没用丢弃
	
print('start listening')
server = SimpleWebSocketServer('127.0.0.1', 6789, SimpleEcho)
server.serveforever()

参考链接

Python3 使用Websocket通信 - qian99
python之SSH(交互式和非交互式) - 绿风1号
python3通过paramiko远程交互式控制Linux服务器 - 太阳花的小绿豆
第二篇:ssh.invoke_shell() 切换root出现的新问题 - FelixApff
Js 之xterm.js终端插件 - 样子2018

你可能感兴趣的:(Linux,python,linux,socket,运维)