impuls/lib/python3.11/site-packages/openstack/baremetal/configdrive.py

174 lines
5.5 KiB
Python
Raw Normal View History

# 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
@contextlib.contextmanager
def populate_directory(
metadata,
user_data=None,
versions=None,
network_data=None,
vendor_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.
:param dict vendor_data: Extra supplied vendor data.
: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 vendor_data:
with open(
os.path.join(subdir, 'vendor_data2.json'), 'w'
) as fp:
json.dump(vendor_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, str) 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,
vendor_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.
:param dict vendor_data: Extra supplied vendor data.
:return: configdrive contents as a base64-encoded string.
"""
with populate_directory(
metadata, user_data, versions, network_data, vendor_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, str):
cd = cd.decode('utf-8')
return cd