Всем привет! На связи Андрей Кундрюков, 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("Миграция завершена!")

Этот скрипт выполняет следующие действия:

  1. Получает информацию о зоне из исходного аккаунта.

  2. Создаёт зону в целевом аккаунте с аналогичным именем.

  3. Получает список DNS-записей из исходной зоны.

  4. Копирует каждую DNS-запись в новую зону целевого аккаунта.

Обратите внимание, что этот процесс может не учитывать все возможные нюансы и параметры зоны. В реальных ситуациях рекомендуется проверять результаты на каждом шаге и учитывать дополнительные аспекты миграции.


Я забил в скрипт токены и zone_id, и скрипт справился: я получил в новом аккаунте домен с DNS-записями. Правда, они дублировались, но об этом позже. Тут стоит отметить, что я даже не проверял код, так как тестировал всё на двух тестовых аккаунтах и не мог навредить продовым доменам. 

Получается, что ChatGPT справился с поставленной задачей. Но мне недостаточно переноса только лишь DNS-записей, поэтому я захотел дописать скрипт. Я решил, что буду представлять, что сам пишу код: буду понемногу добавлять новые возможности, тестировать код и снова добавлять новую функциональность. И мой следующий запрос выглядел так:

Запрос

Добавь в скрипт перенос zone_settings.


Ответ ChatGPT

Для переноса настроек зоны (zone_settings) из одного аккаунта Cloudflare в другой необходимо получить текущие настройки зоны и применить их к новой созданной зоне. Вот как это можно сделать с использованием Cloudflare API и Python:

  1. Получить текущие настройки зоны.

  2. Применить эти настройки к новой зоне.

Ниже приведён обновлённый скрипт, который включает перенесение настроек зоны
(Привожу только релевантный блок кода. — Прим. автора):

# Получение настроек исходной зоны
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} успешно обновлена в целевой зоне")

Обратите внимание:

  1. zone_settings: скрипт получает текущие настройки зоны исходного аккаунта и применяет каждый из них к новой зоне в целевом аккаунте.

  2. Переменная 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, и по итогу скрипт, написанный человеком, обзавёлся новой функциональностью, написанной ИИ. Я просто скинул ему текущий код скрипта и попросил добавить кое-что — и снова отличная работа.

Комментарии (4)


  1. Mr_Volkula
    23.08.2024 07:31
    +7

    Отличный пример того, как зная, что и как должно быть сделано, спасти себя от кучи ненужной, в общем-то, рутины.

    Паттерн работы с нейронками полезный, спасибо. Схороню статью. Буду скидывать людям как пример для "вот так надо".


    1. sten65
      23.08.2024 07:31

      плохой пример. полагаясь на ии просто теряешь свой профессионализм, если понимаешь в чём проблема и не можешь реализовать сам - сформулируй свой вопрос предельно корректно и задать его. в инете найдётся масса ЛЮДЕЙ, которые ответят тебе гораздо лучше, чем любой ии. просто не забудь их поблагодарить за помощь.


      1. Tsegelnikov
        23.08.2024 07:31
        +2

        Скорее всего да. Есть одно НО. Скорость ответа. Сессия с чатом со скармливанием ошибок - минут 15-30. А сколько нужно времени, чтобы получить ответ от человека?


  1. Kden2019
    23.08.2024 07:31
    +3

    Таким же образом скармливанием ошибок чату, за час с небольшим написал парсер сайта.