def _set_creation(self) -> None: """Set creation option for stack.""" print(80 * "-") fzf = Pyfzf() fzf.append_fzf("RollbackOnFailure\n") fzf.append_fzf("TimeoutInMinutes\n") fzf.append_fzf("EnableTerminationProtection\n") selected_options: List[str] = list( fzf.execute_fzf( empty_allow=True, print_col=1, multi_select=True, header="select options to configure", ) ) for option in selected_options: result: str = "" if option == "RollbackOnFailure": fzf.fzf_string = "" fzf.append_fzf("True\n") fzf.append_fzf("False\n") result = str( fzf.execute_fzf( empty_allow=True, print_col=1, header="roll back on failue? (Default: True)", ) ) if result: self._extra_args["OnFailure"] = ( "ROLLBACK" if result == "True" else "DO_NOTHING" ) elif option == "TimeoutInMinutes": message = "Specify number of minutes before stack timeout (Default: no timeout): " timeout = input(message) if timeout: self._extra_args["TimeoutInMinutes"] = int(timeout) elif option == "EnableTerminationProtection": fzf.fzf_string = "" fzf.append_fzf("True\n") fzf.append_fzf("False\n") result = str( fzf.execute_fzf( empty_allow=True, print_col=1, header="enable termination protection? (Default: False)", ) ) if result: self._extra_args["EnableTerminationProtection"] = ( True if result == "True" else False )
def __init__( self, profile: Optional[Union[str, bool]] = None, region: Optional[Union[str, bool]] = None, service_name: str = "", ) -> None: """Construct a BaseSession instance. If profile or region is True value, then fzf will be launched to let user select region or profile. """ session = Session() selected_profile: Optional[str] = None selected_region: Optional[str] = None if profile and type(profile) == bool: fzf = Pyfzf() for profile in session.available_profiles: fzf.append_fzf("%s\n" % profile) selected_profile = str(fzf.execute_fzf(print_col=1)) elif profile and type(profile) is str: selected_profile = str(profile) if region and type(region) == bool: fzf = Pyfzf() regions = session.get_available_regions(service_name) for region in regions: fzf.append_fzf("%s\n" % region) selected_region = str(fzf.execute_fzf(print_col=1)) elif region and type(region) is str: selected_region = str(region) if not selected_profile: selected_profile = os.getenv( "FZFAWS_%s_PROFILE" % service_name.upper(), "") if not selected_profile: selected_profile = os.getenv("FZFAWS_GLOBAL_PROFILE", None) if not selected_region: selected_region = os.getenv( "FZFAWS_%s_REGION" % service_name.upper(), "") if not selected_region: selected_region = os.getenv("FZFAWS_GLOBAL_REGION", None) self.profile: Optional[str] = selected_profile self.region: Optional[str] = selected_region self.session = Session(region_name=selected_region, profile_name=selected_profile) self._client = self.session.client(service_name) # only certain service support resource resources = self.session.get_available_resources() if service_name in resources: self._resource = self.session.resource(service_name)
def set_arns( self, arns: Optional[Union[str, List[str]]] = None, empty_allow: bool = False, header: Optional[str] = None, multi_select: bool = False, ) -> None: """Set the sns arn for other operations. :param arns: arns to init :type arns: Union[list, str], optional :param empty_allow: allow empty fzf selection :type empty_allow: bool, optional :param header: header in fzf :type header: str, optional :param multi_select: allow multi selection in fzf :type multi_select: bool, optional """ if not arns: fzf = Pyfzf() with Spinner.spin(message="Fetching sns topics ..."): paginator = self.client.get_paginator("list_topics") for result in paginator.paginate(): fzf.process_list(result.get("Topics", []), "TopicArn") arns = fzf.execute_fzf(empty_allow=empty_allow, multi_select=multi_select, header=header) if type(arns) == str: self.arns[0] = str(arns) elif type(arns) == list: self.arns = list(arns)
def _get_path_option(self, download: bool = False) -> str: """Pop fzf for user to select what to do with the path. :param download: if not download, insert append option :type download: bool, optional :return: selected option :rtype: str """ fzf = Pyfzf() fzf.append_fzf("root: operate on the root level of the bucket\n") fzf.append_fzf( "interactively: interactively select a path through s3\n") fzf.append_fzf("input: manully input the path/name\n") if not download: fzf.append_fzf( "append: interactively select a path and then input new path/name to append" ) selected_option = str( fzf.execute_fzf( print_col=1, header= "Please select which level of the bucket would you like to operate in", delimiter=": ", )) return selected_option
def set_ACL(self, original: bool = False, version: Optional[List[Dict[str, str]]] = None) -> None: """Set the ACL option. :param original: display original value :type original: bool, optional :param version: version object to set acl for version :type version: List[Dict[str, str]], optional """ if not version: version = [] fzf = Pyfzf() fzf.append_fzf("None (use bucket default ACL setting)\n") fzf.append_fzf( "Canned ACL (predefined set of grantees and permissions)\n") fzf.append_fzf( "Explicit ACL (explicit set grantees and permissions)\n") result = fzf.execute_fzf( empty_allow=True, print_col=1, header= "select a type of ACL to grant, aws accept one of canned ACL or explicit ACL", ) if result == "Canned": self._set_canned_ACL() elif result == "Explicit": self._set_explicit_ACL(original=original, version=version) else: return
def get_security_groups( self, multi_select: bool = False, return_attr: str = "id", header: str = None, no_progress: bool = False, ) -> Union[str, list]: """Use paginator to get the user selected security groups. :param multi_select: allow multiple value selection :type multi_select: bool, optional :param return_attr: what attribute to return (id|name) :type return_attr: str, optional :param header: header to display in fzf :type header: str, optional :param no_progress: don't display progress bar, useful for ls command :type no_progress: bool, optional :return: selected security groups/ids :rtype: Union[str, list] """ fzf = Pyfzf() with Spinner.spin(message="Fetching SecurityGroups ...", no_progress=no_progress): paginator = self.client.get_paginator("describe_security_groups") for result in paginator.paginate(): response_generator = self._name_tag_generator( result.get("SecurityGroups", [])) if return_attr == "id": fzf.process_list(response_generator, "GroupId", "GroupName", "Name") elif return_attr == "name": fzf.process_list(response_generator, "GroupName", "Name") return fzf.execute_fzf(multi_select=multi_select, empty_allow=True, header=header)
def get_vpc_id(self, multi_select: bool = False, header: str = None, no_progress: bool = False) -> Union[str, list]: """Get user selected vpc id through fzf. :param multi_select: allow multiple value selection :type multi_select: bool, optional :param header: header to display in fzf header :type header: str, optional :param no_progress: don't display progress bar, useful for ls command :type no_progress: bool, optional :return: selected vpc id :rtype: Union[str, list] """ fzf = Pyfzf() with Spinner.spin(message="Fetching VPCs ...", no_progress=no_progress): paginator = self.client.get_paginator("describe_vpcs") for result in paginator.paginate(): response_generator = self._name_tag_generator( result.get("Vpcs", [])) fzf.process_list(response_generator, "VpcId", "IsDefault", "CidrBlock", "Name") return fzf.execute_fzf(empty_allow=True, multi_select=multi_select, header=header)
def get_stack_resources( self, empty_allow: bool = False, header: str = None, no_progress: bool = False ) -> List[str]: """List all stack logical resources and return the selected resources. :param empty_allow: allow empty selection :type empty_allow: bool, optional :param header: information to be displayed in fzf header :type header: str, optional :param no_progress: don't display progress bar, useful for ls command :type no_progress: bool, optional :return: selected list of logical resources LogicalResourceId :rtype: List[str] """ fzf = Pyfzf() with Spinner.spin( message="Fetching stack resources ...", no_progress=no_progress ): paginator = self.client.get_paginator("list_stack_resources") for result in paginator.paginate(StackName=self.stack_name): for resource in result.get("StackResourceSummaries"): resource["Drift"] = resource.get("DriftInformation").get( "StackResourceDriftStatus" ) fzf.process_list( result.get("StackResourceSummaries"), "LogicalResourceId", "ResourceType", "Drift", ) return list( fzf.execute_fzf(multi_select=True, header=header, empty_allow=empty_allow) )
def set_arns( self, arns: Optional[Union[str, list]] = None, empty_allow: bool = False, header: Optional[str] = None, multi_select: bool = False, ) -> None: """Set cloudwatch arns for further operations. :param arns: arns to init :type arns: Union[list, str], optional :param empty_allow: allow empty fzf selection :type empty_allow: bool, optional :param header: header in fzf :type header: str, optional :param multi_select: allow multi selection in fzf :type multi_select: bool, optional """ if not arns: fzf = Pyfzf() with Spinner.spin(message="Fetching cloudwatch alarms ..."): paginator = self.client.get_paginator("describe_alarms") for result in paginator.paginate(): if result.get("CompositeAlarms"): fzf.process_list(result["CompositeAlarms"], "AlarmArn") if result.get("MetricAlarms"): fzf.process_list(result["MetricAlarms"], "AlarmArn") arns = fzf.execute_fzf( empty_allow=empty_allow, multi_select=multi_select, header=header ) if type(arns) == str: self.arns[0] = str(arns) elif type(arns) == list: self.arns = list(arns)
def set_encryption(self, original: str = None) -> None: """Set the encryption setting. :param original: previous value of the encryption :type original: str, optional """ header = "select an ecryption setting, esc to use the default encryption setting for the bucket" if original: header += "\nOriginal: %s" % original fzf = Pyfzf() fzf.append_fzf("None (Use bucket default setting)\n") fzf.append_fzf("AES256\n") fzf.append_fzf("aws:kms\n") result: str = str( fzf.execute_fzf(empty_allow=True, print_col=1, header=header)) if result: self._extra_args["ServerSideEncryption"] = result if result == "aws:kms": current_region = self.s3.client.get_bucket_location( Bucket=self.s3.bucket_name) current_region = current_region.get("LocationConstraint") kms = KMS(self.s3.profile, self.s3.region) kms.set_keyids(header="select encryption key to use") self._extra_args["SSEKMSKeyId"] = kms.keyids[0]
def set_ec2_instance(self, multi_select: bool = True, header: str = None, no_progress: bool = False) -> None: """Set ec2 instance for current operation. :param multi_select: enable multi select :type multi_select: bool, optional :param header: helper information to display in fzf header :type header: str, optional :param no_progress: don't display progress bar, useful for ls command :type no_progress: bool, optional """ fzf = Pyfzf() with Spinner.spin(message="Fetching EC2 instances ...", no_progress=no_progress): paginator = self.client.get_paginator("describe_instances") for result in paginator.paginate(): response_generator = self._instance_generator( result["Reservations"]) fzf.process_list( response_generator, "InstanceId", "Status", "InstanceType", "Name", "KeyName", "PublicDnsName", "PublicIpAddress", "PrivateIpAddress", ) selected_instance = fzf.execute_fzf(multi_select=multi_select, header=header, print_col=0) if multi_select: self.instance_ids[:] = [] self.instance_list[:] = [] for instance in selected_instance: curr = fzf.format_selected_to_dict(str(instance)) self.instance_list.append(curr) self.instance_ids.append(curr["InstanceId"]) else: self.instance_ids[:] = [] self.instance_list[:] = [] self.instance_list.append( fzf.format_selected_to_dict(str(selected_instance))) self.instance_ids.append(self.instance_list[0]["InstanceId"]) if len(self.instance_ids) == 0: self.instance_ids = [""] if len(self.instance_list) == 0: self.instance_list = [{}]
def set_s3_bucket(self, header: str = "", no_progress: bool = False) -> None: """List bucket through fzf and let user select a bucket. :param header: header to display in fzf header :type header: str, optional :param no_progress: don't display progress bar, useful for ls command :type no_progress: bool, optional """ fzf = Pyfzf() with Spinner.spin(message="Fetching s3 buckets ...", no_progress=no_progress): response = self.client.list_buckets() fzf.process_list(response["Buckets"], "Name") self.bucket_name = str(fzf.execute_fzf(header=header))
def _get_list_param_value(self, type_name: str, param_header: str) -> List[str]: """Handle operation if parameter type is a list type. This function is almost the same as _get_selected_param_value besides its handling list type rather than single vaiable type. :param type_name: name of the type of the parameter :type type_name: str :param param_header: information about the current parameter :type param_header: str :return: processed list of selection from the user :rtype: List[str] """ fzf = Pyfzf() if type_name == "List<AWS::EC2::AvailabilityZone::Name>": with Spinner.spin(message="Fetching AvailabilityZones ..."): response = self.ec2.client.describe_availability_zones() response_list = response["AvailabilityZones"] fzf.process_list(response_list, "ZoneName", empty_allow=True) elif type_name == "List<AWS::EC2::Instance::Id>": return list( self.ec2.get_instance_id(multi_select=True, header=param_header)) elif type_name == "List<AWS::EC2::SecurityGroup::GroupName>": return list( self.ec2.get_security_groups(multi_select=True, return_attr="name", header=param_header)) elif type_name == "List<AWS::EC2::SecurityGroup::Id>": return list( self.ec2.get_security_groups(multi_select=True, header=param_header)) elif type_name == "List<AWS::EC2::Subnet::Id>": return list( self.ec2.get_subnet_id(multi_select=True, header=param_header)) elif type_name == "List<AWS::EC2::Volume::Id>": return list( self.ec2.get_volume_id(multi_select=True, header=param_header)) elif type_name == "List<AWS::EC2::VPC::Id>": return list( self.ec2.get_vpc_id(multi_select=True, header=param_header)) elif type_name == "List<AWS::Route53::HostedZone::Id>": self.route53.set_zone_id(multi_select=True) return self.route53.zone_ids return list( fzf.execute_fzf(multi_select=True, empty_allow=True, header=param_header))
def _get_capabilities(self, message: str = "") -> List[str]: """Display help message and let user select capabilities. :param message: message to display in fzf header :type message: str, optional :return: selected capabilities to acknowledge :rtype: List[str] """ fzf = Pyfzf() fzf.append_fzf("CAPABILITY_IAM\n") fzf.append_fzf("CAPABILITY_NAMED_IAM\n") fzf.append_fzf("CAPABILITY_AUTO_EXPAND") message += "\nPlease select the capabilities to acknowledge and proceed" message += "\nMore information: https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_CreateStack.html" return list( fzf.execute_fzf( empty_allow=True, print_col=1, multi_select=True, header=message ) )
def _set_canned_ACL(self) -> None: """Set the canned ACL for the current operation.""" fzf = Pyfzf() fzf.append_fzf("private\n") fzf.append_fzf("public-read\n") fzf.append_fzf("public-read-write\n") fzf.append_fzf("authenticated-read\n") fzf.append_fzf("aws-exec-read\n") fzf.append_fzf("bucket-owner-read\n") fzf.append_fzf("bucket-owner-full-control\n") result: str = str( fzf.execute_fzf( empty_allow=True, print_col=1, header= "select a Canned ACL option, esc to use the default ACL setting for the bucket", )) if result: self._extra_args["ACL"] = result
def _get_selected_param_value(self, type_name: str, param_header: str) -> str: """Use fzf to display aws specific parameters. :param type_name: name of the parameter type :type type_name: str :param param_header: information about current parameter :type param_header: str :return: return the selected value :rtype: str """ fzf = Pyfzf() if type_name == "AWS::EC2::KeyPair::KeyName": with Spinner.spin(message="Fetching KeyPair ..."): response = self.ec2.client.describe_key_pairs() response_list = response["KeyPairs"] fzf.process_list(response_list, "KeyName", empty_allow=True) elif type_name == "AWS::EC2::SecurityGroup::Id": return str(self.ec2.get_security_groups(header=param_header)) elif type_name == "AWS::EC2::AvailabilityZone::Name": with Spinner.spin(message="Fetching AvailabilityZones ..."): response = self.ec2.client.describe_availability_zones() response_list = response.get("AvailabilityZones", []) fzf.process_list(response_list, "ZoneName", empty_allow=True) elif type_name == "AWS::EC2::Instance::Id": return str(self.ec2.get_instance_id(header=param_header)) elif type_name == "AWS::EC2::SecurityGroup::GroupName": return str( self.ec2.get_security_groups(return_attr="name", header=param_header)) elif type_name == "AWS::EC2::Subnet::Id": return str(self.ec2.get_subnet_id(header=param_header)) elif type_name == "AWS::EC2::Volume::Id": return str(self.ec2.get_volume_id(header=param_header)) elif type_name == "AWS::EC2::VPC::Id": return str(self.ec2.get_vpc_id(header=param_header)) elif type_name == "AWS::Route53::HostedZone::Id": self.route53.set_zone_id() return self.route53.zone_ids[0] return str(fzf.execute_fzf(empty_allow=True, header=param_header))
def set_stack(self, no_progress=False) -> None: """Store the selected stack into the instance attribute. :param no_progress: don't display progress bar, useful for ls command :type no_progress: bool, optional """ fzf = Pyfzf() with Spinner.spin( message="Fetching cloudformation stacks ...", no_progress=no_progress ): paginator = self.client.get_paginator("describe_stacks") response = paginator.paginate() stack_generator = self._get_stack_generator(response) for result in response: fzf.process_list( result["Stacks"], "StackName", "StackStatus", "Description" ) self.stack_name = str(fzf.execute_fzf(empty_allow=False)) self.stack_details = search_dict_in_list( self.stack_name, stack_generator, "StackName" )
def set_storageclass(self, original: str = None) -> None: """Set valid storage class. :param original: original value of the storage_class :type original: str, optional """ header = "select a storage class, esc to use the default storage class of the bucket setting" if original: header += "\nOriginal: %s" % original fzf = Pyfzf() fzf.append_fzf("STANDARD\n") fzf.append_fzf("REDUCED_REDUNDANCY\n") fzf.append_fzf("STANDARD_IA\n") fzf.append_fzf("ONEZONE_IA\n") fzf.append_fzf("INTELLIGENT_TIERING\n") fzf.append_fzf("GLACIER\n") fzf.append_fzf("DEEP_ARCHIVE\n") result = fzf.execute_fzf(empty_allow=True, print_col=1, header=header) if result: self._extra_args["StorageClass"] = result
def get_instance_id(self, multi_select: bool = False, header: str = None) -> Union[str, list]: """Use paginator to get instance id and return it. :param multi_select: allow multiple value selection :type multi_select: bool, optional :param header: header to display in fzf header :type header: str, optional :return: selected instance id :rtype: Union[str, list] """ fzf = Pyfzf() with Spinner.spin(message="Fetching EC2 instances ..."): paginator = self.client.get_paginator("describe_instances") for result in paginator.paginate(): response_generator = self._instance_id_generator( result.get("Reservations", [])) fzf.process_list(response_generator, "InstanceId", "Name") return fzf.execute_fzf(multi_select=multi_select, empty_allow=True, header=header)
def set_zone_id( self, zone_ids: Optional[Union[str, List[str]]] = None, multi_select: bool = False, ) -> None: """Set the hostedzone id for futher operations. :param zone_ids: list of zone_ids to set :type zone_ids: list, optional :param multi_select: allow multi_select :type multi_select: bool, optional """ if zone_ids is None: fzf = Pyfzf() with Spinner.spin(message="Fetching hostedzones ..."): paginator = self.client.get_paginator("list_hosted_zones") for result in paginator.paginate(): result = self._process_hosted_zone(result["HostedZones"]) fzf.process_list(result, "Id", "Name") zone_ids = fzf.execute_fzf(multi_select=multi_select, empty_allow=True) if type(zone_ids) == str: self.zone_ids[0] = str(zone_ids) elif type(zone_ids) == list: self.zone_ids = list(zone_ids)
def set_keyids( self, keyids: Optional[Union[list, str]] = None, header: Optional[str] = None, multi_select: bool = False, empty_allow: bool = True, ) -> None: """Set the key for kms for further operations. :param keyids: keyids to set :type keyids: Union[list, str], optional :param header: header information in fzf :type header: str, optional :param multi_select: enable multi select :type multi_select: bool, optional :param empty_allow: allow empty selection :type empty_allow: bool, optional """ if not keyids: fzf = Pyfzf() with Spinner.spin(message="Fetching kms keys ..."): paginator = self.client.get_paginator("list_aliases") for result in paginator.paginate(): aliases = [ alias for alias in result.get("Aliases") if alias.get("TargetKeyId") ] fzf.process_list(aliases, "TargetKeyId", "AliasName", "AliasArn") keyids = fzf.execute_fzf(header=header, multi_select=multi_select, empty_allow=empty_allow) if type(keyids) == str: self.keyids[0] = str(keyids) elif type(keyids) == list: self.keyids = list(keyids)
def set_s3_object( self, version: bool = False, multi_select: bool = False, deletemark: bool = False, no_progress: bool = False, ) -> None: """List object within a bucket and let user select a object. Stores the file path and the filetype into the instance attributes using paginator to get all results. All of the deleted object are displayed in red color when version mode is enabled. :param version: enable version search :type version: bool, optional :param multi_select: enable multi selection :type multi_select: bool, optional :param deletemark: show deletemark object in the list :type deletemark: bool, optional :param no_progress: don't display progress bar, useful for ls command :type no_progress: bool, optional :raises NoSelectionMade: when there is no selection made """ fzf = Pyfzf() if not version: paginator = self.client.get_paginator("list_objects") with Spinner.spin(message="Fetching s3 objects ...", no_progress=no_progress): for result in paginator.paginate(Bucket=self.bucket_name): for file in result.get("Contents", []): if file.get("Key").endswith( "/") or not file.get("Key"): # user created dir in S3 console will appear in the result and is not operatable continue fzf.append_fzf("Key: %s\n" % file.get("Key")) if multi_select: self.path_list = list( fzf.execute_fzf(multi_select=True, delimiter=": ")) else: self.path_list[0] = str(fzf.execute_fzf(delimiter=": ")) else: paginator = self.client.get_paginator("list_object_versions") with Spinner.spin(message="Fetching s3 objects ...", no_progress=no_progress): results = paginator.paginate(Bucket=self.bucket_name) version_obj_genrator = self._uniq_object_generator( results, deletemark) generated = False for item in version_obj_genrator: generated = True fzf.append_fzf(item + "\n") if not generated: raise NoSelectionMade if multi_select: self.path_list = list( fzf.execute_fzf(delimiter=": ", multi_select=True)) else: self.path_list[0] = str(fzf.execute_fzf(delimiter=": "))
def _set_explicit_ACL( self, original: bool = False, version: Optional[List[Dict[str, str]]] = None) -> None: """Set explicit ACL for grantees and permissions. Get user id/email first than display fzf allow multi_select to select permissions example version: [{"Key", key, "VersionId": versionid}] :param original: display original value :type original: bool, optional :param version: version of the object :type version: List[Dict[str, str]], optional """ original_acl: Dict[str, List[str]] = { "FULL_CONTROL": [], "WRITE_ACP": [], "READ": [], "READ_ACP": [], } # get original values if original: acls = None if not version: acls = self.s3.client.get_object_acl( Bucket=self.s3.bucket_name, Key=self.s3.path_list[0]) elif len(version) == 1: acls = self.s3.client.get_object_acl( Bucket=self.s3.bucket_name, Key=self.s3.path_list[0], VersionId=version[0].get("VersionId"), ) if acls: owner = acls["Owner"]["ID"] for grantee in acls.get("Grants", []): if grantee["Grantee"].get("EmailAddress"): original_acl[grantee["Permission"]].append( "%s=%s" % ("emailAddress", grantee["Grantee"].get("EmailAddress"))) elif (grantee["Grantee"].get("ID") and grantee["Grantee"].get("ID") != owner): original_acl[grantee["Permission"]].append( "%s=%s" % ("id", grantee["Grantee"].get("ID"))) elif grantee["Grantee"].get("URI"): original_acl[grantee["Permission"]].append( "%s=%s" % ("uri", grantee["Grantee"].get("URI"))) print("Current ACL:") print(json.dumps(original_acl, indent=4, default=str)) print("Note: fzf.aws cannot preserve previous ACL permission") if not get_confirmation("Continue?"): return # get what permission to set fzf = Pyfzf() fzf.append_fzf("GrantFullControl\n") fzf.append_fzf("GrantRead\n") fzf.append_fzf("GrantReadACP\n") fzf.append_fzf("GrantWriteACP\n") results: List[str] = list( fzf.execute_fzf(empty_allow=True, print_col=1, multi_select=True)) if not results: print( "No permission is set, default ACL settings of the bucket would be used" ) else: for result in results: print("Set permisstion for %s" % result) print( "Enter a list of either the Canonical ID, Account email, Predefined Group url to grant permission (Seperate by comma)" ) print( "Format: id=XXX,id=XXX,[email protected],uri=http://acs.amazonaws.com/groups/global/AllUsers" ) if original: print(80 * "-") if result == "GrantFullControl" and original_acl.get( "FULL_CONTROL"): print("Orignal: %s" % ",".join(original_acl.get("FULL_CONTROL", []))) elif result == "GrantRead" and original_acl.get("READ"): print("Orignal: %s" % ",".join(original_acl.get("READ", []))) elif result == "GrantReadACP" and original_acl.get( "READ_ACP"): print("Orignal: %s" % ",".join(original_acl.get("READ_ACP", []))) elif result == "GrantWriteACP" and original_acl.get( "WRITE_ACP"): print("Orignal: %s" % ",".join(original_acl.get("WRITE_ACP", []))) accounts = input("Accounts: ") print(80 * "-") self._extra_args[result] = str(accounts)
def _get_user_input( self, parameter_key: str, parameter_type: str, param_header: str, value_type: str = None, default: str = None, ) -> Union[str, List[str]]: """Get user input. :param parameter_key: the current parameter key to obtain user input :type parameter_key: str :param parameter_type: type of the parameter :type parameter_type: str :param param_header: information about current parameter :type param_header: str :param value_type: determine if the current action is update or new creation ('Default|Original') :type value_type: str, optional :param default: default value of params or orignal value :type default: str, optional :return: return the user selection through fzf or python input :rtype: Union[str, List[str]] """ user_input: Union[str, List[str]] = "" # execute fzf if allowed_value array exists if "AllowedValues" in self.params[parameter_key]: param_header += self._print_parameter_key(parameter_key, value_type, default) fzf = Pyfzf() for allowed_value in self.params[parameter_key]["AllowedValues"]: fzf.append_fzf("%s\n" % allowed_value) user_input = fzf.execute_fzf(empty_allow=True, print_col=1, header=param_header) else: if parameter_type in self._aws_specific_param: param_header += self._print_parameter_key( parameter_key, value_type, default) user_input = self._get_selected_param_value( parameter_type, param_header) elif parameter_type in self._aws_specific_list_param: param_header += self._print_parameter_key( parameter_key, value_type, default) user_input = self._get_list_param_value( parameter_type, param_header) else: print(param_header.rstrip()) if not value_type: user_input = input("%s: " % parameter_key) elif value_type == "Default": user_input = input("%s(Default: %s): " % (parameter_key, default)) elif value_type == "Original": user_input = input("%s(Original: %s): " % (parameter_key, default)) if not user_input and default: return default elif user_input == "''": return "" elif user_input == '""': return "" else: return user_input
def get_object_version( self, bucket: str = "", key: str = "", delete: bool = False, select_all: bool = False, non_current: bool = False, multi_select: bool = True, no_progress: bool = False, ) -> List[Dict[str, str]]: """List object versions through fzf. :param bucket: object's bucketname, if not set, class instance's bucket_name will be used :type bucket: str, optional :param key: object's key, if not set, class instance's path_list[0] will be used :type key: str, optional :param delete: allow to choose delete marker :type delete: bool, optional :param select_all: skip fzf and select all version and put into return list :type select_all: bool, optional :param non_current: only put non_current versions into list :type non_current: bool, optional :param multi_select: allow multi selection :type multi_select: bool, optional :param no_progress: don't display progress bar, useful for ls command :type no_progress: bool, optional :return: list of selected versions :rtype: List[Dict[str, str]] Example return value: [{'Key': s3keypath, 'VersionId': s3objectid}] """ bucket = bucket if bucket else self.bucket_name key_list: list = [] fzf = Pyfzf() if key: key_list.append(key) else: key_list.extend(self.path_list) selected_versions: list = [] for key in key_list: response_generator: Union[list, Generator[Dict[str, str], None, None]] = [] with Spinner.spin(message="Fetching object versions ...", no_progress=no_progress): paginator = self.client.get_paginator("list_object_versions") for result in paginator.paginate(Bucket=bucket, Prefix=key): response_generator = self._version_generator( result.get("Versions", []), result.get("DeleteMarkers", []), non_current, delete, ) if not select_all: fzf.process_list( response_generator, "VersionId", "Key", "IsLatest", "DeleteMarker", "LastModified", ) else: selected_versions.extend([{ "Key": key, "VersionId": version.get("VersionId") } for version in response_generator]) if not select_all: if delete and multi_select: for result in fzf.execute_fzf(multi_select=True): selected_versions.append({ "Key": key, "VersionId": result }) else: selected_versions.append({ "Key": key, "VersionId": str(fzf.execute_fzf()) }) return selected_versions
def set_extra_args( self, storage: bool = False, acl: bool = False, metadata: bool = False, encryption: bool = False, tags: bool = False, version: Optional[List[Dict[str, str]]] = None, upload: bool = False, ) -> None: """Determine what are the extra settings to set. Use fzf menu to let user select attributes to configure. :param storage: set storage class :type storage: bool, optional :param bool: set acl of object :type bool: bool, optional :param metadata: set metadata :type metadata: bool, optional :param encryption: set encryption :type encryption: bool, optional :param tags: set tags :type tags: bool, optional :param version: specify version of object to modify :type version: List[Dict[str, str]], optional :param upload: determine if the fzf menu could have empty selection allow empty selection during upload operation but not for other operations :type upload: bool, optional """ if not version: version = [] attributes: List[str] = [] if version: # only allow modification of the two attributes for versioned object # because other modification would introduce a new version if not metadata and not acl and not tags: fzf = Pyfzf() fzf.append_fzf("ACL\n") fzf.append_fzf("Tagging") attributes = list( fzf.execute_fzf( print_col=1, multi_select=True, empty_allow=False, header="select attributes to configure", )) else: if not storage and not acl and not metadata and not encryption and not tags: fzf = Pyfzf() fzf.append_fzf("StorageClass\n") fzf.append_fzf("ACL\n") fzf.append_fzf("Encryption\n") fzf.append_fzf("Metadata\n") fzf.append_fzf("Tagging\n") attributes = list( fzf.execute_fzf( print_col=1, multi_select=True, empty_allow=upload, header="select attributes to configure", )) for attribute in attributes: if attribute == "StorageClass": storage = True elif attribute == "ACL": acl = True elif attribute == "Metadata": metadata = True elif attribute == "Encryption": encryption = True elif attribute == "Tagging": tags = True old_storage_class: str = "" old_encryption: str = "" old_metadata: str = "" # only show previous values if one object is selected if (not upload and not version and len(self.s3.path_list) == 1 and not self.s3.path_list[0].endswith("/") and self.s3.path_list[0] != ""): s3_obj = self.s3.resource.Object(self.s3.bucket_name, self.s3.path_list[0]) old_storage_class = (s3_obj.storage_class if s3_obj.storage_class else "STANDARD") old_encryption = (s3_obj.server_side_encryption if s3_obj.server_side_encryption else "None") if s3_obj.metadata: old_metadata_list: list = [] for key, value in s3_obj.metadata.items(): old_metadata_list.append("%s=%s" % (key, value)) old_metadata = "&".join(old_metadata_list) if storage: self.set_storageclass(original=old_storage_class) if acl: display_original = (True if not upload and len(self.s3.path_list) == 1 else False) self.set_ACL(original=display_original, version=version) if encryption: self.set_encryption(original=old_encryption) if metadata: self.set_metadata(original=old_metadata) if tags: display_original = (True if not upload and len(self.s3.path_list) == 1 else False) self.set_tags(original=display_original, version=version)
def set_extra_args( self, tags: bool = False, rollback: bool = False, permissions: bool = False, stack_policy: bool = False, creation_option: bool = False, notification: bool = False, update: bool = False, search_from_root: bool = False, dryrun: bool = False, ) -> None: """Set extra arguments. Used to determine what args to set and acts like a router :param tags: configure tags for the stack :type tags: bool, optional :param rollback: set rollback configuration for the stack :type rollback: bool, optional :param iam: use a specific iam role for this operation :type iam: bool, optional :param stack_policy: add stack_policy to the stack :type stack_policy: bool, optional :param creation_policy: configure creation_policy (termination protection, rollback on failure) :type creation_policy: bool, optional :param notification: set sns topic to publish :type notification: bool, optional :param update: determine if is creating stack or updating stack :type update: bool, optional :param search_from_root: search from root for stack_policy :type search_from_root: bool, optional :param dryrun: if true, changeset operation and don't add CreationOption or UpdateOption entry :type dryrun: bool, optional """ attributes: List[str] = [] if (not tags and not rollback and not permissions and not stack_policy and not creation_option and not notification): fzf = Pyfzf() fzf.append_fzf("Tags\n") fzf.append_fzf("Permissions\n") if not dryrun: fzf.append_fzf("StackPolicy\n") fzf.append_fzf("Notifications\n") fzf.append_fzf("RollbackConfiguration\n") if not dryrun and not update: fzf.append_fzf("CreationOption\n") attributes = list( fzf.execute_fzf( empty_allow=True, print_col=1, multi_select=True, header="select options to configure", )) for attribute in attributes: if attribute == "Tags": tags = True elif attribute == "Permissions": permissions = True elif attribute == "StackPolicy": stack_policy = True elif attribute == "Notifications": notification = True elif attribute == "RollbackConfiguration": rollback = True elif attribute == "CreationOption": creation_option = True if tags: self._set_tags(update) if permissions: self._set_permissions(update) if stack_policy: self._set_policy(update, search_from_root) if notification: self._set_notification(update) if rollback: self._set_rollback(update) if creation_option: self._set_creation()
class TestPyfzf(unittest.TestCase): def setUp(self): fileloader = FileLoader() config_path = Path(__file__).resolve().parent.joinpath( "../data/fzfaws.yml") fileloader.load_config_file(config_path=str(config_path)) self.capturedOutput = io.StringIO() sys.stdout = self.capturedOutput self.fzf = Pyfzf() def tearDown(self): sys.stdout = sys.__stdout__ @patch("fzfaws.utils.pyfzf.sys") def test_constructor(self, mocked_sys): self.assertRegex( self.fzf.fzf_path, r".*/fzfaws.*/libs/fzf-[0-9]\.[0-9]+\.[0-9]-(linux|darwin)_(386|amd64)", ) self.assertEqual("", self.fzf.fzf_string) mocked_sys.maxsize = 4294967295 mocked_sys.platform = "linux" fzf = Pyfzf() self.assertRegex( fzf.fzf_path, r".*/fzfaws.*/libs/fzf-[0-9]\.[0-9]+\.[0-9]-linux_386") mocked_sys.maxsize = 42949672951 mocked_sys.platform = "darwin" fzf = Pyfzf() self.assertRegex( fzf.fzf_path, r".*/fzfaws.*/libs/fzf-[0-9]\.[0-9]+\.[0-9]-darwin_amd64") mocked_sys.maxsize = 42949672951 mocked_sys.platform = "windows" mocked_sys.exit.side_effect = sys.exit self.assertRaises(SystemExit, Pyfzf) self.assertEqual( self.capturedOutput.getvalue(), "fzfaws currently is only compatible with python3.6+ on MacOS or Linux\n", ) def test_append_fzf(self): self.fzf.fzf_string = "" self.fzf.append_fzf("hello\n") self.fzf.append_fzf("world\n") self.assertEqual("hello\nworld\n", self.fzf.fzf_string) def test_construct_fzf_command(self): cmd_list = self.fzf._construct_fzf_cmd() self.assertEqual( cmd_list[1:], [ "--ansi", "--expect=ctrl-c", "--color=dark", "--color=fg:-1,bg:-1,hl:#c678dd,fg+:#ffffff,bg+:-1,hl+:#c678dd", "--color=info:#98c379,prompt:#61afef,pointer:#e06c75,marker:#e5c07b,spinner:#61afef,header:#61afef", "--height", "100%", "--layout=reverse", "--border", "--cycle", "--bind=alt-a:toggle-all,alt-j:jump,alt-0:top,alt-s:toggle-sort", ], ) @patch.object(subprocess, "Popen") @patch.object(subprocess, "check_output") def test_execute_fzf(self, mocked_output, mocked_popen): mocked_output.return_value = b"hello" result = self.fzf.execute_fzf(print_col=1) self.assertEqual(result, "hello") mocked_output.assert_called_once() mocked_output.return_value = b"" self.assertRaises(NoSelectionMade, self.fzf.execute_fzf) mocked_output.return_value = b"" result = self.fzf.execute_fzf(empty_allow=True) self.assertEqual("", result) mocked_output.return_value = b"hello" result = self.fzf.execute_fzf(multi_select=True, print_col=1) self.assertEqual(result, ["hello"]) mocked_output.return_value = b"hello\nworld" result = self.fzf.execute_fzf(multi_select=True, print_col=1, preview="hello", header="foo boo") self.assertEqual(result, ["hello", "world"]) mocked_output.return_value = b"hello world\nfoo boo" result = self.fzf.execute_fzf(multi_select=True, print_col=0) self.assertEqual(result, ["hello world", "foo boo"]) @patch.object(subprocess, "Popen") @patch.object(subprocess, "check_output") def test_check_ctrl_c(self, mocked_output, mocked_popen): mocked_output.return_value = b"ctrl-c" self.assertRaises(KeyboardInterrupt, self.fzf.execute_fzf) mocked_output.return_value = b"hello world" try: result = self.fzf.execute_fzf() self.assertEqual(result, "world") except: self.fail("ctrl-c test failed, unexpected exception raise") @patch("fzfaws.utils.Pyfzf._check_fd") @patch.object(subprocess, "Popen") @patch.object(subprocess, "check_output") def test_get_local_file(self, mocked_output, mocked_popen, mocked_check): mocked_check.return_value = False mocked_output.return_value = b"" self.assertRaises(NoSelectionMade, self.fzf.get_local_file) mocked_popen.assert_called_with("find * -type f", shell=True, stderr=ANY, stdout=ANY) mocked_output.return_value = b"hello" result = self.fzf.get_local_file() self.assertEqual("hello", result) mocked_output.return_value = b"hello" result = self.fzf.get_local_file(multi_select=True) self.assertEqual(result, ["hello"]) mocked_output.return_value = b"hello\nworld\n" result = self.fzf.get_local_file(multi_select=True) self.assertEqual(result, ["hello", "world"]) result = self.fzf.get_local_file(directory=True, search_from_root=True) mocked_popen.assert_called_with( "echo \033[33m./\033[0m; find * -type d", shell=True, stderr=ANY, stdout=ANY) result = self.fzf.get_local_file(cloudformation=True) mocked_popen.assert_called_with( 'find * -type f -name "*.json" -o -name "*.yaml" -o -name "*.yml"', shell=True, stderr=ANY, stdout=ANY, ) mocked_output.reset_mock() mocked_check.return_value = True result = self.fzf.get_local_file(cloudformation=True, header="hello") mocked_popen.assert_called_with( "fd --type f --regex '(yaml|yml|json)$'", shell=True, stderr=ANY, stdout=ANY, ) mocked_output.assert_called_once() result = self.fzf.get_local_file(directory=True) mocked_popen.assert_called_with( "echo \033[33m./\033[0m; fd --type d", shell=True, stderr=ANY, stdout=ANY, ) result = self.fzf.get_local_file() mocked_popen.assert_called_with( "fd --type f", shell=True, stderr=ANY, stdout=ANY, ) @patch("fzfaws.utils.pyfzf.subprocess") def test_check_fd(self, mocked_subprocess): mocked_subprocess.run.return_value = True result = self.fzf._check_fd() self.assertEqual(result, True) mocked_subprocess.run.side_effect = Exception( subprocess.CalledProcessError) result = self.fzf._check_fd() self.assertEqual(result, False) def test_process_list(self): self.fzf.fzf_string = "" self.assertRaises(EmptyList, self.fzf.process_list, [], "123") self.fzf.fzf_string = "" self.fzf.process_list([], "123", "asfasd", "bbbb", empty_allow=True) test_list = [{"foo": 1, "boo": 2}, {"foo": "b"}] self.fzf.process_list(test_list, "foo") self.assertEqual(self.fzf.fzf_string, "foo: 1\nfoo: b\n") self.fzf.fzf_string = "" self.fzf.process_list(test_list, "boo") self.assertEqual(self.fzf.fzf_string, "boo: 2\nboo: None\n") self.fzf.fzf_string = "" self.fzf.process_list(test_list, "www") self.assertEqual(self.fzf.fzf_string, "www: None\nwww: None\n") self.fzf.fzf_string = "" self.fzf.process_list(test_list, "foo", "boo") self.assertEqual(self.fzf.fzf_string, "foo: 1 | boo: 2\nfoo: b | boo: None\n") @patch.object(Pyfzf, "execute_fzf") def test_format_selected_to_dict(self, mocked_execute): mocked_execute.return_value = "foo: 1 | boo: 2 | wtf: None" result = str(self.fzf.execute_fzf(print_col=0)) result = self.fzf.format_selected_to_dict(result) self.assertEqual(result, {"boo": "2", "foo": "1", "wtf": None})
def changeset_stack( profile: Union[str, bool] = False, region: Union[str, bool] = False, replace: bool = False, local_path: Union[str, bool] = False, root: bool = False, wait: bool = False, info: bool = False, execute: bool = False, delete: bool = False, extra: bool = False, bucket: str = None, version: Union[str, bool] = False, ) -> None: """Handle changeset actions. Main function to interacte with changeset, use argument to control the actions. This function is using update_stack to handle all the dirty works as both functions are processing cloudformation arguments and having the same arguments. :param profile: use a different profile for this operation :type profile: Union[bool, str], optional :param region: use a different region for this operation :type region: Union[bool, str], optional :param replace: replace the template during update :type replace: bool, optional :param local_path: Select a template from local machine :type local_path: Union[bool, str], optional :param root: Search local file from root directory :type root: bool, optional :param wait: wait for stack to be completed before exiting the program :type wait: bool, optional :param info: display result of a changeset :type info: bool, optional :param execute: execute changeset :type execute: bool, optional :param delete: delete changeset :type delete: bool, optional :param extra: configure extra options for the stack, (tags, IAM, termination protection etc..) :type extra: bool, optional :param bucket: specify a bucket/bucketpath to skip s3 selection :type bucket: str, optional :param version: use previous version of template in s3 bucket :type version: Union[bool, str], optional :raises NoNameEntered: If no changset name is entered """ cloudformation = Cloudformation(profile, region) cloudformation.set_stack() # if not creating new changeset if info or execute or delete: fzf = Pyfzf() response: Dict[str, Any] = cloudformation.client.list_change_sets( StackName=cloudformation.stack_name) # get the changeset name fzf.process_list( response.get("Summaries", []), "ChangeSetName", "StackName", "ExecutionStatus", "Status", "Description", ) if info: selected_changeset = str(fzf.execute_fzf()) describe_changes(cloudformation, selected_changeset) # execute the change set elif execute: selected_changeset = fzf.execute_fzf() if get_confirmation("Execute changeset %s?" % selected_changeset): response = cloudformation.client.execute_change_set( ChangeSetName=selected_changeset, StackName=cloudformation.stack_name, ) cloudformation.wait("stack_update_complete", "Wating for stack to be updated ...") print("Stack updated") elif delete: selected_changeset = fzf.execute_fzf(multi_select=True) for changeset in selected_changeset: print("(dryrun) Delete changeset %s" % changeset) if get_confirmation("Confirm?"): for changeset in selected_changeset: cloudformation.client.delete_change_set( ChangeSetName=changeset, StackName=cloudformation.stack_name) else: changeset_name = input("Enter name of this changeset: ") if not changeset_name: raise NoNameEntered("No changeset name specified") changeset_description = input("Description: ") # since is almost same operation as update stack # let update_stack handle it, but return update details instead of execute cloudformation_args = update_stack( cloudformation.profile, cloudformation.region, replace, local_path, root, wait, extra, bucket, version, dryrun=True, cloudformation=cloudformation, ) cloudformation_args[ "cloudformation_action"] = cloudformation.client.create_change_set cloudformation_args["ChangeSetName"] = changeset_name if changeset_description: cloudformation_args["Description"] = changeset_description response = cloudformation.execute_with_capabilities( **cloudformation_args) response.pop("ResponseMetadata", None) print(json.dumps(response, indent=4, default=str)) print(80 * "-") print("Changeset create initiated") if wait: cloudformation.wait( "change_set_create_complete", "Wating for changset to be created ...", ChangeSetName=changeset_name, ) print("Changeset created") describe_changes(cloudformation, changeset_name)
def set_s3_path(self, download: bool = False) -> None: """Set 'path' of s3 to upload or download. s3 folders are not actually folder, found this path listing on https://github.com/boto/boto3/issues/134#issuecomment-116766812 This method would set the 'path' for s3 however the self.path_list cannot be used as the destination of upload immediately. This only set the path without handling different upload sceanario. Please use the get_s3_destination_key after set_s3_path to obtain the correct destination key :param download: if not download, add append option :type download: bool, optional :raises NoSelectionMade: when user did not make a bucket selection, exit """ selected_option = self._get_path_option(download=download) if selected_option == "input": self.path_list[0] = input("Input the path(newname or newpath/): ") elif selected_option == "root": # print("S3 file path is set to root") pass elif selected_option == "append" or selected_option == "interactively": paginator = self.client.get_paginator("list_objects") fzf = Pyfzf() parents = [] # interactively search down 'folders' in s3 while True: if len(parents) > 0: fzf.append_fzf("\033[34m../\033[0m\n") fzf.append_fzf("\033[33m./\033[0m\n") with Spinner.spin(message="Fetching s3 objects ..."): preview: str = "" for result in paginator.paginate( Bucket=self.bucket_name, Prefix=self.path_list[0], Delimiter="/", ): for prefix in result.get("CommonPrefixes", []): fzf.append_fzf("%s\n" % prefix.get("Prefix")) for content in result.get("Contents", []): preview += content.get("Key") preview += "^" # has to use tr to transform the string to new line during preview by fzf # not sure why, but if directly use \n, fzf preview interpret as a new command # TODO: findout why selected_path = str( fzf.execute_fzf( print_col=0, header= 'PWD: s3://%s/%s (select "./" will the current path)' % (self.bucket_name, self.path_list[0]), preview="echo %s | tr '^' '\n'" % preview.rstrip(), )) if not selected_path: raise NoSelectionMade if selected_path == "../": self.path_list[0] = parents.pop() elif selected_path == "./": break else: parents.append(self.path_list[0]) self.path_list[0] = selected_path # reset fzf string fzf.fzf_string = "" if selected_option == "append": print("Current PWD is s3://%s/%s" % (self.bucket_name, self.path_list[0])) new_path = input( "Input the new path to append(newname or newpath/): ") self.path_list[0] += new_path print("S3 file path is set to %s" % (self.path_list[0] if self.path_list[0] else "root"))