Coverage for nova/db/api/models.py: 95%
214 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-17 15:08 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-17 15:08 +0000
1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
14from oslo_db.sqlalchemy import models
15from oslo_log import log as logging
16import sqlalchemy as sa
17import sqlalchemy.dialects.mysql
18from sqlalchemy.ext import declarative
19from sqlalchemy import orm
20from sqlalchemy import schema
22from nova.db import types
24LOG = logging.getLogger(__name__)
27# NOTE(stephenfin): This is a list of fields that have been removed from
28# various SQLAlchemy models but which still exist in the underlying tables. Our
29# upgrade policy dictates that we remove fields from models at least one cycle
30# before we remove the column from the underlying table. Not doing so would
31# prevent us from applying the new database schema before rolling out any of
32# the new code since the old code could attempt to access data in the removed
33# columns. Alembic identifies this temporary mismatch between the models and
34# underlying tables and attempts to resolve it. Tell it instead to ignore these
35# until we're ready to remove them ourselves.
36REMOVED_COLUMNS = []
38# NOTE(stephenfin): A list of foreign key constraints that were removed when
39# the column they were covering was removed.
40REMOVED_FKEYS = []
42# NOTE(stephenfin): A list of entire models that have been removed.
43REMOVED_TABLES = {
44 # Tables that were moved the placement database in Train. The
45 # models were removed in Y and the tables can be dropped in Z or
46 # later
47 'allocations',
48 'consumers',
49 'inventories',
50 'placement_aggregates',
51 'projects',
52 'resource_classes',
53 'resource_provider_aggregates',
54 'resource_provider_traits',
55 'resource_providers',
56 'traits',
57 'users',
58}
61class _NovaAPIBase(models.ModelBase, models.TimestampMixin):
62 pass
65BASE = declarative.declarative_base(cls=_NovaAPIBase)
68class AggregateHost(BASE):
69 """Represents a host that is member of an aggregate."""
70 __tablename__ = 'aggregate_hosts'
71 __table_args__ = (schema.UniqueConstraint(
72 "host", "aggregate_id",
73 name="uniq_aggregate_hosts0host0aggregate_id"
74 ),
75 )
76 id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
77 host = sa.Column(sa.String(255))
78 aggregate_id = sa.Column(
79 sa.Integer, sa.ForeignKey('aggregates.id'), nullable=False)
82class AggregateMetadata(BASE):
83 """Represents a metadata key/value pair for an aggregate."""
84 __tablename__ = 'aggregate_metadata'
85 __table_args__ = (
86 schema.UniqueConstraint("aggregate_id", "key",
87 name="uniq_aggregate_metadata0aggregate_id0key"
88 ),
89 sa.Index('aggregate_metadata_key_idx', 'key'),
90 )
91 id = sa.Column(sa.Integer, primary_key=True)
92 key = sa.Column(sa.String(255), nullable=False)
93 value = sa.Column(sa.String(255), nullable=False)
94 aggregate_id = sa.Column(
95 sa.Integer, sa.ForeignKey('aggregates.id'), nullable=False)
98class Aggregate(BASE):
99 """Represents a cluster of hosts that exists in this zone."""
100 __tablename__ = 'aggregates'
101 __table_args__ = (
102 sa.Index('aggregate_uuid_idx', 'uuid'),
103 schema.UniqueConstraint("name", name="uniq_aggregate0name")
104 )
105 id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
106 uuid = sa.Column(sa.String(36))
107 name = sa.Column(sa.String(255))
108 _hosts = orm.relationship(
109 AggregateHost,
110 primaryjoin='Aggregate.id == AggregateHost.aggregate_id',
111 cascade='delete')
112 _metadata = orm.relationship(
113 AggregateMetadata,
114 primaryjoin='Aggregate.id == AggregateMetadata.aggregate_id',
115 cascade='delete')
117 @property
118 def _extra_keys(self):
119 return ['hosts', 'metadetails', 'availability_zone']
121 @property
122 def hosts(self):
123 return [h.host for h in self._hosts]
125 @property
126 def metadetails(self):
127 return {m.key: m.value for m in self._metadata}
129 @property
130 def availability_zone(self):
131 if 'availability_zone' not in self.metadetails:
132 return None
133 return self.metadetails['availability_zone']
136class CellMapping(BASE):
137 """Contains information on communicating with a cell"""
138 __tablename__ = 'cell_mappings'
139 __table_args__ = (
140 sa.Index('uuid_idx', 'uuid'),
141 schema.UniqueConstraint('uuid', name='uniq_cell_mappings0uuid'),
142 )
144 id = sa.Column(sa.Integer, primary_key=True)
145 uuid = sa.Column(sa.String(36), nullable=False)
146 name = sa.Column(sa.String(255))
147 transport_url = sa.Column(sa.Text())
148 database_connection = sa.Column(sa.Text())
149 disabled = sa.Column(sa.Boolean, default=False)
151 host_mapping = orm.relationship(
152 'HostMapping',
153 back_populates='cell_mapping',
154 )
155 instance_mapping = orm.relationship(
156 'InstanceMapping',
157 back_populates='cell_mapping',
158 )
161class InstanceMapping(BASE):
162 """Contains the mapping of an instance to which cell it is in"""
163 __tablename__ = 'instance_mappings'
164 __table_args__ = (
165 sa.Index('project_id_idx', 'project_id'),
166 sa.Index('instance_uuid_idx', 'instance_uuid'),
167 schema.UniqueConstraint(
168 'instance_uuid', name='uniq_instance_mappings0instance_uuid'),
169 sa.Index(
170 'instance_mappings_user_id_project_id_idx',
171 'user_id',
172 'project_id',
173 ),
174 )
176 id = sa.Column(sa.Integer, primary_key=True)
177 instance_uuid = sa.Column(sa.String(36), nullable=False)
178 cell_id = sa.Column(
179 sa.Integer, sa.ForeignKey('cell_mappings.id'), nullable=True)
180 project_id = sa.Column(sa.String(255), nullable=False)
181 # FIXME(melwitt): This should eventually be non-nullable, but we need a
182 # transition period first.
183 user_id = sa.Column(sa.String(255), nullable=True)
184 queued_for_delete = sa.Column(sa.Boolean)
186 cell_mapping = orm.relationship(
187 'CellMapping',
188 back_populates='instance_mapping',
189 )
192class HostMapping(BASE):
193 """Contains mapping of a compute host to which cell it is in"""
194 __tablename__ = "host_mappings"
195 __table_args__ = (
196 sa.Index('host_idx', 'host'),
197 schema.UniqueConstraint('host', name='uniq_host_mappings0host'),
198 )
200 id = sa.Column(sa.Integer, primary_key=True)
201 cell_id = sa.Column(
202 sa.Integer, sa.ForeignKey('cell_mappings.id'), nullable=False)
203 host = sa.Column(sa.String(255), nullable=False)
205 cell_mapping = orm.relationship(
206 'CellMapping',
207 back_populates='host_mapping',
208 )
211class RequestSpec(BASE):
212 """Represents the information passed to the scheduler."""
214 __tablename__ = 'request_specs'
215 __table_args__ = (
216 sa.Index('request_spec_instance_uuid_idx', 'instance_uuid'),
217 schema.UniqueConstraint(
218 'instance_uuid', name='uniq_request_specs0instance_uuid'),
219 )
221 id = sa.Column(sa.Integer, primary_key=True)
222 instance_uuid = sa.Column(sa.String(36), nullable=False)
223 spec = sa.Column(types.MediumText(), nullable=False)
226class Flavors(BASE):
227 """Represents possible flavors for instances"""
228 __tablename__ = 'flavors'
229 __table_args__ = (
230 schema.UniqueConstraint("flavorid", name="uniq_flavors0flavorid"),
231 schema.UniqueConstraint("name", name="uniq_flavors0name"))
233 id = sa.Column(sa.Integer, primary_key=True)
234 name = sa.Column(sa.String(255), nullable=False)
235 memory_mb = sa.Column(sa.Integer, nullable=False)
236 vcpus = sa.Column(sa.Integer, nullable=False)
237 root_gb = sa.Column(sa.Integer)
238 ephemeral_gb = sa.Column(sa.Integer)
239 flavorid = sa.Column(sa.String(255), nullable=False)
240 swap = sa.Column(sa.Integer, nullable=False, default=0)
241 rxtx_factor = sa.Column(sa.Float, default=1)
242 vcpu_weight = sa.Column(sa.Integer)
243 disabled = sa.Column(sa.Boolean, default=False)
244 is_public = sa.Column(sa.Boolean, default=True)
245 description = sa.Column(sa.Text)
247 extra_specs = orm.relationship('FlavorExtraSpecs', back_populates='flavor')
248 projects = orm.relationship('FlavorProjects', back_populates='flavor')
251class FlavorExtraSpecs(BASE):
252 """Represents additional specs as key/value pairs for a flavor"""
253 __tablename__ = 'flavor_extra_specs'
254 __table_args__ = (
255 sa.Index('flavor_extra_specs_flavor_id_key_idx', 'flavor_id', 'key'),
256 schema.UniqueConstraint('flavor_id', 'key',
257 name='uniq_flavor_extra_specs0flavor_id0key'),
258 {'mysql_collate': 'utf8_bin'},
259 )
261 id = sa.Column(sa.Integer, primary_key=True)
262 key = sa.Column(sa.String(255), nullable=False)
263 value = sa.Column(sa.String(255))
264 flavor_id = sa.Column(
265 sa.Integer, sa.ForeignKey('flavors.id'), nullable=False)
267 flavor = orm.relationship(Flavors, back_populates='extra_specs')
270class FlavorProjects(BASE):
271 """Represents projects associated with flavors"""
272 __tablename__ = 'flavor_projects'
273 __table_args__ = (schema.UniqueConstraint('flavor_id', 'project_id',
274 name='uniq_flavor_projects0flavor_id0project_id'),)
276 id = sa.Column(sa.Integer, primary_key=True)
277 flavor_id = sa.Column(
278 sa.Integer, sa.ForeignKey('flavors.id'), nullable=False)
279 project_id = sa.Column(sa.String(255), nullable=False)
281 flavor = orm.relationship(Flavors, back_populates='projects')
284class BuildRequest(BASE):
285 """Represents the information passed to the scheduler."""
287 __tablename__ = 'build_requests'
288 __table_args__ = (
289 sa.Index('build_requests_instance_uuid_idx', 'instance_uuid'),
290 sa.Index('build_requests_project_id_idx', 'project_id'),
291 schema.UniqueConstraint(
292 'instance_uuid', name='uniq_build_requests0instance_uuid'),
293 )
295 id = sa.Column(sa.Integer, primary_key=True)
296 # TODO(mriedem): instance_uuid should be nullable=False
297 instance_uuid = sa.Column(sa.String(36))
298 project_id = sa.Column(sa.String(255), nullable=False)
299 instance = sa.Column(types.MediumText())
300 block_device_mappings = sa.Column(types.MediumText())
301 tags = sa.Column(sa.Text())
304class KeyPair(BASE):
305 """Represents a public key pair for ssh / WinRM."""
306 __tablename__ = 'key_pairs'
307 __table_args__ = (
308 schema.UniqueConstraint(
309 "user_id", "name", name="uniq_key_pairs0user_id0name"),
310 )
312 id = sa.Column(sa.Integer, primary_key=True, nullable=False)
313 name = sa.Column(sa.String(255), nullable=False)
314 user_id = sa.Column(sa.String(255), nullable=False)
315 fingerprint = sa.Column(sa.String(255))
316 public_key = sa.Column(sa.Text())
317 type = sa.Column(
318 sa.Enum('ssh', 'x509', name='keypair_types'),
319 nullable=False, server_default='ssh')
322class InstanceGroupMember(BASE):
323 """Represents the members for an instance group."""
324 __tablename__ = 'instance_group_member'
325 __table_args__ = (
326 sa.Index('instance_group_member_instance_idx', 'instance_uuid'),
327 )
328 id = sa.Column(sa.Integer, primary_key=True, nullable=False)
329 instance_uuid = sa.Column(sa.String(255))
330 group_id = sa.Column(
331 sa.Integer, sa.ForeignKey('instance_groups.id'), nullable=False)
334class InstanceGroupPolicy(BASE):
335 """Represents the policy type for an instance group."""
336 __tablename__ = 'instance_group_policy'
337 __table_args__ = (
338 sa.Index('instance_group_policy_policy_idx', 'policy'),
339 )
341 id = sa.Column(sa.Integer, primary_key=True, nullable=False)
342 policy = sa.Column(sa.String(255))
343 group_id = sa.Column(
344 sa.Integer, sa.ForeignKey('instance_groups.id'), nullable=False)
345 rules = sa.Column(sa.Text)
348class InstanceGroup(BASE):
349 """Represents an instance group.
351 A group will maintain a collection of instances and the relationship
352 between them.
353 """
355 __tablename__ = 'instance_groups'
356 __table_args__ = (
357 schema.UniqueConstraint('uuid', name='uniq_instance_groups0uuid'),
358 )
360 id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
361 user_id = sa.Column(sa.String(255))
362 project_id = sa.Column(sa.String(255))
363 uuid = sa.Column(sa.String(36), nullable=False)
364 name = sa.Column(sa.String(255))
366 _policies = orm.relationship(InstanceGroupPolicy)
367 _members = orm.relationship(InstanceGroupMember)
369 @property
370 def policy(self):
371 if len(self._policies) > 1: 371 ↛ 372line 371 didn't jump to line 372 because the condition on line 371 was never true
372 msg = ("More than one policy (%(policies)s) is associated with "
373 "group %(group_name)s, only the first one in the list "
374 "would be returned.")
375 LOG.warning(msg, {"policies": [p.policy for p in self._policies],
376 "group_name": self.name})
377 return self._policies[0] if self._policies else None
379 @property
380 def members(self):
381 return [m.instance_uuid for m in self._members]
384class Quota(BASE):
385 """Represents a single quota override for a project.
387 If there is no row for a given project id and resource, then the
388 default for the quota class is used. If there is no row for a
389 given quota class and resource, then the default for the
390 deployment is used. If the row is present but the hard limit is
391 Null, then the resource is unlimited.
392 """
393 __tablename__ = 'quotas'
394 __table_args__ = (
395 schema.UniqueConstraint(
396 "project_id",
397 "resource",
398 name="uniq_quotas0project_id0resource"
399 ),
400 )
402 id = sa.Column(sa.Integer, primary_key=True)
403 project_id = sa.Column(sa.String(255))
404 resource = sa.Column(sa.String(255), nullable=False)
405 hard_limit = sa.Column(sa.Integer)
408class ProjectUserQuota(BASE):
409 """Represents a single quota override for a user with in a project."""
411 __tablename__ = 'project_user_quotas'
412 __table_args__ = (
413 schema.UniqueConstraint(
414 "user_id",
415 "project_id",
416 "resource",
417 name="uniq_project_user_quotas0user_id0project_id0resource",
418 ),
419 sa.Index(
420 'project_user_quotas_project_id_idx', 'project_id'),
421 sa.Index(
422 'project_user_quotas_user_id_idx', 'user_id',)
423 )
425 id = sa.Column(sa.Integer, primary_key=True, nullable=False)
426 project_id = sa.Column(sa.String(255), nullable=False)
427 user_id = sa.Column(sa.String(255), nullable=False)
428 resource = sa.Column(sa.String(255), nullable=False)
429 hard_limit = sa.Column(sa.Integer)
432class QuotaClass(BASE):
433 """Represents a single quota override for a quota class.
435 If there is no row for a given quota class and resource, then the
436 default for the deployment is used. If the row is present but the
437 hard limit is Null, then the resource is unlimited.
438 """
440 __tablename__ = 'quota_classes'
441 __table_args__ = (
442 sa.Index('quota_classes_class_name_idx', 'class_name'),
443 )
444 id = sa.Column(sa.Integer, primary_key=True)
446 class_name = sa.Column(sa.String(255))
448 resource = sa.Column(sa.String(255))
449 hard_limit = sa.Column(sa.Integer)
452class QuotaUsage(BASE):
453 """Represents the current usage for a given resource."""
455 __tablename__ = 'quota_usages'
456 __table_args__ = (
457 sa.Index('quota_usages_project_id_idx', 'project_id'),
458 sa.Index('quota_usages_user_id_idx', 'user_id'),
459 )
461 id = sa.Column(sa.Integer, primary_key=True)
462 project_id = sa.Column(sa.String(255))
463 user_id = sa.Column(sa.String(255))
464 resource = sa.Column(sa.String(255), nullable=False)
465 in_use = sa.Column(sa.Integer, nullable=False)
466 reserved = sa.Column(sa.Integer, nullable=False)
468 @property
469 def total(self):
470 return self.in_use + self.reserved
472 until_refresh = sa.Column(sa.Integer)
475class Reservation(BASE):
476 """Represents a resource reservation for quotas."""
478 __tablename__ = 'reservations'
479 __table_args__ = (
480 sa.Index('reservations_project_id_idx', 'project_id'),
481 sa.Index('reservations_uuid_idx', 'uuid'),
482 sa.Index('reservations_expire_idx', 'expire'),
483 sa.Index('reservations_user_id_idx', 'user_id'),
484 )
486 id = sa.Column(sa.Integer, primary_key=True, nullable=False)
487 uuid = sa.Column(sa.String(36), nullable=False)
488 usage_id = sa.Column(
489 sa.Integer, sa.ForeignKey('quota_usages.id'), nullable=False)
490 project_id = sa.Column(sa.String(255))
491 user_id = sa.Column(sa.String(255))
492 resource = sa.Column(sa.String(255))
493 delta = sa.Column(sa.Integer, nullable=False)
494 expire = sa.Column(sa.DateTime)
496 usage = orm.relationship('QuotaUsage')