├── .github └── workflows │ ├── casper-node-launcher-pr.yml │ └── casper-node-launcher-publish.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── DOCKER_README.md ├── Dockerfile ├── LICENSE ├── README.md ├── ci ├── build_docker.sh ├── publish_deb_to_repo.sh └── push_docker.sh ├── resources ├── BIN_README.md ├── ETC_README.md ├── VALIDATOR_KEYS_README.md └── maintainer_scripts │ ├── README.md │ ├── casper_node_launcher │ └── casper-node-launcher.service │ ├── debian │ ├── postinst │ └── preinst │ ├── logrotate.d │ └── casper-node │ ├── network_configs │ ├── README.md │ ├── casper-test.conf │ └── casper.conf │ └── node_util.py ├── src ├── launcher.rs ├── logging.rs ├── main.rs └── utils.rs └── test_resources ├── casper-node.in ├── downgrade.in ├── downgrade.sh └── shutdown.in /.github/workflows/casper-node-launcher-pr.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Continuous Integration 4 | 5 | jobs: 6 | test: 7 | name: Test Suite 8 | strategy: 9 | matrix: 10 | include: 11 | - os: ubuntu-20.04 12 | code_name: focal 13 | 14 | runs-on: ${{ matrix.os }} 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: actions-rs/toolchain@v1 19 | with: 20 | profile: minimal 21 | toolchain: stable 22 | override: true 23 | 24 | - name: rustup setup 25 | run: rustup component add rustfmt clippy 26 | 27 | - name: cargo setup 28 | run: cargo install cargo-audit 29 | 30 | - name: fmt 31 | run: cargo fmt -- --check 32 | 33 | - name: clippy 34 | run: cargo clippy -- -D warnings 35 | 36 | - name: audit 37 | run: cargo audit --deny warnings --ignore RUSTSEC-2024-0375 --ignore RUSTSEC-2021-0145 38 | 39 | - name: test 40 | run: cargo test 41 | -------------------------------------------------------------------------------- /.github/workflows/casper-node-launcher-publish.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: publish-casper-node-launcher 3 | permissions: 4 | contents: read 5 | id-token: write 6 | 7 | on: 8 | push: 9 | tags: 10 | - "v*.*.*" 11 | 12 | jobs: 13 | publish_deb: 14 | strategy: 15 | matrix: 16 | include: 17 | - os: ubuntu-20.04 18 | code_name: focal 19 | 20 | runs-on: ${{ matrix.os }} 21 | 22 | steps: 23 | - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b #v3.0.2 24 | with: 25 | key: ${{ matrix.code_name }} 26 | 27 | - name: Configure AWS credentials 28 | uses: aws-actions/configure-aws-credentials@v4 29 | with: 30 | role-to-assume: ${{ secrets.AWS_ACCESS_ROLE_REPO }} 31 | role-session-name: GitHub_to_AWS_via_FederatedOIDC 32 | aws-region: ${{ secrets.AWS_ACCESS_REGION_REPO }} 33 | 34 | - name: Install deps 35 | run: | 36 | echo "deb http://repo.aptly.info/ squeeze main" | sudo tee -a /etc/apt/sources.list.d/aptly.list 37 | wget -qO - https://www.aptly.info/pubkey.txt | sudo apt-key add - 38 | sudo apt-get update 39 | sudo apt-get install -y aptly=1.4.0 40 | aptly config show 41 | 42 | - name: Import GPG key 43 | uses: crazy-max/ghaction-import-gpg@c8bb57c57e8df1be8c73ff3d59deab1dbc00e0d1 #v5.1.0 44 | with: 45 | gpg_private_key: ${{ secrets.APTLY_GPG_KEY }} 46 | passphrase: ${{ secrets.APTLY_GPG_PASS }} 47 | 48 | - name: Install cargo deb 49 | run: cargo install cargo-deb 50 | 51 | - name: Cargo build 52 | run: cargo build --release 53 | 54 | - name: Cargo deb 55 | run: cargo deb --no-build --variant ${{ matrix.code_name }} 56 | 57 | - name: Upload binaries to repo 58 | env: 59 | PLUGIN_REPO_NAME: ${{ secrets.AWS_BUCKET_REPO }} 60 | PLUGIN_REGION: ${{ secrets.AWS_ACCESS_REGION_REPO }} 61 | PLUGIN_GPG_KEY: ${{ secrets.APTLY_GPG_KEY }} 62 | PLUGIN_GPG_PASS: ${{ secrets.APTLY_GPG_PASS }} 63 | PLUGIN_ACL: 'private' 64 | PLUGIN_PREFIX: 'releases' 65 | PLUGIN_DEB_PATH: './target/debian' 66 | PLUGIN_OS_CODENAME: ${{ matrix.code_name }} 67 | run: ./ci/publish_deb_to_repo.sh 68 | 69 | - name: Invalidate CloudFront cache 70 | run: | 71 | aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_CLOUDFRONT_REPO }} --paths "/*" 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /target 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog]. 4 | 5 | [comment]: <> (Added: new features) 6 | [comment]: <> (Changed: changes in existing functionality) 7 | [comment]: <> (Deprecated: soon-to-be removed features) 8 | [comment]: <> (Removed: now removed features) 9 | [comment]: <> (Fixed: any bug fixes) 10 | [comment]: <> (Security: in case of vulnerabilities) 11 | 12 | ## [Unreleased] 13 | ### Added 14 | * Launcher now handles node exit code `103` by running a script at `/etc/casper/casper_shutdown_script` and exiting with its exit code if present, otherwise returning 0. 15 | 16 | ## [1.0.0] - 2022-01-10 17 | 18 | ### Added 19 | * Commented out line provided in systemd unit to allow users to set hard limit of files to 64000 (from default 4096). 20 | * node_util.py updates to expand capability 21 | * Deprecation warning to older scripts 22 | * README.md updates related to configuration of nofile limit 23 | 24 | ## [0.3.5] - 2021-10-25 25 | 26 | ### Added 27 | * node_util.py script to gradually replace various shell scripts in /etc/casper 28 | * BIN_MODE to network configs 29 | 30 | ### Removed 31 | * Docker image build and publish 32 | * bintray deb publish 33 | 34 | ## [0.3.4] - 2021-07-27 35 | 36 | ### Added 37 | * RPM package build 38 | * Publish DEB and RPM package to GitHub tag 39 | * PLATFORM file install to indicate system type 40 | 41 | ### Changed 42 | * License from COSL to Apache 43 | 44 | ## [0.3.3] - 2021-04-06 45 | 46 | ### Added 47 | * Network configurations to allow pulling protocol versions from a configurable location 48 | * Verification of running under casper user for scripts 49 | * Improvement of external IP detection for config_from_example.sh 50 | * Network configurations for casper and casper-test networks 51 | 52 | ## [0.3.2] - 2021-03-19 53 | 54 | ### Changed 55 | * Package install README updates 56 | * Better validation of pull_casper_node_version.sh 57 | 58 | ### Removed 59 | * systemd environment arg for legacy net 60 | 61 | ## [0.3.1] - 2021-03-10 62 | 63 | ### Added 64 | * Docker image build capability 65 | * Better validation to pull_casper_node_version.sh 66 | 67 | ### Changed 68 | * systemd unit restart time limit set to 15 seconds 69 | 70 | ## [0.3.0] - 2021-02-17 71 | 72 | ### Added 73 | * 3 start retry within 1000 seconds and 1 sec restart delay to systemd unit 74 | * copytruncate to logrotate 75 | * Downgrade capability 76 | 77 | ## 0.2.0 - 2021-02-08 78 | 79 | Initial Public Release 80 | 81 | [Keep a Changelog]: https://keepachangelog.com/en/1.0.0 82 | [unreleased]: https://github.com/casper-network/casper-node-launcher/compare/v0.4.0...main 83 | [1.0.0]: https://github.com/casper-network/casper-node-launcher/compare/v0.3.5...v1.0.0 84 | [0.3.5]: https://github.com/casper-network/casper-node-launcher/compare/v0.3.4...v0.3.5 85 | [0.3.4]: https://github.com/casper-network/casper-node-launcher/compare/v0.3.3...v0.3.4 86 | [0.3.3]: https://github.com/casper-network/casper-node-launcher/compare/v0.3.2...v0.3.3 87 | [0.3.2]: https://github.com/casper-network/casper-node-launcher/compare/v0.3.1...v0.3.2 88 | [0.3.1]: https://github.com/casper-network/casper-node-launcher/compare/v0.3.0...v0.3.1 89 | [0.3.0]: https://github.com/casper-network/casper-node-launcher/compare/v0.2.0...v0.3.0 90 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "anyhow" 31 | version = "1.0.95" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" 34 | 35 | [[package]] 36 | name = "atty" 37 | version = "0.2.14" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 40 | dependencies = [ 41 | "hermit-abi", 42 | "libc", 43 | "winapi", 44 | ] 45 | 46 | [[package]] 47 | name = "autocfg" 48 | version = "1.4.0" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 51 | 52 | [[package]] 53 | name = "backtrace" 54 | version = "0.3.74" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 57 | dependencies = [ 58 | "addr2line", 59 | "cfg-if", 60 | "libc", 61 | "miniz_oxide", 62 | "object", 63 | "rustc-demangle", 64 | "windows-targets", 65 | ] 66 | 67 | [[package]] 68 | name = "bitflags" 69 | version = "1.3.2" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 72 | 73 | [[package]] 74 | name = "bitflags" 75 | version = "2.8.0" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" 78 | 79 | [[package]] 80 | name = "casper-node-launcher" 81 | version = "1.0.6" 82 | dependencies = [ 83 | "anyhow", 84 | "backtrace", 85 | "clap", 86 | "nix", 87 | "once_cell", 88 | "semver", 89 | "serde", 90 | "signal-hook", 91 | "tempfile", 92 | "toml", 93 | "tracing", 94 | "tracing-subscriber", 95 | ] 96 | 97 | [[package]] 98 | name = "cc" 99 | version = "1.2.14" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9" 102 | dependencies = [ 103 | "shlex", 104 | ] 105 | 106 | [[package]] 107 | name = "cfg-if" 108 | version = "1.0.0" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 111 | 112 | [[package]] 113 | name = "clap" 114 | version = "3.2.25" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" 117 | dependencies = [ 118 | "atty", 119 | "bitflags 1.3.2", 120 | "clap_lex", 121 | "indexmap", 122 | "once_cell", 123 | "strsim", 124 | "termcolor", 125 | "textwrap", 126 | ] 127 | 128 | [[package]] 129 | name = "clap_lex" 130 | version = "0.2.4" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" 133 | dependencies = [ 134 | "os_str_bytes", 135 | ] 136 | 137 | [[package]] 138 | name = "errno" 139 | version = "0.3.10" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 142 | dependencies = [ 143 | "libc", 144 | "windows-sys", 145 | ] 146 | 147 | [[package]] 148 | name = "fastrand" 149 | version = "2.3.0" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 152 | 153 | [[package]] 154 | name = "getrandom" 155 | version = "0.3.1" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" 158 | dependencies = [ 159 | "cfg-if", 160 | "libc", 161 | "wasi", 162 | "windows-targets", 163 | ] 164 | 165 | [[package]] 166 | name = "gimli" 167 | version = "0.31.1" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 170 | 171 | [[package]] 172 | name = "hashbrown" 173 | version = "0.12.3" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 176 | 177 | [[package]] 178 | name = "hermit-abi" 179 | version = "0.1.19" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 182 | dependencies = [ 183 | "libc", 184 | ] 185 | 186 | [[package]] 187 | name = "indexmap" 188 | version = "1.9.3" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 191 | dependencies = [ 192 | "autocfg", 193 | "hashbrown", 194 | ] 195 | 196 | [[package]] 197 | name = "itoa" 198 | version = "1.0.14" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 201 | 202 | [[package]] 203 | name = "lazy_static" 204 | version = "1.5.0" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 207 | 208 | [[package]] 209 | name = "libc" 210 | version = "0.2.169" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 213 | 214 | [[package]] 215 | name = "linux-raw-sys" 216 | version = "0.4.15" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 219 | 220 | [[package]] 221 | name = "log" 222 | version = "0.4.25" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" 225 | 226 | [[package]] 227 | name = "matchers" 228 | version = "0.1.0" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 231 | dependencies = [ 232 | "regex-automata 0.1.10", 233 | ] 234 | 235 | [[package]] 236 | name = "memchr" 237 | version = "2.7.4" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 240 | 241 | [[package]] 242 | name = "memoffset" 243 | version = "0.6.5" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" 246 | dependencies = [ 247 | "autocfg", 248 | ] 249 | 250 | [[package]] 251 | name = "miniz_oxide" 252 | version = "0.8.4" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" 255 | dependencies = [ 256 | "adler2", 257 | ] 258 | 259 | [[package]] 260 | name = "nix" 261 | version = "0.23.2" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" 264 | dependencies = [ 265 | "bitflags 1.3.2", 266 | "cc", 267 | "cfg-if", 268 | "libc", 269 | "memoffset", 270 | ] 271 | 272 | [[package]] 273 | name = "nu-ansi-term" 274 | version = "0.46.0" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 277 | dependencies = [ 278 | "overload", 279 | "winapi", 280 | ] 281 | 282 | [[package]] 283 | name = "object" 284 | version = "0.36.7" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 287 | dependencies = [ 288 | "memchr", 289 | ] 290 | 291 | [[package]] 292 | name = "once_cell" 293 | version = "1.20.3" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" 296 | 297 | [[package]] 298 | name = "os_str_bytes" 299 | version = "6.6.1" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" 302 | 303 | [[package]] 304 | name = "overload" 305 | version = "0.1.1" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 308 | 309 | [[package]] 310 | name = "pin-project-lite" 311 | version = "0.2.16" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 314 | 315 | [[package]] 316 | name = "proc-macro2" 317 | version = "1.0.93" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 320 | dependencies = [ 321 | "unicode-ident", 322 | ] 323 | 324 | [[package]] 325 | name = "quote" 326 | version = "1.0.38" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 329 | dependencies = [ 330 | "proc-macro2", 331 | ] 332 | 333 | [[package]] 334 | name = "regex" 335 | version = "1.11.1" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 338 | dependencies = [ 339 | "aho-corasick", 340 | "memchr", 341 | "regex-automata 0.4.9", 342 | "regex-syntax 0.8.5", 343 | ] 344 | 345 | [[package]] 346 | name = "regex-automata" 347 | version = "0.1.10" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 350 | dependencies = [ 351 | "regex-syntax 0.6.29", 352 | ] 353 | 354 | [[package]] 355 | name = "regex-automata" 356 | version = "0.4.9" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 359 | dependencies = [ 360 | "aho-corasick", 361 | "memchr", 362 | "regex-syntax 0.8.5", 363 | ] 364 | 365 | [[package]] 366 | name = "regex-syntax" 367 | version = "0.6.29" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 370 | 371 | [[package]] 372 | name = "regex-syntax" 373 | version = "0.8.5" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 376 | 377 | [[package]] 378 | name = "rustc-demangle" 379 | version = "0.1.24" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 382 | 383 | [[package]] 384 | name = "rustix" 385 | version = "0.38.44" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 388 | dependencies = [ 389 | "bitflags 2.8.0", 390 | "errno", 391 | "libc", 392 | "linux-raw-sys", 393 | "windows-sys", 394 | ] 395 | 396 | [[package]] 397 | name = "ryu" 398 | version = "1.0.19" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" 401 | 402 | [[package]] 403 | name = "semver" 404 | version = "1.0.25" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" 407 | dependencies = [ 408 | "serde", 409 | ] 410 | 411 | [[package]] 412 | name = "serde" 413 | version = "1.0.217" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" 416 | dependencies = [ 417 | "serde_derive", 418 | ] 419 | 420 | [[package]] 421 | name = "serde_derive" 422 | version = "1.0.217" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" 425 | dependencies = [ 426 | "proc-macro2", 427 | "quote", 428 | "syn", 429 | ] 430 | 431 | [[package]] 432 | name = "serde_json" 433 | version = "1.0.138" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" 436 | dependencies = [ 437 | "itoa", 438 | "memchr", 439 | "ryu", 440 | "serde", 441 | ] 442 | 443 | [[package]] 444 | name = "sharded-slab" 445 | version = "0.1.7" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 448 | dependencies = [ 449 | "lazy_static", 450 | ] 451 | 452 | [[package]] 453 | name = "shlex" 454 | version = "1.3.0" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 457 | 458 | [[package]] 459 | name = "signal-hook" 460 | version = "0.3.17" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 463 | dependencies = [ 464 | "libc", 465 | "signal-hook-registry", 466 | ] 467 | 468 | [[package]] 469 | name = "signal-hook-registry" 470 | version = "1.4.2" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 473 | dependencies = [ 474 | "libc", 475 | ] 476 | 477 | [[package]] 478 | name = "smallvec" 479 | version = "1.14.0" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" 482 | 483 | [[package]] 484 | name = "strsim" 485 | version = "0.10.0" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 488 | 489 | [[package]] 490 | name = "syn" 491 | version = "2.0.98" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" 494 | dependencies = [ 495 | "proc-macro2", 496 | "quote", 497 | "unicode-ident", 498 | ] 499 | 500 | [[package]] 501 | name = "tempfile" 502 | version = "3.17.1" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" 505 | dependencies = [ 506 | "cfg-if", 507 | "fastrand", 508 | "getrandom", 509 | "once_cell", 510 | "rustix", 511 | "windows-sys", 512 | ] 513 | 514 | [[package]] 515 | name = "termcolor" 516 | version = "1.4.1" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 519 | dependencies = [ 520 | "winapi-util", 521 | ] 522 | 523 | [[package]] 524 | name = "textwrap" 525 | version = "0.16.1" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" 528 | 529 | [[package]] 530 | name = "thread_local" 531 | version = "1.1.8" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 534 | dependencies = [ 535 | "cfg-if", 536 | "once_cell", 537 | ] 538 | 539 | [[package]] 540 | name = "toml" 541 | version = "0.5.11" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" 544 | dependencies = [ 545 | "serde", 546 | ] 547 | 548 | [[package]] 549 | name = "tracing" 550 | version = "0.1.41" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 553 | dependencies = [ 554 | "pin-project-lite", 555 | "tracing-attributes", 556 | "tracing-core", 557 | ] 558 | 559 | [[package]] 560 | name = "tracing-attributes" 561 | version = "0.1.28" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" 564 | dependencies = [ 565 | "proc-macro2", 566 | "quote", 567 | "syn", 568 | ] 569 | 570 | [[package]] 571 | name = "tracing-core" 572 | version = "0.1.33" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 575 | dependencies = [ 576 | "once_cell", 577 | "valuable", 578 | ] 579 | 580 | [[package]] 581 | name = "tracing-log" 582 | version = "0.2.0" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 585 | dependencies = [ 586 | "log", 587 | "once_cell", 588 | "tracing-core", 589 | ] 590 | 591 | [[package]] 592 | name = "tracing-serde" 593 | version = "0.2.0" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" 596 | dependencies = [ 597 | "serde", 598 | "tracing-core", 599 | ] 600 | 601 | [[package]] 602 | name = "tracing-subscriber" 603 | version = "0.3.19" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" 606 | dependencies = [ 607 | "matchers", 608 | "nu-ansi-term", 609 | "once_cell", 610 | "regex", 611 | "serde", 612 | "serde_json", 613 | "sharded-slab", 614 | "smallvec", 615 | "thread_local", 616 | "tracing", 617 | "tracing-core", 618 | "tracing-log", 619 | "tracing-serde", 620 | ] 621 | 622 | [[package]] 623 | name = "unicode-ident" 624 | version = "1.0.17" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" 627 | 628 | [[package]] 629 | name = "valuable" 630 | version = "0.1.1" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 633 | 634 | [[package]] 635 | name = "wasi" 636 | version = "0.13.3+wasi-0.2.2" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" 639 | dependencies = [ 640 | "wit-bindgen-rt", 641 | ] 642 | 643 | [[package]] 644 | name = "winapi" 645 | version = "0.3.9" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 648 | dependencies = [ 649 | "winapi-i686-pc-windows-gnu", 650 | "winapi-x86_64-pc-windows-gnu", 651 | ] 652 | 653 | [[package]] 654 | name = "winapi-i686-pc-windows-gnu" 655 | version = "0.4.0" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 658 | 659 | [[package]] 660 | name = "winapi-util" 661 | version = "0.1.9" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 664 | dependencies = [ 665 | "windows-sys", 666 | ] 667 | 668 | [[package]] 669 | name = "winapi-x86_64-pc-windows-gnu" 670 | version = "0.4.0" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 673 | 674 | [[package]] 675 | name = "windows-sys" 676 | version = "0.59.0" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 679 | dependencies = [ 680 | "windows-targets", 681 | ] 682 | 683 | [[package]] 684 | name = "windows-targets" 685 | version = "0.52.6" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 688 | dependencies = [ 689 | "windows_aarch64_gnullvm", 690 | "windows_aarch64_msvc", 691 | "windows_i686_gnu", 692 | "windows_i686_gnullvm", 693 | "windows_i686_msvc", 694 | "windows_x86_64_gnu", 695 | "windows_x86_64_gnullvm", 696 | "windows_x86_64_msvc", 697 | ] 698 | 699 | [[package]] 700 | name = "windows_aarch64_gnullvm" 701 | version = "0.52.6" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 704 | 705 | [[package]] 706 | name = "windows_aarch64_msvc" 707 | version = "0.52.6" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 710 | 711 | [[package]] 712 | name = "windows_i686_gnu" 713 | version = "0.52.6" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 716 | 717 | [[package]] 718 | name = "windows_i686_gnullvm" 719 | version = "0.52.6" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 722 | 723 | [[package]] 724 | name = "windows_i686_msvc" 725 | version = "0.52.6" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 728 | 729 | [[package]] 730 | name = "windows_x86_64_gnu" 731 | version = "0.52.6" 732 | source = "registry+https://github.com/rust-lang/crates.io-index" 733 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 734 | 735 | [[package]] 736 | name = "windows_x86_64_gnullvm" 737 | version = "0.52.6" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 740 | 741 | [[package]] 742 | name = "windows_x86_64_msvc" 743 | version = "0.52.6" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 746 | 747 | [[package]] 748 | name = "wit-bindgen-rt" 749 | version = "0.33.0" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" 752 | dependencies = [ 753 | "bitflags 2.8.0", 754 | ] 755 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "casper-node-launcher" 3 | version = "1.0.6" 4 | authors = ["Fraser Hutchison ", "Joe Sacher "] 5 | edition = "2018" 6 | description = "A binary which runs and upgrades the casper-node of the Casper network" 7 | readme = "README.md" 8 | documentation = "https://docs.rs/casper-node-launcher" 9 | homepage = "https://casper.network" 10 | repository = "https://github.com/casper-network/casper-node-launcher" 11 | license-file = "./LICENSE" 12 | 13 | [dependencies] 14 | anyhow = "1.0.38" 15 | backtrace = "0.3.56" 16 | clap = { version = "3.2.23", features = ["cargo"] } 17 | once_cell = "1.5.2" 18 | nix = "0.23.0" 19 | semver = { version = "1.0.4", features = ["serde"] } 20 | serde = { version = "1.0.120", features = ["derive"] } 21 | signal-hook = "0.3.4" 22 | toml = "0.5.8" 23 | tracing = "0.1.22" 24 | tracing-subscriber = { version = "0.3.17", features = ["json", "env-filter"] } 25 | 26 | [dev-dependencies] 27 | once_cell = "1.5.2" 28 | tempfile = "3.6.0" 29 | 30 | [package.metadata.deb] 31 | name = "casper-node-launcher" 32 | depends = "curl" 33 | revision = "0" 34 | assets = [ 35 | ["./target/release/casper-node-launcher", "/usr/bin/casper-node-launcher", "755"], 36 | ["./resources/BIN_README.md", "/var/lib/casper/bin/README.md", "755"], 37 | ["./resources/maintainer_scripts/logrotate.d/casper-node", "/etc/logrotate.d/casper-node", "644"], 38 | ["./resources/maintainer_scripts/network_configs/*", "/etc/casper/network_configs/", "644"], 39 | ["./resources/maintainer_scripts/node_util.py", "/etc/casper/node_util.py", "755"], 40 | ["./resources/ETC_README.md", "/etc/casper/README.md", "644"], 41 | ["./resources/VALIDATOR_KEYS_README.md", "/etc/casper/validator_keys/README.md", "644"] 42 | ] 43 | maintainer-scripts = "./resources/maintainer_scripts/debian" 44 | extended-description = """ 45 | Package for Casper Node Launcher 46 | 47 | For information on using package, see https://github.com/casper-network/casper-node-launcher 48 | """ 49 | 50 | [package.metadata.deb.variants.focal] 51 | 52 | [package.metadata.deb.variants.jammy] 53 | 54 | [package.metadata.deb.variants.noble] 55 | 56 | [package.metadata.deb.systemd-units] 57 | unit-name = "casper-node-launcher" 58 | enable = true 59 | unit-scripts = "resources/maintainer_scripts/casper_node_launcher" 60 | start = false 61 | restart-after-upgrade = true 62 | stop-on-upgrade = false 63 | -------------------------------------------------------------------------------- /DOCKER_README.md: -------------------------------------------------------------------------------- 1 | # Container for running casper-node-launcher 2 | 3 | Scripts that would be in the `/etc/casper` directory with a normal debian install exist with the 4 | `casper-node-launcher` executable. 5 | 6 | It is expected that users will mount two volumes to the inside of the container at: 7 | `/etc/casper` and `/var/lib/casper`. 8 | 9 | It is also expected that `/etc/casper/validator_keys` exists and is populated by keys from `casper-client keygen`. 10 | 11 | For minimum node functionality port 35000 needs to be accessible. For full functionality, ports 7777, 8888, and 9999 12 | should also be exposed, unless they have been changed in the config.toml. 13 | 14 | Initial configuration will consist of calling: 15 | `/root/pull_casper_node_version.sh ` 16 | `/root/config_from_example.sh ` 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM casperlabs/node-build-u1804 2 | COPY . /src 3 | RUN cd src && cargo build --release 4 | 5 | FROM ubuntu:bionic 6 | LABEL vendor=CasperLabs \ 7 | description="This container holds casper-node-launcher and scripts for operation of a node on the Casper Network." 8 | 9 | WORKDIR /root/ 10 | RUN apt-get update && \ 11 | apt-get install -y --no-install-recommends curl && \ 12 | rm -rf /var/lib/apt/lists/ 13 | 14 | COPY --from=0 /src/target/release/casper-node-launcher . 15 | COPY ./resources/maintainer_scripts/*.sh /root/ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Casper Association 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # casper-node-launcher 2 | 3 | A binary which runs and upgrades the casper-node of the Casper network. 4 | 5 | ## Usage 6 | 7 | ``` 8 | casper-node-launcher [OPTIONS] 9 | 10 | OPTIONS: 11 | -f, --force-version Forces the launcher to run the specified version of the node, 12 | for example "1.2.3" 13 | -h, --help Print help information 14 | -V, --version Print version information 15 | ``` 16 | 17 | On startup, launcher checks whether the installed node binaries match the installed configs, 18 | by comparing the version numbers. If not, it exits with an error. 19 | 20 | The launcher then checks if the `--force-version` parameter was provided. If yes, it will unconditionally 21 | run the specified node, given it is installed. The requested version is then cached in the state, 22 | so the subsequent runs of the launcher will continue to execute the previously requested version. 23 | 24 | If the `--force-version` parameter was not provided the launcher either tries to read its previously cached state 25 | from disk, or assumes a fresh start. On a fresh start, the launcher searches for the highest installed 26 | version of `casper-node` and starts running it in validator mode. 27 | 28 | After every run of the `casper-node` binary in validator mode, the launcher does the following based upon the exit code 29 | returned by `casper-node`: 30 | * If 0 (success), searches for the immediate next installed version of `casper-node` and runs it in migrate-data mode 31 | * If 102 (downgrade), searches for the immediate previous installed version of `casper-node` and runs it in validator 32 | mode 33 | * If 103 (shutdown), runs the script at `/etc/casper/casper_shutdown_script` if present and exits with its exit code, 34 | otherwise exits with `0`. 35 | * Any other value causes the launcher to exit with an error 36 | 37 | After every run of the `casper-node` binary in migrate-data mode, the launcher does the following based upon the exit 38 | code returned by `casper-node`: 39 | * If 0 (success), runs the same version of `casper-node` in validator mode 40 | * If 102 (downgrade), searches for the immediate previous installed version of `casper-node` and runs it in validator 41 | mode 42 | * If 103 (shutdown), runs the script at `/etc/casper/casper_shutdown_script` if present and exits with its exit code, 43 | otherwise exits with `0`. 44 | * Any other value causes the launcher to exit with an error 45 | 46 | If the launcher cannot find an appropriate version at any stage of upgrading or downgrading, it exits with an error. 47 | 48 | The default path for the casper-node's config file is `/etc/casper/1_0_0/config.toml` where the folder `1_0_0` 49 | indicates the semver version of the node software. 50 | 51 | The default path for the launcher's cached state file is `/etc/casper/casper-node-launcher-state.toml`. 52 | 53 | For testing purposes, the common folder `/etc/casper` can be overridden by setting the environment variable 54 | `CASPER_CONFIG_DIR` to a different folder. 55 | 56 | The default path for the casper-node binary is `/var/lib/casper/bin/1_0_0/casper-node` where the folder `1_0_0` likewise 57 | indicates the version. The default path for the casper-node-launcher binary is 58 | `/var/lib/casper/bin/casper-node-launcher`. 59 | 60 | For testing purposes, the common folder `/var/lib/casper/bin` can be overridden by setting the environment variable 61 | `CASPER_BIN_DIR` to a different folder. 62 | 63 | ## Number of Files Limit 64 | 65 | When `casper-node` launches, it tries to set the `nofiles` for the process to `64000`. With some systems, this will 66 | hit the default hard limit of `4096`. 67 | 68 | Filehandles are used for both files and network connections. The network connections are unpredictable and running 69 | out of file handles can stop critical file writes from occurring. This limit may need to be increased from defaults. 70 | 71 | With `casper-node-launcher` running we can see what the system allocated by finding our process id (PID) for casper-node 72 | with `pgrep "casper-node$"`. 73 | 74 | ```shell 75 | $ pgrep "casper-node$" 76 | 275928 77 | ``` 78 | 79 | This PID will change so you need to run the above command to get the current version with your system. 80 | It will not be `275928` each time. If you get no return, you do not have `casper-node-launcher` running properly. 81 | 82 | To find the current `nofile` (number of open files) hard limit, we can run `prlimit` with this PID: 83 | 84 | ```shell 85 | $ sudo prlimit -n -p 275928 86 | RESOURCE DESCRIPTION SOFT HARD UNITS 87 | NOFILE max number of open files 1024 4096 files 88 | ``` 89 | 90 | We can embed both commands together so it is only `sudo prlimit -n -p $(pgrep "casper-node$")`. 91 | 92 | ```shell 93 | $ sudo prlimit -n -p $(pgrep "casper-node$") 94 | RESOURCE DESCRIPTION SOFT HARD UNITS 95 | NOFILE max number of open files 1024 4096 files 96 | ``` 97 | 98 | If you receive `prlimit: option requires an argument -- 'p'` with the above command then `pgrep "casper-node$"` is not 99 | returning anything because `casper-node` is no longer running. 100 | 101 | ### Manual increase 102 | 103 | This is how you set `nofile` for an active process. It will make sure you don't have issues without having to 104 | restart the `casper-node-launcher` and your node's `casper-node` process. 105 | 106 | We run `sudo prlimit --nofile=64000 --pid=$(pgrep "casper-node$")`. 107 | 108 | After this when we look at `prlimit` it should show the change: 109 | 110 | ```shell 111 | $ sudo prlimit -n -p $(pgrep "casper-node$") 112 | RESOURCE DESCRIPTION SOFT HARD UNITS 113 | NOFILE max number of open files 64000 64000 files 114 | ``` 115 | 116 | This is only active while the `casper-node` process is active and therefore will not persist across server reboots, 117 | casper-node-launcher restarts, and protocol upgrades. We need to do something else to make this permanent. 118 | 119 | ### limits.conf 120 | 121 | Adding the `nofile` setting for `casper` user in `/etc/security/limits.conf` will persist this value. 122 | 123 | Add: 124 | 125 | `casper hard nofile 64000` 126 | 127 | to the bottom of `/etc/security/limits.conf`. 128 | 129 | After doing this you need to log out of any shells you have to enable this change. Restarting the node should 130 | maintain the correct `nofile` setting. 131 | 132 | ### systemd unit modification (bad alternative) 133 | 134 | When `casper-node-launcher` is installed, `/lib/systemd/system/casper-node-launcher.service` is created. 135 | Inside this file, a line is provided which will allow systemd to increase the `nofile` setting at launch. 136 | 137 | `#LimitNOFILE=64000` 138 | 139 | Editing this file with sudo will allow you to uncomment this line and save. After saving you would need to run 140 | `sudo systemctl daemon-reload` to reload your changes. Then you would need to restart `casper-node-launcher`. 141 | 142 | NOTE: The downside of using this method is that with upgrades to `casper-node-launcher`, the service file is replaced 143 | and the update would not be persistent. Editing `/etc/security/limits.conf` is a much preferred method. 144 | 145 | -------------------------------------------------------------------------------- /ci/build_docker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | N=casperlabs/casper-node-launcher 6 | C=${DRONE_COMMIT_SHA:-$(git rev-parse --short HEAD)} 7 | #git fetch -t 8 | V=$(git describe --tags --always) 9 | 10 | 11 | set -x 12 | docker build -t $N:$C . 13 | docker tag $N:$C $N:$V 14 | docker tag $N:$C $N:latest 15 | set +x 16 | -------------------------------------------------------------------------------- /ci/publish_deb_to_repo.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # Verify all variables are present 5 | if [[ -z $PLUGIN_GPG_KEY || -z $PLUGIN_GPG_PASS || -z $PLUGIN_REGION \ 6 | || -z $PLUGIN_REPO_NAME || -z $PLUGIN_ACL || -z $PLUGIN_PREFIX \ 7 | || -z $PLUGIN_DEB_PATH || -z $PLUGIN_OS_CODENAME ]]; then 8 | echo "ERROR: Environment Variable Missing!" 9 | exit 1 10 | fi 11 | 12 | # Verify if its the first time publishing. Will need to know later. 13 | # Probably an easier way to do this check :) 14 | EXISTS_RET=$(aws s3 ls s3://"$PLUGIN_REPO_NAME"/releases/dists/ --region "$PLUGIN_REGION" | grep "$PLUGIN_OS_CODENAME") || EXISTS_RET="false" 15 | 16 | # Sanity Check for later 17 | if [ "$EXISTS_RET" = "false" ]; then 18 | echo "First time uploading repo!" 19 | else 20 | echo "Repo Exists! Defaulting to publish update..." 21 | fi 22 | 23 | ### APTLY SECTION 24 | 25 | # Move old config file to use in jq query 26 | mv ~/.aptly.conf ~/.aptly.conf.orig 27 | 28 | # Inject ENV Variables and save as .aptly.conf 29 | jq --arg region "$PLUGIN_REGION" --arg bucket "$PLUGIN_REPO_NAME" --arg acl "$PLUGIN_ACL" --arg prefix "$PLUGIN_PREFIX" '.S3PublishEndpoints[$bucket] = {"region":$region, "bucket":$bucket, "acl": $acl, "prefix": $prefix}' ~/.aptly.conf.orig > ~/.aptly.conf 30 | 31 | # If aptly repo DOESNT exist locally already 32 | if [ ! "$(aptly repo list | grep $PLUGIN_OS_CODENAME)" ]; then 33 | aptly repo create -distribution="$PLUGIN_OS_CODENAME" -component=main "release-$PLUGIN_OS_CODENAME" 34 | fi 35 | 36 | # If aptly mirror DOESNT exist locally already 37 | if [ ! "$(aptly mirror list | grep $PLUGIN_OS_CODENAME)" ] && [ ! "$EXISTS_RET" = "false" ] ; then 38 | aptly mirror create -ignore-signatures "local-repo-$PLUGIN_OS_CODENAME" https://"${PLUGIN_REPO_NAME}"/"${PLUGIN_PREFIX}"/ "${PLUGIN_OS_CODENAME}" main 39 | fi 40 | 41 | # When it's not the first time uploading. 42 | if [ ! "$EXISTS_RET" = "false" ]; then 43 | aptly mirror update -ignore-signatures "local-repo-$PLUGIN_OS_CODENAME" 44 | # Found an article that said using 'Name' will select all packages for us 45 | aptly repo import "local-repo-$PLUGIN_OS_CODENAME" "release-$PLUGIN_OS_CODENAME" Name 46 | fi 47 | 48 | # Add .debs to the local repo 49 | aptly repo add -force-replace "release-$PLUGIN_OS_CODENAME" "$PLUGIN_DEB_PATH"/*.deb 50 | 51 | # Publish to S3 52 | if [ ! "$(aptly publish list | grep $PLUGIN_REPO_NAME | grep $PLUGIN_OS_CODENAME)" ]; then 53 | # If the repo is new 54 | aptly publish repo -batch -force-overwrite -passphrase="$PLUGIN_GPG_PASS" "release-$PLUGIN_OS_CODENAME" s3:"${PLUGIN_REPO_NAME}": 55 | else 56 | # If the repo exists 57 | aptly publish update -batch -force-overwrite -passphrase="$PLUGIN_GPG_PASS" "$PLUGIN_OS_CODENAME" s3:"${PLUGIN_REPO_NAME}": 58 | fi -------------------------------------------------------------------------------- /ci/push_docker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | N=casperlabs/casper-node-launcher 4 | C=${DRONE_COMMIT_SHA:-$(git rev-parse --short HEAD)} 5 | git fetch -t 6 | V=$(git describe --tags --always) 7 | 8 | builtin echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin 9 | 10 | set -x 11 | docker push $N:$V 12 | docker push $N:latest 13 | set +x 14 | -------------------------------------------------------------------------------- /resources/BIN_README.md: -------------------------------------------------------------------------------- 1 | This directory holds casper-node executables in semver folder using underscores. 2 | 3 | example: 4 | 5 | `casper-node` version `1.0.0` would exist as `./1_0_0/casper-node` -------------------------------------------------------------------------------- /resources/ETC_README.md: -------------------------------------------------------------------------------- 1 | # casper-node-launcher 2 | 3 | This package runs the casper-node software and handles changing execution to newer versions at 4 | determined times based on configuration. This allows simultaneous upgrading of all nodes on the 5 | network. 6 | 7 | Please refer to http://docs.casper.network for information on how to run a node. 8 | 9 | ## systemd 10 | 11 | The deb package installs casper-node-launcher service unit for systemd. If you are unfamiliar with systemd, 12 | the [Arch Linux page on systemd](https://wiki.archlinux.org/index.php/systemd) is a good intro into using it. 13 | 14 | Start the casper-node-launcher with: 15 | 16 | `sudo systemctl start casper-node-launcher` 17 | 18 | Show status of our system: 19 | 20 | `systemctl status casper-node-launcher` 21 | 22 | ### Reading logs 23 | 24 | Logs are created in `/var/log/casper/casper-node.log`. 25 | 26 | Log rotation is setup in `/etc/logrotate.d/casper-node`. 27 | 28 | Logs can be viewed with `sudo cat /var/log/casper/casper-node.log`. 29 | 30 | The logs are in 'json' format. 31 | 32 | ### Crash logs 33 | 34 | Teardown crash logs are created in '/var/log/casper/casper-node.stderr.log'. 35 | 36 | These use the same log rotation as `casper-node.log`. 37 | 38 | Crash logs can be viewed with `sudo cat /var/log/casper/casper-node.strerr.log`. 39 | 40 | 41 | ### Starting and stopping services 42 | 43 | To start service: 44 | 45 | `sudo systemctl start casper-node-launcher` 46 | 47 | Or use the node utility script: 48 | 49 | `sudo /etc/casper/node_util.py start` 50 | 51 | To stop: 52 | 53 | `sudo systemctl stop casper-node-launcher` 54 | 55 | Or use the node utility script: 56 | 57 | `sudo /etc/casper/node_util.py stop` 58 | 59 | ## Local Storage 60 | 61 | If you need to delete the db for a new run use: 62 | 63 | `sudo /etc/casper/node_util.py delete_local_state` 64 | 65 | ## Staging casper-node protocols 66 | 67 | Upgrading is done by staging a new casper-node and configuration prior to the agreed upgrade era. This is done with: 68 | 69 | `sudo -u casper /etc/casper/node_util.py stage_protocols [network config]` 70 | 71 | When the upgrade era occurs, the currently running casper-node will exit and casper-node-launcher will 72 | start the new upgraded version of casper-node. 73 | 74 | ## Bugs 75 | 76 | Please file any bugs as issues on the launcher at [the casper-node-launcher GitHub repo](https://github.com/casper-network/casper-node-launcher). 77 | Please file any bugs as issues on the node at [the casper-node GitHub repo](https://github.com/casper-network/casper-node). 78 | -------------------------------------------------------------------------------- /resources/VALIDATOR_KEYS_README.md: -------------------------------------------------------------------------------- 1 | ## Validator Keys 2 | 3 | Keys should be created by running (assuming you are in this directory for local path "."): 4 | 5 | `sudo -u casper casper-client keygen .` 6 | 7 | This will create `public_key_hex`, `public_key.pem`, and `secret_key.pem` owned by the casper user. 8 | 9 | Note there is no difference between validator keys and account keys. 10 | The `keygen` subcommand can be used to create account keys. There are also 11 | no explicit certificates to manage. Those needed are automatically generated by the system. 12 | 13 | To provide public key for genesis, please send in the results from the following: 14 | 15 | `cat public_key_hex; echo` 16 | 17 | Output should be similar to this: 18 | 19 | `015567f6e69418cbda61e88c4168b9219c259c36e0281d70aa6eee6607c9ceeba0` 20 | 21 | The keys will only need to be created once. These are not cleared out with uninstall of .deb or upgrading 22 | the .deb file. 23 | 24 | Note: the public_key_hex value has a leading `01` in addition to the hex value. This is for the `ed25519` 25 | algorithm used. By having a prefix byte for the hex, we can identify the algorithm for all three generated files. 26 | Additional supported algorithms will increment this leading byte. 27 | -------------------------------------------------------------------------------- /resources/maintainer_scripts/README.md: -------------------------------------------------------------------------------- 1 | This directory holds scripts and systemd files for packaging. -------------------------------------------------------------------------------- /resources/maintainer_scripts/casper_node_launcher/casper-node-launcher.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Casper Node Launcher 3 | Documentation=https://docs.casper.network 4 | After=network-online.target 5 | # Stop restarting after 3 failures in 15 seconds 6 | StartLimitBurst=3 7 | StartLimitIntervalSec=15 8 | 9 | [Service] 10 | Type=simple 11 | # Uncomment the below line to increase hard file limit from default 4096 12 | # Note: better to edit /etc/security/limits.conf (see README.md) 13 | #LimitNOFILE=64000 14 | # StandardOutput can only append log files from systemd version 240 + (Ubuntu 20.04). Use ExecStart with redirection as workaround. 15 | # separate stderr logging as it outputs non-JSON stack traces 16 | ExecStart=/bin/sh -c 'exec /usr/bin/casper-node-launcher 1>> /var/log/casper/casper-node.log 2>> /var/log/casper/casper-node.stderr.log' 17 | Restart=on-failure 18 | RestartSec=1 19 | User=casper 20 | Group=casper 21 | 22 | [Install] 23 | WantedBy=multi-user.target 24 | -------------------------------------------------------------------------------- /resources/maintainer_scripts/debian/postinst: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # Default Variables 5 | # --- 6 | DEFAULT_USERNAME="casper" 7 | DEFAULT_CONFIG_DIRECTORY="/etc/${DEFAULT_USERNAME}" 8 | DEFAULT_DATA_DIRECTORY="/var/lib/${DEFAULT_USERNAME}" 9 | DEFAULT_LOG_DIRECOTRY="/var/log/${DEFAULT_USERNAME}" 10 | 11 | # User Creation 12 | # --- 13 | # Assure DEFAULT_USERNAME user exists 14 | if id -u ${DEFAULT_USERNAME} >/dev/null 2>&1; then 15 | echo "User ${DEFAULT_USERNAME} already exists." 16 | else 17 | adduser --no-create-home --group --system ${DEFAULT_USERNAME} 18 | fi 19 | 20 | # Take ownership of directories and files installed 21 | chown -R ${DEFAULT_USERNAME}:${DEFAULT_USERNAME} ${DEFAULT_DATA_DIRECTORY} 22 | chown -R ${DEFAULT_USERNAME}:${DEFAULT_USERNAME} ${DEFAULT_CONFIG_DIRECTORY} 23 | chown -R ${DEFAULT_USERNAME}:${DEFAULT_USERNAME} ${DEFAULT_LOG_DIRECOTRY} 24 | 25 | # This is required for replacement to insert scripts for systemd by cargo-deb 26 | #DEBHELPER# -------------------------------------------------------------------------------- /resources/maintainer_scripts/debian/preinst: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # Default Variables 5 | # --- 6 | DEFAULT_USERNAME="casper" 7 | DEFAULT_CONFIG_DIRECTORY="/etc/${DEFAULT_USERNAME}" 8 | DEFAULT_DATA_DIRECTORY="/var/lib/${DEFAULT_USERNAME}/bin" 9 | DEFAULT_LOG_DIRECOTRY="/var/log/${DEFAULT_USERNAME}" 10 | 11 | # Creation of Files/Directories 12 | # --- 13 | # Assure DEFAULT_DATA_DIRECTORY is available for state data 14 | if [ -d ${DEFAULT_DATA_DIRECTORY} ] ; then 15 | echo "Directory ${DEFAULT_DATA_DIRECTORY} already exists." 16 | else 17 | mkdir -p ${DEFAULT_DATA_DIRECTORY} 18 | fi 19 | 20 | # Assure DEFAULT_CONFIG_DIRECTORY is available for config data 21 | if [ -d ${DEFAULT_CONFIG_DIRECTORY} ] ; then 22 | echo "Directory ${DEFAULT_CONFIG_DIRECTORY} already exists." 23 | else 24 | mkdir -p ${DEFAULT_CONFIG_DIRECTORY} 25 | fi 26 | 27 | # Assure DEFAULT_LOG_DIRECOTRY is available for logging 28 | if [ -d ${DEFAULT_LOG_DIRECOTRY} ] ; then 29 | echo "Directory ${DEFAULT_LOG_DIRECOTRY} already exists." 30 | else 31 | mkdir -p ${DEFAULT_LOG_DIRECOTRY} 32 | fi 33 | 34 | # This is required for replacement to insert scripts for systemd by cargo-deb 35 | #DEBHELPER# -------------------------------------------------------------------------------- /resources/maintainer_scripts/logrotate.d/casper-node: -------------------------------------------------------------------------------- 1 | /var/log/casper/casper-node*.log { 2 | create 0640 casper casper 3 | dateext 4 | dateformat .%Y-%m-%d-%s 5 | compress 6 | daily 7 | rotate 7 8 | maxsize 1G 9 | missingok 10 | notifempty 11 | copytruncate 12 | } -------------------------------------------------------------------------------- /resources/maintainer_scripts/network_configs/README.md: -------------------------------------------------------------------------------- 1 | # Network Configurations 2 | 3 | Network configurations should be of the format: 4 | ``` 5 | SOURCE_URL= 6 | NETWORK_NAME= 7 | ``` 8 | It is recommended that network_name used is the same as the .conf. This will be executed as 9 | `source "$DIR/network_configs/.conf` to load these variables. 10 | 11 | ## Usage 12 | 13 | These configurations will be sent to `node_util.py stage_protocols` as an argument. 14 | 15 | The target URL is expected to serve HTTP access to `///[bin.tar.gz|config.tar.gz]` 16 | Protocol versions to install is expected to exist in `//protocol_versions` as a protocol version per line. 17 | 18 | Example: 19 | `sudo -u casper /etc/casper/node_util.py stage_protocols casper.conf` 20 | 21 | With `casper.conf` of: 22 | ``` 23 | SOURCE_URL=genesis.casper.network 24 | NETWORK_NAME=casper 25 | ``` 26 | 27 | Will perform: 28 | 29 | Pull and parsing of `genesis.casper.network/casper/protocol_versions`. 30 | Then pulling and installing each protocol listed. 31 | 32 | If `protocol_versions` had: 33 | 34 | ``` 35 | 2_0_0 36 | ``` 37 | 38 | This would download: 39 | ``` 40 | curl -JLO genesis.casper.network/casper/2_0_0/bin.tar.gz 41 | curl -JLO genesis.casper.network/casper/2_0_0/config.tar.gz 42 | ``` 43 | `config.tar.gz` is decompressed into `/etc/casper/`. 44 | `bin.tar.gz` is decompressed into `/var/lib/casper/`. 45 | 46 | Then `/etc/casper/2_0_0/config.toml` is made from `config-example.toml` in the same directory. 47 | -------------------------------------------------------------------------------- /resources/maintainer_scripts/network_configs/casper-test.conf: -------------------------------------------------------------------------------- 1 | SOURCE_URL=genesis.casper.network 2 | NETWORK_NAME=casper-test 3 | -------------------------------------------------------------------------------- /resources/maintainer_scripts/network_configs/casper.conf: -------------------------------------------------------------------------------- 1 | SOURCE_URL=genesis.casper.network 2 | NETWORK_NAME=casper 3 | -------------------------------------------------------------------------------- /resources/maintainer_scripts/node_util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import ipaddress 3 | import shutil 4 | import sys 5 | from pathlib import Path 6 | from urllib import request 7 | import argparse 8 | import enum 9 | import getpass 10 | from ipaddress import ip_address 11 | import tarfile 12 | from collections import Counter 13 | from shutil import chown 14 | import os 15 | import json 16 | import time 17 | import glob 18 | import tempfile 19 | 20 | # protocol 1_0_0 should have accounts.toml 21 | # All other protocols should have chainspec.toml, config.toml and NOT accounts.toml 22 | # Protocols are shipped with config-example.toml to make config.toml 23 | 24 | 25 | class Status(enum.Enum): 26 | UNSTAGED = 1 27 | NO_CONFIG = 2 28 | BIN_ONLY = 3 29 | CONFIG_ONLY = 4 30 | STAGED = 5 31 | WRONG_NETWORK = 6 32 | 33 | 34 | class NodeUtil: 35 | """ 36 | Using non `_` and non uppercase methods to expose for external commands. 37 | Description of command comes from the doc string of method. 38 | """ 39 | CONFIG_PATH = Path("/etc/casper") 40 | BIN_PATH = Path("/var/lib/casper/bin") 41 | DB_PATH = Path("/var/lib/casper/casper-node") 42 | NET_CONFIG_PATH = CONFIG_PATH / "network_configs" 43 | PLATFORM_PATH = CONFIG_PATH / "PLATFORM" 44 | SCRIPT_NAME = "node_util.py" 45 | NODE_IP = "127.0.0.1" 46 | 47 | def __init__(self): 48 | self._network_name = None 49 | self._url = None 50 | self._bin_mode = None 51 | 52 | usage_docs = [f"{self.SCRIPT_NAME} [args]", "Available commands:"] 53 | commands = [] 54 | for function in [f for f in dir(self) if not f.startswith('_') and f[0].islower()]: 55 | try: 56 | usage_docs.append(f" {function} - {getattr(self, function).__doc__.strip()}") 57 | except AttributeError: 58 | raise Exception(f"Error creating usage docs, expecting {function} to be root function and have doc comment." 59 | f" Lead with underscore if not.") 60 | commands.append(function) 61 | usage_docs.append(" ") 62 | 63 | self._external_ip = None 64 | 65 | parser = argparse.ArgumentParser( 66 | description="Utility to help configure casper-node versions and troubleshoot.", 67 | usage="\n".join(usage_docs)) 68 | parser.add_argument("command", help="Subcommand to run.", choices=commands) 69 | args = parser.parse_args(sys.argv[1:2]) 70 | getattr(self, args.command)() 71 | 72 | @staticmethod 73 | def _rpc_call(method: str, server: str, params: list, port: int = 7777, timeout: int = 5): 74 | url = f"http://{server}:{port}/rpc" 75 | req = request.Request(url, method="POST") 76 | req.add_header('content-type', "application/json") 77 | req.add_header('cache-control', "no-cache") 78 | payload = json.dumps({"jsonrpc": "2.0", "method": method, "params": params, "id": 1}).encode('utf-8') 79 | r = request.urlopen(req, payload, timeout=timeout) 80 | json_data = json.loads(r.read()) 81 | return json_data["result"] 82 | 83 | @staticmethod 84 | def _rpc_get_block(server: str, block_height=None, port: int = 7777, timeout: int = 5): 85 | """ 86 | Get block based on block_hash, block_height, or last block if block_identifier is missing 87 | """ 88 | params = [] 89 | if block_height: 90 | params = [{"Height": int(block_height)}] 91 | return NodeUtil._rpc_call("chain_get_block", server, params, port) 92 | 93 | @staticmethod 94 | def _get_platform(): 95 | """ Support old default debian and then newer platforms with PLATFORM files """ 96 | if NodeUtil.PLATFORM_PATH.exists(): 97 | return NodeUtil.PLATFORM_PATH.read_text().strip() 98 | else: 99 | return "deb" 100 | 101 | def _load_config_values(self, config): 102 | """ 103 | Parses config file to get values 104 | 105 | :param file_name: network config filename 106 | """ 107 | source_url = "SOURCE_URL" 108 | network_name = "NETWORK_NAME" 109 | bin_mode = "BIN_MODE" 110 | 111 | file_path = NodeUtil.NET_CONFIG_PATH / config 112 | expected_keys = (source_url, network_name) 113 | config = {} 114 | for line in file_path.read_text().splitlines(): 115 | if line.strip(): 116 | key, value = line.strip().split('=') 117 | config[key] = value 118 | for key in expected_keys: 119 | if key not in config.keys(): 120 | print(f"Expected config value not found: {key} in {file_path}") 121 | exit(1) 122 | self._url = config[source_url] 123 | self._network_name = config[network_name] 124 | self._bin_mode = config.get(bin_mode, "mainnet") 125 | 126 | def _get_protocols(self): 127 | """ Downloads protocol versions for network """ 128 | full_url = f"{self._network_url}/protocol_versions" 129 | r = request.urlopen(full_url) 130 | if r.status != 200: 131 | raise IOError(f"Expected status 200 requesting {full_url}, received {r.status}") 132 | pv = r.read().decode('utf-8') 133 | return [data.strip() for data in pv.splitlines()] 134 | 135 | @staticmethod 136 | def _verify_casper_user(): 137 | if getpass.getuser() != "casper": 138 | print(f"Run with 'sudo -u casper'") 139 | exit(1) 140 | 141 | @staticmethod 142 | def _verify_root_user(): 143 | if getpass.getuser() != "root": 144 | print("Run with 'sudo'") 145 | exit(1) 146 | 147 | @staticmethod 148 | def _status_text(status): 149 | status_display = {Status.UNSTAGED: "Protocol Unstaged", 150 | Status.NO_CONFIG: "No config.toml for Protocol", 151 | Status.BIN_ONLY: "Only bin is staged for Protocol, no config", 152 | Status.CONFIG_ONLY: "Only config is staged for Protocol, no bin", 153 | Status.WRONG_NETWORK: "chainspec.toml is for wrong network", 154 | Status.STAGED: "Protocol Staged"} 155 | return status_display[status] 156 | 157 | def _check_staged_version(self, version): 158 | """ 159 | Checks completeness of staged protocol version 160 | 161 | :param version: protocol version in underscore format such as 1_0_0 162 | :return: Status enum 163 | """ 164 | if not self._network_name: 165 | print("Config not parsed prior to call of _check_staged_version and self._network_name is not populated.") 166 | exit(1) 167 | config_version_path = NodeUtil.CONFIG_PATH / version 168 | chainspec_toml_file_path = config_version_path / "chainspec.toml" 169 | config_toml_file_path = config_version_path / "config.toml" 170 | bin_version_path = NodeUtil.BIN_PATH / version / "casper-node" 171 | if not config_version_path.exists(): 172 | if not bin_version_path.exists(): 173 | return Status.UNSTAGED 174 | return Status.BIN_ONLY 175 | else: 176 | if not bin_version_path.exists(): 177 | return Status.CONFIG_ONLY 178 | if not config_toml_file_path.exists(): 179 | return Status.NO_CONFIG 180 | if NodeUtil._chainspec_name(chainspec_toml_file_path) != self._network_name: 181 | return Status.WRONG_NETWORK 182 | return Status.STAGED 183 | 184 | @staticmethod 185 | def _download_file(url, target_path): 186 | print(f"Downloading {url} to {target_path}") 187 | r = request.urlopen(url) 188 | if r.status != 200: 189 | raise IOError(f"Expected status 200 requesting {url}, received {r.status}") 190 | with open(target_path, 'wb') as f: 191 | f.write(r.read()) 192 | 193 | @staticmethod 194 | def _extract_tar_gz(source_file_path, target_path): 195 | print(f"Extracting {source_file_path} to {target_path}") 196 | with tarfile.TarFile.open(source_file_path) as tf: 197 | for member in tf.getmembers(): 198 | tf.extract(member, target_path) 199 | 200 | @property 201 | def _network_url(self): 202 | return f"http://{self._url}/{self._network_name}" 203 | 204 | def _pull_protocol_version(self, protocol_version, platform="deb"): 205 | self._verify_casper_user() 206 | 207 | if not NodeUtil.BIN_PATH.exists(): 208 | print(f"Error: expected bin file location {NodeUtil.BIN_PATH} not found.") 209 | exit(1) 210 | 211 | if not NodeUtil.CONFIG_PATH.exists(): 212 | print(f"Error: expected config file location {NodeUtil.CONFIG_PATH} not found.") 213 | exit(1) 214 | 215 | bin_file = "bin.tar.gz" 216 | config_file = "config.tar.gz" 217 | 218 | etc_full_path = NodeUtil.CONFIG_PATH / protocol_version 219 | bin_full_path = NodeUtil.BIN_PATH / protocol_version 220 | base_url = f"{self._network_url}/{protocol_version}" 221 | config_url = f"{base_url}/{config_file}" 222 | bin_url = f"{base_url}/{bin_file}" 223 | 224 | if etc_full_path.exists(): 225 | print(f"Error: config version path {etc_full_path} already exists. Aborting.") 226 | exit(1) 227 | if bin_full_path.exists(): 228 | print(f"Error: bin version path {bin_full_path} already exists. Aborting.") 229 | exit(1) 230 | 231 | config_archive_path = NodeUtil.CONFIG_PATH / config_file 232 | self._download_file(config_url, config_archive_path) 233 | self._extract_tar_gz(config_archive_path, etc_full_path) 234 | print(f"Deleting {config_archive_path}") 235 | config_archive_path.unlink() 236 | 237 | bin_archive_path = NodeUtil.BIN_PATH / bin_file 238 | self._download_file(bin_url, bin_archive_path) 239 | self._extract_tar_gz(bin_archive_path, bin_full_path) 240 | print(f"Deleting {bin_archive_path}") 241 | bin_archive_path.unlink() 242 | return True 243 | 244 | def _get_external_ip(self): 245 | """ Query multiple sources to get external IP of node """ 246 | if self._external_ip: 247 | return self._external_ip 248 | services = (("https://checkip.amazonaws.com", "amazonaws.com"), 249 | ("https://4.icanhazip.com/", "icanhazip.com"), 250 | ("https://4.ident.me", "ident.me")) 251 | ips = [] 252 | # Using our own PoolManager for shorter timeouts 253 | print("Querying your external IP...") 254 | for url, service in services: 255 | r = request.urlopen(url) 256 | if r.status != 200: 257 | ip = "" 258 | else: 259 | ip = r.read().decode('utf-8').strip() 260 | print(f" {service} says '{ip}' with Status: {r.status}") 261 | if ip: 262 | ips.append(ip) 263 | if ips: 264 | ip_addr = Counter(ips).most_common(1)[0][0] 265 | if self._is_valid_ip(ip_addr): 266 | self._external_ip = ip_addr 267 | return ip_addr 268 | return None 269 | 270 | @staticmethod 271 | def _is_valid_ip(ip): 272 | """ Check validity of ip address """ 273 | try: 274 | _ = ipaddress.IPv4Network(ip) 275 | except ValueError: 276 | return False 277 | else: 278 | return True 279 | 280 | @staticmethod 281 | def _toml_header(line_data): 282 | data = line_data.strip() 283 | length = len(data) 284 | if data[0] == '[' and data[length - 1] == ']': 285 | return data[1:length - 1] 286 | return None 287 | 288 | @staticmethod 289 | def _toml_name_value(line_data): 290 | data = line_data.strip().split(' = ') 291 | if len(data) != 2: 292 | raise ValueError(f"Expected `name = value` with _toml_name_value for {line_data}") 293 | return data 294 | 295 | @staticmethod 296 | def _is_toml_comment_or_empty(line_data): 297 | data = line_data.strip() 298 | if len(data) == 0: 299 | return True 300 | if data[0] == '#': 301 | return True 302 | return False 303 | 304 | @staticmethod 305 | def _replace_config_values(config_data, replace_file): 306 | """ Replace values in config_data with values for fields in replace_file """ 307 | replace_file_path = Path(replace_file) 308 | if not replace_file_path.exists(): 309 | raise ValueError(f"Cannot replace values in config, {replace_file} does not exist.") 310 | replace_data = replace_file_path.read_text().splitlines() 311 | replacements = [] 312 | last_header = None 313 | for line in replace_data: 314 | if NodeUtil._is_toml_comment_or_empty(line): 315 | continue 316 | header = NodeUtil._toml_header(line) 317 | if header is not None: 318 | last_header = header 319 | continue 320 | name, value = NodeUtil._toml_name_value(line) 321 | replacements.append((last_header, name, value)) 322 | new_output = [] 323 | last_header = None 324 | for line in config_data.splitlines(): 325 | if NodeUtil._is_toml_comment_or_empty(line): 326 | new_output.append(line) 327 | continue 328 | header = NodeUtil._toml_header(line) 329 | if header is not None: 330 | last_header = header 331 | new_output.append(line) 332 | continue 333 | name, value = NodeUtil._toml_name_value(line) 334 | replacement_value = [r_value for r_header, r_name, r_value in replacements 335 | if last_header == r_header and name == r_name] 336 | if replacement_value: 337 | new_value = replacement_value[0] 338 | print(f"Replacing {last_header}:{name} = {value} with {new_value}") 339 | new_output.append(f"{name} = {new_value}") 340 | else: 341 | new_output.append(line) 342 | # Make trailing new line 343 | new_output.append("") 344 | return "\n".join(new_output) 345 | 346 | def _config_from_example(self, protocol_version, ip=None, replace_toml=None): 347 | """ 348 | Internal Method to allow use in larger actions or direct call from config_from_example. 349 | Create config.toml or config.toml.new (if previous exists) from config-example.toml 350 | """ 351 | self._verify_casper_user() 352 | 353 | config_path = NodeUtil.CONFIG_PATH / protocol_version 354 | config_toml_path = config_path / "config.toml" 355 | config_example = config_path / "config-example.toml" 356 | config_toml_new_path = config_path / "config.toml.new" 357 | 358 | if not config_example.exists(): 359 | print(f"Error: {config_example} not found.") 360 | exit(1) 361 | 362 | if ip is None: 363 | ip = self._get_external_ip() 364 | print(f"Using detected ip: {ip}") 365 | else: 366 | print(f"Using provided ip: {ip}") 367 | 368 | if not self._is_valid_ip(ip): 369 | print(f"Error: Invalid IP: {ip}") 370 | exit(1) 371 | 372 | outfile = config_toml_path 373 | if config_toml_path.exists(): 374 | outfile = config_toml_new_path 375 | print(f"Previous {config_toml_path} exists, creating as {outfile} from {config_example}.") 376 | print(f"Replace {config_toml_path} with {outfile} to use the automatically generated configuration.") 377 | 378 | config_text = config_example.read_text() 379 | if replace_toml is not None: 380 | config_text = NodeUtil._replace_config_values(config_text, replace_toml) 381 | 382 | outfile.write_text(config_text.replace("", ip)) 383 | 384 | return True 385 | 386 | def config_from_example(self): 387 | """ Create config.toml from config-example.toml. (use 'sudo -u casper') """ 388 | parser = argparse.ArgumentParser(description=self.config_from_example.__doc__, 389 | usage=(f"{self.SCRIPT_NAME} config_from_example [-h] " 390 | "protocol_version [--replace replace_file.toml] [--ip IP]")) 391 | parser.add_argument("protocol_version", type=str, help=f"protocol version to create under") 392 | parser.add_argument("--ip", 393 | type=ip_address, 394 | help=f"optional ip to use for config.toml instead of detected ip.", 395 | required=False) 396 | parser.add_argument("--replace", 397 | type=str, 398 | help=("optional toml file that holds replacements to make to config.toml " 399 | "from config-example.toml"), 400 | required=False) 401 | args = parser.parse_args(sys.argv[2:]) 402 | ip = str(args.ip) if args.ip else None 403 | self._config_from_example(args.protocol_version, ip, args.replace) 404 | 405 | def stage_protocols(self): 406 | """Stage available protocols if needed (use 'sudo -u casper')""" 407 | parser = argparse.ArgumentParser(description=self.stage_protocols.__doc__, 408 | usage=(f"{self.SCRIPT_NAME} stage_protocols [-h] config " 409 | "[--ip IP] [--replace toml_file]")) 410 | parser.add_argument("config", type=str, help=f"name of config file to use from {NodeUtil.NET_CONFIG_PATH}") 411 | parser.add_argument("--ip", 412 | type=ip_address, 413 | help=f"optional ip to use for config.toml instead of detected ip.", 414 | required=False) 415 | parser.add_argument("--replace", 416 | type=str, 417 | help=("optional toml file that holds replacements to make to config.toml " 418 | "from config-example.toml"), 419 | required=False) 420 | args = parser.parse_args(sys.argv[2:]) 421 | self._load_config_values(args.config) 422 | 423 | self._verify_casper_user() 424 | platform = self._get_platform() 425 | exit_code = 0 426 | for pv in self._get_protocols(): 427 | status = self._check_staged_version(pv) 428 | if status == Status.STAGED: 429 | print(f"{pv}: {self._status_text(status)}") 430 | continue 431 | elif status in (Status.BIN_ONLY, Status.CONFIG_ONLY): 432 | print(f"{pv}: {self._status_text(status)} - Not automatically recoverable.") 433 | exit_code = 1 434 | continue 435 | if status == Status.UNSTAGED: 436 | print(f"Pulling protocol for {pv}.") 437 | if not self._pull_protocol_version(pv, platform): 438 | exit_code = 1 439 | if status in (Status.UNSTAGED, Status.NO_CONFIG): 440 | print(f"Creating config for {pv}.") 441 | ip = str(args.ip) if args.ip else None 442 | if not self._config_from_example(pv, ip, args.replace): 443 | exit_code = 1 444 | exit(exit_code) 445 | 446 | def check_protocols(self): 447 | """ Checks if protocol are fully installed """ 448 | parser = argparse.ArgumentParser(description=self.check_protocols.__doc__, 449 | usage=f"{self.SCRIPT_NAME} check_protocols [-h] config ") 450 | parser.add_argument("config", type=str, help=f"name of config file to use from {NodeUtil.NET_CONFIG_PATH}") 451 | args = parser.parse_args(sys.argv[2:]) 452 | self._load_config_values(args.config) 453 | 454 | exit_code = 0 455 | for pv in self._get_protocols(): 456 | status = self._check_staged_version(pv) 457 | if status != Status.STAGED: 458 | exit_code = 1 459 | print(f"{pv}: {self._status_text(status)}") 460 | exit(exit_code) 461 | 462 | def check_for_upgrade(self): 463 | """ Checks if last protocol is staged """ 464 | parser = argparse.ArgumentParser(description=self.check_for_upgrade.__doc__, 465 | usage=f"{self.SCRIPT_NAME} check_for_upgrade [-h] config ") 466 | parser.add_argument("config", type=str, help=f"name of config file to use from {NodeUtil.NET_CONFIG_PATH}") 467 | args = parser.parse_args(sys.argv[2:]) 468 | self._load_config_values(args.config) 469 | last_protocol = self._get_protocols()[-1] 470 | status = self._check_staged_version(last_protocol) 471 | if status == Status.UNSTAGED: 472 | print(f"{last_protocol}: {self._status_text(status)}") 473 | exit(1) 474 | exit(0) 475 | 476 | @staticmethod 477 | def _is_casper_owned(path) -> bool: 478 | return path.owner() == 'casper' and path.group() == 'casper' 479 | 480 | @staticmethod 481 | def _walk_file_locations(): 482 | for path in NodeUtil.BIN_PATH, NodeUtil.CONFIG_PATH, NodeUtil.DB_PATH: 483 | try: 484 | for _path in NodeUtil._walk_path(path): 485 | yield _path 486 | except FileNotFoundError: 487 | pass 488 | 489 | @staticmethod 490 | def _walk_path(path, include_dir=True): 491 | for p in Path(path).iterdir(): 492 | if p.is_dir(): 493 | if include_dir: 494 | yield p.resolve() 495 | yield from NodeUtil._walk_path(p) 496 | continue 497 | yield p.resolve() 498 | 499 | def check_permissions(self): 500 | """ Checking files are owned by casper. """ 501 | # If a user runs commands under root, it can give files non casper ownership and cause problems. 502 | exit_code = 0 503 | for path in self._walk_file_locations(): 504 | if not self._is_casper_owned(path): 505 | print(f"{path} is owned by {path.owner()}:{path.group()}") 506 | exit_code = 1 507 | if exit_code == 0: 508 | print("Permissions are correct.") 509 | exit(exit_code) 510 | 511 | def fix_permissions(self): 512 | """ Sets all files owner to casper (use 'sudo') """ 513 | self._verify_root_user() 514 | 515 | exit_code = 0 516 | for path in self._walk_file_locations(): 517 | if not self._is_casper_owned(path): 518 | print(f"Correcting ownership of {path}") 519 | chown(path, 'casper', 'casper') 520 | if not self._is_casper_owned(path): 521 | print(f"Ownership set failed.") 522 | exit_code = 1 523 | exit(exit_code) 524 | 525 | def rotate_logs(self): 526 | """ Rotate the logs for casper-node (use 'sudo') """ 527 | self._verify_root_user() 528 | os.system("logrotate -f /etc/logrotate.d/casper-node") 529 | 530 | def restart(self): 531 | """ Restart casper-node-launcher (use 'sudo) """ 532 | # Using stop, pause, start to get full reload not done with systemctl restart 533 | self.stop() 534 | time.sleep(1) 535 | self.start() 536 | 537 | def stop(self): 538 | """ Stop casper-node-launcher (use 'sudo') """ 539 | self._verify_root_user() 540 | os.system("systemctl stop casper-node-launcher") 541 | 542 | def start(self): 543 | """ Start casper-node-launcher (use 'sudo') """ 544 | self._verify_root_user() 545 | os.system("systemctl start casper-node-launcher") 546 | 547 | @staticmethod 548 | def systemd_status(): 549 | """ Status of casper-node-launcher """ 550 | # using op.popen to stop hanging return to terminate 551 | result = os.popen("systemctl status casper-node-launcher") 552 | print(result.read()) 553 | 554 | def delete_local_state(self): 555 | """ Delete local db and status files. (use 'sudo') """ 556 | parser = argparse.ArgumentParser(description=self.delete_local_state.__doc__, 557 | usage=f"{self.SCRIPT_NAME} delete_local_state [-h] --verify-delete-all") 558 | parser.add_argument("--verify_delete_all", 559 | action='store_true', 560 | help="Required for verification that you want to delete everything", 561 | required=False) 562 | args = parser.parse_args(sys.argv[2:]) 563 | self._verify_root_user() 564 | 565 | if not args.verify_delete_all: 566 | print(f"Include '--verify_delete_all' flag to confirm. Exiting.") 567 | exit(1) 568 | 569 | # missing_ok=True arg to unlink only 3.8+, using try/catch. 570 | self._delete_directory(self.DB_PATH) 571 | cnl_state = self.CONFIG_PATH / "casper-node-launcher-state.toml" 572 | try: 573 | cnl_state.unlink() 574 | except FileNotFoundError: 575 | pass 576 | 577 | @staticmethod 578 | def _delete_directory(dir_path, remove_parent=False): 579 | # missing_ok=True arg to unlink only 3.8+, using try/catch. 580 | try: 581 | for path in dir_path.glob('*'): 582 | try: 583 | if path.is_dir(): 584 | shutil.rmtree(path) 585 | else: 586 | path.unlink() 587 | except FileNotFoundError: 588 | pass 589 | if remove_parent: 590 | shutil.rmtree(dir_path) 591 | except FileNotFoundError: 592 | pass 593 | 594 | def force_run_version(self): 595 | """ Force casper-node-launcher to start at a certain protocol version """ 596 | parser = argparse.ArgumentParser(description=self.force_run_version.__doc__, 597 | usage=f"{self.SCRIPT_NAME} force_run_version [-h] protocol_version") 598 | parser.add_argument("protocol_version", help="Protocol version for casper-node-launcher to run.") 599 | args = parser.parse_args(sys.argv[2:]) 600 | version = args.protocol_version 601 | config_path = self.CONFIG_PATH / version 602 | bin_path = self.BIN_PATH / version 603 | if not config_path.exists(): 604 | print(f"/etc/casper/{version} not found. Aborting.") 605 | exit(1) 606 | if not bin_path.exists(): 607 | print(f"/var/lib/casper/bin/{version} not found. Aborting.") 608 | exit(1) 609 | # Need to be root to restart below 610 | self._verify_root_user() 611 | state_path = self.CONFIG_PATH / "casper-node-launcher-state.toml" 612 | lines = ["mode = 'RunNodeAsValidator'", 613 | f"version = '{version.replace('_','.')}'", 614 | f"binary_path = '/var/lib/casper/bin/{version}/casper-node'", 615 | f"config_path = '/etc/casper/{version}/config.toml'"] 616 | state_path.write_text("\n".join(lines)) 617 | # Make file casper:casper owned 618 | import pwd 619 | user = pwd.getpwnam('casper') 620 | os.chown(state_path, user.pw_uid, user.pw_gid) 621 | self.restart() 622 | 623 | def unstage_protocol(self): 624 | """ Unstage (delete) a certain protocol version """ 625 | parser = argparse.ArgumentParser(description=self.force_run_version.__doc__, 626 | usage=f"{self.SCRIPT_NAME} unstage_protocol [-h] protocol_version") 627 | parser.add_argument("protocol_version", help="Protocol version for casper-node-launcher to run.") 628 | parser.add_argument("--verify_delete", 629 | action='store_true', 630 | help="Required for verification that you want to delete protocol", 631 | required=False) 632 | args = parser.parse_args(sys.argv[2:]) 633 | version = args.protocol_version 634 | config_path = self.CONFIG_PATH / version 635 | bin_path = self.BIN_PATH / version 636 | if not config_path.exists() and not bin_path.exists(): 637 | print(f"{config_path} and {bin_path} not found. Aborting.") 638 | exit(1) 639 | 640 | if not args.verify_delete: 641 | print(f"Include '--verify_delete' flag to confirm deleting protocol. Exiting.") 642 | exit(1) 643 | # Need to be root to delete 644 | self._verify_root_user() 645 | 646 | print(f"Deleting {config_path}...") 647 | self._delete_directory(config_path, True) 648 | print(f"Deleting {bin_path}...") 649 | self._delete_directory(bin_path, True) 650 | 651 | @staticmethod 652 | def _ip_address_type(ip_address: str): 653 | """ Validation method for argparse """ 654 | try: 655 | ip = ipaddress.ip_address(ip_address) 656 | except ValueError: 657 | print(f"Error: Invalid IP: {ip_address}") 658 | else: 659 | return str(ip) 660 | 661 | @staticmethod 662 | def _get_status(ip=None, port=8888): 663 | """ Get status data from node """ 664 | if ip is None: 665 | ip = NodeUtil.NODE_IP 666 | full_url = f"http://{ip}:{port}/status" 667 | r = request.urlopen(full_url, timeout=5) 668 | return json.loads(r.read().decode('utf-8')) 669 | 670 | @staticmethod 671 | def _chainspec_name(chainspec_path) -> str: 672 | # Hack to not require toml package install 673 | for line in chainspec_path.read_text().splitlines(): 674 | NAME_DATA = "name = '" 675 | if line[:len(NAME_DATA)] == NAME_DATA: 676 | return line.split(NAME_DATA)[1].split("'")[0] 677 | 678 | @staticmethod 679 | def _format_status(status, external_block_data=None): 680 | try: 681 | if status is None: 682 | status = {} 683 | error = status.get("error") 684 | if error: 685 | return f"status error: {error}" 686 | is_new_status = status.get("available_block_range") is not None 687 | block_info = status.get("last_added_block_info") 688 | output = [] 689 | if block_info is not None: 690 | cur_block = block_info.get('height') 691 | output.append(f"Last Block: {cur_block} (Era: {block_info.get('era_id')})") 692 | if external_block_data is not None: 693 | if len(external_block_data) > 1: 694 | output.append(f" Tip Block: {external_block_data[0]} (Era: {external_block_data[1]})") 695 | output.append(f" Behind: {external_block_data[0] - cur_block}") 696 | output.append("") 697 | output.extend([ 698 | f"Peer Count: {len(status.get('peers', []))}", 699 | f"Uptime: {status.get('uptime', '')}", 700 | f"Build: {status.get('build_version')}", 701 | f"Key: {status.get('our_public_signing_key')}", 702 | f"Next Upgrade: {status.get('next_upgrade')}", 703 | "" 704 | ]) 705 | if is_new_status: 706 | output.append(f"Reactor State: {status.get('reactor_state', '')}") 707 | abr = status.get("available_block_range", {"low": "", "high": ""}) 708 | output.append(f"Available Block Range - Low: {abr.get('low')} High: {abr.get('high')}") 709 | output.append("") 710 | 711 | return "\n".join(output) 712 | except Exception: 713 | return "Cannot parse status return." 714 | 715 | @staticmethod 716 | def _ip_status_height(ip): 717 | try: 718 | status = NodeUtil._get_status(ip=ip) 719 | block_info = status.get("last_added_block_info") 720 | if block_info is None: 721 | return None 722 | return block_info.get('height'), block_info.get('era_id') 723 | except Exception: 724 | return None 725 | 726 | def node_status(self): 727 | """ Get full status of node """ 728 | 729 | parser = argparse.ArgumentParser(description=self.watch.__doc__, 730 | usage=f"{self.SCRIPT_NAME} node_status [-h] [--ip]") 731 | parser.add_argument("--ip", help="ip address of a node at the tip", 732 | type=self._ip_address_type, required=False) 733 | args = parser.parse_args(sys.argv[2:]) 734 | 735 | try: 736 | status = self._get_status() 737 | except Exception as e: 738 | status = {"error": e} 739 | external_block_data = None 740 | if args.ip: 741 | external_block_data = self._ip_status_height(str(args.ip)) 742 | print(self._format_status(status, external_block_data)) 743 | 744 | def watch(self): 745 | """ watch full_node_status """ 746 | DEFAULT = 5 747 | MINIMUM = 5 748 | 749 | parser = argparse.ArgumentParser(description=self.watch.__doc__, 750 | usage=f"{self.SCRIPT_NAME} watch [-h] [-r] [--ip]") 751 | parser.add_argument("-r", "--refresh", help="Refresh time in secs", type=int, default=DEFAULT, required=False) 752 | parser.add_argument("--ip", help="ip address of a node at the tip", 753 | type=self._ip_address_type, required=False) 754 | args = parser.parse_args(sys.argv[2:]) 755 | ip_arg = "" 756 | if args.ip: 757 | ip_arg = f"--ip {str(args.ip)}" 758 | refresh = MINIMUM if args.refresh < MINIMUM else args.refresh 759 | os.system(f"watch -n {refresh} '{sys.argv[0]} node_status {ip_arg}; {sys.argv[0]} systemd_status'") 760 | 761 | def rpc_active(self): 762 | """ Is local RPC active? """ 763 | try: 764 | block = self._rpc_get_block("127.0.0.1", timeout=1) 765 | print("RPC: Ready\n") 766 | exit(0) 767 | except Exception: 768 | print("RPC: Not Ready\n") 769 | exit(1) 770 | 771 | def shift_ports(self): 772 | """ Change ports in config.toml files to allow use of reverse proxy """ 773 | parser = argparse.ArgumentParser(description=self.shift_ports.__doc__, 774 | usage=f"{self.SCRIPT_NAME} shift_ports [--rpc] [--rest] [--sse]") 775 | parser.add_argument("--rpc", 776 | help="Port to use for RPC", 777 | type=int, 778 | required=False, 779 | default=7770) 780 | parser.add_argument("--rest", 781 | help="Port to use for REST", 782 | type=int, 783 | required=False, 784 | default=8880) 785 | parser.add_argument("--sse", 786 | help="Port to use for SSE", 787 | type=int, 788 | required=False, 789 | default=9990) 790 | 791 | args = parser.parse_args(sys.argv[2:]) 792 | 793 | self._verify_root_user() 794 | 795 | for config_file in glob.glob("/etc/casper/*/config.toml"): 796 | with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmp_file: 797 | with open(config_file) as src_file: 798 | for line in src_file: 799 | if "address = '0.0.0.0:7777'" in line: 800 | print(f"Changing RPC (7777) to port {args.rpc}") 801 | tmp_file.write(f"address = '0.0.0.0:{args.rpc}'\n") 802 | elif "address = '0.0.0.0:8888'" in line: 803 | print(f"Changing REST (8888) to port {args.rest}") 804 | tmp_file.write(f"address = '0.0.0.0:{args.rest}'\n") 805 | elif "address = '0.0.0.0:9999'" in line: 806 | print(f"Changing SSE (9999) to port {args.sse}") 807 | tmp_file.write(f"address = '0.0.0.0:{args.sse}'\n") 808 | else: 809 | tmp_file.write(line) 810 | print(f"Replacing {config_file}") 811 | # Preserve old permissions to new file before replacement 812 | shutil.copystat(config_file, tmp_file.name) 813 | shutil.move(tmp_file.name, config_file) 814 | 815 | def get_trusted_hash(self): 816 | """ Retrieve trusted hash from given node ip while verifying network """ 817 | parser = argparse.ArgumentParser(description=self.get_trusted_hash.__doc__, 818 | usage=f"{self.SCRIPT_NAME} get_trusted_hash ip [--protocol] [--block]") 819 | parser.add_argument("ip", 820 | help="Trusted Node IP address ipv4 format", 821 | type=self._ip_address_type) 822 | parser.add_argument("--protocol", 823 | help="Protocol version for chainspec to verify same network", 824 | required=False, 825 | default="1_0_0") 826 | 827 | args = parser.parse_args(sys.argv[2:]) 828 | 829 | status = None 830 | try: 831 | status = self._get_status(args.ip) 832 | except Exception as e: 833 | print(f"Error retrieving status from {args.ip}: {e}") 834 | exit(1) 835 | 836 | remote_network_name = status["chainspec_name"] 837 | chainspec_path = Path("/etc/casper") / args.protocol / "chainspec.toml" 838 | 839 | if not chainspec_path.exists(): 840 | print(f"Cannot find {chainspec_path}, specify valid protocol folder to verify network name.") 841 | exit(1) 842 | 843 | chainspec_name = self._chainspec_name(chainspec_path) 844 | if chainspec_name != remote_network_name: 845 | print(f"Node network name: '{remote_network_name}' does not match {chainspec_path}: '{chainspec_name}'") 846 | exit(1) 847 | 848 | last_added_block_info = status["last_added_block_info"] 849 | if last_added_block_info is None: 850 | print(f"No last_added_block_info in {args.ip} status. Node is not in sync and will not be used.") 851 | exit(1) 852 | block_hash = last_added_block_info["hash"] 853 | print(f"{block_hash}") 854 | 855 | def get_ip(self): 856 | """ Get external IP of node. Can be used to test code used for automatically filling IP 857 | or to check if you need to update the IP in your config.toml file. """ 858 | print(self._get_external_ip()) 859 | 860 | def node_log(self): 861 | """ Get nodes current logs. Same as 'cat /var/log/casper-node.log` with grep added if using arguments 862 | and tail if following. """ 863 | parser = argparse.ArgumentParser(description=self.node_log.__doc__, 864 | usage=f"{self.SCRIPT_NAME} node_log [--err] [--warn] [--follow]") 865 | parser.add_argument("--err", 866 | help="Return log lines with level ERR", 867 | action='store_true', 868 | required=False) 869 | parser.add_argument("--warn", 870 | help="Return log lines with level WARN", 871 | action='store_true', 872 | required=False) 873 | parser.add_argument("--follow", 874 | help="Follow log file as lines are added", 875 | action='store_true', 876 | required=False) 877 | args = parser.parse_args(sys.argv[2:]) 878 | 879 | grep_args = "" 880 | grep_args += " -e '\"level\":\"ERR\"'" if args.err else "" 881 | grep_args += " -e '\"level\":\"WARN\"'" if args.warn else "" 882 | 883 | grep = f" | grep {grep_args}" if len(grep_args) > 0 else "" 884 | 885 | main_cmd = "tail -f" if args.follow else "cat" 886 | 887 | os.system(f"{main_cmd} /var/log/casper/casper-node.log {grep}") 888 | 889 | @staticmethod 890 | def node_error_log(): 891 | """ Get nodes current teardown error log. Same as 'cat /var/log/casper-node.stderr.log` """ 892 | os.system("cat /var/log/casper/casper-node.stderr.log") 893 | 894 | @staticmethod 895 | def sidecar_status(): 896 | """ Get systemd status of casper-sidecar. Same as `systemctl status casper-sidecar` """ 897 | # using op.popen to stop hanging return to terminate 898 | result = os.popen("systemctl status casper-sidecar") 899 | print(result.read()) 900 | 901 | def sidecar_log(self): 902 | """ Get log from casper-sidecar. Same as `journalctl -u casper-sidecar --no-pager` """ 903 | parser = argparse.ArgumentParser(description=self.sidecar_log.__doc__, 904 | usage=f"{self.SCRIPT_NAME} sidecar_log [--follow]") 905 | parser.add_argument("--follow", 906 | help="Follow log file as lines are added", 907 | action='store_true', 908 | required=False) 909 | 910 | args = parser.parse_args(sys.argv[2:]) 911 | follow = "--follow" if args.follow else "" 912 | os.system(f"journalctl -u casper-sidecar --no-pager {follow}") 913 | 914 | def sidecar_stop(self): 915 | """ Stop casper-sidecar. Use 'sudo'. 916 | Same as `sudo systemctl stop casper-sidecar` """ 917 | self._verify_root_user() 918 | os.system("sudo systemctl stop casper-sidecar") 919 | 920 | def sidecar_start(self): 921 | """ Start casper-sidecar. Use 'sudo'. 922 | Same as `sudo systemctl start casper-sidecar` """ 923 | self._verify_root_user() 924 | os.system("sudo systemctl start casper-sidecar") 925 | 926 | def sidecar_restart(self): 927 | """ Restart casper-sidecar. Use 'sudo'. """ 928 | self._verify_root_user() 929 | self.sidecar_stop() 930 | time.sleep(0.5) 931 | self.sidecar_start() 932 | 933 | 934 | if __name__ == '__main__': 935 | NodeUtil() 936 | -------------------------------------------------------------------------------- /src/launcher.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | use std::thread; 3 | #[cfg(not(test))] 4 | use std::{env, process}; 5 | use std::{ 6 | fmt::Debug, 7 | fs, mem, 8 | path::{Path, PathBuf}, 9 | process::Command, 10 | }; 11 | 12 | use anyhow::{bail, Result}; 13 | #[cfg(test)] 14 | use once_cell::sync::Lazy; 15 | use semver::Version; 16 | use serde::{Deserialize, Serialize}; 17 | #[cfg(test)] 18 | use tempfile::TempDir; 19 | use tracing::{debug, error, info, warn}; 20 | 21 | use crate::utils::{self, NodeExitCode}; 22 | 23 | /// The name of the file for the on-disk record of the node-launcher's current state. 24 | const STATE_FILE_NAME: &str = "casper-node-launcher-state.toml"; 25 | /// The path of the node-launcher shutdown script. 26 | #[cfg(not(test))] 27 | const SHUTDOWN_SCRIPT_PATH: &str = "/etc/casper/casper_shutdown_script"; 28 | #[cfg(test)] 29 | const SHUTDOWN_SCRIPT_PATH: &str = "/tmp/test_casper_shutdown_script"; 30 | const SHUTDOWN_TERMINATED_BY_SIGNAL_EXIT_CODE: i32 = 254; 31 | 32 | /// The folder under which casper-node binaries are installed. 33 | #[cfg(not(test))] 34 | const BINARY_ROOT_DIR: &str = "/var/lib/casper/bin"; 35 | /// The name of the casper-node binary. 36 | const NODE_BINARY_NAME: &str = "casper-node"; 37 | /// Environment variable to override the binary root dir. 38 | #[cfg(not(test))] 39 | const BINARY_ROOT_DIR_OVERRIDE: &str = "CASPER_BIN_DIR"; 40 | 41 | /// The folder under which config files are installed. 42 | #[cfg(not(test))] 43 | const CONFIG_ROOT_DIR: &str = "/etc/casper"; 44 | /// The name of the config file for casper-node. 45 | const NODE_CONFIG_NAME: &str = "config.toml"; 46 | /// Environment variable to override the config root dir. 47 | #[cfg(not(test))] 48 | const CONFIG_ROOT_DIR_OVERRIDE: &str = "CASPER_CONFIG_DIR"; 49 | 50 | /// The subcommands and args for casper-node. 51 | const MIGRATE_SUBCOMMAND: &str = "migrate-data"; 52 | const OLD_CONFIG_ARG: &str = "--old-config"; 53 | const NEW_CONFIG_ARG: &str = "--new-config"; 54 | const VALIDATOR_SUBCOMMAND: &str = "validator"; 55 | 56 | /// This "leaks" the tempdir, in that it won't be removed after the tests finish running. However, 57 | /// it is only ever used for testing very small files, and it makes the production code and test 58 | /// code simpler, so it's a worthwhile trade off. 59 | #[cfg(test)] 60 | static TEMP_DIR: Lazy = Lazy::new(|| tempfile::tempdir().expect("should create temp dir")); 61 | 62 | /// Details of the node and its files. 63 | #[derive(PartialEq, Eq, Serialize, Deserialize, Debug)] 64 | pub struct NodeInfo { 65 | /// The version of the node software. 66 | pub version: Version, 67 | /// The path to the node binary. 68 | pub binary_path: PathBuf, 69 | /// The path to the node's config file. 70 | pub config_path: PathBuf, 71 | } 72 | 73 | /// The state of the launcher, cached to disk every time it changes. 74 | #[derive(PartialEq, Eq, Serialize, Deserialize, Debug)] 75 | #[serde(tag = "mode")] 76 | enum State { 77 | RunNodeAsValidator(NodeInfo), 78 | MigrateData { 79 | old_info: NodeInfo, 80 | new_info: NodeInfo, 81 | }, 82 | } 83 | 84 | impl Default for State { 85 | fn default() -> Self { 86 | let node_info = NodeInfo { 87 | version: Version::new(0, 0, 0), 88 | binary_path: PathBuf::new(), 89 | config_path: PathBuf::new(), 90 | }; 91 | State::RunNodeAsValidator(node_info) 92 | } 93 | } 94 | 95 | /// The object responsible for running the casper-node as a child process. 96 | /// 97 | /// It operates as a state machine, iterating between running the node in validator mode and running 98 | /// it in migrate-data mode. 99 | /// 100 | /// At each state transition, it caches its state to disk so that it can resume the same operation 101 | /// if restarted. 102 | #[derive(PartialEq, Eq, Debug)] 103 | pub struct Launcher { 104 | binary_root_dir: PathBuf, 105 | config_root_dir: PathBuf, 106 | state: State, 107 | #[cfg(test)] 108 | exit_code: Option, 109 | } 110 | 111 | impl Default for Launcher { 112 | fn default() -> Self { 113 | Self { 114 | binary_root_dir: Self::binary_root_dir(), 115 | config_root_dir: Self::config_root_dir(), 116 | state: Default::default(), 117 | #[cfg(test)] 118 | exit_code: None, 119 | } 120 | } 121 | } 122 | 123 | impl Launcher { 124 | /// Constructs a new `Launcher`. 125 | /// 126 | /// If the launcher was previously run, this will try and parse its previous state. Otherwise 127 | /// it will search for the latest installed version of casper-node and start running it in 128 | /// validator mode. 129 | /// 130 | /// The launcher may also be instructed to run a fixed version of the node. In such case 131 | /// it'll run it in validator mode and store the version in the local state. 132 | pub fn new(forced_version: Option) -> Result { 133 | let installed_binary_versions = utils::versions_from_path(Self::binary_root_dir())?; 134 | let installed_config_versions = utils::versions_from_path(Self::config_root_dir())?; 135 | 136 | if installed_binary_versions != installed_config_versions { 137 | bail!( 138 | "installed binary versions ({}) don't match installed configs ({})", 139 | utils::iter_to_string(installed_binary_versions), 140 | utils::iter_to_string(installed_config_versions), 141 | ); 142 | } 143 | 144 | match forced_version { 145 | Some(forced_version) => { 146 | // Run the requested node version, if available. 147 | if installed_binary_versions.contains(&forced_version) { 148 | let mut launcher = Launcher::default(); 149 | launcher.set_state(State::RunNodeAsValidator( 150 | launcher.new_node_info(forced_version), 151 | ))?; 152 | Ok(launcher) 153 | } else { 154 | info!(%forced_version, "the requested version is not installed"); 155 | bail!( 156 | "the requested version ({}) is not installed", 157 | forced_version 158 | ) 159 | } 160 | } 161 | None => { 162 | // If state file is missing, run most recent node version. Otherwise, resume from state. 163 | let mut launcher = Launcher::default(); 164 | 165 | let maybe_state = launcher.try_load_state()?; 166 | match maybe_state { 167 | Some(read_state) => { 168 | info!(path=%launcher.state_path().display(), "read stored state"); 169 | launcher.state = read_state; 170 | Ok(launcher) 171 | } 172 | None => { 173 | let node_info = launcher.new_node_info(launcher.most_recent_version()?); 174 | launcher.set_state(State::RunNodeAsValidator(node_info))?; 175 | Ok(launcher) 176 | } 177 | } 178 | } 179 | } 180 | } 181 | 182 | /// Runs the launcher, blocking indefinitely. 183 | pub fn run(&mut self) -> Result<()> { 184 | loop { 185 | self.step()?; 186 | } 187 | } 188 | 189 | /// Provides the path of the file for recording the state of the node-launcher. 190 | fn state_path(&self) -> PathBuf { 191 | self.config_root_dir.join(STATE_FILE_NAME) 192 | } 193 | 194 | /// Sets the given launcher state and stores it on disk. 195 | fn set_state(&mut self, state: State) -> Result<()> { 196 | self.state = state; 197 | self.write() 198 | } 199 | 200 | /// Tries to load the stored state from disk. 201 | fn try_load_state(&self) -> Result> { 202 | let state_path = self.state_path(); 203 | state_path 204 | .exists() 205 | .then(|| { 206 | debug!(path=%state_path.display(), "trying to read stored state"); 207 | let contents = utils::map_and_log_error( 208 | fs::read_to_string(&state_path), 209 | format!("failed to read {}", state_path.display()), 210 | )?; 211 | 212 | Ok(Some(utils::map_and_log_error( 213 | toml::from_str(&contents), 214 | format!("failed to parse {}", state_path.display()), 215 | )?)) 216 | }) 217 | .unwrap_or_else(|| { 218 | debug!(path=%state_path.display(), "stored state doesn't exist"); 219 | Ok(None) 220 | }) 221 | } 222 | 223 | /// Writes `self` to the hard-coded location as a TOML-encoded file. 224 | fn write(&self) -> Result<()> { 225 | let path = self.state_path(); 226 | debug!(path=%path.display(), "trying to store state"); 227 | let contents = utils::map_and_log_error( 228 | toml::to_string_pretty(&self.state), 229 | "failed to encode state as toml".to_string(), 230 | )?; 231 | utils::map_and_log_error( 232 | fs::write(&path, contents.as_bytes()), 233 | format!("failed to write {}", path.display()), 234 | )?; 235 | info!(path=%path.display(), state=?self.state, "stored state"); 236 | Ok(()) 237 | } 238 | 239 | /// Gets the most recent installed binary version. 240 | /// 241 | /// Returns an error when no correct versions can be detected. 242 | fn most_recent_version(&self) -> Result { 243 | let all_versions = utils::versions_from_path(Self::binary_root_dir())?; 244 | 245 | // We are guaranteed to have at least one version in the `all_versions` container, 246 | // because if there are no valid versions installed the `utils::versions_from_path()` bails. 247 | Ok(all_versions 248 | .into_iter() 249 | .next_back() 250 | .expect("must have at least one version")) 251 | } 252 | 253 | /// Gets the next installed version of the node binary and config. 254 | /// 255 | /// Returns an error if the versions cannot be deduced, or if the two versions are different. 256 | fn next_installed_version(&self, current_version: &Version) -> Result { 257 | let next_binary_version = 258 | utils::next_installed_version(&self.binary_root_dir, current_version)?; 259 | let next_config_version = 260 | utils::next_installed_version(&self.config_root_dir, current_version)?; 261 | if next_config_version != next_binary_version { 262 | warn!(%next_binary_version, %next_config_version, "next version mismatch"); 263 | bail!( 264 | "next binary version {} != next config version {}", 265 | next_binary_version, 266 | next_config_version, 267 | ); 268 | } 269 | Ok(next_binary_version) 270 | } 271 | 272 | /// Gets the previous installed version of the node binary and config. 273 | /// 274 | /// Returns an error if the versions cannot be deduced, or if the two versions are different. 275 | fn previous_installed_version(&self, current_version: &Version) -> Result { 276 | let previous_binary_version = 277 | utils::previous_installed_version(&self.binary_root_dir, current_version)?; 278 | let previous_config_version = 279 | utils::previous_installed_version(&self.config_root_dir, current_version)?; 280 | if previous_config_version != previous_binary_version { 281 | warn!(%previous_binary_version, %previous_config_version, "previous version mismatch"); 282 | bail!( 283 | "previous binary version {} != previous config version {}", 284 | previous_binary_version, 285 | previous_config_version, 286 | ); 287 | } 288 | Ok(previous_binary_version) 289 | } 290 | 291 | /// Constructs a new `NodeInfo` based on the given version. 292 | fn new_node_info(&self, version: Version) -> NodeInfo { 293 | let subdir_name = version.to_string().replace('.', "_"); 294 | NodeInfo { 295 | version, 296 | binary_path: self 297 | .binary_root_dir 298 | .join(&subdir_name) 299 | .join(NODE_BINARY_NAME), 300 | config_path: self 301 | .config_root_dir 302 | .join(&subdir_name) 303 | .join(NODE_CONFIG_NAME), 304 | } 305 | } 306 | 307 | /// Provides the path to the binary root folder. casper-node binaries will be installed in a 308 | /// subdir of this path, where the subdir will be named as per the casper-node version. 309 | /// 310 | /// For `test` configuration, this is a folder named `bin` inside a folder in the `TEMP_DIR` 311 | /// named as per the individual test's thread. 312 | /// 313 | /// Otherwise it is `/var/lib/casper/bin`, although this can be overridden (e.g. for external 314 | /// tests), by setting the env var `CASPER_BIN_DIR` to a different folder. 315 | fn binary_root_dir() -> PathBuf { 316 | #[cfg(not(test))] 317 | { 318 | PathBuf::from(match env::var(BINARY_ROOT_DIR_OVERRIDE) { 319 | Ok(path) => path, 320 | Err(_) => BINARY_ROOT_DIR.to_string(), 321 | }) 322 | } 323 | #[cfg(test)] 324 | { 325 | let path = TEMP_DIR 326 | .path() 327 | .join(thread::current().name().unwrap_or("unnamed")) 328 | .join("bin"); 329 | let _ = fs::create_dir_all(&path); 330 | path 331 | } 332 | } 333 | 334 | /// Provides the path to the config root folder. Config files will be installed in a subdir of 335 | /// this path, where the subdir will be named as per the casper-node version. 336 | /// 337 | /// For `test` configuration, this is a folder named `config` inside a folder in the `TEMP_DIR` 338 | /// named as per the individual test's thread. 339 | /// 340 | /// Otherwise it is `/etc/casper`, although this can be overridden (e.g. for external tests), by 341 | /// setting the env var `CASPER_CONFIG_DIR` to a different folder. 342 | fn config_root_dir() -> PathBuf { 343 | #[cfg(not(test))] 344 | { 345 | PathBuf::from(match env::var(CONFIG_ROOT_DIR_OVERRIDE) { 346 | Ok(path) => path, 347 | Err(_) => CONFIG_ROOT_DIR.to_string(), 348 | }) 349 | } 350 | #[cfg(test)] 351 | { 352 | let path = TEMP_DIR 353 | .path() 354 | .join(thread::current().name().unwrap()) 355 | .join("config"); 356 | let _ = fs::create_dir_all(&path); 357 | path 358 | } 359 | } 360 | 361 | /// Sets `self.state` to a new state corresponding to upgrading the current node version. 362 | /// 363 | /// If `self.state` is currently `RunNodeAsValidator`, then finds the next installed version 364 | /// and moves to `MigrateData` if that version is newer (else errors). If it's currently 365 | /// `MigrateData`, moves to `RunNodeAsValidator` using the next installed version. 366 | fn upgrade_state(&mut self) -> Result<()> { 367 | let new_state = match mem::take(&mut self.state) { 368 | State::RunNodeAsValidator(old_info) => { 369 | let next_version = self.next_installed_version(&old_info.version)?; 370 | if next_version <= old_info.version { 371 | let msg = format!( 372 | "no higher version than current {} installed", 373 | old_info.version 374 | ); 375 | warn!("{}", msg); 376 | bail!(msg); 377 | } 378 | 379 | let new_info = self.new_node_info(next_version); 380 | State::MigrateData { old_info, new_info } 381 | } 382 | State::MigrateData { new_info, .. } => State::RunNodeAsValidator(new_info), 383 | }; 384 | 385 | self.state = new_state; 386 | Ok(()) 387 | } 388 | 389 | /// Sets `self.state` to a new state corresponding to downgrading the current node version. 390 | /// 391 | /// Regardless of the current state variant, the returned state is `RunNodeAsValidator` with the 392 | /// previous installed version. 393 | fn downgrade_state(&mut self) -> Result<()> { 394 | let node_info = match &self.state { 395 | State::RunNodeAsValidator(old_info) => old_info, 396 | State::MigrateData { new_info, .. } => new_info, 397 | }; 398 | 399 | let previous_version = self.previous_installed_version(&node_info.version)?; 400 | if previous_version >= node_info.version { 401 | let msg = format!( 402 | "no lower version than current {} installed", 403 | node_info.version 404 | ); 405 | warn!("{}", msg); 406 | bail!(msg); 407 | } 408 | 409 | let new_info = self.new_node_info(previous_version); 410 | self.state = State::RunNodeAsValidator(new_info); 411 | Ok(()) 412 | } 413 | 414 | /// Runs the shutdown script if it exists and exits the node-launcher process 415 | /// with the exit code returned by the script, otherwise returns 0. 416 | fn run_shutdown_script_and_exit(&mut self) -> Result<()> { 417 | let exit_code = if Path::new(SHUTDOWN_SCRIPT_PATH).exists() { 418 | info!("running shutdown script at {}.", SHUTDOWN_SCRIPT_PATH); 419 | let status = utils::map_and_log_error( 420 | Command::new(SHUTDOWN_SCRIPT_PATH).status(), 421 | format!("couldn't execute script at {}", SHUTDOWN_SCRIPT_PATH), 422 | )?; 423 | status.code().unwrap_or_else(|| { 424 | error!("shutdown script was terminated by a signal."); 425 | SHUTDOWN_TERMINATED_BY_SIGNAL_EXIT_CODE 426 | }) 427 | } else { 428 | info!( 429 | "shutdown script not found at {}, exiting.", 430 | SHUTDOWN_SCRIPT_PATH 431 | ); 432 | 0 433 | }; 434 | 435 | #[cfg(not(test))] 436 | process::exit(exit_code); 437 | #[cfg(test)] 438 | { 439 | info!("terminated process with exit code {}", exit_code); 440 | self.exit_code = Some(exit_code); 441 | Ok(()) 442 | } 443 | } 444 | 445 | /// Moves the launcher state forward. 446 | fn transition_state(&mut self, previous_exit_code: NodeExitCode) -> Result<()> { 447 | match previous_exit_code { 448 | NodeExitCode::Success => self.upgrade_state()?, 449 | NodeExitCode::ShouldDowngrade => self.downgrade_state()?, 450 | NodeExitCode::ShouldExitLauncher => self.run_shutdown_script_and_exit()?, 451 | } 452 | self.write() 453 | } 454 | 455 | /// Runs the process for the current state and moves the state forward if the process exits with 456 | /// success. 457 | fn step(&mut self) -> Result<()> { 458 | let exit_code = match &self.state { 459 | State::RunNodeAsValidator(node_info) => { 460 | let mut command = Command::new(&node_info.binary_path); 461 | command 462 | .arg(VALIDATOR_SUBCOMMAND) 463 | .arg(&node_info.config_path); 464 | let exit_code = utils::run_node(command)?; 465 | info!(version=%node_info.version, "finished running node as validator"); 466 | exit_code 467 | } 468 | State::MigrateData { old_info, new_info } => { 469 | let mut command = Command::new(&new_info.binary_path); 470 | command 471 | .arg(MIGRATE_SUBCOMMAND) 472 | .arg(OLD_CONFIG_ARG) 473 | .arg(&old_info.config_path) 474 | .arg(NEW_CONFIG_ARG) 475 | .arg(&new_info.config_path); 476 | let exit_code = utils::run_node(command)?; 477 | info!( 478 | old_version=%old_info.version, 479 | new_version=%new_info.version, 480 | "finished data migration" 481 | ); 482 | exit_code 483 | } 484 | }; 485 | 486 | self.transition_state(exit_code) 487 | } 488 | 489 | #[cfg(test)] 490 | pub(crate) fn exit_code(&self) -> Option { 491 | self.exit_code 492 | } 493 | } 494 | 495 | #[cfg(test)] 496 | mod tests { 497 | use std::{os::unix::fs::PermissionsExt, thread, time::Duration}; 498 | 499 | use super::*; 500 | use crate::logging; 501 | 502 | const NODE_CONTENTS: &str = include_str!("../test_resources/casper-node.in"); 503 | const DOWNGRADE_CONTENTS: &str = include_str!("../test_resources/downgrade.in"); 504 | const SHUTDOWN_CONTENTS: &str = include_str!("../test_resources/shutdown.in"); 505 | /// The duration to wait after starting a mock casper-node instance before "installing" a new 506 | /// version of the mock node. The mock sleeps for 1 second while running in validator mode, so 507 | /// 100ms should be enough to allow the node-launcher step to start. 508 | const DELAY_INSTALL_DURATION: Duration = Duration::from_millis(100); 509 | const SHUTDOWN_SCRIPT_SUCCESS_OUTPUT: &str = "Shutdown script ran successfully"; 510 | const SHUTDOWN_SCRIPT_EXIT_CODE: i32 = 42; 511 | static V1: Lazy = Lazy::new(|| Version::new(1, 0, 0)); 512 | static V2: Lazy = Lazy::new(|| Version::new(2, 0, 0)); 513 | static V3: Lazy = Lazy::new(|| Version::new(3, 0, 0)); 514 | 515 | /// If `upgrade` is true, installs the new version of the mock node binary, assigning an old 516 | /// version for the script with the major version of `new_version` decremented by 1. 517 | /// 518 | /// If `upgrade` is false, installs a copy of the downgrade.sh script in place of the mock node 519 | /// script. This script always exits with a code of 102. 520 | fn install_mock(new_version: &Version, desired_exit_code: NodeExitCode) { 521 | if thread::current().name().is_none() { 522 | panic!( 523 | "install_mock must be called from the main test thread in order for \ 524 | `Launcher::binary_root_dir()` and `Launcher::config_root_dir()` to work" 525 | ); 526 | } 527 | 528 | let subdir_name = new_version.to_string().replace('.', "_"); 529 | 530 | // Create the node script contents. 531 | let old_version = Version::new(new_version.major - 1, new_version.minor, new_version.patch); 532 | let node_contents = match desired_exit_code { 533 | NodeExitCode::Success => NODE_CONTENTS, 534 | NodeExitCode::ShouldDowngrade => DOWNGRADE_CONTENTS, 535 | NodeExitCode::ShouldExitLauncher => SHUTDOWN_CONTENTS, 536 | }; 537 | let node_contents = node_contents.replace( 538 | r#"OLD_VERSION="""#, 539 | &format!(r#"OLD_VERSION="{}""#, old_version), 540 | ); 541 | let node_contents = 542 | node_contents.replace(r#"VERSION="""#, &format!(r#"VERSION="{}""#, new_version)); 543 | 544 | // Create the subdir for the node binary. 545 | let binary_folder = Launcher::binary_root_dir().join(&subdir_name); 546 | fs::create_dir(&binary_folder).unwrap(); 547 | 548 | // Create the node script as an executable file. 549 | let binary_path = binary_folder.join(NODE_BINARY_NAME); 550 | fs::write(&binary_path, node_contents.as_bytes()).unwrap(); 551 | let mut permissions = fs::metadata(&binary_path).unwrap().permissions(); 552 | permissions.set_mode(0o755); 553 | fs::set_permissions(&binary_path, permissions).unwrap(); 554 | 555 | // Create the subdir for the node config. 556 | let config_folder = Launcher::config_root_dir().join(&subdir_name); 557 | fs::create_dir(&config_folder).unwrap(); 558 | 559 | // Create the node config file containing only the version. 560 | let config_path = config_folder.join(NODE_CONFIG_NAME); 561 | fs::write(&config_path, new_version.to_string().as_bytes()).unwrap(); 562 | } 563 | 564 | /// Asserts that `line` equals the last line in the log file which the mock casper-node should 565 | /// have written to. 566 | fn assert_last_log_line_eq(launcher: &Launcher, line: &str) { 567 | let log_path = launcher.binary_root_dir.parent().unwrap().join("log.txt"); 568 | let log_contents = fs::read_to_string(&log_path).unwrap(); 569 | assert_eq!(line, log_contents.lines().last().unwrap()); 570 | } 571 | 572 | /// Asserts that the last line in the log file which the mock casper-node should have written to 573 | /// contains the provided string. 574 | fn assert_last_log_line_contains(launcher: &Launcher, string: &str) { 575 | let log_path = launcher.binary_root_dir.parent().unwrap().join("log.txt"); 576 | let log_contents = fs::read_to_string(&log_path).unwrap(); 577 | let last_line = log_contents.lines().last().unwrap(); 578 | assert!( 579 | last_line.contains(string), 580 | "'{}' doesn't contain '{}'", 581 | last_line, 582 | string 583 | ); 584 | } 585 | 586 | #[test] 587 | fn should_write_state_on_first_run() { 588 | let _ = logging::init(); 589 | 590 | install_mock(&*V1, NodeExitCode::Success); 591 | let launcher = Launcher::new(None).unwrap(); 592 | assert!(launcher.state_path().exists()); 593 | 594 | // Check the state was stored to disk. 595 | let toml_contents = fs::read_to_string(&launcher.state_path()).unwrap(); 596 | let stored_state = toml::from_str(&toml_contents).unwrap(); 597 | assert_eq!(launcher.state, stored_state); 598 | 599 | // Check the stored state is as expected. 600 | let expected_node_info = launcher.new_node_info(V1.clone()); 601 | let expected_state = State::RunNodeAsValidator(expected_node_info); 602 | assert_eq!(expected_state, stored_state); 603 | } 604 | 605 | #[test] 606 | fn should_read_state_on_startup() { 607 | let _ = logging::init(); 608 | 609 | // Write the state to disk (RunNodeAsValidator for V1). 610 | install_mock(&*V1, NodeExitCode::Success); 611 | let _ = Launcher::new(None).unwrap(); 612 | 613 | // Install a new version of node, but ensure a new launcher reads the state from disk rather 614 | // than detecting a new version. 615 | install_mock(&*V2, NodeExitCode::Success); 616 | let launcher = Launcher::new(None).unwrap(); 617 | 618 | let expected_node_info = launcher.new_node_info(V1.clone()); 619 | let expected_state = State::RunNodeAsValidator(expected_node_info); 620 | assert_eq!(expected_state, launcher.state); 621 | } 622 | 623 | #[test] 624 | fn should_error_if_state_corrupted() { 625 | let _ = logging::init(); 626 | 627 | // Write the state to disk (RunNodeAsValidator for V1). 628 | install_mock(&*V1, NodeExitCode::Success); 629 | let launcher = Launcher::new(None).unwrap(); 630 | 631 | // Corrupt the stored state. 632 | fs::write(&launcher.state_path(), "bad value".as_bytes()).unwrap(); 633 | let error = Launcher::new(None).unwrap_err().to_string(); 634 | assert_eq!( 635 | format!("failed to parse {}", launcher.state_path().display()), 636 | error 637 | ); 638 | } 639 | 640 | #[test] 641 | fn should_error_if_node_not_installed_on_first_run() { 642 | let _ = logging::init(); 643 | 644 | let error = Launcher::new(None).unwrap_err().to_string(); 645 | assert_eq!( 646 | format!( 647 | "failed to get a valid version from subdirs in {}", 648 | Launcher::binary_root_dir().display() 649 | ), 650 | error 651 | ); 652 | } 653 | 654 | #[test] 655 | fn should_run_most_recent_version_when_state_file_absent() { 656 | let _ = logging::init(); 657 | 658 | // Set up the test folders as if casper-node has just been staged at v3.0.0. 659 | install_mock(&*V1, NodeExitCode::Success); 660 | install_mock(&*V2, NodeExitCode::Success); 661 | install_mock(&*V3, NodeExitCode::Success); 662 | 663 | let mut launcher = Launcher::new(None).unwrap(); 664 | 665 | // Run the launcher's first and only step - should run node v3.0.0 in validator mode. As there 666 | // will be no further upgraded binary available after the node exits, the step should return 667 | // an error. 668 | let error = launcher.step().unwrap_err().to_string(); 669 | assert_last_log_line_eq(&launcher, "Node v3.0.0 ran as validator"); 670 | assert_eq!("no higher version than current 3.0.0 installed", error); 671 | } 672 | 673 | #[test] 674 | fn should_run_upgrades() { 675 | let _ = logging::init(); 676 | 677 | // Set up the test folders as if casper-node has just been staged at v3.0.0, 678 | // but create the state file, so that the launcher launches the v1.0.0. 679 | install_mock(&*V1, NodeExitCode::Success); 680 | Launcher::new(None).unwrap(); 681 | install_mock(&*V2, NodeExitCode::Success); 682 | install_mock(&*V3, NodeExitCode::Success); 683 | 684 | let mut launcher = Launcher::new(None).unwrap(); 685 | // Run the launcher's first step - should run node v1.0.0 in validator mode. 686 | launcher.step().unwrap(); 687 | assert_last_log_line_eq(&launcher, "Node v1.0.0 ran as validator"); 688 | 689 | // Run the launcher's second step - should run node v2.0.0 in data-migration mode. 690 | launcher.step().unwrap(); 691 | assert_last_log_line_eq(&launcher, "Node v2.0.0 migrated data"); 692 | 693 | // Run the launcher's third step - should run node v2.0.0 in validator mode. 694 | launcher.step().unwrap(); 695 | assert_last_log_line_eq(&launcher, "Node v2.0.0 ran as validator"); 696 | 697 | // Run the launcher's fourth step - should run node v3.0.0 in data-migration mode. 698 | launcher.step().unwrap(); 699 | assert_last_log_line_eq(&launcher, "Node v3.0.0 migrated data"); 700 | 701 | // Run the launcher's fifth step - should run node v3.0.0 in validator mode. As there 702 | // will be no further upgraded binary available after the node exits, the step should return 703 | // an error. 704 | let error = launcher.step().unwrap_err().to_string(); 705 | assert_last_log_line_eq(&launcher, "Node v3.0.0 ran as validator"); 706 | assert_eq!("no higher version than current 3.0.0 installed", error); 707 | } 708 | 709 | #[test] 710 | fn should_not_upgrade_to_lower_version() { 711 | let _ = logging::init(); 712 | 713 | install_mock(&*V2, NodeExitCode::Success); 714 | 715 | // Set up a thread to run the launcher. 716 | let mut launcher = Launcher::new(None).unwrap(); 717 | let worker = thread::spawn(move || { 718 | // Run the launcher's first step - should run node v2.0.0 in validator mode, taking 1 719 | // second to complete, but then fail to find a newer installed version. 720 | let error = launcher.step().unwrap_err().to_string(); 721 | assert_last_log_line_eq(&launcher, "Node v2.0.0 ran as validator"); 722 | assert_eq!("no higher version than current 2.0.0 installed", error); 723 | }); 724 | 725 | // Install node v1.0.0 after v2.0.0 has started running. 726 | thread::sleep(DELAY_INSTALL_DURATION); 727 | install_mock(&*V1, NodeExitCode::Success); 728 | 729 | worker.join().unwrap(); 730 | } 731 | 732 | #[test] 733 | fn should_run_downgrades() { 734 | let _ = logging::init(); 735 | 736 | // Set up the test folders so that v3.0.0 is installed, but it will exit requesting a 737 | // downgrade. 738 | install_mock(&*V3, NodeExitCode::ShouldDowngrade); 739 | 740 | // Set up a thread to run the launcher. 741 | let mut launcher = Launcher::new(None).unwrap(); 742 | let worker = thread::spawn(move || { 743 | // Run the launcher's first step - should run the downgrader, taking 1 second to 744 | // complete. 745 | launcher.step().unwrap(); 746 | assert_last_log_line_eq(&launcher, "Node v3.0.0 exiting to downgrade"); 747 | launcher 748 | }); 749 | 750 | // Install node v2.0.0 also as a downgrader after v3.0.0 has started running. 751 | thread::sleep(DELAY_INSTALL_DURATION); 752 | install_mock(&*V2, NodeExitCode::ShouldDowngrade); 753 | 754 | launcher = worker.join().unwrap(); 755 | 756 | // Set up a thread to run the launcher again. 757 | let worker = thread::spawn(move || { 758 | // Run the launcher's second step - should run the downgrader, taking 1 second to 759 | // complete. 760 | launcher.step().unwrap(); 761 | assert_last_log_line_eq(&launcher, "Node v2.0.0 exiting to downgrade"); 762 | launcher 763 | }); 764 | 765 | // Install node v2.0.0 also as a downgrader after v3.0.0 has started running. 766 | thread::sleep(DELAY_INSTALL_DURATION); 767 | install_mock(&*V1, NodeExitCode::Success); 768 | 769 | launcher = worker.join().unwrap(); 770 | 771 | // Run the launcher's third step - should run node v1.0.0 in validator mode. 772 | launcher.step().unwrap(); 773 | assert_last_log_line_eq(&launcher, "Node v1.0.0 ran as validator"); 774 | } 775 | 776 | #[test] 777 | fn should_not_downgrade_to_higher_version() { 778 | let _ = logging::init(); 779 | 780 | // Set up the test folders so that v2.0.0 is installed, but it will exit requesting a 781 | // downgrade. 782 | install_mock(&*V2, NodeExitCode::ShouldDowngrade); 783 | 784 | // Set up a thread to run the launcher. 785 | let mut launcher = Launcher::new(None).unwrap(); 786 | let worker = thread::spawn(move || { 787 | // Run the launcher's first step - should run the downgrader, taking 1 second to 788 | // complete, but then fail to find an older installed version. 789 | let error = launcher.step().unwrap_err().to_string(); 790 | assert_last_log_line_eq(&launcher, "Node v2.0.0 exiting to downgrade"); 791 | assert_eq!("no lower version than current 2.0.0 installed", error); 792 | }); 793 | 794 | // Install node v3.0.0 after v2.0.0 has started running. 795 | thread::sleep(DELAY_INSTALL_DURATION); 796 | install_mock(&*V3, NodeExitCode::Success); 797 | 798 | worker.join().unwrap(); 799 | } 800 | 801 | #[test] 802 | fn should_run_again_after_crash_while_in_validator_mode() { 803 | let _ = logging::init(); 804 | 805 | // Set up the test folders as if casper-node has just been installed, but provide a config 806 | // file which will cause the node to crash as soon as it starts. 807 | install_mock(&*V1, NodeExitCode::Success); 808 | let mut launcher = Launcher::new(None).unwrap(); 809 | let node_info = launcher.new_node_info(V1.clone()); 810 | let bad_value = "bad value"; 811 | fs::write(&node_info.config_path, bad_value.as_bytes()).unwrap(); 812 | 813 | // Run the launcher step - should return an error indicating the node exited with an 814 | // error, but should leave the node and config unchanged and still runnable. 815 | let error = launcher.step().unwrap_err().to_string(); 816 | assert_last_log_line_contains( 817 | &launcher, 818 | &format!("should contain 1.0.0 but contains {}", bad_value), 819 | ); 820 | assert!(error.ends_with("exited with error"), "{}", error); 821 | 822 | // Fix the config file to be valid and try running the node again. The launcher will 823 | // error out again, but this time after the node has finished running in validator mode due 824 | // to there being no upgraded binary available after the node exits. 825 | fs::write(&node_info.config_path, V1.to_string().as_bytes()).unwrap(); 826 | let error = launcher.step().unwrap_err().to_string(); 827 | assert_last_log_line_eq(&launcher, "Node v1.0.0 ran as validator"); 828 | assert_eq!("no higher version than current 1.0.0 installed", error); 829 | } 830 | 831 | #[test] 832 | fn should_run_again_after_crash_while_in_migration_mode() { 833 | let _ = logging::init(); 834 | 835 | // Set up the test folders as if casper-node has just been installed. 836 | install_mock(&*V1, NodeExitCode::Success); 837 | 838 | // Set up a thread to run the launcher's first two steps. 839 | let mut launcher = Launcher::new(None).unwrap(); 840 | let node_v2_info = launcher.new_node_info(V2.clone()); 841 | let bad_value = "bad value"; 842 | let worker = thread::spawn(move || { 843 | // Run the launcher's first step - should run node v1.0.0 in validator mode, taking 1 844 | // second to complete. 845 | launcher.step().unwrap(); 846 | assert_last_log_line_eq(&launcher, "Node v1.0.0 ran as validator"); 847 | 848 | // Run the launcher's second step - should run node v2.0.0 in data-migration mode, but 849 | // should return an error indicating the node exited with an error, and should leave the 850 | // node and config unchanged and still runnable. 851 | let error = launcher.step().unwrap_err().to_string(); 852 | assert_last_log_line_contains( 853 | &launcher, 854 | &format!("should contain 2.0.0 but contains {}", bad_value), 855 | ); 856 | assert!(error.ends_with("exited with error"), "{}", error); 857 | 858 | launcher 859 | }); 860 | 861 | // Install node v2.0.0 after v1.0.0 has started running, but provide a config file which 862 | // will cause the node to crash as soon as it starts. 863 | thread::sleep(DELAY_INSTALL_DURATION); 864 | install_mock(&*V2, NodeExitCode::Success); 865 | fs::write(&node_v2_info.config_path, bad_value.as_bytes()).unwrap(); 866 | 867 | launcher = worker.join().unwrap(); 868 | 869 | // Fix the config file to be valid and try running the node again. It should run in data- 870 | // migration mode again. 871 | fs::write(&node_v2_info.config_path, V2.to_string().as_bytes()).unwrap(); 872 | launcher.step().unwrap(); 873 | assert_last_log_line_eq(&launcher, "Node v2.0.0 migrated data"); 874 | } 875 | 876 | #[test] 877 | fn should_error_if_bin_and_config_have_different_versions() { 878 | let _ = logging::init(); 879 | 880 | install_mock(&*V1, NodeExitCode::Success); 881 | install_mock(&*V2, NodeExitCode::Success); 882 | install_mock(&*V3, NodeExitCode::Success); 883 | // Rename config folders to emulate the difference. 884 | fs::rename( 885 | Launcher::config_root_dir().join("1_0_0"), 886 | Launcher::config_root_dir().join("2_0_1"), 887 | ) 888 | .unwrap(); 889 | 890 | let error = Launcher::new(None).unwrap_err().to_string(); 891 | assert_eq!( 892 | "installed binary versions (1.0.0, 2.0.0, 3.0.0) don't match installed configs (2.0.0, 2.0.1, 3.0.0)", 893 | error 894 | ); 895 | } 896 | 897 | #[test] 898 | fn should_error_if_no_versions_are_installed() { 899 | let _ = logging::init(); 900 | 901 | let error = Launcher::new(None).unwrap_err().to_string(); 902 | assert!(error.contains("failed to get a valid version from subdirs")); 903 | } 904 | 905 | #[test] 906 | fn should_run_forced_version_and_store_it_in_state() { 907 | let _ = logging::init(); 908 | 909 | install_mock(&*V1, NodeExitCode::Success); 910 | install_mock(&*V2, NodeExitCode::Success); 911 | install_mock(&*V3, NodeExitCode::Success); 912 | 913 | let mut launcher = Launcher::new(Some(V2.clone())).unwrap(); 914 | 915 | // Check if forced version is kept in the local state. 916 | let toml_contents = fs::read_to_string(&launcher.state_path()).unwrap(); 917 | let stored_state = toml::from_str(&toml_contents).unwrap(); 918 | assert!( 919 | matches!(stored_state, State::RunNodeAsValidator(node_info) if node_info.version == *V2), 920 | "incorrect local state stored on disk" 921 | ); 922 | 923 | // Run the launcher's first and only step - should run node v2.0.0 in validator mode. As there 924 | // will be no further upgraded binary available after the node exits, the step should return 925 | // an error. 926 | launcher.step().unwrap(); 927 | assert_last_log_line_eq(&launcher, "Node v2.0.0 ran as validator"); 928 | } 929 | 930 | #[test] 931 | fn should_exit_when_requested_to_run_nonexisting_version() { 932 | let _ = logging::init(); 933 | 934 | install_mock(&*V1, NodeExitCode::Success); 935 | 936 | let error = Launcher::new(Some(V2.clone())).unwrap_err().to_string(); 937 | assert_eq!(error, "the requested version (2.0.0) is not installed"); 938 | } 939 | 940 | #[test] 941 | fn handle_run_shutdown_script() { 942 | let _ = logging::init(); 943 | 944 | let script_path = Path::new(SHUTDOWN_SCRIPT_PATH); 945 | let output_dir = tempfile::tempdir().expect("should create temp dir"); 946 | let output_path = output_dir.path().join("script_output"); 947 | 948 | // Ensure we start the test clean. 949 | if script_path.exists() { 950 | fs::remove_file(script_path).expect("Couldn't remove existing test shutdown script"); 951 | } 952 | if output_path.exists() { 953 | fs::remove_file(output_path.as_path()) 954 | .expect("Couldn't remove existing test shutdown script output"); 955 | } 956 | 957 | // Write our test shutdown script. 958 | let script_contents = format!( 959 | "#!/bin/bash\necho \"{}\" > {} && exit {}", 960 | SHUTDOWN_SCRIPT_SUCCESS_OUTPUT, 961 | output_path.as_os_str().to_str().unwrap(), 962 | SHUTDOWN_SCRIPT_EXIT_CODE 963 | ); 964 | fs::write(script_path, script_contents).expect("Couldn't write shutdown script contents"); 965 | // Give it permission to execute. 966 | let mut script_perms = fs::metadata(script_path) 967 | .expect("Couldn't read shutdown script permissions") 968 | .permissions(); 969 | script_perms.set_mode(0o744); 970 | fs::set_permissions(script_path, script_perms) 971 | .expect("Couldn't modify shutdown script permissions"); 972 | 973 | // Install the mock which exits with 103. 974 | install_mock(&*V1, NodeExitCode::ShouldExitLauncher); 975 | // Run a launcher with the script in place, should run it and return its exit code. 976 | { 977 | let mut launcher = Launcher::new(None).unwrap(); 978 | launcher.step().expect("should step"); 979 | 980 | assert_eq!(launcher.exit_code().unwrap(), SHUTDOWN_SCRIPT_EXIT_CODE); 981 | assert_eq!( 982 | fs::read_to_string(output_path.as_path()) 983 | .unwrap() 984 | .trim_end(), 985 | SHUTDOWN_SCRIPT_SUCCESS_OUTPUT 986 | ); 987 | } 988 | 989 | // We clean up the test resources to test the case where the script is not present. 990 | // We do this in the same test to not have race conditions on the existance of the 991 | // resources between tests, as the paths are hardcoded. 992 | fs::remove_file(script_path).expect("Couldn't clean up test shutdown script"); 993 | fs::remove_file(output_path.as_path()) 994 | .expect("Couldn't clean up test shutdown script output"); 995 | 996 | { 997 | let mut launcher = Launcher::new(None).unwrap(); 998 | launcher.step().expect("should step"); 999 | assert_eq!(launcher.exit_code().unwrap(), 0); 1000 | assert!(!output_path.exists()); 1001 | } 1002 | } 1003 | } 1004 | -------------------------------------------------------------------------------- /src/logging.rs: -------------------------------------------------------------------------------- 1 | use std::{env, io}; 2 | 3 | use anyhow::{Error, Result}; 4 | 5 | use tracing_subscriber::EnvFilter; 6 | 7 | const LOG_ENV_VAR: &str = "RUST_LOG"; 8 | const DEFAULT_LOG_LEVEL: &str = "info"; 9 | 10 | pub fn init() -> Result<()> { 11 | let filter = EnvFilter::new( 12 | env::var(LOG_ENV_VAR) 13 | .as_deref() 14 | .unwrap_or(DEFAULT_LOG_LEVEL), 15 | ); 16 | 17 | tracing_subscriber::fmt() 18 | .with_writer(io::stdout) 19 | .with_env_filter(filter) 20 | .json() 21 | .try_init() 22 | .map_err(Error::msg) 23 | } 24 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![warn(unused_qualifications)] 2 | mod launcher; 3 | mod logging; 4 | mod utils; 5 | 6 | use std::{ 7 | panic::{self, PanicHookInfo}, 8 | str::FromStr, 9 | sync::{ 10 | atomic::{AtomicU32, Ordering}, 11 | Arc, 12 | }, 13 | thread, 14 | }; 15 | 16 | use anyhow::Result; 17 | use backtrace::Backtrace; 18 | use clap::{crate_description, crate_version, Arg, Command}; 19 | use nix::{ 20 | sys::signal::{self, Signal}, 21 | unistd::Pid, 22 | }; 23 | use once_cell::sync::Lazy; 24 | use semver::Version; 25 | use signal_hook::{consts::TERM_SIGNALS, iterator::Signals}; 26 | use tracing::warn; 27 | 28 | use launcher::Launcher; 29 | 30 | const APP_NAME: &str = "Casper node launcher"; 31 | 32 | /// Global variable holding the PID of the current child process. 33 | static CHILD_PID: Lazy> = Lazy::new(|| Arc::new(AtomicU32::new(0))); 34 | 35 | /// Terminates the child process by sending a SIGTERM signal. 36 | fn stop_child() { 37 | let pid = Pid::from_raw(CHILD_PID.load(Ordering::SeqCst) as i32); 38 | let _ = signal::kill(pid, Signal::SIGTERM); 39 | } 40 | 41 | /// A panic handler which ensures the child process is killed before this process exits. 42 | fn panic_hook(info: &PanicHookInfo) { 43 | let backtrace = Backtrace::new(); 44 | 45 | eprintln!("{:?}", backtrace); 46 | 47 | // Print panic info. 48 | if let Some(&string) = info.payload().downcast_ref::<&str>() { 49 | eprintln!("node panicked: {}", string); 50 | } else { 51 | eprintln!("{}", info); 52 | } 53 | 54 | stop_child() 55 | } 56 | 57 | /// A signal handler which ensures the child process is killed before this process exits. 58 | fn signal_handler() { 59 | let mut signals = Signals::new(TERM_SIGNALS).expect("should register signals"); 60 | if signals.forever().next().is_some() { 61 | stop_child() 62 | } 63 | } 64 | 65 | fn main() -> Result<()> { 66 | logging::init()?; 67 | 68 | // Create a panic handler. 69 | panic::set_hook(Box::new(panic_hook)); 70 | 71 | // Register signal handlers for SIGTERM, SIGQUIT and SIGINT. Don't hold on to the joiner for 72 | // this thread as it will block if the child process dies without a signal having been received 73 | // in the main launcher process. 74 | let _ = thread::spawn(signal_handler); 75 | let command = Command::new(APP_NAME) 76 | .version(crate_version!()) 77 | .arg( 78 | Arg::new("force-version") 79 | .short('f') 80 | .long("force-version") 81 | .value_name("version") 82 | .help("Forces the launcher to run the specified version of the node, for example \"1.2.3\"") 83 | .validator(|arg: &str| Version::from_str(arg).map_err(|_| format!("unable to parse '{}' as version", arg))) 84 | .required(false) 85 | .takes_value(true), 86 | ) 87 | .about(crate_description!()); 88 | let matches = command.get_matches(); 89 | 90 | // Safe to unwrap() as we have the string validated by `clap` already. 91 | let forced_version = matches 92 | .value_of("force-version") 93 | .map(|ver| Version::from_str(ver).unwrap()); 94 | 95 | let mut launcher = Launcher::new(forced_version)?; 96 | launcher.run() 97 | } 98 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::BTreeSet, fmt::Display, fs, path::Path, process::Command, str::FromStr, 3 | sync::atomic::Ordering, 4 | }; 5 | 6 | use anyhow::{bail, Error, Result}; 7 | use semver::Version; 8 | use tracing::{debug, warn}; 9 | 10 | /// Represents the exit code of the node process. 11 | #[derive(Clone, Copy, Eq, PartialEq, Debug)] 12 | #[repr(i32)] 13 | pub(crate) enum NodeExitCode { 14 | /// Indicates a successful execution. 15 | Success = 0, 16 | /// Indicates the node version should be downgraded. 17 | ShouldDowngrade = 102, 18 | /// Indicates the node launcher should attempt to run the shutdown script. 19 | ShouldExitLauncher = 103, 20 | } 21 | 22 | /// Iterates the given path, returning the subdir representing the immediate next SemVer version 23 | /// after `current_version`. 24 | /// 25 | /// Subdir names should be semvers with dots replaced with underscores. 26 | pub(crate) fn next_installed_version>( 27 | dir: P, 28 | current_version: &Version, 29 | ) -> Result { 30 | let max_version = Version::new(u64::MAX, u64::MAX, u64::MAX); 31 | 32 | let mut next_version = max_version.clone(); 33 | for installed_version in versions_from_path(dir)? { 34 | if installed_version > *current_version && installed_version < next_version { 35 | next_version = installed_version; 36 | } 37 | } 38 | 39 | if next_version == max_version { 40 | next_version = current_version.clone(); 41 | } 42 | 43 | Ok(next_version) 44 | } 45 | 46 | /// Iterates the given path, returning the subdir representing the immediate previous SemVer version 47 | /// before `current_version`. 48 | /// 49 | /// Subdir names should be semvers with dots replaced with underscores. 50 | pub(crate) fn previous_installed_version>( 51 | dir: P, 52 | current_version: &Version, 53 | ) -> Result { 54 | let min_version = Version::new(0, 0, 0); 55 | 56 | let mut previous_version = min_version.clone(); 57 | for installed_version in versions_from_path(dir)? { 58 | if installed_version < *current_version && installed_version > previous_version { 59 | previous_version = installed_version; 60 | } 61 | } 62 | 63 | if previous_version == min_version { 64 | previous_version = current_version.clone(); 65 | } 66 | 67 | Ok(previous_version) 68 | } 69 | 70 | pub(crate) fn versions_from_path>(dir: P) -> Result> { 71 | let mut versions = BTreeSet::new(); 72 | 73 | for entry in map_and_log_error( 74 | fs::read_dir(dir.as_ref()), 75 | format!("failed to read dir {}", dir.as_ref().display()), 76 | )? { 77 | let path = map_and_log_error( 78 | entry, 79 | format!("bad dir entry in {}", dir.as_ref().display()), 80 | )? 81 | .path(); 82 | let subdir_name = match path.file_name() { 83 | Some(name) => name.to_string_lossy().replace('_', "."), 84 | None => { 85 | debug!("{} has no final path component", path.display()); 86 | continue; 87 | } 88 | }; 89 | let version = match Version::from_str(&subdir_name) { 90 | Ok(version) => version, 91 | Err(error) => { 92 | debug!(%error, path=%path.display(), "failed to get a version"); 93 | continue; 94 | } 95 | }; 96 | 97 | versions.insert(version); 98 | } 99 | 100 | if versions.is_empty() { 101 | let msg = format!( 102 | "failed to get a valid version from subdirs in {}", 103 | dir.as_ref().display() 104 | ); 105 | warn!("{}", msg); 106 | bail!(msg); 107 | } 108 | 109 | Ok(versions) 110 | } 111 | 112 | /// Runs the given command as a child process. 113 | pub(crate) fn run_node(mut command: Command) -> Result { 114 | let mut child = map_and_log_error(command.spawn(), format!("failed to execute {:?}", command))?; 115 | crate::CHILD_PID.store(child.id(), Ordering::SeqCst); 116 | 117 | let exit_status = map_and_log_error( 118 | child.wait(), 119 | format!("failed to wait for completion of {:?}", command), 120 | )?; 121 | match exit_status.code() { 122 | Some(code) if code == NodeExitCode::Success as i32 => { 123 | debug!("successfully finished running {:?}", command); 124 | Ok(NodeExitCode::Success) 125 | } 126 | Some(code) if code == NodeExitCode::ShouldDowngrade as i32 => { 127 | debug!("finished running {:?} - should downgrade now", command); 128 | Ok(NodeExitCode::ShouldDowngrade) 129 | } 130 | Some(code) if code == NodeExitCode::ShouldExitLauncher as i32 => { 131 | debug!( 132 | "finished running {:?} - trying to run shutdown script now", 133 | command 134 | ); 135 | Ok(NodeExitCode::ShouldExitLauncher) 136 | } 137 | _ => { 138 | warn!(%exit_status, "failed running {:?}", command); 139 | bail!("{:?} exited with error", command); 140 | } 141 | } 142 | } 143 | 144 | /// Maps an error to a different type of error, while also logging the error at warn level. 145 | pub(crate) fn map_and_log_error( 146 | result: std::result::Result, 147 | error_msg: String, 148 | ) -> Result { 149 | match result { 150 | Ok(t) => Ok(t), 151 | Err(error) => { 152 | warn!(%error, "{}", error_msg); 153 | Err(Error::new(error).context(error_msg)) 154 | } 155 | } 156 | } 157 | 158 | /// Joins the items into a single string. 159 | /// The input `[1, 2, 3]` will result in a string "1, 2, 3". 160 | pub(crate) fn iter_to_string(iterable: I) -> String 161 | where 162 | I: IntoIterator, 163 | I::Item: Display, 164 | { 165 | // This function should ideally be replaced with `itertools::join()`. 166 | // However, currently, it is only used to produce a proper debug message, 167 | // which is not sufficient justification to add a dependency to `itertools`. 168 | let result = iterable.into_iter().fold(String::new(), |result, item| { 169 | format!("{}{}, ", result, item) 170 | }); 171 | if result.is_empty() { 172 | result 173 | } else { 174 | String::from(&result[0..result.len() - 2]) 175 | } 176 | } 177 | 178 | #[cfg(test)] 179 | mod tests { 180 | use super::*; 181 | use crate::logging; 182 | 183 | #[test] 184 | fn should_get_next_installed_version() { 185 | let _ = logging::init(); 186 | let tempdir = tempfile::tempdir().expect("should create temp dir"); 187 | 188 | let get_next_version = |current_version: &Version| { 189 | next_installed_version(tempdir.path(), current_version).unwrap() 190 | }; 191 | 192 | let mut current = Version::new(0, 0, 0); 193 | let mut next_version = Version::new(1, 0, 0); 194 | fs::create_dir(tempdir.path().join("1_0_0")).unwrap(); 195 | assert_eq!(get_next_version(¤t), next_version); 196 | current = next_version; 197 | 198 | next_version = Version::new(1, 2, 3); 199 | fs::create_dir(tempdir.path().join("1_2_3")).unwrap(); 200 | assert_eq!(get_next_version(¤t), next_version); 201 | current = next_version.clone(); 202 | 203 | fs::create_dir(tempdir.path().join("1_0_3")).unwrap(); 204 | assert_eq!(get_next_version(¤t), next_version); 205 | 206 | fs::create_dir(tempdir.path().join("2_2_2")).unwrap(); 207 | fs::create_dir(tempdir.path().join("3_3_3")).unwrap(); 208 | assert_eq!(get_next_version(¤t), Version::new(2, 2, 2)); 209 | } 210 | 211 | #[test] 212 | fn should_ignore_invalid_versions() { 213 | let _ = logging::init(); 214 | let tempdir = tempfile::tempdir().expect("should create temp dir"); 215 | let current_version = Version::new(0, 0, 0); 216 | 217 | // Try with a non-existent dir. 218 | let non_existent_dir = Path::new("not_a_dir"); 219 | let error = next_installed_version(non_existent_dir, ¤t_version) 220 | .unwrap_err() 221 | .to_string(); 222 | assert_eq!( 223 | format!("failed to read dir {}", non_existent_dir.display()), 224 | error 225 | ); 226 | 227 | // Try with a dir which has no subdirs. 228 | let error = next_installed_version(tempdir.path(), ¤t_version) 229 | .unwrap_err() 230 | .to_string(); 231 | assert_eq!( 232 | format!( 233 | "failed to get a valid version from subdirs in {}", 234 | tempdir.path().display() 235 | ), 236 | error 237 | ); 238 | 239 | // Try with a dir which has one subdir which is not a valid version representation. 240 | fs::create_dir(tempdir.path().join("not_a_version")).unwrap(); 241 | let error = next_installed_version(tempdir.path(), ¤t_version) 242 | .unwrap_err() 243 | .to_string(); 244 | assert_eq!( 245 | format!( 246 | "failed to get a valid version from subdirs in {}", 247 | tempdir.path().display() 248 | ), 249 | error 250 | ); 251 | 252 | // Try with a dir which has a valid and invalid subdir - the invalid one should be ignored. 253 | fs::create_dir(tempdir.path().join("1_2_3")).unwrap(); 254 | assert_eq!( 255 | next_installed_version(tempdir.path(), ¤t_version).unwrap(), 256 | Version::new(1, 2, 3) 257 | ); 258 | } 259 | 260 | #[test] 261 | fn should_not_run_invalid_command() { 262 | let _ = logging::init(); 263 | 264 | // Try with a non-existent binary. 265 | let non_existent_binary = "non-existent-binary"; 266 | let mut command = Command::new(non_existent_binary); 267 | let error = run_node(command).unwrap_err().to_string(); 268 | assert_eq!( 269 | format!(r#"failed to execute "{}""#, non_existent_binary), 270 | error 271 | ); 272 | 273 | // Try a valid binary but use a bad arg to make it exit with a failure. 274 | let cargo = env!("CARGO"); 275 | command = Command::new(cargo); 276 | command.arg("--deliberately-passing-bad-arg-for-test"); 277 | let error = run_node(command).unwrap_err().to_string(); 278 | assert!(error.ends_with("exited with error"), "{}", error); 279 | } 280 | 281 | #[test] 282 | fn should_run_valid_command() { 283 | let _ = logging::init(); 284 | 285 | let cargo = env!("CARGO"); 286 | let mut command = Command::new(cargo); 287 | command.arg("--version"); 288 | assert_eq!(run_node(command).unwrap(), NodeExitCode::Success); 289 | } 290 | 291 | #[test] 292 | fn should_run_command_exiting_with_downgrade_code() { 293 | let _ = logging::init(); 294 | 295 | let script_path = format!("{}/test_resources/downgrade.sh", env!("CARGO_MANIFEST_DIR")); 296 | 297 | let mut command = Command::new("sh"); 298 | command.arg(&script_path); 299 | assert_eq!(run_node(command).unwrap(), NodeExitCode::ShouldDowngrade); 300 | } 301 | 302 | #[test] 303 | fn should_read_versions_from_dir() { 304 | let tempdir = tempfile::tempdir().expect("should create temp dir"); 305 | fs::create_dir(tempdir.path().join("1_0_0")).unwrap(); 306 | fs::create_dir(tempdir.path().join("2_0_0")).unwrap(); 307 | fs::create_dir(tempdir.path().join("3_0_0")).unwrap(); 308 | fs::create_dir(tempdir.path().join("3_0_0_0")).unwrap(); 309 | fs::create_dir(tempdir.path().join("3_A")).unwrap(); 310 | fs::create_dir(tempdir.path().join("2_0_1")).unwrap(); 311 | fs::create_dir(tempdir.path().join("1_0_9145")).unwrap(); 312 | fs::create_dir(tempdir.path().join("1_454875135649876544411657897987_9145")).unwrap(); 313 | 314 | // Should return in ascending order 315 | let expected_version: BTreeSet = [ 316 | Version::new(1, 0, 0), 317 | Version::new(1, 0, 9145), 318 | Version::new(2, 0, 0), 319 | Version::new(2, 0, 1), 320 | Version::new(3, 0, 0), 321 | ] 322 | .iter() 323 | .cloned() 324 | .collect(); 325 | 326 | let actual_versions = versions_from_path(tempdir.path()).unwrap(); 327 | 328 | assert_eq!(expected_version, actual_versions); 329 | } 330 | 331 | #[test] 332 | fn concatenates_iterable_values() { 333 | let input = ["abc"]; 334 | let output = iter_to_string(input); 335 | assert_eq!(output, "abc"); 336 | 337 | let input = ["a", "b", "c"]; 338 | let output = iter_to_string(input); 339 | assert_eq!(output, "a, b, c"); 340 | 341 | let input = Vec::::new(); 342 | let output = iter_to_string(input); 343 | assert_eq!(output, ""); 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /test_resources/casper-node.in: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script is a mock of casper-node. It has the same subcommands which take the same args, and 4 | # writes a message to logfile on success. 5 | # 6 | # The script should be copied to the location where `casper-node` would be expected to have been 7 | # installed to. It should have $VERSION and $OLD_VERSION replaced with valid values. 8 | # 9 | # Running with args `migrate-data --old-config a.toml --new-config b.toml` causes the script to 10 | # check that a.toml contains only the text specified in $OLD_VERSION and b.toml the text specified 11 | # in $VERSION. On success, the value of $MIGRATE_SUCCESS_MESSAGE is written to a file named 12 | # $LOG_FILENAME in the root test folder (two directories up from the location of the script). 13 | # 14 | # Running with args `validator a.toml` causes the script to check that a.toml contains only the 15 | # text specified in $VERSION. On success, the value of $VALIDATOR_SUCCESS_MESSAGE is written to a 16 | # file named $LOG_FILENAME in the root test folder (two directories up from the location of the 17 | # script). 18 | 19 | set -o errexit 20 | set -o nounset 21 | set -o pipefail 22 | 23 | VERSION="" 24 | OLD_VERSION="" 25 | MIGRATE_SUCCESS_MESSAGE="Node v${VERSION} migrated data" 26 | VALIDATOR_SUCCESS_MESSAGE="Node v${VERSION} ran as validator" 27 | LOG_FILENAME="log.txt" 28 | 29 | # In tests, this script will be installed at e.g. /bin/1_0_0. We want to write the log to . 30 | LOG_FILE="$(readlink -f $(dirname ${0})/../..)/${LOG_FILENAME}" 31 | 32 | log() { 33 | printf "%s\n" "${1}" >> "${LOG_FILE}" 34 | } 35 | 36 | migrate_data() { 37 | local EXPECTED_OLD_CONFIG_ARG_NAME="--old-config" 38 | local EXPECTED_NEW_CONFIG_ARG_NAME="--new-config" 39 | 40 | if [[ ${#} -ne 4 || 41 | "${1}" != ${EXPECTED_OLD_CONFIG_ARG_NAME} || 42 | "${3}" != ${EXPECTED_NEW_CONFIG_ARG_NAME} ]] 43 | then 44 | log "Invalid args for migrate-data subcommand: expected ${EXPECTED_OLD_CONFIG_ARG_NAME} ${EXPECTED_NEW_CONFIG_ARG_NAME} ; got ${@}" 45 | exit 101 46 | fi 47 | 48 | if [[ ! -f "${2}" ]]; then 49 | log "Old node config at '${2}' does not exist" 50 | exit 101 51 | fi 52 | 53 | if [[ ! -f "${4}" ]]; then 54 | log "New node config at '${4}' does not exist" 55 | exit 101 56 | fi 57 | 58 | OLD_CONFIG_CONTENTS=$(<"${2}") 59 | if [[ "${OLD_CONFIG_CONTENTS}" != ${OLD_VERSION} ]]; then 60 | log "Old config file at ${2} should contain ${OLD_VERSION} but contains ${OLD_CONFIG_CONTENTS}" 61 | exit 101 62 | fi 63 | 64 | NEW_CONFIG_CONTENTS=$(<"${4}") 65 | if [[ "${NEW_CONFIG_CONTENTS}" != ${VERSION} ]]; then 66 | log "New config file at ${4} should contain ${VERSION} but contains ${NEW_CONFIG_CONTENTS}" 67 | exit 101 68 | fi 69 | 70 | log "${MIGRATE_SUCCESS_MESSAGE}" 71 | } 72 | 73 | validator() { 74 | if [[ ${#} -ne 1 ]]; then 75 | log "Invalid args for validator subcommand: expected ; got ${@}" 76 | exit 101 77 | fi 78 | 79 | if [[ ! -f "${1}" ]]; then 80 | log "Node config at '${1}' does not exist" 81 | exit 101 82 | fi 83 | 84 | CONFIG_CONTENTS=$(<"${1}") 85 | if [[ "${CONFIG_CONTENTS}" != ${VERSION} ]]; then 86 | log "Config file at ${1} should contain ${VERSION} but contains ${CONFIG_CONTENTS}" 87 | exit 101 88 | fi 89 | 90 | sleep 1 91 | log "${VALIDATOR_SUCCESS_MESSAGE}" 92 | } 93 | 94 | main() { 95 | local MIGRATE_DATA_SUBCOMMAND="migrate-data" 96 | local VALIDATOR_SUBCOMMAND="validator" 97 | 98 | if [[ ${#} -eq 0 ]]; then 99 | log "No subcommand passed: expected '${MIGRATE_DATA_SUBCOMMAND}' or '${VALIDATOR_SUBCOMMAND}'" 100 | exit 101 101 | fi 102 | 103 | if [[ "${1}" == $MIGRATE_DATA_SUBCOMMAND ]]; then 104 | shift 105 | migrate_data "${@}" 106 | elif [[ "${1}" == $VALIDATOR_SUBCOMMAND ]]; then 107 | shift 108 | validator "${@}" 109 | else 110 | log "Invalid subcommand passed: expected '${MIGRATE_DATA_SUBCOMMAND}' or '${VALIDATOR_SUBCOMMAND}'" 111 | exit 101 112 | fi 113 | } 114 | 115 | main "${@}" 116 | -------------------------------------------------------------------------------- /test_resources/downgrade.in: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script is used to test that the launcher correctly interprets an exit code of 102 as an instruction to downgrade 4 | # the version of casper-node. 5 | 6 | set -o errexit 7 | set -o nounset 8 | set -o pipefail 9 | 10 | VERSION="" 11 | MESSAGE="Node v${VERSION} exiting to downgrade" 12 | LOG_FILENAME="log.txt" 13 | 14 | # In tests, this script will be installed at e.g. /bin/1_0_0. We want to write the log to . 15 | LOG_FILE="$(readlink -f $(dirname ${0})/../..)/${LOG_FILENAME}" 16 | 17 | log() { 18 | printf "%s\n" "${1}" >> "${LOG_FILE}" 19 | } 20 | 21 | sleep 1 22 | log "${MESSAGE}" 23 | exit 102 24 | -------------------------------------------------------------------------------- /test_resources/downgrade.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script is used in the basic `should_run_command_exiting_with_downgrade_code` unit test. 4 | 5 | exit 102 6 | -------------------------------------------------------------------------------- /test_resources/shutdown.in: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script is used to test that the launcher correctly interprets an exit code of 103 as an instruction to exit 4 | # and run the shutdown script. 5 | 6 | set -o errexit 7 | set -o nounset 8 | set -o pipefail 9 | 10 | VERSION="" 11 | MESSAGE="Node v${VERSION} shutting down" 12 | LOG_FILENAME="log.txt" 13 | 14 | # In tests, this script will be installed at e.g. /bin/1_0_0. We want to write the log to . 15 | LOG_FILE="$(readlink -f $(dirname ${0})/../..)/${LOG_FILENAME}" 16 | 17 | log() { 18 | printf "%s\n" "${1}" >> "${LOG_FILE}" 19 | } 20 | 21 | sleep 1 22 | log "${MESSAGE}" 23 | exit 103 24 | --------------------------------------------------------------------------------