Nova は RPC(メッセージキュー)の他にデータベースを必要とします。 現在は SQLAlchemy ライブラリで利用可能なリレーショナルデータベース(RDBMS)のみ対応していますが、将来的に NoSQL 等が利用できるように、データベースドライバはモジュール構造となっています。
nova-2012.1
nova/
db/
__init__.py
api.py DB API(インターフェース)
base.py
migration.py DB のバージョン管理
sqlalchemy/ SQLalchemy(SQL O/Rマッパ)用ドライバ
__init__.py
api.py DB API実体(SQLAlchemy 用)
migration.py DB バージョン管理
models.py DB モデル定義
session.py
migrate_repo/ DB バージョン関連
README
__init__.py
manage.py
manage.cfg
versions/ DB マイグレーション(バージョンアップ/ダウン)用
__init__.py
001_austin.py
002_bexar.py
...
2つある api.py が気になりますね。nova-2012/nova/db/api.py にはメソッド定義だけで、実際の DB 操作は nova-2012/nova/db/sqlalchemy/api.py 内にあります。よって、DB 操作 API を追加する際には、2つの api.py 両方に記述が必要です。
SQLAlchemy ドライバの場合、DB のスキーマ定義が必要になります。これは nova-2012/nova/db/sqlalchemy/models.py に記述されています。例えば、キーペア用のテーブルスキーマは以下の通りです。
1 2 3 4 5 6 7 8 9 10 11 | class KeyPair(BASE, NovaBase):
"""Represents a public key pair for ssh."""
__tablename__ = 'key_pairs'
id = Column(Integer, primary_key=True)
name = Column(String(255))
user_id = Column(String(255))
fingerprint = Column(String(255))
public_key = Column(Text)
|
注意したいのは、このスキーマモデルクラスが他のスキーマモデルクラスと同様 NovaBase クラスを継承している事です。:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | class NovaBase(object):
"""Base class for Nova Models."""
__table_args__ = {'mysql_engine': 'InnoDB'}
__table_initialized__ = False
created_at = Column(DateTime, default=utils.utcnow)
updated_at = Column(DateTime, onupdate=utils.utcnow)
deleted_at = Column(DateTime)
deleted = Column(Boolean, default=False)
metadata = None
def save(self, session=None):
"""Save this object."""
if not session:
session = get_session()
session.add(self)
try:
session.flush()
except IntegrityError, e:
if str(e).endswith('is not unique'):
raise exception.Duplicate(str(e))
else:
raise
def delete(self, session=None):
"""Delete this object."""
self.deleted = True
self.deleted_at = utils.utcnow()
self.save(session=session)
def __setitem__(self, key, value):
setattr(self, key, value)
def __getitem__(self, key):
return getattr(self, key)
def get(self, key, default=None):
return getattr(self, key, default)
def __iter__(self):
self._i = iter(object_mapper(self).columns)
return self
def next(self):
n = self._i.next().name
return n, getattr(self, n)
def update(self, values):
"""Make the model object behave like a dict"""
for k, v in values.iteritems():
setattr(self, k, v)
def iteritems(self):
"""Make the model object behave like a dict.
Includes attributes from joins."""
local = dict(self)
joined = dict([(k, v) for k, v in self.__dict__.iteritems()
if not k[0] == '_'])
local.update(joined)
return local.iteritems()
|
NovaBase クラスでは、3つの時間属性と1つの真偽値がある事が解ります。
カラム名 | 用途 |
created_at | レコード作成日時 |
updated_at | レコード更新日時 |
deleted_at | レコード削除日時 |
deleted | 削除フラグ |
NovaBase クラスは、SQLalchemy の Base クラス API を拡張するための差分 API を定義しています。主な拡張内容は以下の3つです。
メソッド | 動作 |
save() | DB にレコードとして書き戻す際、”is not unique” で終わるエラー メッセージが出た場合は exception.Duplicate 例外を発生させる |
delete() | DB 上のレコードを削除せず、当該レコードの削除フラグ(deleted) を True、削除時刻(deleted_at)に現在時刻をセットする。 |
__setitem__(), __getitem__(), get(), __iter__(), next(), update(), iteritems() | 辞書型(foo[‘bar’])としての値操作を属性(foo.bar)操作に変換 する。 |
上記の仕様は Nova で使用される全スキーマに適用されます。 よって、現在の Nova ではモデルインスタンスを削除しても DB 上の当該レコードが削除されず、結果として DB が肥大化し続ける仕様となっており、運用面では削除済みのレコードの掃除が課題となってきます。
なお、Nova の DB モデルでは辞書型としてカラム値を扱うのが基本になっています。そのため、上記表の通り NovaBase クラスには SQLAlchemy の属性値を辞書型の値として扱うためのコードが存在します。 この理由については https://lists.launchpad.net/openstack/msg06128.html のスレッドが参考になるでしょう。
DB API は各 manager のベースクラス(nova.db.base)中で self.db にセットされます。よって、通常は self.db.<APIメソッド> の形で使われます。
DB レコードの作成は通常、
self.db.<モデル名>_create(<コンテキスト>, <辞書型のパラメータ>)
で行います。ボリュームレコード作成の例を見てみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | options = {
'size': size,
'user_id': context.user_id,
'project_id': context.project_id,
'snapshot_id': snapshot_id,
'availability_zone': availability_zone,
'status': "creating",
'attach_status': "detached",
'display_name': name,
'display_description': description,
'volume_type_id': volume_type_id,
'metadata': metadata,
}
volume = self.db.volume_create(context, options)
|
DB レコードの取得は通常、
self.db.<モデル名>_get(<コンテキスト>, <ID>)
で行います。ボリュームレコード取得の例は以下の通りです。
1 2 3 4 5 | def get(self, context, volume_id):
rv = self.db.volume_get(context, volume_id)
volume = dict(rv.iteritems())
check_policy(context, 'get', volume)
return volume
|
DB レコードの更新は通常、
self.db.<モデル名>_update(<コンテキスト>, <ID>)
で行います。ボリュームレコード更新の例は以下の通りです。
1 2 3 | @wrap_check_policy
def update(self, context, volume, fields):
self.db.volume_update(context, volume['id'], fields)
|
DB レコードの削除は通常、
self.db.<モデル名>_destroy(<コンテキスト>, <ID>)
で行います。ボリュームレコード削除の例は以下の通りです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | def delete_snapshot(self, context, snapshot_id):
"""Deletes and unexports snapshot."""
context = context.elevated()
snapshot_ref = self.db.snapshot_get(context, snapshot_id)
try:
LOG.debug(_("snapshot %s: deleting"), snapshot_ref['name'])
self.driver.delete_snapshot(snapshot_ref)
except exception.SnapshotIsBusy:
LOG.debug(_("snapshot %s: snapshot is busy"), snapshot_ref['name'])
self.db.snapshot_update(context,
snapshot_ref['id'],
{'status': 'available'})
return True
except Exception:
with utils.save_and_reraise_exception():
self.db.snapshot_update(context,
snapshot_ref['id'],
{'status': 'error_deleting'})
self.db.snapshot_destroy(context, snapshot_id)
LOG.debug(_("snapshot %s: deleted successfully"), snapshot_ref['name'])
return True
|