def app_remove(auth, app): """ Remove app Keyword argument: app -- App(s) to delete """ from yunohost.hook import hook_exec, hook_remove if not _is_installed(app): raise MoulinetteError(errno.EINVAL, m18n.n('app_not_installed', app)) app_setting_path = apps_setting_path + app #TODO: display fail messages from script try: shutil.rmtree('/tmp/yunohost_remove') except: pass os.system( 'cp -a %s /tmp/yunohost_remove && chown -hR admin: /tmp/yunohost_remove' % app_setting_path) os.system('chown -R admin: /tmp/yunohost_remove') os.system('chmod -R u+rX /tmp/yunohost_remove') if hook_exec('/tmp/yunohost_remove/scripts/remove') != 0: pass if os.path.exists(app_setting_path): shutil.rmtree(app_setting_path) shutil.rmtree('/tmp/yunohost_remove') hook_remove(app) app_ssowatconf(auth) msignals.display(m18n.n('app_removed', app), 'success')
def app_remove(auth, app): """ Remove app Keyword argument: app -- App(s) to delete """ from yunohost.hook import hook_exec, hook_remove if not _is_installed(app): raise MoulinetteError(errno.EINVAL, m18n.n('app_not_installed', app)) app_setting_path = apps_setting_path + app #TODO: display fail messages from script try: shutil.rmtree('/tmp/yunohost_remove') except: pass os.system('cp -a %s /tmp/yunohost_remove && chown -hR admin: /tmp/yunohost_remove' % app_setting_path) os.system('chown -R admin: /tmp/yunohost_remove') os.system('chmod -R u+rX /tmp/yunohost_remove') if hook_exec('/tmp/yunohost_remove/scripts/remove') != 0: pass if os.path.exists(app_setting_path): shutil.rmtree(app_setting_path) shutil.rmtree('/tmp/yunohost_remove') hook_remove(app) app_ssowatconf(auth) msignals.display(m18n.n('app_removed', app), 'success')
def diagnosis_run(categories=[], force=False, except_if_never_ran_yet=False, email=False): if (email or except_if_never_ran_yet) and not os.path.exists(DIAGNOSIS_CACHE): return # Get all the categories all_categories = _list_diagnosis_categories() all_categories_names = [category for category, _ in all_categories] # Check the requested category makes sense if categories == []: categories = all_categories_names else: unknown_categories = [ c for c in categories if c not in all_categories_names ] if unknown_categories: raise YunohostValidationError( "diagnosis_unknown_categories", categories=", ".join(unknown_categories)) issues = [] # Call the hook ... diagnosed_categories = [] for category in categories: logger.debug("Running diagnosis for %s ..." % category) path = [p for n, p in all_categories if n == category][0] try: code, report = hook_exec(path, args={"force": force}, env=None) except Exception: import traceback logger.error( m18n.n( "diagnosis_failed_for_category", category=category, error="\n" + traceback.format_exc(), )) else: diagnosed_categories.append(category) if report != {}: issues.extend([ item for item in report["items"] if item["status"] in ["WARNING", "ERROR"] ]) if email: _email_diagnosis_issues() if issues and msettings.get("interface") == "cli": logger.warning(m18n.n("diagnosis_display_tip"))
def app_install(auth, app, label=None, args=None): """ Install apps Keyword argument: app -- Name, local path or git URL of the app to install label -- Custom name for the app args -- Serialize arguments for app installation """ from yunohost.hook import hook_add, hook_remove, hook_exec # Fetch or extract sources try: os.listdir(install_tmp) except OSError: os.makedirs(install_tmp) if app in app_list(raw=True) or ('@' in app) or ('http://' in app) or ('https://' in app): manifest = _fetch_app_from_git(app) elif os.path.exists(app): manifest = _extract_app_from_file(app) else: raise MoulinetteError(errno.EINVAL, m18n.n('app_unknown')) # Check ID if 'id' not in manifest or '__' in manifest['id']: raise MoulinetteError(errno.EINVAL, m18n.n('app_id_invalid')) app_id = manifest['id'] # Check min version if 'min_version' in manifest and __version__ < manifest['min_version']: raise MoulinetteError(errno.EPERM, m18n.n('app_recent_version_required', app_id)) # Check if app can be forked instance_number = _installed_instance_number(app_id, last=True) + 1 if instance_number > 1: if 'multi_instance' not in manifest or not is_true( manifest['multi_instance']): raise MoulinetteError(errno.EEXIST, m18n.n('app_already_installed', app_id)) app_id_forked = app_id + '__' + str(instance_number) # Replace app_id with the new one in scripts for file in os.listdir(app_tmp_folder + '/scripts'): #TODO: do it with sed ? if file[:1] != '.': with open(app_tmp_folder + '/scripts/' + file, "r") as sources: lines = sources.readlines() with open(app_tmp_folder + '/scripts/' + file, "w") as sources: for line in lines: sources.write( re.sub(r'' + app_id + '', app_id_forked, line)) if 'hooks' in os.listdir(app_tmp_folder): for file in os.listdir(app_tmp_folder + '/hooks'): #TODO: do it with sed ? if file[:1] != '.': with open(app_tmp_folder + '/hooks/' + file, "r") as sources: lines = sources.readlines() with open(app_tmp_folder + '/hooks/' + file, "w") as sources: for line in lines: sources.write( re.sub(r'' + app_id + '', app_id_forked, line)) # Change app_id for the rest of the process app_id = app_id_forked # Prepare App settings app_setting_path = apps_setting_path + '/' + app_id #TMP: Remove old settings if os.path.exists(app_setting_path): shutil.rmtree(app_setting_path) os.makedirs(app_setting_path) os.system('touch %s/settings.yml' % app_setting_path) # Add hooks if 'hooks' in os.listdir(app_tmp_folder): for file in os.listdir(app_tmp_folder + '/hooks'): hook_add(app_id, app_tmp_folder + '/hooks/' + file) app_setting(app_id, 'id', app_id) app_setting(app_id, 'install_time', int(time.time())) if label: app_setting(app_id, 'label', label) else: app_setting(app_id, 'label', manifest['name']) os.system('chown -R admin: ' + app_tmp_folder) try: if args is None: args = '' args_dict = dict(urlparse.parse_qsl(args)) except: args_dict = {} # Execute App install script os.system('chown -hR admin: %s' % install_tmp) # Move scripts and manifest to the right place os.system('cp %s/manifest.json %s' % (app_tmp_folder, app_setting_path)) os.system('cp -R %s/scripts %s' % (app_tmp_folder, app_setting_path)) try: if hook_exec(app_tmp_folder + '/scripts/install', args_dict) == 0: shutil.rmtree(app_tmp_folder) os.system('chmod -R 400 %s' % app_setting_path) os.system('chown -R root: %s' % app_setting_path) os.system('chown -R admin: %s/scripts' % app_setting_path) app_ssowatconf(auth) msignals.display(m18n.n('installation_complete'), 'success') else: raise MoulinetteError(errno.EIO, m18n.n('installation_failed')) except: # Execute remove script and clean folders hook_remove(app_id) shutil.rmtree(app_setting_path) shutil.rmtree(app_tmp_folder) # Reraise proper exception try: raise except MoulinetteError: raise except KeyboardInterrupt, EOFError: raise MoulinetteError(errno.EINTR, m18n.g('operation_interrupted')) except Exception as e: import traceback msignals.display(traceback.format_exc().strip(), 'log') raise MoulinetteError(errno.EIO, m18n.n('unexpected_error'))
def app_upgrade(auth, app=[], url=None, file=None): """ Upgrade app Keyword argument: file -- Folder or tarball for upgrade app -- App(s) to upgrade (default all) url -- Git url to fetch for upgrade """ from yunohost.hook import hook_add, hook_exec try: app_list() except MoulinetteError: raise MoulinetteError(errno.ENODATA, m18n.n('app_no_upgrade')) upgraded_apps = [] # If no app is specified, upgrade all apps if not app: app = os.listdir(apps_setting_path) elif not isinstance(app, list): app = [app] for app_id in app: installed = _is_installed(app_id) if not installed: raise MoulinetteError(errno.ENOPKG, m18n.n('app_not_installed', app_id)) if app_id in upgraded_apps: continue if '__' in app_id: original_app_id = app_id[:app_id.index('__')] else: original_app_id = app_id current_app_dict = app_info(app_id, raw=True) new_app_dict = app_info(original_app_id, raw=True) if file: manifest = _extract_app_from_file(file) elif url: manifest = _fetch_app_from_git(url) elif 'lastUpdate' not in new_app_dict or 'git' not in new_app_dict: msignals.display(m18n.n('custom_app_url_required', app_id), 'warning') continue elif (new_app_dict['lastUpdate'] > current_app_dict['lastUpdate']) \ or ('update_time' not in current_app_dict['settings'] \ and (new_app_dict['lastUpdate'] > current_app_dict['settings']['install_time'])) \ or ('update_time' in current_app_dict['settings'] \ and (new_app_dict['lastUpdate'] > current_app_dict['settings']['update_time'])): manifest = _fetch_app_from_git(app_id) else: continue # Check min version if 'min_version' in manifest and __version__ < manifest['min_version']: raise MoulinetteError( errno.EPERM, m18n.n('app_recent_version_required', app_id)) app_setting_path = apps_setting_path + '/' + app_id if original_app_id != app_id: # Replace original_app_id with the forked one in scripts for file in os.listdir(app_tmp_folder + '/scripts'): #TODO: do it with sed ? if file[:1] != '.': with open(app_tmp_folder + '/scripts/' + file, "r") as sources: lines = sources.readlines() with open(app_tmp_folder + '/scripts/' + file, "w") as sources: for line in lines: sources.write( re.sub(r'' + original_app_id + '', app_id, line)) if 'hooks' in os.listdir(app_tmp_folder): for file in os.listdir(app_tmp_folder + '/hooks'): #TODO: do it with sed ? if file[:1] != '.': with open(app_tmp_folder + '/hooks/' + file, "r") as sources: lines = sources.readlines() with open(app_tmp_folder + '/hooks/' + file, "w") as sources: for line in lines: sources.write( re.sub(r'' + original_app_id + '', app_id, line)) # Add hooks if 'hooks' in os.listdir(app_tmp_folder): for file in os.listdir(app_tmp_folder + '/hooks'): hook_add(app_id, app_tmp_folder + '/hooks/' + file) # Execute App upgrade script os.system('chown -hR admin: %s' % install_tmp) if hook_exec(app_tmp_folder + '/scripts/upgrade') != 0: #TODO: display fail messages from script pass else: app_setting(app_id, 'update_time', int(time.time())) # Replace scripts and manifest os.system('rm -rf "%s/scripts" "%s/manifest.json"' % (app_setting_path, app_setting_path)) os.system('mv "%s/manifest.json" "%s/scripts" %s' % (app_tmp_folder, app_tmp_folder, app_setting_path)) # So much win upgraded_apps.append(app_id) msignals.display(m18n.n('app_upgraded', app_id), 'success') if not upgraded_apps: raise MoulinetteError(errno.ENODATA, m18n.n('app_no_upgrade')) msignals.display(m18n.n('upgrade_complete'), 'success')
def app_install(auth, app, label=None, args=None): """ Install apps Keyword argument: app -- Name, local path or git URL of the app to install label -- Custom name for the app args -- Serialize arguments for app installation """ from yunohost.hook import hook_add, hook_remove, hook_exec # Fetch or extract sources try: os.listdir(install_tmp) except OSError: os.makedirs(install_tmp) if app in app_list(raw=True) or ('@' in app) or ('http://' in app) or ('https://' in app): manifest = _fetch_app_from_git(app) elif os.path.exists(app): manifest = _extract_app_from_file(app) else: raise MoulinetteError(errno.EINVAL, m18n.n('app_unknown')) # Check ID if 'id' not in manifest or '__' in manifest['id']: raise MoulinetteError(errno.EINVAL, m18n.n('app_id_invalid')) app_id = manifest['id'] # Check min version if 'min_version' in manifest and __version__ < manifest['min_version']: raise MoulinetteError(errno.EPERM, m18n.n('app_recent_version_required', app_id)) # Check if app can be forked instance_number = _installed_instance_number(app_id, last=True) + 1 if instance_number > 1 : if 'multi_instance' not in manifest or not is_true(manifest['multi_instance']): raise MoulinetteError(errno.EEXIST, m18n.n('app_already_installed', app_id)) app_id_forked = app_id + '__' + str(instance_number) # Replace app_id with the new one in scripts for file in os.listdir(app_tmp_folder +'/scripts'): #TODO: do it with sed ? if file[:1] != '.': with open(app_tmp_folder +'/scripts/'+ file, "r") as sources: lines = sources.readlines() with open(app_tmp_folder +'/scripts/'+ file, "w") as sources: for line in lines: sources.write(re.sub(r''+ app_id +'', app_id_forked, line)) if 'hooks' in os.listdir(app_tmp_folder): for file in os.listdir(app_tmp_folder +'/hooks'): #TODO: do it with sed ? if file[:1] != '.': with open(app_tmp_folder +'/hooks/'+ file, "r") as sources: lines = sources.readlines() with open(app_tmp_folder +'/hooks/'+ file, "w") as sources: for line in lines: sources.write(re.sub(r''+ app_id +'', app_id_forked, line)) # Change app_id for the rest of the process app_id = app_id_forked # Prepare App settings app_setting_path = apps_setting_path +'/'+ app_id #TMP: Remove old settings if os.path.exists(app_setting_path): shutil.rmtree(app_setting_path) os.makedirs(app_setting_path) os.system('touch %s/settings.yml' % app_setting_path) # Add hooks if 'hooks' in os.listdir(app_tmp_folder): for file in os.listdir(app_tmp_folder +'/hooks'): hook_add(app_id, app_tmp_folder +'/hooks/'+ file) app_setting(app_id, 'id', app_id) app_setting(app_id, 'install_time', int(time.time())) if label: app_setting(app_id, 'label', label) else: app_setting(app_id, 'label', manifest['name']) os.system('chown -R admin: '+ app_tmp_folder) try: if args is None: args = '' args_dict = dict(urlparse.parse_qsl(args)) except: args_dict = {} # Execute App install script os.system('chown -hR admin: %s' % install_tmp) # Move scripts and manifest to the right place os.system('cp %s/manifest.json %s' % (app_tmp_folder, app_setting_path)) os.system('cp -R %s/scripts %s' % (app_tmp_folder, app_setting_path)) try: if hook_exec(app_tmp_folder + '/scripts/install', args_dict) == 0: shutil.rmtree(app_tmp_folder) os.system('chmod -R 400 %s' % app_setting_path) os.system('chown -R root: %s' % app_setting_path) os.system('chown -R admin: %s/scripts' % app_setting_path) app_ssowatconf(auth) msignals.display(m18n.n('installation_complete'), 'success') else: raise MoulinetteError(errno.EIO, m18n.n('installation_failed')) except: # Execute remove script and clean folders hook_remove(app_id) shutil.rmtree(app_setting_path) shutil.rmtree(app_tmp_folder) # Reraise proper exception try: raise except MoulinetteError: raise except KeyboardInterrupt, EOFError: raise MoulinetteError(errno.EINTR, m18n.g('operation_interrupted')) except Exception as e: import traceback msignals.display(traceback.format_exc().strip(), 'log') raise MoulinetteError(errno.EIO, m18n.n('unexpected_error'))
def app_upgrade(auth, app=[], url=None, file=None): """ Upgrade app Keyword argument: file -- Folder or tarball for upgrade app -- App(s) to upgrade (default all) url -- Git url to fetch for upgrade """ from yunohost.hook import hook_add, hook_exec try: app_list() except MoulinetteError: raise MoulinetteError(errno.ENODATA, m18n.n('app_no_upgrade')) upgraded_apps = [] # If no app is specified, upgrade all apps if not app: if (not url and not file): app = os.listdir(apps_setting_path) elif not isinstance(app, list): app = [ app ] for app_id in app: installed = _is_installed(app_id) if not installed: raise MoulinetteError(errno.ENOPKG, m18n.n('app_not_installed', app_id)) if app_id in upgraded_apps: continue if '__' in app_id: original_app_id = app_id[:app_id.index('__')] else: original_app_id = app_id current_app_dict = app_info(app_id, raw=True) new_app_dict = app_info(original_app_id, raw=True) if file: manifest = _extract_app_from_file(file) elif url: manifest = _fetch_app_from_git(url) elif 'lastUpdate' not in new_app_dict or 'git' not in new_app_dict: msignals.display(m18n.n('custom_app_url_required', app_id), 'warning') continue elif (new_app_dict['lastUpdate'] > current_app_dict['lastUpdate']) \ or ('update_time' not in current_app_dict['settings'] \ and (new_app_dict['lastUpdate'] > current_app_dict['settings']['install_time'])) \ or ('update_time' in current_app_dict['settings'] \ and (new_app_dict['lastUpdate'] > current_app_dict['settings']['update_time'])): manifest = _fetch_app_from_git(app_id) else: continue # Check min version if 'min_version' in manifest and __version__ < manifest['min_version']: raise MoulinetteError(errno.EPERM, m18n.n('app_recent_version_required', app_id)) app_setting_path = apps_setting_path +'/'+ app_id if original_app_id != app_id: # Replace original_app_id with the forked one in scripts for file in os.listdir(app_tmp_folder +'/scripts'): #TODO: do it with sed ? if file[:1] != '.': with open(app_tmp_folder +'/scripts/'+ file, "r") as sources: lines = sources.readlines() with open(app_tmp_folder +'/scripts/'+ file, "w") as sources: for line in lines: sources.write(re.sub(r''+ original_app_id +'', app_id, line)) if 'hooks' in os.listdir(app_tmp_folder): for file in os.listdir(app_tmp_folder +'/hooks'): #TODO: do it with sed ? if file[:1] != '.': with open(app_tmp_folder +'/hooks/'+ file, "r") as sources: lines = sources.readlines() with open(app_tmp_folder +'/hooks/'+ file, "w") as sources: for line in lines: sources.write(re.sub(r''+ original_app_id +'', app_id, line)) # Add hooks if 'hooks' in os.listdir(app_tmp_folder): for file in os.listdir(app_tmp_folder +'/hooks'): hook_add(app_id, app_tmp_folder +'/hooks/'+ file) # Execute App upgrade script os.system('chown -hR admin: %s' % install_tmp) if hook_exec(app_tmp_folder +'/scripts/upgrade') != 0: #TODO: display fail messages from script pass else: app_setting(app_id, 'update_time', int(time.time())) # Replace scripts and manifest os.system('rm -rf "%s/scripts" "%s/manifest.json"' % (app_setting_path, app_setting_path)) os.system('mv "%s/manifest.json" "%s/scripts" %s' % (app_tmp_folder, app_tmp_folder, app_setting_path)) # So much win upgraded_apps.append(app_id) msignals.display(m18n.n('app_upgraded', app_id), 'success') if not upgraded_apps: raise MoulinetteError(errno.ENODATA, m18n.n('app_no_upgrade')) msignals.display(m18n.n('upgrade_complete'), 'success')
def backup_create(name=None, description=None, output_directory=None, no_compress=False, ignore_hooks=False, hooks=[], ignore_apps=False, apps=[]): """ Create a backup local archive Keyword arguments: name -- Name of the backup archive description -- Short description of the backup output_directory -- Output directory for the backup no_compress -- Do not create an archive file hooks -- List of backup hooks names to execute ignore_hooks -- Do not execute backup hooks apps -- List of application names to backup ignore_apps -- Do not backup apps """ # TODO: Add a 'clean' argument to clean output directory tmp_dir = None env_var = {} # Validate what to backup if ignore_hooks and ignore_apps: raise MoulinetteError(errno.EINVAL, m18n.n('backup_action_required')) # Validate and define backup name timestamp = int(time.time()) if not name: name = time.strftime('%Y%m%d-%H%M%S') if name in backup_list()['archives']: raise MoulinetteError(errno.EINVAL, m18n.n('backup_archive_name_exists')) # Validate additional arguments if no_compress and not output_directory: raise MoulinetteError(errno.EINVAL, m18n.n('backup_output_directory_required')) if output_directory: output_directory = os.path.abspath(output_directory) # Check for forbidden folders if output_directory.startswith(archives_path) or \ re.match(r'^/(|(bin|boot|dev|etc|lib|root|run|sbin|sys|usr|var)(|/.*))$', output_directory): raise MoulinetteError(errno.EINVAL, m18n.n('backup_output_directory_forbidden')) # Create the output directory if not os.path.isdir(output_directory): logger.debug("creating output directory '%s'", output_directory) os.makedirs(output_directory, 0750) # Check that output directory is empty elif no_compress and os.listdir(output_directory): raise MoulinetteError(errno.EIO, m18n.n('backup_output_directory_not_empty')) # Do not compress, so set temporary directory to output one and # disable bind mounting to prevent data loss in case of a rm # See: https://dev.yunohost.org/issues/298 if no_compress: logger.debug('bind mounting will be disabled') tmp_dir = output_directory env_var['CAN_BIND'] = 0 else: output_directory = archives_path # Create archives directory if it does not exists if not os.path.isdir(archives_path): os.mkdir(archives_path, 0750) def _clean_tmp_dir(retcode=0): ret = hook_callback('post_backup_create', args=[tmp_dir, retcode]) if not ret['failed']: filesystem.rm(tmp_dir, True, True) return True else: logger.warning(m18n.n('backup_cleaning_failed')) return False # Create temporary directory if not tmp_dir: tmp_dir = "%s/tmp/%s" % (backup_path, name) if os.path.isdir(tmp_dir): logger.debug("temporary directory for backup '%s' already exists", tmp_dir) if not _clean_tmp_dir(): raise MoulinetteError( errno.EIO, m18n.n('backup_output_directory_not_empty')) filesystem.mkdir(tmp_dir, 0750, parents=True, uid='admin') # Initialize backup info info = { 'description': description or '', 'created_at': timestamp, 'apps': {}, 'hooks': {}, } # Run system hooks if not ignore_hooks: # Check hooks availibility hooks_filtered = set() if hooks: for hook in hooks: try: hook_info('backup', hook) except: logger.error(m18n.n('backup_hook_unknown', hook=hook)) else: hooks_filtered.add(hook) if not hooks or hooks_filtered: logger.info(m18n.n('backup_running_hooks')) ret = hook_callback('backup', hooks_filtered, args=[tmp_dir], env=env_var) if ret['succeed']: info['hooks'] = ret['succeed'] # Save relevant restoration hooks tmp_hooks_dir = tmp_dir + '/hooks/restore' filesystem.mkdir(tmp_hooks_dir, 0750, True, uid='admin') for h in ret['succeed'].keys(): try: i = hook_info('restore', h) except: logger.warning(m18n.n('restore_hook_unavailable', hook=h), exc_info=1) else: for f in i['hooks']: shutil.copy(f['path'], tmp_hooks_dir) # Backup apps if not ignore_apps: # Filter applications to backup apps_list = set(os.listdir('/etc/yunohost/apps')) apps_filtered = set() if apps: for a in apps: if a not in apps_list: logger.warning(m18n.n('unbackup_app', app=a)) else: apps_filtered.add(a) else: apps_filtered = apps_list # Run apps backup scripts tmp_script = '/tmp/backup_' + str(timestamp) for app_instance_name in apps_filtered: app_setting_path = '/etc/yunohost/apps/' + app_instance_name # Check if the app has a backup and restore script app_script = app_setting_path + '/scripts/backup' app_restore_script = app_setting_path + '/scripts/restore' if not os.path.isfile(app_script): logger.warning(m18n.n('unbackup_app', app=app_instance_name)) continue elif not os.path.isfile(app_restore_script): logger.warning(m18n.n('unrestore_app', app=app_instance_name)) tmp_app_dir = '{:s}/apps/{:s}'.format(tmp_dir, app_instance_name) tmp_app_bkp_dir = tmp_app_dir + '/backup' logger.info( m18n.n('backup_running_app_script', app=app_instance_name)) try: # Prepare backup directory for the app filesystem.mkdir(tmp_app_bkp_dir, 0750, True, uid='admin') shutil.copytree(app_setting_path, tmp_app_dir + '/settings') # Copy app backup script in a temporary folder and execute it subprocess.call(['install', '-Dm555', app_script, tmp_script]) # Prepare env. var. to pass to script app_id, app_instance_nb = _parse_app_instance_name( app_instance_name) env_dict = env_var.copy() env_dict["YNH_APP_ID"] = app_id env_dict["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) env_dict["YNH_APP_BACKUP_DIR"] = tmp_app_bkp_dir hook_exec(tmp_script, args=[tmp_app_bkp_dir, app_instance_name], raise_on_error=True, chdir=tmp_app_bkp_dir, env=env_dict) except: logger.exception( m18n.n('backup_app_failed', app=app_instance_name)) # Cleaning app backup directory shutil.rmtree(tmp_app_dir, ignore_errors=True) else: # Add app info i = app_info(app_instance_name) info['apps'][app_instance_name] = { 'version': i['version'], 'name': i['name'], 'description': i['description'], } finally: filesystem.rm(tmp_script, force=True) # Check if something has been saved if not info['hooks'] and not info['apps']: _clean_tmp_dir(1) raise MoulinetteError(errno.EINVAL, m18n.n('backup_nothings_done')) # Calculate total size backup_size = int( subprocess.check_output(['du', '-sb', tmp_dir]).split()[0].decode('utf-8')) info['size'] = backup_size # Create backup info file with open("%s/info.json" % tmp_dir, 'w') as f: f.write(json.dumps(info)) # Create the archive if not no_compress: logger.info(m18n.n('backup_creating_archive')) # Check free space in output directory at first avail_output = subprocess.check_output( ['df', '--block-size=1', '--output=avail', tmp_dir]).split() if len(avail_output) < 2 or int(avail_output[1]) < backup_size: logger.debug('not enough space at %s (free: %s / needed: %d)', output_directory, avail_output[1], backup_size) _clean_tmp_dir(3) raise MoulinetteError( errno.EIO, m18n.n('not_enough_disk_space', path=output_directory)) # Open archive file for writing archive_file = "%s/%s.tar.gz" % (output_directory, name) try: tar = tarfile.open(archive_file, "w:gz") except: logger.debug("unable to open '%s' for writing", archive_file, exc_info=1) _clean_tmp_dir(2) raise MoulinetteError(errno.EIO, m18n.n('backup_archive_open_failed')) # Add files to the archive try: tar.add(tmp_dir, arcname='') tar.close() except IOError as e: logger.error(m18n.n('backup_archive_writing_error'), exc_info=1) _clean_tmp_dir(3) raise MoulinetteError(errno.EIO, m18n.n('backup_creation_failed')) # FIXME : it looks weird that the "move info file" is not enabled if # user activated "no_compress" ... or does it really means # "dont_keep_track_of_this_backup_in_history" ? # Move info file shutil.move(tmp_dir + '/info.json', '{:s}/{:s}.info.json'.format(archives_path, name)) # If backuped to a non-default location, keep a symlink of the archive # to that location if output_directory != archives_path: link = "%s/%s.tar.gz" % (archives_path, name) os.symlink(archive_file, link) # Clean temporary directory if tmp_dir != output_directory: _clean_tmp_dir() logger.success(m18n.n('backup_created')) # Return backup info info['name'] = name return {'archive': info}
def backup_restore(auth, name, hooks=[], ignore_hooks=False, apps=[], ignore_apps=False, force=False): """ Restore from a local backup archive Keyword argument: name -- Name of the local backup archive hooks -- List of restoration hooks names to execute ignore_hooks -- Do not execute backup hooks apps -- List of application names to restore ignore_apps -- Do not restore apps force -- Force restauration on an already installed system """ # Validate what to restore if ignore_hooks and ignore_apps: raise MoulinetteError(errno.EINVAL, m18n.n('restore_action_required')) # Retrieve and open the archive info = backup_info(name) archive_file = info['path'] try: tar = tarfile.open(archive_file, "r:gz") except: logger.debug("cannot open backup archive '%s'", archive_file, exc_info=1) raise MoulinetteError(errno.EIO, m18n.n('backup_archive_open_failed')) # Check temporary directory tmp_dir = "%s/tmp/%s" % (backup_path, name) if os.path.isdir(tmp_dir): logger.debug("temporary directory for restoration '%s' already exists", tmp_dir) os.system('rm -rf %s' % tmp_dir) # Check available disk space statvfs = os.statvfs(backup_path) free_space = statvfs.f_frsize * statvfs.f_bavail if free_space < info['size']: logger.debug("%dB left but %dB is needed", free_space, info['size']) raise MoulinetteError( errno.EIO, m18n.n('not_enough_disk_space', path=backup_path)) def _clean_tmp_dir(retcode=0): ret = hook_callback('post_backup_restore', args=[tmp_dir, retcode]) if not ret['failed']: filesystem.rm(tmp_dir, True, True) else: logger.warning(m18n.n('restore_cleaning_failed')) # Extract the tarball logger.info(m18n.n('backup_extracting_archive')) tar.extractall(tmp_dir) tar.close() # Retrieve backup info info_file = "%s/info.json" % tmp_dir try: with open(info_file, 'r') as f: info = json.load(f) except IOError: logger.debug("unable to load '%s'", info_file, exc_info=1) raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) else: logger.debug("restoring from backup '%s' created on %s", name, time.ctime(info['created_at'])) # Initialize restauration summary result result = { 'apps': [], 'hooks': {}, } # Check if YunoHost is installed if os.path.isfile('/etc/yunohost/installed'): logger.warning(m18n.n('yunohost_already_installed')) if not force: try: # Ask confirmation for restoring i = msignals.prompt( m18n.n('restore_confirm_yunohost_installed', answers='y/N')) except NotImplemented: pass else: if i == 'y' or i == 'Y': force = True if not force: _clean_tmp_dir() raise MoulinetteError(errno.EEXIST, m18n.n('restore_failed')) else: # Retrieve the domain from the backup try: with open("%s/conf/ynh/current_host" % tmp_dir, 'r') as f: domain = f.readline().rstrip() except IOError: logger.debug("unable to retrieve current_host from the backup", exc_info=1) raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) logger.debug("executing the post-install...") tools_postinstall(domain, 'yunohost', True) # Run system hooks if not ignore_hooks: # Filter hooks to execute hooks_list = set(info['hooks'].keys()) _is_hook_in_backup = lambda h: True if hooks: def _is_hook_in_backup(h): if h in hooks_list: return True logger.error(m18n.n('backup_archive_hook_not_exec', hook=h)) return False else: hooks = hooks_list # Check hooks availibility hooks_filtered = set() for h in hooks: if not _is_hook_in_backup(h): continue try: hook_info('restore', h) except: tmp_hooks = glob('{:s}/hooks/restore/*-{:s}'.format( tmp_dir, h)) if not tmp_hooks: logger.exception(m18n.n('restore_hook_unavailable', hook=h)) continue # Add restoration hook from the backup to the system # FIXME: Refactor hook_add and use it instead restore_hook_folder = custom_hook_folder + 'restore' filesystem.mkdir(restore_hook_folder, 755, True) for f in tmp_hooks: logger.debug( "adding restoration hook '%s' to the system " "from the backup archive '%s'", f, archive_file) shutil.copy(f, restore_hook_folder) hooks_filtered.add(h) if hooks_filtered: logger.info(m18n.n('restore_running_hooks')) ret = hook_callback('restore', hooks_filtered, args=[tmp_dir]) result['hooks'] = ret['succeed'] # Add apps restore hook if not ignore_apps: # Filter applications to restore apps_list = set(info['apps'].keys()) apps_filtered = set() if apps: for a in apps: if a not in apps_list: logger.error(m18n.n('backup_archive_app_not_found', app=a)) else: apps_filtered.add(a) else: apps_filtered = apps_list for app_instance_name in apps_filtered: tmp_app_dir = '{:s}/apps/{:s}'.format(tmp_dir, app_instance_name) tmp_app_bkp_dir = tmp_app_dir + '/backup' # Parse app instance name and id # TODO: Use app_id to check if app is installed? app_id, app_instance_nb = _parse_app_instance_name( app_instance_name) # Check if the app is not already installed if _is_installed(app_instance_name): logger.error( m18n.n('restore_already_installed_app', app=app_instance_name)) continue # Check if the app has a restore script app_script = tmp_app_dir + '/settings/scripts/restore' if not os.path.isfile(app_script): logger.warning(m18n.n('unrestore_app', app=app_instance_name)) continue tmp_script = '/tmp/restore_' + app_instance_name app_setting_path = '/etc/yunohost/apps/' + app_instance_name logger.info( m18n.n('restore_running_app_script', app=app_instance_name)) try: # Copy app settings and set permissions # TODO: Copy app hooks too shutil.copytree(tmp_app_dir + '/settings', app_setting_path) filesystem.chmod(app_setting_path, 0555, 0444, True) filesystem.chmod(app_setting_path + '/settings.yml', 0400) # Copy restore script in a tmp file subprocess.call(['install', '-Dm555', app_script, tmp_script]) # Prepare env. var. to pass to script env_dict = {} env_dict["YNH_APP_ID"] = app_id env_dict["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) env_dict["YNH_APP_BACKUP_DIR"] = tmp_app_bkp_dir # Execute app restore script hook_exec(tmp_script, args=[tmp_app_bkp_dir, app_instance_name], raise_on_error=True, chdir=tmp_app_bkp_dir, env=env_dict) except: logger.exception( m18n.n('restore_app_failed', app=app_instance_name)) # Copy remove script in a tmp file filesystem.rm(tmp_script, force=True) app_script = tmp_app_dir + '/settings/scripts/remove' tmp_script = '/tmp/remove_' + app_instance_name subprocess.call(['install', '-Dm555', app_script, tmp_script]) # Setup environment for remove script env_dict_remove = {} env_dict_remove["YNH_APP_ID"] = app_id env_dict_remove["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str( app_instance_nb) # Execute remove script # TODO: call app_remove instead if hook_exec(tmp_script, args=[app_instance_name], env=env_dict_remove) != 0: logger.warning( m18n.n('app_not_properly_removed', app=app_instance_name)) # Cleaning app directory shutil.rmtree(app_setting_path, ignore_errors=True) else: result['apps'].append(app_instance_name) finally: filesystem.rm(tmp_script, force=True) # Check if something has been restored if not result['hooks'] and not result['apps']: _clean_tmp_dir(1) raise MoulinetteError(errno.EINVAL, m18n.n('restore_nothings_done')) if result['apps']: app_ssowatconf(auth) _clean_tmp_dir() logger.success(m18n.n('restore_complete')) return result
def backup_create(name=None, description=None, output_directory=None, no_compress=False, ignore_hooks=False, hooks=[], ignore_apps=False, apps=[]): """ Create a backup local archive Keyword arguments: name -- Name of the backup archive description -- Short description of the backup output_directory -- Output directory for the backup no_compress -- Do not create an archive file hooks -- List of backup hooks names to execute ignore_hooks -- Do not execute backup hooks apps -- List of application names to backup ignore_apps -- Do not backup apps """ # TODO: Add a 'clean' argument to clean output directory tmp_dir = None # Validate what to backup if ignore_hooks and ignore_apps: raise MoulinetteError(errno.EINVAL, m18n.n('backup_action_required')) # Validate and define backup name timestamp = int(time.time()) if not name: name = time.strftime('%Y%m%d-%H%M%S') if name in backup_list()['archives']: raise MoulinetteError(errno.EINVAL, m18n.n('backup_archive_name_exists')) # Validate additional arguments if no_compress and not output_directory: raise MoulinetteError(errno.EINVAL, m18n.n('backup_output_directory_required')) if output_directory: output_directory = os.path.abspath(output_directory) # Check for forbidden folders if output_directory.startswith(archives_path) or \ re.match(r'^/(|(bin|boot|dev|etc|lib|root|run|sbin|sys|usr|var)(|/.*))$', output_directory): raise MoulinetteError(errno.EINVAL, m18n.n('backup_output_directory_forbidden')) # Create the output directory if not os.path.isdir(output_directory): logger.debug("creating output directory '%s'", output_directory) os.makedirs(output_directory, 0750) # Check that output directory is empty elif no_compress and os.listdir(output_directory): raise MoulinetteError(errno.EIO, m18n.n('backup_output_directory_not_empty')) # Define temporary directory if no_compress: tmp_dir = output_directory else: output_directory = archives_path # Create temporary directory if not tmp_dir: tmp_dir = "%s/tmp/%s" % (backup_path, name) if os.path.isdir(tmp_dir): logger.debug("temporary directory for backup '%s' already exists", tmp_dir) filesystem.rm(tmp_dir, recursive=True) filesystem.mkdir(tmp_dir, 0750, parents=True, uid='admin') def _clean_tmp_dir(retcode=0): ret = hook_callback('post_backup_create', args=[tmp_dir, retcode]) if not ret['failed']: filesystem.rm(tmp_dir, True, True) else: logger.warning(m18n.n('backup_cleaning_failed')) # Initialize backup info info = { 'description': description or '', 'created_at': timestamp, 'apps': {}, 'hooks': {}, } # Run system hooks if not ignore_hooks: # Check hooks availibility hooks_filtered = set() if hooks: for hook in hooks: try: hook_info('backup', hook) except: logger.error(m18n.n('backup_hook_unknown', hook=hook)) else: hooks_filtered.add(hook) if not hooks or hooks_filtered: logger.info(m18n.n('backup_running_hooks')) ret = hook_callback('backup', hooks_filtered, args=[tmp_dir]) if ret['succeed']: info['hooks'] = ret['succeed'] # Save relevant restoration hooks tmp_hooks_dir = tmp_dir + '/hooks/restore' filesystem.mkdir(tmp_hooks_dir, 0750, True, uid='admin') for h in ret['succeed'].keys(): try: i = hook_info('restore', h) except: logger.warning(m18n.n('restore_hook_unavailable', hook=h), exc_info=1) else: for f in i['hooks']: shutil.copy(f['path'], tmp_hooks_dir) # Backup apps if not ignore_apps: # Filter applications to backup apps_list = set(os.listdir('/etc/yunohost/apps')) apps_filtered = set() if apps: for a in apps: if a not in apps_list: logger.warning(m18n.n('unbackup_app', app=a)) else: apps_filtered.add(a) else: apps_filtered = apps_list # Run apps backup scripts tmp_script = '/tmp/backup_' + str(timestamp) for app_instance_name in apps_filtered: app_setting_path = '/etc/yunohost/apps/' + app_instance_name # Check if the app has a backup and restore script app_script = app_setting_path + '/scripts/backup' app_restore_script = app_setting_path + '/scripts/restore' if not os.path.isfile(app_script): logger.warning(m18n.n('unbackup_app', app=app_instance_name)) continue elif not os.path.isfile(app_restore_script): logger.warning(m18n.n('unrestore_app', app=app_instance_name)) tmp_app_dir = '{:s}/apps/{:s}'.format(tmp_dir, app_instance_name) tmp_app_bkp_dir = tmp_app_dir + '/backup' logger.info(m18n.n('backup_running_app_script', app=app_instance_name)) try: # Prepare backup directory for the app filesystem.mkdir(tmp_app_bkp_dir, 0750, True, uid='admin') shutil.copytree(app_setting_path, tmp_app_dir + '/settings') # Copy app backup script in a temporary folder and execute it subprocess.call(['install', '-Dm555', app_script, tmp_script]) # Prepare env. var. to pass to script env_dict = {} app_id, app_instance_nb = _parse_app_instance_name(app_instance_name) env_dict["YNH_APP_ID"] = app_id env_dict["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) env_dict["YNH_APP_BACKUP_DIR"] = tmp_app_bkp_dir hook_exec(tmp_script, args=[tmp_app_bkp_dir, app_instance_name], raise_on_error=True, chdir=tmp_app_bkp_dir, env=env_dict) except: logger.exception(m18n.n('backup_app_failed', app=app_instance_name)) # Cleaning app backup directory shutil.rmtree(tmp_app_dir, ignore_errors=True) else: # Add app info i = app_info(app_instance_name) info['apps'][app_instance_name] = { 'version': i['version'], 'name': i['name'], 'description': i['description'], } finally: filesystem.rm(tmp_script, force=True) # Check if something has been saved if not info['hooks'] and not info['apps']: _clean_tmp_dir(1) raise MoulinetteError(errno.EINVAL, m18n.n('backup_nothings_done')) # Calculate total size size = subprocess.check_output( ['du','-sb', tmp_dir]).split()[0].decode('utf-8') info['size'] = int(size) # Create backup info file with open("%s/info.json" % tmp_dir, 'w') as f: f.write(json.dumps(info)) # Create the archive if not no_compress: logger.info(m18n.n('backup_creating_archive')) archive_file = "%s/%s.tar.gz" % (output_directory, name) try: tar = tarfile.open(archive_file, "w:gz") except: tar = None # Create the archives directory and retry if not os.path.isdir(archives_path): os.mkdir(archives_path, 0750) try: tar = tarfile.open(archive_file, "w:gz") except: logger.debug("unable to open '%s' for writing", archive_file, exc_info=1) tar = None else: logger.debug("unable to open '%s' for writing", archive_file, exc_info=1) if tar is None: _clean_tmp_dir(2) raise MoulinetteError(errno.EIO, m18n.n('backup_archive_open_failed')) tar.add(tmp_dir, arcname='') tar.close() # Move info file os.rename(tmp_dir + '/info.json', '{:s}/{:s}.info.json'.format(archives_path, name)) # Clean temporary directory if tmp_dir != output_directory: _clean_tmp_dir() logger.success(m18n.n('backup_complete')) # Return backup info info['name'] = name return { 'archive': info }
def backup_restore(auth, name, hooks=[], ignore_hooks=False, apps=[], ignore_apps=False, force=False): """ Restore from a local backup archive Keyword argument: name -- Name of the local backup archive hooks -- List of restoration hooks names to execute ignore_hooks -- Do not execute backup hooks apps -- List of application names to restore ignore_apps -- Do not restore apps force -- Force restauration on an already installed system """ # Validate what to restore if ignore_hooks and ignore_apps: raise MoulinetteError(errno.EINVAL, m18n.n('restore_action_required')) # Retrieve and open the archive info = backup_info(name) archive_file = info['path'] try: tar = tarfile.open(archive_file, "r:gz") except: logger.debug("cannot open backup archive '%s'", archive_file, exc_info=1) raise MoulinetteError(errno.EIO, m18n.n('backup_archive_open_failed')) # Check temporary directory tmp_dir = "%s/tmp/%s" % (backup_path, name) if os.path.isdir(tmp_dir): logger.debug("temporary directory for restoration '%s' already exists", tmp_dir) os.system('rm -rf %s' % tmp_dir) # Check available disk space statvfs = os.statvfs(backup_path) free_space = statvfs.f_frsize * statvfs.f_bavail if free_space < info['size']: logger.debug("%dB left but %dB is needed", free_space, info['size']) raise MoulinetteError( errno.EIO, m18n.n('not_enough_disk_space', path=backup_path)) def _clean_tmp_dir(retcode=0): ret = hook_callback('post_backup_restore', args=[tmp_dir, retcode]) if not ret['failed']: filesystem.rm(tmp_dir, True, True) else: logger.warning(m18n.n('restore_cleaning_failed')) # Extract the tarball logger.info(m18n.n('backup_extracting_archive')) tar.extractall(tmp_dir) tar.close() # Retrieve backup info info_file = "%s/info.json" % tmp_dir try: with open(info_file, 'r') as f: info = json.load(f) except IOError: logger.debug("unable to load '%s'", info_file, exc_info=1) raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) else: logger.debug("restoring from backup '%s' created on %s", name, time.ctime(info['created_at'])) # Initialize restauration summary result result = { 'apps': [], 'hooks': {}, } # Check if YunoHost is installed if os.path.isfile('/etc/yunohost/installed'): logger.warning(m18n.n('yunohost_already_installed')) if not force: try: # Ask confirmation for restoring i = msignals.prompt(m18n.n('restore_confirm_yunohost_installed', answers='y/N')) except NotImplemented: pass else: if i == 'y' or i == 'Y': force = True if not force: _clean_tmp_dir() raise MoulinetteError(errno.EEXIST, m18n.n('restore_failed')) else: # Retrieve the domain from the backup try: with open("%s/conf/ynh/current_host" % tmp_dir, 'r') as f: domain = f.readline().rstrip() except IOError: logger.debug("unable to retrieve current_host from the backup", exc_info=1) raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) logger.debug("executing the post-install...") tools_postinstall(domain, 'yunohost', True) # Run system hooks if not ignore_hooks: # Filter hooks to execute hooks_list = set(info['hooks'].keys()) _is_hook_in_backup = lambda h: True if hooks: def _is_hook_in_backup(h): if h in hooks_list: return True logger.error(m18n.n('backup_archive_hook_not_exec', hook=h)) return False else: hooks = hooks_list # Check hooks availibility hooks_filtered = set() for h in hooks: if not _is_hook_in_backup(h): continue try: hook_info('restore', h) except: tmp_hooks = glob('{:s}/hooks/restore/*-{:s}'.format(tmp_dir, h)) if not tmp_hooks: logger.exception(m18n.n('restore_hook_unavailable', hook=h)) continue # Add restoration hook from the backup to the system # FIXME: Refactor hook_add and use it instead restore_hook_folder = custom_hook_folder + 'restore' filesystem.mkdir(restore_hook_folder, 755, True) for f in tmp_hooks: logger.debug("adding restoration hook '%s' to the system " "from the backup archive '%s'", f, archive_file) shutil.copy(f, restore_hook_folder) hooks_filtered.add(h) if hooks_filtered: logger.info(m18n.n('restore_running_hooks')) ret = hook_callback('restore', hooks_filtered, args=[tmp_dir]) result['hooks'] = ret['succeed'] # Add apps restore hook if not ignore_apps: # Filter applications to restore apps_list = set(info['apps'].keys()) apps_filtered = set() if apps: for a in apps: if a not in apps_list: logger.error(m18n.n('backup_archive_app_not_found', app=a)) else: apps_filtered.add(a) else: apps_filtered = apps_list for app_instance_name in apps_filtered: tmp_app_dir = '{:s}/apps/{:s}'.format(tmp_dir, app_instance_name) tmp_app_bkp_dir = tmp_app_dir + '/backup' # Check if the app is not already installed if _is_installed(app_instance_name): logger.error(m18n.n('restore_already_installed_app', app=app_instance_name)) continue # Check if the app has a restore script app_script = tmp_app_dir + '/settings/scripts/restore' if not os.path.isfile(app_script): logger.warning(m18n.n('unrestore_app', app=app_instance_name)) continue tmp_script = '/tmp/restore_' + app_instance_name app_setting_path = '/etc/yunohost/apps/' + app_instance_name logger.info(m18n.n('restore_running_app_script', app=app_instance_name)) try: # Copy app settings and set permissions shutil.copytree(tmp_app_dir + '/settings', app_setting_path) filesystem.chmod(app_setting_path, 0555, 0444, True) filesystem.chmod(app_setting_path + '/settings.yml', 0400) # Execute app restore script subprocess.call(['install', '-Dm555', app_script, tmp_script]) # Prepare env. var. to pass to script env_dict = {} app_id, app_instance_nb = _parse_app_instance_name(app_instance_name) env_dict["YNH_APP_ID"] = app_id env_dict["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) env_dict["YNH_APP_BACKUP_DIR"] = tmp_app_bkp_dir hook_exec(tmp_script, args=[tmp_app_bkp_dir, app_instance_name], raise_on_error=True, chdir=tmp_app_bkp_dir, env=env_dict) except: logger.exception(m18n.n('restore_app_failed', app=app_instance_name)) # Cleaning app directory shutil.rmtree(app_setting_path, ignore_errors=True) else: result['apps'].append(app_instance_name) finally: filesystem.rm(tmp_script, force=True) # Check if something has been restored if not result['hooks'] and not result['apps']: _clean_tmp_dir(1) raise MoulinetteError(errno.EINVAL, m18n.n('restore_nothings_done')) if result['apps']: app_ssowatconf(auth) _clean_tmp_dir() logger.success(m18n.n('restore_complete')) return result