resources.py 56 KB

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