services.py 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082
  1. import base64
  2. import logging
  3. import uuid
  4. from azure.common import AzureException
  5. from azure.mgmt.compute.models import DiskCreateOption
  6. from msrestazure.azure_exceptions import CloudError
  7. import cloudbridge.cloud.base.helpers as cb_helpers
  8. from cloudbridge.cloud.base.resources import ClientPagedResultList, \
  9. ServerPagedResultList
  10. from cloudbridge.cloud.base.services import BaseBucketService, \
  11. BaseComputeService, \
  12. BaseImageService, BaseInstanceService, BaseKeyPairService, \
  13. BaseNetworkService, BaseNetworkingService, BaseRegionService, \
  14. BaseRouterService, BaseSecurityService, BaseSnapshotService, \
  15. BaseStorageService, BaseSubnetService, BaseVMFirewallService, \
  16. BaseVMTypeService, BaseVolumeService
  17. from cloudbridge.cloud.interfaces.exceptions import \
  18. DuplicateResourceException, InvalidValueException
  19. from cloudbridge.cloud.interfaces.resources import MachineImage, \
  20. Network, PlacementZone, Snapshot, Subnet, VMFirewall, VMType, Volume
  21. from . import helpers as azure_helpers
  22. from .resources import AzureBucket, \
  23. AzureInstance, AzureKeyPair, \
  24. AzureLaunchConfig, AzureMachineImage, AzureNetwork, \
  25. AzureRegion, AzureRouter, AzureSnapshot, AzureSubnet, \
  26. AzureVMFirewall, AzureVMType, AzureVolume
  27. log = logging.getLogger(__name__)
  28. class AzureSecurityService(BaseSecurityService):
  29. def __init__(self, provider):
  30. super(AzureSecurityService, self).__init__(provider)
  31. # Initialize provider services
  32. self._key_pairs = AzureKeyPairService(provider)
  33. self._vm_firewalls = AzureVMFirewallService(provider)
  34. @property
  35. def key_pairs(self):
  36. return self._key_pairs
  37. @property
  38. def vm_firewalls(self):
  39. return self._vm_firewalls
  40. class AzureVMFirewallService(BaseVMFirewallService):
  41. def __init__(self, provider):
  42. super(AzureVMFirewallService, self).__init__(provider)
  43. def get(self, fw_id):
  44. try:
  45. fws = self.provider.azure_client.get_vm_firewall(fw_id)
  46. return AzureVMFirewall(self.provider, fws)
  47. except (CloudError, InvalidValueException) as cloud_error:
  48. # Azure raises the cloud error if the resource not available
  49. log.exception(cloud_error)
  50. return None
  51. def list(self, limit=None, marker=None):
  52. fws = [AzureVMFirewall(self.provider, fw)
  53. for fw in self.provider.azure_client.list_vm_firewall()]
  54. return ClientPagedResultList(self.provider, fws, limit, marker)
  55. def create(self, name, description, network_id=None):
  56. AzureVMFirewall.assert_valid_resource_name(name)
  57. parameters = {"location": self.provider.region_name,
  58. 'tags': {'Name': name}}
  59. if description:
  60. parameters['tags'].update(Description=description)
  61. fw = self.provider.azure_client.create_vm_firewall(name, parameters)
  62. # Add default rules to negate azure default rules.
  63. # See: https://github.com/CloudVE/cloudbridge/issues/106
  64. # pylint:disable=protected-access
  65. for rule in fw.default_security_rules:
  66. rule_name = "cb-override-" + rule.name
  67. # Transpose rules to priority 4001 onwards, because
  68. # only 0-4096 are allowed for custom rules
  69. rule.priority = rule.priority - 61440
  70. rule.access = "Deny"
  71. self._provider.azure_client.create_vm_firewall_rule(
  72. fw.id, rule_name, rule)
  73. # Add a new custom rule allowing all outbound traffic to the internet
  74. parameters = {"priority": 3000,
  75. "protocol": "*",
  76. "source_port_range": "*",
  77. "source_address_prefix": "*",
  78. "destination_port_range": "*",
  79. "destination_address_prefix": "Internet",
  80. "access": "Allow",
  81. "direction": "Outbound"}
  82. result = self._provider.azure_client.create_vm_firewall_rule(
  83. fw.id, "cb-default-internet-outbound", parameters)
  84. fw.security_rules.append(result)
  85. cb_fw = AzureVMFirewall(self.provider, fw)
  86. return cb_fw
  87. def find(self, **kwargs):
  88. name = kwargs.pop('name', None)
  89. # All kwargs should have been popped at this time.
  90. if len(kwargs) > 0:
  91. raise TypeError("Unrecognised parameters for search: %s."
  92. " Supported attributes: %s" % (kwargs, 'name'))
  93. filters = {'Name': name}
  94. fws = [AzureVMFirewall(self.provider, vm_firewall)
  95. for vm_firewall in azure_helpers.filter_by_tag(
  96. self.provider.azure_client.list_vm_firewall(), filters)]
  97. return ClientPagedResultList(self.provider, fws)
  98. def delete(self, group_id):
  99. self.provider.azure_client.delete_vm_firewall(group_id)
  100. class AzureKeyPairService(BaseKeyPairService):
  101. PARTITION_KEY = '00000000-0000-0000-0000-000000000000'
  102. def __init__(self, provider):
  103. super(AzureKeyPairService, self).__init__(provider)
  104. def get(self, key_pair_id):
  105. try:
  106. key_pair = self.provider.azure_client.\
  107. get_public_key(key_pair_id)
  108. if key_pair:
  109. return AzureKeyPair(self.provider, key_pair)
  110. return None
  111. except AzureException as error:
  112. log.exception(error)
  113. return None
  114. def list(self, limit=None, marker=None):
  115. key_pairs, resume_marker = self.provider.azure_client.list_public_keys(
  116. AzureKeyPairService.PARTITION_KEY, marker=marker,
  117. limit=limit or self.provider.config.default_result_limit)
  118. results = [AzureKeyPair(self.provider, key_pair)
  119. for key_pair in key_pairs]
  120. return ServerPagedResultList(is_truncated=resume_marker,
  121. marker=resume_marker,
  122. supports_total=False,
  123. data=results)
  124. def find(self, **kwargs):
  125. name = kwargs.pop('name', None)
  126. # All kwargs should have been popped at this time.
  127. if len(kwargs) > 0:
  128. raise TypeError("Unrecognised parameters for search: %s."
  129. " Supported attributes: %s" % (kwargs, 'name'))
  130. key_pair = self.get(name)
  131. return ClientPagedResultList(self.provider,
  132. [key_pair] if key_pair else [])
  133. def create(self, name, public_key_material=None):
  134. AzureKeyPair.assert_valid_resource_name(name)
  135. key_pair = self.get(name)
  136. if key_pair:
  137. raise DuplicateResourceException(
  138. 'Keypair already exists with name {0}'.format(name))
  139. private_key = None
  140. if not public_key_material:
  141. public_key_material, private_key = cb_helpers.generate_key_pair()
  142. entity = {
  143. 'PartitionKey': AzureKeyPairService.PARTITION_KEY,
  144. 'RowKey': str(uuid.uuid4()),
  145. 'Name': name,
  146. 'Key': public_key_material
  147. }
  148. self.provider.azure_client.create_public_key(entity)
  149. key_pair = self.get(name)
  150. key_pair.material = private_key
  151. return key_pair
  152. class AzureBucketService(BaseBucketService):
  153. def __init__(self, provider):
  154. super(AzureBucketService, self).__init__(provider)
  155. def get(self, bucket_id):
  156. """
  157. Returns a bucket given its ID. Returns ``None`` if the bucket
  158. does not exist.
  159. """
  160. try:
  161. bucket = self.provider.azure_client.get_container(bucket_id)
  162. return AzureBucket(self.provider, bucket)
  163. except AzureException as error:
  164. log.exception(error)
  165. return None
  166. def find(self, **kwargs):
  167. name = kwargs.pop('name', None)
  168. # All kwargs should have been popped at this time.
  169. if len(kwargs) > 0:
  170. raise TypeError("Unrecognised parameters for search: %s."
  171. " Supported attributes: %s" % (kwargs, 'name'))
  172. buckets = [AzureBucket(self.provider, bucket)
  173. for bucket in
  174. self.provider.azure_client.list_containers(prefix=name)]
  175. return ClientPagedResultList(self.provider, buckets)
  176. def list(self, limit=None, marker=None):
  177. """
  178. List all containers.
  179. """
  180. buckets = [AzureBucket(self.provider, bucket)
  181. for bucket in self.provider.azure_client.list_containers()]
  182. return ClientPagedResultList(self.provider, buckets,
  183. limit=limit, marker=marker)
  184. def create(self, name, location=None):
  185. """
  186. Create a new bucket.
  187. """
  188. AzureBucket.assert_valid_resource_name(name)
  189. bucket = self.provider.azure_client.create_container(name.lower())
  190. return AzureBucket(self.provider, bucket)
  191. class AzureStorageService(BaseStorageService):
  192. def __init__(self, provider):
  193. super(AzureStorageService, self).__init__(provider)
  194. # Initialize provider services
  195. self._volume_svc = AzureVolumeService(self.provider)
  196. self._snapshot_svc = AzureSnapshotService(self.provider)
  197. self._bucket_svc = AzureBucketService(self.provider)
  198. @property
  199. def volumes(self):
  200. return self._volume_svc
  201. @property
  202. def snapshots(self):
  203. return self._snapshot_svc
  204. @property
  205. def buckets(self):
  206. return self._bucket_svc
  207. class AzureVolumeService(BaseVolumeService):
  208. def __init__(self, provider):
  209. super(AzureVolumeService, self).__init__(provider)
  210. def get(self, volume_id):
  211. """
  212. Returns a volume given its id.
  213. """
  214. try:
  215. volume = self.provider.azure_client.get_disk(volume_id)
  216. return AzureVolume(self.provider, volume)
  217. except (CloudError, InvalidValueException) as cloud_error:
  218. # Azure raises the cloud error if the resource not available
  219. log.exception(cloud_error)
  220. return None
  221. def find(self, **kwargs):
  222. name = kwargs.pop('name', None)
  223. # All kwargs should have been popped at this time.
  224. if len(kwargs) > 0:
  225. raise TypeError("Unrecognised parameters for search: %s."
  226. " Supported attributes: %s" % (kwargs, 'name'))
  227. filters = {'Name': name}
  228. cb_vols = [AzureVolume(self.provider, volume)
  229. for volume in azure_helpers.filter_by_tag(
  230. self.provider.azure_client.list_disks(), filters)]
  231. return ClientPagedResultList(self.provider, cb_vols)
  232. def list(self, limit=None, marker=None):
  233. """
  234. List all volumes.
  235. """
  236. azure_vols = self.provider.azure_client.list_disks()
  237. cb_vols = [AzureVolume(self.provider, vol) for vol in azure_vols]
  238. return ClientPagedResultList(self.provider, cb_vols,
  239. limit=limit, marker=marker)
  240. def create(self, name, size, zone=None, snapshot=None, description=None):
  241. """
  242. Creates a new volume.
  243. """
  244. AzureVolume.assert_valid_resource_name(name)
  245. zone_id = zone.id if isinstance(zone, PlacementZone) else zone
  246. snapshot = (self.provider.storage.snapshots.get(snapshot)
  247. if snapshot and isinstance(snapshot, str) else snapshot)
  248. disk_name = "{0}-{1}".format(name, uuid.uuid4().hex[:6])
  249. tags = {'Name': name}
  250. if description:
  251. tags.update(Description=description)
  252. if snapshot:
  253. params = {
  254. 'location':
  255. zone_id or self.provider.azure_client.region_name,
  256. 'creation_data': {
  257. 'create_option': DiskCreateOption.copy,
  258. 'source_uri': snapshot.resource_id
  259. },
  260. 'tags': tags
  261. }
  262. disk = self.provider.azure_client.create_snapshot_disk(disk_name,
  263. params)
  264. else:
  265. params = {
  266. 'location':
  267. zone_id or self.provider.region_name,
  268. 'disk_size_gb': size,
  269. 'creation_data': {
  270. 'create_option': DiskCreateOption.empty
  271. },
  272. 'tags': tags}
  273. disk = self.provider.azure_client.create_empty_disk(disk_name,
  274. params)
  275. azure_vol = self.provider.azure_client.get_disk(disk.id)
  276. cb_vol = AzureVolume(self.provider, azure_vol)
  277. return cb_vol
  278. class AzureSnapshotService(BaseSnapshotService):
  279. def __init__(self, provider):
  280. super(AzureSnapshotService, self).__init__(provider)
  281. def get(self, ss_id):
  282. """
  283. Returns a snapshot given its id.
  284. """
  285. try:
  286. snapshot = self.provider.azure_client.get_snapshot(ss_id)
  287. return AzureSnapshot(self.provider, snapshot)
  288. except (CloudError, InvalidValueException) as cloud_error:
  289. # Azure raises the cloud error if the resource not available
  290. log.exception(cloud_error)
  291. return None
  292. def find(self, **kwargs):
  293. name = kwargs.pop('name', None)
  294. # All kwargs should have been popped at this time.
  295. if len(kwargs) > 0:
  296. raise TypeError("Unrecognised parameters for search: %s."
  297. " Supported attributes: %s" % (kwargs, 'name'))
  298. filters = {'Name': name}
  299. cb_snapshots = [AzureSnapshot(self.provider, snapshot)
  300. for snapshot in azure_helpers.filter_by_tag(
  301. self.provider.azure_client.list_snapshots(), filters)]
  302. return ClientPagedResultList(self.provider, cb_snapshots)
  303. def list(self, limit=None, marker=None):
  304. """
  305. List all snapshots.
  306. """
  307. snaps = [AzureSnapshot(self.provider, obj)
  308. for obj in
  309. self.provider.azure_client.list_snapshots()]
  310. return ClientPagedResultList(self.provider, snaps, limit, marker)
  311. def create(self, name, volume, description=None):
  312. """
  313. Creates a new snapshot of a given volume.
  314. """
  315. AzureSnapshot.assert_valid_resource_name(name)
  316. volume = (self.provider.storage.volumes.get(volume)
  317. if isinstance(volume, str) else volume)
  318. tags = {'Name': name}
  319. snapshot_name = "{0}-{1}".format(name, uuid.uuid4().hex[:6])
  320. if description:
  321. tags.update(Description=description)
  322. params = {
  323. 'location': self.provider.azure_client.region_name,
  324. 'creation_data': {
  325. 'create_option': DiskCreateOption.copy,
  326. 'source_uri': volume.resource_id
  327. },
  328. 'disk_size_gb': volume.size,
  329. 'tags': tags
  330. }
  331. azure_snap = self.provider.azure_client.create_snapshot(snapshot_name,
  332. params)
  333. return AzureSnapshot(self.provider, azure_snap)
  334. class AzureComputeService(BaseComputeService):
  335. def __init__(self, provider):
  336. super(AzureComputeService, self).__init__(provider)
  337. self._vm_type_svc = AzureVMTypeService(self.provider)
  338. self._instance_svc = AzureInstanceService(self.provider)
  339. self._region_svc = AzureRegionService(self.provider)
  340. self._images_svc = AzureImageService(self.provider)
  341. @property
  342. def images(self):
  343. return self._images_svc
  344. @property
  345. def vm_types(self):
  346. return self._vm_type_svc
  347. @property
  348. def instances(self):
  349. return self._instance_svc
  350. @property
  351. def regions(self):
  352. return self._region_svc
  353. class AzureInstanceService(BaseInstanceService):
  354. def __init__(self, provider):
  355. super(AzureInstanceService, self).__init__(provider)
  356. def create(self, name, image, vm_type, subnet=None, zone=None,
  357. key_pair=None, vm_firewalls=None, user_data=None,
  358. launch_config=None, **kwargs):
  359. instance_name = name.replace("_", "-") if name \
  360. else "{0} - {1}".format("cb", uuid.uuid4())
  361. AzureInstance.assert_valid_resource_name(instance_name)
  362. # Key_pair is mandatory in azure and it should not be None.
  363. temp_key_pair = None
  364. if key_pair:
  365. key_pair = (self.provider.security.key_pairs.get(key_pair)
  366. if isinstance(key_pair, str) else key_pair)
  367. else:
  368. # Create a temporary keypair if none is provided to keep Azure
  369. # happy, but the private key will be discarded, so it'll be all
  370. # but useless. However, this will allow an instance to be launched
  371. # without specifying a keypair, so users may still be able to login
  372. # if they have a preinstalled keypair/password baked into the image
  373. default_kp_name = "cb_default_key_pair"
  374. default_kp = self.provider.security.key_pairs.find(
  375. name=default_kp_name)
  376. if default_kp:
  377. key_pair = default_kp[0]
  378. else:
  379. key_pair = self.provider.security.key_pairs.create(
  380. name=default_kp_name)
  381. temp_key_pair = key_pair
  382. image = (image if isinstance(image, AzureMachineImage) else
  383. self.provider.compute.images.get(image))
  384. if not isinstance(image, AzureMachineImage):
  385. raise Exception("Provided image %s is not a valid azure image"
  386. % image)
  387. instance_size = vm_type.id if \
  388. isinstance(vm_type, VMType) else vm_type
  389. if not subnet:
  390. # Azure has only a single zone per region; use the current one
  391. zone = self.provider.compute.regions.get(
  392. self.provider.region_name).zones[0]
  393. subnet = self.provider.networking.subnets.get_or_create_default(
  394. zone)
  395. else:
  396. subnet = (self.provider.networking.subnets.get(subnet)
  397. if isinstance(subnet, str) else subnet)
  398. zone_id = zone.id if isinstance(zone, PlacementZone) else zone
  399. subnet_id, zone_id, vm_firewall_id = \
  400. self._resolve_launch_options(instance_name,
  401. subnet, zone_id, vm_firewalls)
  402. storage_profile = self._create_storage_profile(image, launch_config,
  403. instance_name, zone_id)
  404. nic_params = {
  405. 'location': self._provider.region_name,
  406. 'ip_configurations': [{
  407. 'name': instance_name + '_ip_config',
  408. 'private_ip_allocation_method': 'Dynamic',
  409. 'subnet': {
  410. 'id': subnet_id
  411. }
  412. }]
  413. }
  414. if vm_firewall_id:
  415. nic_params['network_security_group'] = {
  416. 'id': vm_firewall_id
  417. }
  418. nic_info = self.provider.azure_client.create_nic(
  419. instance_name + '_nic',
  420. nic_params
  421. )
  422. # #! indicates shell script
  423. ud = '#cloud-config\n' + user_data \
  424. if user_data and not user_data.startswith('#!')\
  425. and not user_data.startswith('#cloud-config') else user_data
  426. params = {
  427. 'location': zone_id or self._provider.region_name,
  428. 'os_profile': {
  429. 'admin_username': self.provider.vm_default_user_name,
  430. 'computer_name': instance_name,
  431. 'linux_configuration': {
  432. "disable_password_authentication": True,
  433. "ssh": {
  434. "public_keys": [{
  435. "path":
  436. "/home/{}/.ssh/authorized_keys".format(
  437. self.provider.vm_default_user_name),
  438. "key_data": key_pair._key_pair.Key
  439. }]
  440. }
  441. }
  442. },
  443. 'hardware_profile': {
  444. 'vm_size': instance_size
  445. },
  446. 'network_profile': {
  447. 'network_interfaces': [{
  448. 'id': nic_info.id
  449. }]
  450. },
  451. 'storage_profile': storage_profile,
  452. 'tags': {'Name': name}
  453. }
  454. if key_pair:
  455. params['tags'].update(Key_Pair=key_pair.name)
  456. for disk_def in storage_profile.get('data_disks', []):
  457. params['tags'] = dict(disk_def.get('tags', {}), **params['tags'])
  458. if user_data:
  459. custom_data = base64.b64encode(bytes(ud, 'utf-8'))
  460. params['os_profile']['custom_data'] = str(custom_data, 'utf-8')
  461. try:
  462. vm = self.provider.azure_client.create_vm(instance_name, params)
  463. except Exception as e:
  464. # If VM creation fails, attempt to clean up intermediary resources
  465. self.provider.azure_client.delete_nic(nic_info.id)
  466. for disk_def in storage_profile.get('data_disks', []):
  467. if disk_def.get('tags', {}).get('delete_on_terminate'):
  468. disk_id = disk_def.get('managed_disk', {}).get('id')
  469. if disk_id:
  470. vol = self.provider.storage.volumes.get(disk_id)
  471. vol.delete()
  472. raise e
  473. finally:
  474. if temp_key_pair:
  475. temp_key_pair.delete()
  476. return AzureInstance(self.provider, vm)
  477. def _resolve_launch_options(self, name, subnet=None, zone_id=None,
  478. vm_firewalls=None):
  479. if subnet:
  480. # subnet's zone takes precedence
  481. zone_id = subnet.zone.id
  482. vm_firewall_id = None
  483. if isinstance(vm_firewalls, list) and len(vm_firewalls) > 0:
  484. if isinstance(vm_firewalls[0], VMFirewall):
  485. vm_firewalls_ids = [fw.id for fw in vm_firewalls]
  486. vm_firewall_id = vm_firewalls[0].resource_id
  487. else:
  488. vm_firewalls_ids = vm_firewalls
  489. vm_firewall = self.provider.security.\
  490. vm_firewalls.get(vm_firewalls[0])
  491. vm_firewall_id = vm_firewall.resource_id
  492. if len(vm_firewalls) > 1:
  493. new_fw = self.provider.security.vm_firewalls.\
  494. create('{0}-fw'.format(name), 'Merge vm firewall {0}'.
  495. format(','.join(vm_firewalls_ids)))
  496. for fw in vm_firewalls:
  497. new_fw.add_rule(src_dest_fw=fw)
  498. vm_firewall_id = new_fw.resource_id
  499. return subnet.resource_id, zone_id, vm_firewall_id
  500. def _create_storage_profile(self, image, launch_config, instance_name,
  501. zone_id):
  502. if image.is_gallery_image:
  503. reference = image._image.as_dict()
  504. image_ref = {
  505. 'publisher': reference['publisher'],
  506. 'offer': reference['offer'],
  507. 'sku': reference['sku'],
  508. 'version': reference['version']
  509. }
  510. else:
  511. image_ref = {
  512. 'id': image.resource_id
  513. }
  514. storage_profile = {
  515. 'image_reference': image_ref,
  516. "os_disk": {
  517. "name": instance_name + '_os_disk',
  518. "create_option": DiskCreateOption.from_image
  519. },
  520. }
  521. if launch_config:
  522. data_disks, root_disk_size = self._process_block_device_mappings(
  523. launch_config, instance_name, zone_id)
  524. if data_disks:
  525. storage_profile['data_disks'] = data_disks
  526. if root_disk_size:
  527. storage_profile['os_disk']['disk_size_gb'] = root_disk_size
  528. return storage_profile
  529. def _process_block_device_mappings(self, launch_config,
  530. vm_name, zone=None):
  531. """
  532. Processes block device mapping information
  533. and returns a Data disk dictionary list. If new volumes
  534. are requested (source is None and destination is VOLUME), they will be
  535. created and the relevant volume ids included in the mapping.
  536. """
  537. data_disks = []
  538. root_disk_size = None
  539. def append_disk(disk_def, device_no, delete_on_terminate):
  540. # In azure, there is no option to specify terminate disks
  541. # (similar to AWS delete_on_terminate) on VM delete.
  542. # This method uses the azure tags functionality to store
  543. # the delete_on_terminate option when the virtual machine
  544. # is deleted, we parse the tags and delete accordingly
  545. disk_def['lun'] = device_no
  546. disk_def['tags'] = {
  547. 'delete_on_terminate': delete_on_terminate
  548. }
  549. data_disks.append(disk_def)
  550. for device_no, device in enumerate(launch_config.block_devices):
  551. if device.is_volume:
  552. if device.is_root:
  553. root_disk_size = device.size
  554. else:
  555. # In azure, os disk automatically created,
  556. # we are ignoring the root disk, if specified
  557. if isinstance(device.source, Snapshot):
  558. snapshot_vol = device.source.create_volume()
  559. disk_def = {
  560. # pylint:disable=protected-access
  561. 'name': snapshot_vol._volume.name,
  562. 'create_option': DiskCreateOption.attach,
  563. 'managed_disk': {
  564. 'id': snapshot_vol.id
  565. }
  566. }
  567. elif isinstance(device.source, Volume):
  568. disk_def = {
  569. # pylint:disable=protected-access
  570. 'name': device.source._volume.name,
  571. 'create_option': DiskCreateOption.attach,
  572. 'managed_disk': {
  573. 'id': device.source.id
  574. }
  575. }
  576. elif isinstance(device.source, MachineImage):
  577. disk_def = {
  578. # pylint:disable=protected-access
  579. 'name': device.source._volume.name,
  580. 'create_option': DiskCreateOption.from_image,
  581. 'source_resource_id': device.source.id
  582. }
  583. else:
  584. disk_def = {
  585. # pylint:disable=protected-access
  586. 'create_option': DiskCreateOption.empty,
  587. 'disk_size_gb': device.size
  588. }
  589. append_disk(disk_def, device_no,
  590. device.delete_on_terminate)
  591. else: # device is ephemeral
  592. # in azure we cannot add the ephemeral disks explicitly
  593. pass
  594. return data_disks, root_disk_size
  595. def create_launch_config(self):
  596. return AzureLaunchConfig(self.provider)
  597. def list(self, limit=None, marker=None):
  598. """
  599. List all instances.
  600. """
  601. instances = [AzureInstance(self.provider, inst)
  602. for inst in self.provider.azure_client.list_vm()]
  603. return ClientPagedResultList(self.provider, instances,
  604. limit=limit, marker=marker)
  605. def get(self, instance_id):
  606. """
  607. Returns an instance given its id. Returns None
  608. if the object does not exist.
  609. """
  610. try:
  611. vm = self.provider.azure_client.get_vm(instance_id)
  612. return AzureInstance(self.provider, vm)
  613. except (CloudError, InvalidValueException) as cloud_error:
  614. # Azure raises the cloud error if the resource not available
  615. log.exception(cloud_error)
  616. return None
  617. def find(self, **kwargs):
  618. name = kwargs.pop('name', None)
  619. # All kwargs should have been popped at this time.
  620. if len(kwargs) > 0:
  621. raise TypeError("Unrecognised parameters for search: %s."
  622. " Supported attributes: %s" % (kwargs, 'name'))
  623. filtr = {'Name': name}
  624. instances = [AzureInstance(self.provider, inst)
  625. for inst in azure_helpers.filter_by_tag(
  626. self.provider.azure_client.list_vm(), filtr)]
  627. return ClientPagedResultList(self.provider, instances)
  628. class AzureImageService(BaseImageService):
  629. def __init__(self, provider):
  630. super(AzureImageService, self).__init__(provider)
  631. def get(self, image_id):
  632. """
  633. Returns an Image given its id
  634. """
  635. try:
  636. image = self.provider.azure_client.get_image(image_id)
  637. return AzureMachineImage(self.provider, image)
  638. except (CloudError, InvalidValueException) as cloud_error:
  639. # Azure raises the cloud error if the resource not available
  640. log.exception(cloud_error)
  641. return None
  642. def find(self, **kwargs):
  643. name = kwargs.pop('name', None)
  644. # All kwargs should have been popped at this time.
  645. if len(kwargs) > 0:
  646. raise TypeError("Unrecognised parameters for search: %s."
  647. " Supported attributes: %s" % (kwargs, 'name'))
  648. filters = {'Name': name}
  649. cb_images = [AzureMachineImage(self.provider, image)
  650. for image in azure_helpers.filter_by_tag(
  651. self.provider.azure_client.list_images(), filters)]
  652. # All gallery image properties (id, resource_id, name) are the URN
  653. # Improvement: wrap the filters by publisher, offer, etc...
  654. cb_images.extend([AzureMachineImage(self.provider, image) for image
  655. in self.provider.azure_client.list_gallery_refs()
  656. if azure_helpers.generate_urn(image) == name])
  657. return ClientPagedResultList(self.provider, cb_images)
  658. def list(self, limit=None, marker=None):
  659. """
  660. List all images.
  661. """
  662. azure_images = self.provider.azure_client.list_images()
  663. azure_gallery_refs = self.provider.azure_client.list_gallery_refs()
  664. cb_images = [AzureMachineImage(self.provider, img)
  665. for img in azure_images + azure_gallery_refs]
  666. return ClientPagedResultList(self.provider, cb_images,
  667. limit=limit, marker=marker)
  668. class AzureVMTypeService(BaseVMTypeService):
  669. def __init__(self, provider):
  670. super(AzureVMTypeService, self).__init__(provider)
  671. @property
  672. def instance_data(self):
  673. """
  674. Fetch info about the available instances.
  675. """
  676. r = self.provider.azure_client.list_vm_types()
  677. return r
  678. def list(self, limit=None, marker=None):
  679. vm_types = [AzureVMType(self.provider, vm_type)
  680. for vm_type in self.instance_data]
  681. return ClientPagedResultList(self.provider, vm_types,
  682. limit=limit, marker=marker)
  683. class AzureNetworkingService(BaseNetworkingService):
  684. def __init__(self, provider):
  685. super(AzureNetworkingService, self).__init__(provider)
  686. self._network_service = AzureNetworkService(self.provider)
  687. self._subnet_service = AzureSubnetService(self.provider)
  688. self._router_service = AzureRouterService(self.provider)
  689. @property
  690. def networks(self):
  691. return self._network_service
  692. @property
  693. def subnets(self):
  694. return self._subnet_service
  695. @property
  696. def routers(self):
  697. return self._router_service
  698. class AzureNetworkService(BaseNetworkService):
  699. def __init__(self, provider):
  700. super(AzureNetworkService, self).__init__(provider)
  701. def get(self, network_id):
  702. try:
  703. network = self.provider.azure_client.get_network(network_id)
  704. return AzureNetwork(self.provider, network)
  705. except (CloudError, InvalidValueException) as cloud_error:
  706. # Azure raises the cloud error if the resource not available
  707. log.exception(cloud_error)
  708. return None
  709. def list(self, limit=None, marker=None):
  710. """
  711. List all networks.
  712. """
  713. networks = [AzureNetwork(self.provider, network)
  714. for network in self.provider.azure_client.list_networks()]
  715. return ClientPagedResultList(self.provider, networks,
  716. limit=limit, marker=marker)
  717. def find(self, **kwargs):
  718. name = kwargs.pop('name', None)
  719. # All kwargs should have been popped at this time.
  720. if len(kwargs) > 0:
  721. raise TypeError("Unrecognised parameters for search: %s."
  722. " Supported attributes: %s" % (kwargs, 'name'))
  723. filters = {'Name': name}
  724. networks = [
  725. AzureNetwork(self.provider, network) for network
  726. in azure_helpers.filter_by_tag(
  727. self.provider.azure_client.list_networks(), filters)]
  728. return ClientPagedResultList(self.provider, networks)
  729. def create(self, name, cidr_block):
  730. # Azure requires CIDR block to be specified when creating a network
  731. # so set a default one and use the largest allowed netmask.
  732. network_name = AzureNetwork.CB_DEFAULT_NETWORK_NAME
  733. if name:
  734. network_name = "{0}-{1}".format(name, uuid.uuid4().hex[:6])
  735. AzureNetwork.assert_valid_resource_name(network_name)
  736. params = {
  737. 'location': self.provider.azure_client.region_name,
  738. 'address_space': {
  739. 'address_prefixes': [cidr_block]
  740. },
  741. 'tags': {'Name': name or AzureNetwork.CB_DEFAULT_NETWORK_NAME}
  742. }
  743. az_network = self.provider.azure_client.create_network(network_name,
  744. params)
  745. cb_network = AzureNetwork(self.provider, az_network)
  746. return cb_network
  747. def delete(self, network_id):
  748. """
  749. Delete an existing network.
  750. """
  751. self.provider.azure_client.delete_network(network_id)
  752. class AzureRegionService(BaseRegionService):
  753. def __init__(self, provider):
  754. super(AzureRegionService, self).__init__(provider)
  755. def get(self, region_id):
  756. region = None
  757. for azureRegion in self.provider.azure_client.list_locations():
  758. if azureRegion.name == region_id:
  759. region = AzureRegion(self.provider, azureRegion)
  760. break
  761. return region
  762. def list(self, limit=None, marker=None):
  763. regions = [AzureRegion(self.provider, region)
  764. for region in self.provider.azure_client.list_locations()]
  765. return ClientPagedResultList(self.provider, regions,
  766. limit=limit, marker=marker)
  767. @property
  768. def current(self):
  769. return self.get(self.provider.region_name)
  770. class AzureSubnetService(BaseSubnetService):
  771. def __init__(self, provider):
  772. super(AzureSubnetService, self).__init__(provider)
  773. def get(self, subnet_id):
  774. """
  775. Azure does not provide an api to get the subnet directly by id.
  776. It also requires the network id.
  777. To make it consistent across the providers the following code
  778. gets the specific code from the subnet list.
  779. :param subnet_id:
  780. :return:
  781. """
  782. try:
  783. azure_subnet = self.provider.azure_client.get_subnet(subnet_id)
  784. return AzureSubnet(self.provider,
  785. azure_subnet) if azure_subnet else None
  786. except (CloudError, InvalidValueException) as cloud_error:
  787. # Azure raises the cloud error if the resource not available
  788. log.exception(cloud_error)
  789. return None
  790. def list(self, network=None, limit=None, marker=None):
  791. """
  792. List subnets
  793. """
  794. return ClientPagedResultList(self.provider,
  795. self._list_subnets(network),
  796. limit=limit, marker=marker)
  797. def _list_subnets(self, network=None):
  798. result_list = []
  799. if network:
  800. network_id = network.id \
  801. if isinstance(network, Network) else network
  802. result_list = self.provider.azure_client.list_subnets(network_id)
  803. else:
  804. for net in self.provider.networking.networks:
  805. try:
  806. result_list.extend(self.provider.azure_client.list_subnets(
  807. net.id
  808. ))
  809. except CloudError as cloud_error:
  810. if cloud_error.error.error == "ResourceNotFound":
  811. log.exception(cloud_error)
  812. else:
  813. raise cloud_error
  814. subnets = [AzureSubnet(self.provider, subnet)
  815. for subnet in result_list]
  816. return subnets
  817. def find(self, network=None, **kwargs):
  818. obj_list = self._list_subnets(network)
  819. filters = ['name']
  820. matches = cb_helpers.generic_find(filters, kwargs, obj_list)
  821. return ClientPagedResultList(self._provider, list(matches))
  822. def create(self, network, cidr_block, name=None, **kwargs):
  823. """
  824. Create subnet
  825. """
  826. AzureSubnet.assert_valid_resource_name(name)
  827. network_id = network.id \
  828. if isinstance(network, Network) else network
  829. if not name:
  830. subnet_name = AzureSubnet.CB_DEFAULT_SUBNET_NAME
  831. else:
  832. subnet_name = name
  833. subnet_info = self.provider.azure_client\
  834. .create_subnet(
  835. network_id,
  836. subnet_name,
  837. {
  838. 'address_prefix': cidr_block
  839. }
  840. )
  841. return AzureSubnet(self.provider, subnet_info)
  842. def get_or_create_default(self, zone):
  843. default_cidr = '10.0.1.0/24'
  844. # No provider-default Subnet exists, look for a library-default one
  845. matches = self.find(name=AzureSubnet.CB_DEFAULT_SUBNET_NAME)
  846. if matches:
  847. return matches[0]
  848. # No provider-default Subnet exists, try to create it (net + subnets)
  849. networks = self.provider.networking.networks.find(
  850. name=AzureNetwork.CB_DEFAULT_NETWORK_NAME)
  851. if networks:
  852. network = networks[0]
  853. else:
  854. network = self.provider.networking.networks.create(
  855. AzureNetwork.CB_DEFAULT_NETWORK_NAME, '10.0.0.0/16')
  856. subnet = self.create(network, default_cidr,
  857. name=AzureSubnet.CB_DEFAULT_SUBNET_NAME)
  858. return subnet
  859. def delete(self, subnet):
  860. subnet_id = subnet.id if isinstance(subnet, Subnet) else subnet
  861. self.provider.azure_client.delete_subnet(subnet_id)
  862. class AzureRouterService(BaseRouterService):
  863. def __init__(self, provider):
  864. super(AzureRouterService, self).__init__(provider)
  865. def get(self, router_id):
  866. try:
  867. route = self.provider.azure_client.get_route_table(router_id)
  868. return AzureRouter(self.provider, route)
  869. except (CloudError, InvalidValueException) as cloud_error:
  870. # Azure raises the cloud error if the resource not available
  871. log.exception(cloud_error)
  872. return None
  873. def find(self, **kwargs):
  874. name = kwargs.pop('name', None)
  875. # All kwargs should have been popped at this time.
  876. if len(kwargs) > 0:
  877. raise TypeError("Unrecognised parameters for search: %s."
  878. " Supported attributes: %s" % (kwargs, 'name'))
  879. filters = {'Name': name}
  880. routes = [AzureRouter(self.provider, route)
  881. for route in azure_helpers.filter_by_tag(
  882. self.provider.azure_client.list_route_tables(), filters)]
  883. return ClientPagedResultList(self.provider, routes)
  884. def list(self, limit=None, marker=None):
  885. routes = [AzureRouter(self.provider, route)
  886. for route in
  887. self.provider.azure_client.list_route_tables()]
  888. return ClientPagedResultList(self.provider,
  889. routes,
  890. limit=limit, marker=marker)
  891. def create(self, name, network):
  892. AzureRouter.assert_valid_resource_name(name)
  893. parameters = {"location": self.provider.region_name,
  894. 'tags': {'Name': name}}
  895. route = self.provider.azure_client. \
  896. create_route_table(name, parameters)
  897. return AzureRouter(self.provider, route)