resources.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715
  1. """
  2. Base implementation for data objects exposed through a provider or service
  3. """
  4. import inspect
  5. import itertools
  6. import json
  7. import logging
  8. import os
  9. import shutil
  10. import time
  11. import six
  12. from cloudbridge.cloud.interfaces.exceptions \
  13. import InvalidConfigurationException
  14. from cloudbridge.cloud.interfaces.resources import AttachmentInfo
  15. from cloudbridge.cloud.interfaces.resources import Bucket
  16. from cloudbridge.cloud.interfaces.resources import BucketObject
  17. from cloudbridge.cloud.interfaces.resources import CloudResource
  18. from cloudbridge.cloud.interfaces.resources import Instance
  19. from cloudbridge.cloud.interfaces.resources import InstanceState
  20. from cloudbridge.cloud.interfaces.resources import InstanceType
  21. from cloudbridge.cloud.interfaces.resources import KeyPair
  22. from cloudbridge.cloud.interfaces.resources import LaunchConfig
  23. from cloudbridge.cloud.interfaces.resources import MachineImage
  24. from cloudbridge.cloud.interfaces.resources import MachineImageState
  25. from cloudbridge.cloud.interfaces.resources import Network
  26. from cloudbridge.cloud.interfaces.resources import NetworkState
  27. from cloudbridge.cloud.interfaces.resources import ObjectLifeCycleMixin
  28. from cloudbridge.cloud.interfaces.resources import PageableObjectMixin
  29. from cloudbridge.cloud.interfaces.resources import PlacementZone
  30. from cloudbridge.cloud.interfaces.resources import Region
  31. from cloudbridge.cloud.interfaces.resources import Router
  32. from cloudbridge.cloud.interfaces.resources import ResultList
  33. from cloudbridge.cloud.interfaces.resources import SecurityGroup
  34. from cloudbridge.cloud.interfaces.resources import SecurityGroupRule
  35. from cloudbridge.cloud.interfaces.resources import Snapshot
  36. from cloudbridge.cloud.interfaces.resources import SnapshotState
  37. from cloudbridge.cloud.interfaces.resources import Subnet
  38. from cloudbridge.cloud.interfaces.resources import FloatingIP
  39. from cloudbridge.cloud.interfaces.resources import Volume
  40. from cloudbridge.cloud.interfaces.resources import VolumeState
  41. from cloudbridge.cloud.interfaces.exceptions import WaitStateException
  42. log = logging.getLogger(__name__)
  43. class BaseCloudResource(CloudResource):
  44. def __init__(self, provider):
  45. self.__provider = provider
  46. @property
  47. def _provider(self):
  48. return self.__provider
  49. def to_json(self):
  50. # Get all attributes but filter methods and private/magic ones
  51. attr = inspect.getmembers(self, lambda a: not(inspect.isroutine(a)))
  52. js = {k: v for(k, v) in attr if not k.startswith('_')}
  53. return json.dumps(js, sort_keys=True)
  54. class BaseObjectLifeCycleMixin(ObjectLifeCycleMixin):
  55. """
  56. A base implementation of an ObjectLifeCycleMixin.
  57. This base implementation has an implementation of wait_for
  58. which refreshes the object's state till the desired ready states
  59. are reached. Subclasses must still implement the wait_till_ready
  60. method, since the desired ready states are object specific.
  61. """
  62. def wait_for(self, target_states, terminal_states=None, timeout=None,
  63. interval=None):
  64. if timeout is None:
  65. timeout = self._provider.config.default_wait_timeout
  66. if interval is None:
  67. interval = self._provider.config.default_wait_interval
  68. assert timeout >= 0
  69. assert interval >= 0
  70. assert timeout >= interval
  71. end_time = time.time() + timeout
  72. while self.state not in target_states:
  73. if self.state in (terminal_states or []):
  74. raise WaitStateException(
  75. "Object: {0} is in state: {1} which is a terminal state"
  76. " and cannot be waited on.".format(self, self.state))
  77. else:
  78. log.debug(
  79. "Object %s is in state: %s. Waiting another %s"
  80. " seconds to reach target state(s): %s...",
  81. self,
  82. self.state,
  83. int(end_time - time.time()),
  84. target_states)
  85. time.sleep(interval)
  86. if time.time() > end_time:
  87. raise WaitStateException(
  88. "Waited too long for object: {0} to become ready. It's"
  89. " still in state: {1}".format(self, self.state))
  90. self.refresh()
  91. log.debug("Object: %s successfully reached target state: %s",
  92. self, self.state)
  93. return True
  94. class BaseResultList(ResultList):
  95. def __init__(
  96. self, is_truncated, marker, supports_total, total=None, data=None):
  97. # call list constructor
  98. super(BaseResultList, self).__init__(data or [])
  99. self._marker = marker
  100. self._is_truncated = is_truncated
  101. self._supports_total = True if supports_total else False
  102. self._total = total
  103. @property
  104. def marker(self):
  105. return self._marker
  106. @property
  107. def is_truncated(self):
  108. return self._is_truncated
  109. @property
  110. def supports_total(self):
  111. return self._supports_total
  112. @property
  113. def total_results(self):
  114. return self._total
  115. class ServerPagedResultList(BaseResultList):
  116. """
  117. This is a convenience class that extends the :class:`BaseResultList` class
  118. and provides a server side implementation of paging. It is meant for use by
  119. provider developers and is not meant for direct use by end-users.
  120. This class can be used to wrap a partial result list when an operation
  121. supports server side paging.
  122. """
  123. @property
  124. def supports_server_paging(self):
  125. return True
  126. @property
  127. def data(self):
  128. raise NotImplementedError(
  129. "ServerPagedResultLists do not support the data property")
  130. class ClientPagedResultList(BaseResultList):
  131. """
  132. This is a convenience class that extends the :class:`BaseResultList` class
  133. and provides a client side implementation of paging. It is meant for use by
  134. provider developers and is not meant for direct use by end-users.
  135. This class can be used to wrap a full result list when an operation does
  136. not support server side paging. This class will then provide a paged view
  137. of the full result set entirely on the client side.
  138. """
  139. def __init__(self, provider, objects, limit=None, marker=None):
  140. self._objects = objects
  141. limit = limit or provider.config.default_result_limit
  142. total_size = len(objects)
  143. if marker:
  144. from_marker = itertools.dropwhile(
  145. lambda obj: not obj.id == marker, objects)
  146. # skip one past the marker
  147. next(from_marker, None)
  148. objects = list(from_marker)
  149. is_truncated = len(objects) > limit
  150. results = list(itertools.islice(objects, limit))
  151. super(ClientPagedResultList, self).__init__(
  152. is_truncated,
  153. results[-1].id if is_truncated else None,
  154. True, total=total_size,
  155. data=results)
  156. @property
  157. def supports_server_paging(self):
  158. return False
  159. @property
  160. def data(self):
  161. return self._objects
  162. class BasePageableObjectMixin(PageableObjectMixin):
  163. """
  164. A mixin to provide iteration capability for a class
  165. that support a list(limit, marker) method.
  166. """
  167. def __iter__(self):
  168. marker = None
  169. result_list = self.list(marker=marker)
  170. if result_list.supports_server_paging:
  171. for result in result_list:
  172. yield result
  173. while result_list.is_truncated:
  174. result_list = self.list(marker=marker)
  175. for result in result_list:
  176. yield result
  177. marker = result_list.marker
  178. else:
  179. for result in result_list.data:
  180. yield result
  181. class BaseInstanceType(InstanceType, BaseCloudResource):
  182. def __init__(self, provider):
  183. super(BaseInstanceType, self).__init__(provider)
  184. def __eq__(self, other):
  185. return (isinstance(other, InstanceType) and
  186. # pylint:disable=protected-access
  187. self._provider == other._provider and
  188. self.id == other.id)
  189. @property
  190. def size_total_disk(self):
  191. return self.size_root_disk + self.size_ephemeral_disks
  192. def __repr__(self):
  193. return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
  194. self.name, self.id)
  195. class BaseInstance(BaseCloudResource, BaseObjectLifeCycleMixin, Instance):
  196. def __init__(self, provider):
  197. super(BaseInstance, self).__init__(provider)
  198. def __eq__(self, other):
  199. return (isinstance(other, Instance) and
  200. # pylint:disable=protected-access
  201. self._provider == other._provider and
  202. self.id == other.id and
  203. # check from most to least likely mutables
  204. self.state == other.state and
  205. self.name == other.name and
  206. self.security_groups == other.security_groups and
  207. self.public_ips == other.public_ips and
  208. self.private_ips == other.private_ips and
  209. self.image_id == other.image_id)
  210. def wait_till_ready(self, timeout=None, interval=None):
  211. self.wait_for(
  212. [InstanceState.RUNNING],
  213. terminal_states=[InstanceState.TERMINATED, InstanceState.ERROR],
  214. timeout=timeout,
  215. interval=interval)
  216. def __repr__(self):
  217. return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
  218. self.name, self.id)
  219. class BaseLaunchConfig(LaunchConfig):
  220. def __init__(self, provider):
  221. self.provider = provider
  222. self.block_devices = []
  223. class BlockDeviceMapping(object):
  224. """
  225. Represents a block device mapping
  226. """
  227. def __init__(self, is_volume=False, source=None, is_root=None,
  228. size=None, delete_on_terminate=None):
  229. self.is_volume = is_volume
  230. self.source = source
  231. self.is_root = is_root
  232. self.size = size
  233. self.delete_on_terminate = delete_on_terminate
  234. def add_ephemeral_device(self):
  235. block_device = BaseLaunchConfig.BlockDeviceMapping()
  236. self.block_devices.append(block_device)
  237. def add_volume_device(self, source=None, is_root=None, size=None,
  238. delete_on_terminate=None):
  239. block_device = self._validate_volume_device(
  240. source=source, is_root=is_root, size=size,
  241. delete_on_terminate=delete_on_terminate)
  242. self.block_devices.append(block_device)
  243. def _validate_volume_device(self, source=None, is_root=None,
  244. size=None, delete_on_terminate=None):
  245. """
  246. Validates a volume based device and throws an
  247. InvalidConfigurationException if the configuration is incorrect.
  248. """
  249. if source is None and not size:
  250. raise InvalidConfigurationException(
  251. "A size must be specified for a blank new volume")
  252. if source and \
  253. not isinstance(source, (Snapshot, Volume, MachineImage)):
  254. raise InvalidConfigurationException(
  255. "Source must be a Snapshot, Volume, MachineImage or None")
  256. if size:
  257. if not isinstance(size, six.integer_types) or not size > 0:
  258. raise InvalidConfigurationException(
  259. "The size must be None or a number greater than 0")
  260. if is_root:
  261. for bd in self.block_devices:
  262. if bd.is_root:
  263. raise InvalidConfigurationException(
  264. "An existing block device: {0} has already been"
  265. " marked as root. There can only be one root device.")
  266. return BaseLaunchConfig.BlockDeviceMapping(
  267. is_volume=True, source=source, is_root=is_root, size=size,
  268. delete_on_terminate=delete_on_terminate)
  269. class BaseMachineImage(
  270. BaseCloudResource, BaseObjectLifeCycleMixin, MachineImage):
  271. def __init__(self, provider):
  272. super(BaseMachineImage, self).__init__(provider)
  273. def __eq__(self, other):
  274. return (isinstance(other, MachineImage) and
  275. # pylint:disable=protected-access
  276. self._provider == other._provider and
  277. self.id == other.id and
  278. # check from most to least likely mutables
  279. self.state == other.state and
  280. self.name == other.name and
  281. self.description == other.description)
  282. def wait_till_ready(self, timeout=None, interval=None):
  283. self.wait_for(
  284. [MachineImageState.AVAILABLE],
  285. terminal_states=[MachineImageState.ERROR],
  286. timeout=timeout,
  287. interval=interval)
  288. def __repr__(self):
  289. return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
  290. self.name, self.id)
  291. class BaseAttachmentInfo(AttachmentInfo):
  292. def __init__(self, volume, instance_id, device):
  293. self._volume = volume
  294. self._instance_id = instance_id
  295. self._device = device
  296. @property
  297. def volume(self):
  298. return self._volume
  299. @property
  300. def instance_id(self):
  301. return self._instance_id
  302. @property
  303. def device(self):
  304. return self._device
  305. class BaseVolume(BaseCloudResource, BaseObjectLifeCycleMixin, Volume):
  306. def __init__(self, provider):
  307. super(BaseVolume, self).__init__(provider)
  308. def __eq__(self, other):
  309. return (isinstance(other, Volume) and
  310. # pylint:disable=protected-access
  311. self._provider == other._provider and
  312. self.id == other.id and
  313. # check from most to least likely mutables
  314. self.state == other.state and
  315. self.name == other.name)
  316. def wait_till_ready(self, timeout=None, interval=None):
  317. self.wait_for(
  318. [VolumeState.AVAILABLE],
  319. terminal_states=[VolumeState.ERROR, VolumeState.DELETED],
  320. timeout=timeout,
  321. interval=interval)
  322. def __repr__(self):
  323. return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
  324. self.name, self.id)
  325. class BaseSnapshot(BaseCloudResource, BaseObjectLifeCycleMixin, Snapshot):
  326. def __init__(self, provider):
  327. super(BaseSnapshot, self).__init__(provider)
  328. def __eq__(self, other):
  329. return (isinstance(other, Snapshot) and
  330. # pylint:disable=protected-access
  331. self._provider == other._provider and
  332. self.id == other.id and
  333. # check from most to least likely mutables
  334. self.state == other.state and
  335. self.name == other.name)
  336. def wait_till_ready(self, timeout=None, interval=None):
  337. self.wait_for(
  338. [SnapshotState.AVAILABLE],
  339. terminal_states=[SnapshotState.ERROR],
  340. timeout=timeout,
  341. interval=interval)
  342. def __repr__(self):
  343. return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
  344. self.name, self.id)
  345. class BaseKeyPair(KeyPair, BaseCloudResource):
  346. def __init__(self, provider, key_pair):
  347. super(BaseKeyPair, self).__init__(provider)
  348. self._key_pair = key_pair
  349. def __eq__(self, other):
  350. return (isinstance(other, KeyPair) and
  351. # pylint:disable=protected-access
  352. self._provider == other._provider and
  353. self.name == other.name)
  354. @property
  355. def id(self):
  356. """
  357. Return the id of this key pair.
  358. """
  359. return self._key_pair.name
  360. @property
  361. def name(self):
  362. """
  363. Return the name of this key pair.
  364. """
  365. return self._key_pair.name
  366. def delete(self):
  367. """
  368. Delete this KeyPair.
  369. :rtype: bool
  370. :return: True if successful, otherwise False.
  371. """
  372. # This implementation assumes the `delete` method exists across
  373. # multiple providers.
  374. self._key_pair.delete()
  375. def __repr__(self):
  376. return "<CBKeyPair: {0}>".format(self.name)
  377. class BaseSecurityGroup(SecurityGroup, BaseCloudResource):
  378. def __init__(self, provider, security_group):
  379. super(BaseSecurityGroup, self).__init__(provider)
  380. self._security_group = security_group
  381. def __eq__(self, other):
  382. """
  383. Check if all the defined rules match across both security groups.
  384. """
  385. return (isinstance(other, SecurityGroup) and
  386. # pylint:disable=protected-access
  387. self._provider == other._provider and
  388. len(self.rules) == len(other.rules) and # Shortcut
  389. set(self.rules) == set(other.rules))
  390. def __ne__(self, other):
  391. return not self.__eq__(other)
  392. @property
  393. def id(self):
  394. """
  395. Get the ID of this security group.
  396. :rtype: str
  397. :return: Security group ID
  398. """
  399. return self._security_group.id
  400. @property
  401. def name(self):
  402. """
  403. Return the name of this security group.
  404. """
  405. return self._security_group.name
  406. @property
  407. def description(self):
  408. """
  409. Return the description of this security group.
  410. """
  411. return self._security_group.description
  412. def delete(self):
  413. """
  414. Delete this security group.
  415. """
  416. return self._security_group.delete()
  417. def __repr__(self):
  418. return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
  419. self.id, self.name)
  420. class BaseSecurityGroupRule(SecurityGroupRule, BaseCloudResource):
  421. def __init__(self, provider, rule, parent):
  422. super(BaseSecurityGroupRule, self).__init__(provider)
  423. self._rule = rule
  424. self.parent = parent
  425. def __repr__(self):
  426. return ("<CBSecurityGroupRule: IP: {0}; from: {1}; to: {2}; grp: {3}>"
  427. .format(self.ip_protocol, self.from_port, self.to_port,
  428. self.group))
  429. def __eq__(self, other):
  430. return self.ip_protocol == other.ip_protocol and \
  431. self.from_port == other.from_port and \
  432. self.to_port == other.to_port and \
  433. self.cidr_ip == other.cidr_ip
  434. def __ne__(self, other):
  435. return not self.__eq__(other)
  436. def __hash__(self):
  437. """
  438. Return a hash-based interpretation of all of the object's field values.
  439. This is requried for operations on hashed collections including
  440. ``set``, ``frozenset``, and ``dict``.
  441. """
  442. return hash("{0}{1}{2}{3}{4}".format(self.ip_protocol, self.from_port,
  443. self.to_port, self.cidr_ip,
  444. self.group))
  445. class BasePlacementZone(PlacementZone, BaseCloudResource):
  446. def __init__(self, provider):
  447. super(BasePlacementZone, self).__init__(provider)
  448. def __repr__(self):
  449. return "<CB-{0}: {1}>".format(self.__class__.__name__,
  450. self.id)
  451. def __eq__(self, other):
  452. return (isinstance(other, PlacementZone) and
  453. # pylint:disable=protected-access
  454. self._provider == other._provider and
  455. self.id == other.id)
  456. class BaseRegion(Region, BaseCloudResource):
  457. def __init__(self, provider):
  458. super(BaseRegion, self).__init__(provider)
  459. def __repr__(self):
  460. return "<CB-{0}: {1}>".format(self.__class__.__name__,
  461. self.id)
  462. def __eq__(self, other):
  463. return (isinstance(other, Region) and
  464. # pylint:disable=protected-access
  465. self._provider == other._provider and
  466. self.id == other.id)
  467. def to_json(self):
  468. attr = inspect.getmembers(self, lambda a: not(inspect.isroutine(a)))
  469. js = {k: v for(k, v) in attr if not k.startswith('_')}
  470. js['zones'] = [z.name for z in self.zones]
  471. return json.dumps(js, sort_keys=True)
  472. class BaseBucketObject(BucketObject, BaseCloudResource):
  473. def __init__(self, provider):
  474. super(BaseBucketObject, self).__init__(provider)
  475. def save_content(self, target_stream):
  476. """
  477. Download this object and write its
  478. contents to the target_stream.
  479. """
  480. shutil.copyfileobj(self.iter_content(), target_stream)
  481. def __eq__(self, other):
  482. return (isinstance(other, BucketObject) and
  483. # pylint:disable=protected-access
  484. self._provider == other._provider and
  485. self.id == other.id and
  486. # check from most to least likely mutables
  487. self.name == other.name)
  488. def __repr__(self):
  489. return "<CB-{0}: {1}>".format(self.__class__.__name__,
  490. self.name)
  491. class BaseBucket(BasePageableObjectMixin, Bucket, BaseCloudResource):
  492. def __init__(self, provider):
  493. super(BaseBucket, self).__init__(provider)
  494. def __eq__(self, other):
  495. return (isinstance(other, Bucket) and
  496. # pylint:disable=protected-access
  497. self._provider == other._provider and
  498. self.id == other.id and
  499. # check from most to least likely mutables
  500. self.name == other.name)
  501. def __repr__(self):
  502. return "<CB-{0}: {1}>".format(self.__class__.__name__,
  503. self.name)
  504. class BaseNetwork(BaseCloudResource, Network, BaseObjectLifeCycleMixin):
  505. CB_DEFAULT_NETWORK_NAME = os.environ.get('CB_DEFAULT_NETWORK_NAME',
  506. 'CloudBridgeNet')
  507. def __init__(self, provider):
  508. super(BaseNetwork, self).__init__(provider)
  509. def __repr__(self):
  510. return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
  511. self.id, self.name)
  512. def wait_till_ready(self, timeout=None, interval=None):
  513. self.wait_for(
  514. [NetworkState.AVAILABLE],
  515. terminal_states=[NetworkState.ERROR],
  516. timeout=timeout,
  517. interval=interval)
  518. def __eq__(self, other):
  519. return (isinstance(other, Network) and
  520. # pylint:disable=protected-access
  521. self._provider == other._provider and
  522. self.id == other.id)
  523. class BaseSubnet(Subnet, BaseCloudResource):
  524. CB_DEFAULT_SUBNET_NAME = os.environ.get('CB_DEFAULT_SUBNET_NAME',
  525. 'CloudBridgeSubnet')
  526. def __init__(self, provider):
  527. super(BaseSubnet, self).__init__(provider)
  528. def __repr__(self):
  529. return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
  530. self.id, self.name)
  531. def __eq__(self, other):
  532. return (isinstance(other, Subnet) and
  533. # pylint:disable=protected-access
  534. self._provider == other._provider and
  535. self.id == other.id)
  536. class BaseFloatingIP(FloatingIP, BaseCloudResource):
  537. def __init__(self, provider):
  538. super(BaseFloatingIP, self).__init__(provider)
  539. def __repr__(self):
  540. return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
  541. self.id, self.public_ip)
  542. def __eq__(self, other):
  543. return (isinstance(other, FloatingIP) and
  544. # pylint:disable=protected-access
  545. self._provider == other._provider and
  546. self.id == other.id)
  547. class BaseRouter(Router, BaseCloudResource):
  548. CB_DEFAULT_ROUTER_NAME = os.environ.get('CB_DEFAULT_ROUTER_NAME',
  549. 'CloudBridgeRouter')
  550. def __init__(self, provider):
  551. super(BaseRouter, self).__init__(provider)
  552. def __repr__(self):
  553. return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__, self.id,
  554. self.name)
  555. def __eq__(self, other):
  556. return (isinstance(other, Router) and
  557. # pylint:disable=protected-access
  558. self._provider == other._provider and
  559. self.id == other.id)