1 # Copyright (C) 2013 Nippon Telegraph and Telephone Corporation.
2 # Copyright (C) 2013 YAMAMOTO Takashi <yamamoto at valinux co jp>
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
18 CLI application for SSH management.
28 from ryu import version
29 from ryu.lib import hub
30 from ryu.services.protocols.bgp.base import Activity
31 from ryu.services.protocols.bgp.operator.command import Command
32 from ryu.services.protocols.bgp.operator.command import CommandsResponse
33 from ryu.services.protocols.bgp.operator.command import STATUS_OK
34 from ryu.services.protocols.bgp.operator.commands.root import RootCmd
35 from ryu.services.protocols.bgp.operator.internal_api import InternalApi
39 SSH_HOST_KEY = "ssh_host_key"
40 SSH_USERNAME = "ssh_username"
41 SSH_PASSWORD = "ssh_password"
43 DEFAULT_SSH_PORT = 4990
44 DEFAULT_SSH_HOST = "localhost"
45 DEFAULT_SSH_HOST_KEY = None
46 DEFAULT_SSH_USERNAME = "ryu"
47 DEFAULT_SSH_PASSWORD = "ryu"
50 SSH_PORT: DEFAULT_SSH_PORT,
51 SSH_HOST: DEFAULT_SSH_HOST,
52 SSH_HOST_KEY: DEFAULT_SSH_HOST_KEY,
53 SSH_USERNAME: DEFAULT_SSH_USERNAME,
54 SSH_PASSWORD: DEFAULT_SSH_PASSWORD,
57 LOG = logging.getLogger('bgpspeaker.cli')
60 def find_ssh_server_key():
61 if CONF[SSH_HOST_KEY]:
62 return paramiko.RSAKey.from_private_key_file(CONF[SSH_HOST_KEY])
63 elif os.path.exists("/etc/ssh_host_rsa_key"):
65 return paramiko.RSAKey.from_private_key_file(
66 "/etc/ssh_host_rsa_key")
67 elif os.path.exists("/etc/ssh/ssh_host_rsa_key"):
69 return paramiko.RSAKey.from_private_key_file(
70 "/etc/ssh/ssh_host_rsa_key")
72 return paramiko.RSAKey.generate(1024)
75 class SshServer(paramiko.ServerInterface):
78 WELCOME = "\n\rHello, this is Ryu BGP speaker (version %s).\n\r" % version
80 class HelpCmd(Command):
81 help_msg = 'show this help'
84 def action(self, params):
85 return self.parent_cmd.question_mark()[0]
87 class QuitCmd(Command):
88 help_msg = 'exit this session'
91 def action(self, params):
92 self.api.sshserver.end_session()
93 return CommandsResponse(STATUS_OK, True)
95 def __init__(self, sock, addr):
96 super(SshServer, self).__init__()
99 self.is_connected = True
105 self.histindex = None
108 self.promptlen = None
110 # tweak InternalApi and RootCmd for non-bgp related commands
111 self.api = InternalApi(log_handler=logging.StreamHandler(sys.stderr))
112 setattr(self.api, 'sshserver', self)
113 self.root = RootCmd(self.api)
114 self.root.subcommands['help'] = self.HelpCmd
115 self.root.subcommands['quit'] = self.QuitCmd
117 self.transport = paramiko.Transport(self.sock)
118 self.transport.load_server_moduli()
119 host_key = find_ssh_server_key()
120 self.transport.add_server_key(host_key)
121 self.transport.start_server(server=self)
123 def check_auth_none(self, username):
124 return paramiko.AUTH_SUCCESSFUL
126 def check_auth_password(self, username, password):
127 if (username == CONF[SSH_USERNAME]
128 and password == CONF[SSH_PASSWORD]):
129 return paramiko.AUTH_SUCCESSFUL
130 return paramiko.AUTH_FAILED
132 def check_channel_request(self, kind, chanid):
133 if kind == 'session':
134 return paramiko.OPEN_SUCCEEDED
135 return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
137 def check_channel_shell_request(self, channel):
138 hub.spawn(self._handle_shell_request)
141 def check_channel_pty_request(self, channel, term, width, height,
142 pixelwidth, pixelheight, modes):
146 def check_channel_window_change_request(self, channel, width, height,
147 pixelwidth, pixelheight):
152 return not (c < chr(0x20) or c == chr(0x7F))
156 return c == chr(0x0d)
160 return c == chr(0x03)
164 return c == chr(0x1b)
168 return c == chr(0x10) or c == chr(0x0e)
172 return (c == chr(0x04) or c == chr(0x08) or c == chr(0x15)
173 or c == chr(0x17) or c == chr(0x0c) or c == chr(0x7f))
177 return c == chr(0x01) or c == chr(0x02) or c == chr(0x05) \
182 return c == chr(0x09)
184 def _handle_csi_seq(self):
185 c = self.chan.recv(1)
186 c = c.decode() # For Python3 compatibility
188 self._lookup_hist_up()
190 self._lookup_hist_down()
192 self._movcursor(self.curpos + 1)
194 self._movcursor(self.curpos - 1)
196 LOG.error("unknown CSI sequence. do nothing: %c", c)
198 def _handle_esc_seq(self):
199 c = self.chan.recv(1)
200 c = c.decode() # For Python3 compatibility
202 self._handle_csi_seq()
204 LOG.error("non CSI sequence. do nothing")
206 def _send_csi_seq(self, cmd):
207 self.chan.send('\x1b[' + cmd)
209 def _movcursor(self, curpos):
210 if self.prompted and curpos < len(self.PROMPT):
211 self.curpos = len(self.PROMPT)
212 elif self.prompted and curpos > (len(self.PROMPT) + len(self.buf)):
213 self.curpos = len(self.PROMPT) + len(self.buf)
215 self._send_csi_seq('%dG' % (curpos + 1))
218 def _clearscreen(self, prompt=None):
219 if not prompt and self.prompted:
222 self._send_csi_seq('2J')
223 # move cursor to the top
224 self._send_csi_seq('d')
225 # redraw prompt and buf
226 self._refreshline(prompt=prompt)
228 def _clearline(self, prompt=None):
229 if not prompt and self.prompted:
231 self.prompted = False
233 self._send_csi_seq('2K')
236 self.chan.send(prompt)
237 self._movcursor(len(prompt))
240 def _refreshline(self, prompt=None):
241 if not prompt and self.prompted:
244 curpos = copy(self.curpos)
245 self._clearline(prompt=prompt)
246 self.chan.send(''.join(buf))
249 self._movcursor(curpos)
251 def _refreshnewline(self, prompt=None):
252 if not prompt and self.prompted:
255 curpos = copy(self.curpos)
256 self._startnewline(prompt)
257 self.chan.send(''.join(buf))
260 self._movcursor(curpos)
262 def _startnewline(self, prompt=None, buf=None):
264 if not prompt and self.prompted:
266 if isinstance(buf, str):
271 self.chan.send('\n\r' + prompt + ''.join(buf))
272 self.curpos = len(prompt) + len(buf)
275 self.chan.send('\n\r' + ''.join(buf))
276 self.curpos = len(buf)
277 self.prompted = False
279 def _lookup_hist_up(self):
280 if len(self.history) == 0:
282 self.buf = self.history[self.histindex]
283 self.curpos = self.promptlen + len(self.buf)
285 if self.histindex + 1 < len(self.history):
288 def _lookup_hist_down(self):
289 if self.histindex > 0:
291 self.buf = self.history[self.histindex]
292 self.curpos = self.promptlen + len(self.buf)
297 def _do_cmpl(self, buf, is_exec=False):
299 is_spaced = buf[-1] == ' ' if len(buf) > 0 else False
300 cmds = [tkn.strip() for tkn in ''.join(buf).split()]
303 for i, cmd in enumerate(cmds):
304 subcmds = cmpleter.subcommands
305 matches = [x for x in subcmds.keys() if x.startswith(cmd)]
307 if len(matches) == 1:
308 cmpled_cmd = matches[0]
309 cmpleter = subcmds[cmpled_cmd](self.api)
312 ret.append(cmpled_cmd)
315 if (i + 1) == len(cmds):
317 result, cmd = cmpleter('?')
318 result = result.value.replace('\n', '\n\r').rstrip()
319 self.prompted = False
321 self._startnewline(buf=result)
323 self._startnewline(buf=buf)
325 self.buf = buf[:(-1 * len(cmd))] + \
326 list(cmpled_cmd + ' ')
327 self.curpos += len(cmpled_cmd) - len(cmd) + 1
330 self.prompted = False
332 if len(matches) == 0:
333 if cmpleter.param_help_msg:
338 self._startnewline(buf='Error: Not implemented')
340 if (i + 1) < len(cmds):
341 self._startnewline(buf='Error: Ambiguous command')
343 self._startnewline(buf=', '.join(matches))
347 self._startnewline(buf=buf)
352 def _execute_cmd(self, cmds):
353 result, _ = self.root(cmds)
354 LOG.debug("result: %s", result)
355 if cmds[0] == 'quit':
356 self.is_connected = False
358 self.prompted = False
360 output = result.value.replace('\n', '\n\r').rstrip()
361 self.chan.send(output)
366 def end_session(self):
367 self._startnewline(prompt=False, buf='bye.\n\r')
370 def _handle_shell_request(self):
371 LOG.info("session start")
372 chan = self.transport.accept(20)
374 LOG.info("transport.accept timed out")
383 self.chan.send(self.WELCOME)
386 while self.is_connected:
387 c = self.chan.recv(1)
388 c = c.decode() # For Python3 compatibility
393 LOG.debug("ord:%d, hex:0x%x", ord(c), ord(c))
394 self.promptlen = len(self.PROMPT) if self.prompted else 0
397 cmds = [tkn.strip() for tkn in ''.join(self.buf).split()]
399 for i, cmd in enumerate(cmds):
400 subcmds = cmpleter.subcommands
401 matches = [x for x in subcmds.keys() if x.startswith(cmd)]
402 if len(matches) == 1:
403 cmpled_cmd = matches[0]
404 cmpleter = subcmds[cmpled_cmd](self.api)
406 result, cmd = cmpleter('?')
407 result = result.value.replace('\n', '\n\r').rstrip()
408 self.prompted = False
410 self._startnewline(buf=result)
412 self._startnewline(buf=buf)
413 elif self._is_echoable(c):
414 self.buf.insert(self.curpos - self.promptlen, c)
417 elif self._is_esc(c):
418 self._handle_esc_seq()
419 elif self._is_eof(c):
421 elif self._is_curmov(c):
424 self._movcursor(self.promptlen)
427 self._movcursor(self.curpos - 1)
430 self._movcursor(self.promptlen + len(self.buf))
433 self._movcursor(self.curpos + 1)
435 LOG.error("unknown cursor move cmd.")
437 elif self._is_hist(c):
440 self._lookup_hist_up()
443 self._lookup_hist_down()
444 elif self._is_del(c):
447 if self.curpos < (self.promptlen + len(self.buf)):
448 self.buf.pop(self.curpos - self.promptlen)
451 elif c == chr(0x08) or c == chr(0x7f):
452 if self.curpos > self.promptlen:
453 self.buf.pop(self.curpos - self.promptlen - 1)
461 pos = self.curpos - self.promptlen
464 for c in reversed(self.buf[:pos]):
465 if flag and c == ' ':
471 self.curpos = self.promptlen + i
476 elif self._is_cmpl(c):
477 self._do_cmpl(self.buf)
478 elif self._is_enter(c):
479 if len(''.join(self.buf).strip()) != 0:
480 # cmd line interpretation
481 cmds = self._do_cmpl(self.buf, is_exec=True)
483 self.history.insert(0, self.buf)
485 self._execute_cmd(cmds)
487 LOG.debug("no command is interpreted. "
488 "just start a new line.")
491 LOG.debug("blank buf is detected. "
492 "just start a new line.")
495 LOG.debug("curpos: %d, buf: %s, prompted: %s", self.curpos,
496 self.buf, self.prompted)
498 LOG.info("session end")
501 def ssh_server_factory(sock, addr):
502 SshServer(sock, addr)
507 super(Cli, self).__init__()
509 def _run(self, *args, **kwargs):
510 for k, v in kwargs.items():
514 listen_info = (CONF[SSH_HOST], CONF[SSH_PORT])
515 LOG.info("starting ssh server at %s:%d" % listen_info)
516 server = hub.StreamServer(listen_info, ssh_server_factory)
517 server.serve_forever()
520 SSH_CLI_CONTROLLER = Cli()