##// END OF EJS Templates
use default kernel name in kernels service...
MinRK -
Show More
@@ -1,230 +1,230 b''
1 """Tornado handlers for kernels."""
1 """Tornado handlers for kernels."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import json
6 import json
7 import logging
7 import logging
8 from tornado import web
8 from tornado import web
9
9
10 from IPython.utils.jsonutil import date_default
10 from IPython.utils.jsonutil import date_default
11 from IPython.utils.py3compat import string_types
11 from IPython.utils.py3compat import string_types
12 from IPython.html.utils import url_path_join, url_escape
12 from IPython.html.utils import url_path_join, url_escape
13
13
14 from ...base.handlers import IPythonHandler, json_errors
14 from ...base.handlers import IPythonHandler, json_errors
15 from ...base.zmqhandlers import AuthenticatedZMQStreamHandler
15 from ...base.zmqhandlers import AuthenticatedZMQStreamHandler
16
16
17 from IPython.core.release import kernel_protocol_version
17 from IPython.core.release import kernel_protocol_version
18
18
19 class MainKernelHandler(IPythonHandler):
19 class MainKernelHandler(IPythonHandler):
20
20
21 @web.authenticated
21 @web.authenticated
22 @json_errors
22 @json_errors
23 def get(self):
23 def get(self):
24 km = self.kernel_manager
24 km = self.kernel_manager
25 self.finish(json.dumps(km.list_kernels()))
25 self.finish(json.dumps(km.list_kernels()))
26
26
27 @web.authenticated
27 @web.authenticated
28 @json_errors
28 @json_errors
29 def post(self):
29 def post(self):
30 km = self.kernel_manager
30 model = self.get_json_body()
31 model = self.get_json_body()
31 if model is None:
32 if model is None:
32 raise web.HTTPError(400, "No JSON data provided")
33 model = {
33 try:
34 'name': km.default_kernel_name
34 name = model['name']
35 }
35 except KeyError:
36 else:
36 raise web.HTTPError(400, "Missing field in JSON data: name")
37 model.setdefault('name', km.default_kernel_name)
37
38
38 km = self.kernel_manager
39 kernel_id = km.start_kernel(kernel_name=model['name'])
39 kernel_id = km.start_kernel(kernel_name=name)
40 model = km.kernel_model(kernel_id)
40 model = km.kernel_model(kernel_id)
41 location = url_path_join(self.base_url, 'api', 'kernels', kernel_id)
41 location = url_path_join(self.base_url, 'api', 'kernels', kernel_id)
42 self.set_header('Location', url_escape(location))
42 self.set_header('Location', url_escape(location))
43 self.set_status(201)
43 self.set_status(201)
44 self.finish(json.dumps(model))
44 self.finish(json.dumps(model))
45
45
46
46
47 class KernelHandler(IPythonHandler):
47 class KernelHandler(IPythonHandler):
48
48
49 SUPPORTED_METHODS = ('DELETE', 'GET')
49 SUPPORTED_METHODS = ('DELETE', 'GET')
50
50
51 @web.authenticated
51 @web.authenticated
52 @json_errors
52 @json_errors
53 def get(self, kernel_id):
53 def get(self, kernel_id):
54 km = self.kernel_manager
54 km = self.kernel_manager
55 km._check_kernel_id(kernel_id)
55 km._check_kernel_id(kernel_id)
56 model = km.kernel_model(kernel_id)
56 model = km.kernel_model(kernel_id)
57 self.finish(json.dumps(model))
57 self.finish(json.dumps(model))
58
58
59 @web.authenticated
59 @web.authenticated
60 @json_errors
60 @json_errors
61 def delete(self, kernel_id):
61 def delete(self, kernel_id):
62 km = self.kernel_manager
62 km = self.kernel_manager
63 km.shutdown_kernel(kernel_id)
63 km.shutdown_kernel(kernel_id)
64 self.set_status(204)
64 self.set_status(204)
65 self.finish()
65 self.finish()
66
66
67
67
68 class KernelActionHandler(IPythonHandler):
68 class KernelActionHandler(IPythonHandler):
69
69
70 @web.authenticated
70 @web.authenticated
71 @json_errors
71 @json_errors
72 def post(self, kernel_id, action):
72 def post(self, kernel_id, action):
73 km = self.kernel_manager
73 km = self.kernel_manager
74 if action == 'interrupt':
74 if action == 'interrupt':
75 km.interrupt_kernel(kernel_id)
75 km.interrupt_kernel(kernel_id)
76 self.set_status(204)
76 self.set_status(204)
77 if action == 'restart':
77 if action == 'restart':
78 km.restart_kernel(kernel_id)
78 km.restart_kernel(kernel_id)
79 model = km.kernel_model(kernel_id)
79 model = km.kernel_model(kernel_id)
80 self.set_header('Location', '{0}api/kernels/{1}'.format(self.base_url, kernel_id))
80 self.set_header('Location', '{0}api/kernels/{1}'.format(self.base_url, kernel_id))
81 self.write(json.dumps(model))
81 self.write(json.dumps(model))
82 self.finish()
82 self.finish()
83
83
84
84
85 class ZMQChannelHandler(AuthenticatedZMQStreamHandler):
85 class ZMQChannelHandler(AuthenticatedZMQStreamHandler):
86
86
87 def __repr__(self):
87 def __repr__(self):
88 return "%s(%s)" % (self.__class__.__name__, getattr(self, 'kernel_id', 'uninitialized'))
88 return "%s(%s)" % (self.__class__.__name__, getattr(self, 'kernel_id', 'uninitialized'))
89
89
90 def create_stream(self):
90 def create_stream(self):
91 km = self.kernel_manager
91 km = self.kernel_manager
92 meth = getattr(km, 'connect_%s' % self.channel)
92 meth = getattr(km, 'connect_%s' % self.channel)
93 self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession)
93 self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession)
94 # Create a kernel_info channel to query the kernel protocol version.
94 # Create a kernel_info channel to query the kernel protocol version.
95 # This channel will be closed after the kernel_info reply is received.
95 # This channel will be closed after the kernel_info reply is received.
96 self.kernel_info_channel = None
96 self.kernel_info_channel = None
97 self.kernel_info_channel = km.connect_shell(self.kernel_id)
97 self.kernel_info_channel = km.connect_shell(self.kernel_id)
98 self.kernel_info_channel.on_recv(self._handle_kernel_info_reply)
98 self.kernel_info_channel.on_recv(self._handle_kernel_info_reply)
99 self._request_kernel_info()
99 self._request_kernel_info()
100
100
101 def _request_kernel_info(self):
101 def _request_kernel_info(self):
102 """send a request for kernel_info"""
102 """send a request for kernel_info"""
103 self.log.debug("requesting kernel info")
103 self.log.debug("requesting kernel info")
104 self.session.send(self.kernel_info_channel, "kernel_info_request")
104 self.session.send(self.kernel_info_channel, "kernel_info_request")
105
105
106 def _handle_kernel_info_reply(self, msg):
106 def _handle_kernel_info_reply(self, msg):
107 """process the kernel_info_reply
107 """process the kernel_info_reply
108
108
109 enabling msg spec adaptation, if necessary
109 enabling msg spec adaptation, if necessary
110 """
110 """
111 idents,msg = self.session.feed_identities(msg)
111 idents,msg = self.session.feed_identities(msg)
112 try:
112 try:
113 msg = self.session.unserialize(msg)
113 msg = self.session.unserialize(msg)
114 except:
114 except:
115 self.log.error("Bad kernel_info reply", exc_info=True)
115 self.log.error("Bad kernel_info reply", exc_info=True)
116 self._request_kernel_info()
116 self._request_kernel_info()
117 return
117 return
118 else:
118 else:
119 if msg['msg_type'] != 'kernel_info_reply' or 'protocol_version' not in msg['content']:
119 if msg['msg_type'] != 'kernel_info_reply' or 'protocol_version' not in msg['content']:
120 self.log.error("Kernel info request failed, assuming current %s", msg['content'])
120 self.log.error("Kernel info request failed, assuming current %s", msg['content'])
121 else:
121 else:
122 protocol_version = msg['content']['protocol_version']
122 protocol_version = msg['content']['protocol_version']
123 if protocol_version != kernel_protocol_version:
123 if protocol_version != kernel_protocol_version:
124 self.session.adapt_version = int(protocol_version.split('.')[0])
124 self.session.adapt_version = int(protocol_version.split('.')[0])
125 self.log.info("adapting kernel to %s" % protocol_version)
125 self.log.info("adapting kernel to %s" % protocol_version)
126 self.kernel_info_channel.close()
126 self.kernel_info_channel.close()
127 self.kernel_info_channel = None
127 self.kernel_info_channel = None
128
128
129 def initialize(self):
129 def initialize(self):
130 super(ZMQChannelHandler, self).initialize()
130 super(ZMQChannelHandler, self).initialize()
131 self.zmq_stream = None
131 self.zmq_stream = None
132
132
133 def open(self, kernel_id):
133 def open(self, kernel_id):
134 super(ZMQChannelHandler, self).open(kernel_id)
134 super(ZMQChannelHandler, self).open(kernel_id)
135 try:
135 try:
136 self.create_stream()
136 self.create_stream()
137 except web.HTTPError:
137 except web.HTTPError:
138 # WebSockets don't response to traditional error codes so we
138 # WebSockets don't response to traditional error codes so we
139 # close the connection.
139 # close the connection.
140 if not self.stream.closed():
140 if not self.stream.closed():
141 self.stream.close()
141 self.stream.close()
142 self.close()
142 self.close()
143 else:
143 else:
144 self.zmq_stream.on_recv(self._on_zmq_reply)
144 self.zmq_stream.on_recv(self._on_zmq_reply)
145
145
146 def on_message(self, msg):
146 def on_message(self, msg):
147 if self.zmq_stream is None:
147 if self.zmq_stream is None:
148 return
148 return
149 elif self.zmq_stream.closed():
149 elif self.zmq_stream.closed():
150 self.log.info("%s closed, closing websocket.", self)
150 self.log.info("%s closed, closing websocket.", self)
151 self.close()
151 self.close()
152 return
152 return
153 msg = json.loads(msg)
153 msg = json.loads(msg)
154 self.session.send(self.zmq_stream, msg)
154 self.session.send(self.zmq_stream, msg)
155
155
156 def on_close(self):
156 def on_close(self):
157 # This method can be called twice, once by self.kernel_died and once
157 # This method can be called twice, once by self.kernel_died and once
158 # from the WebSocket close event. If the WebSocket connection is
158 # from the WebSocket close event. If the WebSocket connection is
159 # closed before the ZMQ streams are setup, they could be None.
159 # closed before the ZMQ streams are setup, they could be None.
160 if self.zmq_stream is not None and not self.zmq_stream.closed():
160 if self.zmq_stream is not None and not self.zmq_stream.closed():
161 self.zmq_stream.on_recv(None)
161 self.zmq_stream.on_recv(None)
162 # close the socket directly, don't wait for the stream
162 # close the socket directly, don't wait for the stream
163 socket = self.zmq_stream.socket
163 socket = self.zmq_stream.socket
164 self.zmq_stream.close()
164 self.zmq_stream.close()
165 socket.close()
165 socket.close()
166
166
167
167
168 class IOPubHandler(ZMQChannelHandler):
168 class IOPubHandler(ZMQChannelHandler):
169 channel = 'iopub'
169 channel = 'iopub'
170
170
171 def create_stream(self):
171 def create_stream(self):
172 super(IOPubHandler, self).create_stream()
172 super(IOPubHandler, self).create_stream()
173 km = self.kernel_manager
173 km = self.kernel_manager
174 km.add_restart_callback(self.kernel_id, self.on_kernel_restarted)
174 km.add_restart_callback(self.kernel_id, self.on_kernel_restarted)
175 km.add_restart_callback(self.kernel_id, self.on_restart_failed, 'dead')
175 km.add_restart_callback(self.kernel_id, self.on_restart_failed, 'dead')
176
176
177 def on_close(self):
177 def on_close(self):
178 km = self.kernel_manager
178 km = self.kernel_manager
179 if self.kernel_id in km:
179 if self.kernel_id in km:
180 km.remove_restart_callback(
180 km.remove_restart_callback(
181 self.kernel_id, self.on_kernel_restarted,
181 self.kernel_id, self.on_kernel_restarted,
182 )
182 )
183 km.remove_restart_callback(
183 km.remove_restart_callback(
184 self.kernel_id, self.on_restart_failed, 'dead',
184 self.kernel_id, self.on_restart_failed, 'dead',
185 )
185 )
186 super(IOPubHandler, self).on_close()
186 super(IOPubHandler, self).on_close()
187
187
188 def _send_status_message(self, status):
188 def _send_status_message(self, status):
189 msg = self.session.msg("status",
189 msg = self.session.msg("status",
190 {'execution_state': status}
190 {'execution_state': status}
191 )
191 )
192 self.write_message(json.dumps(msg, default=date_default))
192 self.write_message(json.dumps(msg, default=date_default))
193
193
194 def on_kernel_restarted(self):
194 def on_kernel_restarted(self):
195 logging.warn("kernel %s restarted", self.kernel_id)
195 logging.warn("kernel %s restarted", self.kernel_id)
196 self._send_status_message('restarting')
196 self._send_status_message('restarting')
197
197
198 def on_restart_failed(self):
198 def on_restart_failed(self):
199 logging.error("kernel %s restarted failed!", self.kernel_id)
199 logging.error("kernel %s restarted failed!", self.kernel_id)
200 self._send_status_message('dead')
200 self._send_status_message('dead')
201
201
202 def on_message(self, msg):
202 def on_message(self, msg):
203 """IOPub messages make no sense"""
203 """IOPub messages make no sense"""
204 pass
204 pass
205
205
206
206
207 class ShellHandler(ZMQChannelHandler):
207 class ShellHandler(ZMQChannelHandler):
208 channel = 'shell'
208 channel = 'shell'
209
209
210
210
211 class StdinHandler(ZMQChannelHandler):
211 class StdinHandler(ZMQChannelHandler):
212 channel = 'stdin'
212 channel = 'stdin'
213
213
214
214
215 #-----------------------------------------------------------------------------
215 #-----------------------------------------------------------------------------
216 # URL to handler mappings
216 # URL to handler mappings
217 #-----------------------------------------------------------------------------
217 #-----------------------------------------------------------------------------
218
218
219
219
220 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
220 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
221 _kernel_action_regex = r"(?P<action>restart|interrupt)"
221 _kernel_action_regex = r"(?P<action>restart|interrupt)"
222
222
223 default_handlers = [
223 default_handlers = [
224 (r"/api/kernels", MainKernelHandler),
224 (r"/api/kernels", MainKernelHandler),
225 (r"/api/kernels/%s" % _kernel_id_regex, KernelHandler),
225 (r"/api/kernels/%s" % _kernel_id_regex, KernelHandler),
226 (r"/api/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
226 (r"/api/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
227 (r"/api/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
227 (r"/api/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
228 (r"/api/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
228 (r"/api/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
229 (r"/api/kernels/%s/stdin" % _kernel_id_regex, StdinHandler)
229 (r"/api/kernels/%s/stdin" % _kernel_id_regex, StdinHandler)
230 ]
230 ]
@@ -1,123 +1,133 b''
1 """Test the kernels service API."""
1 """Test the kernels service API."""
2
2
3 import json
3 import json
4 import requests
4 import requests
5
5
6 from IPython.html.utils import url_path_join
6 from IPython.html.utils import url_path_join
7 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
7 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
8
8
9 class KernelAPI(object):
9 class KernelAPI(object):
10 """Wrapper for kernel REST API requests"""
10 """Wrapper for kernel REST API requests"""
11 def __init__(self, base_url):
11 def __init__(self, base_url):
12 self.base_url = base_url
12 self.base_url = base_url
13
13
14 def _req(self, verb, path, body=None):
14 def _req(self, verb, path, body=None):
15 response = requests.request(verb,
15 response = requests.request(verb,
16 url_path_join(self.base_url, 'api/kernels', path), data=body)
16 url_path_join(self.base_url, 'api/kernels', path), data=body)
17
17
18 if 400 <= response.status_code < 600:
18 if 400 <= response.status_code < 600:
19 try:
19 try:
20 response.reason = response.json()['message']
20 response.reason = response.json()['message']
21 except:
21 except:
22 pass
22 pass
23 response.raise_for_status()
23 response.raise_for_status()
24
24
25 return response
25 return response
26
26
27 def list(self):
27 def list(self):
28 return self._req('GET', '')
28 return self._req('GET', '')
29
29
30 def get(self, id):
30 def get(self, id):
31 return self._req('GET', id)
31 return self._req('GET', id)
32
32
33 def start(self, name='python'):
33 def start(self, name='python'):
34 body = json.dumps({'name': name})
34 body = json.dumps({'name': name})
35 return self._req('POST', '', body)
35 return self._req('POST', '', body)
36
36
37 def shutdown(self, id):
37 def shutdown(self, id):
38 return self._req('DELETE', id)
38 return self._req('DELETE', id)
39
39
40 def interrupt(self, id):
40 def interrupt(self, id):
41 return self._req('POST', url_path_join(id, 'interrupt'))
41 return self._req('POST', url_path_join(id, 'interrupt'))
42
42
43 def restart(self, id):
43 def restart(self, id):
44 return self._req('POST', url_path_join(id, 'restart'))
44 return self._req('POST', url_path_join(id, 'restart'))
45
45
46 class KernelAPITest(NotebookTestBase):
46 class KernelAPITest(NotebookTestBase):
47 """Test the kernels web service API"""
47 """Test the kernels web service API"""
48 def setUp(self):
48 def setUp(self):
49 self.kern_api = KernelAPI(self.base_url())
49 self.kern_api = KernelAPI(self.base_url())
50
50
51 def tearDown(self):
51 def tearDown(self):
52 for k in self.kern_api.list().json():
52 for k in self.kern_api.list().json():
53 self.kern_api.shutdown(k['id'])
53 self.kern_api.shutdown(k['id'])
54
54
55 def test__no_kernels(self):
55 def test__no_kernels(self):
56 """Make sure there are no kernels running at the start"""
56 """Make sure there are no kernels running at the start"""
57 kernels = self.kern_api.list().json()
57 kernels = self.kern_api.list().json()
58 self.assertEqual(kernels, [])
58 self.assertEqual(kernels, [])
59
59
60 def test_default_kernel(self):
61 # POST request
62 r = self.kern_api._req('POST', '')
63 kern1 = r.json()
64 self.assertEqual(r.headers['location'], '/api/kernels/' + kern1['id'])
65 self.assertEqual(r.status_code, 201)
66 self.assertIsInstance(kern1, dict)
67
68 self.assertEqual(r.headers['x-frame-options'], "SAMEORIGIN")
69
60 def test_main_kernel_handler(self):
70 def test_main_kernel_handler(self):
61 # POST request
71 # POST request
62 r = self.kern_api.start()
72 r = self.kern_api.start()
63 kern1 = r.json()
73 kern1 = r.json()
64 self.assertEqual(r.headers['location'], '/api/kernels/' + kern1['id'])
74 self.assertEqual(r.headers['location'], '/api/kernels/' + kern1['id'])
65 self.assertEqual(r.status_code, 201)
75 self.assertEqual(r.status_code, 201)
66 self.assertIsInstance(kern1, dict)
76 self.assertIsInstance(kern1, dict)
67
77
68 self.assertEqual(r.headers['x-frame-options'], "SAMEORIGIN")
78 self.assertEqual(r.headers['x-frame-options'], "SAMEORIGIN")
69
79
70 # GET request
80 # GET request
71 r = self.kern_api.list()
81 r = self.kern_api.list()
72 self.assertEqual(r.status_code, 200)
82 self.assertEqual(r.status_code, 200)
73 assert isinstance(r.json(), list)
83 assert isinstance(r.json(), list)
74 self.assertEqual(r.json()[0]['id'], kern1['id'])
84 self.assertEqual(r.json()[0]['id'], kern1['id'])
75 self.assertEqual(r.json()[0]['name'], kern1['name'])
85 self.assertEqual(r.json()[0]['name'], kern1['name'])
76
86
77 # create another kernel and check that they both are added to the
87 # create another kernel and check that they both are added to the
78 # list of kernels from a GET request
88 # list of kernels from a GET request
79 kern2 = self.kern_api.start().json()
89 kern2 = self.kern_api.start().json()
80 assert isinstance(kern2, dict)
90 assert isinstance(kern2, dict)
81 r = self.kern_api.list()
91 r = self.kern_api.list()
82 kernels = r.json()
92 kernels = r.json()
83 self.assertEqual(r.status_code, 200)
93 self.assertEqual(r.status_code, 200)
84 assert isinstance(kernels, list)
94 assert isinstance(kernels, list)
85 self.assertEqual(len(kernels), 2)
95 self.assertEqual(len(kernels), 2)
86
96
87 # Interrupt a kernel
97 # Interrupt a kernel
88 r = self.kern_api.interrupt(kern2['id'])
98 r = self.kern_api.interrupt(kern2['id'])
89 self.assertEqual(r.status_code, 204)
99 self.assertEqual(r.status_code, 204)
90
100
91 # Restart a kernel
101 # Restart a kernel
92 r = self.kern_api.restart(kern2['id'])
102 r = self.kern_api.restart(kern2['id'])
93 self.assertEqual(r.headers['Location'], '/api/kernels/'+kern2['id'])
103 self.assertEqual(r.headers['Location'], '/api/kernels/'+kern2['id'])
94 rekern = r.json()
104 rekern = r.json()
95 self.assertEqual(rekern['id'], kern2['id'])
105 self.assertEqual(rekern['id'], kern2['id'])
96 self.assertEqual(rekern['name'], kern2['name'])
106 self.assertEqual(rekern['name'], kern2['name'])
97
107
98 def test_kernel_handler(self):
108 def test_kernel_handler(self):
99 # GET kernel with given id
109 # GET kernel with given id
100 kid = self.kern_api.start().json()['id']
110 kid = self.kern_api.start().json()['id']
101 r = self.kern_api.get(kid)
111 r = self.kern_api.get(kid)
102 kern1 = r.json()
112 kern1 = r.json()
103 self.assertEqual(r.status_code, 200)
113 self.assertEqual(r.status_code, 200)
104 assert isinstance(kern1, dict)
114 assert isinstance(kern1, dict)
105 self.assertIn('id', kern1)
115 self.assertIn('id', kern1)
106 self.assertEqual(kern1['id'], kid)
116 self.assertEqual(kern1['id'], kid)
107
117
108 # Request a bad kernel id and check that a JSON
118 # Request a bad kernel id and check that a JSON
109 # message is returned!
119 # message is returned!
110 bad_id = '111-111-111-111-111'
120 bad_id = '111-111-111-111-111'
111 with assert_http_error(404, 'Kernel does not exist: ' + bad_id):
121 with assert_http_error(404, 'Kernel does not exist: ' + bad_id):
112 self.kern_api.get(bad_id)
122 self.kern_api.get(bad_id)
113
123
114 # DELETE kernel with id
124 # DELETE kernel with id
115 r = self.kern_api.shutdown(kid)
125 r = self.kern_api.shutdown(kid)
116 self.assertEqual(r.status_code, 204)
126 self.assertEqual(r.status_code, 204)
117 kernels = self.kern_api.list().json()
127 kernels = self.kern_api.list().json()
118 self.assertEqual(kernels, [])
128 self.assertEqual(kernels, [])
119
129
120 # Request to delete a non-existent kernel id
130 # Request to delete a non-existent kernel id
121 bad_id = '111-111-111-111-111'
131 bad_id = '111-111-111-111-111'
122 with assert_http_error(404, 'Kernel does not exist: ' + bad_id):
132 with assert_http_error(404, 'Kernel does not exist: ' + bad_id):
123 self.kern_api.shutdown(bad_id)
133 self.kern_api.shutdown(bad_id)
General Comments 0
You need to be logged in to leave comments. Login now