import base64
import errno
import random
import re
import string
import sys
import uuid
import os
import hashlib
import time
from xml.etree import ElementTree
from contextlib import closing

from parallels.core import MigrationNoContextError
from parallels.core.logging import get_logger
from parallels.core.dump.dump_archive_writer import DumpArchiveWriter
from parallels.core.utils.common import cached, find_only, is_unicode_or_ascii_string, safe_format
from parallels.core.utils.common_constants import DATABASE_NO_SOURCE_HOST
from parallels.core.utils.database_server_type import DatabaseServerType
from parallels.core.utils.database_utils import split_mssql_host_parts_str, join_mssql_hostname_and_instance
from parallels.plesk import messages
from parallels.core.utils.common import (
    group_by_id, generate_random_password, is_empty, default
)
from parallels.core.utils.common.xml import (
    attrs, elem, list_elem, text_elem, seq, xml_to_string_pretty, xml_bool
)
from parallels.plesk.hosting_description.input_formats.formats import INPUT_FORMATS
from parallels.plesk.hosting_description.utils import parse_bool_value, create_dict
from parallels.core.utils.safe_structures import safe_get_list, safe_get_dict, safe_get_struct
from parallels.plesk.hosting_description.model import (
    ForwardingType, iter_all_subscriptions,
    HostingDescriptionMailService, HostingDescriptionSubscription,
    HostingDescriptionDatabaseServer, iter_all_database_servers, HostingDescriptionDatabase
)

logger = get_logger(__name__)

DEFAULT_SOURCE_IP = '127.0.0.1'


class HostingDescriptionToPleskDumpConverter(object):
    """Convert hosting description file to Plesk configuration dump format

    :type _dump_writer: parallels.core.dump.dump_archive_writer.DumpArchiveWriter
    """

    def __init__(
        self, data, target_dump_file, target_plesk_version,
        database_servers=None, is_windows=False, source_ip=DEFAULT_SOURCE_IP,
        mail_server_ips=None
    ):
        """
        :type data: dict
        :type target_dump_file: str|unicode
        :type target_plesk_version: tuple[int]
        :type database_servers: list[parallels.custom_panel_migrator.connections.DatabaseServerConfig] | None
        :type is_windows: bool
        :type source_ip: str|unicode
        """
        self._data = data
        self._target_dump_file = target_dump_file
        self._target_plesk_version = target_plesk_version
        self._database_servers = default(database_servers, [])
        self._is_windows = is_windows
        self._source_ip = source_ip
        self._timestamp = time.strftime('%y%m%d%H%M')
        self._dump_writer = None
        self._mail_server_ips = default(mail_server_ips, {})

    def write_dump(self):
        """Write Plesk configuration dump file created out of hosting description file
        """
        if os.path.exists(self._target_dump_file):
            os.remove(self._target_dump_file)
        self._dump_writer = DumpArchiveWriter(self._target_dump_file)
        with closing(self._dump_writer):
            self._create_server_dump()
            for reseller in safe_get_list(self._data, 'resellers'):
                self._create_reseller_dump(reseller)
            for client in safe_get_list(self._data, 'customers'):
                self._create_client_dump(client)
            for domain in safe_get_list(self._data, 'subscriptions'):
                self._create_domain_dump(domain)

    def _create_server_dump(self):
        dump_tree = self._create_dump_xml_tree()
        root = dump_tree.getroot()
        root.append(elem(
            'admin', seq(
                list_elem('resellers', [
                    self._reseller_info_node(reseller) for reseller in safe_get_list(self._data, 'resellers')
                ]),
                list_elem('clients', [
                    self._client_info_node(client) for client in safe_get_list(self._data, 'customers')
                ]),
                list_elem('domains', [
                    self._domain_info_node(domain) for domain in safe_get_list(self._data, 'subscriptions')
                ]),
                list_elem('users', [
                    self._user_node(user) for user in safe_get_list(self._data, 'users')
                ]),
                list_elem('roles', [
                    self._role_node(role) for role in safe_get_list(self._data, 'roles')
                ]),
                elem('preferences', seq(
                    self._admin_descriptions_node(self._data)
                ))
            ), {
                'guid': self._get_guid('client', 'admin'),
            }
        ))

        if self._database_servers:
            database_servers = []
            for database_server in self._database_servers:
                database_servers.append(self._get_database_server(database_server.db_server_id))
        else:
            database_servers = iter_all_database_servers(self._data)

        root.append(
            elem(
                'server', seq(
                    list_elem('db-servers', [
                        self._database_server_node(database_server, include_admin=True)
                        for database_server in database_servers
                    ]),
                    list_elem('account-templates', [
                        self._hosting_plan_node('admin', hosting_plan)
                        for hosting_plan in safe_get_list(self._data, 'hosting_plans')
                    ] + [
                        self._reseller_plan_node('admin', reseller_plan)
                        for reseller_plan in safe_get_list(self._data, 'reseller_plans')
                    ]),
                )
            )
        )
        self._save_dump(xml_to_string_pretty(dump_tree))

    def _create_reseller_dump(self, reseller):
        """
        :type reseller: dict
        """
        dump_tree = self._create_dump_xml_tree()
        root = dump_tree.getroot()
        root.append(self._reseller_node(reseller))
        self._save_dump(
            xml_to_string_pretty(dump_tree),
            reseller_name=reseller['login'],
            guid=reseller.get('guid', self._get_guid('client', reseller['login'])),
        )
        for client in safe_get_list(reseller, 'customers'):
            self._create_client_dump(client, reseller=reseller)
        for domain in safe_get_list(reseller, 'subscriptions'):
            self._create_domain_dump(domain, reseller=reseller)

    def _create_client_dump(self, client, reseller=None):
        """
        :type client: dict
        :type reseller: dict | None
        """
        dump_tree = self._create_dump_xml_tree()
        root = dump_tree.getroot()
        root.append(self._client_node(client))
        self._save_dump(
            xml_to_string_pretty(dump_tree),
            client_name=client['login'],
            reseller_name=reseller['login'] if reseller else None,
            guid=client.get('guid', self._get_guid('client', client['login'])),
        )
        for domain in safe_get_list(client, 'subscriptions'):
            self._create_domain_dump(domain, client=client, reseller=reseller)

    def _create_domain_dump(self, subscription, client=None, reseller=None):
        """
        :type subscription: dict
        :type client: dict | None
        :type reseller: dict | None
        """
        dump_tree = self._create_dump_xml_tree()
        root = dump_tree.getroot()
        owner = reseller if reseller else client
        root.append(self._domain_node(subscription, owner))
        self._save_dump(
            xml_to_string_pretty(dump_tree),
            domain_name=subscription['name'],
            client_name=client['login'] if client else None,
            reseller_name=reseller['login'] if reseller else None,
            guid=subscription.get('guid', self._get_guid('domain', subscription['name'])),
        )

    def _create_dump_xml_tree(self):
        dump_version = '.'.join(map(str, self._target_plesk_version))
        dump_tree = ElementTree.ElementTree(elem(
            'migration-dump', [
                elem(
                    'dump-info', [
                        elem('os-description', [], {'type': 'windows' if self._is_windows else 'unix'})
                    ], {}
                )
            ], {
                'content-included': 'false',
                'agent-name': 'PleskX',
                'dump-format': 'panel',
                'dump-version': dump_version,
                'dump-original-version': dump_version,
            }
        ))
        return dump_tree

    def _reseller_info_node(self, reseller):
        """
        :type reseller: dict
        :rtype: xml.etree.ElementTree.Element
        """
        return elem(
            'reseller-info', [
                self._client_info_node(client) for client in safe_get_list(reseller, 'customers')
            ] + [
                self._domain_info_node(subscription) for subscription in safe_get_list(reseller, 'subscriptions')
            ], {
                'guid': reseller.get('guid', self._get_guid('client', reseller['login'])),
                'name': reseller['login'],
            }
        )

    def _client_info_node(self, client):
        """
        :type client: dict
        :rtype: xml.etree.ElementTree.Element
        """
        return elem(
            'client-info', [
                self._domain_info_node(subscription) for subscription in safe_get_list(client, 'subscriptions')
            ], {
                'guid': client.get('guid', self._get_guid('client', client['login'])),
                'name': client['login'],
            }
        )

    def _domain_info_node(self, subscription):
        """
        :type subscription: dict
        :rtype: xml.etree.ElementTree.Element
        """
        return elem(
            'domain-info', [], {
                'guid': self._get_guid('domain', subscription['name']),
                'name': subscription['name'],
            }
        )

    def _reseller_plan_node(self, owner_name, reseller_plan):
        """
        :type owner_name: str|unicode
        :type reseller_plan: dict
        :rtype: xml.etree.ElementTree.Element
        """
        ip_addresses = safe_get_list(reseller_plan, 'ip_addresses')
        if not ip_addresses:
            ip_addresses = [{'type': 'shared', 'address': self._source_ip}]
        applications_filter = safe_get_struct(reseller_plan, 'applications_filter')
        return elem(
            'reseller-template', [
                text_elem('template-item', item_value, {'name': item_name})
                for item_name, item_value in safe_get_dict(reseller_plan, 'items').items()
            ] + seq(
                self._ip_pool_node(ip_addresses),
                self._applications_filter_node(applications_filter) if applications_filter else None,
            ), {
                'guid': reseller_plan.get('guid', self._get_guid('%s-reseller-plan' % owner_name, reseller_plan['name'])),
                'name': reseller_plan['name'],
            }
        )

    def _hosting_plan_node(self, owner_name, hosting_plan):
        """
        :type owner_name: str|unicode
        :type hosting_plan: dict
        :rtype: xml.etree.ElementTree.Element
        """
        applications_filter = safe_get_struct(hosting_plan, 'applications_filter')
        default_db_servers = safe_get_list(hosting_plan, 'default_db_servers')
        return elem(
            'domain-template', [
                text_elem('template-item', item_value, {'name': item_name})
                for item_name, item_value in safe_get_dict(hosting_plan, 'items').items()
            ] + [
                text_elem('template-item', item_value, {'name': item_name})
                for item_name, item_value in safe_get_dict(hosting_plan, 'web_server_settings').items()
            ] + seq(
                self._log_rotation_node(safe_get_struct(hosting_plan, 'log_rotation')),
                self._applications_filter_node(applications_filter) if applications_filter else None,
                self._php_settings_node(safe_get_struct(hosting_plan, 'php_settings')),
                self._default_db_servers_node(default_db_servers) if default_db_servers else None,
            ), {
                'guid': hosting_plan.get('guid', self._get_guid('%s-hosting-plan' % owner_name, hosting_plan['name'])),
                'name': hosting_plan['name'],
                'is-addon': xml_bool(hosting_plan.get('is_addon', False)),
            }
        )

    def _database_server_node(self, database_server, include_admin=False):
        """
        :type database_server: parallels.plesk.hosting_description.model.HostingDescriptionDatabaseServer
        :rtype: xml.etree.ElementTree.Element
        """
        database_server_admin = database_server.administrator if include_admin else None
        if database_server.db_type == DatabaseServerType.MSSQL:
            if not is_empty(database_server.port) and database_server.port != '0' and database_server.port != 0:
                host_parts = split_mssql_host_parts_str(database_server.host, can_be_incorrect=True)
                if host_parts is None:
                    # failed to parse hostname - just add port at the end
                    host = "%s,%s" % (database_server.host, database_server.port)
                else:
                    # parsed hostname successfully - replace port
                    # that is necessary to avoid port duplication when customer has specified port both in hostname
                    # and as a separate parameter
                    part_host, part_instance, _ = host_parts
                    host = join_mssql_hostname_and_instance(part_host, part_instance, database_server.port)
            else:
                host = database_server.host

            port = None
        else:
            host = database_server.host
            port = database_server.port

        if port is None:
            if database_server.db_type == DatabaseServerType.MYSQL:
                port = '3306'
            elif database_server.db_type == DatabaseServerType.POSTGRESQL:
                port = '5432'
            else:
                port = ''

        return elem(
            'db-server', seq(
                text_elem('host', host),
                text_elem('port', port),
                elem(
                    'db-admin', [
                        self._password_node(database_server_admin.password, database_server_admin.password_type)
                    ], {
                        'name': database_server_admin.username
                    }
                ) if database_server_admin else None,
            ), {
                'type': database_server.db_type
            }
        )

    def _get_database_server(self, database_server_id):
        """
        :type database_server_id: str
        :rtype: parallels.plesk.hosting_description.model.HostingDescriptionDatabaseServer
        """
        database_server_config = find_only(
            self._database_servers, lambda d: d.db_server_id == database_server_id,
            messages.FAILED_TO_FIND_DB_SERVER.format(db_server_id=database_server_id)
        )
        return HostingDescriptionDatabaseServer(create_dict(
            type=database_server_config.db_type,
            host=database_server_config.host,
            port=database_server_config.port,
            admin=create_dict(
                login=database_server_config.login,
                password=database_server_config.password,
                password_type='plain',
            ),
        ))

    def _reseller_node(self, reseller):
        contact_info = reseller.get('contact_info')
        users = [self._client_user(reseller)]
        if safe_get_list(reseller, 'users'):
            users += safe_get_list(reseller, 'users')
        ip_addresses = safe_get_list(reseller, 'ip_addresses')
        if not ip_addresses:
            ip_addresses = [{'type': 'shared', 'address': self._source_ip}]
        return elem(
            'reseller', seq(
                self._reseller_preferences_node(reseller),
                self._reseller_properties_node(reseller),
                self._limits_and_permissions_node(safe_get_dict(reseller, 'limits'), safe_get_dict(reseller, 'permissions')),
                self._ip_pool_node(ip_addresses),
            ) + ([elem(
                'domains', [
                    self._domain_info_node(subscription) for subscription in safe_get_list(reseller, 'subscriptions')
                ]
            )] if safe_get_list(reseller, 'subscriptions') else []) + ([elem(
                'clients', [
                    self._client_info_node(client) for client in safe_get_list(reseller, 'customers')
                ]
            )] if safe_get_list(reseller, 'customers') else []) + [elem(
                'users', [
                    self._user_node(user) for user in users
                ]
            )] + ([elem(
                'roles', [
                    self._role_node(role) for role in safe_get_list(reseller, 'roles')
                ]
            )] if safe_get_list(reseller, 'roles') else []), attrs({
                'guid': reseller.get('guid', self._get_guid('client', reseller['login'])),
                'external-id': reseller.get('external_id'),
                'name': reseller['login'],
                'contact': contact_info.get('name') if contact_info else None,
                'cr-date': reseller.get('creation_date'),
            })
        )

    def _client_node(self, client):
        """
        :type client: dict
        :rtype: xml.etree.ElementTree.Element
        """
        contact_info = client.get('contact_info')
        users = [self._client_user(client)]
        if safe_get_list(client, 'users'):
            users += safe_get_list(client, 'users')
        return elem(
            'client', [
                self._client_preferences_node(client),
                self._client_properties_node(client),
            ] + ([elem(
                'domains', [
                    self._domain_info_node(subscription) for subscription in safe_get_list(client, 'subscriptions')
                ]
            )] if safe_get_list(client, 'subscriptions') else []) + [elem(
                'users', [
                    self._user_node(user) for user in users
                ]
            )] + ([elem(
                'roles', [
                    self._role_node(role) for role in safe_get_list(client, 'roles')
                ]
            )] if safe_get_list(client, 'roles') else []), attrs({
                'guid': client.get('guid', self._get_guid('client', client['login'])),
                'external-id': client.get('external_id'),
                'name': client['login'],
                'contact': contact_info.get('name') if contact_info else None,
                'cr-date': client.get('creation_date'),
            })
        )

    def _client_user(self, client):
        """
        :type client: dict
        :rtype: dict
        """
        return create_dict(
            login=client['login'],
            password=client.get('password'),
            password_type=client.get('password_type'),
            guid=self._get_guid('user', client['login']),
            locale=client.get('locale'),
            creation_date=client.get('creation_date'),
            disabled_by=['admin'] if safe_get_list(client, 'disabled_by') else [],
            role='Owner',
            is_built_in=True,
            contact_info=client.get('contact_info'),
        )

    def _reseller_preferences_node(self, reseller):

        subscription_info = safe_get_struct(reseller, 'subscription_info')
        applications_filter = safe_get_struct(reseller, 'applications_filter')
        return elem(
            'preferences', (
                self._pinfo_nodes(safe_get_struct(reseller, 'contact_info'), reseller.get('locale'))
            ) + [
                self._hosting_plan_node(reseller['login'], hosting_plan)
                for hosting_plan in safe_get_list(reseller, 'hosting_plans')
            ] + [
                self._custom_button_node(custom_button)
                for custom_button in safe_get_list(reseller, 'custom_buttons')
            ] + seq(
                self._iis_application_pool_node(safe_get_struct(reseller, 'iis_application_pool')),
                self._subscription_info_node(subscription_info),
                self._applications_filter_node(applications_filter) if applications_filter else None,
                self._reseller_descriptions_node(reseller)
            )
        )

    def _reseller_properties_node(self, reseller):
        return elem('properties', [
            self._password_node(reseller.get('password'), reseller.get('password_type')),
            self._status_node(safe_get_list(reseller, 'disabled_by'))
        ])

    @staticmethod
    def _limits_and_permissions_node(limits, permissions):
        """
        :type limits: dict | None
        :type permissions: dict | None
        :rtype: xml.etree.ElementTree.Element | None
        """
        limits_and_permissions_nodes = []
        if limits:
            for name, value in limits.items():
                if name is not None and value is not None:
                    limits_and_permissions_nodes.append(text_elem('limit', value, {'name': name}))
        if permissions:
            for name, value in permissions.items():
                if name is not None and value is not None:
                    limits_and_permissions_nodes.append(elem('permission', [], {'name': name, 'value': value}))
        if limits_and_permissions_nodes:
            return elem('limits-and-permissions', limits_and_permissions_nodes)
        return None

    def _client_preferences_node(self, client):
        return elem(
            'preferences', (
                self._pinfo_nodes(client.get('contact_info'), client.get('locale'))
            ) + [
                self._custom_button_node(custom_button) for custom_button in safe_get_list(client, 'custom_buttons')
            ] + seq(
                self._iis_application_pool_node(safe_get_struct(client, 'iis_application_pool')),
            )
        )

    def _client_properties_node(self, client):
        return elem(
            'properties', [
                self._password_node(client.get('password'), client.get('password_type')),
                self._status_node(safe_get_list(client, 'disabled_by'))
            ]
        )

    def _pinfo_nodes(self, contact_info, locale):
        """
        :type contact_info: dict | None
        :type locale: str | unicode | None
        :rtype: list[xml.etree.ElementTree.Element]
        """
        contact_info = default(contact_info, {})
        return seq(
            text_elem('pinfo', contact_info.get('name'), {'name': 'name'}),
            text_elem('pinfo', contact_info.get('email'), {'name': 'email'}),
            text_elem('pinfo', contact_info.get('company'), {'name': 'company'}),
            text_elem('pinfo', contact_info.get('country_code'), {'name': 'country'}),
            text_elem('pinfo', contact_info.get('zip'), {'name': 'zip'}),
            text_elem('pinfo', contact_info.get('state'), {'name': 'state'}),
            text_elem('pinfo', contact_info.get('city'), {'name': 'city'}),
            text_elem('pinfo', contact_info.get('address'), {'name': 'address'}),
            text_elem('pinfo', contact_info.get('phone'), {'name': 'phone'}),
            text_elem('pinfo', contact_info.get('fax'), {'name': 'fax'}),
            text_elem('pinfo', contact_info.get('im'), {'name': 'im'}),
            text_elem('pinfo', contact_info.get('im_type'), {'name': 'im-type'}),
            text_elem('pinfo', contact_info.get('comment'), {'name': 'comment'}),
            text_elem('pinfo', locale, {'name': 'locale'}),
        )

    def _user_node(self, user):
        """
        :type user: dict
        :rtype: xml.etree.ElementTree.Element
        """
        contact_info = user.get('contact_info')
        return elem('user', [
            elem('properties', [
                self._password_node(user.get('password'), user.get('password_type')),
                self._status_node(safe_get_list(user, 'disabled_by'))
            ]),
            elem('limits-and-permissions', [
                text_elem('role-ref', user['role'])
            ]), elem('preferences', self._pinfo_nodes(
                contact_info,
                user.get('locale'),
            ))
        ], attrs({
            'name': user['login'],
            'contact': contact_info.get('name', '') if contact_info else '',
            'guid': user.get('guid', self._get_guid('user', user['login'])),
            'email': contact_info.get('email') if contact_info else None,
            'is-domain-admin': 'false',
            'is-built-in': xml_bool(user.get('is_built_in')),
            'cr-date': user.get('creation_date'),
            'subscription-name': user.get('subscription_name'),
            'external-email': user.get('external_email'),
        }))

    def _role_node(self, role):
        """
        :type role: dict
        :rtype: xml.etree.ElementTree.Element
        """
        return elem('role', [
            elem('limits-and-permissions', [
                elem('permission', [], {'name': name, 'value': value})
                for name, value in safe_get_dict(role, 'permissions').items()
            ] + [
                self._service_permission_node(service_permission)
                for service_permission in safe_get_list(role, 'service_permissions')
            ])
        ] if safe_get_dict(role, 'permissions') or safe_get_list(role, 'service_permissions') else [], attrs({
            'name': role['name'],
            'is-built-in': 'false',
        }))

    def _service_permission_node(self, service_permission):
        """
        :type service_permission: dict
        :rtype: xml.etree.ElementTree.Element
        """
        return elem('service-permission', [], attrs({
            'classname': service_permission['classname'],
            'description': service_permission['description'],
            'externalId': service_permission['external_id'],
            'permissionCode': service_permission['permission_code'],
            'class': service_permission['permission_class'],
            'value': xml_bool(service_permission['value']),
        }))

    def _status_node(self, disabled_by=None, node_name='status'):
        """
        :type disabled_by: list[string] | None
        :type node_name: str | unicode
        :rtype: xml.etree.ElementTree.Element
        """
        if disabled_by:
            return elem(node_name, [
                elem('disabled-by', [], {'name': person})
                for person in disabled_by
            ])
        return elem(node_name, [elem('enabled')])

    def _domain_node(self, domain, owner):
        """
        :type domain: dict
        :type owner: dict | None
        :rtype: xml.etree.ElementTree.Element
        """
        ip_addresses = safe_get_list(domain, 'ip_addresses')
        if not ip_addresses:
            ip_addresses = [{'type': 'shared', 'address': self._source_ip}]
        subscription = HostingDescriptionSubscription(domain)
        return elem(
            'domain', seq(
                self._domain_preferences_node(domain),
                self._domain_properties_node(domain, ip_addresses),
                self._limits_and_permissions_node(
                    safe_get_dict(domain, 'limits'), safe_get_dict(domain, 'permissions')
                ),
                self._mailsystem_node(domain),
                self._databases_node(subscription),
                self._maillists_node(domain),
                self._certificates_node(domain),
                self._hosting_node(domain, ip_addresses, owner)
            ), attrs({
                'guid': domain.get('guid', self._get_guid('domain', domain['name'])),
                'external-id': domain.get('external_id'),
                'www': 'true',
                'name': domain['name'],
                'cr-date': domain.get('creation_date'),
            })
        )

    def _domain_preferences_node(self, domain):
        """
        :type domain: dict
        :rtype: xml.etree.ElementTree.Element
        """
        subscription_info = safe_get_struct(domain, 'subscription_info')
        subscription_properties = None
        mail_service_limits = safe_get_struct(domain, 'mail_service_limits')
        if mail_service_limits:
            subscription_properties = {
                'outgoing_messages_domain_limit': mail_service_limits.get('outgoing_messages_domain_limit'),
                'outgoing_messages_enable_sendmail': mail_service_limits.get('outgoing_messages_enable_sendmail'),
                'outgoing_messages_mbox_limit': mail_service_limits.get('outgoing_messages_mbox_limit'),
                'outgoing_messages_subscription_limit': mail_service_limits.get('outgoing_messages_subscription_limit'),
            }
        applications_filter = safe_get_struct(domain, 'applications_filter')
        default_db_servers = safe_get_list(domain, 'default_db_servers')
        return elem(
            'preferences', [
                self._domain_alias_node(domain_alias) for domain_alias in safe_get_list(domain, 'aliases')
            ] + seq(
                self._subscription_info_node(subscription_info, subscription_properties),
                self._applications_filter_node(applications_filter) if applications_filter else None,
                self._default_db_servers_node(default_db_servers) if default_db_servers else None,
                self._subscription_descriptions_node(domain),
            )
        )

    def _domain_alias_node(self, domain_alias):
        """
        :type domain_alias: dict
        :rtype: xml.etree.ElementTree.Element
        """
        dns_zone = safe_get_struct(domain_alias, 'dns_zone')
        return elem(
            'domain-alias', seq(
                self._status_node(safe_get_list(domain_alias, 'disabled_by')),
                self._dns_zone_node(dns_zone, domain_alias.get('dns')) if dns_zone else None,
            ), attrs({
                'name': domain_alias['name'],
                'mail': xml_bool(domain_alias.get('mail', True)),
                'web': xml_bool(domain_alias.get('web', True)),
                'tomcat': xml_bool(domain_alias.get('tomcat')),
                'dns': xml_bool(domain_alias.get('dns')),
                'seo-redirect': xml_bool(domain_alias.get('seo_redirect')),
            })
        )

    def _dns_zone_node(self, dns_zone, sync_with_parent=None):
        """
        :type dns_zone: dict
        :rtype: xml.etree.ElementTree.Element
        """
        return elem(
            'dns-zone', seq(
                self._status_node(safe_get_list(dns_zone, 'disabled_by')),
                self._dns_zone_parameter_node('ttl', dns_zone),
                self._dns_zone_parameter_node('refresh', dns_zone),
                self._dns_zone_parameter_node('retry', dns_zone),
                self._dns_zone_parameter_node('expire', dns_zone),
                self._dns_zone_parameter_node('minimum', dns_zone),
            ) + [
                self._dns_record_node(dns_record) for dns_record in safe_get_list(dns_zone, 'dns_records')
            ], attrs({
                'email': dns_zone.get('email'),
                'type': dns_zone['type'],
                'serial-format': dns_zone.get('serial_format'),
                'sync-with-parent': xml_bool(sync_with_parent),
            })
        )

    def _dns_zone_parameter_node(self, name, dns_zone):
        """
        :type name: str|unicode
        :type dns_zone: dict
        :rtype: xml.etree.ElementTree.Element | None
        """
        encoded_value = dns_zone.get(name)
        if encoded_value is None:
            return None
        if encoded_value[-1:].isalpha():
            value = encoded_value[:-1]
            unit_code = encoded_value[-1:]
        else:
            value = encoded_value
            unit_code = ''
        units_map = {
            '': 'second',
            'M': 'minutes',
            'H': 'hours',
            'D': 'days',
            'W': 'weeks',
        }
        unit = units_map.get(unit_code)
        if unit is None:
            return None
        return elem(
            'dns-zone-param', [], {
                'name': name,
                'unit': unit,
                'value': value,
            }
        )

    def _dns_record_node(self, dns_record):
        """
        :type dns_record: dict
        :rtype: xml.etree.ElementTree.Element
        """
        return elem(
            'dnsrec', [], attrs({
                'type': dns_record['type'],
                'src': dns_record['src'],
                'dst': dns_record.get('dst'),
                'opt': dns_record.get('opt'),
            })
        )

    def _domain_properties_node(self, domain, ip_addresses):
        """
        :type domain: dict
        :type ip_addresses: list
        :rtype: xml.etree.ElementTree.Element
        """
        dns_zone = safe_get_struct(domain, 'dns_zone')
        return elem(
            'properties',
            self._ip_addresses_nodes(ip_addresses) +
            seq(
                self._status_node(safe_get_list(domain, 'subscription_disabled_by'), 'webspace-status'),
                self._status_node(safe_get_list(domain, 'domain_disabled_by')),
                self._dns_zone_node(dns_zone) if dns_zone else None,
            )
        )

    def _site_properties_node(self, domain):
        """
        :type domain: dict
        :rtype: xml.etree.ElementTree.Element
        """
        dns_zone = safe_get_struct(domain, 'dns_zone')
        ip_addresses = safe_get_list(domain, 'ip_addresses')
        return elem(
            'properties',
            self._ip_addresses_nodes(ip_addresses) + seq(
                self._status_node(safe_get_list(domain, 'disabled_by')),
                self._dns_zone_node(dns_zone) if dns_zone else None,
            )
        )

    def _mailsystem_node(self, domain):
        """
        :type domain: dict
        :rtype: xml.etree.ElementTree.Element | None
        """
        mail_service = safe_get_struct(domain, 'mail_service')
        if not mail_service:
            return None
        return elem(
            'mailsystem', seq(
                self._mailsystem_properties_node(mail_service),
                self._mailusers_node(mail_service),
                self._mailsystem_preferences_node(mail_service),
            )
        )

    def _mailsystem_properties_node(self, mail_service_data):
        """
        :type mail_service_data: dict
        :rtype: xml.etree.ElementTree.Element
        """
        mail_service = HostingDescriptionMailService(mail_service_data)
        ip_nodes = []

        ipv4 = mail_service.mail_ip
        if is_empty(ipv4):
            if mail_service.mail_server in self._mail_server_ips:
                ipv4 = self._mail_server_ips[mail_service.mail_server]
            else:
                ipv4 = self._source_ip

        ip_nodes.append(elem('ip', [
            text_elem('ip-type', 'shared'),
            text_elem('ip-address', ipv4)
        ]))

        if not is_empty(mail_service.mail_ipv6):
            ip_nodes.append(elem('ip', [
                text_elem('ip-type', 'shared'),
                text_elem('ip-address', mail_service.mail_ipv6)
            ]))

        return elem(
            'properties', [
                self._status_node(safe_get_list(mail_service_data, 'disabled_by')),
            ] + ip_nodes
        )

    def _mailsystem_preferences_node(self, mail_service):
        """
        :type mail_service: dict
        :rtype: xml.etree.ElementTree.Element
        """
        grey_listing = None
        if mail_service.get('grey_listing') is not None:
            grey_listing = 'on' if mail_service.get('grey_listing') else 'off'
        return elem(
            'preferences', seq(
                self._catch_all_node(mail_service),
                self._domain_keys_node(mail_service),
                text_elem('web-mail', mail_service.get('webmail')),
                text_elem('grey-listing', grey_listing),
                self._outgoing_messages_node({
                    'outgoing_messages_limit': mail_service.get('outgoing_messages_limit'),
                }),
            )
        )

    def _catch_all_node(self, mail_service):
        """
        :type mail_service: dict
        :rtype: xml.etree.ElementTree.Element | None
        """
        catch_all_action = safe_get_struct(mail_service, 'catch_all_action')
        if not catch_all_action:
            return None
        if catch_all_action.get('name') == 'reject':
            return text_elem('catch-all', 'discard' if self._is_windows else 'reject')
        elif catch_all_action.get('name') == 'forward':
            forward_ip = catch_all_action.get('forward_ip')
            if forward_ip:
                return text_elem('catch-all', forward_ip)
            forward_email = catch_all_action.get('forward_email')
            if forward_email:
                return text_elem('catch-all', forward_email)
        elif catch_all_action.get('name') == 'bounce':
            bounce_message = catch_all_action.get('bounce_message')
            if bounce_message:
                return text_elem('catch-all', 'bounce:' + bounce_message)
        return None

    def _domain_keys_node(self, mail_service):
        """
        :type mail_service: dict
        :rtype: xml.etree.ElementTree.Element | None
        """
        domain_keys = safe_get_struct(mail_service, 'domain_keys')
        if not domain_keys:
            return None
        private_key = None
        if domain_keys.get('private_key'):
            private_key = base64.b64encode(domain_keys.get('private_key').encode('utf-8'))
        return elem(
            'domain-keys',
            [],
            attrs({
                'state': xml_bool(domain_keys['enabled']),
                'private-key': private_key,
                'public-key': domain_keys.get('public_key'),
            })
        )

    def _outgoing_messages_node(self, outgoing_messages):
        """
        :type outgoing_messages: dict
        :rtype: xml.etree.ElementTree.Element | None
        """
        return list_elem(
            'outgoing-messages', [
                elem(
                    'parameter', [
                        text_elem('name', name),
                        text_elem('value', value),
                    ]
                )
                for name, value in outgoing_messages.items() if value is not None
            ]
        )

    def _mailusers_node(self, mail_service):
        """
        :type mail_service: dict
        :rtype: xml.etree.ElementTree.Element | None
        """
        mail_users = safe_get_list(mail_service, 'mail_users')
        if not mail_users:
            return None
        return elem('mailusers', [
            self._mailuser_node(mail_user) for mail_user in mail_users
        ])

    def _mailuser_node(self, mail_user):
        """
        :type mail_user: dict
        :rtype: xml.etree.ElementTree.Element
        """
        mailbox_quota = None
        if mail_user.get('disk_quota') is not None:
            mailbox_quota = str(self._convert_to_bytes(mail_user.get('disk_quota')))
        forwarding = safe_get_struct(mail_user, 'forwarding')
        forwarding_addresses = safe_get_list(forwarding, 'addresses') if forwarding else []
        return elem(
            'mailuser', seq(
                elem(
                    'properties', [
                        self._password_node(mail_user.get('password'), mail_user.get('password_type'))
                    ]
                ),
                elem(
                    'preferences', seq(
                        elem('mailbox', [], {'enabled': xml_bool(mail_user.get('mailbox', True)), 'type': 'mdir'}),
                    ) + [
                        text_elem('alias', alias) for alias in safe_get_list(mail_user, 'aliases')
                    ] + [
                        text_elem('forwarding', address) for address in forwarding_addresses
                    ] + seq(
                        self._auto_responders_node(mail_user),
                        self._spamassassin_node(mail_user),
                        self._virusfilter_node(mail_user),
                        self._mail_user_descriptions_node(mail_user),
                        self._outgoing_messages_node({
                            'outgoing_messages_limit': mail_user.get('outgoing_messages_limit'),
                        }),
                    )
                )
            ), attrs({
                'name': mail_user['name'],
                'forwarding-enabled': xml_bool(forwarding.get('enabled', True)) if forwarding_addresses else 'false',
                'mailbox-quota': mailbox_quota,
                'deployer-action': mail_user.get('deployer_action'),
            })
        )

    def _auto_responders_node(self, mail_user):
        """
        :type mail_user: dict
        :rtype: xml.etree.ElementTree.Element | None
        """
        auto_reply = safe_get_dict(mail_user, 'auto_reply')
        if not auto_reply:
            return None
        attachments = safe_get_list(auto_reply, 'attachments')
        return elem(
            'autoresponders', [
                elem('attach', [], {'file': attachment}) for attachment in attachments
            ] + [
                elem(
                    'autoresponder', seq(
                        text_elem('text', base64.b64encode(auto_reply['message'].encode('utf-8')), {'charset': 'UTF-8'}),
                        text_elem('autoresponder-limit', auto_reply.get('responses_frequency'), {'name': 'ans-freq'}),
                        text_elem('autoresponder-limit', auto_reply.get('end_date'), {'name': 'end-date'}),
                    ) + [
                        elem('attach', [], {'file': attachment}) for attachment in attachments
                    ],
                    attrs({
                        'status': 'on' if auto_reply.get('enabled', True) else 'off',
                        'subject': base64.b64encode(auto_reply['subject'].encode('utf-8')),
                        'redirect': auto_reply.get('forwarding_address'),
                        'content-type': auto_reply.get('content_type'),
                    })
                )
            ]
        )

    def _maillists_node(self, domain):
        """
        :type domain: dict
        :rtype: xml.etree.ElementTree.Element | None
        """
        mail_lists_service = safe_get_struct(domain, 'mail_lists_service')
        if not mail_lists_service:
            return None
        mail_lists = safe_get_list(mail_lists_service, 'mail_lists')
        return elem(
            'maillists', [
                self._maillists_properties_node(mail_lists_service),
            ] + [
                self._maillist_node(mail_list) for mail_list in mail_lists
            ]
        )

    def _maillists_properties_node(self, mail_lists_service):
        """
        :type mail_lists_service: dict
        :rtype: xml.etree.ElementTree.Element
        """
        return elem(
            'properties', [
                self._status_node(safe_get_list(mail_lists_service, 'disabled_by')),
            ]
        )

    def _maillist_node(self, mail_list):
        """
        :type mail_list: dict
        :rtype: xml.etree.ElementTree.Element
        """
        return elem(
            'maillist', [
                self._status_node(safe_get_list(mail_list, 'disabled_by')),
            ] + [
                text_elem('owner', owner) for owner in safe_get_list(mail_list, 'owners')
            ] + [
                self._password_node(mail_list.get('password'), mail_list.get('password_type')),
            ] + [
                text_elem('recipient', subscriber) for subscriber in safe_get_list(mail_list, 'subscribers')
            ], {
                'name': mail_list['name'],
            }
        )

    @staticmethod
    def _convert_to_bytes(value):
        if is_empty(value):
            return -1

        value = value.strip()
        m = re.match('^(\d+)\s*(M|K|)$', value)
        if m is None:
            return -1
        val = m.group(1)
        multiplier = m.group(2)

        if is_empty(multiplier):
            return int(val)
        elif multiplier == 'K':
            return int(val) * 1024
        elif multiplier == 'M':
            return int(val) * 1024 * 1024
        else:
            return -1

    @staticmethod
    def _spamassassin_node(mailbox):
        spamassassin = mailbox.get('spamfilter')
        if isinstance(spamassassin, bool):
            attributes = {
                'status': 'on' if parse_bool_value(spamassassin) else 'off'
            }
            if parse_bool_value(spamassassin):
                attributes['subj-text'] = '***SPAM***'
            return elem(
                'spamassassin', [], attributes
            )
        if isinstance(spamassassin, dict):
            return elem(
                'spamassassin', [
                    text_elem('blacklist-member', member) for member in safe_get_list(spamassassin, 'black_list')
                ] + [
                    text_elem('whitelist-member', member) for member in safe_get_list(spamassassin, 'white_list')
                ] + [
                    text_elem('trusted-locale', locale) for locale in safe_get_list(spamassassin, 'trusted_locales')
                ] + [
                    text_elem('trusted-language', language) for language in safe_get_list(spamassassin, 'trusted_languages')
                ] + [
                    text_elem('trusted-network', network) for network in safe_get_list(spamassassin, 'trusted_networks')
                ], attrs({
                    'status': 'on' if spamassassin.get('enabled') else 'off',
                    'hits': spamassassin.get('sensitivity'),
                    'action': spamassassin.get('action'),
                    'subj-text': spamassassin.get('subject_text'),
                    'alert-text': spamassassin.get('body_text'),
                })
            )
        return None

    @staticmethod
    def _virusfilter_node(mailbox):
        antivirus = mailbox.get('antivirus')
        if antivirus is None:
            return None
        else:
            return elem(
                'virusfilter', [], {
                    'state': antivirus
                }
            )

    @staticmethod
    def _password_node(password, password_type=None):
        if password_type is None:
            password_type = 'plain'
        else:
            password_types_map = {
                'plain': 'plain',
                'sym': 'sym',
                'hash': 'encrypted',
                'crypt': 'encrypted'
            }
            password_type = password_types_map.get(password_type, 'plain')
        if password is None:
            password = generate_random_password()
        return text_elem('password', password, {'type': password_type})

    def _hosting_node(self, domain, ip_addresses, owner):
        """
        :type domain: dict
        :type ip_addresses: list
        :type owner: dict | None
        :rtype: xml.etree.ElementTree.Element | None
        """
        if domain.get('no_web_hosting'):
            return None
        elif domain.get('forwarding_url'):
            return self._forwarding_node(domain, ip_addresses)
        else:
            return self._phosting_node(domain, ip_addresses, owner)

    def _phosting_node(self, domain, ip_addresses, owner):
        """
        :type domain: dict
        :type ip_addresses: list
        :type owner: dict | None
        :rtype: xml.etree.ElementTree.Element
        """
        web_hosting_settings = safe_get_struct(domain, 'web_hosting_settings')
        sys_user = safe_get_dict(domain, 'sys_user')
        scheduled_tasks = safe_get_list(domain, 'scheduled_tasks')
        return elem(
            'phosting', seq(
                elem('preferences', seq(
                    self._sysuser_node(sys_user, scheduled_tasks, owner),
                    self._log_rotation_node(safe_get_struct(domain, 'log_rotation')),
                    self._anonymous_ftp_node(safe_get_struct(domain, 'anonymous_ftp')),
                ) + [
                    self._protected_directory_node(protected_directory) for protected_directory in safe_get_list(domain, 'protected_directories')
                ] + seq(
                    self._performance_node(safe_get_struct(domain, 'performance')),
                    self._hotlink_protection_node(safe_get_struct(domain, 'hotlink_protection')),
                    self._iis_application_pool_node(safe_get_struct(domain, 'iis_application_pool')),
                    self._dynamic_ip_security_node(safe_get_struct(domain, 'dynamic_ip_security')),
                )),
                elem('properties', self._ip_addresses_nodes(ip_addresses)),
                elem('limits-and-permissions', [self._scripting_node(domain)]),
                self._php_settings_node(safe_get_struct(domain, 'php_settings')),
                self._web_settings_node(domain),
                list_elem('applications', [
                    self._aps_application_node(aps_application)
                    for aps_application in safe_get_list(domain, 'aps_applications')
                ]),
                list_elem('webusers', [self._web_user_node(web_user) for web_user in safe_get_list(domain, 'web_users')]),
                list_elem('ftpusers', [self._ftp_user_node(ftp_user) for ftp_user in safe_get_list(domain, 'ftp_users')]),
                self._sites_node(domain)
            ), attrs({
                'www-root': domain.get('target_document_root', 'httpdocs'),
                'webstat': web_hosting_settings.get('webstat') if web_hosting_settings else None,
                'https': xml_bool(web_hosting_settings.get('ssl')) if web_hosting_settings else None,
                'certificate': self._get_certificate_ref(web_hosting_settings, safe_get_list(domain, 'certificates')),
            })
        )

    def _sysuser_node(self, sysuser, scheduled_tasks, owner):
        """
        :type sysuser: dict
        :type scheduled_tasks: list
        :type owner: dict | None
        :rtype: xml.etree.ElementTree.Element
        """
        return elem(
            'sysuser', seq(
                self._password_node(sysuser.get('password'), sysuser.get('password_type')),
                list_elem('scheduled-tasks', [self._scheduled_task_node(scheduled_task, owner) for scheduled_task in scheduled_tasks])
            ), attrs({
                'name': sysuser.get('login', self._random_login('sub')),
                'shell': sysuser.get('shell'),
            })
        )

    def _anonymous_ftp_node(self, anonymous_ftp):
        """
        :type anonymous_ftp: dict | None
        :rtype: xml.etree.ElementTree.Element | None
        """
        if not anonymous_ftp:
            return None
        return elem(
            'anonftp', seq(
                text_elem('anonftp-limit', anonymous_ftp.get('disk_quota'), {'name': 'incoming-disk-quota'}),
                text_elem('anonftp-limit', anonymous_ftp.get('max_connections'), {'name': 'max-connections'}),
                text_elem('anonftp-limit', anonymous_ftp.get('bandwidth'), {'name': 'bandwidth'}),
                elem('anonftp-permission', [], {'name': 'incoming-mkdir'}) if anonymous_ftp.get('allow_directories_creation') else None,
                elem('anonftp-permission', [], {'name': 'incoming-download'}) if anonymous_ftp.get('allow_downloading') else None,
                text_elem('login-message', anonymous_ftp.get('login_message')),
            ), attrs({
                'pub': xml_bool(anonymous_ftp.get('enabled')),
                'incoming': xml_bool(anonymous_ftp.get('allow_uploading')),
                'display-login': xml_bool(anonymous_ftp.get('display_login_message')),
            })
        )

    def _protected_directory_node(self, protected_directory):
        """
        :type protected_directory: dict
        :rtype: xml.etree.ElementTree.Element
        """
        return elem(
            'pdir', [
                self._protected_directory_user_node(protected_directory_user) for protected_directory_user in safe_get_list(protected_directory, 'users')
            ], attrs({
                'name': protected_directory['path'],
                'title': protected_directory.get('title'),
            })
        )

    def _protected_directory_user_node(self, protected_directory_user):
        """
        :type protected_directory_user: dict
        :rtype: xml.etree.ElementTree.Element
        """
        return elem(
            'pduser', [
                self._password_node(protected_directory_user.get('password'), protected_directory_user.get('password_type')),
            ], {
                'name': protected_directory_user['login'],
            }
        )

    def _performance_node(self, performance):
        """
        :type performance: dict | None
        :rtype: xml.etree.ElementTree.Element | None
        """
        if not performance:
            return None
        return elem(
            'perfomance', [
                text_elem('max-connections', performance['max_connections']),
                text_elem('traffic-bandwidth', performance['bandwidth']),
            ]
        )

    def _hotlink_protection_node(self, hotlink_protection):
        """
        :type hotlink_protection: dict | None
        :rtype: xml.etree.ElementTree.Element | None
        """
        if not hotlink_protection:
            return None
        return elem(
            'hotlink-protection', [
                text_elem('extension', extension) for extension in safe_get_list(hotlink_protection, 'extensions')
            ] + [
                text_elem('friend', friend) for friend in safe_get_list(hotlink_protection, 'friends')
            ], attrs({
                'enabled': xml_bool(hotlink_protection.get('enabled'))
            })
        )

    def _dynamic_ip_security_node(self, dynamic_ip_security):
        """
        :type dynamic_ip_security: dict | None
        :rtype: xml.etree.ElementTree.Element | None
        """
        if not dynamic_ip_security:
            return None
        return elem(
            'dynamic-ip-security', [], {
                'deny-by-concurrent-requests': xml_bool(dynamic_ip_security['deny_by_concurrent_requests']),
                'max-concurrent-requests': dynamic_ip_security['max_concurrent_requests'],
                'deny-by-request-rate': xml_bool(dynamic_ip_security['deny_by_request_rate']),
                'max-requests': dynamic_ip_security['max_requests'],
                'request-interval': dynamic_ip_security['request_interval'],
            }
        )

    def _scheduled_task_node(self, scheduled_task, owner):
        """
        :type scheduled_task: dict
        :type owner: dict | None
        :rtype: xml.etree.ElementTree.Element
        """
        notification_map = {
            'do_not_notify': 'ignore',
            'errors_only': 'errors',
            'every_time': 'always',
        }
        periods_map = {
            'cron_style': '0',
            'hourly': '3600',
            'daily': '86400',
            'weekly': '604800',
            'monthly': '2592000',
            'yearly': '31536000',
        }
        email = scheduled_task.get('notifications_email', '')
        if email:
            owner_email = None
            if owner and safe_get_dict(owner, 'contact_info'):
                owner_email = safe_get_dict(owner, 'contact_info').get('email')
            email_type = 'owner' if email == owner_email else 'custom'
        else:
            email_type = 'default'
        return elem('scheduled-task', [], attrs({
            'is-active': xml_bool(scheduled_task.get('is_active', True)),
            'type': scheduled_task['type'],
            'php-handler-id': scheduled_task.get('php_handler_id'),
            'php-version': scheduled_task.get('php_version'),
            'command': scheduled_task['command'],
            'arguments': scheduled_task.get('command_arguments', ''),
            'description': scheduled_task.get('description'),
            'notify': notification_map.get(scheduled_task.get('notify'), 'ignore'),
            'email-type': email_type,
            'email': email,
            'minute': scheduled_task.get('minute', '00'),
            'hour': scheduled_task.get('hour', '00'),
            'day-of-month': scheduled_task.get('day_of_month', '*'),
            'month': scheduled_task.get('month', '*'),
            'day-of-week': scheduled_task.get('day_of_week', '*'),
            'period': periods_map.get(scheduled_task.get('period'), '86400'),
        }))

    def _aps_application_node(self, aps_application):
        """
        :type aps_application: dict
        :rtype: xml.etree.ElementTree.Element
        """
        installation_directory = safe_get_struct(aps_application, 'installation_directory')
        context = aps_application.get('context')
        return elem(
            'sapp-installed', [
                elem(
                    'sapp-spec', seq(
                        text_elem('sapp-package-id', aps_application.get('package_id')),
                        text_elem('sapp-name', aps_application['name']),
                        text_elem('sapp-version', aps_application.get('version')),
                        text_elem('sapp-release', aps_application.get('release')),
                        text_elem('sapp-description', aps_application.get('description')),
                        text_elem('sapp-commercial', xml_bool(aps_application.get('commercial'))),
                        text_elem('sapp-integrated', xml_bool(aps_application.get('integrated'))),
                    )
                )
            ] + [
                self._database_node(HostingDescriptionDatabase(database))
                for database in safe_get_list(aps_application, 'databases')
            ] + [
                elem(
                    'sapp-installdir', seq(
                        text_elem('sapp-prefix', installation_directory['path']),
                        elem('sapp-ssl') if installation_directory.get('ssl') else None,
                        text_elem('aps-registry-id', installation_directory.get('aps_registry_id')),
                    )
                )
            ] + [
                self._aps_application_entry_point_node(entry_point) for entry_point in safe_get_list(aps_application, 'entry_points')
            ] + seq(
                elem('context', [], {'type': context}) if context else None,
                text_elem('aps-registry-id', aps_application.get('aps_registry_id')),
                text_elem('sapp-apsc', aps_application.get('controller_dump')),
                self._aps_application_license_node(aps_application),
                self._aps_application_settings_node(aps_application),
            )
        )

    def _aps_application_entry_point_node(self, entry_point):
        """
        :type entry_point: dict
        :rtype: xml.etree.ElementTree.Element
        """
        permission = safe_get_struct(entry_point, 'permission')
        return elem(
            'sapp-entry-point', seq(
                text_elem('label', entry_point['label']),
                text_elem('description', entry_point['description']),
                text_elem('hidden', xml_bool(entry_point.get('hidden'))),
                text_elem('http', xml_bool(entry_point.get('http'))),
                text_elem('icon', entry_point.get('icon')),
                elem(
                    'limits-and-permissions', [
                        elem('permission', [], {'name': permission['name'], 'class': permission['class']})
                    ]
                )
            )
        )

    def _aps_application_license_node(self, aps_application):
        """
        :type aps_application: dict
        :rtype: xml.etree.ElementTree.Element | None
        """
        aps_application_license = safe_get_struct(aps_application, 'license')
        if not aps_application_license:
            return None
        return elem(
            'sapp-license', seq(
                text_elem('aps-registry-id', aps_application_license.get('aps_registry_id')),
                text_elem('license-type', aps_application_license['type']),
                text_elem('activation-code', aps_application_license['activation_code']),
                text_elem('use-stub', xml_bool(aps_application_license.get('use_stub'))),
            )
        )

    def _aps_application_settings_node(self, aps_application):
        """
        :type aps_application: dict
        :rtype: xml.etree.ElementTree.Element | None
        """
        settings = {}
        force_updates = xml_bool(aps_application.get('force_updates'))
        if force_updates:
            settings['forceUpdates'] = base64.b64encode(force_updates.encode('utf-8'))
        return list_elem(
            'sapp-settings', [
                elem(
                    'setting', [
                        text_elem('name', name),
                        text_elem('value', value),
                    ]
                )
                for name, value in settings.items()
            ]
        )

    def _web_user_node(self, web_user):
        """
        :type web_user: dict
        :rtype: xml.etree.ElementTree.Element
        """
        return elem(
            'webuser', seq(
                elem(
                    'sysuser', [
                        self._password_node(web_user.get('password'), web_user.get('password_type'))
                    ], attrs({
                        'name': web_user['login'],
                        'quota': web_user.get('disk_quota'),
                    })
                ),
                elem(
                    'scripting', [], attrs({
                        'php': xml_bool(web_user.get('php')),
                        'asp_dot_net': xml_bool(web_user.get('asp_dot_net')),
                        'write_modify': xml_bool(web_user.get('write_modify')),
                        'cgi': xml_bool(web_user.get('cgi')),
                        'fastcgi': xml_bool(web_user.get('fastcgi')),
                    })
                ),
            ), attrs({
                'name': web_user['login'],
            })
        )

    def _ftp_user_node(self, ftp_user):
        """
        :type ftp_user: dict
        :rtype: xml.etree.ElementTree.Element
        """
        return elem(
            'ftpuser', seq(
                elem(
                    'sysuser', [
                        self._password_node(ftp_user.get('password'), ftp_user.get('password_type'))
                    ], attrs({
                        'name': ftp_user['login'],
                        'home': ftp_user.get('home_directory'),
                        'quota': ftp_user.get('disk_quota'),
                    })
                ),
                text_elem('permission', 'download') if ftp_user.get('allow_read') else None,
                text_elem('permission', 'upload') if ftp_user.get('allow_write') else None,
            ), attrs({
                'name': ftp_user['login'],
            })
        )

    def _scripting_node(self, domain):
        """
        :type domain: dict
        :rtype: xml.etree.ElementTree.Element | None
        """
        attributes = {}
        if self._is_windows:
            # By default allow write/modify of files inside document root for scripts of web sites.
            # Setting this option is required to migrate sites that rely on write/modify permission,
            # for example sites which have cache on filesystem, which is a common case for custom migrations.
            # In general it is safe to use it on modern Plesk (>= 12.5) when
            # dedicated IIS pools are used (which is by default).
            # The option should be specified in explicit way if you want to disallow write/modify
            # permissions.
            attributes['write_modify'] = xml_bool(True)

        web_hosting_settings = safe_get_struct(domain, 'web_hosting_settings')
        if web_hosting_settings:
            attributes.update(attrs({
                'ssi': xml_bool(web_hosting_settings.get('ssi')),
                'ssi_html': xml_bool(web_hosting_settings.get('ssi_html')),
                'php': xml_bool(web_hosting_settings.get('php')),
                'php_handler_id': web_hosting_settings.get('php_handler_id'),
                'php_version': web_hosting_settings.get('php_version'),
                'php_handler_type': web_hosting_settings.get('php_handler_type'),
                'cgi': xml_bool(web_hosting_settings.get('cgi')),
                'fastcgi': xml_bool(web_hosting_settings.get('fastcgi')),
                'perl': xml_bool(web_hosting_settings.get('perl')),
                'python': xml_bool(web_hosting_settings.get('python')),
                'asp': xml_bool(web_hosting_settings.get('asp')),
                'asp_dot_net': xml_bool(web_hosting_settings.get('asp_dot_net')),
                'managed_runtime_version': web_hosting_settings.get('managed_runtime_version'),
                'webdeploy': xml_bool(web_hosting_settings.get('webdeploy')),
            }))

            if web_hosting_settings.get('write_modify') is not None:
                if web_hosting_settings.get('write_modify'):
                    attributes['write_modify'] = 'true'
                else:
                    attributes['write_modify'] = ''

        return elem('scripting', [], attributes)

    @staticmethod
    def _random_login(prefix):
        random_digits = "".join(random.choice(string.digits) for _ in range(10))
        return "%s_%s" % (prefix, random_digits,)

    def _sites_node(self, domain):
        """
        :type domain: dict
        :rtype: xml.etree.ElementTree.Element | None
        """
        addon_domains = safe_get_list(domain, 'addon_domains')
        subdomains = safe_get_list(domain, 'subdomains')
        if len(addon_domains) + len(subdomains) == 0:
            return None
        else:
            return elem(
                'sites', [
                    self._addon_domain_node(addon_domain) for addon_domain in addon_domains
                ] + [
                    self._subdomain_node(domain, subdomain) for subdomain in subdomains
                ]
            )

    def _addon_domain_node(self, addon_domain):
        """
        :type addon_domain: dict
        :rtype: xml.etree.ElementTree.Element
        """
        return elem(
            'site', seq(
                self._domain_preferences_node(addon_domain),
                self._site_properties_node(addon_domain),
                self._mailsystem_node(addon_domain),
                self._maillists_node(addon_domain),
                self._certificates_node(addon_domain),
                self._site_hosting_node(addon_domain, safe_get_list(addon_domain, 'certificates')),
            ), attrs({
                'guid': addon_domain.get('guid', self._get_guid('domain', addon_domain['name'])),
                'www': 'true',
                'name': addon_domain['name'],
                'cr-date': addon_domain.get('creation_date'),
            })
        )

    def _subdomain_node(self, domain, subdomain):
        """
        :type domain: dict
        :type subdomain: dict
        :rtype: xml.etree.ElementTree.Element
        """
        parent_domain = self._get_parent(domain, subdomain, subdomain.get('parent_domain_name'))
        return elem(
            'site', seq(
                elem('preferences'),
                self._site_properties_node(subdomain),
                self._site_hosting_node(subdomain, safe_get_list(parent_domain, 'certificates')),
            ), attrs({
                'guid': subdomain.get('guid', self._get_guid('domain', subdomain['name'])),
                # wildcard subdomains (*.<domain>) can not have www alias enabled
                'www': 'true' if not subdomain['name'].startswith('*') else 'false',
                'name': subdomain['name'],
                'parent-domain-name': parent_domain['name'],
                'cr-date': subdomain.get('creation_date'),
            })
        )

    @staticmethod
    def _get_parent(domain, subdomain, parent_domain_name):
        """
        :type domain: dict
        :type subdomain: dict
        :type parent_domain_name: str | unicode | None
        :rtype: dict
        """
        all_domains = [domain] + [addon for addon in safe_get_list(domain, 'addon_domains')]
        if parent_domain_name:
            for current_domain in all_domains:
                if parent_domain_name == current_domain['name']:
                    return current_domain
        for current_domain in all_domains:
            if subdomain['name'].endswith('.%s' % current_domain['name']):
                return current_domain

    def _site_hosting_node(self, site, certificates):
        """
        :type site: dict
        :rtype: xml.etree.ElementTree.Element | None
        """
        if site.get('no_web_hosting'):
            return None
        elif site.get('forwarding_url'):
            return self._forwarding_node(site, [])
        else:
            return self._site_phosting_node(site, certificates)

    def _site_phosting_node(self, site, certificates):
        """
        :type site: dict
        :type certificates: list
        :rtype: xml.etree.ElementTree.Element
        """
        web_hosting_settings = safe_get_struct(site, 'web_hosting_settings')
        return elem(
            'phosting', seq(
                elem('preferences', seq(
                    self._log_rotation_node(safe_get_struct(site, 'log_rotation')),
                ) + [
                    self._protected_directory_node(protected_directory) for protected_directory in safe_get_list(site, 'protected_directories')
                ] + seq(
                    self._performance_node(safe_get_struct(site, 'performance')),
                    self._hotlink_protection_node(safe_get_struct(site, 'hotlink_protection')),
                    self._iis_application_pool_node(safe_get_struct(site, 'iis_application_pool')),
                    self._dynamic_ip_security_node(safe_get_struct(site, 'dynamic_ip_security')),
                )),
                elem('limits-and-permissions', [self._scripting_node(site)]),
                self._php_settings_node(safe_get_struct(site, 'php_settings')),
                self._web_settings_node(site),
                list_elem('applications', [
                    self._aps_application_node(aps_application)
                    for aps_application in safe_get_list(site, 'aps_applications')
                ]),
            ), attrs({
                'www-root': self._get_site_document_root(site),
                'webstat': web_hosting_settings.get('webstat') if web_hosting_settings else None,
                'https': xml_bool(web_hosting_settings.get('ssl')) if web_hosting_settings else None,
                'certificate': self._get_certificate_ref(web_hosting_settings, certificates),
            })
        )

    @staticmethod
    def _get_site_document_root(site):
        """
        :type site: dict
        :rtype: str|unicode
        """
        return site.get('target_document_root', site['name'].encode('idna'))

    def _databases_node(self, subscription):
        """
        :type subscription: parallels.plesk.hosting_description.model.HostingDescriptionSubscription
        :rtype: xml.etree.ElementTree.Element | None
        """
        databases = [database for database in subscription.iter_databases()]
        database_users = [database_user for database_user in subscription.iter_database_users()]

        if not databases and not database_users:
            return None

        return elem(
            'databases', [
                self._database_node(database) for database in databases
            ] + seq(
                list_elem('dbusers', [
                    self._database_user_node(database_user) for database_user in database_users
                ])
            )
        )

    def _database_node(self, database):
        """
        :type database: parallels.plesk.hosting_description.model.HostingDescriptionDatabase
        :rtype: xml.etree.ElementTree.Element
        """
        if database.server_id is not None:
            database_server = self._get_database_server(database.server_id)
        else:
            database_server = database.server

        if database_server is None:
            # We don't have information about source database server, so we should not try
            # to copy database content in a regular way by connecting to the source database server.
            # So we mark that in backup dump by special constant 'DATABASE_NO_SOURCE_HOST'.
            database_server = HostingDescriptionDatabaseServer(dict(
                type=database.db_type,
                host=DATABASE_NO_SOURCE_HOST,
                port=0
            ))

        return elem(
            'database', [
                self._database_server_node(database_server),
            ] + [
                self._database_user_node(database_user)
                for database_user in database.iter_database_users()
            ] + seq(
                self._related_sites_node(database.related_domain_name),
            ), attrs({
                'name': database.name,
                'type': database.db_type,
                'version': database.version,
                'charset': database.charset,
                'collation': database.collation,
                'aps-registry-id': database.aps_registry_id,
            })
        )

    def _database_user_node(self, database_user):
        """
        :type database_user: parallels.plesk.hosting_description.model.HostingDescriptionDatabaseUser
        :rtype: xml.etree.ElementTree.Element
        """
        if database_user.server_id is not None:
            database_server = self._get_database_server(database_user.server_id)
        else:
            database_server = database_user.server
        return elem(
            'dbuser', seq(
                self._password_node(database_user.password, database_user.password_type),
                self._database_server_node(database_server) if database_server is not None else None,
            ), {
                'name': database_user.username,
            }
        )

    def _related_sites_node(self, related_domain_name):
        """
        :type related_domain_name: str | unicode | None
        :rtype: xml.etree.ElementTree.Element | None
        """
        if not related_domain_name:
            return None
        return elem(
            'related-sites', [
                elem('related-site', [], {'name': related_domain_name})
            ]
        )

    def _certificates_node(self, domain):
        """
        :type domain: dict
        :rtype: xml.etree.ElementTree.Element | None
        """
        certificates = safe_get_list(domain, 'certificates')
        return list_elem(
            'certificates', [
                self._certificate_node(certificate) for certificate in certificates
            ]
        )

    def _certificate_node(self, certificate):
        """
        :type certificate: dict
        :rtype: xml.etree.ElementTree.Element
        """
        return elem(
            'certificate', seq(
                text_elem(
                    'certificate-data',
                    certificate['certificate'].replace('\r', '') if certificate.get('certificate') else None
                ),
                text_elem(
                    'signing-request',
                    certificate['signing_request'].replace('\r', '') if certificate.get('signing_request') else None
                ),
                text_elem(
                    'ca-certificate',
                    certificate['ca_certificate'].replace('\r', '') if certificate.get('ca_certificate') else None
                ),
                text_elem(
                    'private-key',
                    certificate['private_key'].replace('\r', '') if certificate.get('private_key') else None
                ),
            ), {
                'name': certificate['name'],
            }
        )

    def _forwarding_node(self, domain, ip_addresses):
        """
        :type domain: dict
        :type ip_addresses: list
        :rtype: xml.etree.ElementTree.Element
        """
        if domain.get('forwarding_type') == ForwardingType.FRAME_FORWARDING:
            return elem(
                'fhosting', seq(
                    text_elem('url', domain['forwarding_url']),
                    list_elem('properties', self._ip_addresses_nodes(ip_addresses)),
                )
            )
        elif domain.get('forwarding_type') == ForwardingType.MOVED_TEMPORARILY:
            return elem(
                'shosting', seq(
                    text_elem('url', domain['forwarding_url']),
                    list_elem('properties', self._ip_addresses_nodes(ip_addresses)),
                    text_elem('httpcode', '302'),
                )
            )
        else:
            return elem(
                'shosting', seq(
                    text_elem('url', domain['forwarding_url']),
                    list_elem('properties', self._ip_addresses_nodes(ip_addresses)),
                    text_elem('httpcode', '301'),
                )
            )

    def _ip_pool_node(self, ip_addresses):
        """
        :type ip_addresses: list
        :rtype: xml.etree.ElementTree.Element
        """
        return elem('ip_pool', self._ip_addresses_nodes(ip_addresses))

    def _ip_addresses_nodes(self, ip_addresses):
        """
        :type ip_addresses: list
        :rtype: list[xml.etree.ElementTree.Element]
        """
        return [
            elem(
                'ip', [
                    text_elem('ip-type', ip_address['type']),
                    text_elem('ip-address', ip_address['address']),
                ]
            )
            for ip_address in ip_addresses
        ]

    def _log_rotation_node(self, log_rotation):
        """
        :type log_rotation: dict | None
        :rtype: xml.etree.ElementTree.Element | None
        """
        if not log_rotation:
            return None
        if log_rotation['period_type'] == 'by_time':
            period_node = elem('logrotation-period', [], {'period': log_rotation['period']})
        else:
            period_node = text_elem('logrotation-maxsize', log_rotation['period'])
        return elem('logrotation', [
            period_node
        ], {
            'max-number-of-logfiles': log_rotation['max_number_of_log_files'],
            'compress': xml_bool(log_rotation['compress']),
            'email': log_rotation['email'],
            'enabled': xml_bool(log_rotation['enabled']),
        })

    def _applications_filter_node(self, applications_filter):
        """
        :type applications_filter: dict
        :rtype: xml.etree.ElementTree.Element
        """
        return elem('aps-bundle', [
            elem('filter', [
                elem('item', [
                    text_elem('name', 'name'),
                    text_elem('value', name),
                ])
                for name in applications_filter['names']
            ], {
                'type': applications_filter['type'],
            })
        ])

    def _admin_descriptions_node(self, data):
        """
        :type data: dict
        :rtype: xml.etree.ElementTree.Element | None
        """
        descriptions_nodes = []
        for reseller in safe_get_list(data, 'resellers'):
            self._add_description_node(descriptions_nodes, reseller['login'], 'reseller', reseller.get('description'))
        for customer in safe_get_list(data, 'customers'):
            self._add_description_node(descriptions_nodes, customer['login'], 'client', customer.get('description'))
        for subscription in iter_all_subscriptions(data):
            self._add_description_node(descriptions_nodes, subscription['name'], 'domain', subscription.get('admin_description'))
        return elem('descriptions', descriptions_nodes) if descriptions_nodes else None

    def _reseller_descriptions_node(self, reseller):
        """
        :type reseller: dict
        :rtype: xml.etree.ElementTree.Element | None
        """
        descriptions_nodes = []
        for customer in safe_get_list(reseller, 'customers'):
            self._add_description_node(descriptions_nodes, customer['login'], 'client', customer.get('description'))
        for subscription in iter_all_subscriptions(reseller):
            self._add_description_node(descriptions_nodes, subscription['name'], 'domain', subscription.get('reseller_description'))
        return elem('descriptions', descriptions_nodes) if descriptions_nodes else None

    def _subscription_descriptions_node(self, subscription):
        """
        :type subscription: dict
        :rtype: xml.etree.ElementTree.Element | None
        """
        descriptions_nodes = []
        self._add_description_node(descriptions_nodes, subscription['name'], 'domain', subscription.get('description'))
        return elem('descriptions', descriptions_nodes) if descriptions_nodes else None

    def _mail_user_descriptions_node(self, mail_user):
        """
        :type mail_user: dict
        :rtype: xml.etree.ElementTree.Element | None
        """
        descriptions_nodes = []
        self._add_description_node(descriptions_nodes, mail_user['name'], 'mailuser', mail_user.get('description'))
        return elem('descriptions', descriptions_nodes) if descriptions_nodes else None

    def _add_description_node(self, descriptions_nodes, object_name, object_type, object_description):
        """
        :type descriptions_nodes: list
        :type object_name: str|unicode
        :type object_type: str|unicode
        :type object_description: str|unicode|None
        """
        if object_description is not None:
            descriptions_nodes.append(text_elem(
                'description',
                object_description,
                {'object-name': object_name, 'object-type': object_type}
            ))

    def _php_settings_node(self, php_settings):
        """
        :type php_settings: dict
        :rtype: xml.etree.ElementTree.Element | None
        """
        if not php_settings:
            return None
        return elem('php-settings', (
            [text_elem('notice-text', php_settings.get('notice'))] if php_settings.get('notice') is not None else []
        ) + [
            elem('setting', [
                text_elem('name', name),
                text_elem('value', value),
            ])
            for name, value in php_settings['settings'].items()
        ])

    def _web_settings_node(self, domain):
        """
        :type domain: dict
        :rtype: xml.etree.ElementTree.Element | None
        """
        web_server_settings = safe_get_dict(domain, 'web_server_settings')
        return list_elem(
            'web-settings', [
                elem(
                    'setting', [
                        text_elem('name', name),
                        text_elem('value', value, {'encoding': 'base64'}),
                    ]
                )
                for name, value in web_server_settings.items()
            ], {
                'vhost-name': domain['name'],
            }
        )

    def _custom_button_node(self, custom_button):
        """
        :type custom_button: dict
        :rtype: xml.etree.ElementTree.Element
        """
        url_options = safe_get_list(custom_button, 'url_options')
        return elem('custom-button', [
            elem('url-option', [], {'name': url_option})
            for url_option in url_options
        ] if url_options else [], attrs({
            'url': custom_button['url'],
            'text': custom_button['label'],
            'sort-priority': custom_button['priority'],
            'interface-place': custom_button['location'],
            'conhelp': custom_button.get('tooltip_text'),
            'open-in-same-frame': xml_bool(custom_button['open_in_same_frame']),
            'visible-to-sublogins': xml_bool(custom_button['visible_to_sublogins']),
            'no-frame': xml_bool(custom_button.get('no_frames')),
        }))

    def _iis_application_pool_node(self, iis_application_pool):
        """
        :type iis_application_pool: dict | None
        :rtype: xml.etree.ElementTree.Element | None
        """
        if not iis_application_pool:
            return None
        if not iis_application_pool['turned_on']:
            return elem('iis-application-pool', [], {'turned-on': 'false'})
        return elem('iis-application-pool', seq(
            elem('iis-app-pool-cpu-usage-monitoring', [], attrs({
                'state': 'false',
                'value': iis_application_pool.get('cpu_limit'),
                'action': iis_application_pool.get('cpu_limit_action'),
                'interval': iis_application_pool.get('cpu_limit_interval'),
            })),
            elem('iis-app-pool-recycle', [], attrs({
                'by-time': iis_application_pool.get('recycling_by_time'),
                'by-requests': iis_application_pool.get('recycling_by_requests'),
                'by-virtual-memory': iis_application_pool.get('recycling_by_virtual_memory'),
                'by-private-memory': iis_application_pool.get('recycling_by_private_memory'),
            })),
            elem('iis-app-pool-idle', [], attrs({
                'timeout': iis_application_pool.get('idle_timeout'),
                'timeout-action': iis_application_pool.get('idle_timeout_action'),
            })),
            text_elem('iis-app-pool-max-processes', iis_application_pool.get('max_processes')),
            text_elem('iis-app-pool-managed-pipeline-mode', iis_application_pool.get('managed_pipeline_mode')),
        ), attrs({
            'turned-on': xml_bool(iis_application_pool['turned_on']),
            'identity': iis_application_pool.get('identity'),
        }))

    def _subscription_info_node(self, subscription_info, subscription_properties=None):
        """
        :type subscription_info: dict | None
        :type subscription_properties: dict | None
        :rtype: xml.etree.ElementTree.Element | None
        """
        if not subscription_info and not subscription_properties:
            return None
        subscription_info = default(subscription_info, {})
        return elem(
            'subscription', [
                elem(
                    'plan', [], attrs({
                        'plan-guid': plan_info['guid'],
                        'quantity': plan_info.get('quantity', '1'),
                        'is-addon': xml_bool(plan_info.get('is_addon')),
                    })
                )
                for plan_info in safe_get_list(subscription_info, 'plans')
            ] + seq(
                self._subscription_properties_node(subscription_properties)
            ), attrs({
                'locked': xml_bool(subscription_info.get('locked')),
                'synchronized': xml_bool(subscription_info.get('synchronized')),
                'custom': xml_bool(subscription_info.get('custom')),
            })
        )

    def _subscription_properties_node(self, properties):
        """
        :type properties: dict | None
        :rtype: xml.etree.ElementTree.Element | None
        """
        properties = default(properties, {})
        return list_elem(
            'properties', [
                elem(
                    'property', [
                        text_elem('name', name),
                        text_elem('value', base64.b64encode(value.encode('utf-8'))),
                    ]
                )
                for name, value in properties.items() if value is not None
            ]
        )

    def _default_db_servers_node(self, default_db_servers_data):
        """
        :type default_db_servers_data: list
        :rtype: xml.etree.ElementTree.Element
        """
        default_db_servers = []
        for default_db_server_data in default_db_servers_data:
            default_db_servers.append(HostingDescriptionDatabaseServer(default_db_server_data))
        return elem('default-db-servers', [
            self._database_server_node(db_server)
            for db_server in default_db_servers
        ])

    def _get_certificate_ref(self, web_hosting_settings, certificates):
        """
        :type web_hosting_settings: dict | None
        :type certificates: list
        :rtype: str | unicode | None
        """
        if not web_hosting_settings:
            return None
        certificate_name = web_hosting_settings.get('ssl_certificate')
        if not certificate_name:
            return None
        for certificate in certificates:
            if certificate_name == certificate['name'] and certificate.get('private_key'):
                return hashlib.md5(certificate['private_key'].replace('\r', '')).hexdigest()
        return None

    def _save_dump(self, contents, domain_name=None, client_name=None, reseller_name=None, guid=None):
        """
        :type contents: str | unicode
        :type reseller_name: str | unicode | None
        :type client_name: str | unicode | None
        :type domain_name: str | unicode | None
        """
        self._dump_writer.add_dump(contents, domain_name, client_name, reseller_name, guid)

    # noinspection PyUnusedLocal
    @cached
    def _get_guid(self, entity_type, entity_name):
        return guid_str()


def read_hosting_description_file(hosting_description_config, ignore_binary_data=True):
    """
    :type hosting_description_config: parallels.plesk.hosting_description.config.HostingDescriptionConfig
    """
    input_formats_by_name = group_by_id(INPUT_FORMATS, lambda f: f.name)
    if hosting_description_config.file_format not in input_formats_by_name:
        raise MigrationNoContextError(
            messages.INVALID_FILE_FORMAT % hosting_description_config.path
        )
    file_format = input_formats_by_name[hosting_description_config.file_format]
    try:
        data = file_format.read(hosting_description_config.path)
        if ignore_binary_data:
            data = _filter_out_binary_data(data)
        return data
    except IOError as e:
        logger.exception()
        if e.errno == errno.ENOENT:
            raise MigrationNoContextError(
                messages.HOSTING_DESCRIPTION_FILE_DOES_NOT_EXIST.format(filename=hosting_description_config.path)
            )
        else:
            raise MigrationNoContextError(
                safe_format(
                    messages.HOSTING_DESCRIPTION_FILE_READ_IO_ERROR,
                    filename=hosting_description_config.path, reason=e
                )
            )
    except Exception as e:
        logger.exception()
        raise MigrationNoContextError(
            safe_format(
                messages.HOSTING_DESCRIPTION_FILE_READ_ERROR,
                filename=hosting_description_config.path, reason=e,
                format=file_format.name.upper()
            )
        )


def _filter_out_binary_data(data):
    """Remove (replace with empty strings) all binary non-ASCII strings from specified object recursively

    :type data: list | dict | tuple | str | unicode | None
    :rtype: list | dict | tuple | str | unicode | None
    """
    if sys.version_info.major >= 3:
        str_type = str
        bytes_type = bytes
    else:
        str_type = unicode
        bytes_type = str
    if isinstance(data, list):
        return [_filter_out_binary_data(item) for item in data]
    elif isinstance(data, tuple):
        return (_filter_out_binary_data(item) for item in data)
    elif isinstance(data, dict):
        return {_filter_out_binary_data(key): _filter_out_binary_data(value) for key, value in data.items()}
    elif isinstance(data, str_type) or isinstance(data, bytes_type):
        if is_unicode_or_ascii_string(data):
            return data
        else:
            return ''
    else:
        return data


def guid_str():
    return str(uuid.uuid1())
