IBP: Added driver and objects for building images
As far as building of OS images is nothing more than just a stage of the whole OS installing procedure it is sounds rational to implement this in terms of fuel-agent. Besides, we already have plenty of utilities which could be useful during building of images. And some tasks are the same like pre-configuring some files inside target OS. Related-bug: #1433193 Implements: blueprint ibp-build-ubuntu-images Change-Id: I3fadfb16e06e4ee16926da29b7b83ca005500698
This commit is contained in:
parent
90e4e7ebe2
commit
9c5e3579bf
@ -17,13 +17,34 @@
|
|||||||
# value)
|
# value)
|
||||||
#config_drive_path=/tmp/config-drive.img
|
#config_drive_path=/tmp/config-drive.img
|
||||||
|
|
||||||
|
# Path where to store actual rules for udev daemon (string
|
||||||
|
# value)
|
||||||
|
#udev_rules_dir=/etc/udev/rules.d
|
||||||
|
|
||||||
|
# Path where to store default rules for udev daemon (string
|
||||||
|
# value)
|
||||||
|
#udev_rules_lib_dir=/lib/udev/rules.d
|
||||||
|
|
||||||
|
# Substring to which file extension .rules be renamed (string
|
||||||
|
# value)
|
||||||
|
#udev_rename_substr=.renamedrule
|
||||||
|
|
||||||
|
# Directory where we build images (string value)
|
||||||
|
#image_build_dir=/tmp
|
||||||
|
|
||||||
|
# Directory where we build images (string value)
|
||||||
|
#image_build_suffix=.fuel-agent-image
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Options defined in fuel_agent.cmd.agent
|
# Options defined in fuel_agent.cmd.agent
|
||||||
#
|
#
|
||||||
|
|
||||||
# Provision data file (string value)
|
# Input data file (string value)
|
||||||
#provision_data_file=/tmp/provision.json
|
#input_data_file=/tmp/provision.json
|
||||||
|
|
||||||
|
# Input data (json string) (string value)
|
||||||
|
#input_data=
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -99,6 +120,7 @@ logging_debug_format_suffix=
|
|||||||
# Deprecated group/name - [DEFAULT]/logfile
|
# Deprecated group/name - [DEFAULT]/logfile
|
||||||
log_file=/var/log/fuel-agent.log
|
log_file=/var/log/fuel-agent.log
|
||||||
|
|
||||||
|
|
||||||
# (Optional) The base directory used for relative --log-file
|
# (Optional) The base directory used for relative --log-file
|
||||||
# paths. (string value)
|
# paths. (string value)
|
||||||
# Deprecated group/name - [DEFAULT]/logdir
|
# Deprecated group/name - [DEFAULT]/logdir
|
||||||
@ -120,3 +142,43 @@ log_file=/var/log/fuel-agent.log
|
|||||||
#syslog_log_facility=LOG_USER
|
#syslog_log_facility=LOG_USER
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Options defined in fuel_agent.utils.artifact_utils
|
||||||
|
#
|
||||||
|
|
||||||
|
# Size of data chunk to operate with images (integer value)
|
||||||
|
#data_chunk_size=1048576
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Options defined in fuel_agent.utils.build_utils
|
||||||
|
#
|
||||||
|
|
||||||
|
# Maximum allowed loop devices count to use (integer value)
|
||||||
|
#max_loop_count=255
|
||||||
|
|
||||||
|
# Size of sparse file in MiBs (integer value)
|
||||||
|
#sparse_file_size=2048
|
||||||
|
|
||||||
|
# System-wide major number for loop device (integer value)
|
||||||
|
#loop_dev_major=7
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Options defined in fuel_agent.utils.utils
|
||||||
|
#
|
||||||
|
|
||||||
|
# Maximum retries count for http requests. 0 means infinite
|
||||||
|
# (integer value)
|
||||||
|
#http_max_retries=30
|
||||||
|
|
||||||
|
# Http request timeout in seconds (floating point value)
|
||||||
|
#http_request_timeout=10.0
|
||||||
|
|
||||||
|
# Delay in seconds before the next http request retry
|
||||||
|
# (floating point value)
|
||||||
|
#http_retry_delay=2.0
|
||||||
|
|
||||||
|
# Block size of data to read for calculating checksum (integer
|
||||||
|
# value)
|
||||||
|
#read_chunk_size=1048576
|
||||||
|
@ -12,30 +12,36 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import json
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from oslo.config import cfg
|
from oslo.config import cfg
|
||||||
|
from oslo_serialization import jsonutils as json
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from fuel_agent import manager as manager
|
from fuel_agent import manager as manager
|
||||||
from fuel_agent.openstack.common import log
|
from fuel_agent.openstack.common import log as logging
|
||||||
from fuel_agent import version
|
from fuel_agent import version
|
||||||
|
|
||||||
opts = [
|
cli_opts = [
|
||||||
cfg.StrOpt(
|
cfg.StrOpt(
|
||||||
'provision_data_file',
|
'input_data_file',
|
||||||
default='/tmp/provision.json',
|
default='/tmp/provision.json',
|
||||||
help='Provision data file'
|
help='Input data file'
|
||||||
|
),
|
||||||
|
cfg.StrOpt(
|
||||||
|
'input_data',
|
||||||
|
default='',
|
||||||
|
help='Input data (json string)'
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
CONF.register_opts(opts)
|
CONF.register_cli_opts(cli_opts)
|
||||||
CONF(sys.argv[1:], project='fuel-agent',
|
CONF(sys.argv[1:], project='fuel-agent',
|
||||||
version=version.version_info.release_string())
|
version=version.version_info.release_string())
|
||||||
log.setup('fuel-agent')
|
|
||||||
LOG = log.getLogger(__name__)
|
logging.setup('fuel-agent')
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def provision():
|
def provision():
|
||||||
@ -58,6 +64,10 @@ def bootloader():
|
|||||||
main(['do_bootloader'])
|
main(['do_bootloader'])
|
||||||
|
|
||||||
|
|
||||||
|
def build_image():
|
||||||
|
main(['do_build_image'])
|
||||||
|
|
||||||
|
|
||||||
def print_err(line):
|
def print_err(line):
|
||||||
sys.stderr.write(six.text_type(line))
|
sys.stderr.write(six.text_type(line))
|
||||||
sys.stderr.write('\n')
|
sys.stderr.write('\n')
|
||||||
@ -72,8 +82,12 @@ def handle_exception(exc):
|
|||||||
|
|
||||||
def main(actions=None):
|
def main(actions=None):
|
||||||
try:
|
try:
|
||||||
with open(CONF.provision_data_file) as f:
|
if CONF.input_data:
|
||||||
|
data = json.loads(CONF.input_data)
|
||||||
|
else:
|
||||||
|
with open(CONF.input_data_file) as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
LOG.debug('Input data: %s', data)
|
||||||
|
|
||||||
mgr = manager.Manager(data)
|
mgr = manager.Manager(data)
|
||||||
if actions:
|
if actions:
|
||||||
@ -82,5 +96,6 @@ def main(actions=None):
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
handle_exception(exc)
|
handle_exception(exc)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
27
fuel_agent/drivers/base.py
Normal file
27
fuel_agent/drivers/base.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class BaseDataDriver(object):
|
||||||
|
"""Data driver API is to be put here.
|
||||||
|
For example, data validation methods,
|
||||||
|
methods for getting object schemes, etc.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, data):
|
||||||
|
self.data = data
|
@ -12,10 +12,17 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import itertools
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
import six
|
import six
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from six.moves.urllib.parse import urljoin
|
||||||
|
from six.moves.urllib.parse import urlparse
|
||||||
|
from six.moves.urllib.parse import urlsplit
|
||||||
|
|
||||||
|
from fuel_agent.drivers.base import BaseDataDriver
|
||||||
from fuel_agent.drivers import ks_spaces_validator
|
from fuel_agent.drivers import ks_spaces_validator
|
||||||
from fuel_agent import errors
|
from fuel_agent import errors
|
||||||
from fuel_agent import objects
|
from fuel_agent import objects
|
||||||
@ -23,9 +30,6 @@ from fuel_agent.openstack.common import log as logging
|
|||||||
from fuel_agent.utils import hardware_utils as hu
|
from fuel_agent.utils import hardware_utils as hu
|
||||||
from fuel_agent.utils import utils
|
from fuel_agent.utils import utils
|
||||||
|
|
||||||
from six.moves.urllib.parse import urljoin
|
|
||||||
from six.moves.urllib.parse import urlparse
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -60,11 +64,9 @@ def match_device(hu_disk, ks_disk):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class Nailgun(object):
|
class Nailgun(BaseDataDriver):
|
||||||
def __init__(self, data):
|
def __init__(self, data):
|
||||||
# Here data is expected to be raw provisioning data
|
super(Nailgun, self).__init__(data)
|
||||||
# how it is given by nailgun
|
|
||||||
self.data = data
|
|
||||||
|
|
||||||
# this var is used as a flag that /boot fs
|
# this var is used as a flag that /boot fs
|
||||||
# has already been added. we need this to
|
# has already been added. we need this to
|
||||||
@ -394,16 +396,13 @@ class Nailgun(object):
|
|||||||
filename = os.path.basename(urlparse(root_uri).path).split('.')[0] + \
|
filename = os.path.basename(urlparse(root_uri).path).split('.')[0] + \
|
||||||
'.yaml'
|
'.yaml'
|
||||||
metadata_url = urljoin(root_uri, filename)
|
metadata_url = urljoin(root_uri, filename)
|
||||||
image_meta = {}
|
|
||||||
raw_image_meta = None
|
|
||||||
try:
|
try:
|
||||||
raw_image_meta = yaml.load(
|
image_meta = yaml.load(
|
||||||
utils.init_http_request(metadata_url).text)
|
utils.init_http_request(metadata_url).text)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.exception(e)
|
LOG.exception(e)
|
||||||
LOG.debug('Failed to fetch/decode image meta data')
|
LOG.debug('Failed to fetch/decode image meta data')
|
||||||
if raw_image_meta:
|
image_meta = {}
|
||||||
[image_meta.update(img_info) for img_info in raw_image_meta]
|
|
||||||
# We assume for every file system user may provide a separate
|
# We assume for every file system user may provide a separate
|
||||||
# file system image. For example if partitioning scheme has
|
# file system image. For example if partitioning scheme has
|
||||||
# /, /boot, /var/lib file systems then we will try to get images
|
# /, /boot, /var/lib file systems then we will try to get images
|
||||||
@ -415,13 +414,120 @@ class Nailgun(object):
|
|||||||
LOG.debug('Adding image for fs %s: uri=%s format=%s container=%s' %
|
LOG.debug('Adding image for fs %s: uri=%s format=%s container=%s' %
|
||||||
(mount_point, image_data['uri'],
|
(mount_point, image_data['uri'],
|
||||||
image_data['format'], image_data['container']))
|
image_data['format'], image_data['container']))
|
||||||
|
iname = os.path.basename(urlparse(image_data['uri']).path)
|
||||||
|
imeta = next(itertools.chain(
|
||||||
|
(img for img in image_meta.get('images', [])
|
||||||
|
if img['container_name'] == iname), [{}]))
|
||||||
image_scheme.add_image(
|
image_scheme.add_image(
|
||||||
uri=image_data['uri'],
|
uri=image_data['uri'],
|
||||||
target_device=self.partition_scheme.fs_by_mount(
|
target_device=self.partition_scheme.fs_by_mount(
|
||||||
mount_point).device,
|
mount_point).device,
|
||||||
format=image_data['format'],
|
format=image_data['format'],
|
||||||
container=image_data['container'],
|
container=image_data['container'],
|
||||||
size=image_meta.get(mount_point, {}).get('size'),
|
size=imeta.get('raw_size'),
|
||||||
md5=image_meta.get(mount_point, {}).get('md5'),
|
md5=imeta.get('raw_md5'),
|
||||||
)
|
)
|
||||||
return image_scheme
|
return image_scheme
|
||||||
|
|
||||||
|
|
||||||
|
class NailgunBuildImage(BaseDataDriver):
|
||||||
|
|
||||||
|
# TODO(kozhukalov):
|
||||||
|
# This list of packages is used by default only if another
|
||||||
|
# list isn't given in build image data. In the future
|
||||||
|
# we need to handle package list in nailgun. Even more,
|
||||||
|
# in the future, we'll be building not only ubuntu images
|
||||||
|
# and we'll likely move this list into some kind of config.
|
||||||
|
DEFAULT_TRUSTY_PACKAGES = [
|
||||||
|
"acl",
|
||||||
|
"anacron",
|
||||||
|
"bash-completion",
|
||||||
|
"bridge-utils",
|
||||||
|
"bsdmainutils",
|
||||||
|
"build-essential",
|
||||||
|
"cloud-init",
|
||||||
|
"curl",
|
||||||
|
"daemonize",
|
||||||
|
"debconf-utils",
|
||||||
|
"gdisk",
|
||||||
|
"grub-pc",
|
||||||
|
"linux-firmware",
|
||||||
|
"linux-firmware-nonfree",
|
||||||
|
"linux-headers-generic-lts-trusty",
|
||||||
|
"linux-image-generic-lts-trusty",
|
||||||
|
"lvm2",
|
||||||
|
"mcollective",
|
||||||
|
"mdadm",
|
||||||
|
"nailgun-agent",
|
||||||
|
"nailgun-mcagents",
|
||||||
|
"nailgun-net-check",
|
||||||
|
"ntp",
|
||||||
|
"openssh-client",
|
||||||
|
"openssh-server",
|
||||||
|
"puppet",
|
||||||
|
"python-amqp",
|
||||||
|
"ruby-augeas",
|
||||||
|
"ruby-ipaddress",
|
||||||
|
"ruby-json",
|
||||||
|
"ruby-netaddr",
|
||||||
|
"ruby-openstack",
|
||||||
|
"ruby-shadow",
|
||||||
|
"ruby-stomp",
|
||||||
|
"telnet",
|
||||||
|
"ubuntu-minimal",
|
||||||
|
"ubuntu-standard",
|
||||||
|
"uuid-runtime",
|
||||||
|
"vim",
|
||||||
|
"virt-what",
|
||||||
|
"vlan",
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, data):
|
||||||
|
super(NailgunBuildImage, self).__init__(data)
|
||||||
|
self.parse_schemes()
|
||||||
|
self.parse_operating_system()
|
||||||
|
|
||||||
|
def parse_operating_system(self):
|
||||||
|
if self.data.get('codename').lower() != 'trusty':
|
||||||
|
raise errors.WrongInputDataError(
|
||||||
|
'Currently, only Ubuntu Trusty is supported, given '
|
||||||
|
'codename is {0}'.format(self.data.get('codename')))
|
||||||
|
|
||||||
|
packages = self.data.get('packages', self.DEFAULT_TRUSTY_PACKAGES)
|
||||||
|
|
||||||
|
repos = []
|
||||||
|
for repo in self.data['repos']:
|
||||||
|
repos.append(objects.DEBRepo(
|
||||||
|
name=repo['name'],
|
||||||
|
uri=repo['uri'],
|
||||||
|
suite=repo['suite'],
|
||||||
|
section=repo['section'],
|
||||||
|
priority=repo['priority']))
|
||||||
|
|
||||||
|
self.operating_system = 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)
|
||||||
|
# Loop does not allocate any loop device
|
||||||
|
# during initialization.
|
||||||
|
device = objects.Loop()
|
||||||
|
|
||||||
|
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(
|
||||||
|
device=device,
|
||||||
|
mount=mount,
|
||||||
|
fs_type=image['format'])
|
||||||
|
|
||||||
|
if mount == '/':
|
||||||
|
metadata_filename = filename.split('.', 1)[0] + '.yaml'
|
||||||
|
self.metadata_uri = 'file://' + os.path.join(
|
||||||
|
self.data['output'], metadata_filename)
|
||||||
|
@ -19,6 +19,10 @@ class BaseError(Exception):
|
|||||||
super(BaseError, self).__init__(message, *args, **kwargs)
|
super(BaseError, self).__init__(message, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class WrongInputDataError(BaseError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class WrongPartitionSchemeError(BaseError):
|
class WrongPartitionSchemeError(BaseError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -146,3 +150,11 @@ class ImageChecksumMismatchError(BaseError):
|
|||||||
|
|
||||||
class NoFreeLoopDevices(BaseError):
|
class NoFreeLoopDevices(BaseError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WrongRepositoryError(BaseError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WrongDeviceError(BaseError):
|
||||||
|
pass
|
||||||
|
@ -13,12 +13,18 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
|
import signal
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
import yaml
|
||||||
|
|
||||||
from oslo.config import cfg
|
from oslo.config import cfg
|
||||||
|
|
||||||
from fuel_agent import errors
|
from fuel_agent import errors
|
||||||
from fuel_agent.openstack.common import log as logging
|
from fuel_agent.openstack.common import log as logging
|
||||||
from fuel_agent.utils import artifact_utils as au
|
from fuel_agent.utils import artifact_utils as au
|
||||||
|
from fuel_agent.utils import build_utils as bu
|
||||||
from fuel_agent.utils import fs_utils as fu
|
from fuel_agent.utils import fs_utils as fu
|
||||||
from fuel_agent.utils import grub_utils as gu
|
from fuel_agent.utils import grub_utils as gu
|
||||||
from fuel_agent.utils import lvm_utils as lu
|
from fuel_agent.utils import lvm_utils as lu
|
||||||
@ -27,11 +33,6 @@ from fuel_agent.utils import partition_utils as pu
|
|||||||
from fuel_agent.utils import utils
|
from fuel_agent.utils import utils
|
||||||
|
|
||||||
opts = [
|
opts = [
|
||||||
cfg.StrOpt(
|
|
||||||
'data_driver',
|
|
||||||
default='nailgun',
|
|
||||||
help='Data driver'
|
|
||||||
),
|
|
||||||
cfg.StrOpt(
|
cfg.StrOpt(
|
||||||
'nc_template_path',
|
'nc_template_path',
|
||||||
default='/usr/share/fuel-agent/cloud-init-templates',
|
default='/usr/share/fuel-agent/cloud-init-templates',
|
||||||
@ -67,10 +68,29 @@ opts = [
|
|||||||
default='empty_rule',
|
default='empty_rule',
|
||||||
help='Correct empty rule for udev daemon',
|
help='Correct empty rule for udev daemon',
|
||||||
),
|
),
|
||||||
|
cfg.StrOpt(
|
||||||
|
'image_build_dir',
|
||||||
|
default='/tmp',
|
||||||
|
help='Directory where the image is supposed to be built',
|
||||||
|
),
|
||||||
|
cfg.StrOpt(
|
||||||
|
'image_build_suffix',
|
||||||
|
default='.fuel-agent-image',
|
||||||
|
help='Suffix which is used while creating temporary files',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
cli_opts = [
|
||||||
|
cfg.StrOpt(
|
||||||
|
'data_driver',
|
||||||
|
default='nailgun',
|
||||||
|
help='Data driver'
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
CONF.register_opts(opts)
|
CONF.register_opts(opts)
|
||||||
|
CONF.register_cli_opts(cli_opts)
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -311,22 +331,30 @@ class Manager(object):
|
|||||||
(image.format, image.target_device))
|
(image.format, image.target_device))
|
||||||
fu.extend_fs(image.format, image.target_device)
|
fu.extend_fs(image.format, image.target_device)
|
||||||
|
|
||||||
def mount_target(self, chroot):
|
# TODO(kozhukalov): write tests
|
||||||
|
def mount_target(self, chroot, treat_mtab=True, pseudo=True):
|
||||||
|
"""Mount a set of file systems into a chroot
|
||||||
|
|
||||||
|
:param chroot: Directory where to mount file systems
|
||||||
|
:param treat_mtab: If mtab needs to be actualized (Default: True)
|
||||||
|
:param pseudo: If pseudo file systems
|
||||||
|
need to be mounted (Default: True)
|
||||||
|
"""
|
||||||
LOG.debug('Mounting target file systems')
|
LOG.debug('Mounting target file systems')
|
||||||
# Here we are going to mount all file systems in partition scheme.
|
# Here we are going to mount all file systems in partition scheme.
|
||||||
# Shorter paths earlier. We sort all mount points by their depth.
|
for fs in self.driver.partition_scheme.fs_sorted_by_depth():
|
||||||
# ['/', '/boot', '/var', '/var/lib/mysql']
|
|
||||||
key = lambda x: len(x.mount.rstrip('/').split('/'))
|
|
||||||
for fs in sorted(self.driver.partition_scheme.fss, key=key):
|
|
||||||
if fs.mount == 'swap':
|
if fs.mount == 'swap':
|
||||||
continue
|
continue
|
||||||
mount = chroot + fs.mount
|
mount = chroot + fs.mount
|
||||||
if not os.path.isdir(mount):
|
utils.makedirs_if_not_exists(mount)
|
||||||
os.makedirs(mount, mode=0o755)
|
fu.mount_fs(fs.type, str(fs.device), mount)
|
||||||
fu.mount_fs(fs.type, fs.device, mount)
|
|
||||||
fu.mount_bind(chroot, '/sys')
|
if pseudo:
|
||||||
fu.mount_bind(chroot, '/dev')
|
for path in ('/sys', '/dev', '/proc'):
|
||||||
fu.mount_bind(chroot, '/proc')
|
utils.makedirs_if_not_exists(chroot + path)
|
||||||
|
fu.mount_bind(chroot, path)
|
||||||
|
|
||||||
|
if treat_mtab:
|
||||||
mtab = utils.execute(
|
mtab = utils.execute(
|
||||||
'chroot', chroot, 'grep', '-v', 'rootfs', '/proc/mounts')[0]
|
'chroot', chroot, 'grep', '-v', 'rootfs', '/proc/mounts')[0]
|
||||||
mtab_path = chroot + '/etc/mtab'
|
mtab_path = chroot + '/etc/mtab'
|
||||||
@ -335,17 +363,17 @@ class Manager(object):
|
|||||||
with open(mtab_path, 'wb') as f:
|
with open(mtab_path, 'wb') as f:
|
||||||
f.write(mtab)
|
f.write(mtab)
|
||||||
|
|
||||||
def umount_target(self, chroot):
|
# TODO(kozhukalov): write tests
|
||||||
|
def umount_target(self, chroot, pseudo=True):
|
||||||
LOG.debug('Umounting target file systems')
|
LOG.debug('Umounting target file systems')
|
||||||
fu.umount_fs(chroot + '/proc')
|
if pseudo:
|
||||||
fu.umount_fs(chroot + '/dev')
|
for path in ('/proc', '/dev', '/sys'):
|
||||||
fu.umount_fs(chroot + '/sys')
|
fu.umount_fs(chroot + path)
|
||||||
key = lambda x: len(x.mount.rstrip('/').split('/'))
|
for fs in self.driver.partition_scheme.fs_sorted_by_depth(
|
||||||
for fs in sorted(self.driver.partition_scheme.fss,
|
reverse=True):
|
||||||
key=key, reverse=True):
|
|
||||||
if fs.mount == 'swap':
|
if fs.mount == 'swap':
|
||||||
continue
|
continue
|
||||||
fu.umount_fs(fs.device)
|
fu.umount_fs(chroot + fs.mount)
|
||||||
|
|
||||||
def do_bootloader(self):
|
def do_bootloader(self):
|
||||||
LOG.debug('--- Installing bootloader (do_bootloader) ---')
|
LOG.debug('--- Installing bootloader (do_bootloader) ---')
|
||||||
@ -416,3 +444,200 @@ class Manager(object):
|
|||||||
self.do_configdrive()
|
self.do_configdrive()
|
||||||
self.do_copyimage()
|
self.do_copyimage()
|
||||||
self.do_bootloader()
|
self.do_bootloader()
|
||||||
|
LOG.debug('--- Provisioning END (do_provisioning) ---')
|
||||||
|
|
||||||
|
# TODO(kozhukalov): Split this huge method
|
||||||
|
# into a set of smaller ones
|
||||||
|
# https://bugs.launchpad.net/fuel/+bug/1444090
|
||||||
|
def do_build_image(self):
|
||||||
|
"""Building OS images includes the following steps
|
||||||
|
1) create temporary sparse files for all images (truncate)
|
||||||
|
2) attach temporary files to loop devices (losetup)
|
||||||
|
3) create file systems on these loop devices
|
||||||
|
4) create temporary chroot directory
|
||||||
|
5) mount loop devices into chroot directory
|
||||||
|
6) install operating system (debootstrap and apt-get)
|
||||||
|
7) configure OS (clean sources.list and preferences, etc.)
|
||||||
|
8) umount loop devices
|
||||||
|
9) resize file systems on loop devices
|
||||||
|
10) shrink temporary sparse files (images)
|
||||||
|
11) containerize (gzip) temporary sparse files
|
||||||
|
12) move temporary gzipped files to their final location
|
||||||
|
"""
|
||||||
|
LOG.info('--- Building image (do_build_image) ---')
|
||||||
|
# TODO(kozhukalov): Implement metadata
|
||||||
|
# as a pluggable data driver to avoid any fixed format.
|
||||||
|
metadata = {}
|
||||||
|
|
||||||
|
# TODO(kozhukalov): implement this using image metadata
|
||||||
|
# we need to compare list of packages and repos
|
||||||
|
LOG.info('*** Checking if image exists ***')
|
||||||
|
if all([os.path.exists(img.uri.split('file://', 1)[1])
|
||||||
|
for img in self.driver.image_scheme.images]):
|
||||||
|
LOG.debug('All necessary images are available. '
|
||||||
|
'Nothing needs to be done.')
|
||||||
|
return
|
||||||
|
LOG.debug('At least one of the necessary images is unavailable. '
|
||||||
|
'Starting build process.')
|
||||||
|
|
||||||
|
LOG.info('*** Preparing image space ***')
|
||||||
|
for image in self.driver.image_scheme.images:
|
||||||
|
LOG.debug('Creating temporary sparsed file for the '
|
||||||
|
'image: %s', image.uri)
|
||||||
|
img_tmp_file = bu.create_sparse_tmp_file(
|
||||||
|
dir=CONF.image_build_dir, suffix=CONF.image_build_suffix)
|
||||||
|
LOG.debug('Temporary file: %s', img_tmp_file)
|
||||||
|
|
||||||
|
# we need to remember those files
|
||||||
|
# to be able to shrink them and move in the end
|
||||||
|
image.img_tmp_file = img_tmp_file
|
||||||
|
|
||||||
|
LOG.debug('Looking for a free loop device')
|
||||||
|
image.target_device.name = bu.get_free_loop_device()
|
||||||
|
|
||||||
|
LOG.debug('Attaching temporary image file to free loop device')
|
||||||
|
bu.attach_file_to_loop(img_tmp_file, str(image.target_device))
|
||||||
|
|
||||||
|
# find fs with the same loop device object
|
||||||
|
# as image.target_device
|
||||||
|
fs = self.driver.partition_scheme.fs_by_device(image.target_device)
|
||||||
|
|
||||||
|
LOG.debug('Creating file system on the image')
|
||||||
|
fu.make_fs(
|
||||||
|
fs_type=fs.type,
|
||||||
|
fs_options=fs.options,
|
||||||
|
fs_label=fs.label,
|
||||||
|
dev=str(fs.device))
|
||||||
|
|
||||||
|
LOG.debug('Creating temporary chroot directory')
|
||||||
|
chroot = tempfile.mkdtemp(
|
||||||
|
dir=CONF.image_build_dir, suffix=CONF.image_build_suffix)
|
||||||
|
LOG.debug('Temporary chroot: %s', chroot)
|
||||||
|
|
||||||
|
# mounting all images into chroot tree
|
||||||
|
self.mount_target(chroot, treat_mtab=False, pseudo=False)
|
||||||
|
|
||||||
|
LOG.info('*** Shipping image content ***')
|
||||||
|
LOG.debug('Installing operating system into image')
|
||||||
|
# FIXME(kozhukalov): !!! we need this part to be OS agnostic
|
||||||
|
|
||||||
|
# DEBOOTSTRAP
|
||||||
|
# we use first repo as the main mirror
|
||||||
|
uri = self.driver.operating_system.repos[0].uri
|
||||||
|
suite = self.driver.operating_system.repos[0].suite
|
||||||
|
|
||||||
|
LOG.debug('Preventing services from being get started')
|
||||||
|
bu.suppress_services_start(chroot)
|
||||||
|
LOG.debug('Installing base operating system using debootstrap')
|
||||||
|
bu.run_debootstrap(uri=uri, suite=suite, chroot=chroot)
|
||||||
|
|
||||||
|
# APT-GET
|
||||||
|
LOG.debug('Configuring apt inside chroot')
|
||||||
|
LOG.debug('Setting environment variables')
|
||||||
|
bu.set_apt_get_env()
|
||||||
|
LOG.debug('Allowing unauthenticated repos')
|
||||||
|
bu.pre_apt_get(chroot)
|
||||||
|
|
||||||
|
for repo in self.driver.operating_system.repos:
|
||||||
|
LOG.debug('Adding repository source: name={name}, uri={uri},'
|
||||||
|
'suite={suite}, section={section}'.format(
|
||||||
|
name=repo.name, uri=repo.uri,
|
||||||
|
suite=repo.suite, section=repo.section))
|
||||||
|
bu.add_apt_source(
|
||||||
|
name=repo.name,
|
||||||
|
uri=repo.uri,
|
||||||
|
suite=repo.suite,
|
||||||
|
section=repo.section,
|
||||||
|
chroot=chroot)
|
||||||
|
LOG.debug('Adding repository preference: '
|
||||||
|
'name={name}, priority={priority}'.format(
|
||||||
|
name=repo.name, priority=repo.priority))
|
||||||
|
bu.add_apt_preference(
|
||||||
|
name=repo.name,
|
||||||
|
priority=repo.priority,
|
||||||
|
suite=repo.suite,
|
||||||
|
section=repo.section,
|
||||||
|
chroot=chroot)
|
||||||
|
|
||||||
|
metadata.setdefault('repos', []).append({
|
||||||
|
'type': 'deb',
|
||||||
|
'name': repo.name,
|
||||||
|
'uri': repo.uri,
|
||||||
|
'suite': repo.suite,
|
||||||
|
'section': repo.section,
|
||||||
|
'priority': repo.priority,
|
||||||
|
'meta': repo.meta})
|
||||||
|
|
||||||
|
LOG.debug('Preventing services from being get started')
|
||||||
|
bu.suppress_services_start(chroot)
|
||||||
|
|
||||||
|
packages = self.driver.operating_system.packages
|
||||||
|
metadata['packages'] = packages
|
||||||
|
|
||||||
|
# we need /proc to be mounted for apt-get success
|
||||||
|
proc_path = os.path.join(chroot, 'proc')
|
||||||
|
utils.makedirs_if_not_exists(proc_path)
|
||||||
|
fu.mount_bind(chroot, '/proc')
|
||||||
|
|
||||||
|
LOG.debug('Installing packages using apt-get: %s',
|
||||||
|
' '.join(packages))
|
||||||
|
bu.run_apt_get(chroot, packages=packages)
|
||||||
|
|
||||||
|
LOG.debug('Post-install OS configuration')
|
||||||
|
bu.do_post_inst(chroot)
|
||||||
|
|
||||||
|
LOG.debug('Making sure there are no running processes '
|
||||||
|
'inside chroot before trying to umount chroot')
|
||||||
|
bu.send_signal_to_chrooted_processes(chroot, signal.SIGTERM)
|
||||||
|
# We assume there might be some processes which
|
||||||
|
# require some reasonable time to stop before we try
|
||||||
|
# to send them SIGKILL. Waiting for 2 seconds
|
||||||
|
# looks reasonable here.
|
||||||
|
time.sleep(2)
|
||||||
|
bu.send_signal_to_chrooted_processes(chroot, signal.SIGKILL)
|
||||||
|
|
||||||
|
LOG.info('*** Finalizing image space ***')
|
||||||
|
fu.umount_fs(proc_path)
|
||||||
|
# umounting all loop devices
|
||||||
|
self.umount_target(chroot, pseudo=False)
|
||||||
|
|
||||||
|
for image in self.driver.image_scheme.images:
|
||||||
|
LOG.debug('Deattaching loop device from file: %s',
|
||||||
|
image.img_tmp_file)
|
||||||
|
bu.deattach_loop(str(image.target_device))
|
||||||
|
LOG.debug('Shrinking temporary image file: %s',
|
||||||
|
image.img_tmp_file)
|
||||||
|
bu.shrink_sparse_file(image.img_tmp_file)
|
||||||
|
|
||||||
|
raw_size = os.path.getsize(image.img_tmp_file)
|
||||||
|
raw_md5 = utils.calculate_md5(image.img_tmp_file, raw_size)
|
||||||
|
|
||||||
|
LOG.debug('Containerizing temporary image file: %s',
|
||||||
|
image.img_tmp_file)
|
||||||
|
img_tmp_containerized = bu.containerize(
|
||||||
|
image.img_tmp_file, image.container)
|
||||||
|
img_containerized = image.uri.split('file://', 1)[1]
|
||||||
|
|
||||||
|
# NOTE(kozhukalov): implement abstract publisher
|
||||||
|
LOG.debug('Moving image file to the final location: %s',
|
||||||
|
img_containerized)
|
||||||
|
shutil.move(img_tmp_containerized, img_containerized)
|
||||||
|
|
||||||
|
container_size = os.path.getsize(img_containerized)
|
||||||
|
container_md5 = utils.calculate_md5(
|
||||||
|
img_containerized, container_size)
|
||||||
|
metadata.setdefault('images', []).append({
|
||||||
|
'raw_md5': raw_md5,
|
||||||
|
'raw_size': raw_size,
|
||||||
|
'raw_name': None,
|
||||||
|
'container_name': os.path.basename(img_containerized),
|
||||||
|
'container_md5': container_md5,
|
||||||
|
'container_size': container_size,
|
||||||
|
'container': image.container,
|
||||||
|
'format': image.format})
|
||||||
|
|
||||||
|
# NOTE(kozhukalov): implement abstract publisher
|
||||||
|
LOG.debug('Image metadata: %s', metadata)
|
||||||
|
with open(self.driver.metadata_uri.split('file://', 1)[1], 'w') as f:
|
||||||
|
yaml.safe_dump(metadata, stream=f)
|
||||||
|
LOG.info('--- Building image END (do_build_image) ---')
|
||||||
|
@ -16,8 +16,11 @@ from fuel_agent.objects.configdrive import ConfigDriveCommon
|
|||||||
from fuel_agent.objects.configdrive import ConfigDriveMcollective
|
from fuel_agent.objects.configdrive import ConfigDriveMcollective
|
||||||
from fuel_agent.objects.configdrive import ConfigDrivePuppet
|
from fuel_agent.objects.configdrive import ConfigDrivePuppet
|
||||||
from fuel_agent.objects.configdrive import ConfigDriveScheme
|
from fuel_agent.objects.configdrive import ConfigDriveScheme
|
||||||
|
from fuel_agent.objects.device import Loop
|
||||||
from fuel_agent.objects.image import Image
|
from fuel_agent.objects.image import Image
|
||||||
from fuel_agent.objects.image import ImageScheme
|
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 Fs
|
||||||
from fuel_agent.objects.partition import Lv
|
from fuel_agent.objects.partition import Lv
|
||||||
from fuel_agent.objects.partition import Md
|
from fuel_agent.objects.partition import Md
|
||||||
@ -25,9 +28,14 @@ from fuel_agent.objects.partition import Partition
|
|||||||
from fuel_agent.objects.partition import PartitionScheme
|
from fuel_agent.objects.partition import PartitionScheme
|
||||||
from fuel_agent.objects.partition import Pv
|
from fuel_agent.objects.partition import Pv
|
||||||
from fuel_agent.objects.partition import Vg
|
from fuel_agent.objects.partition import Vg
|
||||||
|
from fuel_agent.objects.repo import DEBRepo
|
||||||
|
from fuel_agent.objects.repo import Repo
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'Partition', 'Pv', 'Vg', 'Lv', 'Md', 'Fs', 'PartitionScheme',
|
'Partition', 'Pv', 'Vg', 'Lv', 'Md', 'Fs', 'PartitionScheme',
|
||||||
'ConfigDriveCommon', 'ConfigDrivePuppet', 'ConfigDriveMcollective',
|
'ConfigDriveCommon', 'ConfigDrivePuppet', 'ConfigDriveMcollective',
|
||||||
'ConfigDriveScheme', 'Image', 'ImageScheme',
|
'ConfigDriveScheme', 'Image', 'ImageScheme',
|
||||||
|
'OperatingSystem', 'Ubuntu',
|
||||||
|
'Repo', 'DEBRepo',
|
||||||
|
'Loop',
|
||||||
]
|
]
|
||||||
|
28
fuel_agent/objects/device.py
Normal file
28
fuel_agent/objects/device.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# 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 import errors
|
||||||
|
|
||||||
|
|
||||||
|
class Loop(object):
|
||||||
|
def __init__(self, name=None):
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.name:
|
||||||
|
return self.name
|
||||||
|
raise errors.WrongDeviceError(
|
||||||
|
'Loop device can not be stringified. '
|
||||||
|
'Name attribute is not set. Current: '
|
||||||
|
'name={0}'.format(self.name))
|
23
fuel_agent/objects/operating_system.py
Normal file
23
fuel_agent/objects/operating_system.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# 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 OperatingSystem(object):
|
||||||
|
def __init__(self, repos, packages):
|
||||||
|
self.repos = repos
|
||||||
|
self.packages = packages
|
||||||
|
|
||||||
|
|
||||||
|
class Ubuntu(OperatingSystem):
|
||||||
|
pass
|
@ -12,6 +12,8 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
from fuel_agent import errors
|
from fuel_agent import errors
|
||||||
from fuel_agent.openstack.common import log as logging
|
from fuel_agent.openstack.common import log as logging
|
||||||
|
|
||||||
@ -289,6 +291,15 @@ class PartitionScheme(object):
|
|||||||
if found:
|
if found:
|
||||||
return found[0]
|
return found[0]
|
||||||
|
|
||||||
|
def fs_sorted_by_depth(self, reverse=False):
|
||||||
|
"""Getting file systems sorted by path length. Shorter paths earlier.
|
||||||
|
['/', '/boot', '/var', '/var/lib/mysql']
|
||||||
|
:param reverse: Sort backward (Default: False)
|
||||||
|
"""
|
||||||
|
def key(x):
|
||||||
|
return x.mount.rstrip(os.path.sep).count(os.path.sep)
|
||||||
|
return sorted(self.fss, key=key, reverse=reverse)
|
||||||
|
|
||||||
def lv_by_device_name(self, device_name):
|
def lv_by_device_name(self, device_name):
|
||||||
found = filter(lambda x: x.device_name == device_name, self.lvs)
|
found = filter(lambda x: x.device_name == device_name, self.lvs)
|
||||||
if found:
|
if found:
|
||||||
|
28
fuel_agent/objects/repo.py
Normal file
28
fuel_agent/objects/repo.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# 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 Repo(object):
|
||||||
|
def __init__(self, name, uri, priority=None):
|
||||||
|
self.name = name
|
||||||
|
self.uri = uri
|
||||||
|
self.priority = priority
|
||||||
|
|
||||||
|
|
||||||
|
class DEBRepo(Repo):
|
||||||
|
def __init__(self, name, uri, suite, section, meta=None, priority=None):
|
||||||
|
super(DEBRepo, self).__init__(name, uri, priority)
|
||||||
|
self.suite = suite
|
||||||
|
self.section = section
|
||||||
|
self.meta = meta
|
@ -14,12 +14,14 @@
|
|||||||
|
|
||||||
import mock
|
import mock
|
||||||
import os
|
import os
|
||||||
|
import signal
|
||||||
|
|
||||||
from oslo.config import cfg
|
from oslo.config import cfg
|
||||||
from oslotest import base as test_base
|
from oslotest import base as test_base
|
||||||
|
|
||||||
from fuel_agent import errors
|
from fuel_agent import errors
|
||||||
from fuel_agent import manager
|
from fuel_agent import manager
|
||||||
|
from fuel_agent import objects
|
||||||
from fuel_agent.objects import partition
|
from fuel_agent.objects import partition
|
||||||
from fuel_agent.tests import test_nailgun
|
from fuel_agent.tests import test_nailgun
|
||||||
from fuel_agent.utils import artifact_utils as au
|
from fuel_agent.utils import artifact_utils as au
|
||||||
@ -328,3 +330,183 @@ class TestManager(test_base.BaseTestCase):
|
|||||||
self.assertEqual(2, len(self.mgr.driver.image_scheme.images))
|
self.assertEqual(2, len(self.mgr.driver.image_scheme.images))
|
||||||
self.assertRaises(errors.ImageChecksumMismatchError,
|
self.assertRaises(errors.ImageChecksumMismatchError,
|
||||||
self.mgr.do_copyimage)
|
self.mgr.do_copyimage)
|
||||||
|
|
||||||
|
@mock.patch('fuel_agent.manager.bu', create=True)
|
||||||
|
@mock.patch('fuel_agent.manager.fu', create=True)
|
||||||
|
@mock.patch('fuel_agent.manager.utils', create=True)
|
||||||
|
@mock.patch('fuel_agent.manager.os', create=True)
|
||||||
|
@mock.patch('fuel_agent.manager.shutil.move')
|
||||||
|
@mock.patch('fuel_agent.manager.open',
|
||||||
|
create=True, new_callable=mock.mock_open)
|
||||||
|
@mock.patch('fuel_agent.manager.tempfile.mkdtemp')
|
||||||
|
@mock.patch('fuel_agent.manager.time.sleep')
|
||||||
|
@mock.patch('fuel_agent.manager.yaml.safe_dump')
|
||||||
|
@mock.patch.object(manager.Manager, 'mount_target')
|
||||||
|
@mock.patch.object(manager.Manager, 'umount_target')
|
||||||
|
def test_do_build_image(self, mock_umount_target, mock_mount_target,
|
||||||
|
mock_yaml_dump, mock_sleep, mock_mkdtemp,
|
||||||
|
mock_open, mock_shutil_move, mock_os,
|
||||||
|
mock_utils, mock_fu, mock_bu):
|
||||||
|
|
||||||
|
loops = [objects.Loop(), objects.Loop()]
|
||||||
|
|
||||||
|
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.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(
|
||||||
|
repos=[
|
||||||
|
objects.DEBRepo('ubuntu', 'http://fakeubuntu',
|
||||||
|
'trusty', 'fakesection'),
|
||||||
|
objects.DEBRepo('mos', 'http://fakemos',
|
||||||
|
'mosX.Y', 'fakesection', priority=1000)],
|
||||||
|
packages=['fakepackage1', 'fakepackage2'])
|
||||||
|
|
||||||
|
mock_os.path.exists.return_value = False
|
||||||
|
mock_os.path.join.return_value = '/tmp/imgdir/proc'
|
||||||
|
mock_os.path.basename.side_effect = ['img.img.gz', 'img-boot.img.gz']
|
||||||
|
mock_bu.create_sparse_tmp_file.side_effect = \
|
||||||
|
['/tmp/img', '/tmp/img-boot']
|
||||||
|
mock_bu.get_free_loop_device.side_effect = ['/dev/loop0', '/dev/loop1']
|
||||||
|
mock_mkdtemp.return_value = '/tmp/imgdir'
|
||||||
|
getsize_side = [20, 2, 10, 1]
|
||||||
|
mock_os.path.getsize.side_effect = getsize_side
|
||||||
|
md5_side = ['fakemd5_raw', 'fakemd5_gzip',
|
||||||
|
'fakemd5_raw_boot', 'fakemd5_gzip_boot']
|
||||||
|
mock_utils.calculate_md5.side_effect = md5_side
|
||||||
|
mock_bu.containerize.side_effect = ['/tmp/img.gz', '/tmp/img-boot.gz']
|
||||||
|
|
||||||
|
self.mgr.do_build_image()
|
||||||
|
self.assertEqual(
|
||||||
|
[mock.call('/fake/img.img.gz'),
|
||||||
|
mock.call('/fake/img-boot.img.gz')],
|
||||||
|
mock_os.path.exists.call_args_list)
|
||||||
|
self.assertEqual([mock.call(dir=CONF.image_build_dir,
|
||||||
|
suffix=CONF.image_build_suffix)] * 2,
|
||||||
|
mock_bu.create_sparse_tmp_file.call_args_list)
|
||||||
|
self.assertEqual([mock.call()] * 2,
|
||||||
|
mock_bu.get_free_loop_device.call_args_list)
|
||||||
|
self.assertEqual([mock.call('/tmp/img', '/dev/loop0'),
|
||||||
|
mock.call('/tmp/img-boot', '/dev/loop1')],
|
||||||
|
mock_bu.attach_file_to_loop.call_args_list)
|
||||||
|
self.assertEqual([mock.call(fs_type='ext4', fs_options='',
|
||||||
|
fs_label='', dev='/dev/loop0'),
|
||||||
|
mock.call(fs_type='ext2', fs_options='',
|
||||||
|
fs_label='', dev='/dev/loop1')],
|
||||||
|
mock_fu.make_fs.call_args_list)
|
||||||
|
mock_mkdtemp.assert_called_once_with(dir=CONF.image_build_dir,
|
||||||
|
suffix=CONF.image_build_suffix)
|
||||||
|
mock_mount_target.assert_called_once_with(
|
||||||
|
'/tmp/imgdir', treat_mtab=False, pseudo=False)
|
||||||
|
self.assertEqual([mock.call('/tmp/imgdir')] * 2,
|
||||||
|
mock_bu.suppress_services_start.call_args_list)
|
||||||
|
mock_bu.run_debootstrap.assert_called_once_with(
|
||||||
|
uri='http://fakeubuntu', suite='trusty', chroot='/tmp/imgdir')
|
||||||
|
mock_bu.set_apt_get_env.assert_called_once_with()
|
||||||
|
mock_bu.pre_apt_get.assert_called_once_with('/tmp/imgdir')
|
||||||
|
self.assertEqual([
|
||||||
|
mock.call(name='ubuntu',
|
||||||
|
uri='http://fakeubuntu',
|
||||||
|
suite='trusty',
|
||||||
|
section='fakesection',
|
||||||
|
chroot='/tmp/imgdir'),
|
||||||
|
mock.call(name='mos',
|
||||||
|
uri='http://fakemos',
|
||||||
|
suite='mosX.Y',
|
||||||
|
section='fakesection',
|
||||||
|
chroot='/tmp/imgdir')],
|
||||||
|
mock_bu.add_apt_source.call_args_list)
|
||||||
|
self.assertEqual([
|
||||||
|
mock.call(name='ubuntu',
|
||||||
|
priority=None,
|
||||||
|
suite='trusty',
|
||||||
|
section='fakesection',
|
||||||
|
chroot='/tmp/imgdir'),
|
||||||
|
mock.call(name='mos',
|
||||||
|
priority=1000,
|
||||||
|
suite='mosX.Y',
|
||||||
|
section='fakesection',
|
||||||
|
chroot='/tmp/imgdir')],
|
||||||
|
mock_bu.add_apt_preference.call_args_list)
|
||||||
|
mock_utils.makedirs_if_not_exists.assert_called_once_with(
|
||||||
|
'/tmp/imgdir/proc')
|
||||||
|
mock_fu.mount_bind.assert_called_once_with('/tmp/imgdir', '/proc')
|
||||||
|
mock_bu.run_apt_get.assert_called_once_with(
|
||||||
|
'/tmp/imgdir', packages=['fakepackage1', 'fakepackage2'])
|
||||||
|
mock_bu.do_post_inst.assert_called_once_with('/tmp/imgdir')
|
||||||
|
signal_calls = mock_bu.send_signal_to_chrooted_processes.call_args_list
|
||||||
|
self.assertEqual([mock.call('/tmp/imgdir', signal.SIGTERM),
|
||||||
|
mock.call('/tmp/imgdir', signal.SIGKILL)],
|
||||||
|
signal_calls)
|
||||||
|
mock_sleep.assert_called_once_with(2)
|
||||||
|
mock_fu.umount_fs.assert_called_once_with('/tmp/imgdir/proc')
|
||||||
|
mock_umount_target.assert_called_once_with('/tmp/imgdir', pseudo=False)
|
||||||
|
self.assertEqual([mock.call('/dev/loop0'), mock.call('/dev/loop1')],
|
||||||
|
mock_bu.deattach_loop.call_args_list)
|
||||||
|
self.assertEqual([mock.call('/tmp/img'), mock.call('/tmp/img-boot')],
|
||||||
|
mock_bu.shrink_sparse_file.call_args_list)
|
||||||
|
self.assertEqual([mock.call('/tmp/img'),
|
||||||
|
mock.call('/fake/img.img.gz'),
|
||||||
|
mock.call('/tmp/img-boot'),
|
||||||
|
mock.call('/fake/img-boot.img.gz')],
|
||||||
|
mock_os.path.getsize.call_args_list)
|
||||||
|
self.assertEqual([mock.call('/tmp/img', 20),
|
||||||
|
mock.call('/fake/img.img.gz', 2),
|
||||||
|
mock.call('/tmp/img-boot', 10),
|
||||||
|
mock.call('/fake/img-boot.img.gz', 1)],
|
||||||
|
mock_utils.calculate_md5.call_args_list)
|
||||||
|
self.assertEqual([mock.call('/tmp/img', 'gzip'),
|
||||||
|
mock.call('/tmp/img-boot', 'gzip')],
|
||||||
|
mock_bu.containerize.call_args_list)
|
||||||
|
mock_open.assert_called_once_with('/fake/img.yaml', 'w')
|
||||||
|
self.assertEqual(
|
||||||
|
[mock.call('/tmp/img.gz', '/fake/img.img.gz'),
|
||||||
|
mock.call('/tmp/img-boot.gz', '/fake/img-boot.img.gz')],
|
||||||
|
mock_shutil_move.call_args_list)
|
||||||
|
|
||||||
|
metadata = {}
|
||||||
|
for repo in self.mgr.driver.operating_system.repos:
|
||||||
|
metadata.setdefault('repos', []).append({
|
||||||
|
'type': 'deb',
|
||||||
|
'name': repo.name,
|
||||||
|
'uri': repo.uri,
|
||||||
|
'suite': repo.suite,
|
||||||
|
'section': repo.section,
|
||||||
|
'priority': repo.priority,
|
||||||
|
'meta': repo.meta})
|
||||||
|
metadata['packages'] = self.mgr.driver.operating_system.packages
|
||||||
|
metadata['images'] = [
|
||||||
|
{
|
||||||
|
'raw_md5': md5_side[0],
|
||||||
|
'raw_size': getsize_side[0],
|
||||||
|
'raw_name': None,
|
||||||
|
'container_name':
|
||||||
|
os.path.basename(
|
||||||
|
self.mgr.driver.image_scheme.images[0].uri.split(
|
||||||
|
'file://', 1)[1]),
|
||||||
|
'container_md5': md5_side[1],
|
||||||
|
'container_size': getsize_side[1],
|
||||||
|
'container': self.mgr.driver.image_scheme.images[0].container,
|
||||||
|
'format': self.mgr.driver.image_scheme.images[0].format
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'raw_md5': md5_side[2],
|
||||||
|
'raw_size': getsize_side[2],
|
||||||
|
'raw_name': None,
|
||||||
|
'container_name':
|
||||||
|
os.path.basename(
|
||||||
|
self.mgr.driver.image_scheme.images[1].uri.split(
|
||||||
|
'file://', 1)[1]),
|
||||||
|
'container_md5': md5_side[3],
|
||||||
|
'container_size': getsize_side[3],
|
||||||
|
'container': self.mgr.driver.image_scheme.images[1].container,
|
||||||
|
'format': self.mgr.driver.image_scheme.images[1].format
|
||||||
|
}
|
||||||
|
]
|
||||||
|
mock_yaml_dump.assert_called_once_with(metadata, stream=mock_open())
|
||||||
|
@ -616,7 +616,8 @@ class TestNailgun(test_base.BaseTestCase):
|
|||||||
@mock.patch.object(utils, 'init_http_request')
|
@mock.patch.object(utils, 'init_http_request')
|
||||||
@mock.patch.object(hu, 'list_block_devices')
|
@mock.patch.object(hu, 'list_block_devices')
|
||||||
def test_image_scheme_with_checksums(self, mock_lbd, mock_http_req):
|
def test_image_scheme_with_checksums(self, mock_lbd, mock_http_req):
|
||||||
fake_image_meta = [{'/': {'md5': 'fakeroot', 'size': 1}}]
|
fake_image_meta = {'images': [{'raw_md5': 'fakeroot', 'raw_size': 1,
|
||||||
|
'container_name': 'fake_image.img.gz'}]}
|
||||||
prop_mock = mock.PropertyMock(return_value=yaml.dump(fake_image_meta))
|
prop_mock = mock.PropertyMock(return_value=yaml.dump(fake_image_meta))
|
||||||
type(mock_http_req.return_value).text = prop_mock
|
type(mock_http_req.return_value).text = prop_mock
|
||||||
mock_lbd.return_value = LIST_BLOCK_DEVICES_SAMPLE
|
mock_lbd.return_value = LIST_BLOCK_DEVICES_SAMPLE
|
||||||
@ -646,8 +647,9 @@ class TestNailgun(test_base.BaseTestCase):
|
|||||||
expected_images[i].format)
|
expected_images[i].format)
|
||||||
self.assertEqual(img.container,
|
self.assertEqual(img.container,
|
||||||
expected_images[i].container)
|
expected_images[i].container)
|
||||||
self.assertEqual(img.size, fake_image_meta[0]['/']['size'])
|
self.assertEqual(
|
||||||
self.assertEqual(img.md5, fake_image_meta[0]['/']['md5'])
|
img.size, fake_image_meta['images'][0]['raw_size'])
|
||||||
|
self.assertEqual(img.md5, fake_image_meta['images'][0]['raw_md5'])
|
||||||
|
|
||||||
def test_getlabel(self):
|
def test_getlabel(self):
|
||||||
self.assertEqual('', self.drv._getlabel(None))
|
self.assertEqual('', self.drv._getlabel(None))
|
||||||
|
244
fuel_agent/tests/test_nailgun_build_image.py
Normal file
244
fuel_agent/tests/test_nailgun_build_image.py
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
# 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 os
|
||||||
|
import six
|
||||||
|
from six.moves.urllib.parse import urlsplit
|
||||||
|
|
||||||
|
from oslotest import base as test_base
|
||||||
|
|
||||||
|
from fuel_agent.drivers.nailgun import NailgunBuildImage
|
||||||
|
from fuel_agent import errors
|
||||||
|
from fuel_agent import objects
|
||||||
|
|
||||||
|
DEFAULT_TRUSTY_PACKAGES = [
|
||||||
|
"acl",
|
||||||
|
"anacron",
|
||||||
|
"bash-completion",
|
||||||
|
"bridge-utils",
|
||||||
|
"bsdmainutils",
|
||||||
|
"build-essential",
|
||||||
|
"cloud-init",
|
||||||
|
"curl",
|
||||||
|
"daemonize",
|
||||||
|
"debconf-utils",
|
||||||
|
"gdisk",
|
||||||
|
"grub-pc",
|
||||||
|
"linux-firmware",
|
||||||
|
"linux-firmware-nonfree",
|
||||||
|
"linux-headers-generic-lts-trusty",
|
||||||
|
"linux-image-generic-lts-trusty",
|
||||||
|
"lvm2",
|
||||||
|
"mcollective",
|
||||||
|
"mdadm",
|
||||||
|
"nailgun-agent",
|
||||||
|
"nailgun-mcagents",
|
||||||
|
"nailgun-net-check",
|
||||||
|
"ntp",
|
||||||
|
"openssh-client",
|
||||||
|
"openssh-server",
|
||||||
|
"puppet",
|
||||||
|
"python-amqp",
|
||||||
|
"ruby-augeas",
|
||||||
|
"ruby-ipaddress",
|
||||||
|
"ruby-json",
|
||||||
|
"ruby-netaddr",
|
||||||
|
"ruby-openstack",
|
||||||
|
"ruby-shadow",
|
||||||
|
"ruby-stomp",
|
||||||
|
"telnet",
|
||||||
|
"ubuntu-minimal",
|
||||||
|
"ubuntu-standard",
|
||||||
|
"uuid-runtime",
|
||||||
|
"vim",
|
||||||
|
"virt-what",
|
||||||
|
"vlan",
|
||||||
|
]
|
||||||
|
|
||||||
|
REPOS_SAMPLE = [
|
||||||
|
{
|
||||||
|
"name": "ubuntu",
|
||||||
|
"section": "main universe multiverse",
|
||||||
|
"uri": "http://archive.ubuntu.com/ubuntu/",
|
||||||
|
"priority": None,
|
||||||
|
"suite": "trusty",
|
||||||
|
"type": "deb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mos",
|
||||||
|
"section": "main restricted",
|
||||||
|
"uri": "http://10.20.0.2:8080/2014.2-6.1/ubuntu/x86_64",
|
||||||
|
"priority": 1050,
|
||||||
|
"suite": "mos6.1",
|
||||||
|
"type": "deb"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
IMAGE_DATA_SAMPLE = {
|
||||||
|
"/boot": {
|
||||||
|
"container": "gzip",
|
||||||
|
"uri": "http://10.20.0.2:8080/path/to/img-boot.img.gz",
|
||||||
|
"format": "ext2"
|
||||||
|
},
|
||||||
|
"/": {
|
||||||
|
"container": "gzip",
|
||||||
|
"uri": "http://10.20.0.2:8080/path/to/img.img.gz",
|
||||||
|
"format": "ext4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestNailgunBuildImage(test_base.BaseTestCase):
|
||||||
|
|
||||||
|
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('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
|
||||||
|
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()
|
||||||
|
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__')
|
||||||
|
def test_parse_operating_system_packages_not_given(
|
||||||
|
self, mock_init, mock_ub):
|
||||||
|
mock_init.return_value = None
|
||||||
|
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()
|
||||||
|
mock_ub.assert_called_once_with(
|
||||||
|
repos=[], packages=NailgunBuildImage.DEFAULT_TRUSTY_PACKAGES)
|
||||||
|
self.assertEqual(driver.operating_system.packages,
|
||||||
|
NailgunBuildImage.DEFAULT_TRUSTY_PACKAGES)
|
||||||
|
|
||||||
|
@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
|
||||||
|
data = {
|
||||||
|
'repos': REPOS_SAMPLE,
|
||||||
|
'codename': 'trusty'
|
||||||
|
}
|
||||||
|
driver = NailgunBuildImage()
|
||||||
|
driver.data = data
|
||||||
|
|
||||||
|
mock_deb_expected_calls = []
|
||||||
|
repos = []
|
||||||
|
for r in REPOS_SAMPLE:
|
||||||
|
kwargs = {
|
||||||
|
'name': r['name'],
|
||||||
|
'uri': r['uri'],
|
||||||
|
'suite': r['suite'],
|
||||||
|
'section': r['section'],
|
||||||
|
'priority': r['priority']
|
||||||
|
}
|
||||||
|
mock_deb_expected_calls.append(mock.call(**kwargs))
|
||||||
|
repos.append(objects.DEBRepo(**kwargs))
|
||||||
|
driver.parse_operating_system()
|
||||||
|
mock_ub_instance = mock_ub.return_value
|
||||||
|
mock_ub_instance.repos = repos
|
||||||
|
mock_ub.assert_called_once_with(
|
||||||
|
repos=repos, packages=NailgunBuildImage.DEFAULT_TRUSTY_PACKAGES)
|
||||||
|
self.assertEqual(mock_deb_expected_calls,
|
||||||
|
mock_deb.call_args_list[:len(REPOS_SAMPLE)])
|
||||||
|
self.assertEqual(driver.operating_system.repos, repos)
|
||||||
|
|
||||||
|
@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.PartitionScheme')
|
||||||
|
@mock.patch('fuel_agent.objects.ImageScheme')
|
||||||
|
@mock.patch.object(NailgunBuildImage, '__init__')
|
||||||
|
def test_parse_schemes(
|
||||||
|
self, mock_init, 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()
|
||||||
|
|
||||||
|
mock_fs_expected_calls = []
|
||||||
|
mock_img_expected_calls = []
|
||||||
|
images = []
|
||||||
|
fss = []
|
||||||
|
data_length = len(data['image_data'].keys())
|
||||||
|
for mount, image in six.iteritems(data['image_data']):
|
||||||
|
filename = os.path.basename(urlsplit(image['uri']).path)
|
||||||
|
img_kwargs = {
|
||||||
|
'uri': 'file://' + os.path.join(data['output'], filename),
|
||||||
|
'format': image['format'],
|
||||||
|
'container': image['container'],
|
||||||
|
'target_device': None
|
||||||
|
}
|
||||||
|
mock_img_expected_calls.append(mock.call(**img_kwargs))
|
||||||
|
images.append(objects.Image(**img_kwargs))
|
||||||
|
|
||||||
|
fs_kwargs = {
|
||||||
|
'device': None,
|
||||||
|
'mount': mount,
|
||||||
|
'fs_type': image['format']
|
||||||
|
}
|
||||||
|
mock_fs_expected_calls.append(mock.call(**fs_kwargs))
|
||||||
|
fss.append(objects.Fs(**fs_kwargs))
|
||||||
|
|
||||||
|
if mount == '/':
|
||||||
|
metadata_filename = filename.split('.', 1)[0] + '.yaml'
|
||||||
|
|
||||||
|
mock_imgsch_instance = mock_imgsch.return_value
|
||||||
|
mock_imgsch_instance.images = images
|
||||||
|
mock_partsch_instance = mock_partsch.return_value
|
||||||
|
mock_partsch_instance.fss = fss
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
driver.metadata_uri, 'file://' + os.path.join(
|
||||||
|
data['output'], metadata_filename))
|
||||||
|
self.assertEqual(mock_img_expected_calls,
|
||||||
|
mock_img.call_args_list[:data_length])
|
||||||
|
self.assertEqual(mock_fs_expected_calls,
|
||||||
|
mock_fs.call_args_list[:data_length])
|
||||||
|
self.assertEqual(driver.image_scheme.images, images)
|
||||||
|
self.assertEqual(driver.partition_scheme.fss, fss)
|
@ -34,7 +34,7 @@ class ExecuteTestCase(testtools.TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(ExecuteTestCase, self).setUp()
|
super(ExecuteTestCase, self).setUp()
|
||||||
fake_driver = stevedore.extension.Extension('fake_driver', None, None,
|
fake_driver = stevedore.extension.Extension('fake_driver', None, None,
|
||||||
'fake_obj')
|
mock.MagicMock)
|
||||||
self.drv_manager = stevedore.driver.DriverManager.make_test_instance(
|
self.drv_manager = stevedore.driver.DriverManager.make_test_instance(
|
||||||
fake_driver)
|
fake_driver)
|
||||||
|
|
||||||
@ -64,7 +64,8 @@ class ExecuteTestCase(testtools.TestCase):
|
|||||||
@mock.patch('stevedore.driver.DriverManager')
|
@mock.patch('stevedore.driver.DriverManager')
|
||||||
def test_get_driver(self, mock_drv_manager):
|
def test_get_driver(self, mock_drv_manager):
|
||||||
mock_drv_manager.return_value = self.drv_manager
|
mock_drv_manager.return_value = self.drv_manager
|
||||||
self.assertEqual('fake_obj', utils.get_driver('fake_driver'))
|
self.assertEqual(mock.MagicMock.__name__,
|
||||||
|
utils.get_driver('fake_driver').__name__)
|
||||||
|
|
||||||
@mock.patch('jinja2.Environment')
|
@mock.patch('jinja2.Environment')
|
||||||
@mock.patch('jinja2.FileSystemLoader')
|
@mock.patch('jinja2.FileSystemLoader')
|
||||||
@ -136,3 +137,26 @@ class ExecuteTestCase(testtools.TestCase):
|
|||||||
mock_req.side_effect = requests.exceptions.ConnectionError()
|
mock_req.side_effect = requests.exceptions.ConnectionError()
|
||||||
self.assertRaises(errors.HttpUrlConnectionError,
|
self.assertRaises(errors.HttpUrlConnectionError,
|
||||||
utils.init_http_request, 'fake_url')
|
utils.init_http_request, 'fake_url')
|
||||||
|
|
||||||
|
@mock.patch('fuel_agent.utils.utils.os.makedirs')
|
||||||
|
@mock.patch('fuel_agent.utils.utils.os.path.isdir', return_value=False)
|
||||||
|
def test_makedirs_if_not_exists(self, mock_isdir, mock_makedirs):
|
||||||
|
utils.makedirs_if_not_exists('/fake/path')
|
||||||
|
mock_isdir.assert_called_once_with('/fake/path')
|
||||||
|
mock_makedirs.assert_called_once_with('/fake/path', mode=0o755)
|
||||||
|
|
||||||
|
@mock.patch('fuel_agent.utils.utils.os.makedirs')
|
||||||
|
@mock.patch('fuel_agent.utils.utils.os.path.isdir', return_value=False)
|
||||||
|
def test_makedirs_if_not_exists_mode_given(
|
||||||
|
self, mock_isdir, mock_makedirs):
|
||||||
|
utils.makedirs_if_not_exists('/fake/path', mode=0o000)
|
||||||
|
mock_isdir.assert_called_once_with('/fake/path')
|
||||||
|
mock_makedirs.assert_called_once_with('/fake/path', mode=0o000)
|
||||||
|
|
||||||
|
@mock.patch('fuel_agent.utils.utils.os.makedirs')
|
||||||
|
@mock.patch('fuel_agent.utils.utils.os.path.isdir', return_value=True)
|
||||||
|
def test_makedirs_if_not_exists_already_exists(
|
||||||
|
self, mock_isdir, mock_makedirs):
|
||||||
|
utils.makedirs_if_not_exists('/fake/path')
|
||||||
|
mock_isdir.assert_called_once_with('/fake/path')
|
||||||
|
self.assertEqual(mock_makedirs.mock_calls, [])
|
||||||
|
@ -135,8 +135,11 @@ def B2MiB(b, ceil=True):
|
|||||||
|
|
||||||
|
|
||||||
def get_driver(name):
|
def get_driver(name):
|
||||||
return stevedore.driver.DriverManager(
|
LOG.debug('Trying to get driver: fuel_agent.drivers.%s', name)
|
||||||
|
driver = stevedore.driver.DriverManager(
|
||||||
namespace='fuel_agent.drivers', name=name).driver
|
namespace='fuel_agent.drivers', name=name).driver
|
||||||
|
LOG.debug('Found driver: %s', driver.__name__)
|
||||||
|
return driver
|
||||||
|
|
||||||
|
|
||||||
def render_and_save(tmpl_dir, tmpl_names, tmpl_data, file_name):
|
def render_and_save(tmpl_dir, tmpl_names, tmpl_data, file_name):
|
||||||
@ -200,3 +203,12 @@ def init_http_request(url, byte_range=0):
|
|||||||
'Exceeded maximum http request retries for %s' % url)
|
'Exceeded maximum http request retries for %s' % url)
|
||||||
response_obj.raise_for_status()
|
response_obj.raise_for_status()
|
||||||
return response_obj
|
return response_obj
|
||||||
|
|
||||||
|
|
||||||
|
def makedirs_if_not_exists(path, mode=0o755):
|
||||||
|
"""Create directory if it does not exist
|
||||||
|
:param path: Directory path
|
||||||
|
:param mode: Directory mode (Default: 0o755)
|
||||||
|
"""
|
||||||
|
if not os.path.isdir(path):
|
||||||
|
os.makedirs(path, mode=mode)
|
||||||
|
@ -3,7 +3,7 @@ eventlet>=0.13.0
|
|||||||
iso8601>=0.1.9
|
iso8601>=0.1.9
|
||||||
jsonschema>=2.3.0
|
jsonschema>=2.3.0
|
||||||
oslo.config>=1.2.0
|
oslo.config>=1.2.0
|
||||||
oslo.serialization>=1.0.0
|
oslo.serialization>=1.4.0
|
||||||
six>=1.5.2
|
six>=1.5.2
|
||||||
pbr>=0.7.0
|
pbr>=0.7.0
|
||||||
Jinja2
|
Jinja2
|
||||||
|
11
setup.cfg
11
setup.cfg
@ -14,14 +14,17 @@ packages =
|
|||||||
|
|
||||||
[entry_points]
|
[entry_points]
|
||||||
console_scripts =
|
console_scripts =
|
||||||
|
# TODO(kozhukalov): rename entry point
|
||||||
provision = fuel_agent.cmd.agent:provision
|
provision = fuel_agent.cmd.agent:provision
|
||||||
partition = fuel_agent.cmd.agent:partition
|
fa_partition = fuel_agent.cmd.agent:partition
|
||||||
configdrive = fuel_agent.cmd.agent:configdrive
|
fa_configdrive = fuel_agent.cmd.agent:configdrive
|
||||||
copyimage = fuel_agent.cmd.agent:copyimage
|
fa_copyimage = fuel_agent.cmd.agent:copyimage
|
||||||
bootloader = fuel_agent.cmd.agent:bootloader
|
fa_bootloader = fuel_agent.cmd.agent:bootloader
|
||||||
|
fa_build_image = fuel_agent.cmd.agent:build_image
|
||||||
|
|
||||||
fuel_agent.drivers =
|
fuel_agent.drivers =
|
||||||
nailgun = fuel_agent.drivers.nailgun:Nailgun
|
nailgun = fuel_agent.drivers.nailgun:Nailgun
|
||||||
|
nailgun_build_image = fuel_agent.drivers.nailgun:NailgunBuildImage
|
||||||
|
|
||||||
[pbr]
|
[pbr]
|
||||||
autodoc_index_modules = True
|
autodoc_index_modules = True
|
||||||
|
Loading…
Reference in New Issue
Block a user