OpenStack Nova Source Code入門

# はじめに

こちらの記事は、自身の頭の整理のために書きます。
OpenStackで提供されるクラウドサービスは各コンポーネントが協調して動くことにより実現されており(Nova, Cinder, Neutron...)。 さらに各コンポーネント自身も各モジュールの集合体(nova-compute, nova-consoleauth....)でできている。というのは、すでに周知のことかもしれませんが、いざソースコードを読もうと思うとディレクトリの多さと似ているファイル名、クラス名の多さにやる気を削がれた人もいらっしゃるかもしれません。Rest APIサーバ用のAPIクラス(nova/openstack/..)、RPC API Client用のAPIクラス(nova/compute/rpcapi.py)、意味のある単位で処理をまとめているソフトェア的な意味合いを込めたAPIクラス(nova/compute/api.py)

おそらくOpenStackに手を出す方は、ソフトウェアが目指す方向性の都合上、大規模ソースコードなソフトウェアの開発のバックグラウンドがない方もいらっしゃるかと思います。そういう方の少しでも役に立てればと思い記事を書きます

Pythonで書いてあるし、簡単に読めるよね？って話もあるかと思います。もちろん、特定のスコープ内での処理は、Pythonの方が読みやすいのは事実だと思いますが、これだけソースコードが多い場合、ソフトウェアの挙動からまず該当スコープのソースを特定することが難しくなります。個人的にはその大変さは言語ではなく、ソースコードの規模に依存すると考えているので。CでもPythonでも変わらないのではと思います。

OSSのこれだけ、大きくなったレポジトリですので各ディレクトリの目的、命名規則がしっかりしていると **思うので** ざっくり全体像が見えれば、ソフトウェアの挙動からソースの特定が簡単にできると思います。 

例えば、VMにフローティングIPをアサイン直後に特定の処理をしたいと思ったとき、どうやって、floating IPのアサインのコードを特定しますか。grep -R -n , git grep と方法はあるかと思いますが。その方法では手探りなので全体像把握は必須だと思います。

# nova-***, nova-compute, nova-conductor....

novaだけでもnova-compute, nova-scheduler, nova-conductor, nova-consoleauthと多くの実行ファイルがありますが、これらの実行ファイルは全く別のtechnology stackでできているかというと、そうではなくてnovaの中でMicro Service Architectureフレームワークっぽいものを定義していて、その仕組みを使ってnova-*** を実現しています。

ですので、どれか１つのデーモンのソースコードを中心に読むことで、他のソースも読みやすくなると思います。

# nova-*** 各モジュールを理解する上で基本的に知っておくべき概念

## CMD (nova/cmd/*)

ユーザが実行する実行ファイル、モジュールごとに作成される。
中身は非常にシンプルで単純にserviceを作成し、実行している。

## Service (nova/service.py)

全ての、常駐プログラムのフレームワークみたいなものであり、nova-compute,nova-scheduler,nova-conductorも同じServiceクラスを利用している。モジュールspecificなコードは、managerで定義されておりserviceはbinary名から適切なmanagerを読み込み、適宜managerのメソッド呼ぶことで、module specific なコードの実行を実現している。
また、RPCサーバを立ち上げることもServiceが実施する、この時managerがRPC Serverのエンドポイントになる。

## Manager (nova/\<module\>/manager.py)

ここでは、module specificなコードをここに書いている。serviceからinit_hostやpre_start_hookなど特定のmethodが適切なタイミングで呼び出されるので、それら実装しなくてはならない。
また、managerがRPC Serverのエンドポイントとしてbindされるので、他のモジュールに使って欲しい機能の実装などをmanager上に書き、他のmoduleからRPC Call経由で実行できるようにしてあげる必要がある。

## DB (nova/db/sqlalchemy/api.py)

database アクセスをします（それ以上にいうことがないです）

## Objects (nova/objects/*)

OpenStackでは、model定義にアクセサが定義されておらず、database上に保存したオブジェクトへのアクセスは、nova/objects 配下に定義しているClassから実施する。また、ここで面白いのは、nova/objects 経由でdb上のオブジェクトへのアクセスをする場合、nova-conductor経由でdatabaseアクセスと直接databaseにアクセスする場合とをmodule開発者(nova-compute開発者)は意識しなくても良いように作られている。

どういうことかというと、objects配下のDB上の情報をrepresentしたClassのアクセサに下記のremotableのデコレータを噛ませることによって、Classにindirection_api変数にconductor rpi api clientが設定されている場合は、databaseアクセスの代わりに、conductorへrpc callを送ります、設定されていない場合は、直接databaseへアクセスを実施します。


https://github.com/openstack/oslo.versionedobjects/blob/master/oslo_versionedobjects/base.py#L168-L230

nova.objects.base.NovaObject.indirection_apiにconductor api clientを設定するのは、だいたいcmd/\<module\>.py で実施されている印象です。


## API関連 (REST API, RPC API, API)

大きく分けて３種類あると思っていいと思います。

* Rest API Server (Rest API Clientは別Repositoryです）
* Software API Client
* RPC API Client (RPC Serverの実装はManager) 

あるモジュールからの抽象度が高い(1 API Callでの計算量の多さ)順に並べると

 Rest API >  Software API  > RPC API

そのため、新しいモジュールを作成する場合に既存の機能と連携したいと考えた際は、上記の抽象度が高い順に、やりたいことができるかどうか調べていくといいと思います。RPC APIから眺めていくと、だいたいやりたいことの実現に10-20 RPC API Callしないといけなく、実装してみたら似たようなことをしているSoftware APIがありました。なんてことになりかねません。


### Rest API Server (nova/api/openstack/*)

APIとつくコードが多いので、紛らわしいですが。外部コンポーネント(cinder, neutron...)に公開するRest APIのサーバサイドのコードはnova/api/openstack配下に格納されます。

### Software API Client (nova/\<module\>/api.py)

なんと表現していいかわからなかったので、Software API Clientと表現しました。これは、同じコンポーネントの他のmoduleへ公開する意味で作成されております。モジュール間は RPC で通信をしますが、RPC Serverが必ずしも他のモジュールからみて都合の良い粒度ではないことがあります。また、あるRPC ServerのRPC API 1は、nova-schedulerのRPC API 2, 3, 4を実行したあとに実行して欲しいと思っている場合は、それをドキュメントするよりきちんと意味のある単位で処理できるAPI Clientを提供した方がいいだろうという意味合いでこのようなAPI Clientが作成されているのだと思います。

### RPC API Client (nova/\<module\>/rpcapi.py)

こちらは、RPC API Clientです。基本的にRPC Server Side(manager)のメソッドと1:1で作成されており、基本的にはRPC Callを単純に呼ぶだけで、それ以上のことはしません。




# nova-***の起動からRPC Server Listenまでをざっくり

nova-computeを題材にみていきます。

キーポイントになると思うモジュール,クラス,メンバ,メソッドを図示化して、適当に矢印を引きました。

<img width="1220" alt="Screen Shot 2017-12-17 at 15.44.41.png" src="https://qiita-image-store.s3.amazonaws.com/0/2242/6873cd55-e37c-8c86-a26e-12be24ee6bd1.png">


Classはこの図、モジュールはこの図とか矢印は参照、メソッドコールはこの矢印みたいな、ルールに沿ってないのでちょっと分かりづらいかもしれませんがそこらへんはご容赦ください

結構面白いなと思うところは、常駐プログラムにするための共通処理は、nova/service.py にまとめられており、各モジュールごとの処理は、Managerという概念に外だししております。

## Serviceのインスタンス化(初期化)

nova-computeを実行すると、まず nova/service.py:Service クラスをインスタンス化します。
このServiceは、nova-computeのライフサイクルを管理しており、シグナル対応など適宜OSからのイベントに応じてプログラムを正常にshutdownするようにしたり、nova-computeが起動したらnova-conductorを通して、自身をserviceとしてdatabaseに登録したりします。これらの処理は、nova-compute, nova-consoleauth, nova-schedulerで変わらないのでServiceとして共通化されております。

Serviceクラスのインスタンス化の際に、もっとも重要になるnova-computeに対応するmanagerをロードして、Serviceオブジェクトのmanager変数に代入をします。このようにして、具体的に各モジュールごとに違う処理をServiceの外側で定義できるようにしてServiceの汎用性を高めています。

nova/service.py

```python
....
54 SERVICE_MANAGERS = {
55     'nova-compute': 'nova.compute.manager.ComputeManager',
...
221     @classmethod
222     def create(cls, host=None, binary=None, topic=None, manager=None,
....
239         if not binary:
240             binary = os.path.basename(sys.argv[0])
241         if not topic:
242             topic = binary.rpartition('nova-')[2]
243         if not manager:
244             manager = SERVICE_MANAGERS.get(binary)
....
254         service_obj = cls(host, binary, topic, manager,
255                           report_interval=report_interval,
256                           periodic_enable=periodic_enable,
257                           periodic_fuzzy_delay=periodic_fuzzy_delay,
258                           periodic_interval_max=periodic_interval_max)
259
260         return service_obj
```

実行ファイル名を240行目で取得し、244行目でClassへのフルパスを取得しております。
フルパス取得後は、importutilsで該当のClassを読み込んでいます。

nova/service.py

```python
....
117     def __init__(self, host, binary, topic, manager, report_interval=None,
118                  periodic_enable=None, periodic_fuzzy_delay=None,
119                  periodic_interval_max=None, *args, **kwargs):
120         super(Service, self).__init__()
124         self.manager_class_name = manager
126         manager_class = importutils.import_class(self.manager_class_name)
127         self.manager = manager_class(host=self.host, *args, **kwargs)
128         self.rpcserver = None
129         self.report_interval = report_interval
130         self.periodic_enable = periodic_enable
131         self.periodic_fuzzy_delay = periodic_fuzzy_delay
132         self.periodic_interval_max = periodic_interval_max
133         self.saved_args, self.saved_kwargs = args, kwargs
134         self.backdoor_port = None
135         if objects_base.NovaObject.indirection_api:
136             conductor_api = conductor.API()
137             conductor_api.wait_until_ready(context.get_admin_context())
....
```

また、137行目のConductor APIですが、実はあまり多くのAPIを提供しておらず基本的にはConductorへのリーチャビリティがあるかどうかをチェックするRPC Callをレスポンスが返ってくるまで送り続けるAPIしか提供されておりません。


Serviceの初期化では、基本的にmanagerクラスの取得とconductorを通したDBアクセスをする場合は、conductorとリーチャビリティがあるかどうかのチェックしか実施しません。そのServiceの初期化が終わると、startメソッドをcallします。startメソッドの中でRPCサーバをListenしているのでその後は他のサーバからのリクエストを待ちながら、定期実行処理を実施します。

## nova-computeの初期化/RPC Serverの立ち上げ

インスタンス化が終了すると、次にstartメソッドが実行されます。

nova/service.py


```python

149     def start(self):
....
161         self.manager.init_host()
162         self.model_disconnected = False
163         ctxt = context.get_admin_context()
164         self.service_ref = objects.Service.get_by_host_and_binary(
165             ctxt, self.host, self.binary)
166         if self.service_ref:
167             _update_service_ref(self.service_ref)
168
169         else:
170             try:
171                 self.service_ref = _create_service_ref(self, ctxt)
172             except (exception.ServiceTopicExists,
173                     exception.ServiceBinaryExists):
174                 # NOTE(danms): If we race to create a record with a sibling
175                 # worker, don't fail here.
176                 self.service_ref = objects.Service.get_by_host_and_binary(
177                     ctxt, self.host, self.binary)
178
179         self.manager.pre_start_hook()
```

ここでは、161行目でnova-computeの初期化を実施しています。しかし、nova-computeの初期化と言ってもmanager.init_host()を呼び出しているだけです、実行ファイルによってmanagerを切り替えており、ここでは前述の通りComputeManagerなので、nova-computeの初期化を実施していることになります。

また、初期化後は164-178行目でservice referenceの登録を実施しています。OpenStackをデプロイされたことがあるとわかると思いますが、nova services list(フルパス忘れました) とかすると、novaの実行中のmodule一覧が表示されたかと思います(nova-computeがどこで動いているかとか)がそれを実現するために、databaseに自分のstatusとhost名を書き込みに行っています。


179行目では、managerに対してpre_start_hookを呼び出しています。ここではRPC Serverを起動する前に実施しておきたい処理を記述しているので、それを呼び出しています。

その後は、下記のようにRCP Serverを起動しています。

nova/service.py

```python
149     def start(self):
..........
186         target = messaging.Target(topic=self.topic, server=self.host)
187
188         endpoints = [
189             self.manager,
190             baserpc.BaseRPCAPI(self.manager.service_name, self.backdoor_port)
191         ]
192         endpoints.extend(self.manager.additional_endpoints)
193
194         serializer = objects_base.NovaObjectSerializer()
195
196         self.rpcserver = rpc.get_server(target, endpoints, serializer)
197         self.rpcserver.start()
198
199         self.manager.post_start_hook()
```

endpointsを見ていただくと、managerをそのまま渡していることがわかります。
そうです、各ManagerはRPC Serverのエンドポイントとしても使われます。baserpc.BaseRPCAPIも登録されていますがこれは、ping関数のみ実装されており、serviceの正常性確認に使われます。

起動後は、managerにpost_start_hookメソッドをコールし、RPC Server起動後に実行したいnova-compute特有の処理を実行します。


```python
149     def start(self):
..........
203         # Add service to the ServiceGroup membership group.
204         self.servicegroup_api.join(self.host, self.topic, self)
205
206         if self.periodic_enable:
207             if self.periodic_fuzzy_delay:
208                 initial_delay = random.randint(0, self.periodic_fuzzy_delay)
209             else:
210                 initial_delay = None
211
212             self.tg.add_dynamic_timer(self.periodic_tasks,
213                                      initial_delay=initial_delay,
214                                      periodic_interval_max=
215                                         self.periodic_interval_max)

```

その後は、204行目でservicegroup_api.join を呼びます、ここでやっているのは定期実行タスクの追加で、定期的にserviceテーブルに登録された自身のserviceエントリのref_countカラムを定期的に、インクリメントするような処理を定期実行タスクとして追加しています。ここでref_countカラムの更新が滞ると、該当サービスが停止したと判断されるようになります。

212行目では、periodic_tasksをtgに追加して、定期実行処理を追加していますが、これの実態はmanagerに定義されています。


```
nova ukinau$ grep periodic_task.periodic_task -n nova/compute/manager.py -1
1363-
1364:    @periodic_task.periodic_task
1365-    def _check_instance_build_time(self, context):
--
--
1645-
1646:    @periodic_task.periodic_task(spacing=CONF.scheduler_instance_sync_interval)
1647-    def _sync_scheduler_instance_info(self, context):
--
--
6429-
6430:    @periodic_task.periodic_task(
6431-        spacing=CONF.heal_instance_info_cache_interval)

```

各managerは、nova.manager:Managerを継承していて、Managerは、oslo_service.periodic_task:PeriodicTaskを継承しているため、各managerでperiodic_taskデコレータを使って定義した関数がmanagerのperiodic_tasksリストに追加され、定期的に実行されます。run_periodic_tasksという関数をthreadgroupに追加していて、run_periodic_tasks内でperiodic_tasksリストをなめて実行しているので、面白いことに定期実行は、1つのスレッドでシリアルで実施されるので、ある定期実行処理に登録した１つの関数の実行に長時間かかると、他の定期実行は遅れて実行されしまいます。
つまり、実行間隔がシビアなタスクは、periodic_tasksの仕組みを使わず、直接tgに登録した方がいいかもしれません。

ここまでで、初期化処理は終了したので、ここから先は、periodic_tasksに追加された関数とself.servicegroup_api.joinで登録したstatus updateの２つの関数を定期的に実行しながら、RPC Serverで他のモジュールからのリクエストを待ちます。

なので、ここまできて下記の図に書いてあるものが全てロードされた状態になります

<img width="1220" alt="Screen Shot 2017-12-17 at 15.44.41.png" src="https://qiita-image-store.s3.amazonaws.com/0/2242/6873cd55-e37c-8c86-a26e-12be24ee6bd1.png">


ここまできたら、あとはユーザからのREST APIをトリガにnova-apiがnova-computeにRPC Callを送ったりして、nova-computeがそのRPC CALLを処理して。。。という形のイベント駆動でプログラムの処理が進むので調べたいREST APIがどんなRPC CALLを呼んでいるか(nova/api/openstack/*)で調べて、managerで該当のメソッドを探して何を実行してるのか調べることができるようになると思います。

気が向いたら、王道なRest APIの裏側でどんなことをするのか解説する記事を書きます。