From fd2f0693962b7ae8ec45ea4be2501bb7024cbcdb Mon Sep 17 00:00:00 2001 From: Mike khiretskiy Date: Thu, 16 Jan 2014 10:21:42 +0400 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE=D0=BD=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D1=80=D0=B5=D0=BF=D0=BE=D0=B7?= =?UTF-8?q?=D0=B8=D1=82=D0=BE=D1=80=D0=B8=D0=B5=D0=BC.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлены переменные для репозиториев. Добавлены методы взаимодействия с git. --- update/update.py | 330 +++++++++++++++++++++++++++++++++++++ update/utils/cl_update.py | 14 +- update/variables/update.py | 176 +++++++++++++++++++- update/wsdl_update.py | 12 +- 4 files changed, 522 insertions(+), 10 deletions(-) create mode 100644 update/update.py diff --git a/update/update.py b/update/update.py new file mode 100644 index 0000000..0ebda90 --- /dev/null +++ b/update/update.py @@ -0,0 +1,330 @@ +#-*- coding: utf-8 -*- + +# Copyright 2014 Calculate Ltd. http://www.calculate-linux.org +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re +import sys +import time +from os import path +from calculate.lib.utils.files import process,getProgPath,STDOUT,removeDir + +class UpdateError(Exception): + """Update Error""" + +class CloneRepError(Exception): + """Clone repository error""" + +from calculate.lib.cl_lang import setLocalTranslate,getLazyLocalTranslate +setLocalTranslate('cl_update3',sys.modules[__name__]) +__ = getLazyLocalTranslate(_) + +class Update: + """Основной объект для выполнения действий связанных с обновлением системы + + """ + def _checkExistsRep(self,rpath): + """ + Проверить путь на наличие репозитория + """ + if path.exists(rpath): + if not path.isdir(rpath): + raise UpdateError( + _("Repository {path} is not directory").format( + path=rpath)) + if not path.isdir(self.__gitDir(rpath)): + raise UpdateError( + _("Repository {path} is not git").format( + path=rpath)) + return True + return False + + def __getGit(self): + """ + Получить утилиту git + """ + git = getProgPath("/usr/bin/git") + if not git: + raise UpdateError(_("Git utility is not found")) + return git + + def __gitDir(self,rpath): + return path.join(rpath,".git") + + def _cloneRepository(self, url, rpath, branch): + """ + Сделать локальную копию репозитория + + Args: + url: откуда качать репозиторий + rpath: куда сохранять репозиторий + branch: ветка на которую необходимо переключиться + """ + git = self.__getGit() + gitClone = process(git,"clone","-q","--no-single-branch", + "--depth=1","-b",branch,url,rpath,stderr=STDOUT) + if gitClone.failed(): + error = gitClone.read() + if "Remote branch %s not found"%branch in error: + raise UpdateError( + _("Branch {branch} not found in {url} repository").format( + branch=branch,url=url)) + self.printERROR(error) + raise UpdateError(_("Failed to clone {url} repository").format( + url=url)) + return True + + def _cloneRevRepository(self, url, rpath, branch,revision): + """ + Сделать локальную копию репозитория с указанной ревизией + + Args: + url: откуда качать репозиторий + rpath: куда сохранять репозиторий + branch: ветка на которую необходимо переключиться + revision: если указана - сделать ревизию текущей + """ + git = self.__getGit() + git_dir = self.__gitDir(rpath) + for cmd in (["init",rpath], + ["remote","add","origin",url], + ["fetch","--depth=1"], + ["checkout","-b",branch,revision], + ["branch",branch,"-u","origin/%s"%branch]): + if cmd[0] == "init": + gitCmd = process(*[git]+cmd,stderr=STDOUT) + else: + gitCmd = process(*[git,"--git-dir",git_dir, + "--work-tree",rpath]+cmd,stderr=STDOUT) + if gitCmd.failed(): + error = gitCmd.read() + if "reference is not a tree" in error: + raise UpdateError( + _("Commit {revision} not found in {url} repository" + ).format(revision=revision,url=url)) + elif "upstream branch '%s' does not exist"%("origin/%s"%branch): + raise UpdateError( + _("Branch {branch} not found in {url} repository" + ).format(branch=branch,url=url)) + else: + self.printERROR(error) + raise UpdateError(_("Failed to clone {url} repository").format( + url=url)) + return True + + def _pullRepository(self,rpath,quiet_error=False): + """ + Обновить репозиторий до последней версии + """ + git = self.__getGit() + gitPull = process(git,"--git-dir",self.__gitDir(rpath), + "pull","--ff-only",stderr=STDOUT) + if gitPull.failed(): + if not quiet_error: + self.printERROR(gitPull.read()) + raise UpdateError( + _("Failed to update repository in {rpath}").format( + rpath=rpath)) + return False + return True + + def _fetchRepository(self,rpath): + """ + Получить изменения из удаленно репозитория + """ + git = self.__getGit() + gitFetch = process(git,"--git-dir",self.__gitDir(rpath), + "fetch",stderr=STDOUT) + if gitFetch.failed(): + self.printERROR(gitFetch.read()) + raise UpdateError( + _("Failed to update repository in {rpath}").format( + rpath=rpath)) + return True + + def _checkChanges(self,rpath): + """ + Проверить наличие изменений пользователем файлов в репозитории + """ + git = self.__getGit() + git_dir = self.__gitDir(rpath) + git_status = process(git,"--git-dir",git_dir,"--work-tree",rpath, + "status","--porcelain",stderr=STDOUT) + if git_status.success(): + for i in git_status: + if i.strip(): + return False + else: + return True + else: + raise UpdateError( + _("Wrong repository in {rpath} directory").format( + rpath=rpath)) + + def _parseStatusInfo(self,data): + """ + Разобрать информацию полученную через git status -b --porcelain + + Returns: + Словарь + # есть ли измененные файлы пользователем + {'files':True/False, + # есть коммиты после текущего + 'ahead':True/False, + # есть коммиты перед текущим (означает, что обновление + # с основной веткой не осуществляется через fast-forward + 'behind':True/False, + # текущая ветка + 'branch':'', + # оригинальная ветка + 'origin':'origin/master'} + """ + reStatus = re.compile("^## (\w+)(?:\.\.\.(\S+)\s+\[(ahead \d+)?" + "(?:, )?(behind \d+)?\])?\n?(.*|$)",re.S) + match = reStatus.search(data) + if not match: + return {} + return {'files':True if match.group(5) else False, + 'ahead':True if match.group(3) else False, + 'behind':True if match.group(4) else False, + 'origin':match.group(2) or "", + 'branch':match.group(1)} + + def _getCurrentCommit(self,rpath): + """ + Получить текущий коммит в репозитории + """ + git = self.__getGit() + git_dir = self.__gitDir(rpath) + git_show = process(git,"--git-dir",git_dir,"show","--format=format:%H", + "--quiet",stderr=STDOUT) + if git_show.success(): + return git_show.read().strip() + else: + raise UpdateError( + _("Failed to get status of repository in " + "{rpath} directory").format( + rpath=rpath)) + + def _getStatusInfo(self,rpath): + """ + Получить информацию об изменениях в репозитории + + Returns: + Словарь выдаваемый функцией _parseStatusInfo + """ + git = self.__getGit() + git_dir = self.__gitDir(rpath) + git_status = process(git,"--git-dir",git_dir,"--work-tree",rpath, + "status","-b","--porcelain",stderr=STDOUT) + if git_status.success(): + retDict = self._parseStatusInfo(git_status.read()) + if not retDict: + raise UpdateError( + _("Failed to get status of repository in " + "{rpath} directory").format( + rpath=rpath)) + return retDict + else: + raise UpdateError( + _("Wrong repository in {rpath} directory").format( + rpath=rpath)) + + def _resetRepository(self, rpath, to_origin=False, to_rev=None, info=None): + """ + Удалить неиндексированные изменения в репозитории + + + Args: + to_origin: откатить все изменения до удаленного репозитория + to_rev: откатить все изменения до определенной ревизии + info: использовать уже полученную информация об изменения в репозитории + """ + git = self.__getGit() + git_dir = self.__gitDir(rpath) + if to_origin and not info: + info = self._getStatusInfo(rpath) + commit = (info['origin'] if to_origin else to_rev) or "HEAD" + git_reset = process(git,"--git-dir",git_dir,"--work-tree",rpath, + "reset","--hard", commit, stderr=STDOUT) + git_clean = process(git,"--git-dir",git_dir,"--work-tree",rpath, + "clean","-fd",stderr=STDOUT) + if git_reset.failed() or git_clean.failed(): + raise UpdateError(_("Failed to clean {rpath} repository").format( + rpath=rpath)) + + def _getBranch(self,rpath): + """ + Получить текущую ветку + """ + return self._getStatusInfo(rpath)['branch'] + + def _checkoutBranch(self,rpath,branch): + """ + Сменить ветку + """ + git = self.__getGit() + git_dir = self.__gitDir(rpath) + git_checkout = process(git,"--git-dir",git_dir, + "--work-tree", rpath, + "checkout","-f",branch, stderr=STDOUT) + if git_checkout.failed(): + error = git_checkout.read() + if "pathspec '%s' did not match"%branch in error: + raise UpdateError( + _("Branch {branch} not found in {rpath} repository").format( + branch=branch,rpath=rpath)) + self.printERROR(error) + raise UpdateError( + _("Failed to change branch to {branch} in " + "{rpath} repository").format(branch=branch, + rpath=rpath)) + return True + + def syncRepositories(self,repositories): + """ + Синхронизировать репозитории + """ + # TODO: удалять не git репозиторий + # TODO: прогресс скачивания + dv = self.clVars + for name, url, rpath, revision, branch in (reversed( + filter(lambda x:x[0] in repositories, + dv.Get('cl_update_rep_data')))): + if not self._checkExistsRep(rpath): + if revision == "last": + self._cloneRepository(url, rpath, branch) + else: + self._cloneRevRepository(url, rpath, branch, revision) + else: + # если нужно обновиться до конкретной ревизии + if revision != "last": + if revision == self._getCurrentCommit(rpath): + if self._getBranch(rpath) == branch: + continue + # получить изменения из удаленного репозитория + self._fetchRepository(rpath) + # если текущая ветка не соответствует нужной + repInfo = self._getStatusInfo(rpath) + if repInfo['branch'] != branch: + # меняем ветку + self._checkoutBranch(rpath,branch) + if revision == "last": + self._resetRepository(rpath, to_origin=True, + info=repInfo) + else: + self._resetRepository(rpath, to_rev=revision, + info=repInfo) + return True diff --git a/update/utils/cl_update.py b/update/utils/cl_update.py index 23c607d..ab4d14b 100644 --- a/update/utils/cl_update.py +++ b/update/utils/cl_update.py @@ -20,6 +20,7 @@ from calculate.lib.cl_lang import setLocalTranslate,getLazyLocalTranslate from calculate.lib.utils.files import FilesError from calculate.install.install import (MigrationError, TemplatesError, InstallError) +from calculate.update.update import UpdateError setLocalTranslate('cl_update3',sys.modules[__name__]) __ = getLazyLocalTranslate(_) @@ -29,17 +30,24 @@ class ClUpdateAction(Action): Действие обновление конфигурационных файлов """ # ошибки, которые отображаются без подробностей - native_error = (FilesError,) + native_error = (FilesError,UpdateError) successMessage = None - failedMessage = __("Revision update failed") + failedMessage = __("Update failed") interruptMessage = __("Update manually interrupted") # список задач для дейсвия tasks = [ + {'name':'sync_reps', + 'message' : __("Syncing {cl_update_sync_rep} repositories"), + 'method':'Update.syncRepositories(update.cl_update_sync_rep)', + 'condition':lambda Get:Get('cl_update_sync_rep') + }, {'name':'dispatch', - 'method':'Install.applyTemplates(install.cl_source,cl_template_clt_set,'\ + 'method':'Update.applyTemplates(install.cl_source,cl_template_clt_set,'\ 'True,None)', + 'condition':lambda Get:(Get('cl_update_rev_set') == 'on' or + Get('cl_rebuild_world_set') == 'on') }, # сообщение удачного завершения при обновлении ревизии {'name':'success_rev', diff --git a/update/variables/update.py b/update/variables/update.py index 408f4fa..12dcd70 100644 --- a/update/variables/update.py +++ b/update/variables/update.py @@ -16,8 +16,12 @@ import os import sys +import re from os import path -from calculate.lib.datavars import Variable,VariableError,ReadonlyVariable +from calculate.lib.datavars import (Variable,VariableError,ReadonlyVariable, + ReadonlyTableVariable,TableVariable) +from calculate.lib.utils.portage import searchProfile +from calculate.lib.utils.files import readLinesFile, readFile from calculate.lib.cl_lang import setLocalTranslate setLocalTranslate('cl_update3',sys.modules[__name__]) @@ -62,11 +66,179 @@ class VariableClUpdateRevSet(Variable): opt = ["--update-rev"] untrusted = True value = "off" + check_after = ["cl_update_sync_rep"] def init(self): self.help = _("revision update") self.label = _("Revision update") def check(self,value): - if value != "on" and self.Get('cl_rebuild_world_set') != 'on': + if ( value == "off" and self.Get('cl_rebuild_world_set') != 'on' and + not self.Get('cl_update_sync_rep')): raise VariableError(_("Select at least one update action")) + +class VariableClUpdateRep(Variable): + """ + Обновлять репозитории до конкретной ревизии или до последней + """ + type = "choice" + value = "rev" + + def choice(self): + return ["last","rev"] + +class VariableClUpdateRepData(ReadonlyTableVariable): + """ + Информация о репозиториях + """ + source = ['cl_update_rep_name', + 'cl_update_rep_url', + 'cl_update_rep_path', + 'cl_update_rep_rev', + 'cl_update_branch_name'] + +class VariableClUpdateRepName(Variable): + """ + Список имен используемых репозиториев + """ + type = "list" + value = [] + +class VariableClUpdateRepUrl(Variable): + """ + Список путей до репозиториев + """ + type = "list" + value = [] + +class VariableClUpdateSystemProfile(ReadonlyVariable): + """ + Профиль системы (симлинк /etc/make.profile') + """ + def get(self): + try: + return path.normpath( + path.join('/etc',os.readlink('/etc/make.profile'))) + except: + raise VariableError(_("Failed to determine system profile")) + +class VariableClUpdateLaymanStorage(ReadonlyVariable): + """ + Путь к репозиториям layman + """ + def get(self): + laymanConf = "/etc/layman/layman.cfg" + reStorage = re.compile("^storage\s*:\s*(\S+)") + if path.exists(laymanConf): + for line in readLinesFile(laymanConf): + match = reStorage.search(line) + if match: + return match.group(1) + return "/var/lib/layman" + +class VariableClUpdateRepPath(ReadonlyVariable): + """ + Пути до репозиториев + """ + type = "list" + mapPath = {'portage':'/usr/portage'} + + def get(self): + repPath = self.Get('cl_update_layman_storage') + def generatePaths(names): + for name in names: + if name in self.mapPath: + yield self.mapPath[name] + else: + yield path.join(repPath,name) + return list(generatePaths(self.Get('cl_update_rep_name'))) + +class VariableClUpdateRepRev(Variable): + """ + Ревизии до которых необходимо обновить репозитории + """ + type = "list" + + def get(self): + if self.Get('cl_update_rep') == 'rev': + revPaths = searchProfile(self.Get('cl_update_system_profile'), + "rev") + if revPaths: + revPath = revPaths[-1] + dictNamesRevs = dict(map(lambda x:x.strip().partition('=')[::2], + readLinesFile(revPath))) + return map(lambda x:dictNamesRevs.get(x,"last"), + self.Get('cl_update_rep_name')) + return ["last"]*len(self.Get('cl_update_rep_name')) + +class VariableClUpdateBranch(TableVariable): + """ + Выбор веток репозиториев до которых необходимо обновиться + """ + opt = ["--branch"] + metavalue = 'BRANCHES' + untrusted = True + source = ["cl_update_branch_rep", + "cl_update_branch_name"] + + def init(self): + self.help = _("set branches for repository (REPOSITORY:BRANCH)") + self.label = _("Repository branch") + + def raiseReadonlyIndexError(self,fieldname="",variablename="", + value=""): + """ + Неизвестный оврелей + """ + raise VariableError(_("Repository %s not found")%value) + +class VariableClUpdateBranchRep(ReadonlyVariable): + """ + Список доступных репозиторием + """ + type = "list" + + def init(self): + self.label = _("Repository") + + def get(self): + return self.Get('cl_update_rep_name') + +class VariableClUpdateBranchName(Variable): + """ + Список доступных репозиторием + """ + type = "choiceedit-list" + + def init(self): + self.label = _("Branch") + + def choice(self): + return ["master","develop","update"] + + def get(self): + def generateBranch(): + for reppath in self.Get('cl_update_rep_path'): + headPath = path.join(reppath,".git/HEAD") + yield readFile(headPath).rpartition('/')[2].strip() or "master" + return list(generateBranch()) + +class VariableClUpdateSyncRep(Variable): + """ + Обновляемый репозиторий + """ + type = "choice-list" + element = "selecttable" + opt = ["cl_update_sync_rep"] + metavalue = "REPOSITORIES" + untrusted = True + + def init(self): + self.help = _("synchronize repositories (all by default)") + self.label = _("Synchronize repositories") + + def get(self): + return self.Get('cl_update_rep_name') + + def choice(self): + return self.Get('cl_update_rep_name') diff --git a/update/wsdl_update.py b/update/wsdl_update.py index 54ff17b..8980aaa 100644 --- a/update/wsdl_update.py +++ b/update/wsdl_update.py @@ -19,7 +19,8 @@ import sys, time, os from calculate.lib.datavars import VariableError,DataVarsError,DataVars from calculate.core.server.func import WsdlBase -from calculate.install.install import InstallError,Install +from calculate.install.install import InstallError +from calculate.update.update import Update,UpdateError from utils.cl_update import ClUpdateAction from calculate.lib.cl_lang import setLocalTranslate,getLazyLocalTranslate setLocalTranslate('cl_update3',sys.modules[__name__]) @@ -37,7 +38,7 @@ class Wsdl(WsdlBase): # категория метода 'category':__('Update'), # заголовок метода - 'title':__("Update configuration"), + 'title':__("Update system"), # иконка для графической консоли 'image':'software-properties,preferences-desktop', # метод присутствует в графической консоли @@ -47,7 +48,7 @@ class Wsdl(WsdlBase): # права для запуска метода 'rights':['update'], # объект содержащий модули для действия - 'logic':{'Install':Install}, + 'logic':{'Update':Update}, # описание действия 'action':ClUpdateAction, # объект переменных @@ -58,9 +59,10 @@ class Wsdl(WsdlBase): 'setvars':{'cl_action!':'sync'}, # описание груп (список лямбда функций) 'groups':[ - lambda group:group(_("Update configuration"), + lambda group:group(_("Update system"), normal=('cl_rebuild_world_set','cl_update_rev_set'), - expert=('cl_templates_locate', + expert=('cl_update_sync_rep', 'cl_update_branch', + 'cl_templates_locate', 'cl_verbose_set','cl_dispatch_conf'), next_label=_("Update"))]}, ]