Files
distcloud/distributedcloud/dccommon/subcloud_enrollment.py
Gustavo Pereira 8cd1a2672d Implement subcloud deploy backend skeleton
This commit creates the structure to handle all enroll
related tasks.

Test Plan:

PASS: Create a subcloud with add command passing --enroll
as a parameter. Verify that the subcloud deploy status is
set as enroll-complete.

PASS: Create a subcloud with subcloud deploy create
command and run deploy enroll command after subcloud
is created. Verify that subcloud deploy status is set
as enroll-complete.

PASS: Run subcloud deploy create with install and
bootstrap values, then run subcloud deploy enroll
without providing install and bootstrap values.
Verify the deploy status is set to enroll-complete.

Story: 2011100
Task: 50137

Change-Id: I835253ade3d2ac0ff7aa2f1d08768269adc61d41
Signed-off-by: Gustavo Pereira <gustavo.lyrapereira@windriver.com>
2024-06-06 01:19:22 -03:00

234 lines
8.6 KiB
Python

# Copyright (c) 2024 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import hashlib
import os
import shutil
import tempfile
import yaml
from eventlet.green import subprocess
from oslo_config import cfg
from oslo_log import log as logging
from dccommon import consts
from dccommon import exceptions
from dccommon.subcloud_install import SubcloudInstall
from dccommon import utils as dccommon_utils
from dcmanager.common import consts as dcmanager_consts
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
SUBCLOUD_ISO_PATH = '/opt/platform/iso'
class SubcloudEnrollmentInit(object):
"""Class to encapsulate the subcloud enrollment init operations.
These operations are necessary to prepare a standalone node for
subcloud enrollment. The enrollment initialization is performed
in the following order:
1. prep:
- Creates required directories
- Initiates cloud-init config files and seed iso generation
- Creates RVMC config
2. enroll_init:
- Invokes the install playbook to run the RVMC script/insert
the generated ISO and reconfigure the standalone node
3. cleanup:
- Removes generated iso
"""
def __init__(self, subcloud_name):
self.sysinv_client = SubcloudInstall.get_sysinv_client()
self.name = subcloud_name
self.www_root = None
self.iso_dir_path = None
self.seed_iso_path = None
self.https_enabled = None
def get_https_enabled(self):
if self.https_enabled is None:
system = self.sysinv_client.get_system()
self.https_enabled = system.capabilities.get('https_enabled',
False)
return self.https_enabled
def _build_seed_network_config(self, path, iso_values):
if not os.path.isdir(path):
msg = f'No directory exists: {path}'
raise exceptions.EnrollInitExecutionFailed(reason=msg)
# TODO(srana): Investigate other bootstrap / install values
# that would need to be covered here.
network_cloud_config = [
{
'type': 'physical',
'name': iso_values['install_values']['bootstrap_interface'],
'subnets': [
{
'type': 'static',
'address': iso_values['external_oam_floating_address'],
'netmask': iso_values['install_values']['network_mask'],
'gateway': iso_values['external_oam_gateway_address'],
}
]
}
]
network_config_file = os.path.join(path, 'network-config')
with open(network_config_file, 'w') as f_out_network_config_file:
contents = {'version': 1, 'config': network_cloud_config}
f_out_network_config_file.write(yaml.dump(contents,
default_flow_style=False,
sort_keys=False))
return True
def _build_seed_user_config(self, path, iso_values):
if not os.path.isdir(path):
msg = f'No directory exists: {path}'
raise exceptions.EnrollInitExecutionFailed(reason=msg)
hashed_password = hashlib.sha256(
iso_values['admin_password'].encode()).hexdigest()
account_config = {
'list': [f'sysadmin:{hashed_password}'],
'expire': 'False'
}
user_data_file = os.path.join(path, 'user-data')
with open(user_data_file, 'w') as f_out_user_data_file:
contents = {'chpasswd': account_config}
f_out_user_data_file.writelines('#cloud-config\n')
f_out_user_data_file.write(yaml.dump(contents,
default_flow_style=False,
sort_keys=False))
return True
def _generate_seed_iso(self, payload):
temp_seed_data_dir = tempfile.mkdtemp(prefix='seed_')
LOG.info(f'Preparing seed iso generation for {self.name}')
# TODO(srana): After integration, extract required bootstrap and install
# into iso_values. For now, pass in payload.
try:
# Generate seed cloud-config files
self._build_seed_network_config(temp_seed_data_dir, payload)
self._build_seed_user_config(temp_seed_data_dir, payload)
except Exception as e:
LOG.exception(f'Unable to generate seed config files '
f'for {self.name}: {e}')
shutil.rmtree(temp_seed_data_dir)
return False
gen_seed_iso_command = [
"genisoimage",
"-o", self.seed_iso_path,
"-volid", "CIDATA",
"-untranslated-filenames",
"-joliet",
"-rock",
"-iso-level", "2",
temp_seed_data_dir
]
LOG.info(f'Running gen_seed_iso_command '
f'for {self.name}: {gen_seed_iso_command}')
result = subprocess.run(gen_seed_iso_command,
# capture both streams in stdout:
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
shutil.rmtree(temp_seed_data_dir)
if result.returncode == 0:
msg = (
f'Finished generating seed iso for {self.name}: '
f'{gen_seed_iso_command}'
)
LOG.info("%s returncode: %s, output: %s",
msg,
result.returncode,
result.stdout.decode('utf-8').replace('\n', ', '))
else:
msg = (
f'Failed to generate seed iso for {self.name}: '
f'{gen_seed_iso_command}'
)
LOG.error("%s returncode: %s, output: %s",
msg,
result.returncode,
result.stdout.decode('utf-8').replace('\n', ', '))
raise Exception(msg)
return True
def prep(self, override_path, payload):
LOG.info(f'Prepare config for {self.name} enroll init')
software_version = str(payload['software_version'])
self.www_root = os.path.join(SUBCLOUD_ISO_PATH, software_version)
self.iso_dir_path = os.path.join(self.www_root, 'nodes', self.name)
self.seed_iso_path = os.path.join(self.iso_dir_path,
consts.ENROLL_INIT_SEED_ISO_NAME)
override_path = os.path.join(override_path, self.name)
if not os.path.isdir(override_path):
os.mkdir(override_path, 0o755)
if not os.path.isdir(self.www_root):
os.mkdir(self.www_root, 0o755)
if not os.path.isdir(self.iso_dir_path):
os.makedirs(self.iso_dir_path, 0o755, exist_ok=True)
elif os.path.exists(self.seed_iso_path):
# Clean up iso file if it already exists
# This may happen if a previous enroll init attempt was abruptly
# terminated
LOG.info(f'Found preexisting seed iso for subcloud {self.name}, '
'cleaning up')
os.remove(self.seed_iso_path)
self._generate_seed_iso(payload)
# get the boot image url for bmc
image_base_url = SubcloudInstall.get_image_base_url(self.get_https_enabled(),
self.sysinv_client)
payload['image'] = os.path.join(image_base_url, 'iso',
software_version, 'nodes',
self.name, consts.ENROLL_INIT_SEED_ISO_NAME)
SubcloudInstall.create_rvmc_config_file(override_path, payload)
return True
def enroll_init(self, log_file_dir, enroll_command):
LOG.info(f'Start enroll init for {self.name}')
subcloud_log_base_path = os.path.join(log_file_dir, self.name)
playbook_log_file = f'{subcloud_log_base_path}_playbook_output.log'
try:
ansible = dccommon_utils.AnsiblePlaybook(self.name)
ansible.run_playbook(playbook_log_file, enroll_command)
return True
except exceptions.PlaybookExecutionFailed:
msg = (
f"Failed to enroll init {self.name}, check individual "
f"logs at {playbook_log_file}. "
f"Run {dcmanager_consts.ERROR_DESC_CMD} for details"
)
raise Exception(msg)
def cleanup(self):
if os.path.exists(self.seed_iso_path):
os.remove(self.seed_iso_path)