0.0.2 refactor of ssh module for uci. Uses only exec. Wrote corresponding test module
parent
11ca82e084
commit
91c3c305de
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"name": "@uci/remote-code",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"description": "module to copy, maintain, and launch hardware modules on other machines",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"testd": "./node_modules/.bin/mocha -r esm --watch --timeout 30000 || exit 0",
|
||||
"test": "./node_modules/.bin/mocha -r esm --timeout 30000 || exit 0",
|
||||
"test": "UCI_PRO=./test/test.log ./node_modules/.bin/mocha -r esm --timeout 30000 || exit 0",
|
||||
"testibc": "istanbul cover ./node_modules/.bin/_mocha test/ --report lcovonly -- -R spec --recursive && codecov || true"
|
||||
},
|
||||
"author": "David Kebler",
|
||||
|
@ -24,6 +24,8 @@
|
|||
},
|
||||
"homepage": "https://github.com/uCOMmandIt/uci-remote-code#readme",
|
||||
"dependencies": {
|
||||
"@uci/logger": "0.0.9",
|
||||
"await-to-js": "^2.1.1",
|
||||
"chokidar": "^2.0.4",
|
||||
"p-settle": "^2.1.0",
|
||||
"rsyncwrapper": "^3.0.1",
|
||||
|
@ -34,6 +36,7 @@
|
|||
"codecov": "^3.1.0",
|
||||
"esm": "^3.1.4",
|
||||
"istanbul": "^0.4.5",
|
||||
"mocha": "^5.x"
|
||||
"mocha": "^5.x",
|
||||
"read-yaml-file": "^1.1.0"
|
||||
}
|
||||
}
|
||||
|
|
140
src/ssh.js
140
src/ssh.js
|
@ -1,97 +1,79 @@
|
|||
const fs = require('fs')
|
||||
const EventEmitter = require('events').EventEmitter
|
||||
const ssh = require('ssh2')
|
||||
|
||||
const Client = ssh.Client
|
||||
import { promisify } from 'util'
|
||||
import to from 'await-to-js'
|
||||
import { readFile } from 'fs'
|
||||
import { Client } from 'ssh2'
|
||||
import logger from '@uci/logger'
|
||||
let log = {} // declare module wide log to be set during construction
|
||||
|
||||
const read = promisify(readFile)
|
||||
// ssh client is already an event emitter
|
||||
class Ssh extends Client {
|
||||
constructor() {
|
||||
constructor(opts={}) {
|
||||
super()
|
||||
this._emitter = new EventEmitter()
|
||||
this._ignoreStdOut = []
|
||||
return this.init()
|
||||
this.opts = opts
|
||||
log = logger({ name: 'remote-code', file:'src/ssh.js', class:'Ssh', id: opts.id })
|
||||
this.ready = false
|
||||
// additional options available
|
||||
this.rpath = opts.rpath || '/opt' // will cd to this path upon connect
|
||||
}
|
||||
|
||||
init() {
|
||||
const conn = this
|
||||
conn.on('ready', () => {
|
||||
conn.shell((err, stream) => {
|
||||
if (err) {
|
||||
throw err
|
||||
}
|
||||
this.stream = stream
|
||||
this._emit('connect', 'Connection established')
|
||||
stream.on('close', () => {
|
||||
this._emit('close', 'Connection closed')
|
||||
conn.end()
|
||||
})
|
||||
stream.on('data', data => this._emit('data', data))
|
||||
stream.stderr.on('data', data => this._emit('error', data))
|
||||
})
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
connect(opts = {}) {
|
||||
opts.privateKey = opts.keyfilePath ? fs.readFileSync(opts.keyfilePath) : null
|
||||
async connect(copts = {}) {
|
||||
const opts = Object.assign({},this.opts,copts) // merge any changes pasted via connect
|
||||
// log.info('connection options ', opts)
|
||||
opts.readyTimeout = opts.readyTimeout | 5000 // default was 20 seconds...too long
|
||||
opts.privateKey = opts.privateKey || opts.privateKeyPath ? await read(opts.privateKeyPath) : null
|
||||
// log.info(opts.privateKeyPath, opts.privateKey)
|
||||
super.connect(opts)
|
||||
return new Promise(resolve => {
|
||||
this.getEventEmitter().on('connect', () => resolve(this))
|
||||
return new Promise( (resolve,reject) => {
|
||||
this.on('ready', async () => {
|
||||
this.ready=true
|
||||
log.info(`connected to ${this.opts.host}`)
|
||||
resolve(`connected to ${this.opts.host}`)
|
||||
})
|
||||
this.on('error', (err) => {
|
||||
log.info('connection error', err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
getEventEmitter() {
|
||||
return this._emitter
|
||||
}
|
||||
|
||||
_emit(...args) {
|
||||
this._emitter.emit(...args)
|
||||
}
|
||||
|
||||
send(line) {
|
||||
this._ignoreStdOut.push(line)
|
||||
if (this.stream.writable) {
|
||||
this.stream.write(`${line}\n`)
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this.ready=false
|
||||
this.end()
|
||||
this.stream = null
|
||||
return this
|
||||
}
|
||||
|
||||
// Supercedes ssh2.exec
|
||||
// execute a single command on the server and close & resolve
|
||||
// uses interactive shell to get stdout/stderr back
|
||||
// stdout, stderr are streams
|
||||
exec(opts, command = 'uptime', stdout, stderr) {
|
||||
let data = ''
|
||||
console.log('executing', opts, command)
|
||||
return new Promise((resolve, reject) => {
|
||||
this
|
||||
.on('ready', () => {
|
||||
this.shell((err, stream) => {
|
||||
stream
|
||||
.on('close', () => {
|
||||
this.end()
|
||||
resolve(data)
|
||||
async exec(command = 'ls -la', opts={}) {
|
||||
if (!this.ready) {
|
||||
log.info('not connected, attempting connect first')
|
||||
let [err] = await to(this.connect())
|
||||
if (err) return err
|
||||
}
|
||||
// log.info('executing command ', command)
|
||||
const remote = this
|
||||
let superexec = super.exec.bind(this)
|
||||
return new Promise(function(resolve, reject) {
|
||||
let reply=[]
|
||||
let error=[]
|
||||
superexec(command, function(err, stream) {
|
||||
if (err) reject(err)
|
||||
stream.on('finish', () => {
|
||||
if (remote.opts.close || opts.close) {
|
||||
log.info('closing connection')
|
||||
remote.close
|
||||
}
|
||||
if (error.length!==0) remote.emit('client:error',error)
|
||||
resolve({command:command,reply:reply,error:error})
|
||||
})
|
||||
.on('data', s => {
|
||||
data += s
|
||||
stdout.write(s)
|
||||
})
|
||||
.stderr.on('data', s => {
|
||||
data += s
|
||||
stderr.write(s)
|
||||
})
|
||||
stream.end(command + '\nexit\n')
|
||||
})
|
||||
})
|
||||
.on('error', e => reject(e))
|
||||
.connect(opts)
|
||||
.on('data', function(data) {
|
||||
reply.push(data)
|
||||
log.info(`${remote.opts.host}$ ${data}`)
|
||||
}).stderr.on('data', function(data) {
|
||||
error.push(data)
|
||||
log.info(`${remote.opts.host}:ERR$ ${data}`)
|
||||
})
|
||||
}) // end super
|
||||
}) // end Promise
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Ssh
|
||||
export default Ssh
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import Ssh from '../src/ssh'
|
||||
import to from 'await-to-js'
|
||||
import { expect } from 'chai'
|
||||
import { it } from 'mocha'
|
||||
import read from 'read-yaml-file'
|
||||
import logger from '@uci/logger'
|
||||
// pause = require('@uci/utils').pPause
|
||||
|
||||
describe('SSH Class Testing ',async ()=> {
|
||||
|
||||
let remote
|
||||
let log
|
||||
before(async () => {
|
||||
log = logger({ name: 'remote-code', test:'/test/ssh.test.js', class:'Ssh',file:'src/ssh.js', id: 'testing' })
|
||||
let opts = await read('./test/ssh.yaml')
|
||||
remote = new Ssh(opts)
|
||||
log.info(`making connection to ${remote.opts.host}`)
|
||||
let [err] = await to(remote.connect())
|
||||
if (err) {
|
||||
log.info('unable to connect aborting test', err)
|
||||
return err
|
||||
}
|
||||
log.info('ready for testing')
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
remote.close()
|
||||
})
|
||||
|
||||
it('simple connect and reply to "cd /opt && pwd"' , async function () {
|
||||
let [err,res] = await to(remote.exec('cd /opt && pwd'))
|
||||
if (err) {
|
||||
log.info('error running command aborting test', err)
|
||||
return
|
||||
}
|
||||
log.info(`result of remote command ${res.command} => ${res.reply.toString().trim()}`)
|
||||
expect(res.reply.toString().trim(), 'test failed').to.equal('/opt')
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
// function hooks(remote) {
|
||||
//
|
||||
|
||||
// // beforeEach(async() => {
|
||||
// // await someasyncfunctiontodobeforeeachtest()
|
||||
// // })
|
||||
//
|
||||
// // after(async() => {
|
||||
// // await someasyncfunctiontodoaftereeachtest()
|
||||
// // })
|
||||
//
|
||||
// }
|
|
@ -0,0 +1,5 @@
|
|||
host: switches.kebler.net
|
||||
username: sysadmin
|
||||
# agent: /run/user/1000/ssh-agent.socket
|
||||
privateKeyPath: /home/david/.ssh/privatekeys/sysadmin.kebler.net
|
||||
passphrase: '51535560'
|
Loading…
Reference in New Issue