Всем привет! На связи Андрей Кундрюков, DevOps-инженер компании «Флант». Некоторое время назад мне нужно было перенести несколько десятков доменов из одного аккаунта в Cloudflare в другой и не потерять настройки, в том числе Page Rules и Rule Sets. Для прежнего аккаунта уже был настроен импорт terraform state в GitLab, поэтому мне требовалось только перенести TF states в другой репозиторий и поменять API-ключ. Но ничего не получалось: часть доменов успешно проходила стадию terraform plan и валилась на apply, а часть доменов не доходила даже до terraform plan. Делал я это с помощью пока ещё сырого проекта cf-terraforming.
Когда я понял, что не решу задачу таким способом, то задумался, как ещё её можно выполнить. Пришёл к выводу, что названная выше утилита обращается к API Cloudflare и конвертирует полученное в terraform state, а потом проделывает обратную процедуру. Получается, я могу вытащить все нужные настройки по API, сохранить и потом импортировать в новый аккаунт так же по API.
Я ознакомился с документацией Cloudflare API и понял, что можно реализовать перенос доменов на любом известном языке, в моём случае это Bash или Python. Я выбрал второй вариант. При этом на Python я пишу примерно раз в год и каждый раз предварительно освежаю знания. Но так как по этой задаче горели сроки, то и разбираться мне было некогда. Тогда мне в голову пришла идея попросить ChatGPT написать скрипт на любом языке, который переносит домены между аккаунтами.
В статье я описал свой опыт в использовании ИИ для автоматизации процесса миграции доменов между аккаунтами Cloudflare. Я расскажу о трудностях, с которыми столкнулся при переносе настроек и DNS-записей, и о том, как использовал ChatGPT для генерации Python-скрипта, который успешно выполнил задачу.
Для написания скрипта я использовал ChatGPT-4o.
Просим ChatGPT написать скрипт
Особых надежд я не питал, но всё равно решил проверить в работе ChatGPT, поэтому задал ему достаточно простой и прямой запрос:
Запрос
Напиши скрипт на любом языке, который мигрирует
zone
из одного аккаунта Cloudflare в другой по API.
Примечание
Чтобы лучше ориентироваться в этой части статьи, я выделил начало и конец ответа ChatGPT чертой.
Ответ ChatGPT
Написание скрипта для миграции зоны из одного аккаунта Cloudflare в другой требует выполнения нескольких шагов с использованием Cloudflare API. Ниже приведён пример на языке Python, использующий модуль requests. Необходимо иметь API-токены для обоих аккаунтов: исходного (SOURCE_API_TOKEN
) и целевого (DESTINATION_API_TOKEN
). Также вам понадобится идентификатор зоны (ZONE_ID
), которую вы хотите переместить.
import requests
# Замените на ваши данные
SOURCE_API_TOKEN = "source_api_token"
DESTINATION_API_TOKEN = "destination_api_token"
ZONE_ID = "zone_id"
# Получение информации о зоне
source_headers = {
"Authorization": f"Bearer {SOURCE_API_TOKEN}",
"Content-Type": "application/json",
}
dest_headers = {
"Authorization": f"Bearer {DESTINATION_API_TOKEN}",
"Content-Type": "application/json",
}
zone_url = f"https://api.cloudflare.com/client/v4/zones/{ZONE_ID}"
response = requests.get(zone_url, headers=source_headers)
zone_data = response.json()
if not response.ok:
print("Не удалось получить информацию о зоне:", zone_data)
exit()
# Создание зоны в целевом аккаунте
create_zone_url = "https://api.cloudflare.com/client/v4/zones"
create_response = requests.post(create_zone_url, headers=dest_headers, json={
"name": zone_data['result']['name'],
"account": {"id": zone_data['result']['account']['id']},
"jump_start": True
})
create_data = create_response.json()
if not create_response.ok:
print("Не удалось создать зону в целевом аккаунте:", create_data)
exit()
new_zone_id = create_data['result']['id']
print(f"Зона успешно создана в целевом аккаунте с ID: {new_zone_id}")
# Получение записей DNS исходной зоны
dns_records_url = f"https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/dns_records"
dns_response = requests.get(dns_records_url, headers=source_headers)
dns_records_data = dns_response.json()
if not dns_response.ok:
print("Не удалось получить DNS записи исходной зоны:", dns_records_data)
exit()
# Копирование записей DNS в целевую зону
for record in dns_records_data['result']:
record.pop('id', None)
new_record_response = requests.post(f"https://api.cloudflare.com/client/v4/zones/{new_zone_id}/dns_records",
headers=dest_headers, json=record)
new_record_data = new_record_response.json()
if not new_record_response.ok:
print("Не удалось создать DNS запись в целевой зоне:", new_record_data)
continue
print(f"DNS запись {record['name']} успешно создана в целевой зоне")
print("Миграция завершена!")
Этот скрипт выполняет следующие действия:
Получает информацию о зоне из исходного аккаунта.
Создаёт зону в целевом аккаунте с аналогичным именем.
Получает список DNS-записей из исходной зоны.
Копирует каждую DNS-запись в новую зону целевого аккаунта.
Обратите внимание, что этот процесс может не учитывать все возможные нюансы и параметры зоны. В реальных ситуациях рекомендуется проверять результаты на каждом шаге и учитывать дополнительные аспекты миграции.
Я забил в скрипт токены и zone_id
, и скрипт справился: я получил в новом аккаунте домен с DNS-записями. Правда, они дублировались, но об этом позже. Тут стоит отметить, что я даже не проверял код, так как тестировал всё на двух тестовых аккаунтах и не мог навредить продовым доменам.
Получается, что ChatGPT справился с поставленной задачей. Но мне недостаточно переноса только лишь DNS-записей, поэтому я захотел дописать скрипт. Я решил, что буду представлять, что сам пишу код: буду понемногу добавлять новые возможности, тестировать код и снова добавлять новую функциональность. И мой следующий запрос выглядел так:
Запрос
Добавь в скрипт перенос
zone_settings
.
Ответ ChatGPT
Для переноса настроек зоны (zone_settings
) из одного аккаунта Cloudflare в другой необходимо получить текущие настройки зоны и применить их к новой созданной зоне. Вот как это можно сделать с использованием Cloudflare API и Python:
Получить текущие настройки зоны.
Применить эти настройки к новой зоне.
Ниже приведён обновлённый скрипт, который включает перенесение настроек зоны
(Привожу только релевантный блок кода. — Прим. автора):
# Получение настроек исходной зоны
zone_settings_url = f"https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/settings"
zone_settings_response = requests.get(zone_settings_url, headers=source_headers)
zone_settings_data = zone_settings_response.json()
if not zone_settings_response.ok:
print("Не удалось получить настройки исходной зоны:", zone_settings_data)
exit()
# Применение настроек к новой зоне
for setting in zone_settings_data['result']:
setting_name = setting['id']
update_setting_url = f"https://api.cloudflare.com/client/v4/zones/{new_zone_id}/settings/{setting_name}"
update_setting_response = requests.patch(update_setting_url, headers=dest_headers, json={
"value": setting['value']
})
update_setting_data = update_setting_response.json()
if not update_setting_response.ok:
print(f"Не удалось обновить настройку {setting_name} в целевой зоне:", update_setting_data)
else:
print(f"Настройка {setting_name} успешно обновлена в целевой зоне")
Обратите внимание:
zone_settings
: скрипт получает текущие настройки зоны исходного аккаунта и применяет каждый из них к новой зоне в целевом аккаунте.Переменная
setting_name
: используется для обращения к каждой отдельной настройке и обновлению их значения на сервере.
Этот скрипт выполняет все ключевые шаги для миграции зон и их настроек из одного аккаунта Cloudflare в другой. В реальных приложениях возможно добавление дополнительной обработки ошибок, журналирования и тестирования перед запуском в производственной среде.
Бездушный ИИ даже напоминает, что нужно протестировать скрипт перед использованием на проде, а также намекает, чем можно расширить функциональность скрипта.
Новая версия скрипта сработала. Дальше я не буду приводить фрагменты нового кода, потому что ИИ каждый раз выдавал мне новую версию полного скрипта. Я буду показывать, какими запросами я «дописывал» скрипт, так как это более ценная информация, чем сам сгенерированный код. Итоговый скрипт будет в конце статьи под спойлером.
Исправляем ошибки в коде с помощью ChatGPT
Примечание
В этой части статьи я в краткой форме передам наши диалоги с ChatGPT и под каждым из них буду оставлять свои комментарии для контекста. Здесь важно обратить внимание на то, как именно я делаю запросы к ChatGPT.
Дальше я писал запросы, где просил ChatGPT вносить конкретные правки в код.
Запрос: Добавь в скрипт перенос
ruleset
.ChatGPT: *присылает код.
Обновляю код скрипта, проверяю: работает.
Запрос: Добавь в скрипт перенос
page_rule
.ChatGPT: *присылает код.
Словил первую ошибку: {'success': False, 'errors': [{'code': 1061, 'message': 'example.domain already exists'}], 'messages': [], 'result': None}
. Кажется, скрипт пытается создать зону в старом аккаунте. Об этом я и сообщаю ChatGPT:
Запрос: Похоже скрипт пытается создать зону в старом аккаунте. Ошибка
{'success': False, 'errors': [{'code': 1061, 'message': 'example.domain already exists'}], 'messages': [], 'result': None}
.Исправь скрипт.
ChatGPT: Вероятно, проблема заключается в попытке использовать
account.id
исходной зоны при создании новой зоны в целевом аккаунте. Вместо этого нужно просто указать имя зоны и довериться новому аккаунту.Исправлю скрипт, чтобы он использовал правильные данные для создания новой зоны в целевом аккаунте и чтобы он не пытался создать зону с тем же идентификатором учетной записи, что и в исходном аккаунте.
*присылает код.
После исправления первой ошибки появились новые. Я также прошу ChatGPT, чтобы он исправил их:
Запрос: Исправь ошибку:
Не удалось создать DNS-запись в целевой зоне:
{'result': None, 'success': False, 'errors': [{'code': 1004, 'message': 'DNS Validation Error', 'error_chain': [{'code': 9100, 'message': 'priority is a required field.'}]}], 'messages': []}
.А также ошибку:
Не удалось создать
ruleset
в целевой зоне:{'result': None, 'success': False, 'errors': [{'message': 'invalid JSON: unknown field "id"'}], 'messages': None}
.ChatGPT: Для исправления ошибок в скрипте нужно добавить обработки, которые учитывают требования к полям JSON и удаляют ненужные поля при копировании записей DNS и правил.
DNS-записи:
Для некоторых типов записей, таких как MX, требуется обязательное полеpriority
. Необходимо добавить обработку этого случая.Rulesets:
Полеid
не требуется при создании новых правил, поэтому его следует удалить из JSON.
Итерация за итерацией исправляем ошибки:
Запрос: Исправь ошибку:
Не удалось создать
ruleset
в целевой зоне:{'result': None, 'success': False, 'errors': [{'message': 'invalid JSON: unknown field "version"'}], 'messages': None}
.ChatGPT: Для исправления ошибки при создании
ruleset
в целевой зоне необходимо удалить не только поле id, но также и поле version, которое, видимо, не требуется и не должно присутствовать в JSON при создании новогоruleset
в Cloudflare API.Я обновлю скрипт так, чтобы он удалял ненужные поля перед созданием нового
ruleset
:*присылает код.
Изначально я даже не вникал, что не так с кодом. Я просто «скармливал» ИИ stdout с ошибками скрипта, и он сам разбирался, в чём проблема. Далее я изменил подход: я пытался объяснить возможные причины ошибок. В итоге на получение корректного кода стало тратиться меньше итераций. А если бы я дополнительно давал и варианты решения, результат стал бы ещё лучше.
Запрос: Перенос настроек с кодом ошибки «Not allowed to edit setting for» следует исключить из переноса, так как они являются read-only. Также добавь в скрипт проверку на то, что dns record уже может существовать в зоне, чтобы исправить ошибку:
Не удалось создать DNS запись в целевой зоне:
{'result': None, 'success': False, 'errors': [{'code': 81058, 'message': 'A record with the same settings already exists.'}], 'messages': []}
.Перенос настроек с кодом ошибки «1015 allowed to edit setting for» следует исключить из переноса, так как они являются read-only. Также исключи из переноса настройку с кодом ошибки 1025, 'message': 'It is not possible to change Visitor IP settings for this zone.
ChatGPT: *присылает исправный код.
Доводим скрипт до совершенства
Далее было несколько неудачных итераций, которые я не буду приводить, и я решил давать ИИ более чёткие инструкции, предварительно изучая соответствующий запрос к API CF:
Запрос: Удали секцию с переносом
ruleset
и напиши заново с учётом новых требований:1. Работа с ruleset осуществляется через
endpoint POST /zones/{zone_id}/rulesets
.2. JSON содержит следующие поля:
-
name
;-
description
;-
kind
;-
phase
;-
rules
, который будет вложенным JSON с rules.3.
kind
может бытьcustom
,root
илиzone
.4.
phase
может бытьhttp_request_transform
,http_request_late_transform
,http_request_cache_settings
,http_config_settings
.Получи
ruleset
в формате JSON из старого аккаунта по адресу API/zones/{zone_id}/rulesets
.ChatGPT: *присылает корректный код.
Я получил корректный код по переносу ruleset
. Настало время навести красоту. Мне надоело вставлять новый код скрипта и заменять там токены и ID_ZONE
. Также мне нужно было для отладки увидеть JSON, который скрипт отправляет в новый аккаунт. Вот что я попросил:
Запрос: Перепиши скрипт так, чтобы переменные
SOURCE_API_TOKEN
,DESTINATION_API_TOKEN
иZONE_ID
лежали в отдельном файлеsettings.py
. Также сохрани на диск JSON, которые собираешься отправлять в новый аккаунт.ChatGPT: *присылает код.
ИИ послушно выдаёт новую, улучшенную версию скрипта. Корректируем, исправляем ошибки. Обратите внимание, я снова не пытался разобраться, в чём проблема. Я просто «скормил» новую ошибку и попросил исправить.
Запрос: Сделай так, чтобы все
zone_setting_*
складывались в один файлzone_settings.json
. Также исправь ошибкуFileNotFoundError: [Errno 2] No such file or directory: 'output/page_rule_update_www.example.domaon/*.json'
.ChatGPT: *присылает код.
Мне надоело удалять зону в новом аккаунте после каждой итерации тестирования скрипта, поэтому я попросил ChatGPT добавить проверку на существование зоны. В примере ниже я не только привожу текст ошибки, но и объясняю ИИ причину её возникновения и вариант решения проблемы:
Запрос: Добавь проверку на то, что zone в новом аккаунте уже существует, чтобы исправить ошибку
{'success': False, 'errors': [{'code': 1061, 'message': 'example.com already exists'}], 'messages': [], 'result': None}
. В таком случае не нужно создавать зону, но нужно выполнить все остальные действия по миграции.ChatGPT: *присылет код.
Тестируем скрипт. Задача почти завершена, но продолжаем наводить блеск.
Запрос: Отлично, всё работает как надо. Остались небольшие правки, сделай их:
1. Вместо переменной
ZONE_ID
я хочу получатьid zone
, указав в качестве аргумента имя домена в форматах example.com, something.dev, www.ru и так далее.2. Директорию
output
, в которую скрипт записывает JSON-файлы, стоит назвать так:output_<example-com>
. То есть префикс всегда будетoutput
, а постфикс — имя домена, который передали в качестве аргумента скрипту.ChatGPT: *присылает код.
Тестируем новый код и ловим ошибку. Снова просто просим исправить без погружения в контекст:
Запрос: Исправь ошибку:
Traceback (most recent call last):
File "/Users/ak/Work/tmp/domains/python/migration.py", line 25, in <module>
ZONE_ID = get_zone_id(DOMAIN_NAME, source_headers)
^^^^^^^^^^^^^^
NameError: name 'source_headers' is not defined
ChatGPT: Извините за пропуск. Переменная
source_headers
действительно должна быть определена до вызова функцииget_zone_id
. Давайте исправим этот момент, а также немного подправим структуру скрипта для большей читабельности.*присылает некорректный код.
Далее снова было несколько неудачных итераций, поэтому я решил дать ИИ хорошее ТЗ: изучил соответствующий запрос к API и предоставил примеры его использования.
Запрос: При переносе
ruleset
скрипт теряет полеrules
. Вот как можно получить списокruleset
:http GET https://api.cloudflare.com/client/v4/zones/zone_id/rulesets \
Authorization:'Bearer undefined' \
Content-Type:application/json
Пример ответа:
"result": [
{
"description": "My ruleset to execute managed rulesets",
"id": "string",
"last_updated": "2000-01-01T00:00:00.000000Z",
"name": "My ruleset",
"version": "1",
"kind": "root",
"phase": "http_request_firewall_custom"
}
],
"success": true
А так создаются
ruleset
:HTTP PUT https://api.cloudflare.com/client/v4
/zones/{zone_id}/rulesets
'{
"description": "My ruleset to execute managed rulesets",
"name": "My ruleset",
"kind": "root",
"phase": "http_request_firewall_custom",
"rules": [
{
"action": "block",
"action_parameters": {
"response": {
"content": "{\n \"success\": false,\n \"error\": \"you have been blocked\"\n}",
"content_type": "application/json",
"status_code": 400
}
},
"description": "Block when the IP address is not 1.1.1.1",
"enabled": true,
"expression": "ip.src ne 1.1.1.1",
"id": "3a03d665bac047339bb530ecb439a90d",
"logging": {
"enabled": true
},
"ref": "my_ref"
}
]
}'
Исправь скрипт, исходя из представленной информации.
ChatGPT: Спасибо за предоставление дополнительной информации. Давайте исправим скрипт для корректного переноса правил
ruleset
. Мы обновим логику для получения и созданияruleset
с учётом необходимых полей, включаяrules
.*присылает корректный код.
После чёткого ТЗ с первой попытки получили рабочий код. Далее шлифуем функциональность и ловим мелкие баги.
Запрос: Отлично. Теперь сделай так, чтобы
ruleset
, в которых отсутствует полеrules
, не переносились.ChatGPT: *присылает код.
Запрос: Хорошо. Теперь сделай так, чтобы перед переносом настроек в целевую зону проверялись значения. Если значение настройки в исходной и целевой зонах совпадают, скрипту не нужно их обновлять. Если же настройки отличаются, тогда обновляем.
ChatGPT: *присылает код.
Далее я столкнулся с ограничением API ChatGPT. На 334 строках скрипта выхлоп обрывался. Похоже, я добрался до лимита body_size
API. Не беда, теперь вчитаемся в код и будем просить исправить конкретные логические блоки.
Запрос: Скрипт слишком большой. Перепиши только тот блок, который отвечает за функцию переноса
ruleset
. В качестве ответа предоставь только переписанный кусок (*вставляю только нужный блок кода).ChatGPT: *присылает код.
Теперь ИИ прислал мне только обновлённый блок кода. Далее я отловил все баги, проверил финальный вариант и за несколько минут перенёс все домены. Для верности импортировал terraform state из нового аккаунта и сравнил с таковым для старого. Задача выполнена.
Скрытый текст
settings.py:
SOURCE_API_TOKEN = "secret"
DESTINATION_API_TOKEN = "secret"
migrate_cloudflare.py:
import os
import sys
import requests
import json
from settings import SOURCE_API_TOKEN, DESTINATION_API_TOKEN
if len(sys.argv) != 2:
print("Использование: python migrate_cloudflare.py <домен>")
sys.exit(1)
DOMAIN_NAME = sys.argv[1]
# Заголовки запросов
source_headers = {
'Authorization': f'Bearer {SOURCE_API_TOKEN}',
'Content-Type': 'application/json',
}
dest_headers = {
'Authorization': f'Bearer {DESTINATION_API_TOKEN}',
'Content-Type': 'application/json',
}
# Получение ID зоны по имени домена из исходного аккаунта
def get_zone_id(domain_name, headers):
url = f'https://api.cloudflare.com/client/v4/zones?name={domain_name}'
response = requests.get(url, headers=headers)
data = response.json()
if response.ok and data['result']:
return data['result'][0]['id']
else:
print(f'Не удалось получить ID зоны для домена {domain_name}:', data)
sys.exit(1)
ZONE_ID = get_zone_id(DOMAIN_NAME, source_headers)
# Создание директории для хранения JSON-файлов, если она не существует
output_dir = f'output_{DOMAIN_NAME.replace(".", "-")}'
os.makedirs(output_dir, exist_ok=True)
# Получение информации о зоне
zone_url = f'https://api.cloudflare.com/client/v4/zones/{ZONE_ID}'
response = requests.get(zone_url, headers=source_headers)
zone_data = response.json()
if not response.ok:
print('Не удалось получить информацию о зоне:', zone_data)
exit()
# Проверка существования зоны в целевом аккаунте
check_zone_url = f'https://api.cloudflare.com/client/v4/zones?name={zone_data["result"]["name"]}'
check_zone_response = requests.get(check_zone_url, headers=dest_headers)
check_zone_data = check_zone_response.json()
if not check_zone_response.ok:
print('Не удалось проверить существование зоны в целевом аккаунте:', check_zone_data)
exit()
zone_exists = any(zone['name'] == zone_data['result']['name'] for zone in check_zone_data['result'])
if zone_exists:
new_zone_id = check_zone_data['result'][0]['id']
print(f'Зона уже существует в целевом аккаунте с ID: {new_zone_id}')
else:
# Создание зоны в целевом аккаунте
create_zone_url = 'https://api.cloudflare.com/client/v4/zones'
create_response = requests.post(create_zone_url, headers=dest_headers, json={
'name': zone_data['result']['name'],
'jump_start': True
})
create_data = create_response.json()
if not create_response.ok:
print('Не удалось создать зону в целевом аккаунте:', create_data)
exit()
new_zone_id = create_data['result']['id']
print(f'Зона успешно создана в целевом аккаунте с ID: {new_zone_id}')
# Получение настроек исходной зоны
zone_settings_url = f'https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/settings'
zone_settings_response = requests.get(zone_settings_url, headers=source_headers)
zone_settings_data = zone_settings_response.json()
if not zone_settings_response.ok:
print('Не удалось получить настройки исходной зоны:', zone_settings_data)
exit()
# Получение настроек целевой зоны
zone_settings_dest_url = f'https://api.cloudflare.com/client/v4/zones/{new_zone_id}/settings'
zone_settings_dest_response = requests.get(zone_settings_dest_url, headers=dest_headers)
zone_settings_dest_data = zone_settings_dest_response.json()
if not zone_settings_dest_response.ok:
print('Не удалось получить настройки целевой зоны:', zone_settings_dest_data)
exit()
# Применение настроек к новой зоне, исключая настройки, которые нельзя редактировать
skip_settings = [
'ciphers', 'filter_logs_to_cloudflare', 'http2', 'log_to_cloudflare',
'long_lived_grgc', 'mirage', 'orange_to_orange', 'origin_error_page_pass_thru',
'polish', 'prefetch_preload', 'proxy_read_timeout', 'response_buffering',
'sort_query_string_for_cache', 'true_client_ip_header', 'visitor_ip',
'waf', 'webp'
]
# Собрать все настройки целевой зоны в словарь
dest_settings_map = {setting['id']: setting['value'] for setting in zone_settings_dest_data['result']}
# Сбор всех применимых настроек в один словарь
applicable_settings = {}
for setting in zone_settings_data['result']:
setting_name = setting['id']
if setting_name in skip_settings:
print(f'Пропускаем настройку {setting_name}, так как она является read-only или не может быть изменена.')
continue
source_value = setting['value']
dest_value = dest_settings_map.get(setting_name)
if source_value == dest_value:
print(f'Настройка {setting_name} совпадает в исходной и целевой зонах, пропускаем обновление.')
continue
update_setting_data = {
'value': source_value
}
applicable_settings[setting_name] = update_setting_data
update_setting_url = f'https://api.cloudflare.com/client/v4/zones/{new_zone_id}/settings/{setting_name}'
update_setting_response = requests.patch(update_setting_url, headers=dest_headers, json=update_setting_data)
update_setting_result = update_setting_response.json()
if not update_setting_response.ok:
error_code = update_setting_result['errors'][0]['code']
if error_code in [1015, 1025]:
print(f'Пропускаем настройку {setting_name}, так как она является read-only или не может быть изменена.')
else:
print(f'Не удалось обновить настройку {setting_name} в целевой зоне:', update_setting_result)
else:
print(f'Настройка {setting_name} успешно обновлена в целевой зоне')
# Сохранение всех примененных настроек в один JSON-файл
with open(f'{output_dir}/zone_settings.json', 'w') as f:
json.dump(applicable_settings, f, indent=2)
# Получение записей DNS исходной зоны
dns_records_url = f'https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/dns_records'
dns_response = requests.get(dns_records_url, headers=source_headers)
dns_records_data = dns_response.json()
if not dns_response.ok:
print('Не удалось получить DNS-записи исходной зоны:', dns_records_data)
exit()
# Получение текущих DNS-записей целевой зоны
dest_dns_records_url = f'https://api.cloudflare.com/client/v4/zones/{new_zone_id}/dns_records'
dest_dns_response = requests.get(dest_dns_records_url, headers=dest_headers)
dest_dns_records_data = dest_dns_response.json()
if not dest_dns_response.ok:
print('Не удалось получить DNS-записи целевой зоны:', dest_dns_records_data)
exit()
# Удаление всех существующих DNS-записей в целевой зоне
for record in dest_dns_records_data['result']:
delete_record_url = f'https://api.cloudflare.com/client/v4/zones/{new_zone_id}/dns_records/{record["id"]}'
delete_response = requests.delete(delete_record_url, headers=dest_headers)
if not delete_response.ok:
print(f'Не удалось удалить DNS-запись {record["name"]} ({record["type"]}) из целевой зоны:', delete_response.json())
else:
print(f'DNS-запись {record["name"]} ({record["type"]}) успешно удалена из целевой зоны')
# Сохранение DNS-записей исходной зоны
with open(f'{output_dir}/dns_records.json', 'w') as f:
json.dump(dns_records_data, f, indent=2)
# Копирование записей DNS в целевую зону
for record in dns_records_data['result']:
record_data = {
'type': record['type'],
'name': record['name'],
'content': record['content'],
'ttl': record['ttl'],
'proxied': record.get('proxied', False) # Параметр "proxied" может отсутствовать для некоторых типов записей
}
# Для MX-записей требуется priority
if record['type'] == 'MX':
record_data['priority'] = record['priority']
# Сохранение DNS-записи, которую будем создавать
with open(f'{output_dir}/dns_record_{record["name"].replace("/", "_")}.json', 'w') as f:
json.dump(record_data, f, indent=2)
new_record_response = requests.post(f'https://api.cloudflare.com/client/v4/zones/{new_zone_id}/dns_records',
headers=dest_headers, json=record_data)
new_record_result = new_record_response.json()
if not new_record_response.ok:
print('Не удалось создать DNS-запись в целевой зоне:', new_record_result)
continue
print(f'DNS-запись {record["name"]} успешно создана в целевой зоне')
# Получение существующих Page Rules
existing_page_rules_url = f'https://api.cloudflare.com/client/v4/zones/{new_zone_id}/pagerules'
existing_page_rules_response = requests.get(existing_page_rules_url, headers=dest_headers)
existing_page_rules_data = existing_page_rules_response.json()
if not existing_page_rules_response.ok:
print('Не удалось получить Page Rules целевой зоны:', existing_page_rules_data)
exit()
existing_page_rules = {rule['targets'][0]['constraint']['value']: rule for rule in existing_page_rules_data['result']}
# Получение Page Rules исходной зоны
page_rules_url = f'https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/pagerules'
page_rules_response = requests.get(page_rules_url, headers=source_headers)
page_rules_data = page_rules_response.json()
if not page_rules_response.ok:
print('Не удалось получить Page Rules для исходной зоны:', page_rules_data)
exit()
# Сохранение Page Rules
with open(f'{output_dir}/page_rules.json', 'w') as f:
json.dump(page_rules_data, f, indent=2)
# Копирование Page Rules в целевую зону
for page_rule in page_rules_data['result']:
rule_url = page_rule['targets'][0]['constraint']['value']
safe_rule_url = rule_url.replace("/", "_").replace(":", "_") # На случай, если URL содержит недопустимые символы
if rule_url in existing_page_rules:
# Обновляем существующий Page Rule
page_rule_id = existing_page_rules[rule_url]['id']
update_page_rule_url = f'https://api.cloudflare.com/client/v4/zones/{new_zone_id}/pagerules/{page_rule_id}'
update_page_rule_data = {
'targets': page_rule['targets'],
'actions': page_rule['actions'],
'priority': page_rule['priority'],
'status': page_rule['status']
}
# Сохранение Page Rule, которую будем обновлять
with open(f'{output_dir}/page_rule_update_{safe_rule_url}.json', 'w') as f:
json.dump(update_page_rule_data, f, indent=2)
update_page_rule_response = requests.put(update_page_rule_url, headers=dest_headers, json=update_page_rule_data)
update_page_rule_result = update_page_rule_response.json()
if not update_page_rule_response.ok:
print(f'Не удалось обновить page rule для {rule_url} в целевой зоне:', update_page_rule_result)
else:
print(f'Page rule для {rule_url} успешно обновлен в целевой зоне')
else:
# Создаем новый Page Rule
new_page_rule_url = f'https://api.cloudflare.com/client/v4/zones/{new_zone_id}/pagerules'
new_page_rule_data = {
'targets': page_rule['targets'],
'actions': page_rule['actions'],
'priority': page_rule['priority'],
'status': page_rule['status']
}
# Сохранение Page Rule, которую будем создавать
with open(f'{output_dir}/page_rule_create_{safe_rule_url}.json', 'w') as f:
json.dump(new_page_rule_data, f, indent=2)
new_page_rule_response = requests.post(new_page_rule_url, headers=dest_headers, json=new_page_rule_data)
new_page_rule_result = new_page_rule_response.json()
if not new_page_rule_response.ok:
print(f'Не удалось создать page rule для {rule_url} в целевой зоне:', new_page_rule_result)
else:
print(f'Page rule для {rule_url} успешно создан в целевой зоне')
# Получение ruleset из исходной зоны
rulesets_url = f'https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/rulesets'
rulesets_response = requests.get(rulesets_url, headers=source_headers)
rulesets_data = rulesets_response.json()
if not rulesets_response.ok:
print('Не удалось получить rulesets для исходной зоны:', rulesets_data)
exit()
# Сохранение ruleset
with open(f'{output_dir}/rulesets.json', 'w') as f:
json.dump(rulesets_data, f, indent=2)
# Указание фаз для переноса
allowed_phases = [
'http_request_transform',
'http_request_late_transform',
'http_request_cache_settings',
'http_config_settings'
]
# Исключение определенных имен ruleset
excluded_names = [
"DDoS L7 ruleset",
"Cloudflare Managed Free Ruleset",
"Cloudflare Normalization Ruleset"
]
# Получение существующих rulesets в целевой зоне
dest_rulesets_url = f'https://api.cloudflare.com/client/v4/zones/{new_zone_id}/rulesets'
dest_rulesets_response = requests.get(dest_rulesets_url, headers=dest_headers)
dest_rulesets_data = dest_rulesets_response.json()
if not dest_rulesets_response.ok:
print('Не удалось получить rulesets для целевой зоны:', dest_rulesets_data)
exit()
# Копирование и обновление правил в rulesets в целевой зоне
for ruleset in rulesets_data['result']:
if ruleset['name'] in excluded_names:
print(f'Пропускаем ruleset с именем {ruleset["name"]}.')
continue
# Проверка ruleset "default"
if ruleset['name'] == "default":
default_ruleset_url = f'https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/rulesets/{ruleset["id"]}'
default_ruleset_response = requests.get(default_ruleset_url, headers=source_headers)
default_ruleset_data = default_ruleset_response.json()
if not default_ruleset_response.ok or 'rules' not in default_ruleset_data['result'] or not default_ruleset_data['result']['rules']:
print(f'Пропускаем пустой ruleset с именем {ruleset["name"]}.')
continue
# Получение детального ruleset'а для получения поля rules
rule_details_url = f'https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/rulesets/{ruleset["id"]}'
rule_details_response = requests.get(rule_details_url, headers=source_headers)
rule_details_data = rule_details_response.json()
if not rule_details_response.ok:
print(f'Не удалось получить детали ruleset для {ruleset["id"]}:', rule_details_data)
continue
if 'rules' not in rule_details_data['result']:
print(f'Пропускаем ruleset {ruleset["id"]}, так как он не содержит правила (rules).')
continue
for phase_name in allowed_phases:
if phase_name == rule_details_data['result']['phase']:
ruleset_cleaned = {
'name': rule_details_data['result']['name'],
'description': rule_details_data['result'].get('description', ''),
'kind': rule_details_data['result']['kind'],
'phase': rule_details_data['result']['phase'],
'rules': rule_details_data['result']['rules']
}
# Очистка правил от ненужных полей
for rule in ruleset_cleaned['rules']:
rule.pop('id', None)
rule.pop('last_updated', None)
rule.pop('version', None)
rule.pop('shareable_entitlement_name', None)
# Проверка на существование ruleset'а в целевой зоне
existing_ruleset = next((r for r in dest_rulesets_data['result']
if r['name'] == ruleset_cleaned['name'] and r['phase'] == phase_name), None)
if existing_ruleset:
existing_ruleset_id = existing_ruleset['id']
# Проверка и обновление существующих правил в целевой зоне
existing_rules_url = f'https://api.cloudflare.com/client/v4/zones/{new_zone_id}/rulesets/{existing_ruleset_id}/rules'
existing_rules_response = requests.get(existing_rules_url, headers=dest_headers)
existing_rules_data = existing_rules_response.json()
if not existing_rules_response.ok:
print(f'Не удалось получить правила для ruleset {ruleset_cleaned["name"]}:', existing_rules_data)
continue
existing_rules = {rule['description']: rule for rule in existing_rules_data['result']}
for rule in ruleset_cleaned['rules']:
rule_description = rule['description']
if rule_description in existing_rules:
rule_id = existing_rules[rule_description]['id']
# Обновление существующего правила
update_rule_url = f'https://api.cloudflare.com/client/v4/zones/{new_zone_id}/rulesets/{existing_ruleset_id}/rules/{rule_id}'
update_rule_response = requests.patch(update_rule_url, headers=dest_headers, json=rule)
if not update_rule_response.ok:
print(f'Не удалось обновить rule в ruleset {ruleset_cleaned["name"]}:', update_rule_response.json())
else:
print(f'Rule успешно обновлен в ruleset {ruleset_cleaned["name"]}')
else:
# Добавление нового правила
create_rule_url = f'https://api.cloudflare.com/client/v4/zones/{new_zone_id}/rulesets/{existing_ruleset_id}/rules'
create_rule_response = requests.post(create_rule_url, headers=dest_headers, json=rule)
if not create_rule_response.ok:
print(f'Не удалось создать rule в ruleset {ruleset_cleaned["name"]}:', create_rule_response.json())
else:
print(f'Rule успешно добавлен в ruleset {ruleset_cleaned["name"]}')
else:
# Сохранение ruleset, которое будем создавать
with open(f'{output_dir}/ruleset_{phase_name}.json', 'w') as f:
json.dump(ruleset_cleaned, f, indent=2)
new_ruleset_url = f'https://api.cloudflare.com/client/v4/zones/{new_zone_id}/rulesets'
new_ruleset_response = requests.post(new_ruleset_url, headers=dest_headers, json=ruleset_cleaned)
new_ruleset_result = new_ruleset_response.json()
if not new_ruleset_response.ok:
print(f'Не удалось создать ruleset для фазы {phase_name} в целевой зоне:', new_ruleset_result)
else:
print(f'Ruleset для фазы {phase_name} успешно создан в целевой зоне')
print('Миграция завершена!')
Вместо заключения
В жизни DevOps-инженера часто бывают задачи, когда нужно что-то по-быстрому автоматизировать, при этом красота и чистота кода на данном этапе не требуются. ChatGPT — прекрасный помощник в этом деле. Возможности ChatGPT-4o в сравнении с ChatGPT-3.5 сильно выросли и теперь это серьёзный инструмент в руках DevOps-инженеров, а не игрушка или просто более продвинутый поисковик. Кстати, у меня на всё про всё ушла пара часов. Сам бы я решал задачу гораздо дольше.
Отмечу, что, пока я писал эту статью, с помощью ChatGPT я добавил в Python-скрипт новые функции, ранее написанные человеком. На этот раз мы работали с API GitLab, и по итогу скрипт, написанный человеком, обзавёлся новой функциональностью, написанной ИИ. Я просто скинул ему текущий код скрипта и попросил добавить кое-что — и снова отличная работа.