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_apps=False): """ 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 ignore_apps -- Do not backup apps """ # TODO: Add a 'clean' argument to clean output directory from yunohost.hook import hook_add from yunohost.hook import hook_callback tmp_dir = None # Validate and define backup name timestamp = int(time.time()) if not name: name = str(timestamp) 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): logger.error("forbidden output directory '%'", output_directory) raise MoulinetteError(errno.EINVAL, m18n.n('backup_output_directory_forbidden')) # Create the output directory if not os.path.isdir(output_directory): logger.info("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): logger.error("not empty output directory '%'", 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.warning("temporary directory for backup '%s' already exists", tmp_dir) os.system('rm -rf %s' % tmp_dir) try: os.mkdir(tmp_dir, 0750) except OSError: # Create temporary directory recursively os.makedirs(tmp_dir, 0750) os.system('chown -hR admin: %s' % backup_path) else: os.system('chown -hR admin: %s' % tmp_dir) # Initialize backup info info = { 'description': description or '', 'created_at': timestamp, 'apps': {}, } # Add apps backup hook if not ignore_apps: from yunohost.app import app_info try: for app_id in os.listdir('/etc/yunohost/apps'): hook = '/etc/yunohost/apps/%s/scripts/backup' % app_id if os.path.isfile(hook): hook_add(app_id, hook) # Add app info i = app_info(app_id) info['apps'][app_id] = { 'version': i['version'], } else: logger.warning("unable to find app's backup hook '%s'", hook) msignals.display(m18n.n('unbackup_app', app_id), 'warning') except IOError as e: logger.info("unable to add apps backup hook: %s", str(e)) # Run hooks msignals.display(m18n.n('backup_running_hooks')) hook_callback('backup', [tmp_dir]) # 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: msignals.display(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.exception("unable to open the archive '%s' for writing " "after creating directory '%s'", archive_file, archives_path) tar = None else: logger.exception("unable to open the archive '%s' for writing", archive_file) if tar is None: raise MoulinetteError(errno.EIO, m18n.n('backup_archive_open_failed')) tar.add(tmp_dir, arcname='') tar.close() # Copy info file os.system('mv %s/info.json %s/%s.info.json' % (tmp_dir, archives_path, name)) # Clean temporary directory if tmp_dir != output_directory: os.system('rm -rf %s' % tmp_dir) msignals.display(m18n.n('backup_complete'), 'success')
def backup_restore(name, ignore_apps=False, force=False): """ Restore from a local backup archive Keyword argument: name -- Name of the local backup archive ignore_apps -- Do not restore apps force -- Force restauration on an already installed system """ from yunohost.hook import hook_add from yunohost.hook import hook_callback # Retrieve and open the archive archive_file = backup_info(name)['path'] try: tar = tarfile.open(archive_file, "r:gz") except: logger.exception("unable to open the archive '%s' for reading", archive_file) 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.warning("temporary directory for restoration '%s' already exists", tmp_dir) os.system('rm -rf %s' % tmp_dir) # Extract the tarball msignals.display(m18n.n('backup_extracting_archive')) tar.extractall(tmp_dir) tar.close() # Retrieve backup info try: with open("%s/info.json" % tmp_dir, 'r') as f: info = json.load(f) except IOError: logger.error("unable to retrieve backup info from '%s/info.json'", tmp_dir) raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) else: logger.info("restoring from backup '%s' created on %s", name, time.ctime(info['created_at'])) # Retrieve domain from the backup try: with open("%s/yunohost/current_host" % tmp_dir, 'r') as f: domain = f.readline().rstrip() except IOError: logger.error("unable to retrieve domain from '%s/yunohost/current_host'", tmp_dir) raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) # Check if YunoHost is installed if os.path.isfile('/etc/yunohost/installed'): msignals.display(m18n.n('yunohost_already_installed'), 'warning') 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: raise MoulinetteError(errno.EEXIST, m18n.n('restore_failed')) else: from yunohost.tools import tools_postinstall logger.info("executing the post-install...") tools_postinstall(domain, 'yunohost', True) # Add apps restore hook if not ignore_apps: for app_id in info['apps'].keys(): hook = "/etc/yunohost/apps/%s/scripts/restore" % app_id if os.path.isfile(hook): hook_add(app_id, hook) logger.info("app '%s' will be restored", app_id) else: msignals.display(m18n.n('unrestore_app', app_id), 'warning') # Run hooks msignals.display(m18n.n('restore_running_hooks')) hook_callback('restore', [tmp_dir]) # Remove temporary directory os.system('rm -rf %s' % tmp_dir) msignals.display(m18n.n('restore_complete'), 'success')