|
19 | 19 | from oslo_log import log as logging |
20 | 20 |
|
21 | 21 | import nova.conf |
| 22 | +from nova import context as nova_context |
| 23 | +from nova.objects.instance import Instance |
| 24 | +from nova.objects.instance import InstanceList |
22 | 25 | from nova.scheduler import filters |
23 | 26 | from nova.scheduler import utils |
24 | 27 | from nova import utils as nova_utils |
|
28 | 31 | CONF = nova.conf.CONF |
29 | 32 |
|
30 | 33 | _SERVICE_AUTH = None |
| 34 | +GARDENER_PREFIX = "kubernetes.io-cluster-shoot--garden--" |
| 35 | +KKS_PREFIX = "kubernikus:kluster" |
| 36 | +HANA_PREFIX = "hana_" |
31 | 37 |
|
32 | 38 |
|
33 | 39 | class ShardFilter(filters.BaseHostFilter): |
@@ -114,11 +120,61 @@ def _get_shards(self, project_id): |
114 | 120 |
|
115 | 121 | return self._PROJECT_SHARD_CACHE.get(project_id) |
116 | 122 |
|
117 | | - def host_passes(self, host_state, spec_obj): |
| 123 | + def _get_k8s_cluster_instances(self, spec_obj): |
| 124 | + """If the instance will be part of a K8S cluster, it returns |
| 125 | + the list of all other instances that are already part of it, |
| 126 | + if any. |
| 127 | + """ |
| 128 | + k8s_filter = self._k8s_instance_query_filter(spec_obj) |
| 129 | + |
| 130 | + if not k8s_filter: |
| 131 | + return [] |
| 132 | + |
| 133 | + k8s_filter['project_id'] = spec_obj.project_id |
| 134 | + |
| 135 | + return InstanceList.get_by_filters( |
| 136 | + nova_context.get_admin_context(), filters=k8s_filter, |
| 137 | + expected_attrs=['flavor', 'metadata', 'tags']) |
| 138 | + |
| 139 | + def _k8s_instance_query_filter(self, spec_obj): |
| 140 | + elevated = nova_context.get_admin_context() |
| 141 | + instance = Instance.get_by_uuid(elevated, spec_obj.instance_uuid, |
| 142 | + expected_attrs=['tags', 'metadata']) |
| 143 | + |
| 144 | + # Kubernikus |
| 145 | + kks_tag = next((t.tag for t in instance.tags |
| 146 | + if t.tag.startswith(KKS_PREFIX)), None) |
| 147 | + if kks_tag: |
| 148 | + return {'tags': [kks_tag]} |
| 149 | + |
| 150 | + # Gardener |
| 151 | + gardener_meta = {k: v for k, v in instance.metadata.items() |
| 152 | + if k.starswith(GARDENER_PREFIX)} |
| 153 | + if gardener_meta: |
| 154 | + return {'metadata': gardener_meta} |
| 155 | + |
| 156 | + return None |
| 157 | + |
| 158 | + def filter_all(self, filter_obj_list, spec_obj): |
| 159 | + """Yield objects that pass the filter. |
| 160 | +
|
| 161 | + Can be overridden in a subclass, if you need to base filtering |
| 162 | + decisions on all objects. Otherwise, one can just override |
| 163 | + _filter_one() to filter a single object. |
| 164 | + """ |
118 | 165 | # Only VMware |
119 | 166 | if utils.is_non_vmware_spec(spec_obj): |
120 | | - return True |
| 167 | + for obj in filter_obj_list: |
| 168 | + yield obj |
| 169 | + return |
| 170 | + |
| 171 | + k8s_instances = self._get_k8s_cluster_instances(spec_obj) |
| 172 | + |
| 173 | + for obj in filter_obj_list: |
| 174 | + if self._host_passes(obj, spec_obj, k8s_instances): |
| 175 | + yield obj |
121 | 176 |
|
| 177 | + def _host_passes(self, host_state, spec_obj, k8s_instances): |
122 | 178 | host_shard_aggrs = [aggr for aggr in host_state.aggregates |
123 | 179 | if aggr.name.startswith(self._SHARD_PREFIX)] |
124 | 180 |
|
@@ -148,18 +204,58 @@ def host_passes(self, host_state, spec_obj): |
148 | 204 | if self._ALL_SHARDS in shards: |
149 | 205 | LOG.debug('project enabled for all shards %(project_shards)s.', |
150 | 206 | {'project_shards': shards}) |
151 | | - return True |
152 | 207 | elif host_shard_names & set(shards): |
153 | 208 | LOG.debug('%(host_state)s shard %(host_shard)s found in project ' |
154 | 209 | 'shards %(project_shards)s.', |
155 | 210 | {'host_state': host_state, |
156 | 211 | 'host_shard': host_shard_names, |
157 | 212 | 'project_shards': shards}) |
158 | | - return True |
159 | 213 | else: |
160 | 214 | LOG.debug('%(host_state)s shard %(host_shard)s not found in ' |
161 | 215 | 'project shards %(project_shards)s.', |
162 | 216 | {'host_state': host_state, |
163 | 217 | 'host_shard': host_shard_names, |
164 | 218 | 'project_shards': shards}) |
165 | 219 | return False |
| 220 | + |
| 221 | + if not utils.request_is_resize(spec_obj): |
| 222 | + # K8S orchestrators are only creating or deleting nodes, |
| 223 | + # therefore we shouldn't infer with resize/migrate requests. |
| 224 | + return self._host_passes_k8s(host_state, host_shard_names, |
| 225 | + spec_obj, k8s_instances) |
| 226 | + |
| 227 | + return True |
| 228 | + |
| 229 | + def _host_passes_k8s(self, host_state, host_shard_names, spec_obj, |
| 230 | + k8s_instances): |
| 231 | + """Instances of a K8S cluster must end up on the same shard. |
| 232 | + The K8S cluster is identified by the metadata or tags set |
| 233 | + by the orchestrator (Gardener or Kubernikus). |
| 234 | + """ |
| 235 | + if not k8s_instances: |
| 236 | + # There are no instances in the cluster, yet. |
| 237 | + # We allow any shard for the first instance. |
| 238 | + return True |
| 239 | + |
| 240 | + def _is_hana(flavor): |
| 241 | + return flavor.name.startswith(HANA_PREFIX) |
| 242 | + |
| 243 | + def _is_same_category(instance, flavor): |
| 244 | + """Check whether instance is from the flavor's family.""" |
| 245 | + if _is_hana(flavor): |
| 246 | + return _is_hana(instance.flavor) |
| 247 | + return True |
| 248 | + |
| 249 | + def _instance_matches(instance): |
| 250 | + if spec_obj.availability_zone: |
| 251 | + if (instance.availability_zone != |
| 252 | + spec_obj.availability_zone): |
| 253 | + return False |
| 254 | + return _is_same_category(instance, spec_obj.flavor) |
| 255 | + |
| 256 | + k8s_hosts = set([i.host for i in k8s_instances |
| 257 | + if _instance_matches(i)]) |
| 258 | + |
| 259 | + return any(agg.name in host_shard_names and |
| 260 | + set(agg.hosts) & k8s_hosts |
| 261 | + for agg in host_state.aggregates) |
0 commit comments