__init__.py 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263
  1. # Copyright 2016 Cloudbase Solutions Srl
  2. # All Rights Reserved.
  3. import collections
  4. import math
  5. import os
  6. import tempfile
  7. import time
  8. import uuid
  9. from cinderclient import client as cinder_client
  10. from glanceclient import client as glance_client
  11. from neutronclient.neutron import client as neutron_client
  12. from novaclient import client as nova_client
  13. from oslo_config import cfg
  14. from oslo_log import log as logging
  15. from oslo_utils import units
  16. import paramiko
  17. from coriolis import constants
  18. from coriolis import exception
  19. from coriolis import keystone
  20. from coriolis.osmorphing import manager as osmorphing_manager
  21. from coriolis.providers import base
  22. from coriolis import schemas
  23. from coriolis import utils
  24. opts = [
  25. cfg.StrOpt('disk_format',
  26. default=constants.DISK_FORMAT_QCOW2,
  27. help='Default image disk format.'),
  28. cfg.StrOpt('container_format',
  29. default='bare',
  30. help='Default image container format.'),
  31. cfg.StrOpt('hypervisor_type',
  32. default=None,
  33. help='Default hypervisor type.'),
  34. cfg.StrOpt('boot_from_volume',
  35. default=True,
  36. help='Set to "True" to boot from volume by default instead of '
  37. 'using local storage.'),
  38. cfg.StrOpt('glance_upload',
  39. default=True,
  40. help='Set to "True" to use Glance to upload images.'),
  41. cfg.DictOpt('migr_image_name_map',
  42. default={},
  43. help='Default image names used for worker instances during '
  44. 'migrations.'),
  45. cfg.StrOpt('migr_flavor_name',
  46. default='m1.small',
  47. help='Default flavor name used for worker instances '
  48. 'during migrations.'),
  49. cfg.StrOpt('migr_network_name',
  50. default='private',
  51. help='Default network name used for worker instances '
  52. 'during migrations.'),
  53. cfg.StrOpt('fip_pool_name',
  54. default='public',
  55. help='Default floating ip pool name.'),
  56. ]
  57. CONF = cfg.CONF
  58. CONF.register_opts(opts, 'openstack_migration_provider')
  59. NOVA_API_VERSION = 2
  60. GLANCE_API_VERSION = 1
  61. NEUTRON_API_VERSION = '2.0'
  62. CINDER_API_VERSION = 2
  63. MIGRATION_TMP_FORMAT = "migration_tmp_%s"
  64. DISK_HEADER_SIZE = 10 * units.Mi
  65. SSH_PORT = 22
  66. WINRM_HTTPS_PORT = 5986
  67. MIGR_USER_DATA = (
  68. "#cloud-config\n"
  69. "users:\n"
  70. " - name: %s\n"
  71. " ssh-authorized-keys:\n"
  72. " - %s\n"
  73. " sudo: ['ALL=(ALL) NOPASSWD:ALL']\n"
  74. " groups: sudo\n"
  75. " shell: /bin/bash\n"
  76. )
  77. MIGR_GUEST_USERNAME = 'cloudbase'
  78. MIGR_GUEST_USERNAME_WINDOWS = "admin"
  79. VOLUME_NAME_FORMAT = "%(instance_name)s %(num)s"
  80. REPLICA_VOLUME_NAME_FORMAT = "Coriolis Replica - %(instance_name)s %(num)s"
  81. LOG = logging.getLogger(__name__)
  82. GlanceImage = collections.namedtuple(
  83. "GlanceImage", "id format size path os_type")
  84. def _get_unique_name():
  85. return MIGRATION_TMP_FORMAT % str(uuid.uuid4())
  86. @utils.retry_on_error()
  87. def _wait_for_image(nova, image_id, expected_status='ACTIVE'):
  88. image = nova.images.get(image_id)
  89. while image.status not in [expected_status, 'ERROR']:
  90. time.sleep(2)
  91. image = nova.images.get(image.id)
  92. if image.status != expected_status:
  93. raise exception.CoriolisException(
  94. "Image is in status: %s" % image.status)
  95. @utils.retry_on_error()
  96. def _wait_for_instance(nova, instance, expected_status='ACTIVE'):
  97. instance = nova.servers.get(instance.id)
  98. while instance.status not in [expected_status, 'ERROR']:
  99. time.sleep(2)
  100. instance = nova.servers.get(instance.id)
  101. if instance.status != expected_status:
  102. raise exception.CoriolisException(
  103. "VM is in status: %s" % instance.status)
  104. @utils.retry_on_error()
  105. def _find_volume(cinder, volume_id):
  106. volumes = cinder.volumes.findall(id=volume_id)
  107. if volumes:
  108. return volumes[0]
  109. @utils.retry_on_error()
  110. def _extend_volume(cinder, volume_id, new_size):
  111. volume_size_gb = math.ceil(new_size / units.Gi)
  112. cinder.volumes.extend(volume_id, volume_size_gb)
  113. @utils.retry_on_error()
  114. def _get_volume_from_snapshot(cinder, snapshot_id):
  115. snapshot = cinder.volume_snapshots.get(snapshot_id)
  116. return cinder.volumes.get(snapshot.volume_id)
  117. @utils.retry_on_error()
  118. def _create_volume(cinder, size, name, image_ref=None, snapshot_id=None):
  119. if snapshot_id:
  120. volume_size_gb = None
  121. else:
  122. volume_size_gb = math.ceil(size / units.Gi)
  123. return cinder.volumes.create(
  124. size=volume_size_gb,
  125. name=name,
  126. imageRef=image_ref,
  127. snapshot_id=snapshot_id)
  128. @utils.retry_on_error()
  129. def _wait_for_volume(cinder, volume_id, expected_status='available'):
  130. volumes = cinder.volumes.findall(id=volume_id)
  131. if not volumes:
  132. raise exception.CoriolisException("Volume not found")
  133. volume = volumes[0]
  134. while volume.status not in [expected_status, 'error']:
  135. time.sleep(2)
  136. volume = cinder.volumes.get(volume.id)
  137. if volume.status != expected_status:
  138. raise exception.CoriolisException(
  139. "Volume is in status: %s" % volume.status)
  140. @utils.retry_on_error()
  141. def _delete_volume(cinder, volume_id):
  142. volumes = cinder.volumes.findall(id=volume_id)
  143. for volume in volumes:
  144. volume.delete()
  145. @utils.retry_on_error()
  146. def _create_volume_snapshot(cinder, volume_id, name):
  147. return cinder.volume_snapshots.create(volume_id, name=name)
  148. @utils.retry_on_error()
  149. def _wait_for_volume_snapshot(cinder, snapshot_id,
  150. expected_status='available'):
  151. snapshots = cinder.volume_snapshots.findall(id=snapshot_id)
  152. if not snapshots:
  153. if expected_status == 'deleted':
  154. return
  155. raise exception.CoriolisException("Volume snapshot not found")
  156. snapshot = snapshots[0]
  157. while snapshot.status not in [expected_status, 'error']:
  158. time.sleep(2)
  159. if expected_status == 'deleted':
  160. snapshots = cinder.volume_snapshots.findall(id=snapshot_id)
  161. if not snapshots:
  162. return
  163. snapshot = snapshots[0]
  164. else:
  165. snapshot = cinder.volume_snapshots.get(snapshot.id)
  166. if snapshot.status != expected_status:
  167. raise exception.CoriolisException(
  168. "Volume snapshot is in status: %s" % snapshot.status)
  169. @utils.retry_on_error()
  170. def _delete_volume_snapshot(cinder, snapshot_id):
  171. snapshots = cinder.volume_snapshots.findall(id=snapshot_id)
  172. for snapshot in snapshots:
  173. return cinder.volume_snapshots.delete(snapshot.id)
  174. class _MigrationResources(object):
  175. def __init__(self, nova, neutron, keypair, instance, port,
  176. floating_ip, guest_port, sec_group, username, password, k):
  177. self._nova = nova
  178. self._neutron = neutron
  179. self._instance = instance
  180. self._port = port
  181. self._floating_ip = floating_ip
  182. self._guest_port = guest_port
  183. self._sec_group = sec_group
  184. self._keypair = keypair
  185. self._k = k
  186. self._username = username
  187. self._password = password
  188. def get_resources_dict(self):
  189. return {
  190. "instance_id": self._instance.id,
  191. "keypair_name": self._keypair.name,
  192. "port_id": self._port["id"],
  193. "floating_ip_id": self._floating_ip.id,
  194. "secgroup_id": self._sec_group.id,
  195. }
  196. @classmethod
  197. @utils.retry_on_error()
  198. def from_resources_dict(cls, nova, neutron, resources_dict):
  199. instance_id = resources_dict["instance_id"]
  200. keypair_name = resources_dict["keypair_name"]
  201. floating_ip_id = resources_dict["floating_ip_id"]
  202. secgroup_id = resources_dict["secgroup_id"]
  203. port_id = resources_dict["port_id"]
  204. instance = None
  205. instances = nova.servers.findall(id=instance_id)
  206. if instances:
  207. instance = instances[0]
  208. keypair = None
  209. keypairs = nova.keypairs.findall(name=keypair_name)
  210. if keypairs:
  211. keypair = keypairs[0]
  212. floating_ip = None
  213. floating_ips = nova.floating_ips.findall(id=floating_ip_id)
  214. if floating_ips:
  215. floating_ip = floating_ips[0]
  216. sec_group = None
  217. sec_groups = nova.security_groups.findall(id=secgroup_id)
  218. if sec_groups:
  219. sec_group = sec_groups[0]
  220. port = None
  221. ports = neutron.list_ports(id=port_id)["ports"]
  222. if ports:
  223. port = ports[0]
  224. return cls(
  225. nova, neutron, keypair, instance, port, floating_ip, None,
  226. sec_group, None, None, None)
  227. def get_guest_connection_info(self):
  228. return {
  229. "ip": self._floating_ip.ip,
  230. "port": self._guest_port,
  231. "username": self._username,
  232. "password": self._password,
  233. "pkey": self._k,
  234. }
  235. @utils.retry_on_error()
  236. def _wait_for_instance_deletion(self, instance_id):
  237. instances = self._nova.servers.findall(id=instance_id)
  238. while instances and instances[0].status != 'ERROR':
  239. time.sleep(2)
  240. instances = self._nova.servers.findall(id=instance_id)
  241. if instances:
  242. raise exception.CoriolisException(
  243. "VM is in status: %s" % instances[0].status)
  244. def get_instance(self):
  245. return self._instance
  246. @utils.retry_on_error()
  247. def delete(self):
  248. if self._instance:
  249. self._nova.servers.delete(self._instance)
  250. self._wait_for_instance_deletion(self._instance.id)
  251. self._instance = None
  252. if self._floating_ip:
  253. self._nova.floating_ips.delete(self._floating_ip)
  254. self._floating_ip = None
  255. if self._port:
  256. self._neutron.delete_port(self._port['id'])
  257. self._port = None
  258. if self._sec_group:
  259. self._nova.security_groups.delete(self._sec_group.id)
  260. self._sec_group = None
  261. if self._keypair:
  262. self._nova.keypairs.delete(self._keypair.name)
  263. self._keypair = None
  264. class ImportProvider(base.BaseReplicaImportProvider):
  265. connection_info_schema = schemas.get_schema(
  266. __name__, schemas.PROVIDER_CONNECTION_INFO_SCHEMA_NAME)
  267. target_environment_schema = schemas.get_schema(
  268. __name__, schemas.PROVIDER_TARGET_ENVIRONMENT_SCHEMA_NAME)
  269. def _create_image(self, glance, name, disk_path, disk_format,
  270. container_format, hypervisor_type):
  271. properties = {}
  272. if hypervisor_type:
  273. properties["hypervisor_type"] = hypervisor_type
  274. if glance.version == 1:
  275. return self._create_image_v1(glance, name, disk_path, disk_format,
  276. container_format, properties)
  277. elif glance.version == 2:
  278. return self._create_image_v2(glance, name, disk_path, disk_format,
  279. container_format, properties)
  280. else:
  281. raise NotImplementedError("Unsupported Glance version")
  282. @utils.retry_on_error()
  283. def _create_image_v2(self, glance, name, disk_path, disk_format,
  284. container_format, properties):
  285. image = glance.images.create(
  286. name=name,
  287. disk_format=disk_format,
  288. container_format=container_format,
  289. **properties)
  290. try:
  291. with open(disk_path, 'rb') as f:
  292. glance.images.upload(image.id, f)
  293. return image
  294. except:
  295. glance.images.delete(image.id)
  296. raise
  297. @utils.retry_on_error()
  298. def _create_image_v1(self, glance, name, disk_path, disk_format,
  299. container_format, properties):
  300. with open(disk_path, 'rb') as f:
  301. return glance.images.create(
  302. name=name,
  303. disk_format=disk_format,
  304. container_format=container_format,
  305. properties=properties,
  306. data=f)
  307. @utils.retry_on_error()
  308. def _create_neutron_port(self, neutron, network_name, mac_address=None):
  309. networks = neutron.list_networks(name=network_name)
  310. network_id = networks['networks'][0]['id']
  311. # make sure that the port is not already existing from a previous
  312. # migration attempt
  313. if mac_address:
  314. ports = neutron.list_ports(
  315. mac_address=mac_address).get('ports', [])
  316. if ports:
  317. neutron.delete_port(ports[0]['id'])
  318. body = {"port": {
  319. "network_id": network_id,
  320. }}
  321. if mac_address:
  322. body["port"]["mac_address"] = mac_address
  323. return neutron.create_port(body=body)['port']
  324. @utils.retry_on_error()
  325. def _create_keypair(self, nova, name, public_key):
  326. if nova.keypairs.findall(name=name):
  327. nova.keypairs.delete(name)
  328. return nova.keypairs.create(name=name, public_key=public_key)
  329. @utils.retry_on_error(max_attempts=10, sleep_seconds=10)
  330. def _get_instance_password(self, instance, k):
  331. self._event_manager.progress_update("Getting instance password")
  332. fd, key_path = tempfile.mkstemp()
  333. try:
  334. k.write_private_key_file(key_path)
  335. return instance.get_password(private_key=key_path).decode()
  336. finally:
  337. os.close(fd)
  338. os.remove(key_path)
  339. @utils.retry_on_error(max_attempts=10, sleep_seconds=30)
  340. def _deploy_migration_resources(self, nova, glance, neutron,
  341. os_type, migr_image_name, migr_flavor_name,
  342. migr_network_name, migr_fip_pool_name):
  343. if not glance.images.findall(name=migr_image_name):
  344. raise exception.CoriolisException(
  345. "Glance image \"%s\" not found" % migr_image_name)
  346. LOG.debug("Migration image name: %s", migr_image_name)
  347. LOG.debug("Migration flavor name: %s", migr_flavor_name)
  348. image = nova.images.find(name=migr_image_name)
  349. flavor = nova.flavors.find(name=migr_flavor_name)
  350. keypair = None
  351. instance = None
  352. floating_ip = None
  353. sec_group = None
  354. port = None
  355. try:
  356. migr_keypair_name = _get_unique_name()
  357. self._event_manager.progress_update(
  358. "Creating migration worker instance keypair")
  359. k = paramiko.RSAKey.generate(2048)
  360. public_key = "ssh-rsa %s tmp@migration" % k.get_base64()
  361. keypair = self._create_keypair(nova, migr_keypair_name, public_key)
  362. self._event_manager.progress_update(
  363. "Creating migration worker instance Neutron port")
  364. port = self._create_neutron_port(neutron, migr_network_name)
  365. # TODO(alexpilotti): use a single username
  366. if os_type == constants.OS_TYPE_WINDOWS:
  367. username = MIGR_GUEST_USERNAME_WINDOWS
  368. else:
  369. username = MIGR_GUEST_USERNAME
  370. userdata = MIGR_USER_DATA % (username, public_key)
  371. instance = nova.servers.create(
  372. name=_get_unique_name(),
  373. image=image,
  374. flavor=flavor,
  375. key_name=migr_keypair_name,
  376. userdata=userdata,
  377. nics=[{'port-id': port['id']}])
  378. self._event_manager.progress_update(
  379. "Adding migration worker instance floating IP")
  380. floating_ip = nova.floating_ips.create(pool=migr_fip_pool_name)
  381. _wait_for_instance(nova, instance, 'ACTIVE')
  382. LOG.info("Floating IP: %s", floating_ip.ip)
  383. instance.add_floating_ip(floating_ip)
  384. self._event_manager.progress_update(
  385. "Adding migration worker instance security group")
  386. if os_type == constants.OS_TYPE_WINDOWS:
  387. guest_port = WINRM_HTTPS_PORT
  388. else:
  389. guest_port = SSH_PORT
  390. migr_sec_group_name = _get_unique_name()
  391. sec_group = nova.security_groups.create(
  392. name=migr_sec_group_name, description=migr_sec_group_name)
  393. nova.security_group_rules.create(
  394. sec_group.id,
  395. ip_protocol="tcp",
  396. from_port=guest_port,
  397. to_port=guest_port)
  398. instance.add_security_group(sec_group.id)
  399. self._event_manager.progress_update(
  400. "Waiting for connectivity on host: %(ip)s:%(port)s" %
  401. {"ip": floating_ip.ip, "port": guest_port})
  402. utils.wait_for_port_connectivity(floating_ip.ip, guest_port)
  403. if os_type == constants.OS_TYPE_WINDOWS:
  404. password = self._get_instance_password(instance, k)
  405. else:
  406. password = None
  407. return _MigrationResources(nova, neutron, keypair, instance, port,
  408. floating_ip, guest_port, sec_group,
  409. username, password, k)
  410. except:
  411. if instance:
  412. nova.servers.delete(instance)
  413. if floating_ip:
  414. nova.floating_ips.delete(floating_ip)
  415. if port:
  416. neutron.delete_port(port['id'])
  417. if sec_group:
  418. nova.security_groups.delete(sec_group.id)
  419. if keypair:
  420. nova.keypairs.delete(keypair.name)
  421. raise
  422. @utils.retry_on_error()
  423. def _attach_volume(self, nova, cinder, instance, volume_id,
  424. volume_dev=None):
  425. # volume can be either a Volume object or an id
  426. volume = nova.volumes.create_server_volume(
  427. instance.id, volume_id, volume_dev)
  428. _wait_for_volume(cinder, volume.id, 'in-use')
  429. return volume
  430. def _get_import_config(self, target_environment, os_type):
  431. config = collections.namedtuple(
  432. "ImportConfig",
  433. ["glance_upload",
  434. "target_disk_format",
  435. "container_format",
  436. "hypervisor_type",
  437. "fip_pool_name",
  438. "network_map",
  439. "keypair_name",
  440. "migr_image_name",
  441. "migr_flavor_name",
  442. "migr_fip_pool_name",
  443. "migr_network_name",
  444. "flavor_name"])
  445. config.glance_upload = target_environment.get(
  446. "glance_upload", CONF.openstack_migration_provider.glance_upload)
  447. config.target_disk_format = target_environment.get(
  448. "disk_format", CONF.openstack_migration_provider.disk_format)
  449. config.container_format = target_environment.get(
  450. "container_format",
  451. CONF.openstack_migration_provider.container_format)
  452. config.hypervisor_type = target_environment.get(
  453. "hypervisor_type",
  454. CONF.openstack_migration_provider.hypervisor_type)
  455. config.fip_pool_name = target_environment.get(
  456. "fip_pool_name", CONF.openstack_migration_provider.fip_pool_name)
  457. config.network_map = target_environment.get("network_map", {})
  458. config.keypair_name = target_environment.get("keypair_name")
  459. config.migr_image_name = target_environment.get(
  460. "migr_image_name",
  461. target_environment.get("migr_image_name_map", {}).get(
  462. os_type,
  463. CONF.openstack_migration_provider.migr_image_name_map.get(
  464. os_type)))
  465. config.migr_flavor_name = target_environment.get(
  466. "migr_flavor_name",
  467. CONF.openstack_migration_provider.migr_flavor_name)
  468. config.migr_fip_pool_name = target_environment.get(
  469. "migr_fip_pool_name",
  470. config.fip_pool_name or
  471. CONF.openstack_migration_provider.fip_pool_name)
  472. config.migr_network_name = target_environment.get(
  473. "migr_network_name",
  474. CONF.openstack_migration_provider.migr_network_name)
  475. config.flavor_name = target_environment.get(
  476. "flavor_name", config.migr_flavor_name)
  477. if not config.migr_image_name:
  478. raise exception.CoriolisException(
  479. "No matching migration image type found")
  480. if not config.migr_network_name:
  481. if len(config.network_map) != 1:
  482. raise exception.CoriolisException(
  483. 'If "migr_network_name" is not provided, "network_map" '
  484. 'must contain exactly one entry')
  485. config.migr_network_name = config.network_map.values()[0]
  486. return config
  487. def _create_images_and_volumes(self, glance, nova, cinder, config,
  488. disks_info):
  489. if not config.glance_upload:
  490. raise exception.CoriolisException(
  491. "Glance upload is currently required for migrations")
  492. images = []
  493. volumes = []
  494. for disk_info in disks_info:
  495. disk_path = disk_info["path"]
  496. disk_file_info = utils.get_disk_info(disk_path)
  497. # if config.target_disk_format == disk_file_info["format"]:
  498. # target_disk_path = disk_path
  499. # else:
  500. # target_disk_path = (
  501. # "%s.%s" % (os.path.splitext(disk_path)[0],
  502. # config.target_disk_format))
  503. # utils.convert_disk_format(disk_path, target_disk_path,
  504. # config.target_disk_format)
  505. self._event_manager.progress_update(
  506. "Uploading Glance image")
  507. disk_format = disk_file_info["format"]
  508. image = self._create_image(
  509. glance, _get_unique_name(),
  510. disk_path, disk_format,
  511. config.container_format,
  512. config.hypervisor_type)
  513. images.append(image)
  514. self._event_manager.progress_update(
  515. "Waiting for Glance image to become active")
  516. _wait_for_image(nova, image.id)
  517. virtual_disk_size = disk_file_info["virtual-size"]
  518. if disk_format != constants.DISK_FORMAT_RAW:
  519. virtual_disk_size += DISK_HEADER_SIZE
  520. self._event_manager.progress_update(
  521. "Creating Cinder volume")
  522. volume = _create_volume(
  523. cinder, virtual_disk_size, _get_unique_name(), image.id)
  524. volumes.append(volume)
  525. return images, volumes
  526. def _create_neutron_ports(self, neutron, config, nics_info):
  527. ports = []
  528. for nic_info in nics_info:
  529. origin_network_name = nic_info.get("network_name")
  530. if not origin_network_name:
  531. self._warn("Origin network name not provided for for nic: "
  532. "%s, skipping", nic_info.get("name"))
  533. continue
  534. network_name = config.network_map.get(origin_network_name)
  535. if not network_name:
  536. raise exception.CoriolisException(
  537. "Network not mapped in network_map: %s" %
  538. origin_network_name)
  539. ports.append(self._create_neutron_port(
  540. neutron, network_name, nic_info.get("mac_address")))
  541. return ports
  542. @utils.retry_on_error()
  543. def _get_replica_volumes(self, cinder, volumes_info):
  544. volumes = []
  545. for volume_id in [v["volume_id"] for v in volumes_info]:
  546. volumes.append(cinder.volumes.get(volume_id))
  547. return volumes
  548. @utils.retry_on_error()
  549. def _rename_volumes(self, cinder, volumes, instance_name):
  550. for i, volume in enumerate(volumes):
  551. new_volume_name = VOLUME_NAME_FORMAT % {
  552. "instance_name": instance_name, "num": i + 1}
  553. cinder.volumes.update(volume.id, name=new_volume_name)
  554. @utils.retry_on_error()
  555. def _set_bootable_volumes(self, cinder, volumes):
  556. # TODO: check if just setting the first volume as bootable is enough
  557. for volume in volumes:
  558. if not volume.bootable or volume.bootable == 'false':
  559. cinder.volumes.set_bootable(volume, True)
  560. def _deploy_instance(self, ctxt, connection_info, target_environment,
  561. instance_name, export_info, volumes_info=None):
  562. session = keystone.create_keystone_session(ctxt, connection_info)
  563. glance_api_version = connection_info.get("image_api_version",
  564. GLANCE_API_VERSION)
  565. nova = nova_client.Client(NOVA_API_VERSION, session=session)
  566. glance = glance_client.Client(glance_api_version, session=session)
  567. neutron = neutron_client.Client(NEUTRON_API_VERSION, session=session)
  568. cinder = cinder_client.Client(CINDER_API_VERSION, session=session)
  569. os_type = export_info.get('os_type')
  570. LOG.info("os_type: %s", os_type)
  571. config = self._get_import_config(target_environment, os_type)
  572. images = []
  573. volumes = []
  574. ports = []
  575. try:
  576. if not volumes_info:
  577. # Migration
  578. disks_info = export_info["devices"]["disks"]
  579. images, volumes = self._create_images_and_volumes(
  580. glance, nova, cinder, config, disks_info)
  581. else:
  582. # Replica
  583. volumes = self._get_replica_volumes(cinder, volumes_info)
  584. migr_resources = self._deploy_migration_resources(
  585. nova, glance, neutron, os_type, config.migr_image_name,
  586. config.migr_flavor_name, config.migr_network_name,
  587. config.migr_fip_pool_name)
  588. nics_info = export_info["devices"].get("nics", [])
  589. try:
  590. for i, volume in enumerate(volumes):
  591. _wait_for_volume(cinder, volume.id)
  592. self._event_manager.progress_update(
  593. "Attaching volume to worker instance")
  594. self._attach_volume(
  595. nova, cinder, migr_resources.get_instance(), volume.id)
  596. conn_info = migr_resources.get_guest_connection_info()
  597. osmorphing_hv_type = self._get_osmorphing_hypervisor_type(
  598. config.hypervisor_type)
  599. self._event_manager.progress_update(
  600. "Preparing instance for target platform")
  601. osmorphing_manager.morph_image(conn_info,
  602. os_type,
  603. osmorphing_hv_type,
  604. constants.PLATFORM_OPENSTACK,
  605. nics_info,
  606. self._event_manager)
  607. finally:
  608. self._event_manager.progress_update(
  609. "Removing worker instance resources")
  610. migr_resources.delete()
  611. self._event_manager.progress_update("Renaming volumes")
  612. self._rename_volumes(cinder, volumes, instance_name)
  613. self._event_manager.progress_update(
  614. "Ensuring volumes are bootable")
  615. self._set_bootable_volumes(cinder, volumes)
  616. self._event_manager.progress_update(
  617. "Creating Neutron ports for migrated instance")
  618. ports = self._create_neutron_ports(neutron, config, nics_info)
  619. self._event_manager.progress_update(
  620. "Creating migrated instance")
  621. self._create_target_instance(
  622. nova, config.flavor_name, instance_name,
  623. config.keypair_name, ports, volumes)
  624. except Exception:
  625. if not volumes_info:
  626. # Don't remove replica volumes
  627. self._event_manager.progress_update("Deleting volumes")
  628. for volume in volumes:
  629. @utils.ignore_exceptions
  630. @utils.retry_on_error()
  631. def _del_volume():
  632. volume.delete()
  633. _del_volume()
  634. self._event_manager.progress_update("Deleting Neutron ports")
  635. for port in ports:
  636. @utils.ignore_exceptions
  637. @utils.retry_on_error()
  638. def _del_port():
  639. neutron.delete_port(port["id"])
  640. _del_port()
  641. raise
  642. finally:
  643. self._event_manager.progress_update("Deleting Glance images")
  644. for image in images:
  645. @utils.ignore_exceptions
  646. @utils.retry_on_error()
  647. def _del_image():
  648. image.delete()
  649. _del_image()
  650. def import_instance(self, ctxt, connection_info, target_environment,
  651. instance_name, export_info):
  652. self._deploy_instance(ctxt, connection_info, target_environment,
  653. instance_name, export_info)
  654. def _get_osmorphing_hypervisor_type(self, hypervisor_type):
  655. if (hypervisor_type and
  656. hypervisor_type.lower() == constants.HYPERVISOR_QEMU):
  657. return constants.HYPERVISOR_KVM
  658. elif hypervisor_type:
  659. return hypervisor_type.lower()
  660. @utils.retry_on_error(max_attempts=10, sleep_seconds=30)
  661. def _create_target_instance(self, nova, flavor_name, instance_name,
  662. keypair_name, ports, volumes):
  663. flavor = nova.flavors.find(name=flavor_name)
  664. block_device_mapping = {}
  665. for i, volume in enumerate(volumes):
  666. # Delete volume on termination
  667. block_device_mapping[
  668. 'vd%s' % chr(ord('a') + i)] = "%s:volume::True" % volume.id
  669. nics = [{'port-id': p['id']} for p in ports]
  670. # Note: Nova requires an image even when booting from volume
  671. LOG.info('Creating target instance...')
  672. instance = nova.servers.create(
  673. name=instance_name,
  674. image='',
  675. flavor=flavor,
  676. key_name=keypair_name,
  677. block_device_mapping=block_device_mapping,
  678. nics=nics)
  679. try:
  680. _wait_for_instance(nova, instance, 'ACTIVE')
  681. return instance
  682. except:
  683. if instance:
  684. nova.servers.delete(instance)
  685. raise
  686. def deploy_replica_instance(self, ctxt, connection_info,
  687. target_environment, instance_name, export_info,
  688. volumes_info):
  689. self._deploy_instance(ctxt, connection_info, target_environment,
  690. instance_name, export_info, volumes_info)
  691. def _update_existing_disk_volumes(self, cinder, disks_info, volumes_info):
  692. for disk_info in disks_info:
  693. disk_id = disk_info["id"]
  694. vi = [v for v in volumes_info
  695. if v["disk_id"] == disk_id and v.get("volume_id")]
  696. if vi:
  697. volume_info = vi[0]
  698. volume_id = volume_info["volume_id"]
  699. volume = _find_volume(cinder, volume_id)
  700. if volume:
  701. virtual_disk_size_gb = math.ceil(
  702. disk_info["size_bytes"] / units.Gi)
  703. if virtual_disk_size_gb > volume.size:
  704. LOG.info(
  705. "Extending volume %(volume_id)s. "
  706. "Current size: %(curr_size)s GB, "
  707. "Requested size: %(requested_size)s GB",
  708. {"volume_id": volume_id,
  709. "curr_size": virtual_disk_size_gb,
  710. "requested_size": volume.size})
  711. self._event_manager.progress_update("Extending volume")
  712. _extend_volume(
  713. cinder, volume_id, virtual_disk_size_gb * units.Gi)
  714. elif virtual_disk_size_gb < volume.size:
  715. LOG.warning(
  716. "Cannot shrink volume %(volume_id)s. "
  717. "Current size: %(curr_size)s GB, "
  718. "Requested size: %(requested_size)s GB",
  719. {"volume_id": volume_id,
  720. "curr_size": volume.size,
  721. "requested_size": virtual_disk_size_gb})
  722. else:
  723. volumes_info.remove(volume_info)
  724. return volumes_info
  725. def _delete_removed_disk_volumes(self, cinder, disks_info, volumes_info):
  726. for volume_info in volumes_info:
  727. if volume_info["disk_id"] not in [
  728. d["id"] for d in disks_info if d["id"]]:
  729. volume_id = volume_info["volume_id"]
  730. volume = _find_volume(cinder, volume_id)
  731. if volume:
  732. self._event_manager.progress_update("Deleting volume")
  733. _delete_volume(cinder, volume_id)
  734. volumes_info.remove(volume_info)
  735. return volumes_info
  736. def _create_new_disk_volumes(self, cinder, disks_info, volumes_info,
  737. instance_name):
  738. try:
  739. new_volumes = []
  740. for i, disk_info in enumerate(disks_info):
  741. disk_id = disk_info["id"]
  742. virtual_disk_size = disk_info["size_bytes"]
  743. if not [v for v in volumes_info if v["disk_id"] == disk_id]:
  744. self._event_manager.progress_update(
  745. "Creating volume")
  746. volume_name = REPLICA_VOLUME_NAME_FORMAT % {
  747. "instance_name": instance_name, "num": i + 1}
  748. volume = _create_volume(
  749. cinder, virtual_disk_size, volume_name)
  750. new_volumes.append(volume)
  751. volumes_info.append({
  752. "volume_id": volume.id,
  753. "disk_id": disk_id})
  754. else:
  755. self._event_manager.progress_update(
  756. "Using previously deployed volume")
  757. for volume in new_volumes:
  758. _wait_for_volume(cinder, volume.id)
  759. return volumes_info
  760. except:
  761. for volume in new_volumes:
  762. _delete_volume(cinder, volume.id)
  763. raise
  764. def deploy_replica_disks(self, ctxt, connection_info, target_environment,
  765. instance_name, export_info, volumes_info):
  766. session = keystone.create_keystone_session(ctxt, connection_info)
  767. cinder = cinder_client.Client(CINDER_API_VERSION, session=session)
  768. disks_info = export_info["devices"]["disks"]
  769. volumes_info = self._update_existing_disk_volumes(
  770. cinder, disks_info, volumes_info)
  771. volumes_info = self._delete_removed_disk_volumes(
  772. cinder, disks_info, volumes_info)
  773. volumes_info = self._create_new_disk_volumes(
  774. cinder, disks_info, volumes_info, instance_name)
  775. return volumes_info
  776. def deploy_replica_target_resources(self, ctxt, connection_info,
  777. target_environment, volumes_info):
  778. session = keystone.create_keystone_session(ctxt, connection_info)
  779. glance_api_version = connection_info.get("image_api_version",
  780. GLANCE_API_VERSION)
  781. nova = nova_client.Client(NOVA_API_VERSION, session=session)
  782. glance = glance_client.Client(glance_api_version, session=session)
  783. neutron = neutron_client.Client(NEUTRON_API_VERSION, session=session)
  784. cinder = cinder_client.Client(CINDER_API_VERSION, session=session)
  785. # Data migration uses a Linux guest binary
  786. os_type = constants.OS_TYPE_LINUX
  787. config = self._get_import_config(target_environment, os_type)
  788. migr_resources = self._deploy_migration_resources(
  789. nova, glance, neutron, os_type, config.migr_image_name,
  790. config.migr_flavor_name, config.migr_network_name,
  791. config.migr_fip_pool_name)
  792. try:
  793. for i, volume_info in enumerate(volumes_info):
  794. self._event_manager.progress_update(
  795. "Attaching volume to worker instance")
  796. volume_id = volume_info["volume_id"]
  797. ret_volume = self._attach_volume(
  798. nova, cinder, migr_resources.get_instance(), volume_id)
  799. volume_info["volume_dev"] = ret_volume.device
  800. return {
  801. "migr_resources": migr_resources.get_resources_dict(),
  802. "volumes_info": volumes_info,
  803. "connection_info": migr_resources.get_guest_connection_info(),
  804. }
  805. except:
  806. self._event_manager.progress_update(
  807. "Removing worker instance resources")
  808. migr_resources.delete()
  809. raise
  810. def delete_replica_target_resources(self, ctxt, connection_info,
  811. migr_resources_dict):
  812. session = keystone.create_keystone_session(ctxt, connection_info)
  813. nova = nova_client.Client(NOVA_API_VERSION, session=session)
  814. neutron = neutron_client.Client(NEUTRON_API_VERSION, session=session)
  815. migr_resources = _MigrationResources.from_resources_dict(
  816. nova, neutron, migr_resources_dict)
  817. self._event_manager.progress_update(
  818. "Removing worker instance resources")
  819. migr_resources.delete()
  820. def delete_replica_disks(self, ctxt, connection_info, volumes_info):
  821. session = keystone.create_keystone_session(ctxt, connection_info)
  822. cinder = cinder_client.Client(CINDER_API_VERSION, session=session)
  823. self._event_manager.progress_update(
  824. "Removing replica disk volumes")
  825. for volume_info in volumes_info:
  826. _delete_volume(cinder, volume_info["volume_id"])
  827. def create_replica_disk_snapshots(self, ctxt, connection_info,
  828. volumes_info):
  829. session = keystone.create_keystone_session(ctxt, connection_info)
  830. cinder = cinder_client.Client(CINDER_API_VERSION, session=session)
  831. snapshots = []
  832. self._event_manager.progress_update(
  833. "Creating replica disk snapshots")
  834. for volume_info in volumes_info:
  835. snapshot = _create_volume_snapshot(
  836. cinder, volume_info["volume_id"], _get_unique_name())
  837. snapshots.append(snapshot)
  838. volume_info["volume_snapshot_id"] = snapshot.id
  839. for snapshot in snapshots:
  840. _wait_for_volume_snapshot(cinder, snapshot.id)
  841. return volumes_info
  842. def delete_replica_disk_snapshots(self, ctxt, connection_info,
  843. volumes_info):
  844. session = keystone.create_keystone_session(ctxt, connection_info)
  845. cinder = cinder_client.Client(CINDER_API_VERSION, session=session)
  846. self._event_manager.progress_update(
  847. "Removing replica disk snapshots")
  848. for volume_info in volumes_info:
  849. snapshot_id = volume_info.get("volume_snapshot_id")
  850. if snapshot_id:
  851. _delete_volume_snapshot(cinder, snapshot_id)
  852. _wait_for_volume_snapshot(cinder, snapshot_id, 'deleted')
  853. volume_info["volume_snapshot_id"] = None
  854. def restore_replica_disk_snapshots(self, ctxt, connection_info,
  855. volumes_info):
  856. session = keystone.create_keystone_session(ctxt, connection_info)
  857. cinder = cinder_client.Client(CINDER_API_VERSION, session=session)
  858. self._event_manager.progress_update(
  859. "Restoring replica disk snapshots")
  860. new_volumes = []
  861. try:
  862. for volume_info in volumes_info:
  863. snapshot_id = volume_info.get("volume_snapshot_id")
  864. if snapshot_id:
  865. original_volume = _get_volume_from_snapshot(
  866. cinder, snapshot_id)
  867. volume_name = original_volume.name
  868. volume = _create_volume(
  869. cinder, None, volume_name, snapshot_id=snapshot_id)
  870. new_volumes.append((volume_info, snapshot_id, volume))
  871. for volume_info, snapshot_id, volume in new_volumes:
  872. old_volume_id = volume_info["volume_id"]
  873. _wait_for_volume(cinder, volume.id)
  874. _delete_volume_snapshot(cinder, snapshot_id)
  875. _wait_for_volume_snapshot(cinder, snapshot_id, 'deleted')
  876. _delete_volume(cinder, old_volume_id)
  877. volume_info["volume_id"] = volume.id
  878. volume_info["volume_snapshot_id"] = None
  879. except:
  880. for _, _, volume in new_volumes:
  881. _delete_volume(cinder, volume.id)
  882. raise
  883. return volumes_info
  884. class ExportProvider(base.BaseExportProvider):
  885. _OS_DISTRO_MAP = {
  886. 'windows': constants.OS_TYPE_WINDOWS,
  887. 'freebsd': constants.OS_TYPE_BSD,
  888. 'netbsd': constants.OS_TYPE_BSD,
  889. 'openbsd': constants.OS_TYPE_BSD,
  890. 'opensolaris': constants.OS_TYPE_SOLARIS,
  891. 'arch': constants.OS_TYPE_LINUX,
  892. 'centos': constants.OS_TYPE_LINUX,
  893. 'debian': constants.OS_TYPE_LINUX,
  894. 'fedora': constants.OS_TYPE_LINUX,
  895. 'gentoo': constants.OS_TYPE_LINUX,
  896. 'mandrake': constants.OS_TYPE_LINUX,
  897. 'mandriva': constants.OS_TYPE_LINUX,
  898. 'mes': constants.OS_TYPE_LINUX,
  899. 'opensuse': constants.OS_TYPE_LINUX,
  900. 'rhel': constants.OS_TYPE_LINUX,
  901. 'sled': constants.OS_TYPE_LINUX,
  902. 'ubuntu': constants.OS_TYPE_LINUX,
  903. }
  904. connection_info_schema = schemas.get_schema(
  905. __name__, schemas.PROVIDER_CONNECTION_INFO_SCHEMA_NAME)
  906. @utils.retry_on_error()
  907. def _get_instance(self, nova, instance_name):
  908. instances = nova.servers.list(search_opts={'name': instance_name})
  909. if len(instances) > 1:
  910. raise exception.CoriolisException(
  911. 'More than one instance exists with name: %s' % instance_name)
  912. elif not instances:
  913. raise exception.CoriolisException(
  914. 'Instance not found: %s' % instance_name)
  915. return instances[0]
  916. def _get_os_type(self, image):
  917. os_type = constants.OS_TYPE_LINUX
  918. os_distro = image.properties.get('os_distro')
  919. if not os_distro:
  920. if 'os_type' in image.properties:
  921. os_type = image.properties['os_type']
  922. else:
  923. self._event_manager.progress_update(
  924. "Image os_distro not set, defaulting to Linux")
  925. elif os_distro not in self._OS_DISTRO_MAP:
  926. self._event_manager.progress_update(
  927. "Image os_distro \"%s\" not found, defaulting to Linux" %
  928. os_distro)
  929. else:
  930. os_type = self._OS_DISTRO_MAP[os_distro]
  931. return os_type
  932. @utils.retry_on_error()
  933. def _export_image(self, glance, export_path, image_id):
  934. path = os.path.join(export_path, image_id)
  935. LOG.debug('Saving snapshot to path: %s', export_path)
  936. with open(path, 'wb') as f:
  937. for chunk in glance.images.data(image_id):
  938. f.write(chunk)
  939. disk_info = utils.get_disk_info(path)
  940. new_path = path + "." + disk_info['format']
  941. os.rename(path, new_path)
  942. LOG.debug('Renamed snapshot path: %s', new_path)
  943. return new_path, disk_info['format']
  944. @utils.retry_on_error()
  945. def _create_snapshot(self, nova, glance, instance, export_path):
  946. image_id = instance.create_image(_get_unique_name())
  947. try:
  948. image = glance.images.get(image_id)
  949. image_size = image.size
  950. if image.container_format != 'bare':
  951. raise exception.CoriolisException(
  952. "Unsupported container format: %s" %
  953. image.container_format)
  954. self._event_manager.progress_update(
  955. "Waiting for instance snapshot to complete")
  956. _wait_for_image(nova, image_id)
  957. self._event_manager.progress_update(
  958. "Exporting instance snapshot")
  959. image_path, image_format = self._export_image(
  960. glance, export_path, image_id)
  961. finally:
  962. self._event_manager.progress_update("Removing instance snapshot")
  963. @utils.ignore_exceptions
  964. @utils.retry_on_error()
  965. def _del_image():
  966. glance.images.delete(image_id)
  967. _del_image()
  968. os_type = self._get_os_type(image)
  969. return GlanceImage(
  970. id=image_id,
  971. path=image_path,
  972. format=image_format,
  973. os_type=os_type,
  974. size=image_size
  975. )
  976. def export_instance(self, ctxt, connection_info, instance_name,
  977. export_path):
  978. session = keystone.create_keystone_session(ctxt, connection_info)
  979. glance_api_version = connection_info.get("image_api_version",
  980. GLANCE_API_VERSION)
  981. nova = nova_client.Client(NOVA_API_VERSION, session=session)
  982. glance = glance_client.Client(glance_api_version, session=session)
  983. self._event_manager.progress_update("Retrieving OpenStack instance")
  984. instance = self._get_instance(nova, instance_name)
  985. @utils.retry_on_error()
  986. def _get_flavor():
  987. return nova.flavors.get(instance.flavor["id"])
  988. flavor = _get_flavor()
  989. nics = []
  990. for iface in instance.interface_list():
  991. ips = set([ip['ip_address'] for ip in iface.fixed_ips])
  992. net_name = [
  993. n for n, v in instance.networks.items() if set(v) & ips][0]
  994. nics.append({'name': iface.port_id,
  995. 'id': iface.port_id,
  996. 'mac_address': iface.mac_addr,
  997. 'ip_addresses': [ip[0] for ip in ips],
  998. 'network_id': iface.net_id,
  999. 'network_name': net_name})
  1000. if instance.status != 'SHUTOFF':
  1001. self._event_manager.progress_update(
  1002. "Shutting down instance")
  1003. @utils.retry_on_error()
  1004. def _stop_instance():
  1005. instance.stop()
  1006. _stop_instance()
  1007. _wait_for_instance(nova, instance, 'SHUTOFF')
  1008. self._event_manager.progress_update("Creating instance snapshot")
  1009. image = self._create_snapshot(
  1010. nova, glance, instance, export_path)
  1011. disks = []
  1012. disks.append({
  1013. 'format': image.format,
  1014. 'path': image.path,
  1015. 'size_bytes': image.size,
  1016. 'id': image.id
  1017. })
  1018. vm_info = {
  1019. 'num_cpu': flavor.vcpus,
  1020. 'num_cores_per_socket': 1,
  1021. 'memory_mb': flavor.ram,
  1022. 'nested_virtualization': False,
  1023. 'name': instance_name,
  1024. 'os_type': image.os_type,
  1025. 'id': instance.id,
  1026. 'flavor_name': flavor.name,
  1027. 'devices': {
  1028. "nics": nics,
  1029. "disks": disks,
  1030. "cdroms": [],
  1031. "serial_ports": [],
  1032. "floppies": [],
  1033. "controllers": []
  1034. }
  1035. }
  1036. LOG.info("vm info: %s" % str(vm_info))
  1037. return vm_info