resources.py 33 KB

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