[PATCH 00/10] Fixes on repository management feature

From: Aline Manera <alinefm@br.ibm.com> This patch set also fixes the issue #336 (https://github.com/kimchi-project/kimchi/issues/336) In addition to several fixes found in repositories.py Aline Manera (10): bug fix: Expose repository management tool name bug fix: Reorganize repository information mockmodel: Move specific repository data under 'config' Update messages used in the repositories management feature bug fix: Raise exception comming from backend bug fix: Sort repositories bug fix: Let package manager tool create the repository ID bug fix: Do not store internal repository information Update test cases to reflect the repositories changes bug fix: Lock yum/apt operations docs/API.md | 82 +++-- po/en_US.po | 186 ++++++++--- po/kimchi.pot | 177 ++++++++--- po/pt_BR.po | 186 ++++++++--- po/zh_CN.po | 186 ++++++++--- src/kimchi/API.json | 111 ++++--- src/kimchi/config.py.in | 3 + src/kimchi/control/host.py | 3 +- src/kimchi/i18n.py | 36 ++- src/kimchi/mockmodel.py | 145 ++++----- src/kimchi/model/config.py | 6 +- src/kimchi/model/host.py | 30 +- src/kimchi/repositories.py | 730 +++++++++++++++++++------------------------- src/kimchi/swupdate.py | 7 + tests/test_model.py | 151 +++++---- tests/test_rest.py | 10 +- 16 files changed, 1188 insertions(+), 861 deletions(-) -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> More than expose if there is a repository management tool recognized by Kimchi, we also need to expose which tool is it. The repository information depends on its type and UI needs to know it to properly build the view. Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- docs/API.md | 4 ++-- src/kimchi/mockmodel.py | 2 +- src/kimchi/model/config.py | 6 +++--- src/kimchi/repositories.py | 4 ++++ 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/API.md b/docs/API.md index 672ef14..147511d 100644 --- a/docs/API.md +++ b/docs/API.md @@ -581,8 +581,8 @@ creation. the system; False, otherwise. * update_tool: True if there is a compatible package manager for the system; False, otherwise - * repo_mngt_tool: True if there is a compatible repository management tool - for the system; False, otherwise + * repo_mngt_tool: 'deb', 'yum' or None - when the repository management + tool is not identified * **POST**: *See Configuration Actions* **Actions (POST):** diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index 743c7c6..d2adb4d 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -69,7 +69,7 @@ class MockModel(object): 'screenshot': True, 'system_report_tool': True, 'update_tool': True, - 'repo_mngt_tool': True} + 'repo_mngt_tool': 'yum'} def reset(self): self._mock_vms = {} diff --git a/src/kimchi/model/config.py b/src/kimchi/model/config.py index 5b27188..a6a25e4 100644 --- a/src/kimchi/model/config.py +++ b/src/kimchi/model/config.py @@ -82,11 +82,11 @@ class CapabilitiesModel(object): update_tool = True try: - Repositories() + repo = Repositories() except Exception: - repo_mngt_tool = False + repo_mngt_tool = None else: - repo_mngt_tool = True + repo_mngt_tool = repo._pkg_mnger.TYPE return {'libvirt_stream_protocols': self.libvirt_stream_protocols, 'qemu_stream': self.qemu_stream, diff --git a/src/kimchi/repositories.py b/src/kimchi/repositories.py index 2727c84..e685991 100644 --- a/src/kimchi/repositories.py +++ b/src/kimchi/repositories.py @@ -193,6 +193,8 @@ class YumRepo(object): It's loaded only on those systems listed at YUM_DISTROS and loads necessary modules in runtime. """ + TYPE = 'yum' + def __init__(self): self._yb = getattr(__import__('yum'), 'YumBase')() self._repos = self._yb.repos @@ -413,6 +415,8 @@ class AptRepo(object): It's loaded only on those systems listed at YUM_DISTROS and loads necessary modules in runtime. """ + TYPE = 'deb' + def __init__(self): getattr(__import__('apt_pkg'), 'init_config')() getattr(__import__('apt_pkg'), 'init_system')() -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> There are some specific repository data according to its type. 'deb' repositories have dist and componenets and that information does not make sense for a 'yum' repository. The same way a 'yum' repository has name, mirrorlist, gpgcheck, gpgkey that only make sense for this type of repo. Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- docs/API.md | 78 +++++++++++++------------------ src/kimchi/API.json | 111 ++++++++++++++++++++++++++++---------------- src/kimchi/control/host.py | 3 +- 3 files changed, 104 insertions(+), 88 deletions(-) diff --git a/docs/API.md b/docs/API.md index 147511d..8d6682b 100644 --- a/docs/API.md +++ b/docs/API.md @@ -848,19 +848,16 @@ Contains the information for a specific package to be updated. * **GET**: Retrieve a summarized list of all repositories available * **POST**: Add a new repository - * repo_id *(optional)*: Unique repository name for each repository, -one word. * baseurl: URL to the repodata directory when "is_mirror" is false. Otherwise, it can be URL to the mirror system for YUM. Can be an http://, ftp:// or file:// URL. - * is_mirror *(optional)*: Set the given URI of baseurl as a mirror -list, instead of use baseurl in repository configuration. - * url_args *(optional)*: Arguments to be passed to baseurl, like the -list of APT repositories provided by the same baseurl. - * gpgkey *(optional)*: URL pointing to the ASCII-armored GPG key -file for the repository. This option is used if yum needs a public key -to verify a package and the required key hasn't been imported into the -RPM database. + * repo_id *(optional)*: Unique YUM repository ID + * config: A dictionary that contains specific data according to repository + type. + * mirrorlist *(optional)*: Specifies a URL to a file containing a + list of baseurls for YUM repository + * dist: Distribution to DEB repository + * comps *(optional)*: List of components to DEB repository ### Resource: Repository @@ -870,47 +867,38 @@ RPM database. * **GET**: Retrieve the full description of a Repository * repo_id: Unique repository name for each repository, one word. - * repo_name: Human-readable string describing the repository. * baseurl: URL to the repodata directory when "is_mirror" is false. Otherwise, it can be URL to the mirror system for YUM. Can be an http://, ftp:// or file:// URL. - * is_mirror: Set the given URI of baseurl as a mirror list, instead -of use baseurl in repository configuration. - * url_args: Arguments to be passed to baseurl, like the list of APT -repositories provided by the same baseurl. - * enabled: Indicates if repository should be included as a package -source: - * false: Do not include the repository. - * true: Include the repository. - * gpgcheck: Indicates if a GPG signature check on the packages gotten -from repository should be performed: - * false: Do not check GPG signature - * true: Check GPG signature - * gpgkey: URL pointing to the ASCII-armored GPG key file for the -repository. This option is used if yum needs a public key to verify a package -and the required key hasn't been imported into the RPM database. + * enabled: True, when repository is enabled; False, otherwise + * config: A dictionary that contains specific data according to repository + type. + * repo_name: Human-readable string describing the YUM repository. + * mirrorlist: Specifies a URL to a file containing a list of baseurls + for YUM repository + * gpgcheck: True, to enable GPG signature verification; False, otherwise. + * gpgkey: URL pointing to the ASCII-armored GPG key file for the YUM + repository. + * dist: Distribution to DEB repository + * comps: List of components to DEB repository + * **DELETE**: Remove the Repository * **POST**: *See Repository Actions* * **PUT**: update the parameters of existing Repository - * repo_id *(otional)*: Unique repository name for each repository, -one word. - * repo_name *(otional)*: Human-readable string describing the -repository. - * baseurl *(optional)*: URL to the repodata directory when -"is_mirror" is false. Otherwise, it can be URL to the mirror system for -YUM. Can be an http://, ftp:// or file:// URL. - * is_mirror *(optional)*: Set the given URI of baseurl as a mirror -list, instead of use baseurl in repository configuration. - * url_args *(optional)*: Arguments to be passed to baseurl, like the -list of APT repositories provided by the same baseurl. - * gpgcheck *(optional)*: Indicates if a GPG signature check on the -packages gotten from repository should be performed: - * false: Do not check GPG signature - * true: Check GPG signature - * gpgkey *(optional)*: URL pointing to the ASCII-armored GPG key -file for the repository. This option is used if yum needs a public key -to verify a package and the required key hasn't been imported into the -RPM database. + * repo_id: Unique repository name for each repository, one word. + * baseurl: URL to the repodata directory when "is_mirror" is false. +Otherwise, it can be URL to the mirror system for YUM. Can be an +http://, ftp:// or file:// URL. + * config: A dictionary that contains specific data according to repository + type. + * repo_name: Human-readable string describing the YUM repository. + * mirrorlist: Specifies a URL to a file containing a list of baseurls + for YUM repository + * gpgcheck: True, to enable GPG signature verification; False, otherwise. + * gpgkey: URL pointing to the ASCII-armored GPG key file for the YUM + repository. + * dist: Distribution to DEB repository + * comps: List of components to DEB repository **Actions (POST):** diff --git a/src/kimchi/API.json b/src/kimchi/API.json index 6e932ed..cf89ce4 100644 --- a/src/kimchi/API.json +++ b/src/kimchi/API.json @@ -522,7 +522,7 @@ "type": "object", "properties": { "repo_id": { - "description": "Unique repository name for each repository, one word.", + "description": "Repository ID used for YUM repository.", "type": "string", "error": "KCHREPOS0001E" }, @@ -532,19 +532,33 @@ "required": true, "error": "KCHREPOS0002E" }, - "is_mirror": { - "description": "Set the given URI of baseurl as a mirror list", - "type": "boolean" - }, - "url_args": { - "description": "Arguments to be passed to baseurl, like the list of APT repositories provided by the same baseurl.", - "type": "string", - "error": "KCHREPOS0003E" - }, - "gpgkey": { - "description": "URL pointing to the ASCII-armored GPG key file for the repository.", - "type": "string", - "error": "KCHREPOS0004E" + "config": { + "description": "Dictionary containing repository configuration", + "type": "object", + "error": "KCHREPOS0003E", + "properties": { + "dist": { + "description": "Distribution to DEB repository", + "type": "string", + "error": "KCHREPOS0004E" + }, + "comps": { + "description": "List of components to DEB repository", + "type": "array", + "error": "KCHREPOS0005E", + "uniqueItems": true, + "items": { + "description": "Component name", + "type": "string", + "error": "KCHREPOS0006E" + } + }, + "mirrorlist": { + "description": "URL to a file containing a list of baseurls", + "type": "string", + "error": "KCHREPOS0007E" + } + } } }, "additionalProperties": false, @@ -553,38 +567,53 @@ "repository_update": { "type": "object", "properties": { - "repo_id": { - "description": "Unique repository name for each repository, one word.", - "type": "string", - "error": "KCHREPOS0001E" - }, - "repo_name": { - "description": "Human-readable string describing the repository.", - "type": "string", - "error": "KCHREPOS0005E" - }, "baseurl": { "description": "URL to the directory where the repodata directory of a repository is located. Can be an http://, ftp:// or file:// URL.", "type": "string", "error": "KCHREPOS0002E" }, - "is_mirror": { - "description": "Set the given URI of baseurl as a mirror list", - "type": "boolean" - }, - "url_args": { - "description": "Arguments to be passed to baseurl, like the list of APT repositories provided by the same baseurl.", - "type": "string", - "error": "KCHREPOS0003E" - }, - "gpgcheck": { - "description": "Indicates if a GPG signature check on the packages gotten from repository should be performed.", - "type": "boolean" - }, - "gpgkey": { - "description": "URL pointing to the ASCII-armored GPG key file for the repository.", - "type": "string", - "error": "KCHREPOS0004E" + "config": { + "description": "Dictionary containing repository configuration", + "type": "object", + "error": "KCHREPOS0003E", + "properties": { + "dist": { + "description": "Distribution to DEB repository", + "type": "string", + "error": "KCHREPOS0004E" + }, + "comps": { + "description": "List of components to DEB repository", + "type": "array", + "error": "KCHREPOS0005E", + "uniqueItems": true, + "items": { + "description": "Component name", + "type": "string", + "error": "KCHREPOS0006E" + } + }, + "repo_name": { + "description": "Human-readable string describing the YUM repository.", + "type": "string", + "error": "KCHREPOS0008E" + }, + "mirrorlist": { + "description": "URL to a file containing a list of baseurls for YUM repository", + "type": "string", + "error": "KCHREPOS0007E" + }, + "gpgcheck": { + "description": "Indicates if a GPG signature check on the packages gotten from repository should be performed.", + "type": "boolean", + "error": "KCHREPOS0009E" + }, + "gpgkey": { + "description": "URL pointing to the ASCII-armored GPG key file for the repository.", + "type": "string", + "error": "KCHREPOS0010E" + } + } } }, "additionalProperties": false, diff --git a/src/kimchi/control/host.py b/src/kimchi/control/host.py index edcc0ad..cfc24bd 100644 --- a/src/kimchi/control/host.py +++ b/src/kimchi/control/host.py @@ -113,8 +113,7 @@ class Repositories(Collection): class Repository(Resource): def __init__(self, model, id): super(Repository, self).__init__(model, id) - self.update_params = ["repo_id", "repo_name", "baseurl", "is_mirror", - "url_args", "gpgcheck", "gpgkey"] + self.update_params = ["config", "baseurl"] self.uri_fmt = "/host/repositories/%s" self.enable = self.generate_action_handler('enable') self.disable = self.generate_action_handler('disable') -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> There are some specific repository data according to its type. 'deb' repositories have dist and componenets and that information does not make sense for a 'yum' repository. The same way a 'yum' repository has name, id, mirrorlist, gpgcheck, gpgkey that only make sense for this type of repo. Also let the exception comming from the backend be raised. As it contains more details about the problem. Otherwise, we will always show generic error message "Unable to update repository" Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/mockmodel.py | 143 ++++++++++++++++++++--------------------------- 1 file changed, 61 insertions(+), 82 deletions(-) diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index d2adb4d..d40f61a 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -840,22 +840,20 @@ class MockModel(object): return self.task_lookup(task_id) def repositories_get_list(self): - return self._mock_host_repositories.getRepositories().keys() + return self._mock_host_repositories.getRepositories() def repositories_create(self, params): - repo_id = params.get('repo_id', None) - # Create a repo_id if not given by user. The repo_id will follow # the format kimchi_repo_<integer>, where integer is the number of # seconds since the Epoch (January 1st, 1970), in UTC. + repo_id = params.get('repo_id', None) if repo_id is None: - repo_id = "kimchi_repo_%s" % int(time.time()) - while repo_id in self.repositories_get_list(): - repo_id = "kimchi_repo_%s" % int(time.time()) + repo_id = "kimchi_repo_%s" % str(int(time.time() * 1000)) params.update({'repo_id': repo_id}) if repo_id in self.repositories_get_list(): - raise InvalidOperation("KCHREPOS0006E", {'repo_id': repo_id}) + raise InvalidOperation("KCHREPOS0022E", {'repo_id': repo_id}) + self._mock_host_repositories.addRepository(params) return repo_id @@ -866,19 +864,13 @@ class MockModel(object): return self._mock_host_repositories.removeRepository(repo_id) def repository_enable(self, repo_id): - if not self._mock_host_repositories.enableRepository(repo_id): - raise OperationFailed("KCHREPOS0007E", {'repo_id': repo_id}) + return self._mock_host_repositories.enableRepository(repo_id) def repository_disable(self, repo_id): - if not self._mock_host_repositories.disableRepository(repo_id): - raise OperationFailed("KCHREPOS0008E", {'repo_id': repo_id}) + return self._mock_host_repositories.disableRepository(repo_id) def repository_update(self, repo_id, params): - try: - self._mock_host_repositories.updateRepository(repo_id, params) - except: - raise OperationFailed("KCHREPOS0009E", {'repo_id': repo_id}) - return repo_id + return self._mock_host_repositories.updateRepository(repo_id, params) class MockVMTemplate(VMTemplate): @@ -1129,95 +1121,82 @@ class MockSoftwareUpdate(object): class MockRepositories(object): def __init__(self): - self._repo_storage = {"kimchi_repo_1392167832": - {"repo_id": "kimchi_repo_1392167832", - "gpgkey": None, - "enabled": True, - "baseurl": "http://www.fedora.org", - "url_args": None, - "gpgcheck": True, - "is_mirror": False, - "repo_name": "kimchi_repo_1392167832"}} - - def addRepository(self, params={}): + self._repos = {"kimchi_repo_1392167832": + {"repo_id": "kimchi_repo_1392167832", + "enabled": True, + "baseurl": "http://www.fedora.org", + "config": {"repo_name": "kimchi_repo_1392167832", + "gpgkey": [], + "gpgcheck": True, + "mirrorlist": ""} + } + } + + def addRepository(self, params): # Create and enable the repository - repo_id = params.get('repo_id') + repo_id = params['repo_id'] repo = {'repo_id': repo_id, - 'repo_name': params.get('repo_name', repo_id), 'baseurl': params.get('baseurl'), - 'url_args': params.get('url_args', None), 'enabled': True, - 'gpgkey': params.get('gpgkey', None), - 'is_mirror': params.get('is_mirror', False)} + 'config': {'repo_name': params.get('repo_name', repo_id), + 'gpgkey': params.get('gpgkey', []), + 'gpgcheck': True, + 'mirrorlist': params.get('mirrorlist', "")} + } - if repo['gpgkey'] is not None: - repo['gpgcheck'] = True - else: - repo['gpgcheck'] = False - - self._repo_storage[repo_id] = repo + self._repos[repo_id] = repo + return repo_id def getRepositories(self): - return self._repo_storage + return self._repos.keys() def getRepository(self, repo_id): - if not repo_id in self._repo_storage.keys(): - raise NotFoundError("KCHREPOS0010E", {'repo_id': repo_id}) - - repo = self._repo_storage[repo_id] - if (isinstance(repo['baseurl'], list)) and (len(repo['baseurl']) > 0): - repo['baseurl'] = repo['baseurl'][0] - - return repo + if not repo_id in self._repos.keys(): + raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id}) - def enabledRepositories(self): - enabled_repos = [] - for repo_id in self._repo_storage.keys(): - if self._repo_storage[repo_id]['enabled']: - enabled_repos.append(repo_id) - return enabled_repos + return self._repos[repo_id] def enableRepository(self, repo_id): + if not repo_id in self._repos.keys(): + raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id}) + + info = self._repos[repo_id] # Check if repo_id is already enabled - if repo_id in self.enabledRepositories(): - raise NotFoundError("KCHREPOS0011E", {'repo_id': repo_id}) + if info['enabled']: + raise NotFoundError("KCHREPOS0015E", {'repo_id': repo_id}) - try: - repo = self.getRepository(repo_id) - repo['enabled'] = True - self.updateRepository(repo_id, repo) - return True - except: - raise OperationFailed("KCHREPOS0007E", {'repo_id': repo_id}) + info['enabled'] = True + self._repos[repo_id] = info + return repo_id def disableRepository(self, repo_id): - # Check if repo_id is already disabled - if not repo_id in self.enabledRepositories(): + if not repo_id in self._repos.keys(): raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id}) - try: - repo = self.getRepository(repo_id) - repo['enabled'] = False - self.updateRepository(repo_id, repo) - return True - except: - raise OperationFailed("KCHREPOS0008E", {'repo_id': repo_id}) + info = self._repos[repo_id] + # Check if repo_id is already disabled + if not info['enabled']: + raise NotFoundError("KCHREPOS0016E", {'repo_id': repo_id}) - def updateRepository(self, repo_id, new_repo={}): - if (len(new_repo) == 0): - raise InvalidParameter("KCHREPOS0013E") + info['enabled'] = False + self._repos[repo_id] = info + return repo_id - repo = self._repo_storage[repo_id] - repo.update(new_repo) - del self._repo_storage[repo_id] - self._repo_storage[repo_id] = repo + def updateRepository(self, repo_id, params): + if not repo_id in self._repos.keys(): + raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id}) + + info = self._repos[repo_id] + info.update(params) + del self._repos[repo_id] + self._repos[info['repo_id']] = info + return info['repo_id'] def removeRepository(self, repo_id): - if not repo_id in self._repo_storage.keys(): - raise NotFoundError("KCHREPOS0010E", {'repo_id': repo_id}) + if not repo_id in self._repos.keys(): + raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id}) - del self._repo_storage[repo_id] - return True + del self._repos[repo_id] def get_mock_environment(): -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> There is no need to have generic error messages. We should provide the most detailed message we can. Also update the .po files to reflect those changes Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- po/en_US.po | 186 +++++++++++++++++++++++++++++++++++++++++----------- po/kimchi.pot | 177 ++++++++++++++++++++++++++++++++++++++----------- po/pt_BR.po | 186 +++++++++++++++++++++++++++++++++++++++++----------- po/zh_CN.po | 186 +++++++++++++++++++++++++++++++++++++++++----------- src/kimchi/i18n.py | 36 +++++----- 5 files changed, 596 insertions(+), 175 deletions(-) diff --git a/po/en_US.po b/po/en_US.po index 23926d6..e20edf7 100644 --- a/po/en_US.po +++ b/po/en_US.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: kimchi 0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-03-07 00:54-0300\n" +"POT-Creation-Date: 2014-03-18 08:09-0300\n" "PO-Revision-Date: 2013-07-11 17:32-0400\n" "Last-Translator: Crístian Viana <vianac@linux.vnet.ibm.com>\n" "Language-Team: English\n" @@ -70,6 +70,13 @@ msgstr "Create" msgid "Edit Guest" msgstr "Edit Guest" +#, fuzzy +msgid "General" +msgstr "Generate" + +msgid "Storage" +msgstr "Storage" + msgid "Name" msgstr "Name" @@ -79,12 +86,18 @@ msgstr "CPUs" msgid "Icon" msgstr "Icon" -msgid "Cancel" -msgstr "Cancel" +msgid "Attach" +msgstr "" msgid "Save" msgstr "Save" +msgid "Replace" +msgstr "" + +msgid "Detach" +msgstr "" + msgid "Start" msgstr "Start" @@ -97,7 +110,10 @@ msgstr "Stop" msgid "Actions" msgstr "Actions" -msgid "Console" +msgid "Connect" +msgstr "Connect" + +msgid "Manage Media" msgstr "" msgid "Edit" @@ -127,9 +143,6 @@ msgstr "Guests" msgid "Templates" msgstr "Templates" -msgid "Storage" -msgstr "Storage" - msgid "Network" msgstr "Network" @@ -151,27 +164,36 @@ msgstr "" msgid "options needed." msgstr "" +msgid "" +"Can not contact the host system. Verify the host system is up and that you " +"have network connectivity to it. HTTP request response %1. " +msgstr "" + msgid "Delete Confirmation" msgstr "Delete Confirmation" msgid "OK" msgstr "OK" +msgid "Cancel" +msgstr "Cancel" + msgid "Confirm" msgstr "Confirm" msgid "Warning" msgstr "Warning" +#, fuzzy +msgid "Loading..." +msgstr "Generating..." + msgid "No iso found" msgstr "No iso found" msgid "This is not a valid ISO file." msgstr "This is not a valid ISO file." -msgid "Create template successfully" -msgstr "Create template successfully" - msgid "It will take long time. Do you want to continue?" msgstr "" @@ -209,6 +231,28 @@ msgstr "" "Shutting down or restarting host will cause unsaved work lost. Continue to " "shut down/restarting?" +msgid "Software Updates" +msgstr "" + +msgid "Package Name" +msgstr "" + +msgid "Version" +msgstr "Version" + +msgid "Architecture" +msgstr "" + +msgid "Repository" +msgstr "" + +msgid "Update All" +msgstr "" + +#, fuzzy +msgid "Updating..." +msgstr "Generating..." + msgid "" "Debug report will be removed permanently and can't be recovered. Do you want " "to continue?" @@ -248,6 +292,27 @@ msgid "" "cannot be undone. Would you like to continue?" msgstr "" +msgid "" +"This CDROM will be detached permanently and you can re-attach it. Continue " +"to detach it?" +msgstr "" + +msgid "Attaching..." +msgstr "" + +#, fuzzy +msgid "Replacing..." +msgstr "Generating..." + +msgid "Successfully attached!" +msgstr "" + +msgid "Successfully replaced!" +msgstr "" + +msgid "Successfully detached!" +msgstr "" + msgid "The VLAN id must be between 1 and 4094." msgstr "The VLAN id must be between 1 and 4094." @@ -312,6 +377,11 @@ msgstr "" msgid "No available partitions found." msgstr "No templates found." +msgid "" +"This storage pool is not persistent. Instead of deactivate, this action will " +"permanently delete it. Would you like to continue?" +msgstr "" + msgid "Help" msgstr "" @@ -427,9 +497,6 @@ msgstr "Shut down" msgid "Restart" msgstr "Restart" -msgid "Connect" -msgstr "Connect" - msgid "Basic Information" msgstr "Basic Information" @@ -448,6 +515,9 @@ msgstr "System Statistics" msgid "Collecting data after leaving this page" msgstr "Collecting data after leaving this page" +msgid "Update Progress" +msgstr "" + msgid "Network Name" msgstr "Network Name" @@ -487,9 +557,6 @@ msgstr "VLAN ID" msgid "No templates found." msgstr "No templates found." -msgid "Version" -msgstr "Version" - msgid "Location" msgstr "Location" @@ -692,6 +759,15 @@ msgid "Bad format while reading volume descriptor in ISO %(filename)s" msgstr "" #, python-format +msgid "" +"The hypervisor doesn't have permission to use this ISO %(filename)s. " +"Consider moving it under /var/lib/libvirt, or set the search permission to " +"file access control lists for '%(user)s' user if possible, or add the " +"'%(user)s' to the ISO path group, or (not recommended) 'chmod -R o+x " +"'path_to_iso'.Details: %(err)s" +msgstr "" + +#, python-format msgid "Virtual machine %(name)s already exists" msgstr "" @@ -1052,6 +1128,18 @@ msgstr "" msgid "Storage type %(type)s does not support volume create and delete" msgstr "" +msgid "Storage volume name must be a string" +msgstr "" + +msgid "Storage volume allocation must be an integer number" +msgstr "" + +msgid "Storage volume format not supported" +msgstr "" + +msgid "Storage volume requires a volume name" +msgstr "" + #, python-format msgid "Interface %(name)s does not exist" msgstr "" @@ -1116,14 +1204,14 @@ msgstr "" #, python-format msgid "" -"Unable to delete network %(name)s. There are still some VMs linked to this " -"network." +"Unable to delete network %(name)s. There are some virtual machines and/or " +"templates linked to this network." msgstr "" #, python-format msgid "" -"Unable to deactivate network %(name)s. There are some VMs running linked to " -"this network." +"Unable to deactivate network %(name)s. There are some virtual machines and/" +"or templates linked to this network." msgstr "" #, python-format @@ -1245,31 +1333,36 @@ msgstr "" msgid "Specify path to update virtual machine disk" msgstr "" -msgid "Repository ID must be one word only string." +msgid "YUM Repository ID must be one word only string." msgstr "" msgid "Repository URL must be an http://, ftp:// or file:// URL." msgstr "" -msgid "Repository URL arguments must be string." +msgid "" +"Repository configuration is a dictionary with specific values according to " +"repository type." msgstr "" -msgid "GPG key must be a URL pointing to the ASCII-armored file." +msgid "Distribution to DEB repository must be a string" msgstr "" -msgid "Repository name must be string." +msgid "Components to DEB repository must be listed in a array" msgstr "" -#, python-format -msgid "Repository %(repo_id)s already exists." +msgid "Components to DEB repository must be a string" msgstr "" -#, python-format -msgid "Could not enable repository %(repo_id)s." +msgid "Mirror list to DEB repository must be a string" msgstr "" -#, python-format -msgid "Could not disable repository %(repo_id)s." +msgid "YUM Repository name must be string." +msgstr "" + +msgid "GPG check must be a boolean value." +msgstr "" + +msgid "GPG key must be a URL pointing to the ASCII-armored file." msgstr "" #, python-format @@ -1277,33 +1370,46 @@ msgid "Could not update repository %(repo_id)s." msgstr "" #, python-format -msgid "Repository %(repo_id)s does not exists." +msgid "Repository %(repo_id)s does not exist." msgstr "" -#, python-format -msgid "There is no disabled repository called %(repo_id)s." +msgid "" +"Specify repository base URL or mirror list in order to create a YUM " +"repository." +msgstr "" + +msgid "Repository management tool was not recognized for your system." msgstr "" #, python-format -msgid "There is no enabled repository called %(repo_id)s." +msgid "Repository %(repo_id)s is already enabled." msgstr "" -msgid "There are no parameters to update repository." +#, python-format +msgid "Repository %(repo_id)s is already disabled." msgstr "" -msgid "OS distro not supported." +#, python-format +msgid "Could not remove repository %(repo_id)s." msgstr "" -msgid "There is no YUM configuration directory." +#, python-format +msgid "Could not write repository configuration file %(repo_file)s" msgstr "" -msgid "There are no parameters to create a new repo file." +msgid "Specify repository distribution in order to create a DEB repository." msgstr "" #, python-format -msgid "Could not write repo file %(repo_file)s" +msgid "Could not enable repository %(repo_id)s." msgstr "" #, python-format -msgid "Could not remove repository %(repo_id)s." +msgid "Could not disable repository %(repo_id)s." msgstr "" + +msgid "YUM Repository ID already exists" +msgstr "" + +#~ msgid "Create template successfully" +#~ msgstr "Create template successfully" diff --git a/po/kimchi.pot b/po/kimchi.pot index a8b3ecb..880f8b1 100755 --- a/po/kimchi.pot +++ b/po/kimchi.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-03-07 00:54-0300\n" +"POT-Creation-Date: 2014-03-18 08:09-0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -70,6 +70,12 @@ msgstr "" msgid "Edit Guest" msgstr "" +msgid "General" +msgstr "" + +msgid "Storage" +msgstr "" + msgid "Name" msgstr "" @@ -79,12 +85,18 @@ msgstr "" msgid "Icon" msgstr "" -msgid "Cancel" +msgid "Attach" msgstr "" msgid "Save" msgstr "" +msgid "Replace" +msgstr "" + +msgid "Detach" +msgstr "" + msgid "Start" msgstr "" @@ -97,7 +109,10 @@ msgstr "" msgid "Actions" msgstr "" -msgid "Console" +msgid "Connect" +msgstr "" + +msgid "Manage Media" msgstr "" msgid "Edit" @@ -127,9 +142,6 @@ msgstr "" msgid "Templates" msgstr "" -msgid "Storage" -msgstr "" - msgid "Network" msgstr "" @@ -151,25 +163,33 @@ msgstr "" msgid "options needed." msgstr "" +msgid "" +"Can not contact the host system. Verify the host system is up and that you " +"have network connectivity to it. HTTP request response %1. " +msgstr "" + msgid "Delete Confirmation" msgstr "" msgid "OK" msgstr "" +msgid "Cancel" +msgstr "" + msgid "Confirm" msgstr "" msgid "Warning" msgstr "" -msgid "No iso found" +msgid "Loading..." msgstr "" -msgid "This is not a valid ISO file." +msgid "No iso found" msgstr "" -msgid "Create template successfully" +msgid "This is not a valid ISO file." msgstr "" msgid "It will take long time. Do you want to continue?" @@ -207,6 +227,27 @@ msgid "" "shut down/restarting?" msgstr "" +msgid "Software Updates" +msgstr "" + +msgid "Package Name" +msgstr "" + +msgid "Version" +msgstr "" + +msgid "Architecture" +msgstr "" + +msgid "Repository" +msgstr "" + +msgid "Update All" +msgstr "" + +msgid "Updating..." +msgstr "" + msgid "" "Debug report will be removed permanently and can't be recovered. Do you want " "to continue?" @@ -244,6 +285,26 @@ msgid "" "cannot be undone. Would you like to continue?" msgstr "" +msgid "" +"This CDROM will be detached permanently and you can re-attach it. Continue " +"to detach it?" +msgstr "" + +msgid "Attaching..." +msgstr "" + +msgid "Replacing..." +msgstr "" + +msgid "Successfully attached!" +msgstr "" + +msgid "Successfully replaced!" +msgstr "" + +msgid "Successfully detached!" +msgstr "" + msgid "The VLAN id must be between 1 and 4094." msgstr "" @@ -303,6 +364,11 @@ msgstr "" msgid "No available partitions found." msgstr "" +msgid "" +"This storage pool is not persistent. Instead of deactivate, this action will " +"permanently delete it. Would you like to continue?" +msgstr "" + msgid "Help" msgstr "" @@ -414,9 +480,6 @@ msgstr "" msgid "Restart" msgstr "" -msgid "Connect" -msgstr "" - msgid "Basic Information" msgstr "" @@ -435,6 +498,9 @@ msgstr "" msgid "Collecting data after leaving this page" msgstr "" +msgid "Update Progress" +msgstr "" + msgid "Network Name" msgstr "" @@ -474,9 +540,6 @@ msgstr "" msgid "No templates found." msgstr "" -msgid "Version" -msgstr "" - msgid "Location" msgstr "" @@ -679,6 +742,15 @@ msgid "Bad format while reading volume descriptor in ISO %(filename)s" msgstr "" #, python-format +msgid "" +"The hypervisor doesn't have permission to use this ISO %(filename)s. " +"Consider moving it under /var/lib/libvirt, or set the search permission to " +"file access control lists for '%(user)s' user if possible, or add the " +"'%(user)s' to the ISO path group, or (not recommended) 'chmod -R o+x " +"'path_to_iso'.Details: %(err)s" +msgstr "" + +#, python-format msgid "Virtual machine %(name)s already exists" msgstr "" @@ -1039,6 +1111,18 @@ msgstr "" msgid "Storage type %(type)s does not support volume create and delete" msgstr "" +msgid "Storage volume name must be a string" +msgstr "" + +msgid "Storage volume allocation must be an integer number" +msgstr "" + +msgid "Storage volume format not supported" +msgstr "" + +msgid "Storage volume requires a volume name" +msgstr "" + #, python-format msgid "Interface %(name)s does not exist" msgstr "" @@ -1103,14 +1187,14 @@ msgstr "" #, python-format msgid "" -"Unable to delete network %(name)s. There are still some VMs linked to this " -"network." +"Unable to delete network %(name)s. There are some virtual machines and/or " +"templates linked to this network." msgstr "" #, python-format msgid "" -"Unable to deactivate network %(name)s. There are some VMs running linked to " -"this network." +"Unable to deactivate network %(name)s. There are some virtual machines and/" +"or templates linked to this network." msgstr "" #, python-format @@ -1232,31 +1316,36 @@ msgstr "" msgid "Specify path to update virtual machine disk" msgstr "" -msgid "Repository ID must be one word only string." +msgid "YUM Repository ID must be one word only string." msgstr "" msgid "Repository URL must be an http://, ftp:// or file:// URL." msgstr "" -msgid "Repository URL arguments must be string." +msgid "" +"Repository configuration is a dictionary with specific values according to " +"repository type." msgstr "" -msgid "GPG key must be a URL pointing to the ASCII-armored file." +msgid "Distribution to DEB repository must be a string" msgstr "" -msgid "Repository name must be string." +msgid "Components to DEB repository must be listed in a array" msgstr "" -#, python-format -msgid "Repository %(repo_id)s already exists." +msgid "Components to DEB repository must be a string" msgstr "" -#, python-format -msgid "Could not enable repository %(repo_id)s." +msgid "Mirror list to DEB repository must be a string" msgstr "" -#, python-format -msgid "Could not disable repository %(repo_id)s." +msgid "YUM Repository name must be string." +msgstr "" + +msgid "GPG check must be a boolean value." +msgstr "" + +msgid "GPG key must be a URL pointing to the ASCII-armored file." msgstr "" #, python-format @@ -1264,33 +1353,43 @@ msgid "Could not update repository %(repo_id)s." msgstr "" #, python-format -msgid "Repository %(repo_id)s does not exists." +msgid "Repository %(repo_id)s does not exist." msgstr "" -#, python-format -msgid "There is no disabled repository called %(repo_id)s." +msgid "" +"Specify repository base URL or mirror list in order to create a YUM " +"repository." +msgstr "" + +msgid "Repository management tool was not recognized for your system." msgstr "" #, python-format -msgid "There is no enabled repository called %(repo_id)s." +msgid "Repository %(repo_id)s is already enabled." msgstr "" -msgid "There are no parameters to update repository." +#, python-format +msgid "Repository %(repo_id)s is already disabled." msgstr "" -msgid "OS distro not supported." +#, python-format +msgid "Could not remove repository %(repo_id)s." msgstr "" -msgid "There is no YUM configuration directory." +#, python-format +msgid "Could not write repository configuration file %(repo_file)s" msgstr "" -msgid "There are no parameters to create a new repo file." +msgid "Specify repository distribution in order to create a DEB repository." msgstr "" #, python-format -msgid "Could not write repo file %(repo_file)s" +msgid "Could not enable repository %(repo_id)s." msgstr "" #, python-format -msgid "Could not remove repository %(repo_id)s." +msgid "Could not disable repository %(repo_id)s." +msgstr "" + +msgid "YUM Repository ID already exists" msgstr "" diff --git a/po/pt_BR.po b/po/pt_BR.po index bca6335..14f7662 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -20,7 +20,7 @@ msgid "" msgstr "" "Project-Id-Version: kimchi 1.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-03-07 00:54-0300\n" +"POT-Creation-Date: 2014-03-18 08:09-0300\n" "PO-Revision-Date: 2013-06-27 10:48+0000\n" "Last-Translator: Crístian Viana <vianac@linux.vnet.ibm.com>\n" "Language-Team: Aline Manera <alinefm@br.ibm.com>\n" @@ -86,6 +86,13 @@ msgstr "Criar" msgid "Edit Guest" msgstr "Editar Máquinas Virtuais" +#, fuzzy +msgid "General" +msgstr "Gerar" + +msgid "Storage" +msgstr "Storage" + msgid "Name" msgstr "Nome" @@ -95,12 +102,18 @@ msgstr "CPUs" msgid "Icon" msgstr "Ícone" -msgid "Cancel" -msgstr "Cancelar" +msgid "Attach" +msgstr "" msgid "Save" msgstr "Salvar" +msgid "Replace" +msgstr "" + +msgid "Detach" +msgstr "" + msgid "Start" msgstr "Iniciar" @@ -113,7 +126,10 @@ msgstr "Parar" msgid "Actions" msgstr "Ações" -msgid "Console" +msgid "Connect" +msgstr "Conectar" + +msgid "Manage Media" msgstr "" msgid "Edit" @@ -145,9 +161,6 @@ msgstr "Máquinas Virtuais" msgid "Templates" msgstr "Modelos" -msgid "Storage" -msgstr "Storage" - msgid "Network" msgstr "Redes" @@ -169,27 +182,36 @@ msgstr "" msgid "options needed." msgstr "" +msgid "" +"Can not contact the host system. Verify the host system is up and that you " +"have network connectivity to it. HTTP request response %1. " +msgstr "" + msgid "Delete Confirmation" msgstr "Confirmação de remoção" msgid "OK" msgstr "OK" +msgid "Cancel" +msgstr "Cancelar" + msgid "Confirm" msgstr "Confirmar" msgid "Warning" msgstr "Aviso" +#, fuzzy +msgid "Loading..." +msgstr "Gerando..." + msgid "No iso found" msgstr "Nenhuma ISO encontrada" msgid "This is not a valid ISO file." msgstr "Esse não é um arquivo ISO válido." -msgid "Create template successfully" -msgstr "Modelo criado com sucesso" - msgid "It will take long time. Do you want to continue?" msgstr "" @@ -227,6 +249,28 @@ msgstr "" "Desligando ou reiniciando do hospedeiro causará na perda de trabalhos não " "salvos. Continuar o processo de desligar/reiniciar?" +msgid "Software Updates" +msgstr "" + +msgid "Package Name" +msgstr "" + +msgid "Version" +msgstr "Versão" + +msgid "Architecture" +msgstr "" + +msgid "Repository" +msgstr "" + +msgid "Update All" +msgstr "" + +#, fuzzy +msgid "Updating..." +msgstr "Gerando..." + msgid "" "Debug report will be removed permanently and can't be recovered. Do you want " "to continue?" @@ -266,6 +310,27 @@ msgid "" "cannot be undone. Would you like to continue?" msgstr "" +msgid "" +"This CDROM will be detached permanently and you can re-attach it. Continue " +"to detach it?" +msgstr "" + +msgid "Attaching..." +msgstr "" + +#, fuzzy +msgid "Replacing..." +msgstr "Gerando..." + +msgid "Successfully attached!" +msgstr "" + +msgid "Successfully replaced!" +msgstr "" + +msgid "Successfully detached!" +msgstr "" + msgid "The VLAN id must be between 1 and 4094." msgstr "VLAN id deve ser um número entre 1 e 4094." @@ -330,6 +395,11 @@ msgstr "" msgid "No available partitions found." msgstr "Nenhum modelo encontrado." +msgid "" +"This storage pool is not persistent. Instead of deactivate, this action will " +"permanently delete it. Would you like to continue?" +msgstr "" + msgid "Help" msgstr "" @@ -442,9 +512,6 @@ msgstr "Desligar" msgid "Restart" msgstr "Iniciar" -msgid "Connect" -msgstr "Conectar" - msgid "Basic Information" msgstr "Informações básicas" @@ -463,6 +530,9 @@ msgstr "Estatísticas do sistema" msgid "Collecting data after leaving this page" msgstr "Coletar dados após deixar essa página." +msgid "Update Progress" +msgstr "" + msgid "Network Name" msgstr "Nome da rede" @@ -502,9 +572,6 @@ msgstr "VLAN ID" msgid "No templates found." msgstr "Nenhum modelo encontrado." -msgid "Version" -msgstr "Versão" - msgid "Location" msgstr "Localização" @@ -707,6 +774,15 @@ msgid "Bad format while reading volume descriptor in ISO %(filename)s" msgstr "" #, python-format +msgid "" +"The hypervisor doesn't have permission to use this ISO %(filename)s. " +"Consider moving it under /var/lib/libvirt, or set the search permission to " +"file access control lists for '%(user)s' user if possible, or add the " +"'%(user)s' to the ISO path group, or (not recommended) 'chmod -R o+x " +"'path_to_iso'.Details: %(err)s" +msgstr "" + +#, python-format msgid "Virtual machine %(name)s already exists" msgstr "" @@ -1067,6 +1143,18 @@ msgstr "" msgid "Storage type %(type)s does not support volume create and delete" msgstr "" +msgid "Storage volume name must be a string" +msgstr "" + +msgid "Storage volume allocation must be an integer number" +msgstr "" + +msgid "Storage volume format not supported" +msgstr "" + +msgid "Storage volume requires a volume name" +msgstr "" + #, python-format msgid "Interface %(name)s does not exist" msgstr "" @@ -1131,14 +1219,14 @@ msgstr "" #, python-format msgid "" -"Unable to delete network %(name)s. There are still some VMs linked to this " -"network." +"Unable to delete network %(name)s. There are some virtual machines and/or " +"templates linked to this network." msgstr "" #, python-format msgid "" -"Unable to deactivate network %(name)s. There are some VMs running linked to " -"this network." +"Unable to deactivate network %(name)s. There are some virtual machines and/" +"or templates linked to this network." msgstr "" #, python-format @@ -1260,31 +1348,36 @@ msgstr "" msgid "Specify path to update virtual machine disk" msgstr "" -msgid "Repository ID must be one word only string." +msgid "YUM Repository ID must be one word only string." msgstr "" msgid "Repository URL must be an http://, ftp:// or file:// URL." msgstr "" -msgid "Repository URL arguments must be string." +msgid "" +"Repository configuration is a dictionary with specific values according to " +"repository type." msgstr "" -msgid "GPG key must be a URL pointing to the ASCII-armored file." +msgid "Distribution to DEB repository must be a string" msgstr "" -msgid "Repository name must be string." +msgid "Components to DEB repository must be listed in a array" msgstr "" -#, python-format -msgid "Repository %(repo_id)s already exists." +msgid "Components to DEB repository must be a string" msgstr "" -#, python-format -msgid "Could not enable repository %(repo_id)s." +msgid "Mirror list to DEB repository must be a string" msgstr "" -#, python-format -msgid "Could not disable repository %(repo_id)s." +msgid "YUM Repository name must be string." +msgstr "" + +msgid "GPG check must be a boolean value." +msgstr "" + +msgid "GPG key must be a URL pointing to the ASCII-armored file." msgstr "" #, python-format @@ -1292,33 +1385,46 @@ msgid "Could not update repository %(repo_id)s." msgstr "" #, python-format -msgid "Repository %(repo_id)s does not exists." +msgid "Repository %(repo_id)s does not exist." msgstr "" -#, python-format -msgid "There is no disabled repository called %(repo_id)s." +msgid "" +"Specify repository base URL or mirror list in order to create a YUM " +"repository." +msgstr "" + +msgid "Repository management tool was not recognized for your system." msgstr "" #, python-format -msgid "There is no enabled repository called %(repo_id)s." +msgid "Repository %(repo_id)s is already enabled." msgstr "" -msgid "There are no parameters to update repository." +#, python-format +msgid "Repository %(repo_id)s is already disabled." msgstr "" -msgid "OS distro not supported." +#, python-format +msgid "Could not remove repository %(repo_id)s." msgstr "" -msgid "There is no YUM configuration directory." +#, python-format +msgid "Could not write repository configuration file %(repo_file)s" msgstr "" -msgid "There are no parameters to create a new repo file." +msgid "Specify repository distribution in order to create a DEB repository." msgstr "" #, python-format -msgid "Could not write repo file %(repo_file)s" +msgid "Could not enable repository %(repo_id)s." msgstr "" #, python-format -msgid "Could not remove repository %(repo_id)s." +msgid "Could not disable repository %(repo_id)s." msgstr "" + +msgid "YUM Repository ID already exists" +msgstr "" + +#~ msgid "Create template successfully" +#~ msgstr "Modelo criado com sucesso" diff --git a/po/zh_CN.po b/po/zh_CN.po index 6e0d538..8298492 100644 --- a/po/zh_CN.po +++ b/po/zh_CN.po @@ -20,7 +20,7 @@ msgid "" msgstr "" "Project-Id-Version: kimchi 0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-03-07 00:54-0300\n" +"POT-Creation-Date: 2014-03-18 08:09-0300\n" "PO-Revision-Date: 2013-06-27 10:48+0000\n" "Last-Translator: ShaoHe Feng <shaohef@linux.vnet.ibm.com>\n" "Language-Team: ShaoHe Feng <shaohef@linux.vnet.ibm.com>\n" @@ -86,6 +86,13 @@ msgstr "创建" msgid "Edit Guest" msgstr "修改客户机" +#, fuzzy +msgid "General" +msgstr "生成" + +msgid "Storage" +msgstr "存储" + msgid "Name" msgstr "名称" @@ -95,12 +102,18 @@ msgstr "中央处理器" msgid "Icon" msgstr "图标" -msgid "Cancel" -msgstr "取消" +msgid "Attach" +msgstr "" msgid "Save" msgstr "保存" +msgid "Replace" +msgstr "" + +msgid "Detach" +msgstr "" + msgid "Start" msgstr "启用" @@ -113,7 +126,10 @@ msgstr "停止" msgid "Actions" msgstr "操作" -msgid "Console" +msgid "Connect" +msgstr "连接到" + +msgid "Manage Media" msgstr "" msgid "Edit" @@ -143,9 +159,6 @@ msgstr "客户机" msgid "Templates" msgstr "模板" -msgid "Storage" -msgstr "存储" - msgid "Network" msgstr "网络" @@ -167,27 +180,36 @@ msgstr "" msgid "options needed." msgstr "" +msgid "" +"Can not contact the host system. Verify the host system is up and that you " +"have network connectivity to it. HTTP request response %1. " +msgstr "" + msgid "Delete Confirmation" msgstr "删除确认" msgid "OK" msgstr "确定" +msgid "Cancel" +msgstr "取消" + msgid "Confirm" msgstr "确认" msgid "Warning" msgstr "警告" +#, fuzzy +msgid "Loading..." +msgstr "正在生成..." + msgid "No iso found" msgstr "没有发现ISO文件" msgid "This is not a valid ISO file." msgstr "这不是一个有效的ISO文件" -msgid "Create template successfully" -msgstr "创建模板成功" - msgid "It will take long time. Do you want to continue?" msgstr "" @@ -223,6 +245,28 @@ msgid "" "shut down/restarting?" msgstr "关闭或者重启主机会导致没有保存的工作丢失。继续关机/重启?" +msgid "Software Updates" +msgstr "" + +msgid "Package Name" +msgstr "" + +msgid "Version" +msgstr "版本" + +msgid "Architecture" +msgstr "" + +msgid "Repository" +msgstr "" + +msgid "Update All" +msgstr "" + +#, fuzzy +msgid "Updating..." +msgstr "正在生成..." + msgid "" "Debug report will be removed permanently and can't be recovered. Do you want " "to continue?" @@ -260,6 +304,27 @@ msgid "" "cannot be undone. Would you like to continue?" msgstr "" +msgid "" +"This CDROM will be detached permanently and you can re-attach it. Continue " +"to detach it?" +msgstr "" + +msgid "Attaching..." +msgstr "" + +#, fuzzy +msgid "Replacing..." +msgstr "正在生成..." + +msgid "Successfully attached!" +msgstr "" + +msgid "Successfully replaced!" +msgstr "" + +msgid "Successfully detached!" +msgstr "" + msgid "The VLAN id must be between 1 and 4094." msgstr "" @@ -320,6 +385,11 @@ msgstr "" msgid "No available partitions found." msgstr "没有发现模板" +msgid "" +"This storage pool is not persistent. Instead of deactivate, this action will " +"permanently delete it. Would you like to continue?" +msgstr "" + msgid "Help" msgstr "" @@ -431,9 +501,6 @@ msgstr "关机" msgid "Restart" msgstr "重启" -msgid "Connect" -msgstr "连接到" - msgid "Basic Information" msgstr "基本信息" @@ -452,6 +519,9 @@ msgstr "系统统计信息" msgid "Collecting data after leaving this page" msgstr "离开该页面继续收集数据" +msgid "Update Progress" +msgstr "" + msgid "Network Name" msgstr "网络名称" @@ -491,9 +561,6 @@ msgstr "" msgid "No templates found." msgstr "没有发现模板" -msgid "Version" -msgstr "版本" - msgid "Location" msgstr "路径" @@ -697,6 +764,15 @@ msgid "Bad format while reading volume descriptor in ISO %(filename)s" msgstr "" #, python-format +msgid "" +"The hypervisor doesn't have permission to use this ISO %(filename)s. " +"Consider moving it under /var/lib/libvirt, or set the search permission to " +"file access control lists for '%(user)s' user if possible, or add the " +"'%(user)s' to the ISO path group, or (not recommended) 'chmod -R o+x " +"'path_to_iso'.Details: %(err)s" +msgstr "" + +#, python-format msgid "Virtual machine %(name)s already exists" msgstr "" @@ -1057,6 +1133,18 @@ msgstr "" msgid "Storage type %(type)s does not support volume create and delete" msgstr "" +msgid "Storage volume name must be a string" +msgstr "" + +msgid "Storage volume allocation must be an integer number" +msgstr "" + +msgid "Storage volume format not supported" +msgstr "" + +msgid "Storage volume requires a volume name" +msgstr "" + #, python-format msgid "Interface %(name)s does not exist" msgstr "" @@ -1121,14 +1209,14 @@ msgstr "" #, python-format msgid "" -"Unable to delete network %(name)s. There are still some VMs linked to this " -"network." +"Unable to delete network %(name)s. There are some virtual machines and/or " +"templates linked to this network." msgstr "" #, python-format msgid "" -"Unable to deactivate network %(name)s. There are some VMs running linked to " -"this network." +"Unable to deactivate network %(name)s. There are some virtual machines and/" +"or templates linked to this network." msgstr "" #, python-format @@ -1250,31 +1338,36 @@ msgstr "" msgid "Specify path to update virtual machine disk" msgstr "" -msgid "Repository ID must be one word only string." +msgid "YUM Repository ID must be one word only string." msgstr "" msgid "Repository URL must be an http://, ftp:// or file:// URL." msgstr "" -msgid "Repository URL arguments must be string." +msgid "" +"Repository configuration is a dictionary with specific values according to " +"repository type." msgstr "" -msgid "GPG key must be a URL pointing to the ASCII-armored file." +msgid "Distribution to DEB repository must be a string" msgstr "" -msgid "Repository name must be string." +msgid "Components to DEB repository must be listed in a array" msgstr "" -#, python-format -msgid "Repository %(repo_id)s already exists." +msgid "Components to DEB repository must be a string" msgstr "" -#, python-format -msgid "Could not enable repository %(repo_id)s." +msgid "Mirror list to DEB repository must be a string" msgstr "" -#, python-format -msgid "Could not disable repository %(repo_id)s." +msgid "YUM Repository name must be string." +msgstr "" + +msgid "GPG check must be a boolean value." +msgstr "" + +msgid "GPG key must be a URL pointing to the ASCII-armored file." msgstr "" #, python-format @@ -1282,33 +1375,46 @@ msgid "Could not update repository %(repo_id)s." msgstr "" #, python-format -msgid "Repository %(repo_id)s does not exists." +msgid "Repository %(repo_id)s does not exist." msgstr "" -#, python-format -msgid "There is no disabled repository called %(repo_id)s." +msgid "" +"Specify repository base URL or mirror list in order to create a YUM " +"repository." +msgstr "" + +msgid "Repository management tool was not recognized for your system." msgstr "" #, python-format -msgid "There is no enabled repository called %(repo_id)s." +msgid "Repository %(repo_id)s is already enabled." msgstr "" -msgid "There are no parameters to update repository." +#, python-format +msgid "Repository %(repo_id)s is already disabled." msgstr "" -msgid "OS distro not supported." +#, python-format +msgid "Could not remove repository %(repo_id)s." msgstr "" -msgid "There is no YUM configuration directory." +#, python-format +msgid "Could not write repository configuration file %(repo_file)s" msgstr "" -msgid "There are no parameters to create a new repo file." +msgid "Specify repository distribution in order to create a DEB repository." msgstr "" #, python-format -msgid "Could not write repo file %(repo_file)s" +msgid "Could not enable repository %(repo_id)s." msgstr "" #, python-format -msgid "Could not remove repository %(repo_id)s." +msgid "Could not disable repository %(repo_id)s." msgstr "" + +msgid "YUM Repository ID already exists" +msgstr "" + +#~ msgid "Create template successfully" +#~ msgstr "创建模板成功" diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py index 589c8cb..c30160d 100644 --- a/src/kimchi/i18n.py +++ b/src/kimchi/i18n.py @@ -225,22 +225,26 @@ messages = { "KCHCDROM0012E": _("Specify type and path to add a new virtual machine disk"), "KCHCDROM0013E": _("Specify path to update virtual machine disk"), - "KCHREPOS0001E": _("Repository ID must be one word only string."), + "KCHREPOS0001E": _("YUM Repository ID must be one word only string."), "KCHREPOS0002E": _("Repository URL must be an http://, ftp:// or file:// URL."), - "KCHREPOS0003E": _("Repository URL arguments must be string."), - "KCHREPOS0004E": _("GPG key must be a URL pointing to the ASCII-armored file."), - "KCHREPOS0005E": _("Repository name must be string."), - "KCHREPOS0006E": _("Repository %(repo_id)s already exists."), - "KCHREPOS0007E": _("Could not enable repository %(repo_id)s."), - "KCHREPOS0008E": _("Could not disable repository %(repo_id)s."), - "KCHREPOS0009E": _("Could not update repository %(repo_id)s."), - "KCHREPOS0010E": _("Repository %(repo_id)s does not exists."), - "KCHREPOS0011E": _("There is no disabled repository called %(repo_id)s."), - "KCHREPOS0012E": _("There is no enabled repository called %(repo_id)s."), - "KCHREPOS0013E": _("There are no parameters to update repository."), + "KCHREPOS0003E": _("Repository configuration is a dictionary with specific values according to repository type."), + "KCHREPOS0004E": _("Distribution to DEB repository must be a string"), + "KCHREPOS0005E": _("Components to DEB repository must be listed in a array"), + "KCHREPOS0006E": _("Components to DEB repository must be a string"), + "KCHREPOS0007E": _("Mirror list to DEB repository must be a string"), + "KCHREPOS0008E": _("YUM Repository name must be string."), + "KCHREPOS0009E": _("GPG check must be a boolean value."), + "KCHREPOS0010E": _("GPG key must be a URL pointing to the ASCII-armored file."), + "KCHREPOS0011E": _("Could not update repository %(repo_id)s."), + "KCHREPOS0012E": _("Repository %(repo_id)s does not exist."), + "KCHREPOS0013E": _("Specify repository base URL or mirror list in order to create a YUM repository."), "KCHREPOS0014E": _("Repository management tool was not recognized for your system."), - "KCHREPOS0015E": _("There is no YUM configuration directory."), - "KCHREPOS0016E": _("There are no parameters to create a new repo file."), - "KCHREPOS0017E": _("Could not write repo file %(repo_file)s"), - "KCHREPOS0018E": _("Could not remove repository %(repo_id)s."), + "KCHREPOS0015E": _("Repository %(repo_id)s is already enabled."), + "KCHREPOS0016E": _("Repository %(repo_id)s is already disabled."), + "KCHREPOS0017E": _("Could not remove repository %(repo_id)s."), + "KCHREPOS0018E": _("Could not write repository configuration file %(repo_file)s"), + "KCHREPOS0019E": _("Specify repository distribution in order to create a DEB repository."), + "KCHREPOS0020E": _("Could not enable repository %(repo_id)s."), + "KCHREPOS0021E": _("Could not disable repository %(repo_id)s."), + "KCHREPOS0022E": _("YUM Repository ID already exists"), } -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> The backend has a more detailed information when a problem occurs, so raise this exception instead of get it and raise a generic error message "Unable to update repository", "Unable to create repository" and so Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model/host.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/kimchi/model/host.py b/src/kimchi/model/host.py index b195a97..e5b6132 100644 --- a/src/kimchi/model/host.py +++ b/src/kimchi/model/host.py @@ -359,25 +359,19 @@ class RepositoryModel(object): if self._repositories is None: raise InvalidOperation('KCHREPOS0014E') - if not self._repositories.enableRepository(repo_id): - raise OperationFailed("KCHREPOS0007E", {'repo_id': repo_id}) + return self._repositories.enableRepository(repo_id) def disable(self, repo_id): if self._repositories is None: raise InvalidOperation('KCHREPOS0014E') - if not self._repositories.disableRepository(repo_id): - raise OperationFailed("KCHREPOS0008E", {'repo_id': repo_id}) + return self._repositories.disableRepository(repo_id) def update(self, repo_id, params): if self._repositories is None: raise InvalidOperation('KCHREPOS0014E') - try: - self._repositories.updateRepository(repo_id, params) - except: - raise OperationFailed("KCHREPOS0009E", {'repo_id': repo_id}) - return repo_id + return self._repositories.updateRepository(repo_id, params) def delete(self, repo_id): if self._repositories is None: -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> Return a sorted list while listing the repository to make sure about their position on UI Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model/host.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kimchi/model/host.py b/src/kimchi/model/host.py index e5b6132..e2a12de 100644 --- a/src/kimchi/model/host.py +++ b/src/kimchi/model/host.py @@ -319,7 +319,7 @@ class RepositoriesModel(object): if self.host_repositories is None: raise InvalidOperation('KCHREPOS0014E') - return self.host_repositories.getRepositories().keys() + return sorted(self.host_repositories.getRepositories()) def create(self, params): if self.host_repositories is None: -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> The repository ID differs according to the repository type: deb or yum. For yum repositories, the repository ID is kept in the repository configuration file - so we can choose an arbitrary one as it will be saved in a file. But for a deb repository the ID does not exist. For internal proposals, we build the ID for it based on url + dist + comps. Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model/host.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/kimchi/model/host.py b/src/kimchi/model/host.py index e2a12de..7b151f2 100644 --- a/src/kimchi/model/host.py +++ b/src/kimchi/model/host.py @@ -325,21 +325,7 @@ class RepositoriesModel(object): if self.host_repositories is None: raise InvalidOperation('KCHREPOS0014E') - repo_id = params.get('repo_id', None) - - # Create a repo_id if not given by user. The repo_id will follow - # the format kimchi_repo_<integer>, where integer is the number of - # seconds since the Epoch (January 1st, 1970), in UTC. - if repo_id is None: - repo_id = "kimchi_repo_%s" % int(time.time()) - while repo_id in self.get_list(): - repo_id = "kimchi_repo_%s" % int(time.time()) - params.update({'repo_id': repo_id}) - - if repo_id in self.get_list(): - raise InvalidOperation("KCHREPOS0006E", {'repo_id': repo_id}) - self.host_repositories.addRepository(params) - return repo_id + return self.host_repositories.addRepository(params) class RepositoryModel(object): -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> The former repository management implementation was storing the repositories data internally. So it got the system data once and use it on it. Which is wrong, as user can manage the system with other tools and kimchi will show inconsistent data. In addition to it, this patch also make many improvements: - Keep repository configuration file consistent on each actions - Use ConfigParser to add a new YUM repository - Let user specify a mirrorlist and a baseurl The mirrorlist can be used instead of or with the baseurl option. For reference: http://linux.die.net/man/5/yum.conf - Remove only the YUM repository section on config file instead of remove the while file - List all repositories (instead of only the enabled ones) Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/repositories.py | 693 +++++++++++++++++--------------------------- 1 file changed, 271 insertions(+), 422 deletions(-) diff --git a/src/kimchi/repositories.py b/src/kimchi/repositories.py index e685991..70f4f5b 100644 --- a/src/kimchi/repositories.py +++ b/src/kimchi/repositories.py @@ -18,9 +18,13 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import os +import time +import urlparse + +from ConfigParser import ConfigParser from kimchi.basemodel import Singleton -from kimchi.exception import InvalidOperation, InvalidParameter +from kimchi.exception import InvalidOperation from kimchi.exception import OperationFailed, NotFoundError, MissingParameter @@ -31,14 +35,6 @@ class Repositories(object): Class to represent and operate with repositories information. """ def __init__(self): - # This stores all repositories for Kimchi perspective. It's a - # dictionary of dictionaries, in the format {<repo_id>: {repo}}, - # where: - # repo = {'repo_id': <string>, 'repo_name': <string>, - # 'baseurl': ([<string>], None), 'url_args': ([<string>, None), - # 'enabled': True/False, 'gpgcheck': True/False, - # 'gpgkey': ([<string>], None), - # 'is_mirror': True/False} try: __import__('yum') self._pkg_mnger = YumRepo() @@ -47,45 +43,13 @@ class Repositories(object): __import__('apt_pkg') self._pkg_mnger = AptRepo() except ImportError: - raise InvalidOperation('KCHREPOS0019E') - - self._repo_storage = {} - # update the self._repo_storage with system's repositories - self._scanSystemRepositories() - - def _scanSystemRepositories(self): - """ - Update repositories._repo_storage with system's (host) repositories. - """ - # Call system pkg_mnger to get the repositories as list of dict. - for repo in self._pkg_mnger.getRepositoriesList(): - self.addRepository(repo) + raise InvalidOperation('KCHREPOS0014E') - def addRepository(self, params={}): + def addRepository(self, params): """ - Add and enable a new repository into repositories._repo_storage. + Add and enable a new repository """ - # Create and enable the repository - repo_id = params.get('repo_id') - repo = {'repo_id': repo_id, - 'repo_name': params.get('repo_name', repo_id), - 'baseurl': params.get('baseurl'), - 'url_args': params.get('url_args', None), - 'enabled': True, - 'gpgkey': params.get('gpgkey', None), - 'is_mirror': params.get('is_mirror', False)} - - if repo['gpgkey'] is not None: - repo['gpgcheck'] = True - else: - repo['gpgcheck'] = False - - self._repo_storage[repo_id] = repo - - # Check in self._pkg_mnger if the repository already exists there - if not repo_id in [irepo['repo_id'] for irepo in - self._pkg_mnger.getRepositoriesList()]: - self._pkg_mnger.addRepo(repo) + return self._pkg_mnger.addRepo(params) def getRepositories(self): """ @@ -93,98 +57,41 @@ class Repositories(object): the format {<repo_id>: {repo}}, where repo is a dictionary in the repositories.Repositories() format. """ - return self._repo_storage + return self._pkg_mnger.getRepositoriesList() def getRepository(self, repo_id): """ Return a dictionary with all info from a given repository ID. """ - if not repo_id in self._repo_storage.keys(): - raise NotFoundError("KCHREPOS0010E", {'repo_id': repo_id}) - - repo = self._repo_storage[repo_id] - if (isinstance(repo['baseurl'], list)) and (len(repo['baseurl']) > 0): - repo['baseurl'] = repo['baseurl'][0] - - return repo - - def getRepositoryFromPkgMnger(self, repo_id): - """ - Return a dictionary with all info from a given repository ID. - All info come from self._pkg_mnger.getRepo(). - """ - return self._pkg_mnger.getRepo(repo_id) - - def enabledRepositories(self): - """ - Return a list with enabled repositories IDs. - """ - enabled_repos = [] - for repo_id in self._repo_storage.keys(): - if self._repo_storage[repo_id]['enabled']: - enabled_repos.append(repo_id) - return enabled_repos + info = self._pkg_mnger.getRepo(repo_id) + info['repo_id'] = repo_id + return info def enableRepository(self, repo_id): """ Enable a repository. """ - # Check if repo_id is already enabled - if repo_id in self.enabledRepositories(): - raise NotFoundError("KCHREPOS0011E", {'repo_id': repo_id}) - - try: - repo = self.getRepository(repo_id) - repo['enabled'] = True - self.updateRepository(repo_id, repo) - self._pkg_mnger.enableRepo(repo_id) - return True - except: - raise OperationFailed("KCHREPOS0007E", {'repo_id': repo_id}) + return self._pkg_mnger.toggleRepo(repo_id, True) def disableRepository(self, repo_id): """ Disable a given repository. """ - # Check if repo_id is already disabled - if not repo_id in self.enabledRepositories(): - raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id}) + return self._pkg_mnger.toggleRepo(repo_id, False) - try: - repo = self.getRepository(repo_id) - repo['enabled'] = False - self.updateRepository(repo_id, repo) - self._pkg_mnger.disableRepo(repo_id) - return True - except: - raise OperationFailed("KCHREPOS0008E", {'repo_id': repo_id}) - - def updateRepository(self, repo_id, new_repo={}): + def updateRepository(self, repo_id, params): """ Update the information of a given repository. The input is the repo_id of the repository to be updated and a dict with the information to be updated. """ - if (len(new_repo) == 0): - raise InvalidParameter("KCHREPOS0013E") - - repo = self._repo_storage[repo_id] - repo.update(new_repo) - - del self._repo_storage[repo_id] - self._repo_storage[repo_id] = repo - self._pkg_mnger.updateRepo(repo_id, self._repo_storage[repo_id]) + return self._pkg_mnger.updateRepo(repo_id, params) def removeRepository(self, repo_id): """ Remove a given repository """ - if not repo_id in self._repo_storage.keys(): - raise NotFoundError("KCHREPOS0010E", {'repo_id': repo_id}) - - del self._repo_storage[repo_id] - self._pkg_mnger.removeRepo(repo_id) - return True + return self._pkg_mnger.removeRepo(repo_id) class YumRepo(object): @@ -194,219 +101,170 @@ class YumRepo(object): modules in runtime. """ TYPE = 'yum' + DEFAULT_CONF_DIR = "/etc/yum.repos.d" def __init__(self): - self._yb = getattr(__import__('yum'), 'YumBase')() - self._repos = self._yb.repos - self._conf = self._yb.conf - self._enabled_repos = self._repos.listEnabled() + self._yb = getattr(__import__('yum'), 'YumBase') + self._conf = getattr(__import__('yum'), 'config') + + self._confdir = self.DEFAULT_CONF_DIR + reposdir = self._yb().conf.reposdir + for d in reposdir: + if os.path.isdir(d): + self._confdir = d + break def getRepositoriesList(self): """ - Return a list of dictionaries in the repositories.Repositories() format - """ - repo_list = [] - for repo in self.enabledRepos(): - irepo = {} - irepo['repo_id'] = repo.id - irepo['repo_name'] = repo.name - irepo['url_args'] = None, - irepo['enabled'] = repo.enabled - irepo['gpgcheck'] = repo.gpgcheck - irepo['gpgkey'] = repo.gpgkey - if len(repo.baseurl) > 0: - irepo['baseurl'] = repo.baseurl - irepo['is_mirror'] = False - else: - irepo['baseurl'] = [repo.mirrorlist] - irepo['is_mirror'] = True - repo_list.append(irepo) - return repo_list - - def addRepo(self, repo={}): - """ - Add a given repository in repositories.Repositories() format to YumBase + Return a list of repositories IDs """ - if len(repo) == 0: - raise InvalidParameter("KCHREPOS0013E") - - # At least one base url, or one mirror, must be given. - # baseurls must be a list of strings specifying the urls - # mirrorlist must be a list of strings specifying a list of mirrors - # Here we creates the lists, or set as None - if repo['is_mirror']: - mirrors = repo['baseurl'] - baseurl = None - else: - baseurl = [repo['baseurl']] - mirrors = None - - self._yb.add_enable_repo(repo['repo_id'], baseurl, mirrors, - name=repo['repo_name'], - gpgcheck=repo['gpgcheck'], - gpgkey=[repo['gpgkey']]) - - # write a repo file in the system with repo{} information. - self._write2disk(repo) + return self._yb().repos.repos.keys() def getRepo(self, repo_id): """ Return a dictionary in the repositories.Repositories() of the given repository ID format with the information of a YumRepository object. """ - try: - repo = self._repos.getRepo(repo_id) - irepo = {} - irepo['repo_id'] = repo.id - irepo['repo_name'] = repo.name - irepo['url_args'] = None, - irepo['enabled'] = repo.enabled - irepo['gpgcheck'] = repo.gpgcheck - irepo['gpgkey'] = repo.gpgkey - if len(repo.baseurl) > 0: - irepo['baseurl'] = repo.baseurl - irepo['is_mirror'] = False - else: - irepo['baseurl'] = [repo.mirrorlist] - irepo['is_mirror'] = True - return irepo - except: - raise OperationFailed("KCHREPOS0010E", {'repo_id': repo_id}) + repos = self._yb().repos + if repo_id not in repos.repos.keys(): + raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id}) - def enabledRepos(self): - """ - Return a list with enabled YUM repositories IDs - """ - return self._enabled_repos + entry = repos.getRepo(repo_id) - def isRepoEnable(self, repo_id): - """ - Return if a given repository ID is enabled or not - """ - for repo in self.enabledRepos(): - if repo_id == repo.id: - return True - return False + info = {} + info['enabled'] = entry.enabled - def enableRepo(self, repo_id): - """ - Enable a given repository - """ - try: - self._repos.getRepo(repo_id).enable() - self._repos.doSetup() - return True - except: - raise OperationFailed("KCHREPOS0007E", {'repo_id': repo_id}) + baseurl = '' + if entry.baseurl: + baseurl = entry.baseurl[0] - def disableRepo(self, repo_id): + info['baseurl'] = baseurl + info['config'] = {} + info['config']['repo_name'] = entry.name + info['config']['gpgcheck'] = entry.gpgcheck + info['config']['gpgkey'] = entry.gpgkey + info['config']['mirrorlist'] = entry.mirrorlist or '' + return info + + def addRepo(self, params): """ - Disable a given repository + Add a given repository to YumBase """ + # At least one base url, or one mirror, must be given. + baseurl = params.get('baseurl', '') + + config = params.get('config', {}) + mirrorlist = config.get('mirrorlist', '') + if not baseurl and not mirrorlist: + raise MissingParameter("KCHREPOS0013E") + + repo_id = params.get('repo_id', None) + if repo_id is None: + repo_id = "kimchi_repo_%s" % str(int(time.time() * 1000)) + + repos = self._yb().repos + if repo_id in repos.repos.keys(): + raise InvalidOperation("KCHREPOS0022E", {'repo_id': repo_id}) + + repo_name = params.get('repo_name', None) + if repo_name is None: + repo_name = repo_id + + repo = {'baseurl': baseurl, 'mirrorlist': mirrorlist, + 'name': repo_name, 'gpgcheck': 1, + 'gpgkey': [], 'enabled': 1} + + # write a repo file in the system with repo{} information. + parser = ConfigParser() + parser.add_section(repo_id) + + for key, value in repo.iteritems(): + if value: + parser.set(repo_id, key, value) + + repofile = os.path.join(self._confdir, repo_id + '.repo') try: - self._repos.getRepo(repo_id).disable() - self._repos.doSetup() - return True + with open(repofile, 'w') as fd: + parser.write(fd) except: - raise OperationFailed("KCHREPOS0008E", {'repo_id': repo_id}) + raise OperationFailed("KCHREPOS0018E", + {'repo_file': repofile}) - def updateRepo(self, repo_id, repo={}): - """ - Update a given repository in repositories.Repositories() format - """ - if len(repo) == 0: - raise MissingParameter("KCHREPOS0013E") + return repo_id - self._repos.delete(repo_id) - self.addRepo(repo) + def toggleRepo(self, repo_id, enable): + repos = self._yb().repos + if repo_id not in repos.repos.keys(): + raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id}) + + entry = repos.getRepo(repo_id) + if enable and entry.enabled: + raise InvalidOperation("KCHREPOS0015E", {'repo_id': repo_id}) + + if not enable and not entry.enabled: + raise InvalidOperation("KCHREPOS0016E", {'repo_id': repo_id}) - def removeRepo(self, repo_id): - """ - Remove a given repository - """ try: - self._repos.delete(repo_id) - self._removefromdisk(repo_id) + if enable: + entry.enable() + else: + entry.disable() + + self._conf.writeRawRepoFile(entry) + return repo_id except: - raise OperationFailed("KCHREPOS0018E", {'repo_id': repo_id}) + if enable: + raise OperationFailed("KCHREPOS0020E", {'repo_id': repo_id}) - def _write2disk(self, repo={}): + raise OperationFailed("KCHREPOS0021E", {'repo_id': repo_id}) + + def updateRepo(self, repo_id, params): """ - Write repository info into disk. + Update a given repository in repositories.Repositories() format """ - # Get a list with all reposdir configured in system's YUM. - conf_dir = self._conf.reposdir - if not conf_dir: - raise NotFoundError("KCHREPOS0015E") - - if len(repo) == 0: - raise InvalidParameter("KCHREPOS0016E") - - # Generate the content to be wrote. - repo_content = '[%s]\n' % repo['repo_id'] - repo_content = repo_content + 'name=%s\n' % repo['repo_name'] + repos = self._yb().repos + if repo_id not in repos.repos.keys(): + raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id}) - if isinstance(repo['baseurl'], list): - link = repo['baseurl'][0] - else: - link = repo['baseurl'] + config = params.get('config', {}) + entry = repos.getRepo(repo_id) - if repo['is_mirror']: - repo_content = repo_content + 'mirrorlist=%s\n' % link - else: - repo_content = repo_content + 'baseurl=%s\n' % link + baseurl = params.get('baseurl', None) + mirrorlist = config.get('mirrorlist', None) - if repo['enabled']: - repo_content = repo_content + 'enabled=1\n' - else: - repo_content = repo_content + 'enabled=0\n' + if baseurl is not None: + entry.baseurl = baseurl - if repo['gpgcheck']: - repo_content = repo_content + 'gpgcheck=1\n' - else: - repo_content = repo_content + 'gpgcheck=0\n' + if mirrorlist is not None: + entry.mirrorlist = mirrorlist - if repo['gpgkey']: - if isinstance(repo['gpgkey'], list): - link = repo['gpgkey'][0] - else: - link = repo['gpgkey'] - repo_content = repo_content + 'gpgckey=%s\n' % link - - # Scan for the confdirs and write the file in the first available - # directory in the system. YUM will scan each confdir for repo files - # and load it contents, so we can write in the first available dir. - for dir in conf_dir: - if os.path.isdir(dir): - repo_file = dir + '/%s.repo' % repo['repo_id'] - if os.path.isfile(repo_file): - os.remove(repo_file) - - try: - with open(repo_file, 'w') as fd: - fd.write(repo_content) - fd.close() - except: - raise OperationFailed("KCHREPOS0017E", - {'repo_file': repo_file}) - break - return True + entry.id = params.get('repo_id', repo_id) + entry.name = config.get('repo_name', entry.name) + entry.gpgcheck = config.get('gpgcheck', entry.gpgcheck) + entry.gpgkey = config.get('gpgkey', entry.gpgkey) + self._conf.writeRawRepoFile(entry) + return repo_id - def _removefromdisk(self, repo_id): + def removeRepo(self, repo_id): """ - Delete the repo file from disk of a given repository + Remove a given repository """ - conf_dir = self._conf.reposdir - if not conf_dir: - raise NotFoundError("KCHREPOS0015E") + repos = self._yb().repos + if repo_id not in repos.repos.keys(): + raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id}) + + entry = repos.getRepo(repo_id) + parser = ConfigParser() + with open(entry.repofile) as fd: + parser.readfp(fd) - for dir in conf_dir: - if os.path.isdir(dir): - repo_file = dir + '/%s.repo' % repo_id - if os.path.isfile(repo_file): - os.remove(repo_file) + if len(parser.sections()) == 1: + os.remove(entry.repofile) + return - return True + parser.remove_section(repo_id) + with open(entry.repofile, "w") as fd: + parser.write(fd) class AptRepo(object): @@ -416,179 +274,170 @@ class AptRepo(object): modules in runtime. """ TYPE = 'deb' + KIMCHI_LIST = "kimchi-source.list" def __init__(self): getattr(__import__('apt_pkg'), 'init_config')() getattr(__import__('apt_pkg'), 'init_system')() - self._config = getattr(__import__('apt_pkg'), 'config') - self._etc_slist = '/%s%s' % (self._config.get('Dir::Etc'), - self._config.get('Dir::Etc::sourcelist')) - self._etc_sparts = '/%s%s' % (self._config.get('Dir::Etc'), - self._config.get('Dir::Etc::sourceparts')) - + config = getattr(__import__('apt_pkg'), 'config') module = __import__('aptsources.sourceslist', globals(), locals(), ['SourcesList'], -1) - self._repos = getattr(module, 'SourcesList')() + + self._sourceparts_path = '/%s%s' % ( + config.get('Dir::Etc'), config.get('Dir::Etc::sourceparts')) + self._sourceslist = getattr(module, 'SourcesList') + self.filename = os.path.join(self._sourceparts_path, self.KIMCHI_LIST) + if not os.path.exists(self.filename): + with open(self.filename, 'w') as fd: + fd.write("# This file is managed by Kimchi and it must not " + "be modified manually\n") + + def _get_repo_id(self, repo): + data = urlparse.urlparse(repo.uri) + name = data.hostname or data.path + return '%s-%s-%s' % (name, repo.dist, "-".join(repo.comps)) + + def _get_source_entry(self, repo_id): + repos = self._sourceslist() + repos.refresh() + + for r in repos: + # Ignore deb-src repositories + if r.type != 'deb': + continue + + if self._get_repo_id(r) != repo_id: + continue + + return r + + return None def getRepositoriesList(self): """ - Return a list of dictionaries in the repositories.Repositories() format + Return a list of repositories IDs + + APT repositories there aren't the concept about repository ID, so for + internal control, the repository ID will be built as described in + _get_repo_id() """ - repo_list = [] - for repo in self.enabledRepos(): - irepo = {} - if repo.file == self._etc_slist: - id = "%s%s" % (repo.uri.split("//")[1], repo.dist) - else: - id = repo.file.split('/')[-1].split('.')[0] - irepo['repo_id'] = id - irepo['baseurl'] = repo.uri - list = [repo.dist] - for comp in repo.comps: - list.append(comp) - irepo['url_args'] = list - irepo['enabled'] = True - irepo['is_mirror'] = False - irepo['gpgcheck'] = False - irepo['gpgkey'] = None - repo_list.append(irepo) - return repo_list - - def addRepo(self, repo={}): - """ - Add a given repository in repositories.Repositories() format to APT - """ - if len(repo) == 0: - raise InvalidParameter("KCHREPOS0013E") - - if repo['url_args'] is not None: - dist = repo['url_args'][0] - args = repo['url_args'][1:] - else: - dist = None - args = [] + repos = self._sourceslist() + repos.refresh() - _file = '%s/%s.list' % (self._etc_sparts, repo['repo_id']) + res = [] + for r in repos: + # Ignore deb-src repositories + if r.type != 'deb': + continue + + res.append(self._get_repo_id(r)) - self._repos.add('deb', repo['baseurl'], dist, args, file=_file) - self._repos.save() + return res def getRepo(self, repo_id): """ Return a dictionary in the repositories.Repositories() format of the given repository ID with the information of a SourceEntry object. """ - for repo in self.enabledRepos(): - if repo.file == self._etc_slist: - id = "%s%s" % (repo.uri.split("//")[1], repo.dist) - else: - id = repo.file.split('/')[-1].split('.')[0] + r = self._get_source_entry(repo_id) + if r is None: + raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id}) - if id != repo_id: - continue + info = {'enabled': not r.disabled, + 'baseurl': r.uri, + 'config': {'dist': r.dist, + 'comps': r.comps}} + return info - irepo = {} - irepo['repo_id'] = id - irepo['baseurl'] = repo.uri - list = [repo.dist] - for comp in repo.comps: - list.append(comp) - irepo['url_args'] = list - irepo['enabled'] = True - irepo['is_mirror'] = False - irepo['gpgcheck'] = False - irepo['gpgkey'] = None - return irepo - raise OperationFailed("KCHREPOS0010E", {'repo_id': repo_id}) - - def enabledRepos(self): - """ - Return a list with enabled APT repositories - """ - enabled_repos = [] - self._repos.refresh() - for repo in self._repos: - if (len(repo.str()) > 3) and (not repo.disabled): - if repo.type == 'deb': - enabled_repos.append(repo) - return enabled_repos - - def enableRepo(self, repo_id): + def addRepo(self, params): """ - Enable a given repository + Add a new APT repository based on <params> """ - try: - lrepo = self._genSourceLine(repo_id) - self._repos.refresh() - for repo in self._repos: - if repo.disabled and (lrepo == repo.line): - repo.set_enabled('True') - self._repos.save() - return True - except: - raise OperationFailed("KCHREPOS0007E", {'repo_id': repo_id}) + # To create a APT repository the dist is a required parameter + # (in addition to baseurl, verified on controller through API.json) + config = params.get('config', None) + if config is None: + raise MissingParameter("KCHREPOS0019E") + + if 'dist' not in config.keys(): + raise MissingParameter("KCHREPOS0019E") - def disableRepo(self, repo_id): + uri = params['baseurl'] + dist = config['dist'] + comps = config.get('comps', []) + + repos = self._sourceslist() + repos.refresh() + source_entry = repos.add('deb', uri, dist, comps, file=self.filename) + repos.save() + + return self._get_repo_id(source_entry) + + def toggleRepo(self, repo_id, enable): """ - Disable a given repository + Enable a given repository """ + r = self._get_source_entry(repo_id) + if r is None: + raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id}) + + if enable and not r.disabled: + raise InvalidOperation("KCHREPOS0015E", {'repo_id': repo_id}) + + if not enable and r.disabled: + raise InvalidOperation("KCHREPOS0016E", {'repo_id': repo_id}) + + if enable: + line = 'deb' + else: + line = '#deb' + try: - lrepo = self._genSourceLine(repo_id) - self._repos.refresh() - for repo in self._repos: - if (not repo.disabled) and (lrepo == repo.line): - repo.set_enabled('False') - self._repos.save() - return True + repos = self._sourceslist() + repos.refresh() + repos.remove(r) + repos.add(line, r.uri, r.dist, r.comps, file=self.filename) + repos.save() + return repo_id except: - raise OperationFailed("KCHREPOS0008E", {'repo_id': repo_id}) + if enable: + raise OperationFailed("KCHREPOS0020E", {'repo_id': repo_id}) - def updateRepo(self, repo_id, repo={}): + raise OperationFailed("KCHREPOS0021E", {'repo_id': repo_id}) + + def updateRepo(self, repo_id, params): """ Update a given repository in repositories.Repositories() format """ - if len(repo) == 0: - raise MissingParameter("KCHREPOS0013E") + r = self._get_source_entry(repo_id) + if r is None: + raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id}) + + info = {'enabled': not r.disabled, + 'baseurl': params.get('baseurl', r.uri), + 'config': {'type': 'deb', 'dist': r.dist, + 'comps': r.comps}} + + if 'config' in params.keys(): + config = params['config'] + info['config']['dist'] = config.get('dist', r.dist) + info['config']['comps'] = config.get('comps', r.comps) self.removeRepo(repo_id) - self.addRepo(repo) + return self.addRepo(info) def removeRepo(self, repo_id): """ Remove a given repository """ + r = self._get_source_entry(repo_id) + if r is None: + raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id}) + try: - lrepo = self._genSourceLine(repo_id) - self._repos.refresh() - for repo in self._repos: - if lrepo == repo.line: - self._repos.remove(repo) - self._repos.save() - self._removefromdisk(repo_id) + repos = self._sourceslist() + repos.refresh() + repos.remove(r) + repos.save() except: - raise OperationFailed("KCHREPOS0018E", {'repo_id': repo_id}) - - def _genSourceLine(self, repo_id): - """ - Generate a source.list line from repo_id information. - """ - line = '' - repo = self.getRepo(repo_id) - if repo['enabled']: - line = 'deb ' - else: - line = '#deb ' - line = line + repo['baseurl'] + ' ' - line = line + ' '.join(repo['url_args']) - line = line + '\n' - return line - - def _removefromdisk(self, repo_id): - """ - Delete the repo file from disk of a given repository - """ - if os.path.isdir(self._etc_sparts): - _file = '%s/%s.list' % (self._etc_sparts, repo_id) - if os.path.isfile(_file): - os.remove(_file) - return True + raise OperationFailed("KCHREPOS0017E", {'repo_id': repo_id}) -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> Based on the lastest repositories changes we need to update the test cases. And make sure to create the correct repository type according to the system. Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- tests/test_model.py | 151 +++++++++++++++++++++++++++++++-------------------- tests/test_rest.py | 10 ++-- 2 files changed, 98 insertions(+), 63 deletions(-) diff --git a/tests/test_model.py b/tests/test_model.py index 60d0b94..c8c54c0 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -989,101 +989,136 @@ class ModelTests(unittest.TestCase): inst = model.Model('test:///default', objstore_loc=self.tmp_store) - system_host_repos = len(inst.repositories_get_list()) - - test_repos = [{'repo_id': 'fedora-fake', - 'baseurl': 'http://www.fedora.org'}, - {'repo_id': 'fedora-updates-fake', - 'baseurl': 'http://www.fedora.org/updates', - 'is_mirror': True, - 'gpgkey': 'file:///tmp/KEY-fedora-updates-fake-19'}] + yum_repos = [{'repo_id': 'fedora-fake', + 'baseurl': 'http://www.fedora.org'}, + {'repo_id': 'fedora-updates-fake', + 'config': + {'mirrorlist': 'http://www.fedora.org/updates', + 'gpgkey': 'file:///tmp/KEY-fedora-updates-fake-19'}}] + + deb_repos = [{'baseurl': 'http://br.archive.ubuntu.com/kimchi/fake', + 'config': {'dist': 'quantal'}}, + {'baseurl': 'http://br.archive.kimchi.com/ubuntu/fake', + 'config': {'dist': 'quantal', 'comps': ['main']}}] + + repo_type = inst.capabilities_lookup()['repo_mngt_tool'] + if repo_type == 'yum': + test_repos = yum_repos + elif repo_type == 'deb': + test_repos = deb_repos + else: + # repository management tool was not recognized by Kimchi + # skip test case + return for repo in test_repos: - inst.repositories_create(repo) - host_repos = inst.repositories_get_list() - self.assertEquals(system_host_repos + len(test_repos), len(host_repos)) + system_host_repos = len(inst.repositories_get_list()) + repo_id = inst.repositories_create(repo) + host_repos = inst.repositories_get_list() + self.assertEquals(system_host_repos + 1, len(host_repos)) - for repo in test_repos: - repo_info = inst.repository_lookup(repo.get('repo_id')) - self.assertEquals(repo.get('repo_id'), repo_info.get('repo_id')) - self.assertEquals(repo.get('baseurl', []), - repo_info.get('baseurl')) - self.assertEquals(repo.get('is_mirror', False), - repo_info.get('is_mirror')) + repo_info = inst.repository_lookup(repo_id) + self.assertEquals(repo_id, repo_info['repo_id']) self.assertEquals(True, repo_info.get('enabled')) + self.assertEquals(repo.get('baseurl', ''), + repo_info.get('baseurl')) + + original_config = repo.get('config', {}) + config_info = repo_info.get('config', {}) - if 'gpgkey' in repo.keys(): - gpgcheck = True + if repo_type == 'yum': + self.assertEquals(original_config.get('mirrorlist', ''), + config_info.get('mirrorlist', '')) + self.assertEquals(True, config_info['gpgcheck']) else: - gpgcheck = False + self.assertEquals(original_config['dist'], config_info['dist']) + self.assertEquals(original_config.get('comps', []), + config_info.get('comps', [])) - self.assertEquals(gpgcheck, repo_info.get('gpgcheck')) + inst.repository_delete(repo_id) + self.assertRaises(NotFoundError, inst.repository_lookup, repo_id) self.assertRaises(NotFoundError, inst.repository_lookup, 'google') - # remove files created - for repo in test_repos: - inst.repository_delete(repo['repo_id']) - self.assertRaises(NotFoundError, - inst.repository_lookup, repo['repo_id']) - def test_repository_update(self): inst = model.Model('test:///default', objstore_loc=self.tmp_store) - system_host_repos = len(inst.repositories_get_list()) + yum_repo = {'repo_id': 'fedora-fake', + 'baseurl': 'http://www.fedora.org'} + yum_new_repo = {'baseurl': 'http://www.fedora.org/updates'} + + deb_repo = {'baseurl': 'http://br.archive.ubuntu.com/kimchi/fake', + 'config': {'dist': 'quantal'}} + deb_new_repo = {'baseurl': 'http://archive.canonical.com/kimchi'} + + repo_type = inst.capabilities_lookup()['repo_mngt_tool'] + if repo_type == 'yum': + repo = yum_repo + new_repo = yum_new_repo + elif repo_type == 'deb': + repo = deb_repo + new_repo = deb_new_repo + else: + # repository management tool was not recognized by Kimchi + # skip test case + return - repo = {'repo_id': 'fedora-fake', - 'repo_name': 'Fedora 19 FAKE', - 'baseurl': 'http://www.fedora.org'} - inst.repositories_create(repo) + system_host_repos = len(inst.repositories_get_list()) + repo_id = inst.repositories_create(repo) host_repos = inst.repositories_get_list() self.assertEquals(system_host_repos + 1, len(host_repos)) - new_repo = {'repo_id': 'fedora-fake', - 'repo_name': 'Fedora 19 Update FAKE', - 'baseurl': 'http://www.fedora.org/update'} + new_repo_id = inst.repository_update(repo_id, new_repo) + repo_info = inst.repository_lookup(new_repo_id) - inst.repository_update(repo['repo_id'], new_repo) - repo_info = inst.repository_lookup(new_repo.get('repo_id')) - self.assertEquals(new_repo.get('repo_id'), repo_info.get('repo_id')) - self.assertEquals(new_repo.get('repo_name'), - repo_info.get('repo_name')) - self.assertEquals(new_repo.get('baseurl', None), - repo_info.get('baseurl')) - self.assertEquals(True, repo_info.get('enabled')) + self.assertEquals(new_repo_id, repo_info['repo_id']) + self.assertEquals(new_repo['baseurl'], repo_info['baseurl']) + self.assertEquals(True, repo_info['enabled']) # remove files creates - inst.repository_delete(repo['repo_id']) + inst.repository_delete(new_repo_id) def test_repository_disable_enable(self): inst = model.Model('test:///default', objstore_loc=self.tmp_store) + yum_repo = {'repo_id': 'fedora-fake', + 'baseurl': 'http://www.fedora.org'} + deb_repo = {'baseurl': 'http://br.archive.ubuntu.com/kimchi/fake', + 'config': {'dist': 'quantal'}} + + repo_type = inst.capabilities_lookup()['repo_mngt_tool'] + if repo_type == 'yum': + repo = yum_repo + elif repo_type == 'deb': + repo = deb_repo + else: + # repository management tool was not recognized by Kimchi + # skip test case + return + system_host_repos = len(inst.repositories_get_list()) - repo = {'repo_id': 'fedora-fake', - 'repo_name': 'Fedora 19 FAKE', - 'baseurl': 'http://www.fedora.org'} - inst.repositories_create(repo) + repo_id = inst.repositories_create(repo) host_repos = inst.repositories_get_list() self.assertEquals(system_host_repos + 1, len(host_repos)) - repo_info = inst.repository_lookup(repo.get('repo_id')) - self.assertEquals(True, repo_info.get('enabled')) + repo_info = inst.repository_lookup(repo_id) + self.assertEquals(True, repo_info['enabled']) - inst.repository_disable(repo.get('repo_id')) - repo_info = inst.repository_lookup(repo.get('repo_id')) - self.assertEquals(False, repo_info.get('enabled')) + inst.repository_disable(repo_id) + repo_info = inst.repository_lookup(repo_id) + self.assertEquals(False, repo_info['enabled']) - inst.repository_enable(repo.get('repo_id')) - repo_info = inst.repository_lookup(repo.get('repo_id')) - self.assertEquals(True, repo_info.get('enabled')) + inst.repository_enable(repo_id) + repo_info = inst.repository_lookup(repo_id) + self.assertEquals(True, repo_info['enabled']) # remove files creates - inst.repository_delete(repo['repo_id']) + inst.repository_delete(repo_id) class BaseModelTests(unittest.TestCase): diff --git a/tests/test_rest.py b/tests/test_rest.py index 831517f..fe8d3c4 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -1594,8 +1594,7 @@ class RestTests(unittest.TestCase): def test_repositories(self): def verify_repo(t, res): - for field in ('repo_id', 'repo_name', 'baseurl', 'is_mirror', - 'url_args', 'enabled', 'gpgcheck', 'gpgkey'): + for field in ('repo_id', 'enabled', 'baseurl', 'config'): if field in t.keys(): self.assertEquals(t[field], res[field]) @@ -1617,9 +1616,10 @@ class RestTests(unittest.TestCase): verify_repo(repo, res) # Update the repository - repo['baseurl'] = 'http://www.fedora.org/update' - req = json.dumps(repo) - resp = self.request('%s/fedora-fake' % base_uri, req, 'PUT') + params = {} + params['baseurl'] = repo['baseurl'] = 'http://www.fedora.org/update' + resp = self.request('%s/fedora-fake' % base_uri, json.dumps(params), + 'PUT') # Verify the repository res = json.loads(self.request('%s/fedora-fake' % base_uri).read()) -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> Just one yum/apt process can run per time so we need to lock those operations to avoid race condition problems. yum has a built in lock but it does not work well in kimchi as it only prevents the user to run two different YUM applications. As the kimchi threads are part of the same process, YUM does not really acquire the lock and then we get the error: [...] File "/usr/lib/python2.7/site-packages/urlgrabber/grabber.py", line 1258, in __init__ self._do_open() File "/usr/lib/python2.7/site-packages/urlgrabber/grabber.py", line 1588, in _do_open self._set_opts() File "/usr/lib/python2.7/site-packages/urlgrabber/grabber.py", line 1375, in _set_opts self.curl_obj.setopt(pycurl.NOPROGRESS, False) error: cannot invoke setopt() - perform() is currently running So create a kimchi lock to solve this problem. This lock can be used in any other place when needed. Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/config.py.in | 3 +++ src/kimchi/repositories.py | 39 ++++++++++++++++++++++++++++++++++++--- src/kimchi/swupdate.py | 7 +++++++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/kimchi/config.py.in b/src/kimchi/config.py.in index 426fbd1..cf8497a 100644 --- a/src/kimchi/config.py.in +++ b/src/kimchi/config.py.in @@ -21,6 +21,7 @@ import libvirt import os import platform +import threading from ConfigParser import SafeConfigParser @@ -31,6 +32,8 @@ from kimchi.xmlutils import xpath_get_text DEFAULT_LOG_LEVEL = "debug" +kimchiLock = threading.Lock() + # Storage pool constant for read-only pool types READONLY_POOL_TYPE = ['iscsi', 'scsi', 'mpath'] diff --git a/src/kimchi/repositories.py b/src/kimchi/repositories.py index 70f4f5b..39dde12 100644 --- a/src/kimchi/repositories.py +++ b/src/kimchi/repositories.py @@ -24,6 +24,7 @@ import urlparse from ConfigParser import ConfigParser from kimchi.basemodel import Singleton +from kimchi.config import kimchiLock from kimchi.exception import InvalidOperation from kimchi.exception import OperationFailed, NotFoundError, MissingParameter @@ -118,14 +119,19 @@ class YumRepo(object): """ Return a list of repositories IDs """ - return self._yb().repos.repos.keys() + kimchiLock.acquire() + repos = self._yb().repos.repos.keys() + kimchiLock.release() + return repos def getRepo(self, repo_id): """ Return a dictionary in the repositories.Repositories() of the given repository ID format with the information of a YumRepository object. """ + kimchiLock.acquire() repos = self._yb().repos + kimchiLock.release() if repo_id not in repos.repos.keys(): raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id}) @@ -162,7 +168,9 @@ class YumRepo(object): if repo_id is None: repo_id = "kimchi_repo_%s" % str(int(time.time() * 1000)) + kimchiLock.acquire() repos = self._yb().repos + kimchiLock.release() if repo_id in repos.repos.keys(): raise InvalidOperation("KCHREPOS0022E", {'repo_id': repo_id}) @@ -193,7 +201,9 @@ class YumRepo(object): return repo_id def toggleRepo(self, repo_id, enable): + kimchiLock.acquire() repos = self._yb().repos + kimchiLock.release() if repo_id not in repos.repos.keys(): raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id}) @@ -204,6 +214,7 @@ class YumRepo(object): if not enable and not entry.enabled: raise InvalidOperation("KCHREPOS0016E", {'repo_id': repo_id}) + kimchiLock.acquire() try: if enable: entry.enable() @@ -211,18 +222,23 @@ class YumRepo(object): entry.disable() self._conf.writeRawRepoFile(entry) - return repo_id except: if enable: raise OperationFailed("KCHREPOS0020E", {'repo_id': repo_id}) raise OperationFailed("KCHREPOS0021E", {'repo_id': repo_id}) + finally: + kimchiLock.release() + + return repo_id def updateRepo(self, repo_id, params): """ Update a given repository in repositories.Repositories() format """ + kimchiLock.acquire() repos = self._yb().repos + kimchiLock.release() if repo_id not in repos.repos.keys(): raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id}) @@ -242,14 +258,18 @@ class YumRepo(object): entry.name = config.get('repo_name', entry.name) entry.gpgcheck = config.get('gpgcheck', entry.gpgcheck) entry.gpgkey = config.get('gpgkey', entry.gpgkey) + kimchiLock.acquire() self._conf.writeRawRepoFile(entry) + kimchiLock.release() return repo_id def removeRepo(self, repo_id): """ Remove a given repository """ + kimchiLock.acquire() repos = self._yb().repos + kimchiLock.release() if repo_id not in repos.repos.keys(): raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id}) @@ -298,8 +318,10 @@ class AptRepo(object): return '%s-%s-%s' % (name, repo.dist, "-".join(repo.comps)) def _get_source_entry(self, repo_id): + kimchiLock.acquire() repos = self._sourceslist() repos.refresh() + kimchiLock.release() for r in repos: # Ignore deb-src repositories @@ -321,8 +343,10 @@ class AptRepo(object): internal control, the repository ID will be built as described in _get_repo_id() """ + kimchiLock.acquire() repos = self._sourceslist() repos.refresh() + kimchiLock.release() res = [] for r in repos: @@ -366,10 +390,12 @@ class AptRepo(object): dist = config['dist'] comps = config.get('comps', []) + kimchiLock.acquire() repos = self._sourceslist() repos.refresh() source_entry = repos.add('deb', uri, dist, comps, file=self.filename) repos.save() + kimchiLock.release() return self._get_repo_id(source_entry) @@ -392,18 +418,22 @@ class AptRepo(object): else: line = '#deb' + kimchiLock.acquire() try: repos = self._sourceslist() repos.refresh() repos.remove(r) repos.add(line, r.uri, r.dist, r.comps, file=self.filename) repos.save() - return repo_id except: if enable: raise OperationFailed("KCHREPOS0020E", {'repo_id': repo_id}) raise OperationFailed("KCHREPOS0021E", {'repo_id': repo_id}) + finally: + kimchiLock.release() + + return repo_id def updateRepo(self, repo_id, params): """ @@ -434,6 +464,7 @@ class AptRepo(object): if r is None: raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id}) + kimchiLock.acquire() try: repos = self._sourceslist() repos.refresh() @@ -441,3 +472,5 @@ class AptRepo(object): repos.save() except: raise OperationFailed("KCHREPOS0017E", {'repo_id': repo_id}) + finally: + kimchiLock.release() diff --git a/src/kimchi/swupdate.py b/src/kimchi/swupdate.py index ff5c9d1..45f7920 100644 --- a/src/kimchi/swupdate.py +++ b/src/kimchi/swupdate.py @@ -21,6 +21,7 @@ import subprocess import time from kimchi.basemodel import Singleton +from kimchi.config import kimchiLock from kimchi.exception import NotFoundError, OperationFailed from kimchi.utils import kimchi_log, run_command @@ -152,7 +153,9 @@ class YumUpdate(object): package = {'package_name': <string>, 'version': <string>, 'arch': <string>, 'repository': <string>} """ + kimchiLock.acquire() self._refreshUpdateList() + kimchiLock.release() pkg_list = [] for pkg in self._pkgs: package = {'package_name': pkg.name, @@ -188,7 +191,9 @@ class AptUpdate(object): package = {'package_name': <string>, 'version': <string>, 'arch': <string>, 'repository': <string>} """ + kimchiLock.acquire() self._refreshUpdateList() + kimchiLock.release() pkg_list = [] for pkg in self._pkgs: package = {'package_name': pkg.shortname, @@ -235,7 +240,9 @@ class ZypperUpdate(object): package = {'package_name': <string>, 'version': <string>, 'arch': <string>, 'repository': <string>} """ + kimchiLock.acquire() self._refreshUpdateList() + kimchiLock.release() pkg_list = [] for pkg in self._pkgs: pkg_list.append(pkg) -- 1.7.10.4

On 03/19/2014 01:04 AM, Aline Manera wrote:
From: Aline Manera <alinefm@br.ibm.com>
Just one yum/apt process can run per time so we need to lock those operations to avoid race condition problems.
yum has a built in lock but it does not work well in kimchi as it only prevents the user to run two different YUM applications. As the kimchi threads are part of the same process, YUM does not really acquire the lock and then we get the error: On my fedora, the system can call yum automatically sometimes, and the user can also call yum manully sometimes. Do we need to avoid this race condition?
[...] File "/usr/lib/python2.7/site-packages/urlgrabber/grabber.py", line 1258, in __init__ self._do_open() File "/usr/lib/python2.7/site-packages/urlgrabber/grabber.py", line 1588, in _do_open self._set_opts() File "/usr/lib/python2.7/site-packages/urlgrabber/grabber.py", line 1375, in _set_opts self.curl_obj.setopt(pycurl.NOPROGRESS, False) error: cannot invoke setopt() - perform() is currently running
So create a kimchi lock to solve this problem. This lock can be used in any other place when needed.
Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/config.py.in | 3 +++ src/kimchi/repositories.py | 39 ++++++++++++++++++++++++++++++++++++--- src/kimchi/swupdate.py | 7 +++++++ 3 files changed, 46 insertions(+), 3 deletions(-)
diff --git a/src/kimchi/config.py.in b/src/kimchi/config.py.in index 426fbd1..cf8497a 100644 --- a/src/kimchi/config.py.in +++ b/src/kimchi/config.py.in @@ -21,6 +21,7 @@ import libvirt import os import platform +import threading
from ConfigParser import SafeConfigParser @@ -31,6 +32,8 @@ from kimchi.xmlutils import xpath_get_text
DEFAULT_LOG_LEVEL = "debug"
+kimchiLock = threading.Lock() + # Storage pool constant for read-only pool types READONLY_POOL_TYPE = ['iscsi', 'scsi', 'mpath']
diff --git a/src/kimchi/repositories.py b/src/kimchi/repositories.py index 70f4f5b..39dde12 100644 --- a/src/kimchi/repositories.py +++ b/src/kimchi/repositories.py @@ -24,6 +24,7 @@ import urlparse from ConfigParser import ConfigParser
from kimchi.basemodel import Singleton +from kimchi.config import kimchiLock from kimchi.exception import InvalidOperation from kimchi.exception import OperationFailed, NotFoundError, MissingParameter
@@ -118,14 +119,19 @@ class YumRepo(object): """ Return a list of repositories IDs """ - return self._yb().repos.repos.keys() + kimchiLock.acquire() + repos = self._yb().repos.repos.keys() + kimchiLock.release() + return repos
def getRepo(self, repo_id): """ Return a dictionary in the repositories.Repositories() of the given repository ID format with the information of a YumRepository object. """ + kimchiLock.acquire() repos = self._yb().repos + kimchiLock.release() if repo_id not in repos.repos.keys(): raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id})
@@ -162,7 +168,9 @@ class YumRepo(object): if repo_id is None: repo_id = "kimchi_repo_%s" % str(int(time.time() * 1000))
+ kimchiLock.acquire() repos = self._yb().repos + kimchiLock.release() if repo_id in repos.repos.keys(): raise InvalidOperation("KCHREPOS0022E", {'repo_id': repo_id})
@@ -193,7 +201,9 @@ class YumRepo(object): return repo_id
def toggleRepo(self, repo_id, enable): + kimchiLock.acquire() repos = self._yb().repos + kimchiLock.release() if repo_id not in repos.repos.keys(): raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id})
@@ -204,6 +214,7 @@ class YumRepo(object): if not enable and not entry.enabled: raise InvalidOperation("KCHREPOS0016E", {'repo_id': repo_id})
+ kimchiLock.acquire() try: if enable: entry.enable() @@ -211,18 +222,23 @@ class YumRepo(object): entry.disable()
self._conf.writeRawRepoFile(entry) - return repo_id except: if enable: raise OperationFailed("KCHREPOS0020E", {'repo_id': repo_id})
raise OperationFailed("KCHREPOS0021E", {'repo_id': repo_id}) + finally: + kimchiLock.release() + + return repo_id
def updateRepo(self, repo_id, params): """ Update a given repository in repositories.Repositories() format """ + kimchiLock.acquire() repos = self._yb().repos + kimchiLock.release() if repo_id not in repos.repos.keys(): raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id})
@@ -242,14 +258,18 @@ class YumRepo(object): entry.name = config.get('repo_name', entry.name) entry.gpgcheck = config.get('gpgcheck', entry.gpgcheck) entry.gpgkey = config.get('gpgkey', entry.gpgkey) + kimchiLock.acquire() self._conf.writeRawRepoFile(entry) + kimchiLock.release() return repo_id
def removeRepo(self, repo_id): """ Remove a given repository """ + kimchiLock.acquire() repos = self._yb().repos + kimchiLock.release() if repo_id not in repos.repos.keys(): raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id})
@@ -298,8 +318,10 @@ class AptRepo(object): return '%s-%s-%s' % (name, repo.dist, "-".join(repo.comps))
def _get_source_entry(self, repo_id): + kimchiLock.acquire() repos = self._sourceslist() repos.refresh() + kimchiLock.release()
for r in repos: # Ignore deb-src repositories @@ -321,8 +343,10 @@ class AptRepo(object): internal control, the repository ID will be built as described in _get_repo_id() """ + kimchiLock.acquire() repos = self._sourceslist() repos.refresh() + kimchiLock.release()
res = [] for r in repos: @@ -366,10 +390,12 @@ class AptRepo(object): dist = config['dist'] comps = config.get('comps', [])
+ kimchiLock.acquire() repos = self._sourceslist() repos.refresh() source_entry = repos.add('deb', uri, dist, comps, file=self.filename) repos.save() + kimchiLock.release()
return self._get_repo_id(source_entry)
@@ -392,18 +418,22 @@ class AptRepo(object): else: line = '#deb'
+ kimchiLock.acquire() try: repos = self._sourceslist() repos.refresh() repos.remove(r) repos.add(line, r.uri, r.dist, r.comps, file=self.filename) repos.save() - return repo_id except: if enable: raise OperationFailed("KCHREPOS0020E", {'repo_id': repo_id})
raise OperationFailed("KCHREPOS0021E", {'repo_id': repo_id}) + finally: + kimchiLock.release() + + return repo_id
def updateRepo(self, repo_id, params): """ @@ -434,6 +464,7 @@ class AptRepo(object): if r is None: raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id})
+ kimchiLock.acquire() try: repos = self._sourceslist() repos.refresh() @@ -441,3 +472,5 @@ class AptRepo(object): repos.save() except: raise OperationFailed("KCHREPOS0017E", {'repo_id': repo_id}) + finally: + kimchiLock.release() diff --git a/src/kimchi/swupdate.py b/src/kimchi/swupdate.py index ff5c9d1..45f7920 100644 --- a/src/kimchi/swupdate.py +++ b/src/kimchi/swupdate.py @@ -21,6 +21,7 @@ import subprocess import time
from kimchi.basemodel import Singleton +from kimchi.config import kimchiLock from kimchi.exception import NotFoundError, OperationFailed from kimchi.utils import kimchi_log, run_command
@@ -152,7 +153,9 @@ class YumUpdate(object): package = {'package_name': <string>, 'version': <string>, 'arch': <string>, 'repository': <string>} """ + kimchiLock.acquire() self._refreshUpdateList() + kimchiLock.release() pkg_list = [] for pkg in self._pkgs: package = {'package_name': pkg.name, @@ -188,7 +191,9 @@ class AptUpdate(object): package = {'package_name': <string>, 'version': <string>, 'arch': <string>, 'repository': <string>} """ + kimchiLock.acquire() self._refreshUpdateList() + kimchiLock.release() pkg_list = [] for pkg in self._pkgs: package = {'package_name': pkg.shortname, @@ -235,7 +240,9 @@ class ZypperUpdate(object): package = {'package_name': <string>, 'version': <string>, 'arch': <string>, 'repository': <string>} """ + kimchiLock.acquire() self._refreshUpdateList() + kimchiLock.release() pkg_list = [] for pkg in self._pkgs: pkg_list.append(pkg)
-- Thanks and best regards! Sheldon Feng(冯少合)<shaohef@linux.vnet.ibm.com> IBM Linux Technology Center

On 03/19/2014 03:59 AM, Sheldon wrote:
On 03/19/2014 01:04 AM, Aline Manera wrote:
From: Aline Manera <alinefm@br.ibm.com>
Just one yum/apt process can run per time so we need to lock those operations to avoid race condition problems.
yum has a built in lock but it does not work well in kimchi as it only prevents the user to run two different YUM applications. As the kimchi threads are part of the same process, YUM does not really acquire the lock and then we get the error: On my fedora, the system can call yum automatically sometimes, and the user can also call yum manully sometimes. Do we need to avoid this race condition?
The race condition happens on cherrypy level - with 2 requests at the same time
[...] File "/usr/lib/python2.7/site-packages/urlgrabber/grabber.py", line 1258, in __init__ self._do_open() File "/usr/lib/python2.7/site-packages/urlgrabber/grabber.py", line 1588, in _do_open self._set_opts() File "/usr/lib/python2.7/site-packages/urlgrabber/grabber.py", line 1375, in _set_opts self.curl_obj.setopt(pycurl.NOPROGRESS, False) error: cannot invoke setopt() - perform() is currently running
So create a kimchi lock to solve this problem. This lock can be used in any other place when needed.
Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/config.py.in | 3 +++ src/kimchi/repositories.py | 39 ++++++++++++++++++++++++++++++++++++--- src/kimchi/swupdate.py | 7 +++++++ 3 files changed, 46 insertions(+), 3 deletions(-)
diff --git a/src/kimchi/config.py.in b/src/kimchi/config.py.in index 426fbd1..cf8497a 100644 --- a/src/kimchi/config.py.in +++ b/src/kimchi/config.py.in @@ -21,6 +21,7 @@ import libvirt import os import platform +import threading
from ConfigParser import SafeConfigParser @@ -31,6 +32,8 @@ from kimchi.xmlutils import xpath_get_text
DEFAULT_LOG_LEVEL = "debug"
+kimchiLock = threading.Lock() + # Storage pool constant for read-only pool types READONLY_POOL_TYPE = ['iscsi', 'scsi', 'mpath']
diff --git a/src/kimchi/repositories.py b/src/kimchi/repositories.py index 70f4f5b..39dde12 100644 --- a/src/kimchi/repositories.py +++ b/src/kimchi/repositories.py @@ -24,6 +24,7 @@ import urlparse from ConfigParser import ConfigParser
from kimchi.basemodel import Singleton +from kimchi.config import kimchiLock from kimchi.exception import InvalidOperation from kimchi.exception import OperationFailed, NotFoundError, MissingParameter
@@ -118,14 +119,19 @@ class YumRepo(object): """ Return a list of repositories IDs """ - return self._yb().repos.repos.keys() + kimchiLock.acquire() + repos = self._yb().repos.repos.keys() + kimchiLock.release() + return repos
def getRepo(self, repo_id): """ Return a dictionary in the repositories.Repositories() of the given repository ID format with the information of a YumRepository object. """ + kimchiLock.acquire() repos = self._yb().repos + kimchiLock.release() if repo_id not in repos.repos.keys(): raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id})
@@ -162,7 +168,9 @@ class YumRepo(object): if repo_id is None: repo_id = "kimchi_repo_%s" % str(int(time.time() * 1000))
+ kimchiLock.acquire() repos = self._yb().repos + kimchiLock.release() if repo_id in repos.repos.keys(): raise InvalidOperation("KCHREPOS0022E", {'repo_id': repo_id})
@@ -193,7 +201,9 @@ class YumRepo(object): return repo_id
def toggleRepo(self, repo_id, enable): + kimchiLock.acquire() repos = self._yb().repos + kimchiLock.release() if repo_id not in repos.repos.keys(): raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id})
@@ -204,6 +214,7 @@ class YumRepo(object): if not enable and not entry.enabled: raise InvalidOperation("KCHREPOS0016E", {'repo_id': repo_id})
+ kimchiLock.acquire() try: if enable: entry.enable() @@ -211,18 +222,23 @@ class YumRepo(object): entry.disable()
self._conf.writeRawRepoFile(entry) - return repo_id except: if enable: raise OperationFailed("KCHREPOS0020E", {'repo_id': repo_id})
raise OperationFailed("KCHREPOS0021E", {'repo_id': repo_id}) + finally: + kimchiLock.release() + + return repo_id
def updateRepo(self, repo_id, params): """ Update a given repository in repositories.Repositories() format """ + kimchiLock.acquire() repos = self._yb().repos + kimchiLock.release() if repo_id not in repos.repos.keys(): raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id})
@@ -242,14 +258,18 @@ class YumRepo(object): entry.name = config.get('repo_name', entry.name) entry.gpgcheck = config.get('gpgcheck', entry.gpgcheck) entry.gpgkey = config.get('gpgkey', entry.gpgkey) + kimchiLock.acquire() self._conf.writeRawRepoFile(entry) + kimchiLock.release() return repo_id
def removeRepo(self, repo_id): """ Remove a given repository """ + kimchiLock.acquire() repos = self._yb().repos + kimchiLock.release() if repo_id not in repos.repos.keys(): raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id})
@@ -298,8 +318,10 @@ class AptRepo(object): return '%s-%s-%s' % (name, repo.dist, "-".join(repo.comps))
def _get_source_entry(self, repo_id): + kimchiLock.acquire() repos = self._sourceslist() repos.refresh() + kimchiLock.release()
for r in repos: # Ignore deb-src repositories @@ -321,8 +343,10 @@ class AptRepo(object): internal control, the repository ID will be built as described in _get_repo_id() """ + kimchiLock.acquire() repos = self._sourceslist() repos.refresh() + kimchiLock.release()
res = [] for r in repos: @@ -366,10 +390,12 @@ class AptRepo(object): dist = config['dist'] comps = config.get('comps', [])
+ kimchiLock.acquire() repos = self._sourceslist() repos.refresh() source_entry = repos.add('deb', uri, dist, comps, file=self.filename) repos.save() + kimchiLock.release()
return self._get_repo_id(source_entry)
@@ -392,18 +418,22 @@ class AptRepo(object): else: line = '#deb'
+ kimchiLock.acquire() try: repos = self._sourceslist() repos.refresh() repos.remove(r) repos.add(line, r.uri, r.dist, r.comps, file=self.filename) repos.save() - return repo_id except: if enable: raise OperationFailed("KCHREPOS0020E", {'repo_id': repo_id})
raise OperationFailed("KCHREPOS0021E", {'repo_id': repo_id}) + finally: + kimchiLock.release() + + return repo_id
def updateRepo(self, repo_id, params): """ @@ -434,6 +464,7 @@ class AptRepo(object): if r is None: raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id})
+ kimchiLock.acquire() try: repos = self._sourceslist() repos.refresh() @@ -441,3 +472,5 @@ class AptRepo(object): repos.save() except: raise OperationFailed("KCHREPOS0017E", {'repo_id': repo_id}) + finally: + kimchiLock.release() diff --git a/src/kimchi/swupdate.py b/src/kimchi/swupdate.py index ff5c9d1..45f7920 100644 --- a/src/kimchi/swupdate.py +++ b/src/kimchi/swupdate.py @@ -21,6 +21,7 @@ import subprocess import time
from kimchi.basemodel import Singleton +from kimchi.config import kimchiLock from kimchi.exception import NotFoundError, OperationFailed from kimchi.utils import kimchi_log, run_command
@@ -152,7 +153,9 @@ class YumUpdate(object): package = {'package_name': <string>, 'version': <string>, 'arch': <string>, 'repository': <string>} """ + kimchiLock.acquire() self._refreshUpdateList() + kimchiLock.release() pkg_list = [] for pkg in self._pkgs: package = {'package_name': pkg.name, @@ -188,7 +191,9 @@ class AptUpdate(object): package = {'package_name': <string>, 'version': <string>, 'arch': <string>, 'repository': <string>} """ + kimchiLock.acquire() self._refreshUpdateList() + kimchiLock.release() pkg_list = [] for pkg in self._pkgs: package = {'package_name': pkg.shortname, @@ -235,7 +240,9 @@ class ZypperUpdate(object): package = {'package_name': <string>, 'version': <string>, 'arch': <string>, 'repository': <string>} """ + kimchiLock.acquire() self._refreshUpdateList() + kimchiLock.release() pkg_list = [] for pkg in self._pkgs: pkg_list.append(pkg)

On 03/19/2014 10:03 AM, Aline Manera wrote:
On 03/19/2014 03:59 AM, Sheldon wrote:
On 03/19/2014 01:04 AM, Aline Manera wrote:
From: Aline Manera <alinefm@br.ibm.com>
Just one yum/apt process can run per time so we need to lock those operations to avoid race condition problems.
yum has a built in lock but it does not work well in kimchi as it only prevents the user to run two different YUM applications. As the kimchi threads are part of the same process, YUM does not really acquire the lock and then we get the error: On my fedora, the system can call yum automatically sometimes, and the user can also call yum manully sometimes. Do we need to avoid this race condition?
The race condition happens on cherrypy level - with 2 requests at the same time
Sorry, let me explain better. The race condition is avoided in the cherrypy level The problem was identified when Kimchi makes the /host/packagesupdate and /host/repositories requests at the same time. You can verify this race condition in 2 terminals: - in one of them run "yum check-update" - in other one run "yum repolist all" The first one will run properly: [root@localhost ~]# yum check-update Loaded plugins: langpacks, refresh-packagekit But the second one will wait to lock be released. [root@localhost ~]# yum repolist all Loaded plugins: langpacks, refresh-packagekit Existing lock /var/run/yum.pid: another copy is running as pid 1703. Another app is currently holding the yum lock; waiting for it to exit... The other application is: yum Memory : 36 M RSS (355 MB VSZ) Started: Wed Mar 19 20:53:30 2014 - 00:02 ago State : Uninterruptible, pid: 1703 Another app is currently holding the yum lock; waiting for it to exit... The other application is: yum Memory : 45 M RSS (364 MB VSZ) Started: Wed Mar 19 20:53:30 2014 - 00:04 ago State : Running, pid: 1703 yum has a built in lock but it does not work well in kimchi as it only prevents the user to run two different YUM applications. As the kimchi threads are part of the same process, YUM does not really acquire the lock
[...] File "/usr/lib/python2.7/site-packages/urlgrabber/grabber.py", line 1258, in __init__ self._do_open() File "/usr/lib/python2.7/site-packages/urlgrabber/grabber.py", line 1588, in _do_open self._set_opts() File "/usr/lib/python2.7/site-packages/urlgrabber/grabber.py", line 1375, in _set_opts self.curl_obj.setopt(pycurl.NOPROGRESS, False) error: cannot invoke setopt() - perform() is currently running
So create a kimchi lock to solve this problem. This lock can be used in any other place when needed.
Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/config.py.in | 3 +++ src/kimchi/repositories.py | 39 ++++++++++++++++++++++++++++++++++++--- src/kimchi/swupdate.py | 7 +++++++ 3 files changed, 46 insertions(+), 3 deletions(-)
diff --git a/src/kimchi/config.py.in b/src/kimchi/config.py.in index 426fbd1..cf8497a 100644 --- a/src/kimchi/config.py.in +++ b/src/kimchi/config.py.in @@ -21,6 +21,7 @@ import libvirt import os import platform +import threading
from ConfigParser import SafeConfigParser @@ -31,6 +32,8 @@ from kimchi.xmlutils import xpath_get_text
DEFAULT_LOG_LEVEL = "debug"
+kimchiLock = threading.Lock() + # Storage pool constant for read-only pool types READONLY_POOL_TYPE = ['iscsi', 'scsi', 'mpath']
diff --git a/src/kimchi/repositories.py b/src/kimchi/repositories.py index 70f4f5b..39dde12 100644 --- a/src/kimchi/repositories.py +++ b/src/kimchi/repositories.py @@ -24,6 +24,7 @@ import urlparse from ConfigParser import ConfigParser
from kimchi.basemodel import Singleton +from kimchi.config import kimchiLock from kimchi.exception import InvalidOperation from kimchi.exception import OperationFailed, NotFoundError, MissingParameter
@@ -118,14 +119,19 @@ class YumRepo(object): """ Return a list of repositories IDs """ - return self._yb().repos.repos.keys() + kimchiLock.acquire() + repos = self._yb().repos.repos.keys() + kimchiLock.release() + return repos
def getRepo(self, repo_id): """ Return a dictionary in the repositories.Repositories() of the given repository ID format with the information of a YumRepository object. """ + kimchiLock.acquire() repos = self._yb().repos + kimchiLock.release() if repo_id not in repos.repos.keys(): raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id})
@@ -162,7 +168,9 @@ class YumRepo(object): if repo_id is None: repo_id = "kimchi_repo_%s" % str(int(time.time() * 1000))
+ kimchiLock.acquire() repos = self._yb().repos + kimchiLock.release() if repo_id in repos.repos.keys(): raise InvalidOperation("KCHREPOS0022E", {'repo_id': repo_id})
@@ -193,7 +201,9 @@ class YumRepo(object): return repo_id
def toggleRepo(self, repo_id, enable): + kimchiLock.acquire() repos = self._yb().repos + kimchiLock.release() if repo_id not in repos.repos.keys(): raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id})
@@ -204,6 +214,7 @@ class YumRepo(object): if not enable and not entry.enabled: raise InvalidOperation("KCHREPOS0016E", {'repo_id': repo_id})
+ kimchiLock.acquire() try: if enable: entry.enable() @@ -211,18 +222,23 @@ class YumRepo(object): entry.disable()
self._conf.writeRawRepoFile(entry) - return repo_id except: if enable: raise OperationFailed("KCHREPOS0020E", {'repo_id': repo_id})
raise OperationFailed("KCHREPOS0021E", {'repo_id': repo_id}) + finally: + kimchiLock.release() + + return repo_id
def updateRepo(self, repo_id, params): """ Update a given repository in repositories.Repositories() format """ + kimchiLock.acquire() repos = self._yb().repos + kimchiLock.release() if repo_id not in repos.repos.keys(): raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id})
@@ -242,14 +258,18 @@ class YumRepo(object): entry.name = config.get('repo_name', entry.name) entry.gpgcheck = config.get('gpgcheck', entry.gpgcheck) entry.gpgkey = config.get('gpgkey', entry.gpgkey) + kimchiLock.acquire() self._conf.writeRawRepoFile(entry) + kimchiLock.release() return repo_id
def removeRepo(self, repo_id): """ Remove a given repository """ + kimchiLock.acquire() repos = self._yb().repos + kimchiLock.release() if repo_id not in repos.repos.keys(): raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id})
@@ -298,8 +318,10 @@ class AptRepo(object): return '%s-%s-%s' % (name, repo.dist, "-".join(repo.comps))
def _get_source_entry(self, repo_id): + kimchiLock.acquire() repos = self._sourceslist() repos.refresh() + kimchiLock.release()
for r in repos: # Ignore deb-src repositories @@ -321,8 +343,10 @@ class AptRepo(object): internal control, the repository ID will be built as described in _get_repo_id() """ + kimchiLock.acquire() repos = self._sourceslist() repos.refresh() + kimchiLock.release()
res = [] for r in repos: @@ -366,10 +390,12 @@ class AptRepo(object): dist = config['dist'] comps = config.get('comps', [])
+ kimchiLock.acquire() repos = self._sourceslist() repos.refresh() source_entry = repos.add('deb', uri, dist, comps, file=self.filename) repos.save() + kimchiLock.release()
return self._get_repo_id(source_entry)
@@ -392,18 +418,22 @@ class AptRepo(object): else: line = '#deb'
+ kimchiLock.acquire() try: repos = self._sourceslist() repos.refresh() repos.remove(r) repos.add(line, r.uri, r.dist, r.comps, file=self.filename) repos.save() - return repo_id except: if enable: raise OperationFailed("KCHREPOS0020E", {'repo_id': repo_id})
raise OperationFailed("KCHREPOS0021E", {'repo_id': repo_id}) + finally: + kimchiLock.release() + + return repo_id
def updateRepo(self, repo_id, params): """ @@ -434,6 +464,7 @@ class AptRepo(object): if r is None: raise NotFoundError("KCHREPOS0012E", {'repo_id': repo_id})
+ kimchiLock.acquire() try: repos = self._sourceslist() repos.refresh() @@ -441,3 +472,5 @@ class AptRepo(object): repos.save() except: raise OperationFailed("KCHREPOS0017E", {'repo_id': repo_id}) + finally: + kimchiLock.release() diff --git a/src/kimchi/swupdate.py b/src/kimchi/swupdate.py index ff5c9d1..45f7920 100644 --- a/src/kimchi/swupdate.py +++ b/src/kimchi/swupdate.py @@ -21,6 +21,7 @@ import subprocess import time
from kimchi.basemodel import Singleton +from kimchi.config import kimchiLock from kimchi.exception import NotFoundError, OperationFailed from kimchi.utils import kimchi_log, run_command
@@ -152,7 +153,9 @@ class YumUpdate(object): package = {'package_name': <string>, 'version': <string>, 'arch': <string>, 'repository': <string>} """ + kimchiLock.acquire() self._refreshUpdateList() + kimchiLock.release() pkg_list = [] for pkg in self._pkgs: package = {'package_name': pkg.name, @@ -188,7 +191,9 @@ class AptUpdate(object): package = {'package_name': <string>, 'version': <string>, 'arch': <string>, 'repository': <string>} """ + kimchiLock.acquire() self._refreshUpdateList() + kimchiLock.release() pkg_list = [] for pkg in self._pkgs: package = {'package_name': pkg.shortname, @@ -235,7 +240,9 @@ class ZypperUpdate(object): package = {'package_name': <string>, 'version': <string>, 'arch': <string>, 'repository': <string>} """ + kimchiLock.acquire() self._refreshUpdateList() + kimchiLock.release() pkg_list = [] for pkg in self._pkgs: pkg_list.append(pkg)
_______________________________________________ Kimchi-devel mailing list Kimchi-devel@ovirt.org http://lists.ovirt.org/mailman/listinfo/kimchi-devel

Tested on Ubuntu 12.10 and Fedora 19 On 03/18/2014 02:04 PM, Aline Manera wrote:
From: Aline Manera <alinefm@br.ibm.com>
This patch set also fixes the issue #336 (https://github.com/kimchi-project/kimchi/issues/336)
In addition to several fixes found in repositories.py
Aline Manera (10): bug fix: Expose repository management tool name bug fix: Reorganize repository information mockmodel: Move specific repository data under 'config' Update messages used in the repositories management feature bug fix: Raise exception comming from backend bug fix: Sort repositories bug fix: Let package manager tool create the repository ID bug fix: Do not store internal repository information Update test cases to reflect the repositories changes bug fix: Lock yum/apt operations
docs/API.md | 82 +++-- po/en_US.po | 186 ++++++++--- po/kimchi.pot | 177 ++++++++--- po/pt_BR.po | 186 ++++++++--- po/zh_CN.po | 186 ++++++++--- src/kimchi/API.json | 111 ++++--- src/kimchi/config.py.in | 3 + src/kimchi/control/host.py | 3 +- src/kimchi/i18n.py | 36 ++- src/kimchi/mockmodel.py | 145 ++++----- src/kimchi/model/config.py | 6 +- src/kimchi/model/host.py | 30 +- src/kimchi/repositories.py | 730 +++++++++++++++++++------------------------- src/kimchi/swupdate.py | 7 + tests/test_model.py | 151 +++++---- tests/test_rest.py | 10 +- 16 files changed, 1188 insertions(+), 861 deletions(-)

I have started adapting repo management UI to the new API. Will post WIP or complete set before I sleep. On 3/18/2014 1:04 PM, Aline Manera wrote:
From: Aline Manera <alinefm@br.ibm.com>
This patch set also fixes the issue #336 (https://github.com/kimchi-project/kimchi/issues/336)
In addition to several fixes found in repositories.py
Aline Manera (10): bug fix: Expose repository management tool name bug fix: Reorganize repository information mockmodel: Move specific repository data under 'config' Update messages used in the repositories management feature bug fix: Raise exception comming from backend bug fix: Sort repositories bug fix: Let package manager tool create the repository ID bug fix: Do not store internal repository information Update test cases to reflect the repositories changes bug fix: Lock yum/apt operations
docs/API.md | 82 +++-- po/en_US.po | 186 ++++++++--- po/kimchi.pot | 177 ++++++++--- po/pt_BR.po | 186 ++++++++--- po/zh_CN.po | 186 ++++++++--- src/kimchi/API.json | 111 ++++--- src/kimchi/config.py.in | 3 + src/kimchi/control/host.py | 3 +- src/kimchi/i18n.py | 36 ++- src/kimchi/mockmodel.py | 145 ++++----- src/kimchi/model/config.py | 6 +- src/kimchi/model/host.py | 30 +- src/kimchi/repositories.py | 730 +++++++++++++++++++------------------------- src/kimchi/swupdate.py | 7 + tests/test_model.py | 151 +++++---- tests/test_rest.py | 10 +- 16 files changed, 1188 insertions(+), 861 deletions(-)
-- Adam King <rak@linux.vnet.ibm.com> IBM C&SI

-- Reviewed-by: Paulo Vital <pvital@linux.vnet.ibm.com> On Tue, 2014-03-18 at 14:04 -0300, Aline Manera wrote:
From: Aline Manera <alinefm@br.ibm.com>
This patch set also fixes the issue #336 (https://github.com/kimchi-project/kimchi/issues/336)
In addition to several fixes found in repositories.py
Aline Manera (10): bug fix: Expose repository management tool name bug fix: Reorganize repository information mockmodel: Move specific repository data under 'config' Update messages used in the repositories management feature bug fix: Raise exception comming from backend bug fix: Sort repositories bug fix: Let package manager tool create the repository ID bug fix: Do not store internal repository information Update test cases to reflect the repositories changes bug fix: Lock yum/apt operations
docs/API.md | 82 +++-- po/en_US.po | 186 ++++++++--- po/kimchi.pot | 177 ++++++++--- po/pt_BR.po | 186 ++++++++--- po/zh_CN.po | 186 ++++++++--- src/kimchi/API.json | 111 ++++--- src/kimchi/config.py.in | 3 + src/kimchi/control/host.py | 3 +- src/kimchi/i18n.py | 36 ++- src/kimchi/mockmodel.py | 145 ++++----- src/kimchi/model/config.py | 6 +- src/kimchi/model/host.py | 30 +- src/kimchi/repositories.py | 730 +++++++++++++++++++------------------------- src/kimchi/swupdate.py | 7 + tests/test_model.py | 151 +++++---- tests/test_rest.py | 10 +- 16 files changed, 1188 insertions(+), 861 deletions(-)
participants (4)
-
Adam King
-
Aline Manera
-
Paulo Ricardo Paz Vital
-
Sheldon