resources.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965
  1. """
  2. Base implementation for data objects exposed through a provider or service
  3. """
  4. import inspect
  5. import itertools
  6. import logging
  7. import os
  8. import re
  9. import shutil
  10. import time
  11. import uuid
  12. import six
  13. from cloudbridge.interfaces.exceptions import \
  14. InvalidConfigurationException
  15. from cloudbridge.interfaces.exceptions import InvalidLabelException
  16. from cloudbridge.interfaces.exceptions import InvalidNameException
  17. from cloudbridge.interfaces.exceptions import WaitStateException
  18. from cloudbridge.interfaces.resources import AttachmentInfo
  19. from cloudbridge.interfaces.resources import Bucket
  20. from cloudbridge.interfaces.resources import BucketObject
  21. from cloudbridge.interfaces.resources import CloudResource
  22. from cloudbridge.interfaces.resources import DnsRecord
  23. from cloudbridge.interfaces.resources import DnsZone
  24. from cloudbridge.interfaces.resources import FloatingIP
  25. from cloudbridge.interfaces.resources import FloatingIpState
  26. from cloudbridge.interfaces.resources import GatewayState
  27. from cloudbridge.interfaces.resources import Instance
  28. from cloudbridge.interfaces.resources import InstanceState
  29. from cloudbridge.interfaces.resources import InternetGateway
  30. from cloudbridge.interfaces.resources import KeyPair
  31. from cloudbridge.interfaces.resources import LaunchConfig
  32. from cloudbridge.interfaces.resources import MachineImage
  33. from cloudbridge.interfaces.resources import MachineImageState
  34. from cloudbridge.interfaces.resources import Network
  35. from cloudbridge.interfaces.resources import NetworkState
  36. from cloudbridge.interfaces.resources import ObjectLifeCycleMixin
  37. from cloudbridge.interfaces.resources import PageableObjectMixin
  38. from cloudbridge.interfaces.resources import PlacementZone
  39. from cloudbridge.interfaces.resources import Region
  40. from cloudbridge.interfaces.resources import ResultList
  41. from cloudbridge.interfaces.resources import Router
  42. from cloudbridge.interfaces.resources import Snapshot
  43. from cloudbridge.interfaces.resources import SnapshotState
  44. from cloudbridge.interfaces.resources import Subnet
  45. from cloudbridge.interfaces.resources import SubnetState
  46. from cloudbridge.interfaces.resources import VMFirewall
  47. from cloudbridge.interfaces.resources import VMFirewallRule
  48. from cloudbridge.interfaces.resources import VMType
  49. from cloudbridge.interfaces.resources import Volume
  50. from cloudbridge.interfaces.resources import VolumeState
  51. from . import helpers as cb_helpers
  52. log = logging.getLogger(__name__)
  53. class BaseCloudResource(CloudResource):
  54. """
  55. Base implementation of a CloudBridge Resource.
  56. """
  57. # Regular expression for valid cloudbridge resource names/labels.
  58. # Can be alphanumeric string that does not start or end with a dash
  59. # Must be at least 3 characters in length.
  60. # Ref: https://stackoverflow.com/questions/2525327/regex-for-a-za-z0-9
  61. # -with-dashes-allowed-in-between-but-not-at-the-start-or-e
  62. CB_NAME_PATTERN = re.compile(r"^[a-z][-a-z0-9]{1,61}[a-z0-9]$")
  63. def __init__(self, provider):
  64. self.__provider = provider
  65. @staticmethod
  66. def is_valid_resource_name(name):
  67. if not name:
  68. return False
  69. else:
  70. return (True if BaseCloudResource.CB_NAME_PATTERN.match(name)
  71. else False)
  72. @staticmethod
  73. def assert_valid_resource_label(name):
  74. if not BaseCloudResource.is_valid_resource_name(name):
  75. log.debug("InvalidLabelException raised on %s", name)
  76. raise InvalidLabelException(
  77. u"Invalid label: %s. Label must be at least 3 characters long"
  78. " and at most 63 characters. It must consist of lowercase"
  79. " letters, numbers, or dashes. The label must start with a "
  80. "letter and not end with a dash." % name)
  81. @staticmethod
  82. def assert_valid_resource_name(name):
  83. if not BaseCloudResource.is_valid_resource_name(name):
  84. log.debug("InvalidLabelException raised on %s", name)
  85. raise InvalidNameException(
  86. u"Invalid name: %s. Name must be at least 3 characters long"
  87. " and at most 63 characters. It must consist of lowercase"
  88. " letters, numbers, or dashes. The name must not start or"
  89. " end with a dash." % name)
  90. @staticmethod
  91. def _generate_name_from_label(label, default):
  92. if not label:
  93. label = default
  94. name = label[:55] + '-' + uuid.uuid4().hex[:6]
  95. BaseCloudResource.assert_valid_resource_name(name)
  96. return name
  97. @property
  98. def _provider(self):
  99. return self.__provider
  100. def to_json(self):
  101. # Get all attributes but filter methods and private/magic ones
  102. attr = inspect.getmembers(self, lambda a: not (inspect.isroutine(a)))
  103. js = {k: v for (k, v) in attr if not k.startswith('_')}
  104. return js
  105. def __repr__(self):
  106. name_or_label = getattr(self, 'label', self.name)
  107. if name_or_label == self.id:
  108. return "<CB-{0}: {1}>".format(
  109. self.__class__.__name__, self.id)
  110. else:
  111. return "<CB-{0}: {1} ({2})>".format(
  112. self.__class__.__name__, name_or_label, self.id)
  113. class BaseObjectLifeCycleMixin(ObjectLifeCycleMixin):
  114. """
  115. A base implementation of an ObjectLifeCycleMixin.
  116. This base implementation has an implementation of wait_for
  117. which refreshes the object's state till the desired ready states
  118. are reached. Subclasses must still implement the wait_till_ready
  119. method, since the desired ready states are object specific.
  120. """
  121. def wait_for(self, target_states, terminal_states=None, timeout=None,
  122. interval=None):
  123. if timeout is None:
  124. timeout = self._provider.config.default_wait_timeout
  125. if interval is None:
  126. interval = self._provider.config.default_wait_interval
  127. assert timeout >= 0
  128. assert interval >= 0
  129. assert timeout >= interval
  130. end_time = time.time() + timeout
  131. while self.state not in target_states:
  132. if self.state in (terminal_states or []):
  133. raise WaitStateException(
  134. "Object: {0} is in state: {1} which is a terminal state"
  135. " and cannot be waited on.".format(self, self.state))
  136. else:
  137. log.debug(
  138. "Object %s is in state: %s. Waiting another %s"
  139. " seconds to reach target state(s): %s...",
  140. self,
  141. self.state,
  142. int(end_time - time.time()),
  143. target_states)
  144. time.sleep(interval)
  145. if time.time() > end_time:
  146. raise WaitStateException(
  147. "Waited too long for object: {0} to reach a desired"
  148. "state: {1}. It's still in state: {2}".format(
  149. self, target_states, self.state))
  150. self.refresh()
  151. log.debug("Object: %s successfully reached target state: %s",
  152. self, self.state)
  153. return True
  154. class BaseResultList(ResultList):
  155. def __init__(
  156. self, is_truncated, marker, supports_total, total=None, data=None):
  157. # call list constructor
  158. super(BaseResultList, self).__init__(data or [])
  159. self._marker = marker
  160. self._is_truncated = is_truncated
  161. self._supports_total = True if supports_total else False
  162. self._total = total
  163. @property
  164. def marker(self):
  165. return self._marker
  166. @property
  167. def is_truncated(self):
  168. return self._is_truncated
  169. @property
  170. def supports_total(self):
  171. return self._supports_total
  172. @property
  173. def total_results(self):
  174. return self._total
  175. class ServerPagedResultList(BaseResultList):
  176. """
  177. This is a convenience class that extends the :class:`BaseResultList` class
  178. and provides a server side implementation of paging. It is meant for use by
  179. provider developers and is not meant for direct use by end-users.
  180. This class can be used to wrap a partial result list when an operation
  181. supports server side paging.
  182. """
  183. @property
  184. def supports_server_paging(self):
  185. return True
  186. @property
  187. def data(self):
  188. raise NotImplementedError(
  189. "ServerPagedResultLists do not support the data property")
  190. class ClientPagedResultList(BaseResultList):
  191. """
  192. This is a convenience class that extends the :class:`BaseResultList` class
  193. and provides a client side implementation of paging. It is meant for use by
  194. provider developers and is not meant for direct use by end-users.
  195. This class can be used to wrap a full result list when an operation does
  196. not support server side paging. This class will then provide a paged view
  197. of the full result set entirely on the client side.
  198. """
  199. def __init__(self, provider, objects, limit=None, marker=None):
  200. self._objects = objects
  201. limit = limit or provider.config.default_result_limit
  202. total_size = len(objects)
  203. if marker:
  204. from_marker = itertools.dropwhile(
  205. lambda obj: not obj.id == marker, objects)
  206. # skip one past the marker
  207. next(from_marker, None)
  208. objects = list(from_marker)
  209. is_truncated = len(objects) > limit
  210. results = list(itertools.islice(objects, limit))
  211. super(ClientPagedResultList, self).__init__(
  212. is_truncated,
  213. results[-1].id if is_truncated else None,
  214. True, total=total_size,
  215. data=results)
  216. @property
  217. def supports_server_paging(self):
  218. return False
  219. @property
  220. def data(self):
  221. return self._objects
  222. class BasePageableObjectMixin(PageableObjectMixin):
  223. """
  224. A mixin to provide iteration capability for a class
  225. that support a list(limit, marker) method.
  226. """
  227. def __iter__(self):
  228. for result in self.iter():
  229. yield result
  230. def iter(self, **kwargs):
  231. result_list = self.list(**kwargs)
  232. if result_list.supports_server_paging:
  233. for result in result_list:
  234. yield result
  235. while result_list.is_truncated:
  236. result_list = self.list(marker=result_list.marker, **kwargs)
  237. for result in result_list:
  238. yield result
  239. else:
  240. for result in result_list.data:
  241. yield result
  242. class BaseVMType(BaseCloudResource, VMType):
  243. def __init__(self, provider):
  244. super(BaseVMType, self).__init__(provider)
  245. def __eq__(self, other):
  246. return (isinstance(other, VMType) and
  247. # pylint:disable=protected-access
  248. self._provider == other._provider and
  249. self.id == other.id)
  250. @property
  251. def size_total_disk(self):
  252. return self.size_root_disk + self.size_ephemeral_disks
  253. class BaseInstance(BaseCloudResource, BaseObjectLifeCycleMixin, Instance):
  254. def __init__(self, provider):
  255. super(BaseInstance, self).__init__(provider)
  256. def __eq__(self, other):
  257. return (isinstance(other, Instance) and
  258. # pylint:disable=protected-access
  259. self._provider == other._provider and
  260. self.id == other.id and
  261. # check from most to least likely mutables
  262. self.state == other.state and
  263. self.label == other.label and
  264. self.vm_firewalls == other.vm_firewalls and
  265. self.public_ips == other.public_ips and
  266. self.private_ips == other.private_ips and
  267. self.image_id == other.image_id)
  268. def wait_till_ready(self, timeout=None, interval=None):
  269. self.wait_for(
  270. [InstanceState.RUNNING],
  271. terminal_states=[InstanceState.DELETED, InstanceState.ERROR],
  272. timeout=timeout,
  273. interval=interval)
  274. def delete(self):
  275. self._provider.compute.instances.delete(self)
  276. class BaseLaunchConfig(LaunchConfig):
  277. def __init__(self, provider):
  278. self.provider = provider
  279. self.block_devices = []
  280. class BlockDeviceMapping(object):
  281. """
  282. Represents a block device mapping
  283. """
  284. def __init__(self, is_volume=False, source=None, is_root=None,
  285. size=None, delete_on_terminate=None):
  286. self.is_volume = is_volume
  287. self.source = source
  288. self.is_root = is_root
  289. self.size = size
  290. self.delete_on_terminate = delete_on_terminate
  291. def add_ephemeral_device(self):
  292. block_device = BaseLaunchConfig.BlockDeviceMapping()
  293. self.block_devices.append(block_device)
  294. def add_volume_device(self, source=None, is_root=None, size=None,
  295. delete_on_terminate=None):
  296. block_device = self._validate_volume_device(
  297. source=source, is_root=is_root, size=size,
  298. delete_on_terminate=delete_on_terminate)
  299. log.debug("Appending %s to the block_devices list",
  300. block_device)
  301. self.block_devices.append(block_device)
  302. def _validate_volume_device(self, source=None, is_root=None,
  303. size=None, delete_on_terminate=None):
  304. """
  305. Validates a volume based device and throws an
  306. InvalidConfigurationException if the configuration is incorrect.
  307. """
  308. if source is None and not size:
  309. log.exception("InvalidConfigurationException raised: "
  310. "no size argument specified.")
  311. raise InvalidConfigurationException(
  312. "A size must be specified for a blank new volume.")
  313. if source and \
  314. not isinstance(source, (Snapshot, Volume, MachineImage)):
  315. log.exception("InvalidConfigurationException raised: "
  316. "source argument not specified correctly.")
  317. raise InvalidConfigurationException(
  318. "Source must be a Snapshot, Volume, MachineImage, or None.")
  319. if size:
  320. if not isinstance(size, six.integer_types) or not size > 0:
  321. log.exception("InvalidConfigurationException raised: "
  322. "size argument must be an integer greater than "
  323. "0. Got type %s and value %s.", type(size), size)
  324. raise InvalidConfigurationException(
  325. "The size must be None or an integer greater than 0.")
  326. if is_root:
  327. for bd in self.block_devices:
  328. if bd.is_root:
  329. log.exception("InvalidConfigurationException raised: "
  330. "%s has already been marked as the root "
  331. "block device.", bd)
  332. raise InvalidConfigurationException(
  333. "An existing block device: {0} has already been"
  334. " marked as root. There can only be one root device.")
  335. return BaseLaunchConfig.BlockDeviceMapping(
  336. is_volume=True, source=source, is_root=is_root, size=size,
  337. delete_on_terminate=delete_on_terminate)
  338. class BaseMachineImage(
  339. BaseCloudResource, BaseObjectLifeCycleMixin, MachineImage):
  340. def __init__(self, provider):
  341. super(BaseMachineImage, self).__init__(provider)
  342. def __eq__(self, other):
  343. return (isinstance(other, MachineImage) and
  344. # pylint:disable=protected-access
  345. self._provider == other._provider and
  346. self.id == other.id and
  347. # check from most to least likely mutables
  348. self.state == other.state and
  349. self.label == other.label and
  350. self.description == other.description)
  351. def wait_till_ready(self, timeout=None, interval=None):
  352. self.wait_for(
  353. [MachineImageState.AVAILABLE],
  354. terminal_states=[MachineImageState.ERROR],
  355. timeout=timeout,
  356. interval=interval)
  357. class BaseAttachmentInfo(AttachmentInfo):
  358. def __init__(self, volume, instance_id, device):
  359. self._volume = volume
  360. self._instance_id = instance_id
  361. self._device = device
  362. @property
  363. def volume(self):
  364. return self._volume
  365. @property
  366. def instance_id(self):
  367. return self._instance_id
  368. @property
  369. def device(self):
  370. return self._device
  371. class BaseVolume(BaseCloudResource, BaseObjectLifeCycleMixin, Volume):
  372. def __init__(self, provider):
  373. super(BaseVolume, self).__init__(provider)
  374. def __eq__(self, other):
  375. return (isinstance(other, Volume) and
  376. # pylint:disable=protected-access
  377. self._provider == other._provider and
  378. self.id == other.id and
  379. # check from most to least likely mutables
  380. self.state == other.state and
  381. self.label == other.label)
  382. def wait_till_ready(self, timeout=None, interval=None):
  383. self.wait_for(
  384. [VolumeState.AVAILABLE],
  385. terminal_states=[VolumeState.ERROR, VolumeState.DELETED],
  386. timeout=timeout,
  387. interval=interval)
  388. def delete(self):
  389. """
  390. Delete this volume.
  391. """
  392. return self._provider.storage.volumes.delete(self)
  393. class BaseSnapshot(BaseCloudResource, BaseObjectLifeCycleMixin, Snapshot):
  394. def __init__(self, provider):
  395. super(BaseSnapshot, self).__init__(provider)
  396. def __eq__(self, other):
  397. return (isinstance(other, Snapshot) and
  398. # pylint:disable=protected-access
  399. self._provider == other._provider and
  400. self.id == other.id and
  401. # check from most to least likely mutables
  402. self.state == other.state and
  403. self.label == other.label)
  404. def wait_till_ready(self, timeout=None, interval=None):
  405. self.wait_for(
  406. [SnapshotState.AVAILABLE],
  407. terminal_states=[SnapshotState.ERROR],
  408. timeout=timeout,
  409. interval=interval)
  410. def delete(self):
  411. """
  412. Delete this snapshot.
  413. """
  414. return self._provider.storage.snapshots.delete(self)
  415. class BaseKeyPair(BaseCloudResource, KeyPair):
  416. def __init__(self, provider, key_pair):
  417. super(BaseKeyPair, self).__init__(provider)
  418. self._key_pair = key_pair
  419. self._private_material = None
  420. def __eq__(self, other):
  421. return (isinstance(other, KeyPair) and
  422. # pylint:disable=protected-access
  423. self._provider == other._provider and
  424. self.name == other.name)
  425. @property
  426. def id(self):
  427. """
  428. Return the id of this key pair.
  429. """
  430. return self._key_pair.name
  431. @property
  432. def name(self):
  433. """
  434. Return the name of this key pair.
  435. """
  436. return self.id
  437. @property
  438. def material(self):
  439. return self._private_material
  440. @material.setter
  441. # pylint:disable=arguments-differ
  442. def material(self, value):
  443. self._private_material = value
  444. def delete(self):
  445. self._provider.security.key_pairs.delete(self)
  446. class BaseVMFirewall(BaseCloudResource, VMFirewall):
  447. def __init__(self, provider, vm_firewall):
  448. super(BaseVMFirewall, self).__init__(provider)
  449. self._vm_firewall = vm_firewall
  450. def __eq__(self, other):
  451. """
  452. Check if all the defined rules match across both VM firewalls.
  453. """
  454. return (isinstance(other, VMFirewall) and
  455. # pylint:disable=protected-access
  456. self._provider == other._provider and
  457. set(self.rules) == set(other.rules))
  458. def __ne__(self, other):
  459. return not self.__eq__(other)
  460. @property
  461. def id(self):
  462. """
  463. Get the ID of this VM firewall.
  464. :rtype: str
  465. :return: VM firewall ID
  466. """
  467. return self._vm_firewall.id
  468. @property
  469. def name(self):
  470. """
  471. Return the name of this VM firewall.
  472. """
  473. return self.id
  474. @property
  475. def description(self):
  476. """
  477. Return the description of this VM firewall.
  478. """
  479. return self._vm_firewall.description
  480. def delete(self):
  481. """
  482. Delete this VM firewall.
  483. """
  484. return self._provider.security.vm_firewalls.delete(self)
  485. class BaseVMFirewallRule(BaseCloudResource, VMFirewallRule):
  486. def __init__(self, parent_fw, rule):
  487. # pylint:disable=protected-access
  488. super(BaseVMFirewallRule, self).__init__(
  489. parent_fw._provider)
  490. self.firewall = parent_fw
  491. self._rule = rule
  492. # Cache name
  493. self._name = "{0}-{1}-{2}-{3}-{4}-{5}".format(
  494. self.direction, self.protocol, self.from_port, self.to_port,
  495. self.cidr, self.src_dest_fw_id).lower()
  496. @property
  497. def name(self):
  498. return self._name
  499. def __repr__(self):
  500. return ("<{0}: id: {1}; direction: {2}; protocol: {3}; from: {4};"
  501. " to: {5}; cidr: {6}, src_dest_fw: {7}>"
  502. .format(self.__class__.__name__, self.id, self.direction,
  503. self.protocol, self.from_port, self.to_port, self.cidr,
  504. self.src_dest_fw_id))
  505. def __eq__(self, other):
  506. return (isinstance(other, VMFirewallRule) and
  507. self.direction == other.direction and
  508. self.protocol == other.protocol and
  509. self.from_port == other.from_port and
  510. self.to_port == other.to_port and
  511. self.cidr == other.cidr and
  512. self.src_dest_fw_id == other.src_dest_fw_id)
  513. def __ne__(self, other):
  514. return not self.__eq__(other)
  515. def __hash__(self):
  516. """
  517. Return a hash-based interpretation of all of the object's field values.
  518. This is requeried for operations on hashed collections including
  519. ``set``, ``frozenset``, and ``dict``.
  520. """
  521. return hash("{0}{1}{2}{3}{4}{5}".format(
  522. self.direction, self.protocol, self.from_port, self.to_port,
  523. self.cidr, self.src_dest_fw_id))
  524. def to_json(self):
  525. attr = inspect.getmembers(self, lambda a: not (inspect.isroutine(a)))
  526. js = {k: v for (k, v) in attr if not k.startswith('_')}
  527. js['src_dest_fw'] = self.src_dest_fw_id
  528. js['firewall'] = self.firewall.id
  529. return js
  530. def delete(self):
  531. self._provider.security._vm_firewall_rules.delete(self.firewall, self)
  532. class BasePlacementZone(BaseCloudResource, PlacementZone):
  533. def __init__(self, provider):
  534. super(BasePlacementZone, self).__init__(provider)
  535. def __eq__(self, other):
  536. return (isinstance(other, PlacementZone) and
  537. # pylint:disable=protected-access
  538. self._provider == other._provider and
  539. self.id == other.id)
  540. class BaseRegion(BaseCloudResource, Region):
  541. def __init__(self, provider):
  542. super(BaseRegion, self).__init__(provider)
  543. def __eq__(self, other):
  544. return (isinstance(other, Region) and
  545. # pylint:disable=protected-access
  546. self._provider == other._provider and
  547. self.id == other.id)
  548. def to_json(self):
  549. attr = inspect.getmembers(self, lambda a: not (inspect.isroutine(a)))
  550. js = {k: v for (k, v) in attr if not k.startswith('_')}
  551. js['zones'] = [z.id for z in self.zones]
  552. return js
  553. @property
  554. def default_zone(self):
  555. return next(iter(self.zones))
  556. class BaseBucketObject(BaseCloudResource, BucketObject):
  557. # Regular expression for valid bucket keys.
  558. # They, must match the following criteria: http://docs.aws.amazon.com/"
  559. # AmazonS3/latest/dev/UsingMetadata.html#object-key-guidelines
  560. #
  561. # Note: The following regex is based on: https://stackoverflow.com/question
  562. # s/537772/what-is-the-most-correct-regular-expression-for-a-unix-file-path
  563. CB_NAME_PATTERN = re.compile(r"[^\0]+")
  564. def __init__(self, provider):
  565. super(BaseBucketObject, self).__init__(provider)
  566. @staticmethod
  567. def is_valid_resource_name(name):
  568. return (True if BaseBucketObject.CB_NAME_PATTERN.match(name)
  569. else False)
  570. @staticmethod
  571. def assert_valid_resource_name(name):
  572. if not BaseBucketObject.is_valid_resource_name(name):
  573. log.debug("InvalidLabelException raised on %s", name,
  574. exc_info=True)
  575. raise InvalidLabelException(
  576. u"Invalid object name: %s. Name must match criteria defined "
  577. "in: http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMeta"
  578. "data.html#object-key-guidelines" % name)
  579. def save_content(self, target_stream):
  580. shutil.copyfileobj(self.iter_content(), target_stream)
  581. def __eq__(self, other):
  582. return (isinstance(other, BucketObject) and
  583. # pylint:disable=protected-access
  584. self._provider == other._provider and
  585. self.id == other.id and
  586. # check from most to least likely mutables
  587. self.name == other.name)
  588. class BaseBucket(BaseCloudResource, Bucket):
  589. def __init__(self, provider):
  590. super(BaseBucket, self).__init__(provider)
  591. def __eq__(self, other):
  592. return (isinstance(other, Bucket) and
  593. # pylint:disable=protected-access
  594. self._provider == other._provider and
  595. self.id == other.id and
  596. # check from most to least likely mutables
  597. self.name == other.name)
  598. def delete(self):
  599. """
  600. Delete this bucket.
  601. """
  602. self._provider.storage.buckets.delete(self.id)
  603. # TODO: Discuss creating `create_object` method, or change docs
  604. class BaseNetwork(BaseCloudResource, BaseObjectLifeCycleMixin, Network):
  605. CB_DEFAULT_NETWORK_LABEL = os.environ.get('CB_DEFAULT_NETWORK_LABEL',
  606. 'cloudbridge-net')
  607. CB_DEFAULT_IPV4RANGE = os.environ.get('CB_DEFAULT_IPV4RANGE',
  608. u'10.0.0.0/16')
  609. def __init__(self, provider):
  610. super(BaseNetwork, self).__init__(provider)
  611. @staticmethod
  612. def cidr_blocks_overlap(block1, block2):
  613. common_length = min(int(block1.split('/')[1]),
  614. int(block2.split('/')[1]))
  615. p1 = [format(int(b), '08b') for b in block1.split('/')[0].split('.')]
  616. prefix1 = ''.join(p1)[:common_length]
  617. p2 = [format(int(b), '08b') for b in block2.split('/')[0].split('.')]
  618. prefix2 = ''.join(p2)[:common_length]
  619. return prefix1 == prefix2
  620. def wait_till_ready(self, timeout=None, interval=None):
  621. self.wait_for(
  622. [NetworkState.AVAILABLE],
  623. terminal_states=[NetworkState.ERROR],
  624. timeout=timeout,
  625. interval=interval)
  626. def delete(self):
  627. self._provider.networking.networks.delete(self)
  628. def __eq__(self, other):
  629. return (isinstance(other, Network) and
  630. # pylint:disable=protected-access
  631. self._provider == other._provider and
  632. self.id == other.id)
  633. class BaseSubnet(BaseCloudResource, BaseObjectLifeCycleMixin, Subnet):
  634. CB_DEFAULT_SUBNET_LABEL = os.environ.get('CB_DEFAULT_SUBNET_LABEL',
  635. 'cloudbridge-subnet')
  636. CB_DEFAULT_SUBNET_IPV4RANGE = os.environ.get('CB_DEFAULT_SUBNET_IPV4RANGE',
  637. '10.0.0.0/24')
  638. def __init__(self, provider):
  639. super(BaseSubnet, self).__init__(provider)
  640. def __eq__(self, other):
  641. return (isinstance(other, Subnet) and
  642. # pylint:disable=protected-access
  643. self._provider == other._provider and
  644. self.id == other.id)
  645. @property
  646. def network(self):
  647. return self._provider.networking.networks.get(self.network_id)
  648. def wait_till_ready(self, timeout=None, interval=None):
  649. self.wait_for(
  650. [SubnetState.AVAILABLE],
  651. terminal_states=[SubnetState.ERROR],
  652. timeout=timeout,
  653. interval=interval)
  654. def delete(self):
  655. self._provider.networking.subnets.delete(self)
  656. class BaseFloatingIP(BaseCloudResource, BaseObjectLifeCycleMixin, FloatingIP):
  657. def __init__(self, provider):
  658. super(BaseFloatingIP, self).__init__(provider)
  659. @property
  660. def name(self):
  661. return self.public_ip
  662. @property
  663. def state(self):
  664. return (FloatingIpState.IN_USE if self.in_use
  665. else FloatingIpState.AVAILABLE)
  666. def wait_till_ready(self, timeout=None, interval=None):
  667. self.wait_for(
  668. [FloatingIpState.AVAILABLE, FloatingIpState.IN_USE],
  669. terminal_states=[FloatingIpState.ERROR],
  670. timeout=timeout,
  671. interval=interval)
  672. def __eq__(self, other):
  673. return (isinstance(other, FloatingIP) and
  674. # pylint:disable=protected-access
  675. self._provider == other._provider and
  676. self.id == other.id)
  677. def delete(self):
  678. # For OS where the gateway is necessary, we pass the gateway when
  679. # deleting, for all others we pass None and it will be ignored
  680. gw = getattr(self, '_gateway_id', None)
  681. self._provider.networking._floating_ips.delete(gw, self.id)
  682. class BaseRouter(BaseCloudResource, Router):
  683. CB_DEFAULT_ROUTER_LABEL = os.environ.get('CB_DEFAULT_ROUTER_LABEL',
  684. 'cloudbridge-router')
  685. def __init__(self, provider):
  686. super(BaseRouter, self).__init__(provider)
  687. def __eq__(self, other):
  688. return (isinstance(other, Router) and
  689. # pylint:disable=protected-access
  690. self._provider == other._provider and
  691. self.id == other.id)
  692. def delete(self):
  693. self._provider.networking.routers.delete(self)
  694. class BaseInternetGateway(BaseCloudResource, BaseObjectLifeCycleMixin,
  695. InternetGateway):
  696. CB_DEFAULT_INET_GATEWAY_NAME = cb_helpers.get_env(
  697. 'CB_DEFAULT_INET_GATEWAY_NAME', 'cloudbridge-inetgateway')
  698. def __init__(self, provider):
  699. super(BaseInternetGateway, self).__init__(provider)
  700. def __eq__(self, other):
  701. return (isinstance(other, InternetGateway) and
  702. # pylint:disable=protected-access
  703. self._provider == other._provider and
  704. self.id == other.id)
  705. def wait_till_ready(self, timeout=None, interval=None):
  706. self.wait_for(
  707. [GatewayState.AVAILABLE],
  708. terminal_states=[GatewayState.ERROR, GatewayState.UNKNOWN],
  709. timeout=timeout,
  710. interval=interval)
  711. def delete(self):
  712. return self._provider.networking._gateways.delete(self.network_id,
  713. self)
  714. class BaseDnsZone(BaseCloudResource, DnsZone):
  715. CB_NAME_PATTERN = re.compile(
  716. r"^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9]"
  717. r"[a-z0-9-]{0,61}[a-z0-9]\.?$")
  718. def __init__(self, provider):
  719. super(BaseDnsZone, self).__init__(provider)
  720. def __eq__(self, other):
  721. return (isinstance(other, BaseDnsZone) and
  722. # pylint:disable=protected-access
  723. self._provider == other._provider and
  724. self.id == other.id)
  725. @staticmethod
  726. def is_valid_resource_name(name):
  727. if not name:
  728. return False
  729. else:
  730. return (True if BaseDnsZone.CB_NAME_PATTERN.match(name)
  731. else False)
  732. @staticmethod
  733. def assert_valid_resource_name(name):
  734. if not BaseDnsZone.is_valid_resource_name(name):
  735. log.debug("InvalidNameException raised on %s", name,
  736. exc_info=True)
  737. raise InvalidNameException(
  738. u"Invalid object name: %s. Name must be fully qualified "
  739. u"(ending with a .) and match criteria defined "
  740. u"in: https://stackoverflow.com/q/10306690/10971151" % name)
  741. def delete(self):
  742. return self._provider.dns.host_zones.delete(self.id)
  743. class BaseDnsRecord(BaseCloudResource, DnsRecord):
  744. CB_NAME_PATTERN = re.compile(
  745. r"^(?:\*\.)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9]"
  746. r"[a-z0-9-]{0,61}[a-z0-9]\.?$")
  747. def __init__(self, provider):
  748. super(BaseDnsRecord, self).__init__(provider)
  749. def __eq__(self, other):
  750. return (isinstance(other, BaseDnsRecord) and
  751. # pylint:disable=protected-access
  752. self._provider == other._provider and
  753. self.id == other.id)
  754. @staticmethod
  755. def is_valid_resource_name(name):
  756. if not name:
  757. return False
  758. else:
  759. return (True if BaseDnsRecord.CB_NAME_PATTERN.match(name)
  760. else False)
  761. @staticmethod
  762. def assert_valid_resource_name(name):
  763. if not BaseDnsRecord.is_valid_resource_name(name):
  764. log.debug("InvalidNameException raised on %s", name,
  765. exc_info=True)
  766. raise InvalidNameException(
  767. u"Invalid object name: %s. Name must be fully qualified "
  768. u"(ending with a .) and match criteria defined "
  769. u"in: https://stackoverflow.com/q/10306690/10971151" % name)