めもめも

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

NTTdocomo-openstackの物理マシン管理

全体像

ここで見たように、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