提交 891cd495 编写于 作者: C Cleber Rosa

avocado.utils: introduce cloudinit module

This addition to the utilities library facilitates the use of the
cloudinit features inside Linux OSs images for the cloud.

For now, it eases the creation of "cidata" ISOs, containing
configuration to cloud-init, and also a simple "phone home"
server.
Signed-off-by: NCleber Rosa <crosa@redhat.com>
上级 5f194347
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See LICENSE for more details.
#
# Copyright: Red Hat Inc. 2018
# Author: Cleber Rosa <crosa@redhat.com>
"""
cloudinit configuration support
This module can be easily used with :mod:`avocado.utils.vmimage`,
to configure operating system images via the cloudinit tooling.
"""
from six.moves import BaseHTTPServer
from . import astring
from . import iso9660
#: The meta-data file template
#: Positional template variables are: instance-id, hostname
METADATA_TEMPLATE = """instance-id: {0}
hostname: {1}
"""
#: The header expected to be found at the beginning of the user-data file
USERDATA_HEADER = "#cloud-config"
#: A password configuration as per cloudinit/config/cc_set_passwords.py
#: Positional template variables are: username, password
PASSWORD_TEMPLATE = """
ssh_pwauth: True
system_info:
default_user:
name: {0}
password: {1}
chpasswd:
expire: False
"""
#: A phone home configuration that will post just the instance id
#: Positional template variables are: address, port
PHONE_HOME_TEMPLATE = """
phone_home:
url: http://{0}:{1}/$INSTANCE_ID/
post: [ instance_id ]
"""
def iso(output_path, instance_id, username=None, password=None,
phone_home_host=None, phone_home_port=None):
"""
Generates an ISO image with cloudinit configuration
The content always include the cloudinit metadata, and optionally
the userdata content. On the userdata file, it may contain a
username/password section (if both parameters are given) and/or a
phone home section (if both host and port are given).
:param output_path: the location of the resulting (to be created) ISO
image containing the cloudinit configuration
:param instance_id: the ID of the cloud instance, a form of identification
for the dynamically created executing instances
:param username: the username to be used when logging interactively on the
instance
:param password: the password to be used along with username when
authenticating with the login services on the instance
:param phone_home_host: the address of the host the instance
should contact once it has finished
booting
:param phone_home_port: the port acting as an HTTP phone home
server that the instance should contact
once it has finished booting
:raises: RuntimeError if the system can not create ISO images. On such
a case, user is expected to install supporting packages, such as
pycdlib.
"""
out = iso9660.iso9660(output_path, ["create", "write"])
if out is None:
raise RuntimeError("The system lacks support for creating ISO images")
out.create(flags={"interchange_level": 3, "joliet": 3, "vol_ident": 'cidata'})
metadata = METADATA_TEMPLATE.format(instance_id,
instance_id).encode(astring.ENCODING)
out.write("/meta-data", metadata)
userdata = USERDATA_HEADER
if username and password:
userdata += PASSWORD_TEMPLATE.format(username, password)
if phone_home_host and phone_home_port:
userdata += PHONE_HOME_TEMPLATE.format(phone_home_host, phone_home_port)
out.write("/user-data", userdata.encode(astring.ENCODING))
out.close()
class PhoneHomeServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def do_POST(self):
path = self.path[1:]
if path[-1] == '/':
path = path[:-1]
if path == self.server.instance_id:
self.server.instance_phoned_back = True
self.send_response(200)
def log_message(self, format_, *args):
pass
class PhoneHomeServer(BaseHTTPServer.HTTPServer):
def __init__(self, address, instance_id):
BaseHTTPServer.HTTPServer.__init__(self, address, PhoneHomeServerHandler)
self.instance_id = instance_id
self.instance_phoned_back = False
def wait_for_phone_home(address, instance_id):
"""
Sets up a phone home server and waits for the given instance to call
This is a shorthand for setting up a server that will keep handling
requests, until it has heard from the specific instance requested.
:param address: a hostname or IP address and port, in the same format
given to socket and other servers
:type address: tuple
:param instance_id: the identification for the instance that should be
calling back, and the condition for the wait to end
:type instance_id: str
"""
s = PhoneHomeServer(address, instance_id)
while not s.instance_phoned_back:
s.handle_request()
import os
import shutil
import tempfile
import threading
import unittest # pylint: disable=C0411
try:
from unittest import mock
except ImportError:
import mock
from six.moves import http_client
from avocado.utils import cloudinit
from avocado.utils import iso9660
from avocado.utils import network
from avocado.utils import data_factory
def has_iso_create_write():
return iso9660.iso9660(os.devnull, ["create", "write"]) is not None
class CloudInit(unittest.TestCase):
def test_iso_no_create_write(self):
with mock.patch('avocado.utils.iso9660.iso9660', return_value=None):
self.assertRaises(RuntimeError, cloudinit.iso, os.devnull, "INSTANCE_ID")
class CloudInitISO(unittest.TestCase):
def setUp(self):
self.tmpdir = tempfile.mkdtemp(prefix="avocado_" + __name__)
@unittest.skipUnless(has_iso_create_write(),
"system lacks support for creating ISO images")
def test_iso_no_phone_home(self):
path = os.path.join(self.tmpdir, "cloudinit.iso")
instance_id = b"INSTANCE_ID"
username = b"AVOCADO_USER"
password = b"AVOCADO_PASSWORD"
cloudinit.iso(path, instance_id, username, password)
iso = iso9660.iso9660(path)
self.assertIn(instance_id, iso.read("/meta-data"))
user_data = iso.read("/user-data")
self.assertIn(username, user_data)
self.assertIn(password, user_data)
def tearDown(self):
shutil.rmtree(self.tmpdir)
class PhoneHome(unittest.TestCase):
def test_phone_home(self):
instance_id = data_factory.generate_random_string(12)
address = '127.0.0.1'
port = network.find_free_port(address=address)
server = cloudinit.PhoneHomeServer((address, port), instance_id)
self.assertFalse(server.instance_phoned_back)
server_thread = threading.Thread(target=server.handle_request)
server_thread.start()
conn = http_client.HTTPConnection(address, port)
# contact the wrong url, and check the server does not
# consider it a call back from the expected caller
conn.request('POST', '/BAD_INSTANCE_ID')
try:
conn.getresponse()
except:
pass
self.assertFalse(server.instance_phoned_back)
conn.close()
# now the right url
server_thread = threading.Thread(target=server.handle_request)
server_thread.start()
conn = http_client.HTTPConnection(address, port)
conn.request('POST', '/' + instance_id)
try:
conn.getresponse()
except:
pass
self.assertTrue(server.instance_phoned_back)
conn.close()
server.server_close()
if __name__ == '__main__':
unittest.main()
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册