Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ps module #438

Open
wants to merge 5 commits into
base: ps-linker
Choose a base branch
from
Open
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
7 changes: 6 additions & 1 deletion src/aaz_dev/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ def invalid_api_usage(e):
return jsonify(e.to_dict()), e.status_code

# register url converters
from .url_converters import Base64Converter, NameConverter, NameWithCapitalConverter, NamesPathConverter, ListPathConvertor
from .url_converters import Base64Converter, NameConverter, NameWithCapitalConverter, NamesPathConverter, ListPathConvertor, PSNamesPathConverter
app.url_map.converters['base64'] = Base64Converter
app.url_map.converters['name'] = NameConverter
app.url_map.converters['Name'] = NameWithCapitalConverter
app.url_map.converters['names_path'] = NamesPathConverter
app.url_map.converters['list_path'] = ListPathConvertor
app.url_map.converters['PSNamesPath'] = PSNamesPathConverter

# register routes of swagger module
from swagger.api import register_blueprints
Expand All @@ -46,6 +47,10 @@ def invalid_api_usage(e):
from cli.api import register_blueprints
register_blueprints(app)

# register routes of ps module
from ps.api import register_blueprints
register_blueprints(app)

return app


Expand Down
8 changes: 8 additions & 0 deletions src/aaz_dev/app/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ def is_port_in_use(host, port):
expose_value=False,
help="The local path of azure-cli-extension repo. Official repo is https://github.com/Azure/azure-cli-extensions"
)
@click.option(
"--powershell-path", '--ps',
type=click.Path(file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True),
default=Config.POWERSHELL_PATH,
callback=Config.validate_and_setup_powershell_path,
expose_value=False,
help="The local path of azure-powershell repo."
)
@click.option(
"--workspaces-path", '-w',
type=click.Path(file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True),
Expand Down
15 changes: 13 additions & 2 deletions src/aaz_dev/app/url_converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ def to_url(self, values):
return super(NamesPathConverter, self).to_url(values)
return '/'.join(super(NamesPathConverter, self).to_url(value) for value in values)


class ListPathConvertor(PathConverter):

def to_python(self, value):
Expand All @@ -43,5 +42,17 @@ def to_url(self, values):
return super(ListPathConvertor, self).to_url(values)
return '/'.join(super(ListPathConvertor, self).to_url(value) for value in values)

class PSNamesPathConverter(PathConverter):
regex = r"([A-Z][a-zA-Z0-9]*)/([A-Z][a-zA-Z0-9]*\.Autorest)"
weight = 200

def to_python(self, value):
return value.split('/')

def to_url(self, values):
if isinstance(values, str):
return super(PSNamesPathConverter, self).to_url(values)
return '/'.join(super(PSNamesPathConverter, self).to_url(value) for value in values)


__all__ = ["Base64Converter", "NameConverter", "NamesPathConverter", "ListPathConvertor"]
__all__ = ["Base64Converter", "NameConverter", "NamesPathConverter", "ListPathConvertor", "PSNamesPathConverter"]
Empty file added src/aaz_dev/ps/__init__.py
Empty file.
6 changes: 6 additions & 0 deletions src/aaz_dev/ps/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

def register_blueprints(app):
from . import _cmds, autorest, powershell
app.register_blueprint(_cmds.bp)
app.register_blueprint(autorest.bp)
app.register_blueprint(powershell.bp)
140 changes: 140 additions & 0 deletions src/aaz_dev/ps/api/_cmds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import click
import logging
from flask import Blueprint
import sys
import os
import subprocess

from utils.config import Config

logger = logging.getLogger('backend')

bp = Blueprint('ps-cmds', __name__, url_prefix='/PS/CMDs', cli_group="ps")
bp.cli.short_help = "Manage powershell commands."


@bp.cli.command("generate-powershell", short_help="Generate powershell code based on selected azure cli module.")
@click.option(
"--aaz-path", '-a',
type=click.Path(file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True),
default=Config.AAZ_PATH,
required=not Config.AAZ_PATH,
callback=Config.validate_and_setup_aaz_path,
expose_value=False,
help="The local path of aaz repo."
)
@click.option(
"--cli-path", '-c',
type=click.Path(file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True),
callback=Config.validate_and_setup_cli_path,
help="The local path of azure-cli repo. Only required when generate from azure-cli module."
)
@click.option(
"--cli-extension-path", '-e',
type=click.Path(file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True),
callback=Config.validate_and_setup_cli_extension_path,
help="The local path of azure-cli-extension repo. Only required when generate from azure-cli extension."
)
@click.option(
"--powershell-path", '--ps',
type=click.Path(file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True),
callback=Config.validate_and_setup_powershell_path,
help="The local path of azure-powershell repo."
)
@click.option(
"--extension-or-module-name", '--name',
required=True,
help="Name of the module in azure-cli or the extension in azure-cli-extensions"
)
@click.option(
"--swagger-path", '-s',
type=click.Path(file_okay=False, dir_okay=True, readable=True, resolve_path=True),
default=Config.SWAGGER_PATH,
required=not Config.SWAGGER_PATH,
callback=Config.validate_and_setup_swagger_path,
expose_value=False,
help="The local path of azure-rest-api-specs repo. Official repo is https://github.com/Azure/azure-rest-api-specs"
)
def generate_powershell(extension_or_module_name, cli_path=None, cli_extension_path=None, powershell_path=None):
from ps.controller.autorest_configuration_generator import PSAutoRestConfigurationGenerator
from cli.controller.az_module_manager import AzMainManager, AzExtensionManager
from ps.templates import get_templates

# Module path in azure-powershell repo

powershell_path = os.path.join(powershell_path, "src")
if not os.path.exists(powershell_path):
logger.error(f"Path `{powershell_path}` not exist")
sys.exit(1)

if cli_path is not None:
assert Config.CLI_PATH is not None
manager = AzMainManager()
else:
assert cli_extension_path is not None
assert Config.CLI_EXTENSION_PATH is not None
manager = AzExtensionManager()

if not manager.has_module(extension_or_module_name):
logger.error(f"Cannot find module or extension `{extension_or_module_name}`")
sys.exit(1)

# generate README.md for powershell from CLI, ex, for Oracle, README.md should be generated in src/Oracle/Oracle.Autorest/README.md in azure-powershell repo
ps_generator = PSAutoRestConfigurationGenerator(manager, extension_or_module_name)
ps_cfg = ps_generator.generate_config()

autorest_module_path = os.path.join(powershell_path, ps_cfg.module_name, f"{ps_cfg.module_name}.Autorest")
if not os.path.exists(autorest_module_path):
os.makedirs(autorest_module_path)
readme_file = os.path.join(autorest_module_path, "README.md")
if os.path.exists(readme_file):
# read until to the "### AutoRest Configuration"
with open(readme_file, "r") as f:
lines = f.readlines()
for i, line in enumerate(lines):
if line.startswith("### AutoRest Configuration"):
lines = lines[:i]
break
else:
lines = []

tmpl = get_templates()['autorest']['configuration']
data = tmpl.render(cfg=ps_cfg)
lines.append(data)
with open(readme_file, "w") as f:
f.writelines(lines)

print(f"Generated {readme_file}")
# Generate and build PowerShell module from the README.md file generated above
print("Start to generate the PowerShell module from the README.md file in " + autorest_module_path)

# Execute autorest to generate the PowerShell module
original_cwd = os.getcwd()
os.chdir(autorest_module_path)
exit_code = os.system("pwsh -Command autorest")

# Print the output of the generation
if (exit_code != 0):
print("Failed to generate the module")
os.chdir(original_cwd)
sys.exit(1)
else:
print("Code generation succeeded.")
# print(result.stdout)

os.chdir(original_cwd)
# Execute autorest to generate the PowerShell module
print("Start to build the generated PowerShell module")
result = subprocess.run(
["pwsh", "-File", 'build-module.ps1'],
capture_output=True,
text=True,
cwd=autorest_module_path
)

if (result.returncode != 0):
print("Failed to build the module, please see following output for details:")
print(result.stderr)
sys.exit(1)
else:
print("Module build succeeds, and you may run the generated module by executing the following command: `./run-module.ps1` in " + autorest_module_path)
16 changes: 16 additions & 0 deletions src/aaz_dev/ps/api/autorest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from flask import Blueprint, jsonify, request, url_for

from utils.config import Config
from utils import exceptions
from command.controller.specs_manager import AAZSpecsManager
import logging

logging.basicConfig(level="INFO")


bp = Blueprint('autorest', __name__, url_prefix='/PS/Autorest')


@bp.route("/Directives", methods=("GET", ))
def az_profiles():
return jsonify({"test": "test"})
66 changes: 66 additions & 0 deletions src/aaz_dev/ps/api/powershell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from flask import Blueprint, jsonify, request, url_for

from utils.config import Config
from utils import exceptions
from ps.controller.ps_module_manager import PSModuleManager
from app.url_converters import PSNamesPathConverter
# from command.controller.specs_manager import AAZSpecsManager
import logging
import re

logging.basicConfig(level="INFO")


bp = Blueprint('powershell', __name__, url_prefix='/PS/Powershell')


@bp.route("/Path", methods=("GET", ))
def powershell_path():
if Config.POWERSHELL_PATH is None:
raise exceptions.InvalidAPIUsage("PowerShell path is not set, please add `--ps` option to `aaz-dev run` command or set up `AAZ_POWERSHELL_PATH` environment variable")
return jsonify({"path": Config.POWERSHELL_PATH})


@bp.route("/Modules", methods=("GET", "POST"))
def powershell_modules():
manager = PSModuleManager()
if request.method == "GET":
modules = manager.list_modules()
result = []
for module in modules:
result.append({
**module,
'url': url_for('powershell.powershell_module', module_names=module['name']),
})
return jsonify(result)
elif request.method == "POST":
# create a new module in powershell
data = request.get_json()
if not data or not isinstance(data, dict) or 'name' not in data:
raise exceptions.InvalidAPIUsage("Invalid request body")
if not re.match(PSNamesPathConverter.regex, data['name'].split('/')):
raise exceptions.InvalidAPIUsage("Invalid module name")
module_names = data['name'].split('/')
# make sure the name is follow the PSNamesPathConverter.regex
module = manager.create_new_mod(module_names)
result = module.to_primitive()
result['url'] = url_for('powershell.powershell_module', module_names=module.name)
else:
raise NotImplementedError()
return jsonify(result)


@bp.route("/Modules/<PSNamesPath:module_names>", methods=("GET", "PUT", "PATCH"))
def powershell_module(module_names):
manager = PSModuleManager()
if request.method == "GET":
result = manager.load_module(module_names)
# result = module.to_primitive()
result['url'] = url_for('powershell.powershell_module', module_names=result['name'])
elif request.method == "PUT":
raise NotImplementedError()
elif request.method == "PATCH":
raise NotImplementedError()
else:
raise NotImplementedError()
return jsonify(result)
Empty file.
Loading