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", "name": "@uci/remote-code",
"version": "0.0.1", "version": "0.0.2",
"description": "module to copy, maintain, and launch hardware modules on other machines", "description": "module to copy, maintain, and launch hardware modules on other machines",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {
"testd": "./node_modules/.bin/mocha -r esm --watch --timeout 30000 || exit 0", "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" "testibc": "istanbul cover ./node_modules/.bin/_mocha test/ --report lcovonly -- -R spec --recursive && codecov || true"
}, },
"author": "David Kebler", "author": "David Kebler",
@ -24,6 +24,8 @@
}, },
"homepage": "https://github.com/uCOMmandIt/uci-remote-code#readme", "homepage": "https://github.com/uCOMmandIt/uci-remote-code#readme",
"dependencies": { "dependencies": {
"@uci/logger": "0.0.9",
"await-to-js": "^2.1.1",
"chokidar": "^2.0.4", "chokidar": "^2.0.4",
"p-settle": "^2.1.0", "p-settle": "^2.1.0",
"rsyncwrapper": "^3.0.1", "rsyncwrapper": "^3.0.1",
@ -34,6 +36,7 @@
"codecov": "^3.1.0", "codecov": "^3.1.0",
"esm": "^3.1.4", "esm": "^3.1.4",
"istanbul": "^0.4.5", "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') import { promisify } from 'util'
const EventEmitter = require('events').EventEmitter import to from 'await-to-js'
const ssh = require('ssh2') import { readFile } from 'fs'
import { Client } from 'ssh2'
const Client = ssh.Client 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 { class Ssh extends Client {
constructor() { constructor(opts={}) {
super() super()
this._emitter = new EventEmitter() this.opts = opts
this._ignoreStdOut = [] log = logger({ name: 'remote-code', file:'src/ssh.js', class:'Ssh', id: opts.id })
return this.init() this.ready = false
// additional options available
this.rpath = opts.rpath || '/opt' // will cd to this path upon connect
} }
init() { async connect(copts = {}) {
const conn = this const opts = Object.assign({},this.opts,copts) // merge any changes pasted via connect
conn.on('ready', () => { // log.info('connection options ', opts)
conn.shell((err, stream) => { opts.readyTimeout = opts.readyTimeout | 5000 // default was 20 seconds...too long
if (err) { opts.privateKey = opts.privateKey || opts.privateKeyPath ? await read(opts.privateKeyPath) : null
throw err // log.info(opts.privateKeyPath, opts.privateKey)
} super.connect(opts)
this.stream = stream return new Promise( (resolve,reject) => {
this._emit('connect', 'Connection established') this.on('ready', async () => {
stream.on('close', () => { this.ready=true
this._emit('close', 'Connection closed') log.info(`connected to ${this.opts.host}`)
conn.end() resolve(`connected to ${this.opts.host}`)
}) })
stream.on('data', data => this._emit('data', data)) this.on('error', (err) => {
stream.stderr.on('data', data => this._emit('error', data)) 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() { close() {
this.ready=false
this.end() this.end()
this.stream = null
return this
} }
// Supercedes ssh2.exec async exec(command = 'ls -la', opts={}) {
// execute a single command on the server and close & resolve if (!this.ready) {
// uses interactive shell to get stdout/stderr back log.info('not connected, attempting connect first')
// stdout, stderr are streams let [err] = await to(this.connect())
exec(opts, command = 'uptime', stdout, stderr) { if (err) return err
let data = '' }
console.log('executing', opts, command) // log.info('executing command ', command)
return new Promise((resolve, reject) => { const remote = this
this let superexec = super.exec.bind(this)
.on('ready', () => { return new Promise(function(resolve, reject) {
this.shell((err, stream) => { let reply=[]
stream let error=[]
.on('close', () => { superexec(command, function(err, stream) {
this.end() if (err) reject(err)
resolve(data) stream.on('finish', () => {
}) if (remote.opts.close || opts.close) {
.on('data', s => { log.info('closing connection')
data += s remote.close
stdout.write(s) }
}) if (error.length!==0) remote.emit('client:error',error)
.stderr.on('data', s => { resolve({command:command,reply:reply,error:error})
data += s
stderr.write(s)
})
stream.end(command + '\nexit\n')
})
}) })
.on('error', e => reject(e)) .on('data', function(data) {
.connect(opts) 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'