From a3096fea9060f472cf299c35e47a41fae7808300 Mon Sep 17 00:00:00 2001 From: Jim Minter Date: Fri, 13 Dec 2019 12:09:53 -0600 Subject: [PATCH] Implement ARO extension Co-authored-by: Mangirdas Judeikis --- .env | 1 + ...st-test.yaml => pull_request-test-go.yaml} | 4 +- .../workflows/pull_request-test-python.yaml | 27 +++ .gitignore | 9 +- Makefile | 22 ++- README.md | 42 ++--- docs/deploy-production-cluster.md | 9 +- env.example | 4 - examples/cluster-v20191231.json | 29 --- python/az/aro/azext_aro/__init__.py | 18 +- python/az/aro/azext_aro/_aad.py | 68 +++++++ python/az/aro/azext_aro/_client_factory.py | 22 ++- python/az/aro/azext_aro/_format.py | 20 ++ python/az/aro/azext_aro/_help.py | 39 ++-- python/az/aro/azext_aro/_params.py | 71 ++++++-- python/az/aro/azext_aro/_rbac.py | 35 ++++ python/az/aro/azext_aro/_validators.py | 172 ++++++++++++++++-- python/az/aro/azext_aro/commands.py | 29 ++- python/az/aro/azext_aro/custom.py | 142 ++++++++++++++- .../tests/latest/test_aro_scenario.py | 11 +- python/az/aro/azext_aro/vendored_sdks | 1 + python/az/aro/setup.py | 13 +- python/client/__init__.py | 0 23 files changed, 622 insertions(+), 166 deletions(-) create mode 100644 .env rename .github/workflows/{pull_request-test.yaml => pull_request-test-go.yaml} (90%) create mode 100644 .github/workflows/pull_request-test-python.yaml delete mode 100644 examples/cluster-v20191231.json create mode 100644 python/az/aro/azext_aro/_aad.py create mode 100644 python/az/aro/azext_aro/_format.py create mode 100644 python/az/aro/azext_aro/_rbac.py create mode 120000 python/az/aro/azext_aro/vendored_sdks create mode 100644 python/client/__init__.py diff --git a/.env b/.env new file mode 100644 index 000000000..1c663742a --- /dev/null +++ b/.env @@ -0,0 +1 @@ +PYTHONPATH=python/az/aro diff --git a/.github/workflows/pull_request-test.yaml b/.github/workflows/pull_request-test-go.yaml similarity index 90% rename from .github/workflows/pull_request-test.yaml rename to .github/workflows/pull_request-test-go.yaml index aae75fea9..4307093e7 100644 --- a/.github/workflows/pull_request-test.yaml +++ b/.github/workflows/pull_request-test-go.yaml @@ -1,4 +1,4 @@ -name: pull_request-test +name: pull_request-test-go on: pull_request: types: @@ -21,5 +21,5 @@ jobs: - name: Test run: | set -x - make test + make test-go [[ -z "$(git status -s)" ]] diff --git a/.github/workflows/pull_request-test-python.yaml b/.github/workflows/pull_request-test-python.yaml new file mode 100644 index 000000000..e11452035 --- /dev/null +++ b/.github/workflows/pull_request-test-python.yaml @@ -0,0 +1,27 @@ +name: pull_request-test-python +on: + pull_request: + types: + - opened + - synchronize +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: + - 2.7 + - 3.5.7 + - 3.6.9 + steps: + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Check out source + uses: actions/checkout@v1 + - name: Test + run: | + set -x + pip install virtualenv + make test-python diff --git a/.gitignore b/.gitignore index 7dae7e842..50bf35d45 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,16 @@ +__pycache__ +*.egg-info +*.pyc +/.vscode /*.crt /*.key -/*.pem /*.kubeconfig +/*.pem /env* !/env.example /id_rsa +/pyenv* +/python/az/aro/build +/python/az/aro/dist /rp /secrets diff --git a/Makefile b/Makefile index 261e471ee..468b6e0c5 100644 --- a/Makefile +++ b/Makefile @@ -3,8 +3,13 @@ COMMIT = $(shell git rev-parse --short HEAD)$(shell [[ $$(git status --porcelain rp: generate go build -ldflags "-X main.gitCommit=$(COMMIT)" ./cmd/rp +az: + cd python/az/aro && python ./setup.py bdist_egg + clean: - rm -f rp + rm -rf python/az/aro/{aro.egg-info,build,dist} rp + find python -type f -name '*.pyc' -delete + find python -type d -name __pycache__ -delete client: generate rm -rf pkg/client python/client @@ -33,6 +38,7 @@ client: generate sudo chown -R $(USER):$(USER) pkg/client python/client rm -rf python/client/azure/mgmt/redhatopenshift/v2019_12_31_preview/aio + >python/client/__init__.py go run ./vendor/golang.org/x/tools/cmd/goimports -w -local=github.com/jim-minter/rp pkg/client @@ -50,7 +56,7 @@ secrets: secrets-update: oc create secret generic aro-v4-dev --from-file=secrets --dry-run -o yaml | oc apply -f - -test: generate +test-go: generate go build ./... gofmt -s -w cmd hack pkg @@ -63,4 +69,14 @@ test: generate go vet ./... go test ./... -.PHONY: rp clean client generate image secrets secrets-update test +test-python: + virtualenv --python=/usr/bin/python${PYTHON_VERSION} pyenv${PYTHON_VERSION} + . pyenv${PYTHON_VERSION}/bin/activate && \ + pip install azdev && \ + azdev setup -r . && \ + sed -i -e "s|^dev_sources = $(PWD)$$|dev_sources = $(PWD)/python|" ~/.azure/config && \ + $(MAKE) az && \ + azdev linter && \ + azdev style + +.PHONY: rp az clean client generate image secrets secrets-update test-go test-python diff --git a/README.md b/README.md index 986a32ef5..0eb4dfcb1 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,17 @@ az login ``` +1. Add the ARO preview extension to `az`: + + ``` + make az + + cat >>~/.azure/config < -#export AZURE_CLUSTER_CLIENT_SECRET= # non-RH ARO engineering: uncomment from here #export AZURE_TENANT_ID= @@ -15,6 +13,4 @@ export RP_MODE=development #export AZURE_FP_CLIENT_ID= #export AZURE_CLIENT_ID= #export AZURE_CLIENT_SECRET= -#export AZURE_CLUSTER_CLIENT_ID= -#export AZURE_CLUSTER_CLIENT_SECRET= #export PULL_SECRET='' diff --git a/examples/cluster-v20191231.json b/examples/cluster-v20191231.json deleted file mode 100644 index 6188bb494..000000000 --- a/examples/cluster-v20191231.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "id": "/subscriptions/$AZURE_SUBSCRIPTION_ID/resourcegroups/$RESOURCEGROUP/providers/Microsoft.RedHatOpenShift/openShiftClusters/$CLUSTER", - "name": "$CLUSTER", - "type": "Microsoft.RedHatOpenShift/openShiftClusters", - "location": "$LOCATION", - "properties": { - "servicePrincipalProfile": { - "clientId": "$AZURE_CLUSTER_CLIENT_ID", - "clientSecret": "$AZURE_CLUSTER_CLIENT_SECRET" - }, - "networkProfile": { - "podCidr": "10.128.0.0/14", - "serviceCidr": "172.30.0.0/16" - }, - "masterProfile": { - "vmSize": "Standard_D8s_v3", - "subnetId": "/subscriptions/$AZURE_SUBSCRIPTION_ID/resourcegroups/$VNET_RESOURCEGROUP/providers/Microsoft.Network/virtualNetworks/vnet/subnets/$CLUSTER-master" - }, - "workerProfiles": [ - { - "name": "worker", - "vmSize": "Standard_D2s_v3", - "diskSizeGB": 128, - "subnetId": "/subscriptions/$AZURE_SUBSCRIPTION_ID/resourcegroups/$VNET_RESOURCEGROUP/providers/Microsoft.Network/virtualNetworks/vnet/subnets/$CLUSTER-worker", - "count": 3 - } - ] - } -} diff --git a/python/az/aro/azext_aro/__init__.py b/python/az/aro/azext_aro/__init__.py index 1ce7351bb..532d57ba7 100644 --- a/python/az/aro/azext_aro/__init__.py +++ b/python/az/aro/azext_aro/__init__.py @@ -1,26 +1,22 @@ +from azext_aro._client_factory import cf_aro +from azext_aro._params import load_arguments +from azext_aro.commands import load_command_table from azure.cli.core import AzCommandsLoader - -from azext_aro._help import helps # pylint: disable=unused-import +from azure.cli.core.commands import CliCommandType class AroCommandsLoader(AzCommandsLoader): - def __init__(self, cli_ctx=None): - from azure.cli.core.commands import CliCommandType - from azext_aro._client_factory import cf_aro - aro_custom = CliCommandType( - operations_tmpl='azext_aro.custom#{}', - client_factory=cf_aro) + aro_custom = CliCommandType(operations_tmpl='azext_aro.custom#{}', + client_factory=cf_aro) super(AroCommandsLoader, self).__init__(cli_ctx=cli_ctx, - custom_command_type=aro_custom) + custom_command_type=aro_custom) def load_command_table(self, args): - from azext_aro.commands import load_command_table load_command_table(self, args) return self.command_table def load_arguments(self, command): - from azext_aro._params import load_arguments load_arguments(self, command) diff --git a/python/az/aro/azext_aro/_aad.py b/python/az/aro/azext_aro/_aad.py new file mode 100644 index 000000000..1f1d1b547 --- /dev/null +++ b/python/az/aro/azext_aro/_aad.py @@ -0,0 +1,68 @@ +import datetime +import uuid + +from azure.cli.core._profile import Profile +from azure.cli.core.commands.client_factory import configure_common_settings +from azure.graphrbac import GraphRbacManagementClient +from azure.graphrbac.models import ApplicationCreateParameters +from azure.graphrbac.models import PasswordCredential +from azure.graphrbac.models import ServicePrincipalCreateParameters + + +class AADManager(object): + MANAGED_APP_PREFIX = "https://az.aro.azure.com/" + + def __init__(self, cli_ctx): + profile = Profile(cli_ctx=cli_ctx) + credentials, _, tenant_id = profile.get_login_credentials( + resource=cli_ctx.cloud.endpoints.active_directory_graph_resource_id) + self.client = GraphRbacManagementClient( + credentials, tenant_id, base_url=cli_ctx.cloud.endpoints.active_directory_graph_resource_id) + configure_common_settings(cli_ctx, self.client) + + def createManagedApplication(self, display_name): + password = uuid.uuid4() + + try: + end_date = datetime.datetime(2299, 12, 31, tzinfo=datetime.timezone.utc) + except AttributeError: + end_date = datetime.datetime(2299, 12, 31) + + app = self.client.applications.create(ApplicationCreateParameters( + display_name=display_name, + identifier_uris=[ + self.MANAGED_APP_PREFIX + str(uuid.uuid4()), + ], + password_credentials=[ + PasswordCredential( + end_date=end_date, + value=password, + ), + ], + )) + + return app, password + + def getApplication(self, app_id): + apps = list(self.client.applications.list( + filter="appId eq '%s'" % app_id)) + if apps: + return apps[0] + return None + + def deleteManagedApplication(self, app_id): + app = self.getApplication(app_id) + if app and app.identifier_uris and app.identifier_uris[0].startswith(self.MANAGED_APP_PREFIX): + self.client.applications.delete(app.object_id) + + def getServicePrincipal(self, app_id): + sps = list(self.client.service_principals.list( + filter="appId eq '%s'" % app_id)) + if sps: + return sps[0] + return None + + def createServicePrincipal(self, app_id): + return self.client.service_principals.create(ServicePrincipalCreateParameters( + app_id=app_id, + )) diff --git a/python/az/aro/azext_aro/_client_factory.py b/python/az/aro/azext_aro/_client_factory.py index aba5fc5b9..8b74b7709 100644 --- a/python/az/aro/azext_aro/_client_factory.py +++ b/python/az/aro/azext_aro/_client_factory.py @@ -1,7 +1,17 @@ -def cf_aro(cli_ctx, *_): +import urllib3 - from azure.cli.core.commands.client_factory import get_mgmt_service_client - # TODO: Replace CONTOSO with the appropriate label and uncomment - # from azure.mgmt.CONTOSO import CONTOSOManagementClient - # return get_mgmt_service_client(cli_ctx, CONTOSOManagementClient) - return None +from azext_aro.custom import rp_mode_development +from azext_aro.vendored_sdks.azure.mgmt.redhatopenshift.v2019_12_31_preview import AzureRedHatOpenShiftClient +from azure.cli.core.commands.client_factory import get_mgmt_service_client + + +def cf_aro(cli_ctx, *_): + client = get_mgmt_service_client( + cli_ctx, AzureRedHatOpenShiftClient).open_shift_clusters + + if rp_mode_development(): + client.config.base_url = "https://localhost:8443/" + client.config.connection.verify = False + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + return client diff --git a/python/az/aro/azext_aro/_format.py b/python/az/aro/azext_aro/_format.py new file mode 100644 index 000000000..e40900392 --- /dev/null +++ b/python/az/aro/azext_aro/_format.py @@ -0,0 +1,20 @@ +import collections + +from msrestazure.tools import parse_resource_id + + +def aro_list_table_format(results): + return [aro_show_table_format(r) for r in results] + + +def aro_show_table_format(result): + parts = parse_resource_id(result["id"]) + + return collections.OrderedDict( + Name=result["name"], + ResourceGroup=parts["resource_group"], + Location=result["location"], + ProvisioningState=result["provisioningState"], + WorkerCount=result["workerProfiles"][0]["count"], + ConsoleURL=result["consoleUrl"], + ) diff --git a/python/az/aro/azext_aro/_help.py b/python/az/aro/azext_aro/_help.py index d92524800..5e0f17151 100644 --- a/python/az/aro/azext_aro/_help.py +++ b/python/az/aro/azext_aro/_help.py @@ -1,34 +1,37 @@ -# coding=utf-8 - -from knack.help_files import helps # pylint: disable=unused-import +from knack.help_files import helps helps['aro'] = """ type: group - short-summary: Commands to manage Aros. + short-summary: Manage Azure Red Hat OpenShift clusters. """ helps['aro create'] = """ type: command - short-summary: Create a Aro. + short-summary: Create a cluster. """ helps['aro list'] = """ type: command - short-summary: List Aros. + short-summary: List clusters. """ -# helps['aro delete'] = """ -# type: command -# short-summary: Delete a Aro. -# """ +helps['aro delete'] = """ + type: command + short-summary: Delete a cluster. +""" -# helps['aro show'] = """ -# type: command -# short-summary: Show details of a Aro. -# """ +helps['aro show'] = """ + type: command + short-summary: Get the details of a cluster. +""" -# helps['aro update'] = """ -# type: command -# short-summary: Update a Aro. -# """ +helps['aro update'] = """ + type: command + short-summary: Update a cluster. +""" + +helps['aro get-credentials'] = """ + type: command + short-summary: Get credentials of a cluster. +""" diff --git a/python/az/aro/azext_aro/_params.py b/python/az/aro/azext_aro/_params.py index f56322c51..6fc0f3307 100644 --- a/python/az/aro/azext_aro/_params.py +++ b/python/az/aro/azext_aro/_params.py @@ -1,19 +1,62 @@ -# pylint: disable=line-too-long - -from knack.arguments import CLIArgumentType +from azext_aro._validators import validate_cidr +from azext_aro._validators import validate_client_id +from azext_aro._validators import validate_client_secret +from azext_aro._validators import validate_subnet +from azext_aro._validators import validate_vnet +from azext_aro._validators import validate_worker_count +from azext_aro._validators import validate_worker_vm_disk_size_gb +from azure.cli.core.commands.parameters import name_type +from azure.cli.core.commands.parameters import resource_group_name_type +from azure.cli.core.commands.parameters import tags_type +from azure.cli.core.commands.validators import get_default_location_from_resource_group def load_arguments(self, _): - - from azure.cli.core.commands.parameters import tags_type - from azure.cli.core.commands.validators import get_default_location_from_resource_group - - aro_name_type = CLIArgumentType(options_list='--aro-name-name', help='Name of the Aro.', id_part='name') - with self.argument_context('aro') as c: - c.argument('tags', tags_type) - c.argument('location', validator=get_default_location_from_resource_group) - c.argument('aro_name', aro_name_type, options_list=['--name', '-n']) + c.argument('location', + validator=get_default_location_from_resource_group) + c.argument('resource_name', + name_type, + help='Name of cluster.') + c.argument('tags', + tags_type) - with self.argument_context('aro list') as c: - c.argument('aro_name', aro_name_type, id_part=None) + c.argument('client_id', + help='Client ID of cluster service principal.', + validator=validate_client_id) + c.argument('client_secret', + help='Client secret of cluster service principal.', + validator=validate_client_secret) + + c.argument('pod_cidr', + help='CIDR of pod network.', + validator=validate_cidr('pod_cidr')) + c.argument('service_cidr', + help='CIDR of service network.', + validator=validate_cidr('service_cidr')) + + c.argument('master_vm_size', + help='Size of master VMs.') + + c.argument('worker_vm_size', + help='Size of worker VMs.') + c.argument('worker_vm_disk_size_gb', + help='Disk size in GB of worker VMs.', + validator=validate_worker_vm_disk_size_gb) + c.argument('worker_count', + help='Count of worker VMs.', + validator=validate_worker_count) + + c.argument('vnet_resource_group_name', + resource_group_name_type, + options_list=['--vnet-resource-group'], + help='Name of vnet resource group.') + c.argument('vnet', + help='Name or ID of vnet. If name is supplied, `--vnet-resource-group` must be supplied.', + validator=validate_vnet) + c.argument('master_subnet', + help='Name or ID of master vnet subnet. If name is supplied, `--vnet` must be supplied.', + validator=validate_subnet('master_subnet')) + c.argument('worker_subnet', + help='Name or ID of worker vnet subnet. If name is supplied, `--vnet` must be supplied.', + validator=validate_subnet('worker_subnet')) diff --git a/python/az/aro/azext_aro/_rbac.py b/python/az/aro/azext_aro/_rbac.py new file mode 100644 index 000000000..e88bca5c0 --- /dev/null +++ b/python/az/aro/azext_aro/_rbac.py @@ -0,0 +1,35 @@ +import uuid + +from azure.cli.core.commands.client_factory import get_mgmt_service_client +from azure.cli.core.commands.client_factory import get_subscription_id +from azure.cli.core.profiles import get_sdk +from azure.cli.core.profiles import ResourceType +from msrestazure.azure_exceptions import CloudError +from msrestazure.tools import resource_id + + +CONTRIBUTOR = "b24988ac-6180-42a0-ab88-20f7382dd24c" + + +def assign_contributor_to_vnet(cli_ctx, vnet, object_id): + client = get_mgmt_service_client(cli_ctx, ResourceType.MGMT_AUTHORIZATION) + + RoleAssignmentCreateParameters = get_sdk(cli_ctx, ResourceType.MGMT_AUTHORIZATION, + 'RoleAssignmentCreateParameters', mod='models', + operation_group='role_assignments') + + try: + client.role_assignments.create(vnet, uuid.uuid4(), RoleAssignmentCreateParameters( + role_definition_id=resource_id( + subscription=get_subscription_id(cli_ctx), + namespace='Microsoft.Authorization', + type='roleDefinitions', + name=CONTRIBUTOR, + ), + principal_id=object_id, + principal_type="ServicePrincipal", + )) + except CloudError as err: + if err.status_code == 409: + return + raise err diff --git a/python/az/aro/azext_aro/_validators.py b/python/az/aro/azext_aro/_validators.py index 3ff7d0a54..3deea631d 100644 --- a/python/az/aro/azext_aro/_validators.py +++ b/python/az/aro/azext_aro/_validators.py @@ -1,14 +1,162 @@ -def example_name_or_id_validator(cmd, namespace): - # Example of a storage account name or ID validator. - # See: https://github.com/Azure/azure-cli/blob/dev/doc/authoring_command_modules/authoring_commands.md#supporting-name-or-id-parameters - from azure.cli.core.commands.client_factory import get_subscription_id - from msrestazure.tools import is_valid_resource_id, resource_id - if namespace.storage_account: - if not is_valid_resource_id(namespace.RESOURCE): - namespace.storage_account = resource_id( +import ipaddress +import uuid + +from azure.cli.core.commands.client_factory import get_mgmt_service_client +from azure.cli.core.commands.client_factory import get_subscription_id +from azure.cli.core.profiles import ResourceType +from knack.util import CLIError +from msrestazure.azure_exceptions import CloudError +from msrestazure.tools import is_valid_resource_id +from msrestazure.tools import parse_resource_id +from msrestazure.tools import resource_id + + +def validate_cidr(key): + def _validate_cidr(namespace): + cidr = getattr(namespace, key) + if cidr is not None: + try: + ipaddress.IPv4Network(cidr) + except ValueError: + raise CLIError("Invalid --%s '%s'." % + (key.replace('_', '-'), cidr)) + + return _validate_cidr + + +def validate_client_id(namespace): + if namespace.client_id is not None: + try: + uuid.UUID(namespace.client_id) + except ValueError: + raise CLIError("Invalid --client-id '%s'." % namespace.client_id) + + if namespace.client_secret is None or not str(namespace.client_secret): + raise CLIError("Must specify --client-secret with --client-id.") + + +def validate_client_secret(namespace): + if namespace.client_secret is not None: + if namespace.client_id is not None or not str(namespace.client_id): + raise CLIError("Must specify --client-id with --client-secret.") + + +def _validate_int(key, i): + try: + i = int(i) + except ValueError: + raise CLIError("Invalid --%s '%s'." % (key.replace('_', '-'), i)) + + return i + + +def validate_subnet(key): + def _validate_subnet(cmd, namespace): + subnet = getattr(namespace, key) + + if not is_valid_resource_id(subnet): + if not namespace.vnet: + raise CLIError( + "Must specify --vnet if --%s is not an id." % key.replace('_', '-')) + + validate_vnet(cmd, namespace) + + subnet = namespace.vnet + "/subnets/" + subnet + setattr(namespace, key, subnet) + + parts = parse_resource_id(subnet) + + if parts["subscription"] != get_subscription_id(cmd.cli_ctx): + raise CLIError("--%s subscription '%s' must equal cluster subscription." % + (key.replace('_', '-'), parts["subscription"])) + + if parts["namespace"].lower() != "microsoft.network": + raise CLIError("--%s namespace '%s' must equal Microsoft.Network." % + (key.replace('_', '-'), parts["namespace"])) + + if parts["type"].lower() != "virtualnetworks": + raise CLIError("--%s type '%s' must equal virtualNetworks." % + (key.replace('_', '-'), parts["type"])) + + if parts["last_child_num"] != 1: + raise CLIError("--%s '%s' must have one child." % + (key.replace('_', '-'), subnet)) + + if "child_namespace_1" in parts: + raise CLIError("--%s '%s' must not have child namespace." % + (key.replace('_', '-'), subnet)) + + if parts["child_type_1"].lower() != "subnets": + raise CLIError("--%s child type '%s' must equal subnets." % + (key.replace('_', '-'), subnet)) + + client = get_mgmt_service_client( + cmd.cli_ctx, ResourceType.MGMT_NETWORK) + try: + client.subnets.get(parts["resource_group"], + parts["name"], parts["child_name_1"]) + except CloudError as err: + raise CLIError(err.message) + + return _validate_subnet + + +def validate_subnets(master_subnet, worker_subnet): + master_parts = parse_resource_id(master_subnet) + worker_parts = parse_resource_id(worker_subnet) + + if master_parts["resource_group"].lower() != worker_parts["resource_group"].lower(): + raise CLIError("--master-subnet resource group '%s' must equal --worker-subnet resource group '%s'." % + (master_parts["resource_group"], worker_parts["resource_group"])) + + if master_parts["name"].lower() != worker_parts["name"].lower(): + raise CLIError("--master-subnet vnet name '%s' must equal --worker-subnet vnet name '%s'." % + (master_parts["name"], worker_parts["name"])) + + if master_parts["child_name_1"].lower() == worker_parts["child_name_1"].lower(): + raise CLIError("--master-subnet name '%s' must not equal --worker-subnet name '%s'." % + (master_parts["child_name_1"], worker_parts["child_name_1"])) + + return resource_id( + subscription=master_parts["subscription"], + resource_group=master_parts["resource_group"], + namespace='Microsoft.Network', + type='virtualNetworks', + name=master_parts["name"], + ) + + +def validate_vnet(cmd, namespace): + if namespace.vnet: + if not is_valid_resource_id(namespace.vnet): + if not namespace.vnet_resource_group_name: + raise CLIError( + "Must specify --vnet-resource-group-name if --vnet is not an id.") + + namespace.vnet = resource_id( subscription=get_subscription_id(cmd.cli_ctx), - resource_group=namespace.resource_group_name, - namespace='Microsoft.Storage', - type='storageAccounts', - name=namespace.storage_account + resource_group=namespace.vnet_resource_group_name, + namespace='Microsoft.Network', + type='virtualNetworks', + name=namespace.vnet, ) + + +def validate_worker_count(namespace): + if namespace.worker_count: + namespace.worker_count = _validate_int( + "worker_count", namespace.worker_count) + + if namespace.worker_count < 3: + raise CLIError( + "--worker-count must be greater than or equal to 3.") + + +def validate_worker_vm_disk_size_gb(namespace): + if namespace.worker_vm_disk_size_gb: + namespace.worker_vm_disk_size_gb = _validate_int( + "worker_vm_disk_size_gb", namespace.worker_vm_disk_size_gb) + + if namespace.worker_vm_disk_size_gb < 128: + raise CLIError( + "--worker_vm_disk_size_gb must be greater than or equal to 128.") diff --git a/python/az/aro/azext_aro/commands.py b/python/az/aro/azext_aro/commands.py index 7acbea666..1a25820d0 100644 --- a/python/az/aro/azext_aro/commands.py +++ b/python/az/aro/azext_aro/commands.py @@ -1,23 +1,20 @@ -# pylint: disable=line-too-long from azure.cli.core.commands import CliCommandType from azext_aro._client_factory import cf_aro +from azext_aro._format import aro_show_table_format +from azext_aro._format import aro_list_table_format +from azext_aro._help import helps # pylint: disable=unused-import def load_command_table(self, _): + aro_sdk = CliCommandType( + operations_tmpl='azext_aro.vendored_sdks.azure.mgmt.redhatopenshift.v2019_12_31_preview.operations#OpenShiftClustersOperations.{}', # pylint: disable=line-too-long + client_factory=cf_aro) - # TODO: Add command type here - # aro_sdk = CliCommandType( - # operations_tmpl='.operations#None.{}', - # client_factory=cf_aro) + with self.command_group('aro', aro_sdk, client_factory=cf_aro, is_preview=True) as g: + g.custom_command('create', 'aro_create', supports_no_wait=True) + g.custom_command('delete', 'aro_delete', supports_no_wait=True) + g.custom_command('list', 'aro_list', table_transformer=aro_list_table_format) + g.custom_show_command('show', 'aro_show', table_transformer=aro_show_table_format) + g.custom_command('update', 'aro_update', supports_no_wait=True) - - with self.command_group('aro') as g: - g.custom_command('create', 'create_aro') - # g.command('delete', 'delete') - g.custom_command('list', 'list_aro') - # g.show_command('show', 'get') - # g.generic_update_command('update', setter_name='update', custom_func_name='update_aro') - - - with self.command_group('aro', is_preview=True): - pass + g.custom_command('get-credentials', 'aro_get_credentials') diff --git a/python/az/aro/azext_aro/custom.py b/python/az/aro/azext_aro/custom.py index 610368642..e827da967 100644 --- a/python/az/aro/azext_aro/custom.py +++ b/python/az/aro/azext_aro/custom.py @@ -1,15 +1,139 @@ -from knack.util import CLIError +import os + +import azext_aro.vendored_sdks.azure.mgmt.redhatopenshift.v2019_12_31_preview.models as v2019_12_31_preview + +from azext_aro._aad import AADManager +from azext_aro._rbac import assign_contributor_to_vnet +from azext_aro._validators import validate_subnets +from azure.cli.core.commands.client_factory import get_subscription_id +from azure.cli.core.util import sdk_no_wait +from msrestazure.azure_exceptions import CloudError -def create_aro(cmd, resource_group_name, aro_name, location=None, tags=None): - raise CLIError('TODO: Implement `aro create`') +FP_CLIENT_ID = "f1dd0a37-89c6-4e07-bcd1-ffd3d43d8875" -def list_aro(cmd, resource_group_name=None): - raise CLIError('TODO: Implement `aro list`') +def aro_create(cmd, # pylint: disable=too-many-locals + client, + resource_group_name, + resource_name, + master_subnet, + worker_subnet, + vnet=None, + vnet_resource_group_name=None, # pylint: disable=unused-argument + location=None, + client_id=None, + client_secret=None, + pod_cidr=None, + service_cidr=None, + master_vm_size=None, + worker_vm_size=None, + worker_vm_disk_size_gb=None, + worker_count=None, + tags=None, + no_wait=False): + vnet = validate_subnets(master_subnet, worker_subnet) + + subscription_id = get_subscription_id(cmd.cli_ctx) + + aad = AADManager(cmd.cli_ctx) + if client_id is None: + app, client_secret = aad.createManagedApplication( + "aro-%s-%s-%s" % (subscription_id, resource_group_name, resource_name)) + client_id = app.app_id + + client_sp = aad.getServicePrincipal(client_id) + if not client_sp: + client_sp = aad.createServicePrincipal(client_id) + + rp_client_id = FP_CLIENT_ID + if rp_mode_development(): + rp_client_id = os.environ['AZURE_FP_CLIENT_ID'] + + rp_client_sp = aad.getServicePrincipal(rp_client_id) + + assign_contributor_to_vnet(cmd.cli_ctx, vnet, client_sp.object_id) + assign_contributor_to_vnet(cmd.cli_ctx, vnet, rp_client_sp.object_id) + + oc = v2019_12_31_preview.OpenShiftCluster( + location=location, + tags=tags, + service_principal_profile=v2019_12_31_preview.ServicePrincipalProfile( + client_id=client_id, + client_secret=client_secret, + ), + network_profile=v2019_12_31_preview.NetworkProfile( + pod_cidr=pod_cidr or "10.128.0.0/14", + service_cidr=service_cidr or "172.30.0.0/16", + ), + master_profile=v2019_12_31_preview.MasterProfile( + vm_size=master_vm_size or "Standard_D8s_v3", + subnet_id=master_subnet, + ), + worker_profiles=[ + v2019_12_31_preview.WorkerProfile( + name="worker", # TODO: "worker" should not be hard-coded + vm_size=worker_vm_size or "Standard_D2s_v3", + disk_size_gb=worker_vm_disk_size_gb or 128, + subnet_id=worker_subnet, + count=worker_count or 3, + ) + ] + ) + + return sdk_no_wait(no_wait, client.create, + resource_group_name=resource_group_name, + resource_name=resource_name, + parameters=oc) -def update_aro(cmd, instance, tags=None): - with cmd.update_context(instance) as c: - c.set_param('tags', tags) - return instance +def aro_delete(cmd, client, resource_group_name, resource_name, + no_wait=False): + if not no_wait: + aad = AADManager(cmd.cli_ctx) + try: + oc = client.get(resource_group_name, resource_name) + except CloudError as err: + if err.status_code == 404: + return + raise err + + sdk_no_wait(no_wait, client.delete, + resource_group_name=resource_group_name, + resource_name=resource_name) + + if not no_wait: + aad.deleteManagedApplication(oc.service_principal_profile.client_id) + + +def aro_list(client, resource_group_name=None): + if resource_group_name: + return client.list_by_resource_group(resource_group_name).value + return client.list().value + + +def aro_show(client, resource_group_name, resource_name): + return client.get(resource_group_name, resource_name) + + +def aro_get_credentials(client, resource_group_name, resource_name): + return client.get_credentials(resource_group_name, resource_name) + + +def aro_update(client, resource_group_name, resource_name, + worker_count=None, + no_wait=False): + oc = client.get(resource_group_name, resource_name) + + if worker_count is not None: + # TODO: [0] should not be hard-coded + oc.worker_profiles[0].count = worker_count + + return sdk_no_wait(no_wait, client.create, + resource_group_name=resource_group_name, + resource_name=resource_name, + parameters=oc) + + +def rp_mode_development(): + return os.environ.get('RP_MODE', '').lower() == 'development' diff --git a/python/az/aro/azext_aro/tests/latest/test_aro_scenario.py b/python/az/aro/azext_aro/tests/latest/test_aro_scenario.py index 2a3de03b6..51d7ca4e2 100644 --- a/python/az/aro/azext_aro/tests/latest/test_aro_scenario.py +++ b/python/az/aro/azext_aro/tests/latest/test_aro_scenario.py @@ -2,17 +2,16 @@ import os import unittest from azure_devtools.scenario_tests import AllowLargeResponse -from azure.cli.testsdk import (ScenarioTest, ResourceGroupPreparer) +from azure.cli.testsdk import ResourceGroupPreparer +from azure.cli.testsdk import ScenarioTest TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..')) class AroScenarioTest(ScenarioTest): - @ResourceGroupPreparer(name_prefix='cli_test_aro') def test_aro(self, resource_group): - self.kwargs.update({ 'name': 'test1' }) @@ -21,15 +20,19 @@ class AroScenarioTest(ScenarioTest): self.check('tags.foo', 'doo'), self.check('name', '{name}') ]) + self.cmd('aro update -g {rg} -n {name} --tags foo=boo', checks=[ self.check('tags.foo', 'boo') ]) + count = len(self.cmd('aro list').get_output_in_json()) - self.cmd('aro show - {rg} -n {name}', checks=[ + self.cmd('aro show -g {rg} -n {name}', checks=[ self.check('name', '{name}'), self.check('resourceGroup', '{rg}'), self.check('tags.foo', 'boo') ]) + self.cmd('aro delete -g {rg} -n {name}') + final_count = len(self.cmd('aro list').get_output_in_json()) self.assertTrue(final_count, count - 1) diff --git a/python/az/aro/azext_aro/vendored_sdks b/python/az/aro/azext_aro/vendored_sdks new file mode 120000 index 000000000..2c1f52acc --- /dev/null +++ b/python/az/aro/azext_aro/vendored_sdks @@ -0,0 +1 @@ +../../../client \ No newline at end of file diff --git a/python/az/aro/setup.py b/python/az/aro/setup.py index afc79ba93..43a26520a 100644 --- a/python/az/aro/setup.py +++ b/python/az/aro/setup.py @@ -8,8 +8,6 @@ except ImportError: from distutils import log as logger logger.warn("Wheel is not available, disabling bdist_wheel hook") -# TODO: Confirm this is the right version number you want and it matches your -# HISTORY.rst entry. VERSION = '0.1.0' # The full list of classifiers is available at @@ -28,7 +26,6 @@ CLASSIFIERS = [ 'License :: OSI Approved :: Apache Software License', ] -# TODO: Add any additional SDK dependencies here DEPENDENCIES = [ 'azure-cli-core' ] @@ -41,12 +38,10 @@ with open('HISTORY.rst', 'r', encoding='utf-8') as f: setup( name='aro', version=VERSION, - description='Microsoft Azure Command-Line Tools Aro Extension', - # TODO: Update author and email, if applicable - author='Microsoft Corporation', - author_email='azpycli@microsoft.com', - # TODO: consider pointing directly to your source code instead of the generic repo - url='https://github.com/Azure/azure-cli-extensions', + description='Microsoft Azure Command-Line Tools ARO Extension', + author='Red Hat, Inc.', + author_email='support@redhat.com', + url='https://github.com/jim-minter/rp', long_description=README + '\n\n' + HISTORY, license='Apache', classifiers=CLASSIFIERS, diff --git a/python/client/__init__.py b/python/client/__init__.py new file mode 100644 index 000000000..e69de29bb