Browse Source

Добавлена синхронизация репозиторием.

Добавлены переменные для репозиториев. Добавлены методы взаимодействия с
git.
tags/3.2.0_alpha1
Mike khiretskiy 7 years ago
parent
commit
fd2f069396
4 changed files with 522 additions and 10 deletions
  1. +330
    -0
      update/update.py
  2. +11
    -3
      update/utils/cl_update.py
  3. +174
    -2
      update/variables/update.py
  4. +7
    -5
      update/wsdl_update.py

+ 330
- 0
update/update.py View File

@@ -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

+ 11
- 3
update/utils/cl_update.py View File

@@ -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',


+ 174
- 2
update/variables/update.py View File

@@ -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')

+ 7
- 5
update/wsdl_update.py View File

@@ -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"))]},
]

Loading…
Cancel
Save