# Copyright 2018 Capital One Services, LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from pcf.core.aws_resource import AWSResource from pcf.core import State from pcf.util import pcf_util from pcf.particle.aws.vpc.vpc_instance import VPCInstance from deepdiff import DeepDiff class SecurityGroup(AWSResource): """ This is the implementation of Amazon's Security Groups """ flavor = "security_group" state_lookup = { "available": State.running, "pending": State.pending, "missing": State.terminated } equivalent_states = { State.running: 1, State.stopped: 0, State.terminated: 0 } START_PARAMS = { "Description", "GroupName", "VpcId", "DryRun" } DEFINITION_FILTER = { "IpPermissionsEgress", "IpPermissions" } UNIQUE_KEYS = ["aws_resource.GroupName"] def __init__(self, particle_definition): super().__init__(particle_definition, "ec2") self._set_unique_keys() self._group_name = self.desired_state_definition.get("GroupName") self._sg_resource = None self._group_id = "" @property def security_group_resource(self): """ The security group resource. Creates Boto Security Group resource for the given group id Returns: boto security group resource """ if not self._sg_resource: self._sg_resource = self.resource.SecurityGroup(self._group_id) return self._sg_resource def _set_unique_keys(self): """ Logic that sets keys from state definition that are used to uniquely identify the security group """ self.unique_keys = SecurityGroup.UNIQUE_KEYS def _set_vpc_id(self): """ Checks to see if user specified a vpc_id in the particle definition. If not the vpc_id is retrieved from it's parent using the get_vpc_id util """ if not self.desired_state_definition.get("VpcId"): # need it defined in the definition for _start() self.desired_state_definition["VpcId"] = pcf_util.get_value_from_particles(self.parents, VPCInstance, "vpc_id") self._vpc_id = self.desired_state_definition.get("VpcId") def _start(self): """ Creates security group and adds the tags and rules Returns: boto3 create_security_group response (groud id) """ resp = self.client.create_security_group(**pcf_util.param_filter(self.desired_state_definition, SecurityGroup.START_PARAMS)) self._group_id = resp.get("GroupId") tags = self.custom_config.get("Tags", []) if tags: self.security_group_resource.create_tags( Tags=tags ) outbound = self.custom_config.get("IpPermissionsEgress", {}) inbound = self.custom_config.get("IpPermissions", {}) if outbound: self.security_group_resource.authorize_egress( IpPermissions=outbound ) if inbound: self.security_group_resource.authorize_ingress( IpPermissions=inbound ) return resp def _terminate(self): """ Calls boto3 delete_security_group() Returns: boto3 delete_security_group() response """ resp = self.client.delete_security_group(GroupId=self._group_id) return resp def _stop(self): """ Calls _terminate() """ return self._terminate() def _update(self): """ removes and adds security group rules based on the new desired definition using boto3 revoke and authorize """ # no boto command for removing tags. create_tags both creates and updates existing tags new_tags = DeepDiff(self.current_state_definition.get("Tags", []), self.custom_config.get("Tags", [])) new_tags.pop("iterable_item_removed", None) if new_tags: self.security_group_resource.create_tags( Tags=self.custom_config.get("Tags") ) dd_egress = DeepDiff(self.current_state_definition.get("IpPermissionsEgress", {}), self.custom_config.get("IpPermissionsEgress")) dd_ingress = DeepDiff(self.current_state_definition.get("IpPermissions", {}), self.custom_config.get("IpPermissions")) if dd_ingress: if dd_ingress.get("iterable_item_removed") or dd_ingress.get("values_changed"): self.security_group_resource.revoke_ingress( IpPermissions=self.current_state_definition.get("IpPermissions") ) if dd_ingress.get("iterable_item_added") or dd_ingress.get("values_changed"): self.security_group_resource.authorize_ingress( IpPermissions=self.custom_config.get("IpPermissions") ) if dd_egress: if dd_egress.get("iterable_item_removed") or dd_egress.get("values_changed"): self.security_group_resource.revoke_egress( IpPermissions=self.current_state_definition.get("IpPermissionsEgress") ) if dd_egress.get("iterable_item_added") or dd_egress.get("values_changed"): self.security_group_resource.authorize_egress( IpPermissions=self.custom_config.get("IpPermissionsEgress") ) def get_current_definition(self): """ Calls boto3 describe_security_groups to return current definition. Returns missing if the security group doesn't exist Returns: status or None """ self._set_vpc_id() security_group_list = self.client.describe_security_groups( Filters=[ { "Name": "vpc-id", "Values": [ self._vpc_id ] }, { "Name": "group-name", "Values": [ self._group_name ] } ] ).get("SecurityGroups", []) # Need to make sure the group name matches exactly, since it is just a filter for sg in security_group_list: if sg.get("GroupName") == self._group_name: self._group_id = sg.get("GroupId") return sg def sync_state(self): """ Uses get_current_definition to determine whether the group exists or not and sets the state Returns: void """ # get the current definition. if it exists, running; if missing, terminated. current_definition = self.get_current_definition() if current_definition: self.state = State.running self.current_state_definition = current_definition else: self.state = State.terminated def is_state_definition_equivalent(self): """ Compares the desired state and current state definition and returns whether they are equivalent Only considers fields defined in the desired definition All fields not specified are left alone in the current state (excluding rules) Both rules lists must be defined even when empty Returns: bool """ self.sync_state() # use filters to remove any extra information desired_config = pcf_util.param_filter(self.desired_state_definition.get("custom_config", {}), SecurityGroup.DEFINITION_FILTER) current_config = pcf_util.param_filter(self.get_current_state_definition(), desired_config.keys()) diff = DeepDiff(current_config, desired_config, ignore_order=True) return diff == {} def is_state_equivalent(self, state1, state2): """ Looks up state equivalents and checks if the two inputs map to the same state Returns: bool """ return SecurityGroup.equivalent_states.get(state1) == SecurityGroup.equivalent_states.get(state2)