From f58a9e76d9e4d7f53c6b2fa2c759926437cfd28b Mon Sep 17 00:00:00 2001 From: Oren Epshtain Date: Sun, 4 Feb 2024 22:25:50 +0000 Subject: [PATCH] Release version 240.1.1 --- .git-blame-ignore-revs | 1 + .gitlab-ci.yml | 4 +- CHANGELOG | 21 +++++ doc/api.rst | 14 +++ doc/events.rst | 56 ++++++----- doc/index.rst | 2 + doc/smb_server_capabilities.rst | 28 ++++++ doc/snapshot_policies.rst | 69 ++++++++++++++ doc/snapshot_policies.rst.doctest_context | 18 ++++ doc/system_object.rst | 8 ++ infinisdk/core/bindings.py | 74 +++++++++++++++ infinisdk/core/translators_and_types.py | 29 +++++- infinisdk/core/utils/resolvers.py | 18 ++++ infinisdk/infinibox/compatibility.py | 44 ++++++++- infinisdk/infinibox/components.py | 1 - infinisdk/infinibox/cons_group.py | 86 ++++++++++++++++- infinisdk/infinibox/dataset.py | 87 ++++++++++++++++- infinisdk/infinibox/infinibox.py | 74 +++++++++++++++ infinisdk/infinibox/ldap_config.py | 4 +- infinisdk/infinibox/network_interface.py | 12 +++ infinisdk/infinibox/replica.py | 16 ++-- infinisdk/infinibox/schedule.py | 108 ++++++++++++++++++++++ infinisdk/infinibox/snapshot_policy.py | 103 +++++++++++++++++++++ infinisdk/infinibox/sso_config.py | 78 ++++++++++++++++ infinisdk/infinibox/volume.py | 9 ++ setup.cfg | 4 +- 26 files changed, 923 insertions(+), 45 deletions(-) create mode 100644 doc/smb_server_capabilities.rst create mode 100644 doc/snapshot_policies.rst create mode 100644 doc/snapshot_policies.rst.doctest_context create mode 100644 infinisdk/core/utils/resolvers.py create mode 100644 infinisdk/infinibox/schedule.py create mode 100644 infinisdk/infinibox/snapshot_policy.py create mode 100644 infinisdk/infinibox/sso_config.py diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 135b8707..b5899647 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,2 +1,3 @@ # Added isort and black and run on infinisdk folder +1aa81b3acec2145d69beb132bd5dab7ec79a1b3e 2b85e6e2a42fa1b0f3e7a4c6268fdf3428502a50 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f97c7c43..ce5fa11e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -117,8 +117,8 @@ test-release: - python -m pip install --upgrade pip - pip install setuptools wheel twine - python setup.py sdist bdist_wheel - - twine upload -u $TEST_PYPI_USER -p $TEST_PYPI_PASSWORD --verbose --non-interactive --repository-url https://test.pypi.org/legacy/ dist/* - - python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple infinisdk + - twine upload -u "__token__" -p $TEST_PYPI_API_TOKEN --verbose --non-interactive --repository-url https://test.pypi.org/legacy/ dist/* + - python -m pip install --pre --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple infinisdk - python -c "import infinisdk" only: refs: diff --git a/CHANGELOG b/CHANGELOG index c6ce8748..5e655e8d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,24 @@ +Version 240.1.1 (Released 2024-01-31) +------------------------------------- +* #16462: Add pagination for the new metadata of get all assigned entities +* #16368: Add RelatedSubObjectBinding +* #16348: Add created_by_schedule_name fields and default policy indicator +* #16335: Add created_by_schedule_name to dataset and cons_group in InfiniSDK +* #16318: Add support for snapshot policies enhancements, rename sso field +* #16306: Rename auth_link_url to sign_on_url in SSO in InfiniSDK +* #16287: Remove encryption_state field from LocalDrive in InfiniSDK [breaking change] +* #16284: Add missing ldap config fields to InfiniSDK +* #16280: Add tests and documentation for ldap config modify when passing an update dictionary +* #16231: Support snapshot policy enhancements +* #16223: Make snapshot policy suffix to contain a random part in its name +* #16198: Support config of SMB server capabilities +* #16155: Support SSA Express +* #16149: Support single sign on (SSO) identity providers management +* #16131: Support disable replication for a single snapshot +* #16044: Support block-ssh +* #15920: Support Snapshot Group (SG) replicate snapshots +* #15888: Add Snapshot Policies (Snap Scheduler) + Version 225.1.1 (Released 2023-05-07) ------------------------------------- * #16212: Add infi.dtypes.nqn to requirements diff --git a/doc/api.rst b/doc/api.rst index fa6f7274..fb6982b4 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -270,6 +270,20 @@ infinibox.smb_users .. autoclass:: SMBUser :members: +infinibox.snapshot_policies +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: infinisdk.infinibox.snapshot_policy +.. autoclass:: SnapshotPolicy + :members: + +infinibox.snapshot_policies.schedules +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: infinisdk.infinibox.schedule +.. autoclass:: Schedule + :members: + infinibox.active_directory_domains ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/events.rst b/doc/events.rst index 5d232d03..e610f755 100644 --- a/doc/events.rst +++ b/doc/events.rst @@ -7,13 +7,18 @@ InfiniSDK represents system events through the *system.events* collection, which >>> for event in system.events: ... print(event) # doctest: +ELLIPSIS - <...:Event id=1000, code=VOLUME_CREATED> - <...:Event id=1001, code=VOLUME_DELETED> - <...:Event id=1002, code=VOLUME_CREATED> - <...:Event id=1003, code=VOLUME_DELETED> - <...:Event id=1004, code=VOLUME_CREATED> - <...:Event id=1005, code=VOLUME_DELETED> - <...:Event id=1006, code=USER_LOGIN_SUCCESS> + <...:Event id=1000, code=SNAPSHOT_POLICY_CREATED> + <...:Event id=1001, code=SNAPSHOT_SCHEDULE_CREATED> + <...:Event id=1002, code=SNAPSHOT_SCHEDULE_ENABLED> + <...:Event id=1003, code=SNAPSHOT_SCHEDULE_CREATED> + <...:Event id=1004, code=SNAPSHOT_SCHEDULE_ENABLED> + <...:Event id=1005, code=VOLUME_CREATED> + <...:Event id=1006, code=VOLUME_DELETED> + <...:Event id=1007, code=VOLUME_CREATED> + <...:Event id=1008, code=VOLUME_DELETED> + <...:Event id=1009, code=VOLUME_CREATED> + <...:Event id=1010, code=VOLUME_DELETED> + <...:Event id=1011, code=USER_LOGIN_SUCCESS> Sorting is determined by the system by default, but we can easily change that. For instance, we can order the events by descending id: @@ -22,13 +27,18 @@ Sorting is determined by the system by default, but we can easily change that. F >>> for event in system.events.find().sort(-system.events.fields.id): ... print(event) # doctest: +ELLIPSIS - <...:Event id=1006, code=USER_LOGIN_SUCCESS> - <...:Event id=1005, code=VOLUME_DELETED> - <...:Event id=1004, code=VOLUME_CREATED> - <...:Event id=1003, code=VOLUME_DELETED> - <...:Event id=1002, code=VOLUME_CREATED> - <...:Event id=1001, code=VOLUME_DELETED> - <...:Event id=1000, code=VOLUME_CREATED> + <...:Event id=1011, code=USER_LOGIN_SUCCESS> + <...:Event id=1010, code=VOLUME_DELETED> + <...:Event id=1009, code=VOLUME_CREATED> + <...:Event id=1008, code=VOLUME_DELETED> + <...:Event id=1007, code=VOLUME_CREATED> + <...:Event id=1006, code=VOLUME_DELETED> + <...:Event id=1005, code=VOLUME_CREATED> + <...:Event id=1004, code=SNAPSHOT_SCHEDULE_ENABLED> + <...:Event id=1003, code=SNAPSHOT_SCHEDULE_CREATED> + <...:Event id=1002, code=SNAPSHOT_SCHEDULE_ENABLED> + <...:Event id=1001, code=SNAPSHOT_SCHEDULE_CREATED> + <...:Event id=1000, code=SNAPSHOT_POLICY_CREATED> We can also combine this with filtering. The following example filters by specific event code: @@ -36,9 +46,9 @@ We can also combine this with filtering. The following example filters by specif >>> for event in system.events.find(code='VOLUME_CREATED').sort(-system.events.fields.id): ... print(event) # doctest: +ELLIPSIS - <...:Event id=1004, code=VOLUME_CREATED> - <...:Event id=1002, code=VOLUME_CREATED> - <...:Event id=1000, code=VOLUME_CREATED> + <...:Event id=1009, code=VOLUME_CREATED> + <...:Event id=1007, code=VOLUME_CREATED> + <...:Event id=1005, code=VOLUME_CREATED> Example: Getting all Events Newer than a Specific Sequence Number ----------------------------------------------------------------- @@ -48,7 +58,11 @@ Example: Getting all Events Newer than a Specific Sequence Number >>> from infinisdk import Q >>> for e in system.events.find(Q.seq_num>=1004): ... print(e) # doctest: +ELLIPSIS - <...:Event id=1004, code=VOLUME_CREATED> - <...:Event id=1005, code=VOLUME_DELETED> - <...:Event id=1006, code=USER_LOGIN_SUCCESS> - + <...:Event id=1004, code=SNAPSHOT_SCHEDULE_ENABLED> + <...:Event id=1005, code=VOLUME_CREATED> + <...:Event id=1006, code=VOLUME_DELETED> + <...:Event id=1007, code=VOLUME_CREATED> + <...:Event id=1008, code=VOLUME_DELETED> + <...:Event id=1009, code=VOLUME_CREATED> + <...:Event id=1010, code=VOLUME_DELETED> + <...:Event id=1011, code=USER_LOGIN_SUCCESS> diff --git a/doc/index.rst b/doc/index.rst index 01174624..42a56422 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -31,6 +31,7 @@ Contents: filesystems mappings snapshots + snapshot_policies cons_groups users system_configuration @@ -48,6 +49,7 @@ Contents: api advanced_usage hooks + smb_server_capabilities Indices and tables diff --git a/doc/smb_server_capabilities.rst b/doc/smb_server_capabilities.rst new file mode 100644 index 00000000..1f540539 --- /dev/null +++ b/doc/smb_server_capabilities.rst @@ -0,0 +1,28 @@ +SMB Server Capabilities +======================= + +These are SMB settings at the level of the tenant, so there is one configuration per tenant. + +Getting Current Server Capabilities +----------------------------------- +You can get the current configuration by: + +.. code-block:: python + + >>> system.get_smb_server_capabilities() # doctest: +SKIP + +The default tenant SMB server capabilities will be returned. You should expect the following fields to be returned: + +* `min_smb_protocol` +* `max_smb_protocol` +* `smb_signing` +* `smb_encryption` + +Updating Server Capabilities +---------------------------- + +To update a field, e.g. `encryption`: + +.. code-block:: python + + >>> system.update_smb_server_capabilities(smb_encryption="disabled") # doctest: +SKIP diff --git a/doc/snapshot_policies.rst b/doc/snapshot_policies.rst new file mode 100644 index 00000000..647d4bd9 --- /dev/null +++ b/doc/snapshot_policies.rst @@ -0,0 +1,69 @@ +Snapshot Policies +================= + +This is a process that automates periodic creation of snapshots on any of the system's storage entities - filesystem, volume, and a consistency-group (CG). Snapshot policies define the rules for snapshot creation. + +Creating a Policy +----------------- +A policy defines how and when to create snapshots. It contains a list of schedules which define when to create a snapshot. + +To create a policy: + +.. code-block:: python + + >>> policy1 = system.snapshot_policies.create() + +An optional parameters of `name` and `suffix` can be passed in creation. The suffix is a string which will be added to the snapshots' names. + +Creating a Schedule +------------------- +After creating a policy you can create a schedule for creating the snapshot: + +.. code-block:: python + + >> from datetime import timedelta + >> schedule1 = policy1.schedules.create(name="every3hours",interval=timedelta(seconds=7200),retention=timedelta(seconds=3600)) + +In this example a snapshot will be taken every 7200 seconds and be retained for 3600 seconds (after which it will be deleted). + +The default type of schedule is `periodic` but you can also specify `type=clock` to denote a specific day and time. To do that you need to specify 2 additional parameters: + 1. `day_of_week` which can get string values of the days of the week ("sunday", "monday", etc.) or "all" for all the days in the week. + 2. `time_of_day` which denotes the time in the day to perform the snapshot operation. + +.. code-block:: python + + >> from datetime import time + >> schedule2 = policy1.schedules.create(name="every3hours",type="clock",day_of_week="sunday",time_of_day=time(20, 30, 10),retention=timedelta(seconds=3600)) # doctest: +SKIP + +In this example the snapshot will be taken every Sunday at 20:30:10 (8 PM, 30 minutes, 10 seconds). + +A schedule cannot be updated. To make changes the schedule should be deleted and a new one with the change should be created. +Each policy can have up to 8 schedules. + +Assigning Policies to Datasets +------------------------------ +Snapshot policies can be assigned to datasets (of type master and snapshot): volumes, filesystems and CGs. +This dataset then will be snapshoted according to the policy schedule. + +.. code-block:: python + + >>> fs1 = system.filesystems.create(name="fs1", pool=pool) + >>> policy1.assign_entity(entity=fs1) + +To unassign an entity from a policy you need to pass the entity instance that you want to unassign: + +.. code-block:: python + + >>> policy1.unassign_entity(entity=fs1) + +To get all the assigned entities for a policy you can do: + +.. code-block:: python + + >>> policy1_entities = policy1.get_assigned_entities() + +You can also pass either one or both of the desired page size and page number in case you have many entities: + +.. code-block:: python + + >>> policy1_entities = policy1.get_assigned_entities(page_size=1, page=1) diff --git a/doc/snapshot_policies.rst.doctest_context b/doc/snapshot_policies.rst.doctest_context new file mode 100644 index 00000000..f642de26 --- /dev/null +++ b/doc/snapshot_policies.rst.doctest_context @@ -0,0 +1,18 @@ +# -*- mode: python -*- +from contextlib import contextmanager +from infinisim.infinibox import Infinibox as Simulator +from infinisdk import InfiniBox + +@contextmanager +def doctest_context(): + simulator = Simulator() + simulator.activate() + system = InfiniBox(simulator, auth=('infinidat', '123456')) + system.login() + try: + yield { + "system": system, + "pool": system.pools.create(), + } + finally: + simulator.deactivate() diff --git a/doc/system_object.rst b/doc/system_object.rst index 9580c51a..3b7c7f1c 100644 --- a/doc/system_object.rst +++ b/doc/system_object.rst @@ -23,5 +23,13 @@ The :meth:`infinisdk.infinibox.InfiniBox.get_model_name` method retrieves the mo .. seealso:: :class:`infinisdk.infinibox.InfiniBox` +Opening and Closing SSH Ports +----------------------------- + +You can open/close the SSH ports of the system as well as querying its status by: +.. code-block:: python + >>> system.open_ssh_ports() # doctest: +SKIP + >>> system.close_ssh_ports() # doctest: +SKIP + >>> system.get_ssh_ports_status() # doctest: +SKIP diff --git a/infinisdk/core/bindings.py b/infinisdk/core/bindings.py index f9ac574d..cbfe6143 100644 --- a/infinisdk/core/bindings.py +++ b/infinisdk/core/bindings.py @@ -1,8 +1,11 @@ +from typing import Callable + from api_object_schema import ObjectAPIBinding from munch import Munch from sentinels import NOTHING from .api.special_values import RawValue, SpecialValue +from .exceptions import InfiniSDKRuntimeException, InvalidUsageException from .translators_and_types import address_type_factory, host_port_from_api # pylint: disable=abstract-method @@ -276,3 +279,74 @@ def get_value_from_api_value(self, system, objtype, obj, api_value): if api_value == self._value_for_none or api_value is None: return None return getattr(system, self._collection_name).get_by_id_lazy(api_value["id"]) + + +class RelatedSubObjectBinding(RelatedObjectBinding): + def __init__( + self, collection_name=None, value_for_none=0, child_collection_resolver=None + ): + super().__init__(collection_name, value_for_none) + self.child_collection_resolver = child_collection_resolver + + def get_value_from_api_value(self, system, objtype, obj, api_value): + if api_value == self._value_for_none or api_value is None: + return None + if self._collection_name is None: + raise InvalidUsageException("collection_name can't be None for this object") + assert ( + "/" in self._collection_name + ), "Subobjects need to be specified as 'parents/childs'" + parent_collection_name, child_collection_name = self._collection_name.split( + "/", maxsplit=1 + ) + assert ( + "/" not in parent_collection_name + ), f"Illegal name: {parent_collection_name}" + assert ( + "/" not in child_collection_name + ), f"Illegal name, {child_collection_name}" + + def default_child_collection_resolver(): + """ + The flow in this function is a default flow - try to get the child + collection and then the instance from the parent. However, in cases + where this will fail, particularly, when there is no parent nor child instances, + (e.g. can happen for the case of snapshot_policies/schedules. + A snapshot that is a member of an SG (snapshot group = snapshoted CG), + which in this case won't have the snapshot policy which created it because + snapshot policies cannot be assigned to SGs or to CG members) + a custom resolver with the specific colletion should be used. + For an example check the snapshot_policies/schedules case. + """ + parent_obj = getattr(system, parent_collection_name, None) + if parent_obj is None: + raise InfiniSDKRuntimeException( + f"No such collection ({parent_collection_name})" + ) + parent_instance = getattr( + obj.get_parent(), f"get_{parent_obj.object_type.get_type_name()}" + )() + # The next line assumes that the collection name is a method on the parent class + child_collection = getattr(parent_instance, child_collection_name, None) + if child_collection is None and parent_instance is not None: + raise InfiniSDKRuntimeException( + f"{child_collection_name} is not a sub-collection of {parent_collection_name}" + ) + elif child_collection is None and parent_instance is None: + raise RuntimeError( + "Both parent and child objects are None. You might want to use a different child_collection_resolver" + ) + return child_collection + + try: + resolved_child_collection = default_child_collection_resolver() + return resolved_child_collection.get_by_id(api_value) + except RuntimeError as e: + if not self.child_collection_resolver: + raise InvalidUsageException( + "default_child_collection_resolver have failed and no other child_collection_resolver was supplied" + ) from e + assert isinstance( + self.child_collection_resolver, Callable + ), f"child_collection_resolver should be a function, got {type(self.child_collection_resolver)} instead" + return self.child_collection_resolver(system, api_value) diff --git a/infinisdk/core/translators_and_types.py b/infinisdk/core/translators_and_types.py index 494b5235..1125f5e1 100644 --- a/infinisdk/core/translators_and_types.py +++ b/infinisdk/core/translators_and_types.py @@ -1,6 +1,7 @@ from __future__ import absolute_import -from datetime import timedelta +from datetime import time, timedelta +from typing import Optional import arrow import munch @@ -170,3 +171,29 @@ def _from_api(self, value): HostPortListType = TypeInfo( type=list, api_type=list, translator=HostPortListTranslator() ) + + +class TimeOfDayTranslator(ValueTranslator): + """ + Converts between time of day to + seconds from midnight + """ + + def _to_api(self, value: Optional[time]): + if value is None: + return None + return int( + timedelta( + hours=value.hour, minutes=value.minute, seconds=value.second + ).total_seconds() + ) + + def _from_api(self, value: Optional[int]): + if value is None: + return None + total_minutes, seconds = divmod(value, 60) + hours, minutes = divmod(total_minutes, 60) + return time(hours, minutes, seconds) + + +TimeOfDayType = TypeInfo(type=time, api_type=int, translator=TimeOfDayTranslator()) diff --git a/infinisdk/core/utils/resolvers.py b/infinisdk/core/utils/resolvers.py new file mode 100644 index 00000000..1cccd350 --- /dev/null +++ b/infinisdk/core/utils/resolvers.py @@ -0,0 +1,18 @@ +from ..exceptions import ObjectNotFound + + +def schedules_resolver(system, api_value): + """ + Returns a schedule with id=api_value. + This is for cases when you can't get + to the schedule from its parent policy, + e.g. when dealing with SG and its snapshots + which don't get the policy copied from the + MASTER entity. + """ + for policy in system.snapshot_policies.to_list(): + try: + return policy.schedules.get_by_id(api_value) + except ObjectNotFound: + continue + raise ObjectNotFound(f"Schedule with id={api_value} was not found") diff --git a/infinisdk/infinibox/compatibility.py b/infinisdk/infinibox/compatibility.py index a5c9cdb6..799304d8 100644 --- a/infinisdk/infinibox/compatibility.py +++ b/infinisdk/infinibox/compatibility.py @@ -261,8 +261,48 @@ def has_vvol_counts(self): return self.get_parsed_system_version() >= "7.0" and self.has_vvol() - def has_standard_and_vvol_counts(self): - return self.get_parsed_system_version() >= '7.0' + def has_ethernet_interface_state(self): + return self.get_parsed_system_version() >= "7.3.10" + + def has_sg_replicate_snapshots(self): + feature_version = self._get_feature_version("replicate_snapshots") + if feature_version is not NOTHING: + return feature_version >= 2 + return self.get_parsed_system_version() >= "7.3.10" + + def has_snapshot_policies(self): + feature_version = self._get_feature_version("snapshot_policies") + if feature_version is not NOTHING: + return feature_version >= 0 + return self.get_parsed_system_version() >= "7.3.10" + + def has_fs_replicate_snapshots(self): + feature_version = self._get_feature_version("replicate_snapshots") + if feature_version is not NOTHING: + return feature_version >= 3 + return self.get_parsed_system_version() >= "7.3.10" + + def has_ssa_express(self): + feature_version = self._get_feature_version("ssa_express") + if feature_version is not NOTHING: + return feature_version >= 1 + return self.get_parsed_system_version() >= "7.3.10" + + def has_sso(self): + feature_version = self._get_feature_version("sso_authentication") + if feature_version is not NOTHING: + return feature_version >= 1 + return False + + def has_smb_server_capabilities(self): + return self.get_parsed_system_version() >= "7.3.10" + + def has_snapshot_policies_enhancements(self): + feature_version = self._get_feature_version("snapshot_policies") + if feature_version is not NOTHING: + return feature_version >= 1 + return self.get_parsed_system_version() >= "7.3.10" + _VERSION_TUPLE_LEN = 5 diff --git a/infinisdk/infinibox/components.py b/infinisdk/infinibox/components.py index a73baa3a..10bdc72d 100644 --- a/infinisdk/infinibox/components.py +++ b/infinisdk/infinibox/components.py @@ -538,7 +538,6 @@ class LocalDrive(InfiniBoxSystemComponent): cached=True, binding=RelatedComponentBinding(), ), - Field("encryption_state", type=bool, feature_name="fips"), ] @classmethod diff --git a/infinisdk/infinibox/cons_group.py b/infinisdk/infinibox/cons_group.py index 8613652f..21f3eec5 100644 --- a/infinisdk/infinibox/cons_group.py +++ b/infinisdk/infinibox/cons_group.py @@ -5,9 +5,10 @@ from ..core import Field, MillisecondsDatetimeType from ..core.api.special_values import OMIT, Autogenerate -from ..core.bindings import RelatedObjectBinding +from ..core.bindings import RelatedObjectBinding, RelatedSubObjectBinding from ..core.object_query import PolymorphicQuery from ..core.utils import end_reraise_context, handle_possible_replication_snapshot +from ..core.utils.resolvers import schedules_resolver from .system_object import InfiniBoxObject _CG_SUFFIX = Autogenerate("_{timestamp}") @@ -87,6 +88,72 @@ class ConsGroup(InfiniBoxObject): is_sortable=True, ), Field("replication_types", type=list, new_to="5.5.0", is_filterable=True), + Field( + "snapshot_retention", + type=int, + optional=True, + creation_parameter=True, + is_filterable=True, + is_sortable=True, + feature_name="sg_replicate_snapshots", + ), + Field( + "snapshot_expires_at", + type=int, + feature_name="sg_replicate_snapshots", + ), + Field( + "snapshot_policy", + api_name="snapshot_policy_id", + type="infinisdk.infinibox.snapshot_policy:SnapshotPolicy", + binding=RelatedObjectBinding("snapshot_policies"), + mutable=True, + creation_parameter=True, + optional=True, + is_filterable=True, + is_sortable=True, + feature_name="snapshot_policies", + ), + Field( + "snapshot_policy_name", + mutable=True, + is_filterable=True, + is_sortable=True, + feature_name="snapshot_policies", + ), + Field( + "created_by_policy", + api_name="created_by_snapshot_policy_id", + type="infinisdk.infinibox.snapshot_policy:SnapshotPolicy", + binding=RelatedObjectBinding("snapshot_policies"), + is_filterable=True, + is_sortable=True, + feature_name="snapshot_policies", + ), + Field( + "created_by_snapshot_policy_name", + is_filterable=True, + is_sortable=True, + feature_name="snapshot_policies", + ), + Field( + "created_by_schedule", + api_name="created_by_schedule_id", + type="infinisdk.infinibox.schedule:Schedule", + binding=RelatedSubObjectBinding( + "snapshot_policies/schedules", + child_collection_resolver=schedules_resolver, + ), + is_filterable=True, + is_sortable=True, + feature_name="snapshot_policies_enhancements", + ), + Field( + "created_by_schedule_name", + is_filterable=True, + is_sortable=True, + feature_name="snapshot_policies_enhancements", + ), ] @classmethod @@ -116,7 +183,12 @@ def get_replicas(self): get_snapgroups = get_children def create_snapgroup( - self, name=None, prefix=None, suffix=None, lock_expires_at=None + self, + name=None, + prefix=None, + suffix=None, + lock_expires_at=None, + replicate_to_async_target=OMIT, ): """Create a snapshot group out of the consistency group.""" hook_tags = self.get_tags_for_object_operations(self) @@ -145,10 +217,14 @@ def create_snapgroup( members = self.get_members() for member in members: member.trigger_begin_fork() - try: - child = self._create( - self.system, self.get_url_path(self.system), data=data, tags=None + + url = self.get_url_path(self.system) + if replicate_to_async_target is not OMIT: + url = url.add_query_param( + "replicate_to_async_target", replicate_to_async_target ) + try: + child = self._create(self.system, url, data=data, tags=None) except Exception as e: # pylint: disable=broad-except with end_reraise_context(): gossip.trigger_with_tags( diff --git a/infinisdk/infinibox/dataset.py b/infinisdk/infinibox/dataset.py index ce57dd3b..313423aa 100644 --- a/infinisdk/infinibox/dataset.py +++ b/infinisdk/infinibox/dataset.py @@ -9,7 +9,11 @@ from ..core import CapacityType, Field, MillisecondsDatetimeType from ..core.api.special_values import OMIT -from ..core.bindings import RelatedObjectBinding, RelatedObjectNamedBinding +from ..core.bindings import ( + RelatedObjectBinding, + RelatedObjectNamedBinding, + RelatedSubObjectBinding, +) from ..core.exceptions import ObjectNotFound, TooManyObjectsFound from ..core.type_binder import PolymorphicBinder, TypeBinder from ..core.utils import ( @@ -18,6 +22,7 @@ end_reraise_context, handle_possible_replication_snapshot, ) +from ..core.utils.resolvers import schedules_resolver from .system_object import InfiniBoxObject _BEGIN_FORK_HOOK = "infinidat.sdk.begin_fork" @@ -237,6 +242,76 @@ class Dataset(InfiniBoxObject): is_sortable=True, feature_name="replicate_snapshots", ), + Field( + "snapshot_policy", + api_name="snapshot_policy_id", + type="infinisdk.infinibox.snapshot_policy:SnapshotPolicy", + binding=RelatedObjectBinding("snapshot_policies"), + mutable=True, + creation_parameter=True, + optional=True, + is_filterable=True, + is_sortable=True, + feature_name="snapshot_policies", + ), + Field( + "snapshot_policy_name", + mutable=True, + is_filterable=True, + is_sortable=True, + feature_name="snapshot_policies", + ), + Field( + "created_by_policy", + api_name="created_by_snapshot_policy_id", + type="infinisdk.infinibox.snapshot_policy:SnapshotPolicy", + binding=RelatedObjectBinding("snapshot_policies"), + is_filterable=True, + is_sortable=True, + feature_name="snapshot_policies", + ), + Field( + "created_by_snapshot_policy_name", + is_filterable=True, + is_sortable=True, + feature_name="snapshot_policies", + ), + Field( + "mgmt_snapshot_guid", + is_filterable=True, + is_sortable=True, + feature_name="fs_replicate_snapshots", + ), + Field( + "ssa_express_enabled", + type=bool, + mutable=True, + is_filterable=True, + is_sortable=True, + feature_name="ssa_express", + ), + Field( + "ssa_express_status", + feature_name="ssa_express", + ), + Field( + "created_by_schedule", + api_name="created_by_schedule_id", + type="infinisdk.infinibox.schedule:Schedule", + binding=RelatedSubObjectBinding( + "snapshot_policies/schedules", + child_collection_resolver=schedules_resolver, + ), + is_filterable=True, + is_sortable=True, + feature_name="snapshot_policies_enhancements", + ), + Field( + "created_by_schedule_name", + is_filterable=True, + is_sortable=True, + feature_name="snapshot_policies_enhancements", + ), ] PROVISIONING = namedtuple("Provisioning", ["Thick", "Thin"])("THICK", "THIN") @@ -307,7 +382,7 @@ def resize(self, delta): assert isinstance(delta, Capacity), "Delta must be an instance of Capacity" return self.update_field("size", self.get_size() + delta) - def _create_child(self, name=None, **kwargs): + def _create_child(self, name=None, replicate_to_async_target=OMIT, **kwargs): hook_tags = self.get_tags_for_object_operations(self.system) gossip.trigger_with_tags( "infinidat.sdk.pre_entity_child_creation", @@ -325,10 +400,16 @@ def _create_child(self, name=None, **kwargs): data[key] = self.fields.get(key).binding.get_api_value_from_value( self.system, type(self), None, value ) + + url = self.get_url_path(self.system) + if replicate_to_async_target is not OMIT: + url = url.add_query_param( + "replicate_to_async_target", replicate_to_async_target + ) try: child = self._create( self.system, - self.get_url_path(self.system), + url, data=data, tags=self.get_tags_for_object_operations(self.system), parent=self, diff --git a/infinisdk/infinibox/infinibox.py b/infinisdk/infinibox/infinibox.py index c970cc01..68cde12c 100644 --- a/infinisdk/infinibox/infinibox.py +++ b/infinisdk/infinibox/infinibox.py @@ -4,6 +4,7 @@ import gossip from mitba import cached_method +from munch import munchify from sentinels import NOTHING from urlobject import URLObject as URL @@ -48,11 +49,14 @@ from .replication_group import ReplicationGroup from .rg_replica import RgReplica from .san_client import SanClients +from .schedule import Schedule from .search_utils import get_search_query_object, safe_get_object_by_id_and_type_lazy from .share import Share from .share_permission import SharePermission from .smb_group import SMBGroup from .smb_user import SMBUser +from .snapshot_policy import SnapshotPolicy +from .sso_config import SSOIdentityProvider from .tenant import Tenant from .treeq import TreeQ from .user import User @@ -101,10 +105,13 @@ class InfiniBox(APITarget): ReplicationGroup, RgReplica, NFSUser, + SnapshotPolicy, + SSOIdentityProvider, ] SUB_OBJECT_TYPES = [ TreeQ, SharePermission, + Schedule, ] SYSTEM_EVENTS_TYPE = Events SYSTEM_COMPONENTS_TYPE = InfiniBoxSystemComponents @@ -380,6 +387,73 @@ def search(self, query=OMIT, type_name=OMIT): return search_query.extend_url(**search_kwargs) + def open_ssh_ports(self): + """ + Opens ssh ports on all 3 nodes and the SA + """ + return self.api.post("system/ssh/open") + + def close_ssh_ports(self): + """ + Closes ssh ports on all 3 nodes and the SA + """ + return self.api.post("system/ssh/close") + + def get_ssh_ports_status(self): + """ + Returns the status of the ssh ports + on all 3 nodes and the SA + """ + return munchify(self.api.get("system/ssh").get_result()) + + def get_ssa_express_info(self): + """ + Returns the status of ssa-express + and total, free, and used capacities + """ + return munchify(self.api.get("system/ssa_express_info").get_result()) + + def get_entity_counts(self): + """ + Returns the counts of all the + entities in the system + """ + return munchify(self.api.get("system/entity_counts").get_result()) + + def get_smb_server_capabilities(self): + """ + Returns a munch object with + the smb server capabilities. + """ + if not self.compat.has_smb_server_capabilities(): + raise VersionNotSupported(self.get_version()) + return munchify(self.api.get("smb/server_capabilities").get_result()[0]) + + def update_smb_server_capabilities( + self, + min_smb_protocol=OMIT, + max_smb_protocol=OMIT, + smb_signing=OMIT, + smb_encryption=OMIT, + ): + """ + Updates chosen smb server capabilities + """ + if not self.compat.has_smb_server_capabilities(): + raise VersionNotSupported(self.get_version()) + updated_data = {} + if min_smb_protocol is not OMIT: + updated_data["min_smb_protocol"] = min_smb_protocol + if max_smb_protocol is not OMIT: + updated_data["max_smb_protocol"] = max_smb_protocol + if smb_signing is not OMIT: + updated_data["smb_signing"] = smb_signing + if smb_encryption is not OMIT: + updated_data["smb_encryption"] = smb_encryption + + if updated_data: + self.api.put("smb/server_capabilities", data=updated_data) + def __hash__(self): return hash(self.get_name()) diff --git a/infinisdk/infinibox/ldap_config.py b/infinisdk/infinibox/ldap_config.py index 1fa2c1e3..0f9feca3 100644 --- a/infinisdk/infinibox/ldap_config.py +++ b/infinisdk/infinibox/ldap_config.py @@ -41,6 +41,8 @@ class LDAPConfig(SystemObject): Field( "name", mutable=True, + is_filterable=True, + is_sortable=True, ), Field( "ldap_port", @@ -89,8 +91,6 @@ class LDAPConfig(SystemObject): Field( "schema_definition", type=MunchType, - creation_parameter=True, - optional=True, mutable=True, ), ] diff --git a/infinisdk/infinibox/network_interface.py b/infinisdk/infinibox/network_interface.py index a01b0458..878ba6ce 100644 --- a/infinisdk/infinibox/network_interface.py +++ b/infinisdk/infinibox/network_interface.py @@ -61,6 +61,12 @@ class NetworkInterface(InfiniBoxObject): optional=True, ), Field("vlan", type=int, creation_parameter=True, optional=True), + Field( + "operational_state", + is_filterable=True, + feature_name="ethernet_interface_state", + ), + Field("operational_state_description", feature_name="ethernet_interface_state"), ] @classmethod @@ -78,6 +84,12 @@ def get_network_spaces(self): if self in network_space.get_interfaces() ] + def describe_ports(self): + """ + :returns: A list of port info (dict) + """ + return self.get_field("ports", raw_value=True) + def add_port(self, port): url = self.get_this_url_path().add_path("ports") data = self.fields.ports.binding.get_api_value_from_value( diff --git a/infinisdk/infinibox/replica.py b/infinisdk/infinibox/replica.py index ef7b4d25..b12f74e9 100644 --- a/infinisdk/infinibox/replica.py +++ b/infinisdk/infinibox/replica.py @@ -13,11 +13,7 @@ TooManyObjectsFound, ) from ..core.system_object import SystemObject -from ..core.translators_and_types import ( - CapacityType, - MillisecondsDeltaType, - SecondsDeltaType, -) +from ..core.translators_and_types import CapacityType, MillisecondsDeltaType from ..core.type_binder import TypeBinder from ..core.utils import end_reraise_context @@ -45,6 +41,13 @@ def replicate_volume(self, volume, remote_volume=None, **kw): """ return self.replicate_entity(entity=volume, remote_entity=remote_volume, **kw) + def replicate_filesystem(self, fs, remote_fs=None, **kw): + """Convenience wrapper around :func:`ReplicaBinder.replicate_entity` + + :seealso: :meth:`.replicate_entity` + """ + return self.replicate_entity(entity=fs, remote_entity=remote_fs, **kw) + def replicate_cons_group(self, cg, remote_cg=None, remote_pool=OMIT, **kw): """Convenience wrapper around :func:`ReplicaBinder.replicate_entity` @@ -530,7 +533,7 @@ class Replica(SystemObject): optional=True, creation_parameter=True, mutable=True, - type=SecondsDeltaType, + type=int, is_filterable=True, is_sortable=True, feature_name="replicate_snapshots_suffix_lock", @@ -614,6 +617,7 @@ def _notify_post_exposure(replica, snapshot): if replica is None or not replica.is_in_system(): return + # pylint: disable=protected-access gossip.trigger_with_tags( "infinidat.sdk.post_replication_snapshot_expose", { diff --git a/infinisdk/infinibox/schedule.py b/infinisdk/infinibox/schedule.py new file mode 100644 index 00000000..cbbfb99d --- /dev/null +++ b/infinisdk/infinibox/schedule.py @@ -0,0 +1,108 @@ +from ..core import Field +from ..core.api.special_values import Autogenerate +from ..core.bindings import RelatedObjectBinding +from ..core.translators_and_types import SecondsDeltaType, TimeOfDayType +from .system_object import InfiniBoxSubObject + + +class Schedule(InfiniBoxSubObject): + URL_PATH = "schedules" + + FIELDS = [ + Field( + "id", + type=int, + is_identity=True, + is_filterable=True, + is_sortable=True, + ), + Field( + "snapshot_policy", + api_name="snapshot_policy_id", + type="infinisdk.infinibox.snapshot_policy:SnapshotPolicy", + binding=RelatedObjectBinding("snapshot_policies"), + is_filterable=True, + is_sortable=True, + is_parent_field=True, + ), + Field( + "name", + creation_parameter=True, + is_filterable=True, + is_sortable=True, + default=Autogenerate("schedule_{uuid}"), + ), + Field( + "enabled", + type=bool, + mutable=True, + is_filterable=True, + is_sortable=True, + add_updater=False, + ), + Field( + "type", + creation_parameter=True, + is_filterable=True, + is_sortable=True, + default="periodic", + ), + Field( + "interval", + type=SecondsDeltaType, + creation_parameter=True, + optional=True, + ), + Field( + "day_of_week", + creation_parameter=True, + optional=True, + is_filterable=True, + is_sortable=True, + ), + Field( + "time_of_day", + type=TimeOfDayType, + creation_parameter=True, + optional=True, + ), + Field( + "retention", + type=SecondsDeltaType, + creation_parameter=True, + is_filterable=True, + is_sortable=True, + ), + Field( + "lock_snapshots", + type=bool, + mutable=True, + optional=True, + is_filterable=True, + is_sortable=True, + ), + ] + + @classmethod + def is_supported(cls, system): + return system.compat.has_snapshot_policies() + + @classmethod + def get_plural_name(cls): + return "schedules" + + def disable(self): + """ + Disables the schedule for when to + create snapshots + """ + url = self.get_this_url_path().add_path("disable") + return self.system.api.post(url) + + def enable(self): + """ + Enables the schedule for when to + create snapshots + """ + url = self.get_this_url_path().add_path("enable") + return self.system.api.post(url) diff --git a/infinisdk/infinibox/snapshot_policy.py b/infinisdk/infinibox/snapshot_policy.py new file mode 100644 index 00000000..aadae6ea --- /dev/null +++ b/infinisdk/infinibox/snapshot_policy.py @@ -0,0 +1,103 @@ +import mitba +from munch import munchify + +from infinisdk.core.system_object import SystemObject + +from ..core import Field +from ..core.api.special_values import Autogenerate +from ..core.type_binder import SubObjectTypeBinder +from .cons_group import ConsGroup +from .filesystem import Filesystem +from .schedule import Schedule +from .volume import Volume + + +class SnapshotPolicy(SystemObject): + URL_PATH = "snapshot_policies" + + FIELDS = [ + Field("id", type=int, is_identity=True, is_filterable=True, is_sortable=True), + Field( + "name", + creation_parameter=True, + mutable=True, + is_filterable=True, + is_sortable=True, + default=Autogenerate("snapshot_{uuid}"), + ), + Field( + "suffix", + creation_parameter=True, + mutable=True, + is_filterable=True, + is_sortable=True, + default=Autogenerate("-target-{uuid}"), + ), + Field( + "default_snapshot_policy", + type=bool, + mutable=True, + feature_name="snapshot_policies_enhancements", + ), + Field( + "assigned_entities_count", + type=int, + ), + ] + + @classmethod + def get_type_name(cls): + return "snapshot_policy" + + @classmethod + def get_plural_name(cls): + return "snapshot_policies" + + @classmethod + def is_supported(cls, system): + return system.compat.has_snapshot_policies() + + @mitba.cached_property + def schedules(self): + return SubObjectTypeBinder(self.system, Schedule, self) + + def assign_entity(self, entity): + """ + Assigns an entity that we want to create + a snapshot for, to the policy. + """ + entity_to_name_map = { + ConsGroup: "CG", + Filesystem: "FILESYSTEM", + Volume: "VOLUME", + } + if entity is not None: + url = self.get_this_url_path().add_path("assign_entity") + entity_type = type(entity) + data = { + "assigned_entity_id": entity.id, + "assigned_entity_type": entity_to_name_map[entity_type], + } + self.system.api.post(url, data=data) + + def unassign_entity(self, entity): + """ + Unassigns an entity from the current policy + """ + url = self.get_this_url_path().add_path("unassign_entity") + data = {"assigned_entity_id": entity.id} + self.system.api.post(url, data=data) + + def get_assigned_entities(self, page_size=None, page=None): + """ + Returns all assigned entities for the + current policy + """ + url = self.get_this_url_path().add_path("assigned_entities") + if page_size is not None: + assert page_size > 0, "Page size must be a positive integer value" + url = url.add_query_param("page_size", page_size) + if page is not None: + assert page > 0, "Page must be a positive integer value" + url = url.add_query_param("page", page) + return munchify(self.system.api.get(url).get_result()) diff --git a/infinisdk/infinibox/sso_config.py b/infinisdk/infinibox/sso_config.py new file mode 100644 index 00000000..252496f0 --- /dev/null +++ b/infinisdk/infinibox/sso_config.py @@ -0,0 +1,78 @@ +from urlobject import URLObject as URL + +from ..core import Field, SystemObject +from ..core.translators_and_types import MillisecondsDatetimeType + + +class SSOIdentityProvider(SystemObject): + URL_PATH = URL("config/sso/idps") + + FIELDS = [ + Field( + "id", + type=int, + is_identity=True, + is_filterable=True, + is_sortable=True, + ), + Field( + "name", + creation_parameter=True, + mutable=True, + is_filterable=True, + is_sortable=True, + ), + Field( + "issuer", + creation_parameter=True, + is_filterable=True, + is_sortable=True, + ), + Field( + "sign_on_url", + creation_parameter=True, + mutable=True, + is_filterable=True, + is_sortable=True, + ), + Field( + "enabled", + type=bool, + mutable=True, + is_filterable=True, + is_sortable=True, + ), + Field( + "signed_response", + type=bool, + mutable=True, + is_filterable=True, + is_sortable=True, + ), + Field( + "signed_assertion", + type=bool, + mutable=True, + is_filterable=True, + is_sortable=True, + ), + Field( + "signing_certificate", + mutable=True, + ), + Field( + "signing_certificate_serial", + ), + Field( + "signing_certificate_expiry", + type=MillisecondsDatetimeType, + ), + ] + + @classmethod + def is_supported(cls, system): + return system.compat.has_sso() + + @classmethod + def get_plural_name(cls): + return "sso_identity_providers" diff --git a/infinisdk/infinibox/volume.py b/infinisdk/infinibox/volume.py index 567ac7ff..a9499db4 100644 --- a/infinisdk/infinibox/volume.py +++ b/infinisdk/infinibox/volume.py @@ -123,6 +123,15 @@ class Volume(Dataset): is_sortable=True, feature_name="replicate_snapshots", ), + Field( + "source_replicated_sg", + api_name="source_replicated_sg_id", + type="infinisdk.infinibox.cons_group:ConsGroup", + binding=RelatedObjectBinding("cons_groups"), + is_filterable=True, + is_sortable=True, + feature_name="sg_replicate_snapshots", + ), ] @classmethod diff --git a/setup.cfg b/setup.cfg index 7f670853..a9ee1485 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,7 +11,7 @@ license = BSD author = Infinidat author_email = info@infinidat.com url = https://infinisdk.readthedocs.io/en/latest/ -version = 225.1.2 +version = 240.1.2 [entry_points] console_scripts = @@ -29,7 +29,7 @@ testing = click~=8.0.4 black~=21.8b0 pyforge waiting - infinisim~=225.1.0 + infinisim~=240.1.0 [tool:pytest] testpaths = infinisdk tests