From 947a27eb5901e0c19b00c755bce883948973f247 Mon Sep 17 00:00:00 2001 From: Leonardo Maycotte Date: Thu, 20 Oct 2016 11:44:51 -0500 Subject: [PATCH] Security Groups and Server Persona Add-ons - Adding a get remote instance client wrapper for the compute method to the networks behaviors. - Adding the PortUpdate Exception - Adding the add_rule method to the security groups behaviors - Adding the remove_rule method to the security groups behaviors - Adding security groups to the server persona - Adding add_security_groups_to_ports to the server persona - Adding remote client to server persona Change-Id: Ib117da573ea4f3a2ce1e7d9614cbb391b322ffef --- cloudcafe/networking/networks/behaviors.py | 15 ++ .../networking/networks/common/exceptions.py | 4 + .../security_groups_api/behaviors.py | 128 +++++++++++++++++- cloudcafe/networking/networks/personas.py | 127 ++++++++++++++++- 4 files changed, 265 insertions(+), 9 deletions(-) diff --git a/cloudcafe/networking/networks/behaviors.py b/cloudcafe/networking/networks/behaviors.py index ae2b420a..3e5925fa 100644 --- a/cloudcafe/networking/networks/behaviors.py +++ b/cloudcafe/networking/networks/behaviors.py @@ -361,6 +361,21 @@ class NetworkingBehaviors(NetworkingBaseBehaviors): resp.entity.admin_pass = server.admin_pass return resp.entity + def get_remote_instance_client(self, server, ip_address, private_key, + ssh_username='root', auth_strategy='key'): + """ + @summary: gets a compute server remote client + """ + + if self.compute is None: + raise UnavailableComputeInteractionException + + remote_client = ( + self.compute.servers.behaviors.get_remote_instance_client( + server=server, ip_address=ip_address, username=ssh_username, + key=private_key, auth_strategy=auth_strategy)) + return remote_client + def list_servers(self, name=None, raise_exception=False): """ @summary: list servers wrapper for networks diff --git a/cloudcafe/networking/networks/common/exceptions.py b/cloudcafe/networking/networks/common/exceptions.py index 42c8780b..74737a77 100644 --- a/cloudcafe/networking/networks/common/exceptions.py +++ b/cloudcafe/networking/networks/common/exceptions.py @@ -75,6 +75,10 @@ class ResourceUpdateException(BaseNetworkingException): MSG = 'Unable to update resource' +class PortUpdateException(BaseNetworkingException): + MSG = 'Unable to update port' + + class ResourceGetException(BaseNetworkingException): MSG = 'Unable to get resource' diff --git a/cloudcafe/networking/networks/extensions/security_groups_api/behaviors.py b/cloudcafe/networking/networks/extensions/security_groups_api/behaviors.py index 05b813b2..782f54cd 100644 --- a/cloudcafe/networking/networks/extensions/security_groups_api/behaviors.py +++ b/cloudcafe/networking/networks/extensions/security_groups_api/behaviors.py @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. """ +import copy import time from cloudcafe.common.tools.datagen import rand_name @@ -21,18 +22,137 @@ from cloudcafe.networking.networks.common.behaviors \ import NetworkingBaseBehaviors, NetworkingResponse from cloudcafe.networking.networks.common.exceptions \ import ResourceBuildException, ResourceDeleteException, \ - ResourceGetException, ResourceListException, ResourceUpdateException + ResourceGetException, ResourceListException, \ + ResourceUpdateException, MissingDataException, \ + UnsupportedTypeException from cloudcafe.networking.networks.extensions.security_groups_api.constants \ import SecurityGroupsResponseCodes class SecurityGroupsBehaviors(NetworkingBaseBehaviors): + ICMP = 'icmp' + TCP = 'tcp' + UDP = 'udp' + def __init__(self, security_groups_client, security_groups_config): super(SecurityGroupsBehaviors, self).__init__() self.config = security_groups_config self.client = security_groups_client + def add_rule(self, security_groups, version=4, protocol='tcp', ports=None, + ingress=True, egress=False): + """ + @summary: Create security group rules ingress/egress + @param security_groups: security groups to add the rule. + @type security_groups: list + @param version: IP version, 4 or 6. + @type version: int + @param protocol: icmp, tcp or udp. + @type protocol: str + @param ports: port ranges min and max, for ex. 442-445, also only one + can be given for both, for ex. 22 + @type ports: str + @param ingress: flag for adding an ingress rule. + @type ingress: bool + @param egress: flag for adding an egress rule. + @type egress: bool + """ + + # Verifying an expected protocol is given + expected_protocols = [self.ICMP, self.TCP, self.UDP] + if protocol not in expected_protocols: + msg = '{0} not within the expected protocols: {1}'.format( + protocol, expected_protocols) + raise MissingDataException(msg) + + # Setting the rule protocol and ethertype from the IP version + ethertype = 'IPv{0}'.format(version) + attrs_kwargs = dict(protocol=protocol, ethertype=ethertype) + + # Verifying a port or port range is given for tcp and udp rules + if protocol in [self.TCP, self.UDP]: + if not ports: + msg = ('{0} protocol requires the ports value, for ex.' + '22 or 442-445').format(protocol) + raise MissingDataException(msg) + + # In case only one port is given as an int + if type(ports) == int: + ports = str(ports) + elif type(ports) != str: + msg = ('{0} should be a string for ex. "22" or ' + '"442-445"').format(ports) + raise UnsupportedTypeException(msg) + + port_list = ports.split('-') + port_range_min = port_list[0] + port_range_max = port_list[-1] + + # Adding the port ranges to the request attributes + attrs_kwargs.update(port_range_min=port_range_min) + attrs_kwargs.update(port_range_max=port_range_max) + + results = [] + + # Adding the ingress/egress rules to the security groups given. + for sg_id in security_groups: + result = dict(security_group_id=sg_id) + request_kwargs = copy.deepcopy(attrs_kwargs) + request_kwargs.update(security_group_id=sg_id) + if ingress: + request_kwargs.update(direction='ingress') + rule = self.create_security_group_rule(**request_kwargs) + result.update(ingress_rule_request=rule) + if egress: + request_kwargs.update(direction='egress') + rule = self.create_security_group_rule(**request_kwargs) + result.update(egress_rule_request=rule) + results.append(result) + + return results + + def remove_rule(self, security_groups, version=None, protocol='', + direction='', all_rules=False): + """ + @summary: remove rules from groups based on criteria + @param security_groups: security groups to remove the rules. + @type security_groups: list + @param version: rules with this IP version will be deleted if given. + @type version: int + @param protocol: rules with this protocol will be deleted if given. + @type protocol: str + @param direction: rules with this direction will be deleted if given. + @type direction: str + @param all_rules: flag to delete all rules within the security group. + @type all_rules: bool + """ + + if version: + ethertype = 'IPv{0}'.format(version) + else: + ethertype = 'DoNotDeleteByIP' + + results = [] + + # Removing rules + for sg_id in security_groups: + sec_group = self.get_security_group(security_group_id=sg_id, + raise_exception=True) + rules = sec_group.response.entity.security_group_rules + for rule in rules: + result = dict(security_group_id=sg_id, + security_group_rule_id=rule.id) + if (all_rules or rule.ethertype.lower() == ethertype.lower() or + rule.protocol.lower() == protocol.lower() or + rule.direction.lower() == direction.lower()): + delete = self.delete_security_group_rule( + security_group_rule_id=rule.id, raise_exception=True) + result.update(delete_request=delete) + results.append(result) + + return results + def create_security_group(self, name=None, description=None, tenant_id=None, resource_build_attempts=None, raise_exception=True, use_exact_name=False, @@ -587,9 +707,10 @@ class SecurityGroupsBehaviors(NetworkingBaseBehaviors): resp = self.client.get_security_group_rule( security_group_rule_id=security_group_rule_id) + status_code = SecurityGroupsResponseCodes.GET_SECURITY_GROUP_RULE resp_check = self.check_response( resp=resp, - status_code=SecurityGroupsResponseCodes.GET_SECURITY_GROUP_RULE, + status_code=status_code, label=security_group_rule_id, message=err_msg) result.response = resp @@ -678,9 +799,10 @@ class SecurityGroupsBehaviors(NetworkingBaseBehaviors): remote_ip_prefix=remote_ip_prefix, tenant_id=tenant_id, limit=limit, marker=marker, page_reverse=page_reverse) + status_code = SecurityGroupsResponseCodes.LIST_SECURITY_GROUP_RULES resp_check = self.check_response( resp=resp, - status_code=SecurityGroupsResponseCodes.LIST_SECURITY_GROUP_RULES, + status_code=status_code, label='', message=err_msg) result.response = resp diff --git a/cloudcafe/networking/networks/personas.py b/cloudcafe/networking/networks/personas.py index a89389ec..c10f6183 100644 --- a/cloudcafe/networking/networks/personas.py +++ b/cloudcafe/networking/networks/personas.py @@ -19,6 +19,11 @@ from cloudcafe.networking.networks.common.behaviors \ from cloudcafe.networking.networks.common.constants \ import NetworkTypes, PortTypes from cloudcafe.networking.networks.composites import NetworkingComposite +from cloudcafe.networking.networks.extensions.security_groups_api.composites \ + import SecurityGroupsComposite + +from cloudcafe.networking.networks.common.exceptions \ + import PortUpdateException class ServerPersona(BaseModel, NetworkingBaseBehaviors): @@ -32,7 +37,7 @@ class ServerPersona(BaseModel, NetworkingBaseBehaviors): pnet_port_count=1, inet_fix_ipv4_count=0, inet_fix_ipv6_count=0, snet_fix_ipv4_count=1, snet_fix_ipv6_count=0, pnet_fix_ipv4_count=1, - pnet_fix_ipv6_count=1): + pnet_fix_ipv6_count=1, keypair=None, ssh_username=None): super(ServerPersona, self).__init__() """ @param server: server entity @@ -74,10 +79,16 @@ class ServerPersona(BaseModel, NetworkingBaseBehaviors): @type pnet_fix_ipv4_count: int @param pnet_fix_ipv6_count: expected public network fixed IPv6s count @type pnet_fix_ipv6_count: int + @param keypair: keypair object with private_key attribute + @type keypair: networking.networks.behaviors create_keypair response + @param ssh_username: remote client username for SSH + @type ssh_username: str """ - # Server entity object + # Server and keypair entity object self.server = server + self.keypair = keypair + self.ssh_username = ssh_username # Server expected networks (bool value) self.pnet = pnet @@ -104,8 +115,9 @@ class ServerPersona(BaseModel, NetworkingBaseBehaviors): self.pnet_fix_ipv4_count = pnet_fix_ipv4_count self.pnet_fix_ipv6_count = pnet_fix_ipv6_count - # Networking composite + # Networking composites self.net = NetworkingComposite() + self.sec = SecurityGroupsComposite() # base config from networking/networks/common/config.py self.config = self.net.config @@ -128,6 +140,8 @@ class ServerPersona(BaseModel, NetworkingBaseBehaviors): 'network {1}. Failures: {2}.') self.fixed_ips_failure_msg = ('Unable to get server {0} fixed IPs ' 'with IPv{1} version for network {2}') + self.update_port_failure_msg = ('Unable to update server {0} ports for' + ' network {1}. Failures: {2}.') def __str__(self): @@ -142,32 +156,37 @@ class ServerPersona(BaseModel, NetworkingBaseBehaviors): return ', '.join(data_str) - data = {'name': self.server.name, 'svr_id':self.server.id, + data = {'name': self.server.name, 'svr_id': self.server.id, 'pub_net_id': self.public_network_id, 'pub_port_ids': self.pnet_port_ids, + 'pub_sec_groups': self.pnet_security_groups, 'pub_ipv4_addr': self.pnet_fix_ipv4, 'pub_ipv6_addr': self.pnet_fix_ipv6, 'svc_net_id': self.service_network_id, 'svc_port_ids': self.snet_port_ids, + 'svc_sec_groups': self.snet_security_groups, 'svc_ipv4_addr': self.snet_fix_ipv4, 'svc_ipv6_addr': self.snet_fix_ipv6, 'iso_net_id': getattr(self.network, 'id', None), 'iso_sub_id': getattr(self.subnetv4, 'id', None), 'iso_port_ids': self.inet_port_ids, - 'iso_ipv4_addr': self.pnet_fix_ipv4, + 'iso_sec_groups': self.inet_security_groups, + 'iso_ipv4_addr': self.inet_fix_ipv4, 'iso_sub_v6_ids': build_data_str(self.subnetv6, 'id'), - 'iso_ipv6_addr': self.pnet_fix_ipv6} + 'iso_ipv6_addr': self.inet_fix_ipv6} msg = "\nServer Name: {name} ({svr_id})\n" msg += "Public Net:\n" msg += "\tNetwork Id: {pub_net_id}\n" msg += "\tPort Ids: {pub_port_ids}\n" + msg += "\tSecurity Groups: {pub_sec_groups}\n" msg += "\tIPv4 Address: {pub_ipv4_addr}\n" msg += "\tIPv6 Address: {pub_ipv6_addr}\n\n" msg += "Service Net:" msg += "\tNetwork Id: {svc_net_id}\n" msg += "\tPort Ids: {svc_port_ids}\n" + msg += "\tSecurity Groups: {svc_sec_groups}\n" msg += "\tIPv4 Address: {svc_ipv4_addr}\n" msg += "\tIPv6 Address: {svc_ipv6_addr}\n\n" @@ -175,6 +194,7 @@ class ServerPersona(BaseModel, NetworkingBaseBehaviors): msg += "\tNetwork Id: {iso_net_id}\n" msg += "\tSubnet Id (IPv4): {iso_sub_id}\n" msg += "\tPort Ids: {iso_port_ids}\n" + msg += "\tSecurity Groups: {iso_sec_groups}\n" msg += "\tIPv4 Address: {iso_ipv4_addr}\n" msg += "\tSubnet Id (IPv6): {iso_sub_v6_ids}\n" msg += "\tIPv6 Address: {iso_ipv6_addr}\n\n" @@ -289,6 +309,34 @@ class ServerPersona(BaseModel, NetworkingBaseBehaviors): """ return self._get_port_ids(port_type=PortTypes.ISOLATED) + @property + def pnet_security_groups(self): + """ + @summary: gets the security groups at the public network ports + """ + return self._get_port_security_groups(port_type=PortTypes.PUBLIC) + + @property + def snet_security_groups(self): + """ + @summary: gets the security groups at the service network ports + """ + return self._get_port_security_groups(port_type=PortTypes.SERVICE) + + @property + def inet_security_groups(self): + """ + @summary: gets the security groups at the isolated network ports + """ + return self._get_port_security_groups(port_type=PortTypes.ISOLATED) + + @property + def remote_client(self): + """ + @summary: gets remote instance client + """ + return self._get_remote_instance_client() + def update_server_persona(self, clear_errors=True): """ @summary: updates the self.server entity doing a GET server call @@ -306,6 +354,49 @@ class ServerPersona(BaseModel, NetworkingBaseBehaviors): if clear_errors: self.errors = [] + def add_security_groups_to_ports(self, port_type, security_group_ids, + port_ids=None, raise_exception=False): + """ + Adds a security group or groups to a server port or ports based on type + @param port_type: port network type, for ex. pnet (public), + snet (service), or inet (isolated). + @type port_type: str + @param security_group_ids: the escurity group or groups to + be added to the port(s). + @type security_group_ids: list + @param port_ids (optional): to target specific ports by ID where the + security group(s) should be added. They must match the network type + param. By default, all the server port(s) by the given network type + will be added the security group(s). + @type port_ids: list + """ + + # Getting all the port ids from the network port type + port_ids_label = '{0}_port_ids'.format(port_type) + persona_port_ids = getattr(self, port_ids_label, None) + + # Targeting specific ports if port_ids given + if port_ids: + port_ids_set = set(port_ids) + port_ids = list(port_ids_set.intersection(persona_port_ids)) + else: + port_ids = persona_port_ids + + # Adding the security group + for port_id in port_ids: + update = self.ports.behaviors.update_port( + port_id=port_id, security_groups=security_group_ids) + if update.failures: + msg = self.update_port_failure_msg.format(self.server.id, + port_type, + update.failures) + self.errors.append(msg) + self._log.error(msg) + if raise_exception: + raise PortUpdateException(msg) + return False + return True + def _port_response(self, network_type): """ @summary: returns server network ports based on network type @@ -327,6 +418,17 @@ class ServerPersona(BaseModel, NetworkingBaseBehaviors): return [] return ports.response.entity + def _get_remote_instance_client(self): + """ + @summary: returns a remote instance client + """ + ip_address = self.pnet_fix_ipv4[0] + remote_client = self.behaviors.get_remote_instance_client( + server=self.server, ip_address=ip_address, + private_key=self.keypair.private_key, + ssh_username=self.ssh_username) + return remote_client + def _get_fixed_ips(self, ip_version, port_type, network_type): """ @summary: gets fixed IPs from server ports @@ -370,3 +472,16 @@ class ServerPersona(BaseModel, NetworkingBaseBehaviors): ports = getattr(self, port_attr, []) port_ids = [port.id for port in ports] return port_ids + + def _get_port_security_groups(self, port_type): + """ + @summary: gets the ports security groups + @param port_type: pnet, snet or inet port type + @type port_type: str + @return: all the security groups of the same port type + @rtype: list(list) + """ + port_attr = '{0}_ports'.format(port_type.lower()) + ports = getattr(self, port_attr, []) + port_sec_groups = [port.security_groups for port in ports] + return port_sec_groups