141 lines
5.0 KiB
Python
141 lines
5.0 KiB
Python
# 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.
|
|
|
|
"""Helpers for building configdrive compatible with the Bare Metal service."""
|
|
|
|
import base64
|
|
import contextlib
|
|
import gzip
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
|
|
import six
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def populate_directory(metadata, user_data=None, versions=None,
|
|
network_data=None):
|
|
"""Populate a directory with configdrive files.
|
|
|
|
:param dict metadata: Metadata.
|
|
:param bytes user_data: Vendor-specific user data.
|
|
:param versions: List of metadata versions to support.
|
|
:param dict network_data: Networking configuration.
|
|
:return: a context manager yielding a directory with files
|
|
"""
|
|
d = tempfile.mkdtemp()
|
|
versions = versions or ('2012-08-10', 'latest')
|
|
try:
|
|
for version in versions:
|
|
subdir = os.path.join(d, 'openstack', version)
|
|
if not os.path.exists(subdir):
|
|
os.makedirs(subdir)
|
|
|
|
with open(os.path.join(subdir, 'meta_data.json'), 'w') as fp:
|
|
json.dump(metadata, fp)
|
|
|
|
if network_data:
|
|
with open(os.path.join(subdir, 'network_data.json'),
|
|
'w') as fp:
|
|
json.dump(network_data, fp)
|
|
|
|
if user_data:
|
|
# Strictly speaking, user data is binary, but in many cases
|
|
# it's actually a text (cloud-init, ignition, etc).
|
|
flag = 't' if isinstance(user_data, six.text_type) else 'b'
|
|
with open(os.path.join(subdir, 'user_data'),
|
|
'w%s' % flag) as fp:
|
|
fp.write(user_data)
|
|
|
|
yield d
|
|
finally:
|
|
shutil.rmtree(d)
|
|
|
|
|
|
def build(metadata, user_data=None, versions=None, network_data=None):
|
|
"""Make a configdrive compatible with the Bare Metal service.
|
|
|
|
Requires the genisoimage utility to be available.
|
|
|
|
:param dict metadata: Metadata.
|
|
:param user_data: Vendor-specific user data.
|
|
:param versions: List of metadata versions to support.
|
|
:param dict network_data: Networking configuration.
|
|
:return: configdrive contents as a base64-encoded string.
|
|
"""
|
|
with populate_directory(metadata, user_data, versions,
|
|
network_data) as path:
|
|
return pack(path)
|
|
|
|
|
|
def pack(path):
|
|
"""Pack a directory with files into a Bare Metal service configdrive.
|
|
|
|
Creates an ISO image with the files and label "config-2".
|
|
|
|
:param str path: Path to directory with files
|
|
:return: configdrive contents as a base64-encoded string.
|
|
"""
|
|
with tempfile.NamedTemporaryFile() as tmpfile:
|
|
# NOTE(toabctl): Luckily, genisoimage, mkisofs and xorrisofs understand
|
|
# the same parameters which are currently used.
|
|
cmds = ['genisoimage', 'mkisofs', 'xorrisofs']
|
|
for c in cmds:
|
|
try:
|
|
p = subprocess.Popen([c,
|
|
'-o', tmpfile.name,
|
|
'-ldots', '-allow-lowercase',
|
|
'-allow-multidot', '-l',
|
|
'-publisher', 'metalsmith',
|
|
'-quiet', '-J',
|
|
'-r', '-V', 'config-2',
|
|
path],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
except OSError as e:
|
|
error = e
|
|
else:
|
|
error = None
|
|
break
|
|
|
|
if error:
|
|
raise RuntimeError(
|
|
'Error generating the configdrive. Make sure the '
|
|
'"genisoimage", "mkisofs" or "xorrisofs" tool is installed. '
|
|
'Error: %s' % error)
|
|
|
|
stdout, stderr = p.communicate()
|
|
if p.returncode != 0:
|
|
raise RuntimeError(
|
|
'Error generating the configdrive.'
|
|
'Stdout: "%(stdout)s". Stderr: "%(stderr)s"' %
|
|
{'stdout': stdout, 'stderr': stderr})
|
|
|
|
tmpfile.seek(0)
|
|
|
|
with tempfile.NamedTemporaryFile() as tmpzipfile:
|
|
with gzip.GzipFile(fileobj=tmpzipfile, mode='wb') as gz_file:
|
|
shutil.copyfileobj(tmpfile, gz_file)
|
|
|
|
tmpzipfile.seek(0)
|
|
cd = base64.b64encode(tmpzipfile.read())
|
|
|
|
# NOTE(dtantsur): Ironic expects configdrive to be a string, but base64
|
|
# returns bytes on Python 3.
|
|
if not isinstance(cd, six.string_types):
|
|
cd = cd.decode('utf-8')
|
|
|
|
return cd
|