resources.py 50 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343
  1. """
  2. Base implementation for data objects exposed through a provider or service
  3. """
  4. import inspect
  5. import io
  6. import itertools
  7. import logging
  8. import os
  9. import queue
  10. import re
  11. import shutil
  12. import time
  13. import uuid
  14. from concurrent.futures import FIRST_COMPLETED
  15. from concurrent.futures import Future
  16. from concurrent.futures import ThreadPoolExecutor
  17. from concurrent.futures import wait
  18. from typing import Any
  19. from typing import IO
  20. from typing import Iterator
  21. from typing import Sequence
  22. from typing import TYPE_CHECKING
  23. from typing import TypeVar
  24. from typing import cast
  25. from cloudbridge.interfaces.exceptions import \
  26. InvalidConfigurationException
  27. from cloudbridge.interfaces.exceptions import InvalidLabelException
  28. from cloudbridge.interfaces.exceptions import InvalidNameException
  29. from cloudbridge.interfaces.exceptions import InvalidValueException
  30. from cloudbridge.interfaces.exceptions import WaitStateException
  31. from cloudbridge.interfaces.provider import CloudProvider
  32. from cloudbridge.interfaces.resources import AttachmentInfo
  33. from cloudbridge.interfaces.resources import Bucket
  34. from cloudbridge.interfaces.resources import BucketObject
  35. from cloudbridge.interfaces.resources import CloudResource
  36. from cloudbridge.interfaces.resources import DnsRecord
  37. from cloudbridge.interfaces.resources import DnsZone
  38. from cloudbridge.interfaces.resources import FloatingIP
  39. from cloudbridge.interfaces.resources import FloatingIpState
  40. from cloudbridge.interfaces.resources import GatewayState
  41. from cloudbridge.interfaces.resources import Instance
  42. from cloudbridge.interfaces.resources import InstanceState
  43. from cloudbridge.interfaces.resources import InternetGateway
  44. from cloudbridge.interfaces.resources import KeyPair
  45. from cloudbridge.interfaces.resources import LaunchConfig
  46. from cloudbridge.interfaces.resources import MachineImage
  47. from cloudbridge.interfaces.resources import MachineImageState
  48. from cloudbridge.interfaces.resources import MultipartUpload
  49. from cloudbridge.interfaces.resources import Network
  50. from cloudbridge.interfaces.resources import NetworkState
  51. from cloudbridge.interfaces.resources import ObjectLifeCycleMixin
  52. from cloudbridge.interfaces.resources import PageableObjectMixin
  53. from cloudbridge.interfaces.resources import PlacementZone
  54. from cloudbridge.interfaces.resources import Region
  55. from cloudbridge.interfaces.resources import ResultList
  56. from cloudbridge.interfaces.resources import Router
  57. from cloudbridge.interfaces.resources import Snapshot
  58. from cloudbridge.interfaces.resources import SnapshotState
  59. from cloudbridge.interfaces.resources import Subnet
  60. from cloudbridge.interfaces.resources import SubnetState
  61. from cloudbridge.interfaces.resources import UploadConfig
  62. from cloudbridge.interfaces.resources import UploadPart
  63. from cloudbridge.interfaces.resources import VMFirewall
  64. from cloudbridge.interfaces.resources import VMFirewallRule
  65. from cloudbridge.interfaces.resources import VMType
  66. from cloudbridge.interfaces.resources import Volume
  67. from cloudbridge.interfaces.resources import VolumeState
  68. from . import helpers as cb_helpers
  69. if TYPE_CHECKING:
  70. from _typeshed import SupportsRead
  71. from cloudbridge.interfaces.services import BucketObjectService
  72. log = logging.getLogger(__name__)
  73. # Element type for the generic pageable collections defined in this module
  74. # (mirrors ``cloudbridge.interfaces.resources.T``).
  75. T = TypeVar("T")
  76. class BaseCloudResource(CloudResource):
  77. """
  78. Base implementation of a CloudBridge Resource.
  79. """
  80. # Regular expression for valid cloudbridge resource names/labels.
  81. # Can be alphanumeric string that does not start or end with a dash
  82. # Must be at least 3 characters in length.
  83. # Ref: https://stackoverflow.com/questions/2525327/regex-for-a-za-z0-9
  84. # -with-dashes-allowed-in-between-but-not-at-the-start-or-e
  85. CB_NAME_PATTERN = re.compile(r"^[a-z][-a-z0-9]{1,61}[a-z0-9]$")
  86. def __init__(self, provider: CloudProvider) -> None:
  87. self.__provider = provider
  88. @staticmethod
  89. def is_valid_resource_name(name: str) -> bool:
  90. if not name:
  91. return False
  92. else:
  93. return (True if BaseCloudResource.CB_NAME_PATTERN.match(name)
  94. else False)
  95. @staticmethod
  96. def assert_valid_resource_label(name: str) -> None:
  97. if not BaseCloudResource.is_valid_resource_name(name):
  98. log.debug("InvalidLabelException raised on %s", name)
  99. raise InvalidLabelException(
  100. u"Invalid label: %s. Label must be at least 3 characters long"
  101. " and at most 63 characters. It must consist of lowercase"
  102. " letters, numbers, or dashes. The label must start with a "
  103. "letter and not end with a dash." % name)
  104. @staticmethod
  105. def assert_valid_resource_name(name: str) -> None:
  106. if not BaseCloudResource.is_valid_resource_name(name):
  107. log.debug("InvalidLabelException raised on %s", name)
  108. raise InvalidNameException(
  109. u"Invalid name: %s. Name must be at least 3 characters long"
  110. " and at most 63 characters. It must consist of lowercase"
  111. " letters, numbers, or dashes. The name must not start or"
  112. " end with a dash." % name)
  113. @staticmethod
  114. def _generate_name_from_label(label: str | None, default: str) -> str:
  115. if not label:
  116. label = default
  117. name = label[:55] + '-' + uuid.uuid4().hex[:6]
  118. BaseCloudResource.assert_valid_resource_name(name)
  119. return name
  120. @property
  121. def _provider(self) -> CloudProvider:
  122. return self.__provider
  123. def to_json(self) -> dict[str, Any]:
  124. # Get all attributes but filter methods and private/magic ones
  125. attr = inspect.getmembers(self, lambda a: not (inspect.isroutine(a)))
  126. js = {k: v for (k, v) in attr if not k.startswith('_')}
  127. return js
  128. def __repr__(self) -> str:
  129. name_or_label = getattr(self, 'label', self.name)
  130. if name_or_label == self.id:
  131. return "<CB-{0}: {1}>".format(
  132. self.__class__.__name__, self.id)
  133. else:
  134. return "<CB-{0}: {1} ({2})>".format(
  135. self.__class__.__name__, name_or_label, self.id)
  136. class BaseObjectLifeCycleMixin(ObjectLifeCycleMixin):
  137. """
  138. A base implementation of an ObjectLifeCycleMixin.
  139. This base implementation has an implementation of wait_for
  140. which refreshes the object's state till the desired ready states
  141. are reached. Subclasses must still implement the wait_till_ready
  142. method, since the desired ready states are object specific.
  143. """
  144. def wait_for(self, target_states: list[str],
  145. terminal_states: list[str] | None = None,
  146. timeout: int | None = None,
  147. interval: int | None = None) -> bool:
  148. if timeout is None:
  149. timeout = self._provider.config.default_wait_timeout
  150. if interval is None:
  151. interval = self._provider.config.default_wait_interval
  152. assert timeout >= 0
  153. assert interval >= 0
  154. assert timeout >= interval
  155. end_time = time.time() + timeout
  156. while self.state not in target_states:
  157. if self.state in (terminal_states or []):
  158. raise WaitStateException(
  159. "Object: {0} is in state: {1} which is a terminal state"
  160. " and cannot be waited on.".format(self, self.state))
  161. else:
  162. log.debug(
  163. "Object %s is in state: %s. Waiting another %s"
  164. " seconds to reach target state(s): %s...",
  165. self,
  166. self.state,
  167. int(end_time - time.time()),
  168. target_states)
  169. time.sleep(interval)
  170. if time.time() > end_time:
  171. raise WaitStateException(
  172. "Waited too long for object: {0} to reach a desired"
  173. "state: {1}. It's still in state: {2}".format(
  174. self, target_states, self.state))
  175. self.refresh()
  176. log.debug("Object: %s successfully reached target state: %s",
  177. self, self.state)
  178. return True
  179. class BaseResultList(ResultList[T]):
  180. def __init__(
  181. self, is_truncated: bool, marker: str | None,
  182. supports_total: bool, total: int | None = None,
  183. data: Sequence[T] | None = None) -> None:
  184. # call list constructor
  185. super(BaseResultList, self).__init__(data or [])
  186. self._marker = marker
  187. self._is_truncated = is_truncated
  188. self._supports_total = True if supports_total else False
  189. self._total = total
  190. @property
  191. def marker(self) -> str | None:
  192. return self._marker
  193. @property
  194. def is_truncated(self) -> bool:
  195. return self._is_truncated
  196. @property
  197. def supports_total(self) -> bool:
  198. return self._supports_total
  199. @property
  200. def total_results(self) -> int:
  201. return cast(int, self._total)
  202. class ServerPagedResultList(BaseResultList[T]):
  203. """
  204. This is a convenience class that extends the :class:`BaseResultList` class
  205. and provides a server side implementation of paging. It is meant for use by
  206. provider developers and is not meant for direct use by end-users.
  207. This class can be used to wrap a partial result list when an operation
  208. supports server side paging.
  209. """
  210. @property
  211. def supports_server_paging(self) -> bool:
  212. return True
  213. @property
  214. def data(self) -> list[T]:
  215. raise NotImplementedError(
  216. "ServerPagedResultLists do not support the data property")
  217. class ClientPagedResultList(BaseResultList[T]):
  218. """
  219. This is a convenience class that extends the :class:`BaseResultList` class
  220. and provides a client side implementation of paging. It is meant for use by
  221. provider developers and is not meant for direct use by end-users.
  222. This class can be used to wrap a full result list when an operation does
  223. not support server side paging. This class will then provide a paged view
  224. of the full result set entirely on the client side.
  225. """
  226. def __init__(self, provider: CloudProvider, objects: Sequence[T],
  227. limit: int | None = None, marker: str | None = None) -> None:
  228. self._objects = list(objects)
  229. limit = limit or provider.config.default_result_limit
  230. total_size = len(objects)
  231. if marker:
  232. from_marker = itertools.dropwhile(
  233. lambda obj: not cast(CloudResource, obj).id == marker, objects)
  234. # skip one past the marker
  235. next(from_marker, None)
  236. objects = list(from_marker)
  237. is_truncated = len(objects) > limit
  238. results = list(itertools.islice(objects, limit))
  239. super(ClientPagedResultList, self).__init__(
  240. is_truncated,
  241. cast(CloudResource, results[-1]).id if is_truncated else None,
  242. True, total=total_size,
  243. data=results)
  244. @property
  245. def supports_server_paging(self) -> bool:
  246. return False
  247. @property
  248. def data(self) -> list[T]:
  249. return self._objects
  250. class BasePageableObjectMixin(PageableObjectMixin[T]):
  251. """
  252. A mixin to provide iteration capability for a class
  253. that support a list(limit, marker) method.
  254. """
  255. def __iter__(self) -> Iterator[T]:
  256. for result in self.iter():
  257. yield result
  258. def iter(self, **kwargs: Any) -> Iterator[T]:
  259. result_list = self.list(**kwargs)
  260. if result_list.supports_server_paging:
  261. for result in result_list:
  262. yield result
  263. while result_list.is_truncated:
  264. result_list = self.list(marker=result_list.marker, **kwargs)
  265. for result in result_list:
  266. yield result
  267. else:
  268. for result in result_list.data:
  269. yield result
  270. class BaseVMType(BaseCloudResource, VMType):
  271. def __init__(self, provider: CloudProvider) -> None:
  272. super(BaseVMType, self).__init__(provider)
  273. def __eq__(self, other: object) -> bool:
  274. return (isinstance(other, VMType) and
  275. # pylint:disable=protected-access
  276. self._provider == other._provider and
  277. self.id == other.id)
  278. @property
  279. def size_total_disk(self) -> int:
  280. return self.size_root_disk + self.size_ephemeral_disks
  281. class BaseInstance(BaseCloudResource, BaseObjectLifeCycleMixin, Instance):
  282. def __init__(self, provider: CloudProvider) -> None:
  283. super(BaseInstance, self).__init__(provider)
  284. def __eq__(self, other: object) -> bool:
  285. return (isinstance(other, Instance) and
  286. # pylint:disable=protected-access
  287. self._provider == other._provider and
  288. self.id == other.id and
  289. # check from most to least likely mutables
  290. self.state == other.state and
  291. self.label == other.label and
  292. self.vm_firewalls == other.vm_firewalls and
  293. self.public_ips == other.public_ips and
  294. self.private_ips == other.private_ips and
  295. self.image_id == other.image_id)
  296. def wait_till_ready(
  297. self, timeout: int | None = None,
  298. interval: int | None = None) -> None:
  299. self.wait_for(
  300. [InstanceState.RUNNING],
  301. terminal_states=[InstanceState.DELETED, InstanceState.ERROR],
  302. timeout=timeout,
  303. interval=interval)
  304. def delete(self) -> None:
  305. # InstanceService.delete is implemented by every provider but is not
  306. # declared on the public typed interface, hence the ignore.
  307. self._provider.compute.instances.delete(self) # type: ignore[attr-defined]
  308. class BaseLaunchConfig(LaunchConfig):
  309. def __init__(self, provider: CloudProvider) -> None:
  310. self.provider = provider
  311. self.block_devices: list[BaseLaunchConfig.BlockDeviceMapping] = []
  312. class BlockDeviceMapping(object):
  313. """
  314. Represents a block device mapping
  315. """
  316. def __init__(self, is_volume: bool = False,
  317. source: Volume | Snapshot | MachineImage | None = None,
  318. is_root: bool | None = None, size: int | None = None,
  319. delete_on_terminate: bool | None = None) -> None:
  320. self.is_volume = is_volume
  321. self.source = source
  322. self.is_root = is_root
  323. self.size = size
  324. self.delete_on_terminate = delete_on_terminate
  325. def add_ephemeral_device(self) -> None:
  326. block_device = BaseLaunchConfig.BlockDeviceMapping()
  327. self.block_devices.append(block_device)
  328. def add_volume_device(
  329. self, source: Volume | Snapshot | MachineImage | None = None,
  330. is_root: bool | None = None, size: int | None = None,
  331. delete_on_terminate: bool | None = None) -> None:
  332. block_device = self._validate_volume_device(
  333. source=source, is_root=is_root, size=size,
  334. delete_on_terminate=delete_on_terminate)
  335. log.debug("Appending %s to the block_devices list",
  336. block_device)
  337. self.block_devices.append(block_device)
  338. def _validate_volume_device(
  339. self, source: Volume | Snapshot | MachineImage | None = None,
  340. is_root: bool | None = None, size: int | None = None,
  341. delete_on_terminate: bool | None = None
  342. ) -> "BaseLaunchConfig.BlockDeviceMapping":
  343. """
  344. Validates a volume based device and throws an
  345. InvalidConfigurationException if the configuration is incorrect.
  346. """
  347. if source is None and not size:
  348. log.exception("InvalidConfigurationException raised: "
  349. "no size argument specified.")
  350. raise InvalidConfigurationException(
  351. "A size must be specified for a blank new volume.")
  352. if source and \
  353. not isinstance(source, (Snapshot, Volume, MachineImage)):
  354. log.exception("InvalidConfigurationException raised: "
  355. "source argument not specified correctly.")
  356. raise InvalidConfigurationException(
  357. "Source must be a Snapshot, Volume, MachineImage, or None.")
  358. if size:
  359. if not isinstance(size, int) or not size > 0:
  360. log.exception("InvalidConfigurationException raised: "
  361. "size argument must be an integer greater than "
  362. "0. Got type %s and value %s.", type(size), size)
  363. raise InvalidConfigurationException(
  364. "The size must be None or an integer greater than 0.")
  365. if is_root:
  366. for bd in self.block_devices:
  367. if bd.is_root:
  368. log.exception("InvalidConfigurationException raised: "
  369. "%s has already been marked as the root "
  370. "block device.", bd)
  371. raise InvalidConfigurationException(
  372. "An existing block device: {0} has already been"
  373. " marked as root. There can only be one root device.")
  374. return BaseLaunchConfig.BlockDeviceMapping(
  375. is_volume=True, source=source, is_root=is_root, size=size,
  376. delete_on_terminate=delete_on_terminate)
  377. class BaseMachineImage(
  378. BaseCloudResource, BaseObjectLifeCycleMixin, MachineImage):
  379. def __init__(self, provider: CloudProvider) -> None:
  380. super(BaseMachineImage, self).__init__(provider)
  381. def __eq__(self, other: object) -> bool:
  382. return (isinstance(other, MachineImage) and
  383. # pylint:disable=protected-access
  384. self._provider == other._provider and
  385. self.id == other.id and
  386. # check from most to least likely mutables
  387. self.state == other.state and
  388. self.label == other.label and
  389. self.description == other.description)
  390. def wait_till_ready(
  391. self, timeout: int | None = None,
  392. interval: int | None = None) -> None:
  393. self.wait_for(
  394. [MachineImageState.AVAILABLE],
  395. terminal_states=[MachineImageState.ERROR],
  396. timeout=timeout,
  397. interval=interval)
  398. class BaseAttachmentInfo(AttachmentInfo):
  399. def __init__(self, volume: Volume, instance_id: str, device: str) -> None:
  400. self._volume = volume
  401. self._instance_id = instance_id
  402. self._device = device
  403. @property
  404. def volume(self) -> Volume:
  405. return self._volume
  406. @property
  407. def instance_id(self) -> str:
  408. return self._instance_id
  409. @property
  410. def device(self) -> str:
  411. return self._device
  412. class BaseVolume(BaseCloudResource, BaseObjectLifeCycleMixin, Volume):
  413. def __init__(self, provider: CloudProvider) -> None:
  414. super(BaseVolume, self).__init__(provider)
  415. def __eq__(self, other: object) -> bool:
  416. return (isinstance(other, Volume) and
  417. # pylint:disable=protected-access
  418. self._provider == other._provider and
  419. self.id == other.id and
  420. # check from most to least likely mutables
  421. self.state == other.state and
  422. self.label == other.label)
  423. def wait_till_ready(
  424. self, timeout: int | None = None,
  425. interval: int | None = None) -> None:
  426. self.wait_for(
  427. [VolumeState.AVAILABLE],
  428. terminal_states=[VolumeState.ERROR, VolumeState.DELETED],
  429. timeout=timeout,
  430. interval=interval)
  431. def delete(self) -> None:
  432. """
  433. Delete this volume.
  434. """
  435. return self._provider.storage.volumes.delete(self)
  436. class BaseSnapshot(BaseCloudResource, BaseObjectLifeCycleMixin, Snapshot):
  437. def __init__(self, provider: CloudProvider) -> None:
  438. super(BaseSnapshot, self).__init__(provider)
  439. def __eq__(self, other: object) -> bool:
  440. return (isinstance(other, Snapshot) and
  441. # pylint:disable=protected-access
  442. self._provider == other._provider and
  443. self.id == other.id and
  444. # check from most to least likely mutables
  445. self.state == other.state and
  446. self.label == other.label)
  447. def wait_till_ready(
  448. self, timeout: int | None = None,
  449. interval: int | None = None) -> None:
  450. self.wait_for(
  451. [SnapshotState.AVAILABLE],
  452. terminal_states=[SnapshotState.ERROR],
  453. timeout=timeout,
  454. interval=interval)
  455. def delete(self) -> None:
  456. """
  457. Delete this snapshot.
  458. """
  459. return self._provider.storage.snapshots.delete(self)
  460. class BaseKeyPair(BaseCloudResource, KeyPair):
  461. def __init__(self, provider: CloudProvider, key_pair: Any) -> None:
  462. super(BaseKeyPair, self).__init__(provider)
  463. self._key_pair = key_pair
  464. self._private_material: str | None = None
  465. def __eq__(self, other: object) -> bool:
  466. return (isinstance(other, KeyPair) and
  467. # pylint:disable=protected-access
  468. self._provider == other._provider and
  469. self.name == other.name)
  470. @property
  471. def id(self) -> str:
  472. """
  473. Return the id of this key pair.
  474. """
  475. return cast(str, self._key_pair.name)
  476. @property
  477. def name(self) -> str:
  478. """
  479. Return the name of this key pair.
  480. """
  481. return self.id
  482. @property
  483. def material(self) -> str | None:
  484. return self._private_material
  485. @material.setter
  486. # pylint:disable=arguments-differ
  487. def material(self, value: str | None) -> None:
  488. self._private_material = value
  489. def delete(self) -> None:
  490. self._provider.security.key_pairs.delete(self)
  491. class BaseVMFirewall(BaseCloudResource, VMFirewall):
  492. def __init__(self, provider: CloudProvider, vm_firewall: Any) -> None:
  493. super(BaseVMFirewall, self).__init__(provider)
  494. self._vm_firewall = vm_firewall
  495. def __eq__(self, other: object) -> bool:
  496. """
  497. Check if all the defined rules match across both VM firewalls.
  498. """
  499. return (isinstance(other, VMFirewall) and
  500. # pylint:disable=protected-access
  501. self._provider == other._provider and
  502. set(self.rules) == set(other.rules))
  503. def __ne__(self, other: object) -> bool:
  504. return not self.__eq__(other)
  505. @property
  506. def id(self) -> str:
  507. """
  508. Get the ID of this VM firewall.
  509. :rtype: str
  510. :return: VM firewall ID
  511. """
  512. return cast(str, self._vm_firewall.id)
  513. @property
  514. def name(self) -> str:
  515. """
  516. Return the name of this VM firewall.
  517. """
  518. return self.id
  519. @property
  520. def description(self) -> str | None:
  521. """
  522. Return the description of this VM firewall.
  523. """
  524. return cast("str | None", self._vm_firewall.description)
  525. def delete(self) -> None:
  526. """
  527. Delete this VM firewall.
  528. """
  529. return self._provider.security.vm_firewalls.delete(self)
  530. class BaseVMFirewallRule(BaseCloudResource, VMFirewallRule):
  531. def __init__(self, parent_fw: VMFirewall, rule: Any) -> None:
  532. # pylint:disable=protected-access
  533. super(BaseVMFirewallRule, self).__init__(
  534. parent_fw._provider)
  535. self.firewall = parent_fw
  536. self._rule = rule
  537. # Cache name
  538. self._name = "{0}-{1}-{2}-{3}-{4}-{5}".format(
  539. self.direction, self.protocol, self.from_port, self.to_port,
  540. self.cidr, self.src_dest_fw_id).lower()
  541. @property
  542. def name(self) -> str:
  543. return self._name
  544. def __repr__(self) -> str:
  545. return ("<{0}: id: {1}; direction: {2}; protocol: {3}; from: {4};"
  546. " to: {5}; cidr: {6}, src_dest_fw: {7}>"
  547. .format(self.__class__.__name__, self.id, self.direction,
  548. self.protocol, self.from_port, self.to_port, self.cidr,
  549. self.src_dest_fw_id))
  550. def __eq__(self, other: object) -> bool:
  551. return (isinstance(other, VMFirewallRule) and
  552. self.direction == other.direction and
  553. self.protocol == other.protocol and
  554. self.from_port == other.from_port and
  555. self.to_port == other.to_port and
  556. self.cidr == other.cidr and
  557. self.src_dest_fw_id == other.src_dest_fw_id)
  558. def __ne__(self, other: object) -> bool:
  559. return not self.__eq__(other)
  560. def __hash__(self) -> int:
  561. """
  562. Return a hash-based interpretation of all of the object's field values.
  563. This is requeried for operations on hashed collections including
  564. ``set``, ``frozenset``, and ``dict``.
  565. """
  566. return hash("{0}{1}{2}{3}{4}{5}".format(
  567. self.direction, self.protocol, self.from_port, self.to_port,
  568. self.cidr, self.src_dest_fw_id))
  569. def to_json(self) -> dict[str, Any]:
  570. attr = inspect.getmembers(self, lambda a: not (inspect.isroutine(a)))
  571. js = {k: v for (k, v) in attr if not k.startswith('_')}
  572. js['src_dest_fw'] = self.src_dest_fw_id
  573. js['firewall'] = self.firewall.id
  574. return js
  575. def delete(self) -> None:
  576. # The interface types the second arg as a rule_id (str), but every
  577. # provider's _vm_firewall_rules.delete accepts the rule object itself.
  578. self._provider.security._vm_firewall_rules.delete(
  579. self.firewall, self) # type: ignore[arg-type]
  580. class BasePlacementZone(BaseCloudResource, PlacementZone):
  581. def __init__(self, provider: CloudProvider) -> None:
  582. super(BasePlacementZone, self).__init__(provider)
  583. def __eq__(self, other: object) -> bool:
  584. return (isinstance(other, PlacementZone) and
  585. # pylint:disable=protected-access
  586. self._provider == other._provider and
  587. self.id == other.id)
  588. class BaseRegion(BaseCloudResource, Region):
  589. def __init__(self, provider: CloudProvider) -> None:
  590. super(BaseRegion, self).__init__(provider)
  591. def __eq__(self, other: object) -> bool:
  592. return (isinstance(other, Region) and
  593. # pylint:disable=protected-access
  594. self._provider == other._provider and
  595. self.id == other.id)
  596. def to_json(self) -> dict[str, Any]:
  597. attr = inspect.getmembers(self, lambda a: not (inspect.isroutine(a)))
  598. js = {k: v for (k, v) in attr if not k.startswith('_')}
  599. js['zones'] = [z.id for z in self.zones]
  600. return js
  601. @property
  602. def default_zone(self) -> PlacementZone:
  603. return next(iter(self.zones))
  604. class BaseUploadPart(UploadPart):
  605. """
  606. A simple, serializable handle for a single uploaded part. Concrete
  607. providers return these from ``upload_part`` and consume them in
  608. ``complete_multipart_upload``.
  609. """
  610. def __init__(self, part_number: int, etag: object) -> None:
  611. self._part_number = part_number
  612. self._etag = etag
  613. @property
  614. def part_number(self) -> int:
  615. return self._part_number
  616. @property
  617. def etag(self) -> object:
  618. return self._etag
  619. def __repr__(self) -> str:
  620. return "<CB-{0}: {1} ({2})>".format(
  621. self.__class__.__name__, self._part_number, self._etag)
  622. class BaseMultipartUpload(BaseCloudResource, MultipartUpload):
  623. """
  624. Base implementation of an in-progress multipart upload. It is a thin
  625. handle that delegates the actual work to the provider's bucket-object
  626. service, mirroring how other base resources delegate to their service
  627. (e.g. ``BaseBucket.delete``).
  628. """
  629. def __init__(self, provider: CloudProvider, bucket: Bucket,
  630. object_name: str, upload_id: str) -> None:
  631. super(BaseMultipartUpload, self).__init__(provider)
  632. self._bucket = bucket
  633. self._object_name = object_name
  634. self._upload_id = upload_id
  635. @property
  636. def id(self) -> str:
  637. return self._upload_id
  638. @property
  639. def name(self) -> str:
  640. return self._object_name
  641. @property
  642. def bucket(self) -> Bucket:
  643. return self._bucket
  644. @property
  645. def object_name(self) -> str:
  646. return self._object_name
  647. def upload_part(self, part_number: int,
  648. data: bytes | IO[bytes]) -> UploadPart:
  649. # pylint:disable=protected-access
  650. # _bucket_objects is a provider-internal service not exposed on the
  651. # public StorageService interface, hence the typed cast + ignore.
  652. return self._bucket_objects.upload_part(
  653. self._bucket, self, part_number, data)
  654. def complete(self, parts: list[UploadPart]) -> BucketObject:
  655. # pylint:disable=protected-access
  656. return self._bucket_objects.complete_multipart_upload(
  657. self._bucket, self, parts)
  658. def abort(self) -> None:
  659. # pylint:disable=protected-access
  660. return self._bucket_objects.abort_multipart_upload(
  661. self._bucket, self)
  662. @property
  663. def _bucket_objects(self) -> "BucketObjectService":
  664. # _bucket_objects is a provider-internal service not exposed on the
  665. # public StorageService interface, hence the typed cast + ignore.
  666. return cast(
  667. "BucketObjectService",
  668. self._provider.storage._bucket_objects) # type: ignore[attr-defined]
  669. class BaseBucketObject(BaseCloudResource, BucketObject):
  670. # Regular expression for valid bucket keys.
  671. # They, must match the following criteria: http://docs.aws.amazon.com/"
  672. # AmazonS3/latest/dev/UsingMetadata.html#object-key-guidelines
  673. #
  674. # Note: The following regex is based on: https://stackoverflow.com/question
  675. # s/537772/what-is-the-most-correct-regular-expression-for-a-unix-file-path
  676. CB_NAME_PATTERN = re.compile(r"[^\0]+")
  677. # Uploads larger than this many bytes are split into parts.
  678. CB_MULTIPART_THRESHOLD = int(os.environ.get(
  679. 'CB_MULTIPART_THRESHOLD', 100 * 1024 * 1024)) # 100 MiB
  680. # The size of each part for multipart uploads.
  681. CB_MULTIPART_PART_SIZE = int(os.environ.get(
  682. 'CB_MULTIPART_PART_SIZE', 50 * 1024 * 1024)) # 50 MiB
  683. # Portable floor: S3 and Swift reject non-final parts smaller than 5 MiB,
  684. # so part sizes below this are rejected up-front.
  685. CB_MULTIPART_MIN_PART_SIZE = 5 * 1024 * 1024
  686. # Number of parts uploaded in parallel by the transparent multipart path.
  687. CB_MULTIPART_MAX_CONCURRENCY = int(os.environ.get(
  688. 'CB_MULTIPART_MAX_CONCURRENCY', 5))
  689. def __init__(self, provider: CloudProvider) -> None:
  690. super(BaseBucketObject, self).__init__(provider)
  691. @property
  692. def bucket(self) -> Bucket:
  693. # Provider-implemented; every concrete BucketObject knows its bucket.
  694. raise NotImplementedError(
  695. "BucketObject subclasses must implement the bucket property")
  696. def _upload_single_shot(
  697. self, data: str | bytes | IO[bytes]) -> BucketObject:
  698. # Provider-implemented single-shot (non-multipart) upload.
  699. raise NotImplementedError(
  700. "BucketObject subclasses must implement _upload_single_shot")
  701. @property
  702. def _bucket_objects(self) -> "BucketObjectService":
  703. # _bucket_objects is a provider-internal service not exposed on the
  704. # public StorageService interface, hence the typed cast + ignore.
  705. return cast(
  706. "BucketObjectService",
  707. self._provider.storage._bucket_objects) # type: ignore[attr-defined]
  708. @staticmethod
  709. def is_valid_resource_name(name: str) -> bool:
  710. return (True if BaseBucketObject.CB_NAME_PATTERN.match(name)
  711. else False)
  712. @staticmethod
  713. def assert_valid_resource_name(name: str) -> None:
  714. if not BaseBucketObject.is_valid_resource_name(name):
  715. log.debug("InvalidLabelException raised on %s", name,
  716. exc_info=True)
  717. raise InvalidLabelException(
  718. u"Invalid object name: %s. Name must match criteria defined "
  719. "in: http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMeta"
  720. "data.html#object-key-guidelines" % name)
  721. def save_content(self, target_stream: IO[bytes]) -> None:
  722. # iter_content() is declared Iterable[bytes] on the interface, but the
  723. # concrete objects returned by providers also support .read(); cast so
  724. # copyfileobj accepts it without changing behavior.
  725. shutil.copyfileobj(
  726. cast("SupportsRead[bytes]", self.iter_content()), target_stream)
  727. # The three resolvers below pick, in order of precedence: an explicit
  728. # per-call UploadConfig field, the provider/global config, then the class
  729. # default constant.
  730. def _multipart_threshold(self, config: UploadConfig | None = None) -> int:
  731. if config is not None and config.threshold is not None:
  732. return int(config.threshold)
  733. # pylint:disable=protected-access
  734. # _get_config_value is a provider-internal helper not on the public
  735. # CloudProvider interface, hence the ignore.
  736. return int(self._provider._get_config_value( # type: ignore[attr-defined]
  737. 'multipart_threshold', self.CB_MULTIPART_THRESHOLD))
  738. def _multipart_part_size(self, config: UploadConfig | None = None) -> int:
  739. if config is not None and config.part_size is not None:
  740. return int(config.part_size)
  741. # pylint:disable=protected-access
  742. return int(self._provider._get_config_value( # type: ignore[attr-defined]
  743. 'multipart_part_size', self.CB_MULTIPART_PART_SIZE))
  744. def _multipart_max_concurrency(
  745. self, config: UploadConfig | None = None) -> int:
  746. if config is not None and config.max_concurrency is not None:
  747. return int(config.max_concurrency)
  748. # pylint:disable=protected-access
  749. return int(self._provider._get_config_value( # type: ignore[attr-defined]
  750. 'multipart_max_concurrency', self.CB_MULTIPART_MAX_CONCURRENCY))
  751. @staticmethod
  752. def _data_size(data: str | bytes | IO[bytes]) -> int | None:
  753. """
  754. Best-effort size of an upload payload, or ``None`` if it cannot be
  755. determined without consuming the data (e.g. a non-seekable stream).
  756. """
  757. if isinstance(data, str):
  758. return len(data.encode('utf-8'))
  759. if isinstance(data, (bytes, bytearray)):
  760. return len(data)
  761. if hasattr(data, 'seek') and hasattr(data, 'tell'):
  762. try:
  763. pos = data.tell()
  764. data.seek(0, os.SEEK_END)
  765. size = data.tell()
  766. data.seek(pos)
  767. return size
  768. except (OSError, ValueError):
  769. return None
  770. return None
  771. @staticmethod
  772. def _as_stream(data: str | bytes | IO[bytes]) -> IO[bytes]:
  773. if isinstance(data, str):
  774. data = data.encode('utf-8')
  775. if isinstance(data, (bytes, bytearray)):
  776. return io.BytesIO(data)
  777. return data
  778. def upload(self, data: str | bytes | IO[bytes],
  779. config: UploadConfig | None = None) -> BucketObject:
  780. size = self._data_size(data)
  781. if size is not None and size > self._multipart_threshold(config):
  782. return self._upload_multipart(self._as_stream(data), config)
  783. return self._upload_single_shot(data)
  784. def upload_from_file(
  785. self, path: str,
  786. config: UploadConfig | None = None) -> BucketObject:
  787. if os.path.getsize(path) > self._multipart_threshold(config):
  788. with open(path, 'rb') as f:
  789. return self._upload_multipart(f, config)
  790. return self._upload_from_file_single_shot(path)
  791. def _upload_multipart(self, stream: IO[bytes],
  792. config: UploadConfig | None = None) -> BucketObject:
  793. """
  794. Drive the explicit multipart lifecycle over a stream, reading it one
  795. part at a time so the whole payload is never held in memory.
  796. Parts are uploaded across a bounded thread pool. To stay safe even on
  797. providers whose SDK client/connection is not thread-safe, each worker
  798. uploads through its own cloned provider (see :meth:`.CloudProvider.
  799. clone`), so no provider state is shared between threads. Any failure
  800. aborts the upload to avoid leaking staged parts.
  801. Providers with an efficient, thread-safe native uploader (e.g. AWS via
  802. boto3's ``upload_fileobj``) override this method to use it directly.
  803. """
  804. part_size = self._multipart_part_size(config)
  805. if part_size < self.CB_MULTIPART_MIN_PART_SIZE:
  806. raise InvalidValueException('multipart_part_size', part_size)
  807. concurrency = max(1, self._multipart_max_concurrency(config))
  808. upload = self.create_multipart_upload()
  809. try:
  810. if concurrency == 1:
  811. parts = self._upload_parts_serially(upload, stream, part_size)
  812. else:
  813. parts = self._upload_parts_concurrently(
  814. upload, stream, part_size, concurrency)
  815. return upload.complete(parts)
  816. except Exception:
  817. upload.abort()
  818. raise
  819. def _upload_parts_serially(self, upload: MultipartUpload,
  820. stream: IO[bytes],
  821. part_size: int) -> list[UploadPart]:
  822. parts = []
  823. part_number = 1
  824. while True:
  825. chunk = self._read_part(stream, part_size)
  826. if not chunk:
  827. break
  828. parts.append(upload.upload_part(part_number, chunk))
  829. part_number += 1
  830. return parts
  831. def _upload_parts_concurrently(self, upload: MultipartUpload,
  832. stream: IO[bytes], part_size: int,
  833. concurrency: int) -> list[UploadPart]:
  834. # A pool of cloned bucket-object services, one per worker, so each
  835. # thread touches an isolated provider/connection.
  836. clones: "queue.Queue[BucketObjectService]" = queue.Queue()
  837. for _ in range(concurrency):
  838. # pylint:disable=protected-access
  839. # _bucket_objects is a provider-internal service not exposed on the
  840. # public StorageService interface, hence the typed cast + ignore.
  841. clones.put(cast(
  842. "BucketObjectService",
  843. self._provider.clone().storage._bucket_objects)) # type: ignore[attr-defined]
  844. def upload_one(part_number: int, chunk: bytes) -> UploadPart:
  845. service = clones.get()
  846. try:
  847. return service.upload_part(
  848. upload.bucket, upload, part_number, chunk)
  849. finally:
  850. clones.put(service)
  851. parts: list[UploadPart] = []
  852. in_flight: set[Future[UploadPart]] = set()
  853. part_number = 1
  854. depleted = False
  855. with ThreadPoolExecutor(max_workers=concurrency) as executor:
  856. while not depleted or in_flight:
  857. # Keep the pool fed but never read more than ``concurrency``
  858. # parts ahead, bounding memory to ~concurrency * part_size.
  859. while not depleted and len(in_flight) < concurrency:
  860. chunk = self._read_part(stream, part_size)
  861. if not chunk:
  862. depleted = True
  863. break
  864. in_flight.add(
  865. executor.submit(upload_one, part_number, chunk))
  866. part_number += 1
  867. if not in_flight:
  868. break
  869. done, in_flight = wait(
  870. in_flight, return_when=FIRST_COMPLETED)
  871. for future in done:
  872. parts.append(future.result())
  873. return parts
  874. @staticmethod
  875. def _read_part(stream: IO[bytes], part_size: int) -> bytes:
  876. """
  877. Read exactly ``part_size`` bytes from ``stream`` (fewer only at EOF),
  878. coalescing short reads so non-final parts always meet the provider
  879. minimum part size.
  880. """
  881. buffer = bytearray()
  882. while len(buffer) < part_size:
  883. chunk = stream.read(part_size - len(buffer))
  884. if not chunk:
  885. break
  886. buffer.extend(chunk)
  887. return bytes(buffer)
  888. def _upload_from_file_single_shot(
  889. self, path: str) -> BucketObject:
  890. """
  891. Default small-file upload: read the file and hand it to the provider's
  892. single-shot upload. Providers with a more efficient native file upload
  893. (e.g. AWS ``upload_file``) override :meth:`upload_from_file` directly.
  894. """
  895. with open(path, 'rb') as f:
  896. return self._upload_single_shot(f)
  897. def create_multipart_upload(self) -> MultipartUpload:
  898. # pylint:disable=protected-access
  899. return self._bucket_objects.create_multipart_upload(
  900. self.bucket, self.name)
  901. def __eq__(self, other: object) -> bool:
  902. return (isinstance(other, BucketObject) and
  903. # pylint:disable=protected-access
  904. self._provider == other._provider and
  905. self.id == other.id and
  906. # check from most to least likely mutables
  907. self.name == other.name)
  908. class BaseBucket(BaseCloudResource, Bucket):
  909. def __init__(self, provider: CloudProvider) -> None:
  910. super(BaseBucket, self).__init__(provider)
  911. def __eq__(self, other: object) -> bool:
  912. return (isinstance(other, Bucket) and
  913. # pylint:disable=protected-access
  914. self._provider == other._provider and
  915. self.id == other.id and
  916. # check from most to least likely mutables
  917. self.name == other.name)
  918. def delete(self, delete_contents: bool = False) -> None:
  919. """
  920. Delete this bucket.
  921. """
  922. if delete_contents:
  923. for obj in self.objects:
  924. obj.delete()
  925. # BucketService.delete is implemented by every provider but is not
  926. # declared on the public typed interface, hence the ignore.
  927. self._provider.storage.buckets.delete(self.id) # type: ignore[attr-defined]
  928. # TODO: Discuss creating `create_object` method, or change docs
  929. class BaseNetwork(BaseCloudResource, BaseObjectLifeCycleMixin, Network):
  930. CB_DEFAULT_NETWORK_LABEL = os.environ.get('CB_DEFAULT_NETWORK_LABEL',
  931. 'cloudbridge-net')
  932. CB_DEFAULT_IPV4RANGE = os.environ.get('CB_DEFAULT_IPV4RANGE',
  933. u'10.0.0.0/16')
  934. def __init__(self, provider: CloudProvider) -> None:
  935. super(BaseNetwork, self).__init__(provider)
  936. @staticmethod
  937. def cidr_blocks_overlap(block1: str, block2: str) -> bool:
  938. common_length = min(int(block1.split('/')[1]),
  939. int(block2.split('/')[1]))
  940. p1 = [format(int(b), '08b') for b in block1.split('/')[0].split('.')]
  941. prefix1 = ''.join(p1)[:common_length]
  942. p2 = [format(int(b), '08b') for b in block2.split('/')[0].split('.')]
  943. prefix2 = ''.join(p2)[:common_length]
  944. return prefix1 == prefix2
  945. def wait_till_ready(
  946. self, timeout: int | None = None,
  947. interval: int | None = None) -> None:
  948. self.wait_for(
  949. [NetworkState.AVAILABLE],
  950. terminal_states=[NetworkState.ERROR],
  951. timeout=timeout,
  952. interval=interval)
  953. def delete(self) -> None:
  954. self._provider.networking.networks.delete(self)
  955. def __eq__(self, other: object) -> bool:
  956. return (isinstance(other, Network) and
  957. # pylint:disable=protected-access
  958. self._provider == other._provider and
  959. self.id == other.id)
  960. class BaseSubnet(BaseCloudResource, BaseObjectLifeCycleMixin, Subnet):
  961. CB_DEFAULT_SUBNET_LABEL = os.environ.get('CB_DEFAULT_SUBNET_LABEL',
  962. 'cloudbridge-subnet')
  963. CB_DEFAULT_SUBNET_IPV4RANGE = os.environ.get('CB_DEFAULT_SUBNET_IPV4RANGE',
  964. '10.0.0.0/24')
  965. def __init__(self, provider: CloudProvider) -> None:
  966. super(BaseSubnet, self).__init__(provider)
  967. def __eq__(self, other: object) -> bool:
  968. return (isinstance(other, Subnet) and
  969. # pylint:disable=protected-access
  970. self._provider == other._provider and
  971. self.id == other.id)
  972. @property
  973. def network(self) -> Network:
  974. # The parent network of an existing subnet always resolves; the
  975. # service get() is typed Network | None, so narrow to Network.
  976. return cast(
  977. Network, self._provider.networking.networks.get(self.network_id))
  978. def wait_till_ready(
  979. self, timeout: int | None = None,
  980. interval: int | None = None) -> None:
  981. self.wait_for(
  982. [SubnetState.AVAILABLE],
  983. terminal_states=[SubnetState.ERROR],
  984. timeout=timeout,
  985. interval=interval)
  986. def delete(self) -> None:
  987. self._provider.networking.subnets.delete(self)
  988. class BaseFloatingIP(BaseCloudResource, BaseObjectLifeCycleMixin, FloatingIP):
  989. def __init__(self, provider: CloudProvider) -> None:
  990. super(BaseFloatingIP, self).__init__(provider)
  991. @property
  992. def name(self) -> str:
  993. return self.public_ip
  994. @property
  995. def state(self) -> str:
  996. return (FloatingIpState.IN_USE if self.in_use
  997. else FloatingIpState.AVAILABLE)
  998. def wait_till_ready(
  999. self, timeout: int | None = None,
  1000. interval: int | None = None) -> None:
  1001. self.wait_for(
  1002. [FloatingIpState.AVAILABLE, FloatingIpState.IN_USE],
  1003. terminal_states=[FloatingIpState.ERROR],
  1004. timeout=timeout,
  1005. interval=interval)
  1006. def __eq__(self, other: object) -> bool:
  1007. return (isinstance(other, FloatingIP) and
  1008. # pylint:disable=protected-access
  1009. self._provider == other._provider and
  1010. self.id == other.id)
  1011. def delete(self) -> None:
  1012. # For OS where the gateway is necessary, we pass the gateway when
  1013. # deleting, for all others we pass None and it will be ignored
  1014. gw: Any = getattr(self, '_gateway_id', None)
  1015. self._provider.networking._floating_ips.delete(gw, self.id)
  1016. class BaseRouter(BaseCloudResource, Router):
  1017. CB_DEFAULT_ROUTER_LABEL = os.environ.get('CB_DEFAULT_ROUTER_LABEL',
  1018. 'cloudbridge-router')
  1019. def __init__(self, provider: CloudProvider) -> None:
  1020. super(BaseRouter, self).__init__(provider)
  1021. def __eq__(self, other: object) -> bool:
  1022. return (isinstance(other, Router) and
  1023. # pylint:disable=protected-access
  1024. self._provider == other._provider and
  1025. self.id == other.id)
  1026. def delete(self) -> None:
  1027. self._provider.networking.routers.delete(self)
  1028. class BaseInternetGateway(BaseCloudResource, BaseObjectLifeCycleMixin,
  1029. InternetGateway):
  1030. CB_DEFAULT_INET_GATEWAY_NAME = cb_helpers.get_env(
  1031. 'CB_DEFAULT_INET_GATEWAY_NAME', 'cloudbridge-inetgateway')
  1032. def __init__(self, provider: CloudProvider) -> None:
  1033. super(BaseInternetGateway, self).__init__(provider)
  1034. def __eq__(self, other: object) -> bool:
  1035. return (isinstance(other, InternetGateway) and
  1036. # pylint:disable=protected-access
  1037. self._provider == other._provider and
  1038. self.id == other.id)
  1039. def wait_till_ready(
  1040. self, timeout: int | None = None,
  1041. interval: int | None = None) -> None:
  1042. self.wait_for(
  1043. [GatewayState.AVAILABLE],
  1044. terminal_states=[GatewayState.ERROR, GatewayState.UNKNOWN],
  1045. timeout=timeout,
  1046. interval=interval)
  1047. def delete(self) -> None:
  1048. # A gateway is always attached to a network when it can be deleted;
  1049. # network_id is typed str | None, so narrow to str for the service.
  1050. return self._provider.networking._gateways.delete(
  1051. cast(str, self.network_id), self)
  1052. class BaseDnsZone(BaseCloudResource, DnsZone):
  1053. CB_NAME_PATTERN = re.compile(
  1054. r"^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9]"
  1055. r"[a-z0-9-]{0,61}[a-z0-9]\.?$")
  1056. def __init__(self, provider: CloudProvider) -> None:
  1057. super(BaseDnsZone, self).__init__(provider)
  1058. def __eq__(self, other: object) -> bool:
  1059. return (isinstance(other, BaseDnsZone) and
  1060. # pylint:disable=protected-access
  1061. self._provider == other._provider and
  1062. self.id == other.id)
  1063. @staticmethod
  1064. def is_valid_resource_name(name: str) -> bool:
  1065. if not name:
  1066. return False
  1067. else:
  1068. return (True if BaseDnsZone.CB_NAME_PATTERN.match(name)
  1069. else False)
  1070. @staticmethod
  1071. def assert_valid_resource_name(name: str) -> None:
  1072. if not BaseDnsZone.is_valid_resource_name(name):
  1073. log.debug("InvalidNameException raised on %s", name,
  1074. exc_info=True)
  1075. raise InvalidNameException(
  1076. u"Invalid object name: %s. Name must be fully qualified "
  1077. u"(ending with a .) and match criteria defined "
  1078. u"in: https://stackoverflow.com/q/10306690/10971151" % name)
  1079. def delete(self) -> None:
  1080. return self._provider.dns.host_zones.delete(self.id)
  1081. class BaseDnsRecord(BaseCloudResource, DnsRecord):
  1082. CB_NAME_PATTERN = re.compile(
  1083. r"^(?:\*\.)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9]"
  1084. r"[a-z0-9-]{0,61}[a-z0-9]\.?$")
  1085. def __init__(self, provider: CloudProvider) -> None:
  1086. super(BaseDnsRecord, self).__init__(provider)
  1087. def __eq__(self, other: object) -> bool:
  1088. return (isinstance(other, BaseDnsRecord) and
  1089. # pylint:disable=protected-access
  1090. self._provider == other._provider and
  1091. self.id == other.id)
  1092. @staticmethod
  1093. def is_valid_resource_name(name: str) -> bool:
  1094. if not name:
  1095. return False
  1096. else:
  1097. return (True if BaseDnsRecord.CB_NAME_PATTERN.match(name)
  1098. else False)
  1099. @staticmethod
  1100. def assert_valid_resource_name(name: str) -> None:
  1101. if not BaseDnsRecord.is_valid_resource_name(name):
  1102. log.debug("InvalidNameException raised on %s", name,
  1103. exc_info=True)
  1104. raise InvalidNameException(
  1105. u"Invalid object name: %s. Name must be fully qualified "
  1106. u"(ending with a .) and match criteria defined "
  1107. u"in: https://stackoverflow.com/q/10306690/10971151" % name)