replicationconfig.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. # -*- coding: utf-8 -*-
  2. # MinIO Python Library for Amazon S3 Compatible Cloud Storage, (C)
  3. # 2020 MinIO, Inc.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. """Request/response of PutBucketReplication and GetBucketReplication APIs."""
  17. from __future__ import absolute_import, annotations
  18. from abc import ABC
  19. from dataclasses import dataclass
  20. from typing import Optional, Type, TypeVar, cast
  21. from xml.etree import ElementTree as ET
  22. from .commonconfig import DISABLED, BaseRule, Filter, check_status
  23. from .xml import Element, SubElement, find, findall, findtext
  24. A = TypeVar("A", bound="Status")
  25. @dataclass(frozen=True)
  26. class Status(ABC):
  27. """Status."""
  28. status: str
  29. @classmethod
  30. def fromxml(cls: Type[A], element: ET.Element) -> A:
  31. """Create new object with values from XML element."""
  32. element = cast(ET.Element, find(element, cls.__name__, True))
  33. status = cast(str, findtext(element, "Status", True))
  34. return cls(status)
  35. def toxml(self, element: Optional[ET.Element]) -> ET.Element:
  36. """Convert to XML."""
  37. if element is None:
  38. raise ValueError("element must be provided")
  39. element = SubElement(element, self.__class__.__name__)
  40. SubElement(element, "Status", self.status)
  41. return element
  42. @dataclass(frozen=True)
  43. class SseKmsEncryptedObjects(Status):
  44. """SSE KMS encrypted objects."""
  45. B = TypeVar("B", bound="SourceSelectionCriteria")
  46. @dataclass(frozen=True)
  47. class SourceSelectionCriteria:
  48. """Source selection criteria."""
  49. sse_kms_encrypted_objects: Optional[SseKmsEncryptedObjects] = None
  50. @classmethod
  51. def fromxml(cls: Type[B], element: ET.Element) -> B:
  52. """Create new object with values from XML element."""
  53. element = cast(
  54. ET.Element,
  55. find(element, "SourceSelectionCriteria", True),
  56. )
  57. return cls(
  58. None if find(element, "SseKmsEncryptedObjects") is None
  59. else SseKmsEncryptedObjects.fromxml(element)
  60. )
  61. def toxml(self, element: Optional[ET.Element]) -> ET.Element:
  62. """Convert to XML."""
  63. if element is None:
  64. raise ValueError("element must be provided")
  65. element = SubElement(element, "SourceSelectionCriteria")
  66. if self.sse_kms_encrypted_objects:
  67. self.sse_kms_encrypted_objects.toxml(element)
  68. return element
  69. @dataclass(frozen=True)
  70. class ExistingObjectReplication(Status):
  71. """Existing object replication."""
  72. @dataclass(frozen=True)
  73. class DeleteMarkerReplication(Status):
  74. """Delete marker replication."""
  75. def __init__(self, status=DISABLED):
  76. super().__init__(status)
  77. C = TypeVar("C", bound="ReplicationTimeValue")
  78. @dataclass(frozen=True)
  79. class ReplicationTimeValue(ABC):
  80. """Replication time value."""
  81. minutes: Optional[int] = 15
  82. @classmethod
  83. def fromxml(cls: Type[C], element: ET.Element) -> C:
  84. """Create new object with values from XML element."""
  85. element = cast(ET.Element, find(element, cls.__name__, True))
  86. minutes = findtext(element, "Minutes")
  87. return cls(int(minutes) if minutes else None)
  88. def toxml(self, element: Optional[ET.Element]) -> ET.Element:
  89. """Convert to XML."""
  90. if element is None:
  91. raise ValueError("element must be provided")
  92. element = SubElement(element, self.__class__.__name__)
  93. if self.minutes is not None:
  94. SubElement(element, "Minutes", str(self.minutes))
  95. return element
  96. @dataclass(frozen=True)
  97. class Time(ReplicationTimeValue):
  98. """Time."""
  99. D = TypeVar("D", bound="ReplicationTime")
  100. @dataclass(frozen=True)
  101. class ReplicationTime:
  102. """Replication time."""
  103. time: Time
  104. status: str
  105. def __post_init__(self,):
  106. if not self.time:
  107. raise ValueError("time must be provided")
  108. check_status(self.status)
  109. @classmethod
  110. def fromxml(cls: Type[D], element: ET.Element) -> D:
  111. """Create new object with values from XML element."""
  112. element = cast(ET.Element, find(element, "ReplicationTime", True))
  113. time = Time.fromxml(element)
  114. status = cast(str, findtext(element, "Status", True))
  115. return cls(time, status)
  116. def toxml(self, element: Optional[ET.Element]) -> ET.Element:
  117. """Convert to XML."""
  118. if element is None:
  119. raise ValueError("element must be provided")
  120. element = SubElement(element, "ReplicationTime")
  121. self.time.toxml(element)
  122. SubElement(element, "Status", self.status)
  123. return element
  124. @dataclass(frozen=True)
  125. class EventThreshold(ReplicationTimeValue):
  126. """Event threshold."""
  127. E = TypeVar("E", bound="Metrics")
  128. @dataclass(frozen=True)
  129. class Metrics:
  130. """Metrics."""
  131. event_threshold: EventThreshold
  132. status: str
  133. def __post_init__(self):
  134. if not self.event_threshold:
  135. raise ValueError("event threshold must be provided")
  136. check_status(self.status)
  137. @classmethod
  138. def fromxml(cls: Type[E], element: ET.Element) -> E:
  139. """Create new object with values from XML element."""
  140. element = cast(ET.Element, find(element, "Metrics", True))
  141. event_threshold = EventThreshold.fromxml(element)
  142. status = cast(str, findtext(element, "Status", True))
  143. return cls(event_threshold, status)
  144. def toxml(self, element: Optional[ET.Element]) -> ET.Element:
  145. """Convert to XML."""
  146. if element is None:
  147. raise ValueError("element must be provided")
  148. element = SubElement(element, "Metrics")
  149. self.event_threshold.toxml(element)
  150. SubElement(element, "Status", self.status)
  151. return element
  152. F = TypeVar("F", bound="EncryptionConfig")
  153. @dataclass(frozen=True)
  154. class EncryptionConfig:
  155. """Encryption configuration."""
  156. replica_kms_key_id: Optional[str] = None
  157. @classmethod
  158. def fromxml(cls: Type[F], element: ET.Element) -> F:
  159. """Create new object with values from XML element."""
  160. element = cast(
  161. ET.Element,
  162. find(element, "EncryptionConfiguration", True),
  163. )
  164. return cls(findtext(element, "ReplicaKmsKeyID"))
  165. def toxml(self, element: Optional[ET.Element]) -> ET.Element:
  166. """Convert to XML."""
  167. if element is None:
  168. raise ValueError("element must be provided")
  169. element = SubElement(element, "EncryptionConfiguration")
  170. SubElement(element, "ReplicaKmsKeyID", self.replica_kms_key_id)
  171. return element
  172. G = TypeVar("G", bound="AccessControlTranslation")
  173. @dataclass(frozen=True)
  174. class AccessControlTranslation:
  175. """Access control translation."""
  176. owner: str = "Destination"
  177. def __post_init__(self):
  178. if not self.owner:
  179. raise ValueError("owner must be provided")
  180. @classmethod
  181. def fromxml(cls: Type[G], element: ET.Element) -> G:
  182. """Create new object with values from XML element."""
  183. element = cast(
  184. ET.Element, find(element, "AccessControlTranslation", True),
  185. )
  186. owner = cast(str, findtext(element, "Owner", True))
  187. return cls(owner)
  188. def toxml(self, element: Optional[ET.Element]) -> ET.Element:
  189. """Convert to XML."""
  190. if element is None:
  191. raise ValueError("element must be provided")
  192. element = SubElement(element, "AccessControlTranslation")
  193. SubElement(element, "Owner", self.owner)
  194. return element
  195. H = TypeVar("H", bound="Destination")
  196. @dataclass(frozen=True)
  197. class Destination:
  198. """Replication destination."""
  199. bucket_arn: str
  200. access_control_translation: Optional[AccessControlTranslation] = None
  201. account: Optional[str] = None
  202. encryption_config: Optional[EncryptionConfig] = None
  203. metrics: Optional[Metrics] = None
  204. replication_time: Optional[ReplicationTime] = None
  205. storage_class: Optional[str] = None
  206. def __post_init__(self):
  207. if not self.bucket_arn:
  208. raise ValueError("bucket ARN must be provided")
  209. @classmethod
  210. def fromxml(cls: Type[H], element: ET.Element) -> H:
  211. """Create new object with values from XML element."""
  212. element = cast(ET.Element, find(element, "Destination", True))
  213. access_control_translation = (
  214. None if find(element, "AccessControlTranslation") is None
  215. else AccessControlTranslation.fromxml(element)
  216. )
  217. account = findtext(element, "Account")
  218. bucket_arn = cast(str, findtext(element, "Bucket", True))
  219. encryption_config = (
  220. None if find(element, "EncryptionConfiguration") is None
  221. else EncryptionConfig.fromxml(element)
  222. )
  223. metrics = (
  224. None if find(element, "Metrics") is None
  225. else Metrics.fromxml(element)
  226. )
  227. replication_time = (
  228. None if find(element, "ReplicationTime") is None
  229. else ReplicationTime.fromxml(element)
  230. )
  231. storage_class = findtext(element, "StorageClass")
  232. return cls(bucket_arn, access_control_translation, account,
  233. encryption_config, metrics, replication_time, storage_class)
  234. def toxml(self, element: Optional[ET.Element]) -> ET.Element:
  235. """Convert to XML."""
  236. if element is None:
  237. raise ValueError("element must be provided")
  238. element = SubElement(element, "Destination")
  239. if self.access_control_translation:
  240. self.access_control_translation.toxml(element)
  241. if self.account is not None:
  242. SubElement(element, "Account", self.account)
  243. SubElement(element, "Bucket", self.bucket_arn)
  244. if self.encryption_config:
  245. self.encryption_config.toxml(element)
  246. if self.metrics:
  247. self.metrics.toxml(element)
  248. if self.replication_time:
  249. self.replication_time.toxml(element)
  250. if self.storage_class:
  251. SubElement(element, "StorageClass", self.storage_class)
  252. return element
  253. I = TypeVar("I", bound="Rule")
  254. @dataclass(frozen=True)
  255. class Rule(BaseRule):
  256. """Replication rule. """
  257. destination: Optional[Destination] = None
  258. delete_marker_replication: Optional[DeleteMarkerReplication] = None
  259. existing_object_replication: Optional[ExistingObjectReplication] = None
  260. rule_filter: Optional[Filter] = None
  261. rule_id: Optional[str] = None
  262. prefix: Optional[str] = None
  263. priority: Optional[int] = None
  264. source_selection_criteria: Optional[SourceSelectionCriteria] = None
  265. def __post_init__(self):
  266. if not self.destination:
  267. raise ValueError("destination must be provided")
  268. def _require_subclass_implementation(self) -> None:
  269. """Dummy abstract method to enforce abstract class behavior."""
  270. @classmethod
  271. def fromxml(cls: Type[I], element: ET.Element) -> I:
  272. """Create new object with values from XML element."""
  273. status, rule_filter, rule_id = cls.parsexml(element)
  274. delete_marker_replication = (
  275. None if find(element, "DeleteMarkerReplication") is None
  276. else DeleteMarkerReplication.fromxml(element)
  277. )
  278. destination = Destination.fromxml(element)
  279. existing_object_replication = (
  280. None if find(element, "ExistingObjectReplication") is None
  281. else ExistingObjectReplication.fromxml(element)
  282. )
  283. prefix = findtext(element, "Prefix")
  284. priority = findtext(element, "Priority")
  285. source_selection_criteria = (
  286. None if find(element, "SourceSelectionCriteria") is None
  287. else SourceSelectionCriteria.fromxml(element)
  288. )
  289. return cls(
  290. status=status,
  291. rule_filter=rule_filter,
  292. rule_id=rule_id,
  293. destination=destination,
  294. delete_marker_replication=delete_marker_replication,
  295. existing_object_replication=existing_object_replication,
  296. prefix=prefix,
  297. priority=int(priority) if priority else None,
  298. source_selection_criteria=source_selection_criteria,
  299. )
  300. def toxml(self, element: Optional[ET.Element]) -> ET.Element:
  301. """Convert to XML."""
  302. if element is None:
  303. raise ValueError("element must be provided")
  304. element = SubElement(element, "Rule")
  305. super().toxml(element)
  306. if self.delete_marker_replication:
  307. self.delete_marker_replication.toxml(element)
  308. if self.destination:
  309. self.destination.toxml(element)
  310. if self.existing_object_replication:
  311. self.existing_object_replication.toxml(element)
  312. if self.prefix is not None:
  313. SubElement(element, "Prefix", self.prefix)
  314. if self.priority is not None:
  315. SubElement(element, "Priority", str(self.priority))
  316. if self.source_selection_criteria:
  317. self.source_selection_criteria.toxml(element)
  318. return element
  319. J = TypeVar("J", bound="ReplicationConfig")
  320. @dataclass(frozen=True)
  321. class ReplicationConfig:
  322. """Replication configuration."""
  323. role: str
  324. rules: list[Rule]
  325. def __post_init__(self):
  326. if not self.rules:
  327. raise ValueError("rules must be provided")
  328. if len(self.rules) > 1000:
  329. raise ValueError("more than 1000 rules are not supported")
  330. @classmethod
  331. def fromxml(cls: Type[J], element: ET.Element) -> J:
  332. """Create new object with values from XML element."""
  333. role = cast(str, findtext(element, "Role", True))
  334. elements = findall(element, "Rule")
  335. rules = []
  336. for tag in elements:
  337. rules.append(Rule.fromxml(tag))
  338. return cls(role, rules)
  339. def toxml(self, element: Optional[ET.Element]) -> ET.Element:
  340. """Convert to XML."""
  341. element = Element("ReplicationConfiguration")
  342. SubElement(element, "Role", self.role)
  343. for rule in self.rules:
  344. rule.toxml(element)
  345. return element