moulinette/yunohost.py

558 lines
18 KiB
Python
Raw Normal View History

2012-10-08 18:16:43 +02:00
# -*- coding: utf-8 -*-
2013-07-06 09:42:26 +02:00
""" License
Copyright (C) 2013 YunoHost
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program; if not, see http://www.gnu.org/licenses
"""
"""
YunoHost core classes & functions
"""
2012-10-08 18:16:43 +02:00
import os
import sys
2012-10-16 14:56:54 +02:00
try:
import ldap
except ImportError:
sys.stderr.write('Error: Yunohost CLI Require LDAP lib\n')
sys.stderr.write('apt-get install python-ldap\n')
sys.exit(1)
2012-10-08 18:16:43 +02:00
import ldap.modlist as modlist
2013-06-28 19:58:13 +02:00
import yaml
2012-10-15 22:48:05 +02:00
import json
2012-10-08 18:16:43 +02:00
import re
import getpass
import random
import string
2013-06-28 19:58:13 +02:00
import argparse
import gettext
import getpass
2012-10-23 19:55:40 +02:00
if not __debug__:
import traceback
2012-10-08 18:16:43 +02:00
2013-03-15 16:59:00 +01:00
win = []
2013-03-02 15:15:57 +01:00
def random_password(length=8):
char_set = string.ascii_uppercase + string.digits + string.ascii_lowercase
return ''.join(random.sample(char_set,length))
2012-10-08 18:16:43 +02:00
def colorize(astr, color):
2013-02-20 21:32:08 +01:00
"""
Print with style ;)
2012-10-08 22:00:16 +02:00
Keyword arguments:
astr -- String to colorize
color -- Name of the color
"""
2012-10-08 18:16:43 +02:00
color_dict = {
'red' : '31',
'green' : '32',
'yellow': '33',
'cyan' : '34',
'purple': '35'
}
2013-02-20 21:32:08 +01:00
return "\033["+ color_dict[color] +"m\033[1m" + astr + "\033[m"
2012-10-08 18:16:43 +02:00
2012-10-10 14:19:17 +02:00
def pretty_print_dict(d, depth=0):
for k,v in sorted(d.items(), key=lambda x: x[0]):
2013-02-20 21:32:08 +01:00
k = colorize(str(k), 'purple')
2012-10-28 17:27:47 +01:00
if isinstance(v, list) and len(v) == 1:
v = v[0]
2012-10-10 14:19:17 +02:00
if isinstance(v, dict):
2013-02-20 21:32:08 +01:00
print((" ") * depth + ("%s: " % str(k)))
2012-10-10 14:19:17 +02:00
pretty_print_dict(v, depth+1)
2012-10-28 17:27:47 +01:00
elif isinstance(v, list):
2013-02-20 21:32:08 +01:00
print((" ") * depth + ("%s: " % str(k)))
2013-03-15 19:12:48 +01:00
for key, value in enumerate(v):
2013-02-27 20:06:17 +01:00
if isinstance(value, tuple):
pretty_print_dict({value[0]: value[1]}, depth+1)
2013-03-15 19:12:48 +01:00
elif isinstance(value, dict):
pretty_print_dict({key: value}, depth+1)
2013-02-27 20:06:17 +01:00
else:
print((" ") * (depth+1) + "- " +str(value))
2012-10-10 14:19:17 +02:00
else:
2013-02-20 21:32:08 +01:00
print((" ") * depth + "%s: %s" % (str(k), str(v)))
2013-03-01 19:28:00 +01:00
def is_true(arg):
true_list = ['yes', 'Yes', 'true', 'True' ]
for string in true_list:
if arg == string:
return True
return False
2012-10-08 18:16:43 +02:00
def win_msg(astr):
2013-02-20 21:32:08 +01:00
"""
Display a success message if isatty
2012-10-08 22:00:16 +02:00
Keyword arguments:
astr -- Win message to display
"""
2013-03-15 16:59:00 +01:00
global win
2012-10-08 18:16:43 +02:00
if os.isatty(1):
print('\n' + colorize(_("Success: "), 'green') + astr + '\n')
2013-11-20 23:40:20 +01:00
2013-06-28 19:58:13 +02:00
win.append(astr)
2013-03-15 16:59:00 +01:00
2012-10-08 18:16:43 +02:00
def str_to_func(astr):
"""
Call a function from a string name
2013-02-20 21:32:08 +01:00
2012-10-08 18:16:43 +02:00
Keyword arguments:
astr -- Name of function to call
2013-02-20 21:32:08 +01:00
Returns:
2012-10-08 18:16:43 +02:00
Function
"""
try:
2012-10-10 19:47:57 +02:00
module, _, function = astr.rpartition('.')
if module:
__import__(module)
mod = sys.modules[module]
else:
mod = sys.modules['__main__'] # default module
2013-02-20 21:32:08 +01:00
2012-10-08 18:16:43 +02:00
func = getattr(mod, function)
2012-10-10 19:47:57 +02:00
except (AttributeError, ImportError):
2012-10-15 22:48:05 +02:00
#raise YunoHostError(168, _('Function is not defined'))
return None
2012-10-08 18:16:43 +02:00
else:
return func
2012-11-09 18:04:15 +01:00
def validate(pattern, array):
2013-02-20 21:32:08 +01:00
"""
Validate attributes with a pattern
2012-10-10 19:47:57 +02:00
Keyword arguments:
2012-11-09 18:04:15 +01:00
pattern -- Regex to match with the strings
array -- List of strings to check
2012-10-10 19:47:57 +02:00
Returns:
Boolean | YunoHostError
"""
2013-06-02 20:08:27 +02:00
if array is None:
return True
2012-11-09 18:04:15 +01:00
if isinstance(array, str):
array = [array]
for string in array:
if re.match(pattern, string):
pass
2012-10-10 19:47:57 +02:00
else:
2012-11-09 18:04:15 +01:00
raise YunoHostError(22, _('Invalid attribute') + ' ' + string)
return True
2012-10-10 19:47:57 +02:00
2012-10-26 15:26:50 +02:00
def get_required_args(args, required_args, password=False):
2013-02-20 21:32:08 +01:00
"""
2012-10-26 15:26:50 +02:00
Input missing values or raise Exception
2013-02-20 21:32:08 +01:00
2012-10-26 15:26:50 +02:00
Keyword arguments:
args -- Available arguments
required_args -- Dictionary of required arguments and input phrase
password -- True|False Hidden password double-input needed
Returns:
args
"""
try:
for arg, phrase in required_args.items():
if not args[arg] and arg != 'password':
if os.isatty(1):
args[arg] = raw_input(colorize(phrase + ': ', 'cyan'))
else:
2013-10-18 14:46:30 +02:00
raise Exception #TODO: fix
2012-10-26 15:26:50 +02:00
# Password
2013-02-20 21:32:08 +01:00
if 'password' in required_args and password:
2012-10-25 19:52:52 +02:00
if not args['password']:
if os.isatty(1):
args['password'] = getpass.getpass(colorize(required_args['password'] + ': ', 'cyan'))
pwd2 = getpass.getpass(colorize('Retype ' + required_args['password'][0].lower() + required_args['password'][1:] + ': ', 'cyan'))
if args['password'] != pwd2:
raise YunoHostError(22, _("Passwords doesn't match"))
else:
raise YunoHostError(22, _("Missing arguments"))
2012-10-26 15:26:50 +02:00
except KeyboardInterrupt, EOFError:
2012-10-29 17:38:05 +01:00
raise YunoHostError(125, _("Interrupted"))
2012-10-26 15:26:50 +02:00
return args
2012-10-10 19:47:57 +02:00
2013-03-15 16:59:00 +01:00
def display_error(error, json_print=False):
2012-10-14 21:48:16 +02:00
"""
Nice error displaying
"""
if not __debug__ :
traceback.print_exc()
2013-03-15 16:59:00 +01:00
if os.isatty(1) and not json_print:
2012-10-14 21:48:16 +02:00
print('\n' + colorize(_("Error: "), 'red') + error.message)
else:
2013-03-15 16:59:00 +01:00
print(json.dumps({ error.code : error.message }))
2012-10-14 21:48:16 +02:00
2012-10-08 18:16:43 +02:00
class YunoHostError(Exception):
2012-10-08 22:00:16 +02:00
"""
Custom exception
2013-02-20 21:32:08 +01:00
2012-10-08 22:00:16 +02:00
Keyword arguments:
code -- Integer error code
message -- Error message to display
"""
2012-10-08 18:16:43 +02:00
def __init__(self, code, message):
code_dict = {
1 : _('Fail'),
13 : _('Permission denied'),
17 : _('Already exists'),
22 : _('Invalid arguments'),
87 : _('Too many users'),
111 : _('Connection refused'),
122 : _('Quota exceeded'),
125 : _('Operation canceled'),
167 : _('Not found'),
168 : _('Undefined'),
169 : _('LDAP operation error')
}
self.code = code
self.message = message
if code_dict[code]:
self.desc = code_dict[code]
else:
self.desc = code
2012-10-25 21:19:34 +02:00
class Singleton(object):
2012-10-25 20:40:44 +02:00
instances = {}
2012-10-25 21:19:34 +02:00
def __new__(cls, *args, **kwargs):
if cls not in cls.instances:
cls.instances[cls] = super(Singleton, cls).__new__(cls, *args, **kwargs)
return cls.instances[cls]
class YunoHostLDAP(Singleton):
2012-10-08 18:16:43 +02:00
""" Specific LDAP functions for YunoHost """
2012-10-25 21:19:34 +02:00
pwd = False
2013-10-24 18:49:40 +02:00
connected = False
2012-10-25 21:19:34 +02:00
conn = ldap.initialize('ldap://localhost:389')
base = 'dc=yunohost,dc=org'
2012-10-25 21:10:53 +02:00
level = 0
2012-10-08 18:16:43 +02:00
2012-10-25 21:19:34 +02:00
def __enter__(self):
2012-10-25 20:40:44 +02:00
return self
2013-10-24 18:49:40 +02:00
def __init__(self, password=False, anonymous=False):
2013-02-20 21:32:08 +01:00
"""
Connect to LDAP base
2012-10-08 22:00:16 +02:00
Initialize to localhost, base yunohost.org, prompt for password
2012-10-08 18:16:43 +02:00
2012-10-08 22:00:16 +02:00
"""
2013-10-24 18:49:40 +02:00
if anonymous:
self.conn.simple_bind_s()
self.connected = True
2013-11-28 17:16:18 +01:00
elif self.connected and not password:
2013-10-24 18:49:40 +02:00
pass
2012-10-25 20:40:44 +02:00
else:
2013-10-24 18:49:40 +02:00
if password:
self.pwd = password
elif self.pwd:
pass
else:
2013-11-20 23:40:20 +01:00
try:
with open('/etc/yunohost/passwd') as f:
self.pwd = f.read()
except IOError:
need_password = True
while need_password:
try:
self.pwd = getpass.getpass(colorize(_('Admin Password: '), 'yellow'))
self.conn.simple_bind_s('cn=admin,' + self.base, self.pwd)
except KeyboardInterrupt, EOFError:
raise YunoHostError(125, _("Interrupted"))
except ldap.INVALID_CREDENTIALS:
print(_('Invalid password... Try again'))
else:
need_password = False
2013-10-24 18:49:40 +02:00
try:
self.conn.simple_bind_s('cn=admin,' + self.base, self.pwd)
self.connected = True
except ldap.INVALID_CREDENTIALS:
raise YunoHostError(13, _('Invalid credentials'))
2012-11-08 19:20:13 +01:00
2013-10-26 20:41:53 +02:00
self.level = self.level+1
2012-10-25 20:44:57 +02:00
def __exit__(self, type, value, traceback):
2012-10-25 21:10:53 +02:00
self.level = self.level-1
if self.level == 0:
try: self.disconnect()
except: pass
2012-10-25 20:44:57 +02:00
2012-10-08 18:16:43 +02:00
def disconnect(self):
2013-02-20 21:32:08 +01:00
"""
Unbind from LDAP
2012-10-08 22:00:16 +02:00
Returns
Boolean | YunoHostError
2012-10-08 18:16:43 +02:00
2012-10-08 22:00:16 +02:00
"""
2012-10-25 20:47:17 +02:00
try:
2013-11-28 17:16:18 +01:00
self.connected = False
self.pwd = False
2012-10-25 20:47:17 +02:00
self.conn.unbind_s()
except:
raise YunoHostError(169, _('An error occured during disconnection'))
else:
return True
2012-10-08 18:16:43 +02:00
2012-10-08 22:00:16 +02:00
def search(self, base=None, filter='(objectClass=*)', attrs=['dn']):
2013-02-20 21:32:08 +01:00
"""
Search in LDAP base
2012-10-08 22:00:16 +02:00
Keyword arguments:
base -- Base to search into
filter -- LDAP filter
attrs -- Array of attributes to fetch
Returns:
Boolean | Dict
"""
2012-10-08 18:16:43 +02:00
if not base:
base = self.base
try:
result = self.conn.search_s(base, ldap.SCOPE_SUBTREE, filter, attrs)
except:
raise YunoHostError(169, _('An error occured during LDAP search'))
if result:
result_list = []
for dn, entry in result:
2012-10-23 19:55:40 +02:00
if attrs != None:
if 'dn' in attrs:
entry['dn'] = [dn]
2012-10-08 18:16:43 +02:00
result_list.append(entry)
2013-02-20 21:32:08 +01:00
return result_list
2012-10-08 18:16:43 +02:00
else:
return False
2012-10-08 22:00:16 +02:00
2012-10-08 18:16:43 +02:00
def add(self, rdn, attr_dict):
2013-02-20 21:32:08 +01:00
"""
Add LDAP entry
2012-10-08 22:00:16 +02:00
Keyword arguments:
rdn -- DN without domain
attr_dict -- Dictionnary of attributes/values to add
Returns:
Boolean | YunoHostError
2012-10-08 18:16:43 +02:00
2012-10-08 22:00:16 +02:00
"""
2012-10-08 18:16:43 +02:00
dn = rdn + ',' + self.base
ldif = modlist.addModlist(attr_dict)
try:
self.conn.add_s(dn, ldif)
except:
raise YunoHostError(169, _('An error occured during LDAP entry creation'))
else:
return True
2012-10-29 12:35:29 +01:00
def remove(self, rdn):
2013-02-20 21:32:08 +01:00
"""
Remove LDAP entry
2012-10-29 12:35:29 +01:00
Keyword arguments:
rdn -- DN without domain
Returns:
Boolean | YunoHostError
"""
dn = rdn + ',' + self.base
try:
self.conn.delete_s(dn)
except:
raise YunoHostError(169, _('An error occured during LDAP entry deletion'))
else:
return True
2012-10-08 18:16:43 +02:00
2012-10-23 19:55:40 +02:00
def update(self, rdn, attr_dict, new_rdn=False):
2013-02-20 21:32:08 +01:00
"""
Modify LDAP entry
2012-10-23 19:55:40 +02:00
Keyword arguments:
rdn -- DN without domain
attr_dict -- Dictionnary of attributes/values to add
new_rdn -- New RDN for modification
Returns:
Boolean | YunoHostError
"""
dn = rdn + ',' + self.base
actual_entry = self.search(base=dn, attrs=None)
2012-10-29 15:43:43 +01:00
ldif = modlist.modifyModlist(actual_entry[0], attr_dict, ignore_oldexistent=1)
2012-10-23 19:55:40 +02:00
try:
if new_rdn:
self.conn.rename_s(dn, new_rdn)
dn = new_rdn + ',' + self.base
self.conn.modify_ext_s(dn, ldif)
except:
raise YunoHostError(169, _('An error occured during LDAP entry update'))
else:
2012-10-23 22:21:18 +02:00
return True
2012-10-23 19:55:40 +02:00
2012-10-08 18:16:43 +02:00
def validate_uniqueness(self, value_dict):
2013-02-20 21:32:08 +01:00
"""
Check uniqueness of values
2012-10-08 22:00:16 +02:00
Keyword arguments:
value_dict -- Dictionnary of attributes/values to check
Returns:
Boolean | YunoHostError
"""
2012-10-08 18:16:43 +02:00
for attr, value in value_dict.items():
if not self.search(filter=attr + '=' + value):
continue
else:
raise YunoHostError(17, _('Attribute already exists') + ' "' + attr + '=' + value + '"')
return True
2013-06-28 19:58:13 +02:00
def parse_dict(action_map):
"""
Turn action dictionnary to parser, subparsers and arguments
Keyword arguments:
action_map -- Multi-level dictionnary of categories/actions/arguments list
Returns:
Namespace of args
"""
# Intialize parsers
parsers = subparsers_category = subparsers_action = {}
parsers['general'] = argparse.ArgumentParser()
subparsers = parsers['general'].add_subparsers()
new_args = []
patterns = {}
# Add general arguments
for arg_name, arg_params in action_map['general_arguments'].items():
if 'full' in arg_params:
arg_names = [arg_name, arg_params['full']]
arg_fullname = arg_params['full']
del arg_params['full']
else: arg_names = [arg_name]
parsers['general'].add_argument(*arg_names, **arg_params)
del action_map['general_arguments']
# Split categories into subparsers
for category, category_params in action_map.items():
if 'category_help' not in category_params: category_params['category_help'] = ''
subparsers_category[category] = subparsers.add_parser(category, help=category_params['category_help'])
subparsers_action[category] = subparsers_category[category].add_subparsers()
# Split actions
if 'actions' in category_params:
for action, action_params in category_params['actions'].items():
if 'action_help' not in action_params: action_params['action_help'] = ''
parsers[category + '_' + action] = subparsers_action[category].add_parser(action, help=action_params['action_help'])
# Set the action s related function
parsers[category + '_' + action].set_defaults(
func=str_to_func('yunohost_' + category + '.'
+ category + '_' + action))
# Add arguments
if 'arguments' in action_params:
for arg_name, arg_params in action_params['arguments'].items():
arg_fullname = False
if 'password' in arg_params:
if arg_params['password']: is_password = True
del arg_params['password']
else: is_password = False
if 'full' in arg_params:
arg_names = [arg_name, arg_params['full']]
arg_fullname = arg_params['full']
del arg_params['full']
else: arg_names = [arg_name]
if 'ask' in arg_params:
require_input = True
if '-h' in sys.argv or '--help' in sys.argv:
require_input = False
if (category != sys.argv[1]) or (action != sys.argv[2]):
require_input = False
for name in arg_names:
if name in sys.argv[2:]: require_input = False
if require_input:
if is_password:
if os.isatty(1):
pwd1 = getpass.getpass(colorize(arg_params['ask'] + ': ', 'cyan'))
pwd2 = getpass.getpass(colorize('Retype ' + arg_params['ask'][0].lower() + arg_params['ask'][1:] + ': ', 'cyan'))
if pwd1 != pwd2:
raise YunoHostError(22, _("Passwords don't match"))
sys.exit(1)
else:
raise YunoHostError(22, _("Missing arguments") + ': ' + arg_name)
if arg_name[0] == '-': arg_extend = [arg_name, pwd1]
else: arg_extend = [pwd1]
else:
if os.isatty(1):
arg_value = raw_input(colorize(arg_params['ask'] + ': ', 'cyan'))
else:
raise YunoHostError(22, _("Missing arguments") + ': ' + arg_name)
if arg_name[0] == '-': arg_extend = [arg_name, arg_value]
else: arg_extend = [arg_value]
new_args.extend(arg_extend)
del arg_params['ask']
if 'pattern' in arg_params:
if (category == sys.argv[1]) and (action == sys.argv[2]):
if 'dest' in arg_params: name = arg_params['dest']
elif arg_fullname: name = arg_fullname[2:]
else: name = arg_name
name = name.replace('-', '_')
patterns[name] = arg_params['pattern']
del arg_params['pattern']
parsers[category + '_' + action].add_argument(*arg_names, **arg_params)
args = parsers['general'].parse_args(sys.argv.extend(new_args))
args_dict = vars(args)
for key, value in patterns.items():
validate(value, args_dict[key])
return args