Add simple data driver for partitioning info

This new "simple" data driver takes serialized partitioning info
that is provided before provisioning from external tool.
It does not make any calculations for that data and expect it to have
all required informations about partitioning.
Other changes:
* added unittest2 to use some of it features
* added requests_mock to mock http requests
* added objects conversion to/from dictionary to make serialization
  easier
* added a common "interface" for data drivers
* fixed test for "do_build_image" - now a correct data driver is used

Change-Id: I673cde6f0ead9945919a87cd1cfce7ed09c6e593
Implements: blueprint volume-manager-refactoring
This commit is contained in:
Sebastian Kalinowski 2015-07-14 14:31:52 +02:00
parent ce4bfd0037
commit 9e22866f3e
15 changed files with 1160 additions and 101 deletions

View File

@ -28,3 +28,23 @@ class BaseDataDriver(object):
def __init__(self, data):
self.data = copy.deepcopy(data)
@abc.abstractproperty
def partition_scheme(self):
"""Retruns instance of PartionScheme object"""
@abc.abstractproperty
def image_scheme(self):
"""Returns instance of ImageScheme object"""
@abc.abstractproperty
def grub(self):
"""Returns instance of Grub object"""
@abc.abstractproperty
def operating_system(self):
"""Returns instance of OperatingSystem object"""
@abc.abstractproperty
def configdrive_scheme(self):
"""Returns instance of ConfigDriveScheme object"""

View File

@ -15,12 +15,12 @@
import itertools
import math
import os
import six
import yaml
import six
from six.moves.urllib.parse import urljoin
from six.moves.urllib.parse import urlparse
from six.moves.urllib.parse import urlsplit
import yaml
from fuel_agent.drivers.base import BaseDataDriver
from fuel_agent.drivers import ks_spaces_validator
@ -67,6 +67,8 @@ def match_device(hu_disk, ks_disk):
class Nailgun(BaseDataDriver):
"""Driver for parsing regular volumes metadata from Nailgun."""
def __init__(self, data):
super(Nailgun, self).__init__(data)
@ -79,11 +81,31 @@ class Nailgun(BaseDataDriver):
# get rid of md over all disks for /boot partition.
self._boot_done = False
self.partition_scheme = self.parse_partition_scheme()
self.grub = self.parse_grub()
self.configdrive_scheme = self.parse_configdrive_scheme()
self._partition_scheme = self.parse_partition_scheme()
self._grub = self.parse_grub()
self._configdrive_scheme = self.parse_configdrive_scheme()
# parsing image scheme needs partition scheme has been parsed
self.image_scheme = self.parse_image_scheme()
self._image_scheme = self.parse_image_scheme()
@property
def partition_scheme(self):
return self._partition_scheme
@property
def image_scheme(self):
return self._image_scheme
@property
def grub(self):
return self._grub
@property
def operating_system(self):
return None
@property
def configdrive_scheme(self):
return self._configdrive_scheme
def partition_data(self):
return self.data['ks_meta']['pm_data']['ks_spaces']
@ -346,7 +368,7 @@ class Nailgun(BaseDataDriver):
for volume in vg['volumes']:
LOG.debug('Processing lv %s' % volume['name'])
if volume['size'] <= 0:
LOG.debug('Lv size is zero. Skipping.')
LOG.debug('LogicalVolume size is zero. Skipping.')
continue
if volume['type'] == 'lv':
@ -543,8 +565,31 @@ class NailgunBuildImage(BaseDataDriver):
def __init__(self, data):
super(NailgunBuildImage, self).__init__(data)
self._image_scheme = objects.ImageScheme()
self._partition_scheme = objects.PartitionScheme()
self.parse_schemes()
self.parse_operating_system()
self._operating_system = self.parse_operating_system()
@property
def partition_scheme(self):
return self._partition_scheme
@property
def image_scheme(self):
return self._image_scheme
@property
def grub(self):
return None
@property
def operating_system(self):
return self._operating_system
@property
def configdrive_scheme(self):
return None
def parse_operating_system(self):
if self.data.get('codename').lower() != 'trusty':
@ -563,11 +608,9 @@ class NailgunBuildImage(BaseDataDriver):
section=repo['section'],
priority=repo['priority']))
self.operating_system = objects.Ubuntu(repos=repos, packages=packages)
return objects.Ubuntu(repos=repos, packages=packages)
def parse_schemes(self):
self.image_scheme = objects.ImageScheme()
self.partition_scheme = objects.PartitionScheme()
for mount, image in six.iteritems(self.data['image_data']):
filename = os.path.basename(urlsplit(image['uri']).path)
@ -575,13 +618,13 @@ class NailgunBuildImage(BaseDataDriver):
# during initialization.
device = objects.Loop()
self.image_scheme.add_image(
self._image_scheme.add_image(
uri='file://' + os.path.join(self.data['output'], filename),
format=image['format'],
container=image['container'],
target_device=device)
self.partition_scheme.add_fs(
self._partition_scheme.add_fs(
device=device,
mount=mount,
fs_type=image['format'])

View File

@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, 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.
from fuel_agent.drivers import nailgun
from fuel_agent import objects
class NailgunSimpleDriver(nailgun.Nailgun):
"""Simple driver that do not make any computations.
This driver digest information that already has all required
information how to perform partitioning.
Service that sends data to fuel_agent is responsible for preparing
it in correct format.
"""
@property
def partition_data(self):
return self.data.get('partitioning', {})
@classmethod
def parse_lv_data(cls, raw_lvs):
return [objects.LV.from_dict(lv) for lv in raw_lvs]
@classmethod
def parse_pv_data(cls, raw_pvs):
return [objects.PV.from_dict(pv) for pv in raw_pvs]
@classmethod
def parse_fs_data(cls, raw_fss):
return [objects.FS.from_dict(fs) for fs in raw_fss]
@classmethod
def parse_vg_data(cls, raw_vgs):
return [objects.VG.from_dict(vg) for vg in raw_vgs]
@classmethod
def parse_md_data(cls, raw_mds):
return [objects.MD.from_dict(md) for md in raw_mds]
@classmethod
def parse_parted_data(cls, raw_parteds):
return [objects.Parted.from_dict(parted) for parted in raw_parteds]
def parse_partition_scheme(self):
partition_scheme = objects.PartitionScheme()
for obj in ('lv', 'pv', 'fs', 'vg', 'md', 'parted'):
attr = '{0}s'.format(obj)
parse_method = getattr(self, 'parse_{0}_data'.format(obj))
raw = self.partition_data.get(attr, {})
setattr(partition_scheme, attr, parse_method(raw))
return partition_scheme

View File

@ -22,18 +22,19 @@ from fuel_agent.objects.image import Image
from fuel_agent.objects.image import ImageScheme
from fuel_agent.objects.operating_system import OperatingSystem
from fuel_agent.objects.operating_system import Ubuntu
from fuel_agent.objects.partition import Fs
from fuel_agent.objects.partition import Lv
from fuel_agent.objects.partition import Md
from fuel_agent.objects.partition import FS
from fuel_agent.objects.partition import LV
from fuel_agent.objects.partition import MD
from fuel_agent.objects.partition import Parted
from fuel_agent.objects.partition import Partition
from fuel_agent.objects.partition import PartitionScheme
from fuel_agent.objects.partition import Pv
from fuel_agent.objects.partition import Vg
from fuel_agent.objects.partition import PV
from fuel_agent.objects.partition import VG
from fuel_agent.objects.repo import DEBRepo
from fuel_agent.objects.repo import Repo
__all__ = [
'Partition', 'Pv', 'Vg', 'Lv', 'Md', 'Fs', 'PartitionScheme',
'Partition', 'Parted', 'PV', 'VG', 'LV', 'MD', 'FS', 'PartitionScheme',
'ConfigDriveCommon', 'ConfigDrivePuppet', 'ConfigDriveMcollective',
'ConfigDriveScheme', 'Image', 'ImageScheme', 'Grub',
'OperatingSystem', 'Ubuntu',

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, 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 abc
import six
from fuel_agent.utils.decorators import abstractclassmethod
@six.add_metaclass(abc.ABCMeta)
class Serializable(object):
@abc.abstractmethod
def to_dict(self):
pass
@abstractclassmethod
def from_dict(cls, data):
pass

View File

@ -12,20 +12,23 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import os
from fuel_agent import errors
from fuel_agent.objects import base
from fuel_agent.openstack.common import log as logging
LOG = logging.getLogger(__name__)
class Parted(object):
def __init__(self, name, label):
class Parted(base.Serializable):
def __init__(self, name, label, partitions=None, install_bootloader=False):
self.name = name
self.label = label
self.partitions = []
self.install_bootloader = False
self.partitions = partitions or []
self.install_bootloader = install_bootloader
def add_partition(self, **kwargs):
# TODO(kozhukalov): validate before appending
@ -99,14 +102,31 @@ class Parted(object):
separator = 'p'
return '%s%s%s' % (self.name, separator, self.next_count())
def to_dict(self):
partitions = [partition.to_dict() for partition in self.partitions]
return {
'name': self.name,
'label': self.label,
'partitions': partitions,
'install_bootloader': self.install_bootloader,
}
@classmethod
def from_dict(cls, data):
data = copy.deepcopy(data)
raw_partitions = data.pop('partitions')
partitions = [Partition.from_dict(partition)
for partition in raw_partitions]
return cls(partitions=partitions, **data)
class Partition(base.Serializable):
class Partition(object):
def __init__(self, name, count, device, begin, end, partition_type,
flags=None, guid=None, configdrive=False):
self.name = name
self.count = count
self.device = device
self.name = name
self.begin = begin
self.end = end
self.type = partition_type
@ -121,15 +141,48 @@ class Partition(object):
def set_guid(self, guid):
self.guid = guid
def to_dict(self):
return {
'name': self.name,
'count': self.count,
'device': self.device,
'begin': self.begin,
'end': self.end,
'partition_type': self.type,
'flags': self.flags,
'guid': self.guid,
'configdrive': self.configdrive,
}
@classmethod
def from_dict(cls, data):
return cls(**data)
class PhysicalVolume(base.Serializable):
class Pv(object):
def __init__(self, name, metadatasize=16, metadatacopies=2):
self.name = name
self.metadatasize = metadatasize
self.metadatacopies = metadatacopies
def to_dict(self):
return {
'name': self.name,
'metadatasize': self.metadatasize,
'metadatacopies': self.metadatacopies,
}
@classmethod
def from_dict(cls, data):
return cls(**data)
PV = PhysicalVolume
class VolumeGroup(base.Serializable):
class Vg(object):
def __init__(self, name, pvnames=None):
self.name = name
self.pvnames = pvnames or []
@ -138,8 +191,22 @@ class Vg(object):
if pvname not in self.pvnames:
self.pvnames.append(pvname)
def to_dict(self):
return {
'name': self.name,
'pvnames': self.pvnames
}
@classmethod
def from_dict(cls, data):
return cls(**data)
VG = VolumeGroup
class LogicalVolume(base.Serializable):
class Lv(object):
def __init__(self, name, vgname, size):
self.name = name
self.vgname = vgname
@ -150,8 +217,23 @@ class Lv(object):
return '/dev/mapper/%s-%s' % (self.vgname.replace('-', '--'),
self.name.replace('-', '--'))
def to_dict(self):
return {
'name': self.name,
'vgname': self.vgname,
'size': self.size,
}
@classmethod
def from_dict(cls, data):
return cls(**data)
LV = LogicalVolume
class MultipleDevice(base.Serializable):
class Md(object):
def __init__(self, name, level,
devices=None, spares=None):
self.name = name
@ -173,8 +255,24 @@ class Md(object):
'device %s is already attached' % device)
self.spares.append(device)
def to_dict(self):
return {
'name': self.name,
'level': self.level,
'devices': self.devices,
'spares': self.spares,
}
@classmethod
def from_dict(cls, data):
return cls(**data)
MD = MultipleDevice
class FileSystem(base.Serializable):
class Fs(object):
def __init__(self, device, mount=None,
fs_type=None, fs_options=None, fs_label=None):
self.device = device
@ -183,6 +281,22 @@ class Fs(object):
self.options = fs_options or ''
self.label = fs_label or ''
def to_dict(self):
return {
'device': self.device,
'mount': self.mount,
'fs_type': self.type,
'fs_options': self.options,
'fs_label': self.label,
}
@classmethod
def from_dict(cls, data):
return cls(**data)
FS = FileSystem
class PartitionScheme(object):
def __init__(self):
@ -199,22 +313,22 @@ class PartitionScheme(object):
return parted
def add_pv(self, **kwargs):
pv = Pv(**kwargs)
pv = PV(**kwargs)
self.pvs.append(pv)
return pv
def add_vg(self, **kwargs):
vg = Vg(**kwargs)
vg = VG(**kwargs)
self.vgs.append(vg)
return vg
def add_lv(self, **kwargs):
lv = Lv(**kwargs)
lv = LV(**kwargs)
self.lvs.append(lv)
return lv
def add_fs(self, **kwargs):
fs = Fs(**kwargs)
fs = FS(**kwargs)
self.fss.append(fs)
return fs
@ -222,7 +336,7 @@ class PartitionScheme(object):
mdkwargs = {}
mdkwargs['name'] = kwargs.get('name') or self.md_next_name()
mdkwargs['level'] = kwargs.get('level') or 'mirror'
md = Md(**mdkwargs)
md = MD(**mdkwargs)
self.mds.append(md)
return md
@ -357,3 +471,13 @@ class PartitionScheme(object):
for prt in parted.partitions:
if prt.configdrive:
return prt.name
def to_dict(self):
return {
'parteds': [parted.to_dict() for parted in self.parteds],
'mds': [md.to_dict() for md in self.mds],
'pvs': [pv.to_dict() for pv in self.pvs],
'vgs': [vg.to_dict() for vg in self.vgs],
'lvs': [lv.to_dict() for lv in self.lvs],
'fss': [fs.to_dict() for fs in self.fss],
}

27
fuel_agent/tests/base.py Normal file
View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, 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 json
import os
FIXTURE_PATH = os.path.join(os.path.dirname(__file__), 'fixtures')
def load_fixture(filename):
path = os.path.join(FIXTURE_PATH, filename)
with open(path) as f:
return json.load(f)

View File

@ -0,0 +1,326 @@
{
"hostname": "node-1.domain.tld",
"interfaces": {
"eth0": {
"dns_name": "node-1.domain.tld",
"ip_address": "10.20.0.3",
"mac_address": "08:00:27:79:da:80",
"netmask": "255.255.255.0",
"static": "0"
},
"eth1": {
"mac_address": "08:00:27:46:43:60",
"static": "0"
},
"eth2": {
"mac_address": "08:00:27:b1:d7:15",
"static": "0"
}
},
"interfaces_extra": {
"eth0": {
"onboot": "yes",
"peerdns": "no"
},
"eth1": {
"onboot": "no",
"peerdns": "no"
},
"eth2": {
"onboot": "no",
"peerdns": "no"
}
},
"kernel_options": {
"netcfg/choose_interface": "08:00:27:79:da:80",
"udevrules": "08:00:27:79:da:80_eth0,08:00:27:46:43:60_eth1,08:00:27:b1:d7:15_eth2"
},
"ks_meta": {
"auth_key": "fake_auth_key",
"authorized_keys": [
"fake_authorized_key1",
"fake_authorized_key2"
],
"fuel_version": "5.0.1",
"gw": "10.20.0.1",
"image_data": {
"/": {
"container": "gzip",
"format": "ext4",
"uri": "http://fake.host.org:123/imgs/fake_image.img.gz"
}
},
"install_log_2_syslog": 1,
"master_ip": "10.20.0.2",
"mco_auto_setup": 1,
"mco_connector": "rabbitmq",
"mco_enable": 1,
"mco_host": "10.20.0.2",
"mco_password": "marionette",
"mco_pskey": "unset",
"mco_user": "mcollective",
"mco_vhost": "mcollective",
"pm_data": {
"kernel_params": "console=ttyS0,9600 console=tty0 rootdelay=90 nomodeset",
"ks_spaces": []
},
"puppet_auto_setup": 1,
"puppet_enable": 0,
"puppet_master": "fuel.domain.tld",
"repo_setup": {
"repos": [
{
"name": "repo1",
"priority": 1001,
"section": "section",
"suite": "suite",
"type": "deb",
"uri": "uri1"
},
{
"name": "repo2",
"priority": 1001,
"section": "section",
"suite": "suite",
"type": "deb",
"uri": "uri2"
}
]
},
"timezone": "America/Los_Angeles"
},
"name": "node-1",
"name_servers": "\"10.20.0.2\"",
"name_servers_search": "\"domain.tld\"",
"netboot_enabled": "1",
"power_address": "10.20.0.253",
"power_pass": "/root/.ssh/bootstrap.rsa",
"power_type": "ssh",
"power_user": "root",
"profile": "pro_fi-le",
"slave_name": "node-1",
"uid": "1",
"partitioning": {
"fss": [
{
"device": "/dev/sda3",
"fs_label": "",
"fs_options": "",
"fs_type": "ext2",
"mount": "/boot"
},
{
"device": "/dev/sda4",
"fs_label": "",
"fs_options": "",
"fs_type": "ext2",
"mount": "/tmp"
},
{
"device": "/dev/mapper/os-root",
"fs_label": "",
"fs_options": "",
"fs_type": "ext4",
"mount": "/"
},
{
"device": "/dev/mapper/os-swap",
"fs_label": "",
"fs_options": "",
"fs_type": "swap",
"mount": "swap"
},
{
"device": "/dev/mapper/image-glance",
"fs_label": "",
"fs_options": "",
"fs_type": "xfs",
"mount": "/var/lib/glance"
}
],
"lvs": [
{
"name": "root",
"size": 15360,
"vgname": "os"
},
{
"name": "swap",
"size": 4014,
"vgname": "os"
},
{
"name": "glance",
"size": 175347,
"vgname": "image"
}
],
"mds": [],
"parteds": [
{
"label": "gpt",
"name": "/dev/sdb",
"partitions": [
{
"begin": 1,
"configdrive": false,
"count": 1,
"device": "/dev/sdb",
"end": 25,
"flags": [
"bios_grub"
],
"guid": null,
"name": "/dev/sdb1",
"partition_type": "primary"
},
{
"begin": 25,
"configdrive": false,
"count": 2,
"device": "/dev/sdb",
"end": 225,
"flags": [],
"guid": null,
"name": "/dev/sdb2",
"partition_type": "primary"
},
{
"begin": 225,
"configdrive": false,
"count": 3,
"device": "/dev/sdb",
"end": 65196,
"flags": [],
"guid": null,
"name": "/dev/sdb3",
"partition_type": "primary"
}
]
},
{
"label": "gpt",
"name": "/dev/sda",
"partitions": [
{
"begin": 1,
"configdrive": false,
"count": 1,
"device": "/dev/sda",
"end": 25,
"flags": [
"bios_grub"
],
"guid": null,
"name": "/dev/sda1",
"partition_type": "primary"
},
{
"begin": 25,
"configdrive": false,
"count": 2,
"device": "/dev/sda",
"end": 225,
"flags": [],
"guid": null,
"name": "/dev/sda2",
"partition_type": "primary"
},
{
"begin": 225,
"configdrive": false,
"count": 3,
"device": "/dev/sda",
"end": 425,
"flags": [],
"guid": null,
"name": "/dev/sda3",
"partition_type": "primary"
},
{
"begin": 425,
"configdrive": false,
"count": 4,
"device": "/dev/sda",
"end": 625,
"flags": [],
"guid": "fake_guid",
"name": "/dev/sda4",
"partition_type": "primary"
},
{
"begin": 625,
"configdrive": false,
"count": 5,
"device": "/dev/sda",
"end": 20063,
"flags": [],
"guid": null,
"name": "/dev/sda5",
"partition_type": "primary"
},
{
"begin": 20063,
"configdrive": false,
"count": 6,
"device": "/dev/sda",
"end": 65660,
"flags": [],
"guid": null,
"name": "/dev/sda6",
"partition_type": "primary"
},
{
"begin": 65660,
"configdrive": true,
"count": 7,
"device": "/dev/sda",
"end": 65680,
"flags": [],
"guid": null,
"name": "/dev/sda7",
"partition_type": "primary"
}
]
}
],
"pvs": [
{
"metadatacopies": 2,
"metadatasize": 28,
"name": "/dev/sda5"
},
{
"metadatacopies": 2,
"metadatasize": 28,
"name": "/dev/sda6"
},
{
"metadatacopies": 2,
"metadatasize": 28,
"name": "/dev/sdb3"
},
{
"metadatacopies": 2,
"metadatasize": 28,
"name": "/dev/sdc3"
}
],
"vgs": [
{
"name": "image",
"pvnames": [
"/dev/sda6",
"/dev/sdb3",
"/dev/sdc3"
]
},
{
"name": "os",
"pvnames": [
"/dev/sda5"
]
}
]
}
}

View File

@ -19,6 +19,7 @@ import signal
from oslo.config import cfg
from oslotest import base as test_base
from fuel_agent.drivers import nailgun
from fuel_agent import errors
from fuel_agent import manager
from fuel_agent import objects
@ -438,7 +439,7 @@ class TestManager(test_base.BaseTestCase):
mock_fu):
mock_os.path.islink.return_value = True
mock_utils.execute.return_value = (None, None)
self.mgr.driver.partition_scheme = objects.PartitionScheme()
self.mgr.driver._partition_scheme = objects.PartitionScheme()
self.mgr.mount_target('fake_chroot')
mock_open.assert_called_once_with('fake_chroot/etc/mtab', 'wb')
mock_os.path.islink.assert_called_once_with('fake_chroot/etc/mtab')
@ -451,7 +452,7 @@ class TestManager(test_base.BaseTestCase):
@mock.patch('fuel_agent.manager.os', create=True)
def test_mount_target(self, mock_os, mock_open, mock_utils, mock_fu):
mock_os.path.islink.return_value = False
self.mgr.driver.partition_scheme = objects.PartitionScheme()
self.mgr.driver._partition_scheme = objects.PartitionScheme()
self.mgr.driver.partition_scheme.add_fs(
device='fake', mount='/var/lib', fs_type='xfs')
self.mgr.driver.partition_scheme.add_fs(
@ -500,7 +501,7 @@ none /run/shm tmpfs rw,nosuid,nodev 0 0"""
@mock.patch('fuel_agent.manager.fu', create=True)
def test_umount_target(self, mock_fu):
self.mgr.driver.partition_scheme = objects.PartitionScheme()
self.mgr.driver._partition_scheme = objects.PartitionScheme()
self.mgr.driver.partition_scheme.add_fs(
device='fake', mount='/var/lib', fs_type='xfs')
self.mgr.driver.partition_scheme.add_fs(
@ -522,6 +523,38 @@ none /run/shm tmpfs rw,nosuid,nodev 0 0"""
mock.call('fake_chroot/', try_lazy_umount=True)],
mock_fu.umount_fs.call_args_list)
class TestImageBuild(test_base.BaseTestCase):
@mock.patch('yaml.load')
@mock.patch.object(utils, 'init_http_request')
@mock.patch.object(utils, 'get_driver')
def setUp(self, mock_driver, mock_http, mock_yaml):
super(self.__class__, self).setUp()
mock_driver.return_value = nailgun.NailgunBuildImage
image_conf = {
"image_data": {
"/": {
"container": "gzip",
"format": "ext4",
"uri": "http:///centos_65_x86_64.img.gz",
},
},
"output": "/var/www/nailgun/targetimages",
"repos": [
{
"name": "repo",
"uri": "http://some",
'type': 'deb',
'suite': '/',
'section': '',
'priority': 1001
}
],
"codename": "trusty"
}
self.mgr = manager.Manager(image_conf)
@mock.patch('fuel_agent.manager.bu', create=True)
@mock.patch('fuel_agent.manager.fu', create=True)
@mock.patch('fuel_agent.manager.utils', create=True)
@ -540,17 +573,17 @@ none /run/shm tmpfs rw,nosuid,nodev 0 0"""
loops = [objects.Loop(), objects.Loop()]
self.mgr.driver.image_scheme = objects.ImageScheme([
self.mgr.driver._image_scheme = objects.ImageScheme([
objects.Image('file:///fake/img.img.gz', loops[0], 'ext4', 'gzip'),
objects.Image('file:///fake/img-boot.img.gz',
loops[1], 'ext2', 'gzip')])
self.mgr.driver.partition_scheme = objects.PartitionScheme()
self.mgr.driver._partition_scheme = objects.PartitionScheme()
self.mgr.driver.partition_scheme.add_fs(
device=loops[0], mount='/', fs_type='ext4')
self.mgr.driver.partition_scheme.add_fs(
device=loops[1], mount='/boot', fs_type='ext2')
self.mgr.driver.metadata_uri = 'file:///fake/img.yaml'
self.mgr.driver.operating_system = objects.Ubuntu(
self.mgr.driver._operating_system = objects.Ubuntu(
repos=[
objects.DEBRepo('ubuntu', 'http://fakeubuntu',
'trusty', 'fakesection', priority=900),

View File

@ -12,12 +12,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import mock
import os
import mock
import six
from six.moves.urllib.parse import urlsplit
from oslotest import base as test_base
import unittest2
from fuel_agent.drivers.nailgun import NailgunBuildImage
from fuel_agent import errors
@ -100,51 +100,45 @@ IMAGE_DATA_SAMPLE = {
}
class TestNailgunBuildImage(test_base.BaseTestCase):
class TestNailgunBuildImage(unittest2.TestCase):
def test_default_trusty_packages(self):
self.assertEqual(NailgunBuildImage.DEFAULT_TRUSTY_PACKAGES,
DEFAULT_TRUSTY_PACKAGES)
@mock.patch.object(NailgunBuildImage, '__init__')
def test_parse_operating_system_error_bad_codename(self, mock_init):
mock_init.return_value = None
driver = NailgunBuildImage()
driver.data = {'codename': 'not-trusty'}
self.assertRaises(errors.WrongInputDataError,
driver.parse_operating_system)
@mock.patch.object(NailgunBuildImage, 'parse_schemes')
def test_parse_operating_system_error_bad_codename(self,
mock_parse_schemes):
with self.assertRaises(errors.WrongInputDataError):
data = {'codename': 'not-trusty'}
NailgunBuildImage(data)
@mock.patch('fuel_agent.objects.Ubuntu')
@mock.patch.object(NailgunBuildImage, '__init__')
def test_parse_operating_system_packages_given(self, mock_init, mock_ub):
mock_init.return_value = None
@mock.patch.object(NailgunBuildImage, 'parse_schemes')
def test_parse_operating_system_packages_given(self, mock_parse_schemes,
mock_ub):
data = {
'repos': [],
'codename': 'trusty',
'packages': ['pack']
}
driver = NailgunBuildImage()
driver.data = data
mock_ub_instance = mock_ub.return_value
mock_ub_instance.packages = data['packages']
driver.parse_operating_system()
driver = NailgunBuildImage(data)
mock_ub.assert_called_once_with(repos=[], packages=data['packages'])
self.assertEqual(driver.operating_system.packages, data['packages'])
@mock.patch('fuel_agent.objects.Ubuntu')
@mock.patch.object(NailgunBuildImage, '__init__')
@mock.patch.object(NailgunBuildImage, 'parse_schemes')
def test_parse_operating_system_packages_not_given(
self, mock_init, mock_ub):
mock_init.return_value = None
self, mock_parse_schemes, mock_ub):
data = {
'repos': [],
'codename': 'trusty'
}
driver = NailgunBuildImage()
driver.data = data
mock_ub_instance = mock_ub.return_value
mock_ub_instance.packages = NailgunBuildImage.DEFAULT_TRUSTY_PACKAGES
driver.parse_operating_system()
driver = NailgunBuildImage(data)
mock_ub.assert_called_once_with(
repos=[], packages=NailgunBuildImage.DEFAULT_TRUSTY_PACKAGES)
self.assertEqual(driver.operating_system.packages,
@ -152,15 +146,13 @@ class TestNailgunBuildImage(test_base.BaseTestCase):
@mock.patch('fuel_agent.objects.DEBRepo')
@mock.patch('fuel_agent.objects.Ubuntu')
@mock.patch.object(NailgunBuildImage, '__init__')
def test_parse_operating_system_repos(self, mock_init, mock_ub, mock_deb):
mock_init.return_value = None
@mock.patch.object(NailgunBuildImage, 'parse_schemes')
def test_parse_operating_system_repos(self, mock_parse_schemes, mock_ub,
mock_deb):
data = {
'repos': REPOS_SAMPLE,
'codename': 'trusty'
}
driver = NailgunBuildImage()
driver.data = data
mock_deb_expected_calls = []
repos = []
@ -174,7 +166,7 @@ class TestNailgunBuildImage(test_base.BaseTestCase):
}
mock_deb_expected_calls.append(mock.call(**kwargs))
repos.append(objects.DEBRepo(**kwargs))
driver.parse_operating_system()
driver = NailgunBuildImage(data)
mock_ub_instance = mock_ub.return_value
mock_ub_instance.repos = repos
mock_ub.assert_called_once_with(
@ -185,21 +177,18 @@ class TestNailgunBuildImage(test_base.BaseTestCase):
@mock.patch('fuel_agent.drivers.nailgun.objects.Loop')
@mock.patch('fuel_agent.objects.Image')
@mock.patch('fuel_agent.objects.Fs')
@mock.patch('fuel_agent.objects.FS')
@mock.patch('fuel_agent.objects.PartitionScheme')
@mock.patch('fuel_agent.objects.ImageScheme')
@mock.patch.object(NailgunBuildImage, '__init__')
@mock.patch.object(NailgunBuildImage, 'parse_operating_system')
def test_parse_schemes(
self, mock_init, mock_imgsch, mock_partsch,
self, mock_parse_os, mock_imgsch, mock_partsch,
mock_fs, mock_img, mock_loop):
mock_init.return_value = None
data = {
'image_data': IMAGE_DATA_SAMPLE,
'output': '/some/local/path',
}
driver = NailgunBuildImage()
driver.data = data
driver.parse_schemes()
driver = NailgunBuildImage(data)
mock_fs_expected_calls = []
mock_img_expected_calls = []
@ -223,7 +212,7 @@ class TestNailgunBuildImage(test_base.BaseTestCase):
'fs_type': image['format']
}
mock_fs_expected_calls.append(mock.call(**fs_kwargs))
fss.append(objects.Fs(**fs_kwargs))
fss.append(objects.FS(**fs_kwargs))
if mount == '/':
metadata_filename = filename.split('.', 1)[0] + '.yaml'

View File

@ -14,16 +14,17 @@
import mock
from oslotest import base as test_base
import unittest2
from fuel_agent import errors
from fuel_agent.objects import partition
class TestMD(test_base.BaseTestCase):
class TestMultipleDevice(unittest2.TestCase):
def setUp(self):
super(TestMD, self).setUp()
self.md = partition.Md('name', 'level')
super(self.__class__, self).setUp()
self.md = partition.MD(name='name', level='level')
def test_add_device_ok(self):
self.assertEqual(0, len(self.md.devices))
@ -59,8 +60,22 @@ class TestMD(test_base.BaseTestCase):
self.assertRaises(errors.MDDeviceDuplicationError, self.md.add_spare,
'device')
def test_conversion(self):
self.md.add_device('device_a')
self.md.add_spare('device_b')
serialized = self.md.to_dict()
assert serialized == {
'name': 'name',
'level': 'level',
'devices': ['device_a', ],
'spares': ['device_b', ],
}
new_md = partition.MD.from_dict(serialized)
assert serialized == new_md.to_dict()
class TestPartition(unittest2.TestCase):
class TestPartition(test_base.BaseTestCase):
def setUp(self):
super(TestPartition, self).setUp()
self.pt = partition.Partition('name', 'count', 'device', 'begin',
@ -72,8 +87,27 @@ class TestPartition(test_base.BaseTestCase):
self.assertEqual(1, len(self.pt.flags))
self.assertIn('fake_flag', self.pt.flags)
def test_conversion(self):
self.pt.flags.append('some_flag')
self.pt.guid = 'some_guid'
serialized = self.pt.to_dict()
assert serialized == {
'begin': 'begin',
'configdrive': False,
'count': 'count',
'device': 'device',
'end': 'end',
'flags': ['some_flag', ],
'guid': 'some_guid',
'name': 'name',
'partition_type': 'partition_type',
}
new_pt = partition.Partition.from_dict(serialized)
assert serialized == new_pt.to_dict()
class TestPartitionScheme(unittest2.TestCase):
class TestPartitionScheme(test_base.BaseTestCase):
def setUp(self):
super(TestPartitionScheme, self).setUp()
self.p_scheme = partition.PartitionScheme()
@ -83,30 +117,30 @@ class TestPartitionScheme(test_base.BaseTestCase):
self.p_scheme.root_device)
def test_fs_by_device(self):
expected_fs = partition.Fs('device')
expected_fs = partition.FS('device')
self.p_scheme.fss.append(expected_fs)
self.p_scheme.fss.append(partition.Fs('wrong_device'))
self.p_scheme.fss.append(partition.FS('wrong_device'))
actual_fs = self.p_scheme.fs_by_device('device')
self.assertEqual(expected_fs, actual_fs)
def test_fs_by_mount(self):
expected_fs = partition.Fs('d', mount='mount')
expected_fs = partition.FS('d', mount='mount')
self.p_scheme.fss.append(expected_fs)
self.p_scheme.fss.append(partition.Fs('w_d', mount='wrong_mount'))
self.p_scheme.fss.append(partition.FS('w_d', mount='wrong_mount'))
actual_fs = self.p_scheme.fs_by_mount('mount')
self.assertEqual(expected_fs, actual_fs)
def test_pv_by_name(self):
expected_pv = partition.Pv('pv')
expected_pv = partition.PV('pv')
self.p_scheme.pvs.append(expected_pv)
self.p_scheme.pvs.append(partition.Pv('wrong_pv'))
self.p_scheme.pvs.append(partition.PV('wrong_pv'))
actual_pv = self.p_scheme.pv_by_name('pv')
self.assertEqual(expected_pv, actual_pv)
def test_vg_by_name(self):
expected_vg = partition.Vg('vg')
expected_vg = partition.VG('vg')
self.p_scheme.vgs.append(expected_vg)
self.p_scheme.vgs.append(partition.Vg('wrong_vg'))
self.p_scheme.vgs.append(partition.VG('wrong_vg'))
actual_vg = self.p_scheme.vg_by_name('vg')
self.assertEqual(expected_vg, actual_vg)
@ -123,33 +157,33 @@ class TestPartitionScheme(test_base.BaseTestCase):
def test_md_next_name_fail(self):
self.p_scheme.mds = [
partition.Md('/dev/md%s' % x, 'level') for x in range(0, 128)]
partition.MD('/dev/md%s' % x, 'level') for x in range(0, 128)]
self.assertRaises(errors.MDAlreadyExistsError,
self.p_scheme.md_next_name)
def test_md_by_name(self):
self.assertEqual(0, len(self.p_scheme.mds))
expected_md = partition.Md('name', 'level')
expected_md = partition.MD('name', 'level')
self.p_scheme.mds.append(expected_md)
self.p_scheme.mds.append(partition.Md('wrong_name', 'level'))
self.p_scheme.mds.append(partition.MD('wrong_name', 'level'))
self.assertEqual(expected_md, self.p_scheme.md_by_name('name'))
def test_md_by_mount(self):
self.assertEqual(0, len(self.p_scheme.mds))
self.assertEqual(0, len(self.p_scheme.fss))
expected_md = partition.Md('name', 'level')
expected_fs = partition.Fs('name', mount='mount')
expected_md = partition.MD('name', 'level')
expected_fs = partition.FS('name', mount='mount')
self.p_scheme.mds.append(expected_md)
self.p_scheme.fss.append(expected_fs)
self.p_scheme.fss.append(partition.Fs('wrong_name',
self.p_scheme.fss.append(partition.FS('wrong_name',
mount='wrong_mount'))
self.assertEqual(expected_md, self.p_scheme.md_by_mount('mount'))
def test_md_attach_by_mount_md_exists(self):
self.assertEqual(0, len(self.p_scheme.mds))
self.assertEqual(0, len(self.p_scheme.fss))
expected_md = partition.Md('name', 'level')
expected_fs = partition.Fs('name', mount='mount')
expected_md = partition.MD('name', 'level')
expected_fs = partition.FS('name', mount='mount')
self.p_scheme.mds.append(expected_md)
self.p_scheme.fss.append(expected_fs)
actual_md = self.p_scheme.md_attach_by_mount('device', 'mount')
@ -171,7 +205,7 @@ class TestPartitionScheme(test_base.BaseTestCase):
self.assertEqual('-F', self.p_scheme.fss[0].options)
class TestParted(test_base.BaseTestCase):
class TestParted(unittest2.TestCase):
def setUp(self):
super(TestParted, self).setUp()
self.prtd = partition.Parted('name', 'label')
@ -254,3 +288,99 @@ class TestParted(test_base.BaseTestCase):
'begin', 'end', 'primary')]
self.prtd.partitions.extend(expected_partitions)
self.assertEqual(expected_partitions, self.prtd.primary)
def test_conversion(self):
prt = partition.Partition(
name='name',
count='count',
device='device',
begin='begin',
end='end',
partition_type='primary'
)
self.prtd.partitions.append(prt)
serialized = self.prtd.to_dict()
assert serialized == {
'label': 'label',
'name': 'name',
'partitions': [
prt.to_dict(),
],
'install_bootloader': False,
}
new_prtd = partition.Parted.from_dict(serialized)
assert serialized == new_prtd.to_dict()
class TestLogicalVolume(unittest2.TestCase):
def test_conversion(self):
lv = partition.LV(
name='lv-name',
vgname='vg-name',
size=1234
)
serialized = lv.to_dict()
assert serialized == {
'name': 'lv-name',
'vgname': 'vg-name',
'size': 1234,
}
new_lv = partition.LV.from_dict(serialized)
assert serialized == new_lv.to_dict()
class TestPhisicalVolume(unittest2.TestCase):
def test_conversion(self):
pv = partition.PV(
name='pv-name',
metadatasize=987,
metadatacopies=112,
)
serialized = pv.to_dict()
assert serialized == {
'name': 'pv-name',
'metadatasize': 987,
'metadatacopies': 112,
}
new_pv = partition.PV.from_dict(serialized)
assert serialized == new_pv.to_dict()
class TestVolumesGroup(unittest2.TestCase):
def test_conversion(self):
vg = partition.VG(
name='vg-name',
pvnames=['pv-name-a', ]
)
serialized = vg.to_dict()
assert serialized == {
'name': 'vg-name',
'pvnames': ['pv-name-a', ]
}
new_vg = partition.VG.from_dict(serialized)
assert serialized == new_vg.to_dict()
class TestFileSystem(unittest2.TestCase):
def test_conversion(self):
fs = partition.FS(
device='some-device',
mount='/mount',
fs_type='type',
fs_options='some-option',
fs_label='some-label',
)
serialized = fs.to_dict()
assert serialized == {
'device': 'some-device',
'mount': '/mount',
'fs_type': 'type',
'fs_options': 'some-option',
'fs_label': 'some-label',
}
new_fs = partition.FS.from_dict(serialized)
assert serialized == new_fs.to_dict()

View File

@ -0,0 +1,224 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, 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 mock
import requests_mock
import unittest2
from fuel_agent.drivers import simple
from fuel_agent import objects
from fuel_agent.tests import base
@mock.patch.multiple(
simple.NailgunSimpleDriver,
parse_grub=lambda x: objects.Grub(),
parse_configdrive_scheme=lambda x: objects.ConfigDriveScheme(),
parse_image_scheme=lambda x: objects.ImageScheme())
class TestObjectDeserialization(unittest2.TestCase):
def test_driver_always_has_correct_objects(self):
driver = simple.NailgunSimpleDriver({})
assert isinstance(driver.partition_scheme, objects.PartitionScheme)
def test_lv_data_is_loaded(self):
lv_data = {
'partitioning': {
'lvs': [
{
'name': 'lv-name',
'size': 12345,
'vgname': 'vg-name',
},
]
}
}
driver = simple.NailgunSimpleDriver(lv_data)
lv = driver.partition_scheme.lvs[0]
assert len(driver.partition_scheme.lvs) == 1
assert isinstance(lv, objects.LV)
assert lv.name == 'lv-name'
assert lv.size == 12345
assert lv.vgname == 'vg-name'
def test_pv_data_is_loaded(self):
pv_data = {
'partitioning': {
'pvs': [
{
'metadatacopies': 2,
'metadatasize': 28,
'name': '/dev/sda5'
},
]
}
}
driver = simple.NailgunSimpleDriver(pv_data)
pv = driver.partition_scheme.pvs[0]
assert len(driver.partition_scheme.pvs) == 1
assert isinstance(pv, objects.PV)
assert pv.name == '/dev/sda5'
assert pv.metadatacopies == 2
assert pv.metadatasize == 28
def test_vg_data_is_loaded(self):
vg_data = {
'partitioning': {
'vgs': [
{
'name': 'image',
'pvnames': [
'/dev/sda6',
'/dev/sdb3',
'/dev/sdc3',
]
},
]
}
}
driver = simple.NailgunSimpleDriver(vg_data)
vg = driver.partition_scheme.vgs[0]
assert len(driver.partition_scheme.vgs) == 1
assert isinstance(vg, objects.VG)
assert vg.name == 'image'
self.assertItemsEqual(
vg.pvnames,
(
'/dev/sda6',
'/dev/sdb3',
'/dev/sdc3',
)
)
def test_fs_data_is_loaded(self):
fs_data = {
'partitioning': {
'fss': [
{
'device': '/dev/sda3',
'fs_label': 'some-label',
'fs_options': 'some-options',
'fs_type': 'ext2',
'mount': '/boot'
},
]
}
}
driver = simple.NailgunSimpleDriver(fs_data)
fs = driver.partition_scheme.fss[0]
assert len(driver.partition_scheme.fss) == 1
assert isinstance(fs, objects.FS)
assert fs.device == '/dev/sda3'
assert fs.label == 'some-label'
assert fs.options == 'some-options'
assert fs.type == 'ext2'
assert fs.mount == '/boot'
def test_parted_data_is_loaded(self):
parted_data = {
'partitioning': {
'parteds': [
{
'label': 'gpt',
'name': '/dev/sdb',
'partitions': [
{
'begin': 1,
'configdrive': False,
'count': 1,
'device': '/dev/sdb',
'end': 25,
'flags': [
'bios_grub',
'xyz',
],
'guid': None,
'name': '/dev/sdb1',
'partition_type': 'primary'
},
]
},
]
}
}
driver = simple.NailgunSimpleDriver(parted_data)
parted = driver.partition_scheme.parteds[0]
partition = parted.partitions[0]
assert len(driver.partition_scheme.parteds) == 1
assert isinstance(parted, objects.Parted)
assert parted.label == 'gpt'
assert parted.name == '/dev/sdb'
assert len(parted.partitions) == 1
assert partition.begin == 1
assert partition.configdrive is False
assert partition.count == 1
assert partition.device == '/dev/sdb'
assert partition.end == 25
self.assertItemsEqual(partition.flags, ['bios_grub', 'xyz'])
assert partition.guid is None
assert partition.name == '/dev/sdb1'
assert partition.type == 'primary'
def test_md_data_is_loaded(self):
md_data = {
'partitioning': {
'mds': [
{
'name': 'some-raid',
'level': 1,
'devices': [
'/dev/sda',
'/dev/sdc',
],
'spares': [
'/dev/sdb',
'/dev/sdd',
]
},
]
}
}
driver = simple.NailgunSimpleDriver(md_data)
md = driver.partition_scheme.mds[0]
assert len(driver.partition_scheme.mds) == 1
assert isinstance(md, objects.MD)
assert md.name == 'some-raid'
assert md.level == 1
self.assertItemsEqual(md.devices, ['/dev/sda', '/dev/sdc'])
self.assertItemsEqual(md.spares, ['/dev/sdb', '/dev/sdd'])
@requests_mock.mock()
class TestFullDataRead(unittest2.TestCase):
PROVISION_DATA = base.load_fixture('simple_nailgun_driver.json')
def test_read_with_no_error(self, mock_requests):
mock_requests.get('http://fake.host.org:123/imgs/fake_image.img.gz',
text='{}')
driver = simple.NailgunSimpleDriver(self.PROVISION_DATA)
scheme = driver.partition_scheme
assert len(scheme.fss) == 5
assert len(scheme.lvs) == 3
assert len(scheme.mds) == 0
assert len(scheme.parteds) == 2
assert len(scheme.pvs) == 4
assert len(scheme.vgs) == 2

View File

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, 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.
class abstractclassmethod(classmethod):
"""A decorator indicating abstract classmethods.
Similar to abstractmethod.
Usage:
class C(object):
__metaclass__ = abc.ABCMeta
@abstractclassmethod
def my_abstract_classmethod(cls, ...):
...
Copied from Python 3.2
"""
__isabstractmethod__ = True
def __init__(self, callable):
callable.__isabstractmethod__ = True
super(abstractclassmethod, self).__init__(callable)

View File

@ -24,6 +24,7 @@ console_scripts =
fuel_agent.drivers =
nailgun = fuel_agent.drivers.nailgun:Nailgun
nailgun_simple = fuel_agent.drivers.simple:NailgunSimpleDriver
nailgun_build_image = fuel_agent.drivers.nailgun:NailgunBuildImage
[pbr]

View File

@ -1,6 +1,9 @@
hacking>=0.8.0,<0.9
mock==1.0.1
# TODO(prmtl): remove oslotest and (probably) testools in favor of unittest2
oslotest==1.0
testtools>=0.9.34
unittest2==1.0.1
pytest>=2.7.2
pytest-cov>=1.8.1
requests-mock>=0.6