前置き
実は、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のタイミングはうまく考えないとだめですね。