- •Пояснительная записка
- •Перечень условных обозначений
- •Введение
- •Основные принципы систем управления пакетами и обзор программных решений
- •Назначение систем управления пакетами
- •Требования систем управления пакетами
- •Анализ существующих решений rpm
- •Portage
- •Проектирование системы управления пакетами
- •Программная реализация системы управления пакетами
- •Выбор средств разработки
- •Реализация системы управления пакетами
- •Тестирование реализации управления пакетами
- •Заключение
- •Библиографический указатель
Реализация системы управления пакетами
Реализация системы управления пакетами была условно разделена на три части:
Работа с удаленным репозиторием с формулами пакетов
Реализация базовых команд для управления пакетами:
Freeze – вывод всех установленных пакетов
Cache – вывод всех доступных для установки пакетов
Outdated – вывод всех установленных пакетов требующих обновления
Update – обновления формул из репозитория
Install – установка выбранного пакета
Uninstall – удаление выбранного пакета
Upgrade – обновление выбранного пакета
Реализация тестового консольного клиента
Основной момент реализации заключается в обновлении доступных пакетов, для хранения формул используется удаленный репозитории под управление системы контроля версий Git, для того чтобы забрать интересующие формулы нам необходимо связать по одному из доступных протоколов (http, https, …) и «склонить» содержимое репозитория, в дальнейшем, при наличии локального репозитория на машине пользователя, будет происходить только обновление локального репозитория посредством команд pull.
Приведенный ниже код позволяет провести все вышеперечисленные операции, а также обрабатывает возможные исключения которые могут возникнуть в ходе данной операции.
#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (c) 2012 Procyon <https://github.com/Gr1N/procyon> # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from __future__ import unicode_literals
from git.cmd import Git from git.exc import InvalidGitRepositoryError, GitCommandError, NoSuchPathError from git.repo.base import Repo as GitRepo
from procyon import settings as procyon_settings
__all__ = ( 'update_repo', )
def open_or_clone_repo(): """Returns opened or cloned repo and 'True' flag if operation successful else 'None' object and 'False' flag. """ try: return GitRepo(path=procyon_settings.REPO_PATH), True except (InvalidGitRepositoryError, NoSuchPathError): pass
try: Git(procyon_settings.PROCYON_PATH).clone(procyon_settings.REMOTE_REPO) except GitCommandError: return None, False
return GitRepo(path=procyon_settings.REPO_PATH), True
def update_repo(): """Returns 'True' and hexshas if operation successful else 'False' and none-hexshas. If raises some erros when repo updating, return 'False', current repo hash and none-hexsha. """ repo, successful = open_or_clone_repo()
if not successful: return False, None, None
hexsha = lambda r: r.heads.master.commit.tree.hexsha before_up_hexhsha = hexsha(repo)
try: origin = repo.remotes.origin origin.pull() except GitCommandError: return False, before_up_hexhsha, None except AssertionError: pass
after_up_hexsha = hexsha(repo) return True, before_up_hexhsha, after_up_hexsha
|
Следующий ключевой момент это формулы, формулы представляют собой Python-модуль в котором описаны все ключевые моменты пакета:
Имя пакета
Информация о пакете
Версия пакета
Домашняя страница
Ссылка по которой будет скачиваться пакет
Md5sum для проверки скаченного архива
Так как любая формула представляет собой Python-модуль это дает широкие возможности мейнтейнерам пакетов, т.к. они могут определить в них все что позволяет язык программирования.
Ниже приведена формула абстрактного пакета:
from procyon.pkg.models import Formula as BaseFormula
class Formula(BaseFormula): name = 'Test awesome package' info = 'This package can help you to save the world!!' home_page = 'http://how-help-to-save-the-world.com/' version = '42' url = 'http://download.me/help.zip' md5sum = 'ff4157cded0b84234a8235e3b4858572'
|
Для хранения информации об установленных пакетах была спроектирована и реализована БД, в качестве БД используется SQLite.
SQLite — легковесная встраиваемая реляционная база данных. Слово «встраиваемый» означает, что SQLite не использует парадигму клиент-сервер, то есть движок SQLite не является отдельно работающим процессом, с которым взаимодействует программа, а предоставляет библиотеку, с которой программа компонуется и движок становится составной частью программы. Таким образом, в качестве протокола обмена используются вызовы функций (API) библиотеки SQLite. Такой подход уменьшает накладные расходы, время отклика и упрощает программу. SQLite хранит всю базу данных (включая определения, таблицы, индексы и данные) в единственном стандартном файле на том компьютере, на котором исполняется программа. Простота реализации достигается за счёт того, что перед началом исполнения транзакции записи весь файл, хранящий базу данных, блокируется; ACID-функции достигаются в том числе за счёт создания файла журнала.
Для удобства работы с БД, был использован ORM peewee, который позволяет не писать SQL-запросы, т.к. все запросы обернуты соответствующими метода. Спроектированная таблица БД, содержит следующие поля:
Name – имя установленного пакета
Formula_name – имя формулы посредством которой был установлен пакета
Version – текущая версия установленного пакета
Updated_at – дата последнего обновления пакета
Ниже приведена текущая реализация БД с использованием ORM peewee:
database = os.path.join(procyon_settings.PROCYON_PATH, procyon_settings.PACKAGES_DB_NAME) database = peewee.SqliteDatabase(database) database.connect()
class Package(peewee.Model): name = peewee.CharField(db_index=True, unique=True) formula_name = peewee.CharField(db_index=True, unique=True) version = peewee.CharField() updated_at = peewee.DateTimeField(default=datetime.now())
class Meta: database = database
if not Package.table_exists(): Package.create_table() |
Приведенный код реализует соединение с БД на локальном диске пользователя и в случае если в БД еще не созданы необходимые таблицы для работы, то создает их.
Реализация команд:
Freeze – данная команда позволяет просмотреть все установленные пакеты посредством разрабатываемой системы управления пакетами. Команда реализует запрос к БД который выбирает все доступные значения. Ниже приведена реализация команды freeze:
def get_installed_packages(): """Returns dictionary with all installed packages. """ installed = {}
for entry in Package.select(): installed.setdefault(entry.name, { 'formula_name': entry.formula_name, 'version': entry.version, 'updated_at': entry.updated_at, })
return installed |
Cache – данная команда позволяет просмотреть все доступные для установки пакеты. Команда смотрит директорию где хранятся формулы, достает из них необходимую информацию и возвращает словарь с полученными данными. Ниже приведена реализация команды cache:
def get_available_packages(): """Returns dictionary with available to install packages from repo. """ available = {}
for modulename in os.listdir(procyon_settings.REPO_PATH): if modulename != '__init__.py' and modulename.endswith('.py'): package_name, package_data = get_package_data(modulename)
if package_name and package_data: available.setdefault(package_name, package_data)
return available |
Outdated – данная команда получает все установленные пакеты из БД, получает все доступные пакеты для установки и сравнивает версии, если версия пакета устарела, то этот пакет помечается как устаревший. Ниже приведена реализация команды outdated:
def check_version(available_version, installed_version): available_lpart, dot, available_rpart = available_version.partition('.') installed_lpart, dot, installed_rpart = installed_version.partition('.')
if int(available_lpart) > int(installed_lpart): return True elif int(available_lpart) < int(installed_lpart): return False elif len(available_rpart) == 0: return False elif len(available_rpart) != 0 and len(installed_rpart) == 0: return True else: return check_version(available_rpart, installed_rpart)
def get_outdated_packages(): """Returns dictionary with outdated installed packages. """ installed = get_installed_packages() available = get_available_packages()
outdated = {}
for package_name, package_data in installed.iteritems(): available_version = available.get(package_name, {}).get('version', None) installed_version = package_data.get('version')
if available_version and check_version(available_version, installed_version): package_data.update({ 'available_version': available_version, }) outdated.setdefault(package_name, package_data)
return outdated |
Search – команда позволяет искать доступные пакеты по имени, алгоритм команды схож с реализацией команды cache, с учетом выборки по имени пакета. Ниже приведена реализация команды search:
def get_available_packages_by_name(name): """Returns dictionary with available to install packages from repo with specified package name. """ def prepare_name(name): name = str(name).lower()
for to_replace in ['-', '_', '/', ' ', '[', ']']: name = name.replace(to_replace, '')
return name
name = prepare_name(name) available = {}
for package_name, package_data in get_available_packages().iteritems(): prepared_pkg_name = prepare_name(package_name)
if name in prepared_pkg_name or prepared_pkg_name in name: available.setdefault(package_name, package_data)
return available elif int(available_lpart) < int(installed_lpart): return False elif len(available_rpart) == 0: return False elif len(available_rpart) != 0 and len(installed_rpart) == 0: return True else: return check_version(available_rpart, installed_rpart)
def get_outdated_packages(): """Returns dictionary with outdated installed packages. """ installed = get_installed_packages() available = get_available_packages()
outdated = {}
for package_name, package_data in installed.iteritems(): available_version = available.get(package_name, {}).get('version', None) installed_version = package_data.get('version')
|
Install – команда посредством которой происходит непосредственно установка пакетов. Команде на вход подается название пакета который необходимо установить, в первую очередь происходит проверка на то, что такой пакет вообще существует, в случае успешной проверки происходит загрузка архива с пакетом по разрешенным протоколам (на данный момент это https и http), далее если в описании указана md5sum со идет проверка на совпадение хэшей, если все удачно, то происходит завершающий этап установки. Ниже приведена реализация команды install:
# procyon/pkg/logic.py def install_package(name): available = get_available_packages() if name not in available: return InstallationStatuses.FORMULA_NOT_FOUND
packages = [package.name for package in Package.select().where(Package.name == name)] if len(packages) > 1: return InstallationStatuses.INSTALL_ERROR elif packages and packages[0] == name: return InstallationStatuses.ALREADY_INSTALLED
package = available.get(name) formula = import_formula_module(package.get('formula_name')) if not formula: return InstallationStatuses.BAD_FORMULA
status = formula.install() if status != InstallationStatuses.INSTALL_OK: return status
Package.create( name=formula.name, formula_name=package.get('formula_name'), version=formula.version )
return status
# procyon/pkg/models.py class Formula(object): name = None info = None version = None homepage = None
url = None md5sum = None
def check_items(self): if self.name and self.info and self.version and self.url: return True
return False
def _check_md5sub(self, tmp_file): md5 = hashlib.md5()
with open(tmp_file, 'rb') as f: for chunk in iter(lambda: f.read(128 * md5.block_size), b''): md5.update(chunk)
return md5.hexdigest() == self.md5sum
def _download(self): allowed_schemes = [ 'http', 'https', ] scheme = urlparse(self.url).scheme if scheme not in allowed_schemes: return InstallationStatuses.BAD_URL, None
try: tmp_file, info = urlretrieve(self.url) except IOError: return InstallationStatuses.DOWNLOAD_ERROR, None
if not zipfile.is_zipfile(tmp_file) and not tarfile.is_tarfile(tmp_file): return InstallationStatuses.BAD_FILE_TYPE, None
if self.md5sum and not self._check_md5sub(tmp_file): return InstallationStatuses.MD5SUM_CHECK_ERROR, None
return InstallationStatuses.DOWNLOAD_OK, tmp_file
def _extract(self, tmp_file): if zipfile.is_zipfile(tmp_file): arc = zipfile.ZipFile(tmp_file) elif tarfile.is_tarfile(tmp_file): arc = tarfile.TarFile(tmp_file) else: return InstallationStatuses.BAD_FILE_TYPE
install_dir = os.path.join(procyon_settings.INSTALL_PATH, self.name) if not os.path.exists(install_dir): os.makedirs(install_dir)
try: arc.extractall(path=install_dir) except (zipfile.BadZipfile, zipfile.LargeZipFile, tarfile.ReadError, tarfile.ExtractError): return InstallationStatuses.EXTRACT_ERROR
return InstallationStatuses.EXTRACT_OK
def install(self): if not self.check_items(): return InstallationStatuses.BAD_FORMULA
status, tmp_file = self._download() if status != InstallationStatuses.DOWNLOAD_OK: return status
# TODO: install dependencies
status = self._extract(tmp_file) if status != InstallationStatuses.EXTRACT_OK: return status
return InstallationStatuses.INSTALL_OK
|
Uninstall – команда реализующая функционально полностью противоположную функциональности команды install, т.е. удаление пакетов. Команде на вход подается название пакета который необходимо удалить, в первую очередь происходит проверка, а установлен ли такой пакет вообще, если установлен, то происходит инициализация процесса удаления пакета: удаляется из БД запить об этом пакете, а также удаляется сам пакет с локального диска пользователя. Ниже приведена реализация команды uninstall:
# procyon/pkg/logic.py def uninstall_package(name): installed = get_installed_packages() if name not in installed: return InstallationStatuses.NOT_INSTALLED
package = installed.get(name) formula = import_formula_module(package.get('formula_name')) if not formula: return InstallationStatuses.BAD_FORMULA
status = formula.uninstall() if status != InstallationStatuses.UNINSTALL_OK: return status
package = Package.get(name=name) package.delete_instance()
return status
# procyon/pkg/models.py class Formula(object): name = None ... def uninstall(self): if not self.check_items(): return InstallationStatuses.BAD_FORMULA
install_dir = os.path.join(procyon_settings.INSTALL_PATH, self.name) if not os.path.exists(install_dir): return InstallationStatuses.NOT_INSTALLED
shutil.rmtree(install_dir)
# TODO: uninstall dependencies
return InstallationStatuses.UNINSTALL_OK
|
Текущая реализация системы управления пакетами представляет собой небольшую библиотеку, т.е. api, необходимое для управления пакетами, описанные выше команды представляют собой основу этого api. Т.к. разработка велась на языке Python, то данная библиотека представляет собой не что иное как пакет который подготовлен для установки в окружения пользователя или посредством install.py скрипта или посредством pip’a.
Как уже говорилось выше разрабатываемая система управления пакетами это всего лишь библиотека и без какой-либо обертки для упрощения использования конечного пользователя, не несет никакой ценности. Поэтому в качестве демонстрации возможностей разрабатываемой системы, был разработан тестовый консольный клиент, клиент полноценно использует все возможности библиотеки. Ниже представлен вывод команды help, которая демонстрирует доступный функционал клиента:
procyon/test_client(branch:master) » python client.py –h usage: procyon [-h] command [parameter [parameter ...]]
Package manager for OSTIS project.
positional arguments: command command name parameter command parameter (file url or command name)
optional arguments: -h, --help show this help message and exit
Supported commands: set <url> - Sets url of repo containing packages. update - Updates package list. install <packages> - Installs new package. remove <packages> - Removes installed package. upgrade <packages> - Installs new version of all installed packages. search <package> - Searches packages with key word. list <list command> - Shows list of specified packages
Supported list commands: installed - Shows list of currently installed packages. available - Shows list of available to install packages. outdated - Shows list of outdated packages. |
Ниже, для примера, приведен вывод команды на получение всех доступных пакетов для установки:
procyon/test_client(branch:master) » python client.py list available Available package list:
Test awesome package v42 This package can help you to save the world!!
Test2 v12 ololo2 |
Как можно увидеть доступно два пакета, а также выведена краткая информация о них и их версии.