Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,30 @@
# pylint: disable=unused-argument, line-too-long, import-outside-toplevel, raise-missing-from
import datetime as dt
from datetime import datetime
import os
import random
import subprocess
import secrets
import string
import yaml
from knack.log import get_logger
from azure.core.paging import ItemPaged
from azure.cli.core.commands.client_factory import get_subscription_id
from azure.cli.core.commands import LongRunningOperation, _is_poller
from azure.cli.core.azclierror import RequiredArgumentMissingError, InvalidArgumentValueError
from azure.cli.command_modules.role.custom import create_service_principal_for_rbac
from azure.mgmt.resource.resources.models import ResourceGroup
from msrestazure.tools import parse_resource_id
from ._client_factory import resource_client_factory, cf_mysql_flexible_location_capabilities, cf_postgres_flexible_location_capabilities
from .flexible_server_custom_common import firewall_rule_create_func

logger = get_logger(__name__)

DEFAULT_LOCATION_PG = 'eastus' # For testing: 'eastus2euap'
DEFAULT_LOCATION_MySQL = 'westus2'
AZURE_CREDENTIALS = 'AZURE_CREDENTIALS'
AZURE_POSTGRESQL_CONNECTION_STRING = 'AZURE_POSTGRESQL_CONNECTION_STRING'
AZURE_MYSQL_CONNECTION_STRING = 'AZURE_MYSQL_CONNECTION_STRING'
GITHUB_ACTION_PATH = '/.github/workflows/'


def resolve_poller(result, cli_ctx, name):
Expand Down Expand Up @@ -78,40 +87,6 @@ def generate_password(administrator_login_password):
return administrator_login_password


def create_firewall_rule(db_context, cmd, resource_group_name, server_name, start_ip, end_ip):
# allow access to azure ip addresses
cf_firewall, logging_name = db_context.cf_firewall, db_context.logging_name # NOQA pylint: disable=unused-variable
now = datetime.now()
firewall_name = 'FirewallIPAddress_{}-{}-{}_{}-{}-{}'.format(now.year, now.month, now.day, now.hour, now.minute,
now.second)
if start_ip == '0.0.0.0' and end_ip == '0.0.0.0':
logger.warning('Configuring server firewall rule, \'azure-access\', to accept connections from all '
'Azure resources...')
firewall_name = 'AllowAllAzureServicesAndResourcesWithinAzureIps_{}-{}-{}_{}-{}-{}'.format(now.year, now.month,
now.day, now.hour,
now.minute,
now.second)
elif start_ip == end_ip:
logger.warning('Configuring server firewall rule to accept connections from \'%s\'...', start_ip)
else:
if start_ip == '0.0.0.0' and end_ip == '255.255.255.255':
firewall_name = 'AllowAll_{}-{}-{}_{}-{}-{}'.format(now.year, now.month, now.day, now.hour, now.minute,
now.second)
logger.warning('Configuring server firewall rule to accept connections from \'%s\' to \'%s\'...', start_ip,
end_ip)
firewall_client = cf_firewall(cmd.cli_ctx, None)

# Commenting out until firewall_id is properly returned from RP
# return resolve_poller(
# firewall_client.create_or_update(resource_group_name, server_name, firewall_name , start_ip, end_ip),
# cmd.cli_ctx, '{} Firewall Rule Create/Update'.format(logging_name))

firewall = firewall_rule_create_func(firewall_client, resource_group_name, server_name, firewall_rule_name=firewall_name,
start_ip_address=start_ip, end_ip_address=end_ip)

return firewall.result().name


# pylint: disable=inconsistent-return-statements
def parse_public_access_input(public_access):
# pylint: disable=no-else-return
Expand Down Expand Up @@ -339,3 +314,92 @@ def _resolve_api_version(client, provider_namespace, resource_type, parent_path)
raise RequiredArgumentMissingError(
'API version is required and could not be resolved for resource {}'
.format(resource_type))


def run_subprocess(command, stdout_show=None):
if stdout_show:
process = subprocess.Popen(command, shell=True)
else:
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
process.wait()
if process.returncode:
logger.warning(process.stderr.read().strip().decode('UTF-8'))


def run_subprocess_get_output(command):
commands = command.split()
process = subprocess.Popen(commands, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
process.wait()
return process


def register_credential_secrets(cmd, server, repository):
resource_group = parse_resource_id(server.id)["resource_group"]
scope = "/subscriptions/{}/resourceGroups/{}".format(get_subscription_id(cmd.cli_ctx), resource_group)

app = create_service_principal_for_rbac(cmd, name=server.name, role='contributor', scopes=[scope])
app['clientId'], app['clientSecret'], app['tenantId'] = app.pop('appId'), app.pop('password'), app.pop('tenant')
app['subscriptionId'] = get_subscription_id(cmd.cli_ctx)
app.pop('displayName')
app.pop('name')

app_key_val = []
for key, val in app.items():
app_key_val.append('"{}": "{}"'.format(key, val))

app_json = ',\n '.join(app_key_val)
app_json = '{\n ' + app_json + '\n}'
credential_file = "./temp_app_credential.txt"
with open(credential_file, "w") as f:
f.write(app_json)
run_subprocess('gh secret set {} --repo {} < {}'.format(AZURE_CREDENTIALS, repository, credential_file))
os.remove(credential_file)


def register_connection_secrets(cmd, database_engine, server, database_name, administrator_login, administrator_login_password, repository):
if database_engine == 'postgresql':
connection_string = "host={} port=5432 dbname={} user={} password={} sslmode=require".format(server.fully_qualified_domain_name, database_name, administrator_login, administrator_login_password)
run_subprocess('gh secret set {} --repo {} -b"{}"'.format(AZURE_POSTGRESQL_CONNECTION_STRING, repository, connection_string))
elif database_engine == 'mysql':
connection_string = "Server={}; Port=3306; Database={}; Uid={}; Pwd={}; SslMode=Preferred;".format(server.fully_qualified_domain_name, database_name, administrator_login, administrator_login_password)
run_subprocess('gh secret set {} --repo {} -b"{}"'.format(AZURE_MYSQL_CONNECTION_STRING, repository, connection_string))


def fill_action_template(cmd, database_engine, server, database_name, administrator_login, administrator_login_password, file_name, action_name, repository):

action_dir = get_git_root_dir() + GITHUB_ACTION_PATH
if not os.path.exists(action_dir):
os.makedirs(action_dir)

process = run_subprocess_get_output("gh secret list --repo {}".format(repository))
github_secrets = process.stdout.read().strip().decode('UTF-8')
connection_string = AZURE_POSTGRESQL_CONNECTION_STRING if database_engine == 'postgresql' else AZURE_MYSQL_CONNECTION_STRING

if AZURE_CREDENTIALS not in github_secrets:
register_credential_secrets(cmd,
server=server,
repository=repository)

if connection_string not in github_secrets:
register_connection_secrets(cmd,
database_engine=database_engine,
server=server,
database_name=database_name,
administrator_login=administrator_login,
administrator_login_password=administrator_login_password,
repository=repository)

current_location = os.path.dirname(__file__)

with open(current_location + "/templates/" + database_engine + "_githubaction_template.yaml", "r") as template_file:
template = yaml.safe_load(template_file)
template['jobs']['build']['steps'][2]['with']['server-name'] = server.fully_qualified_domain_name
template['jobs']['build']['steps'][2]['with']['sql-file'] = file_name
with open(action_dir + action_name + '.yml', 'w', encoding='utf8') as yml_file:
yml_file.write("on: [workflow_dispatch]\n")
yml_file.write(yaml.dump(template))


def get_git_root_dir():
process = run_subprocess_get_output("git rev-parse --show-toplevel")
return process.stdout.read().strip().decode('UTF-8')
Original file line number Diff line number Diff line change
Expand Up @@ -318,3 +318,26 @@
- name: Show connection strings for cmd and programming languages.
text: az mysql flexible-server show-connection-string -s testServer -u username -p password -d databasename
"""

helps['mysql flexible-server deploy'] = """
type: group
short-summary: Enable and run github action workflow for MySQL server
"""

helps['mysql flexible-server deploy setup'] = """
type: command
short-summary: Create github action workflow file for MySQL server.
examples:
- name: Create github action workflow file for MySQL server.
text: az mysql flexible-server deploy setup -s testServer -g testGroup -u username -p password --sql-file test.sql --repo username/userRepo -d flexibleserverdb --action-name testAction
- name: Create github action workflow file for MySQL server and push it to the remote repository
text: az mysql flexible-server deploy setup -s testServer -g testGroup -u username -p password --sql-file test.sql --repo username/userRepo -d flexibleserverdb --action-name testAction --branch userBranch --allow-push
"""

helps['mysql flexible-server deploy run'] = """
type: command
short-summary: Run an existing workflow in your github repository
examples:
- name: Run an existing workflow in your github repository
text: az mysql flexible-server deploy run --action-name testAction --branch userBranch
"""
25 changes: 24 additions & 1 deletion src/azure-cli/azure/cli/command_modules/rdbms/_helptext_pg.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
- name: Create a PostgreSQL flexible server using already existing virtual network and subnet. If provided virtual network and subnet does not exists then virtual network and subnet with default address prefix will be created.
text: |
az postgres flexible-server create --vnet myVnet --subnet mySubnet
- name: Create a PostgreSQL flexible server using already existing virtual network, subnet, and using the subnet ID. The provided subnet should not have any other resource deployed in it and this subnet will be delegated to Microsoft.DBforMySQL/flexibleServers, if not already delegated.
- name: Create a PostgreSQL flexible server using already existing virtual network, subnet, and using the subnet ID. The provided subnet should not have any other resource deployed in it and this subnet will be delegated to Microsoft.DBforPostgreSQL/flexibleServers, if not already delegated.
text: |
az postgres flexible-server create --subnet /subscriptions/{SubID}/resourceGroups/{ResourceGroup}/providers/Microsoft.Network/virtualNetworks/{VNetName}/subnets/{SubnetName}
- name: Create a PostgreSQL flexible server using new virtual network, subnet with non-default address prefix.
Expand Down Expand Up @@ -299,3 +299,26 @@
- name: Show connection strings for cmd and programming languages.
text: az postgres flexible-server show-connection-string -s testServer -u username -p password -d databasename
"""

helps['postgres flexible-server deploy'] = """
type: group
short-summary: Enable and run github action workflow for PostgreSQL server
"""

helps['postgres flexible-server deploy setup'] = """
type: command
short-summary: Create github action workflow file for PostgreSQL server.
examples:
- name: Create github action workflow file for PostgreSQL server.
text: az postgres flexible-server deploy setup -s testServer -g testGroup -u username -p password --sql-file test.sql --repo username/userRepo -d flexibleserverdb --action-name testAction
- name: Create github action workflow file for PostgreSQL server and push it to the remote repository
text: az postgres flexible-server deploy setup -s testServer -g testGroup -u username -p password --sql-file test.sql --repo username/userRepo -d flexibleserverdb --action-name testAction --branch userBranch --allow-push
"""

helps['postgres flexible-server deploy run'] = """
type: command
short-summary: Run an existing workflow in your github repository
examples:
- name: Run an existing workflow in your github repository
text: az postgres flexible-server deploy run --action-name testAction --branch userBranch
"""
17 changes: 17 additions & 0 deletions src/azure-cli/azure/cli/command_modules/rdbms/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -465,5 +465,22 @@ def _flexible_server_params(command_group):
with self.argument_context('{} flexible-server replica stop-replication'.format(command_group)) as c:
c.argument('server_name', options_list=['--name', '-n'], help='Name of the replica server.')

with self.argument_context('{} flexible-server deploy setup'.format(command_group)) as c:
c.argument('resource_group_name', arg_type=resource_group_name_type)
c.argument('server_name', id_part='name', options_list=['--server-name', '-s'], arg_type=server_name_arg_type)
c.argument('database_name', options_list=['--database-name', '-d'], help='The name of the database')
c.argument('administrator_login', options_list=['--admin-user', '-u'], arg_group='Authentication', arg_type=administrator_login_arg_type,
help='Administrator username for the server.')
c.argument('administrator_login_password', options_list=['--admin-password', '-p'], arg_group='Authentication', help='The password of the administrator.')
c.argument('sql_file_path', options_list=['--sql-file'], help='The path of the sql file. The sql file should be already in the repository')
c.argument('action_name', options_list=['--action-name'], help='The name of the github action')
c.argument('repository', options_list=['--repo'], help='The name of your github username and repository e.g., Azure/azure-cli ')
c.argument('branch', options_list=['--branch'], help='The name of the branch you want upload github action file. The default will be your current branch.')
c.argument('allow_push', default=False, options_list=['--allow-push'], arg_type=get_three_state_flag(), help='Push the action yml file to the remote repository. The changes will be pushed to origin repository, speicified branch or current branch if not specified.')

with self.argument_context('{} flexible-server deploy run'.format(command_group)) as c:
c.argument('action_name', options_list=['--action-name'], help='The name of the github action')
c.argument('branch', options_list=['--branch'], help='The name of the branch you want upload github action file. The default will be your current branch.')

_flexible_server_params('postgres')
_flexible_server_params('mysql')
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,13 @@ def load_flexibleserver_command_table(self, _):
g.show_command('show', 'get')
g.command('list', 'list_by_server')

with self.command_group('postgres flexible-server deploy', postgres_flexible_servers_sdk,
custom_command_type=flexible_server_custom_common,
client_factory=cf_postgres_flexible_servers,
is_preview=True) as g:
g.custom_command('setup', 'github_actions_setup')
g.custom_command('run', 'github_actions_run')

# MySQL commands
with self.command_group('mysql flexible-server', mysql_flexible_servers_sdk,
custom_command_type=flexible_servers_custom_mysql,
Expand Down Expand Up @@ -221,3 +228,10 @@ def load_flexibleserver_command_table(self, _):
is_preview=True) as g:
g.custom_command('create', 'flexible_replica_create', supports_no_wait=True)
g.custom_command('stop-replication', 'flexible_replica_stop', confirmation=True)

with self.command_group('mysql flexible-server deploy', mysql_flexible_servers_sdk,
custom_command_type=flexible_server_custom_common,
client_factory=cf_mysql_flexible_servers,
is_preview=True) as g:
g.custom_command('setup', 'github_actions_setup')
g.custom_command('run', 'github_actions_run')
Loading