resources.py 53 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595
  1. """
  2. DataTypes used by this provider
  3. """
  4. from __future__ import annotations
  5. import inspect
  6. import ipaddress
  7. import logging
  8. import os
  9. import re
  10. from datetime import datetime
  11. from typing import Any
  12. from typing import IO
  13. from typing import Iterable
  14. from typing import TYPE_CHECKING
  15. from typing import cast
  16. from urllib.parse import quote
  17. from urllib.parse import urljoin
  18. from urllib.parse import urlparse
  19. import keystoneclient.exceptions as keystoneex
  20. from keystoneclient.v3.regions import Region
  21. import swiftclient
  22. from swiftclient.service import SwiftService
  23. from swiftclient.service import SwiftUploadObject
  24. from swiftclient.utils import generate_temp_url
  25. from cloudbridge.base.resources import BaseAttachmentInfo
  26. from cloudbridge.base.resources import BaseBucket
  27. from cloudbridge.base.resources import BaseBucketObject
  28. from cloudbridge.base.resources import BaseDnsRecord
  29. from cloudbridge.base.resources import BaseDnsZone
  30. from cloudbridge.base.resources import BaseFloatingIP
  31. from cloudbridge.base.resources import BaseInstance
  32. from cloudbridge.base.resources import BaseInternetGateway
  33. from cloudbridge.base.resources import BaseKeyPair
  34. from cloudbridge.base.resources import BaseMachineImage
  35. from cloudbridge.base.resources import BaseNetwork
  36. from cloudbridge.base.resources import BasePlacementZone
  37. from cloudbridge.base.resources import BaseRegion
  38. from cloudbridge.base.resources import BaseRouter
  39. from cloudbridge.base.resources import BaseSnapshot
  40. from cloudbridge.base.resources import BaseSubnet
  41. from cloudbridge.base.resources import BaseVMFirewall
  42. from cloudbridge.base.resources import BaseVMFirewallRule
  43. from cloudbridge.base.resources import BaseVMType
  44. from cloudbridge.base.resources import BaseVolume
  45. from cloudbridge.interfaces.exceptions import ProviderInternalException
  46. from cloudbridge.interfaces.resources import AttachmentInfo
  47. from cloudbridge.interfaces.resources import Bucket
  48. from cloudbridge.interfaces.resources import BucketObject
  49. from cloudbridge.interfaces.resources import FloatingIP
  50. from cloudbridge.interfaces.resources import Gateway
  51. from cloudbridge.interfaces.resources import GatewayState
  52. from cloudbridge.interfaces.resources import Instance
  53. from cloudbridge.interfaces.resources import InstanceState
  54. from cloudbridge.interfaces.resources import MachineImage
  55. from cloudbridge.interfaces.resources import MachineImageState
  56. from cloudbridge.interfaces.resources import NetworkState
  57. from cloudbridge.interfaces.resources import RouterState
  58. from cloudbridge.interfaces.resources import Snapshot
  59. from cloudbridge.interfaces.resources import SnapshotState
  60. from cloudbridge.interfaces.resources import Subnet
  61. from cloudbridge.interfaces.resources import SubnetState
  62. from cloudbridge.interfaces.resources import TrafficDirection
  63. from cloudbridge.interfaces.resources import TransferConfig
  64. from cloudbridge.interfaces.resources import VMFirewall
  65. from cloudbridge.interfaces.resources import VMType
  66. from cloudbridge.interfaces.resources import Volume
  67. from cloudbridge.interfaces.resources import VolumeState
  68. from .subservices import OpenStackBucketObjectSubService
  69. from .subservices import OpenStackDnsRecordSubService
  70. from .subservices import OpenStackFloatingIPSubService
  71. from .subservices import OpenStackGatewaySubService
  72. from .subservices import OpenStackSubnetSubService
  73. from .subservices import OpenStackVMFirewallRuleSubService
  74. if TYPE_CHECKING:
  75. from cloudbridge.providers.openstack.provider import OpenStackCloudProvider
  76. ONE_GIG = 1048576000 # in bytes
  77. FIVE_GIG = ONE_GIG * 5 # in bytes
  78. log = logging.getLogger(__name__)
  79. class OpenStackMachineImage(BaseMachineImage):
  80. # ref: http://docs.openstack.org/developer/glance/statuses.html
  81. IMAGE_STATE_MAP = {
  82. 'queued': MachineImageState.PENDING,
  83. 'saving': MachineImageState.PENDING,
  84. 'active': MachineImageState.AVAILABLE,
  85. 'killed': MachineImageState.ERROR,
  86. 'deleted': MachineImageState.UNKNOWN,
  87. 'pending_delete': MachineImageState.PENDING,
  88. 'deactivated': MachineImageState.ERROR
  89. }
  90. def __init__(self, provider: OpenStackCloudProvider,
  91. os_image: Any) -> None:
  92. super(OpenStackMachineImage, self).__init__(provider)
  93. if isinstance(os_image, OpenStackMachineImage):
  94. # pylint:disable=protected-access
  95. self._os_image = cast(Any, os_image)._os_image
  96. else:
  97. self._os_image = os_image
  98. @property
  99. def id(self) -> str:
  100. """
  101. Get the image identifier.
  102. """
  103. return self._os_image.id
  104. @property
  105. def name(self) -> str:
  106. """
  107. Get the image identifier.
  108. """
  109. return self._os_image.id
  110. @property
  111. def label(self) -> str | None:
  112. """
  113. Get the image label.
  114. """
  115. return self._os_image.name
  116. @label.setter
  117. # pylint:disable=arguments-differ
  118. def label(self, value: str) -> None:
  119. """
  120. Set the image label.
  121. """
  122. self.assert_valid_resource_label(value)
  123. cast("OpenStackCloudProvider", self._provider).os_conn.image \
  124. .update_image(self._os_image, name=value or "")
  125. @property
  126. def description(self) -> str | None:
  127. """
  128. Get the image description.
  129. """
  130. return None
  131. @property
  132. def min_disk(self) -> int | None:
  133. """
  134. Returns the minimum size of the disk that's required to
  135. boot this image (in GB)
  136. :rtype: ``int``
  137. :return: The minimum disk size needed by this image
  138. """
  139. return self._os_image.min_disk
  140. def delete(self) -> None:
  141. """
  142. Delete this image
  143. """
  144. self._os_image.delete(
  145. cast("OpenStackCloudProvider", self._provider).os_conn.image)
  146. @property
  147. def state(self) -> str:
  148. return OpenStackMachineImage.IMAGE_STATE_MAP.get(
  149. self._os_image.status, MachineImageState.UNKNOWN)
  150. def refresh(self) -> None:
  151. """
  152. Refreshes the state of this instance by re-querying the cloud provider
  153. for its latest state.
  154. """
  155. log.debug("Refreshing OpenStack Machine Image")
  156. image = self._provider.compute.images.get(self.id)
  157. if image:
  158. # pylint:disable=protected-access
  159. self._os_image = cast(Any, image)._os_image
  160. else:
  161. # The image no longer exists and cannot be refreshed.
  162. # set the status to unknown
  163. self._os_image.status = 'unknown'
  164. class OpenStackPlacementZone(BasePlacementZone):
  165. def __init__(self, provider: OpenStackCloudProvider, zone: Any,
  166. region: Any) -> None:
  167. super(OpenStackPlacementZone, self).__init__(provider)
  168. if isinstance(zone, OpenStackPlacementZone):
  169. # pylint:disable=protected-access
  170. self._os_zone = cast(Any, zone)._os_zone
  171. # pylint:disable=protected-access
  172. self._os_region = cast(Any, zone)._os_region
  173. else:
  174. self._os_zone = zone
  175. self._os_region = region
  176. @property
  177. def id(self) -> str:
  178. """
  179. Get the zone id
  180. :rtype: ``str``
  181. :return: ID for this zone as returned by the cloud middleware.
  182. """
  183. return self._os_zone
  184. @property
  185. def name(self) -> str:
  186. """
  187. Get the zone name.
  188. :rtype: ``str``
  189. :return: Name for this zone as returned by the cloud middleware.
  190. """
  191. # return self._os_zone.zoneName
  192. return self._os_zone
  193. @property
  194. def region_name(self) -> str:
  195. """
  196. Get the region that this zone belongs to.
  197. :rtype: ``str``
  198. :return: Name of this zone's region as returned by the cloud middleware
  199. """
  200. return self._os_region
  201. class OpenStackVMType(BaseVMType):
  202. def __init__(self, provider: OpenStackCloudProvider,
  203. os_flavor: Any) -> None:
  204. super(OpenStackVMType, self).__init__(provider)
  205. self._os_flavor = os_flavor
  206. self._extra_data: dict[str, Any] | None = None
  207. @property
  208. def id(self) -> str:
  209. return self._os_flavor.id
  210. @property
  211. def name(self) -> str:
  212. return self._os_flavor.name
  213. @property
  214. def family(self) -> str | None:
  215. # TODO: This may not be standardised across OpenStack
  216. # but NeCTAR is using it this way
  217. return self.extra_data.get('flavor_class:name')
  218. @property
  219. def vcpus(self) -> int:
  220. return self._os_flavor.vcpus
  221. @property
  222. def ram(self) -> float:
  223. return int(self._os_flavor.ram) / 1024
  224. @property
  225. def size_root_disk(self) -> int:
  226. return self._os_flavor.disk
  227. @property
  228. def size_ephemeral_disks(self) -> int:
  229. return 0 if self._os_flavor.ephemeral == 'N/A' else \
  230. self._os_flavor.ephemeral
  231. @property
  232. def num_ephemeral_disks(self) -> int:
  233. return 0 if self._os_flavor.ephemeral == 'N/A' else \
  234. self._os_flavor.ephemeral
  235. @property
  236. def extra_data(self) -> dict[str, Any]:
  237. # get_keys() hits Nova's /flavors/<id>/os-extra_specs endpoint.
  238. # Cache the result so repeat property accesses (and family, which
  239. # delegates here) don't fan out to N concurrent API calls under
  240. # pytest-xdist load.
  241. if self._extra_data is None:
  242. extras = self._os_flavor.get_keys()
  243. extras['rxtx_factor'] = self._os_flavor.rxtx_factor
  244. extras['swap'] = self._os_flavor.swap
  245. extras['is_public'] = self._os_flavor.is_public
  246. self._extra_data = extras
  247. return self._extra_data
  248. class OpenStackInstance(BaseInstance):
  249. # ref: http://docs.openstack.org/developer/nova/v2/2.0_server_concepts.html
  250. # and http://developer.openstack.org/api-ref-compute-v2.html
  251. INSTANCE_STATE_MAP = {
  252. 'ACTIVE': InstanceState.RUNNING,
  253. 'BUILD': InstanceState.PENDING,
  254. 'DELETED': InstanceState.DELETED,
  255. 'ERROR': InstanceState.ERROR,
  256. 'HARD_REBOOT': InstanceState.REBOOTING,
  257. 'PASSWORD': InstanceState.PENDING,
  258. 'PAUSED': InstanceState.STOPPED,
  259. 'REBOOT': InstanceState.REBOOTING,
  260. 'REBUILD': InstanceState.CONFIGURING,
  261. 'RESCUE': InstanceState.CONFIGURING,
  262. 'RESIZE': InstanceState.CONFIGURING,
  263. 'REVERT_RESIZE': InstanceState.CONFIGURING,
  264. 'SOFT_DELETED': InstanceState.STOPPED,
  265. 'STOPPED': InstanceState.STOPPED,
  266. 'SUSPENDED': InstanceState.STOPPED,
  267. 'SHUTOFF': InstanceState.STOPPED,
  268. 'UNKNOWN': InstanceState.UNKNOWN,
  269. 'VERIFY_RESIZE': InstanceState.CONFIGURING
  270. }
  271. def __init__(self, provider: OpenStackCloudProvider,
  272. os_instance: Any) -> None:
  273. super(OpenStackInstance, self).__init__(provider)
  274. self._os_instance = os_instance
  275. @property
  276. def id(self) -> str:
  277. """
  278. Get the instance identifier.
  279. """
  280. return self._os_instance.id
  281. @property
  282. def name(self) -> str:
  283. """
  284. Get the instance identifier.
  285. """
  286. return self.id
  287. @property
  288. # pylint:disable=arguments-differ
  289. def label(self) -> str | None:
  290. """
  291. Get the instance label.
  292. """
  293. return self._os_instance.name
  294. @label.setter
  295. # pylint:disable=arguments-differ
  296. def label(self, value: str) -> None:
  297. """
  298. Set the instance label.
  299. """
  300. self.assert_valid_resource_label(value)
  301. self._os_instance.name = value
  302. self._os_instance.update(name=value or "cb-inst")
  303. def _all_addresses(self) -> set[str]:
  304. """All IP addresses associated with this instance.
  305. Nova's info_cache (which backs ``server.addresses``) is refreshed
  306. by a periodic task on a ~60s cadence and is not re-queried on a
  307. plain server-show. That makes it lag both ways: a FIP just
  308. attached via Neutron won't appear, and a FIP just detached via
  309. Neutron will still appear. So we deliberately read only fixed
  310. IPs from Nova and ask Neutron live for the current floating IPs.
  311. """
  312. addrs: set[str] = set()
  313. for _, addr_list in self._os_instance.addresses.items():
  314. for entry in addr_list:
  315. if entry.get('OS-EXT-IPS:type') == 'floating':
  316. continue
  317. ip = entry.get('addr') or entry.get('OS-EXT-IPS-MAC:addr')
  318. if ip:
  319. addrs.add(ip)
  320. try:
  321. os_conn = cast("OpenStackCloudProvider", self._provider).os_conn
  322. for port in os_conn.network.ports(device_id=self.id):
  323. for fip in os_conn.network.ips(port_id=port.id):
  324. if fip.floating_ip_address:
  325. addrs.add(fip.floating_ip_address)
  326. except Exception as e:
  327. log.debug(
  328. "Could not enumerate floating IPs for instance %s: %s",
  329. self.id, e)
  330. return addrs
  331. @property
  332. def public_ips(self) -> list[str]:
  333. """
  334. Get all the public IP addresses for this instance.
  335. """
  336. # OpenStack doesn't provide an easy way to figure our whether an IP is
  337. # public or private, since the returned IPs are grouped by an arbitrary
  338. # network label. Therefore, it's necessary to parse the address and
  339. # determine whether it's public or private
  340. return [a for a in self._all_addresses()
  341. if not ipaddress.ip_address(a).is_private]
  342. @property
  343. def private_ips(self) -> list[str]:
  344. """
  345. Get all the private IP addresses for this instance.
  346. """
  347. return [a for a in self._all_addresses()
  348. if ipaddress.ip_address(a).is_private]
  349. @property
  350. def vm_type_id(self) -> str:
  351. """
  352. Get the VM type name.
  353. """
  354. return self._os_instance.flavor.get('id')
  355. @property
  356. def vm_type(self) -> VMType:
  357. """
  358. Get the VM type object.
  359. """
  360. flavor = cast("OpenStackCloudProvider", self._provider).nova \
  361. .flavors.get(self._os_instance.flavor.get('id'))
  362. return OpenStackVMType(
  363. cast("OpenStackCloudProvider", self._provider), flavor)
  364. @property
  365. def create_time(self) -> str | datetime:
  366. """
  367. Get the instance creation time
  368. """
  369. return datetime.strptime(
  370. self._os_instance.created, '%Y-%m-%dT%H:%M:%SZ')
  371. def start(self) -> None:
  372. pass
  373. def stop(self) -> None:
  374. pass
  375. def reboot(self) -> None:
  376. """
  377. Reboot this instance (using the cloud middleware API).
  378. """
  379. self._os_instance.reboot()
  380. @property
  381. def image_id(self) -> str:
  382. """
  383. Get the image ID for this instance.
  384. """
  385. # In OpenStack, the Machine Image of a running instance may
  386. # be deleted, so make sure the image exists before attempting to
  387. # retrieve its id
  388. return (self._os_instance.image.get("id")
  389. if self._os_instance.image else "")
  390. @property
  391. def zone_id(self) -> str:
  392. """
  393. Get the placement zone where this instance is running.
  394. """
  395. zone = getattr(
  396. self._os_instance, 'OS-EXT-AZ:availability_zone', None)
  397. if zone is None:
  398. raise ProviderInternalException(
  399. "Instance {0} has no availability zone".format(self.id))
  400. return zone
  401. @property
  402. def subnet_id(self) -> str:
  403. """
  404. Extract (one) subnet id associated with this instance.
  405. In OpenStack, instances are associated with ports instead of
  406. subnets so we need to dig through several connections to retrieve
  407. the subnet_id. Further, there can potentially be several ports each
  408. connected to different subnets. This implementation retrieves one
  409. subnet, the one corresponding to port associated with the first
  410. private IP associated with the instance.
  411. """
  412. # MAC address can be used to identify a port so extract the MAC
  413. # address corresponding to the (first) private IP associated with the
  414. # instance.
  415. port = None
  416. addr = None
  417. for net in self._os_instance.to_dict().get('addresses').keys():
  418. for iface in self._os_instance.to_dict().get('addresses')[net]:
  419. if iface.get('OS-EXT-IPS:type') == 'fixed':
  420. port = iface.get('OS-EXT-IPS-MAC:mac_addr')
  421. addr = iface.get('addr')
  422. break
  423. # Now get a handle to a port with the given MAC address and get the
  424. # subnet to which the private IP is connected as the desired id.
  425. neutron = cast("OpenStackCloudProvider", self._provider).neutron
  426. for prt in neutron.list_ports().get('ports'):
  427. if prt.get('mac_address') == port:
  428. for ip in prt.get('fixed_ips'):
  429. if ip.get('ip_address') == addr:
  430. return ip.get('subnet_id')
  431. raise ProviderInternalException(
  432. "Could not determine a subnet for instance {0}".format(self.id))
  433. @property
  434. def vm_firewalls(self) -> list[VMFirewall]:
  435. return cast("list[VMFirewall]", [
  436. self._provider.security.vm_firewalls.get(group.id)
  437. for group in self._os_instance.list_security_group()
  438. ])
  439. @property
  440. def vm_firewall_ids(self) -> list[str]:
  441. """
  442. Get the VM firewall IDs associated with this instance.
  443. """
  444. return [fw.id for fw in self.vm_firewalls]
  445. @property
  446. def key_pair_id(self) -> str:
  447. """
  448. Get the id of the key pair associated with this instance.
  449. """
  450. return self._os_instance.key_name
  451. def create_image(self, label: str) -> MachineImage:
  452. """
  453. Create a new image based on this instance.
  454. """
  455. log.debug("Creating OpenStack Image with the label %s", label)
  456. self.assert_valid_resource_label(label)
  457. image_id = self._os_instance.create_image(label)
  458. img = OpenStackMachineImage(
  459. cast("OpenStackCloudProvider", self._provider),
  460. self._provider.compute.images.get(image_id))
  461. return img
  462. def _get_fip(self, floating_ip: FloatingIP | str) -> Any:
  463. """Get a floating IP object based on the supplied ID."""
  464. # The OpenStack FloatingIP service looks up by id alone and ignores
  465. # the gateway argument, so None is passed deliberately.
  466. # pylint:disable=protected-access
  467. return self._provider.networking._floating_ips.get(
  468. cast(Gateway, None), cast(str, floating_ip))
  469. def _primary_port(self) -> Any:
  470. """Return the first Neutron port on this instance, or None."""
  471. # pylint:disable=protected-access
  472. os_conn = cast("OpenStackCloudProvider", self._provider).os_conn
  473. return next(
  474. iter(os_conn.network.ports(device_id=self.id)),
  475. None)
  476. def add_floating_ip(self, floating_ip: FloatingIP | str) -> None:
  477. """
  478. Add a floating IP address to this instance.
  479. Nova's add_floating_ip server action was removed in microversion
  480. 2.44 (Pike). The supported path is to set the FIP's port_id to
  481. one of the server's Neutron ports.
  482. """
  483. log.debug("Adding floating IP adress: %s", floating_ip)
  484. fip = (floating_ip if isinstance(floating_ip, OpenStackFloatingIP)
  485. else self._get_fip(floating_ip))
  486. port = self._primary_port()
  487. if not port:
  488. raise Exception(
  489. "Cannot add floating IP: instance {0} has no network port"
  490. .format(self.id))
  491. # pylint:disable=protected-access
  492. cast("OpenStackCloudProvider", self._provider).os_conn.network \
  493. .update_ip(fip._ip, port_id=port.id)
  494. def remove_floating_ip(self, floating_ip: FloatingIP | str) -> None:
  495. """
  496. Remove a floating IP address from this instance.
  497. Same rationale as add_floating_ip; the Nova action endpoint is
  498. gone, so detach by clearing port_id on the Neutron FIP. We go
  499. through neutronclient directly rather than openstacksdk
  500. Connection.network.update_ip(...) because some openstacksdk
  501. versions drop ``None`` kwargs from the PUT body, which leaves
  502. port_id unchanged on the server side.
  503. """
  504. log.debug("Removing floating IP adress: %s", floating_ip)
  505. fip = (floating_ip if isinstance(floating_ip, OpenStackFloatingIP)
  506. else self._get_fip(floating_ip))
  507. if fip is None:
  508. return
  509. cast("OpenStackCloudProvider", self._provider).neutron \
  510. .update_floatingip(fip.id, {'floatingip': {'port_id': None}})
  511. def add_vm_firewall(self, firewall: VMFirewall) -> None:
  512. """
  513. Add a VM firewall to this instance
  514. """
  515. log.debug("Adding firewall: %s", firewall)
  516. self._os_instance.add_security_group(firewall.id)
  517. def remove_vm_firewall(self, firewall: VMFirewall) -> None:
  518. """
  519. Remove a VM firewall from this instance
  520. """
  521. log.debug("Removing firewall: %s", firewall)
  522. self._os_instance.remove_security_group(firewall.id)
  523. @property
  524. def state(self) -> str:
  525. return OpenStackInstance.INSTANCE_STATE_MAP.get(
  526. self._os_instance.status, InstanceState.UNKNOWN)
  527. def refresh(self) -> None:
  528. """
  529. Refreshes the state of this instance by re-querying the cloud provider
  530. for its latest state.
  531. """
  532. instance = self._provider.compute.instances.get(
  533. self.id)
  534. if instance:
  535. # pylint:disable=protected-access
  536. self._os_instance = cast(Any, instance)._os_instance
  537. else:
  538. # The instance no longer exists and cannot be refreshed.
  539. # set the status to unknown
  540. self._os_instance.status = 'unknown'
  541. class OpenStackRegion(BaseRegion):
  542. def __init__(self, provider: OpenStackCloudProvider,
  543. os_region: Any) -> None:
  544. super(OpenStackRegion, self).__init__(provider)
  545. self._os_region = os_region
  546. @property
  547. def id(self) -> str:
  548. return (self._os_region.id if type(self._os_region) is Region else
  549. self._os_region)
  550. @property
  551. def name(self) -> str:
  552. return self.id
  553. @property
  554. def zones(self) -> Iterable[OpenStackPlacementZone]:
  555. # ``detailed`` param must be set to ``False`` because the (default)
  556. # ``True`` value requires Admin privileges
  557. provider = cast("OpenStackCloudProvider", self._provider)
  558. if self.name == self._provider.region_name: # optimisation
  559. zones = provider.nova.availability_zones.list(detailed=False)
  560. else:
  561. try:
  562. # pylint:disable=protected-access
  563. region_nova = provider._connect_nova_region(self.name)
  564. zones = region_nova.availability_zones.list(detailed=False)
  565. except keystoneex.EndpointNotFound:
  566. # This region may not have a compute endpoint. If so just
  567. # return an empty list
  568. zones = []
  569. return [OpenStackPlacementZone(provider, z.zoneName, self.name)
  570. for z in zones]
  571. class OpenStackVolume(BaseVolume):
  572. # Ref: http://developer.openstack.org/api-ref-blockstorage-v2.html
  573. VOLUME_STATE_MAP = {
  574. 'creating': VolumeState.CREATING,
  575. 'available': VolumeState.AVAILABLE,
  576. 'attaching': VolumeState.CONFIGURING,
  577. 'in-use': VolumeState.IN_USE,
  578. 'deleting': VolumeState.CONFIGURING,
  579. 'error': VolumeState.ERROR,
  580. 'error_deleting': VolumeState.ERROR,
  581. 'backing-up': VolumeState.CONFIGURING,
  582. 'restoring-backup': VolumeState.CONFIGURING,
  583. 'error_restoring': VolumeState.ERROR,
  584. 'error_extending': VolumeState.ERROR
  585. }
  586. def __init__(self, provider: OpenStackCloudProvider,
  587. volume: Any) -> None:
  588. super(OpenStackVolume, self).__init__(provider)
  589. self._volume = volume
  590. @property
  591. def id(self) -> str:
  592. return self._volume.id
  593. @property
  594. def name(self) -> str:
  595. return self.id
  596. @property
  597. # pylint:disable=arguments-differ
  598. def label(self) -> str | None:
  599. """
  600. Get the volume label.
  601. """
  602. return self._volume.name
  603. @label.setter
  604. # pylint:disable=arguments-differ
  605. def label(self, value: str) -> None:
  606. """
  607. Set the volume label.
  608. """
  609. self.assert_valid_resource_label(value)
  610. self._volume.name = value
  611. self._volume.commit(
  612. cast("OpenStackCloudProvider", self._provider).os_conn
  613. .block_storage)
  614. @property
  615. def description(self) -> str:
  616. return self._volume.description
  617. @description.setter
  618. def description(self, value: str) -> None:
  619. self._volume.description = value
  620. self._volume.commit(
  621. cast("OpenStackCloudProvider", self._provider).os_conn
  622. .block_storage)
  623. @property
  624. def size(self) -> int:
  625. return self._volume.size
  626. @property
  627. def create_time(self) -> str | datetime:
  628. return self._volume.created_at
  629. @property
  630. def zone_id(self) -> str:
  631. return self._volume.availability_zone
  632. @property
  633. def source(self) -> Snapshot | MachineImage | None:
  634. if self._volume.snapshot_id:
  635. return self._provider.storage.snapshots.get(
  636. self._volume.snapshot_id)
  637. return None
  638. @property
  639. def attachments(self) -> AttachmentInfo | None:
  640. if self._volume.attachments:
  641. return BaseAttachmentInfo(
  642. self,
  643. self._volume.attachments[0].get('server_id'),
  644. self._volume.attachments[0].get('device'))
  645. else:
  646. return None
  647. def attach(self, instance: str | Instance, device: str) -> None:
  648. """
  649. Attach this volume to an instance.
  650. """
  651. log.debug("Attaching %s to %s instance", device, instance)
  652. instance_id = instance.id if isinstance(
  653. instance,
  654. OpenStackInstance) else instance
  655. cast("OpenStackCloudProvider", self._provider).os_conn.compute \
  656. .create_volume_attachment(
  657. server=instance_id, volume_id=self.id, device=device)
  658. def detach(self, force: bool = False) -> None:
  659. """
  660. Detach this volume from an instance.
  661. """
  662. for attachment in self._volume.attachments:
  663. cast("OpenStackCloudProvider", self._provider).os_conn.compute \
  664. .delete_volume_attachment(
  665. attachment['id'], attachment['server_id'])
  666. def create_snapshot(self, label: str,
  667. description: str | None = None) -> Snapshot:
  668. """
  669. Create a snapshot of this Volume.
  670. """
  671. log.debug("Creating snapchat of volume: %s with the "
  672. "description: %s", label, description)
  673. return self._provider.storage.snapshots.create(
  674. label, self, description=description)
  675. @property
  676. def state(self) -> str:
  677. return OpenStackVolume.VOLUME_STATE_MAP.get(
  678. self._volume.status, VolumeState.UNKNOWN)
  679. def refresh(self) -> None:
  680. """
  681. Refreshes the state of this volume by re-querying the cloud provider
  682. for its latest state.
  683. """
  684. vol = self._provider.storage.volumes.get(
  685. self.id)
  686. if vol:
  687. # pylint:disable=protected-access
  688. self._volume = cast(Any, vol)._volume
  689. else:
  690. # The volume no longer exists and cannot be refreshed.
  691. # set the status to unknown
  692. self._volume.status = 'unknown'
  693. class OpenStackSnapshot(BaseSnapshot):
  694. # Ref: http://developer.openstack.org/api-ref-blockstorage-v2.html
  695. SNAPSHOT_STATE_MAP = {
  696. 'creating': SnapshotState.PENDING,
  697. 'available': SnapshotState.AVAILABLE,
  698. 'deleting': SnapshotState.CONFIGURING,
  699. 'error': SnapshotState.ERROR,
  700. 'error_deleting': SnapshotState.ERROR
  701. }
  702. def __init__(self, provider: OpenStackCloudProvider,
  703. snapshot: Any) -> None:
  704. super(OpenStackSnapshot, self).__init__(provider)
  705. self._snapshot = snapshot
  706. @property
  707. def id(self) -> str:
  708. return self._snapshot.id
  709. @property
  710. def name(self) -> str:
  711. return self.id
  712. @property
  713. # pylint:disable=arguments-differ
  714. def label(self) -> str | None:
  715. """
  716. Get the snapshot label.
  717. """
  718. return self._snapshot.name
  719. @label.setter
  720. # pylint:disable=arguments-differ
  721. def label(self, value: str) -> None:
  722. """
  723. Set the snapshot label.
  724. """
  725. self.assert_valid_resource_label(value)
  726. self._snapshot.name = value
  727. self._snapshot.commit(
  728. cast("OpenStackCloudProvider", self._provider).os_conn
  729. .block_storage)
  730. @property
  731. def description(self) -> str:
  732. return self._snapshot.description
  733. @description.setter
  734. def description(self, value: str) -> None:
  735. self._snapshot.description = value
  736. self._snapshot.commit(
  737. cast("OpenStackCloudProvider", self._provider).os_conn
  738. .block_storage)
  739. @property
  740. def size(self) -> int:
  741. return self._snapshot.size
  742. @property
  743. def volume_id(self) -> str | None:
  744. return self._snapshot.volume_id
  745. @property
  746. def create_time(self) -> str | datetime:
  747. return self._snapshot.created_at
  748. @property
  749. def state(self) -> str:
  750. return OpenStackSnapshot.SNAPSHOT_STATE_MAP.get(
  751. self._snapshot.status, SnapshotState.UNKNOWN)
  752. def refresh(self) -> None:
  753. """
  754. Refreshes the state of this snapshot by re-querying the cloud provider
  755. for its latest state.
  756. """
  757. snap = self._provider.storage.snapshots.get(
  758. self.id)
  759. if snap:
  760. # pylint:disable=protected-access
  761. self._snapshot = cast(Any, snap)._snapshot
  762. else:
  763. # The snapshot no longer exists and cannot be refreshed.
  764. # set the status to unknown
  765. self._snapshot.status = 'unknown'
  766. def create_volume(self, size: int | None = None,
  767. volume_type: str | None = None,
  768. iops: int | None = None) -> Volume:
  769. """
  770. Create a new Volume from this Snapshot.
  771. """
  772. vol_label = "from-snap-{0}".format(self.label or self.id)
  773. self.assert_valid_resource_label(vol_label)
  774. size = size if size else self._snapshot.size
  775. provider = cast("OpenStackCloudProvider", self._provider)
  776. os_vol = provider.os_conn.block_storage.create_volume(
  777. size=size, name=vol_label, snapshot_id=self._snapshot.id,
  778. availability_zone=provider.zone_name)
  779. cb_vol = OpenStackVolume(provider, os_vol)
  780. return cb_vol
  781. class OpenStackNetwork(BaseNetwork):
  782. # Ref: https://github.com/openstack/neutron/blob/master/neutron/plugins/
  783. # common/constants.py
  784. _NETWORK_STATE_MAP = {
  785. 'PENDING_CREATE': NetworkState.PENDING,
  786. 'PENDING_UPDATE': NetworkState.PENDING,
  787. 'PENDING_DELETE': NetworkState.PENDING,
  788. 'CREATED': NetworkState.PENDING,
  789. 'INACTIVE': NetworkState.PENDING,
  790. 'DOWN': NetworkState.DOWN,
  791. 'ERROR': NetworkState.ERROR,
  792. 'ACTIVE': NetworkState.AVAILABLE
  793. }
  794. def __init__(self, provider: OpenStackCloudProvider,
  795. network: Any) -> None:
  796. super(OpenStackNetwork, self).__init__(provider)
  797. self._network = network
  798. self._gateway_service = OpenStackGatewaySubService(provider, self)
  799. self._subnet_svc = OpenStackSubnetSubService(provider, self)
  800. @property
  801. def id(self) -> str:
  802. return self._network.get('id', None)
  803. @property
  804. def name(self) -> str:
  805. return self.id
  806. @property
  807. def label(self) -> str | None:
  808. return self._network.get('name', None)
  809. @label.setter
  810. def label(self, value: str) -> None:
  811. """
  812. Set the network label.
  813. """
  814. self.assert_valid_resource_label(value)
  815. cast("OpenStackCloudProvider", self._provider).neutron.update_network(
  816. self.id, {'network': {'name': value or ""}})
  817. self.refresh()
  818. @property
  819. def external(self) -> bool:
  820. return self._network.get('router:external', False)
  821. @property
  822. def shared(self) -> bool:
  823. return self._network.get('shared', False)
  824. @property
  825. def state(self) -> str:
  826. self.refresh()
  827. return OpenStackNetwork._NETWORK_STATE_MAP.get(
  828. self._network.get('status', None),
  829. NetworkState.UNKNOWN)
  830. @property
  831. def cidr_block(self) -> str:
  832. # OpenStack does not define a CIDR block for networks
  833. return ''
  834. @property
  835. def subnets(self) -> OpenStackSubnetSubService:
  836. return self._subnet_svc
  837. def refresh(self) -> None:
  838. """Refresh the state of this network by re-querying the provider."""
  839. network = self._provider.networking.networks.get(self.id)
  840. if network:
  841. # pylint:disable=protected-access
  842. self._network = cast(Any, network)._network
  843. else:
  844. # Network no longer exists
  845. self._network = {}
  846. @property
  847. def gateways(self) -> OpenStackGatewaySubService:
  848. return self._gateway_service
  849. class OpenStackSubnet(BaseSubnet):
  850. def __init__(self, provider: OpenStackCloudProvider,
  851. subnet: Any) -> None:
  852. super(OpenStackSubnet, self).__init__(provider)
  853. self._subnet = subnet
  854. self._state: str | None = None
  855. @property
  856. def id(self) -> str:
  857. return self._subnet.get('id', None)
  858. @property
  859. def name(self) -> str:
  860. return self.id
  861. @property
  862. def label(self) -> str | None:
  863. return self._subnet.get('name', None)
  864. @label.setter
  865. def label(self, value: str) -> None: # pylint:disable=arguments-differ
  866. """
  867. Set the subnet label.
  868. """
  869. self.assert_valid_resource_label(value)
  870. cast("OpenStackCloudProvider", self._provider).neutron.update_subnet(
  871. self.id, {'subnet': {'name': value or ""}})
  872. self._subnet['name'] = value
  873. @property
  874. def cidr_block(self) -> str:
  875. return self._subnet.get('cidr', None)
  876. @property
  877. def network_id(self) -> str:
  878. return self._subnet.get('network_id', None)
  879. @property
  880. def zone(self) -> None:
  881. """
  882. OpenStack does not have a notion of placement zone for subnets.
  883. Default to None.
  884. """
  885. return None
  886. @property
  887. def state(self) -> str:
  888. return SubnetState.UNKNOWN if self._state == SubnetState.UNKNOWN \
  889. else SubnetState.AVAILABLE
  890. def refresh(self) -> None:
  891. subnet = self._provider.networking.subnets.get(self.id)
  892. if subnet:
  893. # pylint:disable=protected-access
  894. self._subnet = cast(Any, subnet)._subnet
  895. self._state = SubnetState.AVAILABLE
  896. else:
  897. # subnet no longer exists
  898. self._state = SubnetState.UNKNOWN
  899. class OpenStackFloatingIP(BaseFloatingIP):
  900. def __init__(self, provider: OpenStackCloudProvider,
  901. floating_ip: Any) -> None:
  902. super(OpenStackFloatingIP, self).__init__(provider)
  903. self._ip = floating_ip
  904. @property
  905. def id(self) -> str:
  906. return self._ip.id
  907. @property
  908. def public_ip(self) -> str:
  909. return self._ip.floating_ip_address
  910. @property
  911. def private_ip(self) -> str | None:
  912. return self._ip.fixed_ip_address
  913. @property
  914. def in_use(self) -> bool:
  915. return bool(self._ip.port_id)
  916. def refresh(self) -> None:
  917. net = cast("OpenStackNetwork", self._provider.networking.networks.get(
  918. self._ip.floating_network_id))
  919. gw = net.gateways.get_or_create()
  920. fip = gw.floating_ips.get(self.id)
  921. # pylint:disable=protected-access
  922. self._ip = cast(Any, fip)._ip
  923. @property
  924. def _gateway_id(self) -> str:
  925. return self._ip.floating_network_id
  926. class OpenStackRouter(BaseRouter):
  927. def __init__(self, provider: OpenStackCloudProvider,
  928. router: Any) -> None:
  929. super(OpenStackRouter, self).__init__(provider)
  930. self._router = router
  931. @property
  932. def id(self) -> str:
  933. router_id = getattr(self._router, 'id', None)
  934. if router_id is None:
  935. raise ProviderInternalException("Router has no id")
  936. return router_id
  937. @property
  938. def name(self) -> str:
  939. return self.id
  940. @property
  941. def label(self) -> str | None:
  942. return self._router.name
  943. @label.setter
  944. def label(self, value: str) -> None: # pylint:disable=arguments-differ
  945. """
  946. Set the router label.
  947. """
  948. self.assert_valid_resource_label(value)
  949. self._router = cast("OpenStackCloudProvider", self._provider) \
  950. .os_conn.update_router(self.id, value)
  951. def refresh(self) -> None:
  952. self._router = cast("OpenStackCloudProvider", self._provider) \
  953. .os_conn.get_router(self.id)
  954. @property
  955. def state(self) -> str:
  956. if self._router.external_gateway_info:
  957. return RouterState.ATTACHED
  958. return RouterState.DETACHED
  959. @property
  960. def network_id(self) -> str | None:
  961. ports = cast("OpenStackCloudProvider", self._provider).os_conn \
  962. .list_ports(filters={'device_id': self.id})
  963. if ports:
  964. return ports[0].network_id
  965. return None
  966. def attach_subnet(self, subnet: Subnet | str) -> None:
  967. subnet_id = subnet.id if isinstance(subnet, OpenStackSubnet) else subnet
  968. cast("OpenStackCloudProvider", self._provider).os_conn \
  969. .add_router_interface(self._router.toDict(), subnet_id)
  970. def detach_subnet(self, subnet: Subnet | str) -> None:
  971. subnet_id = subnet.id if isinstance(subnet, OpenStackSubnet) else subnet
  972. cast("OpenStackCloudProvider", self._provider).os_conn \
  973. .remove_router_interface(self._router.toDict(), subnet_id)
  974. @property
  975. def subnets(self) -> list[Subnet]:
  976. # A router and a subnet are linked via a port, so traverse ports
  977. # associated with the current router to find a list of subnets
  978. # associated with it.
  979. subnets: list[Subnet | None] = []
  980. os_conn = cast("OpenStackCloudProvider", self._provider).os_conn
  981. for port in os_conn.list_ports(filters={'device_id': self.id}):
  982. for fixed_ip in port.fixed_ips:
  983. subnets.append(self._provider.networking.subnets.get(
  984. fixed_ip.get('subnet_id')))
  985. return cast("list[Subnet]", subnets)
  986. def attach_gateway(self, gateway: Gateway) -> None:
  987. cast("OpenStackCloudProvider", self._provider).os_conn.update_router(
  988. self.id, ext_gateway_net_id=gateway.id)
  989. def detach_gateway(self, gateway: Gateway) -> None:
  990. # TODO: OpenStack SDK Connection object doesn't appear to have a method
  991. # for detaching/clearing the external gateway.
  992. cast("OpenStackCloudProvider", self._provider).neutron \
  993. .remove_gateway_router(self.id)
  994. class OpenStackInternetGateway(BaseInternetGateway):
  995. GATEWAY_STATE_MAP = {
  996. NetworkState.AVAILABLE: GatewayState.AVAILABLE,
  997. NetworkState.DOWN: GatewayState.ERROR,
  998. NetworkState.ERROR: GatewayState.ERROR,
  999. NetworkState.PENDING: GatewayState.CONFIGURING,
  1000. NetworkState.UNKNOWN: GatewayState.UNKNOWN
  1001. }
  1002. def __init__(self, provider: OpenStackCloudProvider,
  1003. gateway_net: Any) -> None:
  1004. super(OpenStackInternetGateway, self).__init__(provider)
  1005. if isinstance(gateway_net, OpenStackNetwork):
  1006. # pylint:disable=protected-access
  1007. gateway_net = gateway_net._network
  1008. self._gateway_net = gateway_net
  1009. self._fips_container = OpenStackFloatingIPSubService(provider, self)
  1010. @property
  1011. def id(self) -> str:
  1012. return self._gateway_net.get('id', None)
  1013. @property
  1014. def name(self) -> str:
  1015. return self._gateway_net.get('name', None)
  1016. @property
  1017. def network_id(self) -> str:
  1018. return self._gateway_net.get('id')
  1019. def refresh(self) -> None:
  1020. """Refresh the state of this network by re-querying the provider."""
  1021. network = self._provider.networking.networks.get(self.id)
  1022. if network:
  1023. # pylint:disable=protected-access
  1024. self._gateway_net = cast(Any, network)._network
  1025. else:
  1026. # subnet no longer exists
  1027. self._gateway_net.state = NetworkState.UNKNOWN
  1028. @property
  1029. def state(self) -> str:
  1030. return self.GATEWAY_STATE_MAP.get(
  1031. self._gateway_net.state, GatewayState.UNKNOWN)
  1032. @property
  1033. def floating_ips(self) -> OpenStackFloatingIPSubService:
  1034. return self._fips_container
  1035. class OpenStackKeyPair(BaseKeyPair):
  1036. def __init__(self, provider: OpenStackCloudProvider,
  1037. key_pair: Any) -> None:
  1038. super(OpenStackKeyPair, self).__init__(provider, key_pair)
  1039. class OpenStackVMFirewall(BaseVMFirewall):
  1040. _network_id_tag = "CB-auto-associated-network-id: "
  1041. def __init__(self, provider: OpenStackCloudProvider,
  1042. vm_firewall: Any) -> None:
  1043. super(OpenStackVMFirewall, self).__init__(provider, vm_firewall)
  1044. self._rule_svc = OpenStackVMFirewallRuleSubService(provider, self)
  1045. @property
  1046. def network_id(self) -> str | None:
  1047. """
  1048. OpenStack does not associate a fw with a network so extract from desc.
  1049. :return: The network ID supplied when this firewall was created or
  1050. `None` if ID cannot be identified.
  1051. """
  1052. # Extracting networking ID from description
  1053. exp = ".*\\[" + self._network_id_tag + "([^\\]]*)\\].*"
  1054. matches = re.match(exp, self._description)
  1055. if matches:
  1056. return matches.group(1)
  1057. # We generally simulate a network being associated with a firewall;
  1058. # however, because of some networking specificity in Nectar, we must
  1059. # allow `None` return value as well in case an ID was not discovered.
  1060. else:
  1061. return None
  1062. @property
  1063. def _description(self) -> str:
  1064. return self._vm_firewall.description or ""
  1065. @property
  1066. def description(self) -> str | None:
  1067. desc_fragment = " [{}{}]".format(self._network_id_tag,
  1068. self.network_id)
  1069. desc = self._description
  1070. if desc:
  1071. return desc.replace(desc_fragment, "")
  1072. else:
  1073. return None
  1074. @description.setter
  1075. def description(self, value: str) -> None:
  1076. if not value:
  1077. value = ""
  1078. value += " [{}{}]".format(self._network_id_tag,
  1079. self.network_id)
  1080. cast("OpenStackCloudProvider", self._provider).os_conn.network \
  1081. .update_security_group(self.id, description=value)
  1082. self.refresh()
  1083. @property
  1084. def name(self) -> str:
  1085. """
  1086. Return the name of this VM firewall.
  1087. """
  1088. return self.id
  1089. @property
  1090. def label(self) -> str | None:
  1091. return self._vm_firewall.name
  1092. @label.setter
  1093. # pylint:disable=arguments-differ
  1094. def label(self, value: str) -> None:
  1095. self.assert_valid_resource_label(value)
  1096. cast("OpenStackCloudProvider", self._provider).os_conn.network \
  1097. .update_security_group(self.id, name=value or "")
  1098. self.refresh()
  1099. @property
  1100. def rules(self) -> OpenStackVMFirewallRuleSubService:
  1101. return self._rule_svc
  1102. def refresh(self) -> None:
  1103. self._vm_firewall = cast(
  1104. "OpenStackCloudProvider", self._provider).os_conn.network \
  1105. .get_security_group(self.id)
  1106. def to_json(self) -> dict[str, Any]:
  1107. attr = inspect.getmembers(self, lambda a: not (inspect.isroutine(a)))
  1108. js = {k: v for (k, v) in attr if not k.startswith('_')}
  1109. json_rules = [r.to_json() for r in self.rules]
  1110. js['rules'] = json_rules
  1111. return js
  1112. class OpenStackVMFirewallRule(BaseVMFirewallRule):
  1113. def __init__(self, parent_fw: VMFirewall, rule: Any) -> None:
  1114. super(OpenStackVMFirewallRule, self).__init__(parent_fw, rule)
  1115. @property
  1116. def id(self) -> str:
  1117. return self._rule.get('id')
  1118. @property
  1119. def direction(self) -> TrafficDirection:
  1120. direction = self._rule.get('direction')
  1121. if direction == 'ingress':
  1122. return TrafficDirection.INBOUND
  1123. elif direction == 'egress':
  1124. return TrafficDirection.OUTBOUND
  1125. raise ProviderInternalException(
  1126. "Unknown firewall rule direction: {0}".format(direction))
  1127. @property
  1128. def protocol(self) -> str:
  1129. return self._rule.get('protocol')
  1130. @property
  1131. def from_port(self) -> int:
  1132. return self._rule.get('port_range_min')
  1133. @property
  1134. def to_port(self) -> int:
  1135. return self._rule.get('port_range_max')
  1136. @property
  1137. def cidr(self) -> str | None:
  1138. return self._rule.get('remote_ip_prefix')
  1139. @property
  1140. def src_dest_fw_id(self) -> str | None:
  1141. fw = self.src_dest_fw
  1142. if fw:
  1143. return fw.id
  1144. return None
  1145. @property
  1146. def src_dest_fw(self) -> VMFirewall | None:
  1147. fw_id = self._rule.get('remote_group_id')
  1148. if fw_id:
  1149. return self._provider.security.vm_firewalls.get(fw_id)
  1150. return None
  1151. class OpenStackBucketObject(BaseBucketObject):
  1152. def __init__(self, provider: OpenStackCloudProvider,
  1153. cbcontainer: Any, obj: Any) -> None:
  1154. super(OpenStackBucketObject, self).__init__(provider)
  1155. self.cbcontainer = cbcontainer
  1156. self._obj = obj
  1157. @property
  1158. def id(self) -> str:
  1159. return self._obj.get("name")
  1160. @property
  1161. def name(self) -> str:
  1162. """Get this object's name."""
  1163. return self.id
  1164. @property
  1165. def size(self) -> int:
  1166. return self._obj.get("bytes")
  1167. @property
  1168. def last_modified(self) -> str:
  1169. return self._obj.get("last_modified")
  1170. def iter_content(self) -> Iterable[bytes]:
  1171. """Returns this object's content as an iterable."""
  1172. _, content = cast("OpenStackCloudProvider", self._provider).swift \
  1173. .get_object(self.cbcontainer.name, self.name, resp_chunk_size=65536)
  1174. return content
  1175. @property
  1176. def bucket(self) -> Bucket:
  1177. return self.cbcontainer
  1178. def _upload_single_shot(
  1179. self, data: str | bytes | IO[bytes]) -> BucketObject:
  1180. """
  1181. Set the contents of this object in a single request.
  1182. Inputs larger than the multipart threshold are handled transparently
  1183. by the base class via the Static Large Object (SLO) multipart path, so
  1184. this single-request path only runs for smaller payloads.
  1185. """
  1186. cast("OpenStackCloudProvider", self._provider).swift.put_object(
  1187. self.cbcontainer.name, self.name, data)
  1188. return self
  1189. def upload_from_file(self, path: str,
  1190. config: TransferConfig | None = None) -> BucketObject:
  1191. """
  1192. Stores the contents of the file pointed by the ``path`` variable.
  1193. If the file is bigger than 5 Gig, it will be broken into segments.
  1194. Swift uses ``SwiftService`` here, which manages its own segmenting and
  1195. concurrency; the ``config`` argument is accepted for interface
  1196. consistency but does not affect this path.
  1197. :type path: ``str``
  1198. :param path: Absolute path to the file to be uploaded to Swift.
  1199. :rtype: ``bool``
  1200. :return: ``True`` if successful, ``False`` if not.
  1201. .. note::
  1202. * The size of the segments chosen (or any of the other upload
  1203. options) is not under user control.
  1204. * If called this method will remap the
  1205. ``swiftclient.service.get_conn`` factory method to
  1206. ``self._provider._connect_swift``
  1207. .. seealso:: https://github.com/CloudVE/cloudbridge/issues/35#issuecomment-297629661 # noqa
  1208. """
  1209. upload_options: dict[str, Any] = {}
  1210. if 'segment_size' not in upload_options:
  1211. if os.path.getsize(path) >= FIVE_GIG:
  1212. upload_options['segment_size'] = FIVE_GIG
  1213. # remap the swift service's connection factory method
  1214. # pylint:disable=protected-access
  1215. swiftclient.service.get_conn = cast(
  1216. "OpenStackCloudProvider", self._provider)._connect_swift
  1217. with SwiftService() as swift:
  1218. upload_object = SwiftUploadObject(path, object_name=self.name)
  1219. for up_res in swift.upload(self.cbcontainer.name,
  1220. [upload_object, ],
  1221. options=upload_options):
  1222. up_res['success']
  1223. return self
  1224. def delete(self) -> None:
  1225. """
  1226. Delete this object.
  1227. :rtype: ``bool``
  1228. :return: True if successful
  1229. .. note:: If called this method will remap the
  1230. ``swiftclient.service.get_conn`` factory method to
  1231. ``self._provider._connect_swift``
  1232. """
  1233. # remap the swift service's connection factory method
  1234. # pylint:disable=protected-access
  1235. swiftclient.service.get_conn = cast(
  1236. "OpenStackCloudProvider", self._provider)._connect_swift
  1237. with SwiftService() as swift:
  1238. for del_res in swift.delete(self.cbcontainer.name, [self.name, ]):
  1239. del_res['success']
  1240. def generate_url(self, expires_in: int, writable: bool = False,
  1241. content_disposition: str | None = None,
  1242. content_type: str | None = None) -> str:
  1243. http_method = "PUT" if writable else "GET"
  1244. # Set a temp url key on the object (http://bit.ly/2NBiXGD)
  1245. temp_url_key = "cloudbridge-tmp-url-key"
  1246. swift = cast("OpenStackCloudProvider", self._provider).swift
  1247. swift.post_account(
  1248. headers={"x-account-meta-temp-url-key": temp_url_key})
  1249. base_url = urlparse(swift.get_service_auth()[0])
  1250. access_point = "{0}://{1}".format(base_url.scheme, base_url.netloc)
  1251. url_path = "/".join([base_url.path, self.cbcontainer.name, self.name])
  1252. url = urljoin(access_point, generate_temp_url(url_path, expires_in,
  1253. temp_url_key,
  1254. http_method))
  1255. if not writable and content_disposition:
  1256. # Swift's tempurl middleware serves Content-Disposition via the
  1257. # unsigned `filename`/`inline` query parameters (the signature
  1258. # covers only method, path and expiry); Content-Type cannot be
  1259. # overridden.
  1260. match = re.search(r'filename\*?=(?:"([^"]+)"|([^;]+))',
  1261. content_disposition)
  1262. filename = (match.group(1) or match.group(2)).strip() \
  1263. if match else None
  1264. if content_disposition.strip().lower().startswith('inline'):
  1265. url += '&inline'
  1266. if filename:
  1267. url += '&filename=' + quote(filename)
  1268. return url
  1269. def refresh(self) -> None:
  1270. self._obj = self.cbcontainer.objects.get(self.id)._obj
  1271. class OpenStackBucket(BaseBucket):
  1272. def __init__(self, provider: OpenStackCloudProvider,
  1273. bucket: Any) -> None:
  1274. super(OpenStackBucket, self).__init__(provider)
  1275. self._bucket = bucket
  1276. self._object_container = OpenStackBucketObjectSubService(provider,
  1277. self)
  1278. @property
  1279. def id(self) -> str:
  1280. return self._bucket.get("name")
  1281. @property
  1282. def name(self) -> str:
  1283. return self.id
  1284. @property
  1285. def objects(self) -> OpenStackBucketObjectSubService:
  1286. return self._object_container
  1287. class OpenStackDnsZone(BaseDnsZone):
  1288. def __init__(self, provider: OpenStackCloudProvider,
  1289. dns_zone: Any) -> None:
  1290. super(OpenStackDnsZone, self).__init__(provider)
  1291. self._dns_zone = dns_zone
  1292. self._dns_record_container = OpenStackDnsRecordSubService(
  1293. provider, self)
  1294. @property
  1295. def id(self) -> str:
  1296. return self._dns_zone.id
  1297. @property
  1298. def name(self) -> str:
  1299. return self._dns_zone.name
  1300. @property
  1301. def admin_email(self) -> str | None:
  1302. return self._dns_zone.email
  1303. @property
  1304. def records(self) -> OpenStackDnsRecordSubService:
  1305. return self._dns_record_container
  1306. class OpenStackDnsRecord(BaseDnsRecord):
  1307. def __init__(self, provider: OpenStackCloudProvider, dns_zone: Any,
  1308. dns_record: Any) -> None:
  1309. super(OpenStackDnsRecord, self).__init__(provider)
  1310. self._dns_zone = dns_zone
  1311. self._dns_rec = dns_record
  1312. @property
  1313. def id(self) -> str:
  1314. return self._dns_rec.id
  1315. @property
  1316. def name(self) -> str:
  1317. return self._dns_rec.name
  1318. @property
  1319. def zone_id(self) -> str:
  1320. return self._dns_zone.id
  1321. @property
  1322. def type(self) -> str:
  1323. return self._dns_rec.type
  1324. @property
  1325. def data(self) -> list[str]:
  1326. return self._dns_rec.records
  1327. @property
  1328. def ttl(self) -> int:
  1329. return self._dns_rec.ttl
  1330. def delete(self) -> None:
  1331. # pylint:disable=protected-access
  1332. records: Any = self._provider.dns._records
  1333. records.delete(self._dns_zone, self)