Skip to content

Commit b36cfdd

Browse files
author
victor73
committed
Added utility script.
1 parent 19bab51 commit b36cfdd

2 files changed

Lines changed: 401 additions & 1 deletion

File tree

bin/osdf

Lines changed: 397 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,397 @@
1+
#!/usr/bin/env python
2+
3+
import argparse
4+
import json
5+
from jsondiff import diff
6+
import logging
7+
import os
8+
import sys
9+
from osdf import OSDF
10+
11+
def parse_config():
12+
"""
13+
Parses the utility's configuration file, which is in INI format and
14+
returns a tuple containing the configured server/IP address, username,
15+
password, and whether SSL is required, in that order.
16+
"""
17+
home = os.path.expanduser("~")
18+
config_file = os.path.join(home, ".osdf")
19+
20+
perms = oct(os.stat(config_file).st_mode & 0777)
21+
if perms != '0400':
22+
msg = "Permissions on config {} are too loose. Should be 0400."
23+
raise Exception(msg.format(config_file))
24+
25+
import ConfigParser
26+
27+
config = ConfigParser.RawConfigParser()
28+
config.read(config_file)
29+
section = "osdf"
30+
31+
server = config.get(section, 'server')
32+
username = config.get(section, 'username')
33+
password = config.get(section, 'password')
34+
ssl = config.getboolean(section, "ssl")
35+
36+
return (server, username, password, ssl)
37+
38+
def get_client():
39+
"""
40+
Creates and retrieves an OSDF object that is used as the client for all
41+
communications with the OSDF server.
42+
"""
43+
(server, username, password, ssl) = parse_config()
44+
45+
client = OSDF(server, username, password, ssl=ssl)
46+
47+
return client
48+
49+
def init(args):
50+
"""
51+
The function that is first used to establish the utility's configuration
52+
file. We honor interrupts since this is an interactive process where we
53+
ask the user several questions.
54+
"""
55+
try:
56+
init_helper(args)
57+
except KeyboardInterrupt:
58+
print("\nAborted\n.")
59+
sys.exit(0)
60+
61+
def init_helper(args):
62+
"""
63+
Utility function called by init(). Asks the user several questions,
64+
including a password and confirmation. If the process is successful, the
65+
configuration file is created and a restrictive set of permissions applied.
66+
We also take care to notify the user if they are possibliy overwriting
67+
an existing configuration file.
68+
"""
69+
home = os.path.expanduser("~")
70+
config_file = os.path.join(home, ".osdf")
71+
72+
if os.path.isfile(config_file):
73+
replace = raw_input("You have an existing ~/.osdf file. " + \
74+
"Do you want to overwrite it?\n")
75+
if (replace.lower() == "yes" or replace.lower() == "y"):
76+
pass
77+
else:
78+
# Stop everything...
79+
sys.exit(0)
80+
81+
# Get the server
82+
server = ""
83+
while len(server) == 0:
84+
server = raw_input("What is the hostname or IP address of " + \
85+
"the OSDF server?\n")
86+
# Get the username
87+
username = ""
88+
while len(username) == 0:
89+
username = raw_input("What is your OSDF username?\n")
90+
91+
# Get the password (twice) and compare
92+
password = ""
93+
password2 = ""
94+
while len(password) == 0 or password != password2:
95+
import getpass
96+
97+
print("What is your OSDF password? (masked)")
98+
password = getpass.getpass('')
99+
100+
print("Enter the password a 2nd time (masked)")
101+
password2 = getpass.getpass('')
102+
103+
if password != password2:
104+
print("Passwords did not match. Please try again...")
105+
106+
ssl_answer = ""
107+
while len(ssl_answer) == 0:
108+
ssl_answer = raw_input("Is the OSDF server using SSL/TLS?\n")
109+
110+
if (ssl_answer.lower() == "yes" or ssl_answer.lower() == "y"):
111+
ssl = True
112+
else:
113+
ssl = False
114+
115+
if os.path.isfile(config_file):
116+
os.chmod(config_file, 0600)
117+
118+
fh = open(config_file, "w")
119+
fh.write("[osdf]\n")
120+
fh.write("server={}\n".format(server))
121+
fh.write("username={}\n".format(username))
122+
fh.write("password={}\n".format(password))
123+
fh.write("ssl={}\n".format(ssl))
124+
fh.close()
125+
126+
# Set the permissions so that only this user can read the file.
127+
os.chmod(config_file, 0400)
128+
129+
def edit(args):
130+
"""
131+
Edit a node. First, we take the provided node ID and retrieve the document
132+
from the OSDF server. Then we save it to a temporary location and invoke
133+
a text editor (honoring the EDITOR environment variable, if set) on it.
134+
We then examine the edited document, and if there are changes, and the
135+
document is well-formed and valid, we attempt to update the node in the
136+
OSDF server.
137+
"""
138+
node_id = args.node
139+
140+
import subprocess
141+
import tempfile
142+
143+
# Get the editor that the user prefers by way of the EDITOR environment
144+
# variable. If that's not available, just default to vim.
145+
editor = os.environ.get('EDITOR', 'vim')
146+
147+
try:
148+
client = get_client()
149+
data = client.get_node(node_id)
150+
json_doc = json.dumps(data, indent=2)
151+
except Exception as e:
152+
sys.stderr.write("Unable to retrieve node \"{}\".\n".format(node_id))
153+
sys.exit(1)
154+
155+
temp = tempfile.NamedTemporaryFile(suffix=".tmp")
156+
157+
temp.write(json_doc)
158+
temp.flush()
159+
subprocess.call([editor, temp.name])
160+
161+
# Okay, the editor has completed, so now we have a new document
162+
temp.seek(0)
163+
new_data = temp.read()
164+
165+
new_node = None
166+
try:
167+
new_node = json.loads(new_data)
168+
except:
169+
print("Aborted. Edited data resulted in invalid JSON.")
170+
sys.exit(2)
171+
172+
exit_value = 1
173+
difference = diff(data, new_node)
174+
175+
if difference:
176+
new_node = json.loads(new_data)
177+
(valid, error) = client.validate_node(new_node)
178+
179+
if valid:
180+
print("New data is valid.")
181+
exit_value = 0
182+
else:
183+
print("New node information is invalid: {}".format(error))
184+
else:
185+
print("No edits detected.")
186+
exit_value = 0
187+
188+
sys.exit(exit_value)
189+
190+
def info(args):
191+
"""
192+
Retrieves basic information about the OSDF server published by the API.
193+
This is sometimes useful as a check to see if the server is running or
194+
not.
195+
"""
196+
exit_value = 1
197+
198+
try:
199+
client = get_client()
200+
info = client.get_info()
201+
print(json.dumps(info, indent=2, sort_keys=True))
202+
exit_value = 0
203+
except Exception as e:
204+
sys.stderr.write("Unable to retrieve information: {}\n".format(e))
205+
206+
sys.exit(exit_value)
207+
208+
def aux_schemas(args):
209+
"""
210+
Retrieves auxiliary schema information from the OSDF server. When only the
211+
namespace is provided, all the auxiliary schemas are output. Results can
212+
be limited to a specific auxiliary schema when an additional argument
213+
specifying the name is provided.
214+
"""
215+
namespace = args.ns
216+
aux_schema = args.aux_schema
217+
218+
client = get_client()
219+
220+
if aux_schema:
221+
# Here we are retrieving a specific auxiliary schema by name
222+
try:
223+
data = client.get_aux_schema(namespace, aux_schema)
224+
except Exception as e:
225+
msg = "Unable to retrieve {} aux schema \"{}\".\n"
226+
sys.stderr.write(msg.format(namespace, aux_schema))
227+
sys.exit(1)
228+
else:
229+
# Here we are retrieving all auxiliary schemas for the namespace
230+
try:
231+
data = client.get_aux_schemas(namespace)
232+
except Exception as e:
233+
msg = "Unable to retrieve {} aux schemas. Reason: {}\n"
234+
sys.stderr.write(msg.format(namespace, e))
235+
sys.exit(1)
236+
237+
print(json.dumps(data, indent=2, sort_keys=True))
238+
239+
def schemas(args):
240+
"""
241+
Retrieves base schema information from the OSDF server. Base schemas
242+
control the structure of OSDF node types. When only the namespace is
243+
provided, all the base schemas are output, however, results can be limited
244+
to a specific schema when an additional argument specifying the name is
245+
provided.
246+
"""
247+
namespace = args.ns
248+
schema = args.schema
249+
250+
client = get_client()
251+
252+
if schema:
253+
# Here we are retrieving a specific schema by name
254+
try:
255+
data = client.get_schema(namespace, schema)
256+
except Exception as e:
257+
msg = "Unable to retrieve {} schema \"{}.\"\n"
258+
sys.stderr.write(msg.format(namespace, schema))
259+
sys.exit(1)
260+
else:
261+
# Here we are retrieving all the schemas for the namespace
262+
try:
263+
data = client.get_schemas(namespace)
264+
except Exception as e:
265+
msg = "Unable to retrieve {} schemas. Reason: {}\n"
266+
sys.stderr.write(msg.format(namespace, e))
267+
sys.exit(1)
268+
269+
print(json.dumps(data, indent=2, sort_keys=True))
270+
271+
def oql(args):
272+
"""
273+
Given an OQL (OSDF Query Language) query statement, send it to the
274+
configured OSDF server and send the results to STDOUT.
275+
"""
276+
query = args.query
277+
namespace = args.ns
278+
279+
try:
280+
client = get_client()
281+
data = client.oql_query(namespace, query)
282+
print(json.dumps(data, indent=2, sort_keys=True))
283+
except Exception as e:
284+
msg = "Unable to execute OQL \"{}\". Reason: {}\n"
285+
sys.stderr.write(msg.format(query, e))
286+
sys.exit(1)
287+
288+
def search(args):
289+
"""
290+
Given an OSDF query expressed in ElasticSearch query format, send it to the
291+
configured OSDF server and send the results to STDOUT.
292+
"""
293+
query = args.query
294+
namespace = args.ns
295+
296+
try:
297+
client = get_client()
298+
data = client.query(namespace, query)
299+
print(json.dumps(data, indent=2, sort_keys=True))
300+
except Exception as e:
301+
msg = "Unable to execute query \"{}\". Reason: {}\n"
302+
sys.stderr.write(msg.format(query, e))
303+
sys.exit(1)
304+
305+
def cat(args):
306+
"""
307+
Given a node ID, retrieve the data and dump it to STDOUT, much like the
308+
unix `cat` utility operates.
309+
"""
310+
# TODO: Honor multiple node IDs?
311+
node_id = args.node
312+
313+
try:
314+
client = get_client()
315+
data = client.get_node(node_id)
316+
print(json.dumps(data, indent=2, sort_keys=True))
317+
except Exception as e:
318+
sys.stderr.write("Unable to retrieve node \"{}\".\n".format(node_id))
319+
sys.exit(1)
320+
321+
def delete(args):
322+
"""
323+
Deletes the specified node from the OSDF server. This is a irreversible
324+
operation, so use caution.
325+
"""
326+
node_id = args.node
327+
328+
try:
329+
client = get_client()
330+
client.delete_node(node_id)
331+
except Exception as e:
332+
sys.stderr.write("Unable to delete node \"{}\".\n".format(node_id))
333+
sys.exit(1)
334+
335+
def main():
336+
# Create the top-level parser
337+
parser = argparse.ArgumentParser(prog='osdf')
338+
339+
subparsers = parser.add_subparsers(help='sub-command help')
340+
341+
# Create the parser for the "b" command
342+
parser_init = subparsers.add_parser('init', help='Initialize settings.')
343+
parser_init.set_defaults(func=init)
344+
345+
# Create the parser for the "cat" command
346+
parser_cat = subparsers.add_parser('cat', help='Dump a node to STDOUT.')
347+
parser_cat.add_argument('node', type=str, help='A node ID.')
348+
parser_cat.set_defaults(func=cat)
349+
350+
parser_info = subparsers.add_parser('info',
351+
help='Display information about the OSDF server.')
352+
parser_info.set_defaults(func=info)
353+
354+
# Create the parser for the node deletion command
355+
parser_del = subparsers.add_parser('rm', help='Delete a node.')
356+
parser_del.add_argument('node', type=str, help='The node ID to delete.')
357+
parser_del.set_defaults(func=delete)
358+
359+
# Create the parser for OQL (OSDF Query Language) querying.
360+
parser_oql = subparsers.add_parser('oql', help='Perform an OQL query.')
361+
parser_oql.add_argument('ns', type=str, help='The OSDF namespace to search.')
362+
parser_oql.add_argument('query', type=str, help='The OQL query statement.')
363+
parser_oql.set_defaults(func=oql)
364+
365+
# Create the parser for ES (ElasticSearch Query DSL) querying.
366+
parser_dsl = subparsers.add_parser('search',
367+
help='Perform an ElasticSearch DSL query.')
368+
parser_dsl.add_argument('ns', type=str, help='The OSDF namespace to search.')
369+
parser_dsl.add_argument('query', type=str, help='The query statement.')
370+
parser_dsl.set_defaults(func=search)
371+
372+
# Create the parser for schema retrieval.
373+
parser_schemas = subparsers.add_parser('schemas',
374+
help='Retrieve the schemas for a namespace.')
375+
parser_schemas.add_argument('ns', type=str, help='The OSDF namespace.')
376+
parser_schemas.add_argument('schema', nargs='?', type=str,
377+
help='A specific schema to retrieve.')
378+
parser_schemas.set_defaults(func=schemas)
379+
380+
# Create the parser for auxiliary schema retrieval.
381+
parser_aux = subparsers.add_parser('aux',
382+
help='Retrieve the auxiliary schemas for a namespace.')
383+
parser_aux.add_argument('ns', type=str, help='The OSDF namespace.')
384+
parser_aux.add_argument('aux_schema', nargs='?', type=str,
385+
help='A specific auxiliary schema to retrieve.')
386+
parser_aux.set_defaults(func=aux_schemas)
387+
388+
# Create the parser for the "edit" command
389+
parser_edit = subparsers.add_parser('edit', help='Edit a node.')
390+
parser_edit.add_argument('node', type=str, help='The node ID to edit.')
391+
parser_edit.set_defaults(func=edit)
392+
393+
# parse the args and call whatever function was selected
394+
args = parser.parse_args()
395+
args.func(args)
396+
397+
main()

0 commit comments

Comments
 (0)