resources.py 30 KB

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