Skip to content

Commit e87449b

Browse files
Nicholas AdamsNicholas Adams
authored andcommitted
Merge branch 'main' into na_create_pool_monitoring_update
2 parents 8cb7be5 + 72ffc00 commit e87449b

5 files changed

Lines changed: 235 additions & 1 deletion

File tree

cfa/cloudops/_function_app_client.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from typing import Callable, List, Optional, Tuple
99

1010
import duckdb
11+
from azure.identity import DefaultAzureCredential
12+
from azure.mgmt.web import WebSiteManagementClient
1113

1214
from .auth import (
1315
DefaultCredentialHandler,
@@ -22,6 +24,105 @@
2224

2325

2426
class FunctionAppClient:
27+
@classmethod
28+
def get_configuration(
29+
cls,
30+
function_app_name: str,
31+
resource_group: Optional[str] = None,
32+
subscription_id: Optional[str] = None,
33+
) -> Optional[dict]:
34+
resource_group = resource_group or os.getenv("AZURE_RESOURCE_GROUP")
35+
if resource_group is None:
36+
raise ValueError(
37+
"Resource group must be provided either as an argument or through the AZURE_RESOURCE_GROUP environment variable."
38+
)
39+
subscription_id = subscription_id or os.getenv("AZURE_SUBSCRIPTION_ID")
40+
if subscription_id is None:
41+
raise ValueError(
42+
"Subscription ID must be provided either as an argument or through the AZURE_SUBSCRIPTION_ID environment variable."
43+
)
44+
credential = DefaultAzureCredential()
45+
web_mgmt_client = WebSiteManagementClient(credential, subscription_id)
46+
function_app_config = web_mgmt_client.web_apps.get_configuration(
47+
resource_group, function_app_name
48+
)
49+
return function_app_config
50+
51+
@classmethod
52+
def get_tags(
53+
cls,
54+
function_app_name: str,
55+
resource_group: Optional[str] = None,
56+
subscription_id: Optional[str] = None,
57+
) -> Optional[dict]:
58+
return cls.get_configuration(
59+
function_app_name, resource_group, subscription_id
60+
).additional_properties.get("tags", [])
61+
62+
@classmethod
63+
def get_health_check_flag(
64+
cls,
65+
function_app_name: str,
66+
resource_group: Optional[str] = None,
67+
subscription_id: Optional[str] = None,
68+
) -> Optional[dict]:
69+
return (
70+
cls.get_configuration(
71+
function_app_name, resource_group, subscription_id
72+
).health_check_path
73+
is not None
74+
)
75+
76+
@classmethod
77+
def list_functions(
78+
cls,
79+
function_app_name: str,
80+
resource_group: Optional[str] = None,
81+
subscription_id: Optional[str] = None,
82+
) -> Optional[dict]:
83+
resource_group = resource_group or os.getenv("AZURE_RESOURCE_GROUP")
84+
if resource_group is None:
85+
raise ValueError(
86+
"Resource group must be provided either as an argument or through the AZURE_RESOURCE_GROUP environment variable."
87+
)
88+
subscription_id = subscription_id or os.getenv("AZURE_SUBSCRIPTION_ID")
89+
if subscription_id is None:
90+
raise ValueError(
91+
"Subscription ID must be provided either as an argument or through the AZURE_SUBSCRIPTION_ID environment variable."
92+
)
93+
credential = DefaultAzureCredential()
94+
web_mgmt_client = WebSiteManagementClient(credential, subscription_id)
95+
function_list = []
96+
for function in web_mgmt_client.web_apps.list_functions(
97+
resource_group, function_app_name
98+
):
99+
function_list.append(function.as_dict())
100+
return function_list
101+
102+
@classmethod
103+
def get_function_details(
104+
cls,
105+
function_app_name: str,
106+
function_name: str,
107+
resource_group: Optional[str] = None,
108+
subscription_id: Optional[str] = None,
109+
) -> Optional[dict]:
110+
resource_group = resource_group or os.getenv("AZURE_RESOURCE_GROUP")
111+
if resource_group is None:
112+
raise ValueError(
113+
"Resource group must be provided either as an argument or through the AZURE_RESOURCE_GROUP environment variable."
114+
)
115+
subscription_id = subscription_id or os.getenv("AZURE_SUBSCRIPTION_ID")
116+
if subscription_id is None:
117+
raise ValueError(
118+
"Subscription ID must be provided either as an argument or through the AZURE_SUBSCRIPTION_ID environment variable."
119+
)
120+
credential = DefaultAzureCredential()
121+
web_mgmt_client = WebSiteManagementClient(credential, subscription_id)
122+
return web_mgmt_client.web_apps.get_function(
123+
resource_group, function_app_name, function_name
124+
)
125+
25126
def __init__(
26127
self,
27128
function_app_name: Optional[str] = None,

cfa/cloudops/helpers.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,50 @@ def list_acr_tags(registry_name: str, repo_name: str) -> list[str]:
378378
logger.info(
379379
f"Listing tags for ACR repository: {registry_name}.azurecr.io/{repo_name}"
380380
)
381+
# Check whether Azure CLI is already authenticated before attempting login.
382+
auth_check = sp.run(
383+
["az", "account", "show", "--output", "none"],
384+
capture_output=True,
385+
text=True,
386+
)
387+
388+
if auth_check.returncode != 0:
389+
auth_check_error = auth_check.stderr.strip() if auth_check.stderr else ""
390+
if auth_check_error:
391+
logger.debug(f"Azure CLI account check stderr: {auth_check_error}")
392+
logger.info("Azure CLI account check failed; attempting managed identity login")
393+
login_result = sp.run(
394+
["az", "login", "--identity", "--output", "none"],
395+
capture_output=True,
396+
text=True,
397+
)
398+
399+
if login_result.returncode != 0:
400+
login_error = (
401+
login_result.stderr.strip() if login_result.stderr else "Unknown error"
402+
)
403+
logger.warning(
404+
"Managed identity login failed; checking for an existing Azure CLI session. "
405+
f"az login --identity error: {login_error}"
406+
)
407+
408+
account_show_result = sp.run(
409+
["az", "account", "show", "--output", "none"],
410+
capture_output=True,
411+
text=True,
412+
)
413+
if account_show_result.returncode != 0:
414+
raise Exception(
415+
"Azure CLI authentication failed: managed identity login was unsuccessful "
416+
"and no existing authenticated session was found. "
417+
f"az login --identity error: {login_error}"
418+
)
419+
420+
logger.info(
421+
"Managed identity login failed, but an existing Azure CLI session is "
422+
"authenticated; proceeding with the existing session."
423+
)
424+
# get tags command
381425
acr_tags_command = [
382426
"az",
383427
"acr",

changelog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
77
The versioning pattern is `major.minor.patch`.
88

99
---
10+
## v0.3.21
11+
- added metadata lookup methods for Azure Function Apps to the `FunctionAppClient` module
12+
13+
## v0.3.20
14+
- Added managed identity login to `list_acr_tags`
15+
1016
## v0.3.19
1117
- reverted to snake_cake notation for reading batch mount properties
1218

docs/FunctionAppClient/index.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,89 @@ After creating the function app, please contact EDAV team and request permission
363363
- Debugging the function app in Azure requires specific permissions. Currently, debugging is not supported directly. Users may need to work with the EDAV team for additional access if needed.
364364
- Currently the Azure Function Manager can only be invoked from a Linux environment such as Bash shell, Git Bash or Windows Subsystem for Linux. In a future release, support shall be added for Windows Command prompt.
365365

366+
## 6. Scripts for getting metadata about exising Azure Function App
367+
368+
After successfully deploying a function app, you can use one of the following class methods on `FunctionAppClient` to retrieve configuration and runtime details about the function app. All these class methods expect Azure resource group and subscription ID to be provided. There are 2 ways of setting this up:
369+
370+
* set the `AZURE_RESOURCE_GROUP` and `AZURE_SUBSCRIPTION_ID` environment variables, or
371+
* set the `resource_group` and `subscription_id` method parameters on each invocation
372+
373+
#### get_configuration
374+
375+
Retrieves the configuration details an app, such as platform version, runtime environment, handler mappings, IP security restrictions, default documents, virtual applications, Always On, etc.
376+
377+
_Example_:
378+
```python
379+
from cfa.cloudops import FunctionAppClient
380+
FunctionAppClient.get_configuration(function_app_name='cfapredictafmprdfunc03')
381+
```
382+
383+
_Response_:
384+
```json
385+
{'additional_properties': {'location': 'East US', 'tags': {'center': 'cfa', 'dateCreated': '2024-07-25 00:00:00', 'division': 'none', 'environment': 'prd', 'project': 'afm', 'requestor': ' ure7@cdc.gov', 'Purpose': 'Run Scheduled Job', 'costid': 'cfa', 'resourcename': 'cfapredictafmprdfunc03', 'issharedresource': 'no', 'owner': 'edav', 'createdby': 'none'}}, 'id': '/subscriptions/ef340bd6-2809-4635-b18b-7e6583a8803b/resourceGroups/EXT-EDAV-CFA-PRD/providers/Microsoft.Web/sites/cfapredictafmprdfunc03/config/web', 'name': 'cfapredictafmprdfunc03', 'kind': None, 'type': 'Microsoft.Web/sites/config', 'number_of_workers': 1, 'default_documents': ['Default.htm', 'Default.html', 'Default.asp', 'index.htm', 'index.html', 'iisstart.htm', 'default.aspx', 'index.php'], 'net_framework_version': 'v4.0', 'php_version': '', 'python_version': '', 'node_version': '', 'power_shell_version': '', 'linux_fx_version': 'PYTHON|3.9', 'windows_fx_version': None, 'request_tracing_enabled': False, 'request_tracing_expiration_time': None, 'remote_debugging_enabled': False, 'remote_debugging_version': None, 'http_logging_enabled': False, 'acr_use_managed_identity_creds': False, 'acr_user_managed_identity_id': None, 'logs_directory_size_limit': 35, 'detailed_error_logging_enabled': False, 'publishing_username': '$cfapredictafmprdfunc03', 'app_settings': None, 'metadata': None, 'connection_strings': None, 'machine_key': None, 'handler_mappings': None, 'document_root': None, 'scm_type': 'None', 'use32_bit_worker_process': False, 'web_sockets_enabled': False, 'always_on': True, 'java_version': None, 'java_container': None, 'java_container_version': None, 'app_command_line': '', 'managed_pipeline_mode': 'Integrated', 'virtual_applications': [<azure.mgmt.web.models._models_py3.VirtualApplication object at 0x789ab99aee00>], 'load_balancing': 'LeastRequests', 'experiments': <azure.mgmt.web.models._models_py3.Experiments object at 0x789ab99ac340>, 'limits': None, 'auto_heal_enabled': False, 'auto_heal_rules': None, 'tracing_options': None, 'vnet_name': '', 'vnet_route_all_enabled': False, 'vnet_private_ports_count': 0, 'cors': <azure.mgmt.web.models._models_py3.CorsSettings object at 0x789ab99ac5e0>, 'push': None, 'api_definition': None, 'api_management_config': None, 'auto_swap_slot_name': None, 'local_my_sql_enabled': False, 'managed_service_identity_id': None, 'x_managed_service_identity_id': None, 'key_vault_reference_identity': None, 'ip_security_restrictions': [<azure.mgmt.web.models._models_py3.IpSecurityRestriction object at 0x789ab99acfd0>], 'ip_security_restrictions_default_action': 'Allow', 'scm_ip_security_restrictions': [<azure.mgmt.web.models._models_py3.IpSecurityRestriction object at 0x789ab99aebc0>], 'scm_ip_security_restrictions_default_action': 'Allow', 'scm_ip_security_restrictions_use_main': False, 'http20_enabled': False, 'http20_proxy_flag': 0, 'min_tls_version': '1.2', 'min_tls_cipher_suite': None, 'scm_min_tls_version': '1.2', 'ftps_state': 'Disabled', 'pre_warmed_instance_count': 0, 'function_app_scale_limit': 0, 'elastic_web_app_scale_limit': None, 'health_check_path': '/api/HealthCheck', 'functions_runtime_scale_monitoring_enabled': False, 'website_time_zone': None, 'minimum_elastic_instance_count': 1, 'azure_storage_accounts': {}, 'public_network_access': 'Enabled'}
386+
{'location': 'East US', 'tags': {'center': 'cfa', 'dateCreated': '2024-07-25 00:00:00', 'division': 'none', 'environment': 'prd', 'project': 'afm', 'requestor': ' ure7@cdc.gov', 'Purpose': 'Run Scheduled Job', 'costid': 'cfa', 'resourcename': 'cfapredictafmprdfunc03', 'issharedresource': 'no', 'owner': 'edav', 'createdby': 'none'}}
387+
```
388+
389+
#### get_function_details
390+
391+
Fetch details of specific function within a function app. You can invoke the `list_functions` method to obtain list of function names from the function app.
392+
393+
_Example_:
394+
```python
395+
from cfa.cloudops import FunctionAppClient
396+
FunctionAppClient.get_function_details(function_app_name='cfapredictafmprdfunc03', function_name='cfatimer')
397+
```
398+
399+
_Response_:
400+
```json
401+
{'additional_properties': {'location': 'East US'}, 'id': '/subscriptions/ef340bd6-2809-4635-b18b-7e6583a8803b/resourceGroups/EXT-EDAV-CFA-PRD/providers/Microsoft.Web/sites/cfapredictafmprdfunc03/functions/cfatimer', 'name': 'cfapredictafmprdfunc03/cfatimer', 'kind': None, 'type': 'Microsoft.Web/sites/functions', 'function_app_id': None, 'script_root_path_href': None, 'script_href': 'https://cfapredictafmprdfunc03.cfa-prd-app.appserviceenvironment.net/admin/vfs/home/site/wwwroot/function_app.py', 'config_href': None, 'test_data_href': 'https://cfapredictafmprdfunc03.cfa-prd-app.appserviceenvironment.net/admin/vfs/home/data/Functions/sampledata/cfatimer.dat', 'secrets_file_href': None, 'href': 'https://cfapredictafmprdfunc03.cfa-prd-app.appserviceenvironment.net/admin/functions/cfatimer', 'config': {'name': 'cfatimer', 'entryPoint': 'cfatimer', 'scriptFile': 'function_app.py', 'language': 'python', 'functionDirectory': '/home/site/wwwroot', 'bindings': [{'direction': 'IN', 'type': 'timerTrigger', 'name': 'cfatimer', 'schedule': '%CFANotificationV2CRON%', 'runOnStartup': False, 'useMonitor': True}]}, 'files': None, 'test_data': '', 'invoke_url_template': None, 'language': 'python', 'is_disabled': False}
402+
```
403+
404+
#### get_health_check_flag
405+
406+
Retrieves the current status (True or False) of health check enabled on a function app.
407+
408+
_Example_:
409+
```python
410+
from cfa.cloudops import FunctionAppClient
411+
FunctionAppClient.get_health_check_flag(function_app_name='cfapredictafmprdfunc03')
412+
```
413+
414+
_Response_:
415+
```text
416+
True
417+
```
418+
419+
#### get_tags
420+
421+
Retrieves the tags assigned to an app. This is a subset of the response returned from `get_configuration` method.
422+
423+
_Example_:
424+
```python
425+
from cfa.cloudops import FunctionAppClient
426+
FunctionAppClient.get_tags(function_app_name='cfapredictafmprdfunc03')
427+
```
428+
429+
_Response_:
430+
```json
431+
{'location': 'East US', 'tags': {'center': 'cfa', 'dateCreated': '2024-07-25 00:00:00', 'division': 'none', 'environment': 'prd', 'project': 'afm', 'requestor': ' ure7@cdc.gov', 'Purpose': 'Run Scheduled Job', 'costid': 'cfa', 'resourcename': 'cfapredictafmprdfunc03', 'issharedresource': 'no', 'owner': 'edav', 'createdby': 'none'}}
432+
```
433+
434+
#### list_functions
435+
436+
Retrieves list of all functions packaged and deployed to the specified function app.
437+
438+
_Example_:
439+
```python
440+
from cfa.cloudops import FunctionAppClient
441+
FunctionAppClient.list_functions(function_app_name='cfapredictafmprdfunc03')
442+
```
443+
444+
_Response_:
445+
```json
446+
[{'id': '/subscriptions/ef340bd6-2809-4635-b18b-7e6583a8803b/resourceGroups/EXT-EDAV-CFA-PRD/providers/Microsoft.Web/sites/cfapredictafmprdfunc03/functions/cfatimer', 'name': 'cfapredictafmprdfunc03/cfatimer', 'type': 'Microsoft.Web/sites/functions', 'script_href': 'https://cfapredictafmprdfunc03.cfa-prd-app.appserviceenvironment.net/admin/vfs/home/site/wwwroot/function_app.py', 'test_data_href': 'https://cfapredictafmprdfunc03.cfa-prd-app.appserviceenvironment.net/admin/vfs/home/data/Functions/sampledata/cfatimer.dat', 'href': 'https://cfapredictafmprdfunc03.cfa-prd-app.appserviceenvironment.net/admin/functions/cfatimer', 'config': {'name': 'cfatimer', 'entryPoint': 'cfatimer', 'scriptFile': 'function_app.py', 'language': 'python', 'functionDirectory': '/home/site/wwwroot', 'bindings': [{'direction': 'IN', 'type': 'timerTrigger', 'name': 'cfatimer', 'schedule': '%CFANotificationV2CRON%', 'runOnStartup': False, 'useMonitor': True}]}, 'test_data': '', 'language': 'python', 'is_disabled': False}, {'id': '/subscriptions/ef340bd6-2809-4635-b18b-7e6583a8803b/resourceGroups/EXT-EDAV-CFA-PRD/providers/Microsoft.Web/sites/cfapredictafmprdfunc03/functions/HealthCheck', 'name': 'cfapredictafmprdfunc03/HealthCheck', 'type': 'Microsoft.Web/sites/functions', 'script_href': 'https://cfapredictafmprdfunc03.cfa-prd-app.appserviceenvironment.net/admin/vfs/home/site/wwwroot/function_app.py', 'test_data_href': 'https://cfapredictafmprdfunc03.cfa-prd-app.appserviceenvironment.net/admin/vfs/home/data/Functions/sampledata/HealthCheck.dat', 'href': 'https://cfapredictafmprdfunc03.cfa-prd-app.appserviceenvironment.net/admin/functions/HealthCheck', 'config': {'name': 'HealthCheck', 'entryPoint': 'HealthCheck', 'scriptFile': 'function_app.py', 'language': 'python', 'functionDirectory': '/home/site/wwwroot', 'bindings': [{'direction': 'IN', 'type': 'httpTrigger', 'name': 'req', 'authLevel': 'ANONYMOUS', 'route': 'HealthCheck'}, {'direction': 'OUT', 'type': 'http', 'name': '$return'}]}, 'test_data': '', 'invoke_url_template': 'https://cfapredictafmprdfunc03.cfa-prd-app.appserviceenvironment.net/api/healthcheck', 'language': 'python', 'is_disabled': False}]
447+
```
448+
366449
### Conclusion
367450
By following this guide, you should be able to set up and use the CFA Azure Function test effectively. Ensure that all environment variables are correctly set and that your Service Principal has the necessary permissions.
368451

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "cfa.cloudops"
3-
version = "0.3.19"
3+
version = "0.3.21"
44
description = "Cloud storage, batch, functions, MLOps assistance"
55
authors = [
66
{name = "Ryan Raasch", email = "xng3@cdc.gov"}

0 commit comments

Comments
 (0)