Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
20 changes: 20 additions & 0 deletions rest-api/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,26 @@ Output formatting and pagination flags live on individual commands, not on the r

Run `nicocli <command> --help` for the full per-command flag list, including spec-derived query parameters and body fields.

### Bootstrap site prerequisites from a manifest

`nicocli site bootstrap` creates or verifies the REST resources needed to use a Site. It initializes the calling organization, requires the Site to already exist, then processes its auto-created Site IP Blocks, Instance Types, Allocations, VPCs, VPC Prefixes, and optional Instances in dependency order. It never creates a Site.

Start from the [example manifest](examples/site-prerequisites.yaml), replace the organization and resource values, and run:

```bash
nicocli site bootstrap \
--file cli/examples/site-prerequisites.yaml \
--output-file site-prerequisites.resolved.yaml
```

The manifest uses `${...}` references so later requests can consume IDs returned by earlier requests. For example, `${site.id}` resolves to the existing Site ID, `${siteIpBlocks.id}` resolves to a Site IP Block selected from the fabric-prefix inventory, and `${allocations.network.allocationConstraints.0.derivedResourceId}` resolves to the Tenant IP Block created by the network Allocation.

Site IP Blocks are not created by this command. NICo automatically creates them from fabric prefixes reported by the Site; the manifest's `siteIpBlocks` entries are read-only selectors for those existing resources. If the Site inventory has not arrived yet, bootstrap stops with a rerun message instead of posting a Provider-owned IP Block.

For managed resources, the command looks up a recorded ID first, then uses exact name and scope. Matching resources are reused, while a matching resource whose returned configuration differs from the request stops the workflow with a drift error. The output file preserves the requests, selectors, and resolved resource IDs so it can be replayed after an interrupted run or against a replacement installation. All requests use operation paths and methods resolved from the same embedded OpenAPI model that builds the regular CLI commands.

Bootstrap first calls the Service Account endpoint for `provider.org`. In Service Account mode that single call initializes and returns both identities, and the Provider and Tenant organization names must match; otherwise bootstrap falls back to the Provider and Tenant current endpoints. Separate Provider and Tenant organizations use the same token, so it must have the required role in both organizations. `provider.org` may be omitted when the Provider organization already comes from `--org`, `NICO_ORG`, or the selected CLI config. Site registration, fabric-prefix inventory, and machine readiness are external asynchronous steps; if a dependency is not ready yet, complete that step and rerun the same manifest.

## Authentication

```bash
Expand Down
94 changes: 94 additions & 0 deletions rest-api/cli/examples/site-prerequisites.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Example input for `nicocli site bootstrap`. Each `request` object is passed
# to the corresponding REST operation and must contain only attributes allowed
# by that operation's OpenAPI request schema.

provider:
org: provider-org
tenant:
org: tenant-org

# The Site must already exist. Bootstrap looks it up but never creates it.
site:
request:
Comment thread
osu marked this conversation as resolved.
name: sjc4
description: San Jose site 4

siteIpBlocks:
# Site IP Blocks are created automatically from fabric prefixes reported
# by the Site. This selector discovers the existing block; it never posts
# a Provider-created IP Block. Rerun bootstrap if inventory is not ready.
match:
siteId: "${site.id}"
routingType: DatacenterOnly
prefix: 10.40.0.0
prefixLength: 16

instanceTypes:
compute:
request:
name: compute-large
description: Compute profile for SJC4
siteId: "${site.id}"
labels:
tier: compute
# Replace this with a capability reported by machines at the site.
machineCapabilities:
- type: CPU
name: x86-64
count: 1

allocations:
network:
request:
name: tenant-network
description: Tenant network allocation for SJC4
tenantId: "${tenant.id}"
siteId: "${site.id}"
allocationConstraints:
- resourceType: IPBlock
resourceTypeId: "${siteIpBlocks.id}"
constraintType: OnDemand
constraintValue: 24
compute:
request:
name: tenant-compute
description: One reserved machine for the tenant
tenantId: "${tenant.id}"
siteId: "${site.id}"
allocationConstraints:
- resourceType: InstanceType
resourceTypeId: "${instanceTypes.compute.id}"
constraintType: Reserved
constraintValue: 1

vpcs:
tenant:
request:
name: tenant-vpc
description: Tenant VPC at SJC4
siteId: "${site.id}"

vpcPrefixes:
tenant:
request:
name: tenant-prefix
vpcId: "${vpcs.tenant.id}"
ipBlockId: "${allocations.network.allocationConstraints.0.derivedResourceId}"
prefixLength: 24

# Instances are optional. Remove this section until machines assigned to the
# instance type are ready and the compute allocation can be satisfied.
instances:
worker:
request:
name: worker-1
description: First tenant worker
tenantId: "${tenant.id}"
instanceTypeId: "${instanceTypes.compute.id}"
vpcId: "${vpcs.tenant.id}"
interfaces:
- vpcPrefixId: "${vpcPrefixes.tenant.id}"
isPhysical: true
1 change: 1 addition & 0 deletions rest-api/cli/pkg/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func NewApp(specData []byte) (*cli.App, error) {
}

commands := BuildCommands(spec)
commands = addSiteBootstrapCommand(commands, spec)
commands = append(commands, LoginCommand())
commands = append(commands, InitCommand())
commands = append(commands, completionCommand())
Expand Down
47 changes: 44 additions & 3 deletions rest-api/cli/pkg/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package cli

import (
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
Expand All @@ -16,6 +17,11 @@ import (
cli "github.com/urfave/cli/v2"
)

var (
errOpenAPIOperationUnavailable = errors.New("OpenAPI operation is unavailable")
errOpenAPIResourceIDMissing = errors.New("OpenAPI operation has no resource ID path parameter")
)

type resolvedOp struct {
tag string
action string
Expand All @@ -25,6 +31,42 @@ type resolvedOp struct {
pathParams []Parameter
}

type operationIndex map[string]resolvedOp

func newOperationIndex(spec *Spec) operationIndex {
index := make(operationIndex)
for _, operation := range collectOperations(spec) {
index[operation.op.OperationID] = operation
}
return index
}

func (index operationIndex) require(operationID string) (resolvedOp, error) {
operation, ok := index[operationID]
if !ok {
return resolvedOp{}, fmt.Errorf("%w: %q", errOpenAPIOperationUnavailable, operationID)
}
return operation, nil
}

func (operation resolvedOp) parameters() []Parameter {
parameters := append([]Parameter{}, operation.pathParams...)
return append(parameters, operation.op.Parameters...)
}

func (operation resolvedOp) resourceIDParameter() (string, error) {
for _, parameter := range operation.parameters() {
if parameter.In == "path" && parameter.Name != "org" {
return parameter.Name, nil
}
}
return "", fmt.Errorf("%w: %q", errOpenAPIResourceIDMissing, operation.op.OperationID)
}

func (operation resolvedOp) execute(client *Client, pathParams, queryParams map[string]string, body []byte) ([]byte, http.Header, error) {
return client.Do(operation.method, operation.path, pathParams, queryParams, body)
}

// bodyField tracks a request body property for type-aware flag reading.
type bodyField struct {
jsonName string
Expand Down Expand Up @@ -298,8 +340,7 @@ func buildActionCommand(spec *Spec, ro resolvedOp, subResource string) *cli.Comm

var argParams []string

allParams := append([]Parameter{}, ro.pathParams...)
allParams = append(allParams, ro.op.Parameters...)
allParams := ro.parameters()

for _, p := range allParams {
if p.Name == "org" {
Expand Down Expand Up @@ -426,7 +467,7 @@ func buildActionCommand(spec *Spec, ro resolvedOp, subResource string) *cli.Comm
return fetchAllPages(client, ro.method, ro.path, pathParams, queryParams, c.String("output"))
}

respBody, respHeaders, err := client.Do(ro.method, ro.path, pathParams, queryParams, body)
respBody, respHeaders, err := ro.execute(client, pathParams, queryParams, body)
if err != nil {
return err
}
Expand Down
Loading
Loading