227 lines
7.8 KiB
Python
227 lines
7.8 KiB
Python
# Copyright 2023 Red Hat
|
|
#
|
|
# 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.
|
|
from __future__ import absolute_import
|
|
|
|
import netaddr
|
|
import openshift_client as oc
|
|
from oslo_log import log
|
|
|
|
import tobiko
|
|
from tobiko.shell import sh
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
OSP_CONTROLPLANE = 'openstackcontrolplane'
|
|
OSP_DP_NODESET = 'openstackdataplanenodeset'
|
|
DP_SSH_SECRET_NAME = 'secret/dataplane-ansible-ssh-private-key-secret'
|
|
OSP_BM_HOST = 'baremetalhost.metal3.io'
|
|
OSP_BM_CRD = 'baremetalhosts.metal3.io'
|
|
OCP_WORKERS = 'nodes'
|
|
|
|
OVN_DP_SERVICE_NAME = 'ovn'
|
|
COMPUTE_DP_SERVICE_NAMES = ['nova', 'nova-custom', 'nova-custom-ceph']
|
|
|
|
EDPM_COMPUTE_GROUP = 'edpm-compute'
|
|
EDPM_NETWORKER_GROUP = 'edpm-networker'
|
|
EDPM_OTHER_GROUP = 'edpm-other'
|
|
|
|
|
|
_IS_OC_CLIENT_AVAILABLE = None
|
|
_IS_BM_CRD_AVAILABLE = None
|
|
|
|
|
|
def _is_oc_client_available() -> bool:
|
|
# pylint: disable=global-statement
|
|
global _IS_OC_CLIENT_AVAILABLE
|
|
if _IS_OC_CLIENT_AVAILABLE is None:
|
|
_IS_OC_CLIENT_AVAILABLE = False
|
|
try:
|
|
if sh.execute('which oc').exit_status == 0:
|
|
_IS_OC_CLIENT_AVAILABLE = True
|
|
except sh.ShellCommandFailed:
|
|
pass
|
|
return _IS_OC_CLIENT_AVAILABLE
|
|
|
|
|
|
def _is_baremetal_crd_available() -> bool:
|
|
# pylint: disable=global-statement
|
|
global _IS_BM_CRD_AVAILABLE
|
|
if not _is_oc_client_available():
|
|
return False
|
|
if _IS_BM_CRD_AVAILABLE is None:
|
|
try:
|
|
_IS_BM_CRD_AVAILABLE = any(
|
|
[OSP_BM_CRD in n for n in oc.selector("crd").qnames()])
|
|
except oc.OpenShiftPythonException:
|
|
_IS_BM_CRD_AVAILABLE = False
|
|
return _IS_BM_CRD_AVAILABLE
|
|
|
|
|
|
def _get_group(services):
|
|
for compute_dp_service in COMPUTE_DP_SERVICE_NAMES:
|
|
if compute_dp_service in services:
|
|
return EDPM_COMPUTE_GROUP
|
|
if OVN_DP_SERVICE_NAME in services:
|
|
return EDPM_NETWORKER_GROUP
|
|
return EDPM_OTHER_GROUP
|
|
|
|
|
|
def _get_ocp_worker_hostname(worker):
|
|
for address in worker.get('status', {}).get('addresses', []):
|
|
if address.get('type') == 'Hostname':
|
|
return address['address']
|
|
|
|
|
|
def _get_ocp_worker_addresses(worker):
|
|
return [
|
|
netaddr.IPAddress(address['address']) for
|
|
address in worker.get('status', {}).get('addresses', [])
|
|
if address.get('type') != 'Hostname']
|
|
|
|
|
|
def _get_edpm_node_ctlplane_ip_from_status(hostname, node_status):
|
|
all_ips = node_status.get('AllIPs')
|
|
if not all_ips:
|
|
LOG.warning("No IPs found in the Nodeset status: %s",
|
|
node_status)
|
|
return
|
|
host_ips = all_ips.get(hostname)
|
|
if not host_ips:
|
|
LOG.warning("Host %s not found in AllIPs: %s",
|
|
hostname, all_ips)
|
|
return
|
|
return host_ips.get('ctlplane')
|
|
|
|
|
|
def has_podified_cp() -> bool:
|
|
if not _is_oc_client_available():
|
|
LOG.debug("Openshift CLI client isn't installed.")
|
|
return False
|
|
try:
|
|
return bool(oc.selector(OSP_CONTROLPLANE).objects())
|
|
except oc.OpenShiftPythonException:
|
|
return False
|
|
|
|
|
|
def get_dataplane_ssh_keypair():
|
|
private_key = ""
|
|
public_key = ""
|
|
try:
|
|
secret_object = oc.selector(DP_SSH_SECRET_NAME).object()
|
|
private_key = secret_object.as_dict()['data']['ssh-privatekey']
|
|
public_key = secret_object.as_dict()['data']['ssh-publickey']
|
|
except oc.OpenShiftPythonException as err:
|
|
LOG.error("Error while trying to get Dataplane secret SSH Key: %s",
|
|
err)
|
|
return private_key, public_key
|
|
|
|
|
|
def list_edpm_nodes():
|
|
nodes = []
|
|
nodeset_sel = oc.selector(OSP_DP_NODESET)
|
|
for nodeset in nodeset_sel.objects():
|
|
nodeset_spec = nodeset.as_dict()['spec']
|
|
nodeset_status = nodeset.as_dict()['status']
|
|
node_template = nodeset_spec['nodeTemplate']
|
|
nodeset_nodes = nodeset_spec['nodes']
|
|
group_name = _get_group(nodeset_spec['services'])
|
|
for node in nodeset_nodes.values():
|
|
node_hostname = node.get('hostName')
|
|
node_dict = {
|
|
'hostname': node_hostname,
|
|
'host': (node['ansible'].get('ansibleHost') or
|
|
_get_edpm_node_ctlplane_ip_from_status(
|
|
node_hostname, nodeset_status)),
|
|
'group': group_name,
|
|
'port': (
|
|
node.get('ansible', {}).get('ansiblePort') or
|
|
node_template.get('ansible', {}).get('ansiblePort')),
|
|
'username': (
|
|
node.get('ansible', {}).get('ansibleUser') or
|
|
node_template.get('ansible', {}).get('ansibleUser')),
|
|
}
|
|
nodes.append(node_dict)
|
|
return nodes
|
|
|
|
|
|
def list_ocp_workers():
|
|
nodes_sel = oc.selector(OCP_WORKERS)
|
|
ocp_workers = []
|
|
for node in nodes_sel.objects():
|
|
node_dict = node.as_dict()
|
|
ocp_workers.append({
|
|
'hostname': _get_ocp_worker_hostname(node_dict),
|
|
'addresses': _get_ocp_worker_addresses(node_dict)
|
|
})
|
|
return ocp_workers
|
|
|
|
|
|
def power_on_edpm_node(nodename):
|
|
_set_edpm_node_online_status(nodename, online=True)
|
|
|
|
|
|
def power_off_edpm_node(nodename):
|
|
_set_edpm_node_online_status(nodename, online=False)
|
|
|
|
|
|
def _set_edpm_node_online_status(nodename, online):
|
|
if _is_baremetal_crd_available() is False:
|
|
LOG.info("BareMetal operator is not available in the deployment. "
|
|
"Starting and stopping EDPM nodes is not supported.")
|
|
return
|
|
try:
|
|
bm_node = oc.selector(f"{OSP_BM_HOST}/{nodename}").objects()[0]
|
|
except oc.OpenShiftPythonException as err:
|
|
LOG.info(f"Error while trying to get BareMetal Node '{nodename}' "
|
|
f"from Openshift. Error: {err}")
|
|
return
|
|
except IndexError:
|
|
LOG.error(f"Node {nodename} not found in the {OSP_BM_HOST} CRs.")
|
|
return
|
|
bm_node.model.spec['online'] = online
|
|
try:
|
|
# NOTE(slaweq): returned status is 0 when all operations where
|
|
# finished successfully. Otherwise status will be different than
|
|
# 0, like in the shell scripts
|
|
if not bool(bm_node.apply().status()):
|
|
_wait_for_poweredOn_status(nodename, online)
|
|
except oc.OpenShiftPythonException as err:
|
|
LOG.error(f"Error while applying new online state: {online} for "
|
|
f"the node: {nodename}. Error: {err}")
|
|
|
|
|
|
def _wait_for_poweredOn_status(nodename, expected_status,
|
|
timeout: tobiko.Seconds = None):
|
|
for attempt in tobiko.retry(
|
|
timeout=timeout,
|
|
count=10,
|
|
interval=5.,
|
|
default_timeout=30):
|
|
LOG.debug(f"Checking power status of the '{nodename}'.")
|
|
try:
|
|
poweredOn = oc.selector(
|
|
f"{OSP_BM_HOST}/{nodename}"
|
|
).objects()[0].model.status['poweredOn']
|
|
except oc.OpenShiftPythonException as err:
|
|
LOG.error("Error while trying to get 'poweredOn' state of "
|
|
f"the node {nodename}. Error: {err}")
|
|
else:
|
|
if poweredOn == expected_status:
|
|
LOG.debug(f"Actual poweredOn state of the node {nodename} "
|
|
f"is: '{poweredOn}' which is as expected.")
|
|
return True
|
|
LOG.debug(f"Actual poweredOn state is: '{poweredOn}' != "
|
|
f" '{expected_status}'")
|
|
attempt.check_limits()
|