Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

making verdi setup & verdi quicksetup more user friendly #606

Merged
merged 22 commits into from
Sep 12, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2d8cff9
added prompt for profile name to verdi quicksetup
ltalirz Jun 21, 2017
47bb2a0
Merge remote-tracking branch 'upstream/develop' into fix_XXX_verdi_qu…
ltalirz Jun 21, 2017
a59e93d
improved verdi quicksetup help
ltalirz Jun 21, 2017
8f9f24e
fixed underscores and added documentation
ltalirz Jun 21, 2017
fedc731
verdi help now shows command line options
ltalirz Jun 22, 2017
3eefe19
removed default email in verdi setup
ltalirz Jun 22, 2017
5e4da13
added --set-default, --no-set-default options
ltalirz Jul 17, 2017
0432d15
add verdi profile delete
ltalirz Jul 17, 2017
3dbcfc2
Merge branch 'develop' into fix_XXX_verdi_quicksetup
ltalirz Jul 17, 2017
d5efa7d
minor improvements in documentation
ltalirz Jul 17, 2017
f76d431
now deleting profile repository as well
ltalirz Jul 28, 2017
9078c69
Merge branch 'develop' into fix_XXX_verdi_quicksetup
ltalirz Jul 28, 2017
bc75b05
Merge branch 'develop' into fix_XXX_verdi_quicksetup
sphuber Aug 16, 2017
fd76925
add checks, trim overly long lines
ltalirz Aug 17, 2017
43ddd64
Merge branch 'develop' into fix_XXX_verdi_quicksetup
ltalirz Aug 28, 2017
157fbc2
Merge branch 'develop' into fix_XXX_verdi_quicksetup
ltalirz Sep 5, 2017
604413f
add --yes option for verdi profile delete
ltalirz Sep 12, 2017
6c8f05b
Merge branch 'fix_XXX_verdi_quicksetup' of github.com:ltalirz/aiida_c…
ltalirz Sep 12, 2017
9bbf2a5
fix bug verdi profile delete
ltalirz Sep 12, 2017
be21424
Merge branch 'develop' into fix_XXX_verdi_quicksetup
ltalirz Sep 12, 2017
dbc9f8c
improve comments
ltalirz Sep 12, 2017
0783ce1
switch from --yes to --force
ltalirz Sep 12, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
82 changes: 81 additions & 1 deletion aiida/cmdline/commands/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
This allows to manage profiles from command line.
"""
import sys

import click
from aiida.cmdline.baseclass import VerdiCommandWithSubcommands


Expand All @@ -35,6 +35,7 @@ def __init__(self):
'setdefault': (self.profile_setdefault,
self.complete_processes_profiles),
'list': (self.profile_list, self.complete_none),
'delete': (self.profile_delete, self.complete_processes_profiles),
}

def complete_processes_profiles(self, subargs_idx, subargs):
Expand Down Expand Up @@ -135,3 +136,82 @@ def profile_list(self, *args):
start_color, symbol,
bold_sequence, profile, default_str, nobold_sequence, end_color)

def profile_delete(self, *args):
""" Deletes profile

Asks whether to delete associated database and associated database
user.

Specify argument '--force' to skip any questions warning about loss of
data.
"""
from aiida.cmdline.verdilib import Quicksetup
from aiida.common.setup import get_or_create_config, update_config, AIIDA_CONFIG_FOLDER
import os.path
from urlparse import urlparse

args = list(args)
if '--force' in args:
force = True
args.remove('--force')
else:
force = False

#TODO (issue 693): move db-related functions below outside Quicksetup
q = Quicksetup()
pg_info = q._get_pg_access()
pg_execute = pg_info['method']
dbinfo = pg_info['dbinfo']

confs = get_or_create_config()
profiles = confs.get('profiles',{})
users = [ profiles[name].get('AIIDADB_USER', '') for name in profiles.keys()]

profiles_to_delete = args
for profile_to_delete in profiles_to_delete:
try:
profile = profiles[profile_to_delete]
except KeyError:
print("Profile '{}' does not exist".format(profile_to_delete))
continue

db = profile.get('AIIDADB_NAME', '')
if not q._db_exists(db, pg_execute, **dbinfo):
print("Associated database '{}' does not exist.".format(db))
elif force or click.confirm("Delete associated database '{}'?\n" \
"WARNING: All data will be lost.".format(db)):
print("Deleting database '{}'.".format(db))
q._drop_db(db, pg_execute, **dbinfo)

user = profile.get('AIIDADB_USER', '')
if not q._dbuser_exists(user, pg_execute, **dbinfo):
print("Associated database user '{}' does not exist.".format(user))
elif users.count(user) > 1:
print("Associated database user '{}' is used by other profiles "\
"and will not be deleted.".format(user))
elif force or click.confirm("Delete database user '{}'?".format(user)):
print("Deleting user '{}'.".format(user))
q._drop_dbuser(user, pg_execute, **dbinfo)

repo_uri = profile.get('AIIDADB_REPOSITORY_URI','')
repo_path = urlparse(repo_uri).path
repo_path = os.path.expanduser(repo_path)
if not os.path.isabs(repo_path):
print("Associated file repository '{}' does not exist."\
.format(repo_path))
elif not os.path.isdir(repo_path):
print("Associated file repository '{}' is not a directory."\
.format(repo_path))
elif force or click.confirm("Delete associated file repository '{}'?\n" \
"WARNING: All data will be lost.".format(repo_path)):
print("Deleting directory '{}'.".format(repo_path))
import shutil
shutil.rmtree(repo_path)

if force or click.confirm("Delete configuration for profile '{}'?\n" \
"WARNING: Permanently removes profile from the list of AiiDA profiles."\
.format(profile_to_delete)):
print("Deleting configuration for profile '{}'.".format(profile_to_delete))
del profiles[profile_to_delete]
update_config(confs)

151 changes: 111 additions & 40 deletions aiida/cmdline/verdilib.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,10 +344,14 @@ class Setup(VerdiCommand):
"""

def run(self, *args):
ctx = _setup_cmd.make_context('setup', list(args))
ctx = self._ctx(args)
with ctx:
_setup_cmd.invoke(ctx)

@staticmethod
def _ctx(args, info_name='verdi setup', **kwargs):
return _setup_cmd.make_context(info_name, list(args), **kwargs)

def complete(self, subargs_idx, subargs):
"""
No completion after 'verdi install'.
Expand All @@ -362,22 +366,23 @@ def complete(self, subargs_idx, subargs):
@click.argument('profile', default='', type=str)
@click.option('--only-config', is_flag=True)
@click.option('--non-interactive', is_flag=True, help='never prompt the user for input, read values from options')
@click.option('--backend', type=click.Choice(['django', 'sqlalchemy']),
help='ignored unless --non-interactive is given')
@click.option('--email', type=str, help='ignored unless --non-interactive is given')
@click.option('--db_host', type=str, help='ignored unless --non-interactive is given')
@click.option('--db_port', type=int, help='ignored unless --non-interactive is given')
@click.option('--db_name', type=str, help='ignored unless --non-interactive is given')
@click.option('--db_user', type=str, help='ignored unless --non-interactive is given')
@click.option('--db_pass', type=str, help='ignored unless --non-interactive is given')
@click.option('--first-name', type=str, help='ignored unless --non-interactive is given')
@click.option('--last-name', type=str, help='ignored unless --non-interactive is given')
@click.option('--institution', type=str, help='ignored unless --non-interactive is given')
@click.option('--no-password', is_flag=True, help='ignored unless --non-interactive is given')
@click.option('--repo', type=str, help='ignored unless --non-interactive is given')
@click.option('--backend', type=click.Choice(['django', 'sqlalchemy']),)
@click.option('--email', type=str)
@click.option('--db_host', type=str)
@click.option('--db_port', type=int)
@click.option('--db_name', type=str)
@click.option('--db_user', type=str)
@click.option('--db_pass', type=str)
@click.option('--first-name', type=str)
@click.option('--last-name', type=str)
@click.option('--institution', type=str)
@click.option('--no-password', is_flag=True)
@click.option('--repo', type=str)
def _setup_cmd(profile, only_config, non_interactive, backend, email, db_host, db_port, db_name, db_user, db_pass,
first_name, last_name, institution, no_password, repo):
'''verdi setup command, forward cmdline arguments to the setup function.'''
'''verdi setup command, forward cmdline arguments to the setup function.

Note: command line options are IGNORED unless --non-interactive is given.'''
kwargs = dict(
profile=profile,
only_config=only_config,
Expand Down Expand Up @@ -592,23 +597,30 @@ class Quicksetup(VerdiCommand):
'''
Quick setup for the most common usecase (1 user, 1 machine).

Uses click for options. Creates a database user 'aiida_qs_<username>' with random password if it doesn't exist.
Creates a 'aiidadb_qs_<username>' database (prompts to use or change the name if already exists).
Makes sure not to overwrite existing databases or profiles without prompting for confirmation.
Creates a database user 'aiida_qs_<login-name>' with random password (if it
doesn't exist). Creates a database '<profile>_<username>' (if it exists,
prompts user to use or change the name).
'''
from aiida.backends.profile import (BACKEND_DJANGO, BACKEND_SQLA)

_create_user_command = 'CREATE USER "{}" WITH PASSWORD \'{}\''
_drop_user_command = 'DROP USER "{}"'
_create_db_command = 'CREATE DATABASE "{}" OWNER "{}"'
_drop_db_command = 'DROP DATABASE "{}"'
_grant_priv_command = 'GRANT ALL PRIVILEGES ON DATABASE "{}" TO "{}"'
_get_users_command = "SELECT usename FROM pg_user WHERE usename='{}'"
# note: 'usename' is not a typo!

def run(self, *args):
ctx = self._quicksetup_cmd.make_context('quicksetup', list(args))
ctx = self._ctx(args)
with ctx:
ctx.obj = self
self._quicksetup_cmd.invoke(ctx)

@staticmethod
def _ctx(args, info_name='verdi quicksetup', **kwargs):
return Quicksetup._quicksetup_cmd.make_context(info_name, list(args), **kwargs)

def _get_pg_access(self):
'''
find out how postgres can be accessed.
Expand Down Expand Up @@ -684,8 +696,10 @@ def _prompt_db_info(self):
click.echo('Unable to connect to postgres, please try again')
return dbinfo


@click.command('quicksetup', context_settings=CONTEXT_SETTINGS)
@click.option('--email', prompt='Email Address (will be used to identify your data when sharing)', type=str,
@click.option('--profile', prompt='Profile name', type=str, default='quicksetup')
@click.option('--email', prompt='Email Address (identifies your data when sharing)', type=str,
help='This email address will be associated with your data and will be exported along with it, should you choose to share any of your work')
@click.option('--first-name', prompt='First Name', type=str)
@click.option('--last-name', prompt='Last Name', type=str)
Expand All @@ -695,12 +709,12 @@ def _prompt_db_info(self):
@click.option('--db-user', type=str)
@click.option('--db-user-pw', type=str)
@click.option('--db-name', type=str)
@click.option('--profile', type=str)
@click.option('--repo', type=str)
@click.option('--set-default/--no-set-default', default=None, help='Whether to set new profile as default for shell and daemon.')
@click.pass_obj
def _quicksetup_cmd(self, email, first_name, last_name, institution, backend, db_port, db_user, db_user_pw, db_name,
profile, repo):
'''setup a sane aiida configuration with as little interaction as possible.'''
def _quicksetup_cmd(self, profile, email, first_name, last_name, institution, backend, db_port, db_user, db_user_pw, db_name,
repo, set_default):
'''Set up a sane aiida configuration with as little interaction as possible.'''
from aiida.common.setup import create_base_dirs, AIIDA_CONFIG_FOLDER
create_base_dirs()

Expand All @@ -711,15 +725,18 @@ def _quicksetup_cmd(self, email, first_name, last_name, institution, backend, db
pg_execute = pg_info['method']
dbinfo = pg_info['dbinfo']

# check if a database setup already exists
# otherwise setup the database user aiida
# setup the database aiida_qs_<username>
from getpass import getuser
from aiida.common.setup import generate_random_secret_key
osuser = getuser()
# default database name is <profile>_<login-name>
# this ensures that for profiles named test_... the database will also
# be named test_...
import getpass
osuser = getpass.getuser()
dbname = db_name or profile + '_' + osuser

# default database user name is aiida_qs_<login-name>
# default password is random
dbuser = db_user or 'aiida_qs_' + osuser
from aiida.common.setup import generate_random_secret_key
dbpass = db_user_pw or generate_random_secret_key()
dbname = db_name or 'aiidadb_qs_' + osuser

# check if there is a profile that contains the db user already
# and if yes, take the db user password from there
Expand Down Expand Up @@ -805,17 +822,24 @@ def _quicksetup_cmd(self, email, first_name, last_name, institution, backend, db

for process in valid_processes:

default_profile = default_profiles.get(process, '')
default_override = False

if default_profile:
default_override = click.confirm("The default profile for the '{}' process is set to '{}': "
"do you want to set the newly created '{}' as the new default? (can be reverted later)"
.format(process, default_profile, profile_name))
# if the user specifies whether to override that's fine
if set_default in [True, False]:
_set_default = set_default
# otherwise we may need to ask
else:
default_profile = default_profiles.get(process, '')
if default_profile:
_set_default = click.confirm("The default profile for the '{}' process is set to '{}': "
"do you want to set the newly created '{}' as the new default? (can be reverted later)"
.format(process, default_profile, profile_name))
# if there are no other default profiles, we don't need to ask
else:
_set_default = True

if not default_profile or default_override:
if _set_default:
set_default_profile(process, profile_name, force_rewrite=True)

#TODO (issue 693): move db-related functions outside quicksetup
def _try_connect(self, **kwargs):
'''
try to start a psycopg2 connection.
Expand Down Expand Up @@ -857,6 +881,17 @@ def _create_dbuser(self, dbuser, dbpass, method=None, **kwargs):
'''
method(self._create_user_command.format(dbuser, dbpass), **kwargs)

def _drop_dbuser(self, dbuser, method=None, **kwargs):
'''
drop a database user in postgres

:param dbuser: Name of the user to be dropped.
:param method: callable with signature method(psql_command, **connection_info)
where connection_info contains keys for psycopg2.connect.
:param kwargs: connection info as for psycopg2.connect.
'''
method(self._drop_user_command.format(dbuser), **kwargs)

def _create_db(self, dbuser, dbname, method=None, **kwargs):
'''create a database in postgres

Expand All @@ -869,6 +904,17 @@ def _create_db(self, dbuser, dbname, method=None, **kwargs):
method(self._create_db_command.format(dbname, dbuser), **kwargs)
method(self._grant_priv_command.format(dbname, dbuser), **kwargs)

def _drop_db(self, dbname, method=None, **kwargs):
'''drop a database in postgres

:param dbname: Name of the database.
:param method: callable with signature method(psql_command, **connection_info)
where connection_info contains keys for psycopg2.connect.
:param kwargs: connection info as for psycopg2.connect.
'''
method(self._drop_db_command.format(dbname), **kwargs)


def _pg_execute_psyco(self, command, **kwargs):
'''
executes a postgres commandline through psycopg2
Expand Down Expand Up @@ -925,6 +971,10 @@ def _dbuser_exists(self, dbuser, method, **kwargs):
'''return True if postgres user with name dbuser exists, False otherwise.'''
return bool(method(self._get_users_command.format(dbuser), **kwargs))

def _db_exists(self, dbname, method=None, **kwargs):
"""return True if database with dbname exists."""
return bool(method("SELECT datname FROM pg_database WHERE datname='{}'".format(dbname), **kwargs))

def _check_db_name(self, dbname, method=None, **kwargs):
'''looks up if a database with the name exists, prompts for using or creating a differently named one'''
create = True
Expand All @@ -937,6 +987,8 @@ def _check_db_name(self, dbname, method=None, **kwargs):
return dbname, create




class Run(VerdiCommand):
"""
Execute an AiiDA script
Expand Down Expand Up @@ -1133,9 +1185,27 @@ def exec_from_cmdline(argv):

short_doc = {}
long_doc = {}
for k, v in raw_docstrings.iteritems():
for k, cmd in list_commands.iteritems():
if k in hidden_commands:
continue

# assemble help string
# first from python docstring
if cmd.__doc__:
v = cmd.__doc__
else:
v = ""

# if command has parser written with the 'click' module
# we also add a dynamic help string documenting the options
# Note: to enable this for a command, simply add a static
# _ctx method (see e.g. Quicksetup)
if hasattr(cmd, '_ctx'):
v += "\n"
# resilient_parsing suppresses interactive prompts
v += cmd._ctx(args=[], resilient_parsing=True).get_help()
v = v.split('\n') # need list of lines

lines = [l.strip() for l in v]
empty_lines = [bool(l) for l in lines]
try:
Expand All @@ -1148,6 +1218,7 @@ def exec_from_cmdline(argv):
short_doc[k] = lines[first_idx]
long_doc[k] = os.linesep.join(lines[first_idx + 1:])


execname = os.path.basename(argv[0])

try:
Expand Down
3 changes: 1 addition & 2 deletions aiida/common/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -656,8 +656,7 @@ def create_configuration(profile='default'):
# Setting the email
valid_email = False
readline.set_startup_hook(lambda: readline.insert_text(
this_existing_confs.get(DEFAULT_USER_CONFIG_FIELD,
DEFAULT_AIIDA_USER)))
this_existing_confs.get(DEFAULT_AIIDA_USER)))
while not valid_email:
this_new_confs[DEFAULT_USER_CONFIG_FIELD] = raw_input(
'Default user email: ')
Expand Down