Configure cloudimage-based nodes from devops template
- New attributes in Libvirt Node object: cloud_init_volume_name: <string> # Volume name for cloudinit config If set, than Node.define() will try to make an ISO image with metadata for cloudimage, and upload it to the specified Volume. cloud_init_iface_up: <string> # Interface label to set up on boot - New attributes in Libvirt Volume object for cloudinit metadata: cloudinit_meta_data: <string> # Yaml as a string cloudinit_user_data: <string> # Yaml as a string - Resize 'source_image' after upload to the specified 'capacity' size. It's useful for cases when original qcow2 is much smaller than it is required, so 'source_image' with the size 2Gb will be resized to 'capacity', for example 50Gb, after upload to the libvirt pool. For qcow2 images, real disk space is not allocated, it is a virtual resize only. - 'centos_master' node role extension refactored: now pre_define() method just initialize the metadata objects (if they weren't passed from the template). - add an example template Closes-Bug:#1602298 Change-Id: If35b252964a28a4bd43f073318cee93f3dfbe907
This commit is contained in:
parent
ac9c27039a
commit
b5625c1f2f
|
@ -15,12 +15,14 @@
|
|||
import datetime
|
||||
import itertools
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from time import sleep
|
||||
import uuid
|
||||
from warnings import warn
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.functional import cached_property
|
||||
import libvirt
|
||||
|
@ -28,6 +30,7 @@ import netaddr
|
|||
|
||||
from devops.driver.libvirt.libvirt_xml_builder import LibvirtXMLBuilder
|
||||
from devops.error import DevopsError
|
||||
from devops.helpers.cloud_image_settings import generate_cloud_image_settings
|
||||
from devops.helpers.helpers import deepgetattr
|
||||
from devops.helpers.helpers import get_file_size
|
||||
from devops.helpers.helpers import underscored
|
||||
|
@ -679,6 +682,8 @@ class LibvirtVolume(Volume):
|
|||
serial = ParamField()
|
||||
wwn = ParamField()
|
||||
multipath_count = ParamField(default=0)
|
||||
cloudinit_meta_data = ParamField(default=None)
|
||||
cloudinit_user_data = ParamField(default=None)
|
||||
|
||||
@property
|
||||
def _libvirt_volume(self):
|
||||
|
@ -702,10 +707,11 @@ class LibvirtVolume(Volume):
|
|||
backing_store_path = self.backing_store.get_path()
|
||||
backing_store_format = self.backing_store.format
|
||||
|
||||
capacity = int((self.capacity or 0) * 1024 ** 3)
|
||||
if self.source_image is not None:
|
||||
capacity = get_file_size(self.source_image)
|
||||
else:
|
||||
capacity = int(self.capacity * 1024 ** 3)
|
||||
file_size = get_file_size(self.source_image)
|
||||
if file_size > capacity:
|
||||
capacity = file_size
|
||||
|
||||
pool_name = self.driver.storage_pool_name
|
||||
pool = self.driver.conn.storagePoolLookupByName(pool_name)
|
||||
|
@ -726,7 +732,7 @@ class LibvirtVolume(Volume):
|
|||
|
||||
# Upload predefined image to the volume
|
||||
if self.source_image is not None:
|
||||
self.upload(self.source_image)
|
||||
self.upload(self.source_image, capacity)
|
||||
|
||||
@retry(libvirt.libvirtError)
|
||||
def remove(self, *args, **kwargs):
|
||||
|
@ -751,7 +757,7 @@ class LibvirtVolume(Volume):
|
|||
self.format = self.get_format()
|
||||
|
||||
@retry(libvirt.libvirtError, count=2)
|
||||
def upload(self, path):
|
||||
def upload(self, path, capacity=0):
|
||||
def chunk_render(_, _size, _fd):
|
||||
return _fd.read(_size)
|
||||
|
||||
|
@ -772,6 +778,11 @@ class LibvirtVolume(Volume):
|
|||
stream.sendAll(chunk_render, fd)
|
||||
stream.finish()
|
||||
|
||||
if capacity > size:
|
||||
# Resize the uploaded image to specified capacity
|
||||
self._libvirt_volume.resize(capacity)
|
||||
self.save()
|
||||
|
||||
def get_allocation(self):
|
||||
"""Get allocated volume size
|
||||
|
||||
|
@ -837,6 +848,8 @@ class LibvirtNode(Node):
|
|||
has_vnc = ParamField(default=True)
|
||||
bootmenu_timeout = ParamField(default=0)
|
||||
numa = ParamField(default=[])
|
||||
cloud_init_volume_name = ParamField()
|
||||
cloud_init_iface_up = ParamField()
|
||||
|
||||
@property
|
||||
def _libvirt_node(self):
|
||||
|
@ -974,6 +987,9 @@ class LibvirtNode(Node):
|
|||
logger.debug(node_xml)
|
||||
self.uuid = self.driver.conn.defineXML(node_xml).UUIDString()
|
||||
|
||||
if self.cloud_init_volume_name is not None:
|
||||
self._create_cloudimage_settings_iso()
|
||||
|
||||
super(LibvirtNode, self).define()
|
||||
|
||||
def start(self):
|
||||
|
@ -1038,6 +1054,52 @@ class LibvirtNode(Node):
|
|||
def has_snapshot(self, name):
|
||||
return name in self._libvirt_node.snapshotListNames()
|
||||
|
||||
def _create_cloudimage_settings_iso(self):
|
||||
"""Builds setting iso to send basic configuration for cloud image"""
|
||||
|
||||
if self.cloud_init_volume_name is None:
|
||||
return
|
||||
volume = self.get_volume(name=self.cloud_init_volume_name)
|
||||
|
||||
interface = self.interface_set.get(
|
||||
label=self.cloud_init_iface_up)
|
||||
admin_ip = self.get_ip_address_by_network_name(
|
||||
name=None, interface=interface)
|
||||
|
||||
env_name = self.group.environment.name
|
||||
dir_path = os.path.join(settings.CLOUD_IMAGE_DIR, env_name)
|
||||
cloud_image_settings_path = os.path.join(
|
||||
dir_path, 'cloud_settings.iso')
|
||||
meta_data_path = os.path.join(dir_path, "meta-data")
|
||||
user_data_path = os.path.join(dir_path, "user-data")
|
||||
|
||||
interface_name = interface.label
|
||||
admin_ap = interface.l2_network_device.address_pool
|
||||
gateway = str(admin_ap.gateway)
|
||||
admin_netmask = str(admin_ap.ip_network.netmask)
|
||||
admin_network = str(admin_ap.ip_network)
|
||||
hostname = self.name
|
||||
|
||||
generate_cloud_image_settings(
|
||||
cloud_image_settings_path=cloud_image_settings_path,
|
||||
meta_data_path=meta_data_path,
|
||||
user_data_path=user_data_path,
|
||||
admin_network=admin_network,
|
||||
interface_name=interface_name,
|
||||
admin_ip=admin_ip,
|
||||
admin_netmask=admin_netmask,
|
||||
gateway=gateway,
|
||||
hostname=hostname,
|
||||
meta_data_content=volume.cloudinit_meta_data,
|
||||
user_data_content=volume.cloudinit_user_data,
|
||||
)
|
||||
|
||||
volume.upload(cloud_image_settings_path)
|
||||
|
||||
# Clear temporary files
|
||||
if os.path.exists(dir_path):
|
||||
shutil.rmtree(dir_path)
|
||||
|
||||
# EXTERNAL SNAPSHOT
|
||||
def snapshot_create_child_volumes(self, name):
|
||||
for disk in self.disk_devices:
|
||||
|
|
|
@ -18,10 +18,13 @@ import subprocess
|
|||
from devops import logger
|
||||
|
||||
|
||||
def generate_cloud_image_settings(cloud_image_settings_path, admin_network,
|
||||
def generate_cloud_image_settings(cloud_image_settings_path, meta_data_path,
|
||||
user_data_path, admin_network,
|
||||
interface_name, admin_ip, admin_netmask,
|
||||
gateway, dns, dns_ext,
|
||||
hostname, user, password):
|
||||
gateway,
|
||||
hostname,
|
||||
meta_data_content=None,
|
||||
user_data_content=None):
|
||||
|
||||
# create dir for meta_data, user_data and cloud_ISO
|
||||
dir_path = os.path.dirname(cloud_image_settings_path)
|
||||
|
@ -29,11 +32,6 @@ def generate_cloud_image_settings(cloud_image_settings_path, admin_network,
|
|||
if not os.path.exists(dir_path):
|
||||
os.makedirs(dir_path)
|
||||
|
||||
meta_data_path = os.path.join(dir_path,
|
||||
"meta-data")
|
||||
user_data_path = os.path.join(dir_path,
|
||||
"user-data")
|
||||
|
||||
# create meta_data and user_data
|
||||
|
||||
meta_data_context = {
|
||||
|
@ -42,21 +40,20 @@ def generate_cloud_image_settings(cloud_image_settings_path, admin_network,
|
|||
"network": admin_network,
|
||||
"netmask": admin_netmask,
|
||||
"gateway": gateway,
|
||||
"dns": dns,
|
||||
"dns_ext": dns_ext,
|
||||
"hostname": hostname
|
||||
}
|
||||
|
||||
meta_data_content = ("instance-id: iid-local1\n"
|
||||
"network-interfaces: |\n"
|
||||
" auto {interface_name}\n"
|
||||
" iface {interface_name} inet static\n"
|
||||
" address {address}\n"
|
||||
" network {network}\n"
|
||||
" netmask {netmask}\n"
|
||||
" gateway {gateway}\n"
|
||||
" dns-nameservers {dns} {dns_ext}\n"
|
||||
"local-hostname: {hostname}")
|
||||
if meta_data_content is None:
|
||||
meta_data_content = ("instance-id: iid-local1\n"
|
||||
"network-interfaces: |\n"
|
||||
" auto {interface_name}\n"
|
||||
" iface {interface_name} inet static\n"
|
||||
" address {address}\n"
|
||||
" network {network}\n"
|
||||
" netmask {netmask}\n"
|
||||
" gateway {gateway}\n"
|
||||
" dns-nameservers 8.8.8.8\n"
|
||||
"local-hostname: {hostname}")
|
||||
|
||||
logger.debug("meta_data contains next data: \n{}".format(
|
||||
meta_data_content.format(**meta_data_context)))
|
||||
|
@ -67,23 +64,22 @@ def generate_cloud_image_settings(cloud_image_settings_path, admin_network,
|
|||
user_data_context = {
|
||||
"interface_name": interface_name,
|
||||
"gateway": gateway,
|
||||
"user": user,
|
||||
"password": password
|
||||
}
|
||||
|
||||
user_data_content = ("\n#cloud-config\n"
|
||||
"ssh_pwauth: True\n"
|
||||
"chpasswd:\n"
|
||||
" list: |\n"
|
||||
" {user}:{password}\n"
|
||||
" expire: False \n\n"
|
||||
"runcmd:\n"
|
||||
" - sudo ifup {interface_name}\n"
|
||||
" - sudo sed -i -e '/^PermitRootLogin/s/^"
|
||||
".*$/PermitRootLogin yes/' /etc/ssh/sshd_config\n"
|
||||
" - sudo service ssh restart\n"
|
||||
" - sudo route add default gw "
|
||||
"{gateway} {interface_name}")
|
||||
if user_data_content is None:
|
||||
user_data_content = ("\n#cloud-config\n"
|
||||
"ssh_pwauth: True\n"
|
||||
"chpasswd:\n"
|
||||
" list: |\n"
|
||||
" root:r00tme\n"
|
||||
" expire: False\n\n"
|
||||
"runcmd:\n"
|
||||
" - sudo ifup {interface_name}\n"
|
||||
" - sudo sed -i -e '/^PermitRootLogin/s/^"
|
||||
".*$/PermitRootLogin yes/' /etc/ssh/sshd_config\n"
|
||||
" - sudo service ssh restart\n"
|
||||
" - sudo route add default gw "
|
||||
"{gateway} {interface_name}")
|
||||
|
||||
logger.debug("user_data contains next data: \n{}".format(
|
||||
user_data_content.format(**user_data_context)))
|
||||
|
|
|
@ -245,7 +245,7 @@ class Node(six.with_metaclass(ExtendableNodeType, ParamedModel, BaseModel)):
|
|||
# LEGACY, for fuel-qa compatibility
|
||||
@property
|
||||
def is_admin(self):
|
||||
return 'master' in self.role
|
||||
return 'master' in str(self.role)
|
||||
|
||||
# LEGACY, for fuel-qa compatibility
|
||||
@property
|
||||
|
|
|
@ -13,9 +13,6 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import os
|
||||
|
||||
from devops.helpers.cloud_image_settings import generate_cloud_image_settings
|
||||
from devops.helpers.helpers import wait_tcp
|
||||
from devops import logger
|
||||
from devops import settings
|
||||
|
@ -43,38 +40,51 @@ class NodeExtension(object):
|
|||
def get_kernel_cmd(self, **kwargs):
|
||||
return None
|
||||
|
||||
def post_define(self):
|
||||
"""Builds setting iso to send basic configuration for cloud image"""
|
||||
def pre_define(self):
|
||||
|
||||
cloud_image_settings_path = os.path.join(
|
||||
settings.CLOUD_IMAGE_DIR, 'cloud_settings.iso')
|
||||
admin_ip = self.node.get_ip_address_by_network_name(
|
||||
settings.SSH_CREDENTIALS['admin_network'])
|
||||
interface = self.node.get_interface_by_network_name(
|
||||
settings.SSH_CREDENTIALS['admin_network'])
|
||||
interface_name = interface.label
|
||||
user = settings.SSH_CREDENTIALS['login']
|
||||
password = settings.SSH_CREDENTIALS['password']
|
||||
admin_ap = interface.l2_network_device.address_pool
|
||||
gateway = str(admin_ap.gateway)
|
||||
admin_netmask = str(admin_ap.ip_network.netmask)
|
||||
admin_network = str(admin_ap.ip_network)
|
||||
dns = settings.DEFAULT_DNS
|
||||
dns_ext = dns
|
||||
hostname = settings.DEFAULT_MASTER_FQDN
|
||||
if self.node.cloud_init_volume_name is None:
|
||||
self.node.cloud_init_volume_name = 'iso'
|
||||
|
||||
generate_cloud_image_settings(
|
||||
cloud_image_settings_path=cloud_image_settings_path,
|
||||
admin_network=admin_network,
|
||||
interface_name=interface_name,
|
||||
admin_ip=admin_ip,
|
||||
admin_netmask=admin_netmask,
|
||||
gateway=gateway,
|
||||
dns=dns,
|
||||
dns_ext=dns_ext,
|
||||
hostname=hostname,
|
||||
user=user,
|
||||
password=password
|
||||
)
|
||||
if self.node.cloud_init_iface_up is None:
|
||||
interface = self.node.get_interface_by_network_name(
|
||||
settings.SSH_CREDENTIALS['admin_network'])
|
||||
self.node.cloud_init_iface_up = interface.label
|
||||
|
||||
self.node.get_volume(name='iso').upload(cloud_image_settings_path)
|
||||
self.node.save()
|
||||
|
||||
volume = self.node.get_volume(name=self.node.cloud_init_volume_name)
|
||||
|
||||
if volume.cloudinit_meta_data is None:
|
||||
volume.cloudinit_meta_data = (
|
||||
"instance-id: iid-local1\n"
|
||||
"network-interfaces: |\n"
|
||||
" auto {interface_name}\n"
|
||||
" iface {interface_name} inet static\n"
|
||||
" address {address}\n"
|
||||
" network {network}\n"
|
||||
" netmask {netmask}\n"
|
||||
" gateway {gateway}\n" +
|
||||
" dns-nameservers {dns}\n"
|
||||
"local-hostname: {hostname}".format(
|
||||
dns=settings.DEFAULT_DNS,
|
||||
hostname=settings.DEFAULT_MASTER_FQDN))
|
||||
|
||||
if volume.cloudinit_user_data is None:
|
||||
volume.cloudinit_user_data = (
|
||||
"\n#cloud-config\n"
|
||||
"ssh_pwauth: True\n"
|
||||
"chpasswd:\n"
|
||||
" list: |\n" +
|
||||
" {user}:{password}\n".format(
|
||||
user=settings.SSH_CREDENTIALS['login'],
|
||||
password=settings.SSH_CREDENTIALS['password']
|
||||
) +
|
||||
" expire: False\n\n"
|
||||
"runcmd:\n"
|
||||
" - sudo ifup {interface_name}\n"
|
||||
" - sudo sed -i -e '/^PermitRootLogin/s/^"
|
||||
".*$/PermitRootLogin yes/' /etc/ssh/sshd_config\n"
|
||||
" - sudo service ssh restart\n"
|
||||
" - sudo route add default gw "
|
||||
"{gateway} {interface_name}")
|
||||
volume.save()
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
# Copyright 2016 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 collections
|
||||
|
||||
import mock
|
||||
|
||||
from devops.models import Environment
|
||||
from devops.tests.driver.libvirt.base import LibvirtTestCase
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class TestCloudImage(LibvirtTestCase):
|
||||
|
||||
def patch(self, *args, **kwargs):
|
||||
patcher = mock.patch(*args, **kwargs)
|
||||
m = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
return m
|
||||
|
||||
def setUp(self):
|
||||
super(TestCloudImage, self).setUp()
|
||||
|
||||
self.open_mock = mock.mock_open(read_data='image_data')
|
||||
self.patch('devops.driver.libvirt.libvirt_driver.open',
|
||||
self.open_mock, create=True)
|
||||
|
||||
self.os_mock = self.patch('devops.helpers.helpers.os')
|
||||
Size = collections.namedtuple('Size', ['st_size'])
|
||||
self.file_sizes = {
|
||||
'/tmp/test/cloud_settings.iso': Size(st_size=1 * 1024 ** 3),
|
||||
}
|
||||
self.os_mock.stat.side_effect = self.file_sizes.get
|
||||
|
||||
self.generate_cloud_image_settings_mock = self.patch(
|
||||
'devops.driver.libvirt.libvirt_driver'
|
||||
'.generate_cloud_image_settings')
|
||||
|
||||
self.volume_upload_mock = self.patch(
|
||||
'devops.driver.libvirt.Volume.upload')
|
||||
|
||||
# Environment with a 'public' network
|
||||
|
||||
self.env = Environment.create('test')
|
||||
self.group = self.env.add_group(
|
||||
group_name='test_group',
|
||||
driver_name='devops.driver.libvirt',
|
||||
connection_string='test:///default',
|
||||
storage_pool_name='default-pool')
|
||||
|
||||
self.pub_ap = self.env.add_address_pool(
|
||||
name='public-pool01', net='10.109.0.0/16:24', tag=0,
|
||||
ip_reserved=dict(gateway=1, l2_network_device=1),
|
||||
ip_ranges=dict(default=[2, -2]))
|
||||
self.group.add_l2_network_device(
|
||||
name='public', address_pool='public-pool01')
|
||||
|
||||
# Node connected to the 'public' network
|
||||
|
||||
self.node = self.group.add_node(
|
||||
name='test-node',
|
||||
role='cloud-node',
|
||||
architecture='x86_64',
|
||||
hypervisor='test',
|
||||
cloud_init_volume_name='iso',
|
||||
cloud_init_iface_up='enp0s3')
|
||||
|
||||
self.system_volume = self.node.add_volume(name='system')
|
||||
self.iso_volume = self.node.add_volume(name='iso')
|
||||
|
||||
self.adm_iface = self.node.add_interface(
|
||||
label='enp0s3',
|
||||
l2_network_device_name='public',
|
||||
mac_address='64:b6:87:44:14:17',
|
||||
interface_model='e1000')
|
||||
|
||||
self.node.add_network_config(
|
||||
label='enp0s3',
|
||||
networks=['public'])
|
||||
|
||||
@mock.patch('devops.driver.libvirt.libvirt_driver.uuid')
|
||||
@mock.patch('libvirt.virConnect.defineXML')
|
||||
@mock.patch.multiple(settings, CLOUD_IMAGE_DIR='/tmp/')
|
||||
def test_post_define(self, define_xml_mock, uuid_mock):
|
||||
uuid_mock.uuid4.side_effect = (
|
||||
mock.Mock(hex='fe527bd28e0f4a84b9117dc97142c580'),
|
||||
mock.Mock(hex='9cddb80fe82e480eb14c1a89f1c0e11d'),
|
||||
mock.Mock(hex='885674d28e0f4a84b265625673674565'),
|
||||
mock.Mock(hex='91252350fe82e480eb14c1a89f1c0234'))
|
||||
define_xml_mock.return_value.UUIDString.return_value = 'fake_uuid'
|
||||
|
||||
self.system_volume.define()
|
||||
self.iso_volume.define()
|
||||
self.node.define()
|
||||
|
||||
self.generate_cloud_image_settings_mock.assert_called_once_with(
|
||||
admin_ip='10.109.0.2',
|
||||
admin_netmask='255.255.255.0',
|
||||
admin_network='10.109.0.0/24',
|
||||
cloud_image_settings_path='/tmp/test/cloud_settings.iso',
|
||||
meta_data_content=None,
|
||||
meta_data_path='/tmp/test/meta-data',
|
||||
user_data_content=None,
|
||||
user_data_path='/tmp/test/user-data',
|
||||
gateway='10.109.0.1',
|
||||
hostname='test-node',
|
||||
interface_name='enp0s3')
|
||||
|
||||
self.volume_upload_mock.assert_called_once_with(
|
||||
'/tmp/test/cloud_settings.iso')
|
|
@ -50,12 +50,11 @@ class TestCloudImageSettings(unittest.TestCase):
|
|||
admin_netmask='255.255.255.0',
|
||||
admin_network='10.109.0.0/24',
|
||||
cloud_image_settings_path='/mydir/cloud_settings.iso',
|
||||
dns='8.8.8.8', dns_ext='8.8.8.8',
|
||||
meta_data_path='/mydir/meta-data',
|
||||
user_data_path='/mydir/user-data',
|
||||
gateway='10.109.0.1',
|
||||
hostname='nailgun.domain.local',
|
||||
interface_name=u'enp0s3',
|
||||
password='r00tme',
|
||||
user='root')
|
||||
interface_name=u'enp0s3')
|
||||
|
||||
self.os_mock.makedirs.assert_called_once_with('/mydir')
|
||||
|
||||
|
@ -71,7 +70,7 @@ class TestCloudImageSettings(unittest.TestCase):
|
|||
' network 10.109.0.0/24\n'
|
||||
' netmask 255.255.255.0\n'
|
||||
' gateway 10.109.0.1\n'
|
||||
' dns-nameservers 8.8.8.8 8.8.8.8\n'
|
||||
' dns-nameservers 8.8.8.8\n'
|
||||
'local-hostname: nailgun.domain.local'),
|
||||
mock.call().__exit__(None, None, None),
|
||||
mock.call('/mydir/user-data', 'w'),
|
||||
|
@ -83,7 +82,7 @@ class TestCloudImageSettings(unittest.TestCase):
|
|||
"chpasswd:\n"
|
||||
" list: |\n"
|
||||
" root:r00tme\n"
|
||||
" expire: False \n"
|
||||
" expire: False\n"
|
||||
"\n"
|
||||
"runcmd:\n"
|
||||
" - sudo ifup enp0s3\n"
|
||||
|
|
|
@ -12,13 +12,16 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import collections
|
||||
|
||||
import mock
|
||||
|
||||
from devops import settings
|
||||
from devops.tests.driver.driverless import DriverlessTestCase
|
||||
from devops.models import Environment
|
||||
from devops.tests.driver.libvirt.base import LibvirtTestCase
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class TestCentosMasterExt(DriverlessTestCase):
|
||||
class TestCentosMasterExt(LibvirtTestCase):
|
||||
|
||||
def patch(self, *args, **kwargs):
|
||||
patcher = mock.patch(*args, **kwargs)
|
||||
|
@ -29,17 +32,46 @@ class TestCentosMasterExt(DriverlessTestCase):
|
|||
def setUp(self):
|
||||
super(TestCentosMasterExt, self).setUp()
|
||||
|
||||
self.open_mock = mock.mock_open(read_data='image_data')
|
||||
self.patch('devops.driver.libvirt.libvirt_driver.open',
|
||||
self.open_mock, create=True)
|
||||
|
||||
self.os_mock = self.patch('devops.helpers.helpers.os')
|
||||
Size = collections.namedtuple('Size', ['st_size'])
|
||||
self.file_sizes = {
|
||||
'/tmp/test/cloud_settings.iso': Size(st_size=1 * 1024 ** 3),
|
||||
}
|
||||
self.os_mock.stat.side_effect = self.file_sizes.get
|
||||
|
||||
# Environment with an 'admin' network
|
||||
|
||||
self.env = Environment.create('test')
|
||||
self.group = self.env.add_group(
|
||||
group_name='test_group',
|
||||
driver_name='devops.driver.libvirt',
|
||||
connection_string='test:///default',
|
||||
storage_pool_name='default-pool')
|
||||
|
||||
self.pub_ap = self.env.add_address_pool(
|
||||
name='admin-pool01', net='10.109.0.0/16:24', tag=0,
|
||||
ip_reserved=dict(gateway=1, l2_network_device=1),
|
||||
ip_ranges=dict(default=[2, -2]))
|
||||
self.group.add_l2_network_device(
|
||||
name='admin', address_pool='admin-pool01')
|
||||
|
||||
self.node = self.group.add_node(
|
||||
name='test-node',
|
||||
role='centos_master')
|
||||
self.node.add_volume(
|
||||
name='system')
|
||||
self.node.add_volume(
|
||||
name='iso')
|
||||
role='centos_master',
|
||||
architecture='x86_64',
|
||||
hypervisor='test')
|
||||
|
||||
self.system_volume = self.node.add_volume(name='system')
|
||||
self.iso_volume = self.node.add_volume(name='iso')
|
||||
|
||||
self.adm_iface = self.node.add_interface(
|
||||
label='enp0s3',
|
||||
l2_network_device_name='admin',
|
||||
mac_address='64:c6:27:47:14:83',
|
||||
interface_model='e1000')
|
||||
|
||||
self.node.add_network_config(
|
||||
|
@ -49,37 +81,63 @@ class TestCentosMasterExt(DriverlessTestCase):
|
|||
self.wait_tcp_mock = self.patch(
|
||||
'devops.models.node_ext.centos_master.wait_tcp')
|
||||
|
||||
self.generate_cloud_image_settings_mock = self.patch(
|
||||
'devops.models.node_ext.'
|
||||
'centos_master.generate_cloud_image_settings')
|
||||
@mock.patch('devops.driver.libvirt.libvirt_driver.uuid')
|
||||
@mock.patch('libvirt.virConnect.defineXML')
|
||||
@mock.patch.multiple(settings, CLOUD_IMAGE_DIR='/tmp/')
|
||||
def test_001_pre_define(self, define_xml_mock, uuid_mock):
|
||||
uuid_mock.uuid4.side_effect = (
|
||||
mock.Mock(hex='fe527bd28e0f4a84b9117dc97142c580'),
|
||||
mock.Mock(hex='9cddb80fe82e480eb14c1a89f1c0e11d'),
|
||||
mock.Mock(hex='885674d28e0f4a84b265625673674565'),
|
||||
mock.Mock(hex='91252350fe82e480eb14c1a89f1c0234'))
|
||||
define_xml_mock.return_value.UUIDString.return_value = 'fake_uuid'
|
||||
|
||||
self.volume_upload_mock = self.patch(
|
||||
'devops.models.volume.Volume.upload', create=True)
|
||||
|
||||
@mock.patch.multiple(settings, CLOUD_IMAGE_DIR='/mydir/')
|
||||
def test_post_define(self):
|
||||
self.system_volume.define()
|
||||
self.iso_volume.define()
|
||||
self.node.define()
|
||||
self.generate_cloud_image_settings_mock.assert_called_once_with(
|
||||
admin_ip='10.109.0.2',
|
||||
admin_netmask='255.255.255.0',
|
||||
admin_network='10.109.0.0/24',
|
||||
cloud_image_settings_path='/mydir/cloud_settings.iso',
|
||||
dns='8.8.8.8', dns_ext='8.8.8.8',
|
||||
gateway='10.109.0.1',
|
||||
hostname='nailgun.domain.local',
|
||||
interface_name='enp0s3',
|
||||
password='r00tme',
|
||||
user='root')
|
||||
self.volume_upload_mock.assert_called_once_with(
|
||||
'/mydir/cloud_settings.iso')
|
||||
|
||||
def test_deploy_wait(self):
|
||||
assert self.node.cloud_init_iface_up == 'enp0s3'
|
||||
|
||||
assert self.node.cloud_init_volume_name == 'iso'
|
||||
|
||||
volume = self.node.get_volume(
|
||||
name=self.node.cloud_init_volume_name)
|
||||
|
||||
assert volume.cloudinit_meta_data == (
|
||||
"instance-id: iid-local1\n"
|
||||
"network-interfaces: |\n"
|
||||
" auto {interface_name}\n"
|
||||
" iface {interface_name} inet static\n"
|
||||
" address {address}\n"
|
||||
" network {network}\n"
|
||||
" netmask {netmask}\n"
|
||||
" gateway {gateway}\n"
|
||||
" dns-nameservers 8.8.8.8\n"
|
||||
"local-hostname: nailgun.domain.local")
|
||||
|
||||
assert volume.cloudinit_user_data == (
|
||||
"\n#cloud-config\n"
|
||||
"ssh_pwauth: True\n"
|
||||
"chpasswd:\n"
|
||||
" list: |\n"
|
||||
" root:r00tme\n"
|
||||
" expire: False\n\n"
|
||||
"runcmd:\n"
|
||||
" - sudo ifup {interface_name}\n"
|
||||
" - sudo sed -i -e '/^PermitRootLogin/s/^.*$/PermitRootLogin yes/'"
|
||||
" /etc/ssh/sshd_config\n"
|
||||
" - sudo service ssh restart\n"
|
||||
" - sudo route add default gw {gateway} {interface_name}")
|
||||
|
||||
def test_002_deploy_wait(self):
|
||||
self.node.ext.deploy_wait()
|
||||
|
||||
def test_get_kernel_cmd(self):
|
||||
def test_003_get_kernel_cmd(self):
|
||||
assert self.node.ext.get_kernel_cmd() is None
|
||||
|
||||
def test_bootstrap_and_wait(self):
|
||||
@mock.patch('devops.driver.libvirt.Node.is_active')
|
||||
def test_004_bootstrap_and_wait(self, is_active_mock):
|
||||
is_active_mock.return_value = True
|
||||
self.node.ext.bootstrap_and_wait()
|
||||
self.wait_tcp_mock.assert_called_once_with(
|
||||
host='10.109.0.2', port=22, timeout=600,
|
||||
|
|
|
@ -0,0 +1,182 @@
|
|||
.. _cloudinit_example.yaml:
|
||||
|
||||
YAML template for cloud-init nodes
|
||||
==================================
|
||||
|
||||
This template can be used for nodes that are started from Ubuntu
|
||||
cloud images (or CentOS, with necessary changes in cloudinit_user_data).
|
||||
|
||||
The following set of cloudinit_user_data and cloudinit_meta_data is an
|
||||
example, change it for your purposes.
|
||||
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
---
|
||||
aliases:
|
||||
dynamic_addresses_pool:
|
||||
- &pool_default !os_env POOL_DEFAULT, 10.10.0.0/16:24
|
||||
|
||||
default_interface_model:
|
||||
- &interface_model !os_env INTERFACE_MODEL, e1000
|
||||
|
||||
template:
|
||||
devops_settings:
|
||||
env_name: !os_env ENV_NAME
|
||||
|
||||
address_pools:
|
||||
public-pool01:
|
||||
net: *pool_default
|
||||
params:
|
||||
vlan_start: 1210
|
||||
ip_reserved:
|
||||
gateway: +1
|
||||
l2_network_device: +1
|
||||
ip_ranges:
|
||||
dhcp: [+128, -32]
|
||||
rack-01: [+2, +127]
|
||||
private-pool01:
|
||||
net: *pool_default
|
||||
storage-pool01:
|
||||
net: *pool_default
|
||||
management-pool01:
|
||||
net: *pool_default
|
||||
|
||||
groups:
|
||||
- name: default
|
||||
driver:
|
||||
name: devops.driver.libvirt
|
||||
params:
|
||||
connection_string: !os_env CONNECTION_STRING, qemu:///system
|
||||
storage_pool_name: !os_env STORAGE_POOL_NAME, default
|
||||
stp: False
|
||||
hpet: False
|
||||
use_host_cpu: !os_env DRIVER_USE_HOST_CPU, true
|
||||
|
||||
network_pools:
|
||||
public: public-pool01
|
||||
private: private-pool01
|
||||
storage: storage-pool01
|
||||
management: management-pool01
|
||||
|
||||
l2_network_devices:
|
||||
public:
|
||||
address_pool: public-pool01
|
||||
dhcp: true
|
||||
forward:
|
||||
mode: nat
|
||||
|
||||
storage:
|
||||
address_pool: storage-pool01
|
||||
dhcp: false
|
||||
|
||||
management:
|
||||
address_pool: management-pool01
|
||||
dhcp: false
|
||||
|
||||
private:
|
||||
address_pool: private-pool01
|
||||
dhcp: false
|
||||
|
||||
nodes:
|
||||
- name: node-1
|
||||
role: k8s
|
||||
params: &rack-01-node-params
|
||||
vcpu: !os_env SLAVE_NODE_CPU, 2
|
||||
memory: !os_env SLAVE_NODE_MEMORY, 2048
|
||||
boot:
|
||||
- hd
|
||||
cloud_init_volume_name: iso
|
||||
cloud_init_iface_up: enp0s3
|
||||
volumes:
|
||||
- name: system
|
||||
capacity: !os_env NODE_VOLUME_SIZE, 50
|
||||
source_image: !os_env CLOUD_IMAGE_PATH # https://cloud-images.ubuntu.com/xenial/current/xenial-server-cloudimg-amd64-disk1.img
|
||||
format: qcow2
|
||||
- name: iso # Volume with name 'iso' will be used
|
||||
# for store image with cloud-init metadata.
|
||||
capacity: 1
|
||||
format: raw
|
||||
device: cdrom
|
||||
bus: ide
|
||||
cloudinit_meta_data: |
|
||||
# All the data below will be stored as a string object
|
||||
instance-id: iid-local1
|
||||
local-hostname: {hostname}
|
||||
network-interfaces: |
|
||||
auto {interface_name}
|
||||
iface {interface_name} inet static
|
||||
address {address}
|
||||
network {network}
|
||||
netmask {netmask}
|
||||
gateway {gateway}
|
||||
dns-nameservers 8.8.8.8
|
||||
|
||||
cloudinit_user_data: |
|
||||
#cloud-config, see http://cloudinit.readthedocs.io/en/latest/topics/examples.html
|
||||
# All the data below will be stored as a string object
|
||||
|
||||
ssh_pwauth: True
|
||||
users:
|
||||
- name: vagrant
|
||||
sudo: ALL=(ALL) NOPASSWD:ALL
|
||||
shell: /bin/bash
|
||||
chpasswd:
|
||||
list: |
|
||||
vagrant:vagrant
|
||||
expire: False
|
||||
|
||||
bootcmd:
|
||||
# Block access to SSH while node is preparing
|
||||
- cloud-init-per once sudo iptables -A INPUT -p tcp --dport 22 -j DROP
|
||||
runcmd:
|
||||
# Prepare network connection
|
||||
- sudo ifup {interface_name}
|
||||
- sudo route add default gw {gateway} {interface_name}
|
||||
|
||||
# Prepare necessary packages on the node
|
||||
- sudo apt-get update
|
||||
- sudo apt-get upgrade -y
|
||||
- sudo apt-get install -y git python-setuptools python-dev python-pip gcc libssl-dev libffi-dev vim software-properties-common
|
||||
- sudo apt-get autoremove -y
|
||||
- sudo pip install -U setuptools pip
|
||||
- sudo pip install 'cryptography>=1.3.2'
|
||||
- sudo pip install 'cffi>=1.6.0'
|
||||
|
||||
# Node is ready, allow SSH access
|
||||
- sudo iptables -D INPUT -p tcp --dport 22 -j DROP
|
||||
|
||||
interfaces:
|
||||
- label: enp0s3
|
||||
l2_network_device: public
|
||||
interface_model: *interface_model
|
||||
- label: enp0s4
|
||||
l2_network_device: private
|
||||
interface_model: *interface_model
|
||||
- label: enp0s5
|
||||
l2_network_device: storage
|
||||
interface_model: *interface_model
|
||||
- label: enp0s6
|
||||
l2_network_device: management
|
||||
interface_model: *interface_model
|
||||
network_config:
|
||||
enp0s3:
|
||||
networks:
|
||||
- public
|
||||
enp0s4:
|
||||
networks:
|
||||
- private
|
||||
enp0s5:
|
||||
networks:
|
||||
- storage
|
||||
enp0s6:
|
||||
networks:
|
||||
- management
|
||||
|
||||
- name: node-2
|
||||
role: k8s
|
||||
params: *rack-01-node-params
|
||||
|
||||
- name: node-3
|
||||
role: k8s
|
||||
params: *rack-01-node-params
|
Loading…
Reference in New Issue