mirror of
https://github.com/microsoft/onefuzz.git
synced 2025-06-12 10:08:09 +00:00
Bug fixes and documentation (#2694)
This commit is contained in:
80
docs/unmnaged-nodes.md
Normal file
80
docs/unmnaged-nodes.md
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# Unmanaged Nodes
|
||||||
|
The default mode of OneFuzz is to run the agents inside scalesets managed by the the Onefuzz instance. But it is possible to run outside of the Instance infrastructure.
|
||||||
|
This is the unmanaged scenario. In this mode, the user can use their own resource to participate in the fuzzing.
|
||||||
|
|
||||||
|
## Set-up
|
||||||
|
These are the steps to run an unmanaged node
|
||||||
|
|
||||||
|
|
||||||
|
### Create an Application Registration in Azure Active Directory
|
||||||
|
We will create the authentication method for the unmanaged node.
|
||||||
|
From the [azure cli](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) create a new **application registration**:
|
||||||
|
```cmd
|
||||||
|
az ad app create --display-name <registration_name>
|
||||||
|
```
|
||||||
|
Then use the application `app_id` in the result to create the associated **service principal**:
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
az ad sp create --id <app_id>
|
||||||
|
```
|
||||||
|
Take note of the `id` returned by this request. We will call it the `principal_id`.
|
||||||
|
|
||||||
|
Next, create a `client_secret`:
|
||||||
|
|
||||||
|
```
|
||||||
|
az ad app credential reset --id <pp_id> --append
|
||||||
|
```
|
||||||
|
Take note of the `password` returned.
|
||||||
|
|
||||||
|
### Authorize the application in OneFuzz
|
||||||
|
From the OneFuzz `deployment` folder run the following script using the `app_id` from above:
|
||||||
|
``` cmd
|
||||||
|
python .\deploylib\registration.py register_app <onefuzz_instance_id> <subscription_id> --app_id <app_id> --role UnmanagedNode
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create an unmanaged pool
|
||||||
|
Using the OneFuzz CLI:
|
||||||
|
``` cmd
|
||||||
|
onefuzz pools create <pool_name> <os> --unmanaged --object_id <principal_id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Download the agent binaries and the agent configuration
|
||||||
|
Download a zip file containing the agent binaries:
|
||||||
|
```
|
||||||
|
onefuzz tools get <destination_folder>
|
||||||
|
```
|
||||||
|
Extract the zip file in a folder of your choice.
|
||||||
|
|
||||||
|
Download the configuration file for the agent:
|
||||||
|
|
||||||
|
```
|
||||||
|
onefuzz pools get_config <pool_name>
|
||||||
|
```
|
||||||
|
|
||||||
|
Under the `client_credential` section of the agent config file, update `client_id` and `client_secret`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"client_id": "<app_id>",
|
||||||
|
"client_secret": "<password>",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Save the config to the file.
|
||||||
|
|
||||||
|
### Start the agent.
|
||||||
|
Navigate to the folder corresponding to your OS.
|
||||||
|
Set the necessary environment variable by running the script `set-env.ps1` (for Windows) or `set-env.sh` (for Linux).
|
||||||
|
Run the agent with the following command. If you need more nodes use a different `machine_guid` for each one:
|
||||||
|
```cmd
|
||||||
|
onefuzz-agent run --machine_id <machine_guid> -c <path_to_config_file> --reset_lock
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify that the agent is registered to OneFuzz
|
||||||
|
|
||||||
|
Using the OneFuzz CLI run the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
onefuzz nodes get <machine_guid>
|
||||||
|
```
|
||||||
|
|
||||||
|
This should return one entry. Verify that the `pool_name` matched the pool name created earlier.
|
||||||
|
From here you will be able to schedule jobs on that pool and they will be running.
|
@ -152,7 +152,9 @@ public class AgentRegistration {
|
|||||||
MachineId: machineId,
|
MachineId: machineId,
|
||||||
ScalesetId: scalesetId,
|
ScalesetId: scalesetId,
|
||||||
InstanceId: instanceId,
|
InstanceId: instanceId,
|
||||||
Version: version
|
Version: version,
|
||||||
|
Os: os ?? pool.Os,
|
||||||
|
Managed: pool.Managed
|
||||||
);
|
);
|
||||||
|
|
||||||
var r = await _context.NodeOperations.Replace(node);
|
var r = await _context.NodeOperations.Replace(node);
|
||||||
|
@ -110,7 +110,8 @@ public record Node
|
|||||||
|
|
||||||
bool ReimageRequested = false,
|
bool ReimageRequested = false,
|
||||||
bool DeleteRequested = false,
|
bool DeleteRequested = false,
|
||||||
bool DebugKeepNode = false
|
bool DebugKeepNode = false,
|
||||||
|
bool Managed = true
|
||||||
) : StatefulEntityBase<NodeState>(State) {
|
) : StatefulEntityBase<NodeState>(State) {
|
||||||
|
|
||||||
public List<NodeTasks>? Tasks { get; set; }
|
public List<NodeTasks>? Tasks { get; set; }
|
||||||
|
@ -32,6 +32,7 @@ public interface IServiceConfig {
|
|||||||
public ResourceIdentifier? OneFuzzFuncStorage { get; }
|
public ResourceIdentifier? OneFuzzFuncStorage { get; }
|
||||||
public string? OneFuzzInstance { get; }
|
public string? OneFuzzInstance { get; }
|
||||||
public string? OneFuzzInstanceName { get; }
|
public string? OneFuzzInstanceName { get; }
|
||||||
|
public string? OneFuzzEndpoint { get; }
|
||||||
public string? OneFuzzKeyvault { get; }
|
public string? OneFuzzKeyvault { get; }
|
||||||
|
|
||||||
public string? OneFuzzMonitor { get; }
|
public string? OneFuzzMonitor { get; }
|
||||||
@ -117,6 +118,7 @@ public class ServiceConfiguration : IServiceConfig {
|
|||||||
|
|
||||||
public string? OneFuzzInstance { get => GetEnv("ONEFUZZ_INSTANCE"); }
|
public string? OneFuzzInstance { get => GetEnv("ONEFUZZ_INSTANCE"); }
|
||||||
public string? OneFuzzInstanceName { get => GetEnv("ONEFUZZ_INSTANCE_NAME"); }
|
public string? OneFuzzInstanceName { get => GetEnv("ONEFUZZ_INSTANCE_NAME"); }
|
||||||
|
public string? OneFuzzEndpoint { get => GetEnv("ONEFUZZ_ENDPOINT"); }
|
||||||
public string? OneFuzzKeyvault { get => GetEnv("ONEFUZZ_KEYVAULT"); }
|
public string? OneFuzzKeyvault { get => GetEnv("ONEFUZZ_KEYVAULT"); }
|
||||||
public string? OneFuzzMonitor { get => GetEnv("ONEFUZZ_MONITOR"); }
|
public string? OneFuzzMonitor { get => GetEnv("ONEFUZZ_MONITOR"); }
|
||||||
public string? OneFuzzOwner { get => GetEnv("ONEFUZZ_OWNER"); }
|
public string? OneFuzzOwner { get => GetEnv("ONEFUZZ_OWNER"); }
|
||||||
|
@ -105,8 +105,10 @@ public sealed class Creds : ICreds {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public Uri GetInstanceUrl()
|
public Uri GetInstanceUrl() {
|
||||||
=> new($"https://{GetInstanceName()}.azurewebsites.net");
|
var onefuzzEndpoint = _config.OneFuzzEndpoint;
|
||||||
|
return onefuzzEndpoint != null ? new Uri(onefuzzEndpoint) : new($"https://{GetInstanceName()}.azurewebsites.net");
|
||||||
|
}
|
||||||
|
|
||||||
public record ScaleSetIdentity(string principalId);
|
public record ScaleSetIdentity(string principalId);
|
||||||
|
|
||||||
|
@ -337,6 +337,10 @@ public class NodeOperations : StatefulOrm<Node, NodeState, NodeOperations>, INod
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Async.Task<Node> ToReimage(Node node, bool done = false) {
|
public async Async.Task<Node> ToReimage(Node node, bool done = false) {
|
||||||
|
if (!node.Managed) {
|
||||||
|
_logTracer.Info($"skip reimage for unmanaged node: {node.MachineId:Tag:MachineId}");
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
var nodeState = node.State;
|
var nodeState = node.State;
|
||||||
if (done) {
|
if (done) {
|
||||||
|
@ -31,6 +31,8 @@ public sealed class TestServiceConfiguration : IServiceConfig {
|
|||||||
|
|
||||||
// -- Remainder not implemented --
|
// -- Remainder not implemented --
|
||||||
|
|
||||||
|
public string? OneFuzzEndpoint => throw new System.NotImplementedException();
|
||||||
|
|
||||||
public LogDestination[] LogDestinations { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
|
public LogDestination[] LogDestinations { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
|
||||||
|
|
||||||
public SeverityLevel LogSeverityLevel => throw new System.NotImplementedException();
|
public SeverityLevel LogSeverityLevel => throw new System.NotImplementedException();
|
||||||
|
@ -80,7 +80,7 @@ struct RawStaticConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl StaticConfig {
|
impl StaticConfig {
|
||||||
pub async fn new(data: &[u8]) -> Result<Self> {
|
pub async fn new(data: &[u8], machine_identity: Option<MachineIdentity>) -> Result<Self> {
|
||||||
let config: RawStaticConfig = serde_json::from_slice(data)?;
|
let config: RawStaticConfig = serde_json::from_slice(data)?;
|
||||||
|
|
||||||
let credentials = match config.client_credentials {
|
let credentials = match config.client_credentials {
|
||||||
@ -104,7 +104,7 @@ impl StaticConfig {
|
|||||||
managed.into()
|
managed.into()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let machine_identity = match config.machine_identity {
|
let machine_identity = match machine_identity.or(config.machine_identity) {
|
||||||
Some(machine_identity) => machine_identity,
|
Some(machine_identity) => machine_identity,
|
||||||
None => MachineIdentity::from_metadata().await?,
|
None => MachineIdentity::from_metadata().await?,
|
||||||
};
|
};
|
||||||
@ -125,11 +125,14 @@ impl StaticConfig {
|
|||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn from_file(config_path: impl AsRef<Path>) -> Result<Self> {
|
pub async fn from_file(
|
||||||
|
config_path: impl AsRef<Path>,
|
||||||
|
machine_identity: Option<MachineIdentity>,
|
||||||
|
) -> Result<Self> {
|
||||||
let config_path = config_path.as_ref();
|
let config_path = config_path.as_ref();
|
||||||
let data = std::fs::read(config_path)
|
let data = std::fs::read(config_path)
|
||||||
.with_context(|| format!("unable to read config file: {}", config_path.display()))?;
|
.with_context(|| format!("unable to read config file: {}", config_path.display()))?;
|
||||||
Self::new(&data).await
|
Self::new(&data, machine_identity).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_env() -> Result<Self> {
|
pub fn from_env() -> Result<Self> {
|
||||||
|
@ -170,8 +170,6 @@ fn run(opt: RunOpt) -> Result<()> {
|
|||||||
if opt.redirect_output.is_some() {
|
if opt.redirect_output.is_some() {
|
||||||
return redirect(opt);
|
return redirect(opt);
|
||||||
}
|
}
|
||||||
let opt_machine_id = opt.machine_id;
|
|
||||||
let opt_machine_name = opt.machine_name.clone();
|
|
||||||
let rt = tokio::runtime::Runtime::new()?;
|
let rt = tokio::runtime::Runtime::new()?;
|
||||||
let reset_lock = opt.reset_node_lock;
|
let reset_lock = opt.reset_node_lock;
|
||||||
let config = rt.block_on(load_config(opt));
|
let config = rt.block_on(load_config(opt));
|
||||||
@ -184,15 +182,6 @@ fn run(opt: RunOpt) -> Result<()> {
|
|||||||
|
|
||||||
let config = config?;
|
let config = config?;
|
||||||
|
|
||||||
let config = StaticConfig {
|
|
||||||
machine_identity: MachineIdentity {
|
|
||||||
machine_id: opt_machine_id.unwrap_or(config.machine_identity.machine_id),
|
|
||||||
machine_name: opt_machine_name.unwrap_or(config.machine_identity.machine_name),
|
|
||||||
..config.machine_identity
|
|
||||||
},
|
|
||||||
..config
|
|
||||||
};
|
|
||||||
|
|
||||||
if reset_lock {
|
if reset_lock {
|
||||||
done::remove_done_lock(config.machine_identity.machine_id)?;
|
done::remove_done_lock(config.machine_identity.machine_id)?;
|
||||||
} else if done::is_agent_done(config.machine_identity.machine_id)? {
|
} else if done::is_agent_done(config.machine_identity.machine_id)? {
|
||||||
@ -218,10 +207,18 @@ fn run(opt: RunOpt) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn load_config(opt: RunOpt) -> Result<StaticConfig> {
|
async fn load_config(opt: RunOpt) -> Result<StaticConfig> {
|
||||||
info!("loading supervisor agent config");
|
info!("loading supervisor agent config: {:?}", opt);
|
||||||
|
let opt_machine_id = opt.machine_id;
|
||||||
|
let opt_machine_name = opt.machine_name.clone();
|
||||||
|
|
||||||
|
let machine_identity = opt_machine_id.map(|machine_id| MachineIdentity {
|
||||||
|
machine_id,
|
||||||
|
machine_name: opt_machine_name.unwrap_or(format!("{}", machine_id)),
|
||||||
|
scaleset_name: None,
|
||||||
|
});
|
||||||
|
|
||||||
let config = match &opt.config_path {
|
let config = match &opt.config_path {
|
||||||
Some(config_path) => StaticConfig::from_file(config_path).await?,
|
Some(config_path) => StaticConfig::from_file(config_path, machine_identity).await?,
|
||||||
None => StaticConfig::from_env()?,
|
None => StaticConfig::from_env()?,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -113,9 +113,11 @@ impl ClientCredentials {
|
|||||||
pub async fn access_token(&self) -> Result<AccessToken> {
|
pub async fn access_token(&self) -> Result<AccessToken> {
|
||||||
let (authority, scope) = {
|
let (authority, scope) = {
|
||||||
let url = Url::parse(&self.resource.clone())?;
|
let url = Url::parse(&self.resource.clone())?;
|
||||||
let host = url.host_str().ok_or_else(|| {
|
let port = url.port().map(|p| format!(":{}", p)).unwrap_or_default();
|
||||||
|
let host_name = url.host_str().ok_or_else(|| {
|
||||||
anyhow::format_err!("resource URL does not have a host string: {}", url)
|
anyhow::format_err!("resource URL does not have a host string: {}", url)
|
||||||
})?;
|
})?;
|
||||||
|
let host = format!("{}{}", host_name, port);
|
||||||
if let Some(domain) = &self.multi_tenant_domain {
|
if let Some(domain) = &self.multi_tenant_domain {
|
||||||
let instance: Vec<&str> = host.split('.').collect();
|
let instance: Vec<&str> = host.split('.').collect();
|
||||||
(
|
(
|
||||||
|
@ -14,6 +14,7 @@ import uuid
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from shutil import which
|
from shutil import which
|
||||||
from typing import Callable, Dict, List, Optional, Tuple, Type, TypeVar
|
from typing import Callable, Dict, List, Optional, Tuple, Type, TypeVar
|
||||||
|
from urllib.parse import urlparse
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
import semver
|
import semver
|
||||||
@ -1268,6 +1269,16 @@ class Pool(Endpoint):
|
|||||||
if pool.config is None:
|
if pool.config is None:
|
||||||
raise Exception("Missing AgentConfig in response")
|
raise Exception("Missing AgentConfig in response")
|
||||||
|
|
||||||
|
config = pool.config
|
||||||
|
if not pool.managed:
|
||||||
|
config.client_credentials = models.ClientCredentials( # nosec
|
||||||
|
client_id=uuid.UUID(int=0),
|
||||||
|
client_secret="<client_secret>",
|
||||||
|
resource=self.onefuzz._backend.config.endpoint,
|
||||||
|
tenant=urlparse(self.onefuzz._backend.config.authority).path.strip("/"),
|
||||||
|
multi_tenant_domain=self.onefuzz._backend.config.tenant_domain,
|
||||||
|
)
|
||||||
|
|
||||||
return pool.config
|
return pool.config
|
||||||
|
|
||||||
def shutdown(self, name: str, *, now: bool = False) -> responses.BoolResult:
|
def shutdown(self, name: str, *, now: bool = False) -> responses.BoolResult:
|
||||||
|
@ -861,10 +861,13 @@ def main() -> None:
|
|||||||
"--registration_name", help="the name of the cli registration"
|
"--registration_name", help="the name of the cli registration"
|
||||||
)
|
)
|
||||||
register_app_parser = subparsers.add_parser("register_app", parents=[parent_parser])
|
register_app_parser = subparsers.add_parser("register_app", parents=[parent_parser])
|
||||||
register_app_parser.add_argument("--app_id", help="the application id to register")
|
register_app_parser.add_argument(
|
||||||
|
"--app_id", help="the application id to register", required=True
|
||||||
|
)
|
||||||
register_app_parser.add_argument(
|
register_app_parser.add_argument(
|
||||||
"--role",
|
"--role",
|
||||||
help=f"the role of the application to register. Valid values: {', '.join([member.value for member in OnefuzzAppRole])}",
|
help=f"the role of the application to register. Valid values: {', '.join([member.value for member in OnefuzzAppRole])}",
|
||||||
|
required=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
@ -328,6 +328,9 @@ CONTAINER_DEF = Optional[Union[SyncedDir, List[SyncedDir]]]
|
|||||||
class ClientCredentials(BaseModel):
|
class ClientCredentials(BaseModel):
|
||||||
client_id: UUID
|
client_id: UUID
|
||||||
client_secret: str
|
client_secret: str
|
||||||
|
resource: str
|
||||||
|
tenant: str
|
||||||
|
multi_tenant_domain: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
class AgentConfig(BaseModel):
|
class AgentConfig(BaseModel):
|
||||||
|
6
src/runtime-tools/linux/set-env.sh
Normal file
6
src/runtime-tools/linux/set-env.sh
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# Copyright (c) Microsoft Corporation.
|
||||||
|
# Licensed under the MIT License.
|
||||||
|
export DOTNET_ROOT=/onefuzz/tools/dotnet
|
||||||
|
export DOTNET_CLI_HOME="$DOTNET_ROOT"
|
||||||
|
export LLVM_SYMBOLIZER_PATH=/onefuzz/bin/llvm-symbolizer
|
||||||
|
export RUST_LOG = "info"
|
6
src/runtime-tools/win64/set-env.ps1
Normal file
6
src/runtime-tools/win64/set-env.ps1
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# Copyright (c) Microsoft Corporation.
|
||||||
|
# Licensed under the MIT License.
|
||||||
|
|
||||||
|
$env:Path += ";C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\;C:\onefuzz\win64;C:\onefuzz\tools\win64;C:\onefuzz\tools\win64\radamsa;$env:ProgramFiles\LLVM\bin"
|
||||||
|
$env:LLVM_SYMBOLIZER_PATH = "C:\Program Files\LLVM\bin\llvm-symbolizer.exe"
|
||||||
|
$env:RUST_LOG = "info"
|
Reference in New Issue
Block a user