Initial commit

This commit is contained in:
Ben Nemec 2016-06-09 23:54:09 +00:00
commit c689baa002
12 changed files with 490 additions and 0 deletions

21
.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
*.py[cod]
# Unit test / coverage reports
.coverage
cover
.tox
.testrepository
# Packages
.eggs
*.egg
*.egg-info
dist
build
eggs
parts
sdist
develop-eggs
.installed.cfg
lib
lib64

4
.testr.conf Normal file
View File

@ -0,0 +1,4 @@
[DEFAULT]
test_command=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 OS_LOG_CAPTURE=1 ${PYTHON:-python} -m subunit.run discover -t ./dlrn_repo ./dlrn_repo $LISTOPT $IDOPTION
test_id_option=--load-list $IDFILE
test_list_option=--list

11
README.rst Normal file
View File

@ -0,0 +1,11 @@
dlrn-repo
=========
A tool for managing dlrn repos.
See: https://github.com/openstack-packages/DLRN
Note that this is intended as a tool for bootstrapping the repo setup in
things like TripleO, so it should not have any runtime OpenStack dependencies
or we end up in a chicken-and-egg pickle, and let's be honest - no one wants a
chicken and egg pickle.

0
dlrn_repo/__init__.py Normal file
View File

145
dlrn_repo/main.py Executable file
View File

@ -0,0 +1,145 @@
#!/usr/bin/env python
# Copyright 2016 Red Hat, Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
import os
import re
import subprocess
import sys
import requests
TARGET = '/etc/yum.repos.d'
# Uncomment when doing development if you don't want to mess with your actual
# system repos while testing
#TARGET = 'test'
TITLE_RE = re.compile('\[(.*)\]')
# Packages to be included from delorean-current when using current-tripleo
INCLUDE_PKGS = ('includepkgs=diskimage-builder,instack,instack-undercloud,'
'os-apply-config,os-cloud-config,os-collect-config,'
'os-net-config,os-refresh-config,python-tripleoclient,'
'tripleo-common,openstack -tripleo-heat-templates,'
'openstack-tripleo-image-elements,openstack-tripleo,'
'openstack-tripleo-puppet-elements,openstack-puppet-modules'
)
class InvalidArguments(Exception):
pass
class NoRepoTitle(Exception):
pass
def _parse_args():
parser = argparse.ArgumentParser(
description='Download and instll dlrn repos. Note that these repos '
'require yum-plugin-priorities in order to function '
'correctly, so that will also be installed.')
parser.add_argument('repos', metavar='REPO', nargs='+',
help='A list of delorean repos. Available repos: '
'current, deps, current-tripleo. current-tripleo '
'downloads both the current-tripleo and current '
'repos, but sets the current repo to only be used '
'for TripleO projects')
parser.add_argument('-d', '--distro',
default='centos7',
help='Target distro. Currently only centos7 is '
'supported')
parser.add_argument('-b', '--branch',
default='master',
help='Target branch. Should be the lowercase name of '
'the OpenStack release. e.g. liberty')
return parser.parse_args()
def _get_repo(path):
r = requests.get(path)
if r.status_code == 200:
return r.text
else:
r.raise_for_status()
def _write_repo(content):
m = TITLE_RE.match(content)
if not m:
raise NoRepoTitle('Could not find repo title in: \n%s' % content)
filename = m.group(1) + '.repo'
filename = os.path.join(TARGET, filename)
with open(filename, 'w') as f:
f.write(content)
print('Installed repo %s to %s' % (m.group(1), filename))
def _validate_args(args):
if 'current' in args.repos and 'current-tripleo' in args.repos:
raise InvalidArguments('Cannot use "current" and "current-tripleo" '
'together')
if args.branch != 'master' and 'current-tripleo' in args.repos:
raise InvalidArguments('Cannot use current-tripleo on any branch '
'except master')
if args.distro != 'centos7':
raise InvalidArguments('centos7 is the only supported distro')
def _remove_existing():
"""Remove any delorean* repos that already exist"""
for f in os.listdir(TARGET):
if f.startswith('delorean'):
filename = os.path.join(TARGET, f)
os.remove(filename)
print('Removed old repo "%s"' % filename)
def _get_base_path(args):
if args.branch != 'master':
distro_branch = '%s-%s' % (args.distro, args.branch)
else:
distro_branch = args.distro
return 'http://trunk.rdoproject.org/%s/' % distro_branch
def _install_priorities():
try:
subprocess.check_call(['yum', 'install', '-y',
'yum-plugin-priorities'])
except subprocess.CalledProcessError:
print('ERROR: Failed to install yum-plugin-priorities.')
raise
def _install_repos(args, base_path):
for repo in args.repos:
if repo == 'current':
content = _get_repo(base_path + 'current/delorean.repo')
if args.branch != 'master':
content = TITLE_RE.sub('[delorean-%s]' % args.branch, content)
_write_repo(content)
elif repo == 'deps':
content = _get_repo(base_path + 'delorean-deps.repo')
_write_repo(content)
elif repo == 'current-tripleo':
content = _get_repo(base_path + 'current-tripleo/delorean.repo')
content = TITLE_RE.sub('[delorean-current-tripleo]', content)
_write_repo(content)
content = _get_repo(base_path + 'current/delorean.repo')
content += '\n%s' % INCLUDE_PKGS
_write_repo(content)
else:
raise InvalidArguments('Invalid repo "%s" specified' % repo)
def main():
args = _parse_args()
_validate_args(args)
base_path = _get_base_path(args)
_install_priorities()
_remove_existing()
_install_repos(args, base_path)
if __name__ == '__main__':
main()

View File

View File

@ -0,0 +1,214 @@
# Copyright 2016 Red Hat, Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import subprocess
import sys
import mock
import testtools
from dlrn_repo import main
class TestDlrnRepo(testtools.TestCase):
def test_target(self):
self.assertEqual('/etc/yum.repos.d', main.TARGET)
@mock.patch('dlrn_repo.main._parse_args')
@mock.patch('dlrn_repo.main._validate_args')
@mock.patch('dlrn_repo.main._get_base_path')
@mock.patch('dlrn_repo.main._install_priorities')
@mock.patch('dlrn_repo.main._remove_existing')
@mock.patch('dlrn_repo.main._install_repos')
def test_main(self, mock_install, mock_remove, mock_ip, mock_gbp,
mock_validate, mock_parse):
mock_args = mock.Mock()
mock_parse.return_value = mock_args
mock_path = mock.Mock()
mock_gbp.return_value = mock_path
main.main()
mock_validate.assert_called_once_with(mock_args)
mock_gbp.assert_called_once_with(mock_args)
mock_ip.assert_called_once_with()
mock_remove.assert_called_once_with()
mock_install.assert_called_once_with(mock_args, mock_path)
@mock.patch('requests.get')
def test_get_repo(self, mock_get):
mock_response = mock.Mock()
mock_response.status_code = 200
mock_response.text = '88MPH'
mock_get.return_value = mock_response
fake_addr = 'http://lone/pine/mall'
content = main._get_repo(fake_addr)
self.assertEqual('88MPH', content)
mock_get.assert_called_once_with(fake_addr)
@mock.patch('requests.get')
def test_get_repo_404(self, mock_get):
mock_response = mock.Mock()
mock_response.status_code = 404
mock_get.return_value = mock_response
fake_addr = 'http://twin/pines/mall'
main._get_repo(fake_addr)
mock_get.assert_called_once_with(fake_addr)
mock_response.raise_for_status.assert_called_once_with()
@mock.patch('os.listdir')
@mock.patch('os.remove')
def test_remove_existing(self, mock_remove, mock_listdir):
fake_list = ['foo.repo', 'delorean.repo',
'delorean-current-tripleo.repo']
mock_listdir.return_value = fake_list
main._remove_existing()
self.assertIn(mock.call('/etc/yum.repos.d/delorean.repo'),
mock_remove.mock_calls)
self.assertIn(mock.call('/etc/yum.repos.d/'
'delorean-current-tripleo.repo'),
mock_remove.mock_calls)
self.assertNotIn(mock.call('/etc/yum.repos.d/foo.repo'),
mock_remove.mock_calls)
def test_get_base_path(self):
args = mock.Mock()
args.branch = 'master'
args.distro = 'centos7'
path = main._get_base_path(args)
self.assertEqual('http://trunk.rdoproject.org/centos7/', path)
def test_get_base_path_branch(self):
args = mock.Mock()
args.branch = 'liberty'
args.distro = 'centos7'
path = main._get_base_path(args)
self.assertEqual('http://trunk.rdoproject.org/centos7-liberty/', path)
@mock.patch('subprocess.check_call')
def test_install_priorities(self, mock_check_call):
main._install_priorities()
mock_check_call.assert_called_once_with(['yum', 'install', '-y',
'yum-plugin-priorities'])
@mock.patch('subprocess.check_call')
def test_install_priorities_fails(self, mock_check_call):
mock_check_call.side_effect = subprocess.CalledProcessError(88, '88')
self.assertRaises(subprocess.CalledProcessError,
main._install_priorities)
@mock.patch('dlrn_repo.main._get_repo')
@mock.patch('dlrn_repo.main._write_repo')
def test_install_repos_current(self, mock_write, mock_get):
args = mock.Mock()
args.repos = ['current']
args.branch = 'master'
mock_get.return_value = '[delorean]\nMr. Fusion'
main._install_repos(args, 'roads/')
mock_get.assert_called_once_with('roads/current/delorean.repo')
mock_write.assert_called_once_with('[delorean]\nMr. Fusion')
@mock.patch('dlrn_repo.main._get_repo')
@mock.patch('dlrn_repo.main._write_repo')
def test_install_repos_current_mitaka(self, mock_write, mock_get):
args = mock.Mock()
args.repos = ['current']
args.branch = 'mitaka'
mock_get.return_value = '[delorean]\nMr. Fusion'
main._install_repos(args, 'roads/')
mock_get.assert_called_once_with('roads/current/delorean.repo')
mock_write.assert_called_once_with('[delorean-mitaka]\nMr. Fusion')
@mock.patch('dlrn_repo.main._get_repo')
@mock.patch('dlrn_repo.main._write_repo')
def test_install_repos_deps(self, mock_write, mock_get):
args = mock.Mock()
args.repos = ['deps']
args.branch = 'master'
mock_get.return_value = '[delorean-deps]\nMr. Fusion'
main._install_repos(args, 'roads/')
mock_get.assert_called_once_with('roads/delorean-deps.repo')
mock_write.assert_called_once_with('[delorean-deps]\nMr. Fusion')
@mock.patch('dlrn_repo.main._get_repo')
@mock.patch('dlrn_repo.main._write_repo')
def test_install_repos_current_tripleo(self, mock_write, mock_get):
args = mock.Mock()
args.repos = ['current-tripleo']
args.branch = 'master'
mock_get.return_value = '[delorean]\nMr. Fusion'
main._install_repos(args, 'roads/')
mock_get.assert_any_call('roads/current-tripleo/delorean.repo')
mock_write.assert_any_call('[delorean-current-tripleo]\n'
'Mr. Fusion')
mock_get.assert_called_with('roads/current/delorean.repo')
mock_write.assert_called_with('[delorean]\nMr. Fusion\n%s' %
main.INCLUDE_PKGS)
def test_install_repos_invalid(self):
args = mock.Mock()
args.repos = ['roads?']
self.assertRaises(main.InvalidArguments, main._install_repos, args,
'roads/')
def test_write_repo(self):
m = mock.mock_open()
with mock.patch('dlrn_repo.main.open', m, create=True):
main._write_repo('[delorean]\nThis=Heavy')
m.assert_called_once_with('/etc/yum.repos.d/delorean.repo', 'w')
m().write.assert_called_once_with('[delorean]\nThis=Heavy')
def test_write_repo_invalid(self):
self.assertRaises(main.NoRepoTitle, main._write_repo, 'Great Scot!')
def test_parse_args(self):
with mock.patch.object(sys, 'argv', ['', 'current', 'deps', '-d',
'centos7', '-b', 'liberty']):
args = main._parse_args()
self.assertEqual(['current', 'deps'], args.repos)
self.assertEqual('centos7', args.distro)
self.assertEqual('liberty', args.branch)
def test_parse_args_long(self):
with mock.patch.object(sys, 'argv', ['', 'current', '--distro',
'centos7', '--branch',
'mitaka']):
args = main._parse_args()
self.assertEqual(['current'], args.repos)
self.assertEqual('centos7', args.distro)
self.assertEqual('mitaka', args.branch)
class TestValidate(testtools.TestCase):
def setUp(self):
super(TestValidate, self).setUp()
self.args = mock.Mock()
self.args.repos = ['current']
self.args.branch = 'master'
self.args.distro = 'centos7'
def test_good(self):
main._validate_args(self.args)
def test_current_and_tripleo(self):
self.args.repos = ['current', 'current-tripleo']
self.assertRaises(main.InvalidArguments, main._validate_args,
self.args)
def test_branch_and_tripleo(self):
self.args.repos = ['current-tripleo']
self.args.branch = 'liberty'
self.assertRaises(main.InvalidArguments, main._validate_args,
self.args)
def test_invalid_distro(self):
self.args.distro = 'Jigawatts 1.21'
self.assertRaises(main.InvalidArguments, main._validate_args,
self.args)

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
requests

33
setup.cfg Normal file
View File

@ -0,0 +1,33 @@
[metadata]
name = dlrn-repo
summary = A tool for managing dlrn repos
description-file =
README.rst
author = Ben Nemec
author-email = bnemec@redhat.com
home-page = http://www.redhat.com/
classifier =
Environment :: OpenStack
Intended Audience :: Information Technology
Intended Audience :: System Administrators
License :: OSI Approved :: Apache Software License
Operating System :: POSIX :: Linux
Programming Language :: Python
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
Programming Language :: Python :: 3.4
[files]
packages =
dlrn_repo
[entry_points]
console_scripts =
dlrn-repo = dlrn_repo.main:main
[build_sphinx]
all_files = 1
build-dir = doc/build
source-dir = doc/source

23
setup.py Executable file
View File

@ -0,0 +1,23 @@
#!/usr/bin/env python
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
import setuptools
setuptools.setup(
setup_requires=['pbr'],
pbr=True)

7
test-requirements.txt Normal file
View File

@ -0,0 +1,7 @@
coverage>=3.6
discover
fixtures>=0.3.14
python-subunit>=0.0.18
testrepository>=0.0.18
testtools>=0.9.36,!=1.2.0
mock>=1.0

31
tox.ini Normal file
View File

@ -0,0 +1,31 @@
[tox]
minversion = 1.6
skipsdist = True
envlist = py34,py27,pep8
[testenv]
usedevelop = True
setenv = VIRTUAL_ENV={envdir}
deps = -r{toxinidir}/test-requirements.txt
-r{toxinidir}/requirements.txt
commands = python setup.py testr --slowest --testr-args='{posargs}'
[testenv:venv]
commands = {posargs}
[testenv:docs]
commands = python setup.py build_sphinx
[testenv:pep8]
deps = flake8
commands = flake8
[testenv:cover]
commands = python setup.py test --coverage --coverage-package-name=dlrn_repo --testr-args='{posargs}'
[flake8]
ignore = H803
show-source = True
# puppet-stack-config horribly violates E501 (line length), but I'm not
# bothered enough to spend the time to fix it.
exclude = .tox,dist,doc,*.egg,build