#!/usr/bin/env python
# -*- coding: utf-8 -*-

import logging
import time
import uuid
import requests

import saas.tools.ssm.modules.nanny_vault_api as nanny_vault_api
import saas.tools.ssm.modules.nanny_yp_api as nanny_yp_api
import saas.tools.ssm.modules.sandbox_api as sandbox_api
import saas.tools.ssm.modules.yaml_config as yaml_config

from saas.library.python.abc import AbcAPI
from saas.library.python.nanny_rest.service_mutable_proxy.environment_variable import VaultSecretEnv, LiteralEnv
from saas.library.python.token_store import PersistentTokenStore
from saas.library.python.nanny_rest import NannyServiceBase
from saas.library.python.nanny_rest.resource import SandboxFile, StaticFile
from saas.library.python.nanny_rest.service_mutable_proxy.data_volume import VaultSecretVolume
from saas.library.python.nanny_rest.tickets_integration import (
    AutocommitSettings,
    ReleaseType,
    TicketsIntegrationRule
)
from saas.library.python.saas_ctype import SaasCtype
from saas.tools.devops.lib23.nanny_helpers import NannyService


def request(func):
    """
    Handle requests information.
    """

    def decorator(*args, **kwargs):

        def check_request(resp):
            message = 'Status code: %d | URL: %s | Data: %s' % (resp.status_code, resp.url, str(data))
            if resp.ok:
                logging.debug('Success request | %s', message)
                return True
            else:
                logging.error('Failed request | %s | Reason: %s | Response data: %s', message, resp.reason, resp.text)
                return False

        max_retry_cnt = 10
        cur_retry_cnt = 0
        retry_codes_list = [500, 502, 503, 504]
        if len(args) > 1:
            data = args[1]
        else:
            data = {}
        resp = func(*args, **kwargs)
        if not check_request(resp) and resp.status_code in retry_codes_list:
            while cur_retry_cnt < max_retry_cnt:
                time.sleep(cur_retry_cnt)
                resp = func(*args, **kwargs)
                if check_request(resp):
                    break
                cur_retry_cnt += 1
            else:
                raise requests.exceptions.RetryError('Failed request. Max attempts has reached.')
        return resp

    return decorator


class NannyServicesAPI(object):
    """
    Main class for Nanny services API requests.
    API_URL: nanny.yandex-team.ru
    API WIKI: https://wiki.yandex-team.ru/jandekspoisk/sepe/nanny/service-repo-api/
    API request types: GET & POST, PUT with json data
    API answers: json data
    """
    API_URL = 'http://nanny.yandex-team.ru'

    def __init__(self, service_name, debug=False, config='conf/ssm.conf'):
        # Basic settings
        self.CONFIGURATION = yaml_config.read_configuration(config) if config else None
        self._API = self.CONFIGURATION.get('nanny_settings').get('api') if self.CONFIGURATION else self.API_URL

        # Auth settings
        self._OAUTH_TOKEN = PersistentTokenStore.get_token_from_store_env_or_file('nanny')
        self._HEADERS = {
            'Content-Type': 'application/json',
            'Authorization': 'OAuth %s' % self._OAUTH_TOKEN
        }

        # Nanny vault client
        self.nanny_vault = nanny_vault_api.NannyVault(self._OAUTH_TOKEN)
        # Nanny yp client
        self.nanny_yp = nanny_yp_api.SaaSNannyYpWorkflow(service_name, token=self._OAUTH_TOKEN)
        # Nanny replication policy client
        self.nanny_replication = nanny_yp_api.NannyReplicationPolicy(service_name, token=self._OAUTH_TOKEN)
        # Service settings.
        self._service_name = service_name
        self._service_url = '%s/v2/services/%s' % (self._API, service_name)
        # Connection settings.
        self._connection = requests.session()
        self._connection.headers = self._HEADERS
        # Logging settings.
        if debug:
            self._logging_level = logging.DEBUG
        else:
            self._logging_level = logging.INFO
        logging.basicConfig(level=self._logging_level,
                            format='%(asctime)s | %(levelname)-6s | %(message)s',
                            datefmt='%H:%M:%S')

    def _get_nanny_rest_service(self):  # TODO: use nanny_rest library everywhere, create just one instance for ssm
        return NannyServiceBase(self._service_name)

    @request
    def _create_service(self, data):
        request_url = self._service_url.split(self._service_name)[0]
        return self._connection.post(request_url, json=data)

    @request
    def _remove_service(self):
        return self._connection.delete(self._service_url)

    @request
    def _get_runtime_attrs(self):
        request_url = '%s/runtime_attrs/' % self._service_url
        return self._connection.get(request_url)

    @request
    def _set_runtime_attrs(self, data):
        request_url = '%s/runtime_attrs/' % self._service_url
        return self._connection.put(request_url, json=data)

    @request
    def _get_auth_attrs(self):
        request_url = '%s/auth_attrs/' % self._service_url
        return self._connection.get(request_url)

    @request
    def _set_auth_attrs(self, data):
        request_url = '%s/auth_attrs/' % self._service_url
        return self._connection.put(request_url, json=data)

    @request
    def _get_info_attrs(self):
        request_url = '%s/info_attrs/' % self._service_url
        return self._connection.get(request_url)

    @request
    def _set_info_attrs(self, data):
        request_url = '%s/info_attrs/' % self._service_url
        return self._connection.put(request_url, json=data)

    @request
    def _get_allocations(self):
        request_url = '%s/allocations/' % self._service_url
        return self._connection.get(request_url)

    @request
    def _add_allocation(self, data):
        request_url = '%s/allocations/' % self._service_url
        self._connection.headers['X-Req-Id'] = str(uuid.uuid4())
        return self._connection.post(request_url, json=data)

    @request
    def _delete_allocation(self, allocation_id):
        request_url = '%s/allocations/%s' % (self._service_url, allocation_id)
        resp = self._connection.delete(request_url)
        if resp.status_code == 204:
            logging.info('Allocation %s deleted', allocation_id)
        return resp

    @request
    def _copy_service(self, src_service, data):
        request_url = '%s/v2/services/%s/copies/' % (self._API, src_service)
        return self._connection.post(request_url, json=data)

    @request
    def _get_service_events(self):
        request_url = '%s/events/' % self._service_url
        return self._connection.get(request_url)

    @request
    def _set_service_events(self, data):
        request_url = '%s/events/' % self._service_url
        return self._connection.post(request_url, json=data)

    @request
    def _get_current_state(self):
        request_url = '%s/current_state/' % self._service_url
        return self._connection.get(request_url)

    def remove_service_snapshots(self):
        nanny_state = self._get_current_state().json()
        logging.info('Preparing to remove service %s', self._service_name)
        for snapshot in nanny_state['content']['active_snapshots']:
            logging.info('Removing snapshot %s', snapshot['snapshot_id'])
            event = {
                'content': {
                    'snapshot_id': snapshot['snapshot_id'],
                    'state': 'DESTROYED',
                    'comment': 'Destroying all service snapshots'
                },
                'type': 'SET_SNAPSHOT_STATE',
            }
            self._set_service_events(event)
        logging.info('Waiting snapshots complete removing')
        while self._get_current_state().json()['content']['active_snapshots']:
            time.sleep(5)

    def remove_service(self):
        logging.info('Attention. Nanny service must be offline before deleting')
        self.remove_service_snapshots()
        logging.info('Removing service')
        # Check for yp pods
        list_pods = self.nanny_yp.get_list_pods()
        if list_pods:
            logging.info('Removing yp pods')
            for pods in list_pods:
                self.nanny_yp.remove_pods(pods['cluster'])
        self._remove_service()
        logging.info('Service %s completely removed', self._service_name)


class NannyServices(NannyServicesAPI):
    """
    Class for actions with services  that does not required service data.
    """

    CURRENT_STATE = 'UNKNOWN'

    def __init__(self, service_name, config='conf/ssm.conf', debug=False):
        super(NannyServices, self).__init__(service_name, config=config, debug=debug)
        # Nanny basic settings
        self._service_name = service_name

        # Sandbox API client
        self._sandbox = sandbox_api.SandboxAPI()

        if self.check_service_exists():
            self._ns = NannyService(self._service_name)
        else:
            self._ns = None

    def _compose_secret_data(self, secret_data):
        """
        Compose valid secret data for container.
        :param secret_data: type dict
        :return: type dict
        """
        result = {
            'name': secret_data['variable_name'],
            'valueFrom': {
                'type': 'SECRET_ENV',
                'secretEnv': {
                    'keychainSecret': {
                        'keychainId': secret_data['keychain'],
                        'secretId': secret_data['secret'],
                        'secretRevisionId': self.nanny_vault.get_secret_revisions(secret_data['keychain'],
                                                                                  secret_data['secret'])[-1]
                    }
                }
            }
        }
        return result

    def add_container_section(self, section_name):
        """
        Add container section for instance_spec settings.
        :param section_name: type str
        :return: type requests.model.Response
        """
        service_attrs = self._get_runtime_attrs().json()
        content = service_attrs.get('content')
        for container in content['instance_spec']['containers']:
            if section_name == container['name']:
                break
        else:
            content['instance_spec']['containers'].append(
                {
                    'name': section_name,
                    'env': [],
                }
            )
        request_data = {
            'snapshot_id': service_attrs['_id'],
            'content': content,
            'comment': 'Added section %s for instance_spec' % (section_name)
        }
        return self._set_runtime_attrs(request_data)

    def add_env_variable(self, section, varname, value):
        """
        Add literal variable to instance_spec of specified section.
        :param section: type str
        :param varname: type str
        :param value: type str
        :return: type requests.model.Response
        """
        ns = NannyService(self._service_name)
        return ns.add_env_variable(section, varname, value)

    def add_resource_from_string(self, resource_name, resource_data):
        """
        Add resource as static file
        :param resource_name: type str
        :param resource_data: type str
        :return: type requests.model.Response
        """
        service_attrs = self._get_runtime_attrs().json()
        content = service_attrs.get('content')
        for sfile in content['resources']['static_files']:
            if resource_name == sfile['local_path']:
                sfile['content'] = resource_data
                break
        else:
            content['resources']['static_files'].append({'local_path': resource_name, 'content': resource_data})

        request_data = {
            'snapshot_id': service_attrs['_id'],
            'content': content,
            'comment': 'Added file %s to static_files' % resource_name
        }
        return self._set_runtime_attrs(request_data)

    def configure_yp_instances(self, pods_list='', tags=None):
        # Check list of pods
        if not pods_list:
            pods_list = self.nanny_yp.get_list_pods()
        if not tags:
            tags = {}

        service_attrs = self._get_runtime_attrs().json()
        content = service_attrs.get('content')
        content['engines']['engine_type'] = 'YP_LITE'
        if content['instances']['chosen_type'] not in 'YP_POD_IDS':
            content['instances']['chosen_type'] = 'YP_POD_IDS'
        if content['instances'].get('yp_pod_ids') is None:
            content['instances']['yp_pod_ids'] = {
                'orthogonal_tags': {},
                'pods': [],
            }
        content['instances']['yp_pod_ids']['pods'] = pods_list
        content['instances']['yp_pod_ids']['orthogonal_tags'] = {
            'metaprj': tags.get('metaprj', 'unknown'),
            'itype': tags.get('itype', 'unknown'),
            'ctype': tags.get('ctype', 'unknown'),
            'prj': tags.get('prj', self._service_name.replace('_', '-'))
        }
        # Fix yasm with yp
        for container in content['instance_spec']['containers']:
            container['unistatEndpoints'] = [{'path': '/tass', 'port': '80'}]

        # iss hooks settings
        content['instances']['iss_settings'] = {
            'hooks_resource_limits': {
                'iss_hook_start_cpu_weight': 10
            }
        }

        request_data = {
            'snapshot_id': service_attrs['_id'],
            'content': content,
            'comment': 'Configure instances with YP pods.'
        }
        return self._set_runtime_attrs(request_data)

    def change_engine(self, engine='ISS_MULTI'):
        """
        Change engine. I dont know why it here now.
        :return: type requests.model.Response
        """
        service_attrs = self._get_runtime_attrs().json()
        content = service_attrs.get('content')
        content['engines']['engine_type'] = engine

        request_data = {
            'snapshot_id': service_attrs['_id'],
            'content': content,
            'comment': 'Change engine to %s' % engine
        }
        return self._set_runtime_attrs(request_data)

    def check_service_exists(self):
        """
        Check for existing service.
        :return: type boolean
        """
        resp = self._get_runtime_attrs()
        if resp.status_code == 404:
            return False
        else:
            return True

    def get_backend_type(self):
        """
        Get backends type of service
        :return: type str
        """
        service_attrs = self._get_runtime_attrs().json()
        content = service_attrs.get('content')
        if content['instances']['chosen_type'] == 'YP_POD_IDS':
            return 'yp.lite'
        return 'unknown'

    def get_last_resources(self, resources_list, release_type='stable'):
        """
        Get last resources from Sandbox excluding ones with task_id and resource_id.
        :param resources_list: type list
        :param release_type: type str
        :return: type list
        """
        for resource in resources_list:
            if all([resource.get('task_id'), resource.get('resource_id')]):
                continue

            task_id, resource_id = self._sandbox.get_last_release(resource['task_type'],
                                                                  resource['resource_type'],
                                                                  release_type=release_type)
            if task_id and resource_id:
                logging.info('[%s: %s][%s: %s] Update sandbox resources', resource['task_type'], task_id,
                             resource['resource_type'], resource_id)
                resource['task_id'] = task_id
                resource['resource_id'] = resource_id
            else:
                raise ValueError(
                    'Unable to get task_id and resource_id for resource {resource}, '
                    'release_type={release_type}'.format(resource=resource, release_type=release_type)
                )

        return resources_list

    def get_service_state(self):
        """
        Check and return current state of last snapshot.
        :return: type str
        """
        resp = self._get_current_state()
        if resp.status_code == 200:
            self.CURRENT_STATE = resp.json()['content']['active_snapshots'][0]['state']
            return self.CURRENT_STATE
        elif resp.status_code == 404:
            raise ReferenceError('Service {} was not found'.format(self._service_name))
        else:
            return 'ERROR'

    def manage_manual_confirm(self, action):
        """
        Enables/disables manual_confirm recipe parameter.
        :param action: type boolean
        :return: type requests.model.Response
        """
        info_attrs = self._get_info_attrs().json()
        for recipe in info_attrs['content']['recipes']['content']:
            for context in recipe['context']:
                if 'manual_confirm' in context['key']:
                    context['value'] = str(action)
        request_data = {
            'snapshot_id': info_attrs['_id'],
            'content': info_attrs['content'],
            'comment': 'Manual confirm: ' + str(action)
        }
        return self._set_info_attrs(request_data)

    def manage_disk_quotas(self, enabled=True, work_dir=1100, root_fs=1000):
        """
        Enable|disable disk quotas for root dir and working dir.
        :param enabled: type boolean
        :param work_dir: type int
        :param root_fs: type int
        :return: type requests.model.Response
        """
        info_attrs = self._get_info_attrs().json()
        if not enabled:
            info_attrs['content']['disk_quotas'] = {
                'policy': 'DISABLED'
            }
        else:
            info_attrs['content']['disk_quotas'] = {
                'policy': 'ENABLED',
                'work_dir_quota': work_dir,
                'root_fs_quota': root_fs
            }
        request_data = {
            'snapshot_id': info_attrs['_id'],
            'content': info_attrs['content'],
            'comment': 'Disk quotas enabled: ' + str(enabled) + ' work_dir: ' + str(work_dir) + ' root_fs: ' + str(root_fs)
        }
        return self._set_info_attrs(request_data)

    def manage_degrade_level(self, degrade_level):
        """
        Enables/disables manual_confirm recipe parameter.
        :param degrade_level: type str
        :return: type requests.model.Response
        """
        info_attrs = self._get_info_attrs().json()
        for recipe in info_attrs['content']['recipes']['content']:
            for context in recipe['context']:
                if 'degrade_level' in context['key']:
                    context['value'] = str(degrade_level)
        request_data = {
            'snapshot_id': info_attrs['_id'],
            'content': info_attrs['content'],
            'comment': 'Sets degrade level: ' + str(degrade_level)
        }
        return self._set_info_attrs(request_data)

    def manage_balancer_autoupdate(self, enabled=True):
        """
        Enable/disable service balancer autoapdate
        :param enabled: type bool
        :return: type requests.model.Response
        """
        info_attrs = self._get_info_attrs().json()
        info_attrs['content']['balancers_integration']['auto_update_services_balancers'] = enabled
        request_data = {
            'snapshot_id': info_attrs['_id'],
            'content': info_attrs['content'],
            'comment': 'Change services balances autoupdate policy ENABLED: ' + str(enabled)
        }
        return self._set_info_attrs(request_data)

    def snapshot_activate(self, snapshot_id=None, recipe='common', activation_wait=False):
        """
        Activate snapshot for service. If snapshot_id not specified activate
        last snapshot.
        :param snapshot_id: type str
        :param recipe: type str
        :param activation_wait: type boolean
        """
        if not snapshot_id:
            snapshot_id = self._get_runtime_attrs().json()['_id']

        request_data = {
            'content': {
                'snapshot_id': snapshot_id,
                'state': 'ACTIVE',
                'comment': 'Activate %s snapshot' % snapshot_id,
                'recipe': recipe
            },
            'type': 'SET_SNAPSHOT_STATE'
        }
        self._set_service_events(request_data)

        if activation_wait:
            self.wait_for_activation()

    def update_abc_group(self, abc_group):
        """
        Change ABC group for service
        :param abc_group: type int
        :return: type requests.model.Response
        """
        info_attrs = self._get_info_attrs().json()
        info_attrs['content']['abc_group'] = abc_group
        request_data = {
            'snapshot_id': info_attrs['_id'],
            'content': info_attrs['content'],
            'comment': 'Update ABC group to ' + str(abc_group)
        }
        return self._set_info_attrs(request_data)

    def wait_for_activation(self):
        """
        Wait for snapshot activation.
        :return:
        """
        time.sleep(10)
        while self.get_service_state() not in 'ACTIVE':
            logging.info('Waiting ACTIVE nanny snapshot state, current state: %s. Sleep 60 '
                         'seconds.' % self.CURRENT_STATE)
            time.sleep(60)


class SaaSNannyService(NannyServices):
    """
    Class for SaaS specific requests.
    """

    def __init__(self, service_name, service_type='search', service_ctype='',
                 service_ctype_tag='', config='conf/ssm.conf', debug=False):
        super(SaaSNannyService, self).__init__(service_name, config=config, debug=debug)
        self.config_data = self.CONFIGURATION.get('nanny_settings') if self.CONFIGURATION else {}

        # Nanny basic settings
        self._service_name = service_name
        self._service_type = service_type
        self._service_ctype = service_ctype

        # Nanny path settings
        self._service_path = '{base}/{ctype}/'.format(base=self.config_data['service_path'], ctype=self._service_ctype)

        # ABC group for binding
        self._abc_group = self.config_data.get('abc_group')

        # Nanny deploy recipe settings
        self._deploy_settings = self.config_data.get('deploy_settings')

        # Nanny isolation layers settings
        self._isolation_settings = self.config_data.get('isolation_settings')

        # Nanny monitoring settings
        self._monitoring_settings = self.config_data.get('monitoring_settings')

        # Nanny resource settings
        self._resources = self.config_data.get('resources')

        # Nanny secrets settings
        self._secrets = self.config_data.get('secrets_settings')

        # Nanny secret volumes settings
        self._secret_volumes = self.config_data.get('secret_volumes')

        # Nanny ticket integration settings
        self._tickets_integration = self.config_data.get('tickets_integration')

        # Nanny auth settings
        self._auth_settings = self.config_data.get('auth_settings')

        # Nanny tags settings
        self._service_tags = self.config_data.get('service_tags', {})
        if 'prestable' not in service_ctype:
            self.rtyserver_tags = list(
                self._service_tags.get('production_service_tags', {}).get('base', []))
            if service_ctype and service_ctype_tag:
                self.rtyserver_tags.append(service_ctype_tag)
            elif self._service_type in 'search':
                self.rtyserver_tags.append(
                    self._service_tags.get('production_service_tags', {}).get('custom_search'))
            elif self._service_type in 'kv':
                self.rtyserver_tags.append(
                    self._service_tags.get('production_service_tags', {}).get('custom_kv'))
        else:
            self.rtyserver_tags = list(
                self._service_tags.get('prestable_service_tags', {}).get('base'))

        # Rtyserver binary
        if 'kv' in self._service_type:
            self._rtyserver_bin = 'RTYSERVER_LF'
        else:
            self._rtyserver_bin = 'RTYSERVER'

        # External services
        self._abc_api = AbcAPI()
        self._abc_group_id = 664

    def create_service(self):
        """
        Create new service with configuration based on service attrs.
        """
        service_data = {
            'id': self._service_name,
            'info_attrs': {
                'category': self._service_path,
                'desc': 'New service named {name}'.format(name=self._service_name)
            }
        }
        logging.info('Creating service %s', self._service_name)
        self._create_service(service_data)

        # Auth settings
        logging.info('Update auth settings')
        self.update_auth_settings()
        # ABC group
        logging.info('Update abc owner group')
        self.update_abc_group(self._abc_group)
        # Deploy settings
        logging.info('Prepare deploy recipe')
        self.update_deploy_recipe()
        # Monitoring
        logging.info('Prepare monitoring')
        self.update_monitoring()
        # Resources
        logging.info('Prepare service resources')
        self.update_service_resources()
        rtyserver_id, rtyserver_res = self._sandbox.get_last_release('BUILD_RTYSERVER', self._rtyserver_bin, limit=50)
        self.change_rtyserver_release(rtyserver_id, rtyserver_res)
        # Keychains and secrets
        logging.info('Configure secrets')
        self.configure_secrets()
        self.configure_secret_volumes()
        self.add_logbroker_token()
        self.add_tvm_env_variables()
        # Isolation
        logging.info('Prepare isolation')
        self.prepare_isolation(skynet_enable=True, ssh_enable=True, logrotate_enable=True, coredump_enable=True)
        # Tickets integration
        logging.info('Configure tickets integration')
        self.update_tickets_integration()

        self._ns = NannyService(self._service_name)

    @request
    def create_service_balancer(self, balancer_host, balancing_options='', use_mtn=True):
        """
        Create service balancer for service with basic default options.
        Options can be rewrited by variable "balancing_options"
        :param balancer_host: type str (fqdn)
        :param balancing_options: type dict
        :param use_mtn: type bool
        :return: type requests.model.Response

        """
        if not balancing_options:
            balancing_options = {}

        request_url = '%s/v2/services_balancers/production/config/sections/' % self._API
        balancer_section_attrs = self._connection.get(request_url).json()
        runtime_attrs = self._get_runtime_attrs().json()
        request_data = {
            'snapshot_id': balancer_section_attrs['snapshot_id'],
            'content': {
                'id': self._service_name.replace('_', '-'),
                'matcher': {
                    'host': balancer_host
                },
                'headers': [
                    {'name': 'X-Scheme', 'type': 'create_func_weak', 'value': 'scheme'},
                    {'name': 'X-Source-Port', 'type': 'create_func_weak', 'value': 'realport'},
                    {'name': 'X-Forwarded-For', 'type': 'create_func_weak', 'value': 'realip'},
                    {'name': 'X-Req-Id', 'type': 'create_func_weak', 'value': 'reqid'}
                ],
                'balancing_options': {
                    'connection_timeout': balancing_options.get('connection_timeout', '100ms'),
                    'backend_timeout': balancing_options.get('backend_timeout', '40s'),
                    'retries_count': balancing_options.get('retries_count', 2),
                    'keepalive_count': balancing_options.get('keepalive_count', 5),
                    'fail_on_5xx': balancing_options.get('fail_on_5xx', False),
                    'balancer_retries_timeout': balancing_options.get('overall_timeout', '60s'),
                    'balancing_type': {
                        'mode': balancing_options.get('balancing_type', 'rr')
                    },
                },

                'traffic_slices': [
                    {
                        'name': 'main',
                        'weight': 1,
                        'backend': {
                            'service_id': self._service_name,
                            'snapshot_id': runtime_attrs['_id'],
                            'use_mtn': use_mtn,
                        },
                    }
                ]
            }
        }
        return self._connection.post(request_url, json=request_data)

    def add_logbroker_token(self):
        comment = 'Adding LOGBROKER_TOKEN env variable'
        with self._get_nanny_rest_service().runtime_attrs_transaction(comment) as nanny_service:
            nanny_service.instance_spec.containers['saas_daemon'].ensure_env(
                VaultSecretEnv('LOGBROKER_TOKEN', 'sec-01dmzfn3qhxes8y4yyfar0xyhs', field='logbroker_token')
            )

    def add_tvm_env_variables(self):
        saas_ctype = SaasCtype(self._service_ctype)
        tvm_id = saas_ctype.environment.get('TVM_SERVICE_ID', saas_ctype.saas_debug_proxy_tvm_id)

        meta_info = self._abc_api.get_tvm_meta_info(tvm_id, self._abc_group_id)
        secret_id = meta_info['secret_uuid']
        secret_version = meta_info['version_uuid']

        comment = 'Adding TVM env variables for {tvm_id}'.format(tvm_id=tvm_id)
        with self._get_nanny_rest_service().runtime_attrs_transaction(comment) as nanny_service:
            nanny_service.instance_spec.containers['saas_daemon'].ensure_env(LiteralEnv('TVM_CLIENT_ID', str(tvm_id)))
            nanny_service.instance_spec.containers['saas_daemon'].ensure_env(
                VaultSecretEnv('TVM_SECRET', secret_id, secret_version, field='client_secret')
            )

    def configure_secrets(self, secrets_data=None):
        """
        Add service to keychains for access to secrets. Configure ENV variables with secrets.
        :return: type requests.model.Response
        """
        keychains_list = []
        service_attrs = self._get_runtime_attrs().json()
        content = service_attrs.get('content')

        # Collect secrets data and keychains list
        if not secrets_data:
            secrets_data = {}
            for section in self._secrets:
                secrets_data[section] = self._secrets[section]
        for section in secrets_data:
            keychain = secrets_data[section]['keychain']
            if keychain not in keychains_list:
                keychains_list.append(keychain)

        # Add service to keychains
        self.nanny_vault._create_connection()
        for keychain in keychains_list:
            self.nanny_vault.add_service_to_keychain(keychain, self._service_name)

        # Check for already existing keys
        for container in content['instance_spec']['containers']:
            container_name = container['name']
            if container_name in secrets_data.keys():
                for secret in container['env']:
                    if secret['name'] == secrets_data[container_name]['variable_name']:
                        secret['valueFrom'] = self._compose_secret_data(secrets_data[container_name])['valueFrom']
                        secrets_data.pop(container_name)
                        break
            # Add remaining keys
            for section in secrets_data:
                container['env'].append(self._compose_secret_data(secrets_data[section]))
        else:
            for section in secrets_data:
                content['instance_spec']['containers'].append({
                    'name': section,
                    'env': [
                        self._compose_secret_data(secrets_data[section])
                    ]
                })
        request_data = {
            'snapshot_id': service_attrs['_id'],
            'content': content,
            'comment': 'Configured secrets'
        }
        return self._set_runtime_attrs(request_data)

    def configure_secret_volumes(self):
        comment = 'Update secret volumes'
        with self._get_nanny_rest_service().runtime_attrs_transaction(comment) as nanny_service:
            for volume_name, volume_settings in self._secret_volumes.items():
                if nanny_service.instance_spec.get_volume(volume_name):
                    logging.info('Volume %s was already added, skipping...', volume_name)
                    continue

                volume = VaultSecretVolume(volume_name, volume_settings['secret_id'])
                nanny_service.instance_spec.ensure_volume(volume)

    def prepare_isolation(self, ssh_enable=True, logrotate_enable=True, skynet_enable=True, yasm_enable=True, coredump_enable=True):
        """
        Prepare base layer of container with options
        :param ssh_enable: type boolean
        :param logrotate_enable: type boolean
        :param skynet_enable: type boolean
        :param yasm_enable: type boolean
        :param coredump_enable: type boolean
        :return:
        """
        # Collect layers
        resources_list = []
        if self._isolation_settings:
            for layer in self._isolation_settings:
                data = self._isolation_settings[layer]
                resources_list.append(data)
        resources_list = self.get_last_resources(resources_list, release_type='')

        service_attrs = self._get_runtime_attrs().json()
        content = service_attrs.get('content')
        if 'layersConfig' not in content['instance_spec']:
            content['instance_spec']['layersConfig'] = {'layer': []}
        layers_ids = [str(l['fetchableMeta']['sandboxResource']['resourceId']) for l in content['instance_spec']['layersConfig']['layer']]
        for layer in resources_list:
            resource_torrent = self._sandbox.get_resource(layer['resource_id']).get('skynet_id')
            layer_data = {
                'fetchableMeta': {
                    'sandboxResource': {
                        'resourceId': layer['resource_id'],
                        'resourceType': layer['resource_type'],
                        'taskId': layer['task_id'],
                        'taskType': layer['task_type']
                    }
                },
                'url': [resource_torrent]
            }
            if str(layer['resource_id']) not in layers_ids:
                content['instance_spec']['layersConfig']['layer'].append(layer_data)
        # network properties
        network_prop = {
            'resolvConf': 'DEFAULT_RESOLV_CONF',
            'etcHosts': 'KEEP_ETC_HOSTS'
        }
        content['instance_spec']['networkProperties'] = network_prop
        if not content['instance_spec'].get('osContainerSpec'):
            content['instance_spec']['osContainerSpec'] = {
                'networkProperties': {}
            }
        content['instance_spec']['osContainerSpec']['networkProperties'] = network_prop

        # iss hook limits
        content['instances']['iss_settings'] = {
            'hooks_time_limits': {
                'iss_hook_stop': {
                    'max_execution_time': 600
                }
            }
        }

        # coredump policy
        if coredump_enable:
            for container in content['instance_spec']['containers']:
                container['coredumpPolicy'] = {
                    'type': 'COREDUMP',
                    'coredumpProcessor': {
                        'totalSizeLimit': 102400,
                        'probability': 100,
                        'aggregator': {
                            'saas': {
                                'url': 'http://cores.n.yandex-team.ru/corecomes',
                            },
                            'type': 'DISABLED'
                        },
                        'cleanupPolicy': {
                            'type': 'TTL',
                            'ttl': {
                                'seconds': 360000
                            }
                        },
                        'countLimit': 3,
                        'path': '/cores'
                    }
                }

        # enable host provided YASM agent
        if yasm_enable:
            if {u'type': u'YASM_AGENT'} not in content['instance_spec']['hostProvidedDaemons']:
                content['instance_spec']['hostProvidedDaemons'].append({u'type': u'YASM_AGENT'})

        # ssh access check
        if 'instanceAccess' not in content['instance_spec']:
            content['instance_spec']['instanceAccess'] = {'skynetSsh': 'DISABLED'}
        if ssh_enable:
            content['instance_spec']['instanceAccess']['skynetSsh'] = 'ENABLED'
        else:
            content['instance_spec']['instanceAccess']['skynetSsh'] = 'DISABLED'

        # logrotate check
        if logrotate_enable:
            if not {u'type': u'LOGROTATE'} in content['instance_spec']['auxDaemons']:
                content['instance_spec']['auxDaemons'].append({u'type': u'LOGROTATE'})
        else:
            if not {u'type': u'LOGROTATE'} in content['instance_spec']['auxDaemons']:
                content['instance_spec']['auxDaemons'].remove({u'type': u'LOGROTATE'})

        # skynet check
        if skynet_enable:
            if not {u'type': u'HOST_SKYNET'} in content['instance_spec']['hostProvidedDaemons']:
                content['instance_spec']['hostProvidedDaemons'].append({u'type': u'HOST_SKYNET'})
        else:
            if {u'type': u'SKYNET'} in content['instance_spec']['auxDaemons']:
                content['instance_spec']['hostProvidedDaemons'].remove({u'type': u'HOST_SKYNET'})

        # check for MTN enabled and gencfg volumes enabled if EXTENDED_GENCFG_GROUP choiced
        # do not delete this section because nanny service by default configured like gencfg-ready
        if content['instances']['chosen_type'] == 'EXTENDED_GENCFG_GROUPS':
            content['instances']['extended_gencfg_groups']['network_settings'] = {'use_mtn': True}
            content['instances']['extended_gencfg_groups']['gencfg_volumes_settings'] = {'use_volumes': True}

        request_data = {
            'snapshot_id': service_attrs['_id'],
            'content': content,
            'comment': 'Prepare service isolation'
        }
        return self._set_runtime_attrs(request_data)

    def update_deploy_recipe(self, new_recipe='', new_attrs=''):
        """
        Change recipe
        :return: type requests.model.Response
        """
        if not new_recipe:
            new_recipe = self._deploy_settings.get('recipe')
        if not new_attrs:
            new_attrs = self._deploy_settings.get('attrs')

        info_attrs = self._get_info_attrs().json()
        for recipe in info_attrs['content']['recipes']['content']:
            if recipe['name'] not in new_recipe:
                recipe['id'] = 'common'
                recipe['name'] = new_recipe
                recipe['context'] = new_attrs
        # Clear default prepare recipe
        info_attrs['content']['recipes']['prepare_recipes'] = []
        request_data = {
            'snapshot_id': info_attrs['_id'],
            'content': info_attrs['content'],
            'comment': 'Update recipe: ' + str(new_recipe)
        }
        return self._set_info_attrs(request_data)

    def update_monitoring(self, deploy_timeout='', calendar_id=''):
        """
        Change recipe
        :return: type requests.model.Response
        """
        if not deploy_timeout:
            deploy_timeout = self._monitoring_settings.get('deploy_timeout')
        if not calendar_id:
            calendar_id = self._monitoring_settings.get('calendar_id')

        info_attrs = self._get_info_attrs().json()
        if not info_attrs['content']['monitoring_settings'].get('deploy_monitoring'):
            info_attrs['content']['monitoring_settings']['deploy_monitoring'] = {}
        info_attrs['content']['monitoring_settings']['deploy_monitoring'] = {
            'is_enabled': True,
            'content': {
                'deploy_timeout': deploy_timeout,
                'alert_methods': ['sms'],
                'responsible': {
                    'calendar_id': calendar_id
                }
            }
        }
        request_data = {
            'snapshot_id': info_attrs['_id'],
            'content': info_attrs['content'],
            'comment': 'Update monitoring settings'
        }
        return self._set_info_attrs(request_data)

    def update_service_resources(self, sandbox_resources=None):
        """
        Sets resources specified on argument resources_list as service resources.
        :param sandbox_resources: type list
        """
        static_resources = []
        if not sandbox_resources:
            sandbox_resources = []
            for resource in self._resources:
                data = self._resources[resource]
                data['local_path'] = resource
                sandbox_resources.append(data)

            static_resources = [
                StaticFile('saas_ctype', self._service_ctype)
            ]

        sandbox_resources = self.get_last_resources(sandbox_resources)
        comment = 'Updated service resources'

        with self._get_nanny_rest_service().runtime_attrs_transaction(comment) as nanny_service:
            for resource in sandbox_resources:
                nanny_service.set_resource(SandboxFile(**resource))

            for resource in static_resources:
                nanny_service.set_resource(resource)

    def update_auth_settings(self, conf=None, ops=None, owners=None, cc=None):
        """
        Set users or groups as owners operation managers or configuration managers for service.
        Structure of arguments: {'logins': [login1,..], 'groups': [group1,..]}
        :param conf: type dict
        :param ops: type dict
        :param owners: type dict
        :param cc: type dict
        :return: type requests.model.Response
        """
        auth_attrs = self._get_auth_attrs().json()
        content = auth_attrs.get('content')

        # Check configuration first
        if not owners and self._auth_settings.get('owners'):
            owners = self._auth_settings.get('owners')
        if not ops and self._auth_settings.get('support'):
            ops = self._auth_settings.get('support')
        if not conf and self._auth_settings.get('support'):
            conf = self._auth_settings.get('support')
        if not cc and self._auth_settings.get('watchers'):
            cc = self._auth_settings.get('watchers')

        if ops:
            content['ops_managers'] = ops
        if owners:
            content['owners'] = owners
        if conf:
            content['conf_managers'] = conf
        if cc:
            content['observers'] = cc

        request_data = {
            'snapshot_id': auth_attrs['_id'],
            'content': content,
            'comment': 'Update auth attrs'
        }
        return self._set_auth_attrs(request_data)

    def _get_ti_filter_expression(self, rule_release_type, rule_task_type):
        """
        Gets the filter expression for ticket integration rules.

        :param rule_release_type: type str
        :param rule_task_type: type str
        :return: type str
        """
        filter_expression = 'sandbox_release.release_type in ("{release_type}",)'.format(release_type=rule_release_type)
        return filter_expression

    def update_tickets_integration(self):
        """
        Sets tickets integration rules for service.
        """

        task_type_to_rule_conf = self._tickets_integration.get('rules')
        comment = 'Updated service tickets integration rules'

        with self._get_nanny_rest_service().info_attrs_transaction(comment) as proxy:
            proxy.info_attrs_content['queue_id'] = self._tickets_integration['queue']
            proxy.tickets_integration.service_release_tickets_enabled = True

            release_type = ReleaseType.prestable if 'prestable' in self._service_ctype else ReleaseType.stable

            for rule_task_type, rule_conf in task_type_to_rule_conf.items():
                desc = rule_conf['desc']
                if release_type == ReleaseType.prestable:
                    rule_release_type = release_type
                    desc = desc.replace('stable', 'prestable')
                else:
                    rule_release_type = ReleaseType.build(rule_conf['release_type'])

                resource_type = rule_conf.get('resource_type', '') if rule_task_type != 'BUILD_RTYSERVER' else \
                    self._rtyserver_bin

                rule = TicketsIntegrationRule(
                    desc,
                    self._get_ti_filter_expression(rule_release_type.value, rule_task_type),
                    sandbox_task_type=rule_task_type,
                    sandbox_resource_type=resource_type,
                    ticket_priority=rule_conf['priority'].upper(),
                )

                proxy.tickets_integration.add_rule(rule, replace_existing=True)

            proxy.tickets_integration.instancectl_release_rule.enable(
                release_type,
                autocommit=AutocommitSettings(enabled=True, mark_as_disposable=True),
                queue_id=self._tickets_integration['queue'],
            )

    def change_rtyserver_release(self, task_id, resource_id):
        """
        Method for update rtyserver release. Requires sandbox task id and resource id as arguments.
        :param task_id: type str
        :param resource_id: type str
        """
        new_resource = SandboxFile('rtyserver', 'BUILD_RTYSERVER', task_id, self._rtyserver_bin, resource_id)
        comment = 'Update rtyserver to {sandbox_task}'.format(sandbox_task=task_id)

        with self._get_nanny_rest_service().runtime_attrs_transaction(comment) as nanny_service:
            nanny_service.set_resource(new_resource)
