diff --git a/fuel_agent/drivers/base.py b/fuel_agent/drivers/base.py index 77003f9..7fb7f10 100644 --- a/fuel_agent/drivers/base.py +++ b/fuel_agent/drivers/base.py @@ -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""" diff --git a/fuel_agent/drivers/nailgun.py b/fuel_agent/drivers/nailgun.py index 63befa3..155e9f5 100644 --- a/fuel_agent/drivers/nailgun.py +++ b/fuel_agent/drivers/nailgun.py @@ -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']) diff --git a/fuel_agent/drivers/simple.py b/fuel_agent/drivers/simple.py new file mode 100644 index 0000000..6c617bc --- /dev/null +++ b/fuel_agent/drivers/simple.py @@ -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 diff --git a/fuel_agent/objects/__init__.py b/fuel_agent/objects/__init__.py index fd7812b..213f402 100644 --- a/fuel_agent/objects/__init__.py +++ b/fuel_agent/objects/__init__.py @@ -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', diff --git a/fuel_agent/objects/base.py b/fuel_agent/objects/base.py new file mode 100644 index 0000000..ff78d46 --- /dev/null +++ b/fuel_agent/objects/base.py @@ -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 diff --git a/fuel_agent/objects/partition.py b/fuel_agent/objects/partition.py index c4638b1..acd1467 100644 --- a/fuel_agent/objects/partition.py +++ b/fuel_agent/objects/partition.py @@ -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], + } diff --git a/fuel_agent/tests/base.py b/fuel_agent/tests/base.py new file mode 100644 index 0000000..c6bd273 --- /dev/null +++ b/fuel_agent/tests/base.py @@ -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) diff --git a/fuel_agent/tests/fixtures/simple_nailgun_driver.json b/fuel_agent/tests/fixtures/simple_nailgun_driver.json new file mode 100644 index 0000000..c3aeb63 --- /dev/null +++ b/fuel_agent/tests/fixtures/simple_nailgun_driver.json @@ -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" + ] + } + ] + } +} diff --git a/fuel_agent/tests/test_manager.py b/fuel_agent/tests/test_manager.py index 62d26aa..16d3d0b 100644 --- a/fuel_agent/tests/test_manager.py +++ b/fuel_agent/tests/test_manager.py @@ -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), diff --git a/fuel_agent/tests/test_nailgun_build_image.py b/fuel_agent/tests/test_nailgun_build_image.py index 635cbd4..1dfbb09 100644 --- a/fuel_agent/tests/test_nailgun_build_image.py +++ b/fuel_agent/tests/test_nailgun_build_image.py @@ -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' diff --git a/fuel_agent/tests/test_partition.py b/fuel_agent/tests/test_partition.py index 46a8403..72d9d3a 100644 --- a/fuel_agent/tests/test_partition.py +++ b/fuel_agent/tests/test_partition.py @@ -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() diff --git a/fuel_agent/tests/test_simple_nailgun_driver.py b/fuel_agent/tests/test_simple_nailgun_driver.py new file mode 100644 index 0000000..94e1838 --- /dev/null +++ b/fuel_agent/tests/test_simple_nailgun_driver.py @@ -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 diff --git a/fuel_agent/utils/decorators.py b/fuel_agent/utils/decorators.py new file mode 100644 index 0000000..591c539 --- /dev/null +++ b/fuel_agent/utils/decorators.py @@ -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) diff --git a/setup.cfg b/setup.cfg index e05bba6..f2c4338 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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] diff --git a/test-requirements.txt b/test-requirements.txt index dc03284..2659664 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -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