
This commit removes the hardcoded "RegionOne" region name and instead retrieves the region name dynamically from the service configuration. This change prepares for a future update where DC services will be deployed on a standalone system that uses a UUID as the default region name. Test Plan: 01. PASS - Add a subcloud. 02. PASS - Manage and unmanage a subcloud. 03. PASS - List and show subcloud details using subcloud list and subcloud show --detail. 04. PASS - Delete a subcloud. 05. PASS - Run 'dcmanager strategy-config update' using different region names: "RegionOne", "SystemController", and without specifying a region name. Verify that the default options are modified accordingly. 06. PASS - Run the previous test but using 'dcmanager strategy-config show' instead. 07. PASS - Upload a patch using the dcorch proxy (--os-region-name SystemController). 08. PASS - Run prestage orchestration. 09. PASS - Apply a patch to the system controller and then to the subclouds 10. PASS - Review all dcmanager and dcorch logs to ensure no exceptions are raised. Story: 2011312 Task: 51861 Change-Id: I85c93c865c40418a351dab28aac56fc08464af72 Signed-off-by: Gustavo Herzmann <gustavo.herzmann@windriver.com>
833 lines
33 KiB
Python
833 lines
33 KiB
Python
# Copyright (c) 2017-2018, 2022, 2024-2025 Wind River Systems, Inc.
|
|
# All Rights Reserved.
|
|
#
|
|
# 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 keystoneauth1 import exceptions as keystone_exceptions
|
|
from novaclient import client as novaclient
|
|
from novaclient import exceptions as novaclient_exceptions
|
|
from novaclient import utils as novaclient_utils
|
|
from oslo_log import log as logging
|
|
from oslo_serialization import jsonutils
|
|
|
|
from dccommon import consts as dccommon_consts
|
|
from dcorch.common import consts
|
|
from dcorch.common import exceptions
|
|
from dcorch.common import utils
|
|
from dcorch.engine import quota_manager
|
|
from dcorch.engine.sync_thread import SyncThread
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class ComputeSyncThread(SyncThread):
|
|
"""Manages tasks related to resource management for nova."""
|
|
|
|
def __init__(self, subcloud_name, endpoint_type=None, engine_id=None):
|
|
super(ComputeSyncThread, self).__init__(
|
|
subcloud_name, endpoint_type=endpoint_type, engine_id=engine_id
|
|
)
|
|
self.region_name = subcloud_name
|
|
self.endpoint_type = consts.ENDPOINT_TYPE_COMPUTE
|
|
self.sync_handler_map = {
|
|
consts.RESOURCE_TYPE_COMPUTE_FLAVOR: self.sync_compute_resource,
|
|
consts.RESOURCE_TYPE_COMPUTE_KEYPAIR: self.sync_compute_resource,
|
|
consts.RESOURCE_TYPE_COMPUTE_QUOTA_SET: self.sync_compute_resource,
|
|
consts.RESOURCE_TYPE_COMPUTE_QUOTA_CLASS_SET: self.sync_compute_resource,
|
|
}
|
|
self.audit_resources = [
|
|
consts.RESOURCE_TYPE_COMPUTE_QUOTA_CLASS_SET,
|
|
consts.RESOURCE_TYPE_COMPUTE_FLAVOR,
|
|
consts.RESOURCE_TYPE_COMPUTE_KEYPAIR,
|
|
# note: no audit here for quotas, that's handled separately
|
|
]
|
|
self.log_extra = {
|
|
"instance": "{}/{}: ".format(self.region_name, self.endpoint_type)
|
|
}
|
|
self.sc_nova_client = None
|
|
self.initialize()
|
|
LOG.info("ComputeSyncThread initialized", extra=self.log_extra)
|
|
|
|
def initialize_sc_clients(self):
|
|
super(ComputeSyncThread, self).initialize_sc_clients()
|
|
if not self.sc_nova_client and self.sc_admin_session:
|
|
self.sc_nova_client = novaclient.Client(
|
|
"2.38",
|
|
session=self.sc_admin_session,
|
|
endpoint_type=dccommon_consts.KS_ENDPOINT_ADMIN,
|
|
region_name=self.region_name,
|
|
)
|
|
|
|
def initialize(self):
|
|
# Subcloud may be enabled a while after being added.
|
|
# Keystone endpoints for the subcloud could be added in
|
|
# between these 2 steps. Reinitialize the session to
|
|
# get the most up-to-date service catalog.
|
|
super(ComputeSyncThread, self).initialize()
|
|
# todo: update version to 2.53 once on pike
|
|
self.m_nova_client = novaclient.Client(
|
|
"2.38",
|
|
session=self.admin_session,
|
|
endpoint_type=dccommon_consts.KS_ENDPOINT_INTERNAL,
|
|
region_name=dccommon_consts.SYSTEM_CONTROLLER_NAME,
|
|
)
|
|
|
|
self.initialize_sc_clients()
|
|
LOG.info("session and clients initialized", extra=self.log_extra)
|
|
|
|
def sync_compute_resource(self, request, rsrc):
|
|
self.initialize_sc_clients()
|
|
# Invoke function with name format "operationtype_resourcetype".
|
|
# For example: create_flavor()
|
|
try:
|
|
func_name = request.orch_job.operation_type + "_" + rsrc.resource_type
|
|
getattr(self, func_name)(request, rsrc)
|
|
except AttributeError:
|
|
LOG.error(
|
|
"{} not implemented for {}".format(
|
|
request.orch_job.operation_type, rsrc.resource_type
|
|
)
|
|
)
|
|
raise exceptions.SyncRequestFailed
|
|
except (
|
|
keystone_exceptions.connection.ConnectTimeout,
|
|
keystone_exceptions.ConnectFailure,
|
|
) as e:
|
|
LOG.error(
|
|
"sync_compute_resource: {} is not reachable [{}]".format(
|
|
self.region_name, str(e)
|
|
),
|
|
extra=self.log_extra,
|
|
)
|
|
raise exceptions.SyncRequestTimeout
|
|
except exceptions.SyncRequestFailed:
|
|
raise
|
|
except Exception as e:
|
|
LOG.exception(e)
|
|
raise exceptions.SyncRequestFailedRetry
|
|
|
|
# ---- Override common audit functions ----
|
|
def get_resource_id(self, resource_type, resource):
|
|
if hasattr(resource, "master_id"):
|
|
# If resource from DB, return master resource id
|
|
# from master cloud
|
|
return resource.master_id
|
|
|
|
# Else, it is OpenStack resource retrieved from master cloud
|
|
if resource_type == consts.RESOURCE_TYPE_COMPUTE_KEYPAIR:
|
|
# User_id field is set in _info data by audit query code.
|
|
return utils.keypair_construct_id(
|
|
resource.id, resource._info["keypair"]["user_id"]
|
|
)
|
|
if resource_type == consts.RESOURCE_TYPE_COMPUTE_QUOTA_CLASS_SET:
|
|
# We only care about the default class.
|
|
return "default"
|
|
|
|
# Nothing special for other resources (flavor)
|
|
return resource.id
|
|
|
|
def get_resource_info(self, resource_type, resource, operation_type=None):
|
|
if resource_type == consts.RESOURCE_TYPE_COMPUTE_FLAVOR:
|
|
return jsonutils.dumps(resource._info)
|
|
elif resource_type == consts.RESOURCE_TYPE_COMPUTE_KEYPAIR:
|
|
return jsonutils.dumps(resource._info.get("keypair"))
|
|
elif resource_type == consts.RESOURCE_TYPE_COMPUTE_QUOTA_CLASS_SET:
|
|
return jsonutils.dumps(resource._info)
|
|
else:
|
|
return super(ComputeSyncThread, self).get_resource_info(
|
|
resource_type, resource, operation_type
|
|
)
|
|
|
|
def get_subcloud_resources(self, resource_type):
|
|
self.initialize_sc_clients()
|
|
if resource_type == consts.RESOURCE_TYPE_COMPUTE_FLAVOR:
|
|
return self.get_flavor_resources(self.sc_nova_client)
|
|
elif resource_type == consts.RESOURCE_TYPE_COMPUTE_QUOTA_CLASS_SET:
|
|
return self.get_quota_class_resources(self.sc_nova_client)
|
|
else:
|
|
LOG.error(
|
|
"Wrong resource type {}".format(resource_type), extra=self.log_extra
|
|
)
|
|
return None
|
|
|
|
def get_master_resources(self, resource_type):
|
|
if resource_type == consts.RESOURCE_TYPE_COMPUTE_FLAVOR:
|
|
return self.get_flavor_resources(self.m_nova_client)
|
|
elif resource_type == consts.RESOURCE_TYPE_COMPUTE_QUOTA_CLASS_SET:
|
|
return self.get_quota_class_resources(self.m_nova_client)
|
|
else:
|
|
LOG.error(
|
|
"Wrong resource type {}".format(resource_type), extra=self.log_extra
|
|
)
|
|
return None
|
|
|
|
def same_resource(self, resource_type, m_resource, sc_resource):
|
|
if resource_type == consts.RESOURCE_TYPE_COMPUTE_FLAVOR:
|
|
return self.same_flavor(m_resource, sc_resource)
|
|
elif resource_type == consts.RESOURCE_TYPE_COMPUTE_KEYPAIR:
|
|
return self.same_keypair(m_resource, sc_resource)
|
|
elif resource_type == consts.RESOURCE_TYPE_COMPUTE_QUOTA_CLASS_SET:
|
|
return self.same_quota_class(m_resource, sc_resource)
|
|
else:
|
|
return True
|
|
|
|
def audit_discrepancy(self, resource_type, m_resource, sc_resources):
|
|
if resource_type in [
|
|
consts.RESOURCE_TYPE_COMPUTE_FLAVOR,
|
|
consts.RESOURCE_TYPE_COMPUTE_KEYPAIR,
|
|
]:
|
|
# It could be that the flavor details are different
|
|
# between master cloud and subcloud now.
|
|
# Thus, delete the flavor before creating it again.
|
|
# Dependants (ex: flavor-access) will be created again.
|
|
self.schedule_work(
|
|
self.endpoint_type,
|
|
resource_type,
|
|
self.get_resource_id(resource_type, m_resource),
|
|
consts.OPERATION_TYPE_DELETE,
|
|
)
|
|
|
|
# For quota classes there is no delete operation, so we just want
|
|
# to update the existing class. Nothing to do here.
|
|
|
|
# Return true to try creating the resource again
|
|
return True
|
|
|
|
# ---- Flavor & dependants (flavor-access, extra-spec) ----
|
|
def create_flavor(self, request, rsrc):
|
|
flavor_dict = jsonutils.loads(request.orch_job.resource_info)
|
|
name = flavor_dict["name"]
|
|
ram = flavor_dict["ram"]
|
|
vcpus = flavor_dict["vcpus"]
|
|
disk = flavor_dict["disk"]
|
|
kwargs = {}
|
|
# id is always passed in by proxy
|
|
kwargs["flavorid"] = flavor_dict["id"]
|
|
if "OS-FLV-EXT-DATA:ephemeral" in flavor_dict:
|
|
kwargs["ephemeral"] = flavor_dict["OS-FLV-EXT-DATA:ephemeral"]
|
|
if "swap" in flavor_dict and flavor_dict["swap"]:
|
|
kwargs["swap"] = flavor_dict["swap"]
|
|
if "rxtx_factor" in flavor_dict:
|
|
kwargs["rxtx_factor"] = flavor_dict["rxtx_factor"]
|
|
if "os-flavor-access:is_public" in flavor_dict:
|
|
kwargs["is_public"] = flavor_dict["os-flavor-access:is_public"]
|
|
|
|
# todo: maybe we can bypass all the above and just directly call
|
|
# self.sc_nova_client.flavors._create("/flavors", body, "flavor")
|
|
# with "body" made from request.orch_job.resource_info.
|
|
newflavor = None
|
|
try:
|
|
newflavor = self.sc_nova_client.flavors.create(
|
|
name, ram, vcpus, disk, **kwargs
|
|
)
|
|
except novaclient_exceptions.Conflict as e:
|
|
if "already exists" in str(e):
|
|
# FlavorExists or FlavorIdExists.
|
|
LOG.info(
|
|
"Flavor {} already exists in subcloud".format(name),
|
|
extra=self.log_extra,
|
|
)
|
|
# Compare the flavor details and recreate flavor if required.
|
|
newflavor = self.recreate_flavor_if_reqd(name, ram, vcpus, disk, kwargs)
|
|
else:
|
|
LOG.exception(e)
|
|
if not newflavor:
|
|
raise exceptions.SyncRequestFailed
|
|
|
|
subcloud_rsrc_id = self.persist_db_subcloud_resource(rsrc.id, newflavor.id)
|
|
LOG.info(
|
|
"Flavor {}:{} [{}/{}] created".format(
|
|
rsrc.id, subcloud_rsrc_id, name, newflavor.id
|
|
),
|
|
extra=self.log_extra,
|
|
)
|
|
|
|
def recreate_flavor_if_reqd(self, name, ram, vcpus, disk, kwargs):
|
|
# Both the flavor name and the flavor id must be unique.
|
|
# If the conflict is due to same name, but different uuid,
|
|
# we have to fetch the correct id from subcloud before
|
|
# attempting to delete it.
|
|
# Since the flavor details are available, compare with master cloud
|
|
# and recreate the flavor only if required.
|
|
newflavor = None
|
|
try:
|
|
master_flavor = self.m_nova_client.flavors.get(kwargs["flavorid"])
|
|
subcloud_flavor = None
|
|
sc_flavors = self.sc_nova_client.flavors.list(is_public=None)
|
|
for sc_flavor in sc_flavors:
|
|
# subcloud flavor might have the same name and/or the same id
|
|
if name == sc_flavor.name or kwargs["flavorid"] == sc_flavor.id:
|
|
subcloud_flavor = sc_flavor
|
|
break
|
|
if master_flavor and subcloud_flavor:
|
|
if self.same_flavor(master_flavor, subcloud_flavor):
|
|
newflavor = subcloud_flavor
|
|
else:
|
|
LOG.info(
|
|
"recreate_flavor, deleting {}:{}".format(
|
|
subcloud_flavor.name, subcloud_flavor.id
|
|
),
|
|
extra=self.log_extra,
|
|
)
|
|
self.sc_nova_client.flavors.delete(subcloud_flavor.id)
|
|
newflavor = self.sc_nova_client.flavors.create(
|
|
name, ram, vcpus, disk, **kwargs
|
|
)
|
|
except Exception as e:
|
|
LOG.exception(e)
|
|
raise exceptions.SyncRequestFailed
|
|
return newflavor
|
|
|
|
def delete_flavor(self, request, rsrc):
|
|
subcloud_rsrc = self.get_db_subcloud_resource(rsrc.id)
|
|
if not subcloud_rsrc:
|
|
return
|
|
try:
|
|
self.sc_nova_client.flavors.delete(subcloud_rsrc.subcloud_resource_id)
|
|
except novaclient_exceptions.NotFound:
|
|
# Flavor already deleted in subcloud, carry on.
|
|
LOG.info(
|
|
"ResourceNotFound in subcloud, may be already deleted",
|
|
extra=self.log_extra,
|
|
)
|
|
subcloud_rsrc.delete()
|
|
# Master Resource can be deleted only when all subcloud resources
|
|
# are deleted along with corresponding orch_job and orch_requests.
|
|
LOG.info(
|
|
"Flavor {}:{} [{}] deleted".format(
|
|
rsrc.id, subcloud_rsrc.id, subcloud_rsrc.subcloud_resource_id
|
|
),
|
|
extra=self.log_extra,
|
|
)
|
|
|
|
def action_flavor(self, request, rsrc):
|
|
action_dict = jsonutils.loads(request.orch_job.resource_info)
|
|
subcloud_rsrc = self.get_db_subcloud_resource(rsrc.id)
|
|
if not subcloud_rsrc:
|
|
LOG.error(
|
|
"Subcloud resource missing for {}:{}".format(rsrc, action_dict),
|
|
extra=self.log_extra,
|
|
)
|
|
return
|
|
|
|
switcher = {
|
|
consts.ACTION_ADDTENANTACCESS: self.add_tenant_access,
|
|
consts.ACTION_REMOVETENANTACCESS: self.remove_tenant_access,
|
|
consts.ACTION_EXTRASPECS_POST: self.set_extra_specs,
|
|
consts.ACTION_EXTRASPECS_DELETE: self.unset_extra_specs,
|
|
}
|
|
action = list(action_dict.keys())[0]
|
|
if action not in list(switcher.keys()):
|
|
LOG.error(
|
|
"Unsupported flavor action {}".format(action), extra=self.log_extra
|
|
)
|
|
return
|
|
LOG.info(
|
|
"Flavor action [{}]: {}".format(action, action_dict), extra=self.log_extra
|
|
)
|
|
switcher[action](rsrc, action, action_dict, subcloud_rsrc)
|
|
|
|
def add_tenant_access(self, rsrc, action, action_dict, subcloud_rsrc):
|
|
tenant_id = action_dict[action]["tenant"]
|
|
try:
|
|
self.sc_nova_client.flavor_access.add_tenant_access(
|
|
subcloud_rsrc.subcloud_resource_id, tenant_id
|
|
)
|
|
except novaclient_exceptions.Conflict:
|
|
LOG.info(
|
|
"Flavor-access already present {}:{}".format(rsrc, action_dict),
|
|
extra=self.log_extra,
|
|
)
|
|
|
|
def remove_tenant_access(self, rsrc, action, action_dict, subcloud_rsrc):
|
|
tenant_id = action_dict[action]["tenant"]
|
|
try:
|
|
self.sc_nova_client.flavor_access.remove_tenant_access(
|
|
subcloud_rsrc.subcloud_resource_id, tenant_id
|
|
)
|
|
except novaclient_exceptions.NotFound:
|
|
LOG.info(
|
|
"Flavor-access already deleted {}:{}".format(rsrc, action_dict),
|
|
extra=self.log_extra,
|
|
)
|
|
|
|
def set_extra_specs(self, rsrc, action, action_dict, subcloud_rsrc):
|
|
flavor = novaclient_utils.find_resource(
|
|
self.sc_nova_client.flavors,
|
|
subcloud_rsrc.subcloud_resource_id,
|
|
is_public=None,
|
|
)
|
|
flavor.set_keys(action_dict[action])
|
|
# No need to handle "extra-spec already exists" case.
|
|
# Nova throws no exception for that.
|
|
|
|
def unset_extra_specs(self, rsrc, action, action_dict, subcloud_rsrc):
|
|
flavor = novaclient_utils.find_resource(
|
|
self.sc_nova_client.flavors,
|
|
subcloud_rsrc.subcloud_resource_id,
|
|
is_public=None,
|
|
)
|
|
|
|
es_metadata = action_dict[action]
|
|
metadata = {}
|
|
# extra_spec keys passed in could be of format "key1"
|
|
# or "key1;key2;key3"
|
|
for metadatum in es_metadata.split(";"):
|
|
if metadatum:
|
|
metadata[metadatum] = None
|
|
|
|
try:
|
|
flavor.unset_keys(list(metadata.keys()))
|
|
except novaclient_exceptions.NotFound:
|
|
LOG.info(
|
|
"Extra-spec {} not found {}:{}".format(
|
|
list(metadata.keys()), rsrc, action_dict
|
|
),
|
|
extra=self.log_extra,
|
|
)
|
|
|
|
def get_flavor_resources(self, nc):
|
|
try:
|
|
flavors = nc.flavors.list(is_public=None)
|
|
for flavor in flavors:
|
|
# Attach flavor access list to flavor object, so that
|
|
# it can be audited later in audit_dependants()
|
|
if not flavor.is_public:
|
|
try:
|
|
fa_list = nc.flavor_access.list(flavor=flavor.id)
|
|
flavor.attach_fa = fa_list
|
|
except novaclient_exceptions.NotFound:
|
|
# flavor/flavor_access just got deleted
|
|
# (after flavors.list)
|
|
LOG.info(
|
|
"Flavor/flavor_access not found [{}]".format(flavor.id),
|
|
extra=self.log_extra,
|
|
)
|
|
flavor.attach_fa = []
|
|
else:
|
|
flavor.attach_fa = []
|
|
|
|
# Attach extra_spec dict to flavor object, so that
|
|
# it can be audited later in audit_dependants()
|
|
flavor.attach_es = flavor.get_keys()
|
|
return flavors
|
|
except (
|
|
keystone_exceptions.connection.ConnectTimeout,
|
|
keystone_exceptions.ConnectFailure,
|
|
) as e:
|
|
LOG.info(
|
|
"get_flavor: subcloud {} is not reachable [{}]".format(
|
|
self.region_name, str(e)
|
|
),
|
|
extra=self.log_extra,
|
|
)
|
|
return None
|
|
except Exception as e:
|
|
LOG.exception(e)
|
|
return None
|
|
|
|
def same_flavor(self, f1, f2):
|
|
return (
|
|
f1.name == f2.name
|
|
and f1.vcpus == f2.vcpus
|
|
and f1.ram == f2.ram
|
|
and f1.disk == f2.disk
|
|
and f1.swap == f2.swap
|
|
and f1.rxtx_factor == f2.rxtx_factor
|
|
and f1.is_public == f2.is_public
|
|
and f1.ephemeral == f2.ephemeral
|
|
)
|
|
|
|
def audit_dependants(self, resource_type, m_resource, sc_resource):
|
|
num_of_audit_jobs = 0
|
|
if not self.is_subcloud_enabled() or self.should_exit():
|
|
return num_of_audit_jobs
|
|
if resource_type == consts.RESOURCE_TYPE_COMPUTE_FLAVOR:
|
|
num_of_audit_jobs += self.audit_flavor_access(
|
|
resource_type, m_resource, sc_resource
|
|
)
|
|
num_of_audit_jobs += self.audit_extra_specs(
|
|
resource_type, m_resource, sc_resource
|
|
)
|
|
return num_of_audit_jobs
|
|
|
|
def audit_flavor_access(self, resource_type, m_resource, sc_resource):
|
|
num_of_audit_jobs = 0
|
|
sc_fa_attachment = [] # Subcloud flavor-access attachment
|
|
if sc_resource:
|
|
sc_fa_attachment = sc_resource.attach_fa
|
|
|
|
# Flavor-access needs to be audited. flavor-access details are
|
|
# filled in m_resources and sc_resources during query.
|
|
for m_fa in m_resource.attach_fa:
|
|
found = False
|
|
for sc_fa in sc_fa_attachment:
|
|
if m_fa.tenant_id == sc_fa.tenant_id:
|
|
found = True
|
|
sc_resource.attach_fa.remove(sc_fa)
|
|
break
|
|
if not found:
|
|
action_dict = {
|
|
consts.ACTION_ADDTENANTACCESS: {"tenant": m_fa.tenant_id}
|
|
}
|
|
self.schedule_work(
|
|
self.endpoint_type,
|
|
resource_type,
|
|
m_resource.id,
|
|
consts.OPERATION_TYPE_ACTION,
|
|
jsonutils.dumps(action_dict),
|
|
)
|
|
num_of_audit_jobs += 1
|
|
|
|
for sc_fa in sc_fa_attachment:
|
|
action_dict = {
|
|
consts.ACTION_REMOVETENANTACCESS: {"tenant": sc_fa.tenant_id}
|
|
}
|
|
self.schedule_work(
|
|
self.endpoint_type,
|
|
resource_type,
|
|
m_resource.id,
|
|
consts.OPERATION_TYPE_ACTION,
|
|
jsonutils.dumps(action_dict),
|
|
)
|
|
num_of_audit_jobs += 1
|
|
|
|
return num_of_audit_jobs
|
|
|
|
def audit_extra_specs(self, resource_type, m_flavor, sc_resource):
|
|
num_of_audit_jobs = 0
|
|
sc_es_attachment = {} # Subcloud extra-spec attachment
|
|
if sc_resource:
|
|
# sc_resource could be None.
|
|
sc_es_attachment = sc_resource.attach_es
|
|
|
|
# Extra-spec needs to be audited. Extra-spec details are
|
|
# filled in m_resources and sc_resources during query.
|
|
metadata = {}
|
|
for m_key, m_value in m_flavor.attach_es.items():
|
|
found = False
|
|
for sc_key, sc_value in sc_es_attachment.items():
|
|
if m_key == sc_key and m_value == sc_value:
|
|
found = True
|
|
sc_es_attachment.pop(sc_key)
|
|
break
|
|
if not found:
|
|
metadata.update({m_key: m_value})
|
|
if metadata:
|
|
action_dict = {consts.ACTION_EXTRASPECS_POST: metadata}
|
|
self.schedule_work(
|
|
self.endpoint_type,
|
|
resource_type,
|
|
m_flavor.id,
|
|
consts.OPERATION_TYPE_ACTION,
|
|
jsonutils.dumps(action_dict),
|
|
)
|
|
num_of_audit_jobs += 1
|
|
|
|
keys_to_delete = ""
|
|
for sc_key, sc_value in sc_es_attachment.items():
|
|
keys_to_delete += sc_key + ";"
|
|
if keys_to_delete:
|
|
action_dict = {consts.ACTION_EXTRASPECS_DELETE: keys_to_delete}
|
|
self.schedule_work(
|
|
self.endpoint_type,
|
|
resource_type,
|
|
m_flavor.id,
|
|
consts.OPERATION_TYPE_ACTION,
|
|
jsonutils.dumps(action_dict),
|
|
)
|
|
num_of_audit_jobs += 1
|
|
|
|
return num_of_audit_jobs
|
|
|
|
# ---- Keypair resource ----
|
|
def create_keypair(self, request, rsrc):
|
|
keypair_dict = jsonutils.loads(request.orch_job.resource_info)
|
|
name, user_id = utils.keypair_deconstruct_id(rsrc.master_id)
|
|
log_str = rsrc.master_id + " " + name + "/" + user_id
|
|
kwargs = {}
|
|
kwargs["user_id"] = user_id
|
|
if "public_key" in keypair_dict:
|
|
kwargs["public_key"] = keypair_dict["public_key"]
|
|
if "type" in keypair_dict:
|
|
kwargs["key_type"] = keypair_dict["type"]
|
|
log_str += "/" + kwargs["key_type"]
|
|
newkeypair = None
|
|
try:
|
|
newkeypair = self.sc_nova_client.keypairs.create(name, **kwargs)
|
|
except novaclient_exceptions.Conflict:
|
|
# KeyPairExists: keypair with same name already exists.
|
|
LOG.info(
|
|
"Keypair {} already exists in subcloud".format(log_str),
|
|
extra=self.log_extra,
|
|
)
|
|
newkeypair = self.recreate_keypair(name, kwargs)
|
|
if not newkeypair:
|
|
raise exceptions.SyncRequestFailed
|
|
|
|
subcloud_rsrc_id = self.persist_db_subcloud_resource(rsrc.id, rsrc.master_id)
|
|
LOG.info(
|
|
"Keypair {}:{} [{}] created".format(rsrc.id, subcloud_rsrc_id, log_str),
|
|
extra=self.log_extra,
|
|
)
|
|
|
|
def recreate_keypair(self, name, kwargs):
|
|
newkeypair = None
|
|
try:
|
|
# Not worth doing additional api calls to compare the
|
|
# master and subcloud keypairs. Delete and create again.
|
|
# This is different from recreate_flavor_if_reqd().
|
|
# Here for keypair, name and user_id are already available
|
|
# and query api can be avoided.
|
|
delete_kw = {"user_id": kwargs["user_id"]}
|
|
LOG.info(
|
|
"recreate_keypair, deleting {}:{}".format(name, delete_kw),
|
|
extra=self.log_extra,
|
|
)
|
|
self.sc_nova_client.keypairs.delete(name, **delete_kw)
|
|
newkeypair = self.sc_nova_client.keypairs.create(name, **kwargs)
|
|
except Exception as e:
|
|
LOG.exception(e)
|
|
raise exceptions.SyncRequestFailed
|
|
return newkeypair
|
|
|
|
def delete_keypair(self, request, rsrc):
|
|
subcloud_rsrc = self.get_db_subcloud_resource(rsrc.id)
|
|
if not subcloud_rsrc:
|
|
return
|
|
name, user_id = utils.keypair_deconstruct_id(rsrc.master_id)
|
|
log_str = subcloud_rsrc.subcloud_resource_id + " " + name + "/" + user_id
|
|
kwargs = {}
|
|
kwargs["user_id"] = user_id
|
|
try:
|
|
self.sc_nova_client.keypairs.delete(name, **kwargs)
|
|
except novaclient_exceptions.NotFound:
|
|
# Keypair already deleted in subcloud, carry on.
|
|
LOG.info(
|
|
"Keypair {} not found in subcloud, may be already deleted".format(
|
|
log_str
|
|
),
|
|
extra=self.log_extra,
|
|
)
|
|
subcloud_rsrc.delete()
|
|
# Master Resource can be deleted only when all subcloud resources
|
|
# are deleted along with corresponding orch_job and orch_requests.
|
|
# pylint: disable=E1101
|
|
LOG.info(
|
|
"Keypair {}:{} [{}] deleted".format(rsrc.id, subcloud_rsrc.id, log_str),
|
|
extra=self.log_extra,
|
|
)
|
|
|
|
def get_all_resources(self, resource_type):
|
|
if resource_type == consts.RESOURCE_TYPE_COMPUTE_KEYPAIR:
|
|
# Keypair has unique id (name) per user. And, there is no API to
|
|
# retrieve all keypairs at once. So, keypair for each user is
|
|
# retrieved individually.
|
|
try:
|
|
m_resources = []
|
|
sc_resources = []
|
|
users = self.ks_client.users.list()
|
|
users_with_kps = set()
|
|
for user in users:
|
|
user_keypairs = self.get_keypair_resources(
|
|
self.m_nova_client, user.id
|
|
)
|
|
if user_keypairs:
|
|
m_resources.extend(user_keypairs)
|
|
users_with_kps.add(user.id)
|
|
db_resources = self.get_db_master_resources(resource_type)
|
|
# Query the subcloud for only the users-with-keypairs in the
|
|
# master cloud
|
|
for userid in users_with_kps:
|
|
sc_user_keypairs = self.get_keypair_resources(
|
|
self.sc_nova_client, userid
|
|
)
|
|
if sc_user_keypairs:
|
|
sc_resources.extend(sc_user_keypairs)
|
|
LOG.info(
|
|
"get_all_resources: users_with_kps={}".format(users_with_kps),
|
|
extra=self.log_extra,
|
|
)
|
|
return m_resources, db_resources, sc_resources
|
|
except (
|
|
keystone_exceptions.connection.ConnectTimeout,
|
|
keystone_exceptions.ConnectFailure,
|
|
) as e:
|
|
LOG.info(
|
|
"get_all_resources: subcloud {} is not reachable [{}]".format(
|
|
self.region_name, str(e)
|
|
),
|
|
extra=self.log_extra,
|
|
)
|
|
return None, None, None
|
|
except Exception as e:
|
|
LOG.exception(e)
|
|
return None, None, None
|
|
else:
|
|
return super(ComputeSyncThread, self).get_all_resources(resource_type)
|
|
|
|
def get_keypair_resources(self, nc, user_id):
|
|
keypairs = nc.keypairs.list(user_id)
|
|
for keypair in keypairs:
|
|
keypair._info["keypair"]["user_id"] = user_id
|
|
return keypairs
|
|
|
|
def same_keypair(self, k1, k2):
|
|
return (
|
|
k1.name == k2.name
|
|
and k1.type == k2.type
|
|
and k1.fingerprint == k2.fingerprint
|
|
and (k1._info["keypair"]["user_id"] == k2._info["keypair"]["user_id"])
|
|
)
|
|
|
|
# ---- quota_set resource operations ----
|
|
def put_compute_quota_set(self, request, rsrc):
|
|
project_id = request.orch_job.source_resource_id
|
|
|
|
# Get the new global limits from the request.
|
|
quota_dict = jsonutils.loads(request.orch_job.resource_info)
|
|
|
|
# Extract the user_id if there is one.
|
|
user_id = quota_dict.pop("user_id", None)
|
|
|
|
# Calculate the new limits for this subcloud (factoring in the
|
|
# existing usage).
|
|
quota_dict = quota_manager.QuotaManager.calculate_subcloud_project_quotas(
|
|
project_id, user_id, quota_dict, self.region_name
|
|
)
|
|
|
|
# Force the update in case existing usage is higher.
|
|
quota_dict["force"] = True
|
|
|
|
# Apply the limits to the subcloud.
|
|
self.sc_nova_client.quotas.update(project_id, user_id=user_id, **quota_dict)
|
|
# Persist the subcloud resource. (Not really applicable for quotas.)
|
|
self.persist_db_subcloud_resource(rsrc.id, rsrc.master_id)
|
|
LOG.info(
|
|
"Updated quotas {} for tenant {} and user {}".format(
|
|
quota_dict, rsrc.master_id, user_id
|
|
),
|
|
extra=self.log_extra,
|
|
)
|
|
|
|
def delete_compute_quota_set(self, request, rsrc):
|
|
# There's tricky behaviour here, pay attention!
|
|
|
|
# If you delete a quota-set for a tenant nova will automatically
|
|
# delete all tenant/user quota-sets within that tenant.
|
|
|
|
# If we delete a tenant/user quota-set in the master then we want to
|
|
# delete it in the subcloud as well. Nothing more is needed.
|
|
#
|
|
# If we delete a tenant quota-set in the master then we want to delete
|
|
# it in the subcloud as well (to force deletion of all related
|
|
# tenant/user quota-sets. However, we then need to recalculate the
|
|
# quota-set for that tenant in all the subclouds based on the current
|
|
# usage and the default quotas.
|
|
|
|
project_id = request.orch_job.source_resource_id
|
|
|
|
# Get the request info from the request.
|
|
req_info = jsonutils.loads(request.orch_job.resource_info)
|
|
|
|
# Extract the user_id if there is one.
|
|
user_id = req_info.pop("user_id", None)
|
|
|
|
# Delete the quota set in the subcloud. If user_id is None this will
|
|
# also delete the quota-sets for all users within this project.
|
|
self.sc_nova_client.quotas.delete(project_id, user_id)
|
|
|
|
# Clean up the subcloud resource entry in the DB.
|
|
subcloud_rsrc = self.get_db_subcloud_resource(rsrc.id)
|
|
if subcloud_rsrc:
|
|
subcloud_rsrc.delete()
|
|
|
|
# If we deleted a user/tenant quota-set we're done.
|
|
if user_id is not None:
|
|
return
|
|
|
|
# If we deleted a tenant quota-set we need to recalculate the
|
|
# tenant quota-set in the subcloud based on the default quotas
|
|
# in the master cloud.
|
|
|
|
# Get the new global quotas
|
|
quota_resource = self.m_nova_client.quotas.get(project_id)
|
|
quota_dict = quota_resource.to_dict()
|
|
|
|
# Get rid of the "id" field before doing any calculations
|
|
quota_dict.pop("id", None)
|
|
|
|
# Calculate the new limits for this subcloud (factoring in the
|
|
# existing usage).
|
|
quota_dict = quota_manager.QuotaManager.calculate_subcloud_project_quotas(
|
|
project_id, user_id, quota_dict, self.region_name
|
|
)
|
|
|
|
# Force the update in case existing usage is higher.
|
|
quota_dict["force"] = True
|
|
|
|
# Apply the limits to the subcloud.
|
|
self.sc_nova_client.quotas.update(project_id, user_id=user_id, **quota_dict)
|
|
|
|
# ---- quota_set resource operations ----
|
|
def put_quota_class_set(self, request, rsrc):
|
|
# Only a class_id of "default" is meaningful to nova.
|
|
class_id = request.orch_job.source_resource_id
|
|
|
|
# Get the new quota class limits from the request.
|
|
quota_dict = jsonutils.loads(request.orch_job.resource_info)
|
|
|
|
# If this is coming from the audit we need to remove the "id" field.
|
|
quota_dict.pop("id", None)
|
|
|
|
# Apply the new quota class limits to the subcloud.
|
|
self.sc_nova_client.quota_classes.update(class_id, **quota_dict)
|
|
|
|
# Persist the subcloud resource. (Not really applicable for quotas.)
|
|
self.persist_db_subcloud_resource(rsrc.id, rsrc.master_id)
|
|
LOG.info(
|
|
"Updated quota classes {} for class {}".format(quota_dict, rsrc.master_id),
|
|
extra=self.log_extra,
|
|
)
|
|
|
|
# This will only be called by the audit code.
|
|
def create_quota_class_set(self, request, rsrc):
|
|
self.put_quota_class_set(request, rsrc)
|
|
|
|
def same_quota_class(self, qc1, qc2):
|
|
# The audit code will pass in QuotaClassSet objects, we need to
|
|
# convert them before comparing them.
|
|
return qc1.to_dict() == qc2.to_dict()
|
|
|
|
def get_quota_class_resources(self, nc):
|
|
# We only care about the "default" class since it's the only one
|
|
# that actually affects nova.
|
|
try:
|
|
quota_class = nc.quota_classes.get("default")
|
|
return [quota_class]
|
|
except (
|
|
keystone_exceptions.connection.ConnectTimeout,
|
|
keystone_exceptions.ConnectFailure,
|
|
) as e:
|
|
LOG.info(
|
|
"get_quota_class: subcloud {} is not reachable [{}]".format(
|
|
self.region_name, str(e)
|
|
),
|
|
extra=self.log_extra,
|
|
)
|
|
return None
|
|
except Exception as e:
|
|
LOG.exception(e)
|
|
return None
|