Skip to content
Open
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
62 changes: 58 additions & 4 deletions server/entitymanager/entitymanager.go
Copy link
Copy Markdown
Contributor

@Chounoki Chounoki May 11, 2026

Choose a reason for hiding this comment

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

We are going to rewrite the entire EntityManager soon to utlize the "ChassisManager" interface.

type ChassisManager interface {

It probably doesn't affect your PR, only as a heads-up.

Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
bpb "github.com/openconfig/bootz/proto/bootz"
epb "github.com/openconfig/bootz/server/entitymanager/proto/entity"
apb "github.com/openconfig/gnsi/authz"
ppb "github.com/openconfig/gnsi/pathz"
)

const defaultRealm = "prod"
Expand Down Expand Up @@ -74,6 +75,14 @@ func (m *InMemoryEntityManager) ResolveChassis(ctx context.Context, lookup *serv
if err != nil {
return nil, err
}
credzConf, err := m.populateCredentialsConfig(chassis)
if err != nil {
return nil, err
}
pathzConf, err := m.populatePathzConfig(chassis)
if err != nil {
return nil, err
}
authzConf, err := m.populateAuthzConfig(chassis)
if err != nil {
return nil, err
Expand All @@ -88,7 +97,10 @@ func (m *InMemoryEntityManager) ResolveChassis(ctx context.Context, lookup *serv
Serial: chassis.GetSerialNumber(),
ControlCards: cards,
BootConfig: bootCfg,
Credentials: credzConf,
Pathz: pathzConf,
Authz: authzConf,
CertzProfiles: chassis.GetConfig().GetGnsiConfig().GetCertzProfiles(),
BootloaderPasswordHash: chassis.GetBootloaderPasswordHash(),
}, nil
}
Expand Down Expand Up @@ -160,6 +172,48 @@ func (m *InMemoryEntityManager) populateAuthzConfig(ch *epb.Chassis) (*apb.Uploa
return gnsiAuthzReq, nil
}

func (m *InMemoryEntityManager) populatePathzConfig(ch *epb.Chassis) (*ppb.UploadRequest, error) {
gnsiConf := ch.GetConfig().GetGnsiConfig()
gnsiPathzReq := gnsiConf.GetPathzUpload()
gnsiPathzReqFile := gnsiConf.GetPathzUploadFile()
if gnsiPathzReq.GetVersion() != "" && gnsiPathzReq.GetPolicy() != nil {
return gnsiPathzReq, nil
}
if gnsiPathzReqFile == "" {
Comment on lines +178 to +182
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The populatePathzConfig function is missing a fallback to the global Pathz configuration if the chassis-specific configuration file is not provided. This is inconsistent with how populateAuthzConfig is implemented and will prevent global Pathz policies from being applied.

gnsiPathzReqFile := gnsiConf.GetPathzUploadFile()
	if gnsiPathzReqFile == "" && m.defaults != nil && m.defaults.GnsiGlobalConfig != nil {
		gnsiPathzReqFile = m.defaults.GnsiGlobalConfig.GetPathzUploadFile()
	}
	if gnsiPathzReq.GetVersion() != "" && gnsiPathzReq.GetPolicy() != nil {
		return gnsiPathzReq, nil
	}
	if gnsiPathzReqFile == "" {

return nil, nil
}
data, err := os.ReadFile(gnsiPathzReqFile)
if err != nil {
return nil, status.Errorf(codes.Internal, "Error opening file %s: %v", gnsiPathzReqFile, err)
}
gnsiPathzReq = &ppb.UploadRequest{}
if err := prototext.Unmarshal(data, gnsiPathzReq); err != nil {
return nil, status.Errorf(codes.Internal, "File %s config is not a valid pathz Upload Request: %v", gnsiPathzReqFile, err)
}
return gnsiPathzReq, nil
}

func (m *InMemoryEntityManager) populateCredentialsConfig(ch *epb.Chassis) (*bpb.Credentials, error) {
gnsiConf := ch.GetConfig().GetGnsiConfig()
gnsiCredzReq := gnsiConf.GetCredentials()
gnsiCredzReqFile := gnsiConf.GetCredentialsFile()
if len(gnsiCredzReq.GetCredentials()) > 0 || len(gnsiCredzReq.GetUsers()) > 0 || len(gnsiCredzReq.GetPasswords()) > 0 {
return gnsiCredzReq, nil
}
if gnsiCredzReqFile == "" {
Comment on lines +199 to +203
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Similar to populatePathzConfig, populateCredentialsConfig should also fallback to the global credentials configuration if the chassis-specific file is missing.

gnsiCredzReqFile := gnsiConf.GetCredentialsFile()
	if gnsiCredzReqFile == "" && m.defaults != nil && m.defaults.GnsiGlobalConfig != nil {
		gnsiCredzReqFile = m.defaults.GnsiGlobalConfig.GetCredentialsFile()
	}
	if len(gnsiCredzReq.GetCredentials()) > 0 || len(gnsiCredzReq.GetUsers()) > 0 || len(gnsiCredzReq.GetPasswords()) > 0 {
		return gnsiCredzReq, nil
	}
	if gnsiCredzReqFile == "" {

return nil, nil
}
data, err := os.ReadFile(gnsiCredzReqFile)
if err != nil {
return nil, status.Errorf(codes.Internal, "Error opening file %s: %v", gnsiCredzReqFile, err)
}
gnsiCredzReq = &bpb.Credentials{}
if err := prototext.Unmarshal(data, gnsiCredzReq); err != nil {
return nil, status.Errorf(codes.Internal, "File %s config is not valid Bootz credentials: %v", gnsiCredzReqFile, err)
}
return gnsiCredzReq, nil
}

func populateBootConfig(conf *epb.BootConfig) (*bpb.BootConfig, error) {
bootConfig := &bpb.BootConfig{}
if conf.GetOcConfigFile() != "" {
Expand All @@ -186,16 +240,16 @@ func populateBootConfig(conf *epb.BootConfig) (*bpb.BootConfig, error) {

// GetBootstrapData fetches and returns the bootstrap data response from the server.
func (m *InMemoryEntityManager) GetBootstrapData(ctx context.Context, chassis *service.Chassis, serial string) (*bpb.BootstrapDataResponse, error) {
// TODO: Populate gnsi config
return &bpb.BootstrapDataResponse{
SerialNum: serial,
IntendedImage: chassis.SoftwareImage,
BootPasswordHash: chassis.BootloaderPasswordHash,
ServerTrustCert: base64.StdEncoding.EncodeToString(m.secArtifacts.TrustAnchor.Raw),
BootConfig: chassis.BootConfig,
Credentials: &bpb.Credentials{},
// TODO: Populate pathz, authz and certificates.
Authz: chassis.Authz,
Credentials: chassis.Credentials,
Pathz: chassis.Pathz,
Authz: chassis.Authz,
CertzProfiles: chassis.CertzProfiles,
}, nil
}

Expand Down
42 changes: 41 additions & 1 deletion server/entitymanager/entitymanager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ import (
bpb "github.com/openconfig/bootz/proto/bootz"
epb "github.com/openconfig/bootz/server/entitymanager/proto/entity"
apb "github.com/openconfig/gnsi/authz"
cpb "github.com/openconfig/gnsi/certz"
kpb "github.com/openconfig/gnsi/credentialz"
ppb "github.com/openconfig/gnsi/pathz"
)

// MustMarshalBootstrapDataSigned is a helper function that marshals a BootstrapDataSigned message.
Expand Down Expand Up @@ -480,11 +483,30 @@ func TestGetBootstrapData(t *testing.T) {
VendorConfig: []byte(""),
OcConfig: []byte(""),
},
Credentials: &bpb.Credentials{
Credentials: []*kpb.AuthorizedKeysRequest{{
Credentials: []*kpb.AccountCredentials{{
Account: "gnetch",
AuthorizedKeys: []*kpb.AccountCredentials_AuthorizedKey{{
AuthorizedKey: []byte("AAAA-test-key"),
}},
}},
}},
},
Pathz: &ppb.UploadRequest{
Version: "pathz-v1",
},
Authz: &apb.UploadRequest{
Version: "v0.1694813669807611349",
CreatedOn: 1694813669807,
Policy: "{\"name\":\"default\",\"request\":{\"paths\":[\"*\"]},\"source\":{\"principals\":[\"cafyauto\"]}}",
},
CertzProfiles: &bpb.CertzProfiles{
Profiles: []*bpb.CertzProfile{{
SslProfileId: "tls",
Certz: &cpb.UploadRequest{},
}},
},
},
want: &bpb.BootstrapDataResponse{
SerialNum: "123",
Expand All @@ -501,12 +523,30 @@ func TestGetBootstrapData(t *testing.T) {
VendorConfig: []byte(""),
OcConfig: []byte(""),
},
Credentials: &bpb.Credentials{},
Credentials: &bpb.Credentials{
Credentials: []*kpb.AuthorizedKeysRequest{{
Credentials: []*kpb.AccountCredentials{{
Account: "gnetch",
AuthorizedKeys: []*kpb.AccountCredentials_AuthorizedKey{{
AuthorizedKey: []byte("AAAA-test-key"),
}},
}},
}},
},
Pathz: &ppb.UploadRequest{
Version: "pathz-v1",
},
Authz: &apb.UploadRequest{
Version: "v0.1694813669807611349",
CreatedOn: 1694813669807,
Policy: "{\"name\":\"default\",\"request\":{\"paths\":[\"*\"]},\"source\":{\"principals\":[\"cafyauto\"]}}",
},
CertzProfiles: &bpb.CertzProfiles{
Profiles: []*bpb.CertzProfile{{
SslProfileId: "tls",
Certz: &cpb.UploadRequest{},
}},
},
},
wantErr: false,
},
Expand Down
4 changes: 3 additions & 1 deletion server/entitymanager/proto/entity.proto
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ message GNSIConfig {
// gnsi credential config
bootz.Credentials credentials = 8;

// Certz profiles to create, including the target SSL profile ID.
bootz.CertzProfiles certz_profiles = 9;

}

message DHCPConfig {
Expand Down Expand Up @@ -163,4 +166,3 @@ message Chassis {
}



13 changes: 11 additions & 2 deletions server/entitymanager/proto/entity/entity.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 36 additions & 1 deletion server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ import (
"github.com/openconfig/bootz/server/entitymanager"
"github.com/openconfig/bootz/server/service"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/status"

bpb "github.com/openconfig/bootz/proto/bootz"
)
Expand Down Expand Up @@ -88,9 +90,37 @@ type InterceptorOpts struct {

func (*InterceptorOpts) isbootzServerOpts() {}

// DisableBootstrapStream disables the deprecated BootstrapStream RPC while
// keeping unary RPCs available.
type DisableBootstrapStream struct{}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We don't need to disable Streaming Bootz on the global sever level.
This is already implemented on a per chassis level.

if !chassis.StreamingSupported {

if !session.chassis.StreamingSupported {


func (*DisableBootstrapStream) isbootzServerOpts() {}

type bootstrapServer struct {
bpb.UnimplementedBootstrapServer
service *service.Service
disableBootstrapStream bool
}

func (s *bootstrapServer) GetBootstrapData(ctx context.Context, req *bpb.GetBootstrapDataRequest) (*bpb.GetBootstrapDataResponse, error) {
return s.service.GetBootstrapData(ctx, req)
}

func (s *bootstrapServer) ReportStatus(ctx context.Context, req *bpb.ReportStatusRequest) (*bpb.EmptyResponse, error) {
return s.service.ReportStatus(ctx, req)
}

func (s *bootstrapServer) BootstrapStream(stream bpb.Bootstrap_BootstrapStreamServer) error {
if s.disableBootstrapStream {
return status.Error(codes.Unimplemented, "BootstrapStream disabled")
}
return s.service.BootstrapStream(stream)
}

// NewServer start a new Bootz gRPC , dhcp, and image server based on specified flags.
func NewServer(bootzAddr string, em *entitymanager.InMemoryEntityManager, sa *service.SecurityArtifacts, opts ...bootzServerOpts) (*Server, error) {
var interceptor grpc.ServerOption
disableBootstrapStream := false
server := &Server{}
for _, opt := range opts {
switch opt := opt.(type) {
Expand All @@ -102,6 +132,8 @@ func NewServer(bootzAddr string, em *entitymanager.InMemoryEntityManager, sa *se
server.httpSrv = StartImageServer(opt)
case *InterceptorOpts:
interceptor = grpc.UnaryInterceptor(opt.BootzInterceptor)
case *DisableBootstrapStream:
disableBootstrapStream = true
default:
continue
}
Expand All @@ -124,7 +156,10 @@ func NewServer(bootzAddr string, em *entitymanager.InMemoryEntityManager, sa *se
s = grpc.NewServer(grpc.Creds(credentials.NewTLS(tls)))
}

bpb.RegisterBootstrapServer(s, c)
bpb.RegisterBootstrapServer(s, &bootstrapServer{
service: c,
disableBootstrapStream: disableBootstrapStream,
})

lis, err := net.Listen("tcp", bootzAddr)
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions server/service/service.go
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could you sync your branch to the latest of main branch?
You change was done on a stale version.

Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
log "github.com/golang/glog"
bpb "github.com/openconfig/bootz/proto/bootz"
apb "github.com/openconfig/gnsi/authz"
ppb "github.com/openconfig/gnsi/pathz"
)

// OVList is a mapping of control card serial number to ownership voucher.
Expand Down Expand Up @@ -97,7 +98,10 @@ type Chassis struct {
// The below fields are normally unset and are primarily used for
// cases where this data should be hardcoded e.g. for testing.
BootConfig *bpb.BootConfig
Credentials *bpb.Credentials
Pathz *ppb.UploadRequest
Authz *apb.UploadRequest
CertzProfiles *bpb.CertzProfiles
BootloaderPasswordHash string
}

Expand Down
16 changes: 14 additions & 2 deletions testdata/artifacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"encoding/xml"
"fmt"
"math/big"
"net"
"time"

"github.com/openconfig/bootz/server/service"
Expand Down Expand Up @@ -58,11 +59,20 @@ type Inner struct {
DomainCertRevocationChecks bool `json:"domain-cert-revocation-checks" xml:"domain-cert-revocation-checks"`
}

func certificateSANs(serverName string) ([]string, []net.IP) {
if ip := net.ParseIP(serverName); ip != nil {
return nil, []net.IP{ip}
}
return []string{serverName}, nil
}

// NewCertificateAuthority creates a new self-signed CA for the chosen organization.
func NewCertificateAuthority(commonName, org, serverName string) (*x509.Certificate, *rsa.PrivateKey, error) {
dnsNames, ipAddresses := certificateSANs(serverName)
// Create the certificate authority.
ca := &x509.Certificate{
DNSNames: []string{serverName},
DNSNames: dnsNames,
IPAddresses: ipAddresses,
SerialNumber: big.NewInt(int64(time.Now().Year())),
Subject: pkix.Name{
CommonName: commonName,
Expand Down Expand Up @@ -100,9 +110,11 @@ func NewCertificateAuthority(commonName, org, serverName string) (*x509.Certific

// NewSignedCertificate creates a new cert/private keypair signed by the provided Certificate Authority.
func NewSignedCertificate(commonName, org, serverName string, ca *x509.Certificate, caPrivateKey crypto.PrivateKey) (*x509.Certificate, *rsa.PrivateKey, error) {
dnsNames, ipAddresses := certificateSANs(serverName)
// Create the certificate template. Geographic information is the same as the Certificate Authority by default.
template := &x509.Certificate{
DNSNames: []string{serverName},
DNSNames: dnsNames,
IPAddresses: ipAddresses,
SerialNumber: big.NewInt(int64(time.Now().Year())),
Subject: pkix.Name{
CommonName: commonName,
Expand Down