めもめも

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

dodai-computeのソースを読みながらBareMetalインストールの方法を考える

前置き

実は、Novaのコード読んだこと無いんですが、いきなりdodai-computeのソース読みます。まぁ、逆をたどって、Novaの構造もわかってくるでしょう。きっと。ソースを読み始めるのって、こういう「取っ掛かり」が大切ですね。

ちなみに、Diabloベースのようです。

(参考)dodai projectの紹介

儀式

# mkdir dodai-compute
# cd dodai-compute
# git init
# git pull https://github.com/nii-cloud/dodai-compute.git

物理サーバをVMとみなして管理するハイパーバイザっぽいもの

ざーーーーーと、ソースを一巡りして、スタート地点になりそうな所を見つけました。

nova/virt/connection.py

 40 def get_connection(read_only=False):
 41     """
 42     Returns an object representing the connection to a virtualization
 43     platform.
 44 
 45     This could be :mod:`nova.virt.fake.FakeConnection` in test mode,
 46     a connection to KVM, QEMU, or UML via :mod:`libvirt_conn`, or a connection
 47     to XenServer or Xen Cloud Platform via :mod:`xenapi`.
 48 
 49     Any object returned here must conform to the interface documented by
 50     :mod:`FakeConnection`.
 51 
 52     **Related flags**
 53 
 54     :connection_type:  A string literal that falls through a if/elif structure
 55                        to determine what virtualization mechanism to use.
 56                        Values may be
 57 
 58                             * fake
 59                             * libvirt
 60                             * xenapi
 61     """
 62     # TODO(termie): maybe lazy load after initial check for permissions
 63     # TODO(termie): check whether we can be disconnected
 64     t = FLAGS.connection_type
 65     if t == 'fake':
 66         conn = fake.get_connection(read_only)
 67     elif t == 'libvirt':
 68         conn = libvirt_conn.get_connection(read_only)
 69     elif t == 'xenapi':
 70         conn = xenapi_conn.get_connection(read_only)
 71     elif t == 'hyperv':
 72         conn = hyperv.get_connection(read_only)
 73     elif t == 'vmwareapi':
 74         conn = vmwareapi_conn.get_connection(read_only)
 75     elif t == 'dodai':
 76         conn = dodai_conn.get_connection(read_only) # <-- ハイパーバイザのフリをして紛れ込むdodai
 77     else:
 78         raise Exception('Unknown connection type "%s"' % t)

libvirt(kvm)/xen/hyper-v/vmwareなど、ハイパーバイザの種類ごとに、ハイパーバイザに接続していろいろ操作するオブジェクトを取得しているようです。ここに、dadaiがハイパーバイザのフリをして紛れ込んでいます。Novaから見ると、あくまで新種のハイパーバイザの1つとして、抽象化しているようです。

このオブジェクトを提供するクラスがここに。(たぶん、シングルトン)

nova/virt/dodai/connection.py

 47 def get_connection(_):
 48     # The read_only parameter is ignored.
 49     return DodaiConnection.instance()
 50 
 51 
 52 class DodaiConnection(driver.ComputeDriver):
 53     """Dodai hypervisor driver"""

たとえば、インスタンス情報を取得するメソッドを見てみると・・・

nova/virt/dodai/connection.py

130     def list_instances_detail(self, context):
131         """Return a list of InstanceInfo for all registered VMs"""
132         LOG.debug("list_instances_detail")
133 
134         info_list = []
135         bmms = db.bmm_get_all_by_instance_id_not_null(context) # <- dodai用にDBを拡張しているっぽい。
136         for bmm in bmms:
137             instance = db.instance_get(context, bmm["instance_id"])
138             status = PowerManager(bmm["ipmi_ip"]).status() # <- ipmiから電源情報を取得しているっぽい。
139             if status == "off":
140                 inst_power_state = power_state.SHUTOFF
141 
142                 if instance["vm_state"] == vm_states.ACTIVE:
143                     db.instance_update(context, instance["id"], {"vm_state": vm_states.STOPPED})
144             else:
145                 inst_power_state = power_state.RUNNING
146 
147                 if instance["vm_state"] == vm_states.STOPPED:
148                     db.instance_update(context, instance["id"], {"vm_state": vm_states.ACTIVE})
149 
150             info_list.append(driver.InstanceInfo(self._instance_id_to_name(bmm["instance_id"]),
151                                                  inst_power_state))
152 
153         return info_list

これを見ると、dodai用にベアメタルインスタンス情報を格納するDB領域(テーブル?)を追加して、インスタンスの起動ステータスは、IPMIで電源の状態を直接みているっぽいことが分かります。

やっぱり興味あるのは、インスタンスを起動する所ですよね。

nova/virt/dodai/connection.py

161     def spawn(self, context, instance,
162               network_info=None, block_device_info=None):
163         """
164         Create a new instance/VM/domain on the virtualization platform.
165 
166         Once this successfully completes, the instance should be
167         running (power_state.RUNNING).
168 
169         If this fails, any partial instance should be completely
170         cleaned up, and the virtualization platform should be in the state
171         that it was before this call began.
172 
173         :param context: security context
174         :param instance: Instance object as returned by DB layer.
175                          This function should use the data there to guide
176                          the creation of the new instance.
177         :param network_info:
178            :py:meth:`~nova.network.manager.NetworkManager.get_instance_nw_info`
179         :param block_device_info:
180         """
181         LOG.debug("spawn")
182 
183         instance_zone, cluster_name, vlan_id, create_cluster = self._parse_zone(instance["availabili    ty_zone"])
184 
185         # update instances table
186         bmm, reuse = self._select_machine(context, instance)
187         instance["display_name"] = bmm["name"]
188         instance["availability_zone"] = instance_zone
189         db.instance_update(context,
190                            instance["id"],
191                            {"display_name": bmm["name"],
192                             "availability_zone": instance_zone})
193         if vlan_id:
194             db.bmm_update(context, bmm["id"], {"availability_zone": cluster_name,
195                                                "vlan_id": vlan_id,
196                                                "service_ip": None})
197 
198         if instance_zone == "resource_pool":
199             self._install_machine(context, instance, bmm, cluster_name, vlan_id)
                 #^--- ベアメタルにOSをインストールするっぽい。
200         else: # instance_zone != "resource_pool"の場合。。。後で考える。
201             self._update_ofc(bmm, cluster_name)
202             if bmm["instance_id"]:
203                 db.instance_destroy(context, bmm["instance_id"])
204 
205             if reuse:
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

242     def _install_machine(self, context, instance, bmm, cluster_name, vlan_id, update_instance=False):
243         db.bmm_update(context, bmm["id"], {"instance_id": instance["id"]})
244         mac = self._get_pxe_mac(bmm)
245 
246         # fetch image
247         image_base_path = self._get_cobbler_image_path()
248         if not os.path.exists(image_base_path):
249             utils.execute('mkdir', '-p', image_base_path)
250 
251         image_path = self._get_cobbler_image_path(instance)
252         if not os.path.exists(image_path):
253             image_meta = images.fetch(context,
254                                       instance["image_ref"],
255                                       image_path,
256                                       instance["user_id"],
257                                       instance["project_id"])
258         else:
259             image_meta = images.show(context, instance["image_ref"])
260 
261         image_type = "server"
262         image_name = image_meta["name"] or image_meta["properties"]["image_location"]
263         if image_name.find("dodai-deploy") == -1:
264             image_type = "node"
265 
266         # begin to install os
267         pxe_ip = bmm["pxe_ip"] or "None"
268         pxe_mac = bmm["pxe_mac"] or "None"
269         storage_ip = bmm["storage_ip"] or "None"
270         storage_mac = bmm["storage_mac"] or "None"
271         service_mac1 = bmm["service_mac1"] or "None"
272         service_mac2 = bmm["service_mac2"] or "None"
273 
274         instance_path = self._get_cobbler_instance_path(instance)
275         if not os.path.exists(instance_path):
276             utils.execute('mkdir', '-p', instance_path)
277 
            # v--「create.sh」をどこかに用意している。
278         self._cp_template("create.sh",
279                           self._get_cobbler_instance_path(instance, "create.sh"),
280                           {"INSTANCE_ID": instance["id"],
281                            "IMAGE_ID": instance["image_ref"],
282                            "COBBLER": FLAGS.cobbler,
283                            "HOST_NAME": bmm["name"],
284                            "STORAGE_IP": storage_ip,
285                            "STORAGE_MAC": storage_mac,
286                            "PXE_IP": pxe_ip,
287                            "PXE_MAC": pxe_mac,
288                            "SERVICE_MAC1": bmm["service_mac1"],
289                            "SERVICE_MAC2": bmm["service_mac2"],
290                            "IMAGE_TYPE": image_type,
291                            "MONITOR_PORT": FLAGS.dodai_monitor_port,
292                            "ROOT_SIZE": FLAGS.dodai_partition_root_gb,
293                            "SWAP_SIZE": FLAGS.dodai_partition_swap_gb,
294                            "EPHEMERAL_SIZE": FLAGS.dodai_partition_ephemeral_gb,
295                            "KDUMP_SIZE": FLAGS.dodai_partition_kdump_gb})
296 
                       # v--「pxeboot_action」をどこかに用意している。
297         self._cp_template("pxeboot_action",
298                           self._get_pxe_boot_file(mac),
299                           {"INSTANCE_ID": instance["id"],
300                            "COBBLER": FLAGS.cobbler,
301                            "PXE_MAC": pxe_mac,
302                            "ACTION": "create"})
303 
304         LOG.debug("Reboot or power on.")
305         self._reboot_or_power_on(bmm["ipmi_ip"]) # <- おもむろにサーバを再起動
306 
307         # wait until starting to install os
308         while self._get_state(context, instance) != "install":
309             greenthread.sleep(20)
310             LOG.debug("Wait until begin to install instance %s." % instance["id"])
311         self._cp_template("pxeboot_start", self._get_pxe_boot_file(mac), {})
312 
313         # wait until starting to reboot 
314         while self._get_state(context, instance) != "install_reboot":
315             greenthread.sleep(20)
316             LOG.debug("Wait until begin to reboot instance %s after os has been installed." % instance["id"])
317         power_manager = PowerManager(bmm["ipmi_ip"])
318         power_manager.soft_off()
319         while power_manager.status() == "on":
320             greenthread.sleep(20)
321             LOG.debug("Wait unit the instance %s shuts down." % instance["id"])
322         power_manager.on()
323 
324         # wait until installation of os finished
325         while self._get_state(context, instance) != "installed":
326             greenthread.sleep(20)
327             LOG.debug("Wait until instance %s installation finished." % instance["id"])
328 
329         if cluster_name == "resource_pool":
330             status = "active"
331         else:
332             status = "used"
333 
334         db.bmm_update(context, bmm["id"], {"status": status})
335 
336         if update_instance:
337             db.instance_update(context, instance["id"], {"vm_state": vm_states.ACTIVE})

「create.sh」「pxeboot_action」をどこかに用意して、おもむろにサーバを再起動していますね。これは、どこに用意しているんだろう。。。。

nova/virt/dodai/connection.py

468     def _cp_template(self, template_name, dest_path, params):
469         f = open(utils.abspath("virt/dodai/" + template_name + ".template"), "r")
470         content = f.read()
471         f.close()
472 
473         path = os.path.dirname(dest_path)
474         if not os.path.exists(path):
475            os.makedirs(path)
476 
477         for key, value in params.iteritems():
478             content = content.replace(key, str(value))
479 
480         f = open(dest_path, "w")
481         f.write(content)
482         f.close

うーん。「self._get_cobbler_instance_path(instance, "create.sh")」と「self._get_pxe_boot_file(mac)」がファイルパスですね。

実体は。。。。

nova/virt/dodai/connection.py

433     def _get_cobbler_instance_path(self, instance, file_name = ""):
434         return os.path.join(FLAGS.cobbler_path,
435                      "instances",
436                      str(instance["id"]),
437                      file_name)
...
448     def _get_pxe_boot_file(self, mac):
449         return os.path.join(FLAGS.pxe_boot_path, mac)

nova/flags.py

432 DEFINE_string('cobbler_path', '/var/www/cobbler', 'Path of cobbler')
433 DEFINE_string('pxe_boot_path', '/var/lib/tftpboot/pxelinux.cfg', 'Path of pxeboot folder')

おぉ。「pxeboot_action」は、tftpboot/pexlinux.cfg/hogehoge に該当サーバのMAC指定で対応するpxebootのconfigを与えているっぽいですね。

「create.sh」は、きっと、pxeboot後に、cobblerでサーバに送り込んで、インストール処理をさせるシェルでしょう。間違いない。

というわけで、これらのテンプレートを確認します。

nova/virt/dodai/pxeboot_action.template

  1 DEFAULT pxeboot
  2 TIMEOUT 20
  3 PROMPT 0
  4 LABEL pxeboot
  5         KERNEL /os-duper/vmlinuz0
  6         APPEND initrd=/os-duper/initrd0.img root=live:/os-duper.iso root=/os-duper.iso rootfstype=auto ro     liveimg quiet rhgb rd_NO_LUKS rd_NO_MD rd_NO_DM dodai_script=http://COBBLER/cobbler/instances/INSTANCE_ID/    ACTION.sh dodai_pxe_mac=PXE_MAC

pxebootでは、Live DVDのisoを送り込んで、サーバを起動して、「dodai_script」パラメータで、create.shをキックしているっぽい。

そして、親玉(?)のcreate.shです。まずは、メインルーチン(?)の流れ。

nova/virt/dodai/create.sh.template

...
134 notify "install"
135 
136 sync_time
137 partition_and_format
138 copy_fs
139 set_hostname
140 create_files
141 grub_install
142 setup_network
143 sync_target_machine_time
144 
145 notify "install_reboot"
146 
147 echo "Initialization finished."

関数名からやっていることは想像つきますね。肝はきっと、「copy_fs」と「grub_install」

 36 function copy_fs {
 37   image_dev=sda4
 38 
 39   mkdir /mnt/$image_dev
 40   mount /dev/$image_dev /mnt/$image_dev
 41 
 42   wget -O /mnt/$image_dev/image http://$cobbler/cobbler/images/$image_id
 43   mkdir /mnt/image
 44   mount -o loop -t ext4 /mnt/$image_dev/image /mnt/image
 45 
 46   if [[ -n `file /mnt/$image_dev/image | grep -i ext3` ]]; then
 47      MKFS="mkfs.ext3"
 48   elif [[ -n `file /mnt/$image_dev/image | grep -i ext4` ]]; then
 49      MKFS="mkfs.ext4"
 50   else
 51      MKFS="mkfs.ext3"
 52   fi
 53   $MKFS /dev/sda1
 54   $MKFS /dev/sda2
 55 
 56   mkdir /mnt/sda2
 57   mount /dev/sda2 /mnt/sda2
 58 
 59   rsync -PavHS /mnt/image/ /mnt/sda2 > /dev/null
 60 
 61   if [[ -n `grep '/mnt' /mnt/sda2/etc/fstab | grep ext3` ]]; then
 62      MKFS="mkfs.ext3"
 63   elif [[ -n `grep '/mnt' /mnt/sda2/etc/fstab | grep ext4` ]]; then
 64      MKFS="mkfs.ext4"
 65   else
 66      MKFS="mkfs.ext3"
 67   fi
 68   $MKFS /dev/sda5
 69 
 70   umount /mnt/image
 71   rm -rf /mnt/$image_dev/image
 72   umount /mnt/$image_dev
 73 }
...
 93 function grub_install {
 94   mount -o bind /dev/ /mnt/sda2/dev
 95   mount -t proc none /mnt/sda2/proc
 96   echo I | chroot /mnt/sda2 parted /dev/sda set 1 bios_grub on
 97   chroot /mnt/sda2 grub-install /dev/sda
 98 }

イメージをローカルにwgetしてきて、中身をrsyncして、最後に、grub-installを叩いています。

考察

最後の漢らしいインストール方法は、対応できるOSの種類が限定されそうですね。ファイルシステムもext3/ext4に決め打ちですし。

とはいえ・・・

あらゆるOSに対応できるインストール方法なんてあるわけもないんですよね。

たとえば、アップストリームにBluePrintが出ている、NTTdocomo-openstackの場合は、

・インストール対象のサーバでtgtdを上げて、内蔵ディスクをiSCSIボリュームとして公開して、「インストールサーバ」側で、LUNをアタッチしてイメージを書き込む。

という離れ業を使っているようですが、この場合は、grub-installが難しいみたいな事を誰かが言ってました。(間違ってたらすいません。)

で、1つのアイデアは、インストール方法をdodai-computeから外だしにして、複数選べるようにするということ。

Novaから見れば、spawnを呼んだ所で、後は知らない世界なので、どんな方法でインストールしようが勝手なはず。例えば、イメージのメタデータにインストール方法のタグを付けておいて、

・dodai方式で漢らしくwget/ローカルrsyncでインストール
・NTTdocomo-openstack方式で「iSCSIインストールサーバ(?)」を使用する
・普通にKickStartでインストール
・オペレータを呼び出して、手動でインストールさせる
・いっそのこと内蔵ディスクつかわずにLive Imageで起動してしまう
・etc....

を選択できるとよいのかと。インストール後に設定すべき項目と、インストール完了の通知方法は、標準化が必要ですね。

特に、KickStartを使う場合に、「ks.cfgの在り処」をイメージのメタ情報として入れたとするじゃないですか。そうすると、イメージを登録する人は、自分の管理下にある場所のks.cfgを指定すれば、実際にインストールする内容は、イメージを再登録することなく、ks.cfgを書き換えることで、自由に変更できるわけですよ。なんとなく便利になる気がしませんか?

あと、複数インストール方式に対応するには、Key injectionのタイミングはうまく考えないとだめですね。