isc.util.process.rename()
-VERSION_STRING = 'b10-cmdctl-usermgr @PACKAGE_VERSION@'
-DEFAULT_FILE = SYSCONFPATH + '/cmdctl-accounts.csv'
+VERSION_STRING = "b10-cmdctl-usermgr @PACKAGE_VERSION@"
+DEFAULT_FILE = SYSCONFPATH + "/cmdctl-accounts.csv"
+
+ACTION_ADD = "add"
+ACTION_DELETE = "delete"
+
+# Non-zero return codes, used in tests
+BAD_ARGUMENTS = 1
+FILE_ERROR = 2
+USER_EXISTS = 3
+USER_DOES_NOT_EXIST = 4
class UserManager:
- def __init__(self, options):
+ def __init__(self, options, args):
self.options = options
+ self.args = args
def print(self, msg):
if not self.options.quiet:
Add the given username/password combination to the file.
First checks if the username exists, and returns False if so.
If not, it is added, and this method returns True.
+ (if it cannot write to the file it raises an exception)
"""
if self.username_exists(name):
return False
self.save_userinfo(name, pw, salt, self.options.output_file)
return True
- def interactive_mode(self):
+ def delete_user(self, name):
+ if not self.username_exists(name):
+ return False
+ rows = []
+ with open(self.options.output_file) as csvfile:
+ reader = csv.reader(csvfile)
+ for row in reader:
+ if row[0] != name:
+ rows.append(row)
+ with open(self.options.output_file, 'w') as csvfile:
+ writer = csv.writer(csvfile)
+ writer.writerows(rows)
+ return True
+
+ def prompt_for_username(self, action):
while True :
- name = input("Username to add: ")
- if name == '':
- print("error, username can't be empty")
+ name = input("Username to " + action + ": ")
+ if name == "":
+ # Note, direct print here is intentional
+ print("Error username can't be empty")
continue
- if self.username_exists(name):
- print("user already exists!")
+ if action == ACTION_ADD and self.username_exists(name):
+ print("user already exists")
+ continue
+ elif action == ACTION_DELETE and not self.username_exists(name):
+ print("user does not exist")
continue
- while True:
- pwd1 = getpass.getpass("Choose a password: ")
- pwd2 = getpass.getpass("Re-enter password: ")
- if pwd1 != pwd2:
- print("passwords do not match, try again")
- else:
- break;
+ return name
- if not self.add_user(name, pwd1):
- self.print("Error: username exists")
+ def prompt_for_password(self):
+ while True:
+ pwd1 = getpass.getpass("Choose a password: ")
+ if pwd1 == "":
+ # Note, direct print here is intentional
+ print("Error: password cannot be empty")
+ continue
+ pwd2 = getpass.getpass("Re-enter password: ")
+ if pwd1 != pwd2:
+ print("passwords do not match, try again")
+ continue
+ return pwd1
- inputdata = input('Add another user (y/n)? ')
- if inputdata not in ['y', 'Y']:
- break
+ def verify_options_and_args(self):
+ """
+ Basic sanity checks on command line arguments
+ """
+ if len(self.args) < 1:
+ self.print("Error: must specify an action")
+ return False
+ if len(self.args) > 3:
+ self.print("Error: extraneous arguments")
+ return False
+ if self.args[0] not in [ ACTION_ADD, ACTION_DELETE ]:
+ self.print("Error: action must be either add or delete")
+ return False
+ if self.args[0] == ACTION_DELETE and len(self.args) > 2:
+ self.print("Error: delete only needs username, not a password")
+ return False
+ return True
def run(self):
- self.print("Using accounts file: " + self.options.output_file)
- filename = self.options.output_file
-
- if self.options.username is not None or\
- self.options.password is not None:
- if self.options.username is None:
- self.print("Error: password but no username given")
- return 1
- if self.options.password is None:
- self.print("Error: username but no password given")
- return 1
- if not self.add_user(self.options.username,
- self.options.password):
- self.print("Error: username exists")
- else:
- try:
- self.interactive_mode()
- except KeyboardInterrupt:
- pass
- return 0
+ if not self.verify_options_and_args():
+ return BAD_ARGUMENTS
+
+ try:
+ self.print("Using accounts file: " + self.options.output_file)
+
+ action = self.args[0]
+
+ if len(self.args) > 1:
+ username = self.args[1]
+ else:
+ username = self.prompt_for_username(action)
+
+ if action == ACTION_ADD:
+ if len(self.args) > 2:
+ password = self.args[2]
+ else:
+ password = self.prompt_for_password()
+ if not self.add_user(username, password):
+ print("Error: username exists")
+ return USER_EXISTS
+ elif action == ACTION_DELETE:
+ if not self.delete_user(username):
+ print("Error: username does not exist")
+ return USER_DOES_NOT_EXIST
+
+ return 0
+ except IOError as ioe:
+ self.print("Error accessing " + ioe.filename +\
+ ": " + str(ioe.strerror))
+ return FILE_ERROR
def set_options(parser):
parser.add_option("-f", "--file",
dest="output_file", default=DEFAULT_FILE,
- help="Specify the file to append user name and password"
- )
- parser.add_option("-u", "--username",
- dest="username", default=None,
- help="Specify username to add"
- )
- parser.add_option("-p", "--password",
- dest="password", default=None,
- help="Specify password to add"
+ help="Accounts file to modify"
)
parser.add_option("-q", "--quiet",
dest="quiet", action="store_true", default=False,
)
def main():
- parser = OptionParser(version = VERSION_STRING)
+ usage = "usage: %prog [options] <action> [username] [password]\n\n"\
+ "Arguments:\n"\
+ " action\t\teither 'add' or 'delete'\n"\
+ " username\t\tthe username to add or delete\n"\
+ " password\t\tthe password to set for the added user\n"\
+ "\n"\
+ "If username or password are not specified, %prog will\n"\
+ "prompt for them."
+ parser = OptionParser(usage=usage, version=VERSION_STRING)
set_options(parser)
- (options, _) = parser.parse_args()
+ (options, args) = parser.parse_args()
- usermgr = UserManager(options)
+ usermgr = UserManager(options, args)
return usermgr.run()
-
if __name__ == '__main__':
result = main()
sys.exit(result)
import imp
import os
import subprocess
+import stat
import unittest
from bind10_config import SYSCONFPATH
def run(command):
"""
- Small helper function that returns a tuple of (rcode, stdout, stderr) after
- running the given command (an array of command and arguments, as passed on
- to subprocess).
+ Small helper function that returns a tuple of (rcode, stdout, stderr)
+ after running the given command (an array of command and arguments, as
+ passed on to subprocess).
"""
- subp = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ subp = subprocess.Popen(command, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
(stdout, stderr) = subp.communicate()
return (subp.returncode, stdout, stderr)
def test_help(self):
self.run_check(0,
-'''Usage: b10-cmdctl-usermgr [options]
+'''Usage: b10-cmdctl-usermgr [options] <action> [username] [password]
+
+Arguments:
+ action either 'add' or 'delete'
+ username the username to add or delete
+ password the password to set for the added user
+
+If username or password are not specified, b10-cmdctl-usermgr will
+prompt for them.
Options:
--version show program's version number and exit
-h, --help show this help message and exit
-f OUTPUT_FILE, --file=OUTPUT_FILE
- Specify the file to append user name and password
- -u USERNAME, --username=USERNAME
- Specify username to add
- -p PASSWORD, --password=PASSWORD
- Specify password to add
+ Accounts file to modify
-q, --quiet Quiet mode, don't print any output
''',
'',
[self.TOOL, '-h'])
- def test_create_users(self):
+ def test_add_delete_users_ok(self):
"""
Test that a file is created, and users are added.
Also tests quiet mode for adding a user to an existing file.
'',
[ self.TOOL,
'-f', self.OUTPUT_FILE,
- '-u', 'user1',
- '-p', 'pass1' ])
+ 'add', 'user1', 'pass1'
+ ])
expected_content.append(('user1', 'pass1'))
self.check_output_file(expected_content)
'',
[ self.TOOL,
'-f', self.OUTPUT_FILE,
- '-u', 'user2',
- '-p', 'pass2' ])
+ 'add', 'user2', 'pass2'
+ ])
expected_content.append(('user2', 'pass2'))
self.check_output_file(expected_content)
'',
[ self.TOOL, '-q',
'-f', self.OUTPUT_FILE,
- '-u', 'user3',
- '-p', 'pass3' ])
+ 'add', 'user3', 'pass3'
+ ])
expected_content.append(('user3', 'pass3'))
self.check_output_file(expected_content)
+ # Delete a user (let's pick the middle one)
+ self.run_check(0,
+ '',
+ '',
+ [ self.TOOL, '-q',
+ '-f', self.OUTPUT_FILE,
+ 'delete', 'user2'
+ ])
+ del expected_content[1]
+ self.check_output_file(expected_content)
+
+ def test_add_delete_users_bad(self):
+ """
+ More add/delete tests, this time for some error scenarios
+ """
+ # content is a list of (user, pass) tuples
+ expected_content = []
+ # First add one
+ self.run_check(0, None, None,
+ [ self.TOOL,
+ '-f', self.OUTPUT_FILE,
+ 'add', 'user', 'pass'
+ ])
+ expected_content.append(('user', 'pass'))
+ self.check_output_file(expected_content)
+
+ # Adding it again should error
+ self.run_check(3,
+ 'Using accounts file: test_users.csv\n'
+ 'Error: username exists\n',
+ '',
+ [ self.TOOL,
+ '-f', self.OUTPUT_FILE,
+ 'add', 'user', 'pass'
+ ])
+ self.check_output_file(expected_content)
+
+ # Deleting a non-existent one should fail too
+ self.run_check(4,
+ 'Using accounts file: test_users.csv\n'
+ 'Error: username does not exist\n',
+ '',
+ [ self.TOOL,
+ '-f', self.OUTPUT_FILE,
+ 'delete', 'nosuchuser'
+ ])
+ self.check_output_file(expected_content)
+
+ def test_bad_arguments(self):
+ """
+ Assorted tests with bad command-line arguments
+ """
+ self.run_check(1,
+ 'Error: must specify an action\n',
+ '',
+ [ self.TOOL ])
+ self.run_check(1,
+ 'Error: action must be either add or delete\n',
+ '',
+ [ self.TOOL, 'foo' ])
+ self.run_check(1,
+ 'Error: extraneous arguments\n',
+ '',
+ [ self.TOOL, 'add', 'user', 'pass', 'toomuch' ])
+ self.run_check(1,
+ 'Error: delete only needs username, not a password\n',
+ '',
+ [ self.TOOL, 'delete', 'user', 'pass' ])
+
def test_default_file(self):
"""
Check the default file is the correct one.
self.assertEqual(SYSCONFPATH + '/cmdctl-accounts.csv',
usermgr.DEFAULT_FILE)
+ def test_bad_file(self):
+ """
+ Check for graceful handling of bad file argument
+ """
+ self.run_check(2,
+ 'Using accounts file: /\n'
+ 'Error accessing /: Is a directory\n',
+ '',
+ [ self.TOOL, '-f', '/', 'add', 'user', 'pass' ])
+
+ # Make sure we can initially write to the test file
+ self.run_check(0, None, None,
+ [ self.TOOL,
+ '-f', self.OUTPUT_FILE,
+ 'add', 'user1', 'pass1'
+ ])
+
+ # Make it non-writable (don't worry about cleanup, the
+ # file should be deleted after each test anyway
+ os.chmod(self.OUTPUT_FILE, stat.S_IRUSR)
+ self.run_check(2,
+ 'Using accounts file: test_users.csv\n'
+ 'Error accessing test_users.csv: Permission denied\n',
+ '',
+ [ self.TOOL,
+ '-f', self.OUTPUT_FILE,
+ 'add', 'user2', 'pass1'
+ ])
+
+ self.run_check(2,
+ 'Using accounts file: test_users.csv\n'
+ 'Error accessing test_users.csv: Permission denied\n',
+ '',
+ [ self.TOOL,
+ '-f', self.OUTPUT_FILE,
+ 'delete', 'user1'
+ ])
+
+ # Making it write-only should have the same effect
+ os.chmod(self.OUTPUT_FILE, stat.S_IWUSR)
+ self.run_check(2,
+ 'Using accounts file: test_users.csv\n'
+ 'Error accessing test_users.csv: Permission denied\n',
+ '',
+ [ self.TOOL,
+ '-f', self.OUTPUT_FILE,
+ 'add', 'user2', 'pass1'
+ ])
+
+ self.run_check(2,
+ 'Using accounts file: test_users.csv\n'
+ 'Error accessing test_users.csv: Permission denied\n',
+ '',
+ [ self.TOOL,
+ '-f', self.OUTPUT_FILE,
+ 'delete', 'user1'
+ ])
+
+
if __name__== '__main__':
unittest.main()