全体像
ここで見たように、BareMetalDriverクラスによって、ハイパーバイザの一形式としてNovaに結合しています。
それと同時に、Novaを拡張して、1個のNova Computeで複数のホストを管理できるようにしています。これにより、BareMetalDriverが稼働するNova Computeから、管理対象の物理マシンをすべて、独立したホストしとして、Nova Schedulerに登録します。つまり、Schedulerからは、個々の物理マシンは、「インスタンスを1個だけ起動できる(1個起動したら残りリソースが0になる)ホスト」として、扱われます。
仮想化ホストとの区別は、Instance_type_extra_specs Extensionを使用します。
たとえば、nova.confに「instance_type_extra_specs=cpu_arch:hogehoge」と指定すると、このホストは、「cpu_arch:hogehoge」というCapabilityを持つ事をSchedulerに通知します。一方、Instance Typeの定義に対して、
# nova-manage instance_type set_key --name=hoge.small --key cpu_arch --value 'hogehoge'
という指定をします。このinstance_typeを起動する時、Schedulerは該当のCapabilityを持つホストを使用します。
NTTdocomo-openstackでは、この機能において、特に「cpu_arch:x86_64」を物理マシンのCapability指定として固定的に利用しています。
まとめると、この手法では、インスタンスとして起動する物理マシンの選択を既存のNova Schedulerに任せられるという利点があります。一方、既存のSchedulerは、「仮想マシンを起動するためのホスト」の選択を念頭においたロジックなので、「特定の物理マシン群をまとめてクラスタ化したい」など、物理マシン管理ならではの使い方に対応しずらい、というデメリットもありそうです。おそらく、今後は、物理マシン管理も念頭において、Schedulerの拡張がなされることでしょう。
ソースを確認
管理対象の物理マシンは、専用のDBに予め登録しておきます。主なテーブルはこちら。
nova/virt/baremetal/db/sqlalchemy/models.py
35 class BareMetalNode(BASE, models.NovaBase): # 個々の物理を登録 36 """Represents a bare metal node.""" 37 38 __tablename__ = 'bm_nodes' 39 id = Column(Integer, primary_key=True) 40 service_host = Column(String(255)) 41 instance_uuid = Column(String(36), nullable=True) # <--- 使用中はinstance_uuidが入る 42 cpus = Column(Integer) 43 memory_mb = Column(Integer) 44 local_gb = Column(Integer) 45 pm_address = Column(Text) 46 pm_user = Column(Text) 47 pm_password = Column(Text) 48 prov_mac_address = Column(Text) 49 registration_status = Column(String(16)) 50 task_state = Column(String(255)) # <---- 物理マシンの電源状態 51 prov_vlan_id = Column(Integer) 52 terminal_port = Column(Integer) 53 54 55 class BareMetalPxeIp(BASE, models.NovaBase): # 物理マシンの管理IP(PXE Boot等に使用) 56 __tablename__ = 'bm_pxe_ips' 57 id = Column(Integer, primary_key=True) 58 address = Column(String(255), unique=True) 59 server_address = Column(String(255), unique=True) 60 bm_node_id = Column(Integer, ForeignKey('bm_nodes.id'), nullable=True) 61 62 63 class BareMetalInterface(BASE, models.NovaBase): # 物理マシンのサービスNIC 64 __tablename__ = 'bm_interfaces' 65 id = Column(Integer, primary_key=True) 66 bm_node_id = Column(Integer, ForeignKey('bm_nodes.id'), nullable=True) 67 address = Column(String(255), unique=True) 68 datapath_id = Column(String(255)) # OpenFlowで制御する際のdatapath ID 69 port_no = Column(Integer) # 接続先にスイッチポート番号 70 vif_uuid = Column(String(36), unique=True) 71 72 73 class BareMetalDeployment(BASE, models.NovaBase): 74 __tablename__ = 'bm_deployments' 75 id = Column(Integer, primary_key=True) 76 key = Column(String(255)) 77 image_path = Column(String(255)) 78 pxe_config_path = Column(String(255)) 79 root_mb = Column(Integer) 80 swap_mb = Column(Integer)
1つのNova Computeで複数ホストを管理できるようにしてある部分は、この辺り。
nova/compute/manager.py
3059 @manager.periodic_task 3060 def _report_driver_status(self, context): 3061 curr_time = time.time() 3062 if curr_time - self._last_host_check > CONF.host_state_interval: 3063 self._last_host_check = curr_time 3064 LOG.info(_("Updating host status")) 3065 # This will grab info about the host and queue it 3066 # to be sent to the Schedulers. 3067 capabilities = self.driver.get_host_stats(refresh=True) # ^-- 複数ホストを管理するドライバの場合、個々のホストのstatsのリストが返る 3068 for capability in (capabilities if isinstance(capabilities, list) 3069 else [capabilities]): # 単一ホストの場合は無理やりリストに変換している。。。 3070 capability['host_ip'] = CONF.my_ip 3071 self.update_service_capabilities(capabilities) ... 3229 @manager.periodic_task 3230 def update_available_resource(self, context): 3231 """See driver.get_available_resource() 3232 3233 Periodic process that keeps that the compute host's understanding of 3234 resource availability and usage in sync with the underlying hypervisor. 3235 3236 :param context: security context 3237 """ 3238 new_resource_tracker_dict = {} 3239 nodenames = self.driver.get_available_nodes() # このNava Comupteが管理するホスト一覧 3240 for nodename in nodenames: # 個々のホストをresource_trackerに登録 3241 rt = self._get_resource_tracker(nodename) 3242 rt.update_available_resource(context) 3243 new_resource_tracker_dict[nodename] = rt 3244 self._resource_tracker_dict = new_resource_tracker_dict
nova/manager
243 def update_service_capabilities(self, capabilities): 244 """Remember these capabilities to send on next periodic update.""" 245 if not isinstance(capabilities, list): # 複数ホストのcapablitiesがリストで来る 246 capabilities = [capabilities] 247 self.last_capabilities = capabilities
BareMetalDriverがget_host_statsで個々の物理マシンのstatsを返す部分はここ
nova/virt/baremetal/driver.py
370 def get_host_stats(self, refresh=False): 371 caps = [] 372 context = nova_context.get_admin_context() 373 nodes = bmdb.bm_node_get_all(context, # 管理対象の物理マシンをDBから取得 374 service_host=CONF.host) 375 for node in nodes: 376 res = self._node_resource(node) 377 nodename = str(node['id']) 378 data = {} 379 data['vcpus'] = res['vcpus'] 380 data['vcpus_used'] = res['vcpus_used'] 381 data['cpu_info'] = res['cpu_info'] 382 data['disk_total'] = res['local_gb'] 383 data['disk_used'] = res['local_gb_used'] 384 data['disk_available'] = res['local_gb'] - res['local_gb_used'] 385 data['host_memory_total'] = res['memory_mb'] 386 data['host_memory_free'] = res['memory_mb'] - res['memory_mb_used'] 387 data['hypervisor_type'] = res['hypervisor_type'] 388 data['hypervisor_version'] = res['hypervisor_version'] 389 data['hypervisor_hostname'] = nodename 390 data['supported_instances'] = self._supported_instances 391 data.update(self._extra_specs) # ここで、instance_type_extra_specsの情報を通知している 392 data['host'] = CONF.host 393 data['node'] = nodename 394 # TODO(NTTdocomo): put node's extra specs here 395 caps.append(data) # それぞれの物理マシンのリソース情報をリストに詰め込む 396 return caps
instance_type_extra_specsによるフィルタ
BaremetalDriver起動時に、nova.confのinstance_type_extra_specsがパースされます。
nova/virt/baremetal/driver.py
114 def __init__(self, virtapi, read_only=False): 115 super(BareMetalDriver, self).__init__(virtapi) ... 127 extra_specs = {} 128 extra_specs["baremetal_driver"] = CONF.baremetal_driver 129 for pair in CONF.instance_type_extra_specs: 130 keyval = pair.split(':', 1) 131 keyval[0] = keyval[0].strip() 132 keyval[1] = keyval[1].strip() 133 extra_specs[keyval[0]] = keyval[1] 134 if not 'cpu_arch' in extra_specs: 135 LOG.warning( 136 _('cpu_arch is not found in instance_type_extra_specs')) 137 extra_specs['cpu_arch'] = '' 138 self._extra_specs = extra_specs # extra_specsのリストを保存 139 140 self._supported_instances = [ 141 (extra_specs['cpu_arch'], 'baremetal', 'baremetal'), 142 ] # それとは別にsupported_instancesの指定にも流用している
上記のコードにあるように、extra_specsの「cpu_arch」指定は、supported_instancesの指定にも流用されています。これは、一般に、仮想化ハイパーバイザについて(arch, hypervisor_type, vm_mode)のタプルを提供するもので、イメージの指定するアーキテクチャとのマッチングに利用されます。
これらの情報は上記の「get_host_stats関数」でスケジューラに通知されます。
スケジューラがextra_specsでホストをフィルタリングする部分は、こちら。
nova/scheduler/filters/compute_capabilities_filter.py
24 class ComputeCapabilitiesFilter(filters.BaseHostFilter): 25 """HostFilter hard-coded to work with InstanceType records.""" 26 27 def _satisfies_extra_specs(self, capabilities, instance_type): 28 """Check that the capabilities provided by the compute service 29 satisfy the extra specs associated with the instance type""" 30 if 'extra_specs' not in instance_type: 31 return True 32 33 for key, req in instance_type['extra_specs'].iteritems(): 34 # Either not scope format, or in capabilities scope 35 scope = key.split(':') 36 if len(scope) > 1 and scope[0] != "capabilities": 37 continue 38 elif scope[0] == "capabilities": 39 del scope[0] 40 cap = capabilities 41 for index in range(0, len(scope)): 42 try: 43 cap = cap.get(scope[index], None) 44 except AttributeError: 45 return False 46 if cap is None: 47 return False 48 if not extra_specs_ops.match(cap, req): 49 return False 50 return True
公式ドキュメントを見る限りは、Instance Typeに「cpu_arch=x86_64」を指定することで、仮想化ホストではなく、物理マシンを選択する使い方が想定されているようですが、instance_type_extra_specsは複数属性が指定できるので、追加属性(「location:rack11」とか)を利用すると、物理マシンのグルーピングなどもできる気がします。
ネットワーク管理
ネットワークに関しては、QuantumのNEC OpenFlow Pluginが前提で、基本的には、Quantumのネットワークモデルに従うように見えます。Quantumで「ネットワーク」(プライベートなL2セグメント)を定義しておき、インスタンス起動時に接続する「ネットワーク」を指定すると、物理マシンのNICが接続されたOpenFlow Switchポートに対して、指定のネットワークと通信可能にするエントリがフローテーブルに追加されます。
物理マシンのNICは、前述のDBに登録されたNICを端から順番に使って行きます。特定のNICを指定する機能はありません。(Nova/Quantumは、もともとが仮想マシンのNICを接続する前提のAPIですからね・・・)
該当のソースはこのあたりです。
nova/virt/baremetal/driver.py
398 def plug_vifs(self, instance, network_info): 399 """Plugin VIFs into networks.""" 400 self._plug_vifs(instance, network_info) 401 402 def _plug_vifs(self, instance, network_info, context=None): 403 if not context: 404 context = nova_context.get_admin_context() 405 node = _get_baremetal_node_by_instance_uuid(instance['uuid']) 406 if node: 407 pifs = bmdb.bm_interface_get_all_by_bm_node_id(context, node['id']) 408 for pif in pifs: 409 if pif['vif_uuid']: 410 bmdb.bm_interface_set_vif_uuid(context, pif['id'], None) 411 for (network, mapping) in network_info: # 接続先networkと接続対象vifのリストごとに・・ 412 self._vif_driver.plug(instance, (network, mapping)) # _vif_driver.plugを呼び出す。
nova/virt/baremetal/vif_driver.py
36 def plug(self, instance, vif): 37 LOG.debug(_("plug: instance_uuid=%(uuid)s vif=%(vif)s"), 38 {'uuid': instance['uuid'], 'vif': vif}) 39 network, mapping = vif 40 vif_uuid = mapping['vif_uuid'] 41 ctx = context.get_admin_context() 42 node = bmdb.bm_node_get_by_instance_uuid(ctx, instance['uuid']) 43 44 # TODO(deva): optimize this database query 45 # this is just searching for a free physical interface 46 pifs = bmdb.bm_interface_get_all_by_bm_node_id(ctx, node['id']) # 物理マシンの全NICのリストを取得 47 for pif in pifs: 48 if not pif['vif_uuid']: # 未使用のNICを見つけて使う 49 bmdb.bm_interface_set_vif_uuid(ctx, pif['id'], vif_uuid) 50 LOG.debug(_("pif:%(id)s is plugged (vif_uuid=%(vif_uuid)s)"), 51 {'id': pif['id'], 'vif_uuid': vif_uuid}) 52 self._after_plug(instance, network, mapping, pif) # この先でOpenFlowの設定を行なっている 53 return
nova/virt/baremetal/pfs/vif_driver.py
32 class OFSVIFDriver(vif_driver.BareMetalVIFDriver): 33 34 def _after_plug(self, instance, network, mapping, pif): 35 client = VIFINFOClient(FLAGS.quantum_connection_host, 36 FLAGS.quantum_connection_port) 37 vi = client.show_vifinfo(mapping['vif_uuid']) 38 if not vi: 39 client.create_vifinfo(mapping['vif_uuid'], # 指定ポートを「ネットワーク」にマッピングするエントリをフローてブルに追加 40 pif['datapath_id'], 41 pif['port_no']) 42 else: 43 LOG.debug('vifinfo: %s', vi) 44 LOG.debug('pif: %s', pif.__dict__) 45 vi = vi.get('vifinfo', {}) 46 ofsport = vi.get('ofs_port', {}) 47 dpid = ofsport.get('datapath_id') 48 port_no = ofsport.get('port_no') 49 if dpid != pif['datapath_id'] or int(port_no) != pif['port_no']: 50 raise exception.NovaException("vif_uuid %s exists" 51 % mapping['vif_uuid'])
その他
冒頭の「インスタンスを1つだけ起動できるホスト」のフリをする仕掛けはこのあたりです。
nova/scheduler/baremetal_host_manager.py
24 class BaremetalNodeState(host_manager.HostState): 25 """Mutable and immutable information tracked for a host. 26 This is an attempt to remove the ad-hoc data structures 27 previously used and lock down access. 28 """ 29 30 def update_from_compute_node(self, compute): 31 """Update information about a host from its compute_node info.""" 32 all_ram_mb = compute['memory_mb'] 33 34 free_disk_mb = compute['free_disk_gb'] * 1024 35 free_ram_mb = compute['free_ram_mb'] 36 37 self.free_ram_mb = free_ram_mb 38 self.total_usable_ram_mb = all_ram_mb 39 self.free_disk_mb = free_disk_mb 40 self.vcpus_total = compute['vcpus'] 41 self.vcpus_used = compute['vcpus_used'] 42 43 def consume_from_instance(self, instance): 44 self.free_ram_mb = 0 # インスタンスが起動してたら残りリソースは必ず0 45 self.free_disk_mb = 0 46 self.vcpus_used = self.vcpus_total