めもめも

このブログに記載の内容は個人の見解であり、必ずしも所属組織の立場、戦略、意見を代表するものではありません。

dodai-computeにおける物理マシンとネットワーク(VLAN)のライフサイクル管理

ライフサイクル管理の全体像

仮想マシンと違い、各物理マシンは常に「インスタンス」として起動している状態として扱います。各物理マシンに「availability_zone」の属性を持たせて、この値で使用状況を管理します。また「instance_type」の属性を持たせることで、物理マシンのグループ分けを行います。(起動時にユーザが指定したinstance_typeに対して、同じinstance_typeを持つ物理マシンの中から、使用するマシンを選択する。)

また、各マシンの状態は、status属性で分類。

status 状態
inactive 電源オフ
active 電源オンで未使用
processing 処理中
used 使用中

例えば、Terminateしたインスタンスは、「availability_zone="resource_pool"」として、デフォルトイメージで起動した状態になります。

nova/compute/manager.py

 517     @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
 518     @checks_instance_lock
 519     def terminate_instance(self, context, instance_id):
 520         """Terminate an instance on this host."""
 521         def _inner_terminate_instance():
 522             try:
 523                 self._instance_update(context,
 524                                       instance_id,
 525                                       vm_state=vm_states.DELETED,
 526                                       task_state=None,
 527                                       terminated_at=utils.utcnow())
 528 
 529                 bmm = self._shutdown_instance(context, instance_id, 'Terminating')
 530                 instance = self.db.instance_get(context.elevated(), instance_id)
 531                 instance_new = db.instance_create(context,
 532                     {"availability_zone": "resource_pool",
 533                      "user_id": instance["user_id"],
 534                      "project_id": instance["project_id"],
 535                      "kernel_id": instance["kernel_id"],
 536                      "host": instance["host"],
 537                      "display_name": instance["display_name"],
 538                      "instance_type_id": instance["instance_type_id"],
 539                      "vcpus": instance["vcpus"],
 540                      "vm_state": vm_states.BUILDING,
 541                      "image_ref": FLAGS.dodai_default_image})
 542 
 543                 self.db.instance_destroy(context, instance_id)
...
 550                 self.driver.add_to_resource_pool(context, instance_new, bmm)
 551             except Exception as ex:
 552                 LOG.exception(ex)
 553 
 554         greenthread.spawn(_inner_terminate_instance)

nova/virt/dodai/connection.py

554     def add_to_resource_pool(self, context, instance, bmm):
555         # begin to install default os
556         self._install_machine(context, instance, bmm, "resource_pool", None, True)

物理マシンの使用を開始する際は、「availability_zone="[C](新規作成指定),,"」の形式で、クラスタ名とVLAN IDをavailability_zoneに指定して起動します。

nova/virt/dodai/connection.py

225     def _parse_zone(self, zone):
226         create_cluster = False
227         vlan_id = None
228         cluster_name = "resource_pool"
229         instance_zone = zone
230         parts = zone.split(",")
231         if len(parts) >= 2:
232             if parts[0] == "C":
233                 parts.pop(0)
234                 create_cluster = True
235 
236             cluster_name, vlan_id = parts
237             vlan_id = int(vlan_id)
238             instance_zone = ",".join(parts)
239 
240         return instance_zone, cluster_name, vlan_id, create_cluster

ただし、connection.pyのコード内では実質的に意味を持つのは、cluster_nameだけで、対応するvlan_idなどは、この後で見るように、Nova EC2 APIでrun_instanceを実行した時点でスイッチ側に紐づけなどの登録作業が行われます。

実際にspawnする部分を追うと、次のようになります。

nova/virt/dodai/connection.py

161     def spawn(self, context, instance, # <- instanceは起動属性を含むハッシュ
162               network_info=None, block_device_info=None):
...
183         instance_zone, cluster_name, vlan_id, create_cluster = self._parse_zone(instance["availability_zone"])
            # ^-- ここでクラスタ情報を取得
186         bmm, reuse = self._select_machine(context, instance)
            # ^-- ここで使用する物理マシンを選択
...
198         if instance_zone == "resource_pool":  # "rosource_pool"に戻す場合
199             self._install_machine(context, instance, bmm, cluster_name, vlan_id)
200         else:
201             self._update_ofc(bmm, cluster_name)
202             if bmm["instance_id"]:  # 使用中マシンを選択した場合は、一旦、アンインストール
203                 db.instance_destroy(context, bmm["instance_id"])
204 
205             if reuse: # インストール済みのOSを再利用する場合
206                 db.bmm_update(context, bmm["id"], {"status": "used",
207                                                    "instance_id": instance["id"]})
208             else:  # それ以外は、新規インストール
209                 self._install_machine(context, instance, bmm, cluster_name, vlan_id)
210 
211             if instance["key_data"]:
212                 self._inject_key(bmm["pxe_ip"], str(instance["key_data"]))

使用するマシンの選択ロジックは次の通り。

nova/virt/dodai/connection.py

373     def _select_machine(self, context, instance):
374         inst_type = instance_types.get_instance_type(instance['instance_type_id'])
...
382         try:
                # v--- 指定のインスタンスタイプの物理マシン一覧から選択
383             bmms = db.bmm_get_all_by_instance_type(context, inst_type["name"], session)
                # v--- 新規導入マシンを初期化して「resource_pool」に入れる場合の処理
384             if instance["availability_zone"] == "resource_pool": #Add a machine to resource pool.
385                 for bmm in bmms:
386                     if bmm["availability_zone"] != "resource_pool":
387                         continue
388 
389                     if bmm["status"] != "inactive":
390                         continue
391                         
392                     bmm_found = bmm
393                     break
             # ^-- まだ「resource_pool」になくて、電源の入っていない「inactive」なマシンを対象とする
394             else:   # <-- ここからがクラスタを指定して利用を開始する場合の選択
395                 for bmm in bmms:
396                     if bmm["availability_zone"] != "resource_pool":
397                         continue
398                         
399                     if bmm["status"] != "active":
400                         continue 
401                         
402                     instance_ref = db.instance_get(context, bmm["instance_id"])
403                     if instance_ref["image_ref"] != instance["image_ref"]:
404                         continue
405                         
406                     bmm_found = bmm
407                     reuse = True
408                     break 
                        # ^--- まずは「resource_pool」で指定のOSをインストール済み(i.e. 指定のOSがデフォルトOS)
                        #      のマシンを探して再利用を試みる
409                     
410                 if not bmm_found:
411                     for bmm in bmms:
412                         if bmm["status"] == "used" or bmm["status"] == "processing":
413                             continue
414                             
415                         bmm_found = bmm
416                         reuse = False
417                         break 
                         # ^--- 再利用マシンがない場合は、「used」「processing」以外(つまり空いているマシン)を利用
                         #      クラスタに入っているけど、空いているマシンが選ばれる可能性もあるが、現実の起こりえるか不明
                         #      まだ「resouce_pool」に入っていない電源起動前マシンを選ぶことを想定しているのかも?

ネットワーク関連

ネットワークの物理構成の概要はこんな感じ。(この他に、ストレージ接続用ネットワークもあるようです。)

管理ネットワークのポート/IPは固定で、事前に物理マシン属性として登録します。これにより、dodai-computeから管理操作を行います。

サービスネットワークのポート(2個)は、openFlow対応スイッチに接続して、接続ポート番号を物理マシン属性として事前登録します。物理マシンを特定のクラスタにアサインしたタイミングで、openFlowを使ってスイッチに指示を出して、接続ポートを該当クラスタのVLANに接続します。(特にopenFlowでなくても、PVID設定を動的に変更すれば対応できる気もするけど。。。。)

物理マシンのサービスIPの設定は、独自の手法を採用しています。物理マシンのゲストOSで専用のAgentを起動して、これが管理ネットワーク側にhttpの口を開けて、dodai-computeからの指示を待ちます。dodai-computeは、これに対して、特定のサービスIPの割り当てや認証キーの埋め込みなどの指示を出します。(AWSのように、ゲストOS側から「ユーザデータ」を取りに行く方式を採用しなかったのは、何か理由があるんでしょうか。。。。)

このAgentのコードは、GitHubには上がってないようですが、こっそり入手してみた所、Bondingの構成などもAgentで行なっています。便利ですが、ゲストOS依存が高くなる点が少し懸念点。

openFlow周りのコードを見ておきます。専用のユーティリィライブラリが用意されています。

クラスタ名は、「region_name」にマッピングされており、新しいregionを作ると、対応するVLANが外部ネットワーク側のトランクポートに追加されます。

nova/virt/dodai/ofc_utils.py

 35 def create_region(service_url, region_name, vlan_id):
 36     client = Client(service_url + "?wsdl")
 37     try:
 38         client.service.createRegion(region_name)
 39         client.service.save()
 40     except:
 41         raise exception.OFCRegionCreationFailed(region_name=region_name)
 42 
 43     try:
 44         switches = db.switch_get_all(None)
 45         for switch in switches:
 46             client.service.setOuterPortAssociationSetting(switch["dpid"], switch["outer_port"], vlan_id, 65535, region_name)
 47 
 48         client.service.save()
 49     except:
 50         client.service.destroyRegion(region_name)
 51         client.service.save()
 52         raise exception.OFCRegionSettingOuterPortAssocFailed(region_name=region_name, vlan_id=vlan_id)

物理マシンを特定クラスタに追加したタイミングで、該当マシンのポートを対応する「region」にアサインします。(ライブラリを使っているので詳細は不明ですが、おそらく、このポートから指定regionのVLANにパケットが通るようになるものと思われます。)

nova/virt/dodai/ofc_utils.py

  9 def update_for_run_instance(service_url, region_name, server_port1, server_port2, dpid1, dpid2):
 10     # check region name
 11     client = Client(service_url + "?wsdl")
 12 
 13     client.service.setServerPort(dpid1, server_port1, region_name)
 14     client.service.setServerPort(dpid2, server_port2, region_name)
 15     client.service.save()

一方、Terminateした物理マシンのポートをregionから削除する処理はここ。

nova/virt/dodai/ofc_utils.py

 17 def update_for_terminate_instance(service_url, region_name, server_port1, server_port2, dpid1, dpid2, vlan_id):
 18     client = Client(service_url + "?wsdl")
 19     client.service.clearServerPort(dpid1, server_port1)
 20     client.service.clearServerPort(dpid2, server_port2)
 21     client.service.save()
 22 
 23     dpid_datas = client.service.showSwitchDatapathId()
 24     for dpid_data in dpid_datas:
 25         ports = client.service.showPorts(dpid_data.dpid)
 26         for port in ports:
 27             if port.type != "ServerPort":
 28                 continue
 29 
 30             if port.regionName == region_name:
 31                 return
 32     # 該当regionの物理マシンがいなくなったら、regionそのものを削除する。
 33     remove_region(service_url, region_name, vlan_id)

実際に、物理マシンの利用を開始するタイミングで、ポートを設定する流れは次の部分になります。

nova/virt/dodai/connection.py

161     def spawn(self, context, instance,
162               network_info=None, block_device_info=None):
...
201             self._update_ofc(bmm, cluster_name) # スイッチポートを設定してから
...                     # v--- 物理マシンのインスタンスを開始
209                 self._install_machine(context, instance, bmm, cluster_name, vlan_id)
...
339     def _update_ofc(self, bmm, cluster_name): # スイッチポートを設定するところ
340         try:
341             ofc_utils.update_for_run_instance(FLAGS.ofc_service_url,
342                                               cluster_name, # cluster_nameをregion_nameに指定
343                                               bmm["server_port1"],
344                                               bmm["server_port2"],
345                                               bmm["dpid1"],
346                                               bmm["dpid2"])
347         except Exception as ex:
348             LOG.exception(_("OFC exception %s"), unicode(ex))

なお、regionの作成は、dodai-compute外部で別途、行う必要があります。現在の実装では、NovaのEC2 APIの方を拡張して、run_instanceを実行するタイミングで、作成を行います。

nova/api/ec2/cloud.py

1378     def run_instances(self, context, **kwargs):
1379         max_count = int(kwargs.get('max_count', 1))
...
1399         zone = kwargs.get('placement').get('availability_zone')
1400         def _validate_zone(zone):  # ユーザが指定したavailability_zoneを解析
1401             if zone == "resource_pool":
1402                 return False, zone, None
...
1411             create_cluster = False
1412             if len(parts) == 3:
1413                 create_cluster = True
1414                 parts.pop(0)
1415 
1416             cluster_name, vlan_id = parts
1417             has_cluster = ofc_utils.has_region(FLAGS.ofc_service_url, cluster_name)
1418             if create_cluster and has_cluster: # 既存クラスタを作成指定するとエラー
1419                 raise exception.OFCRegionExisted(region_name=cluster_name)
1420 
1421             if not create_cluster and not has_cluster: # 再生指定なしで、存在しないクラスタを指定するとエラー
1422                 raise exception.OFCRegionNotFound(region_name=cluster_name)
1423             # 「新規クラスタ作成フラグ、クラスタ名、VLAN ID」を返す
                 # ただし、既存クラスタに異なるVLAN IDを指定した場合は、後続処理では、VLAN IDは無視されるっぽい・・・(本当?)
1424             return create_cluster, cluster_name, vlan_id
...
1441         create_cluster, cluster_name, vlan_id = _validate_zone(zone)
1442         if create_cluster and cluster_name != "resource_pool" : # ここで新規cluster/regionを作成
1443             ofc_utils.create_region(FLAGS.ofc_service_url, cluster_name, vlan_id)

この時点で、cluster_name(region_name)とvlan_idは一意に紐づくので、connection.pyの内部では、vlan_idを扱う必要ないはずですが、今の実装では、vlan_idが(実際には利用されない)パラメータとして引き回されている点がちょっとよくないですね。cluster_name(region_name)とvlan_idのマッピングを明示的にデータモデル化した方がよいかも知れません。

あと、独自Agentに指示を出しているのは、次の2箇所です。1つは、OSインストール後にKey Injectionする部分。

nova/virt/dodai/connection.py

214     def _inject_key(self, pxe_ip, key_data):
215         conn = httplib.HTTPConnection(pxe_ip, "4567")
216         params = urllib.urlencode({"key_data": key_data.strip()})
217         headers = {'Content-type': 'application/x-www-form-urlencoded', 'Accept': 'text/plain'}
218         conn.request("PUT", "/services/dodai-instance/key.json", params, headers)
219         response = conn.getresponse()
220         data = response.read()

もうひとつは、Nova APIでFloaint IPを割り当てる部分。つまり、サービスIP(Private IP)はデフォルトではアサインされず、かならずFloating IPとして割り当てる前提となっているようです。

nova/compute/api.py

1408     def associate_floating_ip(self, context, instance_id, address):
1409         """Makes calls to network_api to associate_floating_ip.
1410 
1411         :param address: is a string floating ip address
1412         """
...
1438         def _associate_ip():
1439             instance = self.get(context, instance_id)
1440             bmm = db.bmm_get_by_instance_id(context, instance_id)
1441 
1442             conn = httplib.HTTPConnection(bmm["pxe_ip"], "4567")
1443             params = {'ip_address': ip,
1444                       'subnet_mask': netmask,
1445                       'default_gateway': gw,
1446                       'dns': dns}
1447             index = 0
1448             if bmm["service_mac1"]:
1449                 params["mac_address[%d]" % index] = bmm["service_mac1"]
1450                 index += 1
1451             if bmm["service_mac2"]:
1452                 params["mac_address[%d]" % index] = bmm["service_mac2"]
1453 
1454             params = urllib.urlencode(params)
1455             headers = {'Content-type': 'application/x-www-form-urlencoded', 'Accept': 'text/plain'}
1456             conn.request("PUT", "/services/dodai-instance/networks.json", params, headers)
1457             response = conn.getresponse()
1458             data = response.read()