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

AzExt - Adding support for the v2021-09-01-preview API #1739

Merged
merged 6 commits into from
Oct 5, 2021
Merged
Show file tree
Hide file tree
Changes from 3 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
62 changes: 62 additions & 0 deletions docs/disk-encryption-set.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Using a custom Disk Encryption Set

## What is the Disk Encryption Set used for?

In summary: it allows the customer to control the keys that are used to encrypt/decrypt the VM disks.
See https://docs.microsoft.com/en-us/azure/virtual-machines/disks-enable-host-based-encryption-portal#deploy-a-vm-with-customer-managed-keys for more information.

## How to deploy?
First install and use the AzureCLI extension with
```
make az
```

You can check if the extension is in use by running
```
$ az extension list

[
{
"experimental": false,
"extensionType": "dev",
"name": "aro",
"path": "<path to go SRC>/github.com/Azure/ARO-RP/python/az/aro",
"preview": true,
"version": "1.0.1"
}
```

Follow https://docs.microsoft.com/en-us/azure/openshift/tutorial-create-cluster but don't run the `az aro create` command. Instead:

- set additional env variables
```
export KEYVAULT_NAME=$USER-enckv
export KEYVAULT_KEY_NAME=$USER-key
export DISK_ENCRYPTION_SET_NAME=$USER-des
```
- create the KeyVault and Key
```

az keyvault create -n $KEYVAULT_NAME -g $RESOURCEGROUP -l $LOCATION --enable-purge-protection true --enable-soft-delete true

az keyvault key create --vault-name $KEYVAULT_NAME -n $KEYVAULT_KEY_NAME --protection software

KEYVAULT_ID=$(az keyvault show --name $KEYVAULT_NAME --query "[id]" -o tsv)

KEYVAULT_KEY_URL=$(az keyvault key show --vault-name $KEYVAULT_NAME --name $KEYVAULT_KEY_NAME --query "[key.kid]" -o tsv)
```
- create the DES and add permissions to use the KeyVault
```
az disk-encryption-set create -n $DISK_ENCRYPTION_SET_NAME -l $LOCATION -g $RESOURCEGROUP --source-vault $KEYVAULT_ID --key-url $KEYVAULT_KEY_URL

DES_IDENTITY=$(az disk-encryption-set show -n $DISK_ENCRYPTION_SET_NAME -g $RESOURCEGROUP --query "[identity.principalId]" -o tsv)

az keyvault set-policy -n $KEYVAULT_NAME -g $RESOURCEGROUP --object-id $DES_IDENTITY --key-permissions wrapkey unwrapkey get
```
- run the az aro create command
```
az aro create --resource-group $RESOURCEGROUP --name $CLUSTER --vnet aro-vnet --master-subnet master-subnet --worker-subnet worker-subnet --disk-encryption-set $DES_ID
```

After creating the cluster all VMs should have the customer controlled Disk Encryption Set.
Remember to delete the disk-encryption-set and keyvault after you're done.
4 changes: 4 additions & 0 deletions python/az/aro/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ Release History
1.0.0
++++++
* Remove preview flag.

1.0.1
++++++
* Switch to new preview API
2 changes: 1 addition & 1 deletion python/az/aro/azext_aro/_client_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import urllib3

from azext_aro.custom import rp_mode_development
from azext_aro.vendored_sdks.azure.mgmt.redhatopenshift.v2020_04_30 import AzureRedHatOpenShiftClient
from azext_aro.vendored_sdks.azure.mgmt.redhatopenshift.v2021_09_01_preview import AzureRedHatOpenShiftClient
from azure.cli.core.commands.client_factory import get_mgmt_service_client


Expand Down
18 changes: 18 additions & 0 deletions python/az/aro/azext_aro/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
from azext_aro._validators import validate_cidr
from azext_aro._validators import validate_client_id
from azext_aro._validators import validate_cluster_resource_group
from azext_aro._validators import validate_disk_encryption_set
from azext_aro._validators import validate_domain
from azext_aro._validators import validate_encryption_at_host
from azext_aro._validators import validate_pull_secret
from azext_aro._validators import validate_sdn
from azext_aro._validators import validate_subnet
from azext_aro._validators import validate_client_secret
from azext_aro._validators import validate_visibility
Expand Down Expand Up @@ -54,10 +57,25 @@ def load_arguments(self, _):
c.argument('service_cidr',
help='CIDR of service network. Must be a minimum of /18 or larger.',
validator=validate_cidr('service_cidr'))
c.argument('software_defined_network', arg_type=get_enum_type(['OVNKubernetes', 'OpenShiftSDN']),
options_list=['--sdn-type'],
m1kola marked this conversation as resolved.
Show resolved Hide resolved
help='SDN type either "OVNKubernetes" or "OpenShiftSDN (default)"',
Makdaam marked this conversation as resolved.
Show resolved Hide resolved
validator=validate_sdn)

c.argument('disk_encryption_set',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed with @Makdaam that encryption parameters need a clear documentation on docs.microsoft.com.
disk_encryption_set is providing customer keys and makes customer responsible for managing them. Without this option set, disks will get encrypted with Azure provided and rotated keys.

This will be documented later on.

help='ResourceID of the DiskEncryptionSet to be used for master and worker VMs.',
validator=validate_disk_encryption_set)
c.argument('master_encryption_at_host', arg_type=get_enum_type(['Enabled', 'Disabled']),
options_list=['--master-enc-at-host'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there reason we use --master-enc-at-host and not --master-encryption-at-host? Matches original flag name

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on using full name.

Also I see that az vm create uses --encryption-at-host argument as a boolean flag (either present or not, no value). If I read this code correctly our command is going to be --master-enc-at-host Enabled.

Maybe we also need to use it as boolean for simplicity? If present - send Enabled to the API, if not - send Disabled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had master-encryption-at-host, but that doesn't pass validation since the parameter name is too long.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, found the linter rule, and at least one version of the parameter has to be <22 characters (not counting -- or - in front). I'm wondering if I should shorten the short versions even more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To sum up:

  • added long names and shortened the short names a bit
  • changed the param type to tri-state (so that you don't have to do "Enabled")
  • removed param validator because it's not needed.

help='Encryption at host flag for master VMs. Correct values are "Enabled" or "Disabled" (default)',
validator=validate_encryption_at_host('master_encryption_at_host'))
c.argument('master_vm_size',
help='Size of master VMs.')

c.argument('worker_encryption_at_host', arg_type=get_enum_type(['Enabled', 'Disabled']),
options_list=['--worker-enc-at-host'],
help='Encryption at host flag for worker VMs. Correct values are "Enabled" or "Disabled" (default)',
validator=validate_encryption_at_host('worker_encryption_at_host'))
c.argument('worker_vm_size',
help='Size of worker VMs.')
c.argument('worker_vm_disk_size_gb',
Expand Down
11 changes: 6 additions & 5 deletions python/az/aro/azext_aro/_rbac.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
from msrest.exceptions import ValidationError
from msrestazure.tools import resource_id

NETWORK_CONTRIBUTOR = '4d97b98b-1d4f-4787-a291-c67834d212e7'
ROLE_NETWORK_CONTRIBUTOR = '4d97b98b-1d4f-4787-a291-c67834d212e7'
ROLE_READER = 'acdd72a7-3385-48ef-bd42-f606fba81ae7'

logger = get_logger(__name__)

Expand All @@ -34,7 +35,7 @@ def _create_role_assignment(auth_client, resource, params):
logger.warning("%s; retry %d of %d", ex, retries, max_retries)


def assign_network_contributor_to_resource(cli_ctx, resource, object_id):
def assign_role_to_resource(cli_ctx, resource, object_id, role_name):
auth_client = get_mgmt_service_client(cli_ctx, ResourceType.MGMT_AUTHORIZATION)

RoleAssignmentCreateParameters = get_sdk(cli_ctx, ResourceType.MGMT_AUTHORIZATION,
Expand All @@ -45,7 +46,7 @@ def assign_network_contributor_to_resource(cli_ctx, resource, object_id):
subscription=get_subscription_id(cli_ctx),
namespace='Microsoft.Authorization',
type='roleDefinitions',
name=NETWORK_CONTRIBUTOR,
name=role_name,
)

_create_role_assignment(auth_client, resource, RoleAssignmentCreateParameters(
Expand All @@ -55,14 +56,14 @@ def assign_network_contributor_to_resource(cli_ctx, resource, object_id):
))


def has_network_contributor_on_resource(cli_ctx, resource, object_id):
def has_role_assignment_on_resource(cli_ctx, resource, object_id, role_name):
auth_client = get_mgmt_service_client(cli_ctx, ResourceType.MGMT_AUTHORIZATION)

role_definition_id = resource_id(
subscription=get_subscription_id(cli_ctx),
namespace='Microsoft.Authorization',
type='roleDefinitions',
name=NETWORK_CONTRIBUTOR,
name=role_name,
)

for assignment in auth_client.role_assignments.list_for_scope(resource):
Expand Down
35 changes: 35 additions & 0 deletions python/az/aro/azext_aro/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,22 @@ def validate_cluster_resource_group(cmd, namespace):
namespace.cluster_resource_group)


def validate_disk_encryption_set(cmd, namespace):
if namespace.disk_encryption_set is not None:
if not is_valid_resource_id(namespace.disk_encryption_set):
raise InvalidArgumentValueError(
"Invalid --disk-encryption-set '%s', has to be a resource ID." %
namespace.disk_encryption_set)

desid = parse_resource_id(namespace.disk_encryption_set)
compute_client = get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_COMPUTE)
try:
compute_client.disk_encryption_sets.get(resource_group_name=desid['resource_group'],
disk_encryption_set_name=desid['name'])
except CloudError as err:
raise CLIInternalError(err.message) from err
m1kola marked this conversation as resolved.
Show resolved Hide resolved


def validate_domain(namespace):
if namespace.domain is not None:
if not re.match(r'^' +
Expand All @@ -74,6 +90,18 @@ def validate_domain(namespace):
namespace.domain)


def validate_encryption_at_host(key):
def _validate_encryption_at_host(namespace):
eah = getattr(namespace, key)
if eah is not None:
eah = eah.capitalize()
if eah not in ['Enabled', 'Disabled']:
raise InvalidArgumentValueError("Invalid --%s '%s'." %
(key.replace('_', '-'), eah))

return _validate_encryption_at_host


def validate_pull_secret(namespace):
if namespace.pull_secret is None:
# TODO: add aka.ms link here
Expand All @@ -90,6 +118,13 @@ def validate_pull_secret(namespace):
raise InvalidArgumentValueError("Invalid --pull-secret.") from e


def validate_sdn(namespace):
if namespace.software_defined_network is not None:
if namespace.software_defined_network not in ['OVNKubernetes', 'OpenshiftSDN']:
raise InvalidArgumentValueError("Invalid --software-defined-network '%s'." %
namespace.software_defined_network)


def validate_subnet(key):
def _validate_subnet(cmd, namespace):
subnet = getattr(namespace, key)
Expand Down
2 changes: 1 addition & 1 deletion python/az/aro/azext_aro/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

def load_command_table(self, _):
aro_sdk = CliCommandType(
operations_tmpl='azext_aro.vendored_sdks.azure.mgmt.redhatopenshift.v2020_04_30.operations#OpenShiftClustersOperations.{}', # pylint: disable=line-too-long
operations_tmpl='azext_aro.vendored_sdks.azure.mgmt.redhatopenshift.v2021_09_01_preview.operations#OpenShiftClustersOperations.{}', # pylint: disable=line-too-long
client_factory=cf_aro)

with self.command_group('aro', aro_sdk, client_factory=cf_aro) as g:
Expand Down
57 changes: 38 additions & 19 deletions python/az/aro/azext_aro/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
from msrest.exceptions import HttpOperationError
from knack.log import get_logger

import azext_aro.vendored_sdks.azure.mgmt.redhatopenshift.v2020_04_30.models as openshiftcluster
import azext_aro.vendored_sdks.azure.mgmt.redhatopenshift.v2021_09_01_preview.models as openshiftcluster

from azext_aro._aad import AADManager
from azext_aro._rbac import assign_network_contributor_to_resource, has_network_contributor_on_resource
from azext_aro._rbac import assign_role_to_resource, has_role_assignment_on_resource
from azext_aro._rbac import ROLE_NETWORK_CONTRIBUTOR, ROLE_READER
from azext_aro._validators import validate_subnets

logger = get_logger(__name__)
Expand All @@ -42,7 +43,11 @@ def aro_create(cmd, # pylint: disable=too-many-locals
client_secret=None,
pod_cidr=None,
service_cidr=None,
software_defined_network=None,
disk_encryption_set=None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API Accepts separate disk encryption sets for master and workers, should we not add 2 options here too?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mjudeikis API does accept two valeus, but we decided to force worker encrpyion set id match master one for now (see code below).

if !strings.EqualFold(mp.DiskEncryptionSetID, wp.DiskEncryptionSetID) {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".subnetId", "The provided worker disk encryption set '%s' is invalid: must be the same as master disk encryption set '%s'.", wp.DiskEncryptionSetID, mp.DiskEncryptionSetID)
}

I don't worry much about implementation: I'm ok with aro_create accepting two separate params.

But if we are talking about exposing separate CLI params to the customers - I would be in favour of keeping 1 param for now to avoid confusion. If we decide to relax this validation - we will add a second param (--disk-encryption-set and new --master-disk-encryption-set) or rename existing and add new one (so we have, for example, --worker-disk-encryption-set and --master-disk-encryption-set).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we decide to relax I'd leave the old --disk-encryption-set, and add 2 new (one for master, one for worker). Then throw an error if the old one is specified at the same time as one of the new ones. This will keep backwards compatibility if someone uses az aro in a script.

master_encryption_at_host=None,
master_vm_size=None,
worker_encryption_at_host=None,
worker_vm_size=None,
worker_vm_disk_size_gb=None,
worker_count=None,
Expand Down Expand Up @@ -104,10 +109,13 @@ def aro_create(cmd, # pylint: disable=too-many-locals
network_profile=openshiftcluster.NetworkProfile(
pod_cidr=pod_cidr or '10.128.0.0/14',
service_cidr=service_cidr or '172.30.0.0/16',
software_defined_network=software_defined_network or 'OpenShiftSDN'
),
master_profile=openshiftcluster.MasterProfile(
vm_size=master_vm_size or 'Standard_D8s_v3',
subnet_id=master_subnet,
encryption_at_host=master_encryption_at_host or 'Disabled',
disk_encryption_set_id=disk_encryption_set,
),
worker_profiles=[
openshiftcluster.WorkerProfile(
Expand All @@ -116,6 +124,8 @@ def aro_create(cmd, # pylint: disable=too-many-locals
disk_size_gb=worker_vm_disk_size_gb or 128,
subnet_id=worker_subnet,
count=worker_count or 3,
encryption_at_host=worker_encryption_at_host or 'Disabled',
disk_encryption_set_id=disk_encryption_set,
)
],
apiserver_profile=openshiftcluster.APIServerProfile(
Expand Down Expand Up @@ -279,6 +289,13 @@ def get_network_resources(cli_ctx, subnets, vnet):
return resources


def get_disk_encryption_resources(oc):
disk_encryption_set = oc.master_profile.disk_encryption_set_id
resources = set()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After a discussion, the set() is used to keep code consistency even when there will be one object. This makes code more streamlined.

resources.add(disk_encryption_set)
return resources


# cluster_application_update manages cluster application & service principal update
# If called without parameters it should be best-effort
# If called with parameters it fails if something is not possible
Expand Down Expand Up @@ -363,8 +380,9 @@ def resolve_rp_client_id():

def ensure_resource_permissions(cli_ctx, oc, fail, sp_obj_ids):
try:
# Get cluster resources we need to assign network contributor on
resources = get_cluster_network_resources(cli_ctx, oc)
# Get cluster resources we need to assign permissions on, sort to ensure the same order of operations
resources = {ROLE_NETWORK_CONTRIBUTOR: sorted(get_cluster_network_resources(cli_ctx, oc)),
ROLE_READER: sorted(get_disk_encryption_resources(oc))}
Makdaam marked this conversation as resolved.
Show resolved Hide resolved
except (CloudError, HttpOperationError) as e:
if fail:
logger.error(e.message)
Expand All @@ -373,18 +391,19 @@ def ensure_resource_permissions(cli_ctx, oc, fail, sp_obj_ids):
return

for sp_id in sp_obj_ids:
for resource in sorted(resources):
# Create the role assignment if it doesn't exist
# Assume that the role assignment exists if we fail to look it up
resource_contributor_exists = True

try:
resource_contributor_exists = has_network_contributor_on_resource(cli_ctx, resource, sp_id)
except CloudError as e:
if fail:
logger.error(e.message)
raise
logger.info(e.message)

if not resource_contributor_exists:
assign_network_contributor_to_resource(cli_ctx, resource, sp_id)
for role in resources:
for resource in resources[role]:
# Create the role assignment if it doesn't exist
# Assume that the role assignment exists if we fail to look it up
resource_contributor_exists = True

try:
resource_contributor_exists = has_role_assignment_on_resource(cli_ctx, resource, sp_id, role)
except CloudError as e:
if fail:
logger.error(e.message)
raise
logger.info(e.message)

if not resource_contributor_exists:
assign_role_to_resource(cli_ctx, resource, sp_id, role)