0.0.2 refactor of ssh module for uci. Uses only exec. Wrote corresponding test module

master
David Kebler 2019-01-30 20:13:54 -08:00
parent 11ca82e084
commit 91c3c305de
4 changed files with 126 additions and 83 deletions

View File

@ -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"
}
}

View File

@ -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))
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,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)
})
})
return this
}
connect(opts = {}) {
opts.privateKey = opts.keyfilePath ? fs.readFileSync(opts.keyfilePath) : null
super.connect(opts)
return new Promise(resolve => {
this.getEventEmitter().on('connect', () => resolve(this))
})
}
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)
})
.on('data', s => {
data += s
stdout.write(s)
})
.stderr.on('data', s => {
data += s
stderr.write(s)
})
stream.end(command + '\nexit\n')
})
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('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

53
test/ssh.test.js Normal file
View File

@ -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()
// // })
//
// }

5
test/ssh.yaml Normal file
View File

@ -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'