__init__.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. import math
  2. import time
  3. import uuid
  4. from cinderclient import client as cinder_client
  5. from glanceclient import client as glance_client
  6. from neutronclient.neutron import client as neutron_client
  7. from novaclient import client as nova_client
  8. from oslo_config import cfg
  9. from oslo_log import log as logging
  10. from oslo_utils import units
  11. import paramiko
  12. from coriolis import constants
  13. from coriolis import exception
  14. from coriolis import keystone
  15. from coriolis.osmorphing import manager as osmorphing_manager
  16. from coriolis.providers import base
  17. from coriolis import utils
  18. opts = [
  19. cfg.StrOpt('disk_format',
  20. default=constants.DISK_FORMAT_QCOW2,
  21. help='Default image disk format.'),
  22. cfg.StrOpt('container_format',
  23. default='bare',
  24. help='Default image container format.'),
  25. cfg.StrOpt('hypervisor_type',
  26. default=None,
  27. help='Default hypervisor type.'),
  28. cfg.StrOpt('boot_from_volume',
  29. default=True,
  30. help='Set to "True" to boot from volume by default instead of '
  31. 'using local storage.'),
  32. cfg.StrOpt('glance_upload',
  33. default=True,
  34. help='Set to "True" to use Glance to upload images.'),
  35. cfg.StrOpt('migr_image_name',
  36. default=None,
  37. help='Default image name used for worker instances '
  38. 'during migrations.'),
  39. cfg.StrOpt('migr_flavor_name',
  40. default='m1.small',
  41. help='Default flavor name used for worker instances '
  42. 'during migrations.'),
  43. cfg.StrOpt('migr_network_name',
  44. default='private',
  45. help='Default network name used for worker instances '
  46. 'during migrations.'),
  47. cfg.StrOpt('fip_pool_name',
  48. default='public',
  49. help='Default floating ip pool name.'),
  50. ]
  51. CONF = cfg.CONF
  52. CONF.register_opts(opts, 'openstack_migration_provider')
  53. NOVA_API_VERSION = 2
  54. GLANCE_API_VERSION = 1
  55. NEUTRON_API_VERSION = '2.0'
  56. CINDER_API_VERSION = 2
  57. MIGRATION_TMP_FORMAT = "migration_tmp_%s"
  58. DISK_HEADER_SIZE = 10 * units.Mi
  59. SSH_PORT = 22
  60. MIGR_USER_DATA = (
  61. "#cloud-config\n"
  62. "users:\n"
  63. " - name: %s\n"
  64. " ssh-authorized-keys:\n"
  65. " - %s\n"
  66. " sudo: ['ALL=(ALL) NOPASSWD:ALL']\n"
  67. " groups: sudo\n"
  68. " shell: /bin/bash\n"
  69. )
  70. MIGR_GUEST_USERNAME = 'cloudbase'
  71. LOG = logging.getLogger(__name__)
  72. class _MigrationResources(object):
  73. def __init__(self, nova, neutron, keypair, instance, port,
  74. floating_ip, sec_group, k):
  75. self._nova = nova
  76. self._neutron = neutron
  77. self._instance = instance
  78. self._port = port
  79. self._floating_ip = floating_ip
  80. self._sec_group = sec_group
  81. self._keypair = keypair
  82. self._k = k
  83. def get_guest_connection_info(self):
  84. return {
  85. "ip": self._floating_ip.ip,
  86. "port": SSH_PORT,
  87. "username": MIGR_GUEST_USERNAME,
  88. "pkey": self._k,
  89. }
  90. @utils.retry_on_error()
  91. def _wait_for_instance_deletion(self, instance_id):
  92. instances = self._nova.servers.findall(id=instance_id)
  93. while instances and instances[0].status != 'ERROR':
  94. time.sleep(2)
  95. instances = self._nova.servers.findall(id=instance_id)
  96. if instances:
  97. raise exception.CoriolisException(
  98. "VM is in status: %s" % instances[0].status)
  99. def get_instance(self):
  100. return self._instance
  101. @utils.retry_on_error()
  102. def delete(self):
  103. if self._instance:
  104. self._nova.servers.delete(self._instance)
  105. self._wait_for_instance_deletion(self._instance.id)
  106. self._instance = None
  107. if self._floating_ip:
  108. self._nova.floating_ips.delete(self._floating_ip)
  109. self._floating_ip = None
  110. if self._port:
  111. self._neutron.delete_port(self._port['id'])
  112. self._port = None
  113. if self._sec_group:
  114. self._nova.security_groups.delete(self._sec_group.id)
  115. self._sec_group = None
  116. if self._keypair:
  117. self._nova.keypairs.delete(self._keypair.name)
  118. self._keypair = None
  119. class ImportProvider(base.BaseImportProvider):
  120. def validate_connection_info(self, connection_info):
  121. return True
  122. @utils.retry_on_error()
  123. def _create_image(self, glance, name, disk_path, disk_format,
  124. container_format, hypervisor_type):
  125. properties = {}
  126. if hypervisor_type:
  127. properties["hypervisor_type"] = hypervisor_type
  128. with open(disk_path, 'rb') as f:
  129. return glance.images.create(
  130. name=name,
  131. disk_format=disk_format,
  132. container_format=container_format,
  133. properties=properties,
  134. data=f)
  135. @utils.retry_on_error()
  136. def _wait_for_volume(self, nova, volume, expected_status='in-use'):
  137. volume = nova.volumes.findall(id=volume.id)[0]
  138. while volume.status not in [expected_status, 'error']:
  139. time.sleep(2)
  140. volume = nova.volumes.get(volume.id)
  141. if volume.status != expected_status:
  142. raise exception.CoriolisException(
  143. "Volume is in status: %s" % volume.status)
  144. @utils.retry_on_error()
  145. def _wait_for_instance(self, nova, instance, expected_status='ACTIVE'):
  146. instance = nova.servers.get(instance.id)
  147. while instance.status not in [expected_status, 'ERROR']:
  148. time.sleep(2)
  149. instance = nova.servers.get(instance.id)
  150. if instance.status != expected_status:
  151. raise exception.CoriolisException(
  152. "VM is in status: %s" % instance.status)
  153. def _get_unique_name(self):
  154. return MIGRATION_TMP_FORMAT % str(uuid.uuid4())
  155. @utils.retry_on_error()
  156. def _create_neutron_port(self, neutron, network_name, mac_address=None):
  157. networks = neutron.list_networks(name=network_name)
  158. network_id = networks['networks'][0]['id']
  159. # make sure that the port is not already existing from a previous
  160. # migration attempt
  161. if mac_address:
  162. ports = neutron.list_ports(
  163. mac_address=mac_address).get('ports', [])
  164. if ports:
  165. neutron.delete_port(ports[0]['id'])
  166. body = {"port": {
  167. "network_id": network_id,
  168. }}
  169. if mac_address:
  170. body["port"]["mac_address"] = mac_address
  171. return neutron.create_port(body=body)['port']
  172. @utils.retry_on_error()
  173. def _create_keypair(self, nova, name, public_key):
  174. if nova.keypairs.findall(name=name):
  175. nova.keypairs.delete(name)
  176. return nova.keypairs.create(name=name, public_key=public_key)
  177. @utils.retry_on_error()
  178. def _deploy_migration_resources(self, nova, glance, neutron,
  179. migr_image_name, migr_flavor_name,
  180. migr_network_name, migr_fip_pool_name):
  181. if not glance.images.findall(name=migr_image_name):
  182. raise exception.CoriolisException(
  183. "Glance image \"%s\" not found" % migr_image_name)
  184. image = nova.images.find(name=migr_image_name)
  185. flavor = nova.flavors.find(name=migr_flavor_name)
  186. keypair = None
  187. instance = None
  188. floating_ip = None
  189. sec_group = None
  190. port = None
  191. try:
  192. migr_keypair_name = self._get_unique_name()
  193. self._event_manager.progress_update(
  194. "Creating migration worker instance keypair")
  195. k = paramiko.RSAKey.generate(2048)
  196. public_key = "ssh-rsa %s tmp@migration" % k.get_base64()
  197. keypair = self._create_keypair(nova, migr_keypair_name, public_key)
  198. self._event_manager.progress_update(
  199. "Creating migration worker instance Neutron port")
  200. port = self._create_neutron_port(neutron, migr_network_name)
  201. userdata = MIGR_USER_DATA % (MIGR_GUEST_USERNAME, public_key)
  202. instance = nova.servers.create(
  203. name=self._get_unique_name(),
  204. image=image,
  205. flavor=flavor,
  206. key_name=migr_keypair_name,
  207. userdata=userdata,
  208. nics=[{'port-id': port['id']}])
  209. self._event_manager.progress_update(
  210. "Adding migration worker instance floating IP")
  211. floating_ip = nova.floating_ips.create(pool=migr_fip_pool_name)
  212. self._wait_for_instance(nova, instance, 'ACTIVE')
  213. LOG.info("Floating IP: %s", floating_ip.ip)
  214. instance.add_floating_ip(floating_ip)
  215. self._event_manager.progress_update(
  216. "Adding migration worker instance security group")
  217. migr_sec_group_name = self._get_unique_name()
  218. sec_group = nova.security_groups.create(
  219. name=migr_sec_group_name, description=migr_sec_group_name)
  220. nova.security_group_rules.create(
  221. sec_group.id,
  222. ip_protocol="tcp",
  223. from_port=SSH_PORT,
  224. to_port=SSH_PORT)
  225. instance.add_security_group(sec_group.id)
  226. self._event_manager.progress_update(
  227. "Waiting for connectivity on host: %(ip)s:%(port)s" %
  228. {"ip": floating_ip.ip, "port": SSH_PORT})
  229. utils.wait_for_port_connectivity(floating_ip.ip, SSH_PORT)
  230. return _MigrationResources(nova, neutron, keypair, instance, port,
  231. floating_ip, sec_group, k)
  232. except:
  233. if instance:
  234. nova.servers.delete(instance)
  235. if floating_ip:
  236. nova.floating_ips.delete(floating_ip)
  237. if port:
  238. neutron.delete_port(port['id'])
  239. if sec_group:
  240. nova.security_groups.delete(sec_group.id)
  241. if keypair:
  242. nova.keypairs.delete(keypair.name)
  243. raise
  244. @utils.retry_on_error()
  245. def _attach_volume(self, nova, instance, volume, volume_dev):
  246. nova.volumes.create_server_volume(
  247. instance.id, volume.id, volume_dev)
  248. self._wait_for_volume(nova, volume, 'in-use')
  249. def import_instance(self, ctxt, connection_info, target_environment,
  250. instance_name, export_info):
  251. session = keystone.create_keystone_session(ctxt, connection_info)
  252. nova = nova_client.Client(NOVA_API_VERSION, session=session)
  253. glance = glance_client.Client(GLANCE_API_VERSION, session=session)
  254. neutron = neutron_client.Client(NEUTRON_API_VERSION, session=session)
  255. cinder = cinder_client.Client(CINDER_API_VERSION, session=session)
  256. glance_upload = target_environment.get(
  257. "glance_upload", CONF.openstack_migration_provider.glance_upload)
  258. target_disk_format = target_environment.get(
  259. "disk_format", CONF.openstack_migration_provider.disk_format)
  260. container_format = target_environment.get(
  261. "container_format",
  262. CONF.openstack_migration_provider.container_format)
  263. hypervisor_type = target_environment.get(
  264. "hypervisor_type",
  265. CONF.openstack_migration_provider.hypervisor_type)
  266. fip_pool_name = target_environment.get(
  267. "fip_pool_name", CONF.openstack_migration_provider.fip_pool_name)
  268. network_map = target_environment.get("network_map", {})
  269. keypair_name = target_environment.get("keypair_name")
  270. migr_image_name = target_environment.get(
  271. "migr_image_name",
  272. CONF.openstack_migration_provider.migr_image_name)
  273. migr_flavor_name = target_environment.get(
  274. "migr_flavor_name",
  275. CONF.openstack_migration_provider.migr_flavor_name)
  276. migr_fip_pool_name = target_environment.get(
  277. "migr_fip_pool_name",
  278. CONF.openstack_migration_provider.fip_pool_name or fip_pool_name)
  279. migr_network_name = target_environment.get(
  280. "migr_network_name",
  281. CONF.openstack_migration_provider.migr_network_name)
  282. flavor_name = target_environment.get("flavor_name", migr_flavor_name)
  283. if not migr_network_name:
  284. if len(network_map) != 1:
  285. raise exception.CoriolisException(
  286. 'If "migr_network_name" is not provided, "network_map" '
  287. 'must contain exactly one entry')
  288. migr_network_name = network_map.values()[0]
  289. disks_info = export_info["devices"]["disks"]
  290. images = []
  291. volumes = []
  292. volume_devs = []
  293. if glance_upload:
  294. for disk_info in disks_info:
  295. disk_path = disk_info["path"]
  296. disk_file_info = utils.get_disk_info(disk_path)
  297. # if target_disk_format == disk_file_info["format"]:
  298. # target_disk_path = disk_path
  299. # else:
  300. # target_disk_path = (
  301. # "%s.%s" % (os.path.splitext(disk_path)[0],
  302. # target_disk_format))
  303. # utils.convert_disk_format(disk_path, target_disk_path,
  304. # target_disk_format)
  305. self._event_manager.progress_update("Uploading Glance image")
  306. disk_format = disk_file_info["format"]
  307. image = self._create_image(
  308. glance, self._get_unique_name(),
  309. disk_path, disk_format,
  310. container_format, hypervisor_type)
  311. images.append(image)
  312. virtual_disk_size = disk_file_info["virtual-size"]
  313. if disk_format != constants.DISK_FORMAT_RAW:
  314. virtual_disk_size += DISK_HEADER_SIZE
  315. self._event_manager.progress_update("Creating Cinder volume")
  316. volume_size_gb = math.ceil(virtual_disk_size / units.Gi)
  317. volume = nova.volumes.create(
  318. size=volume_size_gb,
  319. display_name=self._get_unique_name(),
  320. imageRef=image.id)
  321. volumes.append(volume)
  322. migr_resources = self._deploy_migration_resources(
  323. nova, glance, neutron, migr_image_name, migr_flavor_name,
  324. migr_network_name, migr_fip_pool_name)
  325. nics_info = export_info["devices"].get("nics", [])
  326. try:
  327. for i, volume in enumerate(volumes):
  328. self._wait_for_volume(nova, volume, 'available')
  329. self._event_manager.progress_update("Deleting Glance image")
  330. glance.images.delete(images[i].id)
  331. self._event_manager.progress_update(
  332. "Attaching volume to worker instance")
  333. # TODO: improve device assignment
  334. if hypervisor_type == constants.HYPERVISOR_HYPERV:
  335. volume_dev_base = "/dev/sd%s"
  336. else:
  337. volume_dev_base = "/dev/vd%s"
  338. volume_dev = volume_dev_base % chr(ord('a') + i + 1)
  339. volume_devs.append(volume_dev)
  340. self._attach_volume(nova, migr_resources.get_instance(),
  341. volume, volume_dev)
  342. guest_conn_info = migr_resources.get_guest_connection_info()
  343. os_type = None
  344. self._event_manager.progress_update(
  345. "Preparing instance for target platform")
  346. osmorphing_manager.morph_image(guest_conn_info,
  347. os_type,
  348. hypervisor_type,
  349. constants.PLATFORM_OPENSTACK,
  350. volume_devs,
  351. nics_info,
  352. self._event_manager)
  353. finally:
  354. self._event_manager.progress_update(
  355. "Removing worker instance resources")
  356. migr_resources.delete()
  357. self._event_manager.progress_update("Renaming volumes")
  358. for i, volume in enumerate(volumes):
  359. new_volume_name = "%s %s" % (instance_name, i + 1)
  360. cinder.volumes.update(volume.id, name=new_volume_name)
  361. ports = []
  362. for nic_info in nics_info:
  363. self._event_manager.progress_update(
  364. "Creating Neutron port for migrated instance")
  365. origin_network_name = nic_info.get("network_name")
  366. if not origin_network_name:
  367. self._warn("Origin network name not provided for for nic: %s, "
  368. "skipping", nic_info.get("name"))
  369. continue
  370. network_name = network_map.get(origin_network_name)
  371. if not network_name:
  372. raise exception.CoriolisException(
  373. "Network not mapped in network_map: %s" %
  374. origin_network_name)
  375. ports.append(self._create_neutron_port(
  376. neutron, network_name, nic_info.get("mac_address")))
  377. self._event_manager.progress_update(
  378. "Creating migrated instance")
  379. instance = self._create_target_instance(
  380. nova, flavor_name, instance_name, keypair_name, ports, volumes,
  381. migr_image_name)
  382. @utils.retry_on_error()
  383. def _create_target_instance(self, nova, flavor_name, instance_name,
  384. keypair_name, ports, volumes, image_name):
  385. flavor = nova.flavors.find(name=flavor_name)
  386. image = nova.images.find(name=image_name)
  387. block_device_mapping = {}
  388. for i, volume in enumerate(volumes):
  389. # Delete volume on termination
  390. block_device_mapping[
  391. 'vd%s' % chr(ord('a') + i)] = "%s:volume::True" % volume.id
  392. nics = [{'port-id': p['id']} for p in ports]
  393. # Note: Nova requires an image even when booting from volume
  394. LOG.info('Creating target instance...')
  395. instance = nova.servers.create(
  396. name=instance_name,
  397. image=image,
  398. flavor=flavor,
  399. key_name=keypair_name,
  400. block_device_mapping=block_device_mapping,
  401. nics=nics)
  402. try:
  403. self._wait_for_instance(nova, instance, 'ACTIVE')
  404. return instance
  405. except:
  406. if instance:
  407. nova.servers.delete(instance)
  408. raise