def _accounts(self): def get_session_for_account(cross_account_role, aws_account): # get a token for the cross account role and use it to create a session try: session_name = "{}-scheduler-{}".format( self._service.service_name, aws_account) # assume a role token = self._sts.assume_role_with_retries( RoleArn=cross_account_role, RoleSessionName=session_name) credentials = token["Credentials"] # create a session using the assumed role credentials return boto3.Session( aws_access_key_id=credentials["AccessKeyId"], aws_secret_access_key=credentials["SecretAccessKey"], aws_session_token=credentials["SessionToken"]) except Exception as ex: self._logger.error( ERR_ASSUMING_ROLE.format(cross_account_role, aws_account, str(ex))) return None # keep track of accounts processed accounts_done = [] # return session for lambda account if processing instances in that account if self._configuration.schedule_lambda_account: accounts_done.append(self._lambda_account) yield as_namedtuple( "Account", { "session": boto3.Session(), "name": self._lambda_account, "role": None }) # iterate through cross account roles for role in self._configuration.cross_account_roles: # get the account role_elements = role.split(":") if len(role_elements) < 5: self._logger.error(ERR_INVALID_ARN, role) continue # test if account already processed account = role_elements[4] if account in accounts_done: self._logger.warning(WARN_DUPLICATE_ACCOUNT, account, role) continue # get a session for the role session = get_session_for_account(role, account) if session is not None: yield as_namedtuple("Account", { "session": session, "name": account, "role": role })
def test_get_desired_state_and_type_2(mocker): instance = {} schedule = InstanceSchedule(name='test-1', periods={}, timezone='UTC', override_status=None, description=None, use_metrics=None, stop_new_instances=None, schedule_dt=None, use_maintenance_window=True, ssm_maintenance_window=True, enforced=False, hibernate=False, retain_running=False) instance['maintenance_window'] = None instance["account"] = 'test' instance["region"] = 'us-east-1' instance["service"] = 'ec2' instance["id"] = 'ut12y21232u' inst = as_namedtuple('ec2' + "Instance", instance, excludes=["tags"]) ec2_service = Ec2Service() scheduler_configuration = {} scheduler = InstanceScheduler(ec2_service, scheduler_configuration) mocker.patch.object(scheduler, '_logger') inst_state, inst_type = scheduler.get_desired_state_and_type( schedule, inst) assert inst_state == 'stopped'
def _scheduled_instances_in_region(self, account, region): # use service strategy to get a list of instances that can be scheduled for that service for instance in self._service.get_schedulable_instances({ schedulers.PARAM_SESSION: account.session, schedulers.PARAM_ACCOUNT: account.name, schedulers.PARAM_ROLE: account.role, schedulers.PARAM_REGION: region, schedulers.PARAM_CONFIG: self._configuration, schedulers.PARAM_LOGGER: self._logger, schedulers.PARAM_CONTEXT: self._context }): instance["account"] = account.name instance["region"] = region instance["service"] = self._service.service_name instance["instance_str"] = self._instance_display_str( instance["id"], instance["name"]) inst = as_namedtuple(self._service.service_name + "Instance", instance, excludes=["tags"]) yield inst
def _transform_returned_resource(self, client, resource, resource_name, tags, tags_as_dict, use_tuple, **kwargs): name = "" data = "" if resource_name == OBJECT_VERSIONS: if "Versions" in resource: name = "Versions" data = resource["Versions"] elif "DeleteMarkers" in resource: name = "DeleteMarkers" data = resource["DeleteMarkers"] else: name = resource_name data = resource if resource_name == OBJECTS: data["Bucket"] = kwargs.get("Bucket") if tags: resource["Tags"] = self._get_tags_for_resource(client, resource, resource_name) if "ResponseMetadata" in resource: resource.pop("ResponseMetadata") if tags_as_dict: self._convert_tags_to_dictionaries(data) if use_tuple: return as_namedtuple(name, data, deep=True, namefunc=self._tuple_name_func, exludes=self._tuple_excludes) else: return resource
def describe(self, as_tuple=None, **kwargs): """ This method is to retrieve a pseudo UTC time resource, method parameters are only used signature compatibility :param as_tuple: Set to true to return results as immutable named dictionaries instead of dictionaries :return: Pseudo time resource """ def use_tuple(): return (as_tuple is not None and as_tuple) or (as_tuple is None and self._as_tuple) region = kwargs.get("region") result = { "Time": datetime.datetime.now(pytz.timezone("UTC")), "AwsAccount": self.aws_account, "Region": region if region else boto3.Session().region_name } return [as_namedtuple("Time", result)] if use_tuple() else [result]
def _transform_returned_resource(self, client, resource, resource_name, tags, tags_as_dict, use_tuple, **kwargs): """ This method takes the resource from the boto "describe" method and transforms them into the requested output format of the service class describe function :param client: boto client for the service that can be used to retrieve additional attributes, eg tags :param resource: The resource returned from the boto call :param resource_name: The name of the resource type :param tags: Set true true if the tags must be retrieved for this resource :param tags_as_dict: Set to true to convert the tags into Python dictionaries :param use_tuple: Set to true to return the resources as un-mutable named tuples instead of dictionaries :param kwargs: Additional service specific arguments for the transformation :return: The transformed service resource """ # get tags for the resource if tags: if self._resources_with_tags is None: raise Exception("Service {} does not support tags".format( self.service_name)) if resource_name not in self._resources_with_tags: raise Exception( "Resource {} for service {} does not support tags".format( resource_name, self.service_name)) resource["Tags"] = self._get_tags_for_resource( client, resource, resource_name) # convert tags to dictionaries if tags_as_dict: resource = self._convert_tags_to_dictionaries(resource) # convert resource to named tuple if use_tuple: return as_namedtuple(resource_name, resource, deep=True, namefunc=self._tuple_name_func, exludes=self._tuple_excludes) else: return resource
def calculate_schedule_usage_for_period(self, schedule_name, start_dt, stop_dt=None, logger=None): result = {} def running_seconds(startdt, stopdt): return max(int((stopdt - startdt).total_seconds()), 60) def running_hours(startdt, stopdt): return int(((stopdt - startdt).total_seconds() - 1) / 3600) + 1 def make_period(started_dt, stopped_dt): running_period = ({ "begin": started_dt, "end": stopped_dt, "billing_hours": running_hours(started_dt, stopped_dt), "billing_seconds": running_seconds(started_dt, stopped_dt) }) return running_period self._logger = logger stop = stop_dt or start_dt if start_dt > stop: raise ValueError(ERR_STOP_MUST_BE_LATER_OR_EQUAL_TO_START) dt = start_dt if isinstance(start_dt, datetime) else datetime( start_dt.year, start_dt.month, start_dt.day) config_data = ConfigDynamodbAdapter(self._table.name).config while dt <= stop: self._configuration = SchedulerConfigBuilder( logger=self._logger).build(config_data) conf = configuration.SchedulerConfigBuilder(self._logger).build( config=config_data, dt=dt) schedule = conf.get_schedule(schedule_name) timeline = {dt.replace(hour=0, minute=0)} for p in schedule.periods: begintime = p["period"].begintime endtime = p["period"].endtime if begintime is None and endtime is None: timeline.add(dt.replace(hour=0, minute=0)) timeline.add(dt.replace(hour=23, minute=59)) else: if begintime: timeline.add( dt.replace(hour=begintime.hour, minute=begintime.minute)) if endtime: timeline.add( dt.replace(hour=endtime.hour, minute=endtime.minute)) running_periods = {} started = None starting_period = None current_state = None inst = as_namedtuple("Instance", { "instance_str": "instance", "allow_resize": False }) for tm in sorted(list(timeline)): desired_state, instance_type, period = schedule.get_desired_state( inst, self._logger, tm, False) if current_state != desired_state: if desired_state == InstanceSchedule.STATE_RUNNING: started = tm current_state = InstanceSchedule.STATE_RUNNING starting_period = period elif desired_state == InstanceSchedule.STATE_STOPPED: stopped = tm desired_state_with_adj_check, _, __ = schedule.get_desired_state( inst, self._logger, tm, True) if desired_state_with_adj_check == InstanceSchedule.STATE_RUNNING: stopped += timedelta(minutes=1) if current_state == InstanceSchedule.STATE_RUNNING: current_state = InstanceSchedule.STATE_STOPPED running_periods[starting_period] = (make_period( started, stopped)) if current_state == InstanceSchedule.STATE_RUNNING: stopped = dt.replace(hour=23, minute=59) + timedelta(minutes=1) running_periods[starting_period] = (make_period( started, stopped)) result[str(dt.date())] = { "running_periods": running_periods, "billing_seconds": sum([ running_periods[ps]["billing_seconds"] for ps in running_periods ]), "billing_hours": sum([ running_periods[ph]["billing_hours"] for ph in running_periods ]) } dt += timedelta(days=1) return {"schedule": schedule_name, "usage": result}
def get_usage(self, start_dt, stop_dt=None, instance=None, logger=None): """ Get running periods for a schedule in a period :param instance: instance id :param start_dt: start date of the period, None is today :param stop_dt: end date of the period, None is today :param logger: logger for output of scheduling logic :return: dictionary containing the periods in the specified in which instances are running as well as the % saving in running hours """ result = {} def running_seconds(startdt, stopdt): return max(int((stopdt - startdt).total_seconds()), 60) def running_hours(startdt, stopdt): return int(((stopdt - startdt).total_seconds() - 1) / 3600) + 1 def make_period(started_dt, stopped_dt, inst_type): running_period = ({ "begin": started_dt, "end": stopped_dt, "billing_hours": running_hours(started_dt, stopped_dt), "billing_seconds": running_seconds(started_dt, stopped_dt) }) if inst_type is not None: running_period["instancetype"] = inst_type return running_period self._logger = logger stop = stop_dt or start_dt if start_dt > stop: raise ValueError(ERR_STOP_MUST_BE_LATER_OR_EQUAL_TO_START) dt = start_dt if isinstance(start_dt, datetime) else datetime( start_dt.year, start_dt.month, start_dt.day) while dt <= stop: timeline = {dt.replace(hour=0, minute=0)} for p in self.periods: begintime = p["period"].begintime endtime = p["period"].endtime if begintime is None and endtime is None: timeline.add(dt.replace(hour=0, minute=0)) timeline.add(dt.replace(hour=23, minute=59)) else: if begintime: timeline.add( dt.replace(hour=begintime.hour, minute=begintime.minute)) if endtime: timeline.add( dt.replace(hour=endtime.hour, minute=endtime.minute)) running_periods = {} started = None starting_period = None current_state = None instance_type = None inst = instance or as_namedtuple("Instance", { "instance_str": "instance", "allow_resize": False }) for tm in sorted(list(timeline)): desired_state, instance_type, period = self.get_desired_state( inst, self._logger, tm) if current_state != desired_state: if desired_state == InstanceSchedule.STATE_RUNNING: started = tm current_state = InstanceSchedule.STATE_RUNNING starting_period = period elif desired_state == InstanceSchedule.STATE_STOPPED: stopped = tm if current_state == InstanceSchedule.STATE_RUNNING: current_state = InstanceSchedule.STATE_STOPPED running_periods[starting_period] = (make_period( started, stopped, instance_type)) if current_state == InstanceSchedule.STATE_RUNNING: stopped = dt.replace(hour=23, minute=59) + timedelta(minutes=1) running_periods[starting_period] = (make_period( started, stopped, instance_type)) result[str(dt.date())] = { "running_periods": running_periods, "billing_seconds": sum([ running_periods[ps]["billing_seconds"] for ps in running_periods ]), "billing_hours": sum([ running_periods[ph]["billing_hours"] for ph in running_periods ]) } dt += timedelta(days=1) return {"schedule": self.name, "usage": result}