Merge pull request #10528 from gavindsouza/migrate-to-new-feat
feat(migrate-to): switch Teams and restore existing Sites
This commit is contained in:
commit
eabdb84947
3 changed files with 234 additions and 92 deletions
|
|
@ -7,7 +7,6 @@ from frappe.integrations.frappe_providers.frappecloud import frappecloud_migrato
|
|||
|
||||
def migrate_to(local_site, frappe_provider):
|
||||
if frappe_provider in ("frappe.cloud", "frappecloud.com"):
|
||||
frappe_provider = "frappecloud.com"
|
||||
return frappecloud_migrator(local_site, frappe_provider)
|
||||
else:
|
||||
print("{} is not supported yet".format(frappe_provider))
|
||||
|
|
|
|||
|
|
@ -13,7 +13,129 @@ import requests
|
|||
import frappe
|
||||
import frappe.utils.backups
|
||||
from frappe.utils import get_installed_apps_info
|
||||
from frappe.utils.commands import render_table, add_line_after
|
||||
from frappe.utils.commands import render_table, add_line_after, add_line_before
|
||||
|
||||
|
||||
# TODO: check upgrade compatibility
|
||||
|
||||
|
||||
def render_actions_table():
|
||||
actions_table = [["#", "Action"]]
|
||||
actions = []
|
||||
|
||||
for n, action in enumerate(migrator_actions):
|
||||
actions_table.append([n+1, action["title"]])
|
||||
actions.append(action["fn"])
|
||||
|
||||
render_table(actions_table)
|
||||
return actions
|
||||
|
||||
|
||||
def render_site_table(sites_info):
|
||||
sites_table = [["#", "Site Name", "Status"]]
|
||||
available_sites = []
|
||||
|
||||
for n, site_data in enumerate(sites_info):
|
||||
name, status = site_data["name"], site_data["status"]
|
||||
if status in ("Active", "Broken"):
|
||||
sites_table.append([n + 1, name, status])
|
||||
available_sites.append(name)
|
||||
|
||||
render_table(sites_table)
|
||||
return available_sites
|
||||
|
||||
|
||||
def render_teams_table(teams):
|
||||
teams_table = [["#", "Team"]]
|
||||
|
||||
for n, team in enumerate(teams):
|
||||
teams_table.append([n+1, team])
|
||||
|
||||
render_table(teams_table)
|
||||
|
||||
|
||||
def render_plan_table(plans_list):
|
||||
plans_table = [["Plan", "CPU Time"]]
|
||||
visible_headers = ["name", "cpu_time_per_day"]
|
||||
|
||||
for plan in plans_list:
|
||||
plan, cpu_time = [plan[header] for header in visible_headers]
|
||||
plans_table.append([plan, "{} hour{}/day".format(cpu_time, "" if cpu_time < 2 else "s")])
|
||||
|
||||
render_table(plans_table)
|
||||
|
||||
|
||||
def render_group_table(app_groups):
|
||||
# title row
|
||||
app_groups_table = [["#", "App Group", "Apps"]]
|
||||
|
||||
# all rows
|
||||
for idx, app_group in enumerate(app_groups):
|
||||
apps_list = ", ".join(["{}:{}".format(app["scrubbed"], app["branch"]) for app in app_group["apps"]])
|
||||
row = [idx + 1, app_group["name"], apps_list]
|
||||
app_groups_table.append(row)
|
||||
|
||||
render_table(app_groups_table)
|
||||
|
||||
|
||||
def handle_request_failure(request=None, message=None, traceback=True, exit_code=1):
|
||||
message = message or "Request failed with error code {}".format(request.status_code)
|
||||
response = html2text(request.text) if traceback else ""
|
||||
|
||||
print("{0}{1}".format(message, "\n" + response))
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
@add_line_after
|
||||
def select_primary_action():
|
||||
actions = render_actions_table()
|
||||
idx = click.prompt("What do you want to do?", type=click.IntRange(1, len(actions))) - 1
|
||||
|
||||
return actions[idx]
|
||||
|
||||
|
||||
@add_line_after
|
||||
def select_site():
|
||||
get_all_sites_request = session.post(all_site_url, headers={
|
||||
"accept": "application/json",
|
||||
"accept-encoding": "gzip, deflate, br",
|
||||
"content-type": "application/json; charset=utf-8"
|
||||
})
|
||||
|
||||
if get_all_sites_request.ok:
|
||||
all_sites = get_all_sites_request.json()["message"]
|
||||
available_sites = render_site_table(all_sites)
|
||||
|
||||
while True:
|
||||
selected_site = click.prompt("Name of the site you want to restore to", type=str).strip()
|
||||
if selected_site in available_sites:
|
||||
return selected_site
|
||||
else:
|
||||
print("Site {} does not exist. Try again ❌".format(selected_site))
|
||||
else:
|
||||
print("Couldn't retrive sites list...Try again later")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@add_line_before
|
||||
def select_team(session):
|
||||
# get team options
|
||||
account_details_sc = session.post(account_details_url)
|
||||
if account_details_sc.ok:
|
||||
account_details = account_details_sc.json()["message"]
|
||||
available_teams = account_details["teams"]
|
||||
|
||||
# ask if they want to select, go ahead with if only one exists
|
||||
if len(available_teams) == 1:
|
||||
team = available_teams[0]
|
||||
else:
|
||||
render_teams_table(available_teams)
|
||||
idx = click.prompt("Select Team", type=click.IntRange(1, len(available_teams))) - 1
|
||||
team = available_teams[idx]
|
||||
|
||||
print("Team '{}' set for current session".format(team))
|
||||
|
||||
return team
|
||||
|
||||
|
||||
def get_new_site_options():
|
||||
|
|
@ -46,21 +168,6 @@ def is_subdomain_available(subdomain):
|
|||
return available
|
||||
|
||||
|
||||
def render_plan_table(plans_list):
|
||||
plans_table = []
|
||||
|
||||
# title row
|
||||
visible_headers = ["name", "cpu_time_per_day"]
|
||||
plans_table.append(["Plan", "CPU Time"])
|
||||
|
||||
# all rows
|
||||
for plan in plans_list:
|
||||
plan, cpu_time = [plan[header] for header in visible_headers]
|
||||
plans_table.append([plan, "{} hour{}/day".format(cpu_time, "" if cpu_time < 2 else "s")])
|
||||
|
||||
render_table(plans_table)
|
||||
|
||||
|
||||
@add_line_after
|
||||
def choose_plan(plans_list):
|
||||
print("{} plans available".format(len(plans_list)))
|
||||
|
|
@ -113,19 +220,6 @@ def check_app_compat(available_group):
|
|||
return is_compat, filtered_apps
|
||||
|
||||
|
||||
def render_group_table(app_groups):
|
||||
# title row
|
||||
app_groups_table = [["#", "App Group", "Apps"]]
|
||||
|
||||
# all rows
|
||||
for idx, app_group in enumerate(app_groups):
|
||||
apps_list = ", ".join(["{}:{}".format(app["scrubbed"], app["branch"]) for app in app_group["apps"]])
|
||||
row = [idx + 1, app_group["name"], apps_list]
|
||||
app_groups_table.append(row)
|
||||
|
||||
render_table(app_groups_table)
|
||||
|
||||
|
||||
@add_line_after
|
||||
def filter_apps(app_groups):
|
||||
render_group_table(app_groups)
|
||||
|
|
@ -148,24 +242,6 @@ def filter_apps(app_groups):
|
|||
|
||||
return selected_group["name"], filtered_apps
|
||||
|
||||
@add_line_after
|
||||
def create_session():
|
||||
# take user input from STDIN
|
||||
username = click.prompt("Username").strip()
|
||||
password = getpass.unix_getpass()
|
||||
|
||||
auth_credentials = {"usr": username, "pwd": password}
|
||||
|
||||
session = requests.Session()
|
||||
login_sc = session.post(login_url, auth_credentials)
|
||||
|
||||
if login_sc.ok:
|
||||
print("Authorization Successful! ✅")
|
||||
session.headers.update({"X-Press-Team": username})
|
||||
return session
|
||||
else:
|
||||
print("Authorization Failed with Error Code {}".format(login_sc.status_code))
|
||||
|
||||
|
||||
@add_line_after
|
||||
def get_subdomain(domain):
|
||||
|
|
@ -208,61 +284,119 @@ def upload_backup(local_site):
|
|||
return files_uploaded
|
||||
|
||||
|
||||
def frappecloud_migrator(local_site, remote_site):
|
||||
global login_url, upload_url, files_url, options_url, site_exists_url, session
|
||||
def new_site(local_site):
|
||||
# get new site options
|
||||
site_options = get_new_site_options()
|
||||
|
||||
# set preferences from site options
|
||||
subdomain = get_subdomain(site_options["domain"])
|
||||
plan = choose_plan(site_options["plans"])
|
||||
|
||||
app_groups = site_options["groups"]
|
||||
selected_group, filtered_apps = filter_apps(app_groups)
|
||||
files_uploaded = upload_backup(local_site)
|
||||
|
||||
# push to frappe_cloud
|
||||
payload = json.dumps({
|
||||
"site": {
|
||||
"apps": filtered_apps,
|
||||
"files": files_uploaded,
|
||||
"group": selected_group,
|
||||
"name": subdomain,
|
||||
"plan": plan
|
||||
}
|
||||
})
|
||||
|
||||
session.headers.update({"Content-Type": "application/json; charset=utf-8"})
|
||||
site_creation_request = session.post(upload_url, payload)
|
||||
|
||||
if site_creation_request.ok:
|
||||
site_url = site_creation_request.json()["message"]
|
||||
print("Your site {} is being migrated ✨".format(local_site))
|
||||
print("View your site dashboard at {}/dashboard/#/sites/{}".format(remote_site, site_url))
|
||||
print("Your site URL: {}".format(site_url))
|
||||
else:
|
||||
handle_request_failure(site_creation_request)
|
||||
|
||||
|
||||
def restore_site(local_site):
|
||||
# get list of existing sites they can restore
|
||||
selected_site = select_site()
|
||||
|
||||
# TODO: check if they can restore it
|
||||
|
||||
click.confirm("This is an irreversible action. Are you sure you want to continue?", abort=True)
|
||||
|
||||
# backup site
|
||||
files_uploaded = upload_backup(local_site)
|
||||
|
||||
# push to frappe_cloud
|
||||
payload = json.dumps({
|
||||
"name": selected_site,
|
||||
"files": files_uploaded
|
||||
})
|
||||
headers = {"Content-Type": "application/json; charset=utf-8"}
|
||||
site_restore_request = session.post(restore_site_url, payload, headers=headers)
|
||||
|
||||
if site_restore_request.ok:
|
||||
print("Your site {0} is being restored on {1} ✨".format(local_site, selected_site))
|
||||
print("View your site dashboard at {}/dashboard/#/sites/{}".format(remote_site, selected_site))
|
||||
print("Your site URL: {}".format(selected_site))
|
||||
else:
|
||||
handle_request_failure(site_restore_request)
|
||||
|
||||
|
||||
@add_line_after
|
||||
def create_session():
|
||||
print("Frappe Cloud credentials @ {}".format(remote_site))
|
||||
|
||||
# take user input from STDIN
|
||||
username = click.prompt("Username").strip()
|
||||
password = getpass.unix_getpass()
|
||||
|
||||
auth_credentials = {"usr": username, "pwd": password}
|
||||
|
||||
session = requests.Session()
|
||||
login_sc = session.post(login_url, auth_credentials)
|
||||
|
||||
if login_sc.ok:
|
||||
print("Authorization Successful! ✅")
|
||||
team = select_team(session)
|
||||
session.headers.update({"X-Press-Team": team })
|
||||
return session
|
||||
else:
|
||||
handle_request_failure(message="Authorization Failed with Error Code {}".format(login_sc.status_code), traceback=False)
|
||||
|
||||
|
||||
def frappecloud_migrator(local_site, frappecloud_site):
|
||||
global login_url, upload_url, files_url, options_url, site_exists_url, restore_site_url, account_details_url, all_site_url
|
||||
global session, migrator_actions, remote_site
|
||||
|
||||
remote_site = frappe.conf.frappecloud_url or "frappecloud.com"
|
||||
|
||||
login_url = "https://{}/api/method/login".format(remote_site)
|
||||
upload_url = "https://{}/api/method/press.api.site.new".format(remote_site)
|
||||
files_url = "https://{}/api/method/upload_file".format(remote_site)
|
||||
options_url = "https://{}/api/method/press.api.site.options_for_new".format(remote_site)
|
||||
site_exists_url = "https://{}/api/method/press.api.site.exists".format(remote_site)
|
||||
account_details_url = "https://{}/api/method/press.api.account.get".format(remote_site)
|
||||
all_site_url = "https://{}/api/method/press.api.site.all".format(remote_site)
|
||||
restore_site_url = "https://{}/api/method/press.api.site.restore".format(remote_site)
|
||||
|
||||
print("Frappe Cloud credentials @ {}".format(remote_site))
|
||||
migrator_actions = [
|
||||
{ "title": "Create a new site", "fn": new_site },
|
||||
{ "title": "Restore to an existing site", "fn": restore_site }
|
||||
]
|
||||
|
||||
# get credentials + auth user + start session
|
||||
session = create_session()
|
||||
|
||||
if session:
|
||||
# connect to site db
|
||||
frappe.init(site=local_site)
|
||||
frappe.connect()
|
||||
# available actions defined in migrator_actions
|
||||
primary_action = select_primary_action()
|
||||
|
||||
# get new site options
|
||||
site_options = get_new_site_options()
|
||||
frappe.init(site=local_site)
|
||||
frappe.connect()
|
||||
|
||||
# set preferences from site options
|
||||
subdomain = get_subdomain(site_options["domain"])
|
||||
plan = choose_plan(site_options["plans"])
|
||||
primary_action(local_site)
|
||||
|
||||
app_groups = site_options["groups"]
|
||||
selected_group, filtered_apps = filter_apps(app_groups)
|
||||
files_uploaded = upload_backup(local_site)
|
||||
|
||||
# push to frappe_cloud
|
||||
payload = json.dumps({
|
||||
"site": {
|
||||
"apps": filtered_apps,
|
||||
"files": files_uploaded,
|
||||
"group": selected_group,
|
||||
"name": subdomain,
|
||||
"plan": plan
|
||||
}
|
||||
})
|
||||
|
||||
session.headers.update({"Content-Type": "application/json; charset=utf-8"})
|
||||
site_creation_request = session.post(upload_url, payload)
|
||||
frappe.destroy()
|
||||
|
||||
if site_creation_request.ok:
|
||||
site_url = site_creation_request.json()["message"]
|
||||
print("Your site {} is being migrated ✨".format(local_site))
|
||||
print("View your site dashboard at {}/dashboard/#/sites/{}".format(remote_site, site_url))
|
||||
print("Your site URL: {}".format(site_url))
|
||||
else:
|
||||
print("Request failed with error code {}".format(site_creation_request.status_code))
|
||||
reason = html2text(site_creation_request.text)
|
||||
print(reason)
|
||||
sys.exit(1)
|
||||
|
||||
else:
|
||||
sys.exit(1)
|
||||
frappe.destroy()
|
||||
|
|
|
|||
|
|
@ -27,6 +27,15 @@ def add_line_after(function):
|
|||
return empty_line
|
||||
|
||||
|
||||
def add_line_before(function):
|
||||
"""Adds an extra line to STDOUT before the execution of a function this decorates"""
|
||||
def empty_line(*args, **kwargs):
|
||||
print()
|
||||
result = function(*args, **kwargs)
|
||||
return result
|
||||
return empty_line
|
||||
|
||||
|
||||
def log(message, colour=''):
|
||||
"""Coloured log outputs to STDOUT"""
|
||||
colours = {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue