2013-03-27 16:23:03 +00:00
|
|
|
#!/usr/bin/python
|
|
|
|
#
|
|
|
|
# test_mon_config_key - Test 'ceph config-key' interface
|
|
|
|
#
|
|
|
|
# Copyright (C) 2013 Inktank
|
|
|
|
#
|
|
|
|
# This is free software; you can redistribute it and/or
|
|
|
|
# modify it under the terms of the GNU Lesser General Public
|
|
|
|
# License version 2.1, as published by the Free Software
|
|
|
|
# Foundation. See file COPYING.
|
|
|
|
#
|
|
|
|
import sys
|
|
|
|
import os
|
|
|
|
import base64
|
|
|
|
import time
|
|
|
|
import errno
|
|
|
|
import random
|
|
|
|
import subprocess
|
|
|
|
import string
|
|
|
|
import logging
|
|
|
|
import argparse
|
|
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
# Accepted Environment variables:
|
|
|
|
# CEPH_TEST_VERBOSE - be more verbose; '1' enables; '0' disables
|
|
|
|
# CEPH_TEST_DURATION - test duration in seconds
|
|
|
|
# CEPH_TEST_SEED - seed to be used during the test
|
|
|
|
#
|
|
|
|
# Accepted arguments and options (see --help):
|
|
|
|
# -v, --verbose - be more verbose
|
|
|
|
# -d, --duration SECS - test duration in seconds
|
|
|
|
# -s, --seed SEED - seed to be used during the test
|
|
|
|
#
|
|
|
|
|
|
|
|
|
|
|
|
LOG = logging.getLogger(os.path.basename(sys.argv[0].replace('.py','')))
|
|
|
|
|
2013-04-03 14:08:41 +00:00
|
|
|
SIZES = [
|
2013-03-27 16:23:03 +00:00
|
|
|
(0, 0),
|
|
|
|
(10, 0),
|
|
|
|
(25, 0),
|
|
|
|
(50, 0),
|
|
|
|
(100, 0),
|
|
|
|
(1000, 0),
|
|
|
|
(4096, 0),
|
|
|
|
(4097, -errno.EFBIG),
|
|
|
|
(8192, -errno.EFBIG)
|
|
|
|
]
|
|
|
|
|
2013-04-03 14:08:41 +00:00
|
|
|
OPS = {
|
2013-03-27 16:23:03 +00:00
|
|
|
'put':['existing','new'],
|
|
|
|
'del':['existing','enoent'],
|
|
|
|
'exists':['existing','enoent'],
|
|
|
|
'get':['existing','enoent']
|
|
|
|
}
|
|
|
|
|
2013-04-03 14:08:41 +00:00
|
|
|
CONFIG_PUT = [] #list: keys
|
|
|
|
CONFIG_DEL = [] #list: keys
|
|
|
|
CONFIG_EXISTING = {} #map: key -> size
|
2013-03-27 16:23:03 +00:00
|
|
|
|
|
|
|
def run_cmd(cmd, expects=0):
|
2013-04-03 13:56:30 +00:00
|
|
|
full_cmd = [ 'ceph', 'config-key' ] + cmd
|
|
|
|
|
|
|
|
if expects < 0:
|
|
|
|
expects = -expects
|
|
|
|
|
2013-04-03 14:09:53 +00:00
|
|
|
cmdlog = LOG.getChild('run_cmd')
|
|
|
|
cmdlog.debug('{fc}'.format(fc=' '.join(full_cmd)))
|
2013-04-03 13:56:30 +00:00
|
|
|
|
|
|
|
proc = subprocess.Popen(full_cmd,
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
stderr=subprocess.PIPE)
|
|
|
|
|
|
|
|
stdout = []
|
|
|
|
stderr = []
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
(out, err) = proc.communicate()
|
|
|
|
if out is not None:
|
2013-04-25 14:00:09 +00:00
|
|
|
stdout += str(out).split('\n')
|
2013-04-03 14:09:53 +00:00
|
|
|
cmdlog.debug('stdout: {s}'.format(s=out))
|
2013-04-03 13:56:30 +00:00
|
|
|
if err is not None:
|
2013-04-25 14:00:09 +00:00
|
|
|
stdout += str(err).split('\n')
|
2013-04-03 14:09:53 +00:00
|
|
|
cmdlog.debug('stderr: {s}'.format(s=err))
|
2013-04-03 13:56:30 +00:00
|
|
|
except ValueError:
|
|
|
|
ret = proc.wait()
|
|
|
|
break
|
|
|
|
|
|
|
|
if ret != expects:
|
2013-04-03 14:09:53 +00:00
|
|
|
cmdlog.error('cmd > {cmd}'.format(cmd=full_cmd))
|
|
|
|
cmdlog.error('expected return \'{expected}\' got \'{got}\''.format(
|
2013-04-03 13:56:30 +00:00
|
|
|
expected=expects,got=ret))
|
2013-04-03 14:09:53 +00:00
|
|
|
cmdlog.error('stdout')
|
2013-04-03 13:56:30 +00:00
|
|
|
for i in stdout:
|
2013-04-03 14:09:53 +00:00
|
|
|
cmdlog.error('{x}'.format(x=i))
|
|
|
|
cmdlog.error('stderr')
|
2013-04-03 13:56:30 +00:00
|
|
|
for i in stderr:
|
2013-04-03 14:09:53 +00:00
|
|
|
cmdlog.error('{x}'.format(x=i))
|
2013-03-27 16:23:03 +00:00
|
|
|
|
|
|
|
#end run_cmd
|
|
|
|
|
2013-04-03 14:01:49 +00:00
|
|
|
def gen_data(size, rnd):
|
2013-04-03 13:56:30 +00:00
|
|
|
chars = string.ascii_letters + string.digits
|
|
|
|
return ''.join(rnd.choice(chars) for i in range(size))
|
2013-03-27 16:23:03 +00:00
|
|
|
|
|
|
|
def gen_key(rnd):
|
2013-04-03 14:01:49 +00:00
|
|
|
return gen_data(20, rnd)
|
2013-03-27 16:23:03 +00:00
|
|
|
|
|
|
|
def gen_tmp_file_path(rnd):
|
2013-04-03 14:01:49 +00:00
|
|
|
file_name = gen_data(20, rnd)
|
2013-04-03 13:56:30 +00:00
|
|
|
file_path = os.path.join('/tmp', 'ceph-test.'+file_name)
|
|
|
|
return file_path
|
2013-03-27 16:23:03 +00:00
|
|
|
|
|
|
|
def destroy_tmp_file(fpath):
|
2013-04-03 13:56:30 +00:00
|
|
|
if os.path.exists(fpath) and os.path.isfile(fpath):
|
|
|
|
os.unlink(fpath)
|
2013-03-27 16:23:03 +00:00
|
|
|
|
|
|
|
def write_data_file(data, rnd):
|
2013-04-03 13:56:30 +00:00
|
|
|
file_path = gen_tmp_file_path(rnd)
|
2013-04-03 14:25:55 +00:00
|
|
|
data_file = open(file_path, 'wr+')
|
|
|
|
data_file.truncate()
|
|
|
|
data_file.write(data)
|
|
|
|
data_file.close()
|
2013-04-03 13:56:30 +00:00
|
|
|
return file_path
|
2013-03-27 16:23:03 +00:00
|
|
|
#end write_data_file
|
|
|
|
|
|
|
|
def choose_random_op(rnd):
|
2013-04-03 14:08:41 +00:00
|
|
|
op = rnd.choice(OPS.keys())
|
|
|
|
sop = rnd.choice(OPS[op])
|
2013-04-03 14:01:49 +00:00
|
|
|
return (op, sop)
|
2013-03-27 16:23:03 +00:00
|
|
|
|
|
|
|
|
|
|
|
def parse_args(args):
|
2013-04-03 13:56:30 +00:00
|
|
|
parser = argparse.ArgumentParser(
|
|
|
|
description='Test the monitor\'s \'config-key\' API',
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'-v', '--verbose',
|
|
|
|
action='store_true',
|
|
|
|
help='be more verbose',
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'-s', '--seed',
|
|
|
|
metavar='SEED',
|
|
|
|
help='use SEED instead of generating it in run-time',
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'-d', '--duration',
|
|
|
|
metavar='SECS',
|
|
|
|
help='run test for SECS seconds (default: 300)',
|
|
|
|
)
|
|
|
|
parser.set_defaults(
|
|
|
|
seed=None,
|
|
|
|
duration=300,
|
|
|
|
verbose=False,
|
|
|
|
)
|
|
|
|
return parser.parse_args(args)
|
2013-03-27 16:23:03 +00:00
|
|
|
|
|
|
|
def main():
|
|
|
|
|
2013-04-03 13:56:30 +00:00
|
|
|
args = parse_args(sys.argv[1:])
|
|
|
|
|
|
|
|
verbose = args.verbose
|
|
|
|
if os.environ.get('CEPH_TEST_VERBOSE') is not None:
|
|
|
|
verbose = (os.environ.get('CEPH_TEST_VERBOSE') == '1')
|
|
|
|
|
|
|
|
duration = int(os.environ.get('CEPH_TEST_DURATION', args.duration))
|
|
|
|
seed = os.environ.get('CEPH_TEST_SEED', args.seed)
|
|
|
|
seed = int(time.time()) if seed is None else int(seed)
|
|
|
|
|
|
|
|
rnd = random.Random()
|
|
|
|
rnd.seed(seed)
|
|
|
|
|
|
|
|
loglevel = logging.INFO
|
|
|
|
if verbose:
|
|
|
|
loglevel = logging.DEBUG
|
|
|
|
|
|
|
|
logging.basicConfig(level=loglevel,)
|
|
|
|
|
|
|
|
LOG.info('seed: {s}'.format(s=seed))
|
|
|
|
|
|
|
|
start = time.time()
|
|
|
|
|
|
|
|
while (time.time() - start) < duration:
|
2013-04-03 14:01:49 +00:00
|
|
|
(op, sop) = choose_random_op(rnd)
|
2013-04-03 13:56:30 +00:00
|
|
|
|
2013-04-03 14:01:49 +00:00
|
|
|
LOG.info('{o}({s})'.format(o=op, s=sop))
|
2013-04-03 14:28:14 +00:00
|
|
|
op_log = LOG.getChild('{o}({s})'.format(o=op, s=sop))
|
2013-04-03 13:56:30 +00:00
|
|
|
|
|
|
|
if op == 'put':
|
2013-04-03 14:01:49 +00:00
|
|
|
via_file = (rnd.uniform(0, 100) < 50.0)
|
2013-04-03 13:56:30 +00:00
|
|
|
|
|
|
|
expected = 0
|
|
|
|
cmd = [ 'put' ]
|
|
|
|
key = None
|
|
|
|
|
|
|
|
if sop == 'existing':
|
2013-04-03 14:08:41 +00:00
|
|
|
if len(CONFIG_EXISTING) == 0:
|
2013-04-03 14:28:14 +00:00
|
|
|
op_log.debug('no existing keys; continue')
|
2013-04-03 13:56:30 +00:00
|
|
|
continue
|
2013-04-03 14:08:41 +00:00
|
|
|
key = rnd.choice(CONFIG_PUT)
|
|
|
|
assert key in CONFIG_EXISTING, \
|
|
|
|
'key \'{k_}\' not in CONFIG_EXISTING'.format(k_=key)
|
2013-04-03 13:56:30 +00:00
|
|
|
|
|
|
|
expected = 0 # the store just overrides the value if the key exists
|
|
|
|
#end if sop == 'existing'
|
|
|
|
elif sop == 'new':
|
2013-04-03 14:01:49 +00:00
|
|
|
for x in xrange(0, 10):
|
2013-04-03 13:56:30 +00:00
|
|
|
key = gen_key(rnd)
|
2013-04-03 14:08:41 +00:00
|
|
|
if key not in CONFIG_EXISTING:
|
2013-04-03 13:56:30 +00:00
|
|
|
break
|
|
|
|
key = None
|
|
|
|
if key is None:
|
2013-04-03 14:28:14 +00:00
|
|
|
op_log.error('unable to generate an unique key -- try again later.')
|
2013-04-03 13:56:30 +00:00
|
|
|
continue
|
|
|
|
|
2013-04-03 14:08:41 +00:00
|
|
|
assert key not in CONFIG_PUT and key not in CONFIG_EXISTING, \
|
2013-04-03 13:56:30 +00:00
|
|
|
'key {k} was not supposed to exist!'.format(k=key)
|
|
|
|
|
|
|
|
assert key is not None, \
|
|
|
|
'key must be != None'
|
|
|
|
|
|
|
|
cmd += [ key ]
|
|
|
|
|
2013-04-03 14:08:41 +00:00
|
|
|
(size, error) = rnd.choice(SIZES)
|
2013-04-03 13:56:30 +00:00
|
|
|
if size > 25:
|
|
|
|
via_file = True
|
|
|
|
|
2013-04-03 14:01:49 +00:00
|
|
|
data = gen_data(size, rnd)
|
2013-04-03 13:56:30 +00:00
|
|
|
if error == 0: # only add if we expect the put to be successful
|
|
|
|
if sop == 'new':
|
2013-04-03 14:08:41 +00:00
|
|
|
CONFIG_PUT.append(key)
|
|
|
|
CONFIG_EXISTING[key] = size
|
2013-04-03 13:56:30 +00:00
|
|
|
expected = error
|
|
|
|
|
|
|
|
if via_file:
|
|
|
|
data_file = write_data_file(data, rnd)
|
|
|
|
cmd += [ '-i', data_file ]
|
|
|
|
else:
|
|
|
|
cmd += [ data ]
|
|
|
|
|
2013-04-03 14:28:14 +00:00
|
|
|
op_log.debug('size: {sz}, via: {v}'.format(
|
2013-04-03 13:56:30 +00:00
|
|
|
sz=size,
|
|
|
|
v='file: {f}'.format(f=data_file) if via_file == True else 'cli')
|
|
|
|
)
|
|
|
|
run_cmd(cmd, expects=expected)
|
|
|
|
if via_file:
|
|
|
|
destroy_tmp_file(data_file)
|
|
|
|
continue
|
2013-03-27 16:23:03 +00:00
|
|
|
|
2013-04-03 13:56:30 +00:00
|
|
|
elif op == 'del':
|
|
|
|
expected = 0
|
|
|
|
cmd = [ 'del' ]
|
|
|
|
key = None
|
|
|
|
|
|
|
|
if sop == 'existing':
|
2013-04-03 14:08:41 +00:00
|
|
|
if len(CONFIG_EXISTING) == 0:
|
2013-04-03 14:28:14 +00:00
|
|
|
op_log.debug('no existing keys; continue')
|
2013-04-03 13:56:30 +00:00
|
|
|
continue
|
2013-04-03 14:08:41 +00:00
|
|
|
key = rnd.choice(CONFIG_PUT)
|
|
|
|
assert key in CONFIG_EXISTING, \
|
|
|
|
'key \'{k_}\' not in CONFIG_EXISTING'.format(k_=key)
|
2013-04-03 13:56:30 +00:00
|
|
|
|
|
|
|
if sop == 'enoent':
|
2013-04-03 14:01:49 +00:00
|
|
|
for x in xrange(0, 10):
|
2013-04-03 13:56:30 +00:00
|
|
|
key = base64.b64encode(os.urandom(20))
|
2013-04-03 14:08:41 +00:00
|
|
|
if key not in CONFIG_EXISTING:
|
2013-04-03 13:56:30 +00:00
|
|
|
break
|
|
|
|
key = None
|
|
|
|
if key is None:
|
2013-04-03 14:28:14 +00:00
|
|
|
op_log.error('unable to generate an unique key -- try again later.')
|
2013-04-03 13:56:30 +00:00
|
|
|
continue
|
2013-04-03 14:08:41 +00:00
|
|
|
assert key not in CONFIG_PUT and key not in CONFIG_EXISTING, \
|
2013-04-03 13:56:30 +00:00
|
|
|
'key {k} was not supposed to exist!'.format(k=key)
|
|
|
|
expected = 0 # deleting a non-existent key succeeds
|
|
|
|
|
|
|
|
assert key is not None, \
|
|
|
|
'key must be != None'
|
|
|
|
|
|
|
|
cmd += [ key ]
|
2013-04-03 14:28:14 +00:00
|
|
|
op_log.debug('key: {k}'.format(k=key))
|
2013-04-03 13:56:30 +00:00
|
|
|
run_cmd(cmd, expects=expected)
|
|
|
|
if sop == 'existing':
|
2013-04-03 14:08:41 +00:00
|
|
|
CONFIG_DEL.append(key)
|
|
|
|
CONFIG_PUT.remove(key)
|
|
|
|
del CONFIG_EXISTING[key]
|
2013-04-03 13:56:30 +00:00
|
|
|
continue
|
2013-03-27 16:23:03 +00:00
|
|
|
|
2013-04-03 13:56:30 +00:00
|
|
|
elif op == 'exists':
|
|
|
|
expected = 0
|
|
|
|
cmd = [ 'exists' ]
|
|
|
|
key = None
|
|
|
|
|
|
|
|
if sop == 'existing':
|
2013-04-03 14:08:41 +00:00
|
|
|
if len(CONFIG_EXISTING) == 0:
|
2013-04-03 14:28:14 +00:00
|
|
|
op_log.debug('no existing keys; continue')
|
2013-04-03 13:56:30 +00:00
|
|
|
continue
|
2013-04-03 14:08:41 +00:00
|
|
|
key = rnd.choice(CONFIG_PUT)
|
|
|
|
assert key in CONFIG_EXISTING, \
|
|
|
|
'key \'{k_}\' not in CONFIG_EXISTING'.format(k_=key)
|
2013-04-03 13:56:30 +00:00
|
|
|
|
|
|
|
if sop == 'enoent':
|
2013-04-03 14:01:49 +00:00
|
|
|
for x in xrange(0, 10):
|
2013-04-03 13:56:30 +00:00
|
|
|
key = base64.b64encode(os.urandom(20))
|
2013-04-03 14:08:41 +00:00
|
|
|
if key not in CONFIG_EXISTING:
|
2013-04-03 13:56:30 +00:00
|
|
|
break
|
|
|
|
key = None
|
|
|
|
if key is None:
|
2013-04-03 14:28:14 +00:00
|
|
|
op_log.error('unable to generate an unique key -- try again later.')
|
2013-04-03 13:56:30 +00:00
|
|
|
continue
|
2013-04-03 14:08:41 +00:00
|
|
|
assert key not in CONFIG_PUT and key not in CONFIG_EXISTING, \
|
2013-04-03 13:56:30 +00:00
|
|
|
'key {k} was not supposed to exist!'.format(k=key)
|
|
|
|
expected = -errno.ENOENT
|
|
|
|
|
|
|
|
assert key is not None, \
|
|
|
|
'key must be != None'
|
|
|
|
|
|
|
|
cmd += [ key ]
|
2013-04-03 14:28:14 +00:00
|
|
|
op_log.debug('key: {k}'.format(k=key))
|
2013-04-03 13:56:30 +00:00
|
|
|
run_cmd(cmd, expects=expected)
|
|
|
|
continue
|
2013-03-27 16:23:03 +00:00
|
|
|
|
2013-04-03 13:56:30 +00:00
|
|
|
elif op == 'get':
|
|
|
|
expected = 0
|
|
|
|
cmd = [ 'get' ]
|
|
|
|
key = None
|
|
|
|
|
|
|
|
if sop == 'existing':
|
2013-04-03 14:08:41 +00:00
|
|
|
if len(CONFIG_EXISTING) == 0:
|
2013-04-03 14:28:14 +00:00
|
|
|
op_log.debug('no existing keys; continue')
|
2013-04-03 13:56:30 +00:00
|
|
|
continue
|
2013-04-03 14:08:41 +00:00
|
|
|
key = rnd.choice(CONFIG_PUT)
|
|
|
|
assert key in CONFIG_EXISTING, \
|
|
|
|
'key \'{k_}\' not in CONFIG_EXISTING'.format(k_=key)
|
2013-04-03 13:56:30 +00:00
|
|
|
|
|
|
|
if sop == 'enoent':
|
2013-04-03 14:01:49 +00:00
|
|
|
for x in xrange(0, 10):
|
2013-04-03 13:56:30 +00:00
|
|
|
key = base64.b64encode(os.urandom(20))
|
2013-04-03 14:08:41 +00:00
|
|
|
if key not in CONFIG_EXISTING:
|
2013-04-03 13:56:30 +00:00
|
|
|
break
|
|
|
|
key = None
|
|
|
|
if key is None:
|
2013-04-03 14:28:14 +00:00
|
|
|
op_log.error('unable to generate an unique key -- try again later.')
|
2013-04-03 13:56:30 +00:00
|
|
|
continue
|
2013-04-03 14:08:41 +00:00
|
|
|
assert key not in CONFIG_PUT and key not in CONFIG_EXISTING, \
|
2013-04-03 13:56:30 +00:00
|
|
|
'key {k} was not supposed to exist!'.format(k=key)
|
|
|
|
expected = -errno.ENOENT
|
|
|
|
|
|
|
|
assert key is not None, \
|
|
|
|
'key must be != None'
|
|
|
|
|
|
|
|
file_path = gen_tmp_file_path(rnd)
|
|
|
|
cmd += [ key, '-o', file_path ]
|
2013-04-03 14:28:14 +00:00
|
|
|
op_log.debug('key: {k}'.format(k=key))
|
2013-04-03 13:56:30 +00:00
|
|
|
run_cmd(cmd, expects=expected)
|
|
|
|
if sop == 'existing':
|
|
|
|
try:
|
2013-04-03 14:37:04 +00:00
|
|
|
temp_file = open(file_path, 'r+')
|
2013-04-03 13:56:30 +00:00
|
|
|
except IOError as err:
|
|
|
|
if err.errno == errno.ENOENT:
|
2013-04-03 14:08:41 +00:00
|
|
|
assert CONFIG_EXISTING[key] == 0, \
|
2013-04-03 13:56:30 +00:00
|
|
|
'error opening \'{fp}\': {e}'.format(fp=file_path,e=err)
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
assert False, \
|
|
|
|
'some error occurred: {e}'.format(e=err)
|
|
|
|
cnt = 0
|
|
|
|
while True:
|
2013-04-03 14:37:04 +00:00
|
|
|
read_data = temp_file.read()
|
|
|
|
if read_data == '':
|
2013-04-03 13:57:37 +00:00
|
|
|
break
|
2013-04-03 14:37:04 +00:00
|
|
|
cnt += len(read_data)
|
2013-04-03 14:08:41 +00:00
|
|
|
assert cnt == CONFIG_EXISTING[key], \
|
2013-04-03 13:56:30 +00:00
|
|
|
'wrong size from store for key \'{k}\': {sz}, expected {es}'.format(
|
2013-04-03 14:08:41 +00:00
|
|
|
k=key,sz=cnt,es=CONFIG_EXISTING[key])
|
2013-04-03 13:56:30 +00:00
|
|
|
destroy_tmp_file(file_path)
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
assert False, 'unknown op {o}'.format(o=op)
|
2013-03-27 16:23:03 +00:00
|
|
|
|
2013-04-03 14:08:41 +00:00
|
|
|
# check if all keys in 'CONFIG_PUT' exist and
|
|
|
|
# if all keys on 'CONFIG_DEL' don't.
|
|
|
|
# but first however, remove all keys in CONFIG_PUT that might
|
|
|
|
# be in CONFIG_DEL as well.
|
|
|
|
config_put_set = set(CONFIG_PUT)
|
|
|
|
config_del_set = set(CONFIG_DEL).difference(config_put_set)
|
2013-03-27 16:23:03 +00:00
|
|
|
|
2013-04-03 13:56:30 +00:00
|
|
|
LOG.info('perform sanity checks on store')
|
2013-03-27 16:23:03 +00:00
|
|
|
|
2013-04-03 13:56:30 +00:00
|
|
|
for k in config_put_set:
|
|
|
|
LOG.getChild('check(puts)').debug('key: {k_}'.format(k_=k))
|
|
|
|
run_cmd(['exists', k], expects=0)
|
|
|
|
for k in config_del_set:
|
|
|
|
LOG.getChild('check(dels)').debug('key: {k_}'.format(k_=k))
|
|
|
|
run_cmd(['exists', k], expects=-errno.ENOENT)
|
2013-03-27 16:23:03 +00:00
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2013-04-03 13:56:30 +00:00
|
|
|
main()
|