From 91c3c305de39e3093bbc9d3ae2811442d7c82918 Mon Sep 17 00:00:00 2001 From: David Kebler Date: Wed, 30 Jan 2019 20:13:54 -0800 Subject: [PATCH] 0.0.2 refactor of ssh module for uci. Uses only exec. Wrote corresponding test module --- package.json | 9 ++- src/ssh.js | 142 +++++++++++++++++++++-------------------------- test/ssh.test.js | 53 ++++++++++++++++++ test/ssh.yaml | 5 ++ 4 files changed, 126 insertions(+), 83 deletions(-) create mode 100644 test/ssh.test.js create mode 100644 test/ssh.yaml diff --git a/package.json b/package.json index c35a716..0c25935 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/ssh.js b/src/ssh.js index c7929bc..1ffeb0e 100644 --- a/src/ssh.js +++ b/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)) + 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 diff --git a/test/ssh.test.js b/test/ssh.test.js new file mode 100644 index 0000000..d89d69a --- /dev/null +++ b/test/ssh.test.js @@ -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() +// // }) +// +// } diff --git a/test/ssh.yaml b/test/ssh.yaml new file mode 100644 index 0000000..87d6866 --- /dev/null +++ b/test/ssh.yaml @@ -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'