--- admin/plib/modules/panel-migrator/backend/lib/python/parallels/core/converter/dns.py +++ admin/plib/modules/panel-migrator/backend/lib/python/parallels/core/converter/dns.py @@ -2,8 +2,8 @@ import itertools from collections import defaultdict from parallels.core import messages -from parallels.core.utils.common import split_string_by_whitespace_chars -from parallels.core.utils.common.ip import is_ipv4, is_ipv6 +from parallels.core.utils.common import split_string_by_whitespace_chars, is_empty +from parallels.core.utils.common.ip import is_ipv4, is_ipv6, is_ip from parallels.core.utils.common.logging import create_safe_logger from parallels.core.utils.entity import Entity from parallels.core.utils.migrator_utils import safe_idn_decode, is_unicode_domain @@ -38,7 +38,7 @@ class Rec(Entity): return hash((self._rec_type, self._src, self._dst, self._opt)) -def replace_resource_record_ips(subscription): +def replace_dns_record_ips(subscription, target_dns_template=None): """Replace source panel IPs with target IPs in subscription DNS zone data. Assuming, that each source subscription has a single assigned IP address of @@ -46,20 +46,22 @@ def replace_resource_record_ips(subscription): new ones of the target server. :type subscription: parallels.core.migrated_subscription.MigratedSubscription + :type target_dns_template: parallels.core.hosting_repository.panel.DNSTemplate + :rtype: None """ - target_ips = [] + target_web_ips = [] if subscription.target_public_web_ipv4 is not None: - target_ips.append(subscription.target_public_web_ipv4) + target_web_ips.append(subscription.target_public_web_ipv4) if subscription.target_public_web_ipv6 is not None: - target_ips.append(subscription.target_public_web_ipv6) + target_web_ips.append(subscription.target_public_web_ipv6) - if len(target_ips) == 0: + if len(target_web_ips) == 0: # Subscription has no web hosting, try to take IP of mail hosting. # In most cases that should work fine. if subscription.target_public_mail_ipv4 is not None: - target_ips.append(subscription.target_public_mail_ipv4) + target_web_ips.append(subscription.target_public_mail_ipv4) - if len(target_ips) == 0: + if len(target_web_ips) == 0: logger.ferror( messages.SUBSCRIPTION_HAS_NO_IP_ADDRESSES_TARGET, subscription=subscription.name ) @@ -79,20 +81,36 @@ def replace_resource_record_ips(subscription): dump_dns_zone = domain.dns_zone dns_zone = UniqueDnsZoneAdapter(dump_dns_zone) - source_ips = [ + source_ips = { subscription.converted_dump.ip, subscription.converted_dump.ipv6, - ] + } if domain.name in subscription_domain_names: raw_domain = subscription.raw_dump.get_domain(domain.name) - source_ips.extend([ + source_ips.update({ raw_domain.web_ips.v4, raw_domain.web_ips.v6 - ]) + }) + if subscription.mail_source_server is not None: + source_mail_server_ip = subscription.mail_source_server.ip() + if not is_empty(source_mail_server_ip): + source_ips.add(source_mail_server_ip) + + target_mail_ips = _detect_target_mail_ips(target_web_ips, target_dns_template) + + mx_record_dests = [ + rec.dst.lower() for rec in dump_dns_zone.iter_dns_records() if rec.rec_type == 'MX' + ] for rec in list(dump_dns_zone.iter_dns_records()): if rec.dst in source_ips: dns_zone.remove_dns_record(rec) + + if rec.src.lower() in mx_record_dests: # A record which corresponds to MX record + target_ips = target_mail_ips + else: + target_ips = target_web_ips + for new_ip in target_ips: rec_type = rec.rec_type if rec_type in ('A', 'AAAA'): @@ -149,6 +167,25 @@ def replace_resource_record_ips(subscription): )) +def _detect_target_mail_ips(target_web_ips, target_dns_template): + """ + :type target_web_ips: list[str | unicode] + :type target_dns_template: parallels.core.hosting_repository.panel.DNSTemplate + :rtype: list[str | unicode] + """ + target_mail_ips = target_web_ips + + if target_dns_template is not None: + mx_records = [rec for rec in target_dns_template.dns_records if rec.rec_type == 'MX'] + a_records = {rec.src.lower(): rec for rec in target_dns_template.dns_records if rec.rec_type in ('A', 'AAAA')} + target_mail_templates = [a_records[rec.dst.lower()].dst for rec in mx_records if rec.dst in a_records] + + if all(is_ip(s) for s in target_mail_templates): + target_mail_ips = target_mail_templates + + return target_mail_ips + + def _convert_subdomain_form(domain, subdomain): # Subdomain already ends with domain name - leave it as is if ( --- admin/plib/modules/panel-migrator/backend/lib/python/parallels/core/hosting_repository/panel.py +++ admin/plib/modules/panel-migrator/backend/lib/python/parallels/core/hosting_repository/panel.py @@ -1,3 +1,6 @@ +from parallels.core.utils.entity import Entity + + class PanelModel(object): def refresh_components(self): """Initiate update of cached data about installed target panel components; after that target panel should @@ -26,4 +29,69 @@ class PanelModel(object): :type subscription_name: str | unicode :rtype: dict[str | unicode, str | unicode] """ - raise NotImplementedError() \ No newline at end of file + raise NotImplementedError() + + def get_dns_template(self, subscription_name): + """Get server-wide DNS template of the panel + + :type subscription_name: str | unicode + :rtype: parallels.core.hosting_repository.panel.DNSTemplate + """ + raise NotImplementedError() + + +class DNSTemplate(Entity): + def __init__(self, dns_records): + """ + :type dns_records: list[parallels.core.hosting_repository.panel.DNSTemplateRecord] + """ + self._dns_records = dns_records + + @property + def dns_records(self): + """ + :rtype: list[parallels.core.hosting_repository.panel.DNSTemplateRecord] + """ + return self._dns_records + + +class DNSTemplateRecord(Entity): + def __init__(self, rec_type, src, dst, opt): + """ + :type rec_type: str | unicode + :type src: str | unicode + :type dst: str | unicode + :type opt: str | unicode + """ + self._rec_type = rec_type + self._src = src + self._dst = dst + self._opt = opt + + @property + def rec_type(self): + """ + :rtype: str | unicode + """ + return self._rec_type + + @property + def src(self): + """ + :rtype: str | unicode + """ + return self._src + + @property + def dst(self): + """ + :rtype: str | unicode + """ + return self._dst + + @property + def opt(self): + """ + :rtype: str | unicode + """ + return self._opt --- admin/plib/modules/panel-migrator/backend/lib/python/parallels/core/messages/__init__.py +++ admin/plib/modules/panel-migrator/backend/lib/python/parallels/core/messages/__init__.py @@ -1916,8 +1916,8 @@ FAILED_REMOVE_EXTERNAL_IDS_FOR_SUBSCRIPTION = single_line_message(""" REMOVE_EXTERNAL_IDS_FROM_BACKUP = single_line_message(""" Remove subscriptions External ID from source dump """) -FAILED_CONVERT_DNS_RECORDS_FOR_SUBSCRIPTION_2 = single_line_message(""" - Failed to convert DNS records for subscription '%s' +ACTION_CONVERT_DNS_RECORDS_FAILURE = single_line_message(""" + Failed to convert DNS records for subscription '{subscription}' """) ACTION_CHECK_SERVICE_PLAN_ACCORDANCE_DESCRIPTION = single_line_message(""" Check service plans accordance @@ -2532,7 +2532,7 @@ SOURCE_WEB_PATH_ERROR_NO_VHOST = single_line_message(""" UNSUPPORTED_TARGET_WEB_PATH_TYPE = single_line_message(""" Unsupported target web path type """) -ACTION_CONVERT_DNS_RECORDS = single_line_message(""" +ACTION_CONVERT_DNS_RECORDS_DESCRIPTION = single_line_message(""" Convert DNS records """) DUPLICATE_DNS_RECORD_REMOVED = single_line_message(""" --- admin/plib/modules/panel-migrator/backend/lib/python/parallels/core/utils/common/ip.py +++ admin/plib/modules/panel-migrator/backend/lib/python/parallels/core/utils/common/ip.py @@ -5,6 +5,15 @@ from parallels.core.utils.common import unique_list from parallels.core import messages +def is_ip(ip_address): + """Check if specified string is an IPv4 or IPv6 address + + :type ip_address: str | unicode + :rtype: bool + """ + return is_ipv4(ip_address) or is_ipv6(ip_address) + + def is_ipv4(ip_address): """Check if specified string is an IPv4 address --- admin/plib/modules/panel-migrator/backend/lib/python/parallels/plesk/actions/hosting_settings/convert/dns.py +++ admin/plib/modules/panel-migrator/backend/lib/python/parallels/plesk/actions/hosting_settings/convert/dns.py @@ -1,4 +1,4 @@ -from parallels.core import messages +from parallels.core import messages, safe_format from parallels.core.actions.base.subscription_action import SubscriptionAction from parallels.core.actions.utils.logging_properties import LoggingProperties from parallels.core.converter import dns as dns_converter @@ -6,17 +6,26 @@ from parallels.core.converter import dns as dns_converter class ConvertDns(SubscriptionAction): def get_description(self): - return messages.ACTION_CONVERT_DNS_RECORDS + """Get short description of action as string - def get_logging_properties(self): - return LoggingProperties(info_log=False) + :rtype: str | unicode + """ + return messages.ACTION_CONVERT_DNS_RECORDS_DESCRIPTION def get_failure_message(self, global_context, subscription): - """ + """Get message for situation when action failed + :type global_context: parallels.core.global_context.GlobalMigrationContext :type subscription: parallels.core.migrated_subscription.MigratedSubscription """ - return messages.FAILED_CONVERT_DNS_RECORDS_FOR_SUBSCRIPTION_2 % subscription.name + return safe_format(messages.ACTION_CONVERT_DNS_RECORDS_FAILURE, subscription=subscription.name) + + def get_logging_properties(self): + """Get how action should be logged to migration tools end-user + + :rtype: parallels.core.actions.utils.logging_properties.LoggingProperties + """ + return LoggingProperties(info_log=False) def run(self, global_context, subscription): """Make subscription DNS records resolve to target server IP. @@ -24,4 +33,5 @@ class ConvertDns(SubscriptionAction): :type global_context: parallels.core.global_context.GlobalMigrationContext :type subscription: parallels.core.migrated_subscription.MigratedSubscription """ - dns_converter.replace_resource_record_ips(subscription) + target_dns_template = global_context.hosting_repository.panel.get_dns_template(subscription.name) + dns_converter.replace_dns_record_ips(subscription, target_dns_template) --- admin/plib/modules/panel-migrator/backend/lib/python/parallels/plesk/hosting_repository/panel.py +++ admin/plib/modules/panel-migrator/backend/lib/python/parallels/plesk/hosting_repository/panel.py @@ -1,6 +1,7 @@ -from parallels.core.hosting_repository.panel import PanelModel +from parallels.core.hosting_repository.panel import PanelModel, DNSTemplateRecord, DNSTemplate from parallels.core.registry import Registry from parallels.core.utils import plesk_utils +from parallels.core.utils.common import cached from parallels.core.utils.common_constants import ADMIN_ID from parallels.plesk.hosting_repository.base import PleskBaseModel @@ -74,3 +75,29 @@ class PleskPanelModel(PanelModel, PleskBaseModel): rows = self.plesk_server.query_panel_db(query) return {row['name']: row['version'] for row in rows} + + def get_dns_template(self, subscription_name): + """Get server-wide DNS template of the panel + + :type subscription_name: str | unicode + :rtype: parallels.core.hosting_repository.panel.DNSTemplate + """ + return self._get_server_dns_template() + + @cached + def _get_server_dns_template(self): + """Get server-wide DNS template of the panel + + :rtype: parallels.core.hosting_repository.panel.DNSTemplate + """ + query = "SELECT type, host, val, opt FROM dns_recs_t" + + rows = self.plesk_server.query_panel_db(query) + dns_records = [] + + for row in rows: + dns_records.append(DNSTemplateRecord( + rec_type=row['type'], src=row['host'], dst=row['val'], opt=row['opt'] + )) + + return DNSTemplate(dns_records) --- admin/plib/modules/panel-migrator/backend/lib/python/parallels/plesk_multi_server/hosting_repository/panel.py +++ admin/plib/modules/panel-migrator/backend/lib/python/parallels/plesk_multi_server/hosting_repository/panel.py @@ -35,3 +35,11 @@ class PleskMultiServerPanelModel(PanelModel, PleskMultiServerBaseModel): """ server = self.get_subscription_service_server(subscription_name) return PleskPanelModel(server).get_installed_components(subscription_name) + + def get_dns_template(self, subscription_name): + """Get server-wide DNS template of the panel + + :rtype: parallels.core.hosting_repository.panel.DNSTemplate + """ + server = self.get_subscription_service_server(subscription_name) + return PleskPanelModel(server).get_dns_template(subscription_name)