def tools_maindomain(auth, old_domain=None, new_domain=None, dyndns=False): """ Main domain change tool Keyword argument: new_domain old_domain """ if not old_domain: with open('/etc/yunohost/current_host', 'r') as f: old_domain = f.readline().rstrip() if not new_domain: return { 'current_main_domain': old_domain } if not new_domain: raise MoulinetteError(errno.EINVAL, m18n.n('new_domain_required')) if new_domain not in domain_list(auth)['domains']: domain_add(auth, new_domain) os.system('rm /etc/ssl/private/yunohost_key.pem') os.system('rm /etc/ssl/certs/yunohost_crt.pem') command_list = [ 'ln -s /etc/yunohost/certs/%s/key.pem /etc/ssl/private/yunohost_key.pem' % new_domain, 'ln -s /etc/yunohost/certs/%s/crt.pem /etc/ssl/certs/yunohost_crt.pem' % new_domain, 'echo %s > /etc/yunohost/current_host' % new_domain, ] for command in command_list: if os.system(command) != 0: raise MoulinetteError(errno.EPERM, m18n.n('maindomain_change_failed')) if dyndns and len(new_domain.split('.')) >= 3: try: r = requests.get('https://dyndns.yunohost.org/domains') except requests.ConnectionError: pass else: dyndomains = json.loads(r.text) dyndomain = '.'.join(new_domain.split('.')[1:]) if dyndomain in dyndomains: dyndns_subscribe(domain=new_domain) try: with open('/etc/yunohost/installed', 'r') as f: service_regen_conf() except IOError: pass logger.success(m18n.n('maindomain_changed'))
def app_makedefault(auth, app, domain=None): """ Redirect domain root to an app Keyword argument: app domain """ from yunohost.domain import domain_list if not _is_installed(app): raise MoulinetteError(errno.EINVAL, m18n.n('app_not_installed', app)) with open(apps_setting_path + app +'/settings.yml') as f: app_settings = yaml.load(f) app_domain = app_settings['domain'] app_path = app_settings['path'] if domain is None: domain = app_domain elif domain not in domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) if '/' in app_map(raw=True)[domain]: raise MoulinetteError(errno.EEXIST, m18n.n('app_location_already_used')) try: with open('/etc/ssowat/conf.json.persistent') as json_conf: ssowat_conf = json.loads(str(json_conf.read())) except IOError: ssowat_conf = {} if 'redirected_urls' not in ssowat_conf: ssowat_conf['redirected_urls'] = {} ssowat_conf['redirected_urls'][domain +'/'] = app_domain + app_path with open('/etc/ssowat/conf.json.persistent', 'w+') as f: json.dump(ssowat_conf, f, sort_keys=True, indent=4) os.system('chmod 644 /etc/ssowat/conf.json.persistent') msignals.display(m18n.n('ssowat_conf_updated'), 'success')
def app_checkurl(auth, url, app=None): """ Check availability of a web path Keyword argument: url -- Url to check app -- Write domain & path to app settings for further checks """ from yunohost.domain import domain_list if "https://" == url[:8]: url = url[8:] elif "http://" == url[:7]: url = url[7:] if url[-1:] != '/': url = url + '/' domain = url[:url.index('/')] path = url[url.index('/'):] if path[-1:] != '/': path = path + '/' apps_map = app_map(raw=True) if domain not in domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) if domain in apps_map: if path in apps_map[domain]: raise MoulinetteError(errno.EINVAL, m18n.n('app_location_already_used')) for app_path, v in apps_map[domain].items(): if app_path in path and app_path.count('/') < path.count('/'): raise MoulinetteError(errno.EPERM, m18n.n('app_location_install_failed')) if app is not None: app_setting(app, 'domain', value=domain) app_setting(app, 'path', value=path)
def user_create(auth, username, firstname, lastname, mail, password, mailbox_quota=0): """ Create user Keyword argument: firstname lastname username -- Must be unique mail -- Main mail address must be unique password mailbox_quota -- Mailbox size quota """ import pwd from yunohost.domain import domain_list from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf # Validate uniqueness of username and mail in LDAP auth.validate_uniqueness({ 'uid' : username, 'mail' : mail }) # Validate uniqueness of username in system users try: pwd.getpwnam(username) except KeyError: pass else: raise MoulinetteError(errno.EEXIST, m18n.n('system_username_exists')) # Check that the mail domain exists if mail[mail.find('@')+1:] not in domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('mail_domain_unknown', domain=mail[mail.find('@')+1:])) # Get random UID/GID uid_check = gid_check = 0 while uid_check == 0 and gid_check == 0: uid = str(random.randint(200, 99999)) uid_check = os.system("getent passwd %s" % uid) gid_check = os.system("getent group %s" % uid) # Adapt values for LDAP fullname = '%s %s' % (firstname, lastname) rdn = 'uid=%s,ou=users' % username char_set = string.ascii_uppercase + string.digits salt = ''.join(random.sample(char_set,8)) salt = '$1$' + salt + '$' user_pwd = '{CRYPT}' + crypt.crypt(str(password), salt) attr_dict = { 'objectClass' : ['mailAccount', 'inetOrgPerson', 'posixAccount'], 'givenName' : firstname, 'sn' : lastname, 'displayName' : fullname, 'cn' : fullname, 'uid' : username, 'mail' : mail, 'maildrop' : username, 'mailuserquota' : mailbox_quota, 'userPassword' : user_pwd, 'gidNumber' : uid, 'uidNumber' : uid, 'homeDirectory' : '/home/' + username, 'loginShell' : '/bin/false' } # If it is the first user, add some aliases if not auth.search(base='ou=users,dc=yunohost,dc=org', filter='uid=*'): with open('/etc/yunohost/current_host') as f: main_domain = f.readline().rstrip() aliases = [ 'root@'+ main_domain, 'admin@'+ main_domain, 'webmaster@'+ main_domain, 'postmaster@'+ main_domain, ] attr_dict['mail'] = [ attr_dict['mail'] ] + aliases # If exists, remove the redirection from the SSO try: with open('/etc/ssowat/conf.json.persistent') as json_conf: ssowat_conf = json.loads(str(json_conf.read())) if 'redirected_urls' in ssowat_conf and '/' in ssowat_conf['redirected_urls']: del ssowat_conf['redirected_urls']['/'] with open('/etc/ssowat/conf.json.persistent', 'w+') as f: json.dump(ssowat_conf, f, sort_keys=True, indent=4) except IOError: pass if auth.add(rdn, attr_dict): # Invalidate passwd to take user creation into account subprocess.call(['nscd', '-i', 'passwd']) # Update SFTP user group memberlist = auth.search(filter='cn=sftpusers', attrs=['memberUid'])[0]['memberUid'] memberlist.append(username) if auth.update('cn=sftpusers,ou=groups', { 'memberUid': memberlist }): try: # Attempt to create user home folder subprocess.check_call( ['su', '-', username, '-c', "''"]) except subprocess.CalledProcessError: if not os.path.isdir('/home/{0}'.format(username)): logger.warning(m18n.n('user_home_creation_failed'), exc_info=1) app_ssowatconf(auth) #TODO: Send a welcome mail to user logger.success(m18n.n('user_created')) hook_callback('post_user_create', args=[username, mail, password, firstname, lastname]) return { 'fullname' : fullname, 'username' : username, 'mail' : mail } raise MoulinetteError(169, m18n.n('user_creation_failed'))
def user_update(auth, username, firstname=None, lastname=None, mail=None, change_password=None, add_mailforward=None, remove_mailforward=None, add_mailalias=None, remove_mailalias=None, mailbox_quota=None): """ Update user informations Keyword argument: lastname mail firstname add_mailalias -- Mail aliases to add remove_mailforward -- Mailforward addresses to remove username -- Username of user to update add_mailforward -- Mailforward addresses to add change_password -- New password to set remove_mailalias -- Mail aliases to remove """ from yunohost.domain import domain_list from yunohost.app import app_ssowatconf attrs_to_fetch = ['givenName', 'sn', 'mail', 'maildrop'] new_attr_dict = {} domains = domain_list(auth)['domains'] # Populate user informations result = auth.search(base='ou=users,dc=yunohost,dc=org', filter='uid=' + username, attrs=attrs_to_fetch) if not result: raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown', user=username)) user = result[0] # Get modifications from arguments if firstname: new_attr_dict['givenName'] = firstname # TODO: Validate new_attr_dict['cn'] = new_attr_dict['displayName'] = firstname + ' ' + user['sn'][0] if lastname: new_attr_dict['sn'] = lastname # TODO: Validate new_attr_dict['cn'] = new_attr_dict['displayName'] = user['givenName'][0] + ' ' + lastname if lastname and firstname: new_attr_dict['cn'] = new_attr_dict['displayName'] = firstname + ' ' + lastname if change_password: char_set = string.ascii_uppercase + string.digits salt = ''.join(random.sample(char_set,8)) salt = '$1$' + salt + '$' new_attr_dict['userPassword'] = '******' + crypt.crypt(str(change_password), salt) if mail: auth.validate_uniqueness({ 'mail': mail }) if mail[mail.find('@')+1:] not in domains: raise MoulinetteError(errno.EINVAL, m18n.n('mail_domain_unknown', domain=mail[mail.find('@')+1:])) del user['mail'][0] new_attr_dict['mail'] = [mail] + user['mail'] if add_mailalias: if not isinstance(add_mailalias, list): add_mailalias = [ add_mailalias ] for mail in add_mailalias: auth.validate_uniqueness({ 'mail': mail }) if mail[mail.find('@')+1:] not in domains: raise MoulinetteError(errno.EINVAL, m18n.n('mail_domain_unknown', domain=mail[mail.find('@')+1:])) user['mail'].append(mail) new_attr_dict['mail'] = user['mail'] if remove_mailalias: if not isinstance(remove_mailalias, list): remove_mailalias = [ remove_mailalias ] for mail in remove_mailalias: if len(user['mail']) > 1 and mail in user['mail'][1:]: user['mail'].remove(mail) else: raise MoulinetteError(errno.EINVAL, m18n.n('mail_alias_remove_failed', mail=mail)) new_attr_dict['mail'] = user['mail'] if add_mailforward: if not isinstance(add_mailforward, list): add_mailforward = [ add_mailforward ] for mail in add_mailforward: if mail in user['maildrop'][1:]: continue user['maildrop'].append(mail) new_attr_dict['maildrop'] = user['maildrop'] if remove_mailforward: if not isinstance(remove_mailforward, list): remove_mailforward = [ remove_mailforward ] for mail in remove_mailforward: if len(user['maildrop']) > 1 and mail in user['maildrop'][1:]: user['maildrop'].remove(mail) else: raise MoulinetteError(errno.EINVAL, m18n.n('mail_forward_remove_failed', mail=mail)) new_attr_dict['maildrop'] = user['maildrop'] if mailbox_quota is not None: new_attr_dict['mailuserquota'] = mailbox_quota if auth.update('uid=%s,ou=users' % username, new_attr_dict): logger.success(m18n.n('user_updated')) app_ssowatconf(auth) return user_info(auth, username) else: raise MoulinetteError(169, m18n.n('user_update_failed'))
def user_create( operation_logger, username, firstname, lastname, domain, password, mailbox_quota="0", mail=None, ): from yunohost.domain import domain_list, _get_maindomain from yunohost.hook import hook_callback from yunohost.utils.password import assert_password_is_strong_enough from yunohost.utils.ldap import _get_ldap_interface # Ensure sufficiently complex password assert_password_is_strong_enough("user", password) if mail is not None: logger.warning( "Packagers ! Using --mail in 'yunohost user create' is deprecated ... please use --domain instead." ) domain = mail.split("@")[-1] # Validate domain used for email address/xmpp account if domain is None: if msettings.get("interface") == "api": raise YunohostValidationError( "Invalid usage, you should specify a domain argument") else: # On affiche les differents domaines possibles msignals.display(m18n.n("domains_available")) for domain in domain_list()["domains"]: msignals.display("- {}".format(domain)) maindomain = _get_maindomain() domain = msignals.prompt( m18n.n("ask_user_domain") + " (default: %s)" % maindomain) if not domain: domain = maindomain # Check that the domain exists if domain not in domain_list()["domains"]: raise YunohostValidationError("domain_name_unknown", domain=domain) mail = username + "@" + domain ldap = _get_ldap_interface() if username in user_list()["users"]: raise YunohostValidationError("user_already_exists", user=username) # Validate uniqueness of username and mail in LDAP try: ldap.validate_uniqueness({ "uid": username, "mail": mail, "cn": username }) except Exception as e: raise YunohostValidationError("user_creation_failed", user=username, error=e) # Validate uniqueness of username in system users all_existing_usernames = {x.pw_name for x in pwd.getpwall()} if username in all_existing_usernames: raise YunohostValidationError("system_username_exists") main_domain = _get_maindomain() aliases = [ "root@" + main_domain, "admin@" + main_domain, "webmaster@" + main_domain, "postmaster@" + main_domain, "abuse@" + main_domain, ] if mail in aliases: raise YunohostValidationError("mail_unavailable") operation_logger.start() # Get random UID/GID all_uid = {str(x.pw_uid) for x in pwd.getpwall()} all_gid = {str(x.gr_gid) for x in grp.getgrall()} uid_guid_found = False while not uid_guid_found: # LXC uid number is limited to 65536 by default uid = str(random.randint(1001, 65000)) uid_guid_found = uid not in all_uid and uid not in all_gid # Adapt values for LDAP fullname = "%s %s" % (firstname, lastname) attr_dict = { "objectClass": [ "mailAccount", "inetOrgPerson", "posixAccount", "userPermissionYnh", ], "givenName": [firstname], "sn": [lastname], "displayName": [fullname], "cn": [fullname], "uid": [username], "mail": mail, # NOTE: this one seems to be already a list "maildrop": [username], "mailuserquota": [mailbox_quota], "userPassword": [_hash_user_password(password)], "gidNumber": [uid], "uidNumber": [uid], "homeDirectory": ["/home/" + username], "loginShell": ["/bin/bash"], } # If it is the first user, add some aliases if not ldap.search(base="ou=users,dc=yunohost,dc=org", filter="uid=*"): attr_dict["mail"] = [attr_dict["mail"]] + aliases try: ldap.add("uid=%s,ou=users" % username, attr_dict) except Exception as e: raise YunohostError("user_creation_failed", user=username, error=e) # Invalidate passwd and group to take user and group creation into account subprocess.call(["nscd", "-i", "passwd"]) subprocess.call(["nscd", "-i", "group"]) try: # Attempt to create user home folder subprocess.check_call(["mkhomedir_helper", username]) except subprocess.CalledProcessError: if not os.path.isdir("/home/{0}".format(username)): logger.warning(m18n.n("user_home_creation_failed"), exc_info=1) try: subprocess.check_call( ["setfacl", "-m", "g:all_users:---", "/home/%s" % username]) except subprocess.CalledProcessError: logger.warning("Failed to protect /home/%s" % username, exc_info=1) # Create group for user and add to group 'all_users' user_group_create(groupname=username, gid=uid, primary_group=True, sync_perm=False) user_group_update(groupname="all_users", add=username, force=True, sync_perm=True) # Trigger post_user_create hooks env_dict = { "YNH_USER_USERNAME": username, "YNH_USER_MAIL": mail, "YNH_USER_PASSWORD": password, "YNH_USER_FIRSTNAME": firstname, "YNH_USER_LASTNAME": lastname, } hook_callback("post_user_create", args=[username, mail], env=env_dict) # TODO: Send a welcome mail to user logger.success(m18n.n("user_created")) return {"fullname": fullname, "username": username, "mail": mail}
def user_update( operation_logger, username, firstname=None, lastname=None, mail=None, change_password=None, add_mailforward=None, remove_mailforward=None, add_mailalias=None, remove_mailalias=None, mailbox_quota=None, ): """ Update user informations Keyword argument: lastname mail firstname add_mailalias -- Mail aliases to add remove_mailforward -- Mailforward addresses to remove username -- Username of user to update add_mailforward -- Mailforward addresses to add change_password -- New password to set remove_mailalias -- Mail aliases to remove """ from yunohost.domain import domain_list, _get_maindomain from yunohost.app import app_ssowatconf from yunohost.utils.password import assert_password_is_strong_enough from yunohost.utils.ldap import _get_ldap_interface from yunohost.hook import hook_callback domains = domain_list()["domains"] # Populate user informations ldap = _get_ldap_interface() attrs_to_fetch = ["givenName", "sn", "mail", "maildrop"] result = ldap.search( base="ou=users,dc=yunohost,dc=org", filter="uid=" + username, attrs=attrs_to_fetch, ) if not result: raise YunohostValidationError("user_unknown", user=username) user = result[0] env_dict = {"YNH_USER_USERNAME": username} # Get modifications from arguments new_attr_dict = {} if firstname: new_attr_dict["givenName"] = [firstname] # TODO: Validate new_attr_dict["cn"] = new_attr_dict["displayName"] = [ firstname + " " + user["sn"][0] ] env_dict["YNH_USER_FIRSTNAME"] = firstname if lastname: new_attr_dict["sn"] = [lastname] # TODO: Validate new_attr_dict["cn"] = new_attr_dict["displayName"] = [ user["givenName"][0] + " " + lastname ] env_dict["YNH_USER_LASTNAME"] = lastname if lastname and firstname: new_attr_dict["cn"] = new_attr_dict["displayName"] = [ firstname + " " + lastname ] # change_password is None if user_update is not called to change the password if change_password is not None: # when in the cli interface if the option to change the password is called # without a specified value, change_password will be set to the const 0. # In this case we prompt for the new password. if msettings.get("interface") == "cli" and not change_password: change_password = msignals.prompt(m18n.n("ask_password"), True, True) # Ensure sufficiently complex password assert_password_is_strong_enough("user", change_password) new_attr_dict["userPassword"] = [_hash_user_password(change_password)] env_dict["YNH_USER_PASSWORD"] = change_password if mail: main_domain = _get_maindomain() aliases = [ "root@" + main_domain, "admin@" + main_domain, "webmaster@" + main_domain, "postmaster@" + main_domain, ] try: ldap.validate_uniqueness({"mail": mail}) except Exception as e: raise YunohostValidationError("user_update_failed", user=username, error=e) if mail[mail.find("@") + 1:] not in domains: raise YunohostValidationError("mail_domain_unknown", domain=mail[mail.find("@") + 1:]) if mail in aliases: raise YunohostValidationError("mail_unavailable") del user["mail"][0] new_attr_dict["mail"] = [mail] + user["mail"] if add_mailalias: if not isinstance(add_mailalias, list): add_mailalias = [add_mailalias] for mail in add_mailalias: try: ldap.validate_uniqueness({"mail": mail}) except Exception as e: raise YunohostValidationError("user_update_failed", user=username, error=e) if mail[mail.find("@") + 1:] not in domains: raise YunohostValidationError("mail_domain_unknown", domain=mail[mail.find("@") + 1:]) user["mail"].append(mail) new_attr_dict["mail"] = user["mail"] if remove_mailalias: if not isinstance(remove_mailalias, list): remove_mailalias = [remove_mailalias] for mail in remove_mailalias: if len(user["mail"]) > 1 and mail in user["mail"][1:]: user["mail"].remove(mail) else: raise YunohostValidationError("mail_alias_remove_failed", mail=mail) new_attr_dict["mail"] = user["mail"] if "mail" in new_attr_dict: env_dict["YNH_USER_MAILS"] = ",".join(new_attr_dict["mail"]) if add_mailforward: if not isinstance(add_mailforward, list): add_mailforward = [add_mailforward] for mail in add_mailforward: if mail in user["maildrop"][1:]: continue user["maildrop"].append(mail) new_attr_dict["maildrop"] = user["maildrop"] if remove_mailforward: if not isinstance(remove_mailforward, list): remove_mailforward = [remove_mailforward] for mail in remove_mailforward: if len(user["maildrop"]) > 1 and mail in user["maildrop"][1:]: user["maildrop"].remove(mail) else: raise YunohostValidationError("mail_forward_remove_failed", mail=mail) new_attr_dict["maildrop"] = user["maildrop"] if "maildrop" in new_attr_dict: env_dict["YNH_USER_MAILFORWARDS"] = ",".join(new_attr_dict["maildrop"]) if mailbox_quota is not None: new_attr_dict["mailuserquota"] = [mailbox_quota] env_dict["YNH_USER_MAILQUOTA"] = mailbox_quota operation_logger.start() try: ldap.update("uid=%s,ou=users" % username, new_attr_dict) except Exception as e: raise YunohostError("user_update_failed", user=username, error=e) # Trigger post_user_update hooks hook_callback("post_user_update", env=env_dict) logger.success(m18n.n("user_updated")) app_ssowatconf() return user_info(username)
def tools_diagnosis(auth, private=False): """ Return global info about current yunohost instance to help debugging """ diagnosis = OrderedDict(); # Debian release try: with open('/etc/debian_version', 'r') as f: debian_version = f.read().rstrip() except IOError as e: logger.warning(m18n.n('diagnosis_debian_version_error', error=format(e)), exc_info=1) else: diagnosis['host'] = "Debian %s" % debian_version # Kernel version try: with open('/proc/sys/kernel/osrelease', 'r') as f: kernel_version = f.read().rstrip() except IOError as e: logger.warning(m18n.n('diagnosis_kernel_version_error', error=format(e)), exc_info=1) else: diagnosis['kernel'] = kernel_version # Packages version diagnosis['packages'] = ynh_packages_version() # Server basic monitoring diagnosis['system'] = OrderedDict() try: disks = monitor_disk(units=['filesystem'], human_readable=True) except MoulinetteError as e: logger.warning(m18n.n('diagnosis_monitor_disk_error', error=format(e)), exc_info=1) else: diagnosis['system']['disks'] = {} for disk in disks: diagnosis['system']['disks'][disk] = 'Mounted on %s, %s (%s free)' % ( disks[disk]['mnt_point'], disks[disk]['size'], disks[disk]['avail'] ) try: system = monitor_system(units=['cpu', 'memory'], human_readable=True) except MoulinetteError as e: logger.warning(m18n.n('diagnosis_monitor_system_error', error=format(e)), exc_info=1) else: diagnosis['system']['memory'] = { 'ram' : '%s (%s free)' % (system['memory']['ram']['total'], system['memory']['ram']['free']), 'swap' : '%s (%s free)' % (system['memory']['swap']['total'], system['memory']['swap']['free']), } # Services status services = service_status() diagnosis['services'] = {} for service in services: diagnosis['services'][service] = "%s (%s)" % (services[service]['status'], services[service]['loaded']) # YNH Applications try: applications = app_list()['apps'] except MoulinetteError as e: diagnosis['applications'] = m18n.n('diagnosis_no_apps') else: diagnosis['applications'] = {} for application in applications: if application['installed']: diagnosis['applications'][application['id']] = application['label'] if application['label'] else application['name'] # Private data if private: diagnosis['private'] = OrderedDict() # Public IP diagnosis['private']['public_ip'] = {} try: diagnosis['private']['public_ip']['IPv4'] = get_public_ip(4) except MoulinetteError as e: pass try: diagnosis['private']['public_ip']['IPv6'] = get_public_ip(6) except MoulinetteError as e: pass # Domains diagnosis['private']['domains'] = domain_list(auth)['domains'] return diagnosis
def _validate_and_sanitize_permission_url(url, app_base_path, app): """ Check and normalize the urls passed for all permissions Also check that the Regex is valid As documented in the 'ynh_permission_create' helper: If provided, 'url' is assumed to be relative to the app domain/path if they start with '/'. For example: / -> domain.tld/app /admin -> domain.tld/app/admin domain.tld/app/api -> domain.tld/app/api domain.tld -> domain.tld 'url' can be later treated as a regex if it starts with "re:". For example: re:/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ re:domain.tld/app/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ We can also have less-trivial regexes like: re:^/api/.*|/scripts/api.js$ """ from yunohost.domain import domain_list from yunohost.app import _assert_no_conflicting_apps domains = domain_list()["domains"] # # Regexes # def validate_regex(regex): if "%" in regex: logger.warning( "/!\\ Packagers! You are probably using a lua regex. You should use a PCRE regex instead." ) return try: re.compile(regex) except Exception: raise YunohostError("invalid_regex", regex=regex) if url.startswith("re:"): # regex without domain # we check for the first char after 're:' if url[3] in ["/", "^", "\\"]: validate_regex(url[3:]) return url # regex with domain if "/" not in url: raise YunohostError("regex_with_only_domain") domain, path = url[3:].split("/", 1) path = "/" + path if domain.replace("%", "").replace("\\", "") not in domains: raise YunohostError("domain_name_unknown", domain=domain) validate_regex(path) return "re:" + domain + path # # "Regular" URIs # def split_domain_path(url): url = url.strip("/") (domain, path) = url.split("/", 1) if "/" in url else (url, "/") if path != "/": path = "/" + path return (domain, path) # uris without domain if url.startswith("/"): # if url is for example /admin/ # we want sanitized_url to be: /admin # and (domain, path) to be : (domain.tld, /app/admin) sanitized_url = "/" + url.strip("/") domain, path = split_domain_path(app_base_path) path = "/" + path.strip("/") + sanitized_url # uris with domain else: # if url is for example domain.tld/wat/ # we want sanitized_url to be: domain.tld/wat # and (domain, path) to be : (domain.tld, /wat) domain, path = split_domain_path(url) sanitized_url = domain + path if domain not in domains: raise YunohostError("domain_name_unknown", domain=domain) _assert_no_conflicting_apps(domain, path, ignore_app=app) return sanitized_url
def setup_function(function): clean_user_groups_permission() global maindomain global other_domains maindomain = _get_maindomain() markers = { m.name: { "args": m.args, "kwargs": m.kwargs } for m in function.__dict__.get("pytestmark", []) } if "other_domains" in markers: other_domains = [ "domain_%s.dev" % string.ascii_lowercase[number] for number in range(markers["other_domains"]["kwargs"]["number"]) ] for domain in other_domains: if domain not in domain_list()["domains"]: domain_add(domain) # Dirty patch of DNS resolution. Force the DNS to 127.0.0.1 address even if dnsmasq have the public address. # Mainly used for 'can_access_webpage' function dns_cache = {(maindomain, 443, 0, 1): [(2, 1, 6, "", ("127.0.0.1", 443))]} for domain in other_domains: dns_cache[(domain, 443, 0, 1)] = [(2, 1, 6, "", ("127.0.0.1", 443))] def new_getaddrinfo(*args): try: return dns_cache[args] except KeyError: res = prv_getaddrinfo(*args) dns_cache[args] = res return res socket.getaddrinfo = new_getaddrinfo user_create("alice", "Alice", "White", maindomain, dummy_password) user_create("bob", "Bob", "Snow", maindomain, dummy_password) _permission_create_with_dummy_app( permission="wiki.main", url="/", additional_urls=["/whatever", "/idontnow"], auth_header=False, label="Wiki", show_tile=True, allowed=["all_users"], protected=False, sync_perm=False, domain=maindomain, path="/wiki", ) _permission_create_with_dummy_app( permission="blog.main", url="/", auth_header=True, show_tile=False, protected=False, sync_perm=False, allowed=["alice"], domain=maindomain, path="/blog", ) _permission_create_with_dummy_app(permission="blog.api", allowed=["visitors"], protected=True, sync_perm=True)
def run(self): all_domains = domain_list()["domains"] domains_to_check = [] for domain in all_domains: # If the diagnosis location ain't defined, can't do diagnosis, # probably because nginx conf manually modified... nginx_conf = "/etc/nginx/conf.d/%s.conf" % domain if ".well-known/ynh-diagnosis/" not in read_file(nginx_conf): yield dict( meta={"domain": domain}, status="WARNING", summary="diagnosis_http_nginx_conf_not_up_to_date", details=[ "diagnosis_http_nginx_conf_not_up_to_date_details" ], ) else: domains_to_check.append(domain) self.nonce = "".join( random.choice("0123456789abcedf") for i in range(16)) os.system("rm -rf /tmp/.well-known/ynh-diagnosis/") os.system("mkdir -p /tmp/.well-known/ynh-diagnosis/") os.system("touch /tmp/.well-known/ynh-diagnosis/%s" % self.nonce) if not domains_to_check: return # To perform hairpinning test, we gotta make sure that port forwarding # is working and therefore we'll do it only if at least one ipv4 domain # works. self.do_hairpinning_test = False ipversions = [] ipv4 = Diagnoser.get_cached_report("ip", item={"test": "ipv4"}) or {} if ipv4.get("status") == "SUCCESS": ipversions.append(4) # To be discussed: we could also make this check dependent on the # existence of an AAAA record... ipv6 = Diagnoser.get_cached_report("ip", item={"test": "ipv6"}) or {} if ipv6.get("status") == "SUCCESS": ipversions.append(6) for item in self.test_http(domains_to_check, ipversions): yield item # If at least one domain is correctly exposed to the outside, # attempt to diagnose hairpinning situations. On network with # hairpinning issues, the server may be correctly exposed on the # outside, but from the outside, it will be as if the port forwarding # was not configured... Hence, calling for example # "curl --head the.global.ip" will simply timeout... if self.do_hairpinning_test: global_ipv4 = ipv4.get("data", {}).get("global", None) if global_ipv4: try: requests.head("http://" + global_ipv4, timeout=5) except requests.exceptions.Timeout: yield dict( meta={"test": "hairpinning"}, status="WARNING", summary="diagnosis_http_hairpinning_issue", details=["diagnosis_http_hairpinning_issue_details"], ) except Exception: # Well I dunno what to do if that's another exception # type... That'll most probably *not* be an hairpinning # issue but something else super weird ... pass
def user_create(auth, username, firstname, lastname, mail, password): """ Create user Keyword argument: firstname lastname username -- Must be unique mail -- Main mail address must be unique password """ from yunohost.domain import domain_list from yunohost.hook import hook_callback # Validate password length if len(password) < 4: raise MoulinetteError(errno.EINVAL, m18n.n('password_too_short')) auth.validate_uniqueness({ 'uid' : username, 'mail' : mail }) if mail[mail.find('@')+1:] not in domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('mail_domain_unknown') % mail[mail.find('@')+1:]) # Get random UID/GID uid_check = gid_check = 0 while uid_check == 0 and gid_check == 0: uid = str(random.randint(200, 99999)) uid_check = os.system("getent passwd %s" % uid) gid_check = os.system("getent group %s" % uid) # Adapt values for LDAP fullname = '%s %s' % (firstname, lastname) rdn = 'uid=%s,ou=users' % username char_set = string.ascii_uppercase + string.digits salt = ''.join(random.sample(char_set,8)) salt = '$1$' + salt + '$' pwd = '{CRYPT}' + crypt.crypt(str(password), salt) attr_dict = { 'objectClass' : ['mailAccount', 'inetOrgPerson', 'posixAccount'], 'givenName' : firstname, 'sn' : lastname, 'displayName' : fullname, 'cn' : fullname, 'uid' : username, 'mail' : mail, 'maildrop' : username, 'userPassword' : pwd, 'gidNumber' : uid, 'uidNumber' : uid, 'homeDirectory' : '/home/' + username, 'loginShell' : '/bin/false' } # If it is the first user, add some aliases if not auth.search(base='ou=users,dc=yunohost,dc=org', filter='uid=*'): with open('/etc/yunohost/current_host') as f: main_domain = f.readline().rstrip() aliases = [ 'root@'+ main_domain, 'admin@'+ main_domain, 'webmaster@'+ main_domain, 'postmaster@'+ main_domain, ] attr_dict['mail'] = [ attr_dict['mail'] ] + aliases # If exists, remove the redirection from the SSO try: with open('/etc/ssowat/conf.json.persistent') as json_conf: ssowat_conf = json.loads(str(json_conf.read())) if 'redirected_urls' in ssowat_conf and '/' in ssowat_conf['redirected_urls']: del ssowat_conf['redirected_urls']['/'] with open('/etc/ssowat/conf.json.persistent', 'w+') as f: json.dump(ssowat_conf, f, sort_keys=True, indent=4) except IOError: pass if auth.add(rdn, attr_dict): # Update SFTP user group memberlist = auth.search(filter='cn=sftpusers', attrs=['memberUid'])[0]['memberUid'] memberlist.append(username) if auth.update('cn=sftpusers,ou=groups', { 'memberUid': memberlist }): os.system("su - %s -c ''" % username) os.system('yunohost app ssowatconf > /dev/null 2>&1') #TODO: Send a welcome mail to user msignals.display(m18n.n('user_created'), 'success') hook_callback('post_user_create', [username, mail, password, firstname, lastname]) return { 'fullname' : fullname, 'username' : username, 'mail' : mail } raise MoulinetteError(169, m18n.n('user_creation_failed'))
def tools_maindomain(auth, old_domain=None, new_domain=None, dyndns=False): """ Main domain change tool Keyword argument: new_domain old_domain """ from yunohost.domain import domain_add, domain_list from yunohost.dyndns import dyndns_subscribe if not old_domain: with open('/etc/yunohost/current_host', 'r') as f: old_domain = f.readline().rstrip() if not new_domain: return { 'current_main_domain': old_domain } if not new_domain: raise MoulinetteError(errno.EINVAL, m18n.n('new_domain_required')) if new_domain not in domain_list(auth)['domains']: domain_add(auth, new_domain) config_files = [ '/etc/postfix/main.cf', '/etc/metronome/metronome.cfg.lua', '/etc/dovecot/dovecot.conf', '/usr/share/yunohost/yunohost-config/others/startup', '/etc/amavis/conf.d/05-node_id', '/etc/amavis/conf.d/50-user' ] config_dir = [] for dir in config_dir: for file in os.listdir(dir): config_files.append(dir + '/' + file) for file in config_files: with open(file, "r") as sources: lines = sources.readlines() with open(file, "w") as sources: for line in lines: sources.write(re.sub(r''+ old_domain +'', new_domain, line)) ## Update DNS zone file for old and new domains main_subdomains = ['pubsub', 'muc', 'vjud'] try: with open('/var/lib/bind/%s.zone' % old_domain, 'r') as f: old_zone = f.read() except IOError: pass else: # Remove unneeded subdomains entries for sub in main_subdomains: old_zone = re.sub( r'^({sub}.{domain}.|{sub})[\ \t]+(IN).*$[\n]?'.format( sub=sub, domain=old_domain), '', old_zone, 1, re.MULTILINE) with open('/var/lib/bind/%s.zone' % old_domain, 'w') as f: f.write(old_zone) try: with open('/var/lib/bind/%s.zone' % new_domain, 'r') as f: new_zone = f.read() except IOError: msignals.display(m18n.n('domain_zone_not_found', new_domain), 'warning') else: # Add main subdomains entries for sub in main_subdomains: new_zone += '{sub} IN CNAME {domain}.\n'.format( sub=sub, domain=new_domain) with open('/var/lib/bind/%s.zone' % new_domain, 'w') as f: f.write(new_zone) os.system('rm /etc/ssl/private/yunohost_key.pem') os.system('rm /etc/ssl/certs/yunohost_crt.pem') command_list = [ 'rm -f /etc/nginx/conf.d/%s.d/yunohost_local.conf' % old_domain, 'cp /usr/share/yunohost/yunohost-config/nginx/yunohost_local.conf /etc/nginx/conf.d/%s.d/' % new_domain, 'ln -s /etc/yunohost/certs/%s/key.pem /etc/ssl/private/yunohost_key.pem' % new_domain, 'ln -s /etc/yunohost/certs/%s/crt.pem /etc/ssl/certs/yunohost_crt.pem' % new_domain, 'echo %s > /etc/yunohost/current_host' % new_domain, 'service metronome restart', 'service postfix restart', 'service dovecot restart', 'service amavis restart', 'service nginx restart', ] for command in command_list: if os.system(command) != 0: raise MoulinetteError(errno.EPERM, m18n.n('maindomain_change_failed')) if dyndns and len(new_domain.split('.')) >= 3: try: r = requests.get('https://dyndns.yunohost.org/domains') except ConnectionError: pass else: dyndomains = json.loads(r.text) dyndomain = '.'.join(new_domain.split('.')[1:]) if dyndomain in dyndomains: dyndns_subscribe(domain=new_domain) msignals.display(m18n.n('maindomain_changed'), 'success')
def user_create(operation_logger, username, firstname, lastname, domain, password, mailbox_quota="0", mail=None): from yunohost.domain import domain_list, _get_maindomain from yunohost.hook import hook_callback from yunohost.utils.password import assert_password_is_strong_enough from yunohost.utils.ldap import _get_ldap_interface # Ensure sufficiently complex password assert_password_is_strong_enough("user", password) if mail is not None: logger.warning( "Packagers ! Using --mail in 'yunohost user create' is deprecated ... please use --domain instead." ) domain = mail.split("@")[-1] # Validate domain used for email address/xmpp account if domain is None: if msettings.get('interface') == 'api': raise YunohostError('Invalide usage, specify domain argument') else: # On affiche les differents domaines possibles msignals.display(m18n.n('domains_available')) for domain in domain_list()['domains']: msignals.display("- {}".format(domain)) maindomain = _get_maindomain() domain = msignals.prompt( m18n.n('ask_user_domain') + ' (default: %s)' % maindomain) if not domain: domain = maindomain # Check that the domain exists if domain not in domain_list()['domains']: raise YunohostError('domain_name_unknown', domain=domain) mail = username + '@' + domain ldap = _get_ldap_interface() if username in user_list()["users"]: raise YunohostError("user_already_exists", user=username) # Validate uniqueness of username and mail in LDAP try: ldap.validate_uniqueness({ 'uid': username, 'mail': mail, 'cn': username }) except Exception as e: raise YunohostError('user_creation_failed', user=username, error=e) # Validate uniqueness of username in system users all_existing_usernames = {x.pw_name for x in pwd.getpwall()} if username in all_existing_usernames: raise YunohostError('system_username_exists') main_domain = _get_maindomain() aliases = [ 'root@' + main_domain, 'admin@' + main_domain, 'webmaster@' + main_domain, 'postmaster@' + main_domain, 'abuse@' + main_domain, ] if mail in aliases: raise YunohostError('mail_unavailable') operation_logger.start() # Get random UID/GID all_uid = {str(x.pw_uid) for x in pwd.getpwall()} all_gid = {str(x.gr_gid) for x in grp.getgrall()} uid_guid_found = False while not uid_guid_found: # LXC uid number is limited to 65536 by default uid = str(random.randint(1001, 65000)) uid_guid_found = uid not in all_uid and uid not in all_gid # Adapt values for LDAP fullname = '%s %s' % (firstname, lastname) attr_dict = { 'objectClass': ['mailAccount', 'inetOrgPerson', 'posixAccount', 'userPermissionYnh'], 'givenName': [firstname], 'sn': [lastname], 'displayName': [fullname], 'cn': [fullname], 'uid': [username], 'mail': mail, # NOTE: this one seems to be already a list 'maildrop': [username], 'mailuserquota': [mailbox_quota], 'userPassword': [_hash_user_password(password)], 'gidNumber': [uid], 'uidNumber': [uid], 'homeDirectory': ['/home/' + username], 'loginShell': ['/bin/false'] } # If it is the first user, add some aliases if not ldap.search(base='ou=users,dc=yunohost,dc=org', filter='uid=*'): attr_dict['mail'] = [attr_dict['mail']] + aliases try: ldap.add('uid=%s,ou=users' % username, attr_dict) except Exception as e: raise YunohostError('user_creation_failed', user=username, error=e) # Invalidate passwd and group to take user and group creation into account subprocess.call(['nscd', '-i', 'passwd']) subprocess.call(['nscd', '-i', 'group']) try: # Attempt to create user home folder subprocess.check_call(["mkhomedir_helper", username]) except subprocess.CalledProcessError: if not os.path.isdir('/home/{0}'.format(username)): logger.warning(m18n.n('user_home_creation_failed'), exc_info=1) # Create group for user and add to group 'all_users' user_group_create(groupname=username, gid=uid, primary_group=True, sync_perm=False) user_group_update(groupname='all_users', add=username, force=True, sync_perm=True) # Trigger post_user_create hooks env_dict = { "YNH_USER_USERNAME": username, "YNH_USER_MAIL": mail, "YNH_USER_PASSWORD": password, "YNH_USER_FIRSTNAME": firstname, "YNH_USER_LASTNAME": lastname } hook_callback('post_user_create', args=[username, mail], env=env_dict) # TODO: Send a welcome mail to user logger.success(m18n.n('user_created')) return {'fullname': fullname, 'username': username, 'mail': mail}
def app_ssowatconf(auth): """ Regenerate SSOwat configuration file """ from yunohost.domain import domain_list from yunohost.user import user_list with open('/etc/yunohost/current_host', 'r') as f: main_domain = f.readline().rstrip() domains = domain_list(auth)['domains'] users = {} for user in user_list(auth)['users']: users[user['username']] = app_map(user=user['username']) skipped_urls = [] skipped_regex = [] unprotected_urls = [] unprotected_regex = [] protected_urls = [] protected_regex = [] redirected_regex = { main_domain +'/yunohost[\/]?$': 'https://'+ main_domain +'/yunohost/sso/' } redirected_urls ={} apps = {} try: apps_list = app_list()['apps'] except: apps_list = [] for app in apps_list: if _is_installed(app['id']): with open(apps_setting_path + app['id'] +'/settings.yml') as f: app_settings = yaml.load(f) if 'skipped_uris' in app_settings: for item in app_settings['skipped_uris'].split(','): if item[-1:] == '/': item = item[:-1] skipped_urls.append(app_settings['domain'] + app_settings['path'][:-1] + item) if 'skipped_regex' in app_settings: for item in app_settings['skipped_regex'].split(','): skipped_regex.append(item) if 'unprotected_uris' in app_settings: for item in app_settings['unprotected_uris'].split(','): if item[-1:] == '/': item = item[:-1] unprotected_urls.append(app_settings['domain'] + app_settings['path'][:-1] + item) if 'unprotected_regex' in app_settings: for item in app_settings['unprotected_regex'].split(','): unprotected_regex.append(item) if 'protected_uris' in app_settings: for item in app_settings['protected_uris'].split(','): if item[-1:] == '/': item = item[:-1] protected_urls.append(app_settings['domain'] + app_settings['path'][:-1] + item) if 'redirected_urls' in app_settings: redirected_urls.update(app_settings['redirected_urls']) if 'redirected_regex' in app_settings: redirected_regex.update(app_settings['redirected_regex']) for domain in domains: skipped_urls.extend(['/yunohost/admin', '/yunohost/api']) conf_dict = { 'portal_domain': main_domain, 'portal_path': '/yunohost/sso/', 'additional_headers': { 'Auth-User': '******', 'Remote-User': '******', 'Name': 'cn', 'Email': 'mail' }, 'domains': domains, 'skipped_urls': skipped_urls, 'unprotected_urls': unprotected_urls, 'protected_urls': protected_urls, 'skipped_regex': skipped_regex, 'unprotected_regex': unprotected_regex, 'protected_regex': protected_regex, 'redirected_urls': redirected_urls, 'redirected_regex': redirected_regex, 'users': users, } with open('/etc/ssowat/conf.json', 'w+') as f: json.dump(conf_dict, f, sort_keys=True, indent=4) msignals.display(m18n.n('ssowat_conf_generated'), 'success')
def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run=False, list_pending=False): """ Regenerate the configuration file(s) Keyword argument: names -- Categories to regenerate configuration of with_diff -- Show differences in case of configuration changes force -- Override all manual modifications in configuration files dry_run -- Show what would have been regenerated list_pending -- List pending configuration files and exit """ result = {} # Return the list of pending conf if list_pending: pending_conf = _get_pending_conf(names) if not with_diff: return pending_conf for category, conf_files in pending_conf.items(): for system_path, pending_path in conf_files.items(): pending_conf[category][system_path] = { 'pending_conf': pending_path, 'diff': _get_files_diff(system_path, pending_path, True), } return pending_conf if not dry_run: operation_logger.related_to = [('configuration', x) for x in names] if not names: operation_logger.name_parameter_override = 'all' elif len(names) != 1: operation_logger.name_parameter_override = str( len(operation_logger.related_to)) + '_categories' operation_logger.start() # Clean pending conf directory if os.path.isdir(PENDING_CONF_DIR): if not names: shutil.rmtree(PENDING_CONF_DIR, ignore_errors=True) else: for name in names: shutil.rmtree(os.path.join(PENDING_CONF_DIR, name), ignore_errors=True) else: filesystem.mkdir(PENDING_CONF_DIR, 0o755, True) # Format common hooks arguments common_args = [1 if force else 0, 1 if dry_run else 0] # Execute hooks for pre-regen pre_args = [ 'pre', ] + common_args def _pre_call(name, priority, path, args): # create the pending conf directory for the category category_pending_path = os.path.join(PENDING_CONF_DIR, name) filesystem.mkdir(category_pending_path, 0o755, True, uid='root') # return the arguments to pass to the script return pre_args + [ category_pending_path, ] ssh_explicitly_specified = isinstance(names, list) and "ssh" in names # By default, we regen everything if not names: names = hook_list('conf_regen', list_by='name', show_info=False)['hooks'] # Dirty hack for legacy code : avoid attempting to regen the conf for # glances because it got removed ... This is only needed *once* # during the upgrade from 3.7 to 3.8 because Yunohost will attempt to # regen glance's conf *before* it gets automatically removed from # services.yml (which will happens only during the regen-conf of # 'yunohost', so at the very end of the regen-conf cycle) Anyway, # this can be safely removed once we're in >= 4.0 if "glances" in names: names.remove("glances") # [Optimization] We compute and feed the domain list to the conf regen # hooks to avoid having to call "yunohost domain list" so many times which # ends up in wasted time (about 3~5 seconds per call on a RPi2) from yunohost.domain import domain_list env = {} # Well we can only do domain_list() if postinstall is done ... # ... but hooks that effectively need the domain list are only # called only after the 'installed' flag is set so that's all good, # though kinda tight-coupled to the postinstall logic :s if os.path.exists("/etc/yunohost/installed"): env["YNH_DOMAINS"] = " ".join(domain_list()["domains"]) pre_result = hook_callback('conf_regen', names, pre_callback=_pre_call, env=env) # Keep only the hook names with at least one success names = [ hook for hook, infos in pre_result.items() if any(result["state"] == "succeed" for result in infos.values()) ] # FIXME : what do in case of partial success/failure ... if not names: ret_failed = [ hook for hook, infos in pre_result.items() if any(result["state"] == "failed" for result in infos.values()) ] raise YunohostError('regenconf_failed', categories=', '.join(ret_failed)) # Set the processing method _regen = _process_regen_conf if not dry_run else lambda *a, **k: True operation_logger.related_to = [] # Iterate over categories and process pending conf for category, conf_files in _get_pending_conf(names).items(): if not dry_run: operation_logger.related_to.append(('configuration', category)) if dry_run: logger.debug( m18n.n('regenconf_pending_applying', category=category)) else: logger.debug( m18n.n('regenconf_dry_pending_applying', category=category)) conf_hashes = _get_conf_hashes(category) succeed_regen = {} failed_regen = {} # Here we are doing some weird legacy shit # The thing is, on some very old or specific setup, the sshd_config file # was absolutely not managed by the regenconf ... # But we now want to make sure that this file is managed. # However, we don't want to overwrite a specific custom sshd_config # which may make the admin unhappy ... # So : if the hash for this file does not exists, we set the hash as the # hash of the pending configuration ... # That way, the file will later appear as manually modified. sshd_config = "/etc/ssh/sshd_config" if category == "ssh" and sshd_config not in conf_hashes and sshd_config in conf_files: conf_hashes[sshd_config] = _calculate_hash(conf_files[sshd_config]) _update_conf_hashes(category, conf_hashes) # Consider the following scenario: # - you add a domain foo.bar # - the regen-conf creates file /etc/dnsmasq.d/foo.bar # - the admin manually *deletes* /etc/dnsmasq.d/foo.bar # - the file is now understood as manually deleted because there's the old file hash in regenconf.yml # # ... so far so good, that's the expected behavior. # # But then: # - the admin remove domain foo.bar entirely # - but now the hash for /etc/dnsmasq.d/foo.bar is *still* in # regenconf.yml and and the file is still flagged as manually # modified/deleted... And the user cannot even do anything about it # except removing the hash in regenconf.yml... # # Expected behavior: it should forget about that # hash because dnsmasq's regen-conf doesn't say anything about what's # the state of that file so it should assume that it should be deleted. # # - then the admin tries to *re-add* foo.bar ! # - ... but because the file is still flagged as manually modified # the regen-conf refuses to re-create the file. # # Excepted behavior : the regen-conf should have forgot about the hash # from earlier and this wouldnt happen. # ------ # conf_files contain files explicitly set by the current regen conf run # conf_hashes contain all files known from the past runs # we compare these to get the list of stale hashes and flag the file as # "should be removed" stale_files = set(conf_hashes.keys()) - set(conf_files.keys()) stale_files_with_non_empty_hash = [ f for f in stale_files if conf_hashes.get(f) ] for f in stale_files_with_non_empty_hash: conf_files[f] = None # </> End discussion about stale file hashes force_update_hashes_for_this_category = False for system_path, pending_path in conf_files.items(): logger.debug("processing pending conf '%s' to system conf '%s'", pending_path, system_path) conf_status = None regenerated = False # Get the diff between files conf_diff = _get_files_diff(system_path, pending_path, True) if with_diff else None # Check if the conf must be removed to_remove = True if pending_path and os.path.getsize( pending_path) == 0 else False # Retrieve and calculate hashes system_hash = _calculate_hash(system_path) saved_hash = conf_hashes.get(system_path, None) new_hash = None if to_remove else _calculate_hash(pending_path) # -> configuration was previously managed by yunohost but should now # be removed / unmanaged if system_path in stale_files_with_non_empty_hash: # File is already deleted, so let's just silently forget about this hash entirely if not system_hash: logger.debug("> forgetting about stale file/hash") conf_hashes[system_path] = None conf_status = 'forget-about-it' regenerated = True # Otherwise there's still a file on the system but it's not managed by # Yunohost anymore... But if user requested --force we shall # force-erase it elif force: logger.debug("> force-remove stale file") regenerated = _regen(system_path) conf_status = 'force-removed' # Otherwise, flag the file as manually modified else: logger.warning( m18n.n('regenconf_file_manually_modified', conf=system_path)) conf_status = 'modified' # -> system conf does not exists elif not system_hash: if to_remove: logger.debug("> system conf is already removed") os.remove(pending_path) conf_hashes[system_path] = None conf_status = 'forget-about-it' force_update_hashes_for_this_category = True continue elif not saved_hash or force: if force: logger.debug("> system conf has been manually removed") conf_status = 'force-created' else: logger.debug("> system conf does not exist yet") conf_status = 'created' regenerated = _regen(system_path, pending_path, save=False) else: logger.info( m18n.n('regenconf_file_manually_removed', conf=system_path)) conf_status = 'removed' # -> system conf is not managed yet elif not saved_hash: logger.debug("> system conf is not managed yet") if system_hash == new_hash: logger.debug("> no changes to system conf has been made") conf_status = 'managed' regenerated = True elif not to_remove: # If the conf exist but is not managed yet, and is not to be removed, # we assume that it is safe to regen it, since the file is backuped # anyway (by default in _regen), as long as we warn the user # appropriately. logger.info( m18n.n('regenconf_now_managed_by_yunohost', conf=system_path, category=category)) regenerated = _regen(system_path, pending_path) conf_status = 'new' elif force: regenerated = _regen(system_path) conf_status = 'force-removed' else: logger.info( m18n.n('regenconf_file_kept_back', conf=system_path, category=category)) conf_status = 'unmanaged' # -> system conf has not been manually modified elif system_hash == saved_hash: if to_remove: regenerated = _regen(system_path) conf_status = 'removed' elif system_hash != new_hash: regenerated = _regen(system_path, pending_path) conf_status = 'updated' else: logger.debug("> system conf is already up-to-date") os.remove(pending_path) continue else: logger.debug("> system conf has been manually modified") if system_hash == new_hash: logger.debug("> new conf is as current system conf") conf_status = 'managed' regenerated = True elif force and system_path == sshd_config and not ssh_explicitly_specified: logger.warning( m18n.n('regenconf_need_to_explicitly_specify_ssh')) conf_status = 'modified' elif force: regenerated = _regen(system_path, pending_path) conf_status = 'force-updated' else: logger.warning( m18n.n('regenconf_file_manually_modified', conf=system_path)) conf_status = 'modified' # Store the result conf_result = {'status': conf_status} if conf_diff is not None: conf_result['diff'] = conf_diff if regenerated: succeed_regen[system_path] = conf_result conf_hashes[system_path] = new_hash if pending_path and os.path.isfile(pending_path): os.remove(pending_path) else: failed_regen[system_path] = conf_result # Check for category conf changes if not succeed_regen and not failed_regen: logger.debug(m18n.n('regenconf_up_to_date', category=category)) continue elif not failed_regen: if not dry_run: logger.success(m18n.n('regenconf_updated', category=category)) else: logger.success( m18n.n('regenconf_would_be_updated', category=category)) if (succeed_regen or force_update_hashes_for_this_category) and not dry_run: _update_conf_hashes(category, conf_hashes) # Append the category results result[category] = {'applied': succeed_regen, 'pending': failed_regen} # Return in case of dry run if dry_run: return result # Execute hooks for post-regen post_args = [ 'post', ] + common_args def _pre_call(name, priority, path, args): # append coma-separated applied changes for the category if name in result and result[name]['applied']: regen_conf_files = ','.join(result[name]['applied'].keys()) else: regen_conf_files = '' return post_args + [ regen_conf_files, ] hook_callback('conf_regen', names, pre_callback=_pre_call, env=env) operation_logger.success() return result