diff --git a/.travis.yml b/.travis.yml index eb323e67..fdc5ba3e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,15 +3,14 @@ python: - "2.7" install: - - pip install -r package/requirements.txt --extra-index-url https://testpypi.python.org/simple - - pip install cloudshell-shell-core --extra-index-url https://testpypi.python.org/simple - - pip install cloudshell-core --extra-index-url https://testpypi.python.org/simple + - pip install -r external_requirements.txt - pip install -r test_requirements.txt - - pip install coveralls + - pip install "cloudshell-core>=2.0.0,<2.1.0" --extra-index-url https://testpypi.python.org/simple + - pip install "cloudshell-shell-core>=2.0.0,<2.1.0" --extra-index-url https://testpypi.python.org/simple + - pip install "cloudshell-automation-api>=7.1.0.0,<7.2.0.0" --extra-index-url https://testpypi.python.org/simple script: - pushd package - - pip install "cloudshell-automation-api>=7.1.0.0,<7.2.0.0" --extra-index-url https://testpypi.python.org/simple - python setup.py develop - popd - python runtests.py --with-coverage --cover-package=package --exclude-dir=integration diff --git a/external_requirements.txt b/external_requirements.txt new file mode 100644 index 00000000..f0a84092 --- /dev/null +++ b/external_requirements.txt @@ -0,0 +1,3 @@ +pyvmomi==6.0.0 +jsonpickle==0.9.3 +enum==0.4.6 \ No newline at end of file diff --git a/package/cloudshell/cp/vcenter/commands/command_orchestrator.py b/package/cloudshell/cp/vcenter/commands/command_orchestrator.py index 33202c8d..40b85c92 100644 --- a/package/cloudshell/cp/vcenter/commands/command_orchestrator.py +++ b/package/cloudshell/cp/vcenter/commands/command_orchestrator.py @@ -1,6 +1,9 @@ import time - +from datetime import date import jsonpickle +from cloudshell.cp.vcenter.models.OrchestrationSaveResult import OrchestrationSaveResult +from cloudshell.cp.vcenter.models.OrchestrationSavedArtifactsInfo import OrchestrationSavedArtifactsInfo +from cloudshell.cp.vcenter.models.OrchestrationSavedArtifact import OrchestrationSavedArtifact from pyVim.connect import SmartConnect, Disconnect from cloudshell.cp.vcenter.commands.connect_dvswitch import VirtualSwitchConnectCommand @@ -17,7 +20,7 @@ from cloudshell.cp.vcenter.common.cloud_shell.driver_helper import CloudshellDriverHelper from cloudshell.cp.vcenter.common.cloud_shell.resource_remover import CloudshellResourceRemover from cloudshell.cp.vcenter.common.model_factory import ResourceModelParser -from cloudshell.cp.vcenter.common.utilites.command_result import set_command_result +from cloudshell.cp.vcenter.common.utilites.command_result import set_command_result, get_result_from_command_output from cloudshell.cp.vcenter.common.utilites.common_name import generate_unique_name from cloudshell.cp.vcenter.common.utilites.common_utils import back_slash_to_front_converter from cloudshell.cp.vcenter.common.utilites.context_based_logger_factory import ContextBasedLoggerFactory @@ -406,10 +409,11 @@ def save_snapshot(self, context, snapshot_name): :return: """ resource_details = self._parse_remote_model(context) - self.command_wrapper.execute_command_with_connection(context, - self.snapshot_saver.save_snapshot, - resource_details.vm_uuid, - snapshot_name) + created_snapshot_path = self.command_wrapper.execute_command_with_connection(context, + self.snapshot_saver.save_snapshot, + resource_details.vm_uuid, + snapshot_name) + return set_command_result(created_snapshot_path) def restore_snapshot(self, context, snapshot_name): """ @@ -439,3 +443,50 @@ def get_snapshots(self, context): self.snapshots_retriever.get_snapshots, resource_details.vm_uuid) return set_command_result(result=res, unpicklable=False) + + def orchestration_save(self, context, mode="shallow", custom_params=None): + """ + Creates a snapshot with a unique name and returns SavedResults as JSON + :param context: resource context of the vCenterShell + :param mode: Snapshot save mode, default shallow. Currently not it use + :param custom_params: Set of custom parameter to be supported in the future + :return: SavedResults serialized as JSON + :rtype: SavedResults + """ + resource_details = self._parse_remote_model(context) + created_date = date.today() + snapshot_name = created_date.strftime('%y_%m_%d %H_%M_%S_%f') + created_snapshot_path = self.save_snapshot(context=context, snapshot_name=snapshot_name) + + created_snapshot_path = self._strip_double_quotes(created_snapshot_path) + + orchestration_saved_artifact = OrchestrationSavedArtifact() + orchestration_saved_artifact.artifact_type = 'vcenter_snapshot' + orchestration_saved_artifact.identifier = created_snapshot_path + + saved_artifacts_info = OrchestrationSavedArtifactsInfo( + resource_name=resource_details.cloud_provider, + created_date=created_date, + restore_rules={'requires_same_resource': True}, + saved_artifact=orchestration_saved_artifact) + + orchestration_save_result = OrchestrationSaveResult(saved_artifacts_info) + + return set_command_result(result=orchestration_save_result, unpicklable=False) + + @staticmethod + def _strip_double_quotes(created_snapshot_path): + if created_snapshot_path.startswith('"') and created_snapshot_path.endswith('"'): + created_snapshot_path = created_snapshot_path[1:-1] + return created_snapshot_path + + def orchestration_restore(self, context, saved_details): + """ + + :param context: + :param saved_details: + :return: + """ + saved_artifacts_info = get_result_from_command_output(saved_details) + snapshot_name = saved_artifacts_info['saved_artifacts_info']['saved_artifact']['identifier'] + return self.restore_snapshot(context=context, snapshot_name=snapshot_name) diff --git a/package/cloudshell/cp/vcenter/commands/save_snapshot.py b/package/cloudshell/cp/vcenter/commands/save_snapshot.py index 71c5b4c3..cffae668 100644 --- a/package/cloudshell/cp/vcenter/commands/save_snapshot.py +++ b/package/cloudshell/cp/vcenter/commands/save_snapshot.py @@ -34,11 +34,13 @@ def save_snapshot(self, si, logger, vm_uuid, snapshot_name): """ vm = self.pyvmomi_service.find_by_uuid(si, vm_uuid) - self._verify_snapshot_name_does_not_exists(snapshot_name, vm) + snapshot_path_to_be_created = SaveSnapshotCommand._get_snapshot_name_to_be_created(snapshot_name, vm) + SaveSnapshotCommand._verify_snapshot_uniquness(snapshot_path_to_be_created, vm) task = self._create_snapshot(logger, snapshot_name, vm) - return self.task_waiter.wait_for_task(task=task, logger=logger, action_name='Create Snapshot') + self.task_waiter.wait_for_task(task=task, logger=logger, action_name='Create Snapshot') + return snapshot_path_to_be_created @staticmethod def _create_snapshot(logger, snapshot_name, vm): @@ -49,11 +51,14 @@ def _create_snapshot(logger, snapshot_name, vm): return task @staticmethod - def _verify_snapshot_name_does_not_exists(snapshot_name, vm): - current_snapshot_name = SnapshotRetriever.get_current_snapshot_name(vm) - if not current_snapshot_name: - return - snapshot_path_to_be_created = SnapshotRetriever.combine(current_snapshot_name, snapshot_name) + def _verify_snapshot_uniquness(snapshot_path_to_be_created, vm): all_snapshots = SnapshotRetriever.get_vm_snapshots(vm) if snapshot_path_to_be_created in all_snapshots: raise SnapshotAlreadyExistsException(SNAPSHOT_ALREADY_EXISTS) + + @staticmethod + def _get_snapshot_name_to_be_created(snapshot_name, vm): + current_snapshot_name = SnapshotRetriever.get_current_snapshot_name(vm) + if not current_snapshot_name: + return '' + return SnapshotRetriever.combine(current_snapshot_name, snapshot_name) diff --git a/package/cloudshell/cp/vcenter/models/OrchestrationSaveResult.py b/package/cloudshell/cp/vcenter/models/OrchestrationSaveResult.py new file mode 100644 index 00000000..8d35c8c4 --- /dev/null +++ b/package/cloudshell/cp/vcenter/models/OrchestrationSaveResult.py @@ -0,0 +1,6 @@ +class OrchestrationSaveResult(object): + def __init__(self, saved_artifacts_info): + """ + :type saved_artifacts_info: OrchestrationSavedArtifactsInfo + """ + self.saved_artifacts_info = saved_artifacts_info diff --git a/package/cloudshell/cp/vcenter/models/OrchestrationSavedArtifact.py b/package/cloudshell/cp/vcenter/models/OrchestrationSavedArtifact.py new file mode 100644 index 00000000..31d1f046 --- /dev/null +++ b/package/cloudshell/cp/vcenter/models/OrchestrationSavedArtifact.py @@ -0,0 +1,4 @@ +class OrchestrationSavedArtifact(object): + def __init__(self): + self.artifact_type = '' + self.identifier = '' diff --git a/package/cloudshell/cp/vcenter/models/OrchestrationSavedArtifactsInfo.py b/package/cloudshell/cp/vcenter/models/OrchestrationSavedArtifactsInfo.py new file mode 100644 index 00000000..aae5c257 --- /dev/null +++ b/package/cloudshell/cp/vcenter/models/OrchestrationSavedArtifactsInfo.py @@ -0,0 +1,12 @@ +class OrchestrationSavedArtifactsInfo(object): + def __init__(self, resource_name, created_date, restore_rules, saved_artifact): + """ + :type resource_name: str + :type created_date: date + :type restore_rules: dict + :type saved_artifact: OrchestrationSavedArtifact + """ + self.resource_name = resource_name + self.created_date = created_date + self.restore_rules = restore_rules + self.saved_artifact = saved_artifact diff --git a/package/cloudshell/tests/test_commands/test_command_orchestrator.py b/package/cloudshell/tests/test_commands/test_command_orchestrator.py index 703d3bb1..26a87410 100644 --- a/package/cloudshell/tests/test_commands/test_command_orchestrator.py +++ b/package/cloudshell/tests/test_commands/test_command_orchestrator.py @@ -1,8 +1,13 @@ from unittest import TestCase +import jsonpickle from cloudshell.api.cloudshell_api import ResourceInfo from cloudshell.cp.vcenter.commands.command_orchestrator import CommandOrchestrator -from mock import Mock, create_autospec +from cloudshell.shell.core.driver_context import ResourceRemoteCommandContext, ResourceContextDetails, AppContext +from mock import Mock, create_autospec, patch + +RESTORE_SNAPSHOT = 'cloudshell.cp.vcenter.commands.command_orchestrator.CommandOrchestrator.restore_snapshot' +SAVE_SNAPSHOT = 'cloudshell.cp.vcenter.commands.command_orchestrator.CommandOrchestrator.save_snapshot' class TestCommandOrchestrator(TestCase): @@ -124,4 +129,52 @@ def test_restore_snapshot(self): def test_get_snapshots(self): self.command_orchestrator.get_snapshots(self.context) - self.assertTrue(self.command_orchestrator.command_wrapper.execute_command_with_connection.called) \ No newline at end of file + self.assertTrue(self.command_orchestrator.command_wrapper.execute_command_with_connection.called) + + def test_orchestration_save(self): + # Arrange + with patch(SAVE_SNAPSHOT) as save_snapshot_mock: + save_snapshot_mock.return_value = '"new_snapshot"' + + remote_command_context = create_autospec(ResourceRemoteCommandContext) + remote_command_context.resource = create_autospec(ResourceContextDetails) + remote_command_context.resource.fullname = 'vcenter' + endpoint = create_autospec(ResourceContextDetails) + endpoint.fullname = 'vm_111' + endpoint.app_context = create_autospec(AppContext) + endpoint.app_context.deployed_app_json = '{"vmdetails": {"uid": "vm_uuid1"}}' + remote_command_context.remote_endpoints = [endpoint] + + # Act + saved_result = CommandOrchestrator().orchestration_save(context=remote_command_context, + mode='shallow', + custom_params=None) + + # Assert + save_snapshot_mock.assert_called_once() + saved_result_dict = jsonpickle.decode(saved_result) + self.assertEqual(saved_result_dict['saved_artifacts_info']['saved_artifact']['artifact_type'], + 'vcenter_snapshot') + self.assertEqual(saved_result_dict['saved_artifacts_info']['saved_artifact']['identifier'], 'new_snapshot') + self.assertEqual(saved_result_dict['saved_artifacts_info']['resource_name'], 'vcenter') + self.assertIsNotNone(saved_result_dict['saved_artifacts_info']['created_date']) + + def test_orchestration_restore(self): + # Arrange + with patch(RESTORE_SNAPSHOT) as mock_restore_snapshot: + # Act + self.command_orchestrator.orchestration_restore(self.context, '''{ + "saved_artifacts_info": { + "resource_name": "ex cillum sed laboris", + "created_date": "4313-10-12T18:03:15.053Z", + "restore_rules": { + "requires_same_resource": false + }, + "saved_artifact": { + "artifact_type": "veniam in qui", + "identifier": "deserunt1" + } + } + }''') + # Assert + mock_restore_snapshot.assert_called_once_with(context=self.context, snapshot_name='deserunt1') diff --git a/test_requirements.txt b/test_requirements.txt index 7d963ba7..b238846d 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -3,5 +3,5 @@ coverage unittest2 mock teamcity-messages -jsonpickle -nose-exclude \ No newline at end of file +nose-exclude +coveralls \ No newline at end of file diff --git a/vcentershell_driver/driver.py b/vcentershell_driver/driver.py index c371c1f3..cab7300a 100644 --- a/vcentershell_driver/driver.py +++ b/vcentershell_driver/driver.py @@ -105,5 +105,11 @@ def remote_get_snapshots(self, context, ports): """ return self.command_orchestrator.get_snapshots(context) + def orchestration_save(self, context, ports, mode="shallow", custom_params=None): + return self.command_orchestrator.orchestration_save(context, mode, custom_params) + + def orchestration_restore(self, context, ports, saved_details): + return self.command_orchestrator.orchestration_restore(context, saved_details) + def get_vm_uuid(self, context, vm_name): return self.command_orchestrator.get_vm_uuid_by_name(context, vm_name) diff --git a/vcentershell_driver/drivermetadata.xml b/vcentershell_driver/drivermetadata.xml index fb848b5f..728ce2b1 100644 --- a/vcentershell_driver/drivermetadata.xml +++ b/vcentershell_driver/drivermetadata.xml @@ -25,6 +25,8 @@ + +