ConfigLMM 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/CNAME +1 -0
  4. data/Examples/.lmm.state.yaml +159 -0
  5. data/Examples/ConfigLMM.mm.yaml +32 -0
  6. data/Examples/Implemented.mm.yaml +252 -4
  7. data/Examples/SmallBusiness.mm.yaml +492 -0
  8. data/Plugins/Apps/Answer/answer.lmm.rb +165 -0
  9. data/Plugins/Apps/Answer/answer@.service +40 -0
  10. data/Plugins/Apps/ArchiSteamFarm/ArchiSteamFarm.conf.erb +0 -3
  11. data/Plugins/Apps/ArchiSteamFarm/ArchiSteamFarm.lmm.rb +0 -1
  12. data/Plugins/Apps/Authentik/Authentik-ProxyOutpost.container +7 -1
  13. data/Plugins/Apps/Authentik/Authentik-Server.container +6 -1
  14. data/Plugins/Apps/Authentik/Authentik-Worker.container +6 -1
  15. data/Plugins/Apps/Authentik/Authentik.conf.erb +12 -7
  16. data/Plugins/Apps/Authentik/Authentik.lmm.rb +226 -61
  17. data/Plugins/Apps/BookStack/BookStack.conf.erb +0 -3
  18. data/Plugins/Apps/BookStack/BookStack.container +5 -0
  19. data/Plugins/Apps/BookStack/BookStack.lmm.rb +14 -3
  20. data/Plugins/Apps/Cassandra/Cassandra.lmm.rb +9 -19
  21. data/Plugins/Apps/ClickHouse/ClickHouse.container +28 -0
  22. data/Plugins/Apps/ClickHouse/ClickHouse.lmm.rb +113 -0
  23. data/Plugins/Apps/ClickHouse/Config/listen.yaml +2 -0
  24. data/Plugins/Apps/ClickHouse/Config/logger.yaml +8 -0
  25. data/Plugins/Apps/ClickHouse/Config/zookeepers.yaml +5 -0
  26. data/Plugins/Apps/ClickHouse/Connection.rb +96 -0
  27. data/Plugins/Apps/Discourse/Discourse-Sidekiq.container +5 -0
  28. data/Plugins/Apps/Discourse/Discourse.conf.erb +1 -4
  29. data/Plugins/Apps/Discourse/Discourse.container +4 -0
  30. data/Plugins/Apps/Discourse/Discourse.lmm.rb +116 -55
  31. data/Plugins/Apps/Dovecot/Dovecot.lmm.rb +74 -62
  32. data/Plugins/Apps/ERPNext/ERPNext-Frontend.container +6 -1
  33. data/Plugins/Apps/ERPNext/ERPNext-Queue.container +5 -0
  34. data/Plugins/Apps/ERPNext/ERPNext-Scheduler.container +5 -0
  35. data/Plugins/Apps/ERPNext/ERPNext-Websocket.container +6 -1
  36. data/Plugins/Apps/ERPNext/ERPNext.container +6 -1
  37. data/Plugins/Apps/ERPNext/ERPNext.lmm.rb +138 -127
  38. data/Plugins/Apps/GitLab/GitLab.container +6 -0
  39. data/Plugins/Apps/GitLab/GitLab.lmm.rb +43 -49
  40. data/Plugins/Apps/Homepage/Homepage.conf.erb +86 -0
  41. data/Plugins/Apps/Homepage/Homepage.container +19 -0
  42. data/Plugins/Apps/Homepage/Homepage.lmm.rb +54 -0
  43. data/Plugins/Apps/IPFS/IPFS.conf.erb +0 -3
  44. data/Plugins/Apps/IPFS/IPFS.lmm.rb +0 -1
  45. data/Plugins/Apps/InfluxDB/InfluxDB.conf.erb +0 -3
  46. data/Plugins/Apps/InfluxDB/InfluxDB.lmm.rb +0 -1
  47. data/Plugins/Apps/Jackett/Jackett.conf.erb +0 -3
  48. data/Plugins/Apps/Jackett/Jackett.lmm.rb +0 -1
  49. data/Plugins/Apps/Jellyfin/Jellyfin.conf.erb +0 -3
  50. data/Plugins/Apps/Jellyfin/Jellyfin.lmm.rb +0 -1
  51. data/Plugins/Apps/LetsEncrypt/LetsEncrypt.lmm.rb +49 -28
  52. data/Plugins/Apps/LibreTranslate/LibreTranslate.container +21 -0
  53. data/Plugins/Apps/LibreTranslate/LibreTranslate.lmm.rb +34 -0
  54. data/Plugins/Apps/Lobsters/Containerfile +81 -0
  55. data/Plugins/Apps/Lobsters/Lobsters-Tasks.container +26 -0
  56. data/Plugins/Apps/Lobsters/Lobsters.conf.erb +99 -0
  57. data/Plugins/Apps/Lobsters/Lobsters.container +27 -0
  58. data/Plugins/Apps/Lobsters/Lobsters.lmm.rb +196 -0
  59. data/Plugins/Apps/Lobsters/crontab +3 -0
  60. data/Plugins/Apps/Lobsters/database.yml +26 -0
  61. data/Plugins/Apps/Lobsters/entrypoint.sh +30 -0
  62. data/Plugins/Apps/Lobsters/generateCredentials.rb +19 -0
  63. data/Plugins/Apps/Lobsters/lobsters-cron.sh +25 -0
  64. data/Plugins/Apps/Lobsters/lobsters-daily.sh +23 -0
  65. data/Plugins/Apps/Lobsters/puma.rb +49 -0
  66. data/Plugins/Apps/MariaDB/Connection.rb +55 -0
  67. data/Plugins/Apps/MariaDB/MariaDB.lmm.rb +60 -53
  68. data/Plugins/Apps/Mastodon/Mastodon-Sidekiq.container +22 -0
  69. data/Plugins/Apps/Mastodon/Mastodon-Streaming.container +20 -0
  70. data/Plugins/Apps/Mastodon/Mastodon.conf.erb +34 -45
  71. data/Plugins/Apps/Mastodon/Mastodon.container +28 -0
  72. data/Plugins/Apps/Mastodon/Mastodon.lmm.rb +240 -5
  73. data/Plugins/Apps/Mastodon/configlmm.rake +30 -0
  74. data/Plugins/Apps/Mastodon/entrypoint.sh +16 -0
  75. data/Plugins/Apps/Matrix/Element.container +5 -0
  76. data/Plugins/Apps/Matrix/Matrix.conf.erb +2 -8
  77. data/Plugins/Apps/Matrix/Matrix.lmm.rb +100 -71
  78. data/Plugins/Apps/Matrix/Synapse.container +5 -0
  79. data/Plugins/Apps/Netdata/Netdata.conf.erb +0 -3
  80. data/Plugins/Apps/Netdata/Netdata.lmm.rb +0 -1
  81. data/Plugins/Apps/Nextcloud/Nextcloud.conf.erb +3 -4
  82. data/Plugins/Apps/Nextcloud/Nextcloud.lmm.rb +150 -68
  83. data/Plugins/Apps/Nextcloud/autoconfig.php +13 -0
  84. data/Plugins/Apps/Nextcloud/config.php +10 -1
  85. data/Plugins/Apps/Nextcloud/nextcloudcron.service +8 -0
  86. data/Plugins/Apps/Nextcloud/nextcloudcron.timer +10 -0
  87. data/Plugins/Apps/Nginx/Connection.rb +93 -0
  88. data/Plugins/Apps/Nginx/conf.d/configlmm.conf +50 -9
  89. data/Plugins/Apps/Nginx/conf.d/languages.conf +21 -0
  90. data/Plugins/Apps/Nginx/config-lmm/errors.conf +25 -20
  91. data/Plugins/Apps/Nginx/config-lmm/gateway-errors.conf +20 -0
  92. data/Plugins/Apps/Nginx/config-lmm/proxy.conf +1 -1
  93. data/Plugins/Apps/Nginx/main.conf.erb +7 -3
  94. data/Plugins/Apps/Nginx/nginx.conf +2 -2
  95. data/Plugins/Apps/Nginx/nginx.lmm.rb +99 -81
  96. data/Plugins/Apps/Nginx/proxy.conf.erb +11 -3
  97. data/Plugins/Apps/Odoo/Odoo.conf.erb +0 -3
  98. data/Plugins/Apps/Odoo/Odoo.container +5 -0
  99. data/Plugins/Apps/Odoo/Odoo.lmm.rb +4 -5
  100. data/Plugins/Apps/Ollama/Ollama.container +26 -0
  101. data/Plugins/Apps/Ollama/Ollama.lmm.rb +73 -0
  102. data/Plugins/Apps/OpenTelemetry/Config/config.yaml +704 -0
  103. data/Plugins/Apps/OpenTelemetry/OpenTelemetry.lmm.rb +154 -0
  104. data/Plugins/Apps/OpenVidu/Ingress.container +5 -0
  105. data/Plugins/Apps/OpenVidu/OpenVidu.conf.erb +0 -3
  106. data/Plugins/Apps/OpenVidu/OpenVidu.container +5 -0
  107. data/Plugins/Apps/OpenVidu/OpenVidu.lmm.rb +7 -3
  108. data/Plugins/Apps/OpenVidu/OpenViduCall.conf.erb +0 -3
  109. data/Plugins/Apps/OpenVidu/OpenViduCall.container +5 -0
  110. data/Plugins/Apps/PHP-FPM/Connection.rb +91 -0
  111. data/Plugins/Apps/PHP-FPM/PHP-FPM.lmm.rb +31 -4
  112. data/Plugins/Apps/Peppermint/Peppermint.conf.erb +2 -5
  113. data/Plugins/Apps/Peppermint/Peppermint.container +5 -0
  114. data/Plugins/Apps/Peppermint/Peppermint.lmm.rb +29 -33
  115. data/Plugins/Apps/Perplexica/Perplexica.container +25 -0
  116. data/Plugins/Apps/Perplexica/Perplexica.lmm.rb +92 -0
  117. data/Plugins/Apps/Perplexica/config.toml +26 -0
  118. data/Plugins/Apps/Podman/Connection.rb +24 -0
  119. data/Plugins/Apps/Podman/Podman.lmm.rb +80 -0
  120. data/Plugins/Apps/Podman/storage.conf +6 -0
  121. data/Plugins/Apps/Postfix/Postfix.lmm.rb +242 -164
  122. data/Plugins/Apps/PostgreSQL/Connection.rb +97 -0
  123. data/Plugins/Apps/PostgreSQL/PostgreSQL.lmm.rb +184 -148
  124. data/Plugins/Apps/Pterodactyl/Pterodactyl.conf.erb +0 -3
  125. data/Plugins/Apps/Pterodactyl/Pterodactyl.lmm.rb +0 -2
  126. data/Plugins/Apps/Pterodactyl/Wings.conf.erb +0 -3
  127. data/Plugins/Apps/RVM/RVM.lmm.rb +57 -0
  128. data/Plugins/Apps/Roundcube/Roundcube.conf.erb +0 -3
  129. data/Plugins/Apps/Roundcube/Roundcube.lmm.rb +15 -19
  130. data/Plugins/Apps/SSH/SSH.lmm.rb +9 -15
  131. data/Plugins/Apps/SearXNG/SearXNG.container +22 -0
  132. data/Plugins/Apps/SearXNG/SearXNG.lmm.rb +79 -0
  133. data/Plugins/Apps/SearXNG/limiter.toml +40 -0
  134. data/Plugins/Apps/SearXNG/settings.yml +2 -0
  135. data/Plugins/Apps/SigNoz/Config/alerts.yml +11 -0
  136. data/Plugins/Apps/SigNoz/Config/otel-collector-config.yaml +110 -0
  137. data/Plugins/Apps/SigNoz/Config/otel-collector-opamp-config.yaml +1 -0
  138. data/Plugins/Apps/SigNoz/Config/prometheus.yml +18 -0
  139. data/Plugins/Apps/SigNoz/SigNoz-Collector.container +23 -0
  140. data/Plugins/Apps/SigNoz/SigNoz-Migrator.container +17 -0
  141. data/Plugins/Apps/SigNoz/SigNoz.conf.erb +61 -0
  142. data/Plugins/Apps/SigNoz/SigNoz.container +26 -0
  143. data/Plugins/Apps/SigNoz/SigNoz.lmm.rb +319 -0
  144. data/Plugins/Apps/Solr/log4j2.xml +89 -0
  145. data/Plugins/Apps/Solr/solr.lmm.rb +82 -0
  146. data/Plugins/Apps/Sunshine/Sunshine.conf.erb +0 -3
  147. data/Plugins/Apps/Sunshine/Sunshine.lmm.rb +0 -1
  148. data/Plugins/Apps/Tunnel/tunnel.lmm.rb +33 -37
  149. data/Plugins/Apps/UVdesk/UVdesk.conf.erb +0 -3
  150. data/Plugins/Apps/Umami/Umami.container +19 -0
  151. data/Plugins/Apps/Umami/Umami.lmm.rb +108 -0
  152. data/Plugins/Apps/Valkey/Valkey.lmm.rb +54 -42
  153. data/Plugins/Apps/Vaultwarden/Vaultwarden.conf.erb +9 -6
  154. data/Plugins/Apps/Vaultwarden/Vaultwarden.container +7 -1
  155. data/Plugins/Apps/Vaultwarden/Vaultwarden.lmm.rb +64 -29
  156. data/Plugins/Apps/Wiki.js/Wiki.js.conf.erb +1 -4
  157. data/Plugins/Apps/Wiki.js/Wiki.js.container +5 -0
  158. data/Plugins/Apps/Wiki.js/Wiki.js.lmm.rb +31 -37
  159. data/Plugins/Apps/YaCy/YaCy.conf.erb +93 -0
  160. data/Plugins/Apps/YaCy/YaCy.container +21 -0
  161. data/Plugins/Apps/YaCy/YaCy.lmm.rb +160 -0
  162. data/Plugins/Apps/ZooKeeper/ZooKeeper.container +24 -0
  163. data/Plugins/Apps/ZooKeeper/ZooKeeper.lmm.rb +68 -0
  164. data/Plugins/Apps/bitmagnet/bitmagnet.conf.erb +0 -3
  165. data/Plugins/Apps/bitmagnet/bitmagnet.lmm.rb +0 -1
  166. data/Plugins/Apps/gollum/gollum.conf.erb +2 -4
  167. data/Plugins/Apps/gollum/gollum.container +6 -0
  168. data/Plugins/Apps/gollum/gollum.lmm.rb +51 -50
  169. data/Plugins/Apps/llama.cpp/llama.cpp.container +28 -0
  170. data/Plugins/Apps/llama.cpp/llama.cpp.lmm.rb +90 -0
  171. data/Plugins/Apps/vLLM/vLLM.container +32 -0
  172. data/Plugins/Apps/vLLM/vLLM.lmm.rb +89 -0
  173. data/Plugins/OS/General/Utils.lmm.rb +26 -0
  174. data/Plugins/OS/Linux/Connection.rb +472 -0
  175. data/Plugins/OS/Linux/Debian/preseed.cfg.erb +25 -6
  176. data/Plugins/OS/Linux/Flavours.yaml +13 -0
  177. data/Plugins/OS/Linux/Grub/grub.cfg +10 -0
  178. data/Plugins/OS/Linux/HTTP.rb +32 -0
  179. data/Plugins/OS/Linux/Linux.lmm.rb +533 -187
  180. data/Plugins/OS/Linux/Packages.yaml +20 -1
  181. data/Plugins/OS/Linux/Services.yaml +8 -0
  182. data/Plugins/OS/Linux/Shell.rb +70 -0
  183. data/Plugins/OS/Linux/Syslinux/default +8 -0
  184. data/Plugins/OS/Linux/WireGuard/WireGuard.lmm.rb +83 -59
  185. data/Plugins/OS/Linux/WireGuard/wg0.conf.erb +3 -0
  186. data/Plugins/OS/Linux/openSUSE/autoinst.xml.erb +29 -3
  187. data/Plugins/OS/Linux/systemd/systemd.lmm.rb +13 -11
  188. data/Plugins/OS/Routers/Aruba/ArubaInstant.lmm.rb +6 -5
  189. data/Plugins/Platforms/GitHub.lmm.rb +73 -28
  190. data/Plugins/Platforms/GoDaddy/GoDaddy.lmm.rb +9 -6
  191. data/Plugins/Platforms/Proxmox/Proxmox.lmm.rb +402 -0
  192. data/Plugins/Platforms/Proxmox/XTerm.rb +321 -0
  193. data/Plugins/Platforms/libvirt/libvirt.lmm.rb +38 -13
  194. data/Plugins/Platforms/porkbun.lmm.rb +12 -2
  195. data/Plugins/Platforms/porkbun_spec.rb +2 -2
  196. data/Plugins/Services/DNS/AmberBit.lmm.rb +1 -1
  197. data/Plugins/Services/DNS/ArubaItDNS.lmm.rb +1 -1
  198. data/Plugins/Services/DNS/NICLV.lmm.rb +1 -1
  199. data/Plugins/Services/DNS/PowerDNS.lmm.rb +70 -68
  200. data/Plugins/Services/DNS/tonic.lmm.rb +22 -12
  201. data/lib/ConfigLMM/Framework/plugins/dns.rb +4 -3
  202. data/lib/ConfigLMM/Framework/plugins/linuxApp.rb +145 -184
  203. data/lib/ConfigLMM/Framework/plugins/nginxApp.rb +34 -17
  204. data/lib/ConfigLMM/Framework/plugins/plugin.rb +53 -181
  205. data/lib/ConfigLMM/Framework/plugins/store.rb +4 -4
  206. data/lib/ConfigLMM/Framework/variables.rb +75 -0
  207. data/lib/ConfigLMM/Framework.rb +1 -0
  208. data/lib/ConfigLMM/cli.rb +12 -6
  209. data/lib/ConfigLMM/commands/configsCommand.rb +37 -6
  210. data/lib/ConfigLMM/commands/diff.rb +33 -9
  211. data/lib/ConfigLMM/context.rb +22 -3
  212. data/lib/ConfigLMM/io/configList.rb +82 -6
  213. data/lib/ConfigLMM/io/connection.rb +143 -0
  214. data/lib/ConfigLMM/io/dhcp.rb +330 -0
  215. data/lib/ConfigLMM/io/http.rb +78 -0
  216. data/lib/ConfigLMM/io/local.rb +207 -0
  217. data/lib/ConfigLMM/io/pxe.rb +92 -0
  218. data/lib/ConfigLMM/io/ssh.rb +156 -0
  219. data/lib/ConfigLMM/io/tftp.rb +105 -0
  220. data/lib/ConfigLMM/io.rb +2 -0
  221. data/lib/ConfigLMM/secrets/envStore.rb +39 -0
  222. data/lib/ConfigLMM/secrets/fileStore.rb +43 -0
  223. data/lib/ConfigLMM/state.rb +2 -1
  224. data/lib/ConfigLMM/version.rb +2 -1
  225. data/lib/ConfigLMM.rb +1 -0
  226. data/{Examples → scripts}/configlmmAuth.sh +7 -5
  227. metadata +205 -8
@@ -6,12 +6,18 @@ Arch Linux:
6
6
  CyrusSASL: cyrus-sasl
7
7
  Dovecot: dovecot
8
8
  firewalld: firewalld
9
+ go: go
9
10
  MariaDB: mariadb
10
11
  Nextcloud: nextcloud
11
12
  nginx: nginx
13
+ nodejs: nodejs
14
+ otelcol-contrib: AUR|otelcol-contrib
12
15
  PHP-FPM: php-fpm
13
16
  php-pecl: php-pecl
14
- Podman: podman
17
+ pnpm: pnpm
18
+ Podman:
19
+ - podman
20
+ - fuse-overlayfs
15
21
  PostgreSQL: postgresql
16
22
  Postfix: postfix
17
23
  PowerDNS: powerdns
@@ -20,6 +26,7 @@ Arch Linux:
20
26
  sshd: openssh
21
27
  Valkey: redis
22
28
  WireGuard: wireguard-tools
29
+ xorriso: libisoburn
23
30
  Yarn: yarn
24
31
 
25
32
  openSUSE Leap:
@@ -31,17 +38,22 @@ openSUSE Leap:
31
38
  CyrusSASL: cyrus-sasl-plain
32
39
  Dovecot: dovecot
33
40
  firewalld: firewalld
41
+ go: go
34
42
  MariaDB: mariadb
35
43
  Nextcloud: server:php:applications|nextcloud
36
44
  nginx: nginx
45
+ nodejs: nodejs-default
46
+ otelcol-contrib: GitHub|open-telemetry/opentelemetry-collector-releases:otelcol-contrib_*_linux_amd64.rpm
37
47
  PHP-FPM:
38
48
  - php8-devel
39
49
  - php8-mbstring
40
50
  - php8-fpm
51
+ - php8-imagick
41
52
  - php8-redis
42
53
  - php8-pgsql
43
54
  - php8-mysql
44
55
  php-pecl: php8-pecl
56
+ pnpm: GitHub|pnpm/pnpm:pnpm-linux-x64
45
57
  Podman: podman
46
58
  PostgreSQL:
47
59
  - postgresql-server
@@ -57,6 +69,7 @@ openSUSE Leap:
57
69
  sshd: openssh
58
70
  Valkey: redis
59
71
  WireGuard: wireguard-tools
72
+ xorriso: xorriso
60
73
  Yarn: yarn
61
74
 
62
75
  Debian:
@@ -70,16 +83,21 @@ Debian:
70
83
  - dovecot-lmtpd
71
84
  - dovecot-submissiond
72
85
  firewalld: firewalld
86
+ go: golang
73
87
  MariaDB: mariadb-server
74
88
  Nextcloud:
75
89
  nginx: nginx
90
+ nodejs: nodejs
91
+ otelcol-contrib: GitHub|open-telemetry/opentelemetry-collector-releases:otelcol-contrib_*_linux_amd64.deb
76
92
  PHP-FPM:
77
93
  - php-mbstring
78
94
  - php-fpm
95
+ - php-imagick
79
96
  - php-redis
80
97
  - php-pgsql
81
98
  - php-mysql
82
99
  php-pecl:
100
+ pnpm: GitHub|pnpm/pnpm:pnpm-linux-x64
83
101
  Podman: podman
84
102
  PostgreSQL: postgresql
85
103
  Postfix: postfix-lmdb
@@ -93,4 +111,5 @@ Debian:
93
111
  sshd: openssh-server
94
112
  Valkey: redis
95
113
  WireGuard: wireguard
114
+ xorriso: xorriso
96
115
  Yarn: yarnpkg
@@ -0,0 +1,8 @@
1
+ Arch Linux:
2
+ sshd: sshd
3
+
4
+ openSUSE Leap:
5
+ sshd: sshd
6
+
7
+ Debian:
8
+ sshd: ssh
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'open3'
5
+
6
+ module ConfigLMM
7
+ module LMM
8
+ class LinuxShell
9
+
10
+ attr_reader :connection
11
+ attr_reader :user
12
+
13
+ def initialize(connection, user)
14
+ @connection = connection
15
+ @user = user
16
+ end
17
+
18
+ def distroID
19
+ @connection.distroID
20
+ end
21
+
22
+ def updateFile(*args, &block)
23
+ @connection.updateFile(*args, &block)
24
+ end
25
+
26
+ def exec(command, allowFailure = false, options = {})
27
+ cmd = "su --login #{@user.shellescape} --shell /usr/bin/sh --command #{command.shellescape}"
28
+ if options[:hide]
29
+ cmd = ' ' + cmd
30
+ end
31
+ @connection.exec(cmd, allowFailure, options)
32
+ end
33
+
34
+ def rm(*args)
35
+ @connection.rm(*args)
36
+ end
37
+
38
+ def fileWrite(target, data, options = {})
39
+ hide = ''
40
+ hide = ' ' if options[:hide]
41
+ self.exec("#{hide}echo #{data.shellescape} > #{target}", false, options)
42
+ end
43
+
44
+ def fileAppend(target, data, options = {})
45
+ hide = ''
46
+ hide = ' ' if options[:hide]
47
+ self.exec("#{hide}echo #{data.shellescape} >> #{target}", false, options)
48
+ end
49
+
50
+ def fileMerge(target, file, options = {})
51
+ self.exec("cat #{file.shellescape} >> #{target}", false, options)
52
+ end
53
+
54
+ def fileReplace(target, placeholder, result, options = {})
55
+ hide = ''
56
+ hide = ' ' if options[:hide]
57
+ pattern = "s|#{placeholder}|#{result.gsub('\\', '\\\\\\').gsub('&', '\\\\&').gsub('|', '\\\\|')}|"
58
+ self.exec("#{hide}sed -i #{pattern.shellescape} #{target}", false, options)
59
+ end
60
+
61
+ def createDirs(options, *paths)
62
+ exec("mkdir -p #{paths.join(' ')}", false, options)
63
+ end
64
+
65
+ def self.escapeSingleQuotes(command)
66
+ command.gsub("'", "'\"'\"'")
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,8 @@
1
+
2
+ default auto
3
+ prompt 0
4
+ timeout 1
5
+
6
+ label auto
7
+ kernel linux
8
+ append initrd=initrd splash=silent $OPTIONS
@@ -13,45 +13,64 @@ module ConfigLMM
13
13
 
14
14
  def actionWireGuardDeploy(id, target, activeState, context, options)
15
15
  self.prepareConfig(target)
16
- if target['Location'] && target['Location'] != '@me'
17
- uri = Addressable::URI.parse(target['Location'])
18
- raise Framework::PluginProcessError.new("#{id}: Unknown Protocol: #{uri.scheme}!") if uri.scheme != 'ssh'
19
- self.class.sshStart(uri) do |ssh|
20
- self.class.sshExec!(ssh, "firewall-cmd -q --permanent --add-port='#{PORT}/udp'")
21
- self.class.sshExec!(ssh, "firewall-cmd -q --add-port='#{PORT}/udp'")
22
- self.class.sshExec!(ssh, "firewall-cmd -q --permanent --zone=trusted --add-source=#{SUBNET}")
23
- self.class.sshExec!(ssh, "firewall-cmd -q --zone=trusted --add-source=#{SUBNET}")
24
- self.class.sshExec!(ssh, "firewall-cmd -q --permanent --direct --add-rule ipv4 nat POSTROUTING 0 -s #{SUBNET} ! -d #{SUBNET} -j MASQUERADE")
25
- self.class.sshExec!(ssh, "firewall-cmd -q --direct --add-rule ipv4 nat POSTROUTING 0 -s #{SUBNET} ! -d #{SUBNET} -j MASQUERADE")
26
-
27
- self.class.ensurePackages([WIREGUARD_PACKAGE], ssh)
28
- self.class.ensureServiceAutoStartOverSSH(SERVICE_NAME, ssh)
16
+ self.withConnection(target['Location'], target) do |connection|
17
+ Linux.withConnection(connection) do |linuxConnection|
18
+ linuxConnection.firewallAddPort("#{PORT}/udp", options)
19
+ linuxConnection.exec("firewall-cmd -q --permanent --zone=trusted --add-source=#{SUBNET}", options)
20
+ linuxConnection.exec("firewall-cmd -q --zone=trusted --add-source=#{SUBNET}", options)
21
+ linuxConnection.exec("firewall-cmd -q --permanent --direct --add-rule ipv4 nat POSTROUTING 0 -s #{SUBNET} ! -d #{SUBNET} -j MASQUERADE", options)
22
+ linuxConnection.exec("firewall-cmd -q --direct --add-rule ipv4 nat POSTROUTING 0 -s #{SUBNET} ! -d #{SUBNET} -j MASQUERADE", options)
23
+
24
+ linuxConnection.ensurePackage(WIREGUARD_PACKAGE, options)
25
+ linuxConnection.ensureServiceAutoStart(SERVICE_NAME, options)
29
26
 
30
27
  dir = options['output'] + '/' + id + '/etc/wireguard/'
31
28
  mkdir(dir, false)
32
29
  template = ERB.new(File.read(__dir__ + '/wg0.conf.erb'))
33
30
 
34
- if self.class.remoteFilePresent?(CONFIG_FILE, ssh)
31
+ target = target.dup
32
+ target['PrivateKey'] = context.secrets.load(target['SecretId'], 'PRIVATEKEY')
33
+ if target['PrivateKey'].nil?
34
+ target['PrivateKey'] = genkey(connection, options)
35
+ if !options['dry']
36
+ context.secrets.store(target['SecretId'], 'PRIVATEKEY', target['PrivateKey'])
37
+ context.secrets.print("Private Key", target['PrivateKey'])
38
+ end
39
+ end
40
+
41
+ if connection.filePresent?(CONFIG_FILE, { **options, 'dry' => false })
35
42
  # TODO Implement adding and removing peers
36
43
  else
37
- if !target['PrivateKey']
38
- target['PrivateKey'] = ENV['WIREGUARD_PRIVATEKEY_' + id]
39
- if !target['PrivateKey']
40
- target['PrivateKey'] = genkeyOverSSH(ssh)
41
- end
42
- end
43
- publicKey = pubkeyOverSSH(target['PrivateKey'], ssh)
44
- self.class.sshExec!(ssh, "echo '#{publicKey}' > /etc/wireguard/pubkey")
44
+ publicKey = pubkey(target['PrivateKey'], connection, options)
45
+ context.secrets.store(target['SecretId'], 'PUBLICKEY', publicKey) unless options['dry']
46
+ connection.exec("echo '#{publicKey}' > /etc/wireguard/pubkey", false, options)
47
+
45
48
  target['Peers'].each do |name, data|
46
- if !data['PublicKey']
47
- data['PrivateKey'] = genkeyOverSSH(ssh)
48
- data['PublicKey'] = pubkeyOverSSH(data['PrivateKey'], ssh)
49
- end
50
- if !data['PresharedKey']
51
- data['PresharedKey'] = ENV['WIREGUARD_PRESHAREDKEY_' + id + '_' + name]
52
- if !data['PresharedKey']
53
- data['PresharedKey'] = genpskOverSSH(ssh)
49
+ if data['SecretId']
50
+ data['PublicKey'] = context.secrets.load(data['SecretId'], 'PUBLICKEY')
51
+ data['PrivateKey'] = context.secrets.load(data['SecretId'], 'PRIVATEKEY')
52
+ if data['PublicKey'].nil?
53
+ data['PrivateKey'] = genkey(connection, options)
54
+ data['PublicKey'] = pubkey(data['PrivateKey'], connection, options)
55
+ if !options['dry']
56
+ context.secrets.store(data['SecretId'], 'PRIVATEKEY', data['PrivateKey'])
57
+ context.secrets.store(data['SecretId'], 'PUBLICKEY', data['PublicKey'])
58
+ end
54
59
  end
60
+ sharedSecretId = "#{target['SecretId'].upcase}_#{data['SecretId'].upcase}"
61
+ data['PresharedKey'] = context.secrets.load(sharedSecretId, 'PRESHAREDKEY')
62
+ if data['PresharedKey'].nil?
63
+ sharedSecretId2 = "#{data['SecretId'].upcase}_#{target['SecretId'].upcase}"
64
+ data['PresharedKey'] = context.secrets.load(sharedSecretId2, 'PRESHAREDKEY')
65
+ if data['PresharedKey'].nil?
66
+ data['PresharedKey'] = genpsk(connection, options)
67
+ context.secrets.store(sharedSecretId, 'PRESHAREDKEY', data['PresharedKey']) unless options['dry']
68
+ end
69
+ end
70
+ else
71
+ data['PrivateKey'] = genkey(connection, options)
72
+ data['PublicKey'] = pubkey(data['PrivateKey'], connection, options)
73
+ data['PresharedKey'] = genpsk(connection, options)
55
74
  end
56
75
  end
57
76
 
@@ -68,7 +87,7 @@ module ConfigLMM
68
87
  psk = otherData[pskIdB]
69
88
  else
70
89
  pskIdA = 'PresharedKey_' + name + '_' + otherName
71
- data[pskIdA] = genpskOverSSH(ssh)
90
+ data[pskIdA] = genpsk(connection, options)
72
91
  psk = data[pskIdA]
73
92
  end
74
93
  templateData['Peers'][otherName] = { 'PublicKey' => otherData['PublicKey'], 'PresharedKey' => psk }
@@ -78,59 +97,64 @@ module ConfigLMM
78
97
  end
79
98
 
80
99
  renderTemplate(template, target, dir + 'wg0.conf', options)
81
- ssh.scp.upload!(dir + 'wg0.conf', CONFIG_FILE)
100
+ connection.upload(dir + 'wg0.conf', CONFIG_FILE, options)
82
101
  end
83
102
 
103
+ linuxConnection.restartService(SERVICE_NAME, options)
84
104
  end
85
- else
86
- # TODO
87
105
  end
88
- self.startService(SERVICE_NAME, target['Location'])
89
-
90
- activeState['Status'] = State::STATUS_DEPLOYED
91
106
  end
92
107
 
93
108
  def cleanup(configs, state, context, options)
94
- cleanupType(:WireGuard, configs, state, context, options) do |item, id, state, context, options, ssh|
95
- Framework::LinuxApp.stopService(SERVICE_NAME, ssh, options[:dry])
96
- Framework::LinuxApp.disableService(SERVICE_NAME, ssh, options[:dry])
97
- Framework::LinuxApp.removePackage(WIREGUARD_PACKAGE, ssh, options[:dry])
98
-
99
- self.class.exec("firewall-cmd -q --permanent --remove-port='#{PORT}/udp'", ssh, false, options[:dry])
100
- self.class.exec("firewall-cmd -q --remove-port='#{PORT}/udp'", ssh, false, options[:dry])
101
- self.class.exec("firewall-cmd -q --permanent --zone=trusted --remove-source=#{SUBNET}", ssh, false, options[:dry])
102
- self.class.exec("firewall-cmd -q --zone=trusted --remove-source=#{SUBNET}", ssh, false, options[:dry])
103
- self.class.exec("firewall-cmd -q --permanent --direct --remove-rule ipv4 nat POSTROUTING 0 -s #{SUBNET} ! -d #{SUBNET} -j MASQUERADE", ssh, false, options[:dry])
104
- self.class.exec("firewall-cmd -q --direct --remove-rule ipv4 nat POSTROUTING 0 -s #{SUBNET} ! -d #{SUBNET} -j MASQUERADE", ssh, false, options[:dry])
109
+ cleanupType(:WireGuard, configs, state, context, options) do |item, id, state, context, options, connection|
110
+ Linux.withConnection(connection) do |linuxConnection|
111
+ linuxConnection.stopService(SERVICE_NAME, options[:dry])
112
+ linuxConnection.disableService(SERVICE_NAME, options[:dry])
113
+ linuxConnection.removePackage(WIREGUARD_PACKAGE, options[:dry])
114
+
115
+ linuxConnection.firewallRemovePort("#{PORT}/udp", options)
116
+ linuxConnection.exec("firewall-cmd -q --permanent --zone=trusted --remove-source=#{SUBNET}", false, options[:dry])
117
+ linuxConnection.exec("firewall-cmd -q --zone=trusted --remove-source=#{SUBNET}", false, options[:dry])
118
+ linuxConnection.exec("firewall-cmd -q --permanent --direct --remove-rule ipv4 nat POSTROUTING 0 -s #{SUBNET} ! -d #{SUBNET} -j MASQUERADE", false, options[:dry])
119
+ linuxConnection.exec("firewall-cmd -q --direct --remove-rule ipv4 nat POSTROUTING 0 -s #{SUBNET} ! -d #{SUBNET} -j MASQUERADE", false, options[:dry])
120
+ end
105
121
 
106
122
  state.item(id)['Status'] = State::STATUS_DELETED unless options[:dry]
107
123
 
108
124
  if options[:destroy]
109
- rm('/etc/wireguard', options[:dry], ssh)
125
+ connection.rm('/etc/wireguard', options[:dry])
110
126
 
111
127
  state.item(id)['Status'] = State::STATUS_DESTROYED unless options[:dry]
112
128
  end
113
129
  end
114
130
  end
115
131
 
116
- def genkeyOverSSH(ssh)
117
- self.class.sshExec!(ssh, 'wg genkey')
132
+ def genkey(connection, options)
133
+ key = connection.exec('wg genkey', false, options).strip
134
+ if options['dry']
135
+ key = connection.exec('wg genkey', false, { **options, 'dry' => false }).strip
136
+ end
137
+ key
118
138
  end
119
139
 
120
- def genpskOverSSH(ssh)
121
- self.class.sshExec!(ssh, 'wg genpsk')
140
+ def genpsk(connection, options)
141
+ key = connection.exec('wg genpsk', false, options).strip
142
+ if options['dry']
143
+ key = connection.exec('wg genpsk', false, { **options, 'dry' => false }).strip
144
+ end
145
+ key
122
146
  end
123
147
 
124
- def pubkeyOverSSH(privateKey, ssh)
125
- self.class.sshExec!(ssh, " echo '#{privateKey}' | wg pubkey")
148
+ def pubkey(privateKey, connection, options)
149
+ key = connection.exec(" echo '#{privateKey}' | wg pubkey", false, { **options, hide: true }).strip
150
+ if options['dry']
151
+ key = connection.exec(" echo '#{privateKey}' | wg pubkey", false, { **options, 'dry' => false, hide: true }).strip
152
+ end
153
+ key
126
154
  end
127
155
 
128
156
  def prepareConfig(target)
129
157
  target['Address'] = '172.20.0.1' unless target['Address']
130
- target['Peers'].each do |name, data|
131
- target['Peers'][name] ||= {}
132
- target['Peers'][name]['AllowedIPs'] = SUBNET unless target['Peers'][name]['AllowedIPs']
133
- end
134
158
  end
135
159
  end
136
160
  end
@@ -12,4 +12,7 @@ AllowedIPs = <%= peer['AllowedIPs'] %>
12
12
  <% if peer['Endpoint'] %>
13
13
  Endpoint = <%= peer['Endpoint'] %>:51820
14
14
  <% end %>
15
+ <% if peer['PersistentKeepalive'] %>
16
+ PersistentKeepalive = <%= peer['PersistentKeepalive'] %>
17
+ <% end %>
15
18
  <% end %>
@@ -31,9 +31,35 @@
31
31
  <dns t="map">
32
32
  <hostname><%= config['HostName'] %></hostname>
33
33
  <% if config['Domain'] %>
34
- <domain><%= config['Domain'] %></domain>
34
+ <domain><%= Addressable::IDNA.to_ascii(config['Domain']) %></domain>
35
+ <% end %>
36
+ <% if config['DefaultNetwork']['DNS'] %>
37
+ <nameservers config:type="list">
38
+ <nameserver><%= config['DefaultNetwork']['DNS'] %></nameserver>
39
+ </nameservers>
35
40
  <% end %>
36
41
  </dns>
42
+ <% if config['DefaultNetwork']['IP'] %>
43
+ <interfaces config:type="list">
44
+ <interface>
45
+ <bootproto><%= config['DefaultNetwork']['IP'] == 'dhcp' ? 'dhcp' : 'static' %></bootproto>
46
+ <name>eth0</name>
47
+ <% if config['DefaultNetwork']['IP'] != 'dhcp' %><ipaddr><%= config['DefaultNetwork']['IP'] %></ipaddr><% end %>
48
+ <startmode>auto</startmode>
49
+ </interface>
50
+ </interfaces>
51
+ <% end %>
52
+ <% if config['DefaultNetwork']['Gateway'] %>
53
+ <routing>
54
+ <routes config:type="list">
55
+ <route>
56
+ <destination>default</destination>
57
+ <device>eth0</device>
58
+ <gateway><%= config['DefaultNetwork']['Gateway'] %></gateway>
59
+ </route>
60
+ </routes>
61
+ </routing>
62
+ <% end %>
37
63
  </networking>
38
64
  <software t="map">
39
65
  <% if !config['Apps'].to_a.empty? %>
@@ -49,7 +75,7 @@
49
75
  <% if !config['Services'].to_a.empty? %>
50
76
  <enable t="list">
51
77
  <% config['Services'].each do |service| %>
52
- <service><%= service %></service>
78
+ <service><%= service.to_s %></service>
53
79
  <% end %>
54
80
  </enable>
55
81
  <% end %>
@@ -62,7 +88,7 @@
62
88
  <users config:type="list">
63
89
  <% config['Users'].each do |user, info| %>
64
90
  <user>
65
- <username>root</username>
91
+ <username><%= user %></username>
66
92
  <% if info['PasswordHash'] %>
67
93
  <encrypted config:type="boolean">true</encrypted>
68
94
  <user_password><%= info['PasswordHash'] %></user_password>
@@ -7,20 +7,22 @@ module ConfigLMM
7
7
  USER_SERVICE_DIR = '/etc/systemd/system/user@.service.d/'
8
8
 
9
9
  def actionSystemdDeploy(id, target, activeState, context, options)
10
- if target['Location'] && target['Location'] != '@me'
11
- if target['UserCgroups']
12
- uri = Addressable::URI.parse(target['Location'])
13
- raise Framework::PluginProcessError.new("#{id}: Unknown Protocol: #{uri.scheme}!") if uri.scheme != 'ssh'
14
- self.class.sshStart(uri) do |ssh|
15
- self.class.sshExec!(ssh, "mkdir -p #{USER_SERVICE_DIR}")
16
- ssh.scp.upload!(__dir__ + '/user-0.slice', SYSTEMD_CONFIG_PATH)
17
- ssh.scp.upload!(__dir__ + '/user@.service.d/delegate.conf', USER_SERVICE_DIR)
10
+ self.withConnection(target['Location'], target) do |connection|
11
+ Linux.withConnection(connection) do |linuxConnection|
12
+ if target['UserCgroups']
13
+ linuxConnection.createDirs(options, USER_SERVICE_DIR)
14
+ linuxConnection.upload(__dir__ + '/user-0.slice', SYSTEMD_CONFIG_PATH, options)
15
+ linuxConnection.upload(__dir__ + '/user@.service.d/delegate.conf', USER_SERVICE_DIR, options)
16
+ end
17
+ if target['InstallServices']
18
+ target['InstallServices'].each do |file, data|
19
+ linuxConnection.upload(file, SYSTEMD_CONFIG_PATH, options)
20
+ linuxConnection.reloadServiceManager(options)
21
+ linuxConnection.ensureServiceAutoStart(File.basename(file), options)
22
+ end
18
23
  end
19
24
  end
20
- else
21
- # TODO
22
25
  end
23
-
24
26
  end
25
27
 
26
28
  end
@@ -46,7 +46,7 @@ module ConfigLMM
46
46
  end
47
47
 
48
48
  creds = parseLocation(target['Location'])
49
- password = ENV['ARUBA_INSTANT_PASSWORD']
49
+ password = context.secrets.load(target['SecretId'], 'PASSWORD')
50
50
 
51
51
  # Couldn't get it working with net-ssh gem so using `ssh` as workaround
52
52
  # Net::SSH.start(creds[:hostname], creds[:user], password: password, port: creds[:port]) do |ssh|
@@ -128,14 +128,15 @@ module ConfigLMM
128
128
  end
129
129
 
130
130
  def authenticate(actionMethod, target, activeState, context, options)
131
- if ENV['ARUBA_INSTANT_PASSWORD'].to_s.empty? || ENV['ARUBA_INSTANT_PASSWORD'].to_s.empty?
132
- prompt.error('Set your Aruba Instant SSH password to ARUBA_INSTANT_PASSWORD as Environment Variable')
133
- raise Framework::PluginPrerequisite.new('Need ARUBA_INSTANT_PASSWORD')
131
+ authSecret = context.secrets.load(target['SecretId'], 'PASSWORD')
132
+ if authSecret.to_s.empty?
133
+ prompt.error("Set your Aruba Instant SSH password in #{target['SecretId']}_PASSWORD")
134
+ raise Framework::PluginPrerequisite.new('Need Aruba Instant password!')
134
135
  else
135
136
  if !target['Location']
136
137
  raise Framework::PluginProcessError.new('Location must be provided!')
137
138
  end
138
- checkSSHAuth!(target['Location'], ENV['ARUBA_INSTANT_PASSWORD'])
139
+ checkSSHAuth!(target['Location'], authSecret)
139
140
  end
140
141
  true
141
142
  end
@@ -5,53 +5,98 @@ module ConfigLMM
5
5
  module LMM
6
6
  class GitHub < Framework::Plugin
7
7
 
8
- def actionGitHubOrganizationRefresh(id, target, activeState, context, options)
9
-
10
- client = Octokit::Client.new(:access_token => ENV['GITHUB_TOKEN'])
11
- orgs = client.organizations.select { |org| org[:login] == target['Name'] }
12
- if orgs.empty?
13
- prompt.say("Didn\'t find organization with name #{target['Name']}")
14
- prompt.say('You need to create it manually - https://github.com/organizations/plan')
15
- raise Framework::PluginPrerequisite.new('Organization must exist!')
8
+ def actionGitHubRefresh(id, target, activeState, context, options)
9
+ if !target['Organizations'].to_h.empty?
10
+ activeState['Organizations'] = {}
11
+ target['Organizations'].each do |name, organization|
12
+ organizationRefresh(name, target, activeState, context, options)
13
+ end
16
14
  end
15
+ end
17
16
 
18
- raise "This shouldn't happen!" if orgs.length != 1
19
-
20
- activeState.clear
17
+ def actionGitHubDiff(id, target, activeState, context, options)
18
+ state = prepareState(target, activeState)
19
+ shouldMatch(id, state, 'Organizations', target, 'Organizations')
20
+ end
21
21
 
22
- orgs.first.each do |name, value|
23
- activeState[name.to_s] = value
22
+ def actionGitHubDeploy(id, target, activeState, context, options)
23
+ actionGitHubDiff(id, target, activeState, context, options)
24
+ diff.each do |name, states|
25
+ # TODO FIXME
24
26
  end
25
27
  end
26
28
 
27
- def actionGitHubOrganizationDiff(id, target, activeState, context, options)
28
- shouldMatch(id, 'Name', 'login', target, activeState)
29
- shouldMatch(id, 'Description', 'description', target, activeState)
29
+ def prepareState(target, activeState)
30
+ state = activeState.dup
31
+ state['Organizations'] ||= {}
32
+ state['Organizations'].each do |name, data|
33
+ #state['Organizations'][name]['Name'] = state['Organizations'][name].delete('Login')
34
+ state['Organizations'][name]['Description'] = state['Organizations'][name].delete('description')
35
+ end
36
+ state
30
37
  end
31
38
 
32
- def actionGitHubOrganizationDeploy(id, target, activeState, context, options)
33
- actionGitHubOrganizationDiff(id, target, activeState, context, options)
34
- diff.each do |name, states|
35
- if name == 'Name'
36
- # TODO
37
- elsif name == 'Description'
38
- # TODO
39
+ def organizationRefresh(name, target, activeState, context, options)
40
+ authToken = context.secrets.load(target['SecretId'], 'TOKEN')
41
+ authToken = context.secrets.load('GITHUB', 'TOKEN') if authToken.nil?
42
+ client = Octokit::Client.new(:access_token => authToken)
43
+
44
+ allOrgs = client.organizations
45
+ if allOrgs.empty?
46
+ # Fine-grained access token never returns any orgs
47
+ org = client.organization(name)
48
+ else
49
+ orgs = allOrgs.select { |org| org[:login] == name }
50
+ if orgs.empty?
51
+ prompt.say("Didn\'t find organization with name #{name}")
52
+ prompt.say('You need to create it manually - https://github.com/organizations/plan')
53
+ raise Framework::PluginPrerequisite.new('Organization must exist!')
39
54
  end
55
+ org = orgs.first
56
+ end
57
+
58
+ activeState['Organizations'][org.login] ||= {}
59
+
60
+ org.each do |name, value|
61
+ data = value
62
+ data = value.to_h if value && value.respond_to?(:to_h)
63
+ data = value.to_s if value.is_a?(Time)
64
+ activeState['Organizations'][org.login][name.to_s] = data
40
65
  end
41
- # TODO FIXME
42
- raise 'Not implemented!'
43
66
  end
44
67
 
45
68
  def authenticate(actionMethod, target, activeState, context, options)
46
- authToken = ENV['GITHUB_TOKEN']
69
+ authToken = context.secrets.load(target['SecretId'], 'TOKEN')
70
+ authToken = context.secrets.load('GITHUB', 'TOKEN') if authToken.nil?
47
71
  if authToken.to_s.empty?
48
72
  prompt.say('Open https://github.com/settings/tokens and create a token!')
49
- prompt.say('Then set it\'s value to GITHUB_TOKEN as Environment Variable')
50
- raise Framework::PluginPrerequisite.new('Need GITHUB_TOKEN!')
73
+ prompt.say("Then set it\'s value in #{target['SecretId']}_TOKEN")
74
+ raise Framework::PluginPrerequisite.new('Need GitHub token!')
51
75
  end
52
76
  true
53
77
  end
54
78
 
79
+ def self.getReleases(repoId, logger, context, options)
80
+ response = HTTP.get("https://api.github.com/repos/#{repoId}/releases")
81
+ if response.status.success?
82
+ releases = response.parse
83
+ releases.reject! { |release| release['draft'] || release['prerelease'] }
84
+ return releases
85
+ end
86
+ logger.error("Failed to load GitHub release for #{repoId}")
87
+ raise response
88
+ end
89
+
90
+ def self.getReleaseAsset(name, releases)
91
+ pattern = name.gsub('.', '\\.').gsub('*', '.*')
92
+ releases.each do |release|
93
+ release['assets'].each do |asset|
94
+ return asset if asset['name'].match?(pattern)
95
+ end
96
+ end
97
+ raise "Couldn't find GitHub asset #{name}!"
98
+ end
99
+
55
100
  end
56
101
  end
57
102
  end