未验证 提交 e9768393 编写于 作者: C Caio Carrara

Merge remote-tracking branch 'clebergnu/cloudinit_v2'

Signed-off-by: NCaio Carrara <ccarrara@redhat.com>
# 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.
先完成此消息的编辑!
想要评论请 注册