|
|
@@ -55,10 +55,12 @@ def check_res_code(res): |
|
|
|
sys.exit(1) |
|
|
|
elif res.status_code != HTTP_200_OK: |
|
|
|
try: |
|
|
|
print('Got status: %d, json: %s' % (res.status_code, res.json())) |
|
|
|
print('Got status: %d, json: %s' % (res.status_code, |
|
|
|
res.json())) |
|
|
|
except json.decoder.JSONDecodeError: |
|
|
|
# body is JSON |
|
|
|
print('Got status: %d, body: %s' % (res.status_code, repr(res.text))) |
|
|
|
print('Got status: %d, body: %s' % (res.status_code, |
|
|
|
repr(res.text))) |
|
|
|
sys.exit(1) |
|
|
|
|
|
|
|
def makebool(s): |
|
|
@@ -125,18 +127,22 @@ async def fwd_data(reader, writer): |
|
|
|
await writer.drain() |
|
|
|
|
|
|
|
async def run_exec(baseurl, authkey, board, args): |
|
|
|
url = urllib.parse.urljoin(baseurl, 'board/%s/exec' % urllib.parse.quote(board, safe='')) |
|
|
|
url = urllib.parse.urljoin(baseurl, 'board/%s/exec' % |
|
|
|
urllib.parse.quote(board, safe='')) |
|
|
|
url = convert_to_ws(url) |
|
|
|
stdin, stdout = await aioconsole.stream.get_standard_streams() |
|
|
|
|
|
|
|
async with websockets.connect(url) as ws, wsfwd.WSFWDClient(ws.recv, ws.send) as client: |
|
|
|
async with websockets.connect(url) as ws, wsfwd.WSFWDClient(ws.recv, |
|
|
|
ws.send) as client: |
|
|
|
try: |
|
|
|
await client.auth(dict(bearer=authkey)) |
|
|
|
|
|
|
|
proc = await client.exec(args=args) |
|
|
|
|
|
|
|
toexec_task = asyncio.create_task(fwd_data(stdin, proc.stdin)) |
|
|
|
fromexec_task = asyncio.create_task(fwd_data(proc.stdout, stdout)) |
|
|
|
toexec_task = asyncio.create_task(fwd_data(stdin, |
|
|
|
proc.stdin)) |
|
|
|
fromexec_task = asyncio.create_task(fwd_data( |
|
|
|
proc.stdout, stdout)) |
|
|
|
|
|
|
|
r = await proc.wait() |
|
|
|
|
|
|
@@ -159,20 +165,30 @@ async def real_main(): |
|
|
|
dest='subparser_name', |
|
|
|
description='valid subcommands', help='additional help') |
|
|
|
|
|
|
|
parse_list = subparsers.add_parser('list', help='list available board classes') |
|
|
|
|
|
|
|
parser_reserve = subparsers.add_parser('reserve', aliases=[ 'release' ], help='reserve/release a board') |
|
|
|
parser_reserve.add_argument('-i', metavar='identity_file', type=str, help='file name for ssh public key') |
|
|
|
parser_reserve.add_argument('board', type=str, help='name of the board or class') |
|
|
|
|
|
|
|
parser_set = subparsers.add_parser('set', help='set attributes on a board') |
|
|
|
parser_set.add_argument('setvars', type=str, nargs='+', help='name of the board or class') |
|
|
|
parser_set.add_argument('board', type=str, help='name of the board or class') |
|
|
|
|
|
|
|
parser_exec = subparsers.add_parser('exec', help='run a program in the jail for a board') |
|
|
|
parser_exec.add_argument('board', type=str, help='name of the board or class') |
|
|
|
parse_list = subparsers.add_parser('list', |
|
|
|
help='list available board classes') |
|
|
|
|
|
|
|
parser_reserve = subparsers.add_parser('reserve', aliases=[ 'release' ], |
|
|
|
help='reserve/release a board') |
|
|
|
parser_reserve.add_argument('-i', metavar='identity_file', type=str, |
|
|
|
help='file name for ssh public key') |
|
|
|
parser_reserve.add_argument('board', type=str, |
|
|
|
help='name of the board or class') |
|
|
|
|
|
|
|
parser_set = subparsers.add_parser('set', |
|
|
|
help='set attributes on a board') |
|
|
|
parser_set.add_argument('setvars', type=str, nargs='+', |
|
|
|
help='name of the board or class') |
|
|
|
parser_set.add_argument('board', type=str, |
|
|
|
help='name of the board or class') |
|
|
|
|
|
|
|
parser_exec = subparsers.add_parser('exec', |
|
|
|
help='run a program in the jail for a board') |
|
|
|
parser_exec.add_argument('board', type=str, |
|
|
|
help='name of the board or class') |
|
|
|
parser_exec.add_argument('prog', type=str, help='program to exec') |
|
|
|
parser_exec.add_argument('args', type=str, nargs='*', help='arguments for program') |
|
|
|
parser_exec.add_argument('args', type=str, nargs='*', |
|
|
|
help='arguments for program') |
|
|
|
|
|
|
|
args = parser.parse_args() |
|
|
|
#print(repr(args), file=sys.__stderr__) |
|
|
@@ -202,7 +218,8 @@ async def real_main(): |
|
|
|
elif args.subparser_name in ('reserve', 'release'): |
|
|
|
kwargs = _httpxargs.copy() |
|
|
|
with contextlib.suppress(IOError): |
|
|
|
kwargs['json'] = dict(sshpubkey=get_sshpubkey(args.i)) |
|
|
|
kwargs['json'] = dict(sshpubkey=get_sshpubkey( |
|
|
|
args.i)) |
|
|
|
res = await client.post('board/%s/%s' % |
|
|
|
(urllib.parse.quote(args.board, safe=''), |
|
|
|
args.subparser_name), |
|
|
@@ -260,7 +277,8 @@ class TestExecClient(unittest.IsolatedAsyncioTestCase): |
|
|
|
@contextlib.contextmanager |
|
|
|
def make_pipe(self): |
|
|
|
r, w = os.pipe() |
|
|
|
with os.fdopen(r, 'rb', buffering=65536) as readfl, os.fdopen(w, 'wb', buffering=65536) as writefl: |
|
|
|
with os.fdopen(r, 'rb', buffering=65536) as readfl, \ |
|
|
|
os.fdopen(w, 'wb', buffering=65536) as writefl: |
|
|
|
yield readfl, writefl |
|
|
|
|
|
|
|
# too lazy to make this async since async file-like objects |
|
|
@@ -284,23 +302,30 @@ class TestExecClient(unittest.IsolatedAsyncioTestCase): |
|
|
|
stdout = io.BytesIO() |
|
|
|
|
|
|
|
# Data path: |
|
|
|
# stdin -> stdin_task -> stdinwriter -> pipe -> stdinreader -> sys.stdin -> real_main -> |
|
|
|
# sys.stdout -> stdoutwriter -> pipe -> stdoutreader -> stdoud_task -> stdout |
|
|
|
# stdin -> stdin_task -> stdinwriter -> pipe -> |
|
|
|
# stdinreader -> sys.stdin -> real_main -> sys.stdout -> |
|
|
|
# stdoutwriter -> pipe -> stdoutreader -> stdoud_task -> stdout |
|
|
|
# |
|
|
|
# How things get closed: |
|
|
|
# stdin is already "closed", stdin_task will close stdinwriter when eof encountered (doclose). |
|
|
|
# stdin is already "closed", stdin_task will close |
|
|
|
# stdinwriter when eof encountered (doclose). |
|
|
|
|
|
|
|
# create the pipes needed |
|
|
|
with self.make_pipe() as (stdinreader, stdinwriter), self.make_pipe() as (stdoutreader, stdoutwriter): |
|
|
|
with self.make_pipe() as (stdinreader, stdinwriter), \ |
|
|
|
self.make_pipe() as (stdoutreader, stdoutwriter): |
|
|
|
# setup the threads to move data |
|
|
|
loop = asyncio.get_running_loop() |
|
|
|
stdin_task = loop.run_in_executor(None, self.copytask, io.BytesIO(stdin), stdinwriter) |
|
|
|
stdin_task = loop.run_in_executor(None, self.copytask, |
|
|
|
io.BytesIO(stdin), stdinwriter) |
|
|
|
|
|
|
|
# do not close stdout, otherwise we cannot obtain the value |
|
|
|
stdout_task = loop.run_in_executor(None, self.copytask, stdoutreader, stdout, False) |
|
|
|
# do not close stdout, otherwise we cannot obtain |
|
|
|
# the value |
|
|
|
stdout_task = loop.run_in_executor(None, self.copytask, |
|
|
|
stdoutreader, stdout, False) |
|
|
|
|
|
|
|
# insert the pipes |
|
|
|
with patch.dict(sys.__dict__, dict(stdin=TextIOWrapper(stdinreader), |
|
|
|
with patch.dict(sys.__dict__, |
|
|
|
dict(stdin=TextIOWrapper(stdinreader), |
|
|
|
stdout=TextIOWrapper(stdoutwriter))): |
|
|
|
try: |
|
|
|
# run the function |
|
|
@@ -341,18 +366,21 @@ class TestExecClient(unittest.IsolatedAsyncioTestCase): |
|
|
|
raise RuntimeError('badauth') |
|
|
|
|
|
|
|
server = TestServer(self.toserver.get, self.toclient.put) |
|
|
|
with patch.dict(sys.__dict__, dict(argv=[ 'rand', 'exec', 'cora-z7s', 'program', 'arg1', 'arg2' ])), \ |
|
|
|
with patch.dict(sys.__dict__, dict(argv=[ 'rand', 'exec', |
|
|
|
'cora-z7s', 'program', 'arg1', 'arg2' ])), \ |
|
|
|
patch('websockets.connect') as webcon: |
|
|
|
self.setup_websockets_mock(webcon) |
|
|
|
|
|
|
|
ret, stdout = await self.runAsyncMain() |
|
|
|
|
|
|
|
webcon.assert_called_with(urllib.parse.urljoin('ws://someserver/', 'board/cora-z7s/exec')) |
|
|
|
webcon.assert_called_with(urllib.parse.urljoin( |
|
|
|
'ws://someserver/', 'board/cora-z7s/exec')) |
|
|
|
|
|
|
|
await server.__aexit__(None, None, None) |
|
|
|
|
|
|
|
await asyncio.sleep(.1) |
|
|
|
self.assertEqual(stdout.decode(), 'failed to exec: Got auth error: \'badauth\'\n') |
|
|
|
self.assertEqual(stdout.decode(), |
|
|
|
'failed to exec: Got auth error: \'badauth\'\n') |
|
|
|
|
|
|
|
self.assertEqual(ret, 1) |
|
|
|
|
|
|
@@ -368,11 +396,13 @@ class TestExecClient(unittest.IsolatedAsyncioTestCase): |
|
|
|
|
|
|
|
async def handle_chanclose(self, msg): |
|
|
|
self.add_tasks(asyncio.create_task(self.sendcmd( |
|
|
|
dict(cmd='chanclose', chan=self._stdout_stream)))) |
|
|
|
dict(cmd='chanclose', |
|
|
|
chan=self._stdout_stream)))) |
|
|
|
|
|
|
|
async def handle_exec(self, msg): |
|
|
|
self._stdout_stream = msg['stdout'] |
|
|
|
assert msg['args'] == [ 'program', 'arg1', 'arg2' ] |
|
|
|
assert msg['args'] == [ 'program', 'arg1', |
|
|
|
'arg2' ] |
|
|
|
self.add_stream_handler(msg['stdin'], |
|
|
|
functools.partial(self.echo_handler, |
|
|
|
msg['stdout'])) |
|
|
@@ -383,7 +413,8 @@ class TestExecClient(unittest.IsolatedAsyncioTestCase): |
|
|
|
|
|
|
|
server = TestServer(self.toserver.get, self.toclient.put) |
|
|
|
|
|
|
|
with patch.dict(sys.__dict__, dict(argv=[ 'rand', 'exec', 'cora-z7s', 'program', 'arg1', 'arg2' ])), \ |
|
|
|
with patch.dict(sys.__dict__, dict(argv=[ 'rand', 'exec', |
|
|
|
'cora-z7s', 'program', 'arg1', 'arg2' ])), \ |
|
|
|
patch('websockets.connect') as webcon: |
|
|
|
self.setup_websockets_mock(webcon) |
|
|
|
|
|
|
@@ -435,21 +466,25 @@ class TestClient(unittest.TestCase): |
|
|
|
] |
|
|
|
|
|
|
|
for orig, new in testpairs: |
|
|
|
self.assertEqual(convert_to_ws(orig), new, 'failed to convert: %s' % repr(orig)) |
|
|
|
self.assertEqual(convert_to_ws(orig), new, |
|
|
|
'failed to convert: %s' % repr(orig)) |
|
|
|
|
|
|
|
def test_sshpubkey(self): |
|
|
|
fname = 'fname' |
|
|
|
rdata = 'foo' |
|
|
|
with patch('builtins.open', mock_open(read_data=rdata)) as mock_file: |
|
|
|
with patch('builtins.open', mock_open(read_data=rdata)) as \ |
|
|
|
mock_file: |
|
|
|
self.assertEqual(get_sshpubkey(fname), rdata) |
|
|
|
mock_file.assert_called_with(fname) |
|
|
|
|
|
|
|
with patch('builtins.open') as mock_file: |
|
|
|
mock_file.side_effect = [ IOError(), mock_open(read_data=rdata)() ] |
|
|
|
mock_file.side_effect = [ IOError(), |
|
|
|
mock_open(read_data=rdata)() ] |
|
|
|
self.assertEqual(get_sshpubkey(None), rdata) |
|
|
|
|
|
|
|
for i in ('id_ed25519', 'id_rsa'): |
|
|
|
fname = os.path.expanduser(os.path.join('~', '.ssh', i + '.pub')) |
|
|
|
fname = os.path.expanduser(os.path.join('~', |
|
|
|
'.ssh', i + '.pub')) |
|
|
|
mock_file.assert_any_call(fname) |
|
|
|
|
|
|
|
with patch('builtins.open') as mock_file: |
|
|
@@ -477,7 +512,8 @@ class TestClient(unittest.TestCase): |
|
|
|
|
|
|
|
ac.assert_called_with(base_url='http://someserver/') |
|
|
|
|
|
|
|
acg.assert_called_with('board/classes', auth=BiteAuth('thisisanapikey'), **_httpxargs) |
|
|
|
acg.assert_called_with('board/classes', |
|
|
|
auth=BiteAuth('thisisanapikey'), **_httpxargs) |
|
|
|
|
|
|
|
# XXX - add error cases for UI |
|
|
|
|
|
|
@@ -500,7 +536,8 @@ class TestClient(unittest.TestCase): |
|
|
|
|
|
|
|
ac.assert_called_with(base_url='http://someserver/') |
|
|
|
|
|
|
|
acg.assert_called_with('board/classes', auth=BiteAuth('thisisanapikey'), **_httpxargs) |
|
|
|
acg.assert_called_with('board/classes', |
|
|
|
auth=BiteAuth('thisisanapikey'), **_httpxargs) |
|
|
|
|
|
|
|
# XXX - add error cases for UI |
|
|
|
|
|
|
@@ -542,7 +579,8 @@ Attributes: |
|
|
|
auth=BiteAuth('thisisanapikey'), **_httpxargs) |
|
|
|
|
|
|
|
@patch('bitelab.__main__.get_sshpubkey') |
|
|
|
@patch.dict(sys.__dict__, dict(argv=[ '', 'reserve', '-i', 'bogusfilename', 'cora-z7s' ])) |
|
|
|
@patch.dict(sys.__dict__, dict(argv=[ '', 'reserve', '-i', |
|
|
|
'bogusfilename', 'cora-z7s' ])) |
|
|
|
def test_reserve_ssh(self, gspk): |
|
|
|
ac = self.ac |
|
|
|
acp = self.acp |
|
|
@@ -606,8 +644,10 @@ Attributes: |
|
|
|
acp.assert_called_with('board/cora-z7s/release', |
|
|
|
auth=BiteAuth('thisisanapikey'), **_httpxargs) |
|
|
|
|
|
|
|
@patch('bitelab.__main__._typeconv', dict(power=makebool, other=makebool)) |
|
|
|
@patch.dict(sys.__dict__, dict(argv=[ '', 'set', 'power=on', 'other=off', 'cora-z7s' ])) |
|
|
|
@patch('bitelab.__main__._typeconv', dict(power=makebool, |
|
|
|
other=makebool)) |
|
|
|
@patch.dict(sys.__dict__, dict(argv=[ '', 'set', 'power=on', |
|
|
|
'other=off', 'cora-z7s' ])) |
|
|
|
def test_set(self): |
|
|
|
ac = self.ac |
|
|
|
acp = self.acp |
|
|
@@ -634,8 +674,8 @@ Attributes: |
|
|
|
ac.assert_called_with(base_url='http://someserver/') |
|
|
|
|
|
|
|
acp.assert_called_with('board/cora-z7s/attrs', |
|
|
|
auth=BiteAuth('thisisanapikey'), json=dict(power=True, other=False), |
|
|
|
**_httpxargs) |
|
|
|
auth=BiteAuth('thisisanapikey'), json=dict(power=True, |
|
|
|
other=False), **_httpxargs) |
|
|
|
|
|
|
|
def test_make_attrs(self): |
|
|
|
self.assertEqual(make_attrs('power=on'), dict(power=True)) |
|
|
@@ -650,7 +690,8 @@ Attributes: |
|
|
|
def test_check_res_code(self): |
|
|
|
res = Mock() |
|
|
|
res.status_code = HTTP_404_NOT_FOUND |
|
|
|
res.json.side_effect = json.decoder.JSONDecodeError('foo', 'bar', 1) |
|
|
|
res.json.side_effect = json.decoder.JSONDecodeError('foo', |
|
|
|
'bar', 1) |
|
|
|
res.text = 'body' |
|
|
|
|
|
|
|
ret, output = self.runMain(lambda: check_res_code(res)) |
|
|
|