diff --git a/drivers/deployment_drivers/openstack_nova_image_instance/drivermetadata.xml b/drivers/deployment_drivers/openstack_nova_image_instance/drivermetadata.xml index d2dd8af..74fabb4 100644 --- a/drivers/deployment_drivers/openstack_nova_image_instance/drivermetadata.xml +++ b/drivers/deployment_drivers/openstack_nova_image_instance/drivermetadata.xml @@ -1,7 +1,7 @@ - + - \ No newline at end of file + diff --git a/drivers/deployment_drivers/version.txt b/drivers/deployment_drivers/version.txt index 43b2961..ceddfb2 100644 --- a/drivers/deployment_drivers/version.txt +++ b/drivers/deployment_drivers/version.txt @@ -1 +1 @@ -0.0.13 +0.0.15 diff --git a/drivers/openstack_shell/src/driver.py b/drivers/openstack_shell/src/driver.py index 91c7aa8..f451d4f 100644 --- a/drivers/openstack_shell/src/driver.py +++ b/drivers/openstack_shell/src/driver.py @@ -29,6 +29,9 @@ def deploy_from_image(self, context, request): # "cloud_provider_resource_name" : "openstack"}, # unpicklable=False)) + def ApplyConnectivityChanges(self, context, request): + return self.os_shell.apply_connectivity(context, request) + def PowerOn(self, context, ports): return self.os_shell.power_on(context) diff --git a/drivers/openstack_shell/src/drivermetadata.xml b/drivers/openstack_shell/src/drivermetadata.xml index 68db4b8..43503fc 100644 --- a/drivers/openstack_shell/src/drivermetadata.xml +++ b/drivers/openstack_shell/src/drivermetadata.xml @@ -1,10 +1,11 @@ - + + @@ -15,4 +16,4 @@ - \ No newline at end of file + diff --git a/drivers/openstack_shell/version.txt b/drivers/openstack_shell/version.txt index 43b2961..9beca35 100644 --- a/drivers/openstack_shell/version.txt +++ b/drivers/openstack_shell/version.txt @@ -1 +1 @@ -0.0.13 +0.0.15 \ No newline at end of file diff --git a/drivers/openstack_shellPackage/Configuration/shellconfig.xml b/drivers/openstack_shellPackage/Configuration/shellconfig.xml index c72d317..0d8f377 100644 --- a/drivers/openstack_shellPackage/Configuration/shellconfig.xml +++ b/drivers/openstack_shellPackage/Configuration/shellconfig.xml @@ -15,6 +15,7 @@ + diff --git a/drivers/openstack_shellPackage/DataModel/datamodel.xml b/drivers/openstack_shellPackage/DataModel/datamodel.xml index 9676c01..9c34136 100644 --- a/drivers/openstack_shellPackage/DataModel/datamodel.xml +++ b/drivers/openstack_shellPackage/DataModel/datamodel.xml @@ -63,6 +63,14 @@ + + + + + + + + @@ -151,6 +159,9 @@ + + + diff --git a/drivers/version.txt b/drivers/version.txt index 43b2961..ceddfb2 100644 --- a/drivers/version.txt +++ b/drivers/version.txt @@ -1 +1 @@ -0.0.13 +0.0.15 diff --git a/package/cloudshell/cp/openstack/command/operations/connectivity_operation.py b/package/cloudshell/cp/openstack/command/operations/connectivity_operation.py index 7eebb09..62b51fc 100644 --- a/package/cloudshell/cp/openstack/command/operations/connectivity_operation.py +++ b/package/cloudshell/cp/openstack/command/operations/connectivity_operation.py @@ -1,12 +1,21 @@ from cloudshell.cp.openstack.domain.services.nova.nova_instance_service import NovaInstanceService +from cloudshell.cp.openstack.domain.services.neutron.neutron_network_service import NeutronNetworkService from cloudshell.cp.openstack.domain.services.waiters.instance import InstanceWaiter +from cloudshell.cp.openstack.common.deploy_data_holder import DeployDataHolder + +from cloudshell.cp.openstack.models.connectivity_action_result_model import ConnectivityActionResultModel +from cloudshell.cp.openstack.models.driver_response_model import DriverResponse, DriverResponseRoot + +import jsonpickle + class ConnectivityOperation(object): public_ip = "Public IP" def __init__(self): self.instance_waiter = InstanceWaiter() self.instance_service = NovaInstanceService(self.instance_waiter) + self.network_service = NeutronNetworkService() def refresh_ip(self, openstack_session, cloudshell_session, deployed_app_resource, private_ip, resource_fullname, @@ -38,3 +47,176 @@ def refresh_ip(self, openstack_session, cloudshell_session, # FIXME : hardcoded public IP right now. Get it from floating IP later. cloudshell_session.SetAttributeValue(resource_fullname, ConnectivityOperation.public_ip, "192.168.1.1") + + def apply_connectivity(self, openstack_session, cp_resource_model, conn_request, logger): + """ + Implements Apply connectivity - parses the conn_requests and creates + :param keystoneauth1.session.Session openstack_session: + :param OpenStackResourceModel cp_resource_model: + :param str conn_request: Connectivty Request JSON + :return DriverResponseRoot: + """ + + conn_req_deploy_data = DeployDataHolder(jsonpickle.decode(conn_request)) + + # Now collect following dict + # Key: (vlanID) + # value: List of (Resource_Name, VM_UUID, actionID) + # For each item, create network, and assign a nic on that network + + actions = conn_req_deploy_data.driverRequest.actions + + set_vlan_actions_dict = {} + remove_vlan_actions_dict = {} + + # Add more description + # TODO : implement remove actions dict + for action in actions: + # FIXME: Move this "ifs into a separate function + if action.type == 'setVlan': + curr_dict = set_vlan_actions_dict + # FIXME: Check whether this is 'removeVlan' + else: + curr_dict = remove_vlan_actions_dict + + action_vlanid = action.connectionParams.vlanId + actionid = action.actionId + + deployed_app_res_name = action.actionTarget.fullName + + for cust_attr in action.customActionAttributes : + if cust_attr.attributeName == 'VM_UUID': + vm_uuid = cust_attr.attributeValue + # FIXME : changed this to object for later readability + resource_info = (deployed_app_res_name, vm_uuid, actionid) + if action_vlanid in curr_dict.keys(): + curr_dict[action_vlanid].append(resource_info) + else: + curr_dict[action_vlanid] = [resource_info] + + results = [] + if set_vlan_actions_dict: + result = self._do_set_vlan_actions(openstack_session=openstack_session, + cp_resource_model=cp_resource_model, + vlan_actions=set_vlan_actions_dict, + logger=logger) + + results += result + + if remove_vlan_actions_dict: + result = self._do_remove_vlan_actions(openstack_session=openstack_session, + cp_resource_model=cp_resource_model, + vlan_actions=set_vlan_actions_dict, + logger=logger) + results += result + + # We have apply Connectivity results - We should send out the JSON and encode it + driver_response = DriverResponse() + driver_response.actionResults = results + driver_response_root = DriverResponseRoot() + driver_response_root.driverResponse = driver_response + + return driver_response_root + + def _do_set_vlan_actions(self, openstack_session, cp_resource_model, vlan_actions, logger): + """ + + :param keystoneauth1.session.Session openstack_session: + :param OpenStackResourceModel cp_resource_model: + :param dict vlan_actions: + :param LoggingSessionContext logger: + :return ConnectivityActionResult List : + """ + + # For each VLAN ID (create VLAN network) + results = [] + + for k, values in vlan_actions.iteritems(): + # FIXME: results getting overwritten + # FIXME: update the nethod name + net = self.network_service.create_network_with_vlanid(openstack_session=openstack_session, + vlanid=int(k), + logger=logger) + if not net: + # FIXME : create error for the action + results = self._set_fail_results(values=values, + action_type='setVlan', + failure_text="Failed to Create Network with VLAN ID {0}".format(k)) + else: + net_id = net['id'] + + subnet = net['subnets'] + if not subnet: + # FIXME: Rename this function to create_and_attach + subnet = self.network_service.attach_subnet_to_net(openstack_session=openstack_session, + cp_resource_model=cp_resource_model, + net_id=net_id, + logger=logger) + else: + subnet = subnet[0] + if not subnet: + # FIXME: create error for action + results = self._set_fail_results(values=values, + action_type='setVlan', + failure_text="Failed to attach Subnet to Network {0}".format(net_id)) + else: + attach_results = [] + # FIXME: let's move this + for val in values: + + instance_id = val[1] + # returns MAC Address of the attached port - which is reflected in updated Port + result = self.instance_service.attach_nic_to_net(openstack_session, instance_id, net_id, logger) + if not result: + action_result = ConnectivityActionResultModel() + action_result.success = False + action_result.actionId = val[2] + action_result.errorMessage = \ + "Failed to Attach NIC on Network {0} to Instance {1}".format(net_id, val[0]) + action_result.infoMessage = None + action_result.updatedInterface = None + else: + action_result = ConnectivityActionResultModel() + action_result.success = "True" + action_result.actionId = val[2] + action_result.errorMessage = "" + action_result.infoMessage = \ + "Successfully Attached NIC on Network {0} to Instance {1}".format(net_id, val[0]) + action_result.updatedInterface = result + action_result.type = 'setVlan' + attach_results.append(action_result) + results = attach_results + return results + + def _do_remove_vlan_actions(self, openstack_session, cp_resource_model, vlan_actions, logger): + """ + Function implementing Remove VLANs in apply_connectivity + :param keystoneauth1.session.Session openstack_session: + :param OpenStckResourceModel cp_resource_model: + :param dict vlan_actions: + :param LoggingSessionContext logger: + :return: + """ + logger.info("_do_remove_vlan_actions called.") + return [] + + def _set_fail_results(self, values, failure_text, action_type, logger=None): + """ + For all connections (obtained from values), set the failed results text, useful in generating output + :param tuple values: + :param str failure_text: + :param str action_type + :param logger: + :return ConnectivityActionResultModel List: + """ + results = [] + for value in values: + action_result = ConnectivityActionResultModel() + action_result.success = False + action_result.actionId = value[2] + action_result.infoMessage = None + action_result.errorMessage = failure_text + action_result.type = action_type + action_result.updatedInterface = None + results.append(action_result) + return results \ No newline at end of file diff --git a/package/cloudshell/cp/openstack/domain/services/neutron/neutron_network_service.py b/package/cloudshell/cp/openstack/domain/services/neutron/neutron_network_service.py new file mode 100644 index 0000000..9ed7fdd --- /dev/null +++ b/package/cloudshell/cp/openstack/domain/services/neutron/neutron_network_service.py @@ -0,0 +1,111 @@ +from neutronclient.v2_0 import client as neutron_client +from neutronclient.common.exceptions import Conflict as NetCreateConflict + +class NeutronNetworkService(object): + """ + A wrapper class around Neutron API + """ + + def __init__(self): + self.cidr_base = None + self.cidr_subnet_num = 0 + self.allocated_subnets = [] + + def create_network_with_vlanid(self, openstack_session, vlanid, logger): + """ + + :param keystoneauth1.session.Session openstack_session: + :param int vlanid: + :param LoggingSessionContext logger: + :return dict : + """ + + client = neutron_client.Client(session=openstack_session) + + nw_name = "net_vlanid_{0}".format(vlanid) + create_nw_json = {'provider:physical_network': 'public', + 'provider:network_type': 'vlan', + 'provider:segmentation_id': vlanid, + 'name': nw_name, + 'admin_state_up': True} + + # FIXME : If an exception is raised - we just raise it all the way back? For now yes + try: + new_net = client.create_network({'network': create_nw_json}) + new_net = new_net['network'] + except NetCreateConflict: + new_net = client.list_networks(**{'provider:segmentation_id':vlanid}) + new_net = new_net['networks'][0] + except Exception as e: + logger.error("Exception {0} Occurred while creating network".format(e)) + return None + + return new_net + + def attach_subnet_to_net(self, openstack_session, cp_resource_model, net_id, logger): + """ + Atttach a subnet to the network with given net_id. + + :param keystoneauth1.session.Session openstack_session: + :param OpenStackResourceModel cp_resource_model: + :param str net_id: UUID string + :return dict: + """ + + client = neutron_client.Client(session=openstack_session) + + cidr = self._get_unused_cidr(cp_resvd_cidrs=cp_resource_model.reserved_networks, logger=logger) + if cidr is None: + logger.error("Cannot allocate new subnet. All subnets exhausted") + return None + + create_subnet_json = {'cidr': cidr, + 'network_id': net_id, + 'ip_version': 4} + + try: + new_subnet = client.create_subnet({'subnet':create_subnet_json}) + new_subnet = new_subnet['subnet'] + except Exception as e: + logger.error("Exception {0} Occurred while creating network".format(e)) + return None + + return new_subnet + + def _get_unused_cidr(self, cp_resvd_cidrs, logger): + """ + Gets unused CIDR that excludes the reserved CIDRs + :param str cp_resvd_cidrs: + :return str: + """ + + # Algorithm below is a very simplistic one where we choose one of the three prefixes and then use + # /24 networks starting with that prefix. This algorithm will break if all three 10.X, 192.168.X and 172.X + # networks are used in a given On Prem Network. + # FIXME: Get subnets from openstack and not simply. + if self.cidr_base is not None: + cidr = ".".join([self.cidr_base, str(self.cidr_subnet_num), "0/24"]) + if self.cidr_subnet_num not in self.allocated_subnets: + self.allocated_subnets.append(self.cidr_subnet_num) + self.cidr_subnet_num += 1 + if self.cidr_subnet_num == 255: + self.cidr_subnet_num = 0 + return cidr + else: + candidate_prefixes = {'10': '10.0', '192.168': '192.168', '172': '172.0'} + cp_resvd_cidrs = cp_resvd_cidrs.split(",") + logger.error(cp_resvd_cidrs) + possible_prefixes = filter(lambda x: any(map(lambda y: not y.strip().startswith(x), cp_resvd_cidrs)), + candidate_prefixes.keys()) + + logger.info(possible_prefixes) + if not possible_prefixes: + return None + else: + prefix = possible_prefixes[0] + self.cidr_base = candidate_prefixes[prefix] + cidr = ".".join([self.cidr_base, str(self.cidr_subnet_num), "0/24"]) + self.allocated_subnets.append(self.cidr_subnet_num) + self.cidr_subnet_num += 1 + return cidr + return None \ No newline at end of file diff --git a/package/cloudshell/cp/openstack/domain/services/nova/nova_instance_service.py b/package/cloudshell/cp/openstack/domain/services/nova/nova_instance_service.py index 0a26efa..2858bba 100644 --- a/package/cloudshell/cp/openstack/domain/services/nova/nova_instance_service.py +++ b/package/cloudshell/cp/openstack/domain/services/nova/nova_instance_service.py @@ -181,3 +181,28 @@ def get_instance_from_instance_id(self, openstack_session, instance_id, logger, return None except Exception: raise + + def attach_nic_to_net(self, openstack_session, instance_id, net_id, logger): + """ + + :param openstack_session: + :param instance_id: + :param net_id: + :param logger: + :return: + """ + + instance = self.get_instance_from_instance_id(openstack_session=openstack_session, + instance_id=instance_id, + logger=logger) + if instance is None : + return None + + try: + res = instance.interface_attach(net_id=net_id, port_id=None, fixed_ip=None) + iface_mac = res.to_dict().get('mac_addr') + return iface_mac + except Exception as e: + logger.info("Exception: {0} during interface attach".format(e)) + + return None \ No newline at end of file diff --git a/package/cloudshell/cp/openstack/models/connectivity_action_result_model.py b/package/cloudshell/cp/openstack/models/connectivity_action_result_model.py new file mode 100644 index 0000000..e65977f --- /dev/null +++ b/package/cloudshell/cp/openstack/models/connectivity_action_result_model.py @@ -0,0 +1,10 @@ + + +class ConnectivityActionResultModel(object): + def __init__(self): + self.actionId = None + self.success = None + self.infoMessage = None + self.errorMessage = None + self.type = None + self.updatedInterface = None \ No newline at end of file diff --git a/package/cloudshell/cp/openstack/models/driver_response_model.py b/package/cloudshell/cp/openstack/models/driver_response_model.py new file mode 100644 index 0000000..9c0134b --- /dev/null +++ b/package/cloudshell/cp/openstack/models/driver_response_model.py @@ -0,0 +1,8 @@ + +class DriverResponse(object): + def __init__(self): + self.actionResults = [] + +class DriverResponseRoot(object): + def __init__(self): + self.driverResponse = None diff --git a/package/cloudshell/cp/openstack/models/model_parser.py b/package/cloudshell/cp/openstack/models/model_parser.py index 9957750..bf8e246 100644 --- a/package/cloudshell/cp/openstack/models/model_parser.py +++ b/package/cloudshell/cp/openstack/models/model_parser.py @@ -25,6 +25,7 @@ def get_resource_model_from_context(resource): os_res_model.os_user_password = attrs['OpenStack User Password'] os_res_model.qs_mgmt_os_net_uuid = attrs['Quali Management Network UUID'] os_res_model.os_floating_ip_pool = attrs['Floating IP Pool'] + os_res_model.reserved_networks = attrs['Reserved Networks'] return os_res_model @staticmethod diff --git a/package/cloudshell/cp/openstack/models/openstack_resource_model.py b/package/cloudshell/cp/openstack/models/openstack_resource_model.py index bd54187..7217d24 100644 --- a/package/cloudshell/cp/openstack/models/openstack_resource_model.py +++ b/package/cloudshell/cp/openstack/models/openstack_resource_model.py @@ -11,6 +11,7 @@ def __init__(self): self.os_user_password = '' self.qs_mgmt_os_net_uuid = '' self.os_floating_ip_pool = '' + self.reserved_networks = '' def __str__(self): desc = "OpenStack Resource: controller_url: {0}, domain: {1}, project_name : {2}, os_user_name : {3}".format( diff --git a/package/cloudshell/cp/openstack/openstack_shell.py b/package/cloudshell/cp/openstack/openstack_shell.py index 5d52b39..b6835f6 100644 --- a/package/cloudshell/cp/openstack/openstack_shell.py +++ b/package/cloudshell/cp/openstack/openstack_shell.py @@ -82,6 +82,7 @@ def power_off(self, command_context): :param cloudshell.shell.core.context.ResourceRemoteCommandContext command_context: :rtype None: """ + with LoggingSessionContext(command_context) as logger: with ErrorHandlingContext(logger): with CloudShellSessionContext(command_context) as cs_session: @@ -214,4 +215,27 @@ def refresh_ip(self, command_context): resource_fullname=deployed_app_fullname, logger=logger) + def apply_connectivity(self, command_context, connectivity_request): + """ + + :param cloudshell.shell.core.context.ResourceRemoteCommandContext command_context: + :param str connectivity_request: Connectivity Request JSON string + :return: + """ + with LoggingSessionContext(command_context) as logger: + with ErrorHandlingContext(logger): + with CloudShellSessionContext(command_context) as cs_session: + # FIXME: When implementing a context manager create all clients inside the contextManager. + logger.info(connectivity_request) + cp_resource_model = self.model_parser.get_resource_model_from_context(command_context.resource) + + logger.debug(cp_resource_model) + os_session = self.os_session_provider.get_openstack_session(cs_session, cp_resource_model, logger) + connectivity_result = self.connectivity_operation.apply_connectivity(openstack_session=os_session, + cp_resource_model=cp_resource_model, + conn_request=connectivity_request, + logger=logger) + + return self.command_result_parser.set_command_result(connectivity_result) + # Connectivity Operations End diff --git a/package/requirements.txt b/package/requirements.txt index 04c85a5..39832c7 100644 --- a/package/requirements.txt +++ b/package/requirements.txt @@ -2,4 +2,5 @@ cloudshell-automation-api>=7.0.0.0,<7.1.0.0 cloudshell-core>=2.1.0,<2.2.0 cloudshell-shell-core>=2.2.0,<2.3.0 python-novaclient>=4.0.8,<5.1.0 +python-neutronclient>=5.0.0,<=6.0.0 jsonpickle==0.9.3 diff --git a/package/tests/test_cp/test_openstack/test_models/test_model_parser.py b/package/tests/test_cp/test_openstack/test_models/test_model_parser.py index 6755217..80d69a5 100644 --- a/package/tests/test_cp/test_openstack/test_models/test_model_parser.py +++ b/package/tests/test_cp/test_openstack/test_models/test_model_parser.py @@ -21,6 +21,7 @@ def test_get_resource_model_from_context(self): test_resource.attributes['OpenStack User Password'] = 'test_pass' test_resource.attributes['Quali Management Network UUID'] = '1234-56-78' test_resource.attributes['Floating IP Pool'] = '10.0.0.100-10.0.0.101' + test_resource.attributes['Reserved Networks'] = '172.22.0.0/16' result = self.tested_class.get_resource_model_from_context(test_resource) @@ -32,6 +33,7 @@ def test_get_resource_model_from_context(self): self.assertEqual(result.os_user_password, 'test_pass') self.assertEqual(result.qs_mgmt_os_net_uuid, '1234-56-78') self.assertEqual(result.os_floating_ip_pool, '10.0.0.100-10.0.0.101') + self.assertEqual(result.reserved_networks, '172.22.0.0/16') @mock.patch("cloudshell.cp.openstack.models.model_parser.jsonpickle") @mock.patch("cloudshell.cp.openstack.models.model_parser.DeployOSNovaImageInstanceResourceModel") diff --git a/package/version.txt b/package/version.txt index 43b2961..9beca35 100644 --- a/package/version.txt +++ b/package/version.txt @@ -1 +1 @@ -0.0.13 +0.0.15 \ No newline at end of file diff --git a/version.txt b/version.txt index 43b2961..ceddfb2 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.0.13 +0.0.15