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",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
140
src/ssh.js
140
src/ssh.js
|
@ -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)
|
||||||
}
|
|
||||||
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
|
|
||||||
super.connect(opts)
|
super.connect(opts)
|
||||||
return new Promise(resolve => {
|
return new Promise( (resolve,reject) => {
|
||||||
this.getEventEmitter().on('connect', () => resolve(this))
|
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() {
|
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) {
|
||||||
|
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 => {
|
.on('data', function(data) {
|
||||||
data += s
|
reply.push(data)
|
||||||
stdout.write(s)
|
log.info(`${remote.opts.host}$ ${data}`)
|
||||||
})
|
}).stderr.on('data', function(data) {
|
||||||
.stderr.on('data', s => {
|
error.push(data)
|
||||||
data += s
|
log.info(`${remote.opts.host}:ERR$ ${data}`)
|
||||||
stderr.write(s)
|
|
||||||
})
|
|
||||||
stream.end(command + '\nexit\n')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.on('error', e => reject(e))
|
|
||||||
.connect(opts)
|
|
||||||
})
|
})
|
||||||
|
}) // 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