Merge lp:~ensoft-opensource/ensoft-sextant/v1_ssh into lp:ensoft-sextant
- v1_ssh
- Merge into whiteline
Status: | Merged |
---|---|
Approved by: | ChrisD |
Approved revision: | 42 |
Merged at revision: | 12 |
Proposed branch: | lp:~ensoft-opensource/ensoft-sextant/v1_ssh |
Merge into: | lp:ensoft-sextant |
Diff against target: |
985 lines (+524/-233) 9 files modified
bin/sextant (+0/-10) doc/wiki/Reference (+10/-2) etc/sextant.conf (+13/-5) setup.py (+1/-1) src/sextant/__main__.py (+353/-194) src/sextant/environment.py (+98/-1) src/sextant/export.py (+1/-1) src/sextant/query.py (+40/-17) src/sextant/update_db.py (+8/-2) |
To merge this branch: | bzr merge lp:~ensoft-opensource/ensoft-sextant/v1_ssh |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
James | Approve | ||
Review via email: mp+232403@code.launchpad.net |
Commit message
Add support for SSH-ing into the Neo4j instance, rather than connecting directly.
Description of the change
Add support for SSH-ing into the Neo4j instance, rather than connecting directly.
Updates the documentation to add a new SSH section; adds a section to sextant.__main__ to handle SSHing. This parses the input arguments to Sextant and searches for remote_neo4j, then uses that or whichever configured default exists. It picks a random high-numbered open port, creates a port forward between localhost:highport and remote_neo4j, then passes localhost:highport in to Sextant proper. Once Sextant has finished, closes the port forward.
Adds an option display_neo4j to Sextant's internal functions (hidden to end-users) so that Sextant functions can use remoteneo4j:7474 rather than localhost:highport in messages output to the user.
Adds a use_ssh_tunnel config option (defaulting to True) to make Sextant connect directly rather than by SSH.
Moves any configuration-
Also a small bugfix where sextant.
Patrick Stevens (patrickas) wrote : | # |
James (jamesh-f) wrote : | # |
Please see comments on Documentation.
Patrick Stevens (patrickas) : | # |
ChrisD (gingerchris) wrote : | # |
Big question (that I should perhaps know the answer to):
Why did we decide to implement this as a wrapper to sextant and not simply make it optional functionality that sextant can provide?
I'm perhaps missing something, but if we integrate this with sextant that removes a lot of faff e.g. wrappers extra odd args...
Note that some of my inline comments were added prior to this realisation.
Patrick Stevens (patrickas) wrote : | # |
> Big question (that I should perhaps know the answer to):
> Why did we decide to implement this as a wrapper to sextant and not simply
> make it optional functionality that sextant can provide?
>
> I'm perhaps missing something, but if we integrate this with sextant that
> removes a lot of faff e.g. wrappers extra odd args...
>
> Note that some of my inline comments were added prior to this realisation.
This was after discussion with Rob - it also seems logically sensible to me, to separate the fact that we're SSHing for all of Sextant from Sextant itself. (It doesn't get rid of that many extra args to put it inline - we still want to pass stuff in to sextant.query, sextant.add_program and so forth...)
ChrisD (gingerchris) wrote : | # |
But by virtue of having those args and the wrapper script parsing sextants
config, importing its modules and understating its cli there is already no
meaningful separation.
Further, trying to maintain separation is introducing hacks.
Some args may still exist to internal Apis but there won't be any need to
have extra external (e.g. Cli) args.
On Friday, 29 August 2014, Patrick Stevens <email address hidden> wrote:
> > Big question (that I should perhaps know the answer to):
> > Why did we decide to implement this as a wrapper to sextant and not
> simply
> > make it optional functionality that sextant can provide?
> >
> > I'm perhaps missing something, but if we integrate this with sextant that
> > removes a lot of faff e.g. wrappers extra odd args...
> >
> > Note that some of my inline comments were added prior to this
> realisation.
>
> This was after discussion with Rob - it also seems logically sensible to
> me, to separate the fact that we're SSHing for all of Sextant from Sextant
> itself. (It doesn't get rid of that many extra args to put it inline - we
> still want to pass stuff in to sextant.query, sextant.add_program and so
> forth...)
> --
>
> https:/
> Your team Ensoft Open Source is subscribed to branch
> lp:~ensoft-opensource/ensoft-sextant/v1_ssh.
>
Patrick Stevens (patrickas) : | # |
Patrick Stevens (patrickas) wrote : | # |
Changes made per Chuck's comments. In particular, elide sextant_wrapper into sextant.__main__.
ChrisD (gingerchris) : | # |
ChrisD (gingerchris) : | # |
Patrick Stevens (patrickas) wrote : | # |
Two comments (I've made Chuck's changes on anything I haven't commented)
ChrisD (gingerchris) : | # |
Patrick Stevens (patrickas) wrote : | # |
I tried that - replaced the <for key in parsers> loop with:
argumentpar
argumentpar
and that causes the call <./bin/sextant query programs --remote-neo4j http://
error: unrecognized arguments: --remote-neo4j http://
According to [1], the correct way is to use parents (so parser.
James (jamesh-f) : | # |
Preview Diff
1 | === added file 'bin/sextant' |
2 | --- bin/sextant 1970-01-01 00:00:00 +0000 |
3 | +++ bin/sextant 2014-08-29 14:18:31 +0000 |
4 | @@ -0,0 +1,12 @@ |
5 | +#!/bin/bash |
6 | + |
7 | +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" |
8 | + |
9 | +export PYTHONPATH=${PYTHONPATH}:${DIR}/../src |
10 | + |
11 | +# We allow people to specify whether they want to run Sextant under Python 2 |
12 | +# or 3, by setting the environment variable SEXTANT_PYTHON to the Python they |
13 | +# want to use. |
14 | +export SEXTANT_PYTHON=${SEXTANT_PYTHON:=python} |
15 | + |
16 | +$SEXTANT_PYTHON -m sextant $@ |
17 | |
18 | === removed file 'bin/sextant' |
19 | --- bin/sextant 2014-08-19 16:04:41 +0000 |
20 | +++ bin/sextant 1970-01-01 00:00:00 +0000 |
21 | @@ -1,10 +0,0 @@ |
22 | -#!/bin/bash |
23 | - |
24 | -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" |
25 | - |
26 | -export PYTHONPATH=${PYTHONPATH}:${DIR}/../src |
27 | - |
28 | -export SEXTANT_PYTHON=${SEXTANT_PYTHON:=python} |
29 | - |
30 | -$SEXTANT_PYTHON -m sextant $@ |
31 | - |
32 | |
33 | === modified file 'doc/wiki/Reference' |
34 | --- doc/wiki/Reference 2014-08-26 11:04:04 +0000 |
35 | +++ doc/wiki/Reference 2014-08-29 14:18:31 +0000 |
36 | @@ -19,6 +19,10 @@ |
37 | {{{ |
38 | sextant delete-program <program name to be deleted as stored in database> |
39 | }}} |
40 | + |
41 | +== Audit == |
42 | +To see a detailed view of a list of programs on the server, type {{{ sextant audit }}} into the command line. This will return a list of programs stored on the Neo4j server, along with who uploaded them, that person's username on the computer they used to upload, and the date they were uploaded. |
43 | + |
44 | == Queries == |
45 | === Command Line === |
46 | Command line queries produce a text output, either as a list or in GraphML which can be opened in yED. |
47 | @@ -40,9 +44,13 @@ |
48 | {{{ |
49 | sextant query -h |
50 | }}} |
51 | + |
52 | === Web === |
53 | To run the web server type |
54 | {{{ sextant web }}} |
55 | into the command line; then navigate a web browser to the port specified. |
56 | -== Audit == |
57 | -To see a more detailed view of a list of programs on the server type {{{ sextant audit }}} into the command line. This will return a list of programs on the Neo4j server, specified in either the config file of command line, along with their unique program number, who uploaded them, that persons id and the date they were uploaded. |
58 | \ No newline at end of file |
59 | + |
60 | +== SSH == |
61 | +Sextant allows you to create an SSH tunnel between your client and the Neo4J server. Sextant picks a random high-numbered port which is not in use, sets up an SSH tunnel between {{{ localhost:{that port} }}} and the specified {{{ remote_neo4j }}} in the configuration file (or as the {{{ --remote-neo4j }}} command-line option), and then uses {{{ localhost:{port} }}} for communications. This happens by default; if you do not wish for this to happen, set the config option {{{ use_ssh_tunnel }}} to {{{ False }}} in the configuration file. |
62 | + |
63 | +Warning: if you have {{{ remote_neo4j }}} set to some variant of {{{ localhost }}}, then SSH may fail (because a machine can't easily SSH to itself as currently implemented). We have some error-catching here - if we detect that we are attempting to SSH to ourself, then we skip using SSH at all - but it can probably be broken by a determined user. If you do encounter an error for this reason, the workaround at present is to set {{{ use_ssh_tunnel }}} to {{{ False }}} in the configuration file. |
64 | |
65 | === modified file 'etc/sextant.conf' |
66 | --- etc/sextant.conf 2014-08-22 14:49:33 +0000 |
67 | +++ etc/sextant.conf 2014-08-29 14:18:31 +0000 |
68 | @@ -1,7 +1,15 @@ |
69 | [Neo4j] |
70 | # URL of the Neo4J server |
71 | -remote_neo4j = http://localhost:7474 |
72 | -# port on which to serve Sextant Web |
73 | -port = 2905 |
74 | -# number of calls incoming before we consider a function to be 'common' |
75 | -common_function_calls = 10 |
76 | +remote_neo4j = |
77 | +# Port on which to serve Sextant Web |
78 | +#port = 2905 |
79 | +# Number of calls incoming before we consider a function to be 'common'. |
80 | +# Defaults to 10. |
81 | +#common_function_calls = 10 |
82 | +# Whether to connect using SSH to the remote server. Warning: if remote_neo4j |
83 | +# is some kind of localhost, we won't be able to SSH. Default is True (that is, |
84 | +# automatically SSH). |
85 | +#use_ssh_tunnel = True |
86 | +# Which username to use on the remote SSH server. Default, or empty, is your |
87 | +# currently-logged-in username. |
88 | +#ssh_username = |
89 | |
90 | === modified file 'setup.py' |
91 | --- setup.py 2014-08-18 08:04:35 +0000 |
92 | +++ setup.py 2014-08-29 14:18:31 +0000 |
93 | @@ -18,7 +18,7 @@ |
94 | packages=['sextant', 'sextant.web', 'resources', 'etc'], |
95 | package_dir={'sextant': 'src/sextant', 'resources': 'resources', 'etc': 'etc'}, |
96 | scripts=['bin/sextant'], |
97 | - install_requires=['neo4jrestclient', 'twisted'], |
98 | + install_requires=['twisted'], |
99 | package_data={'resources': ['sextant/web/*'], 'etc': ['*.conf']}, |
100 | ) |
101 | |
102 | |
103 | === modified file 'src/sextant/__main__.py' |
104 | --- src/sextant/__main__.py 2014-08-26 11:04:04 +0000 |
105 | +++ src/sextant/__main__.py 2014-08-29 14:18:31 +0000 |
106 | @@ -7,223 +7,123 @@ |
107 | |
108 | from __future__ import absolute_import, print_function |
109 | |
110 | -import argparse |
111 | -try: |
112 | - import ConfigParser |
113 | -except ImportError: |
114 | - import configparser as ConfigParser |
115 | +import sys |
116 | +import random |
117 | +import socket |
118 | import logging |
119 | import logging.config |
120 | -import os |
121 | +import argparse |
122 | import requests |
123 | -import sys |
124 | - |
125 | -from . import update_db |
126 | +import subprocess |
127 | + |
128 | +try: |
129 | + from urllib import parse |
130 | +except ImportError: # fall back to Python 2 |
131 | + import urlparse as parse |
132 | + |
133 | from . import query |
134 | from . import db_api |
135 | +from . import update_db |
136 | from . import environment |
137 | |
138 | - |
139 | -# @@@ Logging level should be configurable (with some structure to setting up |
140 | -# logging). |
141 | -logging.config.dictConfig({ |
142 | - "version": 1, |
143 | - "handlers": { |
144 | - "console": { |
145 | - "class": "logging.StreamHandler", |
146 | - "level": logging.INFO, |
147 | - "stream": "ext://sys.stderr", |
148 | - }, |
149 | - }, |
150 | - "root": { |
151 | - "level": logging.DEBUG, |
152 | - "handlers": ["console"], |
153 | - }, |
154 | -}) |
155 | -log = logging.getLogger() |
156 | - |
157 | - |
158 | -def get_config_file(): |
159 | - # get the config file option for neo4j server location and web port number |
160 | - _ROOT = os.path.abspath(os.path.dirname(__file__)) |
161 | - |
162 | - def get_data(path, file_name): |
163 | - return os.path.join(_ROOT, path, file_name) |
164 | - |
165 | - home_config = environment.HOME_CONFIG_FILE |
166 | - env_config = os.environ.get(environment.ENV_CONFIG_FILE, "") |
167 | - example_config = environment.DEFAULT_CONFIG_FILE |
168 | - |
169 | - try: |
170 | - conffile = next(p |
171 | - for p in (home_config, env_config, example_config) |
172 | - if os.path.exists(p)) |
173 | - except StopIteration: |
174 | - #No config files accessable |
175 | - if environment.ENV_CONFIG_FILE in os.environ: |
176 | - #SEXTANT_CONFIG environment variable is set |
177 | - log.error("{} file %r doesn't exist.".format(environment.ENV_CONFIG_FILE), env_config) |
178 | - log.error("Sextant requires a configuration file.") |
179 | - sys.exit(1) |
180 | - |
181 | - log.info("Sextant is using config file {}".format(conffile)) |
182 | - return conffile |
183 | - |
184 | -conffile = get_config_file() |
185 | - |
186 | -conf = ConfigParser.ConfigParser() |
187 | -conf.read(conffile) |
188 | - |
189 | -remote_neo4j = 'http://localhost:7474' |
190 | -web_port = 2905 |
191 | -common_def = 10 # definition of a 'common' node |
192 | -try: |
193 | - options = conf.options('Neo4j') |
194 | -except ConfigParser.NoSectionError: |
195 | - pass |
196 | -else: |
197 | - try: |
198 | - remote_neo4j = conf.get('Neo4j', 'remote_neo4j') |
199 | - except ConfigParser.NoOptionError: |
200 | - pass |
201 | - |
202 | - try: |
203 | - web_port = conf.get('Neo4j', 'port') |
204 | - except ConfigParser.NoOptionError: |
205 | - pass |
206 | - |
207 | - try: |
208 | - common_def = conf.get('Neo4j', 'common_function_calls') |
209 | - except ConfigParser.NoOptionError: |
210 | - common_def = 10 |
211 | - |
212 | -argumentparser = argparse.ArgumentParser(description="Invoke part of the SEXTANT program") |
213 | -subparsers = argumentparser.add_subparsers(title="subcommands") |
214 | - |
215 | -#set what will be defined as a "common function" |
216 | -db_api.set_common_cutoff(common_def) |
217 | - |
218 | -parsers = dict() |
219 | - |
220 | -# add each subparser in turn to the parsers dictionary |
221 | - |
222 | -parsers['add'] = subparsers.add_parser('add-program', |
223 | - help="add a program to the database") |
224 | -parsers['add'].add_argument('input_file', metavar="FILE_NAME", |
225 | - help="name of file to be put into database", |
226 | - type=str) |
227 | -parsers['add'].add_argument('--name-in-db', metavar="PROGRAM_NAME", |
228 | - help="string to store this program under", type=str, |
229 | - nargs=1) |
230 | -parsers['add'].add_argument('--not-object-file', |
231 | - help='default False, if the input file is an ' |
232 | - 'object to be disassembled', |
233 | - action='store_true') |
234 | - |
235 | -parsers['delete'] = subparsers.add_parser('delete-program', |
236 | - help="delete a program from the database") |
237 | -parsers['delete'].add_argument('program_name', metavar="PROG_NAME", |
238 | - help="name of program as stored in the database", |
239 | - type=str) |
240 | - |
241 | -parsers['query'] = subparsers.add_parser('query', |
242 | - help="make a query of the database") |
243 | -parsers['query'].add_argument('query', metavar="QUERY", |
244 | - help="functions-calling, functions-called-by, " |
245 | - "all-call-paths, whole-program, " |
246 | - "shortest-call-path, programs or " |
247 | - "functions; if the latter, " |
248 | - "supply argument --program", |
249 | - type=str) |
250 | -parsers['query'].add_argument('--program', metavar="PROG_NAME", |
251 | - help="name of program as stored in the database; " |
252 | - "required for all queries except 'programs'", |
253 | - type=str, nargs=1) |
254 | -parsers['query'].add_argument('--funcs', metavar='FUNCS', |
255 | - help='functions to pass to the query', |
256 | - type=str, nargs='+') |
257 | -parsers['query'].add_argument('--suppress-common', metavar='BOOL', |
258 | - help='suppress commonly called functions (True or False)', |
259 | - type=str, nargs=1) |
260 | - |
261 | -parsers['web'] = subparsers.add_parser('web', help="start the web server") |
262 | -parsers['web'].add_argument('--port', metavar='PORT', type=int, |
263 | - help='port on which to serve Sextant Web', |
264 | - default=web_port) |
265 | - |
266 | -parsers['audit'] = subparsers.add_parser('audit', help='view usage of Sextant') |
267 | - |
268 | -for parser_key in parsers: |
269 | - parsers[parser_key].add_argument('--remote-neo4j', metavar="URL", |
270 | - help="URL of neo4j server", type=str, |
271 | - default=remote_neo4j) |
272 | +config = environment.load_config() |
273 | + |
274 | + |
275 | +def _displayable_url(args): |
276 | + """ |
277 | + Return the URL specified by the user for Sextant to look at. |
278 | + |
279 | + This is needed because we may be using SSH (that is, Sextant sees a |
280 | + localhost port for the Neo4j server, which ssh is forwarding somewhere |
281 | + else); in this case, we want to display the URL that the user thinks |
282 | + Sextant is using, rather than the URL it's actually using. |
283 | + |
284 | + :param args: the argparse object Sextant was invoked with |
285 | + :return: the URL the Sextant invoker expects Sextant to be using |
286 | + |
287 | + """ |
288 | + try: |
289 | + if args.display_neo4j: |
290 | + return args.display_neo4j |
291 | + except AttributeError: |
292 | + return args.remote_neo4j |
293 | + |
294 | + return args.remote_neo4j |
295 | + |
296 | + |
297 | +# Beginning of functions which handle the actual invocation of Sextant |
298 | + |
299 | +def _start_web(args): |
300 | + # Don't import at top level - makes twisted dependency semi-optional, |
301 | + # allowing non-web functionality to work with Python 3. |
302 | + if sys.version_info[0] == 2: |
303 | + from .web import server |
304 | + else: # twisted won't be available - Python 2 required |
305 | + logging.error('Web server must be run on Python 2.') |
306 | + return |
307 | + logging.info("Serving site on port {}".format(args.port)) |
308 | + |
309 | + # server is .web.server, imported a couple of lines ago |
310 | + server.serve_site(input_database_url=args.remote_neo4j, port=args.port) |
311 | + |
312 | |
313 | def _audit(args): |
314 | try: |
315 | audited = query.audit(args.remote_neo4j) |
316 | except requests.exceptions.ConnectionError as e: |
317 | - logging.error('Connection error to server {}: {}'.format(args.remote_neo4j, e)) |
318 | + msg = 'Connection error to server {url}: {exception}' |
319 | + logging.error(msg.format(url=_displayable_url(args)), exception=e) |
320 | |
321 | if not audited: |
322 | - print('No programs on database at {}.'.format(args.remote_neo4j)) |
323 | + location = _displayable_url(args) |
324 | + logging.warning('No programs on database at {}.'.format(location)) |
325 | else: |
326 | for program in audited: |
327 | - st = 'Program {} with {} functions uploaded by {} (id {}) on {}.' |
328 | - print(st.format(program.program_name, program.number, |
329 | - program.uploader, program.uploader_id, |
330 | - program.date)) |
331 | - |
332 | -parsers['audit'].set_defaults(func=_audit) |
333 | - |
334 | -def _start_web(args): |
335 | - # Don't import at top level -- this makes twisted dependency semi-optional, |
336 | - # allowing non-web functionality to work with Python 3. |
337 | - try: |
338 | - from .web import server |
339 | - except ImportError as e: # twisted wasn't available - Python 2 |
340 | - log.error('Web server must be run on Python 2.') |
341 | - log.error(e) |
342 | - return |
343 | - log.info("Serving site on port %s", args.port) |
344 | - server.serve_site(input_database_url=args.remote_neo4j, port=args.port) |
345 | - |
346 | -parsers['web'].set_defaults(func=_start_web) |
347 | - |
348 | -def add_file(namespace): |
349 | - try: |
350 | - alternative_name = namespace.name_in_db[0] |
351 | + st = ('Program {progname} with {numfuncs} functions ' |
352 | + 'uploaded by {uploader} (id {uploaderid}) on {date}.') |
353 | + print(st.format(progname=program.program_name, |
354 | + numfuncs=program.number, |
355 | + uploader=program.uploader, |
356 | + uploaderid=program.uploader_id, |
357 | + date=program.date)) |
358 | + |
359 | + |
360 | +def _add_program(args): |
361 | + try: |
362 | + alternative_name = args.name_in_db[0] |
363 | except TypeError: |
364 | alternative_name = None |
365 | |
366 | - not_object_file = namespace.not_object_file |
367 | + not_object_file = args.not_object_file |
368 | # the default is "yes, this is an object file" if not-object-file was |
369 | - # unsupplied= |
370 | + # unsupplied |
371 | + |
372 | try: |
373 | - update_db.upload_program(namespace.input_file[0], |
374 | - namespace.remote_neo4j, |
375 | - alternative_name, |
376 | - not_object_file) |
377 | + update_db.upload_program(file_path=args.input_file, |
378 | + db_url=args.remote_neo4j, |
379 | + alternative_name=alternative_name, |
380 | + not_object_file=not_object_file, |
381 | + display_url=_displayable_url(args)) |
382 | except requests.exceptions.ConnectionError as e: |
383 | - logging.error('Connection error to server {}: {}'.format(namespace.remote_neo4j, |
384 | - e)) |
385 | + msg = 'Connection error to server {}: {}' |
386 | + logging.error(msg.format(_displayable_url(args), e)) |
387 | |
388 | except IOError as e: |
389 | # in case of Python 2, where FileNotFoundError is undefined |
390 | - logging.error('Input file {} was not found.'.format(namespace.input_file[0])) |
391 | + # note: ConnectionError subclasses IOError, so must come first |
392 | + logging.error('Input file {} was not found.'.format(args.input_file[0])) |
393 | logging.error(e) |
394 | logging.debug(e, exc_info=True) |
395 | except ValueError as e: |
396 | logging.error(e) |
397 | |
398 | -def delete_file(namespace): |
399 | + |
400 | +def _delete_program(namespace): |
401 | update_db.delete_program(namespace.program_name, |
402 | namespace.remote_neo4j) |
403 | |
404 | -parsers['add'].set_defaults(func=add_file) |
405 | -parsers['delete'].set_defaults(func=delete_file) |
406 | - |
407 | - |
408 | -def make_query(namespace): |
409 | + |
410 | +def _make_query(namespace): |
411 | arg1 = None |
412 | arg2 = None |
413 | try: |
414 | @@ -245,17 +145,276 @@ |
415 | suppress_common = False |
416 | |
417 | query.query(remote_neo4j=namespace.remote_neo4j, |
418 | + display_neo4j=_displayable_url(namespace), |
419 | input_query=namespace.query, |
420 | - program_name=program_name, argument_1=arg1, argument_2=arg2, |
421 | + program_name=program_name, |
422 | + argument_1=arg1, argument_2=arg2, |
423 | suppress_common=suppress_common) |
424 | |
425 | -parsers['query'].set_defaults(func=make_query) |
426 | - |
427 | -# parse the arguments |
428 | - |
429 | -parsed = argumentparser.parse_args() |
430 | - |
431 | -# call the appropriate function |
432 | - |
433 | -parsed.func(parsed) |
434 | +# End of functions which invoke Sextant |
435 | + |
436 | +def parse_arguments(): |
437 | + """ |
438 | + Parses the command-line arguments to Sextant. |
439 | + |
440 | + The resulting object, result, has a .func property, which is a method to be |
441 | + called with result as its only parameter. This .func method runs whichever |
442 | + of Sextant's functionality is appropriate. |
443 | + :return: namespace summarising the arguments |
444 | + |
445 | + """ |
446 | + |
447 | + argumentparser = argparse.ArgumentParser(description="Invoke part of the SEXTANT program") |
448 | + subparsers = argumentparser.add_subparsers(title="subcommands") |
449 | + |
450 | + #set what will be defined as a "common function" |
451 | + db_api.set_common_cutoff(config.common_cutoff) |
452 | + |
453 | + parsers = dict() |
454 | + |
455 | + # add each subparser in turn to the parsers dictionary |
456 | + |
457 | + parsers['add'] = subparsers.add_parser('add-program', |
458 | + help="add a program to the database") |
459 | + parsers['add'].add_argument('input_file', metavar="FILE_NAME", |
460 | + help="name of file to be put into database", |
461 | + type=str) |
462 | + parsers['add'].add_argument('--name-in-db', metavar="PROGRAM_NAME", |
463 | + help="string to store this program under", type=str, |
464 | + nargs=1) |
465 | + parsers['add'].add_argument('--not-object-file', |
466 | + help='default False, if the input file is an ' |
467 | + 'object to be disassembled', |
468 | + action='store_true') |
469 | + |
470 | + parsers['delete'] = subparsers.add_parser('delete-program', |
471 | + help="delete a program from the database") |
472 | + parsers['delete'].add_argument('program_name', metavar="PROG_NAME", |
473 | + help="name of program as stored in the database", |
474 | + type=str) |
475 | + |
476 | + parsers['query'] = subparsers.add_parser('query', |
477 | + help="make a query of the database") |
478 | + parsers['query'].add_argument('query', metavar="QUERY", |
479 | + help="functions-calling, functions-called-by, " |
480 | + "all-call-paths, whole-program, " |
481 | + "shortest-call-path, programs or " |
482 | + "functions; if the latter, " |
483 | + "supply argument --program", |
484 | + type=str) |
485 | + parsers['query'].add_argument('--program', metavar="PROG_NAME", |
486 | + help="name of program as stored in the database; " |
487 | + "required for all queries except 'programs'", |
488 | + type=str, nargs=1) |
489 | + parsers['query'].add_argument('--funcs', metavar='FUNCS', |
490 | + help='functions to pass to the query', |
491 | + type=str, nargs='+') |
492 | + parsers['query'].add_argument('--suppress-common', metavar='BOOL', |
493 | + help='suppress commonly called functions (True or False)', |
494 | + type=str, nargs=1) |
495 | + |
496 | + parsers['web'] = subparsers.add_parser('web', help="start the web server") |
497 | + parsers['web'].add_argument('--port', metavar='PORT', type=int, |
498 | + help='port on which to serve Sextant Web', |
499 | + default=config.web_port) |
500 | + |
501 | + parsers['audit'] = subparsers.add_parser('audit', help='view usage of Sextant') |
502 | + |
503 | + for key in parsers: |
504 | + parsers[key].add_argument('--remote-neo4j', metavar="URL", |
505 | + help="URL of neo4j server", type=str, |
506 | + default=config.remote_neo4j) |
507 | + parsers[key].add_argument('--use-ssh-tunnel', metavar="BOOL", type=str, |
508 | + help="whether to SSH into the remote server," |
509 | + "True/False", |
510 | + default=str(config.use_ssh_tunnel)) |
511 | + |
512 | + parsers['audit'].set_defaults(func=_audit) |
513 | + parsers['web'].set_defaults(func=_start_web) |
514 | + parsers['add'].set_defaults(func=_add_program) |
515 | + parsers['delete'].set_defaults(func=_delete_program) |
516 | + parsers['query'].set_defaults(func=_make_query) |
517 | + |
518 | + # parse the arguments |
519 | + |
520 | + return argumentparser.parse_args() |
521 | + |
522 | + |
523 | +def _start_tunnel(local_port, remote_host, remote_port, ssh_user=''): |
524 | + """ |
525 | + Creates an SSH port-forward. |
526 | + |
527 | + This will result in localhost:local_port appearing to be |
528 | + remote_host:remote_port. |
529 | + |
530 | + :param local_port: integer port number to open at localhost |
531 | + :param remote_host: string address of remote host (no port number) |
532 | + :param remote_port: port to 'open' on the remote host |
533 | + :param ssh_user: user to log in on the remote_host as |
534 | + |
535 | + """ |
536 | + |
537 | + if not (isinstance(local_port, int) and local_port > 0): |
538 | + raise ValueError( |
539 | + 'Local port {} must be a positive integer.'.format(local_port)) |
540 | + if not (isinstance(remote_port, int) and remote_port > 0): |
541 | + raise ValueError( |
542 | + 'Remote port {} must be a positive integer.'.format(remote_port)) |
543 | + |
544 | + logging.debug('Starting SSH tunnel...') |
545 | + |
546 | + # this cmd string will be .format()ed in a few lines' time |
547 | + cmd = ['ssh'] |
548 | + |
549 | + if ssh_user: |
550 | + # ssh -l {user} ... sets the remote login username |
551 | + cmd += ['-l', ssh_user] |
552 | + |
553 | + # -L localport:localhost:remoteport forwards the port |
554 | + # -M makes SSH able to accept slave connections |
555 | + # -S sets the location of a control socket (in this case, sextant-controller |
556 | + # with a unique identifier appended, just in case we run sextant twice |
557 | + # simultaneously), so we know how to close the port again |
558 | + # -f goes into background; -N does not execute a remote command; |
559 | + # -T says to remote host that we don't want a text shell. |
560 | + cmd += ['-M', |
561 | + '-S', 'sextantcontroller{tunnel_id}'.format(tunnel_id=local_port), |
562 | + '-fNT', |
563 | + '-L', '{0}:localhost:{1}'.format(local_port, remote_port), |
564 | + remote_host] |
565 | + |
566 | + logging.debug('Running {}'.format(' '.join(cmd))) |
567 | + |
568 | + exit_code = subprocess.call(cmd) |
569 | + if exit_code: |
570 | + raise OSError('SSH setup failed with error {}'.format(exit_code)) |
571 | + |
572 | + logging.debug('SSH tunnel created.') |
573 | + |
574 | + |
575 | +def _stop_tunnel(local_port, remote_host): |
576 | + """ |
577 | + Tear down an SSH port-forward which was previously set up with start_tunnel. |
578 | + |
579 | + We use local_port as an identifier. |
580 | + :param local_port: the port on localhost we are using as the entrypoint |
581 | + :param remote_host: remote host we tunnelled into |
582 | + |
583 | + """ |
584 | + |
585 | + logging.debug('Shutting down SSH tunnel...') |
586 | + |
587 | + # ssh -O sends a command to the slave specified in -S |
588 | + cmd = ['ssh', |
589 | + '-S', 'sextantcontroller{}'.format(local_port), |
590 | + '-O', 'exit', |
591 | + remote_host] |
592 | + |
593 | + return_code = subprocess.call(cmd) |
594 | + if return_code == 0: |
595 | + logging.debug('Shut down successfully.') |
596 | + else: |
597 | + logging.warning( |
598 | + 'SSH tunnel shutdown returned error code {}'.format(return_code)) |
599 | + logging.warning( |
600 | + 'You may need to close sextantcontroller{} manually.'.format(local_port)) |
601 | + |
602 | + |
603 | +def _is_port_used(port): |
604 | + """ |
605 | + Checks with the OS to see whether a port is open. |
606 | + |
607 | + Beware: port is passed directly to the shell. Make sure it is an integer. |
608 | + We raise ValueError if it is not. |
609 | + :param port: integer port to check for openness |
610 | + :return: bool(port is in use) |
611 | + |
612 | + """ |
613 | + |
614 | + # we follow http://stackoverflow.com/questions/2838244/get-open-tcp-port-in-python |
615 | + if not (isinstance(port, int) and port > 0): |
616 | + raise ValueError('port {} must be a positive integer.'.format(port)) |
617 | + |
618 | + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
619 | + try: |
620 | + sock.bind(('127.0.0.1', port)) |
621 | + except socket.error as e: |
622 | + if e.errno == 98: # Address already in use |
623 | + return True |
624 | + raise |
625 | + |
626 | + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
627 | + |
628 | + return False # that is, the port is not used |
629 | + |
630 | + |
631 | +def _get_unused_port(): |
632 | + """ |
633 | + Returns a port number between 10000 and 50000 which is not currently open. |
634 | + |
635 | + """ |
636 | + |
637 | + keep_going = True |
638 | + while keep_going: |
639 | + portnum = random.randint(10000, 50000) |
640 | + keep_going = _is_port_used(portnum) |
641 | + return portnum |
642 | + |
643 | + |
644 | +def _get_host_and_port(url): |
645 | + """Given a URL as http://host:port, returns (host, port).""" |
646 | + parsed = parse.urlparse(url) |
647 | + return (parsed.hostname, parsed.port) |
648 | + |
649 | + |
650 | +def _is_localhost(host, port): |
651 | + """Checks whether a host is an alias to localhost.""" |
652 | + return socket.getaddrinfo(host, port)[0][4][0] in ('127.0.0.1', '::1') |
653 | + |
654 | + |
655 | +def main(): |
656 | + args = parse_arguments() |
657 | + |
658 | + if args.use_ssh_tunnel.lower() == 'true': |
659 | + localport = _get_unused_port() |
660 | + |
661 | + remotehost, remoteport = _get_host_and_port(args.remote_neo4j) |
662 | + |
663 | + if _is_localhost(remotehost, remoteport): |
664 | + # we are attempting to connect to localhost anyway, so we won't |
665 | + # bother to SSH to it. |
666 | + # There may be some ways the user can trick us into trying to SSH |
667 | + # to localhost anyway, but this will do as a first pass. |
668 | + # SSHing to localhost is undesirable because on my test computer, |
669 | + # we get 'connection refused' if we try. |
670 | + args.func(args) |
671 | + |
672 | + else: # we need to SSH |
673 | + try: |
674 | + _start_tunnel(localport, remotehost, remoteport, |
675 | + ssh_user=config.ssh_user) |
676 | + except OSError as e: |
677 | + logging.error(str(e)) |
678 | + return |
679 | + |
680 | + try: |
681 | + args.display_neo4j = args.remote_neo4j |
682 | + args.remote_neo4j = 'http://localhost:{}'.format(localport) |
683 | + args.func(args) |
684 | + except KeyboardInterrupt: |
685 | + # this probably happened because we were running Sextant Web |
686 | + # and Ctrl-C'ed out of it |
687 | + logging.info('Keyboard interrupt detected. Halting.') |
688 | + pass |
689 | + |
690 | + finally: |
691 | + _stop_tunnel(localport, remotehost) |
692 | + |
693 | + else: # no need to set up the ssh, just run sextant |
694 | + args.func(args) |
695 | + |
696 | + |
697 | +if __name__ == '__main__': |
698 | + main() |
699 | + |
700 | |
701 | |
702 | === modified file 'src/sextant/environment.py' |
703 | --- src/sextant/environment.py 2014-08-21 13:03:24 +0000 |
704 | +++ src/sextant/environment.py 2014-08-29 14:18:31 +0000 |
705 | @@ -10,9 +10,17 @@ |
706 | """ |
707 | |
708 | __all__ = ("RESOURCES_DIR", "DEFAULT_CONFIG_FILE", "ENV_CONFIG_FILE", |
709 | - "HOME_CONFIG_FILE") |
710 | + "HOME_CONFIG_FILE", "load_config") |
711 | |
712 | import os |
713 | +import logging |
714 | +import logging.config |
715 | +import collections |
716 | + |
717 | +try: |
718 | + import ConfigParser |
719 | +except ImportError: |
720 | + import configparser as ConfigParser |
721 | |
722 | |
723 | RESOURCES_DIR = "" # where the resources folder is located |
724 | @@ -41,3 +49,92 @@ |
725 | RESOURCES_DIR = _find_folder('resources') |
726 | _conf_path = _find_folder('etc') |
727 | DEFAULT_CONFIG_FILE = os.path.join(_conf_path, 'sextant.conf') |
728 | + |
729 | + |
730 | +Config = collections.namedtuple('Config', ['web_port', 'remote_neo4j', |
731 | + 'common_cutoff', 'use_ssh_tunnel', |
732 | + 'ssh_user']) |
733 | + |
734 | + |
735 | +def load_config(check_here=''): |
736 | + """ |
737 | + Loads in the config file (checking in the various locations). |
738 | + |
739 | + If check_here is supplied as a location for the config file, it is used |
740 | + preferentially. Then, in order, we use HOME_CONFIG_FILE, the contents of |
741 | + the SEXTANT_CONFIG environment variable, and finally the bundled |
742 | + etc/sextant.conf if no other config file was found. |
743 | + |
744 | + :return: configuration object with properties for each config option |
745 | + """ |
746 | + |
747 | + # @@@ Logging level should be configurable (with some structure to setting up |
748 | + # logging). |
749 | + logging.config.dictConfig({ |
750 | + "version": 1, |
751 | + "handlers": { |
752 | + "console": { |
753 | + "class": "logging.StreamHandler", |
754 | + "level": logging.WARNING, |
755 | + "stream": "ext://sys.stderr", |
756 | + }, |
757 | + }, |
758 | + "root": { |
759 | + "level": logging.DEBUG, |
760 | + "handlers": ["console"], |
761 | + }, |
762 | + }) |
763 | + log = logging.getLogger() |
764 | + |
765 | + def get_config_file(check=''): |
766 | + env_config = os.environ.get(ENV_CONFIG_FILE, "") |
767 | + try: |
768 | + conffile = next(p |
769 | + for p in (check, HOME_CONFIG_FILE, |
770 | + env_config, DEFAULT_CONFIG_FILE) |
771 | + if os.path.exists(p)) |
772 | + except StopIteration: |
773 | + # No config files accessible |
774 | + if ENV_CONFIG_FILE in os.environ: |
775 | + # SEXTANT_CONFIG environment variable is set |
776 | + log.error("{} file %r doesn't exist.".format(ENV_CONFIG_FILE), env_config) |
777 | + raise Exception("Sextant requires a configuration file.") |
778 | + |
779 | + log.info("Sextant is using config file {}".format(conffile)) |
780 | + return conffile |
781 | + |
782 | + conf_file = get_config_file(check_here) |
783 | + |
784 | + conf = ConfigParser.ConfigParser({'remote_neo4j': 'http://localhost:7474', |
785 | + 'port': '2905', |
786 | + 'common_function_calls': '10', |
787 | + 'use_ssh_tunnel': 'True', |
788 | + 'ssh_username': ''}) |
789 | + # It turns out that getint, getboolean etc want the defaults to be strings. |
790 | + # When getboolean is called and the default is not a string, and the |
791 | + # default is used, it complains that it can't work with non-strings. |
792 | + # Ultimately, however, we output an object with int common_function_calls |
793 | + # and port. |
794 | + |
795 | + conf.read(conf_file) |
796 | + |
797 | + remote_neo4j = '' |
798 | + web_port = 0 |
799 | + common_def = 0 # definition of a 'common' node |
800 | + use_ssh_tunnel = True |
801 | + ssh_user = '' |
802 | + try: |
803 | + conf.options('Neo4j') |
804 | + except ConfigParser.NoSectionError: |
805 | + pass |
806 | + else: |
807 | + remote_neo4j = conf.get('Neo4j', 'remote_neo4j') |
808 | + web_port = conf.getint('Neo4j', 'port') |
809 | + common_def = conf.getint('Neo4j', 'common_function_calls') |
810 | + use_ssh_tunnel = conf.getboolean('Neo4j', 'use_ssh_tunnel') |
811 | + ssh_user = conf.get('Neo4j', 'ssh_username') |
812 | + |
813 | + return Config(remote_neo4j=remote_neo4j, web_port=web_port, |
814 | + common_cutoff=common_def, use_ssh_tunnel=use_ssh_tunnel, |
815 | + ssh_user=ssh_user) |
816 | + |
817 | |
818 | === modified file 'src/sextant/export.py' |
819 | --- src/sextant/export.py 2014-08-26 11:04:04 +0000 |
820 | +++ src/sextant/export.py 2014-08-29 14:18:31 +0000 |
821 | @@ -33,7 +33,7 @@ |
822 | return name |
823 | |
824 | @staticmethod |
825 | - def to_dot(program, suppress_common_nodes=False): |
826 | + def to_dot(program, suppress_common_nodes=False, remove_self_calls=False): |
827 | """ |
828 | Convert the program to DOT output format. |
829 | program: Nodes and edges to be outputted |
830 | |
831 | === modified file 'src/sextant/query.py' |
832 | --- src/sextant/query.py 2014-08-26 11:04:04 +0000 |
833 | +++ src/sextant/query.py 2014-08-29 14:18:31 +0000 |
834 | @@ -14,12 +14,37 @@ |
835 | from .export import ProgramConverter |
836 | |
837 | |
838 | -def query(remote_neo4j, input_query, program_name=None, argument_1=None, argument_2=None, suppress_common=False): |
839 | +def query(remote_neo4j, input_query, display_neo4j='', program_name=None, |
840 | + argument_1=None, argument_2=None, suppress_common=False): |
841 | + """ |
842 | + Run a query against the database at remote_neo4j. |
843 | + |
844 | + input_query may be in ('functions-calling', 'functions-called-by', |
845 | + 'all-call-paths', 'whole-program', 'shortest-call-path', 'functions', |
846 | + 'programs'). |
847 | + |
848 | + If we wish to display a different URL to the user than remote_neo4j, |
849 | + display_neo4j should be used for that different URL. |
850 | + |
851 | + :param remote_neo4j: location to sent HTTP requests to |
852 | + :param input_query: string as detailed above |
853 | + :param display_neo4j: Neo4j URL to display in messages to the user |
854 | + :param program_name: string name of program |
855 | + :param argument_1: string argument 1 to the query |
856 | + :param argument_2: string argument 2 to the query |
857 | + :param suppress_common: whether or not to suppress links to common functions |
858 | + |
859 | + """ |
860 | + |
861 | + if display_neo4j: |
862 | + display_url = display_neo4j |
863 | + else: |
864 | + display_url = remote_neo4j |
865 | |
866 | try: |
867 | db = db_api.SextantConnection(remote_neo4j) |
868 | except requests.exceptions.ConnectionError as err: |
869 | - logging.error("Could not connect to Neo4J server {}. Are you sure it is running?".format(remote_neo4j)) |
870 | + logging.error("Could not connect to Neo4J server {}. Are you sure it is running?".format(display_url)) |
871 | logging.error(str(err)) |
872 | return 2 |
873 | #Not supported in python 2 |
874 | @@ -38,50 +63,51 @@ |
875 | remove_self_calls = False |
876 | |
877 | if input_query == 'functions-calling': |
878 | - if argument_1 == None: |
879 | + if argument_1 is None: |
880 | print('Supply one function name to functions-calling.') |
881 | return 1 |
882 | prog = db.get_all_functions_calling(program_name, argument_1) |
883 | elif input_query == 'functions-called-by': |
884 | - if argument_1 == None: |
885 | + if argument_1 is None: |
886 | print('Supply one function name to functions-called-by.') |
887 | return 1 |
888 | prog = db.get_all_functions_called(program_name, argument_1) |
889 | elif input_query == 'all-call-paths': |
890 | - if (argument_1 == None and argument_2 == None): |
891 | + if argument_1 is None and argument_2 is None: |
892 | print('Supply two function names to calls-between.') |
893 | return 1 |
894 | prog = db.get_call_paths(program_name, argument_1, argument_2) |
895 | elif input_query == 'whole-program': |
896 | prog = db.get_whole_program(program_name) |
897 | elif input_query == 'shortest-call-path': |
898 | - if argument_1 == None and argument_2 == None: |
899 | + if argument_1 is None and argument_2 is None: |
900 | print('Supply two function names to shortest-path.') |
901 | return 1 |
902 | prog = db.get_shortest_path_between_functions(program_name, argument_1, argument_2) |
903 | elif input_query == 'functions': |
904 | - if program_name != None: |
905 | + if program_name is not None: |
906 | func_names = db.get_function_names(program_name) |
907 | if func_names: |
908 | names_list = list(func_names) |
909 | else: |
910 | - print('No functions were found in program %s on server %s.' % (program_name, remote_neo4j)) |
911 | + print('No functions were found in program %s on server %s.' % (program_name, display_url)) |
912 | else: |
913 | list_of_programs = db.get_program_names() |
914 | if not list_of_programs: |
915 | - print('Server %s database empty.' % (remote_neo4j)) |
916 | + print('Server %s database empty.' % (display_url)) |
917 | return 0 |
918 | - func_list = [] |
919 | - for prog_name in list_of_programs: |
920 | - func_list += db.get_function_names(prog_name) |
921 | + |
922 | + func_list = [db.get_function_names(prog_name) |
923 | + for prog_name in list_of_programs] |
924 | + |
925 | if not func_list: |
926 | - print('Server %s contains no functions.' % (remote_neo4j)) |
927 | + print('Server %s contains no functions.' % (display_url)) |
928 | else: |
929 | names_list = func_list |
930 | elif input_query == 'programs': |
931 | list_found = list(db.get_program_names()) |
932 | if not list_found: |
933 | - print('No programs were found on server {}.'.format(remote_neo4j)) |
934 | + print('No programs were found on server {}.'.format(display_url)) |
935 | else: |
936 | names_list = list_found |
937 | else: |
938 | @@ -100,6 +126,3 @@ |
939 | db = db_api.SextantConnection(remote_neo4j) |
940 | |
941 | return db.programs_with_metadata() |
942 | - |
943 | -if __name__ == '__main__': |
944 | - main() |
945 | |
946 | === modified file 'src/sextant/update_db.py' |
947 | --- src/sextant/update_db.py 2014-08-21 16:08:10 +0000 |
948 | +++ src/sextant/update_db.py 2014-08-29 14:18:31 +0000 |
949 | @@ -13,21 +13,27 @@ |
950 | import logging |
951 | |
952 | |
953 | -def upload_program(file_path, db_url, |
954 | +def upload_program(file_path, db_url, display_url='', |
955 | alternative_name=None, |
956 | not_object_file=False): |
957 | """ |
958 | Uploads a program to the remote database. |
959 | + |
960 | Raises requests.exceptions.ConnectionError if the server didn't exist. |
961 | Raises IOError if file_path doesn't correspond to a file. |
962 | Raises ValueError if the desired alternative_name (or the default, if no |
963 | alternative_name was specified) already exists in the database. |
964 | :param file_path: the path to the local file we wish to upload |
965 | :param db_url: the URL of the database (eg. http://localhost:7474) |
966 | + :param display_url: alternative URL to display instead of db_url |
967 | :param alternative_name: a name to give the program to override the default |
968 | :param object_file: bool(the file is an objdump text output file, rather than a compiled binary) |
969 | + |
970 | """ |
971 | |
972 | + if not display_url: |
973 | + display_url = db_url |
974 | + |
975 | connection = SextantConnection(db_url) |
976 | |
977 | program_names = connection.get_program_names() |
978 | @@ -61,7 +67,7 @@ |
979 | if not program_representation.add_function_call(obj.name, called[-1]): # called is a tuple (address, name) |
980 | logging.error('Validation error: {} calling {}'.format(obj.name, called[-1])) |
981 | |
982 | - logging.info('Sending {} named objects to server {}...'.format(len(parsed_objects), db_url)) |
983 | + logging.info('Sending {} named objects to server {}...'.format(len(parsed_objects), display_url)) |
984 | program_representation.commit() |
985 | logging.info('Sending complete! Exiting.') |
986 |
The major qualm I have with this is that the config file is currently parsed twice (because environment. load_config is called once in the wrapper and once in sextant.__main__) - this isn't really a problem, but it seems sub-optimal. Can anyone think of any easy fixes?