forked from TrueCloudLab/frostfs-node
Compare commits
1 commit
master
...
neofs-adm-
Author | SHA1 | Date | |
---|---|---|---|
|
6daf5cc438 |
1284 changed files with 50901 additions and 71752 deletions
|
@ -1,19 +1,19 @@
|
||||||
FROM golang:1.20 as builder
|
FROM golang:1.17 as builder
|
||||||
ARG BUILD=now
|
ARG BUILD=now
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
ARG REPO=repository
|
ARG REPO=repository
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY . /src
|
COPY . /src
|
||||||
|
|
||||||
RUN make bin/frostfs-adm
|
RUN make bin/neofs-adm
|
||||||
|
|
||||||
# Executable image
|
# Executable image
|
||||||
FROM alpine AS frostfs-adm
|
FROM alpine AS neofs-adm
|
||||||
RUN apk add --no-cache bash
|
RUN apk add --no-cache bash
|
||||||
|
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
|
|
||||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
COPY --from=builder /src/bin/frostfs-adm /bin/frostfs-adm
|
COPY --from=builder /src/bin/neofs-adm /bin/neofs-adm
|
||||||
|
|
||||||
CMD ["frostfs-adm"]
|
CMD ["neofs-adm"]
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
FROM golang:1.20
|
|
||||||
|
|
||||||
WORKDIR /tmp
|
|
||||||
|
|
||||||
# Install apt packages
|
|
||||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
|
||||||
pip \
|
|
||||||
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Dash → Bash
|
|
||||||
RUN echo "dash dash/sh boolean false" | debconf-set-selections
|
|
||||||
RUN DEBIAN_FRONTEND=noninteractive dpkg-reconfigure dash
|
|
||||||
|
|
||||||
RUN useradd -u 1234 -d /home/ci -m ci
|
|
||||||
USER ci
|
|
||||||
|
|
||||||
ENV PATH="$PATH:/home/ci/.local/bin"
|
|
||||||
|
|
||||||
COPY .pre-commit-config.yaml .
|
|
||||||
|
|
||||||
RUN pip install "pre-commit==3.1.1" \
|
|
||||||
&& git init . \
|
|
||||||
&& pre-commit install-hooks \
|
|
||||||
&& rm -rf /tmp/*
|
|
|
@ -1,19 +1,19 @@
|
||||||
FROM golang:1.20 as builder
|
FROM golang:1.17 as builder
|
||||||
ARG BUILD=now
|
ARG BUILD=now
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
ARG REPO=repository
|
ARG REPO=repository
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY . /src
|
COPY . /src
|
||||||
|
|
||||||
RUN make bin/frostfs-cli
|
RUN make bin/neofs-cli
|
||||||
|
|
||||||
# Executable image
|
# Executable image
|
||||||
FROM alpine AS frostfs-cli
|
FROM alpine AS neofs-cli
|
||||||
RUN apk add --no-cache bash
|
RUN apk add --no-cache bash
|
||||||
|
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
|
|
||||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
COPY --from=builder /src/bin/frostfs-cli /bin/frostfs-cli
|
COPY --from=builder /src/bin/neofs-cli /bin/neofs-cli
|
||||||
|
|
||||||
CMD ["frostfs-cli"]
|
CMD ["neofs-cli"]
|
||||||
|
|
|
@ -3,6 +3,6 @@ RUN apk add --no-cache bash ca-certificates
|
||||||
|
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
|
|
||||||
COPY bin/frostfs-adm /bin/frostfs-adm
|
COPY bin/neofs-adm /bin/neofs-adm
|
||||||
|
|
||||||
CMD ["frostfs-adm"]
|
CMD ["neofs-adm"]
|
||||||
|
|
|
@ -3,6 +3,6 @@ RUN apk add --no-cache bash ca-certificates
|
||||||
|
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
|
|
||||||
COPY bin/frostfs-cli /bin/frostfs-cli
|
COPY bin/neofs-cli /bin/neofs-cli
|
||||||
|
|
||||||
CMD ["frostfs-cli"]
|
CMD ["neofs-cli"]
|
||||||
|
|
|
@ -3,6 +3,6 @@ RUN apk add --no-cache bash ca-certificates
|
||||||
|
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
|
|
||||||
COPY bin/frostfs-ir /bin/frostfs-ir
|
COPY bin/neofs-ir /bin/neofs-ir
|
||||||
|
|
||||||
CMD ["frostfs-ir"]
|
CMD ["neofs-ir"]
|
||||||
|
|
|
@ -3,6 +3,6 @@ RUN apk add --no-cache bash ca-certificates
|
||||||
|
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
|
|
||||||
COPY bin/frostfs-node /bin/frostfs-node
|
COPY bin/neofs-node /bin/neofs-node
|
||||||
|
|
||||||
CMD ["frostfs-node"]
|
CMD ["neofs-node"]
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
FROM golang:1.20 as builder
|
FROM golang:1.17 as builder
|
||||||
ARG BUILD=now
|
ARG BUILD=now
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
ARG REPO=repository
|
ARG REPO=repository
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY . /src
|
COPY . /src
|
||||||
|
|
||||||
RUN make bin/frostfs-ir
|
RUN make bin/neofs-ir
|
||||||
|
|
||||||
# Executable image
|
# Executable image
|
||||||
FROM alpine AS frostfs-ir
|
FROM alpine AS neofs-ir
|
||||||
RUN apk add --no-cache bash
|
RUN apk add --no-cache bash
|
||||||
|
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
|
|
||||||
COPY --from=builder /src/bin/frostfs-ir /bin/frostfs-ir
|
COPY --from=builder /src/bin/neofs-ir /bin/neofs-ir
|
||||||
|
|
||||||
CMD ["frostfs-ir"]
|
CMD ["neofs-ir"]
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
FROM golang:1.20 as builder
|
FROM golang:1.17 as builder
|
||||||
ARG BUILD=now
|
ARG BUILD=now
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
ARG REPO=repository
|
ARG REPO=repository
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY . /src
|
COPY . /src
|
||||||
|
|
||||||
RUN make bin/frostfs-node
|
RUN make bin/neofs-node
|
||||||
|
|
||||||
# Executable image
|
# Executable image
|
||||||
FROM alpine AS frostfs-node
|
FROM alpine AS neofs-node
|
||||||
RUN apk add --no-cache bash
|
RUN apk add --no-cache bash
|
||||||
|
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
|
|
||||||
COPY --from=builder /src/bin/frostfs-node /bin/frostfs-node
|
COPY --from=builder /src/bin/neofs-node /bin/neofs-node
|
||||||
|
|
||||||
CMD ["frostfs-node"]
|
CMD ["neofs-node"]
|
||||||
|
|
19
.docker/Dockerfile.storage-testnet
Normal file
19
.docker/Dockerfile.storage-testnet
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
FROM golang:1.17 as builder
|
||||||
|
ARG BUILD=now
|
||||||
|
ARG VERSION=dev
|
||||||
|
ARG REPO=repository
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . /src
|
||||||
|
|
||||||
|
RUN make bin/neofs-node
|
||||||
|
|
||||||
|
# Executable image
|
||||||
|
FROM alpine AS neofs-node
|
||||||
|
RUN apk add --no-cache bash
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
COPY --from=builder /src/bin/neofs-node /bin/neofs-node
|
||||||
|
COPY --from=builder /src/config/testnet/config.yml /config.yml
|
||||||
|
|
||||||
|
CMD ["neofs-node", "--config", "/config.yml"]
|
|
@ -5,5 +5,4 @@ docker-compose.yml
|
||||||
Dockerfile
|
Dockerfile
|
||||||
temp
|
temp
|
||||||
.dockerignore
|
.dockerignore
|
||||||
docker
|
docker
|
||||||
.cache
|
|
|
@ -1,38 +0,0 @@
|
||||||
name: Build
|
|
||||||
|
|
||||||
on: [pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build Components
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
go_versions: [ '1.19', '1.20' ]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
# Allows to fetch all history for all branches and tags.
|
|
||||||
# Need this for proper versioning.
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: '${{ matrix.go_versions }}'
|
|
||||||
|
|
||||||
- name: Build CLI
|
|
||||||
run: make bin/frostfs-cli
|
|
||||||
|
|
||||||
- name: Build NODE
|
|
||||||
run: make bin/frostfs-node
|
|
||||||
|
|
||||||
- name: Build IR
|
|
||||||
run: make bin/frostfs-ir
|
|
||||||
|
|
||||||
- name: Build ADM
|
|
||||||
run: make bin/frostfs-adm
|
|
||||||
|
|
||||||
- name: Build LENS
|
|
||||||
run: make bin/frostfs-lens
|
|
|
@ -1,72 +0,0 @@
|
||||||
name: Tests and linters
|
|
||||||
on: [pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
name: Lint
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: '1.20'
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: golangci-lint
|
|
||||||
uses: https://github.com/golangci/golangci-lint-action@v3
|
|
||||||
with:
|
|
||||||
version: latest
|
|
||||||
|
|
||||||
tests:
|
|
||||||
name: Tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
go_versions: [ '1.19', '1.20' ]
|
|
||||||
fail-fast: false
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: '${{ matrix.go_versions }}'
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: make test
|
|
||||||
|
|
||||||
tests-race:
|
|
||||||
name: Tests with -race
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: '1.20'
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: go test ./... -count=1 -race
|
|
||||||
|
|
||||||
staticcheck:
|
|
||||||
name: Staticcheck
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: '1.20'
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: Install staticcheck
|
|
||||||
run: make staticcheck-install
|
|
||||||
|
|
||||||
- name: Run staticcheck
|
|
||||||
run: make staticcheck-run
|
|
1
.gitattributes
vendored
1
.gitattributes
vendored
|
@ -1,3 +1,2 @@
|
||||||
/**/*.pb.go -diff -merge
|
/**/*.pb.go -diff -merge
|
||||||
/**/*.pb.go linguist-generated=true
|
/**/*.pb.go linguist-generated=true
|
||||||
/go.sum -diff
|
|
||||||
|
|
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -2,7 +2,7 @@
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a report to help us improve
|
about: Create a report to help us improve
|
||||||
title: ''
|
title: ''
|
||||||
labels: community, triage, bug
|
labels: community, triage
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
@ -18,11 +18,8 @@ assignees: ''
|
||||||
If suggesting a change/improvement, explain the difference from current behavior -->
|
If suggesting a change/improvement, explain the difference from current behavior -->
|
||||||
|
|
||||||
## Possible Solution
|
## Possible Solution
|
||||||
<!-- Not obligatory
|
<!-- Not obligatory, but suggest a fix/reason for the bug,
|
||||||
If no reason/fix/additions for the bug can be suggested,
|
or ideas how to implement the addition or change -->
|
||||||
uncomment the following phrase:
|
|
||||||
|
|
||||||
No fix can be suggested by a QA engineer. Further solutions shall be up to developers. -->
|
|
||||||
|
|
||||||
## Steps to Reproduce (for bugs)
|
## Steps to Reproduce (for bugs)
|
||||||
<!-- Provide a link to a live example, or an unambiguous set of steps
|
<!-- Provide a link to a live example, or an unambiguous set of steps
|
||||||
|
|
8
.github/ISSUE_TEMPLATE/feature_request.md
vendored
8
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
@ -18,11 +18,3 @@ assignees: ''
|
||||||
|
|
||||||
## Additional context
|
## Additional context
|
||||||
<!-- Add any other context or screenshots about the feature request here. -->
|
<!-- Add any other context or screenshots about the feature request here. -->
|
||||||
|
|
||||||
## Don't forget to add labels!
|
|
||||||
- component label (`neofs-adm`, `neofs-storage`, ...)
|
|
||||||
- issue type (`enhancement`, `refactor`, ...)
|
|
||||||
- `goodfirstissue`, `helpwanted` if needed
|
|
||||||
- does this issue belong to an epic?
|
|
||||||
- priority (`P0`-`P4`) if already triaged
|
|
||||||
- quarter label (`202XQY`) if possible
|
|
||||||
|
|
197
.github/logo.svg
vendored
197
.github/logo.svg
vendored
|
@ -1,70 +1,129 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<!-- Generator: Adobe Illustrator 25.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
<svg
|
||||||
<svg version="1.1" id="Слой_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
viewBox="0 0 184.2 51.8" style="enable-background:new 0 0 184.2 51.8;" xml:space="preserve">
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
<style type="text/css">
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
.st0{display:none;}
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
.st1{display:inline;}
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
.st2{fill:#01E397;}
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
.st3{display:inline;fill:#010032;}
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
.st4{display:inline;fill:#00E599;}
|
sodipodi:docname="logo_fs.svg"
|
||||||
.st5{display:inline;fill:#00AF92;}
|
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
|
||||||
.st6{fill:#00C3E5;}
|
id="svg57"
|
||||||
</style>
|
version="1.1"
|
||||||
<g id="Layer_2">
|
viewBox="0 0 105 25"
|
||||||
<g id="Layer_1-2" class="st0">
|
height="25mm"
|
||||||
<g class="st1">
|
width="105mm">
|
||||||
<path class="st2" d="M146.6,18.3v7.2h10.9V29h-10.9v10.7h-4V14.8h18v3.5H146.6z"/>
|
<defs
|
||||||
<path class="st2" d="M180,15.7c1.7,0.9,3,2.2,4,3.8l-3,2.7c-0.6-1.3-1.5-2.4-2.6-3.3c-1.3-0.7-2.8-1-4.3-1
|
id="defs51">
|
||||||
c-1.4-0.1-2.8,0.3-4,1.1c-0.9,0.5-1.5,1.5-1.4,2.6c0,1,0.5,1.9,1.4,2.4c1.5,0.8,3.2,1.3,4.9,1.5c1.9,0.3,3.7,0.8,5.4,1.6
|
<clipPath
|
||||||
c1.2,0.5,2.2,1.3,2.9,2.3c0.6,1,1,2.2,0.9,3.4c0,1.4-0.5,2.7-1.3,3.8c-0.9,1.2-2.1,2.1-3.5,2.6c-1.7,0.6-3.4,0.9-5.2,0.8
|
clipPathUnits="userSpaceOnUse"
|
||||||
c-5,0-8.6-1.6-10.7-5l2.9-2.8c0.7,1.4,1.8,2.5,3.1,3.3c1.5,0.7,3.1,1.1,4.7,1c1.5,0.1,2.9-0.2,4.2-0.9c0.9-0.5,1.5-1.5,1.5-2.6
|
id="clipPath434">
|
||||||
c0-0.9-0.5-1.8-1.3-2.2c-1.5-0.7-3.1-1.2-4.8-1.5c-1.9-0.3-3.7-0.8-5.5-1.5c-1.2-0.5-2.2-1.4-3-2.4c-0.6-1-1-2.2-0.9-3.4
|
<path
|
||||||
c0-1.4,0.4-2.7,1.2-3.8c0.8-1.2,2-2.2,3.3-2.8c1.6-0.7,3.4-1.1,5.2-1C176.1,14.3,178.2,14.8,180,15.7z"/>
|
d="M 0,0 H 1366 V 768 H 0 Z"
|
||||||
</g>
|
id="path432" />
|
||||||
<path class="st3" d="M73.3,16.3c1.9,1.9,2.9,4.5,2.7,7.1v15.9h-4V24.8c0-2.6-0.5-4.5-1.6-5.7c-1.2-1.2-2.8-1.8-4.5-1.7
|
</clipPath>
|
||||||
c-1.3,0-2.5,0.3-3.7,0.8c-1.2,0.7-2.2,1.7-2.9,2.9c-0.8,1.5-1.1,3.2-1.1,4.9v13.3h-4V15.1l3.6,1.5v1.7c0.8-1.5,2.1-2.6,3.6-3.3
|
</defs>
|
||||||
c1.5-0.8,3.2-1.2,4.9-1.1C68.9,13.8,71.3,14.7,73.3,16.3z"/>
|
<sodipodi:namedview
|
||||||
<path class="st3" d="M104.4,28.3H85.6c0.1,2.2,1,4.3,2.5,5.9c1.5,1.4,3.5,2.2,5.6,2.1c1.6,0.1,3.2-0.2,4.6-0.9
|
inkscape:window-maximized="0"
|
||||||
c1.1-0.6,2-1.6,2.5-2.8l3.3,1.8c-0.9,1.7-2.3,3.1-4,4c-2,1-4.2,1.5-6.4,1.4c-3.7,0-6.7-1.1-8.8-3.4s-3.2-5.5-3.2-9.6s1-7.2,3-9.5
|
inkscape:window-y="0"
|
||||||
s5-3.4,8.7-3.4c2.1-0.1,4.2,0.5,6.1,1.5c1.6,1,3,2.5,3.8,4.2c0.9,1.8,1.3,3.9,1.3,5.9C104.6,26.4,104.6,27.4,104.4,28.3z
|
inkscape:window-x="130"
|
||||||
M88.1,19.3c-1.4,1.5-2.2,3.4-2.4,5.5h15.1c-0.2-2-1-3.9-2.3-5.5c-1.4-1.3-3.2-2-5.1-1.9C91.5,17.3,89.6,18,88.1,19.3z"/>
|
inkscape:window-height="1040"
|
||||||
<path class="st3" d="M131,17.3c2.2,2.3,3.2,5.5,3.2,9.5s-1,7.3-3.2,9.6s-5.1,3.4-8.8,3.4s-6.7-1.1-8.9-3.4s-3.2-5.5-3.2-9.6
|
inkscape:window-width="1274"
|
||||||
s1.1-7.2,3.2-9.5s5.1-3.4,8.9-3.4S128.9,15,131,17.3z M116.2,19.9c-1.5,2-2.2,4.4-2.1,6.9c-0.2,2.5,0.6,5,2.1,7
|
height="50mm"
|
||||||
c1.5,1.7,3.7,2.7,6,2.6c2.3,0.1,4.4-0.9,5.9-2.6c1.5-2,2.3-4.5,2.1-7c0.1-2.5-0.6-4.9-2.1-6.9c-1.5-1.7-3.6-2.7-5.9-2.6
|
units="mm"
|
||||||
C119.9,17.2,117.7,18.2,116.2,19.9z"/>
|
showgrid="false"
|
||||||
<polygon class="st4" points="0,9.1 0,43.7 22.5,51.8 22.5,16.9 46.8,7.9 24.8,0 "/>
|
inkscape:document-rotation="0"
|
||||||
<polygon class="st5" points="24.3,17.9 24.3,36.8 46.8,44.9 46.8,9.6 "/>
|
inkscape:current-layer="layer1"
|
||||||
</g>
|
inkscape:document-units="mm"
|
||||||
<g>
|
inkscape:cy="344.49897"
|
||||||
<g>
|
inkscape:cx="468.64708"
|
||||||
<path class="st6" d="M41.6,17.5H28.2v6.9h10.4v3.3H28.2v10.2h-3.9V14.2h17.2V17.5z"/>
|
inkscape:zoom="0.7"
|
||||||
<path class="st6" d="M45.8,37.9v-18h3.3l0.4,3.2c0.5-1.2,1.2-2.1,2.1-2.7c0.9-0.6,2.1-0.9,3.5-0.9c0.4,0,0.7,0,1.1,0.1
|
inkscape:pageshadow="2"
|
||||||
c0.4,0.1,0.7,0.2,0.9,0.3l-0.5,3.4c-0.3-0.1-0.6-0.2-0.9-0.2C55.4,23,54.9,23,54.4,23c-0.7,0-1.5,0.2-2.2,0.6
|
inkscape:pageopacity="0.0"
|
||||||
c-0.7,0.4-1.3,1-1.8,1.8s-0.7,1.8-0.7,3v9.5H45.8z"/>
|
borderopacity="1.0"
|
||||||
<path class="st6" d="M68.6,19.6c1.8,0,3.3,0.4,4.6,1.1c1.3,0.7,2.4,1.8,3.1,3.2s1.1,3.1,1.1,5c0,1.9-0.4,3.6-1.1,5
|
bordercolor="#666666"
|
||||||
c-0.8,1.4-1.8,2.5-3.1,3.2c-1.3,0.7-2.9,1.1-4.6,1.1s-3.3-0.4-4.6-1.1c-1.3-0.7-2.4-1.8-3.2-3.2c-0.8-1.4-1.2-3.1-1.2-5
|
pagecolor="#ffffff"
|
||||||
c0-1.9,0.4-3.6,1.2-5s1.8-2.5,3.2-3.2C65.3,19.9,66.8,19.6,68.6,19.6z M68.6,22.6c-1.1,0-2,0.2-2.8,0.7c-0.8,0.5-1.3,1.2-1.7,2.1
|
id="base" />
|
||||||
s-0.6,2.1-0.6,3.5c0,1.3,0.2,2.5,0.6,3.4s1,1.7,1.7,2.2s1.7,0.7,2.8,0.7c1.1,0,2-0.2,2.7-0.7c0.7-0.5,1.3-1.2,1.7-2.2
|
<metadata
|
||||||
s0.6-2.1,0.6-3.4c0-1.4-0.2-2.5-0.6-3.5s-1-1.6-1.7-2.1C70.6,22.8,69.6,22.6,68.6,22.6z"/>
|
id="metadata54">
|
||||||
<path class="st6" d="M89.2,38.3c-1.8,0-3.4-0.3-4.9-1c-1.5-0.7-2.7-1.7-3.5-3l2.7-2.3c0.5,1,1.3,1.8,2.3,2.4
|
<rdf:RDF>
|
||||||
c1,0.6,2.2,0.9,3.6,0.9c1.1,0,2-0.2,2.6-0.6c0.6-0.4,1-0.9,1-1.6c0-0.5-0.2-0.9-0.5-1.2s-0.9-0.6-1.7-0.8l-3.8-0.8
|
<cc:Work
|
||||||
c-1.9-0.4-3.3-1-4.1-1.9c-0.8-0.9-1.2-1.9-1.2-3.3c0-1,0.3-1.9,0.9-2.7c0.6-0.8,1.4-1.5,2.5-2s2.5-0.8,4-0.8c1.8,0,3.3,0.3,4.6,1
|
rdf:about="">
|
||||||
c1.3,0.6,2.2,1.5,2.9,2.7l-2.7,2.2c-0.5-1-1.1-1.7-2-2.1c-0.9-0.5-1.8-0.7-2.8-0.7c-0.8,0-1.4,0.1-2,0.3c-0.6,0.2-1,0.5-1.3,0.8
|
<dc:format>image/svg+xml</dc:format>
|
||||||
c-0.3,0.3-0.4,0.7-0.4,1.2c0,0.5,0.2,0.9,0.5,1.3s1,0.6,1.9,0.8l4.1,0.9c1.7,0.3,2.9,0.9,3.7,1.7c0.7,0.8,1.1,1.8,1.1,2.9
|
<dc:type
|
||||||
c0,1.2-0.3,2.2-0.9,3c-0.6,0.9-1.5,1.6-2.6,2C92.1,38.1,90.7,38.3,89.2,38.3z"/>
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
<path class="st6" d="M112.8,19.9v3H99.3v-3H112.8z M106.6,14.6v17.9c0,0.9,0.2,1.5,0.7,1.9c0.5,0.4,1.1,0.6,1.9,0.6
|
<dc:title></dc:title>
|
||||||
c0.6,0,1.2-0.1,1.7-0.3c0.5-0.2,0.9-0.5,1.3-0.8l0.9,2.8c-0.6,0.5-1.2,0.9-2,1.1c-0.8,0.3-1.7,0.4-2.7,0.4c-1,0-2-0.2-2.8-0.5
|
</cc:Work>
|
||||||
s-1.5-0.9-2-1.6c-0.5-0.8-0.7-1.7-0.8-3V15.7L106.6,14.6z"/>
|
</rdf:RDF>
|
||||||
<path d="M137.9,17.5h-13.3v6.9h10.4v3.3h-10.4v10.2h-3.9V14.2h17.2V17.5z"/>
|
</metadata>
|
||||||
<path d="M150.9,13.8c2.1,0,4,0.4,5.5,1.2c1.6,0.8,2.9,2,4,3.5l-2.6,2.5c-0.9-1.4-1.9-2.4-3.1-3c-1.1-0.6-2.5-0.9-4-0.9
|
<g
|
||||||
c-1.2,0-2.1,0.2-2.8,0.5c-0.7,0.3-1.3,0.7-1.6,1.2c-0.3,0.5-0.5,1.1-0.5,1.7c0,0.7,0.3,1.4,0.8,1.9c0.5,0.6,1.5,1,2.9,1.3
|
id="layer1"
|
||||||
l4.8,1.1c2.3,0.5,3.9,1.3,4.9,2.3c1,1,1.4,2.3,1.4,3.9c0,1.5-0.4,2.7-1.2,3.8c-0.8,1.1-1.9,1.9-3.3,2.5s-3.1,0.9-5,0.9
|
inkscape:groupmode="layer"
|
||||||
c-1.7,0-3.2-0.2-4.5-0.6c-1.3-0.4-2.5-1-3.5-1.8c-1-0.7-1.8-1.6-2.5-2.6l2.7-2.7c0.5,0.8,1.1,1.6,1.9,2.2
|
inkscape:label="Layer 1">
|
||||||
c0.8,0.7,1.7,1.2,2.7,1.5c1,0.4,2.2,0.5,3.4,0.5c1.1,0,2.1-0.1,2.9-0.4c0.8-0.3,1.4-0.7,1.8-1.2c0.4-0.5,0.6-1.1,0.6-1.9
|
<g
|
||||||
c0-0.7-0.2-1.3-0.7-1.8c-0.5-0.5-1.3-0.9-2.6-1.2l-5.2-1.2c-1.4-0.3-2.6-0.8-3.6-1.3c-0.9-0.6-1.6-1.3-2.1-2.1s-0.7-1.8-0.7-2.8
|
id="g424"
|
||||||
c0-1.3,0.4-2.6,1.1-3.7c0.7-1.1,1.8-2,3.2-2.6C147.3,14.1,148.9,13.8,150.9,13.8z"/>
|
transform="matrix(0.35277777,0,0,-0.35277777,63.946468,10.194047)">
|
||||||
</g>
|
<path
|
||||||
</g>
|
d="m 0,0 v -8.093 h 12.287 v -3.94 H 0 V -24.067 H -4.534 V 3.898 H 15.677 V 0 Z"
|
||||||
</g>
|
style="fill:#00e396;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path426" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
transform="matrix(0.35277777,0,0,-0.35277777,-315.43002,107.34005)"
|
||||||
|
id="g428">
|
||||||
|
<g
|
||||||
|
id="g430"
|
||||||
|
clip-path="url(#clipPath434)">
|
||||||
|
<g
|
||||||
|
id="g436"
|
||||||
|
transform="translate(1112.874,278.2981)">
|
||||||
|
<path
|
||||||
|
d="M 0,0 C 1.822,-0.932 3.354,-2.359 4.597,-4.28 L 1.165,-7.373 c -0.791,1.695 -1.779,2.924 -2.966,3.686 -1.186,0.763 -2.768,1.145 -4.745,1.145 -1.949,0 -3.461,-0.389 -4.534,-1.166 -1.074,-0.777 -1.61,-1.772 -1.61,-2.987 0,-1.13 0.523,-2.027 1.568,-2.69 1.045,-0.664 2.909,-1.236 5.593,-1.716 2.514,-0.452 4.512,-1.024 5.995,-1.716 1.483,-0.693 2.564,-1.554 3.242,-2.585 0.677,-1.031 1.016,-2.309 1.016,-3.834 0,-1.639 -0.466,-3.079 -1.398,-4.322 -0.932,-1.243 -2.239,-2.197 -3.919,-2.86 -1.681,-0.664 -3.623,-0.996 -5.826,-0.996 -5.678,0 -9.689,1.892 -12.033,5.678 l 3.178,3.178 c 0.903,-1.695 2.068,-2.939 3.495,-3.729 1.426,-0.791 3.199,-1.186 5.318,-1.186 2.005,0 3.58,0.345 4.724,1.038 1.144,0.692 1.716,1.674 1.716,2.945 0,1.017 -0.516,1.835 -1.547,2.457 -1.031,0.621 -2.832,1.172 -5.402,1.653 -2.571,0.479 -4.618,1.073 -6.143,1.779 -1.526,0.706 -2.635,1.582 -3.326,2.627 -0.693,1.045 -1.039,2.316 -1.039,3.813 0,1.582 0.438,3.023 1.314,4.322 0.875,1.299 2.14,2.33 3.792,3.093 1.653,0.763 3.58,1.144 5.783,1.144 C -4.018,1.398 -1.822,0.932 0,0"
|
||||||
|
style="fill:#00e396;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path438" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g440"
|
||||||
|
transform="translate(993.0239,277.5454)">
|
||||||
|
<path
|
||||||
|
d="m 0,0 c 2.054,-1.831 3.083,-4.465 3.083,-7.902 v -17.935 h -4.484 v 16.366 c 0,2.914 -0.626,5.024 -1.877,6.332 -1.253,1.308 -2.924,1.962 -5.016,1.962 -1.495,0 -2.896,-0.327 -4.204,-0.981 -1.308,-0.654 -2.381,-1.719 -3.222,-3.194 -0.841,-1.477 -1.261,-3.335 -1.261,-5.576 v -14.909 h -4.484 V 1.328 l 4.086,-1.674 0.118,-1.84 c 0.933,1.681 2.222,2.923 3.867,3.727 1.643,0.803 3.493,1.205 5.548,1.205 C -4.671,2.746 -2.055,1.83 0,0"
|
||||||
|
style="fill:#000033;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path442" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g444"
|
||||||
|
transform="translate(1027.9968,264.0386)">
|
||||||
|
<path
|
||||||
|
d="m 0,0 h -21.128 c 0.261,-2.84 1.205,-5.044 2.83,-6.613 1.625,-1.57 3.727,-2.355 6.305,-2.355 2.054,0 3.763,0.356 5.128,1.065 1.363,0.71 2.288,1.738 2.774,3.083 l 3.755,-1.961 c -1.121,-1.981 -2.616,-3.495 -4.484,-4.54 -1.868,-1.046 -4.259,-1.569 -7.173,-1.569 -4.223,0 -7.538,1.289 -9.948,3.867 -2.41,2.578 -3.615,6.146 -3.615,10.704 0,4.558 1.149,8.127 3.447,10.705 2.298,2.578 5.557,3.867 9.779,3.867 2.615,0 4.876,-0.58 6.782,-1.738 1.905,-1.158 3.343,-2.728 4.315,-4.707 C -0.262,7.827 0.224,5.605 0.224,3.139 0.224,2.092 0.149,1.046 0,0 m -18.298,10.144 c -1.513,-1.457 -2.438,-3.512 -2.775,-6.165 h 16.982 c -0.3,2.615 -1.159,4.661 -2.578,6.137 -1.42,1.476 -3.307,2.214 -5.661,2.214 -2.466,0 -4.455,-0.728 -5.968,-2.186"
|
||||||
|
style="fill:#000033;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path446" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g448"
|
||||||
|
transform="translate(1057.8818,276.4246)">
|
||||||
|
<path
|
||||||
|
d="m 0,0 c 2.41,-2.578 3.615,-6.147 3.615,-10.705 0,-4.558 -1.205,-8.126 -3.615,-10.704 -2.41,-2.578 -5.726,-3.867 -9.948,-3.867 -4.222,0 -7.537,1.289 -9.947,3.867 -2.41,2.578 -3.615,6.146 -3.615,10.704 0,4.558 1.205,8.127 3.615,10.705 2.41,2.578 5.725,3.867 9.947,3.867 C -5.726,3.867 -2.41,2.578 0,0 m -16.617,-2.858 c -1.607,-1.906 -2.41,-4.522 -2.41,-7.847 0,-3.326 0.803,-5.94 2.41,-7.846 1.607,-1.905 3.83,-2.858 6.669,-2.858 2.839,0 5.063,0.953 6.67,2.858 1.606,1.906 2.41,4.52 2.41,7.846 0,3.325 -0.804,5.941 -2.41,7.847 C -4.885,-0.953 -7.109,0 -9.948,0 c -2.839,0 -5.062,-0.953 -6.669,-2.858"
|
||||||
|
style="fill:#000033;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path450" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g452"
|
||||||
|
transform="matrix(0.35277777,0,0,-0.35277777,5.8329581,6.5590171)">
|
||||||
|
<path
|
||||||
|
d="m 0,0 0.001,-38.946 25.286,-9.076 V -8.753 L 52.626,1.321 27.815,10.207 Z"
|
||||||
|
style="fill:#00e599;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path454" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g456"
|
||||||
|
transform="matrix(0.35277777,0,0,-0.35277777,15.479008,10.041927)">
|
||||||
|
<path
|
||||||
|
d="M 0,0 V -21.306 L 25.293,-30.364 25.282,9.347 Z"
|
||||||
|
style="fill:#00b091;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path458" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 6.5 KiB |
21
.github/workflows/dco.yml
vendored
Normal file
21
.github/workflows/dco.yml
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
name: DCO check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
commits_check_job:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Commits Check
|
||||||
|
steps:
|
||||||
|
- name: Get PR Commits
|
||||||
|
id: 'get-pr-commits'
|
||||||
|
uses: tim-actions/get-pr-commits@master
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: DCO Check
|
||||||
|
uses: tim-actions/dco@master
|
||||||
|
with:
|
||||||
|
commits: ${{ steps.get-pr-commits.outputs.commits }}
|
57
.github/workflows/go.yml
vendored
Normal file
57
.github/workflows/go.yml
vendored
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
name: neofs-node tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths-ignore:
|
||||||
|
- '*.md'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths-ignore:
|
||||||
|
- '*.md'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go: [ '1.17.x', '1.18.x' ]
|
||||||
|
steps:
|
||||||
|
- name: Setup go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go }}
|
||||||
|
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Cache go mod
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-${{ matrix.go }}-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-${{ matrix.go }}-
|
||||||
|
|
||||||
|
- name: Run go test
|
||||||
|
run: go test -coverprofile=coverage.txt -covermode=atomic ./...
|
||||||
|
|
||||||
|
- name: Codecov
|
||||||
|
env:
|
||||||
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
run: bash <(curl -s https://codecov.io/bash)
|
||||||
|
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v2
|
||||||
|
with:
|
||||||
|
version: v1.45.2
|
||||||
|
args: --timeout=5m
|
||||||
|
only-new-issues: true
|
49
.gitignore
vendored
49
.gitignore
vendored
|
@ -1,51 +1,12 @@
|
||||||
# IDE
|
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
bin
|
||||||
|
release
|
||||||
# Vendoring
|
|
||||||
vendor
|
|
||||||
|
|
||||||
# tempfiles
|
|
||||||
.DS_Store
|
|
||||||
*~
|
|
||||||
.cache
|
|
||||||
|
|
||||||
temp
|
temp
|
||||||
tmp
|
|
||||||
|
|
||||||
# binary
|
|
||||||
bin/
|
|
||||||
release/
|
|
||||||
|
|
||||||
# coverage
|
|
||||||
coverage.txt
|
|
||||||
coverage.html
|
|
||||||
|
|
||||||
# testing
|
|
||||||
cmd/test
|
cmd/test
|
||||||
/plugins/
|
/plugins/
|
||||||
testfile
|
/vendor/
|
||||||
|
|
||||||
# misc
|
testfile
|
||||||
.neofs-cli.yml
|
.neofs-cli.yml
|
||||||
|
|
||||||
# debhelpers
|
.cache
|
||||||
debian/*debhelper*
|
|
||||||
|
|
||||||
# logfiles
|
|
||||||
debian/*.log
|
|
||||||
|
|
||||||
# .substvars
|
|
||||||
debian/*.substvars
|
|
||||||
|
|
||||||
# .bash-completion
|
|
||||||
debian/*.bash-completion
|
|
||||||
|
|
||||||
# Install folders and files
|
|
||||||
debian/frostfs-cli/
|
|
||||||
debian/frostfs-ir/
|
|
||||||
debian/files
|
|
||||||
debian/frostfs-storage/
|
|
||||||
debian/changelog
|
|
||||||
man/
|
|
||||||
debs/
|
|
||||||
|
|
11
.gitlint
11
.gitlint
|
@ -1,11 +0,0 @@
|
||||||
[general]
|
|
||||||
fail-without-commits=True
|
|
||||||
regex-style-search=True
|
|
||||||
contrib=CC1
|
|
||||||
|
|
||||||
[title-match-regex]
|
|
||||||
regex=^\[\#[0-9Xx]+\]\s
|
|
||||||
|
|
||||||
[ignore-by-title]
|
|
||||||
regex=^Release(.*)
|
|
||||||
ignore=title-match-regex
|
|
|
@ -4,7 +4,7 @@
|
||||||
# options for analysis running
|
# options for analysis running
|
||||||
run:
|
run:
|
||||||
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
||||||
timeout: 20m
|
timeout: 5m
|
||||||
|
|
||||||
# include test files or not, default is true
|
# include test files or not, default is true
|
||||||
tests: false
|
tests: false
|
||||||
|
@ -24,19 +24,6 @@ linters-settings:
|
||||||
govet:
|
govet:
|
||||||
# report about shadowed variables
|
# report about shadowed variables
|
||||||
check-shadowing: false
|
check-shadowing: false
|
||||||
staticcheck:
|
|
||||||
checks: ["all", "-SA1019"] # TODO Enable SA1019 after deprecated warning are fixed.
|
|
||||||
funlen:
|
|
||||||
lines: 80 # default 60
|
|
||||||
statements: 60 # default 40
|
|
||||||
gocognit:
|
|
||||||
min-complexity: 40 # default 30
|
|
||||||
importas:
|
|
||||||
no-unaliased: true
|
|
||||||
no-extra-aliases: false
|
|
||||||
alias:
|
|
||||||
pkg: git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object
|
|
||||||
alias: objectSDK
|
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
enable:
|
enable:
|
||||||
|
@ -47,27 +34,16 @@ linters:
|
||||||
# some default golangci-lint linters
|
# some default golangci-lint linters
|
||||||
- errcheck
|
- errcheck
|
||||||
- gosimple
|
- gosimple
|
||||||
- godot
|
|
||||||
- ineffassign
|
- ineffassign
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- typecheck
|
- typecheck
|
||||||
- unused
|
|
||||||
|
|
||||||
# extra linters
|
# extra linters
|
||||||
- bidichk
|
|
||||||
- durationcheck
|
|
||||||
- exhaustive
|
- exhaustive
|
||||||
- exportloopref
|
|
||||||
- gofmt
|
- gofmt
|
||||||
- goimports
|
|
||||||
- misspell
|
|
||||||
- predeclared
|
|
||||||
- reassign
|
|
||||||
- whitespace
|
- whitespace
|
||||||
- containedctx
|
- goimports
|
||||||
- funlen
|
- unused
|
||||||
- gocognit
|
|
||||||
- contextcheck
|
|
||||||
- importas
|
|
||||||
disable-all: true
|
disable-all: true
|
||||||
fast: false
|
fast: false
|
||||||
|
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
ci:
|
|
||||||
autofix_prs: false
|
|
||||||
|
|
||||||
repos:
|
|
||||||
- repo: https://github.com/jorisroovers/gitlint
|
|
||||||
rev: v0.19.1
|
|
||||||
hooks:
|
|
||||||
- id: gitlint
|
|
||||||
stages: [commit-msg]
|
|
||||||
- id: gitlint-ci
|
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
||||||
rev: v4.4.0
|
|
||||||
hooks:
|
|
||||||
- id: check-added-large-files
|
|
||||||
- id: check-case-conflict
|
|
||||||
- id: check-executables-have-shebangs
|
|
||||||
- id: check-shebang-scripts-are-executable
|
|
||||||
- id: check-merge-conflict
|
|
||||||
- id: check-json
|
|
||||||
- id: check-xml
|
|
||||||
- id: check-yaml
|
|
||||||
- id: trailing-whitespace
|
|
||||||
args: [--markdown-linebreak-ext=md]
|
|
||||||
- id: end-of-file-fixer
|
|
||||||
exclude: ".key$"
|
|
||||||
|
|
||||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
|
||||||
rev: v0.9.0.2
|
|
||||||
hooks:
|
|
||||||
- id: shellcheck
|
|
||||||
|
|
||||||
- repo: https://github.com/golangci/golangci-lint
|
|
||||||
rev: v1.52.2
|
|
||||||
hooks:
|
|
||||||
- id: golangci-lint
|
|
||||||
|
|
||||||
- repo: local
|
|
||||||
hooks:
|
|
||||||
- id: go-unit-tests
|
|
||||||
name: go unit tests
|
|
||||||
entry: make test
|
|
||||||
pass_filenames: false
|
|
||||||
types: [go]
|
|
||||||
language: system
|
|
||||||
|
|
||||||
- repo: https://github.com/TekWizely/pre-commit-golang
|
|
||||||
rev: v1.0.0-rc.1
|
|
||||||
hooks:
|
|
||||||
- id: go-staticcheck-repo-mod
|
|
||||||
- id: go-mod-tidy
|
|
|
@ -1,11 +0,0 @@
|
||||||
pipeline:
|
|
||||||
# Kludge for non-root containers under WoodPecker
|
|
||||||
fix-ownership:
|
|
||||||
image: alpine:latest
|
|
||||||
commands: chown -R 1234:1234 .
|
|
||||||
|
|
||||||
pre-commit:
|
|
||||||
image: git.frostfs.info/truecloudlab/frostfs-ci:v0.36
|
|
||||||
commands:
|
|
||||||
- export HOME="$(getent passwd $(id -u) | cut '-d:' -f6)"
|
|
||||||
- pre-commit run --hook-stage manual
|
|
1182
CHANGELOG.md
1182
CHANGELOG.md
File diff suppressed because it is too large
Load diff
|
@ -3,8 +3,8 @@
|
||||||
First, thank you for contributing! We love and encourage pull requests from
|
First, thank you for contributing! We love and encourage pull requests from
|
||||||
everyone. Please follow the guidelines:
|
everyone. Please follow the guidelines:
|
||||||
|
|
||||||
- Check the open [issues](https://github.com/TrueCloudLab/frostfs-node/issues) and
|
- Check the open [issues](https://github.com/nspcc-dev/neofs-node/issues) and
|
||||||
[pull requests](https://github.com/TrueCloudLab/frostfs-node/pulls) for existing
|
[pull requests](https://github.com/nspcc-dev/neofs-node/pulls) for existing
|
||||||
discussions.
|
discussions.
|
||||||
|
|
||||||
- Open an issue first, to discuss a new feature or enhancement.
|
- Open an issue first, to discuss a new feature or enhancement.
|
||||||
|
@ -23,23 +23,23 @@ everyone. Please follow the guidelines:
|
||||||
|
|
||||||
## Development Workflow
|
## Development Workflow
|
||||||
|
|
||||||
Start by forking the `frostfs-node` repository, make changes in a branch and then
|
Start by forking the `neofs-node` repository, make changes in a branch and then
|
||||||
send a pull request. We encourage pull requests to discuss code changes. Here
|
send a pull request. We encourage pull requests to discuss code changes. Here
|
||||||
are the steps in details:
|
are the steps in details:
|
||||||
|
|
||||||
### Set up your GitHub Repository
|
### Set up your GitHub Repository
|
||||||
Fork [FrostFS node upstream](https://github.com/TrueCloudLab/frostfs-node/fork) source
|
Fork [NeoFS node upstream](https://github.com/nspcc-dev/neofs-node/fork) source
|
||||||
repository to your own personal repository. Copy the URL of your fork (you will
|
repository to your own personal repository. Copy the URL of your fork (you will
|
||||||
need it for the `git clone` command below).
|
need it for the `git clone` command below).
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ git clone https://github.com/TrueCloudLab/frostfs-node
|
$ git clone https://github.com/nspcc-dev/neofs-node
|
||||||
```
|
```
|
||||||
|
|
||||||
### Set up git remote as ``upstream``
|
### Set up git remote as ``upstream``
|
||||||
```sh
|
```sh
|
||||||
$ cd frostfs-node
|
$ cd neofs-node
|
||||||
$ git remote add upstream https://github.com/TrueCloudLab/frostfs-node
|
$ git remote add upstream https://github.com/nspcc-dev/neofs-node
|
||||||
$ git fetch upstream
|
$ git fetch upstream
|
||||||
$ git merge upstream/master
|
$ git merge upstream/master
|
||||||
...
|
...
|
||||||
|
@ -79,7 +79,7 @@ Description
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
$ git commit -sam '[#123] Add some feature'
|
$ git commit -am '[#123] Add some feature'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Push to the branch
|
### Push to the branch
|
||||||
|
@ -106,8 +106,7 @@ contributors".
|
||||||
To sign your work, just add a line like this at the end of your commit message:
|
To sign your work, just add a line like this at the end of your commit message:
|
||||||
|
|
||||||
```
|
```
|
||||||
Signed-off-by: Samii Sakisaka <samii@ivunojikan.co.jp>
|
Signed-off-by: Samii Sakisaka <samii@nspcc.ru>
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This can easily be done with the `--signoff` option to `git commit`.
|
This can easily be done with the `--signoff` option to `git commit`.
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
# Credits
|
# Credits
|
||||||
|
|
||||||
FrostFS continues the development of NeoFS.
|
|
||||||
|
|
||||||
Initial NeoFS research and development (2018-2020) was done by
|
Initial NeoFS research and development (2018-2020) was done by
|
||||||
[NeoSPCC](https://nspcc.ru) team.
|
[NeoSPCC](https://nspcc.ru) team.
|
||||||
|
|
||||||
|
@ -10,7 +8,7 @@ In alphabetical order:
|
||||||
- Alexey Vanin
|
- Alexey Vanin
|
||||||
- Anastasia Prasolova
|
- Anastasia Prasolova
|
||||||
- Anatoly Bogatyrev
|
- Anatoly Bogatyrev
|
||||||
- Evgeny Kulikov
|
- Evgeny Kulikov
|
||||||
- Evgeny Stratonikov
|
- Evgeny Stratonikov
|
||||||
- Leonard Liubich
|
- Leonard Liubich
|
||||||
- Sergei Liubich
|
- Sergei Liubich
|
||||||
|
|
96
Makefile
Executable file → Normal file
96
Makefile
Executable file → Normal file
|
@ -2,13 +2,14 @@
|
||||||
SHELL = bash
|
SHELL = bash
|
||||||
|
|
||||||
REPO ?= $(shell go list -m)
|
REPO ?= $(shell go list -m)
|
||||||
VERSION ?= $(shell git describe --tags --dirty --match "v*" --always --abbrev=8 2>/dev/null || cat VERSION 2>/dev/null || echo "develop")
|
VERSION ?= $(shell git describe --tags --dirty --always)
|
||||||
|
BUILD ?= $(shell date -u --iso=seconds)
|
||||||
|
DEBUG ?= false
|
||||||
|
|
||||||
HUB_IMAGE ?= truecloudlab/frostfs
|
HUB_IMAGE ?= nspccdev/neofs
|
||||||
HUB_TAG ?= "$(shell echo ${VERSION} | sed 's/^v//')"
|
HUB_TAG ?= "$(shell echo ${VERSION} | sed 's/^v//')"
|
||||||
|
|
||||||
GO_VERSION ?= 1.19
|
GO_VERSION ?= 1.17
|
||||||
LINT_VERSION ?= 1.52.2
|
|
||||||
ARCH = amd64
|
ARCH = amd64
|
||||||
|
|
||||||
BIN = bin
|
BIN = bin
|
||||||
|
@ -16,31 +17,24 @@ RELEASE = release
|
||||||
DIRS = $(BIN) $(RELEASE)
|
DIRS = $(BIN) $(RELEASE)
|
||||||
|
|
||||||
# List of binaries to build.
|
# List of binaries to build.
|
||||||
CMDS = $(notdir $(basename $(wildcard cmd/frostfs-*)))
|
CMDS = $(notdir $(basename $(wildcard cmd/*)))
|
||||||
BINS = $(addprefix $(BIN)/, $(CMDS))
|
BINS = $(addprefix $(BIN)/, $(CMDS))
|
||||||
|
|
||||||
# .deb package versioning
|
.PHONY: help all images dep clean fmts fmt imports test lint docker/lint prepare-release
|
||||||
OS_RELEASE = $(shell lsb_release -cs)
|
|
||||||
PKG_VERSION ?= $(shell echo $(VERSION) | sed "s/^v//" | \
|
|
||||||
sed -E "s/(.*)-(g[a-fA-F0-9]{6,8})(.*)/\1\3~\2/" | \
|
|
||||||
sed "s/-/~/")-${OS_RELEASE}
|
|
||||||
|
|
||||||
.PHONY: help all images dep clean fmts fmt imports test lint docker/lint
|
|
||||||
prepare-release debpackage pre-commit unpre-commit
|
|
||||||
|
|
||||||
# To build a specific binary, use it's name prefix with bin/ as a target
|
# To build a specific binary, use it's name prefix with bin/ as a target
|
||||||
# For example `make bin/frostfs-node` will build only storage node binary
|
# For example `make bin/neofs-node` will build only storage node binary
|
||||||
# Just `make` will build all possible binaries
|
# Just `make` will build all possible binaries
|
||||||
all: $(DIRS) $(BINS)
|
all: $(DIRS) $(BINS)
|
||||||
|
|
||||||
# help target
|
|
||||||
include help.mk
|
|
||||||
|
|
||||||
$(BINS): $(DIRS) dep
|
$(BINS): $(DIRS) dep
|
||||||
@echo "⇒ Build $@"
|
@echo "⇒ Build $@"
|
||||||
CGO_ENABLED=0 \
|
CGO_ENABLED=0 \
|
||||||
|
GO111MODULE=on \
|
||||||
go build -v -trimpath \
|
go build -v -trimpath \
|
||||||
-ldflags "-X $(REPO)/misc.Version=$(VERSION)" \
|
-ldflags "-X $(REPO)/misc.Version=$(VERSION) \
|
||||||
|
-X $(REPO)/misc.Build=$(BUILD) \
|
||||||
|
-X $(REPO)/misc.Debug=$(DEBUG)" \
|
||||||
-o $@ ./cmd/$(notdir $@)
|
-o $@ ./cmd/$(notdir $@)
|
||||||
|
|
||||||
$(DIRS):
|
$(DIRS):
|
||||||
|
@ -50,7 +44,7 @@ $(DIRS):
|
||||||
# Prepare binaries and archives for release
|
# Prepare binaries and archives for release
|
||||||
.ONESHELL:
|
.ONESHELL:
|
||||||
prepare-release: docker/all
|
prepare-release: docker/all
|
||||||
@for file in `ls -1 $(BIN)/frostfs-*`; do
|
@for file in `ls -1 $(BIN)/neofs-*`; do
|
||||||
cp $$file $(RELEASE)/`basename $$file`-$(ARCH)
|
cp $$file $(RELEASE)/`basename $$file`-$(ARCH)
|
||||||
strip $(RELEASE)/`basename $$file`-$(ARCH)
|
strip $(RELEASE)/`basename $$file`-$(ARCH)
|
||||||
tar -czf $(RELEASE)/`basename $$file`-$(ARCH).tar.gz $(RELEASE)/`basename $$file`-$(ARCH)
|
tar -czf $(RELEASE)/`basename $$file`-$(ARCH).tar.gz $(RELEASE)/`basename $$file`-$(ARCH)
|
||||||
|
@ -60,33 +54,32 @@ prepare-release: docker/all
|
||||||
dep:
|
dep:
|
||||||
@printf "⇒ Download requirements: "
|
@printf "⇒ Download requirements: "
|
||||||
CGO_ENABLED=0 \
|
CGO_ENABLED=0 \
|
||||||
|
GO111MODULE=on \
|
||||||
go mod download && echo OK
|
go mod download && echo OK
|
||||||
@printf "⇒ Tidy requirements : "
|
@printf "⇒ Tidy requirements : "
|
||||||
CGO_ENABLED=0 \
|
CGO_ENABLED=0 \
|
||||||
|
GO111MODULE=on \
|
||||||
go mod tidy -v && echo OK
|
go mod tidy -v && echo OK
|
||||||
|
|
||||||
# Regenerate proto files:
|
# Regenerate proto files:
|
||||||
protoc:
|
protoc:
|
||||||
@GOPRIVATE=github.com/TrueCloudLab go mod vendor
|
@GOPRIVATE=github.com/nspcc-dev go mod vendor
|
||||||
# Install specific version for protobuf lib
|
# Install specific version for protobuf lib
|
||||||
@go list -f '{{.Path}}/...@{{.Version}}' -m github.com/golang/protobuf | xargs go install -v
|
@go list -f '{{.Path}}/...@{{.Version}}' -m github.com/golang/protobuf | xargs go install -v
|
||||||
@GOBIN=$(abspath $(BIN)) go install -mod=mod -v git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/util/protogen
|
|
||||||
# Protoc generate
|
# Protoc generate
|
||||||
@for f in `find . -type f -name '*.proto' -not -path './vendor/*'`; do \
|
@for f in `find . -type f -name '*.proto' -not -path './vendor/*'`; do \
|
||||||
echo "⇒ Processing $$f "; \
|
echo "⇒ Processing $$f "; \
|
||||||
protoc \
|
protoc \
|
||||||
--proto_path=.:./vendor:/usr/local/include \
|
--proto_path=.:./vendor:/usr/local/include \
|
||||||
--plugin=protoc-gen-go-frostfs=$(BIN)/protogen \
|
|
||||||
--go-frostfs_out=. --go-frostfs_opt=paths=source_relative \
|
|
||||||
--go_out=. --go_opt=paths=source_relative \
|
--go_out=. --go_opt=paths=source_relative \
|
||||||
--go-grpc_opt=require_unimplemented_servers=false \
|
--go-grpc_opt=require_unimplemented_servers=false \
|
||||||
--go-grpc_out=. --go-grpc_opt=paths=source_relative $$f; \
|
--go-grpc_out=. --go-grpc_opt=paths=source_relative $$f; \
|
||||||
done
|
done
|
||||||
rm -rf vendor
|
rm -rf vendor
|
||||||
|
|
||||||
# Build FrostFS component's docker image
|
# Build NeoFS component's docker image
|
||||||
image-%:
|
image-%:
|
||||||
@echo "⇒ Build FrostFS $* docker image "
|
@echo "⇒ Build NeoFS $* docker image "
|
||||||
@docker build \
|
@docker build \
|
||||||
--build-arg REPO=$(REPO) \
|
--build-arg REPO=$(REPO) \
|
||||||
--build-arg VERSION=$(VERSION) \
|
--build-arg VERSION=$(VERSION) \
|
||||||
|
@ -95,7 +88,7 @@ image-%:
|
||||||
-t $(HUB_IMAGE)-$*:$(HUB_TAG) .
|
-t $(HUB_IMAGE)-$*:$(HUB_TAG) .
|
||||||
|
|
||||||
# Build all Docker images
|
# Build all Docker images
|
||||||
images: image-storage image-ir image-cli image-adm
|
images: image-storage image-ir image-cli image-adm image-storage-testnet
|
||||||
|
|
||||||
# Build dirty local Docker images
|
# Build dirty local Docker images
|
||||||
dirty-images: image-dirty-storage image-dirty-ir image-dirty-cli image-dirty-adm
|
dirty-images: image-dirty-storage image-dirty-ir image-dirty-cli image-dirty-adm
|
||||||
|
@ -116,68 +109,45 @@ fmts: fmt imports
|
||||||
# Reformat code
|
# Reformat code
|
||||||
fmt:
|
fmt:
|
||||||
@echo "⇒ Processing gofmt check"
|
@echo "⇒ Processing gofmt check"
|
||||||
@gofmt -s -w cmd/ pkg/ misc/
|
@GO111MODULE=on gofmt -s -w cmd/ pkg/ misc/
|
||||||
|
|
||||||
# Reformat imports
|
# Reformat imports
|
||||||
imports:
|
imports:
|
||||||
@echo "⇒ Processing goimports check"
|
@echo "⇒ Processing goimports check"
|
||||||
@goimports -w cmd/ pkg/ misc/
|
@GO111MODULE=on goimports -w cmd/ pkg/ misc/
|
||||||
|
|
||||||
# Run Unit Test with go test
|
# Run Unit Test with go test
|
||||||
test:
|
test:
|
||||||
@echo "⇒ Running go test"
|
@echo "⇒ Running go test"
|
||||||
@go test ./... -count=1
|
@GO111MODULE=on go test ./...
|
||||||
|
|
||||||
pre-commit-run:
|
|
||||||
@pre-commit run -a --hook-stage manual
|
|
||||||
|
|
||||||
# Run linters
|
# Run linters
|
||||||
lint:
|
lint:
|
||||||
@golangci-lint --timeout=5m run
|
@golangci-lint --timeout=5m run
|
||||||
|
|
||||||
# Install staticcheck
|
|
||||||
staticcheck-install:
|
|
||||||
@go install honnef.co/go/tools/cmd/staticcheck@latest
|
|
||||||
|
|
||||||
# Run staticcheck
|
|
||||||
staticcheck-run:
|
|
||||||
@staticcheck ./...
|
|
||||||
|
|
||||||
# Run linters in Docker
|
# Run linters in Docker
|
||||||
docker/lint:
|
docker/lint:
|
||||||
docker run --rm -t \
|
docker run --rm -t \
|
||||||
-v `pwd`:/src \
|
-v `pwd`:/src \
|
||||||
-u `stat -c "%u:%g" .` \
|
-u `stat -c "%u:%g" .` \
|
||||||
--env HOME=/src \
|
--env HOME=/src \
|
||||||
golangci/golangci-lint:v$(LINT_VERSION) bash -c 'cd /src/ && make lint'
|
golangci/golangci-lint:v1.42.1 bash -c 'cd /src/ && make lint'
|
||||||
|
|
||||||
# Activate pre-commit hooks
|
|
||||||
pre-commit:
|
|
||||||
pre-commit install -t pre-commit -t commit-msg
|
|
||||||
|
|
||||||
# Deactivate pre-commit hooks
|
|
||||||
unpre-commit:
|
|
||||||
pre-commit uninstall -t pre-commit -t commit-msg
|
|
||||||
|
|
||||||
# Print version
|
# Print version
|
||||||
version:
|
version:
|
||||||
@echo $(VERSION)
|
@echo $(VERSION)
|
||||||
|
|
||||||
# Delete built artifacts
|
# Show this help prompt
|
||||||
|
help:
|
||||||
|
@echo ' Usage:'
|
||||||
|
@echo ''
|
||||||
|
@echo ' make <target>'
|
||||||
|
@echo ''
|
||||||
|
@echo ' Targets:'
|
||||||
|
@echo ''
|
||||||
|
@awk '/^#/{ comment = substr($$0,3) } comment && /^[a-zA-Z][a-zA-Z0-9_-]+ ?:/{ print " ", $$1, comment }' $(MAKEFILE_LIST) | column -t -s ':' | grep -v 'IGNORE' | sort -u
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf vendor
|
rm -rf vendor
|
||||||
rm -rf .cache
|
|
||||||
rm -rf $(BIN)
|
rm -rf $(BIN)
|
||||||
rm -rf $(RELEASE)
|
rm -rf $(RELEASE)
|
||||||
|
|
||||||
# Package for Debian
|
|
||||||
debpackage:
|
|
||||||
dch -b --package frostfs-node \
|
|
||||||
--controlmaint \
|
|
||||||
--newversion $(PKG_VERSION) \
|
|
||||||
--distribution $(OS_RELEASE) \
|
|
||||||
"Please see CHANGELOG.md for code changes for $(VERSION)"
|
|
||||||
dpkg-buildpackage --no-sign -b
|
|
||||||
|
|
||||||
debclean:
|
|
||||||
dh clean
|
|
||||||
|
|
66
README.md
66
README.md
|
@ -1,80 +1,50 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./.github/logo.svg" width="500px" alt="FrostFS">
|
<img src="./.github/logo.svg" width="500px" alt="NeoFS">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://frostfs.info">FrostFS</a> is a decentralized distributed object storage integrated with the <a href="https://neo.org">NEO Blockchain</a>.
|
<a href="https://fs.neo.org">NeoFS</a> is a decentralized distributed object storage integrated with the <a href="https://neo.org">NEO Blockchain</a>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
[![Report](https://goreportcard.com/badge/github.com/TrueCloudLab/frostfs-node)](https://goreportcard.com/report/github.com/TrueCloudLab/frostfs-node)
|
[![Report](https://goreportcard.com/badge/github.com/nspcc-dev/neofs-node)](https://goreportcard.com/report/github.com/nspcc-dev/neofs-node)
|
||||||
![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/TrueCloudLab/frostfs-node?sort=semver)
|
![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/nspcc-dev/neofs-node?sort=semver)
|
||||||
![License](https://img.shields.io/github/license/TrueCloudLab/frostfs-node.svg?style=popout)
|
![License](https://img.shields.io/github/license/nspcc-dev/neofs-node.svg?style=popout)
|
||||||
|
|
||||||
# Overview
|
# Overview
|
||||||
|
|
||||||
FrostFS Nodes are organized in a peer-to-peer network that takes care of storing
|
NeoFS Nodes are organized in a peer-to-peer network that takes care of storing
|
||||||
and distributing user's data. Any Neo user may participate in the network and
|
and distributing user's data. Any Neo user may participate in the network and
|
||||||
get paid for providing storage resources to other users or store their data in
|
get paid for providing storage resources to other users or store his data in
|
||||||
FrostFS and pay a competitive price for it.
|
NeoFS and pay a competitive price for it.
|
||||||
|
|
||||||
Users can reliably store object data in the FrostFS network and have a transparent
|
Users can reliably store object data in the NeoFS network and have a transparent
|
||||||
data placement process due to a decentralized architecture and flexible storage
|
data placement process due to a decentralized architecture and flexible storage
|
||||||
policies. Each node is responsible for executing the storage policies that the
|
policies. Each node is responsible for executing the storage policies that the
|
||||||
users select for geographical location, reliability level, number of nodes, type
|
users select for geographical location, reliability level, number of nodes, type
|
||||||
of disks, capacity, etc. Thus, FrostFS gives full control over data to users.
|
of disks, capacity, etc. Thus, NeoFS gives full control over data to users.
|
||||||
|
|
||||||
Deep [Neo Blockchain](https://neo.org) integration allows FrostFS to be used by
|
Deep [Neo Blockchain](https://neo.org) integration allows NeoFS to be used by
|
||||||
dApps directly from
|
dApps directly from
|
||||||
[NeoVM](https://docs.neo.org/docs/en-us/basic/technology/neovm.html) on the
|
[NeoVM](https://docs.neo.org/docs/en-us/basic/technology/neovm.html) on the
|
||||||
[Smart Contract](https://docs.neo.org/docs/en-us/intro/glossary.html)
|
[Smart Contract](https://docs.neo.org/docs/en-us/intro/glossary.html)
|
||||||
code level. This way dApps are not limited to on-chain storage and can
|
code level. This way dApps are not limited to on-chain storage and can
|
||||||
manipulate large amounts of data without paying a prohibitive price.
|
manipulate large amounts of data without paying a prohibitive price.
|
||||||
|
|
||||||
FrostFS has a native [gRPC API](https://git.frostfs.info/TrueCloudLab/frostfs-api) and has
|
NeoFS has a native [gRPC API](https://github.com/nspcc-dev/neofs-api) and has
|
||||||
protocol gateways for popular protocols such as [AWS
|
protocol gateways for popular protocols such as [AWS
|
||||||
S3](https://github.com/TrueCloudLab/frostfs-s3-gw),
|
S3](https://github.com/nspcc-dev/neofs-s3-gw),
|
||||||
[HTTP](https://github.com/TrueCloudLab/frostfs-http-gw),
|
[HTTP](https://github.com/nspcc-dev/neofs-http-gw),
|
||||||
[FUSE](https://wikipedia.org/wiki/Filesystem_in_Userspace) and
|
[FUSE](https://wikipedia.org/wiki/Filesystem_in_Userspace) and
|
||||||
[sFTP](https://en.wikipedia.org/wiki/SSH_File_Transfer_Protocol) allowing
|
[sFTP](https://en.wikipedia.org/wiki/SSH_File_Transfer_Protocol) allowing
|
||||||
developers to integrate applications without rewriting their code.
|
developers to integrate applications without rewriting their code.
|
||||||
|
|
||||||
# Supported platforms
|
# Supported platforms
|
||||||
|
|
||||||
Now, we only support GNU/Linux on amd64 CPUs with AVX/AVX2 instructions. More
|
For now we only support GNU/Linux on amd64 CPUs with AVX/AVX2 instructions. More
|
||||||
platforms will be officially supported after release `1.0`.
|
platforms will be officially supported after release `1.0`.
|
||||||
|
|
||||||
The latest version of frostfs-node works with frostfs-contract
|
The latest version of neofs-node works with neofs-contract
|
||||||
[v0.16.0](https://github.com/TrueCloudLab/frostfs-contract/releases/tag/v0.16.0).
|
[v0.13.0](https://github.com/nspcc-dev/neofs-contract/releases/tag/v0.13.0).
|
||||||
|
|
||||||
# Building
|
|
||||||
|
|
||||||
To make all binaries you need Go 1.19+ and `make`:
|
|
||||||
```
|
|
||||||
make all
|
|
||||||
```
|
|
||||||
The resulting binaries will appear in `bin/` folder.
|
|
||||||
|
|
||||||
To make a specific binary use:
|
|
||||||
```
|
|
||||||
make bin/frostfs-<name>
|
|
||||||
```
|
|
||||||
See the list of all available commands in the `cmd` folder.
|
|
||||||
|
|
||||||
## Building with Docker
|
|
||||||
|
|
||||||
Building can also be performed in a container:
|
|
||||||
```
|
|
||||||
make docker/all # build all binaries
|
|
||||||
make docker/bin/frostfs-<name> # build a specific binary
|
|
||||||
```
|
|
||||||
|
|
||||||
## Docker images
|
|
||||||
|
|
||||||
To make docker images suitable for use in [frostfs-dev-env](https://github.com/TrueCloudLab/frostfs-dev-env/) use:
|
|
||||||
```
|
|
||||||
make images
|
|
||||||
```
|
|
||||||
|
|
||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
|
@ -86,7 +56,7 @@ the feature/topic you are going to implement.
|
||||||
|
|
||||||
# Credits
|
# Credits
|
||||||
|
|
||||||
FrostFS is maintained by [True Cloud Lab](https://github.com/TrueCloudLab/) with the help and
|
NeoFS is maintained by [NeoSPCC](https://nspcc.ru) with the help and
|
||||||
contributions from community members.
|
contributions from community members.
|
||||||
|
|
||||||
Please see [CREDITS](CREDITS.md) for details.
|
Please see [CREDITS](CREDITS.md) for details.
|
||||||
|
|
1
VERSION
1
VERSION
|
@ -1 +0,0 @@
|
||||||
v0.36.0
|
|
|
@ -1,14 +0,0 @@
|
||||||
package commonflags
|
|
||||||
|
|
||||||
const (
|
|
||||||
ConfigFlag = "config"
|
|
||||||
ConfigFlagShorthand = "c"
|
|
||||||
ConfigFlagUsage = "Config file"
|
|
||||||
|
|
||||||
ConfigDirFlag = "config-dir"
|
|
||||||
ConfigDirFlagUsage = "Config directory"
|
|
||||||
|
|
||||||
Verbose = "verbose"
|
|
||||||
VerboseShorthand = "v"
|
|
||||||
VerboseUsage = "Verbose output"
|
|
||||||
)
|
|
|
@ -1,29 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
const configPathFlag = "path"
|
|
||||||
|
|
||||||
var (
|
|
||||||
// RootCmd is a root command of config section.
|
|
||||||
RootCmd = &cobra.Command{
|
|
||||||
Use: "config",
|
|
||||||
Short: "Section for frostfs-adm config related commands",
|
|
||||||
}
|
|
||||||
|
|
||||||
initCmd = &cobra.Command{
|
|
||||||
Use: "init",
|
|
||||||
Short: "Initialize basic frostfs-adm configuration file",
|
|
||||||
Example: `frostfs-adm config init
|
|
||||||
frostfs-adm config init --path .config/frostfs-adm.yml`,
|
|
||||||
RunE: initConfig,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
RootCmd.AddCommand(initCmd)
|
|
||||||
|
|
||||||
initCmd.Flags().String(configPathFlag, "", "Path to config (default ~/.frostfs/adm/config.yml)")
|
|
||||||
}
|
|
|
@ -1,242 +0,0 @@
|
||||||
package morph
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/elliptic"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-contract/nns"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/gas"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/rolemgmt"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
type accBalancePair struct {
|
|
||||||
scriptHash util.Uint160
|
|
||||||
balance *big.Int
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
dumpBalancesStorageFlag = "storage"
|
|
||||||
dumpBalancesAlphabetFlag = "alphabet"
|
|
||||||
dumpBalancesProxyFlag = "proxy"
|
|
||||||
dumpBalancesUseScriptHashFlag = "script-hash"
|
|
||||||
)
|
|
||||||
|
|
||||||
func dumpBalances(cmd *cobra.Command, _ []string) error {
|
|
||||||
var (
|
|
||||||
dumpStorage, _ = cmd.Flags().GetBool(dumpBalancesStorageFlag)
|
|
||||||
dumpAlphabet, _ = cmd.Flags().GetBool(dumpBalancesAlphabetFlag)
|
|
||||||
dumpProxy, _ = cmd.Flags().GetBool(dumpBalancesProxyFlag)
|
|
||||||
nnsCs *state.Contract
|
|
||||||
nmHash util.Uint160
|
|
||||||
)
|
|
||||||
|
|
||||||
c, err := getN3Client(viper.GetViper())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
inv := invoker.New(c, nil)
|
|
||||||
|
|
||||||
if dumpStorage || dumpAlphabet || dumpProxy {
|
|
||||||
nnsCs, err = c.GetContractStateByID(1)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't get NNS contract info: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
nmHash, err = nnsResolveHash(inv, nnsCs.Hash, netmapContract+".frostfs")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't get netmap contract hash: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
irList, err := fetchIRNodes(c, rolemgmt.Hash)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := fetchBalances(inv, gas.Hash, irList); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
printBalances(cmd, "Inner ring nodes balances:", irList)
|
|
||||||
|
|
||||||
if dumpStorage {
|
|
||||||
if err := printStorageNodeBalances(cmd, inv, nmHash); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if dumpProxy {
|
|
||||||
if err := printProxyContractBalance(cmd, inv, nnsCs.Hash); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if dumpAlphabet {
|
|
||||||
if err := printAlphabetContractBalances(cmd, c, inv, len(irList), nnsCs.Hash); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func printStorageNodeBalances(cmd *cobra.Command, inv *invoker.Invoker, nmHash util.Uint160) error {
|
|
||||||
arr, err := unwrap.Array(inv.Call(nmHash, "netmap"))
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("can't fetch the list of storage nodes")
|
|
||||||
}
|
|
||||||
|
|
||||||
snList := make([]accBalancePair, len(arr))
|
|
||||||
for i := range arr {
|
|
||||||
node, ok := arr[i].Value().([]stackitem.Item)
|
|
||||||
if !ok || len(node) == 0 {
|
|
||||||
return errors.New("can't parse the list of storage nodes")
|
|
||||||
}
|
|
||||||
bs, err := node[0].TryBytes()
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("can't parse the list of storage nodes")
|
|
||||||
}
|
|
||||||
var ni netmap.NodeInfo
|
|
||||||
if err := ni.Unmarshal(bs); err != nil {
|
|
||||||
return fmt.Errorf("can't parse the list of storage nodes: %w", err)
|
|
||||||
}
|
|
||||||
pub, err := keys.NewPublicKeyFromBytes(ni.PublicKey(), elliptic.P256())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't parse storage node public key: %w", err)
|
|
||||||
}
|
|
||||||
snList[i].scriptHash = pub.GetScriptHash()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := fetchBalances(inv, gas.Hash, snList); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
printBalances(cmd, "\nStorage node balances:", snList)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func printProxyContractBalance(cmd *cobra.Command, inv *invoker.Invoker, nnsHash util.Uint160) error {
|
|
||||||
h, err := nnsResolveHash(inv, nnsHash, proxyContract+".frostfs")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't get hash of the proxy contract: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyList := []accBalancePair{{scriptHash: h}}
|
|
||||||
if err := fetchBalances(inv, gas.Hash, proxyList); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
printBalances(cmd, "\nProxy contract balance:", proxyList)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func printAlphabetContractBalances(cmd *cobra.Command, c Client, inv *invoker.Invoker, count int, nnsHash util.Uint160) error {
|
|
||||||
alphaList := make([]accBalancePair, count)
|
|
||||||
|
|
||||||
w := io.NewBufBinWriter()
|
|
||||||
for i := range alphaList {
|
|
||||||
emit.AppCall(w.BinWriter, nnsHash, "resolve", callflag.ReadOnly,
|
|
||||||
getAlphabetNNSDomain(i),
|
|
||||||
int64(nns.TXT))
|
|
||||||
}
|
|
||||||
if w.Err != nil {
|
|
||||||
panic(w.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
alphaRes, err := c.InvokeScript(w.Bytes(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't fetch info from NNS: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range alphaList {
|
|
||||||
h, err := parseNNSResolveResult(alphaRes.Stack[i])
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't fetch the alphabet contract #%d hash: %w", i, err)
|
|
||||||
}
|
|
||||||
alphaList[i].scriptHash = h
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := fetchBalances(inv, gas.Hash, alphaList); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
printBalances(cmd, "\nAlphabet contracts balances:", alphaList)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchIRNodes(c Client, desigHash util.Uint160) ([]accBalancePair, error) {
|
|
||||||
inv := invoker.New(c, nil)
|
|
||||||
|
|
||||||
height, err := c.GetBlockCount()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't get block height: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
arr, err := getDesignatedByRole(inv, desigHash, noderoles.NeoFSAlphabet, height)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("can't fetch list of IR nodes from the netmap contract")
|
|
||||||
}
|
|
||||||
|
|
||||||
irList := make([]accBalancePair, len(arr))
|
|
||||||
for i := range arr {
|
|
||||||
irList[i].scriptHash = arr[i].GetScriptHash()
|
|
||||||
}
|
|
||||||
return irList, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func printBalances(cmd *cobra.Command, prefix string, accounts []accBalancePair) {
|
|
||||||
useScriptHash, _ := cmd.Flags().GetBool(dumpBalancesUseScriptHashFlag)
|
|
||||||
|
|
||||||
cmd.Println(prefix)
|
|
||||||
for i := range accounts {
|
|
||||||
var addr string
|
|
||||||
if useScriptHash {
|
|
||||||
addr = accounts[i].scriptHash.StringLE()
|
|
||||||
} else {
|
|
||||||
addr = address.Uint160ToString(accounts[i].scriptHash)
|
|
||||||
}
|
|
||||||
cmd.Printf("%s: %s\n", addr, fixedn.ToString(accounts[i].balance, 8))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchBalances(c *invoker.Invoker, gasHash util.Uint160, accounts []accBalancePair) error {
|
|
||||||
w := io.NewBufBinWriter()
|
|
||||||
for i := range accounts {
|
|
||||||
emit.AppCall(w.BinWriter, gasHash, "balanceOf", callflag.ReadStates, accounts[i].scriptHash)
|
|
||||||
}
|
|
||||||
if w.Err != nil {
|
|
||||||
panic(w.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := c.Run(w.Bytes())
|
|
||||||
if err != nil || res.State != vmstate.Halt.String() || len(res.Stack) != len(accounts) {
|
|
||||||
return errors.New("can't fetch account balances")
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range accounts {
|
|
||||||
bal, err := res.Stack[i].TryInteger()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't parse account balance: %w", err)
|
|
||||||
}
|
|
||||||
accounts[i].balance = bal
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,166 +0,0 @@
|
||||||
package morph
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"text/tabwriter"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/netmap"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
const forceConfigSet = "force"
|
|
||||||
|
|
||||||
func dumpNetworkConfig(cmd *cobra.Command, _ []string) error {
|
|
||||||
c, err := getN3Client(viper.GetViper())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't create N3 client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
inv := invoker.New(c, nil)
|
|
||||||
|
|
||||||
cs, err := c.GetContractStateByID(1)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't get NNS contract info: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
nmHash, err := nnsResolveHash(inv, cs.Hash, netmapContract+".frostfs")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't get netmap contract hash: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
arr, err := unwrap.Array(inv.Call(nmHash, "listConfig"))
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("can't fetch list of network config keys from the netmap contract")
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := bytes.NewBuffer(nil)
|
|
||||||
tw := tabwriter.NewWriter(buf, 0, 2, 2, ' ', 0)
|
|
||||||
|
|
||||||
m, err := parseConfigFromNetmapContract(arr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for k, v := range m {
|
|
||||||
switch k {
|
|
||||||
case netmap.AuditFeeConfig, netmap.BasicIncomeRateConfig,
|
|
||||||
netmap.ContainerFeeConfig, netmap.ContainerAliasFeeConfig,
|
|
||||||
netmap.EpochDurationConfig, netmap.IrCandidateFeeConfig,
|
|
||||||
netmap.MaxObjectSizeConfig, netmap.WithdrawFeeConfig:
|
|
||||||
nbuf := make([]byte, 8)
|
|
||||||
copy(nbuf[:], v)
|
|
||||||
n := binary.LittleEndian.Uint64(nbuf)
|
|
||||||
_, _ = tw.Write([]byte(fmt.Sprintf("%s:\t%d (int)\n", k, n)))
|
|
||||||
case netmap.HomomorphicHashingDisabledKey, netmap.MaintenanceModeAllowedConfig:
|
|
||||||
if len(v) == 0 || len(v) > 1 {
|
|
||||||
return invalidConfigValueErr(k)
|
|
||||||
}
|
|
||||||
_, _ = tw.Write([]byte(fmt.Sprintf("%s:\t%t (bool)\n", k, v[0] == 1)))
|
|
||||||
default:
|
|
||||||
_, _ = tw.Write([]byte(fmt.Sprintf("%s:\t%s (hex)\n", k, hex.EncodeToString(v))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = tw.Flush()
|
|
||||||
cmd.Print(buf.String())
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setConfigCmd(cmd *cobra.Command, args []string) error {
|
|
||||||
if len(args) == 0 {
|
|
||||||
return errors.New("empty config pairs")
|
|
||||||
}
|
|
||||||
|
|
||||||
wCtx, err := newInitializeContext(cmd, viper.GetViper())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't initialize context: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cs, err := wCtx.Client.GetContractStateByID(1)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't get NNS contract info: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
nmHash, err := nnsResolveHash(wCtx.ReadOnlyInvoker, cs.Hash, netmapContract+".frostfs")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't get netmap contract hash: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
forceFlag, _ := cmd.Flags().GetBool(forceConfigSet)
|
|
||||||
|
|
||||||
bw := io.NewBufBinWriter()
|
|
||||||
for _, arg := range args {
|
|
||||||
k, v, err := parseConfigPair(arg, forceFlag)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// In NeoFS this is done via Notary contract. Here, however, we can form the
|
|
||||||
// transaction locally. The first `nil` argument is required only for notary
|
|
||||||
// disabled environment which is not supported by that command.
|
|
||||||
emit.AppCall(bw.BinWriter, nmHash, "setConfig", callflag.All, nil, k, v)
|
|
||||||
if bw.Err != nil {
|
|
||||||
return fmt.Errorf("can't form raw transaction: %w", bw.Err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = wCtx.sendConsensusTx(bw.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return wCtx.awaitTx()
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseConfigPair(kvStr string, force bool) (key string, val any, err error) {
|
|
||||||
k, v, found := strings.Cut(kvStr, "=")
|
|
||||||
if !found {
|
|
||||||
return "", nil, fmt.Errorf("invalid parameter format: must be 'key=val', got: %s", kvStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
key = k
|
|
||||||
valRaw := v
|
|
||||||
|
|
||||||
switch key {
|
|
||||||
case netmap.AuditFeeConfig, netmap.BasicIncomeRateConfig,
|
|
||||||
netmap.ContainerFeeConfig, netmap.ContainerAliasFeeConfig,
|
|
||||||
netmap.EpochDurationConfig, netmap.IrCandidateFeeConfig,
|
|
||||||
netmap.MaxObjectSizeConfig, netmap.WithdrawFeeConfig:
|
|
||||||
val, err = strconv.ParseInt(valRaw, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("could not parse %s's value '%s' as int: %w", key, valRaw, err)
|
|
||||||
}
|
|
||||||
case netmap.HomomorphicHashingDisabledKey, netmap.MaintenanceModeAllowedConfig:
|
|
||||||
val, err = strconv.ParseBool(valRaw)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("could not parse %s's value '%s' as bool: %w", key, valRaw, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
if !force {
|
|
||||||
return "", nil, fmt.Errorf(
|
|
||||||
"'%s' key is not well-known, use '--%s' flag if want to set it anyway",
|
|
||||||
key, forceConfigSet)
|
|
||||||
}
|
|
||||||
|
|
||||||
val = valRaw
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func invalidConfigValueErr(key string) error {
|
|
||||||
return fmt.Errorf("invalid %s config value from netmap contract", key)
|
|
||||||
}
|
|
|
@ -1,447 +0,0 @@
|
||||||
package morph
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errInvalidContainerResponse = errors.New("invalid response from container contract")
|
|
||||||
|
|
||||||
func getContainerContractHash(cmd *cobra.Command, inv *invoker.Invoker, c Client) (util.Uint160, error) {
|
|
||||||
s, err := cmd.Flags().GetString(containerContractFlag)
|
|
||||||
var ch util.Uint160
|
|
||||||
if err == nil {
|
|
||||||
ch, err = util.Uint160DecodeStringLE(s)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
nnsCs, err := c.GetContractStateByID(1)
|
|
||||||
if err != nil {
|
|
||||||
return util.Uint160{}, fmt.Errorf("can't get NNS contract state: %w", err)
|
|
||||||
}
|
|
||||||
ch, err = nnsResolveHash(inv, nnsCs.Hash, containerContract+".frostfs")
|
|
||||||
if err != nil {
|
|
||||||
return util.Uint160{}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ch, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func iterateContainerList(inv *invoker.Invoker, ch util.Uint160, f func([]byte) error) error {
|
|
||||||
sid, r, err := unwrap.SessionIterator(inv.Call(ch, "containersOf", ""))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%w: %v", errInvalidContainerResponse, err)
|
|
||||||
}
|
|
||||||
// Nothing bad, except live session on the server, do not report to the user.
|
|
||||||
defer func() { _ = inv.TerminateSession(sid) }()
|
|
||||||
|
|
||||||
items, err := inv.TraverseIterator(sid, &r, 0)
|
|
||||||
for err == nil && len(items) != 0 {
|
|
||||||
for j := range items {
|
|
||||||
b, err := items[j].TryBytes()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%w: %v", errInvalidContainerResponse, err)
|
|
||||||
}
|
|
||||||
if err := f(b); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items, err = inv.TraverseIterator(sid, &r, 0)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func dumpContainers(cmd *cobra.Command, _ []string) error {
|
|
||||||
filename, err := cmd.Flags().GetString(containerDumpFlag)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid filename: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := getN3Client(viper.GetViper())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't create N3 client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
inv := invoker.New(c, nil)
|
|
||||||
|
|
||||||
ch, err := getContainerContractHash(cmd, inv, c)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to get contaract hash: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
isOK, err := getCIDFilterFunc(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.OpenFile(filename, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0o660)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
_, err = f.Write([]byte{'['})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
written := 0
|
|
||||||
enc := json.NewEncoder(f)
|
|
||||||
bw := io.NewBufBinWriter()
|
|
||||||
iterErr := iterateContainerList(inv, ch, func(id []byte) error {
|
|
||||||
if !isOK(id) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cnt, err := dumpSingleContainer(bw, ch, inv, id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Writing directly to the file is ok, because json.Encoder does no internal buffering.
|
|
||||||
if written != 0 {
|
|
||||||
_, err = f.Write([]byte{','})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
written++
|
|
||||||
return enc.Encode(cnt)
|
|
||||||
})
|
|
||||||
if iterErr != nil {
|
|
||||||
return iterErr
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = f.Write([]byte{']'})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func dumpSingleContainer(bw *io.BufBinWriter, ch util.Uint160, inv *invoker.Invoker, id []byte) (*Container, error) {
|
|
||||||
bw.Reset()
|
|
||||||
emit.AppCall(bw.BinWriter, ch, "get", callflag.All, id)
|
|
||||||
emit.AppCall(bw.BinWriter, ch, "eACL", callflag.All, id)
|
|
||||||
res, err := inv.Run(bw.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't get container info: %w", err)
|
|
||||||
}
|
|
||||||
if len(res.Stack) != 2 {
|
|
||||||
return nil, fmt.Errorf("%w: expected 2 items on stack", errInvalidContainerResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
cnt := new(Container)
|
|
||||||
err = cnt.FromStackItem(res.Stack[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%w: %v", errInvalidContainerResponse, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ea := new(EACL)
|
|
||||||
err = ea.FromStackItem(res.Stack[1])
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%w: %v", errInvalidContainerResponse, err)
|
|
||||||
}
|
|
||||||
if len(ea.Value) != 0 {
|
|
||||||
cnt.EACL = ea
|
|
||||||
}
|
|
||||||
return cnt, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func listContainers(cmd *cobra.Command, _ []string) error {
|
|
||||||
c, err := getN3Client(viper.GetViper())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't create N3 client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
inv := invoker.New(c, nil)
|
|
||||||
|
|
||||||
ch, err := getContainerContractHash(cmd, inv, c)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to get contaract hash: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return iterateContainerList(inv, ch, func(id []byte) error {
|
|
||||||
var idCnr cid.ID
|
|
||||||
err = idCnr.Decode(id)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to decode container id: %w", err)
|
|
||||||
}
|
|
||||||
cmd.Println(idCnr)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func restoreContainers(cmd *cobra.Command, _ []string) error {
|
|
||||||
filename, err := cmd.Flags().GetString(containerDumpFlag)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid filename: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
wCtx, err := newInitializeContext(cmd, viper.GetViper())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer wCtx.close()
|
|
||||||
|
|
||||||
containers, err := parseContainers(filename)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ch, err := fetchContainerContractHash(wCtx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
isOK, err := getCIDFilterFunc(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = restoreOrPutContainers(containers, isOK, cmd, wCtx, ch)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return wCtx.awaitTx()
|
|
||||||
}
|
|
||||||
|
|
||||||
func restoreOrPutContainers(containers []Container, isOK func([]byte) bool, cmd *cobra.Command, wCtx *initializeContext, ch util.Uint160) error {
|
|
||||||
bw := io.NewBufBinWriter()
|
|
||||||
for _, cnt := range containers {
|
|
||||||
hv := hash.Sha256(cnt.Value)
|
|
||||||
if !isOK(hv[:]) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
bw.Reset()
|
|
||||||
restored, err := isContainerRestored(cmd, wCtx, ch, bw, hv)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if restored {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
bw.Reset()
|
|
||||||
|
|
||||||
putContainer(bw, ch, cnt)
|
|
||||||
|
|
||||||
if bw.Err != nil {
|
|
||||||
panic(bw.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := wCtx.sendConsensusTx(bw.Bytes()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func putContainer(bw *io.BufBinWriter, ch util.Uint160, cnt Container) {
|
|
||||||
emit.AppCall(bw.BinWriter, ch, "put", callflag.All,
|
|
||||||
cnt.Value, cnt.Signature, cnt.PublicKey, cnt.Token)
|
|
||||||
if ea := cnt.EACL; ea != nil {
|
|
||||||
emit.AppCall(bw.BinWriter, ch, "setEACL", callflag.All,
|
|
||||||
ea.Value, ea.Signature, ea.PublicKey, ea.Token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isContainerRestored(cmd *cobra.Command, wCtx *initializeContext, containerHash util.Uint160, bw *io.BufBinWriter, hashValue util.Uint256) (bool, error) {
|
|
||||||
emit.AppCall(bw.BinWriter, containerHash, "get", callflag.All, hashValue.BytesBE())
|
|
||||||
res, err := wCtx.Client.InvokeScript(bw.Bytes(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("can't check if container is already restored: %w", err)
|
|
||||||
}
|
|
||||||
if len(res.Stack) == 0 {
|
|
||||||
return false, errors.New("empty stack")
|
|
||||||
}
|
|
||||||
|
|
||||||
old := new(Container)
|
|
||||||
if err := old.FromStackItem(res.Stack[0]); err != nil {
|
|
||||||
return false, fmt.Errorf("%w: %v", errInvalidContainerResponse, err)
|
|
||||||
}
|
|
||||||
if len(old.Value) != 0 {
|
|
||||||
var id cid.ID
|
|
||||||
id.SetSHA256(hashValue)
|
|
||||||
cmd.Printf("Container %s is already deployed.\n", id)
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseContainers(filename string) ([]Container, error) {
|
|
||||||
data, err := os.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't read dump file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var containers []Container
|
|
||||||
err = json.Unmarshal(data, &containers)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't parse dump file: %w", err)
|
|
||||||
}
|
|
||||||
return containers, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchContainerContractHash(wCtx *initializeContext) (util.Uint160, error) {
|
|
||||||
nnsCs, err := wCtx.Client.GetContractStateByID(1)
|
|
||||||
if err != nil {
|
|
||||||
return util.Uint160{}, fmt.Errorf("can't get NNS contract state: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ch, err := nnsResolveHash(wCtx.ReadOnlyInvoker, nnsCs.Hash, containerContract+".frostfs")
|
|
||||||
if err != nil {
|
|
||||||
return util.Uint160{}, fmt.Errorf("can't fetch container contract hash: %w", err)
|
|
||||||
}
|
|
||||||
return ch, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Container represents container struct in contract storage.
|
|
||||||
type Container struct {
|
|
||||||
Value []byte `json:"value"`
|
|
||||||
Signature []byte `json:"signature"`
|
|
||||||
PublicKey []byte `json:"public_key"`
|
|
||||||
Token []byte `json:"token"`
|
|
||||||
EACL *EACL `json:"eacl"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// EACL represents extended ACL struct in contract storage.
|
|
||||||
type EACL struct {
|
|
||||||
Value []byte `json:"value"`
|
|
||||||
Signature []byte `json:"signature"`
|
|
||||||
PublicKey []byte `json:"public_key"`
|
|
||||||
Token []byte `json:"token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToStackItem implements stackitem.Convertible.
|
|
||||||
func (c *Container) ToStackItem() (stackitem.Item, error) {
|
|
||||||
return stackitem.NewStruct([]stackitem.Item{
|
|
||||||
stackitem.NewByteArray(c.Value),
|
|
||||||
stackitem.NewByteArray(c.Signature),
|
|
||||||
stackitem.NewByteArray(c.PublicKey),
|
|
||||||
stackitem.NewByteArray(c.Token),
|
|
||||||
}), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromStackItem implements stackitem.Convertible.
|
|
||||||
func (c *Container) FromStackItem(item stackitem.Item) error {
|
|
||||||
arr, ok := item.Value().([]stackitem.Item)
|
|
||||||
if !ok || len(arr) != 4 {
|
|
||||||
return errors.New("invalid stack item type")
|
|
||||||
}
|
|
||||||
|
|
||||||
value, err := arr[0].TryBytes()
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("invalid container value")
|
|
||||||
}
|
|
||||||
|
|
||||||
sig, err := arr[1].TryBytes()
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("invalid container signature")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub, err := arr[2].TryBytes()
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("invalid container public key")
|
|
||||||
}
|
|
||||||
|
|
||||||
tok, err := arr[3].TryBytes()
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("invalid container token")
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Value = value
|
|
||||||
c.Signature = sig
|
|
||||||
c.PublicKey = pub
|
|
||||||
c.Token = tok
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToStackItem implements stackitem.Convertible.
|
|
||||||
func (c *EACL) ToStackItem() (stackitem.Item, error) {
|
|
||||||
return stackitem.NewStruct([]stackitem.Item{
|
|
||||||
stackitem.NewByteArray(c.Value),
|
|
||||||
stackitem.NewByteArray(c.Signature),
|
|
||||||
stackitem.NewByteArray(c.PublicKey),
|
|
||||||
stackitem.NewByteArray(c.Token),
|
|
||||||
}), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromStackItem implements stackitem.Convertible.
|
|
||||||
func (c *EACL) FromStackItem(item stackitem.Item) error {
|
|
||||||
arr, ok := item.Value().([]stackitem.Item)
|
|
||||||
if !ok || len(arr) != 4 {
|
|
||||||
return errors.New("invalid stack item type")
|
|
||||||
}
|
|
||||||
|
|
||||||
value, err := arr[0].TryBytes()
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("invalid eACL value")
|
|
||||||
}
|
|
||||||
|
|
||||||
sig, err := arr[1].TryBytes()
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("invalid eACL signature")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub, err := arr[2].TryBytes()
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("invalid eACL public key")
|
|
||||||
}
|
|
||||||
|
|
||||||
tok, err := arr[3].TryBytes()
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("invalid eACL token")
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Value = value
|
|
||||||
c.Signature = sig
|
|
||||||
c.PublicKey = pub
|
|
||||||
c.Token = tok
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getCIDFilterFunc returns filtering function for container IDs.
|
|
||||||
// Raw byte slices are used because it works with structures returned
|
|
||||||
// from contract.
|
|
||||||
func getCIDFilterFunc(cmd *cobra.Command) (func([]byte) bool, error) {
|
|
||||||
rawIDs, err := cmd.Flags().GetStringSlice(containerIDsFlag)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(rawIDs) == 0 {
|
|
||||||
return func([]byte) bool { return true }, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range rawIDs {
|
|
||||||
err := new(cid.ID).DecodeString(rawIDs[i])
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't parse CID %s: %w", rawIDs[i], err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort.Strings(rawIDs)
|
|
||||||
return func(rawID []byte) bool {
|
|
||||||
var v [32]byte
|
|
||||||
copy(v[:], rawID)
|
|
||||||
|
|
||||||
var id cid.ID
|
|
||||||
id.SetSHA256(v)
|
|
||||||
idStr := id.EncodeToString()
|
|
||||||
n := sort.Search(len(rawIDs), func(i int) bool { return rawIDs[i] >= idStr })
|
|
||||||
return n < len(rawIDs) && rawIDs[n] == idStr
|
|
||||||
}, nil
|
|
||||||
}
|
|
|
@ -1,232 +0,0 @@
|
||||||
package morph
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-contract/nns"
|
|
||||||
"github.com/nspcc-dev/neo-go/cli/cmdargs"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/services/rpcsrv/params"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
contractPathFlag = "contract"
|
|
||||||
updateFlag = "update"
|
|
||||||
customZoneFlag = "domain"
|
|
||||||
)
|
|
||||||
|
|
||||||
var deployCmd = &cobra.Command{
|
|
||||||
Use: "deploy",
|
|
||||||
Short: "Deploy additional smart-contracts",
|
|
||||||
Long: `Deploy additional smart-contract which are not related to core.
|
|
||||||
All contracts are deployed by the committee, so access to the alphabet wallets is required.
|
|
||||||
Optionally, arguments can be provided to be passed to a contract's _deploy function.
|
|
||||||
The syntax is the same as for 'neo-go contract testinvokefunction' command.
|
|
||||||
Compiled contract file name must contain '_contract.nef' suffix.
|
|
||||||
Contract's manifest file name must be 'config.json'.
|
|
||||||
NNS name is taken by stripping '_contract.nef' from the NEF file (similar to frostfs contracts).`,
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
|
||||||
},
|
|
||||||
RunE: deployContractCmd,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
ff := deployCmd.Flags()
|
|
||||||
|
|
||||||
ff.String(alphabetWalletsFlag, "", "Path to alphabet wallets dir")
|
|
||||||
_ = deployCmd.MarkFlagFilename(alphabetWalletsFlag)
|
|
||||||
|
|
||||||
ff.StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
|
||||||
ff.String(contractPathFlag, "", "Path to the contract directory")
|
|
||||||
_ = deployCmd.MarkFlagFilename(contractPathFlag)
|
|
||||||
|
|
||||||
ff.Bool(updateFlag, false, "Update an existing contract")
|
|
||||||
ff.String(customZoneFlag, "frostfs", "Custom zone for NNS")
|
|
||||||
}
|
|
||||||
|
|
||||||
func deployContractCmd(cmd *cobra.Command, args []string) error {
|
|
||||||
v := viper.GetViper()
|
|
||||||
c, err := newInitializeContext(cmd, v)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("initialization error: %w", err)
|
|
||||||
}
|
|
||||||
defer c.close()
|
|
||||||
|
|
||||||
ctrPath, _ := cmd.Flags().GetString(contractPathFlag)
|
|
||||||
ctrName, err := probeContractName(ctrPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cs, err := readContract(ctrPath, ctrName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
nnsCs, err := c.Client.GetContractStateByID(1)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't fetch NNS contract state: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
callHash := management.Hash
|
|
||||||
method := deployMethodName
|
|
||||||
zone, _ := cmd.Flags().GetString(customZoneFlag)
|
|
||||||
domain := ctrName + "." + zone
|
|
||||||
isUpdate, _ := cmd.Flags().GetBool(updateFlag)
|
|
||||||
if isUpdate {
|
|
||||||
cs.Hash, err = nnsResolveHash(c.ReadOnlyInvoker, nnsCs.Hash, domain)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't fetch contract hash from NNS: %w", err)
|
|
||||||
}
|
|
||||||
callHash = cs.Hash
|
|
||||||
method = updateMethodName
|
|
||||||
} else {
|
|
||||||
cs.Hash = state.CreateContractHash(
|
|
||||||
c.CommitteeAcc.Contract.ScriptHash(),
|
|
||||||
cs.NEF.Checksum,
|
|
||||||
cs.Manifest.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
writer := io.NewBufBinWriter()
|
|
||||||
if err := emitDeploymentArguments(writer.BinWriter, args); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
emit.Bytes(writer.BinWriter, cs.RawManifest)
|
|
||||||
emit.Bytes(writer.BinWriter, cs.RawNEF)
|
|
||||||
emit.Int(writer.BinWriter, 3)
|
|
||||||
emit.Opcodes(writer.BinWriter, opcode.PACK)
|
|
||||||
emit.AppCallNoArgs(writer.BinWriter, callHash, method, callflag.All)
|
|
||||||
emit.Opcodes(writer.BinWriter, opcode.DROP) // contract state on stack
|
|
||||||
if !isUpdate {
|
|
||||||
err := registerNNS(nnsCs, c, zone, domain, cs, writer)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if writer.Err != nil {
|
|
||||||
panic(fmt.Errorf("BUG: can't create deployment script: %w", writer.Err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.sendCommitteeTx(writer.Bytes(), false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.awaitTx()
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerNNS(nnsCs *state.Contract, c *initializeContext, zone string, domain string, cs *contractState, writer *io.BufBinWriter) error {
|
|
||||||
bw := io.NewBufBinWriter()
|
|
||||||
emit.Instruction(bw.BinWriter, opcode.INITSSLOT, []byte{1})
|
|
||||||
emit.AppCall(bw.BinWriter, nnsCs.Hash, "getPrice", callflag.All)
|
|
||||||
emit.Opcodes(bw.BinWriter, opcode.STSFLD0)
|
|
||||||
emit.AppCall(bw.BinWriter, nnsCs.Hash, "setPrice", callflag.All, 1)
|
|
||||||
|
|
||||||
start := bw.Len()
|
|
||||||
needRecord := false
|
|
||||||
|
|
||||||
ok, err := c.nnsRootRegistered(nnsCs.Hash, zone)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if !ok {
|
|
||||||
needRecord = true
|
|
||||||
|
|
||||||
emit.AppCall(bw.BinWriter, nnsCs.Hash, "register", callflag.All,
|
|
||||||
zone, c.CommitteeAcc.Contract.ScriptHash(),
|
|
||||||
"ops@nspcc.ru", int64(3600), int64(600), int64(defaultExpirationTime), int64(3600))
|
|
||||||
emit.Opcodes(bw.BinWriter, opcode.ASSERT)
|
|
||||||
|
|
||||||
emit.AppCall(bw.BinWriter, nnsCs.Hash, "register", callflag.All,
|
|
||||||
domain, c.CommitteeAcc.Contract.ScriptHash(),
|
|
||||||
"ops@nspcc.ru", int64(3600), int64(600), int64(defaultExpirationTime), int64(3600))
|
|
||||||
emit.Opcodes(bw.BinWriter, opcode.ASSERT)
|
|
||||||
} else {
|
|
||||||
s, ok, err := c.nnsRegisterDomainScript(nnsCs.Hash, cs.Hash, domain)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
needRecord = !ok
|
|
||||||
if len(s) != 0 {
|
|
||||||
bw.WriteBytes(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if needRecord {
|
|
||||||
emit.AppCall(bw.BinWriter, nnsCs.Hash, "deleteRecords", callflag.All, domain, int64(nns.TXT))
|
|
||||||
emit.AppCall(bw.BinWriter, nnsCs.Hash, "addRecord", callflag.All,
|
|
||||||
domain, int64(nns.TXT), address.Uint160ToString(cs.Hash))
|
|
||||||
}
|
|
||||||
|
|
||||||
if bw.Err != nil {
|
|
||||||
panic(fmt.Errorf("BUG: can't create deployment script: %w", writer.Err))
|
|
||||||
} else if bw.Len() != start {
|
|
||||||
writer.WriteBytes(bw.Bytes())
|
|
||||||
emit.Opcodes(writer.BinWriter, opcode.LDSFLD0, opcode.PUSH1, opcode.PACK)
|
|
||||||
emit.AppCallNoArgs(writer.BinWriter, nnsCs.Hash, "setPrice", callflag.All)
|
|
||||||
|
|
||||||
if needRecord {
|
|
||||||
c.Command.Printf("NNS: Set %s -> %s\n", domain, cs.Hash.StringLE())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func emitDeploymentArguments(w *io.BinWriter, args []string) error {
|
|
||||||
_, ps, err := cmdargs.ParseParams(args, true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ps) == 0 {
|
|
||||||
emit.Opcodes(w, opcode.NEWARRAY0)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ps) != 1 {
|
|
||||||
return fmt.Errorf("at most one argument is expected for deploy, got %d", len(ps))
|
|
||||||
}
|
|
||||||
|
|
||||||
// We could emit this directly, but round-trip through JSON is more robust.
|
|
||||||
// This a CLI, so optimizing the conversion is not worth the effort.
|
|
||||||
data, err := json.Marshal(ps)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var pp params.Params
|
|
||||||
if err := json.Unmarshal(data, &pp); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return params.ExpandArrayIntoScript(w, pp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func probeContractName(ctrPath string) (string, error) {
|
|
||||||
ds, err := os.ReadDir(ctrPath)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("can't read directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var ctrName string
|
|
||||||
for i := range ds {
|
|
||||||
if strings.HasSuffix(ds[i].Name(), "_contract.nef") {
|
|
||||||
ctrName = strings.TrimSuffix(ds[i].Name(), "_contract.nef")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctrName == "" {
|
|
||||||
return "", fmt.Errorf("can't find any NEF files in %s", ctrPath)
|
|
||||||
}
|
|
||||||
return ctrName, nil
|
|
||||||
}
|
|
|
@ -1,251 +0,0 @@
|
||||||
package morph
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"text/tabwriter"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-contract/nns"
|
|
||||||
morphClient "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
const lastGlagoliticLetter = 41
|
|
||||||
|
|
||||||
type contractDumpInfo struct {
|
|
||||||
hash util.Uint160
|
|
||||||
name string
|
|
||||||
version string
|
|
||||||
}
|
|
||||||
|
|
||||||
func dumpContractHashes(cmd *cobra.Command, _ []string) error {
|
|
||||||
c, err := getN3Client(viper.GetViper())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't create N3 client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cs, err := c.GetContractStateByID(1)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
zone, _ := cmd.Flags().GetString(customZoneFlag)
|
|
||||||
if zone != "" {
|
|
||||||
return dumpCustomZoneHashes(cmd, cs.Hash, zone, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
infos := []contractDumpInfo{{name: nnsContract, hash: cs.Hash}}
|
|
||||||
|
|
||||||
irSize := 0
|
|
||||||
for ; irSize < lastGlagoliticLetter; irSize++ {
|
|
||||||
ok, err := nnsIsAvailable(c, cs.Hash, getAlphabetNNSDomain(irSize))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bw := io.NewBufBinWriter()
|
|
||||||
|
|
||||||
if irSize != 0 {
|
|
||||||
bw.Reset()
|
|
||||||
for i := 0; i < irSize; i++ {
|
|
||||||
emit.AppCall(bw.BinWriter, cs.Hash, "resolve", callflag.ReadOnly,
|
|
||||||
getAlphabetNNSDomain(i),
|
|
||||||
int64(nns.TXT))
|
|
||||||
}
|
|
||||||
|
|
||||||
alphaRes, err := c.InvokeScript(bw.Bytes(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't fetch info from NNS: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < irSize; i++ {
|
|
||||||
info := contractDumpInfo{name: fmt.Sprintf("alphabet %d", i)}
|
|
||||||
if h, err := parseNNSResolveResult(alphaRes.Stack[i]); err == nil {
|
|
||||||
info.hash = h
|
|
||||||
}
|
|
||||||
infos = append(infos, info)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ctrName := range contractList {
|
|
||||||
bw.Reset()
|
|
||||||
emit.AppCall(bw.BinWriter, cs.Hash, "resolve", callflag.ReadOnly,
|
|
||||||
ctrName+".frostfs", int64(nns.TXT))
|
|
||||||
|
|
||||||
res, err := c.InvokeScript(bw.Bytes(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't fetch info from NNS: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
info := contractDumpInfo{name: ctrName}
|
|
||||||
if len(res.Stack) != 0 {
|
|
||||||
if h, err := parseNNSResolveResult(res.Stack[0]); err == nil {
|
|
||||||
info.hash = h
|
|
||||||
}
|
|
||||||
}
|
|
||||||
infos = append(infos, info)
|
|
||||||
}
|
|
||||||
|
|
||||||
fillContractVersion(cmd, c, infos)
|
|
||||||
printContractInfo(cmd, infos)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func dumpCustomZoneHashes(cmd *cobra.Command, nnsHash util.Uint160, zone string, c Client) error {
|
|
||||||
const nnsMaxTokens = 100
|
|
||||||
|
|
||||||
inv := invoker.New(c, nil)
|
|
||||||
|
|
||||||
if !strings.HasPrefix(zone, ".") {
|
|
||||||
zone = "." + zone
|
|
||||||
}
|
|
||||||
|
|
||||||
var infos []contractDumpInfo
|
|
||||||
processItem := func(item stackitem.Item) {
|
|
||||||
bs, err := item.TryBytes()
|
|
||||||
if err != nil {
|
|
||||||
cmd.PrintErrf("Invalid NNS record: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.HasSuffix(bs, []byte(zone)) || bytes.HasPrefix(bs, []byte(morphClient.NNSGroupKeyName)) {
|
|
||||||
// Related https://github.com/nspcc-dev/neofs-contract/issues/316.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h, err := nnsResolveHash(inv, nnsHash, string(bs))
|
|
||||||
if err != nil {
|
|
||||||
cmd.PrintErrf("Could not resolve name %s: %v\n", string(bs), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
infos = append(infos, contractDumpInfo{
|
|
||||||
hash: h,
|
|
||||||
name: strings.TrimSuffix(string(bs), zone),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionID, iter, err := unwrap.SessionIterator(inv.Call(nnsHash, "tokens"))
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, unwrap.ErrNoSessionID) {
|
|
||||||
items, err := unwrap.Array(inv.CallAndExpandIterator(nnsHash, "tokens", nnsMaxTokens))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't get a list of NNS domains: %w", err)
|
|
||||||
}
|
|
||||||
if len(items) == nnsMaxTokens {
|
|
||||||
cmd.PrintErrln("Provided RPC endpoint doesn't support sessions, some hashes might be lost.")
|
|
||||||
}
|
|
||||||
for i := range items {
|
|
||||||
processItem(items[i])
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
defer func() {
|
|
||||||
_ = inv.TerminateSession(sessionID)
|
|
||||||
}()
|
|
||||||
|
|
||||||
items, err := inv.TraverseIterator(sessionID, &iter, nnsMaxTokens)
|
|
||||||
for err == nil && len(items) != 0 {
|
|
||||||
for i := range items {
|
|
||||||
processItem(items[i])
|
|
||||||
}
|
|
||||||
items, err = inv.TraverseIterator(sessionID, &iter, nnsMaxTokens)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error during NNS domains iteration: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fillContractVersion(cmd, c, infos)
|
|
||||||
printContractInfo(cmd, infos)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseContractVersion(item stackitem.Item) string {
|
|
||||||
bi, err := item.TryInteger()
|
|
||||||
if err != nil || bi.Sign() == 0 || !bi.IsInt64() {
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
|
|
||||||
v := bi.Int64()
|
|
||||||
major := v / 1_000_000
|
|
||||||
minor := (v % 1_000_000) / 1000
|
|
||||||
patch := v % 1_000
|
|
||||||
return fmt.Sprintf("v%d.%d.%d", major, minor, patch)
|
|
||||||
}
|
|
||||||
|
|
||||||
func printContractInfo(cmd *cobra.Command, infos []contractDumpInfo) {
|
|
||||||
if len(infos) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := bytes.NewBuffer(nil)
|
|
||||||
tw := tabwriter.NewWriter(buf, 0, 2, 2, ' ', 0)
|
|
||||||
for _, info := range infos {
|
|
||||||
if info.version == "" {
|
|
||||||
info.version = "unknown"
|
|
||||||
}
|
|
||||||
_, _ = tw.Write([]byte(fmt.Sprintf("%s\t(%s):\t%s\n",
|
|
||||||
info.name, info.version, info.hash.StringLE())))
|
|
||||||
}
|
|
||||||
_ = tw.Flush()
|
|
||||||
|
|
||||||
cmd.Print(buf.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func fillContractVersion(cmd *cobra.Command, c Client, infos []contractDumpInfo) {
|
|
||||||
bw := io.NewBufBinWriter()
|
|
||||||
sub := io.NewBufBinWriter()
|
|
||||||
for i := range infos {
|
|
||||||
if infos[i].hash.Equals(util.Uint160{}) {
|
|
||||||
emit.Int(bw.BinWriter, 0)
|
|
||||||
} else {
|
|
||||||
sub.Reset()
|
|
||||||
emit.AppCall(sub.BinWriter, infos[i].hash, "version", callflag.NoneFlag)
|
|
||||||
if sub.Err != nil {
|
|
||||||
panic(fmt.Errorf("BUG: can't create version script: %w", bw.Err))
|
|
||||||
}
|
|
||||||
|
|
||||||
script := sub.Bytes()
|
|
||||||
emit.Instruction(bw.BinWriter, opcode.TRY, []byte{byte(3 + len(script) + 2), 0})
|
|
||||||
bw.BinWriter.WriteBytes(script)
|
|
||||||
emit.Instruction(bw.BinWriter, opcode.ENDTRY, []byte{2 + 1})
|
|
||||||
emit.Opcodes(bw.BinWriter, opcode.PUSH0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
emit.Opcodes(bw.BinWriter, opcode.NOP) // for the last ENDTRY target
|
|
||||||
if bw.Err != nil {
|
|
||||||
panic(fmt.Errorf("BUG: can't create version script: %w", bw.Err))
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := c.InvokeScript(bw.Bytes(), nil)
|
|
||||||
if err != nil {
|
|
||||||
cmd.Printf("Can't fetch version from NNS: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.State == vmstate.Halt.String() {
|
|
||||||
for i := range res.Stack {
|
|
||||||
infos[i].version = parseContractVersion(res.Stack[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,514 +0,0 @@
|
||||||
package morph
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/config"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/innerring"
|
|
||||||
morphClient "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// maxAlphabetNodes is the maximum number of candidates allowed, which is currently limited by the size
|
|
||||||
// of the invocation script.
|
|
||||||
// See: https://github.com/nspcc-dev/neo-go/blob/740488f7f35e367eaa99a71c0a609c315fe2b0fc/pkg/core/transaction/witness.go#L10
|
|
||||||
maxAlphabetNodes = 22
|
|
||||||
)
|
|
||||||
|
|
||||||
type cache struct {
|
|
||||||
nnsCs *state.Contract
|
|
||||||
groupKey *keys.PublicKey
|
|
||||||
}
|
|
||||||
|
|
||||||
type initializeContext struct {
|
|
||||||
clientContext
|
|
||||||
cache
|
|
||||||
// CommitteeAcc is used for retrieving the committee address and the verification script.
|
|
||||||
CommitteeAcc *wallet.Account
|
|
||||||
// ConsensusAcc is used for retrieving the committee address and the verification script.
|
|
||||||
ConsensusAcc *wallet.Account
|
|
||||||
Wallets []*wallet.Wallet
|
|
||||||
// ContractWallet is a wallet for providing the contract group signature.
|
|
||||||
ContractWallet *wallet.Wallet
|
|
||||||
// Accounts contains simple signature accounts in the same order as in Wallets.
|
|
||||||
Accounts []*wallet.Account
|
|
||||||
Contracts map[string]*contractState
|
|
||||||
Command *cobra.Command
|
|
||||||
ContractPath string
|
|
||||||
}
|
|
||||||
|
|
||||||
var ErrTooManyAlphabetNodes = fmt.Errorf("too many alphabet nodes (maximum allowed is %d)", maxAlphabetNodes)
|
|
||||||
|
|
||||||
func initializeSideChainCmd(cmd *cobra.Command, _ []string) error {
|
|
||||||
initCtx, err := newInitializeContext(cmd, viper.GetViper())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("initialization error: %w", err)
|
|
||||||
}
|
|
||||||
defer initCtx.close()
|
|
||||||
|
|
||||||
// 1. Transfer funds to committee accounts.
|
|
||||||
cmd.Println("Stage 1: transfer GAS to alphabet nodes.")
|
|
||||||
if err := initCtx.transferFunds(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Println("Stage 2: set notary and alphabet nodes in designate contract.")
|
|
||||||
if err := initCtx.setNotaryAndAlphabetNodes(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Deploy NNS contract.
|
|
||||||
cmd.Println("Stage 3: deploy NNS contract.")
|
|
||||||
if err := initCtx.deployNNS(deployMethodName); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Deploy NeoFS contracts.
|
|
||||||
cmd.Println("Stage 4: deploy NeoFS contracts.")
|
|
||||||
if err := initCtx.deployContracts(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Println("Stage 4.1: Transfer GAS to proxy contract.")
|
|
||||||
if err := initCtx.transferGASToProxy(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Println("Stage 5: register candidates.")
|
|
||||||
if err := initCtx.registerCandidates(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Println("Stage 6: transfer NEO to alphabet contracts.")
|
|
||||||
if err := initCtx.transferNEOToAlphabetContracts(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Println("Stage 7: set addresses in NNS.")
|
|
||||||
return initCtx.setNNS()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *initializeContext) close() {
|
|
||||||
if local, ok := c.Client.(*localClient); ok {
|
|
||||||
err := local.dump()
|
|
||||||
if err != nil {
|
|
||||||
c.Command.PrintErrf("Can't write dump: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newInitializeContext(cmd *cobra.Command, v *viper.Viper) (*initializeContext, error) {
|
|
||||||
walletDir := config.ResolveHomePath(viper.GetString(alphabetWalletsFlag))
|
|
||||||
wallets, err := openAlphabetWallets(v, walletDir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(wallets) > maxAlphabetNodes {
|
|
||||||
return nil, ErrTooManyAlphabetNodes
|
|
||||||
}
|
|
||||||
|
|
||||||
needContracts := cmd.Name() == "update-contracts" || cmd.Name() == "init"
|
|
||||||
|
|
||||||
var w *wallet.Wallet
|
|
||||||
w, err = getWallet(cmd, v, needContracts, walletDir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := createClient(cmd, v, wallets)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
committeeAcc, err := getWalletAccount(wallets[0], committeeAccountName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't find committee account: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
consensusAcc, err := getWalletAccount(wallets[0], consensusAccountName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't find consensus account: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := validateInit(cmd); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctrPath, err := getContractsPath(cmd, needContracts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := checkNotaryEnabled(c); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
accounts, err := createWalletAccounts(wallets)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cliCtx, err := defaultClientContext(c, committeeAcc)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("client context: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
initCtx := &initializeContext{
|
|
||||||
clientContext: *cliCtx,
|
|
||||||
ConsensusAcc: consensusAcc,
|
|
||||||
CommitteeAcc: committeeAcc,
|
|
||||||
ContractWallet: w,
|
|
||||||
Wallets: wallets,
|
|
||||||
Accounts: accounts,
|
|
||||||
Command: cmd,
|
|
||||||
Contracts: make(map[string]*contractState),
|
|
||||||
ContractPath: ctrPath,
|
|
||||||
}
|
|
||||||
|
|
||||||
if needContracts {
|
|
||||||
err := initCtx.readContracts(fullContractList)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return initCtx, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateInit(cmd *cobra.Command) error {
|
|
||||||
if cmd.Name() != "init" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if viper.GetInt64(epochDurationInitFlag) <= 0 {
|
|
||||||
return fmt.Errorf("epoch duration must be positive")
|
|
||||||
}
|
|
||||||
|
|
||||||
if viper.GetInt64(maxObjectSizeInitFlag) <= 0 {
|
|
||||||
return fmt.Errorf("max object size must be positive")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func createClient(cmd *cobra.Command, v *viper.Viper, wallets []*wallet.Wallet) (Client, error) {
|
|
||||||
var c Client
|
|
||||||
var err error
|
|
||||||
if v.GetString(localDumpFlag) != "" {
|
|
||||||
if v.GetString(endpointFlag) != "" {
|
|
||||||
return nil, fmt.Errorf("`%s` and `%s` flags are mutually exclusive", endpointFlag, localDumpFlag)
|
|
||||||
}
|
|
||||||
c, err = newLocalClient(cmd, v, wallets)
|
|
||||||
} else {
|
|
||||||
c, err = getN3Client(v)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't create N3 client: %w", err)
|
|
||||||
}
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getWallet(cmd *cobra.Command, v *viper.Viper, needContracts bool, walletDir string) (*wallet.Wallet, error) {
|
|
||||||
if !needContracts {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return openContractWallet(v, cmd, walletDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getContractsPath(cmd *cobra.Command, needContracts bool) (string, error) {
|
|
||||||
if !needContracts {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ctrPath, err := cmd.Flags().GetString(contractsInitFlag)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("invalid contracts path: %w", err)
|
|
||||||
}
|
|
||||||
return ctrPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func createWalletAccounts(wallets []*wallet.Wallet) ([]*wallet.Account, error) {
|
|
||||||
accounts := make([]*wallet.Account, len(wallets))
|
|
||||||
for i, w := range wallets {
|
|
||||||
acc, err := getWalletAccount(w, singleAccountName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("wallet %s is invalid (no single account): %w", w.Path(), err)
|
|
||||||
}
|
|
||||||
accounts[i] = acc
|
|
||||||
}
|
|
||||||
return accounts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func openAlphabetWallets(v *viper.Viper, walletDir string) ([]*wallet.Wallet, error) {
|
|
||||||
walletFiles, err := os.ReadDir(walletDir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't read alphabet wallets dir: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var size int
|
|
||||||
loop:
|
|
||||||
for i := 0; i < len(walletFiles); i++ {
|
|
||||||
name := innerring.GlagoliticLetter(i).String() + ".json"
|
|
||||||
for j := range walletFiles {
|
|
||||||
if walletFiles[j].Name() == name {
|
|
||||||
size++
|
|
||||||
continue loop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if size == 0 {
|
|
||||||
return nil, errors.New("alphabet wallets dir is empty (run `generate-alphabet` command first)")
|
|
||||||
}
|
|
||||||
|
|
||||||
wallets := make([]*wallet.Wallet, size)
|
|
||||||
for i := 0; i < size; i++ {
|
|
||||||
letter := innerring.GlagoliticLetter(i).String()
|
|
||||||
p := filepath.Join(walletDir, letter+".json")
|
|
||||||
w, err := wallet.NewWalletFromFile(p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't open wallet: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
password, err := config.GetPassword(v, letter)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't fetch password: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range w.Accounts {
|
|
||||||
if err := w.Accounts[i].Decrypt(password, keys.NEP2ScryptParams()); err != nil {
|
|
||||||
return nil, fmt.Errorf("can't unlock wallet: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wallets[i] = w
|
|
||||||
}
|
|
||||||
|
|
||||||
return wallets, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *initializeContext) awaitTx() error {
|
|
||||||
return c.clientContext.awaitTx(c.Command)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *initializeContext) nnsContractState() (*state.Contract, error) {
|
|
||||||
if c.nnsCs != nil {
|
|
||||||
return c.nnsCs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cs, err := c.Client.GetContractStateByID(1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.nnsCs = cs
|
|
||||||
return cs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *initializeContext) getSigner(tryGroup bool, acc *wallet.Account) transaction.Signer {
|
|
||||||
if tryGroup && c.groupKey != nil {
|
|
||||||
return transaction.Signer{
|
|
||||||
Account: acc.Contract.ScriptHash(),
|
|
||||||
Scopes: transaction.CustomGroups,
|
|
||||||
AllowedGroups: keys.PublicKeys{c.groupKey},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
signer := transaction.Signer{
|
|
||||||
Account: acc.Contract.ScriptHash(),
|
|
||||||
Scopes: transaction.Global, // Scope is important, as we have nested call to container contract.
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tryGroup {
|
|
||||||
return signer
|
|
||||||
}
|
|
||||||
|
|
||||||
nnsCs, err := c.nnsContractState()
|
|
||||||
if err != nil {
|
|
||||||
return signer
|
|
||||||
}
|
|
||||||
|
|
||||||
groupKey, err := nnsResolveKey(c.ReadOnlyInvoker, nnsCs.Hash, morphClient.NNSGroupKeyName)
|
|
||||||
if err == nil {
|
|
||||||
c.groupKey = groupKey
|
|
||||||
|
|
||||||
signer.Scopes = transaction.CustomGroups
|
|
||||||
signer.AllowedGroups = keys.PublicKeys{groupKey}
|
|
||||||
}
|
|
||||||
return signer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *clientContext) awaitTx(cmd *cobra.Command) error {
|
|
||||||
if len(c.SentTxs) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if local, ok := c.Client.(*localClient); ok {
|
|
||||||
if err := local.putTransactions(); err != nil {
|
|
||||||
return fmt.Errorf("can't persist transactions: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := awaitTx(cmd, c.Client, c.SentTxs)
|
|
||||||
c.SentTxs = c.SentTxs[:0]
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func awaitTx(cmd *cobra.Command, c Client, txs []hashVUBPair) error {
|
|
||||||
cmd.Println("Waiting for transactions to persist...")
|
|
||||||
|
|
||||||
const pollInterval = time.Second
|
|
||||||
|
|
||||||
tick := time.NewTicker(pollInterval)
|
|
||||||
defer tick.Stop()
|
|
||||||
|
|
||||||
at := trigger.Application
|
|
||||||
|
|
||||||
var retErr error
|
|
||||||
|
|
||||||
currBlock, err := c.GetBlockCount()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't fetch current block height: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
loop:
|
|
||||||
for i := range txs {
|
|
||||||
res, err := c.GetApplicationLog(txs[i].hash, &at)
|
|
||||||
if err == nil {
|
|
||||||
if retErr == nil && len(res.Executions) > 0 && res.Executions[0].VMState != vmstate.Halt {
|
|
||||||
retErr = fmt.Errorf("tx %d persisted in %s state: %s",
|
|
||||||
i, res.Executions[0].VMState, res.Executions[0].FaultException)
|
|
||||||
}
|
|
||||||
continue loop
|
|
||||||
}
|
|
||||||
if txs[i].vub < currBlock {
|
|
||||||
return fmt.Errorf("tx was not persisted: vub=%d, height=%d", txs[i].vub, currBlock)
|
|
||||||
}
|
|
||||||
for range tick.C {
|
|
||||||
// We must fetch current height before application log, to avoid race condition.
|
|
||||||
currBlock, err = c.GetBlockCount()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't fetch current block height: %w", err)
|
|
||||||
}
|
|
||||||
res, err := c.GetApplicationLog(txs[i].hash, &at)
|
|
||||||
if err == nil {
|
|
||||||
if retErr == nil && len(res.Executions) > 0 && res.Executions[0].VMState != vmstate.Halt {
|
|
||||||
retErr = fmt.Errorf("tx %d persisted in %s state: %s",
|
|
||||||
i, res.Executions[0].VMState, res.Executions[0].FaultException)
|
|
||||||
}
|
|
||||||
continue loop
|
|
||||||
}
|
|
||||||
if txs[i].vub < currBlock {
|
|
||||||
return fmt.Errorf("tx was not persisted: vub=%d, height=%d", txs[i].vub, currBlock)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return retErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// sendCommitteeTx creates transaction from script, signs it by committee nodes and sends it to RPC.
|
|
||||||
// If tryGroup is false, global scope is used for the signer (useful when
|
|
||||||
// working with native contracts).
|
|
||||||
func (c *initializeContext) sendCommitteeTx(script []byte, tryGroup bool) error {
|
|
||||||
return c.sendMultiTx(script, tryGroup, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// sendConsensusTx creates transaction from script, signs it by alphabet nodes and sends it to RPC.
|
|
||||||
// Not that because this is used only after the contracts were initialized and deployed,
|
|
||||||
// we always try to have a group scope.
|
|
||||||
func (c *initializeContext) sendConsensusTx(script []byte) error {
|
|
||||||
return c.sendMultiTx(script, true, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *initializeContext) sendMultiTx(script []byte, tryGroup bool, withConsensus bool) error {
|
|
||||||
var act *actor.Actor
|
|
||||||
var err error
|
|
||||||
|
|
||||||
withConsensus = withConsensus && !c.ConsensusAcc.Contract.ScriptHash().Equals(c.CommitteeAcc.ScriptHash())
|
|
||||||
if tryGroup {
|
|
||||||
// Even for consensus signatures we need the committee to pay.
|
|
||||||
signers := make([]actor.SignerAccount, 1, 2)
|
|
||||||
signers[0] = actor.SignerAccount{
|
|
||||||
Signer: c.getSigner(tryGroup, c.CommitteeAcc),
|
|
||||||
Account: c.CommitteeAcc,
|
|
||||||
}
|
|
||||||
if withConsensus {
|
|
||||||
signers = append(signers, actor.SignerAccount{
|
|
||||||
Signer: c.getSigner(tryGroup, c.ConsensusAcc),
|
|
||||||
Account: c.ConsensusAcc,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
act, err = actor.New(c.Client, signers)
|
|
||||||
} else {
|
|
||||||
if withConsensus {
|
|
||||||
panic("BUG: should never happen")
|
|
||||||
}
|
|
||||||
act, err = c.CommitteeAct, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not create actor: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := act.MakeUnsignedRun(script, []transaction.Attribute{{Type: transaction.HighPriority}})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not perform test invocation: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.multiSign(tx, committeeAccountName); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if withConsensus {
|
|
||||||
if err := c.multiSign(tx, consensusAccountName); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.sendTx(tx, c.Command, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getWalletAccount(w *wallet.Wallet, typ string) (*wallet.Account, error) {
|
|
||||||
for i := range w.Accounts {
|
|
||||||
if w.Accounts[i].Label == typ {
|
|
||||||
return w.Accounts[i], nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("account for '%s' not found", typ)
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkNotaryEnabled(c Client) error {
|
|
||||||
ns, err := c.GetNativeContracts()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't get native contract hashes: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
notaryEnabled := false
|
|
||||||
nativeHashes := make(map[string]util.Uint160, len(ns))
|
|
||||||
for i := range ns {
|
|
||||||
if ns[i].Manifest.Name == nativenames.Notary {
|
|
||||||
notaryEnabled = len(ns[i].UpdateHistory) > 0
|
|
||||||
}
|
|
||||||
nativeHashes[ns[i].Manifest.Name] = ns[i].Hash
|
|
||||||
}
|
|
||||||
if !notaryEnabled {
|
|
||||||
return errors.New("notary contract must be enabled")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,623 +0,0 @@
|
||||||
package morph
|
|
||||||
|
|
||||||
import (
|
|
||||||
"archive/tar"
|
|
||||||
"compress/gzip"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-contract/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-contract/nns"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/innerring"
|
|
||||||
morphClient "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/netmap"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
|
||||||
io2 "github.com/nspcc-dev/neo-go/pkg/io"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/nef"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
nnsContract = "nns"
|
|
||||||
frostfsContract = "frostfs" // not deployed in side-chain.
|
|
||||||
processingContract = "processing" // not deployed in side-chain.
|
|
||||||
alphabetContract = "alphabet"
|
|
||||||
auditContract = "audit"
|
|
||||||
balanceContract = "balance"
|
|
||||||
containerContract = "container"
|
|
||||||
frostfsIDContract = "frostfsid"
|
|
||||||
netmapContract = "netmap"
|
|
||||||
proxyContract = "proxy"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
contractList = []string{
|
|
||||||
auditContract,
|
|
||||||
balanceContract,
|
|
||||||
containerContract,
|
|
||||||
frostfsIDContract,
|
|
||||||
netmapContract,
|
|
||||||
proxyContract,
|
|
||||||
}
|
|
||||||
|
|
||||||
fullContractList = append([]string{
|
|
||||||
frostfsContract,
|
|
||||||
processingContract,
|
|
||||||
nnsContract,
|
|
||||||
alphabetContract,
|
|
||||||
}, contractList...)
|
|
||||||
|
|
||||||
netmapConfigKeys = []string{
|
|
||||||
netmap.EpochDurationConfig,
|
|
||||||
netmap.MaxObjectSizeConfig,
|
|
||||||
netmap.AuditFeeConfig,
|
|
||||||
netmap.ContainerFeeConfig,
|
|
||||||
netmap.ContainerAliasFeeConfig,
|
|
||||||
netmap.BasicIncomeRateConfig,
|
|
||||||
netmap.IrCandidateFeeConfig,
|
|
||||||
netmap.WithdrawFeeConfig,
|
|
||||||
netmap.HomomorphicHashingDisabledKey,
|
|
||||||
netmap.MaintenanceModeAllowedConfig,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
type contractState struct {
|
|
||||||
NEF *nef.File
|
|
||||||
RawNEF []byte
|
|
||||||
Manifest *manifest.Manifest
|
|
||||||
RawManifest []byte
|
|
||||||
Hash util.Uint160
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
updateMethodName = "update"
|
|
||||||
deployMethodName = "deploy"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *initializeContext) deployNNS(method string) error {
|
|
||||||
cs := c.getContract(nnsContract)
|
|
||||||
h := cs.Hash
|
|
||||||
|
|
||||||
nnsCs, err := c.nnsContractState()
|
|
||||||
if err == nil {
|
|
||||||
if nnsCs.NEF.Checksum == cs.NEF.Checksum {
|
|
||||||
if method == deployMethodName {
|
|
||||||
c.Command.Println("NNS contract is already deployed.")
|
|
||||||
} else {
|
|
||||||
c.Command.Println("NNS contract is already updated.")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
h = nnsCs.Hash
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.addManifestGroup(h, cs)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't sign manifest group: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
params := getContractDeployParameters(cs, nil)
|
|
||||||
signer := transaction.Signer{
|
|
||||||
Account: c.CommitteeAcc.Contract.ScriptHash(),
|
|
||||||
Scopes: transaction.CalledByEntry,
|
|
||||||
}
|
|
||||||
|
|
||||||
invokeHash := management.Hash
|
|
||||||
if method == updateMethodName {
|
|
||||||
invokeHash = nnsCs.Hash
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := invokeFunction(c.Client, invokeHash, method, params, []transaction.Signer{signer})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't deploy NNS contract: %w", err)
|
|
||||||
}
|
|
||||||
if res.State != vmstate.Halt.String() {
|
|
||||||
return fmt.Errorf("can't deploy NNS contract: %s", res.FaultException)
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := c.Client.CreateTxFromScript(res.Script, c.CommitteeAcc, res.GasConsumed, 0, []rpcclient.SignerAccount{{
|
|
||||||
Signer: signer,
|
|
||||||
Account: c.CommitteeAcc,
|
|
||||||
}})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create deploy tx for %s: %w", nnsContract, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.multiSignAndSend(tx, committeeAccountName); err != nil {
|
|
||||||
return fmt.Errorf("can't send deploy transaction: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.awaitTx()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *initializeContext) updateContracts() error {
|
|
||||||
alphaCs := c.getContract(alphabetContract)
|
|
||||||
|
|
||||||
nnsCs, err := c.nnsContractState()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
nnsHash := nnsCs.Hash
|
|
||||||
|
|
||||||
w := io2.NewBufBinWriter()
|
|
||||||
|
|
||||||
// Update script size for a single-node committee is close to the maximum allowed size of 65535.
|
|
||||||
// Because of this we want to reuse alphabet contract NEF and manifest for different updates.
|
|
||||||
// The generated script is as following.
|
|
||||||
// 1. Initialize static slot for alphabet NEF.
|
|
||||||
// 2. Store NEF into the static slot.
|
|
||||||
// 3. Push parameters for each alphabet contract on stack.
|
|
||||||
// 4. Add contract group to the manifest.
|
|
||||||
// 5. For each alphabet contract, invoke `update` using parameters on stack and
|
|
||||||
// NEF from step 2 and manifest from step 4.
|
|
||||||
emit.Instruction(w.BinWriter, opcode.INITSSLOT, []byte{1})
|
|
||||||
emit.Bytes(w.BinWriter, alphaCs.RawNEF)
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.STSFLD0)
|
|
||||||
|
|
||||||
keysParam, err := c.deployAlphabetAccounts(nnsHash, w, alphaCs)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Reset()
|
|
||||||
|
|
||||||
if err = c.deployOrUpdateContracts(w, nnsHash, keysParam); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
groupKey := c.ContractWallet.Accounts[0].PrivateKey().PublicKey()
|
|
||||||
_, _, err = c.emitUpdateNNSGroupScript(w, nnsHash, groupKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.Command.Printf("NNS: Set %s -> %s\n", morphClient.NNSGroupKeyName, hex.EncodeToString(groupKey.Bytes()))
|
|
||||||
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.LDSFLD0)
|
|
||||||
emit.Int(w.BinWriter, 1)
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.PACK)
|
|
||||||
emit.AppCallNoArgs(w.BinWriter, nnsHash, "setPrice", callflag.All)
|
|
||||||
|
|
||||||
if err := c.sendCommitteeTx(w.Bytes(), false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.awaitTx()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *initializeContext) deployOrUpdateContracts(w *io2.BufBinWriter, nnsHash util.Uint160, keysParam []any) error {
|
|
||||||
emit.Instruction(w.BinWriter, opcode.INITSSLOT, []byte{1})
|
|
||||||
emit.AppCall(w.BinWriter, nnsHash, "getPrice", callflag.All)
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.STSFLD0)
|
|
||||||
emit.AppCall(w.BinWriter, nnsHash, "setPrice", callflag.All, 1)
|
|
||||||
|
|
||||||
for _, ctrName := range contractList {
|
|
||||||
cs := c.getContract(ctrName)
|
|
||||||
|
|
||||||
method := updateMethodName
|
|
||||||
ctrHash, err := nnsResolveHash(c.ReadOnlyInvoker, nnsHash, ctrName+".frostfs")
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, errMissingNNSRecord) {
|
|
||||||
// if contract not found we deploy it instead of update
|
|
||||||
method = deployMethodName
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("can't resolve hash for contract update: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.addManifestGroup(ctrHash, cs)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't sign manifest group: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
invokeHash := management.Hash
|
|
||||||
if method == updateMethodName {
|
|
||||||
invokeHash = ctrHash
|
|
||||||
}
|
|
||||||
|
|
||||||
params := getContractDeployParameters(cs, c.getContractDeployData(ctrName, keysParam, updateMethodName))
|
|
||||||
res, err := c.CommitteeAct.MakeCall(invokeHash, method, params...)
|
|
||||||
if err != nil {
|
|
||||||
if method != updateMethodName || !strings.Contains(err.Error(), common.ErrAlreadyUpdated) {
|
|
||||||
return fmt.Errorf("deploy contract: %w", err)
|
|
||||||
}
|
|
||||||
c.Command.Printf("%s contract is already updated.\n", ctrName)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteBytes(res.Script)
|
|
||||||
|
|
||||||
if method == deployMethodName {
|
|
||||||
// same actions are done in initializeContext.setNNS, can be unified
|
|
||||||
domain := ctrName + ".frostfs"
|
|
||||||
script, ok, err := c.nnsRegisterDomainScript(nnsHash, cs.Hash, domain)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
w.WriteBytes(script)
|
|
||||||
emit.AppCall(w.BinWriter, nnsHash, "deleteRecords", callflag.All, domain, int64(nns.TXT))
|
|
||||||
emit.AppCall(w.BinWriter, nnsHash, "addRecord", callflag.All,
|
|
||||||
domain, int64(nns.TXT), cs.Hash.StringLE())
|
|
||||||
emit.AppCall(w.BinWriter, nnsHash, "addRecord", callflag.All,
|
|
||||||
domain, int64(nns.TXT), address.Uint160ToString(cs.Hash))
|
|
||||||
}
|
|
||||||
c.Command.Printf("NNS: Set %s -> %s\n", domain, cs.Hash.StringLE())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *initializeContext) deployAlphabetAccounts(nnsHash util.Uint160, w *io2.BufBinWriter, alphaCs *contractState) ([]any, error) {
|
|
||||||
var keysParam []any
|
|
||||||
|
|
||||||
baseGroups := alphaCs.Manifest.Groups
|
|
||||||
|
|
||||||
// alphabet contracts should be deployed by individual nodes to get different hashes.
|
|
||||||
for i, acc := range c.Accounts {
|
|
||||||
ctrHash, err := nnsResolveHash(c.ReadOnlyInvoker, nnsHash, getAlphabetNNSDomain(i))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't resolve hash for contract update: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
keysParam = append(keysParam, acc.PrivateKey().PublicKey().Bytes())
|
|
||||||
|
|
||||||
params := c.getAlphabetDeployItems(i, len(c.Wallets))
|
|
||||||
emit.Array(w.BinWriter, params...)
|
|
||||||
|
|
||||||
alphaCs.Manifest.Groups = baseGroups
|
|
||||||
err = c.addManifestGroup(ctrHash, alphaCs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't sign manifest group: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
emit.Bytes(w.BinWriter, alphaCs.RawManifest)
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.LDSFLD0)
|
|
||||||
emit.Int(w.BinWriter, 3)
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.PACK)
|
|
||||||
emit.AppCallNoArgs(w.BinWriter, ctrHash, updateMethodName, callflag.All)
|
|
||||||
}
|
|
||||||
if err := c.sendCommitteeTx(w.Bytes(), false); err != nil {
|
|
||||||
if !strings.Contains(err.Error(), common.ErrAlreadyUpdated) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
c.Command.Println("Alphabet contracts are already updated.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return keysParam, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *initializeContext) deployContracts() error {
|
|
||||||
alphaCs := c.getContract(alphabetContract)
|
|
||||||
|
|
||||||
var keysParam []any
|
|
||||||
|
|
||||||
baseGroups := alphaCs.Manifest.Groups
|
|
||||||
|
|
||||||
// alphabet contracts should be deployed by individual nodes to get different hashes.
|
|
||||||
for i, acc := range c.Accounts {
|
|
||||||
ctrHash := state.CreateContractHash(acc.Contract.ScriptHash(), alphaCs.NEF.Checksum, alphaCs.Manifest.Name)
|
|
||||||
if c.isUpdated(ctrHash, alphaCs) {
|
|
||||||
c.Command.Printf("Alphabet contract #%d is already deployed.\n", i)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
alphaCs.Manifest.Groups = baseGroups
|
|
||||||
err := c.addManifestGroup(ctrHash, alphaCs)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't sign manifest group: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
keysParam = append(keysParam, acc.PrivateKey().PublicKey().Bytes())
|
|
||||||
params := getContractDeployParameters(alphaCs, c.getAlphabetDeployItems(i, len(c.Wallets)))
|
|
||||||
|
|
||||||
act, err := actor.NewSimple(c.Client, acc)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not create actor: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
txHash, vub, err := act.SendCall(management.Hash, deployMethodName, params...)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't deploy alphabet #%d contract: %w", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.SentTxs = append(c.SentTxs, hashVUBPair{hash: txHash, vub: vub})
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ctrName := range contractList {
|
|
||||||
cs := c.getContract(ctrName)
|
|
||||||
|
|
||||||
ctrHash := cs.Hash
|
|
||||||
if c.isUpdated(ctrHash, cs) {
|
|
||||||
c.Command.Printf("%s contract is already deployed.\n", ctrName)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err := c.addManifestGroup(ctrHash, cs)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't sign manifest group: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
params := getContractDeployParameters(cs, c.getContractDeployData(ctrName, keysParam, deployMethodName))
|
|
||||||
res, err := c.CommitteeAct.MakeCall(management.Hash, deployMethodName, params...)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't deploy %s contract: %w", ctrName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.sendCommitteeTx(res.Script, false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.awaitTx()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *initializeContext) isUpdated(ctrHash util.Uint160, cs *contractState) bool {
|
|
||||||
realCs, err := c.Client.GetContractStateByHash(ctrHash)
|
|
||||||
return err == nil && realCs.NEF.Checksum == cs.NEF.Checksum
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *initializeContext) getContract(ctrName string) *contractState {
|
|
||||||
return c.Contracts[ctrName]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *initializeContext) readContracts(names []string) error {
|
|
||||||
var (
|
|
||||||
fi os.FileInfo
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if c.ContractPath != "" {
|
|
||||||
fi, err = os.Stat(c.ContractPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid contracts path: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.ContractPath != "" && fi.IsDir() {
|
|
||||||
for _, ctrName := range names {
|
|
||||||
cs, err := readContract(filepath.Join(c.ContractPath, ctrName), ctrName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.Contracts[ctrName] = cs
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var r io.ReadCloser
|
|
||||||
if c.ContractPath == "" {
|
|
||||||
return errors.New("contracts flag is missing")
|
|
||||||
}
|
|
||||||
r, err = os.Open(c.ContractPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't open contracts archive: %w", err)
|
|
||||||
}
|
|
||||||
defer r.Close()
|
|
||||||
|
|
||||||
m, err := readContractsFromArchive(r, names)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, name := range names {
|
|
||||||
if err := m[name].parse(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.Contracts[name] = m[name]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ctrName := range names {
|
|
||||||
if ctrName != alphabetContract {
|
|
||||||
cs := c.Contracts[ctrName]
|
|
||||||
cs.Hash = state.CreateContractHash(c.CommitteeAcc.Contract.ScriptHash(),
|
|
||||||
cs.NEF.Checksum, cs.Manifest.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readContract(ctrPath, ctrName string) (*contractState, error) {
|
|
||||||
rawNef, err := os.ReadFile(filepath.Join(ctrPath, ctrName+"_contract.nef"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't read NEF file for %s contract: %w", ctrName, err)
|
|
||||||
}
|
|
||||||
rawManif, err := os.ReadFile(filepath.Join(ctrPath, "config.json"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't read manifest file for %s contract: %w", ctrName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cs := &contractState{
|
|
||||||
RawNEF: rawNef,
|
|
||||||
RawManifest: rawManif,
|
|
||||||
}
|
|
||||||
|
|
||||||
return cs, cs.parse()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cs *contractState) parse() error {
|
|
||||||
nf, err := nef.FileFromBytes(cs.RawNEF)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't parse NEF file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
m := new(manifest.Manifest)
|
|
||||||
if err := json.Unmarshal(cs.RawManifest, m); err != nil {
|
|
||||||
return fmt.Errorf("can't parse manifest file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cs.NEF = &nf
|
|
||||||
cs.Manifest = m
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readContractsFromArchive(file io.Reader, names []string) (map[string]*contractState, error) {
|
|
||||||
m := make(map[string]*contractState, len(names))
|
|
||||||
for i := range names {
|
|
||||||
m[names[i]] = new(contractState)
|
|
||||||
}
|
|
||||||
|
|
||||||
gr, err := gzip.NewReader(file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("contracts file must be tar.gz archive: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r := tar.NewReader(gr)
|
|
||||||
for h, err := r.Next(); ; h, err = r.Next() {
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
dir, _ := filepath.Split(h.Name)
|
|
||||||
ctrName := filepath.Base(dir)
|
|
||||||
|
|
||||||
cs, ok := m[ctrName]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case strings.HasSuffix(h.Name, filepath.Join(ctrName, ctrName+"_contract.nef")):
|
|
||||||
cs.RawNEF, err = io.ReadAll(r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't read NEF file for %s contract: %w", ctrName, err)
|
|
||||||
}
|
|
||||||
case strings.HasSuffix(h.Name, "config.json"):
|
|
||||||
cs.RawManifest, err = io.ReadAll(r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't read manifest file for %s contract: %w", ctrName, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m[ctrName] = cs
|
|
||||||
}
|
|
||||||
|
|
||||||
for ctrName, cs := range m {
|
|
||||||
if cs.RawNEF == nil {
|
|
||||||
return nil, fmt.Errorf("NEF for %s contract wasn't found", ctrName)
|
|
||||||
}
|
|
||||||
if cs.RawManifest == nil {
|
|
||||||
return nil, fmt.Errorf("manifest for %s contract wasn't found", ctrName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getContractDeployParameters(cs *contractState, deployData []any) []any {
|
|
||||||
return []any{cs.RawNEF, cs.RawManifest, deployData}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *initializeContext) getContractDeployData(ctrName string, keysParam []any, method string) []any {
|
|
||||||
items := make([]any, 1, 6)
|
|
||||||
items[0] = false // notaryDisabled is false
|
|
||||||
|
|
||||||
switch ctrName {
|
|
||||||
case frostfsContract:
|
|
||||||
items = append(items,
|
|
||||||
c.Contracts[processingContract].Hash,
|
|
||||||
keysParam,
|
|
||||||
smartcontract.Parameter{})
|
|
||||||
case processingContract:
|
|
||||||
items = append(items, c.Contracts[frostfsContract].Hash)
|
|
||||||
return items[1:] // no notary info
|
|
||||||
case auditContract:
|
|
||||||
items = append(items, c.Contracts[netmapContract].Hash)
|
|
||||||
case balanceContract:
|
|
||||||
items = append(items,
|
|
||||||
c.Contracts[netmapContract].Hash,
|
|
||||||
c.Contracts[containerContract].Hash)
|
|
||||||
case containerContract:
|
|
||||||
// In case if NNS is updated multiple times, we can't calculate
|
|
||||||
// it's actual hash based on local data, thus query chain.
|
|
||||||
nnsCs, err := c.Client.GetContractStateByID(1)
|
|
||||||
if err != nil {
|
|
||||||
panic("NNS is not yet deployed")
|
|
||||||
}
|
|
||||||
items = append(items,
|
|
||||||
c.Contracts[netmapContract].Hash,
|
|
||||||
c.Contracts[balanceContract].Hash,
|
|
||||||
c.Contracts[frostfsIDContract].Hash,
|
|
||||||
nnsCs.Hash,
|
|
||||||
"container")
|
|
||||||
case frostfsIDContract:
|
|
||||||
items = append(items,
|
|
||||||
c.Contracts[netmapContract].Hash,
|
|
||||||
c.Contracts[containerContract].Hash)
|
|
||||||
case netmapContract:
|
|
||||||
md := getDefaultNetmapContractConfigMap()
|
|
||||||
if method == updateMethodName {
|
|
||||||
arr, err := c.getNetConfigFromNetmapContract()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
m, err := parseConfigFromNetmapContract(arr)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
for k, v := range m {
|
|
||||||
for _, key := range netmapConfigKeys {
|
|
||||||
if k == key {
|
|
||||||
md[k] = v
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var configParam []any
|
|
||||||
for k, v := range md {
|
|
||||||
configParam = append(configParam, k, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
items = append(items,
|
|
||||||
c.Contracts[balanceContract].Hash,
|
|
||||||
c.Contracts[containerContract].Hash,
|
|
||||||
keysParam,
|
|
||||||
configParam)
|
|
||||||
case proxyContract:
|
|
||||||
items = nil
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("invalid contract name: %s", ctrName))
|
|
||||||
}
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *initializeContext) getNetConfigFromNetmapContract() ([]stackitem.Item, error) {
|
|
||||||
cs, err := c.Client.GetContractStateByID(1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("NNS is not yet deployed: %w", err)
|
|
||||||
}
|
|
||||||
nmHash, err := nnsResolveHash(c.ReadOnlyInvoker, cs.Hash, netmapContract+".frostfs")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't get netmap contract hash: %w", err)
|
|
||||||
}
|
|
||||||
arr, err := unwrap.Array(c.ReadOnlyInvoker.Call(nmHash, "listConfig"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't fetch list of network config keys from the netmap contract")
|
|
||||||
}
|
|
||||||
return arr, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *initializeContext) getAlphabetDeployItems(i, n int) []any {
|
|
||||||
items := make([]any, 6)
|
|
||||||
items[0] = false
|
|
||||||
items[1] = c.Contracts[netmapContract].Hash
|
|
||||||
items[2] = c.Contracts[proxyContract].Hash
|
|
||||||
items[3] = innerring.GlagoliticLetter(i).String()
|
|
||||||
items[4] = int64(i)
|
|
||||||
items[5] = int64(n)
|
|
||||||
return items
|
|
||||||
}
|
|
|
@ -1,301 +0,0 @@
|
||||||
package morph
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-contract/nns"
|
|
||||||
morphClient "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
|
||||||
nnsClient "github.com/nspcc-dev/neo-go/pkg/rpcclient/nns"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
|
|
||||||
)
|
|
||||||
|
|
||||||
const defaultExpirationTime = 10 * 365 * 24 * time.Hour / time.Second
|
|
||||||
|
|
||||||
func (c *initializeContext) setNNS() error {
|
|
||||||
nnsCs, err := c.Client.GetContractStateByID(1)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ok, err := c.nnsRootRegistered(nnsCs.Hash, "frostfs")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if !ok {
|
|
||||||
bw := io.NewBufBinWriter()
|
|
||||||
emit.AppCall(bw.BinWriter, nnsCs.Hash, "register", callflag.All,
|
|
||||||
"frostfs", c.CommitteeAcc.Contract.ScriptHash(),
|
|
||||||
"ops@nspcc.ru", int64(3600), int64(600), int64(defaultExpirationTime), int64(3600))
|
|
||||||
emit.Opcodes(bw.BinWriter, opcode.ASSERT)
|
|
||||||
if err := c.sendCommitteeTx(bw.Bytes(), true); err != nil {
|
|
||||||
return fmt.Errorf("can't add domain root to NNS: %w", err)
|
|
||||||
}
|
|
||||||
if err := c.awaitTx(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
alphaCs := c.getContract(alphabetContract)
|
|
||||||
for i, acc := range c.Accounts {
|
|
||||||
alphaCs.Hash = state.CreateContractHash(acc.Contract.ScriptHash(), alphaCs.NEF.Checksum, alphaCs.Manifest.Name)
|
|
||||||
|
|
||||||
domain := getAlphabetNNSDomain(i)
|
|
||||||
if err := c.nnsRegisterDomain(nnsCs.Hash, alphaCs.Hash, domain); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.Command.Printf("NNS: Set %s -> %s\n", domain, alphaCs.Hash.StringLE())
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ctrName := range contractList {
|
|
||||||
cs := c.getContract(ctrName)
|
|
||||||
|
|
||||||
domain := ctrName + ".frostfs"
|
|
||||||
if err := c.nnsRegisterDomain(nnsCs.Hash, cs.Hash, domain); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.Command.Printf("NNS: Set %s -> %s\n", domain, cs.Hash.StringLE())
|
|
||||||
}
|
|
||||||
|
|
||||||
groupKey := c.ContractWallet.Accounts[0].PrivateKey().PublicKey()
|
|
||||||
err = c.updateNNSGroup(nnsCs.Hash, groupKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.Command.Printf("NNS: Set %s -> %s\n", morphClient.NNSGroupKeyName, hex.EncodeToString(groupKey.Bytes()))
|
|
||||||
|
|
||||||
return c.awaitTx()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *initializeContext) updateNNSGroup(nnsHash util.Uint160, pub *keys.PublicKey) error {
|
|
||||||
bw := io.NewBufBinWriter()
|
|
||||||
keyAlreadyAdded, domainRegCodeEmitted, err := c.emitUpdateNNSGroupScript(bw, nnsHash, pub)
|
|
||||||
if keyAlreadyAdded || err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
script := bw.Bytes()
|
|
||||||
if domainRegCodeEmitted {
|
|
||||||
w := io.NewBufBinWriter()
|
|
||||||
emit.Instruction(w.BinWriter, opcode.INITSSLOT, []byte{1})
|
|
||||||
wrapRegisterScriptWithPrice(w, nnsHash, script)
|
|
||||||
script = w.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.sendCommitteeTx(script, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// emitUpdateNNSGroupScript emits script for updating group key stored in NNS.
|
|
||||||
// First return value is true iff the key is already there and nothing should be done.
|
|
||||||
// Second return value is true iff a domain registration code was emitted.
|
|
||||||
func (c *initializeContext) emitUpdateNNSGroupScript(bw *io.BufBinWriter, nnsHash util.Uint160, pub *keys.PublicKey) (bool, bool, error) {
|
|
||||||
isAvail, err := nnsIsAvailable(c.Client, nnsHash, morphClient.NNSGroupKeyName)
|
|
||||||
if err != nil {
|
|
||||||
return false, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isAvail {
|
|
||||||
currentPub, err := nnsResolveKey(c.ReadOnlyInvoker, nnsHash, morphClient.NNSGroupKeyName)
|
|
||||||
if err != nil {
|
|
||||||
return false, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if pub.Equal(currentPub) {
|
|
||||||
return true, false, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if isAvail {
|
|
||||||
emit.AppCall(bw.BinWriter, nnsHash, "register", callflag.All,
|
|
||||||
morphClient.NNSGroupKeyName, c.CommitteeAcc.Contract.ScriptHash(),
|
|
||||||
"ops@nspcc.ru", int64(3600), int64(600), int64(defaultExpirationTime), int64(3600))
|
|
||||||
emit.Opcodes(bw.BinWriter, opcode.ASSERT)
|
|
||||||
}
|
|
||||||
|
|
||||||
emit.AppCall(bw.BinWriter, nnsHash, "deleteRecords", callflag.All, "group.frostfs", int64(nns.TXT))
|
|
||||||
emit.AppCall(bw.BinWriter, nnsHash, "addRecord", callflag.All,
|
|
||||||
"group.frostfs", int64(nns.TXT), hex.EncodeToString(pub.Bytes()))
|
|
||||||
|
|
||||||
return false, isAvail, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAlphabetNNSDomain(i int) string {
|
|
||||||
return alphabetContract + strconv.FormatUint(uint64(i), 10) + ".frostfs"
|
|
||||||
}
|
|
||||||
|
|
||||||
// wrapRegisterScriptWithPrice wraps a given script with `getPrice`/`setPrice` calls for NNS.
|
|
||||||
// It is intended to be used for a single transaction, and not as a part of other scripts.
|
|
||||||
// It is assumed that script already contains static slot initialization code, the first one
|
|
||||||
// (with index 0) is used to store the price.
|
|
||||||
func wrapRegisterScriptWithPrice(w *io.BufBinWriter, nnsHash util.Uint160, s []byte) {
|
|
||||||
if len(s) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
emit.AppCall(w.BinWriter, nnsHash, "getPrice", callflag.All)
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.STSFLD0)
|
|
||||||
emit.AppCall(w.BinWriter, nnsHash, "setPrice", callflag.All, 1)
|
|
||||||
|
|
||||||
w.WriteBytes(s)
|
|
||||||
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.LDSFLD0, opcode.PUSH1, opcode.PACK)
|
|
||||||
emit.AppCallNoArgs(w.BinWriter, nnsHash, "setPrice", callflag.All)
|
|
||||||
|
|
||||||
if w.Err != nil {
|
|
||||||
panic(fmt.Errorf("BUG: can't wrap register script: %w", w.Err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *initializeContext) nnsRegisterDomainScript(nnsHash, expectedHash util.Uint160, domain string) ([]byte, bool, error) {
|
|
||||||
ok, err := nnsIsAvailable(c.Client, nnsHash, domain)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if ok {
|
|
||||||
bw := io.NewBufBinWriter()
|
|
||||||
emit.AppCall(bw.BinWriter, nnsHash, "register", callflag.All,
|
|
||||||
domain, c.CommitteeAcc.Contract.ScriptHash(),
|
|
||||||
"ops@nspcc.ru", int64(3600), int64(600), int64(defaultExpirationTime), int64(3600))
|
|
||||||
emit.Opcodes(bw.BinWriter, opcode.ASSERT)
|
|
||||||
|
|
||||||
if bw.Err != nil {
|
|
||||||
panic(bw.Err)
|
|
||||||
}
|
|
||||||
return bw.Bytes(), false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
s, err := nnsResolveHash(c.ReadOnlyInvoker, nnsHash, domain)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
return nil, s == expectedHash, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *initializeContext) nnsRegisterDomain(nnsHash, expectedHash util.Uint160, domain string) error {
|
|
||||||
script, ok, err := c.nnsRegisterDomainScript(nnsHash, expectedHash, domain)
|
|
||||||
if ok || err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
w := io.NewBufBinWriter()
|
|
||||||
emit.Instruction(w.BinWriter, opcode.INITSSLOT, []byte{1})
|
|
||||||
wrapRegisterScriptWithPrice(w, nnsHash, script)
|
|
||||||
|
|
||||||
emit.AppCall(w.BinWriter, nnsHash, "deleteRecords", callflag.All, domain, int64(nns.TXT))
|
|
||||||
emit.AppCall(w.BinWriter, nnsHash, "addRecord", callflag.All,
|
|
||||||
domain, int64(nns.TXT), expectedHash.StringLE())
|
|
||||||
emit.AppCall(w.BinWriter, nnsHash, "addRecord", callflag.All,
|
|
||||||
domain, int64(nns.TXT), address.Uint160ToString(expectedHash))
|
|
||||||
return c.sendCommitteeTx(w.Bytes(), true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *initializeContext) nnsRootRegistered(nnsHash util.Uint160, zone string) (bool, error) {
|
|
||||||
res, err := c.CommitteeAct.Call(nnsHash, "isAvailable", "name."+zone)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.State == vmstate.Halt.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var errMissingNNSRecord = errors.New("missing NNS record")
|
|
||||||
|
|
||||||
// Returns errMissingNNSRecord if invocation fault exception contains "token not found".
|
|
||||||
func nnsResolveHash(inv *invoker.Invoker, nnsHash util.Uint160, domain string) (util.Uint160, error) {
|
|
||||||
item, err := nnsResolve(inv, nnsHash, domain)
|
|
||||||
if err != nil {
|
|
||||||
return util.Uint160{}, err
|
|
||||||
}
|
|
||||||
return parseNNSResolveResult(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
func nnsResolve(inv *invoker.Invoker, nnsHash util.Uint160, domain string) (stackitem.Item, error) {
|
|
||||||
return unwrap.Item(inv.Call(nnsHash, "resolve", domain, int64(nns.TXT)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func nnsResolveKey(inv *invoker.Invoker, nnsHash util.Uint160, domain string) (*keys.PublicKey, error) {
|
|
||||||
res, err := nnsResolve(inv, nnsHash, domain)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if _, ok := res.Value().(stackitem.Null); ok {
|
|
||||||
return nil, errors.New("NNS record is missing")
|
|
||||||
}
|
|
||||||
arr, ok := res.Value().([]stackitem.Item)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("API of the NNS contract method `resolve` has changed")
|
|
||||||
}
|
|
||||||
for i := range arr {
|
|
||||||
var bs []byte
|
|
||||||
bs, err = arr[i].TryBytes()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return keys.NewPublicKeyFromString(string(bs))
|
|
||||||
}
|
|
||||||
return nil, errors.New("no valid keys are found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseNNSResolveResult parses the result of resolving NNS record.
|
|
||||||
// It works with multiple formats (corresponding to multiple NNS versions).
|
|
||||||
// If array of hashes is provided, it returns only the first one.
|
|
||||||
func parseNNSResolveResult(res stackitem.Item) (util.Uint160, error) {
|
|
||||||
arr, ok := res.Value().([]stackitem.Item)
|
|
||||||
if !ok {
|
|
||||||
arr = []stackitem.Item{res}
|
|
||||||
}
|
|
||||||
if _, ok := res.Value().(stackitem.Null); ok || len(arr) == 0 {
|
|
||||||
return util.Uint160{}, errors.New("NNS record is missing")
|
|
||||||
}
|
|
||||||
for i := range arr {
|
|
||||||
bs, err := arr[i].TryBytes()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// We support several formats for hash encoding, this logic should be maintained in sync
|
|
||||||
// with nnsResolve from pkg/morph/client/nns.go
|
|
||||||
h, err := util.Uint160DecodeStringLE(string(bs))
|
|
||||||
if err == nil {
|
|
||||||
return h, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
h, err = address.StringToUint160(string(bs))
|
|
||||||
if err == nil {
|
|
||||||
return h, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return util.Uint160{}, errors.New("no valid hashes are found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func nnsIsAvailable(c Client, nnsHash util.Uint160, name string) (bool, error) {
|
|
||||||
switch c.(type) {
|
|
||||||
case *rpcclient.Client:
|
|
||||||
inv := invoker.New(c, nil)
|
|
||||||
reader := nnsClient.NewReader(inv, nnsHash)
|
|
||||||
return reader.IsAvailable(name)
|
|
||||||
default:
|
|
||||||
b, err := unwrap.Bool(invokeFunction(c, nnsHash, "isAvailable", []any{name}, nil))
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("`isAvailable`: invalid response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,164 +0,0 @@
|
||||||
package morph
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/native"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/neo"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
|
||||||
)
|
|
||||||
|
|
||||||
// initialAlphabetNEOAmount represents the total amount of GAS distributed between alphabet nodes.
|
|
||||||
const (
|
|
||||||
initialAlphabetNEOAmount = native.NEOTotalSupply
|
|
||||||
registerBatchSize = transaction.MaxAttributes - 1
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *initializeContext) registerCandidateRange(start, end int) error {
|
|
||||||
regPrice, err := c.getCandidateRegisterPrice()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't fetch registration price: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
w := io.NewBufBinWriter()
|
|
||||||
emit.AppCall(w.BinWriter, neo.Hash, "setRegisterPrice", callflag.States, 1)
|
|
||||||
for _, acc := range c.Accounts[start:end] {
|
|
||||||
emit.AppCall(w.BinWriter, neo.Hash, "registerCandidate", callflag.States, acc.PrivateKey().PublicKey().Bytes())
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.ASSERT)
|
|
||||||
}
|
|
||||||
emit.AppCall(w.BinWriter, neo.Hash, "setRegisterPrice", callflag.States, regPrice)
|
|
||||||
if w.Err != nil {
|
|
||||||
panic(fmt.Sprintf("BUG: %v", w.Err))
|
|
||||||
}
|
|
||||||
|
|
||||||
signers := []rpcclient.SignerAccount{{
|
|
||||||
Signer: c.getSigner(false, c.CommitteeAcc),
|
|
||||||
Account: c.CommitteeAcc,
|
|
||||||
}}
|
|
||||||
for _, acc := range c.Accounts[start:end] {
|
|
||||||
signers = append(signers, rpcclient.SignerAccount{
|
|
||||||
Signer: transaction.Signer{
|
|
||||||
Account: acc.Contract.ScriptHash(),
|
|
||||||
Scopes: transaction.CustomContracts,
|
|
||||||
AllowedContracts: []util.Uint160{neo.Hash},
|
|
||||||
},
|
|
||||||
Account: acc,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := c.Client.CreateTxFromScript(w.Bytes(), c.CommitteeAcc, -1, 0, signers)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't create tx: %w", err)
|
|
||||||
}
|
|
||||||
if err := c.multiSign(tx, committeeAccountName); err != nil {
|
|
||||||
return fmt.Errorf("can't sign a transaction: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
network := c.CommitteeAct.GetNetwork()
|
|
||||||
for _, acc := range c.Accounts[start:end] {
|
|
||||||
if err := acc.SignTx(network, tx); err != nil {
|
|
||||||
return fmt.Errorf("can't sign a transaction: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.sendTx(tx, c.Command, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *initializeContext) registerCandidates() error {
|
|
||||||
cc, err := unwrap.Array(c.ReadOnlyInvoker.Call(neo.Hash, "getCandidates"))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("`getCandidates`: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
need := len(c.Accounts)
|
|
||||||
have := len(cc)
|
|
||||||
|
|
||||||
if need == have {
|
|
||||||
c.Command.Println("Candidates are already registered.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register candidates in batches in order to overcome the signers amount limit.
|
|
||||||
// See: https://github.com/nspcc-dev/neo-go/blob/master/pkg/core/transaction/transaction.go#L27
|
|
||||||
for i := 0; i < need; i += registerBatchSize {
|
|
||||||
start, end := i, i+registerBatchSize
|
|
||||||
if end > need {
|
|
||||||
end = need
|
|
||||||
}
|
|
||||||
// This check is sound because transactions are accepted/rejected atomically.
|
|
||||||
if have >= end {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := c.registerCandidateRange(start, end); err != nil {
|
|
||||||
return fmt.Errorf("registering candidates %d..%d: %q", start, end-1, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *initializeContext) transferNEOToAlphabetContracts() error {
|
|
||||||
neoHash := neo.Hash
|
|
||||||
|
|
||||||
ok, err := c.transferNEOFinished(neoHash)
|
|
||||||
if ok || err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cs := c.getContract(alphabetContract)
|
|
||||||
amount := initialAlphabetNEOAmount / len(c.Wallets)
|
|
||||||
|
|
||||||
bw := io.NewBufBinWriter()
|
|
||||||
for _, acc := range c.Accounts {
|
|
||||||
h := state.CreateContractHash(acc.Contract.ScriptHash(), cs.NEF.Checksum, cs.Manifest.Name)
|
|
||||||
emit.AppCall(bw.BinWriter, neoHash, "transfer", callflag.All,
|
|
||||||
c.CommitteeAcc.Contract.ScriptHash(), h, int64(amount), nil)
|
|
||||||
emit.Opcodes(bw.BinWriter, opcode.ASSERT)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.sendCommitteeTx(bw.Bytes(), false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.awaitTx()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *initializeContext) transferNEOFinished(neoHash util.Uint160) (bool, error) {
|
|
||||||
bal, err := c.Client.NEP17BalanceOf(neoHash, c.CommitteeAcc.Contract.ScriptHash())
|
|
||||||
return bal < native.NEOTotalSupply, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var errGetPriceInvalid = errors.New("`getRegisterPrice`: invalid response")
|
|
||||||
|
|
||||||
func (c *initializeContext) getCandidateRegisterPrice() (int64, error) {
|
|
||||||
switch c.Client.(type) {
|
|
||||||
case *rpcclient.Client:
|
|
||||||
inv := invoker.New(c.Client, nil)
|
|
||||||
reader := neo.NewReader(inv)
|
|
||||||
return reader.GetRegisterPrice()
|
|
||||||
default:
|
|
||||||
neoHash := neo.Hash
|
|
||||||
res, err := invokeFunction(c.Client, neoHash, "getRegisterPrice", nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if len(res.Stack) == 0 {
|
|
||||||
return 0, errGetPriceInvalid
|
|
||||||
}
|
|
||||||
bi, err := res.Stack[0].TryInteger()
|
|
||||||
if err != nil || !bi.IsInt64() {
|
|
||||||
return 0, errGetPriceInvalid
|
|
||||||
}
|
|
||||||
return bi.Int64(), nil
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,141 +0,0 @@
|
||||||
package morph
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/innerring"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/config"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
contractsPath = "../../../../../../frostfs-contract/frostfs-contract-v0.16.0.tar.gz"
|
|
||||||
protoFileName = "proto.yml"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestInitialize(t *testing.T) {
|
|
||||||
// This test needs frostfs-contract tarball, so it is skipped by default.
|
|
||||||
// It is here for performing local testing after the changes.
|
|
||||||
t.Skip()
|
|
||||||
|
|
||||||
t.Run("1 nodes", func(t *testing.T) {
|
|
||||||
testInitialize(t, 1)
|
|
||||||
})
|
|
||||||
t.Run("4 nodes", func(t *testing.T) {
|
|
||||||
testInitialize(t, 4)
|
|
||||||
})
|
|
||||||
t.Run("7 nodes", func(t *testing.T) {
|
|
||||||
testInitialize(t, 7)
|
|
||||||
})
|
|
||||||
t.Run("16 nodes", func(t *testing.T) {
|
|
||||||
testInitialize(t, 16)
|
|
||||||
})
|
|
||||||
t.Run("max nodes", func(t *testing.T) {
|
|
||||||
testInitialize(t, maxAlphabetNodes)
|
|
||||||
})
|
|
||||||
t.Run("too many nodes", func(t *testing.T) {
|
|
||||||
require.ErrorIs(t, generateTestData(t, t.TempDir(), maxAlphabetNodes+1), ErrTooManyAlphabetNodes)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func testInitialize(t *testing.T, committeeSize int) {
|
|
||||||
testdataDir := t.TempDir()
|
|
||||||
v := viper.GetViper()
|
|
||||||
|
|
||||||
require.NoError(t, generateTestData(t, testdataDir, committeeSize))
|
|
||||||
v.Set(protoConfigPath, filepath.Join(testdataDir, protoFileName))
|
|
||||||
|
|
||||||
// Set to the path or remove the next statement to download from the network.
|
|
||||||
require.NoError(t, initCmd.Flags().Set(contractsInitFlag, contractsPath))
|
|
||||||
v.Set(localDumpFlag, filepath.Join(testdataDir, "out"))
|
|
||||||
v.Set(alphabetWalletsFlag, testdataDir)
|
|
||||||
v.Set(epochDurationInitFlag, 1)
|
|
||||||
v.Set(maxObjectSizeInitFlag, 1024)
|
|
||||||
|
|
||||||
setTestCredentials(v, committeeSize)
|
|
||||||
require.NoError(t, initializeSideChainCmd(initCmd, nil))
|
|
||||||
|
|
||||||
t.Run("force-new-epoch", func(t *testing.T) {
|
|
||||||
require.NoError(t, forceNewEpochCmd(forceNewEpoch, nil))
|
|
||||||
})
|
|
||||||
t.Run("set-config", func(t *testing.T) {
|
|
||||||
require.NoError(t, setConfigCmd(setConfig, []string{"MaintenanceModeAllowed=true"}))
|
|
||||||
})
|
|
||||||
t.Run("set-policy", func(t *testing.T) {
|
|
||||||
require.NoError(t, setPolicyCmd(setPolicy, []string{"ExecFeeFactor=1"}))
|
|
||||||
})
|
|
||||||
t.Run("remove-node", func(t *testing.T) {
|
|
||||||
pk, err := keys.NewPrivateKey()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
pub := hex.EncodeToString(pk.PublicKey().Bytes())
|
|
||||||
require.NoError(t, removeNodesCmd(removeNodes, []string{pub}))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateTestData(t *testing.T, dir string, size int) error {
|
|
||||||
v := viper.GetViper()
|
|
||||||
v.Set(alphabetWalletsFlag, dir)
|
|
||||||
|
|
||||||
sizeStr := strconv.FormatUint(uint64(size), 10)
|
|
||||||
if err := generateAlphabetCmd.Flags().Set(alphabetSizeFlag, sizeStr); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
setTestCredentials(v, size)
|
|
||||||
if err := generateAlphabetCreds(generateAlphabetCmd, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var pubs []string
|
|
||||||
for i := 0; i < size; i++ {
|
|
||||||
p := filepath.Join(dir, innerring.GlagoliticLetter(i).String()+".json")
|
|
||||||
w, err := wallet.NewWalletFromFile(p)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("wallet doesn't exist: %w", err)
|
|
||||||
}
|
|
||||||
for _, acc := range w.Accounts {
|
|
||||||
if acc.Label == singleAccountName {
|
|
||||||
pub, ok := vm.ParseSignatureContract(acc.Contract.Script)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("could not parse signature script for %s", acc.Address)
|
|
||||||
}
|
|
||||||
pubs = append(pubs, hex.EncodeToString(pub))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := config.Config{}
|
|
||||||
cfg.ProtocolConfiguration.Magic = 12345
|
|
||||||
cfg.ProtocolConfiguration.ValidatorsCount = uint32(size)
|
|
||||||
cfg.ProtocolConfiguration.TimePerBlock = time.Second
|
|
||||||
cfg.ProtocolConfiguration.StandbyCommittee = pubs // sorted by glagolic letters
|
|
||||||
cfg.ProtocolConfiguration.P2PSigExtensions = true
|
|
||||||
cfg.ProtocolConfiguration.VerifyTransactions = true
|
|
||||||
data, err := yaml.Marshal(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
protoPath := filepath.Join(dir, protoFileName)
|
|
||||||
return os.WriteFile(protoPath, data, os.ModePerm)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setTestCredentials(v *viper.Viper, size int) {
|
|
||||||
for i := 0; i < size; i++ {
|
|
||||||
v.Set("credentials."+innerring.GlagoliticLetter(i).String(), strconv.FormatUint(uint64(i), 10))
|
|
||||||
}
|
|
||||||
v.Set("credentials.contract", testContractPassword)
|
|
||||||
}
|
|
|
@ -1,505 +0,0 @@
|
||||||
package morph
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/elliptic"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/config"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/block"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/chaindump"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/fee"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/storage"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/network/payload"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
type localClient struct {
|
|
||||||
bc *core.Blockchain
|
|
||||||
transactions []*transaction.Transaction
|
|
||||||
dumpPath string
|
|
||||||
accounts []*wallet.Account
|
|
||||||
maxGasInvoke int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func newLocalClient(cmd *cobra.Command, v *viper.Viper, wallets []*wallet.Wallet) (*localClient, error) {
|
|
||||||
cfg, err := config.LoadFile(v.GetString(protoConfigPath))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
bc, err := core.NewBlockchain(storage.NewMemoryStore(), cfg.Blockchain(), zap.NewNop())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
m := smartcontract.GetDefaultHonestNodeCount(int(cfg.ProtocolConfiguration.ValidatorsCount))
|
|
||||||
accounts := make([]*wallet.Account, len(wallets))
|
|
||||||
for i := range accounts {
|
|
||||||
accounts[i], err = getWalletAccount(wallets[i], consensusAccountName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
indexMap := make(map[string]int)
|
|
||||||
for i, pub := range cfg.ProtocolConfiguration.StandbyCommittee {
|
|
||||||
indexMap[pub] = i
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(accounts, func(i, j int) bool {
|
|
||||||
pi := accounts[i].PrivateKey().PublicKey().Bytes()
|
|
||||||
pj := accounts[j].PrivateKey().PublicKey().Bytes()
|
|
||||||
return indexMap[string(pi)] < indexMap[string(pj)]
|
|
||||||
})
|
|
||||||
sort.Slice(accounts[:cfg.ProtocolConfiguration.ValidatorsCount], func(i, j int) bool {
|
|
||||||
return accounts[i].PublicKey().Cmp(accounts[j].PublicKey()) == -1
|
|
||||||
})
|
|
||||||
|
|
||||||
go bc.Run()
|
|
||||||
|
|
||||||
dumpPath := v.GetString(localDumpFlag)
|
|
||||||
if cmd.Name() != "init" {
|
|
||||||
f, err := os.OpenFile(dumpPath, os.O_RDONLY, 0600)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't open local dump: %w", err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
r := io.NewBinReaderFromIO(f)
|
|
||||||
|
|
||||||
var skip uint32
|
|
||||||
if bc.BlockHeight() != 0 {
|
|
||||||
skip = bc.BlockHeight() + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
count := r.ReadU32LE() - skip
|
|
||||||
if err := chaindump.Restore(bc, r, skip, count, nil); err != nil {
|
|
||||||
return nil, fmt.Errorf("can't restore local dump: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &localClient{
|
|
||||||
bc: bc,
|
|
||||||
dumpPath: dumpPath,
|
|
||||||
accounts: accounts[:m],
|
|
||||||
maxGasInvoke: 15_0000_0000,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *localClient) GetBlockCount() (uint32, error) {
|
|
||||||
return l.bc.BlockHeight(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *localClient) GetContractStateByID(id int32) (*state.Contract, error) {
|
|
||||||
h, err := l.bc.GetContractScriptHash(id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return l.GetContractStateByHash(h)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *localClient) GetContractStateByHash(h util.Uint160) (*state.Contract, error) {
|
|
||||||
if cs := l.bc.GetContractState(h); cs != nil {
|
|
||||||
return cs, nil
|
|
||||||
}
|
|
||||||
return nil, storage.ErrKeyNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *localClient) GetNativeContracts() ([]state.NativeContract, error) {
|
|
||||||
return l.bc.GetNatives(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *localClient) GetNetwork() (netmode.Magic, error) {
|
|
||||||
return l.bc.GetConfig().Magic, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *localClient) GetApplicationLog(h util.Uint256, t *trigger.Type) (*result.ApplicationLog, error) {
|
|
||||||
aer, err := l.bc.GetAppExecResults(h, *t)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
a := result.NewApplicationLog(h, aer, *t)
|
|
||||||
return &a, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *localClient) CreateTxFromScript(script []byte, acc *wallet.Account, sysFee int64, netFee int64, cosigners []rpcclient.SignerAccount) (*transaction.Transaction, error) {
|
|
||||||
signers, accounts, err := getSigners(acc, cosigners)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to construct tx signers: %w", err)
|
|
||||||
}
|
|
||||||
if sysFee < 0 {
|
|
||||||
res, err := l.InvokeScript(script, signers)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't add system fee to transaction: %w", err)
|
|
||||||
}
|
|
||||||
if res.State != "HALT" {
|
|
||||||
return nil, fmt.Errorf("can't add system fee to transaction: bad vm state: %s due to an error: %s", res.State, res.FaultException)
|
|
||||||
}
|
|
||||||
sysFee = res.GasConsumed
|
|
||||||
}
|
|
||||||
|
|
||||||
tx := transaction.New(script, sysFee)
|
|
||||||
tx.Signers = signers
|
|
||||||
tx.ValidUntilBlock = l.bc.BlockHeight() + 2
|
|
||||||
|
|
||||||
err = l.AddNetworkFee(tx, netFee, accounts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to add network fee: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *localClient) GetCommittee() (keys.PublicKeys, error) {
|
|
||||||
// not used by `morph init` command
|
|
||||||
panic("unexpected call")
|
|
||||||
}
|
|
||||||
|
|
||||||
// InvokeFunction is implemented via `InvokeScript`.
|
|
||||||
func (l *localClient) InvokeFunction(h util.Uint160, method string, sPrm []smartcontract.Parameter, ss []transaction.Signer) (*result.Invoke, error) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
pp := make([]any, len(sPrm))
|
|
||||||
for i, p := range sPrm {
|
|
||||||
pp[i], err = smartcontract.ExpandParameterToEmitable(p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("incorrect parameter type %s: %w", p.Type, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return invokeFunction(l, h, method, pp, ss)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *localClient) CalculateNotaryFee(_ uint8) (int64, error) {
|
|
||||||
// not used by `morph init` command
|
|
||||||
panic("unexpected call")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *localClient) SignAndPushP2PNotaryRequest(_ *transaction.Transaction, _ []byte, _ int64, _ int64, _ uint32, _ *wallet.Account) (*payload.P2PNotaryRequest, error) {
|
|
||||||
// not used by `morph init` command
|
|
||||||
panic("unexpected call")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *localClient) SignAndPushInvocationTx(_ []byte, _ *wallet.Account, _ int64, _ fixedn.Fixed8, _ []rpcclient.SignerAccount) (util.Uint256, error) {
|
|
||||||
// not used by `morph init` command
|
|
||||||
panic("unexpected call")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *localClient) TerminateSession(_ uuid.UUID) (bool, error) {
|
|
||||||
// not used by `morph init` command
|
|
||||||
panic("unexpected call")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *localClient) TraverseIterator(_, _ uuid.UUID, _ int) ([]stackitem.Item, error) {
|
|
||||||
// not used by `morph init` command
|
|
||||||
panic("unexpected call")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetVersion return default version.
|
|
||||||
func (l *localClient) GetVersion() (*result.Version, error) {
|
|
||||||
c := l.bc.GetConfig()
|
|
||||||
return &result.Version{
|
|
||||||
Protocol: result.Protocol{
|
|
||||||
AddressVersion: address.NEO3Prefix,
|
|
||||||
Network: c.Magic,
|
|
||||||
MillisecondsPerBlock: int(c.TimePerBlock / time.Millisecond),
|
|
||||||
MaxTraceableBlocks: c.MaxTraceableBlocks,
|
|
||||||
MaxValidUntilBlockIncrement: c.MaxValidUntilBlockIncrement,
|
|
||||||
MaxTransactionsPerBlock: c.MaxTransactionsPerBlock,
|
|
||||||
MemoryPoolMaxTransactions: c.MemPoolSize,
|
|
||||||
ValidatorsCount: byte(c.ValidatorsCount),
|
|
||||||
InitialGasDistribution: c.InitialGASSupply,
|
|
||||||
CommitteeHistory: c.CommitteeHistory,
|
|
||||||
P2PSigExtensions: c.P2PSigExtensions,
|
|
||||||
StateRootInHeader: c.StateRootInHeader,
|
|
||||||
ValidatorsHistory: c.ValidatorsHistory,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *localClient) InvokeContractVerify(util.Uint160, []smartcontract.Parameter, []transaction.Signer, ...transaction.Witness) (*result.Invoke, error) {
|
|
||||||
// not used by `morph init` command
|
|
||||||
panic("unexpected call")
|
|
||||||
}
|
|
||||||
|
|
||||||
// CalculateNetworkFee calculates network fee for the given transaction.
|
|
||||||
// Copied from neo-go with minor corrections (no need to support non-notary mode):
|
|
||||||
// https://github.com/nspcc-dev/neo-go/blob/v0.99.2/pkg/services/rpcsrv/server.go#L744
|
|
||||||
func (l *localClient) CalculateNetworkFee(tx *transaction.Transaction) (int64, error) {
|
|
||||||
hashablePart, err := tx.EncodeHashableFields()
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("failed to compute tx size: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
size := len(hashablePart) + io.GetVarSize(len(tx.Signers))
|
|
||||||
ef := l.bc.GetBaseExecFee()
|
|
||||||
|
|
||||||
var netFee int64
|
|
||||||
for i, signer := range tx.Signers {
|
|
||||||
var verificationScript []byte
|
|
||||||
for _, w := range tx.Scripts {
|
|
||||||
if w.VerificationScript != nil && hash.Hash160(w.VerificationScript).Equals(signer.Account) {
|
|
||||||
verificationScript = w.VerificationScript
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if verificationScript == nil {
|
|
||||||
gasConsumed, err := l.bc.VerifyWitness(signer.Account, tx, &tx.Scripts[i], l.maxGasInvoke)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("invalid signature: %w", err)
|
|
||||||
}
|
|
||||||
netFee += gasConsumed
|
|
||||||
size += io.GetVarSize([]byte{}) + io.GetVarSize(tx.Scripts[i].InvocationScript)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
fee, sizeDelta := fee.Calculate(ef, verificationScript)
|
|
||||||
netFee += fee
|
|
||||||
size += sizeDelta
|
|
||||||
}
|
|
||||||
|
|
||||||
fee := l.bc.FeePerByte()
|
|
||||||
netFee += int64(size) * fee
|
|
||||||
|
|
||||||
return netFee, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddNetworkFee adds network fee for each witness script and optional extra
|
|
||||||
// network fee to transaction. `accs` is an array signer's accounts.
|
|
||||||
// Copied from neo-go with minor corrections (no need to support contract signers):
|
|
||||||
// https://github.com/nspcc-dev/neo-go/blob/6ff11baa1b9e4c71ef0d1de43b92a8c541ca732c/pkg/rpc/client/rpc.go#L960
|
|
||||||
func (l *localClient) AddNetworkFee(tx *transaction.Transaction, extraFee int64, accs ...*wallet.Account) error {
|
|
||||||
if len(tx.Signers) != len(accs) {
|
|
||||||
return errors.New("number of signers must match number of scripts")
|
|
||||||
}
|
|
||||||
|
|
||||||
size := io.GetVarSize(tx)
|
|
||||||
ef := l.bc.GetBaseExecFee()
|
|
||||||
for i := range tx.Signers {
|
|
||||||
netFee, sizeDelta := fee.Calculate(ef, accs[i].Contract.Script)
|
|
||||||
tx.NetworkFee += netFee
|
|
||||||
size += sizeDelta
|
|
||||||
}
|
|
||||||
|
|
||||||
tx.NetworkFee += int64(size)*l.bc.FeePerByte() + extraFee
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getSigners returns an array of transaction signers and corresponding accounts from
|
|
||||||
// given sender and cosigners. If cosigners list already contains sender, the sender
|
|
||||||
// will be placed at the start of the list.
|
|
||||||
// Copied from neo-go with minor corrections:
|
|
||||||
// https://github.com/nspcc-dev/neo-go/blob/6ff11baa1b9e4c71ef0d1de43b92a8c541ca732c/pkg/rpc/client/rpc.go#L735
|
|
||||||
func getSigners(sender *wallet.Account, cosigners []rpcclient.SignerAccount) ([]transaction.Signer, []*wallet.Account, error) {
|
|
||||||
var (
|
|
||||||
signers []transaction.Signer
|
|
||||||
accounts []*wallet.Account
|
|
||||||
)
|
|
||||||
|
|
||||||
from := sender.Contract.ScriptHash()
|
|
||||||
s := transaction.Signer{
|
|
||||||
Account: from,
|
|
||||||
Scopes: transaction.None,
|
|
||||||
}
|
|
||||||
for _, c := range cosigners {
|
|
||||||
if c.Signer.Account == from {
|
|
||||||
s = c.Signer
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
signers = append(signers, c.Signer)
|
|
||||||
accounts = append(accounts, c.Account)
|
|
||||||
}
|
|
||||||
signers = append([]transaction.Signer{s}, signers...)
|
|
||||||
accounts = append([]*wallet.Account{sender}, accounts...)
|
|
||||||
return signers, accounts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *localClient) NEP17BalanceOf(h util.Uint160, acc util.Uint160) (int64, error) {
|
|
||||||
res, err := invokeFunction(l, h, "balanceOf", []any{acc}, nil)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if res.State != vmstate.Halt.String() || len(res.Stack) == 0 {
|
|
||||||
return 0, fmt.Errorf("`balance`: invalid response (empty: %t): %s",
|
|
||||||
len(res.Stack) == 0, res.FaultException)
|
|
||||||
}
|
|
||||||
bi, err := res.Stack[0].TryInteger()
|
|
||||||
if err != nil || !bi.IsInt64() {
|
|
||||||
return 0, fmt.Errorf("`balance`: invalid response")
|
|
||||||
}
|
|
||||||
return bi.Int64(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *localClient) InvokeScript(script []byte, signers []transaction.Signer) (*result.Invoke, error) {
|
|
||||||
lastBlock, err := l.bc.GetBlock(l.bc.CurrentBlockHash())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tx := transaction.New(script, 0)
|
|
||||||
tx.Signers = signers
|
|
||||||
tx.ValidUntilBlock = l.bc.BlockHeight() + 2
|
|
||||||
|
|
||||||
ic, err := l.bc.GetTestVM(trigger.Application, tx, &block.Block{
|
|
||||||
Header: block.Header{
|
|
||||||
Index: lastBlock.Index + 1,
|
|
||||||
Timestamp: lastBlock.Timestamp + 1,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("get test VM: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ic.VM.GasLimit = 100_0000_0000
|
|
||||||
ic.VM.LoadScriptWithFlags(script, callflag.All)
|
|
||||||
|
|
||||||
var errStr string
|
|
||||||
if err := ic.VM.Run(); err != nil {
|
|
||||||
errStr = err.Error()
|
|
||||||
}
|
|
||||||
return &result.Invoke{
|
|
||||||
State: ic.VM.State().String(),
|
|
||||||
GasConsumed: ic.VM.GasConsumed(),
|
|
||||||
Script: script,
|
|
||||||
Stack: ic.VM.Estack().ToArray(),
|
|
||||||
FaultException: errStr,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *localClient) SendRawTransaction(tx *transaction.Transaction) (util.Uint256, error) {
|
|
||||||
// We need to test that transaction was formed correctly to catch as many errors as we can.
|
|
||||||
bs := tx.Bytes()
|
|
||||||
_, err := transaction.NewTransactionFromBytes(bs)
|
|
||||||
if err != nil {
|
|
||||||
return tx.Hash(), fmt.Errorf("invalid transaction: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.transactions = append(l.transactions, tx)
|
|
||||||
return tx.Hash(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *localClient) putTransactions() error {
|
|
||||||
// 1. Prepare new block.
|
|
||||||
lastBlock, err := l.bc.GetBlock(l.bc.CurrentBlockHash())
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer func() { l.transactions = l.transactions[:0] }()
|
|
||||||
|
|
||||||
b := &block.Block{
|
|
||||||
Header: block.Header{
|
|
||||||
NextConsensus: l.accounts[0].Contract.ScriptHash(),
|
|
||||||
Script: transaction.Witness{
|
|
||||||
VerificationScript: l.accounts[0].Contract.Script,
|
|
||||||
},
|
|
||||||
Timestamp: lastBlock.Timestamp + 1,
|
|
||||||
},
|
|
||||||
Transactions: l.transactions,
|
|
||||||
}
|
|
||||||
|
|
||||||
if l.bc.GetConfig().StateRootInHeader {
|
|
||||||
b.StateRootEnabled = true
|
|
||||||
b.PrevStateRoot = l.bc.GetStateModule().CurrentLocalStateRoot()
|
|
||||||
}
|
|
||||||
b.PrevHash = lastBlock.Hash()
|
|
||||||
b.Index = lastBlock.Index + 1
|
|
||||||
b.RebuildMerkleRoot()
|
|
||||||
|
|
||||||
// 2. Sign prepared block.
|
|
||||||
var invocationScript []byte
|
|
||||||
|
|
||||||
magic := l.bc.GetConfig().Magic
|
|
||||||
for _, acc := range l.accounts {
|
|
||||||
sign := acc.PrivateKey().SignHashable(uint32(magic), b)
|
|
||||||
invocationScript = append(invocationScript, byte(opcode.PUSHDATA1), 64)
|
|
||||||
invocationScript = append(invocationScript, sign...)
|
|
||||||
}
|
|
||||||
b.Script.InvocationScript = invocationScript
|
|
||||||
|
|
||||||
// 3. Persist block.
|
|
||||||
return l.bc.AddBlock(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func invokeFunction(c Client, h util.Uint160, method string, parameters []any, signers []transaction.Signer) (*result.Invoke, error) {
|
|
||||||
w := io.NewBufBinWriter()
|
|
||||||
emit.Array(w.BinWriter, parameters...)
|
|
||||||
emit.AppCallNoArgs(w.BinWriter, h, method, callflag.All)
|
|
||||||
if w.Err != nil {
|
|
||||||
panic(fmt.Sprintf("BUG: invalid parameters for '%s': %v", method, w.Err))
|
|
||||||
}
|
|
||||||
return c.InvokeScript(w.Bytes(), signers)
|
|
||||||
}
|
|
||||||
|
|
||||||
var errGetDesignatedByRoleResponse = errors.New("`getDesignatedByRole`: invalid response")
|
|
||||||
|
|
||||||
func getDesignatedByRole(inv *invoker.Invoker, h util.Uint160, role noderoles.Role, u uint32) (keys.PublicKeys, error) {
|
|
||||||
arr, err := unwrap.Array(inv.Call(h, "getDesignatedByRole", int64(role), int64(u)))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errGetDesignatedByRoleResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
pubs := make(keys.PublicKeys, len(arr))
|
|
||||||
for i := range arr {
|
|
||||||
bs, err := arr[i].TryBytes()
|
|
||||||
if err != nil {
|
|
||||||
return nil, errGetDesignatedByRoleResponse
|
|
||||||
}
|
|
||||||
pubs[i], err = keys.NewPublicKeyFromBytes(bs, elliptic.P256())
|
|
||||||
if err != nil {
|
|
||||||
return nil, errGetDesignatedByRoleResponse
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pubs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *localClient) dump() (err error) {
|
|
||||||
defer l.bc.Close()
|
|
||||||
|
|
||||||
f, err := os.Create(l.dumpPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
closeErr := f.Close()
|
|
||||||
if err == nil && closeErr != nil {
|
|
||||||
err = closeErr
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
w := io.NewBinWriterFromIO(f)
|
|
||||||
w.WriteU32LE(l.bc.BlockHeight() + 1)
|
|
||||||
err = chaindump.Dump(l.bc, w, 0, l.bc.BlockHeight()+1)
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -1,122 +0,0 @@
|
||||||
package morph
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/network/payload"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Client represents N3 client interface capable of test-invoking scripts
|
|
||||||
// and sending signed transactions to chain.
|
|
||||||
type Client interface {
|
|
||||||
invoker.RPCInvoke
|
|
||||||
|
|
||||||
GetBlockCount() (uint32, error)
|
|
||||||
GetContractStateByID(int32) (*state.Contract, error)
|
|
||||||
GetContractStateByHash(util.Uint160) (*state.Contract, error)
|
|
||||||
GetNativeContracts() ([]state.NativeContract, error)
|
|
||||||
GetNetwork() (netmode.Magic, error)
|
|
||||||
GetApplicationLog(util.Uint256, *trigger.Type) (*result.ApplicationLog, error)
|
|
||||||
GetVersion() (*result.Version, error)
|
|
||||||
CreateTxFromScript([]byte, *wallet.Account, int64, int64, []rpcclient.SignerAccount) (*transaction.Transaction, error)
|
|
||||||
NEP17BalanceOf(util.Uint160, util.Uint160) (int64, error)
|
|
||||||
SendRawTransaction(*transaction.Transaction) (util.Uint256, error)
|
|
||||||
GetCommittee() (keys.PublicKeys, error)
|
|
||||||
CalculateNotaryFee(uint8) (int64, error)
|
|
||||||
CalculateNetworkFee(tx *transaction.Transaction) (int64, error)
|
|
||||||
AddNetworkFee(*transaction.Transaction, int64, ...*wallet.Account) error
|
|
||||||
SignAndPushInvocationTx([]byte, *wallet.Account, int64, fixedn.Fixed8, []rpcclient.SignerAccount) (util.Uint256, error)
|
|
||||||
SignAndPushP2PNotaryRequest(*transaction.Transaction, []byte, int64, int64, uint32, *wallet.Account) (*payload.P2PNotaryRequest, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type hashVUBPair struct {
|
|
||||||
hash util.Uint256
|
|
||||||
vub uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
type clientContext struct {
|
|
||||||
Client Client // a raw neo-go client OR a local chain implementation
|
|
||||||
CommitteeAct *actor.Actor // committee actor with the Global witness scope
|
|
||||||
ReadOnlyInvoker *invoker.Invoker // R/O contract invoker, does not contain any signer
|
|
||||||
SentTxs []hashVUBPair
|
|
||||||
}
|
|
||||||
|
|
||||||
func getN3Client(v *viper.Viper) (Client, error) {
|
|
||||||
// number of opened connections
|
|
||||||
// by neo-go client per one host
|
|
||||||
const (
|
|
||||||
maxConnsPerHost = 10
|
|
||||||
requestTimeout = time.Second * 10
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
endpoint := v.GetString(endpointFlag)
|
|
||||||
if endpoint == "" {
|
|
||||||
return nil, errors.New("missing endpoint")
|
|
||||||
}
|
|
||||||
c, err := rpcclient.New(ctx, endpoint, rpcclient.Options{
|
|
||||||
MaxConnsPerHost: maxConnsPerHost,
|
|
||||||
RequestTimeout: requestTimeout,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := c.Init(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultClientContext(c Client, committeeAcc *wallet.Account) (*clientContext, error) {
|
|
||||||
commAct, err := actor.New(c, []actor.SignerAccount{{
|
|
||||||
Signer: transaction.Signer{
|
|
||||||
Account: committeeAcc.Contract.ScriptHash(),
|
|
||||||
Scopes: transaction.Global,
|
|
||||||
},
|
|
||||||
Account: committeeAcc,
|
|
||||||
}})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &clientContext{
|
|
||||||
Client: c,
|
|
||||||
CommitteeAct: commAct,
|
|
||||||
ReadOnlyInvoker: invoker.New(c, nil),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *clientContext) sendTx(tx *transaction.Transaction, cmd *cobra.Command, await bool) error {
|
|
||||||
h, err := c.Client.SendRawTransaction(tx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if h != tx.Hash() {
|
|
||||||
return fmt.Errorf("sent and actual tx hashes mismatch:\n\tsent: %v\n\tactual: %v", tx.Hash().StringLE(), h.StringLE())
|
|
||||||
}
|
|
||||||
|
|
||||||
c.SentTxs = append(c.SentTxs, hashVUBPair{hash: h, vub: tx.ValidUntilBlock})
|
|
||||||
|
|
||||||
if await {
|
|
||||||
return c.awaitTx(cmd)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
package morph
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/netmap"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
func listNetmapCandidatesNodes(cmd *cobra.Command, _ []string) {
|
|
||||||
c, err := getN3Client(viper.GetViper())
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't create N3 client: %w", err)
|
|
||||||
|
|
||||||
inv := invoker.New(c, nil)
|
|
||||||
|
|
||||||
cs, err := c.GetContractStateByID(1)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't get NNS contract info: %w", err)
|
|
||||||
|
|
||||||
nmHash, err := nnsResolveHash(inv, cs.Hash, netmapContract+".frostfs")
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't get netmap contract hash: %w", err)
|
|
||||||
|
|
||||||
res, err := inv.Call(nmHash, "netmapCandidates")
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't fetch list of network config keys from the netmap contract", err)
|
|
||||||
nm, err := netmap.DecodeNetMap(res.Stack)
|
|
||||||
commonCmd.ExitOnErr(cmd, "unable to decode netmap: %w", err)
|
|
||||||
commonCmd.PrettyPrintNetMap(cmd, *nm, !viper.GetBool(commonflags.Verbose))
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
package morph
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/netmap"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getDefaultNetmapContractConfigMap() map[string]any {
|
|
||||||
m := make(map[string]any)
|
|
||||||
m[netmap.EpochDurationConfig] = viper.GetInt64(epochDurationInitFlag)
|
|
||||||
m[netmap.MaxObjectSizeConfig] = viper.GetInt64(maxObjectSizeInitFlag)
|
|
||||||
m[netmap.AuditFeeConfig] = viper.GetInt64(auditFeeInitFlag)
|
|
||||||
m[netmap.ContainerFeeConfig] = viper.GetInt64(containerFeeInitFlag)
|
|
||||||
m[netmap.ContainerAliasFeeConfig] = viper.GetInt64(containerAliasFeeInitFlag)
|
|
||||||
m[netmap.BasicIncomeRateConfig] = viper.GetInt64(incomeRateInitFlag)
|
|
||||||
m[netmap.IrCandidateFeeConfig] = viper.GetInt64(candidateFeeInitFlag)
|
|
||||||
m[netmap.WithdrawFeeConfig] = viper.GetInt64(withdrawFeeInitFlag)
|
|
||||||
m[netmap.HomomorphicHashingDisabledKey] = viper.GetBool(homomorphicHashDisabledInitFlag)
|
|
||||||
m[netmap.MaintenanceModeAllowedConfig] = viper.GetBool(maintenanceModeAllowedInitFlag)
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseConfigFromNetmapContract(arr []stackitem.Item) (map[string][]byte, error) {
|
|
||||||
m := make(map[string][]byte, len(arr))
|
|
||||||
for _, param := range arr {
|
|
||||||
tuple, ok := param.Value().([]stackitem.Item)
|
|
||||||
if !ok || len(tuple) != 2 {
|
|
||||||
return nil, errors.New("invalid ListConfig response from netmap contract")
|
|
||||||
}
|
|
||||||
|
|
||||||
k, err := tuple[0].TryBytes()
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("invalid config key from netmap contract")
|
|
||||||
}
|
|
||||||
|
|
||||||
v, err := tuple[1].TryBytes()
|
|
||||||
if err != nil {
|
|
||||||
return nil, invalidConfigValueErr(string(k))
|
|
||||||
}
|
|
||||||
m[string(k)] = v
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
package morph
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
netmapcontract "git.frostfs.info/TrueCloudLab/frostfs-contract/netmap"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
func removeNodesCmd(cmd *cobra.Command, args []string) error {
|
|
||||||
if len(args) == 0 {
|
|
||||||
return errors.New("at least one node key must be provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeKeys := make(keys.PublicKeys, len(args))
|
|
||||||
for i := range args {
|
|
||||||
var err error
|
|
||||||
nodeKeys[i], err = keys.NewPublicKeyFromString(args[i])
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't parse node public key: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wCtx, err := newInitializeContext(cmd, viper.GetViper())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't initialize context: %w", err)
|
|
||||||
}
|
|
||||||
defer wCtx.close()
|
|
||||||
|
|
||||||
cs, err := wCtx.Client.GetContractStateByID(1)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't get NNS contract info: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
nmHash, err := nnsResolveHash(wCtx.ReadOnlyInvoker, cs.Hash, netmapContract+".frostfs")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't get netmap contract hash: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bw := io.NewBufBinWriter()
|
|
||||||
for i := range nodeKeys {
|
|
||||||
emit.AppCall(bw.BinWriter, nmHash, "updateStateIR", callflag.All,
|
|
||||||
int64(netmapcontract.NodeStateOffline), nodeKeys[i].Bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := emitNewEpochCall(bw, wCtx, nmHash); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := wCtx.sendConsensusTx(bw.Bytes()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return wCtx.awaitTx()
|
|
||||||
}
|
|
|
@ -1,394 +0,0 @@
|
||||||
package morph
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
alphabetWalletsFlag = "alphabet-wallets"
|
|
||||||
alphabetSizeFlag = "size"
|
|
||||||
endpointFlag = "rpc-endpoint"
|
|
||||||
storageWalletFlag = "storage-wallet"
|
|
||||||
storageWalletLabelFlag = "label"
|
|
||||||
storageGasCLIFlag = "initial-gas"
|
|
||||||
storageGasConfigFlag = "storage.initial_gas"
|
|
||||||
contractsInitFlag = "contracts"
|
|
||||||
maxObjectSizeInitFlag = "network.max_object_size"
|
|
||||||
maxObjectSizeCLIFlag = "max-object-size"
|
|
||||||
epochDurationInitFlag = "network.epoch_duration"
|
|
||||||
epochDurationCLIFlag = "epoch-duration"
|
|
||||||
incomeRateInitFlag = "network.basic_income_rate"
|
|
||||||
incomeRateCLIFlag = "basic-income-rate"
|
|
||||||
auditFeeInitFlag = "network.fee.audit"
|
|
||||||
auditFeeCLIFlag = "audit-fee"
|
|
||||||
containerFeeInitFlag = "network.fee.container"
|
|
||||||
containerAliasFeeInitFlag = "network.fee.container_alias"
|
|
||||||
containerFeeCLIFlag = "container-fee"
|
|
||||||
containerAliasFeeCLIFlag = "container-alias-fee"
|
|
||||||
candidateFeeInitFlag = "network.fee.candidate"
|
|
||||||
candidateFeeCLIFlag = "candidate-fee"
|
|
||||||
homomorphicHashDisabledInitFlag = "network.homomorphic_hash_disabled"
|
|
||||||
maintenanceModeAllowedInitFlag = "network.maintenance_mode_allowed"
|
|
||||||
homomorphicHashDisabledCLIFlag = "homomorphic-disabled"
|
|
||||||
withdrawFeeInitFlag = "network.fee.withdraw"
|
|
||||||
withdrawFeeCLIFlag = "withdraw-fee"
|
|
||||||
containerDumpFlag = "dump"
|
|
||||||
containerContractFlag = "container-contract"
|
|
||||||
containerIDsFlag = "cid"
|
|
||||||
refillGasAmountFlag = "gas"
|
|
||||||
walletAccountFlag = "account"
|
|
||||||
notaryDepositTillFlag = "till"
|
|
||||||
localDumpFlag = "local-dump"
|
|
||||||
protoConfigPath = "protocol"
|
|
||||||
walletAddressFlag = "wallet-address"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// RootCmd is a root command of config section.
|
|
||||||
RootCmd = &cobra.Command{
|
|
||||||
Use: "morph",
|
|
||||||
Short: "Section for morph network configuration commands",
|
|
||||||
}
|
|
||||||
|
|
||||||
generateAlphabetCmd = &cobra.Command{
|
|
||||||
Use: "generate-alphabet",
|
|
||||||
Short: "Generate alphabet wallets for consensus nodes of the morph network",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
// PreRun fixes https://github.com/spf13/viper/issues/233
|
|
||||||
_ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag))
|
|
||||||
},
|
|
||||||
RunE: generateAlphabetCreds,
|
|
||||||
}
|
|
||||||
|
|
||||||
initCmd = &cobra.Command{
|
|
||||||
Use: "init",
|
|
||||||
Short: "Initialize side chain network with smart-contracts and network settings",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
|
||||||
_ = viper.BindPFlag(epochDurationInitFlag, cmd.Flags().Lookup(epochDurationCLIFlag))
|
|
||||||
_ = viper.BindPFlag(maxObjectSizeInitFlag, cmd.Flags().Lookup(maxObjectSizeCLIFlag))
|
|
||||||
_ = viper.BindPFlag(incomeRateInitFlag, cmd.Flags().Lookup(incomeRateCLIFlag))
|
|
||||||
_ = viper.BindPFlag(homomorphicHashDisabledInitFlag, cmd.Flags().Lookup(homomorphicHashDisabledCLIFlag))
|
|
||||||
_ = viper.BindPFlag(auditFeeInitFlag, cmd.Flags().Lookup(auditFeeCLIFlag))
|
|
||||||
_ = viper.BindPFlag(candidateFeeInitFlag, cmd.Flags().Lookup(candidateFeeCLIFlag))
|
|
||||||
_ = viper.BindPFlag(containerFeeInitFlag, cmd.Flags().Lookup(containerFeeCLIFlag))
|
|
||||||
_ = viper.BindPFlag(containerAliasFeeInitFlag, cmd.Flags().Lookup(containerAliasFeeCLIFlag))
|
|
||||||
_ = viper.BindPFlag(withdrawFeeInitFlag, cmd.Flags().Lookup(withdrawFeeCLIFlag))
|
|
||||||
_ = viper.BindPFlag(protoConfigPath, cmd.Flags().Lookup(protoConfigPath))
|
|
||||||
_ = viper.BindPFlag(localDumpFlag, cmd.Flags().Lookup(localDumpFlag))
|
|
||||||
},
|
|
||||||
RunE: initializeSideChainCmd,
|
|
||||||
}
|
|
||||||
|
|
||||||
generateStorageCmd = &cobra.Command{
|
|
||||||
Use: "generate-storage-wallet",
|
|
||||||
Short: "Generate storage node wallet for the morph network",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
|
||||||
_ = viper.BindPFlag(storageGasConfigFlag, cmd.Flags().Lookup(storageGasCLIFlag))
|
|
||||||
},
|
|
||||||
RunE: generateStorageCreds,
|
|
||||||
}
|
|
||||||
|
|
||||||
refillGasCmd = &cobra.Command{
|
|
||||||
Use: "refill-gas",
|
|
||||||
Short: "Refill GAS of storage node's wallet in the morph network",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
|
||||||
_ = viper.BindPFlag(refillGasAmountFlag, cmd.Flags().Lookup(refillGasAmountFlag))
|
|
||||||
},
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
return refillGas(cmd, refillGasAmountFlag, false)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
forceNewEpoch = &cobra.Command{
|
|
||||||
Use: "force-new-epoch",
|
|
||||||
Short: "Create new FrostFS epoch event in the side chain",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
|
||||||
},
|
|
||||||
RunE: forceNewEpochCmd,
|
|
||||||
}
|
|
||||||
|
|
||||||
removeNodes = &cobra.Command{
|
|
||||||
Use: "remove-nodes key1 [key2 [...]]",
|
|
||||||
Short: "Remove storage nodes from the netmap",
|
|
||||||
Long: `Move nodes to the Offline state in the candidates list and tick an epoch to update the netmap`,
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
|
||||||
},
|
|
||||||
RunE: removeNodesCmd,
|
|
||||||
}
|
|
||||||
|
|
||||||
setConfig = &cobra.Command{
|
|
||||||
Use: "set-config key1=val1 [key2=val2 ...]",
|
|
||||||
DisableFlagsInUseLine: true,
|
|
||||||
Short: "Add/update global config value in the FrostFS network",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
|
||||||
},
|
|
||||||
Args: cobra.MinimumNArgs(1),
|
|
||||||
RunE: setConfigCmd,
|
|
||||||
}
|
|
||||||
|
|
||||||
setPolicy = &cobra.Command{
|
|
||||||
Use: "set-policy [ExecFeeFactor=<n1>] [StoragePrice=<n2>] [FeePerByte=<n3>]",
|
|
||||||
DisableFlagsInUseLine: true,
|
|
||||||
Short: "Set global policy values",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
|
||||||
},
|
|
||||||
RunE: setPolicyCmd,
|
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
||||||
return []string{"ExecFeeFactor=", "StoragePrice=", "FeePerByte="}, cobra.ShellCompDirectiveNoSpace
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
dumpContractHashesCmd = &cobra.Command{
|
|
||||||
Use: "dump-hashes",
|
|
||||||
Short: "Dump deployed contract hashes",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
|
||||||
},
|
|
||||||
RunE: dumpContractHashes,
|
|
||||||
}
|
|
||||||
|
|
||||||
dumpNetworkConfigCmd = &cobra.Command{
|
|
||||||
Use: "dump-config",
|
|
||||||
Short: "Dump FrostFS network config",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
|
||||||
},
|
|
||||||
RunE: dumpNetworkConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
dumpBalancesCmd = &cobra.Command{
|
|
||||||
Use: "dump-balances",
|
|
||||||
Short: "Dump GAS balances",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
|
||||||
},
|
|
||||||
RunE: dumpBalances,
|
|
||||||
}
|
|
||||||
|
|
||||||
updateContractsCmd = &cobra.Command{
|
|
||||||
Use: "update-contracts",
|
|
||||||
Short: "Update FrostFS contracts",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
|
||||||
},
|
|
||||||
RunE: updateContracts,
|
|
||||||
}
|
|
||||||
|
|
||||||
dumpContainersCmd = &cobra.Command{
|
|
||||||
Use: "dump-containers",
|
|
||||||
Short: "Dump FrostFS containers to file",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
|
||||||
},
|
|
||||||
RunE: dumpContainers,
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreContainersCmd = &cobra.Command{
|
|
||||||
Use: "restore-containers",
|
|
||||||
Short: "Restore FrostFS containers from file",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
|
||||||
},
|
|
||||||
RunE: restoreContainers,
|
|
||||||
}
|
|
||||||
|
|
||||||
listContainersCmd = &cobra.Command{
|
|
||||||
Use: "list-containers",
|
|
||||||
Short: "List FrostFS containers",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
|
||||||
},
|
|
||||||
RunE: listContainers,
|
|
||||||
}
|
|
||||||
|
|
||||||
depositNotaryCmd = &cobra.Command{
|
|
||||||
Use: "deposit-notary",
|
|
||||||
Short: "Deposit GAS for notary service",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
|
||||||
},
|
|
||||||
RunE: depositNotary,
|
|
||||||
}
|
|
||||||
|
|
||||||
netmapCandidatesCmd = &cobra.Command{
|
|
||||||
Use: "netmap-candidates",
|
|
||||||
Short: "List netmap candidates nodes",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
|
||||||
_ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag))
|
|
||||||
},
|
|
||||||
Run: listNetmapCandidatesNodes,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
initGenerateAlphabetCmd()
|
|
||||||
initInitCmd()
|
|
||||||
initDeployCmd()
|
|
||||||
initGenerateStorageCmd()
|
|
||||||
initForceNewEpochCmd()
|
|
||||||
initRemoveNodesCmd()
|
|
||||||
initSetPolicyCmd()
|
|
||||||
initDumpContractHashesCmd()
|
|
||||||
initDumpNetworkConfigCmd()
|
|
||||||
initSetConfigCmd()
|
|
||||||
initDumpBalancesCmd()
|
|
||||||
initUpdateContractsCmd()
|
|
||||||
initDumpContainersCmd()
|
|
||||||
initRestoreContainersCmd()
|
|
||||||
initListContainersCmd()
|
|
||||||
initRefillGasCmd()
|
|
||||||
initDepositoryNotaryCmd()
|
|
||||||
initNetmapCandidatesCmd()
|
|
||||||
}
|
|
||||||
|
|
||||||
func initNetmapCandidatesCmd() {
|
|
||||||
RootCmd.AddCommand(netmapCandidatesCmd)
|
|
||||||
netmapCandidatesCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initDepositoryNotaryCmd() {
|
|
||||||
RootCmd.AddCommand(depositNotaryCmd)
|
|
||||||
depositNotaryCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
|
||||||
depositNotaryCmd.Flags().String(storageWalletFlag, "", "Path to storage node wallet")
|
|
||||||
depositNotaryCmd.Flags().String(walletAccountFlag, "", "Wallet account address")
|
|
||||||
depositNotaryCmd.Flags().String(refillGasAmountFlag, "", "Amount of GAS to deposit")
|
|
||||||
depositNotaryCmd.Flags().String(notaryDepositTillFlag, "", "Notary deposit duration in blocks")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initRefillGasCmd() {
|
|
||||||
RootCmd.AddCommand(refillGasCmd)
|
|
||||||
refillGasCmd.Flags().String(alphabetWalletsFlag, "", "Path to alphabet wallets dir")
|
|
||||||
refillGasCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
|
||||||
refillGasCmd.Flags().String(storageWalletFlag, "", "Path to storage node wallet")
|
|
||||||
refillGasCmd.Flags().String(walletAddressFlag, "", "Address of wallet")
|
|
||||||
refillGasCmd.Flags().String(refillGasAmountFlag, "", "Additional amount of GAS to transfer")
|
|
||||||
refillGasCmd.MarkFlagsMutuallyExclusive(walletAddressFlag, storageWalletFlag)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initListContainersCmd() {
|
|
||||||
RootCmd.AddCommand(listContainersCmd)
|
|
||||||
listContainersCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
|
||||||
listContainersCmd.Flags().String(containerContractFlag, "", "Container contract hash (for networks without NNS)")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initRestoreContainersCmd() {
|
|
||||||
RootCmd.AddCommand(restoreContainersCmd)
|
|
||||||
restoreContainersCmd.Flags().String(alphabetWalletsFlag, "", "Path to alphabet wallets dir")
|
|
||||||
restoreContainersCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
|
||||||
restoreContainersCmd.Flags().String(containerDumpFlag, "", "File to restore containers from")
|
|
||||||
restoreContainersCmd.Flags().StringSlice(containerIDsFlag, nil, "Containers to restore")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initDumpContainersCmd() {
|
|
||||||
RootCmd.AddCommand(dumpContainersCmd)
|
|
||||||
dumpContainersCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
|
||||||
dumpContainersCmd.Flags().String(containerDumpFlag, "", "File where to save dumped containers")
|
|
||||||
dumpContainersCmd.Flags().String(containerContractFlag, "", "Container contract hash (for networks without NNS)")
|
|
||||||
dumpContainersCmd.Flags().StringSlice(containerIDsFlag, nil, "Containers to dump")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initUpdateContractsCmd() {
|
|
||||||
RootCmd.AddCommand(updateContractsCmd)
|
|
||||||
updateContractsCmd.Flags().String(alphabetWalletsFlag, "", "Path to alphabet wallets dir")
|
|
||||||
updateContractsCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
|
||||||
updateContractsCmd.Flags().String(contractsInitFlag, "", "Path to archive with compiled FrostFS contracts")
|
|
||||||
_ = updateContractsCmd.MarkFlagRequired(contractsInitFlag)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initDumpBalancesCmd() {
|
|
||||||
RootCmd.AddCommand(dumpBalancesCmd)
|
|
||||||
dumpBalancesCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
|
||||||
dumpBalancesCmd.Flags().BoolP(dumpBalancesStorageFlag, "s", false, "Dump balances of storage nodes from the current netmap")
|
|
||||||
dumpBalancesCmd.Flags().BoolP(dumpBalancesAlphabetFlag, "a", false, "Dump balances of alphabet contracts")
|
|
||||||
dumpBalancesCmd.Flags().BoolP(dumpBalancesProxyFlag, "p", false, "Dump balances of the proxy contract")
|
|
||||||
dumpBalancesCmd.Flags().Bool(dumpBalancesUseScriptHashFlag, false, "Use script-hash format for addresses")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initSetConfigCmd() {
|
|
||||||
RootCmd.AddCommand(setConfig)
|
|
||||||
setConfig.Flags().String(alphabetWalletsFlag, "", "Path to alphabet wallets dir")
|
|
||||||
setConfig.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
|
||||||
setConfig.Flags().Bool(forceConfigSet, false, "Force setting not well-known configuration key")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initDumpNetworkConfigCmd() {
|
|
||||||
RootCmd.AddCommand(dumpNetworkConfigCmd)
|
|
||||||
dumpNetworkConfigCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initDumpContractHashesCmd() {
|
|
||||||
RootCmd.AddCommand(dumpContractHashesCmd)
|
|
||||||
dumpContractHashesCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
|
||||||
dumpContractHashesCmd.Flags().String(customZoneFlag, "", "Custom zone to search.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initSetPolicyCmd() {
|
|
||||||
RootCmd.AddCommand(setPolicy)
|
|
||||||
setPolicy.Flags().String(alphabetWalletsFlag, "", "Path to alphabet wallets dir")
|
|
||||||
setPolicy.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initRemoveNodesCmd() {
|
|
||||||
RootCmd.AddCommand(removeNodes)
|
|
||||||
removeNodes.Flags().String(alphabetWalletsFlag, "", "Path to alphabet wallets dir")
|
|
||||||
removeNodes.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initForceNewEpochCmd() {
|
|
||||||
RootCmd.AddCommand(forceNewEpoch)
|
|
||||||
forceNewEpoch.Flags().String(alphabetWalletsFlag, "", "Path to alphabet wallets dir")
|
|
||||||
forceNewEpoch.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initGenerateStorageCmd() {
|
|
||||||
RootCmd.AddCommand(generateStorageCmd)
|
|
||||||
generateStorageCmd.Flags().String(alphabetWalletsFlag, "", "Path to alphabet wallets dir")
|
|
||||||
generateStorageCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
|
||||||
generateStorageCmd.Flags().String(storageWalletFlag, "", "Path to new storage node wallet")
|
|
||||||
generateStorageCmd.Flags().String(storageGasCLIFlag, "", "Initial amount of GAS to transfer")
|
|
||||||
generateStorageCmd.Flags().StringP(storageWalletLabelFlag, "l", "", "Wallet label")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initInitCmd() {
|
|
||||||
RootCmd.AddCommand(initCmd)
|
|
||||||
initCmd.Flags().String(alphabetWalletsFlag, "", "Path to alphabet wallets dir")
|
|
||||||
initCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
|
||||||
initCmd.Flags().String(contractsInitFlag, "", "Path to archive with compiled FrostFS contracts")
|
|
||||||
_ = initCmd.MarkFlagRequired(contractsInitFlag)
|
|
||||||
initCmd.Flags().Uint(epochDurationCLIFlag, 240, "Amount of side chain blocks in one FrostFS epoch")
|
|
||||||
initCmd.Flags().Uint(maxObjectSizeCLIFlag, 67108864, "Max single object size in bytes")
|
|
||||||
initCmd.Flags().Bool(homomorphicHashDisabledCLIFlag, false, "Disable object homomorphic hashing")
|
|
||||||
// Defaults are taken from neo-preodolenie.
|
|
||||||
initCmd.Flags().Uint64(containerFeeCLIFlag, 1000, "Container registration fee")
|
|
||||||
initCmd.Flags().Uint64(containerAliasFeeCLIFlag, 500, "Container alias fee")
|
|
||||||
initCmd.Flags().String(protoConfigPath, "", "Path to the consensus node configuration")
|
|
||||||
initCmd.Flags().String(localDumpFlag, "", "Path to the blocks dump file")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initGenerateAlphabetCmd() {
|
|
||||||
RootCmd.AddCommand(generateAlphabetCmd)
|
|
||||||
generateAlphabetCmd.Flags().String(alphabetWalletsFlag, "", "Path to alphabet wallets dir")
|
|
||||||
generateAlphabetCmd.Flags().Uint(alphabetSizeFlag, 7, "Amount of alphabet wallets to generate")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initDeployCmd() {
|
|
||||||
RootCmd.AddCommand(deployCmd)
|
|
||||||
}
|
|
|
@ -1,85 +0,0 @@
|
||||||
package modules
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/config"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/storagecfg"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/misc"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/autocomplete"
|
|
||||||
utilConfig "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/config"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/gendoc"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
rootCmd = &cobra.Command{
|
|
||||||
Use: "frostfs-adm",
|
|
||||||
Short: "FrostFS Administrative Tool",
|
|
||||||
Long: `FrostFS Administrative Tool provides functions to setup and
|
|
||||||
manage FrostFS network deployment.`,
|
|
||||||
RunE: entryPoint,
|
|
||||||
SilenceUsage: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
cobra.OnInitialize(func() { initConfig(rootCmd) })
|
|
||||||
// we need to init viper config to bind viper and cobra configurations for
|
|
||||||
// rpc endpoint, alphabet wallet dir, key credentials, etc.
|
|
||||||
|
|
||||||
// use stdout as default output for cmd.Print()
|
|
||||||
rootCmd.SetOut(os.Stdout)
|
|
||||||
|
|
||||||
rootCmd.PersistentFlags().StringP(commonflags.ConfigFlag, commonflags.ConfigFlagShorthand, "", commonflags.ConfigFlagUsage)
|
|
||||||
rootCmd.PersistentFlags().String(commonflags.ConfigDirFlag, "", commonflags.ConfigDirFlagUsage)
|
|
||||||
rootCmd.PersistentFlags().BoolP(commonflags.Verbose, commonflags.VerboseShorthand, false, commonflags.VerboseUsage)
|
|
||||||
_ = viper.BindPFlag(commonflags.Verbose, rootCmd.PersistentFlags().Lookup(commonflags.Verbose))
|
|
||||||
rootCmd.Flags().Bool("version", false, "Application version")
|
|
||||||
|
|
||||||
rootCmd.AddCommand(config.RootCmd)
|
|
||||||
rootCmd.AddCommand(morph.RootCmd)
|
|
||||||
rootCmd.AddCommand(storagecfg.RootCmd)
|
|
||||||
|
|
||||||
rootCmd.AddCommand(autocomplete.Command("frostfs-adm"))
|
|
||||||
rootCmd.AddCommand(gendoc.Command(rootCmd))
|
|
||||||
}
|
|
||||||
|
|
||||||
func Execute() error {
|
|
||||||
return rootCmd.Execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
func entryPoint(cmd *cobra.Command, _ []string) error {
|
|
||||||
printVersion, _ := cmd.Flags().GetBool("version")
|
|
||||||
if printVersion {
|
|
||||||
cmd.Print(misc.BuildInfo("FrostFS Adm"))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return cmd.Usage()
|
|
||||||
}
|
|
||||||
|
|
||||||
func initConfig(cmd *cobra.Command) {
|
|
||||||
configFile, err := cmd.Flags().GetString(commonflags.ConfigFlag)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if configFile != "" {
|
|
||||||
viper.SetConfigType("yml")
|
|
||||||
viper.SetConfigFile(configFile)
|
|
||||||
_ = viper.ReadInConfig() // if config file is set but unavailable, ignore it
|
|
||||||
}
|
|
||||||
|
|
||||||
configDir, err := cmd.Flags().GetString(commonflags.ConfigDirFlag)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if configDir != "" {
|
|
||||||
_ = utilConfig.ReadConfigDir(viper.GetViper(), configDir) // if config files cannot be read, ignore it
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
# How FrostFS CLI uses session mechanism of the FrostFS
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
FrostFS sessions implement a mechanism for issuing a power of attorney by one
|
|
||||||
party to another. A trusted party can provide a so-called session token as
|
|
||||||
proof of the right to act on behalf of another member of the network. The
|
|
||||||
client of operations carried out with such a token will be the user who opened
|
|
||||||
the session. The token contains information which limits power of attorney like
|
|
||||||
action context or lifetime.
|
|
||||||
|
|
||||||
The client confirms trust in a third party by signing its public (session) key
|
|
||||||
with his private key. Any operation signed using private session key with
|
|
||||||
attached session token is treated as performed by the original client.
|
|
||||||
|
|
||||||
## Types
|
|
||||||
|
|
||||||
FrostFS CLI supports two ways to execute operation within a session depending on
|
|
||||||
whether the user of the command application is an original user (1) or a trusted
|
|
||||||
one (2).
|
|
||||||
|
|
||||||
### Dynamic
|
|
||||||
|
|
||||||
For case (1) CLI user can only open dynamic sessions. Protocol call
|
|
||||||
`SessionService.Create` is used for this purpose. As a result of the call, a
|
|
||||||
private session key will be generated on the server, thus making the remote
|
|
||||||
server trusted. This type of session is useful when the client needs to
|
|
||||||
transfer part of the responsibility for the formation of strict system elements
|
|
||||||
to the trusted server. At the moment, the approach is applicable only to
|
|
||||||
creating objects.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ frostfs-cli session create --rpc-endpoint <server_ip> --out ./blank_token
|
|
||||||
```
|
|
||||||
After this example command remote node holds session private key while its
|
|
||||||
public part is written into the session token encoded into the output file.
|
|
||||||
Later this token can be attached to the operations which support dynamic
|
|
||||||
sessions. Then the token will be finally formed and signed by CLI itself.
|
|
||||||
|
|
||||||
### Static
|
|
||||||
|
|
||||||
For case (2) CLI user can act on behalf of the person who issued the session
|
|
||||||
token to him. Unlike (1) the token must be fully prepared on the side of the
|
|
||||||
original client, and the CLI uses it only for reading. Ready token MUST have:
|
|
||||||
- correct context (object, container, etc.)
|
|
||||||
- valid lifetime
|
|
||||||
- public session key corresponding to the CLI key
|
|
||||||
- valid client signature
|
|
||||||
|
|
||||||
To sign the session token, exec:
|
|
||||||
```shell
|
|
||||||
$ frostfs-cli --wallet <client_wallet> util sign session-token --from ./blank_token --to ./token
|
|
||||||
```
|
|
||||||
Once the token is signed, it MUST NOT be modified.
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
### Object
|
|
||||||
|
|
||||||
Here are sub-commands of `object` command which support only dynamic sessions (1):
|
|
||||||
- `put`
|
|
||||||
- `delete`
|
|
||||||
- `lock`
|
|
||||||
|
|
||||||
These commands accept blank token of the dynamically opened session or open
|
|
||||||
session internally if it has not been opened yet.
|
|
||||||
|
|
||||||
All other `object` sub-commands support only static sessions (2).
|
|
||||||
|
|
||||||
### Container
|
|
||||||
|
|
||||||
List of commands supporting sessions (static only):
|
|
||||||
- `create`
|
|
||||||
- `delete`
|
|
||||||
- `set-eacl`
|
|
|
@ -1,33 +0,0 @@
|
||||||
# Extended headers
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Extended headers are used for request/response. They may contain any
|
|
||||||
user-defined headers to be interpreted on application level. Key name must be a
|
|
||||||
unique valid UTF-8 string. Value can't be empty. Requests or Responses with
|
|
||||||
duplicated header names or headers with empty values are considered invalid.
|
|
||||||
|
|
||||||
## Existing headers
|
|
||||||
|
|
||||||
There are some "well-known" headers starting with `__SYSTEM__` prefix that
|
|
||||||
affect system behaviour. For backward compatibility, the same set of
|
|
||||||
"well-known" headers may also use `__NEOFS__` prefix:
|
|
||||||
|
|
||||||
* `__SYSTEM__NETMAP_EPOCH` - netmap epoch to use for object placement calculation. The `value` is string
|
|
||||||
encoded `uint64` in decimal presentation. If set to '0' or omitted, the
|
|
||||||
current epoch only will be used.
|
|
||||||
* `__SYSTEM__NETMAP_LOOKUP_DEPTH` - if object can't be found using current epoch's netmap, this header limits
|
|
||||||
how many past epochs the node can look up through. Depth is applied to a current epoch or the value
|
|
||||||
of `__SYSTEM__NETMAP_EPOCH` attribute. The `value` is string encoded `uint64` in decimal presentation.
|
|
||||||
If set to '0' or not set, only the current epoch is used.
|
|
||||||
|
|
||||||
## `frostfs-cli` commands with `--xhdr`
|
|
||||||
|
|
||||||
List of commands with support of extended headers:
|
|
||||||
* `container list-objects`
|
|
||||||
* `object delete/get/hash/head/lock/put/range/search`
|
|
||||||
|
|
||||||
Example:
|
|
||||||
```shell
|
|
||||||
$ frostfs-cli object put -r s01.frostfs.devenv:8080 -w wallet.json --cid CID --file FILE --xhdr "__SYSTEM__NETMAP_EPOCH=777"
|
|
||||||
```
|
|
|
@ -1,930 +0,0 @@
|
||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/accounting"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
|
|
||||||
containerSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
|
||||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
|
||||||
objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
|
||||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/version"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BalanceOfPrm groups parameters of BalanceOf operation.
|
|
||||||
type BalanceOfPrm struct {
|
|
||||||
commonPrm
|
|
||||||
client.PrmBalanceGet
|
|
||||||
}
|
|
||||||
|
|
||||||
// BalanceOfRes groups the resulting values of BalanceOf operation.
|
|
||||||
type BalanceOfRes struct {
|
|
||||||
cliRes *client.ResBalanceGet
|
|
||||||
}
|
|
||||||
|
|
||||||
// Balance returns the current balance.
|
|
||||||
func (x BalanceOfRes) Balance() accounting.Decimal {
|
|
||||||
return x.cliRes.Amount()
|
|
||||||
}
|
|
||||||
|
|
||||||
// BalanceOf requests the current balance of a FrostFS user.
|
|
||||||
//
|
|
||||||
// Returns any error which prevented the operation from completing correctly in error return.
|
|
||||||
func BalanceOf(ctx context.Context, prm BalanceOfPrm) (res BalanceOfRes, err error) {
|
|
||||||
res.cliRes, err = prm.cli.BalanceGet(ctx, prm.PrmBalanceGet)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListContainersPrm groups parameters of ListContainers operation.
|
|
||||||
type ListContainersPrm struct {
|
|
||||||
commonPrm
|
|
||||||
client.PrmContainerList
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListContainersRes groups the resulting values of ListContainers operation.
|
|
||||||
type ListContainersRes struct {
|
|
||||||
cliRes *client.ResContainerList
|
|
||||||
}
|
|
||||||
|
|
||||||
// IDList returns list of identifiers of user's containers.
|
|
||||||
func (x ListContainersRes) IDList() []cid.ID {
|
|
||||||
return x.cliRes.Containers()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListContainers requests a list of FrostFS user's containers.
|
|
||||||
//
|
|
||||||
// Returns any error which prevented the operation from completing correctly in error return.
|
|
||||||
func ListContainers(ctx context.Context, prm ListContainersPrm) (res ListContainersRes, err error) {
|
|
||||||
res.cliRes, err = prm.cli.ContainerList(ctx, prm.PrmContainerList)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutContainerPrm groups parameters of PutContainer operation.
|
|
||||||
type PutContainerPrm struct {
|
|
||||||
commonPrm
|
|
||||||
client.PrmContainerPut
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutContainerRes groups the resulting values of PutContainer operation.
|
|
||||||
type PutContainerRes struct {
|
|
||||||
cnr cid.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
// ID returns identifier of the created container.
|
|
||||||
func (x PutContainerRes) ID() cid.ID {
|
|
||||||
return x.cnr
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutContainer sends a request to save the container in FrostFS.
|
|
||||||
//
|
|
||||||
// Operation is asynchronous and not guaranteed even in the absence of errors.
|
|
||||||
// The required time is also not predictable.
|
|
||||||
//
|
|
||||||
// Success can be verified by reading by identifier.
|
|
||||||
//
|
|
||||||
// Returns any error which prevented the operation from completing correctly in error return.
|
|
||||||
func PutContainer(ctx context.Context, prm PutContainerPrm) (res PutContainerRes, err error) {
|
|
||||||
cliRes, err := prm.cli.ContainerPut(ctx, prm.PrmContainerPut)
|
|
||||||
if err == nil {
|
|
||||||
res.cnr = cliRes.ID()
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetContainerPrm groups parameters of GetContainer operation.
|
|
||||||
type GetContainerPrm struct {
|
|
||||||
commonPrm
|
|
||||||
cliPrm client.PrmContainerGet
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetContainer sets identifier of the container to be read.
|
|
||||||
func (x *GetContainerPrm) SetContainer(id cid.ID) {
|
|
||||||
x.cliPrm.SetContainer(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetContainerRes groups the resulting values of GetContainer operation.
|
|
||||||
type GetContainerRes struct {
|
|
||||||
cliRes *client.ResContainerGet
|
|
||||||
}
|
|
||||||
|
|
||||||
// Container returns structured of the requested container.
|
|
||||||
func (x GetContainerRes) Container() containerSDK.Container {
|
|
||||||
return x.cliRes.Container()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetContainer reads a container from FrostFS by ID.
|
|
||||||
//
|
|
||||||
// Returns any error which prevented the operation from completing correctly in error return.
|
|
||||||
func GetContainer(ctx context.Context, prm GetContainerPrm) (res GetContainerRes, err error) {
|
|
||||||
res.cliRes, err = prm.cli.ContainerGet(ctx, prm.cliPrm)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsACLExtendable checks if ACL of the container referenced by the given identifier
|
|
||||||
// can be extended. Client connection MUST BE correctly established in advance.
|
|
||||||
func IsACLExtendable(ctx context.Context, c *client.Client, cnr cid.ID) (bool, error) {
|
|
||||||
var prm GetContainerPrm
|
|
||||||
prm.SetClient(c)
|
|
||||||
prm.SetContainer(cnr)
|
|
||||||
|
|
||||||
res, err := GetContainer(ctx, prm)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("get container from the FrostFS: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.Container().BasicACL().Extendable(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteContainerPrm groups parameters of DeleteContainerPrm operation.
|
|
||||||
type DeleteContainerPrm struct {
|
|
||||||
commonPrm
|
|
||||||
client.PrmContainerDelete
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteContainerRes groups the resulting values of DeleteContainer operation.
|
|
||||||
type DeleteContainerRes struct{}
|
|
||||||
|
|
||||||
// DeleteContainer sends a request to remove a container from FrostFS by ID.
|
|
||||||
//
|
|
||||||
// Operation is asynchronous and not guaranteed even in the absence of errors.
|
|
||||||
// The required time is also not predictable.
|
|
||||||
//
|
|
||||||
// Success can be verified by reading by identifier.
|
|
||||||
//
|
|
||||||
// Returns any error which prevented the operation from completing correctly in error return.
|
|
||||||
func DeleteContainer(ctx context.Context, prm DeleteContainerPrm) (res DeleteContainerRes, err error) {
|
|
||||||
_, err = prm.cli.ContainerDelete(ctx, prm.PrmContainerDelete)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// EACLPrm groups parameters of EACL operation.
|
|
||||||
type EACLPrm struct {
|
|
||||||
commonPrm
|
|
||||||
client.PrmContainerEACL
|
|
||||||
}
|
|
||||||
|
|
||||||
// EACLRes groups the resulting values of EACL operation.
|
|
||||||
type EACLRes struct {
|
|
||||||
cliRes *client.ResContainerEACL
|
|
||||||
}
|
|
||||||
|
|
||||||
// EACL returns requested eACL table.
|
|
||||||
func (x EACLRes) EACL() eacl.Table {
|
|
||||||
return x.cliRes.Table()
|
|
||||||
}
|
|
||||||
|
|
||||||
// EACL reads eACL table from FrostFS by container ID.
|
|
||||||
//
|
|
||||||
// Returns any error which prevented the operation from completing correctly in error return.
|
|
||||||
func EACL(ctx context.Context, prm EACLPrm) (res EACLRes, err error) {
|
|
||||||
res.cliRes, err = prm.cli.ContainerEACL(ctx, prm.PrmContainerEACL)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetEACLPrm groups parameters of SetEACL operation.
|
|
||||||
type SetEACLPrm struct {
|
|
||||||
commonPrm
|
|
||||||
client.PrmContainerSetEACL
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetEACLRes groups the resulting values of SetEACL operation.
|
|
||||||
type SetEACLRes struct{}
|
|
||||||
|
|
||||||
// SetEACL requests to save an eACL table in FrostFS.
|
|
||||||
//
|
|
||||||
// Operation is asynchronous and no guaranteed even in the absence of errors.
|
|
||||||
// The required time is also not predictable.
|
|
||||||
//
|
|
||||||
// Success can be verified by reading by container identifier.
|
|
||||||
//
|
|
||||||
// Returns any error which prevented the operation from completing correctly in error return.
|
|
||||||
func SetEACL(ctx context.Context, prm SetEACLPrm) (res SetEACLRes, err error) {
|
|
||||||
_, err = prm.cli.ContainerSetEACL(ctx, prm.PrmContainerSetEACL)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetworkInfoPrm groups parameters of NetworkInfo operation.
|
|
||||||
type NetworkInfoPrm struct {
|
|
||||||
commonPrm
|
|
||||||
client.PrmNetworkInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetworkInfoRes groups the resulting values of NetworkInfo operation.
|
|
||||||
type NetworkInfoRes struct {
|
|
||||||
cliRes *client.ResNetworkInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetworkInfo returns structured information about the FrostFS network.
|
|
||||||
func (x NetworkInfoRes) NetworkInfo() netmap.NetworkInfo {
|
|
||||||
return x.cliRes.Info()
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetworkInfo reads information about the FrostFS network.
|
|
||||||
//
|
|
||||||
// Returns any error which prevented the operation from completing correctly in error return.
|
|
||||||
func NetworkInfo(ctx context.Context, prm NetworkInfoPrm) (res NetworkInfoRes, err error) {
|
|
||||||
res.cliRes, err = prm.cli.NetworkInfo(ctx, prm.PrmNetworkInfo)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// NodeInfoPrm groups parameters of NodeInfo operation.
|
|
||||||
type NodeInfoPrm struct {
|
|
||||||
commonPrm
|
|
||||||
client.PrmEndpointInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
// NodeInfoRes groups the resulting values of NodeInfo operation.
|
|
||||||
type NodeInfoRes struct {
|
|
||||||
cliRes *client.ResEndpointInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
// NodeInfo returns information about the node from netmap.
|
|
||||||
func (x NodeInfoRes) NodeInfo() netmap.NodeInfo {
|
|
||||||
return x.cliRes.NodeInfo()
|
|
||||||
}
|
|
||||||
|
|
||||||
// LatestVersion returns the latest FrostFS API version in use.
|
|
||||||
func (x NodeInfoRes) LatestVersion() version.Version {
|
|
||||||
return x.cliRes.LatestVersion()
|
|
||||||
}
|
|
||||||
|
|
||||||
// NodeInfo requests information about the remote server from FrostFS netmap.
|
|
||||||
//
|
|
||||||
// Returns any error which prevented the operation from completing correctly in error return.
|
|
||||||
func NodeInfo(ctx context.Context, prm NodeInfoPrm) (res NodeInfoRes, err error) {
|
|
||||||
res.cliRes, err = prm.cli.EndpointInfo(ctx, prm.PrmEndpointInfo)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetMapSnapshotPrm groups parameters of NetMapSnapshot operation.
|
|
||||||
type NetMapSnapshotPrm struct {
|
|
||||||
commonPrm
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetMapSnapshotRes groups the resulting values of NetMapSnapshot operation.
|
|
||||||
type NetMapSnapshotRes struct {
|
|
||||||
cliRes *client.ResNetMapSnapshot
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetMap returns current local snapshot of the FrostFS network map.
|
|
||||||
func (x NetMapSnapshotRes) NetMap() netmap.NetMap {
|
|
||||||
return x.cliRes.NetMap()
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetMapSnapshot requests current network view of the remote server.
|
|
||||||
//
|
|
||||||
// Returns any error which prevented the operation from completing correctly in error return.
|
|
||||||
func NetMapSnapshot(ctx context.Context, prm NetMapSnapshotPrm) (res NetMapSnapshotRes, err error) {
|
|
||||||
res.cliRes, err = prm.cli.NetMapSnapshot(ctx, client.PrmNetMapSnapshot{})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateSessionPrm groups parameters of CreateSession operation.
|
|
||||||
type CreateSessionPrm struct {
|
|
||||||
commonPrm
|
|
||||||
client.PrmSessionCreate
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateSessionRes groups the resulting values of CreateSession operation.
|
|
||||||
type CreateSessionRes struct {
|
|
||||||
cliRes *client.ResSessionCreate
|
|
||||||
}
|
|
||||||
|
|
||||||
// ID returns session identifier.
|
|
||||||
func (x CreateSessionRes) ID() []byte {
|
|
||||||
return x.cliRes.ID()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SessionKey returns public session key in a binary format.
|
|
||||||
func (x CreateSessionRes) SessionKey() []byte {
|
|
||||||
return x.cliRes.PublicKey()
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateSession opens a new unlimited session with the remote node.
|
|
||||||
//
|
|
||||||
// Returns any error which prevented the operation from completing correctly in error return.
|
|
||||||
func CreateSession(ctx context.Context, prm CreateSessionPrm) (res CreateSessionRes, err error) {
|
|
||||||
res.cliRes, err = prm.cli.SessionCreate(ctx, prm.PrmSessionCreate)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutObjectPrm groups parameters of PutObject operation.
|
|
||||||
type PutObjectPrm struct {
|
|
||||||
commonObjectPrm
|
|
||||||
|
|
||||||
copyNum []uint32
|
|
||||||
|
|
||||||
hdr *objectSDK.Object
|
|
||||||
|
|
||||||
rdr io.Reader
|
|
||||||
|
|
||||||
headerCallback func(*objectSDK.Object)
|
|
||||||
|
|
||||||
prepareLocally bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetHeader sets object header.
|
|
||||||
func (x *PutObjectPrm) SetHeader(hdr *objectSDK.Object) {
|
|
||||||
x.hdr = hdr
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetPayloadReader sets reader of the object payload.
|
|
||||||
func (x *PutObjectPrm) SetPayloadReader(rdr io.Reader) {
|
|
||||||
x.rdr = rdr
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetHeaderCallback sets callback which is called on the object after the header is received
|
|
||||||
// but before the payload is written.
|
|
||||||
func (x *PutObjectPrm) SetHeaderCallback(f func(*objectSDK.Object)) {
|
|
||||||
x.headerCallback = f
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCopiesNumberByVectors sets ordered list of minimal required object copies numbers
|
|
||||||
// per placement vector.
|
|
||||||
func (x *PutObjectPrm) SetCopiesNumberByVectors(copiesNumbers []uint32) {
|
|
||||||
x.copyNum = copiesNumbers
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrepareLocally generate object header on the client side.
|
|
||||||
// For big object - split locally too.
|
|
||||||
func (x *PutObjectPrm) PrepareLocally() {
|
|
||||||
x.prepareLocally = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *PutObjectPrm) convertToSDKPrm(ctx context.Context) (client.PrmObjectPutInit, error) {
|
|
||||||
var putPrm client.PrmObjectPutInit
|
|
||||||
if !x.prepareLocally && x.sessionToken != nil {
|
|
||||||
putPrm.WithinSession(*x.sessionToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
if x.bearerToken != nil {
|
|
||||||
putPrm.WithBearerToken(*x.bearerToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
if x.local {
|
|
||||||
putPrm.MarkLocal()
|
|
||||||
}
|
|
||||||
|
|
||||||
putPrm.WithXHeaders(x.xHeaders...)
|
|
||||||
putPrm.SetCopiesNumberByVectors(x.copyNum)
|
|
||||||
|
|
||||||
if x.prepareLocally {
|
|
||||||
res, err := x.cli.NetworkInfo(ctx, client.PrmNetworkInfo{})
|
|
||||||
if err != nil {
|
|
||||||
return client.PrmObjectPutInit{}, err
|
|
||||||
}
|
|
||||||
putPrm.WithObjectMaxSize(res.Info().MaxObjectSize())
|
|
||||||
putPrm.WithEpochSource(epochSource(res.Info().CurrentEpoch()))
|
|
||||||
putPrm.WithoutHomomorphicHash(res.Info().HomomorphicHashingDisabled())
|
|
||||||
}
|
|
||||||
return putPrm, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutObjectRes groups the resulting values of PutObject operation.
|
|
||||||
type PutObjectRes struct {
|
|
||||||
id oid.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
// ID returns identifier of the created object.
|
|
||||||
func (x PutObjectRes) ID() oid.ID {
|
|
||||||
return x.id
|
|
||||||
}
|
|
||||||
|
|
||||||
type epochSource uint64
|
|
||||||
|
|
||||||
func (s epochSource) CurrentEpoch() uint64 {
|
|
||||||
return uint64(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutObject saves the object in FrostFS network.
|
|
||||||
//
|
|
||||||
// Returns any error which prevented the operation from completing correctly in error return.
|
|
||||||
func PutObject(ctx context.Context, prm PutObjectPrm) (*PutObjectRes, error) {
|
|
||||||
sdkPrm, err := prm.convertToSDKPrm(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to create parameters of object put operation: %w", err)
|
|
||||||
}
|
|
||||||
wrt, err := prm.cli.ObjectPutInit(ctx, sdkPrm)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("init object writing: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if wrt.WriteHeader(ctx, *prm.hdr) {
|
|
||||||
if prm.headerCallback != nil {
|
|
||||||
prm.headerCallback(prm.hdr)
|
|
||||||
}
|
|
||||||
|
|
||||||
sz := prm.hdr.PayloadSize()
|
|
||||||
|
|
||||||
if data := prm.hdr.Payload(); len(data) > 0 {
|
|
||||||
if prm.rdr != nil {
|
|
||||||
prm.rdr = io.MultiReader(bytes.NewReader(data), prm.rdr)
|
|
||||||
} else {
|
|
||||||
prm.rdr = bytes.NewReader(data)
|
|
||||||
sz = uint64(len(data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if prm.rdr != nil {
|
|
||||||
const defaultBufferSizePut = 3 << 20 // Maximum chunk size is 3 MiB in the SDK.
|
|
||||||
|
|
||||||
if sz == 0 || sz > defaultBufferSizePut {
|
|
||||||
sz = defaultBufferSizePut
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]byte, sz)
|
|
||||||
|
|
||||||
var n int
|
|
||||||
|
|
||||||
for {
|
|
||||||
n, err = prm.rdr.Read(buf)
|
|
||||||
if n > 0 {
|
|
||||||
if !wrt.WritePayloadChunk(ctx, buf[:n]) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if errors.Is(err, io.EOF) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("read payload: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cliRes, err := wrt.Close(ctx)
|
|
||||||
if err != nil { // here err already carries both status and client errors
|
|
||||||
return nil, fmt.Errorf("client failure: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &PutObjectRes{
|
|
||||||
id: cliRes.StoredObjectID(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteObjectPrm groups parameters of DeleteObject operation.
|
|
||||||
type DeleteObjectPrm struct {
|
|
||||||
commonObjectPrm
|
|
||||||
objectAddressPrm
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteObjectRes groups the resulting values of DeleteObject operation.
|
|
||||||
type DeleteObjectRes struct {
|
|
||||||
tomb oid.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tombstone returns the ID of the created object with tombstone.
|
|
||||||
func (x DeleteObjectRes) Tombstone() oid.ID {
|
|
||||||
return x.tomb
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteObject marks an object to be removed from FrostFS through tombstone placement.
|
|
||||||
//
|
|
||||||
// Returns any error which prevented the operation from completing correctly in error return.
|
|
||||||
func DeleteObject(ctx context.Context, prm DeleteObjectPrm) (*DeleteObjectRes, error) {
|
|
||||||
var delPrm client.PrmObjectDelete
|
|
||||||
delPrm.FromContainer(prm.objAddr.Container())
|
|
||||||
delPrm.ByID(prm.objAddr.Object())
|
|
||||||
|
|
||||||
if prm.sessionToken != nil {
|
|
||||||
delPrm.WithinSession(*prm.sessionToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
if prm.bearerToken != nil {
|
|
||||||
delPrm.WithBearerToken(*prm.bearerToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
delPrm.WithXHeaders(prm.xHeaders...)
|
|
||||||
|
|
||||||
cliRes, err := prm.cli.ObjectDelete(ctx, delPrm)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("remove object via client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &DeleteObjectRes{
|
|
||||||
tomb: cliRes.Tombstone(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetObjectPrm groups parameters of GetObject operation.
|
|
||||||
type GetObjectPrm struct {
|
|
||||||
commonObjectPrm
|
|
||||||
objectAddressPrm
|
|
||||||
rawPrm
|
|
||||||
payloadWriterPrm
|
|
||||||
headerCallback func(*objectSDK.Object)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetHeaderCallback sets callback which is called on the object after the header is received
|
|
||||||
// but before the payload is written.
|
|
||||||
func (p *GetObjectPrm) SetHeaderCallback(f func(*objectSDK.Object)) {
|
|
||||||
p.headerCallback = f
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetObjectRes groups the resulting values of GetObject operation.
|
|
||||||
type GetObjectRes struct {
|
|
||||||
hdr *objectSDK.Object
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header returns the header of the request object.
|
|
||||||
func (x GetObjectRes) Header() *objectSDK.Object {
|
|
||||||
return x.hdr
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetObject reads an object by address.
|
|
||||||
//
|
|
||||||
// Interrupts on any writer error. If successful, payload is written to the writer.
|
|
||||||
//
|
|
||||||
// Returns any error which prevented the operation from completing correctly in error return.
|
|
||||||
// For raw reading, returns *object.SplitInfoError error if object is virtual.
|
|
||||||
func GetObject(ctx context.Context, prm GetObjectPrm) (*GetObjectRes, error) {
|
|
||||||
var getPrm client.PrmObjectGet
|
|
||||||
getPrm.FromContainer(prm.objAddr.Container())
|
|
||||||
getPrm.ByID(prm.objAddr.Object())
|
|
||||||
|
|
||||||
if prm.sessionToken != nil {
|
|
||||||
getPrm.WithinSession(*prm.sessionToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
if prm.bearerToken != nil {
|
|
||||||
getPrm.WithBearerToken(*prm.bearerToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
if prm.raw {
|
|
||||||
getPrm.MarkRaw()
|
|
||||||
}
|
|
||||||
|
|
||||||
if prm.local {
|
|
||||||
getPrm.MarkLocal()
|
|
||||||
}
|
|
||||||
|
|
||||||
getPrm.WithXHeaders(prm.xHeaders...)
|
|
||||||
|
|
||||||
rdr, err := prm.cli.ObjectGetInit(ctx, getPrm)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("init object reading on client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var hdr objectSDK.Object
|
|
||||||
|
|
||||||
if !rdr.ReadHeader(&hdr) {
|
|
||||||
_, err = rdr.Close()
|
|
||||||
return nil, fmt.Errorf("read object header: %w", err)
|
|
||||||
}
|
|
||||||
if prm.headerCallback != nil {
|
|
||||||
prm.headerCallback(&hdr)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = io.Copy(prm.wrt, rdr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("copy payload: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &GetObjectRes{
|
|
||||||
hdr: &hdr,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HeadObjectPrm groups parameters of HeadObject operation.
|
|
||||||
type HeadObjectPrm struct {
|
|
||||||
commonObjectPrm
|
|
||||||
objectAddressPrm
|
|
||||||
rawPrm
|
|
||||||
|
|
||||||
mainOnly bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetMainOnlyFlag sets flag to get only main fields of an object header in terms of FrostFS API.
|
|
||||||
func (x *HeadObjectPrm) SetMainOnlyFlag(v bool) {
|
|
||||||
x.mainOnly = v
|
|
||||||
}
|
|
||||||
|
|
||||||
// HeadObjectRes groups the resulting values of HeadObject operation.
|
|
||||||
type HeadObjectRes struct {
|
|
||||||
hdr *objectSDK.Object
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header returns the requested object header.
|
|
||||||
func (x HeadObjectRes) Header() *objectSDK.Object {
|
|
||||||
return x.hdr
|
|
||||||
}
|
|
||||||
|
|
||||||
// HeadObject reads an object header by address.
|
|
||||||
//
|
|
||||||
// Returns any error which prevented the operation from completing correctly in error return.
|
|
||||||
// For raw reading, returns *object.SplitInfoError error if object is virtual.
|
|
||||||
func HeadObject(ctx context.Context, prm HeadObjectPrm) (*HeadObjectRes, error) {
|
|
||||||
var cliPrm client.PrmObjectHead
|
|
||||||
cliPrm.FromContainer(prm.objAddr.Container())
|
|
||||||
cliPrm.ByID(prm.objAddr.Object())
|
|
||||||
|
|
||||||
if prm.sessionToken != nil {
|
|
||||||
cliPrm.WithinSession(*prm.sessionToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
if prm.bearerToken != nil {
|
|
||||||
cliPrm.WithBearerToken(*prm.bearerToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
if prm.raw {
|
|
||||||
cliPrm.MarkRaw()
|
|
||||||
}
|
|
||||||
|
|
||||||
if prm.local {
|
|
||||||
cliPrm.MarkLocal()
|
|
||||||
}
|
|
||||||
|
|
||||||
cliPrm.WithXHeaders(prm.xHeaders...)
|
|
||||||
|
|
||||||
res, err := prm.cli.ObjectHead(ctx, cliPrm)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("read object header via client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var hdr objectSDK.Object
|
|
||||||
|
|
||||||
if !res.ReadHeader(&hdr) {
|
|
||||||
return nil, fmt.Errorf("missing header in response")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &HeadObjectRes{
|
|
||||||
hdr: &hdr,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SearchObjectsPrm groups parameters of SearchObjects operation.
|
|
||||||
type SearchObjectsPrm struct {
|
|
||||||
commonObjectPrm
|
|
||||||
containerIDPrm
|
|
||||||
|
|
||||||
filters objectSDK.SearchFilters
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFilters sets search filters.
|
|
||||||
func (x *SearchObjectsPrm) SetFilters(filters objectSDK.SearchFilters) {
|
|
||||||
x.filters = filters
|
|
||||||
}
|
|
||||||
|
|
||||||
// SearchObjectsRes groups the resulting values of SearchObjects operation.
|
|
||||||
type SearchObjectsRes struct {
|
|
||||||
ids []oid.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
// IDList returns identifiers of the matched objects.
|
|
||||||
func (x SearchObjectsRes) IDList() []oid.ID {
|
|
||||||
return x.ids
|
|
||||||
}
|
|
||||||
|
|
||||||
// SearchObjects selects objects from the container which match the filters.
|
|
||||||
//
|
|
||||||
// Returns any error which prevented the operation from completing correctly in error return.
|
|
||||||
func SearchObjects(ctx context.Context, prm SearchObjectsPrm) (*SearchObjectsRes, error) {
|
|
||||||
var cliPrm client.PrmObjectSearch
|
|
||||||
cliPrm.InContainer(prm.cnrID)
|
|
||||||
cliPrm.SetFilters(prm.filters)
|
|
||||||
|
|
||||||
if prm.sessionToken != nil {
|
|
||||||
cliPrm.WithinSession(*prm.sessionToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
if prm.bearerToken != nil {
|
|
||||||
cliPrm.WithBearerToken(*prm.bearerToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
if prm.local {
|
|
||||||
cliPrm.MarkLocal()
|
|
||||||
}
|
|
||||||
|
|
||||||
cliPrm.WithXHeaders(prm.xHeaders...)
|
|
||||||
|
|
||||||
rdr, err := prm.cli.ObjectSearchInit(ctx, cliPrm)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("init object search: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]oid.ID, 10)
|
|
||||||
var list []oid.ID
|
|
||||||
var n int
|
|
||||||
var ok bool
|
|
||||||
|
|
||||||
for {
|
|
||||||
n, ok = rdr.Read(buf)
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
list = append(list, buf[i])
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = rdr.Close()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("read object list: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &SearchObjectsRes{
|
|
||||||
ids: list,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HashPayloadRangesPrm groups parameters of HashPayloadRanges operation.
|
|
||||||
type HashPayloadRangesPrm struct {
|
|
||||||
commonObjectPrm
|
|
||||||
objectAddressPrm
|
|
||||||
|
|
||||||
tz bool
|
|
||||||
|
|
||||||
rngs []*objectSDK.Range
|
|
||||||
|
|
||||||
salt []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// TZ sets flag to request Tillich-Zemor hashes.
|
|
||||||
func (x *HashPayloadRangesPrm) TZ() {
|
|
||||||
x.tz = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetRanges sets a list of payload ranges to hash.
|
|
||||||
func (x *HashPayloadRangesPrm) SetRanges(rngs []*objectSDK.Range) {
|
|
||||||
x.rngs = rngs
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetSalt sets data for each range to be XOR'ed with.
|
|
||||||
func (x *HashPayloadRangesPrm) SetSalt(salt []byte) {
|
|
||||||
x.salt = salt
|
|
||||||
}
|
|
||||||
|
|
||||||
// HashPayloadRangesRes groups the resulting values of HashPayloadRanges operation.
|
|
||||||
type HashPayloadRangesRes struct {
|
|
||||||
cliRes *client.ResObjectHash
|
|
||||||
}
|
|
||||||
|
|
||||||
// HashList returns a list of hashes of the payload ranges keeping order.
|
|
||||||
func (x HashPayloadRangesRes) HashList() [][]byte {
|
|
||||||
return x.cliRes.Checksums()
|
|
||||||
}
|
|
||||||
|
|
||||||
// HashPayloadRanges requests hashes (by default SHA256) of the object payload ranges.
|
|
||||||
//
|
|
||||||
// Returns any error which prevented the operation from completing correctly in error return.
|
|
||||||
// Returns an error if number of received hashes differs with the number of requested ranges.
|
|
||||||
func HashPayloadRanges(ctx context.Context, prm HashPayloadRangesPrm) (*HashPayloadRangesRes, error) {
|
|
||||||
var cliPrm client.PrmObjectHash
|
|
||||||
cliPrm.FromContainer(prm.objAddr.Container())
|
|
||||||
cliPrm.ByID(prm.objAddr.Object())
|
|
||||||
|
|
||||||
if prm.local {
|
|
||||||
cliPrm.MarkLocal()
|
|
||||||
}
|
|
||||||
|
|
||||||
cliPrm.UseSalt(prm.salt)
|
|
||||||
|
|
||||||
rngs := make([]uint64, 2*len(prm.rngs))
|
|
||||||
|
|
||||||
for i := range prm.rngs {
|
|
||||||
rngs[2*i] = prm.rngs[i].GetOffset()
|
|
||||||
rngs[2*i+1] = prm.rngs[i].GetLength()
|
|
||||||
}
|
|
||||||
|
|
||||||
cliPrm.SetRangeList(rngs...)
|
|
||||||
|
|
||||||
if prm.tz {
|
|
||||||
cliPrm.TillichZemorAlgo()
|
|
||||||
}
|
|
||||||
|
|
||||||
if prm.sessionToken != nil {
|
|
||||||
cliPrm.WithinSession(*prm.sessionToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
if prm.bearerToken != nil {
|
|
||||||
cliPrm.WithBearerToken(*prm.bearerToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
cliPrm.WithXHeaders(prm.xHeaders...)
|
|
||||||
|
|
||||||
res, err := prm.cli.ObjectHash(ctx, cliPrm)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("read payload hashes via client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &HashPayloadRangesRes{
|
|
||||||
cliRes: res,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PayloadRangePrm groups parameters of PayloadRange operation.
|
|
||||||
type PayloadRangePrm struct {
|
|
||||||
commonObjectPrm
|
|
||||||
objectAddressPrm
|
|
||||||
rawPrm
|
|
||||||
payloadWriterPrm
|
|
||||||
|
|
||||||
rng *objectSDK.Range
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetRange sets payload range to read.
|
|
||||||
func (x *PayloadRangePrm) SetRange(rng *objectSDK.Range) {
|
|
||||||
x.rng = rng
|
|
||||||
}
|
|
||||||
|
|
||||||
// PayloadRangeRes groups the resulting values of PayloadRange operation.
|
|
||||||
type PayloadRangeRes struct{}
|
|
||||||
|
|
||||||
// PayloadRange reads object payload range from FrostFS and writes it to the specified writer.
|
|
||||||
//
|
|
||||||
// Interrupts on any writer error.
|
|
||||||
//
|
|
||||||
// Returns any error which prevented the operation from completing correctly in error return.
|
|
||||||
// For raw reading, returns *object.SplitInfoError error if object is virtual.
|
|
||||||
func PayloadRange(ctx context.Context, prm PayloadRangePrm) (*PayloadRangeRes, error) {
|
|
||||||
var cliPrm client.PrmObjectRange
|
|
||||||
cliPrm.FromContainer(prm.objAddr.Container())
|
|
||||||
cliPrm.ByID(prm.objAddr.Object())
|
|
||||||
|
|
||||||
if prm.sessionToken != nil {
|
|
||||||
cliPrm.WithinSession(*prm.sessionToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
if prm.bearerToken != nil {
|
|
||||||
cliPrm.WithBearerToken(*prm.bearerToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
if prm.raw {
|
|
||||||
cliPrm.MarkRaw()
|
|
||||||
}
|
|
||||||
|
|
||||||
if prm.local {
|
|
||||||
cliPrm.MarkLocal()
|
|
||||||
}
|
|
||||||
|
|
||||||
cliPrm.SetOffset(prm.rng.GetOffset())
|
|
||||||
cliPrm.SetLength(prm.rng.GetLength())
|
|
||||||
|
|
||||||
cliPrm.WithXHeaders(prm.xHeaders...)
|
|
||||||
|
|
||||||
rdr, err := prm.cli.ObjectRangeInit(ctx, cliPrm)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("init payload reading: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = io.Copy(prm.wrt, rdr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("copy payload: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return new(PayloadRangeRes), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SyncContainerPrm groups parameters of SyncContainerSettings operation.
|
|
||||||
type SyncContainerPrm struct {
|
|
||||||
commonPrm
|
|
||||||
c *containerSDK.Container
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetContainer sets a container that is required to be synced.
|
|
||||||
func (s *SyncContainerPrm) SetContainer(c *containerSDK.Container) {
|
|
||||||
s.c = c
|
|
||||||
}
|
|
||||||
|
|
||||||
// SyncContainerRes groups resulting values of SyncContainerSettings
|
|
||||||
// operation.
|
|
||||||
type SyncContainerRes struct{}
|
|
||||||
|
|
||||||
// SyncContainerSettings reads global network config from FrostFS and
|
|
||||||
// syncs container settings with it.
|
|
||||||
//
|
|
||||||
// Interrupts on any writer error.
|
|
||||||
//
|
|
||||||
// Panics if a container passed as a parameter is nil.
|
|
||||||
func SyncContainerSettings(ctx context.Context, prm SyncContainerPrm) (*SyncContainerRes, error) {
|
|
||||||
if prm.c == nil {
|
|
||||||
panic("sync container settings with the network: nil container")
|
|
||||||
}
|
|
||||||
|
|
||||||
err := client.SyncContainerWithNetwork(ctx, prm.c, prm.cli)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return new(SyncContainerRes), nil
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
// Package internal provides functionality for FrostFS CLI application
|
|
||||||
// communication with FrostFS network.
|
|
||||||
//
|
|
||||||
// The base client for accessing remote nodes via FrostFS API is a FrostFS SDK
|
|
||||||
// Go API client. However, although it encapsulates a useful piece of business
|
|
||||||
// logic (e.g. the signature mechanism), the FrostFS CLI application does not
|
|
||||||
// fully use the client's flexible interface.
|
|
||||||
//
|
|
||||||
// In this regard, this package provides functions over base API client
|
|
||||||
// necessary for the application. This allows you to concentrate the entire
|
|
||||||
// spectrum of the client's use in one place (this will be convenient both when
|
|
||||||
// updating the base client and for evaluating the UX of SDK library). So it is
|
|
||||||
// expected that all application packages will be limited to this package for
|
|
||||||
// the development of functionality requiring FrostFS API communication.
|
|
||||||
package internal
|
|
|
@ -1,92 +0,0 @@
|
||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
|
|
||||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
|
||||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
|
||||||
)
|
|
||||||
|
|
||||||
// here are small structures with public setters to share between parameter structures
|
|
||||||
|
|
||||||
type commonPrm struct {
|
|
||||||
cli *client.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetClient sets the base client for FrostFS API communication.
|
|
||||||
func (x *commonPrm) SetClient(cli *client.Client) {
|
|
||||||
x.cli = cli
|
|
||||||
}
|
|
||||||
|
|
||||||
type containerIDPrm struct {
|
|
||||||
cnrID cid.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetContainerID sets the container identifier.
|
|
||||||
func (x *containerIDPrm) SetContainerID(id cid.ID) {
|
|
||||||
x.cnrID = id
|
|
||||||
}
|
|
||||||
|
|
||||||
type bearerTokenPrm struct {
|
|
||||||
bearerToken *bearer.Token
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetBearerToken sets the bearer token to be attached to the request.
|
|
||||||
func (x *bearerTokenPrm) SetBearerToken(tok *bearer.Token) {
|
|
||||||
x.bearerToken = tok
|
|
||||||
}
|
|
||||||
|
|
||||||
type objectAddressPrm struct {
|
|
||||||
objAddr oid.Address
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *objectAddressPrm) SetAddress(addr oid.Address) {
|
|
||||||
x.objAddr = addr
|
|
||||||
}
|
|
||||||
|
|
||||||
type rawPrm struct {
|
|
||||||
raw bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetRawFlag sets flag of raw request.
|
|
||||||
func (x *rawPrm) SetRawFlag(raw bool) {
|
|
||||||
x.raw = raw
|
|
||||||
}
|
|
||||||
|
|
||||||
type payloadWriterPrm struct {
|
|
||||||
wrt io.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetPayloadWriter sets the writer of the object payload.
|
|
||||||
func (x *payloadWriterPrm) SetPayloadWriter(wrt io.Writer) {
|
|
||||||
x.wrt = wrt
|
|
||||||
}
|
|
||||||
|
|
||||||
type commonObjectPrm struct {
|
|
||||||
commonPrm
|
|
||||||
bearerTokenPrm
|
|
||||||
|
|
||||||
sessionToken *session.Object
|
|
||||||
|
|
||||||
local bool
|
|
||||||
|
|
||||||
xHeaders []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetTTL sets request TTL value.
|
|
||||||
func (x *commonObjectPrm) SetTTL(ttl uint32) {
|
|
||||||
x.local = ttl < 2
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetXHeaders sets request X-Headers.
|
|
||||||
func (x *commonObjectPrm) SetXHeaders(hs []string) {
|
|
||||||
x.xHeaders = hs
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetSessionToken sets the token of the session within which the request should be sent.
|
|
||||||
func (x *commonObjectPrm) SetSessionToken(tok *session.Object) {
|
|
||||||
x.sessionToken = tok
|
|
||||||
}
|
|
|
@ -1,101 +0,0 @@
|
||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/rand"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/network"
|
|
||||||
tracing "git.frostfs.info/TrueCloudLab/frostfs-observability/tracing/grpc"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errInvalidEndpoint = errors.New("provided RPC endpoint is incorrect")
|
|
||||||
|
|
||||||
// GetSDKClientByFlag returns default frostfs-sdk-go client using the specified flag for the address.
|
|
||||||
// On error, outputs to stderr of cmd and exits with non-zero code.
|
|
||||||
func GetSDKClientByFlag(cmd *cobra.Command, key *ecdsa.PrivateKey, endpointFlag string) *client.Client {
|
|
||||||
cli, err := getSDKClientByFlag(cmd, key, endpointFlag)
|
|
||||||
if err != nil {
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't create API client: %w", err)
|
|
||||||
}
|
|
||||||
return cli
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSDKClientByFlag(cmd *cobra.Command, key *ecdsa.PrivateKey, endpointFlag string) (*client.Client, error) {
|
|
||||||
var addr network.Address
|
|
||||||
|
|
||||||
err := addr.FromString(viper.GetString(endpointFlag))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%v: %w", errInvalidEndpoint, err)
|
|
||||||
}
|
|
||||||
return GetSDKClient(cmd.Context(), cmd, key, addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSDKClient returns default frostfs-sdk-go client.
|
|
||||||
func GetSDKClient(ctx context.Context, cmd *cobra.Command, key *ecdsa.PrivateKey, addr network.Address) (*client.Client, error) {
|
|
||||||
var (
|
|
||||||
c client.Client
|
|
||||||
prmInit client.PrmInit
|
|
||||||
prmDial client.PrmDial
|
|
||||||
)
|
|
||||||
|
|
||||||
prmInit.SetDefaultPrivateKey(*key)
|
|
||||||
prmInit.ResolveFrostFSFailures()
|
|
||||||
prmDial.SetServerURI(addr.URIAddr())
|
|
||||||
if timeout := viper.GetDuration(commonflags.Timeout); timeout > 0 {
|
|
||||||
// In CLI we can only set a timeout for the whole operation.
|
|
||||||
// By also setting stream timeout we ensure that no operation hands
|
|
||||||
// for too long.
|
|
||||||
prmDial.SetTimeout(timeout)
|
|
||||||
prmDial.SetStreamTimeout(timeout)
|
|
||||||
|
|
||||||
common.PrintVerbose(cmd, "Set request timeout to %s.", timeout)
|
|
||||||
}
|
|
||||||
prmDial.SetGRPCDialOptions(
|
|
||||||
grpc.WithChainUnaryInterceptor(tracing.NewUnaryClientInteceptor()),
|
|
||||||
grpc.WithChainStreamInterceptor(tracing.NewStreamClientInterceptor()))
|
|
||||||
|
|
||||||
c.Init(prmInit)
|
|
||||||
|
|
||||||
if err := c.Dial(ctx, prmDial); err != nil {
|
|
||||||
return nil, fmt.Errorf("can't init SDK client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCurrentEpoch returns current epoch.
|
|
||||||
func GetCurrentEpoch(ctx context.Context, cmd *cobra.Command, endpoint string) (uint64, error) {
|
|
||||||
var addr network.Address
|
|
||||||
|
|
||||||
if err := addr.FromString(endpoint); err != nil {
|
|
||||||
return 0, fmt.Errorf("can't parse RPC endpoint: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("can't generate key to sign query: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := GetSDKClient(ctx, cmd, key, addr)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ni, err := c.NetworkInfo(ctx, client.PrmNetworkInfo{})
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ni.Info().CurrentEpoch(), nil
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/version"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
|
||||||
versionSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/version"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errUnsupportedEACLFormat = errors.New("unsupported eACL format")
|
|
||||||
|
|
||||||
// ReadEACL reads extended ACL table from eaclPath.
|
|
||||||
func ReadEACL(cmd *cobra.Command, eaclPath string) *eacl.Table {
|
|
||||||
_, err := os.Stat(eaclPath) // check if `eaclPath` is an existing file
|
|
||||||
if err != nil {
|
|
||||||
commonCmd.ExitOnErr(cmd, "", errors.New("incorrect path to file with EACL"))
|
|
||||||
}
|
|
||||||
|
|
||||||
PrintVerbose(cmd, "Reading EACL from file: %s", eaclPath)
|
|
||||||
|
|
||||||
data, err := os.ReadFile(eaclPath)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't read file with EACL: %w", err)
|
|
||||||
|
|
||||||
table := eacl.NewTable()
|
|
||||||
|
|
||||||
if err = table.UnmarshalJSON(data); err == nil {
|
|
||||||
validateAndFixEACLVersion(table)
|
|
||||||
PrintVerbose(cmd, "Parsed JSON encoded EACL table")
|
|
||||||
return table
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = table.Unmarshal(data); err == nil {
|
|
||||||
validateAndFixEACLVersion(table)
|
|
||||||
PrintVerbose(cmd, "Parsed binary encoded EACL table")
|
|
||||||
return table
|
|
||||||
}
|
|
||||||
|
|
||||||
commonCmd.ExitOnErr(cmd, "", errUnsupportedEACLFormat)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateAndFixEACLVersion(table *eacl.Table) {
|
|
||||||
if !version.IsValid(table.Version()) {
|
|
||||||
table.SetVersion(versionSDK.Current())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ParseEpoch parses epoch argument. Second return value is true if
|
|
||||||
// the specified epoch is relative, and false otherwise.
|
|
||||||
func ParseEpoch(cmd *cobra.Command, flag string) (uint64, bool, error) {
|
|
||||||
s, _ := cmd.Flags().GetString(flag)
|
|
||||||
if len(s) == 0 {
|
|
||||||
return 0, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
relative := s[0] == '+'
|
|
||||||
if relative {
|
|
||||||
s = s[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
epoch, err := strconv.ParseUint(s, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, relative, fmt.Errorf("can't parse epoch for %s argument: %w", flag, err)
|
|
||||||
}
|
|
||||||
return epoch, relative, nil
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PrettyPrintJSON prints m as an indented JSON to the cmd output.
|
|
||||||
func PrettyPrintJSON(cmd *cobra.Command, m json.Marshaler, entity string) {
|
|
||||||
data, err := m.MarshalJSON()
|
|
||||||
if err != nil {
|
|
||||||
PrintVerbose(cmd, "Can't convert %s to json: %w", entity, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
if err := json.Indent(buf, data, "", " "); err != nil {
|
|
||||||
PrintVerbose(cmd, "Can't pretty print json: %w", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cmd.Println(buf)
|
|
||||||
}
|
|
|
@ -1,67 +0,0 @@
|
||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ReadBearerToken reads bearer token from the path provided in a specified flag.
|
|
||||||
func ReadBearerToken(cmd *cobra.Command, flagname string) *bearer.Token {
|
|
||||||
path, err := cmd.Flags().GetString(flagname)
|
|
||||||
commonCmd.ExitOnErr(cmd, "", err)
|
|
||||||
|
|
||||||
if len(path) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
PrintVerbose(cmd, "Reading bearer token from file [%s]...", path)
|
|
||||||
|
|
||||||
var tok bearer.Token
|
|
||||||
|
|
||||||
err = ReadBinaryOrJSON(cmd, &tok, path)
|
|
||||||
commonCmd.ExitOnErr(cmd, "invalid bearer token: %v", err)
|
|
||||||
|
|
||||||
return &tok
|
|
||||||
}
|
|
||||||
|
|
||||||
// BinaryOrJSON is an interface of entities which provide json.Unmarshaler
|
|
||||||
// and FrostFS binary decoder.
|
|
||||||
type BinaryOrJSON interface {
|
|
||||||
Unmarshal([]byte) error
|
|
||||||
json.Unmarshaler
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadBinaryOrJSON reads file data using provided path and decodes
|
|
||||||
// BinaryOrJSON from the data.
|
|
||||||
func ReadBinaryOrJSON(cmd *cobra.Command, dst BinaryOrJSON, fPath string) error {
|
|
||||||
PrintVerbose(cmd, "Reading file [%s]...", fPath)
|
|
||||||
|
|
||||||
// try to read session token from file
|
|
||||||
data, err := os.ReadFile(fPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("read file <%s>: %w", fPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
PrintVerbose(cmd, "Trying to decode binary...")
|
|
||||||
|
|
||||||
err = dst.Unmarshal(data)
|
|
||||||
if err != nil {
|
|
||||||
PrintVerbose(cmd, "Failed to decode binary: %v", err)
|
|
||||||
|
|
||||||
PrintVerbose(cmd, "Trying to decode JSON...")
|
|
||||||
|
|
||||||
err = dst.UnmarshalJSON(data)
|
|
||||||
if err != nil {
|
|
||||||
PrintVerbose(cmd, "Failed to decode JSON: %v", err)
|
|
||||||
return errors.New("invalid format")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/misc"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"go.opentelemetry.io/otel/trace"
|
|
||||||
)
|
|
||||||
|
|
||||||
type spanKey struct{}
|
|
||||||
|
|
||||||
// StopClientCommandSpan stops tracing span for the command and prints trace ID on the standard output.
|
|
||||||
func StopClientCommandSpan(cmd *cobra.Command, _ []string) {
|
|
||||||
span, ok := cmd.Context().Value(spanKey{}).(trace.Span)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
span.End()
|
|
||||||
|
|
||||||
// Noop provider cannot fail on flush.
|
|
||||||
_ = tracing.Shutdown(cmd.Context())
|
|
||||||
|
|
||||||
cmd.PrintErrf("Trace ID: %s\n", span.SpanContext().TraceID())
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartClientCommandSpan starts tracing span for the command.
|
|
||||||
func StartClientCommandSpan(cmd *cobra.Command) {
|
|
||||||
enableTracing, err := cmd.Flags().GetBool(commonflags.TracingFlag)
|
|
||||||
if err != nil || !enableTracing {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = tracing.Setup(cmd.Context(), tracing.Config{
|
|
||||||
Enabled: true,
|
|
||||||
Exporter: tracing.NoOpExporter,
|
|
||||||
Service: "frostfs-cli",
|
|
||||||
Version: misc.Version,
|
|
||||||
})
|
|
||||||
commonCmd.ExitOnErr(cmd, "init tracing: %w", err)
|
|
||||||
|
|
||||||
var components sort.StringSlice
|
|
||||||
for c := cmd; c != nil; c = c.Parent() {
|
|
||||||
components = append(components, c.Name())
|
|
||||||
}
|
|
||||||
for i, j := 0, len(components)-1; i < j; {
|
|
||||||
components.Swap(i, j)
|
|
||||||
i++
|
|
||||||
j--
|
|
||||||
}
|
|
||||||
|
|
||||||
operation := strings.Join(components, ".")
|
|
||||||
ctx, span := tracing.StartSpanFromContext(cmd.Context(), operation)
|
|
||||||
ctx = context.WithValue(ctx, spanKey{}, span)
|
|
||||||
cmd.SetContext(ctx)
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/checksum"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PrintVerbose prints to the stdout if the commonflags.Verbose flag is on.
|
|
||||||
func PrintVerbose(cmd *cobra.Command, format string, a ...any) {
|
|
||||||
if viper.GetBool(commonflags.Verbose) {
|
|
||||||
cmd.Printf(format+"\n", a...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrettyPrintUnixTime interprets s as unix timestamp and prints it as
|
|
||||||
// a date. Is s is invalid, "malformed" is returned.
|
|
||||||
func PrettyPrintUnixTime(s string) string {
|
|
||||||
unixTime, err := strconv.ParseInt(s, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return "malformed"
|
|
||||||
}
|
|
||||||
|
|
||||||
timestamp := time.Unix(unixTime, 0)
|
|
||||||
|
|
||||||
return timestamp.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrintChecksum prints checksum.
|
|
||||||
func PrintChecksum(cmd *cobra.Command, name string, recv func() (checksum.Checksum, bool)) {
|
|
||||||
var strVal string
|
|
||||||
|
|
||||||
cs, csSet := recv()
|
|
||||||
if csSet {
|
|
||||||
strVal = hex.EncodeToString(cs.Value())
|
|
||||||
} else {
|
|
||||||
strVal = "<empty>"
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Printf("%s: %s\n", name, strVal)
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
package commonflags
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
TTL = "ttl"
|
|
||||||
TTLShorthand = ""
|
|
||||||
TTLDefault = 2
|
|
||||||
TTLUsage = "TTL value in request meta header"
|
|
||||||
|
|
||||||
XHeadersKey = "xhdr"
|
|
||||||
XHeadersShorthand = "x"
|
|
||||||
XHeadersUsage = "Request X-Headers in form of Key=Value"
|
|
||||||
)
|
|
||||||
|
|
||||||
// InitAPI inits common flags for storage node services.
|
|
||||||
func InitAPI(cmd *cobra.Command) {
|
|
||||||
ff := cmd.Flags()
|
|
||||||
|
|
||||||
ff.StringSliceP(XHeadersKey, XHeadersShorthand, []string{}, XHeadersUsage)
|
|
||||||
ff.Uint32P(TTL, TTLShorthand, TTLDefault, TTLUsage)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BindAPI binds API flags of storage node services to the viper.
|
|
||||||
func BindAPI(cmd *cobra.Command) {
|
|
||||||
ff := cmd.Flags()
|
|
||||||
|
|
||||||
_ = viper.BindPFlag(TTL, ff.Lookup(TTL))
|
|
||||||
_ = viper.BindPFlag(XHeadersKey, ff.Lookup(XHeadersKey))
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
package commonflags
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ExpireAt is a flag for setting last epoch of an object or a token.
|
|
||||||
ExpireAt = "expire-at"
|
|
||||||
// Lifetime is a flag for setting the lifetime of an object or a token,
|
|
||||||
// starting from the current epoch.
|
|
||||||
Lifetime = "lifetime"
|
|
||||||
)
|
|
|
@ -1,89 +0,0 @@
|
||||||
package commonflags
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Common CLI flag keys, shorthands, default
|
|
||||||
// values and their usage descriptions.
|
|
||||||
const (
|
|
||||||
GenerateKey = "generate-key"
|
|
||||||
generateKeyShorthand = "g"
|
|
||||||
generateKeyDefault = false
|
|
||||||
generateKeyUsage = "Generate new private key"
|
|
||||||
|
|
||||||
WalletPath = "wallet"
|
|
||||||
WalletPathShorthand = "w"
|
|
||||||
WalletPathDefault = ""
|
|
||||||
WalletPathUsage = "Path to the wallet or binary key"
|
|
||||||
|
|
||||||
Account = "address"
|
|
||||||
AccountShorthand = ""
|
|
||||||
AccountDefault = ""
|
|
||||||
AccountUsage = "Address of wallet account"
|
|
||||||
|
|
||||||
RPC = "rpc-endpoint"
|
|
||||||
RPCShorthand = "r"
|
|
||||||
RPCDefault = ""
|
|
||||||
RPCUsage = "Remote node address (as 'multiaddr' or '<host>:<port>')"
|
|
||||||
|
|
||||||
Timeout = "timeout"
|
|
||||||
TimeoutShorthand = "t"
|
|
||||||
TimeoutDefault = 15 * time.Second
|
|
||||||
TimeoutUsage = "Timeout for an operation"
|
|
||||||
|
|
||||||
Verbose = "verbose"
|
|
||||||
VerboseShorthand = "v"
|
|
||||||
VerboseUsage = "Verbose output"
|
|
||||||
|
|
||||||
ForceFlag = "force"
|
|
||||||
ForceFlagShorthand = "f"
|
|
||||||
|
|
||||||
CIDFlag = "cid"
|
|
||||||
CIDFlagUsage = "Container ID."
|
|
||||||
|
|
||||||
OIDFlag = "oid"
|
|
||||||
OIDFlagUsage = "Object ID."
|
|
||||||
|
|
||||||
TracingFlag = "trace"
|
|
||||||
TracingFlagUsage = "Generate trace ID and print it."
|
|
||||||
)
|
|
||||||
|
|
||||||
// Init adds common flags to the command:
|
|
||||||
// - GenerateKey,
|
|
||||||
// - WalletPath,
|
|
||||||
// - Account,
|
|
||||||
// - RPC,
|
|
||||||
// - Tracing,
|
|
||||||
// - Timeout.
|
|
||||||
func Init(cmd *cobra.Command) {
|
|
||||||
InitWithoutRPC(cmd)
|
|
||||||
|
|
||||||
ff := cmd.Flags()
|
|
||||||
ff.StringP(RPC, RPCShorthand, RPCDefault, RPCUsage)
|
|
||||||
ff.Bool(TracingFlag, false, TracingFlagUsage)
|
|
||||||
ff.DurationP(Timeout, TimeoutShorthand, TimeoutDefault, TimeoutUsage)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitWithoutRPC is similar to Init but doesn't create the RPC flag.
|
|
||||||
func InitWithoutRPC(cmd *cobra.Command) {
|
|
||||||
ff := cmd.Flags()
|
|
||||||
|
|
||||||
ff.BoolP(GenerateKey, generateKeyShorthand, generateKeyDefault, generateKeyUsage)
|
|
||||||
ff.StringP(WalletPath, WalletPathShorthand, WalletPathDefault, WalletPathUsage)
|
|
||||||
ff.StringP(Account, AccountShorthand, AccountDefault, AccountUsage)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bind binds common command flags to the viper.
|
|
||||||
func Bind(cmd *cobra.Command) {
|
|
||||||
ff := cmd.Flags()
|
|
||||||
|
|
||||||
_ = viper.BindPFlag(GenerateKey, ff.Lookup(GenerateKey))
|
|
||||||
_ = viper.BindPFlag(WalletPath, ff.Lookup(WalletPath))
|
|
||||||
_ = viper.BindPFlag(Account, ff.Lookup(Account))
|
|
||||||
_ = viper.BindPFlag(RPC, ff.Lookup(RPC))
|
|
||||||
_ = viper.BindPFlag(Timeout, ff.Lookup(Timeout))
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
package commonflags
|
|
||||||
|
|
||||||
const JSON = "json"
|
|
|
@ -1,19 +0,0 @@
|
||||||
package commonflags
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
const SessionToken = "session"
|
|
||||||
|
|
||||||
// InitSession registers SessionToken flag representing file path to the token of
|
|
||||||
// the session with the given name. Supports FrostFS-binary and JSON files.
|
|
||||||
func InitSession(cmd *cobra.Command, name string) {
|
|
||||||
cmd.Flags().String(
|
|
||||||
SessionToken,
|
|
||||||
"",
|
|
||||||
fmt.Sprintf("Filepath to a JSON- or binary-encoded token of the %s session", name),
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
package key
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errCantGenerateKey = errors.New("can't generate new private key")
|
|
||||||
|
|
||||||
// Get returns private key from wallet or binary file.
|
|
||||||
// Ideally we want to touch file-system on the last step.
|
|
||||||
// This function assumes that all flags were bind to viper in a `PersistentPreRun`.
|
|
||||||
func Get(cmd *cobra.Command) *ecdsa.PrivateKey {
|
|
||||||
pk, err := get(cmd)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't fetch private key: %w", err)
|
|
||||||
return pk
|
|
||||||
}
|
|
||||||
|
|
||||||
func get(cmd *cobra.Command) (*ecdsa.PrivateKey, error) {
|
|
||||||
keyDesc := viper.GetString(commonflags.WalletPath)
|
|
||||||
data, err := os.ReadFile(keyDesc)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%w: %v", ErrFs, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
priv, err := keys.NewPrivateKeyFromBytes(data)
|
|
||||||
if err != nil {
|
|
||||||
w, err := wallet.NewWalletFromFile(keyDesc)
|
|
||||||
if err == nil {
|
|
||||||
return FromWallet(cmd, w, viper.GetString(commonflags.Account))
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("%w: %v", ErrInvalidKey, err)
|
|
||||||
}
|
|
||||||
return &priv.PrivateKey, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetOrGenerate is similar to get but generates a new key if commonflags.GenerateKey is set.
|
|
||||||
func GetOrGenerate(cmd *cobra.Command) *ecdsa.PrivateKey {
|
|
||||||
pk, err := getOrGenerate(cmd)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't fetch private key: %w", err)
|
|
||||||
return pk
|
|
||||||
}
|
|
||||||
|
|
||||||
func getOrGenerate(cmd *cobra.Command) (*ecdsa.PrivateKey, error) {
|
|
||||||
if viper.GetBool(commonflags.GenerateKey) {
|
|
||||||
priv, err := keys.NewPrivateKey()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%w: %v", errCantGenerateKey, err)
|
|
||||||
}
|
|
||||||
return &priv.PrivateKey, nil
|
|
||||||
}
|
|
||||||
return get(cmd)
|
|
||||||
}
|
|
|
@ -1,70 +0,0 @@
|
||||||
package key
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
|
||||||
"github.com/nspcc-dev/neo-go/cli/flags"
|
|
||||||
"github.com/nspcc-dev/neo-go/cli/input"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Key-related errors.
|
|
||||||
var (
|
|
||||||
ErrFs = errors.New("unable to read file from given path")
|
|
||||||
ErrInvalidKey = errors.New("provided key is incorrect, only wallet or binary key supported")
|
|
||||||
ErrInvalidAddress = errors.New("--address option must be specified and valid")
|
|
||||||
ErrInvalidPassword = errors.New("invalid password for the encrypted key")
|
|
||||||
)
|
|
||||||
|
|
||||||
// FromWallet returns private key of the wallet account.
|
|
||||||
func FromWallet(cmd *cobra.Command, w *wallet.Wallet, addrStr string) (*ecdsa.PrivateKey, error) {
|
|
||||||
var (
|
|
||||||
addr util.Uint160
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
if addrStr == "" {
|
|
||||||
common.PrintVerbose(cmd, "Using default wallet address")
|
|
||||||
addr = w.GetChangeAddress()
|
|
||||||
} else {
|
|
||||||
addr, err = flags.ParseAddress(addrStr)
|
|
||||||
if err != nil {
|
|
||||||
common.PrintVerbose(cmd, "Can't parse address: %s", addrStr)
|
|
||||||
return nil, ErrInvalidAddress
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
acc := w.GetAccount(addr)
|
|
||||||
if acc == nil {
|
|
||||||
common.PrintVerbose(cmd, "Can't find wallet account for %s", addrStr)
|
|
||||||
return nil, ErrInvalidAddress
|
|
||||||
}
|
|
||||||
|
|
||||||
pass, err := getPassword()
|
|
||||||
if err != nil {
|
|
||||||
common.PrintVerbose(cmd, "Can't read password: %v", err)
|
|
||||||
return nil, ErrInvalidPassword
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := acc.Decrypt(pass, keys.NEP2ScryptParams()); err != nil {
|
|
||||||
common.PrintVerbose(cmd, "Can't decrypt account: %v", err)
|
|
||||||
return nil, ErrInvalidPassword
|
|
||||||
}
|
|
||||||
|
|
||||||
return &acc.PrivateKey().PrivateKey, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPassword() (string, error) {
|
|
||||||
// this check allows empty passwords
|
|
||||||
if viper.IsSet("password") {
|
|
||||||
return viper.GetString("password"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return input.ReadPassword("Enter password > ")
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import cmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
cmd.Execute()
|
|
||||||
}
|
|
|
@ -1,70 +0,0 @@
|
||||||
package accounting
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/big"
|
|
||||||
|
|
||||||
internalclient "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/precision"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/accounting"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ownerFlag = "owner"
|
|
||||||
)
|
|
||||||
|
|
||||||
var accountingBalanceCmd = &cobra.Command{
|
|
||||||
Use: "balance",
|
|
||||||
Short: "Get internal balance of FrostFS account",
|
|
||||||
Long: `Get internal balance of FrostFS account`,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
var idUser user.ID
|
|
||||||
|
|
||||||
pk := key.GetOrGenerate(cmd)
|
|
||||||
|
|
||||||
balanceOwner, _ := cmd.Flags().GetString(ownerFlag)
|
|
||||||
if balanceOwner == "" {
|
|
||||||
user.IDFromKey(&idUser, pk.PublicKey)
|
|
||||||
} else {
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't decode owner ID wallet address: %w", idUser.DecodeString(balanceOwner))
|
|
||||||
}
|
|
||||||
|
|
||||||
cli := internalclient.GetSDKClientByFlag(cmd, pk, commonflags.RPC)
|
|
||||||
|
|
||||||
var prm internalclient.BalanceOfPrm
|
|
||||||
prm.SetClient(cli)
|
|
||||||
prm.SetAccount(idUser)
|
|
||||||
|
|
||||||
res, err := internalclient.BalanceOf(cmd.Context(), prm)
|
|
||||||
commonCmd.ExitOnErr(cmd, "rpc error: %w", err)
|
|
||||||
|
|
||||||
// print to stdout
|
|
||||||
prettyPrintDecimal(cmd, res.Balance())
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func initAccountingBalanceCmd() {
|
|
||||||
ff := accountingBalanceCmd.Flags()
|
|
||||||
|
|
||||||
ff.StringP(commonflags.WalletPath, commonflags.WalletPathShorthand, commonflags.WalletPathDefault, commonflags.WalletPathUsage)
|
|
||||||
ff.StringP(commonflags.Account, commonflags.AccountShorthand, commonflags.AccountDefault, commonflags.AccountUsage)
|
|
||||||
ff.StringP(commonflags.RPC, commonflags.RPCShorthand, commonflags.RPCDefault, commonflags.RPCUsage)
|
|
||||||
ff.String(ownerFlag, "", "owner of balance account (omit to use owner from private key)")
|
|
||||||
}
|
|
||||||
|
|
||||||
func prettyPrintDecimal(cmd *cobra.Command, decimal accounting.Decimal) {
|
|
||||||
if viper.GetBool(commonflags.Verbose) {
|
|
||||||
cmd.Println("value:", decimal.Value())
|
|
||||||
cmd.Println("precision:", decimal.Precision())
|
|
||||||
} else {
|
|
||||||
amountF8 := precision.Convert(decimal.Precision(), 8, big.NewInt(decimal.Value()))
|
|
||||||
|
|
||||||
cmd.Println(fixedn.ToString(amountF8, 8))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
package accounting
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Cmd represents the accounting command.
|
|
||||||
var Cmd = &cobra.Command{
|
|
||||||
Use: "accounting",
|
|
||||||
Short: "Operations with accounts and balances",
|
|
||||||
Long: `Operations with accounts and balances`,
|
|
||||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
|
||||||
flags := cmd.Flags()
|
|
||||||
|
|
||||||
_ = viper.BindPFlag(commonflags.WalletPath, flags.Lookup(commonflags.WalletPath))
|
|
||||||
_ = viper.BindPFlag(commonflags.Account, flags.Lookup(commonflags.Account))
|
|
||||||
_ = viper.BindPFlag(commonflags.RPC, flags.Lookup(commonflags.RPC))
|
|
||||||
common.StartClientCommandSpan(cmd)
|
|
||||||
},
|
|
||||||
PersistentPostRun: common.StopClientCommandSpan,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
Cmd.AddCommand(accountingBalanceCmd)
|
|
||||||
|
|
||||||
initAccountingBalanceCmd()
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
package basic
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/util"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var printACLCmd = &cobra.Command{
|
|
||||||
Use: "print",
|
|
||||||
Short: "Pretty print basic ACL from the HEX representation",
|
|
||||||
Example: `frostfs-cli acl basic print 0x1C8C8CCC`,
|
|
||||||
Long: `Pretty print basic ACL from the HEX representation.
|
|
||||||
Few roles have exclusive default access to set of operation, even if particular bit deny it.
|
|
||||||
Container have access to the operations of the data replication mechanism:
|
|
||||||
Get, Head, Put, Search, Hash.
|
|
||||||
InnerRing members are allowed to data audit ops only:
|
|
||||||
Get, Head, Hash, Search.`,
|
|
||||||
Run: printACL,
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
}
|
|
||||||
|
|
||||||
func printACL(cmd *cobra.Command, args []string) {
|
|
||||||
var bacl acl.Basic
|
|
||||||
commonCmd.ExitOnErr(cmd, "unable to parse basic acl: %w", bacl.DecodeString(args[0]))
|
|
||||||
util.PrettyPrintTableBACL(cmd, &bacl)
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
package basic
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Cmd = &cobra.Command{
|
|
||||||
Use: "basic",
|
|
||||||
Short: "Operations with Basic Access Control Lists",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
Cmd.AddCommand(printACLCmd)
|
|
||||||
}
|
|
|
@ -1,127 +0,0 @@
|
||||||
package extended
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/util"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var createCmd = &cobra.Command{
|
|
||||||
Use: "create",
|
|
||||||
Short: "Create extended ACL from the text representation",
|
|
||||||
Long: `Create extended ACL from the text representation.
|
|
||||||
|
|
||||||
Rule consist of these blocks: <action> <operation> [<filter1> ...] [<target1> ...]
|
|
||||||
|
|
||||||
Action is 'allow' or 'deny'.
|
|
||||||
|
|
||||||
Operation is an object service verb: 'get', 'head', 'put', 'search', 'delete', 'getrange', or 'getrangehash'.
|
|
||||||
|
|
||||||
Filter consists of <typ>:<key><match><value>
|
|
||||||
Typ is 'obj' for object applied filter or 'req' for request applied filter.
|
|
||||||
Key is a valid unicode string corresponding to object or request header key.
|
|
||||||
Well-known system object headers start with '$Object:' prefix.
|
|
||||||
User defined headers start without prefix.
|
|
||||||
Read more about filter keys at git.frostfs.info.com/TrueCloudLab/frostfs-api/src/branch/master/proto-docs/acl.md#message-eaclrecordfilter
|
|
||||||
Match is '=' for matching and '!=' for non-matching filter.
|
|
||||||
Value is a valid unicode string corresponding to object or request header value.
|
|
||||||
|
|
||||||
Target is
|
|
||||||
'user' for container owner,
|
|
||||||
'system' for Storage nodes in container and Inner Ring nodes,
|
|
||||||
'others' for all other request senders,
|
|
||||||
'pubkey:<key1>,<key2>,...' for exact request sender, where <key> is a hex-encoded 33-byte public key.
|
|
||||||
|
|
||||||
When both '--rule' and '--file' arguments are used, '--rule' records will be placed higher in resulting extended ACL table.
|
|
||||||
`,
|
|
||||||
Example: `frostfs-cli acl extended create --cid EutHBsdT1YCzHxjCfQHnLPL1vFrkSyLSio4vkphfnEk -f rules.txt --out table.json
|
|
||||||
frostfs-cli acl extended create --cid EutHBsdT1YCzHxjCfQHnLPL1vFrkSyLSio4vkphfnEk -r 'allow get obj:Key=Value others' -r 'deny put others'`,
|
|
||||||
Run: createEACL,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
createCmd.Flags().StringArrayP("rule", "r", nil, "Extended ACL table record to apply")
|
|
||||||
createCmd.Flags().StringP("file", "f", "", "Read list of extended ACL table records from text file")
|
|
||||||
createCmd.Flags().StringP("out", "o", "", "Save JSON formatted extended ACL table in file")
|
|
||||||
createCmd.Flags().StringP(commonflags.CIDFlag, "", "", commonflags.CIDFlagUsage)
|
|
||||||
|
|
||||||
_ = cobra.MarkFlagFilename(createCmd.Flags(), "file")
|
|
||||||
_ = cobra.MarkFlagFilename(createCmd.Flags(), "out")
|
|
||||||
}
|
|
||||||
|
|
||||||
func createEACL(cmd *cobra.Command, _ []string) {
|
|
||||||
rules, _ := cmd.Flags().GetStringArray("rule")
|
|
||||||
fileArg, _ := cmd.Flags().GetString("file")
|
|
||||||
outArg, _ := cmd.Flags().GetString("out")
|
|
||||||
cidArg, _ := cmd.Flags().GetString(commonflags.CIDFlag)
|
|
||||||
|
|
||||||
var containerID cid.ID
|
|
||||||
if cidArg != "" {
|
|
||||||
if err := containerID.DecodeString(cidArg); err != nil {
|
|
||||||
cmd.PrintErrf("invalid container ID: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rulesFile, err := getRulesFromFile(fileArg)
|
|
||||||
if err != nil {
|
|
||||||
cmd.PrintErrf("can't read rules from file: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
rules = append(rules, rulesFile...)
|
|
||||||
if len(rules) == 0 {
|
|
||||||
cmd.PrintErrln("no extended ACL rules has been provided")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
tb := eacl.NewTable()
|
|
||||||
commonCmd.ExitOnErr(cmd, "unable to parse provided rules: %w", util.ParseEACLRules(tb, rules))
|
|
||||||
|
|
||||||
tb.SetCID(containerID)
|
|
||||||
|
|
||||||
data, err := tb.MarshalJSON()
|
|
||||||
if err != nil {
|
|
||||||
cmd.PrintErrln(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
err = json.Indent(buf, data, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
cmd.PrintErrln(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(outArg) == 0 {
|
|
||||||
cmd.Println(buf)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.WriteFile(outArg, buf.Bytes(), 0644)
|
|
||||||
if err != nil {
|
|
||||||
cmd.PrintErrln(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRulesFromFile(filename string) ([]string, error) {
|
|
||||||
if len(filename) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := os.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Split(strings.TrimSpace(string(data)), "\n"), nil
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
package extended
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/util"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var printEACLCmd = &cobra.Command{
|
|
||||||
Use: "print",
|
|
||||||
Short: "Pretty print extended ACL from the file(in text or json format) or for given container.",
|
|
||||||
Run: printEACL,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
flags := printEACLCmd.Flags()
|
|
||||||
flags.StringP("file", "f", "",
|
|
||||||
"Read list of extended ACL table records from text or json file")
|
|
||||||
_ = printEACLCmd.MarkFlagRequired("file")
|
|
||||||
}
|
|
||||||
|
|
||||||
func printEACL(cmd *cobra.Command, _ []string) {
|
|
||||||
file, _ := cmd.Flags().GetString("file")
|
|
||||||
eaclTable := new(eacl.Table)
|
|
||||||
data, err := os.ReadFile(file)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't read file with EACL: %w", err)
|
|
||||||
if strings.HasSuffix(file, ".json") {
|
|
||||||
commonCmd.ExitOnErr(cmd, "unable to parse json: %w", eaclTable.UnmarshalJSON(data))
|
|
||||||
} else {
|
|
||||||
rules := strings.Split(strings.TrimSpace(string(data)), "\n")
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't parse file with EACL: %w", util.ParseEACLRules(eaclTable, rules))
|
|
||||||
}
|
|
||||||
util.PrettyPrintTableEACL(cmd, eaclTable)
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
package acl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/acl/basic"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/acl/extended"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Cmd = &cobra.Command{
|
|
||||||
Use: "acl",
|
|
||||||
Short: "Operations with Access Control Lists",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
Cmd.AddCommand(extended.Cmd)
|
|
||||||
Cmd.AddCommand(basic.Cmd)
|
|
||||||
}
|
|
|
@ -1,126 +0,0 @@
|
||||||
package bearer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
internalclient "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
|
||||||
eaclSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
eaclFlag = "eacl"
|
|
||||||
issuedAtFlag = "issued-at"
|
|
||||||
notValidBeforeFlag = "not-valid-before"
|
|
||||||
ownerFlag = "owner"
|
|
||||||
outFlag = "out"
|
|
||||||
jsonFlag = commonflags.JSON
|
|
||||||
)
|
|
||||||
|
|
||||||
var createCmd = &cobra.Command{
|
|
||||||
Use: "create",
|
|
||||||
Short: "Create bearer token",
|
|
||||||
Long: `Create bearer token.
|
|
||||||
|
|
||||||
All epoch flags can be specified relative to the current epoch with the +n syntax.
|
|
||||||
In this case --` + commonflags.RPC + ` flag should be specified and the epoch in bearer token
|
|
||||||
is set to current epoch + n.
|
|
||||||
`,
|
|
||||||
Run: createToken,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
createCmd.Flags().StringP(eaclFlag, "e", "", "Path to the extended ACL table")
|
|
||||||
createCmd.Flags().StringP(issuedAtFlag, "i", "", "Epoch to issue token at")
|
|
||||||
createCmd.Flags().StringP(notValidBeforeFlag, "n", "", "Not valid before epoch")
|
|
||||||
createCmd.Flags().StringP(commonflags.ExpireAt, "x", "", "The last active epoch for the token")
|
|
||||||
createCmd.Flags().StringP(ownerFlag, "o", "", "Token owner")
|
|
||||||
createCmd.Flags().String(outFlag, "", "File to write token to")
|
|
||||||
createCmd.Flags().Bool(jsonFlag, false, "Output token in JSON")
|
|
||||||
createCmd.Flags().StringP(commonflags.RPC, commonflags.RPCShorthand, commonflags.RPCDefault, commonflags.RPCUsage)
|
|
||||||
|
|
||||||
_ = cobra.MarkFlagFilename(createCmd.Flags(), eaclFlag)
|
|
||||||
|
|
||||||
_ = cobra.MarkFlagRequired(createCmd.Flags(), issuedAtFlag)
|
|
||||||
_ = cobra.MarkFlagRequired(createCmd.Flags(), notValidBeforeFlag)
|
|
||||||
_ = cobra.MarkFlagRequired(createCmd.Flags(), commonflags.ExpireAt)
|
|
||||||
_ = cobra.MarkFlagRequired(createCmd.Flags(), ownerFlag)
|
|
||||||
_ = cobra.MarkFlagRequired(createCmd.Flags(), outFlag)
|
|
||||||
}
|
|
||||||
|
|
||||||
func createToken(cmd *cobra.Command, _ []string) {
|
|
||||||
iat, iatRelative, err := common.ParseEpoch(cmd, issuedAtFlag)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't parse --"+issuedAtFlag+" flag: %w", err)
|
|
||||||
|
|
||||||
exp, expRelative, err := common.ParseEpoch(cmd, commonflags.ExpireAt)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't parse --"+commonflags.ExpireAt+" flag: %w", err)
|
|
||||||
|
|
||||||
nvb, nvbRelative, err := common.ParseEpoch(cmd, notValidBeforeFlag)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't parse --"+notValidBeforeFlag+" flag: %w", err)
|
|
||||||
|
|
||||||
if iatRelative || expRelative || nvbRelative {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
endpoint, _ := cmd.Flags().GetString(commonflags.RPC)
|
|
||||||
currEpoch, err := internalclient.GetCurrentEpoch(ctx, cmd, endpoint)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't fetch current epoch: %w", err)
|
|
||||||
|
|
||||||
if iatRelative {
|
|
||||||
iat += currEpoch
|
|
||||||
}
|
|
||||||
if expRelative {
|
|
||||||
exp += currEpoch
|
|
||||||
}
|
|
||||||
if nvbRelative {
|
|
||||||
nvb += currEpoch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if exp < nvb {
|
|
||||||
commonCmd.ExitOnErr(cmd, "",
|
|
||||||
fmt.Errorf("expiration epoch is less than not-valid-before epoch: %d < %d", exp, nvb))
|
|
||||||
}
|
|
||||||
|
|
||||||
ownerStr, _ := cmd.Flags().GetString(ownerFlag)
|
|
||||||
|
|
||||||
var ownerID user.ID
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't parse recipient: %w", ownerID.DecodeString(ownerStr))
|
|
||||||
|
|
||||||
var b bearer.Token
|
|
||||||
b.SetExp(exp)
|
|
||||||
b.SetNbf(nvb)
|
|
||||||
b.SetIat(iat)
|
|
||||||
b.ForUser(ownerID)
|
|
||||||
|
|
||||||
eaclPath, _ := cmd.Flags().GetString(eaclFlag)
|
|
||||||
if eaclPath != "" {
|
|
||||||
table := eaclSDK.NewTable()
|
|
||||||
raw, err := os.ReadFile(eaclPath)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't read extended ACL file: %w", err)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't parse extended ACL: %w", json.Unmarshal(raw, table))
|
|
||||||
b.SetEACLTable(*table)
|
|
||||||
}
|
|
||||||
|
|
||||||
var data []byte
|
|
||||||
|
|
||||||
toJSON, _ := cmd.Flags().GetBool(jsonFlag)
|
|
||||||
if toJSON {
|
|
||||||
data, err = json.Marshal(b)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't mashal token to JSON: %w", err)
|
|
||||||
} else {
|
|
||||||
data = b.Marshal()
|
|
||||||
}
|
|
||||||
|
|
||||||
out, _ := cmd.Flags().GetString(outFlag)
|
|
||||||
err = os.WriteFile(out, data, 0644)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't write token to file: %w", err)
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/autocomplete"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rootCmd.AddCommand(autocomplete.Command("frostfs-cli"))
|
|
||||||
}
|
|
|
@ -1,218 +0,0 @@
|
||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
containerApi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
|
|
||||||
internalclient "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
containerACL string
|
|
||||||
containerPolicy string
|
|
||||||
containerAttributes []string
|
|
||||||
containerAwait bool
|
|
||||||
containerName string
|
|
||||||
containerNnsName string
|
|
||||||
containerNnsZone string
|
|
||||||
containerNoTimestamp bool
|
|
||||||
force bool
|
|
||||||
)
|
|
||||||
|
|
||||||
var createContainerCmd = &cobra.Command{
|
|
||||||
Use: "create",
|
|
||||||
Short: "Create new container",
|
|
||||||
Long: `Create new container and register it in the FrostFS.
|
|
||||||
It will be stored in sidechain when inner ring will accepts it.`,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
placementPolicy, err := parseContainerPolicy(cmd, containerPolicy)
|
|
||||||
commonCmd.ExitOnErr(cmd, "", err)
|
|
||||||
|
|
||||||
key := key.Get(cmd)
|
|
||||||
cli := internalclient.GetSDKClientByFlag(cmd, key, commonflags.RPC)
|
|
||||||
|
|
||||||
if !force {
|
|
||||||
var prm internalclient.NetMapSnapshotPrm
|
|
||||||
prm.SetClient(cli)
|
|
||||||
|
|
||||||
resmap, err := internalclient.NetMapSnapshot(cmd.Context(), prm)
|
|
||||||
commonCmd.ExitOnErr(cmd, "unable to get netmap snapshot to validate container placement, "+
|
|
||||||
"use --force option to skip this check: %w", err)
|
|
||||||
|
|
||||||
nodesByRep, err := resmap.NetMap().ContainerNodes(*placementPolicy, nil)
|
|
||||||
commonCmd.ExitOnErr(cmd, "could not build container nodes based on given placement policy, "+
|
|
||||||
"use --force option to skip this check: %w", err)
|
|
||||||
|
|
||||||
for i, nodes := range nodesByRep {
|
|
||||||
if placementPolicy.ReplicaNumberByIndex(i) > uint32(len(nodes)) {
|
|
||||||
commonCmd.ExitOnErr(cmd, "", fmt.Errorf(
|
|
||||||
"the number of nodes '%d' in selector is not enough for the number of replicas '%d', "+
|
|
||||||
"use --force option to skip this check",
|
|
||||||
len(nodes),
|
|
||||||
placementPolicy.ReplicaNumberByIndex(i),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var cnr container.Container
|
|
||||||
cnr.Init()
|
|
||||||
|
|
||||||
err = parseAttributes(&cnr, containerAttributes)
|
|
||||||
commonCmd.ExitOnErr(cmd, "", err)
|
|
||||||
|
|
||||||
var basicACL acl.Basic
|
|
||||||
commonCmd.ExitOnErr(cmd, "decode basic ACL string: %w", basicACL.DecodeString(containerACL))
|
|
||||||
|
|
||||||
tok := getSession(cmd)
|
|
||||||
|
|
||||||
if tok != nil {
|
|
||||||
issuer := tok.Issuer()
|
|
||||||
cnr.SetOwner(issuer)
|
|
||||||
} else {
|
|
||||||
var idOwner user.ID
|
|
||||||
user.IDFromKey(&idOwner, key.PublicKey)
|
|
||||||
|
|
||||||
cnr.SetOwner(idOwner)
|
|
||||||
}
|
|
||||||
|
|
||||||
cnr.SetPlacementPolicy(*placementPolicy)
|
|
||||||
cnr.SetBasicACL(basicACL)
|
|
||||||
|
|
||||||
var syncContainerPrm internalclient.SyncContainerPrm
|
|
||||||
syncContainerPrm.SetClient(cli)
|
|
||||||
syncContainerPrm.SetContainer(&cnr)
|
|
||||||
|
|
||||||
_, err = internalclient.SyncContainerSettings(cmd.Context(), syncContainerPrm)
|
|
||||||
commonCmd.ExitOnErr(cmd, "syncing container's settings rpc error: %w", err)
|
|
||||||
|
|
||||||
var putPrm internalclient.PutContainerPrm
|
|
||||||
putPrm.SetClient(cli)
|
|
||||||
putPrm.SetContainer(cnr)
|
|
||||||
|
|
||||||
if tok != nil {
|
|
||||||
putPrm.WithinSession(*tok)
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := internalclient.PutContainer(cmd.Context(), putPrm)
|
|
||||||
commonCmd.ExitOnErr(cmd, "put container rpc error: %w", err)
|
|
||||||
|
|
||||||
id := res.ID()
|
|
||||||
|
|
||||||
cmd.Println("container ID:", id)
|
|
||||||
|
|
||||||
if containerAwait {
|
|
||||||
cmd.Println("awaiting...")
|
|
||||||
|
|
||||||
var getPrm internalclient.GetContainerPrm
|
|
||||||
getPrm.SetClient(cli)
|
|
||||||
getPrm.SetContainer(id)
|
|
||||||
|
|
||||||
for i := 0; i < awaitTimeout; i++ {
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
|
|
||||||
_, err := internalclient.GetContainer(cmd.Context(), getPrm)
|
|
||||||
if err == nil {
|
|
||||||
cmd.Println("container has been persisted on sidechain")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commonCmd.ExitOnErr(cmd, "", errCreateTimeout)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func initContainerCreateCmd() {
|
|
||||||
flags := createContainerCmd.Flags()
|
|
||||||
|
|
||||||
// Init common flags
|
|
||||||
flags.StringP(commonflags.RPC, commonflags.RPCShorthand, commonflags.RPCDefault, commonflags.RPCUsage)
|
|
||||||
flags.Bool(commonflags.TracingFlag, false, commonflags.TracingFlagUsage)
|
|
||||||
flags.DurationP(commonflags.Timeout, commonflags.TimeoutShorthand, commonflags.TimeoutDefault, commonflags.TimeoutUsage)
|
|
||||||
flags.StringP(commonflags.WalletPath, commonflags.WalletPathShorthand, commonflags.WalletPathDefault, commonflags.WalletPathUsage)
|
|
||||||
flags.StringP(commonflags.Account, commonflags.AccountShorthand, commonflags.AccountDefault, commonflags.AccountUsage)
|
|
||||||
|
|
||||||
flags.StringVar(&containerACL, "basic-acl", acl.NamePrivate, fmt.Sprintf("HEX encoded basic ACL value or keywords like '%s', '%s', '%s'",
|
|
||||||
acl.NamePublicRW, acl.NamePrivate, acl.NamePublicROExtended,
|
|
||||||
))
|
|
||||||
flags.StringVarP(&containerPolicy, "policy", "p", "", "QL-encoded or JSON-encoded placement policy or path to file with it")
|
|
||||||
flags.StringSliceVarP(&containerAttributes, "attributes", "a", nil, "Comma separated pairs of container attributes in form of Key1=Value1,Key2=Value2")
|
|
||||||
flags.BoolVar(&containerAwait, "await", false, "Block execution until container is persisted")
|
|
||||||
flags.StringVar(&containerName, "name", "", "Container name attribute")
|
|
||||||
flags.StringVar(&containerNnsName, "nns-name", "", "Container nns name attribute")
|
|
||||||
flags.StringVar(&containerNnsZone, "nns-zone", "", "Container nns zone attribute")
|
|
||||||
flags.BoolVar(&containerNoTimestamp, "disable-timestamp", false, "Disable timestamp container attribute")
|
|
||||||
flags.BoolVarP(&force, commonflags.ForceFlag, commonflags.ForceFlagShorthand, false,
|
|
||||||
"Skip placement validity check")
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseContainerPolicy(cmd *cobra.Command, policyString string) (*netmap.PlacementPolicy, error) {
|
|
||||||
_, err := os.Stat(policyString) // check if `policyString` is a path to file with placement policy
|
|
||||||
if err == nil {
|
|
||||||
common.PrintVerbose(cmd, "Reading placement policy from file: %s", policyString)
|
|
||||||
|
|
||||||
data, err := os.ReadFile(policyString)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't read file with placement policy: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
policyString = string(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
var result netmap.PlacementPolicy
|
|
||||||
|
|
||||||
err = result.DecodeString(policyString)
|
|
||||||
if err == nil {
|
|
||||||
common.PrintVerbose(cmd, "Parsed QL encoded policy")
|
|
||||||
return &result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = result.UnmarshalJSON([]byte(policyString)); err == nil {
|
|
||||||
common.PrintVerbose(cmd, "Parsed JSON encoded policy")
|
|
||||||
return &result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, errors.New("can't parse placement policy")
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseAttributes(dst *container.Container, attributes []string) error {
|
|
||||||
for i := range attributes {
|
|
||||||
k, v, found := strings.Cut(attributes[i], attributeDelimiter)
|
|
||||||
if !found {
|
|
||||||
return errors.New("invalid container attribute")
|
|
||||||
}
|
|
||||||
|
|
||||||
dst.SetAttribute(k, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !containerNoTimestamp {
|
|
||||||
container.SetCreationTime(dst, time.Now())
|
|
||||||
}
|
|
||||||
|
|
||||||
if containerName != "" {
|
|
||||||
container.SetName(dst, containerName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if containerNnsName != "" {
|
|
||||||
dst.SetAttribute(containerApi.SysAttributeName, containerNnsName)
|
|
||||||
}
|
|
||||||
if containerNnsZone != "" {
|
|
||||||
dst.SetAttribute(containerApi.SysAttributeZone, containerNnsZone)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,132 +0,0 @@
|
||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
internalclient "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var deleteContainerCmd = &cobra.Command{
|
|
||||||
Use: "delete",
|
|
||||||
Short: "Delete existing container",
|
|
||||||
Long: `Delete existing container.
|
|
||||||
Only owner of the container has a permission to remove container.`,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
id := parseContainerID(cmd)
|
|
||||||
|
|
||||||
tok := getSession(cmd)
|
|
||||||
|
|
||||||
pk := key.Get(cmd)
|
|
||||||
cli := internalclient.GetSDKClientByFlag(cmd, pk, commonflags.RPC)
|
|
||||||
|
|
||||||
if force, _ := cmd.Flags().GetBool(commonflags.ForceFlag); !force {
|
|
||||||
common.PrintVerbose(cmd, "Reading the container to check ownership...")
|
|
||||||
|
|
||||||
var getPrm internalclient.GetContainerPrm
|
|
||||||
getPrm.SetClient(cli)
|
|
||||||
getPrm.SetContainer(id)
|
|
||||||
|
|
||||||
resGet, err := internalclient.GetContainer(cmd.Context(), getPrm)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't get the container: %w", err)
|
|
||||||
|
|
||||||
owner := resGet.Container().Owner()
|
|
||||||
|
|
||||||
if tok != nil {
|
|
||||||
common.PrintVerbose(cmd, "Checking session issuer...")
|
|
||||||
|
|
||||||
if !tok.Issuer().Equals(owner) {
|
|
||||||
commonCmd.ExitOnErr(cmd, "", fmt.Errorf("session issuer differs with the container owner: expected %s, has %s", owner, tok.Issuer()))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
common.PrintVerbose(cmd, "Checking provided account...")
|
|
||||||
|
|
||||||
var acc user.ID
|
|
||||||
user.IDFromKey(&acc, pk.PublicKey)
|
|
||||||
|
|
||||||
if !acc.Equals(owner) {
|
|
||||||
commonCmd.ExitOnErr(cmd, "", fmt.Errorf("provided account differs with the container owner: expected %s, has %s", owner, acc))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.PrintVerbose(cmd, "Account matches the container owner.")
|
|
||||||
|
|
||||||
if tok != nil {
|
|
||||||
common.PrintVerbose(cmd, "Skip searching for LOCK objects - session provided.")
|
|
||||||
} else {
|
|
||||||
fs := objectSDK.NewSearchFilters()
|
|
||||||
fs.AddTypeFilter(objectSDK.MatchStringEqual, objectSDK.TypeLock)
|
|
||||||
|
|
||||||
var searchPrm internalclient.SearchObjectsPrm
|
|
||||||
searchPrm.SetClient(cli)
|
|
||||||
searchPrm.SetContainerID(id)
|
|
||||||
searchPrm.SetFilters(fs)
|
|
||||||
searchPrm.SetTTL(2)
|
|
||||||
|
|
||||||
common.PrintVerbose(cmd, "Searching for LOCK objects...")
|
|
||||||
|
|
||||||
res, err := internalclient.SearchObjects(cmd.Context(), searchPrm)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't search for LOCK objects: %w", err)
|
|
||||||
|
|
||||||
if len(res.IDList()) != 0 {
|
|
||||||
commonCmd.ExitOnErr(cmd, "",
|
|
||||||
fmt.Errorf("container wasn't removed because LOCK objects were found, "+
|
|
||||||
"use --%s flag to remove anyway", commonflags.ForceFlag))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var delPrm internalclient.DeleteContainerPrm
|
|
||||||
delPrm.SetClient(cli)
|
|
||||||
delPrm.SetContainer(id)
|
|
||||||
|
|
||||||
if tok != nil {
|
|
||||||
delPrm.WithinSession(*tok)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := internalclient.DeleteContainer(cmd.Context(), delPrm)
|
|
||||||
commonCmd.ExitOnErr(cmd, "rpc error: %w", err)
|
|
||||||
|
|
||||||
cmd.Println("container delete method invoked")
|
|
||||||
|
|
||||||
if containerAwait {
|
|
||||||
cmd.Println("awaiting...")
|
|
||||||
|
|
||||||
var getPrm internalclient.GetContainerPrm
|
|
||||||
getPrm.SetClient(cli)
|
|
||||||
getPrm.SetContainer(id)
|
|
||||||
|
|
||||||
for i := 0; i < awaitTimeout; i++ {
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
|
|
||||||
_, err := internalclient.GetContainer(cmd.Context(), getPrm)
|
|
||||||
if err != nil {
|
|
||||||
cmd.Println("container has been removed:", containerID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commonCmd.ExitOnErr(cmd, "", errDeleteTimeout)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func initContainerDeleteCmd() {
|
|
||||||
flags := deleteContainerCmd.Flags()
|
|
||||||
|
|
||||||
flags.StringP(commonflags.WalletPath, commonflags.WalletPathShorthand, commonflags.WalletPathDefault, commonflags.WalletPathUsage)
|
|
||||||
flags.StringP(commonflags.Account, commonflags.AccountShorthand, commonflags.AccountDefault, commonflags.AccountUsage)
|
|
||||||
flags.StringP(commonflags.RPC, commonflags.RPCShorthand, commonflags.RPCDefault, commonflags.RPCUsage)
|
|
||||||
flags.Bool(commonflags.TracingFlag, false, commonflags.TracingFlagUsage)
|
|
||||||
|
|
||||||
flags.StringVar(&containerID, commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
|
||||||
flags.BoolVar(&containerAwait, "await", false, "Block execution until container is removed")
|
|
||||||
flags.BoolP(commonflags.ForceFlag, commonflags.ForceFlagShorthand, false, "Skip validation checks (ownership, presence of LOCK objects)")
|
|
||||||
}
|
|
|
@ -1,160 +0,0 @@
|
||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
internalclient "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/util"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
|
|
||||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
fromFlag = "from"
|
|
||||||
fromFlagUsage = "Path to file with encoded container"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
containerID string
|
|
||||||
containerPathFrom string
|
|
||||||
containerPathTo string
|
|
||||||
containerJSON bool
|
|
||||||
)
|
|
||||||
|
|
||||||
var getContainerInfoCmd = &cobra.Command{
|
|
||||||
Use: "get",
|
|
||||||
Short: "Get container field info",
|
|
||||||
Long: `Get container field info`,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
cnr, _ := getContainer(cmd)
|
|
||||||
|
|
||||||
prettyPrintContainer(cmd, cnr, containerJSON)
|
|
||||||
|
|
||||||
if containerPathTo != "" {
|
|
||||||
var (
|
|
||||||
data []byte
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
if containerJSON {
|
|
||||||
data, err = cnr.MarshalJSON()
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't JSON encode container: %w", err)
|
|
||||||
} else {
|
|
||||||
data = cnr.Marshal()
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.WriteFile(containerPathTo, data, 0644)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't write container to file: %w", err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func initContainerInfoCmd() {
|
|
||||||
commonflags.Init(getContainerInfoCmd)
|
|
||||||
|
|
||||||
flags := getContainerInfoCmd.Flags()
|
|
||||||
|
|
||||||
flags.StringVar(&containerID, commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
|
||||||
flags.StringVar(&containerPathTo, "to", "", "Path to dump encoded container")
|
|
||||||
flags.StringVar(&containerPathFrom, fromFlag, "", fromFlagUsage)
|
|
||||||
flags.BoolVar(&containerJSON, commonflags.JSON, false, "Print or dump container in JSON format")
|
|
||||||
}
|
|
||||||
|
|
||||||
type stringWriter cobra.Command
|
|
||||||
|
|
||||||
func (x *stringWriter) WriteString(s string) (n int, err error) {
|
|
||||||
(*cobra.Command)(x).Print(s)
|
|
||||||
return len(s), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func prettyPrintContainer(cmd *cobra.Command, cnr container.Container, jsonEncoding bool) {
|
|
||||||
if jsonEncoding {
|
|
||||||
common.PrettyPrintJSON(cmd, cnr, "container")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var id cid.ID
|
|
||||||
container.CalculateID(&id, cnr)
|
|
||||||
cmd.Println("container ID:", id)
|
|
||||||
|
|
||||||
cmd.Println("owner ID:", cnr.Owner())
|
|
||||||
|
|
||||||
basicACL := cnr.BasicACL()
|
|
||||||
prettyPrintBasicACL(cmd, basicACL)
|
|
||||||
|
|
||||||
cmd.Println("created:", container.CreatedAt(cnr))
|
|
||||||
|
|
||||||
cmd.Println("attributes:")
|
|
||||||
cnr.IterateAttributes(func(key, val string) {
|
|
||||||
cmd.Printf("\t%s=%s\n", key, val)
|
|
||||||
})
|
|
||||||
|
|
||||||
cmd.Println("placement policy:")
|
|
||||||
commonCmd.ExitOnErr(cmd, "write policy: %w", cnr.PlacementPolicy().WriteStringTo((*stringWriter)(cmd)))
|
|
||||||
cmd.Println()
|
|
||||||
}
|
|
||||||
|
|
||||||
func prettyPrintBasicACL(cmd *cobra.Command, basicACL acl.Basic) {
|
|
||||||
cmd.Printf("basic ACL: %s", basicACL.EncodeToString())
|
|
||||||
|
|
||||||
var prettyName string
|
|
||||||
|
|
||||||
switch basicACL {
|
|
||||||
case acl.Private:
|
|
||||||
prettyName = acl.NamePrivate
|
|
||||||
case acl.PrivateExtended:
|
|
||||||
prettyName = acl.NamePrivateExtended
|
|
||||||
case acl.PublicRO:
|
|
||||||
prettyName = acl.NamePublicRO
|
|
||||||
case acl.PublicROExtended:
|
|
||||||
prettyName = acl.NamePublicROExtended
|
|
||||||
case acl.PublicRW:
|
|
||||||
prettyName = acl.NamePublicRW
|
|
||||||
case acl.PublicRWExtended:
|
|
||||||
prettyName = acl.NamePublicRWExtended
|
|
||||||
case acl.PublicAppend:
|
|
||||||
prettyName = acl.NamePublicAppend
|
|
||||||
case acl.PublicAppendExtended:
|
|
||||||
prettyName = acl.NamePublicAppendExtended
|
|
||||||
}
|
|
||||||
|
|
||||||
if prettyName != "" {
|
|
||||||
cmd.Printf(" (%s)", prettyName)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Println()
|
|
||||||
util.PrettyPrintTableBACL(cmd, &basicACL)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getContainer(cmd *cobra.Command) (container.Container, *ecdsa.PrivateKey) {
|
|
||||||
var cnr container.Container
|
|
||||||
var pk *ecdsa.PrivateKey
|
|
||||||
if containerPathFrom != "" {
|
|
||||||
data, err := os.ReadFile(containerPathFrom)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't read file: %w", err)
|
|
||||||
|
|
||||||
err = cnr.Unmarshal(data)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't unmarshal container: %w", err)
|
|
||||||
} else {
|
|
||||||
id := parseContainerID(cmd)
|
|
||||||
pk = key.GetOrGenerate(cmd)
|
|
||||||
cli := internalclient.GetSDKClientByFlag(cmd, pk, commonflags.RPC)
|
|
||||||
|
|
||||||
var prm internalclient.GetContainerPrm
|
|
||||||
prm.SetClient(cli)
|
|
||||||
prm.SetContainer(id)
|
|
||||||
|
|
||||||
res, err := internalclient.GetContainer(cmd.Context(), prm)
|
|
||||||
commonCmd.ExitOnErr(cmd, "rpc error: %w", err)
|
|
||||||
|
|
||||||
cnr = res.Container()
|
|
||||||
}
|
|
||||||
return cnr, pk
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
internalclient "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var getExtendedACLCmd = &cobra.Command{
|
|
||||||
Use: "get-eacl",
|
|
||||||
Short: "Get extended ACL table of container",
|
|
||||||
Long: `Get extended ACL table of container`,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
id := parseContainerID(cmd)
|
|
||||||
pk := key.GetOrGenerate(cmd)
|
|
||||||
cli := internalclient.GetSDKClientByFlag(cmd, pk, commonflags.RPC)
|
|
||||||
|
|
||||||
var eaclPrm internalclient.EACLPrm
|
|
||||||
eaclPrm.SetClient(cli)
|
|
||||||
eaclPrm.SetContainer(id)
|
|
||||||
|
|
||||||
res, err := internalclient.EACL(cmd.Context(), eaclPrm)
|
|
||||||
commonCmd.ExitOnErr(cmd, "rpc error: %w", err)
|
|
||||||
|
|
||||||
eaclTable := res.EACL()
|
|
||||||
|
|
||||||
if containerPathTo == "" {
|
|
||||||
cmd.Println("eACL: ")
|
|
||||||
common.PrettyPrintJSON(cmd, &eaclTable, "eACL")
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var data []byte
|
|
||||||
|
|
||||||
if containerJSON {
|
|
||||||
data, err = eaclTable.MarshalJSON()
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't encode to JSON: %w", err)
|
|
||||||
} else {
|
|
||||||
data, err = eaclTable.Marshal()
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't encode to binary: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Println("dumping data to file:", containerPathTo)
|
|
||||||
|
|
||||||
err = os.WriteFile(containerPathTo, data, 0644)
|
|
||||||
commonCmd.ExitOnErr(cmd, "could not write eACL to file: %w", err)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func initContainerGetEACLCmd() {
|
|
||||||
commonflags.Init(getExtendedACLCmd)
|
|
||||||
|
|
||||||
flags := getExtendedACLCmd.Flags()
|
|
||||||
|
|
||||||
flags.StringVar(&containerID, commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
|
||||||
flags.StringVar(&containerPathTo, "to", "", "Path to dump encoded container (default: binary encoded)")
|
|
||||||
flags.BoolVar(&containerJSON, commonflags.JSON, false, "Encode EACL table in json format")
|
|
||||||
}
|
|
|
@ -1,105 +0,0 @@
|
||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
|
|
||||||
internalclient "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
containerSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
// flags of list command.
|
|
||||||
const (
|
|
||||||
flagListPrintAttr = "with-attr"
|
|
||||||
flagListContainerOwner = "owner"
|
|
||||||
flagListName = "name"
|
|
||||||
)
|
|
||||||
|
|
||||||
// flag vars of list command.
|
|
||||||
var (
|
|
||||||
flagVarListPrintAttr bool
|
|
||||||
flagVarListContainerOwner string
|
|
||||||
flagVarListName string
|
|
||||||
)
|
|
||||||
|
|
||||||
var listContainersCmd = &cobra.Command{
|
|
||||||
Use: "list",
|
|
||||||
Short: "List all created containers",
|
|
||||||
Long: "List all created containers",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
var idUser user.ID
|
|
||||||
|
|
||||||
key := key.GetOrGenerate(cmd)
|
|
||||||
|
|
||||||
if flagVarListContainerOwner == "" {
|
|
||||||
user.IDFromKey(&idUser, key.PublicKey)
|
|
||||||
} else {
|
|
||||||
err := idUser.DecodeString(flagVarListContainerOwner)
|
|
||||||
commonCmd.ExitOnErr(cmd, "invalid user ID: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cli := internalclient.GetSDKClientByFlag(cmd, key, commonflags.RPC)
|
|
||||||
|
|
||||||
var prm internalclient.ListContainersPrm
|
|
||||||
prm.SetClient(cli)
|
|
||||||
prm.SetAccount(idUser)
|
|
||||||
|
|
||||||
res, err := internalclient.ListContainers(cmd.Context(), prm)
|
|
||||||
commonCmd.ExitOnErr(cmd, "rpc error: %w", err)
|
|
||||||
|
|
||||||
var prmGet internalclient.GetContainerPrm
|
|
||||||
prmGet.SetClient(cli)
|
|
||||||
|
|
||||||
containerIDs := res.IDList()
|
|
||||||
for _, cnrID := range containerIDs {
|
|
||||||
if flagVarListName == "" && !flagVarListPrintAttr {
|
|
||||||
cmd.Println(cnrID.String())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
prmGet.SetContainer(cnrID)
|
|
||||||
res, err := internalclient.GetContainer(cmd.Context(), prmGet)
|
|
||||||
if err != nil {
|
|
||||||
cmd.Printf(" failed to read attributes: %v\n", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
cnr := res.Container()
|
|
||||||
if cnrName := containerSDK.Name(cnr); flagVarListName != "" && cnrName != flagVarListName {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cmd.Println(cnrID.String())
|
|
||||||
|
|
||||||
if flagVarListPrintAttr {
|
|
||||||
cnr.IterateAttributes(func(key, val string) {
|
|
||||||
if !strings.HasPrefix(key, container.SysAttributePrefix) && !strings.HasPrefix(key, container.SysAttributePrefixNeoFS) {
|
|
||||||
// FIXME(@cthulhu-rider): https://git.frostfs.info/TrueCloudLab/frostfs-sdk-go/issues/97
|
|
||||||
// Use dedicated method to skip system attributes.
|
|
||||||
cmd.Printf(" %s: %s\n", key, val)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func initContainerListContainersCmd() {
|
|
||||||
commonflags.Init(listContainersCmd)
|
|
||||||
|
|
||||||
flags := listContainersCmd.Flags()
|
|
||||||
|
|
||||||
flags.StringVar(&flagVarListName, flagListName, "",
|
|
||||||
"List containers by the attribute name",
|
|
||||||
)
|
|
||||||
flags.StringVar(&flagVarListContainerOwner, flagListContainerOwner, "",
|
|
||||||
"Owner of containers (omit to use owner from private key)",
|
|
||||||
)
|
|
||||||
flags.BoolVar(&flagVarListPrintAttr, flagListPrintAttr, false,
|
|
||||||
"Request and print attributes of each container",
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,97 +0,0 @@
|
||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
|
||||||
internalclient "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
|
||||||
objectCli "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/object"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
|
||||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
// flags of list-object command.
|
|
||||||
const (
|
|
||||||
flagListObjectPrintAttr = "with-attr"
|
|
||||||
)
|
|
||||||
|
|
||||||
// flag vars of list-objects command.
|
|
||||||
var (
|
|
||||||
flagVarListObjectsPrintAttr bool
|
|
||||||
)
|
|
||||||
|
|
||||||
var listContainerObjectsCmd = &cobra.Command{
|
|
||||||
Use: "list-objects",
|
|
||||||
Short: "List existing objects in container",
|
|
||||||
Long: `List existing objects in container`,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
id := parseContainerID(cmd)
|
|
||||||
|
|
||||||
filters := new(objectSDK.SearchFilters)
|
|
||||||
filters.AddRootFilter() // search only user created objects
|
|
||||||
|
|
||||||
cli := internalclient.GetSDKClientByFlag(cmd, key.GetOrGenerate(cmd), commonflags.RPC)
|
|
||||||
|
|
||||||
var prmSearch internalclient.SearchObjectsPrm
|
|
||||||
var prmHead internalclient.HeadObjectPrm
|
|
||||||
|
|
||||||
prmSearch.SetClient(cli)
|
|
||||||
|
|
||||||
if flagVarListObjectsPrintAttr {
|
|
||||||
prmHead.SetClient(cli)
|
|
||||||
objectCli.Prepare(cmd, &prmSearch, &prmHead)
|
|
||||||
} else {
|
|
||||||
objectCli.Prepare(cmd, &prmSearch)
|
|
||||||
}
|
|
||||||
|
|
||||||
prmSearch.SetContainerID(id)
|
|
||||||
prmSearch.SetFilters(*filters)
|
|
||||||
|
|
||||||
res, err := internalclient.SearchObjects(cmd.Context(), prmSearch)
|
|
||||||
commonCmd.ExitOnErr(cmd, "rpc error: %w", err)
|
|
||||||
|
|
||||||
objectIDs := res.IDList()
|
|
||||||
|
|
||||||
for i := range objectIDs {
|
|
||||||
cmd.Println(objectIDs[i].String())
|
|
||||||
|
|
||||||
if flagVarListObjectsPrintAttr {
|
|
||||||
var addr oid.Address
|
|
||||||
addr.SetContainer(id)
|
|
||||||
addr.SetObject(objectIDs[i])
|
|
||||||
prmHead.SetAddress(addr)
|
|
||||||
|
|
||||||
resHead, err := internalclient.HeadObject(cmd.Context(), prmHead)
|
|
||||||
if err == nil {
|
|
||||||
attrs := resHead.Header().Attributes()
|
|
||||||
for i := range attrs {
|
|
||||||
attrKey := attrs[i].Key()
|
|
||||||
if !strings.HasPrefix(attrKey, v2object.SysAttributePrefix) && !strings.HasPrefix(attrKey, v2object.SysAttributePrefixNeoFS) {
|
|
||||||
// FIXME(@cthulhu-rider): https://git.frostfs.info/TrueCloudLab/frostfs-sdk-go/issues/97
|
|
||||||
// Use dedicated method to skip system attributes.
|
|
||||||
cmd.Printf(" %s: %s\n", attrKey, attrs[i].Value())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cmd.Printf(" failed to read attributes: %v\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func initContainerListObjectsCmd() {
|
|
||||||
commonflags.Init(listContainerObjectsCmd)
|
|
||||||
objectCli.InitBearer(listContainerObjectsCmd)
|
|
||||||
|
|
||||||
flags := listContainerObjectsCmd.Flags()
|
|
||||||
|
|
||||||
flags.StringVar(&containerID, commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
|
||||||
flags.BoolVar(&flagVarListObjectsPrintAttr, flagListObjectPrintAttr, false,
|
|
||||||
"Request and print user attributes of each object",
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha256"
|
|
||||||
|
|
||||||
internalclient "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
containerAPI "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
|
||||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var short bool
|
|
||||||
|
|
||||||
var containerNodesCmd = &cobra.Command{
|
|
||||||
Use: "nodes",
|
|
||||||
Short: "Show nodes for container",
|
|
||||||
Long: "Show nodes taking part in a container at the current epoch.",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
var cnr, pkey = getContainer(cmd)
|
|
||||||
|
|
||||||
if pkey == nil {
|
|
||||||
pkey = key.GetOrGenerate(cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
cli := internalclient.GetSDKClientByFlag(cmd, pkey, commonflags.RPC)
|
|
||||||
|
|
||||||
var prm internalclient.NetMapSnapshotPrm
|
|
||||||
prm.SetClient(cli)
|
|
||||||
|
|
||||||
resmap, err := internalclient.NetMapSnapshot(cmd.Context(), prm)
|
|
||||||
commonCmd.ExitOnErr(cmd, "unable to get netmap snapshot", err)
|
|
||||||
|
|
||||||
var id cid.ID
|
|
||||||
containerAPI.CalculateID(&id, cnr)
|
|
||||||
binCnr := make([]byte, sha256.Size)
|
|
||||||
id.Encode(binCnr)
|
|
||||||
|
|
||||||
policy := cnr.PlacementPolicy()
|
|
||||||
|
|
||||||
var cnrNodes [][]netmap.NodeInfo
|
|
||||||
cnrNodes, err = resmap.NetMap().ContainerNodes(policy, binCnr)
|
|
||||||
commonCmd.ExitOnErr(cmd, "could not build container nodes for given container: %w", err)
|
|
||||||
|
|
||||||
for i := range cnrNodes {
|
|
||||||
cmd.Printf("Descriptor #%d, REP %d:\n", i+1, policy.ReplicaNumberByIndex(i))
|
|
||||||
for j := range cnrNodes[i] {
|
|
||||||
commonCmd.PrettyPrintNodeInfo(cmd, cnrNodes[i][j], j, "\t", short)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func initContainerNodesCmd() {
|
|
||||||
commonflags.Init(containerNodesCmd)
|
|
||||||
|
|
||||||
flags := containerNodesCmd.Flags()
|
|
||||||
flags.StringVar(&containerID, commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
|
||||||
flags.StringVar(&containerPathFrom, fromFlag, "", fromFlagUsage)
|
|
||||||
flags.BoolVar(&short, "short", false, "Shortens output of node info")
|
|
||||||
}
|
|
|
@ -1,178 +0,0 @@
|
||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
internalclient "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
type policyPlaygroundREPL struct {
|
|
||||||
cmd *cobra.Command
|
|
||||||
args []string
|
|
||||||
nodes map[string]netmap.NodeInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPolicyPlaygroundREPL(cmd *cobra.Command, args []string) (*policyPlaygroundREPL, error) {
|
|
||||||
return &policyPlaygroundREPL{
|
|
||||||
cmd: cmd,
|
|
||||||
args: args,
|
|
||||||
nodes: map[string]netmap.NodeInfo{},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repl *policyPlaygroundREPL) handleLs(args []string) error {
|
|
||||||
if len(args) > 0 {
|
|
||||||
return fmt.Errorf("too many arguments for command 'ls': got %d, want 0", len(args))
|
|
||||||
}
|
|
||||||
i := 1
|
|
||||||
for id, node := range repl.nodes {
|
|
||||||
var attrs []string
|
|
||||||
node.IterateAttributes(func(k, v string) {
|
|
||||||
attrs = append(attrs, fmt.Sprintf("%s:%s", k, v))
|
|
||||||
})
|
|
||||||
fmt.Printf("\t%2d: id=%s attrs={%v}\n", i, id, strings.Join(attrs, " "))
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repl *policyPlaygroundREPL) handleAdd(args []string) error {
|
|
||||||
if len(args) == 0 {
|
|
||||||
return fmt.Errorf("too few arguments for command 'add': got %d, want >0", len(args))
|
|
||||||
}
|
|
||||||
id := args[0]
|
|
||||||
key, err := hex.DecodeString(id)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("node id must be a hex string: got %q: %v", id, err)
|
|
||||||
}
|
|
||||||
node := repl.nodes[id]
|
|
||||||
node.SetPublicKey(key)
|
|
||||||
for _, attr := range args[1:] {
|
|
||||||
kv := strings.Split(attr, ":")
|
|
||||||
if len(kv) != 2 {
|
|
||||||
return fmt.Errorf("node attributes must be in the format 'KEY:VALUE': got %q", attr)
|
|
||||||
}
|
|
||||||
node.SetAttribute(kv[0], kv[1])
|
|
||||||
}
|
|
||||||
repl.nodes[id] = node
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repl *policyPlaygroundREPL) handleRemove(args []string) error {
|
|
||||||
if len(args) == 0 {
|
|
||||||
return fmt.Errorf("too few arguments for command 'remove': got %d, want >0", len(args))
|
|
||||||
}
|
|
||||||
id := args[0]
|
|
||||||
if _, exists := repl.nodes[id]; exists {
|
|
||||||
delete(repl.nodes, id)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("node not found: id=%q", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repl *policyPlaygroundREPL) handleEval(args []string) error {
|
|
||||||
policyStr := strings.Join(args, " ")
|
|
||||||
placementPolicy, err := parseContainerPolicy(repl.cmd, policyStr)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parsing placement policy: %v", err)
|
|
||||||
}
|
|
||||||
nm := repl.netMap()
|
|
||||||
nodes, err := nm.ContainerNodes(*placementPolicy, nil)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("building container nodes: %v", err)
|
|
||||||
}
|
|
||||||
for i, ns := range nodes {
|
|
||||||
var ids []string
|
|
||||||
for _, node := range ns {
|
|
||||||
ids = append(ids, hex.EncodeToString(node.PublicKey()))
|
|
||||||
}
|
|
||||||
fmt.Printf("\t%2d: %v\n", i+1, ids)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repl *policyPlaygroundREPL) netMap() netmap.NetMap {
|
|
||||||
var nm netmap.NetMap
|
|
||||||
var nodes []netmap.NodeInfo
|
|
||||||
for _, node := range repl.nodes {
|
|
||||||
nodes = append(nodes, node)
|
|
||||||
}
|
|
||||||
nm.SetNodes(nodes)
|
|
||||||
return nm
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repl *policyPlaygroundREPL) run() error {
|
|
||||||
if len(viper.GetString(commonflags.RPC)) > 0 {
|
|
||||||
key := key.GetOrGenerate(repl.cmd)
|
|
||||||
cli := internalclient.GetSDKClientByFlag(repl.cmd, key, commonflags.RPC)
|
|
||||||
|
|
||||||
var prm internalclient.NetMapSnapshotPrm
|
|
||||||
prm.SetClient(cli)
|
|
||||||
|
|
||||||
resp, err := internalclient.NetMapSnapshot(repl.cmd.Context(), prm)
|
|
||||||
commonCmd.ExitOnErr(repl.cmd, "unable to get netmap snapshot to populate initial netmap: %w", err)
|
|
||||||
|
|
||||||
for _, node := range resp.NetMap().Nodes() {
|
|
||||||
id := hex.EncodeToString(node.PublicKey())
|
|
||||||
repl.nodes[id] = node
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cmdHandlers := map[string]func([]string) error{
|
|
||||||
"ls": repl.handleLs,
|
|
||||||
"add": repl.handleAdd,
|
|
||||||
"remove": repl.handleRemove,
|
|
||||||
"eval": repl.handleEval,
|
|
||||||
}
|
|
||||||
for reader := bufio.NewReader(os.Stdin); ; {
|
|
||||||
fmt.Print("> ")
|
|
||||||
line, err := reader.ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
if err == io.EOF {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("reading line: %v", err)
|
|
||||||
}
|
|
||||||
parts := strings.Fields(line)
|
|
||||||
if len(parts) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cmd := parts[0]
|
|
||||||
handler, exists := cmdHandlers[cmd]
|
|
||||||
if exists {
|
|
||||||
if err := handler(parts[1:]); err != nil {
|
|
||||||
fmt.Printf("error: %v\n", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Printf("error: unknown command %q\n", cmd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var policyPlaygroundCmd = &cobra.Command{
|
|
||||||
Use: "policy-playground",
|
|
||||||
Short: "A REPL for testing placement policies",
|
|
||||||
Long: `A REPL for testing placement policies.
|
|
||||||
If a wallet and endpoint is provided, the initial netmap data will be loaded from the snapshot of the node. Otherwise, an empty playground is created.`,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
repl, err := newPolicyPlaygroundREPL(cmd, args)
|
|
||||||
commonCmd.ExitOnErr(cmd, "could not create policy playground: %w", err)
|
|
||||||
commonCmd.ExitOnErr(cmd, "policy playground failed: %w", repl.run())
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func initContainerPolicyPlaygroundCmd() {
|
|
||||||
commonflags.Init(policyPlaygroundCmd)
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Cmd represents the container command.
|
|
||||||
var Cmd = &cobra.Command{
|
|
||||||
Use: "container",
|
|
||||||
Short: "Operations with containers",
|
|
||||||
Long: "Operations with containers",
|
|
||||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
|
||||||
// bind exactly that cmd's flags to
|
|
||||||
// the viper before execution
|
|
||||||
commonflags.Bind(cmd)
|
|
||||||
commonflags.BindAPI(cmd)
|
|
||||||
common.StartClientCommandSpan(cmd)
|
|
||||||
},
|
|
||||||
PersistentPostRun: common.StopClientCommandSpan,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
containerChildCommand := []*cobra.Command{
|
|
||||||
listContainersCmd,
|
|
||||||
createContainerCmd,
|
|
||||||
deleteContainerCmd,
|
|
||||||
listContainerObjectsCmd,
|
|
||||||
getContainerInfoCmd,
|
|
||||||
getExtendedACLCmd,
|
|
||||||
setExtendedACLCmd,
|
|
||||||
containerNodesCmd,
|
|
||||||
policyPlaygroundCmd,
|
|
||||||
}
|
|
||||||
|
|
||||||
Cmd.AddCommand(containerChildCommand...)
|
|
||||||
|
|
||||||
initContainerListContainersCmd()
|
|
||||||
initContainerCreateCmd()
|
|
||||||
initContainerDeleteCmd()
|
|
||||||
initContainerListObjectsCmd()
|
|
||||||
initContainerInfoCmd()
|
|
||||||
initContainerGetEACLCmd()
|
|
||||||
initContainerSetEACLCmd()
|
|
||||||
initContainerNodesCmd()
|
|
||||||
initContainerPolicyPlaygroundCmd()
|
|
||||||
|
|
||||||
for _, containerCommand := range containerChildCommand {
|
|
||||||
commonflags.InitAPI(containerCommand)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, el := range []struct {
|
|
||||||
cmd *cobra.Command
|
|
||||||
verb string
|
|
||||||
}{
|
|
||||||
{createContainerCmd, "PUT"},
|
|
||||||
{deleteContainerCmd, "DELETE"},
|
|
||||||
{setExtendedACLCmd, "SETEACL"},
|
|
||||||
} {
|
|
||||||
commonflags.InitSession(el.cmd, "container "+el.verb)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,104 +0,0 @@
|
||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
internalclient "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var flagVarsSetEACL struct {
|
|
||||||
noPreCheck bool
|
|
||||||
|
|
||||||
srcPath string
|
|
||||||
}
|
|
||||||
|
|
||||||
var setExtendedACLCmd = &cobra.Command{
|
|
||||||
Use: "set-eacl",
|
|
||||||
Short: "Set new extended ACL table for container",
|
|
||||||
Long: `Set new extended ACL table for container.
|
|
||||||
Container ID in EACL table will be substituted with ID from the CLI.`,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
id := parseContainerID(cmd)
|
|
||||||
eaclTable := common.ReadEACL(cmd, flagVarsSetEACL.srcPath)
|
|
||||||
|
|
||||||
tok := getSession(cmd)
|
|
||||||
|
|
||||||
eaclTable.SetCID(id)
|
|
||||||
|
|
||||||
pk := key.GetOrGenerate(cmd)
|
|
||||||
cli := internalclient.GetSDKClientByFlag(cmd, pk, commonflags.RPC)
|
|
||||||
|
|
||||||
if !flagVarsSetEACL.noPreCheck {
|
|
||||||
cmd.Println("Checking the ability to modify access rights in the container...")
|
|
||||||
|
|
||||||
extendable, err := internalclient.IsACLExtendable(cmd.Context(), cli, id)
|
|
||||||
commonCmd.ExitOnErr(cmd, "Extensibility check failure: %w", err)
|
|
||||||
|
|
||||||
if !extendable {
|
|
||||||
commonCmd.ExitOnErr(cmd, "", errors.New("container ACL is immutable"))
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Println("ACL extension is enabled in the container, continue processing.")
|
|
||||||
}
|
|
||||||
|
|
||||||
var setEACLPrm internalclient.SetEACLPrm
|
|
||||||
setEACLPrm.SetClient(cli)
|
|
||||||
setEACLPrm.SetTable(*eaclTable)
|
|
||||||
|
|
||||||
if tok != nil {
|
|
||||||
setEACLPrm.WithinSession(*tok)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := internalclient.SetEACL(cmd.Context(), setEACLPrm)
|
|
||||||
commonCmd.ExitOnErr(cmd, "rpc error: %w", err)
|
|
||||||
|
|
||||||
if containerAwait {
|
|
||||||
exp, err := eaclTable.Marshal()
|
|
||||||
commonCmd.ExitOnErr(cmd, "broken EACL table: %w", err)
|
|
||||||
|
|
||||||
cmd.Println("awaiting...")
|
|
||||||
|
|
||||||
var getEACLPrm internalclient.EACLPrm
|
|
||||||
getEACLPrm.SetClient(cli)
|
|
||||||
getEACLPrm.SetContainer(id)
|
|
||||||
|
|
||||||
for i := 0; i < awaitTimeout; i++ {
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
|
|
||||||
res, err := internalclient.EACL(cmd.Context(), getEACLPrm)
|
|
||||||
if err == nil {
|
|
||||||
// compare binary values because EACL could have been set already
|
|
||||||
table := res.EACL()
|
|
||||||
got, err := table.Marshal()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if bytes.Equal(exp, got) {
|
|
||||||
cmd.Println("EACL has been persisted on sidechain")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commonCmd.ExitOnErr(cmd, "", errSetEACLTimeout)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func initContainerSetEACLCmd() {
|
|
||||||
commonflags.Init(setExtendedACLCmd)
|
|
||||||
|
|
||||||
flags := setExtendedACLCmd.Flags()
|
|
||||||
flags.StringVar(&containerID, commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
|
||||||
flags.StringVar(&flagVarsSetEACL.srcPath, "table", "", "path to file with JSON or binary encoded EACL table")
|
|
||||||
flags.BoolVar(&containerAwait, "await", false, "block execution until EACL is persisted")
|
|
||||||
flags.BoolVar(&flagVarsSetEACL.noPreCheck, "no-precheck", false, "do not pre-check the extensibility of the container ACL")
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
attributeDelimiter = "="
|
|
||||||
|
|
||||||
awaitTimeout = 120 // in seconds
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errCreateTimeout = errors.New("timeout: container has not been persisted on sidechain")
|
|
||||||
errDeleteTimeout = errors.New("timeout: container has not been removed from sidechain")
|
|
||||||
errSetEACLTimeout = errors.New("timeout: EACL has not been persisted on sidechain")
|
|
||||||
)
|
|
||||||
|
|
||||||
func parseContainerID(cmd *cobra.Command) cid.ID {
|
|
||||||
if containerID == "" {
|
|
||||||
commonCmd.ExitOnErr(cmd, "", errors.New("container ID is not set"))
|
|
||||||
}
|
|
||||||
|
|
||||||
var id cid.ID
|
|
||||||
err := id.DecodeString(containerID)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't decode container ID value: %w", err)
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
// decodes session.Container from the file by path provided in
|
|
||||||
// commonflags.SessionToken flag. Returns nil if the path is not specified.
|
|
||||||
func getSession(cmd *cobra.Command) *session.Container {
|
|
||||||
common.PrintVerbose(cmd, "Reading container session...")
|
|
||||||
|
|
||||||
path, _ := cmd.Flags().GetString(commonflags.SessionToken)
|
|
||||||
if path == "" {
|
|
||||||
common.PrintVerbose(cmd, "Session not provided.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
common.PrintVerbose(cmd, "Reading container session from the file [%s]...", path)
|
|
||||||
|
|
||||||
var res session.Container
|
|
||||||
|
|
||||||
err := common.ReadBinaryOrJSON(cmd, &res, path)
|
|
||||||
commonCmd.ExitOnErr(cmd, "read container session: %v", err)
|
|
||||||
|
|
||||||
common.PrintVerbose(cmd, "Session successfully read.")
|
|
||||||
|
|
||||||
return &res
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
package control
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
concurrencyFlag = "concurrency"
|
|
||||||
removeDuplicatesFlag = "remove-duplicates"
|
|
||||||
)
|
|
||||||
|
|
||||||
var doctorCmd = &cobra.Command{
|
|
||||||
Use: "doctor",
|
|
||||||
Short: "Restructure node's storage",
|
|
||||||
Long: "Restructure node's storage",
|
|
||||||
Run: doctor,
|
|
||||||
}
|
|
||||||
|
|
||||||
func doctor(cmd *cobra.Command, _ []string) {
|
|
||||||
pk := key.Get(cmd)
|
|
||||||
|
|
||||||
req := &control.DoctorRequest{Body: new(control.DoctorRequest_Body)}
|
|
||||||
req.Body.Concurrency, _ = cmd.Flags().GetUint32(concurrencyFlag)
|
|
||||||
req.Body.RemoveDuplicates, _ = cmd.Flags().GetBool(removeDuplicatesFlag)
|
|
||||||
|
|
||||||
signRequest(cmd, pk, req)
|
|
||||||
|
|
||||||
cli := getClient(cmd, pk)
|
|
||||||
|
|
||||||
var resp *control.DoctorResponse
|
|
||||||
var err error
|
|
||||||
err = cli.ExecRaw(func(client *client.Client) error {
|
|
||||||
resp, err = control.Doctor(client, req)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
commonCmd.ExitOnErr(cmd, "rpc error: %w", err)
|
|
||||||
|
|
||||||
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
|
|
||||||
|
|
||||||
cmd.Println("Operation has finished.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initControlDoctorCmd() {
|
|
||||||
initControlFlags(doctorCmd)
|
|
||||||
|
|
||||||
ff := doctorCmd.Flags()
|
|
||||||
ff.Uint32(concurrencyFlag, 0, "Number of parallel threads to use")
|
|
||||||
ff.Bool(removeDuplicatesFlag, false, "Remove duplicate objects")
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
package control
|
|
||||||
|
|
||||||
import (
|
|
||||||
rawclient "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
const dropObjectsFlag = "objects"
|
|
||||||
|
|
||||||
var dropObjectsCmd = &cobra.Command{
|
|
||||||
Use: "drop-objects",
|
|
||||||
Short: "Drop objects from the node's local storage",
|
|
||||||
Long: "Drop objects from the node's local storage",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
pk := key.Get(cmd)
|
|
||||||
|
|
||||||
dropObjectsList, _ := cmd.Flags().GetStringSlice(dropObjectsFlag)
|
|
||||||
binAddrList := make([][]byte, len(dropObjectsList))
|
|
||||||
|
|
||||||
for i := range dropObjectsList {
|
|
||||||
binAddrList[i] = []byte(dropObjectsList[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
body := new(control.DropObjectsRequest_Body)
|
|
||||||
body.SetAddressList(binAddrList)
|
|
||||||
|
|
||||||
req := new(control.DropObjectsRequest)
|
|
||||||
req.SetBody(body)
|
|
||||||
|
|
||||||
signRequest(cmd, pk, req)
|
|
||||||
|
|
||||||
cli := getClient(cmd, pk)
|
|
||||||
|
|
||||||
var resp *control.DropObjectsResponse
|
|
||||||
var err error
|
|
||||||
err = cli.ExecRaw(func(client *rawclient.Client) error {
|
|
||||||
resp, err = control.DropObjects(client, req)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
commonCmd.ExitOnErr(cmd, "", err)
|
|
||||||
|
|
||||||
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
|
|
||||||
|
|
||||||
cmd.Println("Objects were successfully marked to be removed.")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func initControlDropObjectsCmd() {
|
|
||||||
initControlFlags(dropObjectsCmd)
|
|
||||||
|
|
||||||
flags := dropObjectsCmd.Flags()
|
|
||||||
flags.StringSliceP(dropObjectsFlag, "o", nil,
|
|
||||||
"List of object addresses to be removed in string format")
|
|
||||||
|
|
||||||
_ = dropObjectsCmd.MarkFlagRequired(dropObjectsFlag)
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
package control
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
const ignoreErrorsFlag = "no-errors"
|
|
||||||
|
|
||||||
var evacuateShardCmd = &cobra.Command{
|
|
||||||
Use: "evacuate",
|
|
||||||
Short: "Evacuate objects from shard",
|
|
||||||
Long: "Evacuate objects from shard to other shards",
|
|
||||||
Run: evacuateShard,
|
|
||||||
Deprecated: "use frostfs-cli control shards evacuation start",
|
|
||||||
}
|
|
||||||
|
|
||||||
func evacuateShard(cmd *cobra.Command, _ []string) {
|
|
||||||
pk := key.Get(cmd)
|
|
||||||
|
|
||||||
req := &control.EvacuateShardRequest{Body: new(control.EvacuateShardRequest_Body)}
|
|
||||||
req.Body.Shard_ID = getShardIDList(cmd)
|
|
||||||
req.Body.IgnoreErrors, _ = cmd.Flags().GetBool(ignoreErrorsFlag)
|
|
||||||
|
|
||||||
signRequest(cmd, pk, req)
|
|
||||||
|
|
||||||
cli := getClient(cmd, pk)
|
|
||||||
|
|
||||||
var resp *control.EvacuateShardResponse
|
|
||||||
var err error
|
|
||||||
err = cli.ExecRaw(func(client *client.Client) error {
|
|
||||||
resp, err = control.EvacuateShard(client, req)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
commonCmd.ExitOnErr(cmd, "rpc error: %w", err)
|
|
||||||
|
|
||||||
cmd.Printf("Objects moved: %d\n", resp.GetBody().GetCount())
|
|
||||||
|
|
||||||
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
|
|
||||||
|
|
||||||
cmd.Println("Shard has successfully been evacuated.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initControlEvacuateShardCmd() {
|
|
||||||
initControlFlags(evacuateShardCmd)
|
|
||||||
|
|
||||||
flags := evacuateShardCmd.Flags()
|
|
||||||
flags.StringSlice(shardIDFlag, nil, "List of shard IDs in base58 encoding")
|
|
||||||
flags.Bool(shardAllFlag, false, "Process all shards")
|
|
||||||
flags.Bool(ignoreErrorsFlag, false, "Skip invalid/unreadable objects")
|
|
||||||
|
|
||||||
evacuateShardCmd.MarkFlagsMutuallyExclusive(shardIDFlag, shardAllFlag)
|
|
||||||
}
|
|
|
@ -1,315 +0,0 @@
|
||||||
package control
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/shard"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control"
|
|
||||||
clientSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
awaitFlag = "await"
|
|
||||||
noProgressFlag = "no-progress"
|
|
||||||
)
|
|
||||||
|
|
||||||
var evacuationShardCmd = &cobra.Command{
|
|
||||||
Use: "evacuation",
|
|
||||||
Short: "Objects evacuation from shard",
|
|
||||||
Long: "Objects evacuation from shard to other shards",
|
|
||||||
}
|
|
||||||
|
|
||||||
var startEvacuationShardCmd = &cobra.Command{
|
|
||||||
Use: "start",
|
|
||||||
Short: "Start evacuate objects from shard",
|
|
||||||
Long: "Start evacuate objects from shard to other shards",
|
|
||||||
Run: startEvacuateShard,
|
|
||||||
}
|
|
||||||
|
|
||||||
var getEvacuationShardStatusCmd = &cobra.Command{
|
|
||||||
Use: "status",
|
|
||||||
Short: "Get evacuate objects from shard status",
|
|
||||||
Long: "Get evacuate objects from shard to other shards status",
|
|
||||||
Run: getEvacuateShardStatus,
|
|
||||||
}
|
|
||||||
|
|
||||||
var stopEvacuationShardCmd = &cobra.Command{
|
|
||||||
Use: "stop",
|
|
||||||
Short: "Stop running evacuate process",
|
|
||||||
Long: "Stop running evacuate process from shard to other shards",
|
|
||||||
Run: stopEvacuateShardStatus,
|
|
||||||
}
|
|
||||||
|
|
||||||
func startEvacuateShard(cmd *cobra.Command, _ []string) {
|
|
||||||
pk := key.Get(cmd)
|
|
||||||
|
|
||||||
ignoreErrors, _ := cmd.Flags().GetBool(ignoreErrorsFlag)
|
|
||||||
|
|
||||||
req := &control.StartShardEvacuationRequest{
|
|
||||||
Body: &control.StartShardEvacuationRequest_Body{
|
|
||||||
Shard_ID: getShardIDList(cmd),
|
|
||||||
IgnoreErrors: ignoreErrors,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
signRequest(cmd, pk, req)
|
|
||||||
|
|
||||||
cli := getClient(cmd, pk)
|
|
||||||
|
|
||||||
var resp *control.StartShardEvacuationResponse
|
|
||||||
var err error
|
|
||||||
err = cli.ExecRaw(func(client *client.Client) error {
|
|
||||||
resp, err = control.StartShardEvacuation(client, req)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
commonCmd.ExitOnErr(cmd, "Start evacuate shards failed, rpc error: %w", err)
|
|
||||||
|
|
||||||
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
|
|
||||||
|
|
||||||
cmd.Println("Shard evacuation has been successfully started.")
|
|
||||||
|
|
||||||
if awaitCompletion, _ := cmd.Flags().GetBool(awaitFlag); awaitCompletion {
|
|
||||||
noProgress, _ := cmd.Flags().GetBool(noProgressFlag)
|
|
||||||
waitEvacuateCompletion(cmd, pk, cli, !noProgress, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getEvacuateShardStatus(cmd *cobra.Command, _ []string) {
|
|
||||||
pk := key.Get(cmd)
|
|
||||||
req := &control.GetShardEvacuationStatusRequest{
|
|
||||||
Body: &control.GetShardEvacuationStatusRequest_Body{},
|
|
||||||
}
|
|
||||||
|
|
||||||
signRequest(cmd, pk, req)
|
|
||||||
|
|
||||||
cli := getClient(cmd, pk)
|
|
||||||
|
|
||||||
var resp *control.GetShardEvacuationStatusResponse
|
|
||||||
var err error
|
|
||||||
err = cli.ExecRaw(func(client *client.Client) error {
|
|
||||||
resp, err = control.GetShardEvacuationStatus(client, req)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
commonCmd.ExitOnErr(cmd, "Get evacuate shards status failed, rpc error: %w", err)
|
|
||||||
|
|
||||||
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
|
|
||||||
|
|
||||||
printStatus(cmd, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopEvacuateShardStatus(cmd *cobra.Command, _ []string) {
|
|
||||||
pk := key.Get(cmd)
|
|
||||||
req := &control.StopShardEvacuationRequest{
|
|
||||||
Body: &control.StopShardEvacuationRequest_Body{},
|
|
||||||
}
|
|
||||||
|
|
||||||
signRequest(cmd, pk, req)
|
|
||||||
|
|
||||||
cli := getClient(cmd, pk)
|
|
||||||
|
|
||||||
var resp *control.StopShardEvacuationResponse
|
|
||||||
var err error
|
|
||||||
err = cli.ExecRaw(func(client *client.Client) error {
|
|
||||||
resp, err = control.StopShardEvacuation(client, req)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
commonCmd.ExitOnErr(cmd, "Stop evacuate shards failed, rpc error: %w", err)
|
|
||||||
|
|
||||||
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
|
|
||||||
|
|
||||||
waitEvacuateCompletion(cmd, pk, cli, false, false)
|
|
||||||
|
|
||||||
cmd.Println("Evacuation stopped.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func waitEvacuateCompletion(cmd *cobra.Command, pk *ecdsa.PrivateKey, cli *clientSDK.Client, printProgress, printCompleted bool) {
|
|
||||||
const statusPollingInterval = 1 * time.Second
|
|
||||||
const reportIntervalSeconds = 5
|
|
||||||
var resp *control.GetShardEvacuationStatusResponse
|
|
||||||
reportResponse := new(atomic.Pointer[control.GetShardEvacuationStatusResponse])
|
|
||||||
pollingCompleted := make(chan struct{})
|
|
||||||
progressReportCompleted := make(chan struct{})
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer close(progressReportCompleted)
|
|
||||||
if !printProgress {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cmd.Printf("Progress will be reported every %d seconds.\n", reportIntervalSeconds)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-pollingCompleted:
|
|
||||||
return
|
|
||||||
case <-time.After(reportIntervalSeconds * time.Second):
|
|
||||||
r := reportResponse.Load()
|
|
||||||
if r == nil || r.GetBody().GetStatus() == control.GetShardEvacuationStatusResponse_Body_COMPLETED {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
printStatus(cmd, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
req := &control.GetShardEvacuationStatusRequest{
|
|
||||||
Body: &control.GetShardEvacuationStatusRequest_Body{},
|
|
||||||
}
|
|
||||||
signRequest(cmd, pk, req)
|
|
||||||
|
|
||||||
var err error
|
|
||||||
err = cli.ExecRaw(func(client *client.Client) error {
|
|
||||||
resp, err = control.GetShardEvacuationStatus(client, req)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
|
|
||||||
reportResponse.Store(resp)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
commonCmd.ExitOnErr(cmd, "Failed to get evacuate status, rpc error: %w", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if resp.GetBody().GetStatus() != control.GetShardEvacuationStatusResponse_Body_RUNNING {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(statusPollingInterval)
|
|
||||||
}
|
|
||||||
close(pollingCompleted)
|
|
||||||
<-progressReportCompleted
|
|
||||||
if printCompleted {
|
|
||||||
printCompletedStatusMessage(cmd, resp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func printCompletedStatusMessage(cmd *cobra.Command, resp *control.GetShardEvacuationStatusResponse) {
|
|
||||||
cmd.Println("Shard evacuation has been completed.")
|
|
||||||
sb := &strings.Builder{}
|
|
||||||
appendShardIDs(sb, resp)
|
|
||||||
appendCounts(sb, resp)
|
|
||||||
appendError(sb, resp)
|
|
||||||
appendStartedAt(sb, resp)
|
|
||||||
appendDuration(sb, resp)
|
|
||||||
cmd.Println(sb.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func printStatus(cmd *cobra.Command, resp *control.GetShardEvacuationStatusResponse) {
|
|
||||||
if resp.GetBody().GetStatus() == control.GetShardEvacuationStatusResponse_Body_EVACUATE_SHARD_STATUS_UNDEFINED {
|
|
||||||
cmd.Println("There is no running or completed evacuation.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sb := &strings.Builder{}
|
|
||||||
appendShardIDs(sb, resp)
|
|
||||||
appendStatus(sb, resp)
|
|
||||||
appendCounts(sb, resp)
|
|
||||||
appendError(sb, resp)
|
|
||||||
appendStartedAt(sb, resp)
|
|
||||||
appendDuration(sb, resp)
|
|
||||||
appendEstimation(sb, resp)
|
|
||||||
cmd.Println(sb.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func appendEstimation(sb *strings.Builder, resp *control.GetShardEvacuationStatusResponse) {
|
|
||||||
if resp.GetBody().GetStatus() != control.GetShardEvacuationStatusResponse_Body_RUNNING ||
|
|
||||||
resp.GetBody().GetDuration() == nil ||
|
|
||||||
resp.GetBody().GetTotal() == 0 ||
|
|
||||||
resp.GetBody().GetEvacuated()+resp.GetBody().GetFailed() == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
durationSeconds := float64(resp.GetBody().GetDuration().GetSeconds())
|
|
||||||
evacuated := float64(resp.GetBody().GetEvacuated() + resp.GetBody().GetFailed())
|
|
||||||
avgObjEvacuationTimeSeconds := durationSeconds / evacuated
|
|
||||||
objectsLeft := float64(resp.GetBody().GetTotal()) - evacuated
|
|
||||||
leftSeconds := avgObjEvacuationTimeSeconds * objectsLeft
|
|
||||||
leftMinutes := int(leftSeconds / 60)
|
|
||||||
|
|
||||||
sb.WriteString(fmt.Sprintf(" Estimated time left: %d minutes.", leftMinutes))
|
|
||||||
}
|
|
||||||
|
|
||||||
func appendDuration(sb *strings.Builder, resp *control.GetShardEvacuationStatusResponse) {
|
|
||||||
if resp.GetBody().GetDuration() != nil {
|
|
||||||
duration := time.Second * time.Duration(resp.GetBody().GetDuration().GetSeconds())
|
|
||||||
hour := int(duration.Seconds() / 3600)
|
|
||||||
minute := int(duration.Seconds()/60) % 60
|
|
||||||
second := int(duration.Seconds()) % 60
|
|
||||||
sb.WriteString(fmt.Sprintf(" Duration: %02d:%02d:%02d.", hour, minute, second))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func appendStartedAt(sb *strings.Builder, resp *control.GetShardEvacuationStatusResponse) {
|
|
||||||
if resp.GetBody().GetStartedAt() != nil {
|
|
||||||
startedAt := time.Unix(resp.GetBody().GetStartedAt().GetValue(), 0).UTC()
|
|
||||||
sb.WriteString(fmt.Sprintf(" Started at: %s UTC.", startedAt.Format(time.RFC3339)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func appendError(sb *strings.Builder, resp *control.GetShardEvacuationStatusResponse) {
|
|
||||||
if len(resp.Body.GetErrorMessage()) > 0 {
|
|
||||||
sb.WriteString(fmt.Sprintf(" Error: %s.", resp.Body.GetErrorMessage()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func appendStatus(sb *strings.Builder, resp *control.GetShardEvacuationStatusResponse) {
|
|
||||||
var status string
|
|
||||||
switch resp.GetBody().GetStatus() {
|
|
||||||
case control.GetShardEvacuationStatusResponse_Body_COMPLETED:
|
|
||||||
status = "completed"
|
|
||||||
case control.GetShardEvacuationStatusResponse_Body_RUNNING:
|
|
||||||
status = "running"
|
|
||||||
default:
|
|
||||||
status = "undefined"
|
|
||||||
}
|
|
||||||
sb.WriteString(fmt.Sprintf(" Status: %s.", status))
|
|
||||||
}
|
|
||||||
|
|
||||||
func appendShardIDs(sb *strings.Builder, resp *control.GetShardEvacuationStatusResponse) {
|
|
||||||
sb.WriteString("Shard IDs: ")
|
|
||||||
for idx, shardID := range resp.GetBody().GetShard_ID() {
|
|
||||||
shardIDStr := shard.NewIDFromBytes(shardID).String()
|
|
||||||
if idx > 0 {
|
|
||||||
sb.WriteString(", ")
|
|
||||||
}
|
|
||||||
sb.WriteString(shardIDStr)
|
|
||||||
if idx == len(resp.GetBody().GetShard_ID())-1 {
|
|
||||||
sb.WriteString(".")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func appendCounts(sb *strings.Builder, resp *control.GetShardEvacuationStatusResponse) {
|
|
||||||
sb.WriteString(fmt.Sprintf(" Evacuated %d object out of %d, failed to evacuate %d objects.",
|
|
||||||
resp.GetBody().GetEvacuated(),
|
|
||||||
resp.Body.GetTotal(),
|
|
||||||
resp.Body.GetFailed()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func initControlEvacuationShardCmd() {
|
|
||||||
evacuationShardCmd.AddCommand(startEvacuationShardCmd)
|
|
||||||
evacuationShardCmd.AddCommand(getEvacuationShardStatusCmd)
|
|
||||||
evacuationShardCmd.AddCommand(stopEvacuationShardCmd)
|
|
||||||
|
|
||||||
initControlStartEvacuationShardCmd()
|
|
||||||
initControlFlags(getEvacuationShardStatusCmd)
|
|
||||||
initControlFlags(stopEvacuationShardCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initControlStartEvacuationShardCmd() {
|
|
||||||
initControlFlags(startEvacuationShardCmd)
|
|
||||||
|
|
||||||
flags := startEvacuationShardCmd.Flags()
|
|
||||||
flags.StringSlice(shardIDFlag, nil, "List of shard IDs in base58 encoding")
|
|
||||||
flags.Bool(shardAllFlag, false, "Process all shards")
|
|
||||||
flags.Bool(ignoreErrorsFlag, true, "Skip invalid/unreadable objects")
|
|
||||||
flags.Bool(awaitFlag, false, "Block execution until evacuation is completed")
|
|
||||||
flags.Bool(noProgressFlag, false, fmt.Sprintf("Print progress if %s provided", awaitFlag))
|
|
||||||
|
|
||||||
startEvacuationShardCmd.MarkFlagsMutuallyExclusive(shardIDFlag, shardAllFlag)
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
package control
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var flushCacheCmd = &cobra.Command{
|
|
||||||
Use: "flush-cache",
|
|
||||||
Short: "Flush objects from the write-cache to the main storage",
|
|
||||||
Long: "Flush objects from the write-cache to the main storage",
|
|
||||||
Run: flushCache,
|
|
||||||
}
|
|
||||||
|
|
||||||
func flushCache(cmd *cobra.Command, _ []string) {
|
|
||||||
pk := key.Get(cmd)
|
|
||||||
|
|
||||||
req := &control.FlushCacheRequest{Body: new(control.FlushCacheRequest_Body)}
|
|
||||||
req.Body.Shard_ID = getShardIDList(cmd)
|
|
||||||
|
|
||||||
signRequest(cmd, pk, req)
|
|
||||||
|
|
||||||
cli := getClient(cmd, pk)
|
|
||||||
|
|
||||||
var resp *control.FlushCacheResponse
|
|
||||||
var err error
|
|
||||||
err = cli.ExecRaw(func(client *client.Client) error {
|
|
||||||
resp, err = control.FlushCache(client, req)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
commonCmd.ExitOnErr(cmd, "rpc error: %w", err)
|
|
||||||
|
|
||||||
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
|
|
||||||
|
|
||||||
cmd.Println("Write-cache has been flushed.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initControlFlushCacheCmd() {
|
|
||||||
initControlFlags(flushCacheCmd)
|
|
||||||
|
|
||||||
ff := flushCacheCmd.Flags()
|
|
||||||
ff.StringSlice(shardIDFlag, nil, "List of shard IDs in base58 encoding")
|
|
||||||
ff.Bool(shardAllFlag, false, "Process all shards")
|
|
||||||
|
|
||||||
flushCacheCmd.MarkFlagsMutuallyExclusive(shardIDFlag, shardAllFlag)
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
package control
|
|
||||||
|
|
||||||
import (
|
|
||||||
rawclient "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
healthcheckIRFlag = "ir"
|
|
||||||
)
|
|
||||||
|
|
||||||
var healthCheckCmd = &cobra.Command{
|
|
||||||
Use: "healthcheck",
|
|
||||||
Short: "Health check for FrostFS storage nodes",
|
|
||||||
Long: "Health check for FrostFS storage nodes.",
|
|
||||||
Run: healthCheck,
|
|
||||||
}
|
|
||||||
|
|
||||||
func initControlHealthCheckCmd() {
|
|
||||||
initControlFlags(healthCheckCmd)
|
|
||||||
|
|
||||||
flags := healthCheckCmd.Flags()
|
|
||||||
flags.Bool(healthcheckIRFlag, false, "Communicate with IR node")
|
|
||||||
_ = flags.MarkDeprecated(healthcheckIRFlag, "for health check of inner ring nodes, use the 'control ir healthcheck' command instead.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func healthCheck(cmd *cobra.Command, args []string) {
|
|
||||||
if isIR, _ := cmd.Flags().GetBool(healthcheckIRFlag); isIR {
|
|
||||||
irHealthCheck(cmd, args)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pk := key.Get(cmd)
|
|
||||||
cli := getClient(cmd, pk)
|
|
||||||
|
|
||||||
req := new(control.HealthCheckRequest)
|
|
||||||
req.SetBody(new(control.HealthCheckRequest_Body))
|
|
||||||
|
|
||||||
signRequest(cmd, pk, req)
|
|
||||||
|
|
||||||
var resp *control.HealthCheckResponse
|
|
||||||
var err error
|
|
||||||
err = cli.ExecRaw(func(client *rawclient.Client) error {
|
|
||||||
resp, err = control.HealthCheck(client, req)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
commonCmd.ExitOnErr(cmd, "rpc error: %w", err)
|
|
||||||
|
|
||||||
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
|
|
||||||
|
|
||||||
cmd.Printf("Network status: %s\n", resp.GetBody().GetNetmapStatus())
|
|
||||||
cmd.Printf("Health status: %s\n", resp.GetBody().GetHealthStatus())
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
package control
|
|
||||||
|
|
||||||
import "github.com/spf13/cobra"
|
|
||||||
|
|
||||||
var irCmd = &cobra.Command{
|
|
||||||
Use: "ir",
|
|
||||||
Short: "Operations with inner ring nodes",
|
|
||||||
Long: "Operations with inner ring nodes",
|
|
||||||
}
|
|
||||||
|
|
||||||
func initControlIRCmd() {
|
|
||||||
irCmd.AddCommand(tickEpochCmd)
|
|
||||||
irCmd.AddCommand(removeNodeCmd)
|
|
||||||
irCmd.AddCommand(irHealthCheckCmd)
|
|
||||||
|
|
||||||
initControlIRTickEpochCmd()
|
|
||||||
initControlIRRemoveNodeCmd()
|
|
||||||
initControlIRHealthCheckCmd()
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
package control
|
|
||||||
|
|
||||||
import (
|
|
||||||
rawclient "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
ircontrol "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control/ir"
|
|
||||||
ircontrolsrv "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control/ir/server"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var irHealthCheckCmd = &cobra.Command{
|
|
||||||
Use: "healthcheck",
|
|
||||||
Short: "Health check for FrostFS inner ring nodes",
|
|
||||||
Long: "Health check for FrostFS inner ring nodes.",
|
|
||||||
Run: irHealthCheck,
|
|
||||||
}
|
|
||||||
|
|
||||||
func initControlIRHealthCheckCmd() {
|
|
||||||
initControlFlags(irHealthCheckCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func irHealthCheck(cmd *cobra.Command, _ []string) {
|
|
||||||
pk := key.Get(cmd)
|
|
||||||
cli := getClient(cmd, pk)
|
|
||||||
|
|
||||||
req := new(ircontrol.HealthCheckRequest)
|
|
||||||
|
|
||||||
req.SetBody(new(ircontrol.HealthCheckRequest_Body))
|
|
||||||
|
|
||||||
err := ircontrolsrv.SignMessage(pk, req)
|
|
||||||
commonCmd.ExitOnErr(cmd, "could not sign request: %w", err)
|
|
||||||
|
|
||||||
var resp *ircontrol.HealthCheckResponse
|
|
||||||
err = cli.ExecRaw(func(client *rawclient.Client) error {
|
|
||||||
resp, err = ircontrol.HealthCheck(client, req)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
commonCmd.ExitOnErr(cmd, "rpc error: %w", err)
|
|
||||||
|
|
||||||
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
|
|
||||||
|
|
||||||
cmd.Printf("Health status: %s\n", resp.GetBody().GetHealthStatus())
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
package control
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
rawclient "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
ircontrol "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control/ir"
|
|
||||||
ircontrolsrv "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control/ir/server"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var removeNodeCmd = &cobra.Command{
|
|
||||||
Use: "remove-node",
|
|
||||||
Short: "Forces a node removal from netmap",
|
|
||||||
Long: "Forces a node removal from netmap via a notary request. It should be executed on other IR nodes as well.",
|
|
||||||
Run: removeNode,
|
|
||||||
}
|
|
||||||
|
|
||||||
func initControlIRRemoveNodeCmd() {
|
|
||||||
initControlFlags(removeNodeCmd)
|
|
||||||
|
|
||||||
flags := removeNodeCmd.Flags()
|
|
||||||
flags.String("node", "", "Node public key as a hex string")
|
|
||||||
_ = removeNodeCmd.MarkFlagRequired("node")
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeNode(cmd *cobra.Command, _ []string) {
|
|
||||||
pk := key.Get(cmd)
|
|
||||||
c := getClient(cmd, pk)
|
|
||||||
|
|
||||||
nodeKeyStr, _ := cmd.Flags().GetString("node")
|
|
||||||
if len(nodeKeyStr) == 0 {
|
|
||||||
commonCmd.ExitOnErr(cmd, "parsing node public key: ", errors.New("key cannot be empty"))
|
|
||||||
}
|
|
||||||
nodeKey, err := hex.DecodeString(nodeKeyStr)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't decode node public key: %w", err)
|
|
||||||
|
|
||||||
req := new(ircontrol.RemoveNodeRequest)
|
|
||||||
req.SetBody(&ircontrol.RemoveNodeRequest_Body{
|
|
||||||
Key: nodeKey,
|
|
||||||
})
|
|
||||||
|
|
||||||
commonCmd.ExitOnErr(cmd, "could not sign request: %w", ircontrolsrv.SignMessage(pk, req))
|
|
||||||
|
|
||||||
var resp *ircontrol.RemoveNodeResponse
|
|
||||||
err = c.ExecRaw(func(client *rawclient.Client) error {
|
|
||||||
resp, err = ircontrol.RemoveNode(client, req)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
commonCmd.ExitOnErr(cmd, "rpc error: %w", err)
|
|
||||||
|
|
||||||
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
|
|
||||||
|
|
||||||
cmd.Println("Node removed")
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
package control
|
|
||||||
|
|
||||||
import (
|
|
||||||
rawclient "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
ircontrol "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control/ir"
|
|
||||||
ircontrolsrv "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control/ir/server"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var tickEpochCmd = &cobra.Command{
|
|
||||||
Use: "tick-epoch",
|
|
||||||
Short: "Forces a new epoch",
|
|
||||||
Long: "Forces a new epoch via a notary request. It should be executed on other IR nodes as well.",
|
|
||||||
Run: tickEpoch,
|
|
||||||
}
|
|
||||||
|
|
||||||
func initControlIRTickEpochCmd() {
|
|
||||||
initControlFlags(tickEpochCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func tickEpoch(cmd *cobra.Command, _ []string) {
|
|
||||||
pk := key.Get(cmd)
|
|
||||||
c := getClient(cmd, pk)
|
|
||||||
|
|
||||||
req := new(ircontrol.TickEpochRequest)
|
|
||||||
req.SetBody(new(ircontrol.TickEpochRequest_Body))
|
|
||||||
|
|
||||||
err := ircontrolsrv.SignMessage(pk, req)
|
|
||||||
commonCmd.ExitOnErr(cmd, "could not sign request: %w", err)
|
|
||||||
|
|
||||||
var resp *ircontrol.TickEpochResponse
|
|
||||||
err = c.ExecRaw(func(client *rawclient.Client) error {
|
|
||||||
resp, err = ircontrol.TickEpoch(client, req)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
commonCmd.ExitOnErr(cmd, "rpc error: %w", err)
|
|
||||||
|
|
||||||
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
|
|
||||||
|
|
||||||
cmd.Println("Epoch tick requested")
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
package control
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Cmd = &cobra.Command{
|
|
||||||
Use: "control",
|
|
||||||
Short: "Operations with storage node",
|
|
||||||
Long: `Operations with storage node`,
|
|
||||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
|
||||||
ff := cmd.Flags()
|
|
||||||
|
|
||||||
_ = viper.BindPFlag(commonflags.WalletPath, ff.Lookup(commonflags.WalletPath))
|
|
||||||
_ = viper.BindPFlag(commonflags.Account, ff.Lookup(commonflags.Account))
|
|
||||||
_ = viper.BindPFlag(controlRPC, ff.Lookup(controlRPC))
|
|
||||||
_ = viper.BindPFlag(commonflags.Timeout, ff.Lookup(commonflags.Timeout))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
controlRPC = "endpoint"
|
|
||||||
controlRPCDefault = ""
|
|
||||||
controlRPCUsage = "Remote node control address (as 'multiaddr' or '<host>:<port>')"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
Cmd.AddCommand(
|
|
||||||
healthCheckCmd,
|
|
||||||
setNetmapStatusCmd,
|
|
||||||
dropObjectsCmd,
|
|
||||||
shardsCmd,
|
|
||||||
synchronizeTreeCmd,
|
|
||||||
irCmd,
|
|
||||||
)
|
|
||||||
|
|
||||||
initControlHealthCheckCmd()
|
|
||||||
initControlSetNetmapStatusCmd()
|
|
||||||
initControlDropObjectsCmd()
|
|
||||||
initControlShardsCmd()
|
|
||||||
initControlSynchronizeTreeCmd()
|
|
||||||
initControlIRCmd()
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue