Files
onefuzz/src/api-service/__app__/onefuzzlib/extension.py
George Pollard 4fa6e74241 Enable .NET functions in check-pr for Agent-specific functions (#2119)
Enable the .NET functions for the agent by sending the agent the URI for the `-net` service.

Also fix some things causing failures when using the .NET functions (`CouldShrinkScaleset` was not implemented).

Improve error handling around table serialization/deserialization, fix an issue with int64/long mismatch between Python & C# code.

----

For `check-pr` testing:

1. There's a new parameter `enable_dotnet` which maps directly to the `--enable_dotnet` switch on `deploy.py`.
2. If you put `agent` there, all the `agent_*` functions will be enabled for .NET and disabled for Python.
3. If `agent_can_schedule` is disabled on the Python side, it will automatically tell the agent to use the .NET functions.

So to test the .NET agent functions, do a `check-pr` run with `enable_dotnet` set to `agent` and it should all work.
2022-07-20 20:40:30 +00:00

516 lines
16 KiB
Python

#!/usr/bin/env python
#
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
import os
from typing import List, Optional
from uuid import UUID, uuid4
from onefuzztypes.enums import OS, AgentMode
from onefuzztypes.models import (
AgentConfig,
AzureMonitorExtensionConfig,
KeyvaultExtensionConfig,
Pool,
ReproConfig,
Scaleset,
)
from onefuzztypes.primitives import Container, Extension, Region
from .azure.containers import (
get_container_sas_url,
get_file_sas_url,
get_file_url,
save_blob,
)
from .azure.creds import get_agent_instance_url, get_instance_id
from .azure.log_analytics import get_monitor_settings
from .azure.queue import get_queue_sas
from .azure.storage import StorageType
from .config import InstanceConfig
from .reports import get_report
def generic_extensions(region: Region, vm_os: OS) -> List[Extension]:
instance_config = InstanceConfig.fetch()
extensions = [monitor_extension(region, vm_os)]
dependency = dependency_extension(region, vm_os)
if dependency:
extensions.append(dependency)
if instance_config.extensions:
if instance_config.extensions.keyvault:
keyvault = keyvault_extension(
region, instance_config.extensions.keyvault, vm_os
)
extensions.append(keyvault)
if instance_config.extensions.geneva and vm_os == OS.windows:
geneva = geneva_extension(region)
extensions.append(geneva)
if instance_config.extensions.azure_monitor and vm_os == OS.linux:
azmon = azmon_extension(region, instance_config.extensions.azure_monitor)
extensions.append(azmon)
if instance_config.extensions.azure_security and vm_os == OS.linux:
azsec = azsec_extension(region)
extensions.append(azsec)
return extensions
def monitor_extension(region: Region, vm_os: OS) -> Extension:
settings = get_monitor_settings()
if vm_os == OS.windows:
return {
"name": "OMSExtension",
"publisher": "Microsoft.EnterpriseCloud.Monitoring",
"type": "MicrosoftMonitoringAgent",
"typeHandlerVersion": "1.0",
"location": region,
"autoUpgradeMinorVersion": True,
"settings": {"workspaceId": settings["id"]},
"protectedSettings": {"workspaceKey": settings["key"]},
}
elif vm_os == OS.linux:
return {
"name": "OMSExtension",
"publisher": "Microsoft.EnterpriseCloud.Monitoring",
"type": "OmsAgentForLinux",
"typeHandlerVersion": "1.12",
"location": region,
"autoUpgradeMinorVersion": True,
"settings": {"workspaceId": settings["id"]},
"protectedSettings": {"workspaceKey": settings["key"]},
}
raise NotImplementedError("unsupported os: %s" % vm_os)
def dependency_extension(region: Region, vm_os: OS) -> Optional[Extension]:
if vm_os == OS.windows:
extension = {
"name": "DependencyAgentWindows",
"publisher": "Microsoft.Azure.Monitoring.DependencyAgent",
"type": "DependencyAgentWindows",
"typeHandlerVersion": "9.5",
"location": region,
"autoUpgradeMinorVersion": True,
}
return extension
else:
# TODO: dependency agent for linux is not reliable
# extension = {
# "name": "DependencyAgentLinux",
# "publisher": "Microsoft.Azure.Monitoring.DependencyAgent",
# "type": "DependencyAgentLinux",
# "typeHandlerVersion": "9.5",
# "location": vm.region,
# "autoUpgradeMinorVersion": True,
# }
return None
def geneva_extension(region: Region) -> Extension:
return {
"name": "Microsoft.Azure.Geneva.GenevaMonitoring",
"publisher": "Microsoft.Azure.Geneva",
"type": "GenevaMonitoring",
"typeHandlerVersion": "2.0",
"location": region,
"autoUpgradeMinorVersion": True,
"enableAutomaticUpgrade": True,
"settings": {},
"protectedSettings": {},
}
def azmon_extension(
region: Region, azure_monitor: AzureMonitorExtensionConfig
) -> Extension:
auth_id = azure_monitor.monitoringGCSAuthId
config_version = azure_monitor.config_version
moniker = azure_monitor.moniker
namespace = azure_monitor.namespace
environment = azure_monitor.monitoringGSEnvironment
account = azure_monitor.monitoringGCSAccount
auth_id_type = azure_monitor.monitoringGCSAuthIdType
return {
"name": "AzureMonitorLinuxAgent",
"publisher": "Microsoft.Azure.Monitor",
"location": region,
"type": "AzureMonitorLinuxAgent",
"typeHandlerVersion": "1.0",
"autoUpgradeMinorVersion": True,
"settings": {"GCS_AUTO_CONFIG": True},
"protectedsettings": {
"configVersion": config_version,
"moniker": moniker,
"namespace": namespace,
"monitoringGCSEnvironment": environment,
"monitoringGCSAccount": account,
"monitoringGCSRegion": region,
"monitoringGCSAuthId": auth_id,
"monitoringGCSAuthIdType": auth_id_type,
},
}
def azsec_extension(region: Region) -> Extension:
return {
"name": "AzureSecurityLinuxAgent",
"publisher": "Microsoft.Azure.Security.Monitoring",
"location": region,
"type": "AzureSecurityLinuxAgent",
"typeHandlerVersion": "2.0",
"autoUpgradeMinorVersion": True,
"settings": {"enableGenevaUpload": True, "enableAutoConfig": True},
}
def keyvault_extension(
region: Region, keyvault: KeyvaultExtensionConfig, vm_os: OS
) -> Extension:
keyvault_name = keyvault.keyvault_name
cert_name = keyvault.cert_name
uri = keyvault_name + cert_name
if vm_os == OS.windows:
return {
"name": "KVVMExtensionForWindows",
"location": region,
"publisher": "Microsoft.Azure.KeyVault",
"type": "KeyVaultForWindows",
"typeHandlerVersion": "1.0",
"autoUpgradeMinorVersion": True,
"settings": {
"secretsManagementSettings": {
"pollingIntervalInS": "3600",
"certificateStoreName": "MY",
"linkOnRenewal": False,
"certificateStoreLocation": "LocalMachine",
"requireInitialSync": True,
"observedCertificates": [uri],
}
},
}
elif vm_os == OS.linux:
cert_path = keyvault.cert_path
extension_store = keyvault.extension_store
cert_location = cert_path + extension_store
return {
"name": "KVVMExtensionForLinux",
"location": region,
"publisher": "Microsoft.Azure.KeyVault",
"type": "KeyVaultForLinux",
"typeHandlerVersion": "2.0",
"autoUpgradeMinorVersion": True,
"settings": {
"secretsManagementSettings": {
"pollingIntervalInS": "3600",
"certificateStoreLocation": cert_location,
"observedCertificates": [uri],
},
},
}
raise NotImplementedError("unsupported os: %s" % vm_os)
def build_scaleset_script(pool: Pool, scaleset: Scaleset) -> str:
commands = []
extension = "ps1" if pool.os == OS.windows else "sh"
filename = f"{scaleset.scaleset_id}/scaleset-setup.{extension}"
sep = "\r\n" if pool.os == OS.windows else "\n"
if pool.os == OS.windows and scaleset.auth is not None:
ssh_key = scaleset.auth.public_key.strip()
ssh_path = "$env:ProgramData/ssh/administrators_authorized_keys"
commands += [f'Set-Content -Path {ssh_path} -Value "{ssh_key}"']
save_blob(
Container("vm-scripts"), filename, sep.join(commands) + sep, StorageType.config
)
return get_file_url(Container("vm-scripts"), filename, StorageType.config)
def build_pool_config(pool: Pool) -> str:
config = AgentConfig(
pool_name=pool.name,
onefuzz_url=get_agent_instance_url(),
heartbeat_queue=get_queue_sas(
"node-heartbeat",
StorageType.config,
add=True,
),
instance_telemetry_key=os.environ.get("APPINSIGHTS_INSTRUMENTATIONKEY"),
microsoft_telemetry_key=os.environ.get("ONEFUZZ_TELEMETRY"),
instance_id=get_instance_id(),
)
multi_tenant_domain = os.environ.get("MULTI_TENANT_DOMAIN")
if multi_tenant_domain:
config.multi_tenant_domain = multi_tenant_domain
filename = f"{pool.name}/config.json"
save_blob(
Container("vm-scripts"),
filename,
config.json(),
StorageType.config,
)
return config_url(Container("vm-scripts"), filename, False)
def update_managed_scripts() -> None:
commands = [
"azcopy sync '%s' instance-specific-setup"
% (
get_container_sas_url(
Container("instance-specific-setup"),
StorageType.config,
read=True,
list_=True,
)
),
"azcopy sync '%s' tools"
% (
get_container_sas_url(
Container("tools"), StorageType.config, read=True, list_=True
)
),
]
save_blob(
Container("vm-scripts"),
"managed.ps1",
"\r\n".join(commands) + "\r\n",
StorageType.config,
)
save_blob(
Container("vm-scripts"),
"managed.sh",
"\n".join(commands) + "\n",
StorageType.config,
)
def config_url(container: Container, filename: str, with_sas: bool) -> str:
if with_sas:
return get_file_sas_url(container, filename, StorageType.config, read=True)
else:
return get_file_url(container, filename, StorageType.config)
def agent_config(
region: Region,
vm_os: OS,
mode: AgentMode,
*,
urls: Optional[List[str]] = None,
with_sas: bool = False,
) -> Extension:
update_managed_scripts()
if urls is None:
urls = []
if vm_os == OS.windows:
urls += [
config_url(Container("vm-scripts"), "managed.ps1", with_sas),
config_url(Container("tools"), "win64/azcopy.exe", with_sas),
config_url(
Container("tools"),
"win64/setup.ps1",
with_sas,
),
config_url(
Container("tools"),
"win64/onefuzz.ps1",
with_sas,
),
]
to_execute_cmd = (
"powershell -ExecutionPolicy Unrestricted -File win64/setup.ps1 "
"-mode %s" % (mode.name)
)
extension = {
"name": "CustomScriptExtension",
"type": "CustomScriptExtension",
"publisher": "Microsoft.Compute",
"location": region,
"force_update_tag": uuid4(),
"type_handler_version": "1.9",
"auto_upgrade_minor_version": True,
"settings": {
"commandToExecute": to_execute_cmd,
"fileUris": urls,
},
"protectedSettings": {
"managedIdentity": {},
},
}
return extension
elif vm_os == OS.linux:
urls += [
config_url(
Container("vm-scripts"),
"managed.sh",
with_sas,
),
config_url(
Container("tools"),
"linux/azcopy",
with_sas,
),
config_url(
Container("tools"),
"linux/setup.sh",
with_sas,
),
]
to_execute_cmd = "sh setup.sh %s" % (mode.name)
extension = {
"name": "CustomScript",
"publisher": "Microsoft.Azure.Extensions",
"type": "CustomScript",
"typeHandlerVersion": "2.1",
"location": region,
"force_update_tag": uuid4(),
"autoUpgradeMinorVersion": True,
"settings": {
"commandToExecute": to_execute_cmd,
"fileUris": urls,
},
"protectedSettings": {
"managedIdentity": {},
},
}
return extension
raise NotImplementedError("unsupported OS: %s" % vm_os)
def fuzz_extensions(pool: Pool, scaleset: Scaleset) -> List[Extension]:
urls = [build_pool_config(pool), build_scaleset_script(pool, scaleset)]
fuzz_extension = agent_config(scaleset.region, pool.os, AgentMode.fuzz, urls=urls)
extensions = generic_extensions(scaleset.region, pool.os)
extensions += [fuzz_extension]
return extensions
def repro_extensions(
region: Region,
repro_os: OS,
repro_id: UUID,
repro_config: ReproConfig,
setup_container: Optional[Container],
) -> List[Extension]:
# TODO - what about contents of repro.ps1 / repro.sh?
report = get_report(repro_config.container, repro_config.path)
if report is None:
raise Exception("invalid report: %s" % repro_config)
if report.input_blob is None:
raise Exception("unable to perform reproduction without an input blob")
commands = []
if setup_container:
commands += [
"azcopy sync '%s' ./setup"
% (
get_container_sas_url(
setup_container, StorageType.corpus, read=True, list_=True
)
),
]
urls = [
get_file_sas_url(
repro_config.container, repro_config.path, StorageType.corpus, read=True
),
get_file_sas_url(
report.input_blob.container,
report.input_blob.name,
StorageType.corpus,
read=True,
),
]
repro_files = []
if repro_os == OS.windows:
repro_files = ["%s/repro.ps1" % repro_id]
task_script = "\r\n".join(commands)
script_name = "task-setup.ps1"
else:
repro_files = ["%s/repro.sh" % repro_id, "%s/repro-stdout.sh" % repro_id]
commands += ["chmod -R +x setup"]
task_script = "\n".join(commands)
script_name = "task-setup.sh"
save_blob(
Container("task-configs"),
"%s/%s" % (repro_id, script_name),
task_script,
StorageType.config,
)
for repro_file in repro_files:
urls += [
get_file_sas_url(
Container("repro-scripts"),
repro_file,
StorageType.config,
read=True,
),
get_file_sas_url(
Container("task-configs"),
"%s/%s" % (repro_id, script_name),
StorageType.config,
read=True,
),
]
base_extension = agent_config(
region, repro_os, AgentMode.repro, urls=urls, with_sas=True
)
extensions = generic_extensions(region, repro_os)
extensions += [base_extension]
return extensions
def proxy_manager_extensions(region: Region, proxy_id: UUID) -> List[Extension]:
urls = [
get_file_sas_url(
Container("proxy-configs"),
"%s/%s/config.json" % (region, proxy_id),
StorageType.config,
read=True,
),
get_file_sas_url(
Container("tools"),
"linux/onefuzz-proxy-manager",
StorageType.config,
read=True,
),
]
base_extension = agent_config(
region, OS.linux, AgentMode.proxy, urls=urls, with_sas=True
)
extensions = generic_extensions(region, OS.linux)
extensions += [base_extension]
return extensions