Last active
January 31, 2017 06:13
-
-
Save hiway/a5f0f1944f34a88961ffaa2d5c412f61 to your computer and use it in GitHub Desktop.
A quick hack to explore ideas for scripting automated ssh interactions in Python3.5 (Example at bottom creates and bootstraps a FreeBSD jail.)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import asyncio | |
import logging | |
import os | |
import threading | |
from typing import List | |
import asyncssh | |
import janus | |
LOG_LEVEL = logging.DEBUG | |
logger = logging.getLogger(__name__) | |
logger.setLevel(LOG_LEVEL) | |
handler = logging.StreamHandler() | |
handler.setLevel(LOG_LEVEL) | |
formatter = logging.Formatter('%(asctime)s %(message)s', '%H:%M:%S') | |
handler.setFormatter(formatter) | |
logger.addHandler(handler) | |
event_loop = asyncio.get_event_loop() | |
class StdException(Exception): | |
pass | |
class StdOutException(StdException): | |
pass | |
class StdErrException(StdException): | |
pass | |
class Server(object): | |
def __init__(self, *args, event_loop=None, **kwargs): | |
self._args = args | |
self._kwargs = kwargs | |
self._loop = event_loop or asyncio.get_event_loop() | |
self._command_queue = janus.Queue(loop=self._loop) | |
self._response_queue = janus.Queue(loop=self._loop) | |
self._connection_thread = None | |
async def _connect(self): | |
async with asyncssh.connect(*self._args, **self._kwargs) as conn: | |
while True: | |
(cmd, args, kwargs, expect, abort, | |
ignore) = await self._command_queue.async_q.get() | |
if kwargs and 'timeout' in kwargs: | |
timeout = kwargs['timeout'] | |
del kwargs['timeout'] | |
else: | |
timeout = None | |
if cmd is None: | |
await self._response_queue.async_q.put(None) | |
break | |
if timeout: | |
try: | |
response = await asyncio.wait_for( | |
conn.run(cmd, *args, **kwargs), timeout) | |
except asyncio.TimeoutError as e: | |
await self._response_queue.async_q.put(e) | |
else: | |
response = await conn.run(cmd, *args, **kwargs) | |
await self._response_queue.async_q.put(response) | |
def _threaded_connect(self): | |
asyncio.set_event_loop(self._loop) | |
self._loop.run_until_complete(self._connect()) | |
def __enter__(self): | |
self._connection_thread = threading.Thread( | |
name='SSH', | |
target=self._threaded_connect) | |
self._connection_thread.start() | |
return self | |
def __exit__(self, exc_type, exc_val, exc_tb): | |
if self._loop.is_running(): | |
self._command_queue.sync_q.put( | |
(None, None, None, None, None, None)) | |
self._connection_thread.join() | |
def run(self, command, *args, | |
expect: [str, List[str], None] = None, | |
abort: [str, List[str], None] = None, | |
ignore: [str, List[str], None] = None, | |
ignore_timeout=False, | |
**kwargs): | |
logger.debug('*** {}'.format(command)) | |
self._command_queue.sync_q.put( | |
(command, args, kwargs, expect, abort, ignore)) | |
response = self._response_queue.sync_q.get() | |
if isinstance(response, Exception): | |
if isinstance(response, asyncio.TimeoutError): | |
if ignore_timeout is False: | |
raise TimeoutError() | |
else: | |
return None | |
raise response | |
if response.stderr and not ignore: | |
raise StdErrException('', response.stdout, | |
response.stderr, 'Error.') | |
if response.exit_status != 0 and not ignore: | |
logger.warning('exit_status: {}'.format(response.exit_status)) | |
raise StdOutException(str(response.exit_status), response.stdout, | |
response.stderr, 'Error.') | |
if ignore and not isinstance(ignore, bool): | |
if isinstance(ignore, str): | |
ignore = [ignore] | |
for phrase in ignore: | |
if phrase in response.stderr: | |
logger.debug(' - {}'.format(phrase)) | |
continue | |
else: | |
raise StdErrException(phrase, | |
response.stdout, | |
response.stderr, | |
'Expected but not found.') | |
if abort: | |
if isinstance(abort, str): | |
abort = [abort] | |
for phrase in abort: | |
if phrase in response.stdout: | |
raise StdOutException(phrase, response.stdout, | |
response.stderr, 'Aborting.') | |
if response.stderr and phrase in response.stderr: | |
raise StdErrException(phrase, response.stdout, | |
response.stderr, 'Aborting.') | |
if expect: | |
if isinstance(expect, str): | |
expect = [expect] | |
for phrase in expect: | |
if phrase in response.stdout: | |
logger.debug(' + {}'.format(phrase)) | |
continue | |
else: | |
raise StdOutException(phrase, | |
response.stdout, | |
response.stderr, | |
'Expected but not found.') | |
return response | |
def create_freebsd_jail(server, name, address): | |
try: | |
with server() as server: | |
server.run('sudo iocage list'.format(name), abort=[name, address], ignore=True) | |
server.run('sudo iocage create -r "11.0-RELEASE" tag="{}" ip4_addr="lo1|{}"'.format(name, address)) | |
server.run('sudo iocage start {}'.format(name), timeout=10, ignore_timeout=True) | |
resolv_conf = '/iocage/tags/{}/root/etc/resolv.conf'.format(name) | |
server.run('echo "nameserver 10.0.0.1" | sudo tee {}'.format(resolv_conf), timeout=1, ignore_timeout=True) | |
server.run('echo "options edns0" | sudo tee -a {}'.format(resolv_conf), timeout=1, ignore_timeout=True) | |
server.run('sudo iocage exec {} -- env ASSUME_ALWAYS_YES=YES pkg bootstrap -f'.format(name), timeout=30, | |
ignore_timeout=True) | |
except Exception as e: | |
logger.exception(e) | |
if __name__ == '__main__': | |
_server = Server( | |
'example.com', | |
port=22, | |
username='example_user', | |
client_keys=[os.path.expanduser('~/.ssh/id_rsa')] | |
) | |
create_freebsd_jail(_server, 'example_jail', '10.0.0.9') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment