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

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. 

12 

13 

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 

21 

22from nova.db import types 

23 

24LOG = logging.getLogger(__name__) 

25 

26 

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 = [] 

37 

38# NOTE(stephenfin): A list of foreign key constraints that were removed when 

39# the column they were covering was removed. 

40REMOVED_FKEYS = [] 

41 

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} 

59 

60 

61class _NovaAPIBase(models.ModelBase, models.TimestampMixin): 

62 pass 

63 

64 

65BASE = declarative.declarative_base(cls=_NovaAPIBase) 

66 

67 

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) 

80 

81 

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) 

96 

97 

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') 

116 

117 @property 

118 def _extra_keys(self): 

119 return ['hosts', 'metadetails', 'availability_zone'] 

120 

121 @property 

122 def hosts(self): 

123 return [h.host for h in self._hosts] 

124 

125 @property 

126 def metadetails(self): 

127 return {m.key: m.value for m in self._metadata} 

128 

129 @property 

130 def availability_zone(self): 

131 if 'availability_zone' not in self.metadetails: 

132 return None 

133 return self.metadetails['availability_zone'] 

134 

135 

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 ) 

143 

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) 

150 

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 ) 

159 

160 

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 ) 

175 

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) 

185 

186 cell_mapping = orm.relationship( 

187 'CellMapping', 

188 back_populates='instance_mapping', 

189 ) 

190 

191 

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 ) 

199 

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) 

204 

205 cell_mapping = orm.relationship( 

206 'CellMapping', 

207 back_populates='host_mapping', 

208 ) 

209 

210 

211class RequestSpec(BASE): 

212 """Represents the information passed to the scheduler.""" 

213 

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 ) 

220 

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) 

224 

225 

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")) 

232 

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) 

246 

247 extra_specs = orm.relationship('FlavorExtraSpecs', back_populates='flavor') 

248 projects = orm.relationship('FlavorProjects', back_populates='flavor') 

249 

250 

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 ) 

260 

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) 

266 

267 flavor = orm.relationship(Flavors, back_populates='extra_specs') 

268 

269 

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'),) 

275 

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) 

280 

281 flavor = orm.relationship(Flavors, back_populates='projects') 

282 

283 

284class BuildRequest(BASE): 

285 """Represents the information passed to the scheduler.""" 

286 

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 ) 

294 

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()) 

302 

303 

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 ) 

311 

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') 

320 

321 

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) 

332 

333 

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 ) 

340 

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) 

346 

347 

348class InstanceGroup(BASE): 

349 """Represents an instance group. 

350 

351 A group will maintain a collection of instances and the relationship 

352 between them. 

353 """ 

354 

355 __tablename__ = 'instance_groups' 

356 __table_args__ = ( 

357 schema.UniqueConstraint('uuid', name='uniq_instance_groups0uuid'), 

358 ) 

359 

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)) 

365 

366 _policies = orm.relationship(InstanceGroupPolicy) 

367 _members = orm.relationship(InstanceGroupMember) 

368 

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 

378 

379 @property 

380 def members(self): 

381 return [m.instance_uuid for m in self._members] 

382 

383 

384class Quota(BASE): 

385 """Represents a single quota override for a project. 

386 

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 ) 

401 

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) 

406 

407 

408class ProjectUserQuota(BASE): 

409 """Represents a single quota override for a user with in a project.""" 

410 

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 ) 

424 

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) 

430 

431 

432class QuotaClass(BASE): 

433 """Represents a single quota override for a quota class. 

434 

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 """ 

439 

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) 

445 

446 class_name = sa.Column(sa.String(255)) 

447 

448 resource = sa.Column(sa.String(255)) 

449 hard_limit = sa.Column(sa.Integer) 

450 

451 

452class QuotaUsage(BASE): 

453 """Represents the current usage for a given resource.""" 

454 

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 ) 

460 

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) 

467 

468 @property 

469 def total(self): 

470 return self.in_use + self.reserved 

471 

472 until_refresh = sa.Column(sa.Integer) 

473 

474 

475class Reservation(BASE): 

476 """Represents a resource reservation for quotas.""" 

477 

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 ) 

485 

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) 

495 

496 usage = orm.relationship('QuotaUsage')