ConfigLMM 0.2.0 → 0.4.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 (121) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -0
  3. data/Examples/Implemented.mm.yaml +75 -1
  4. data/Plugins/Apps/Authentik/Authentik-ProxyOutpost.container +14 -0
  5. data/Plugins/Apps/Authentik/Authentik-Server.container +19 -0
  6. data/Plugins/Apps/Authentik/Authentik-Worker.container +18 -0
  7. data/Plugins/Apps/Authentik/Authentik.conf.erb +42 -0
  8. data/Plugins/Apps/Authentik/Authentik.lmm.rb +95 -0
  9. data/Plugins/Apps/BookStack/BookStack.conf.erb +41 -0
  10. data/Plugins/Apps/BookStack/BookStack.container +15 -0
  11. data/Plugins/Apps/BookStack/BookStack.lmm.rb +80 -0
  12. data/Plugins/Apps/Cassandra/Cassandra.lmm.rb +41 -0
  13. data/Plugins/Apps/Discourse/Discourse-Sidekiq.container +17 -0
  14. data/Plugins/Apps/Discourse/Discourse.conf.erb +41 -0
  15. data/Plugins/Apps/Discourse/Discourse.container +17 -0
  16. data/Plugins/Apps/Discourse/Discourse.lmm.rb +95 -0
  17. data/Plugins/Apps/Dovecot/Dovecot.lmm.rb +171 -0
  18. data/Plugins/Apps/ERPNext/ERPNext-Frontend.container +19 -0
  19. data/Plugins/Apps/ERPNext/ERPNext-Queue.container +17 -0
  20. data/Plugins/Apps/ERPNext/ERPNext-Scheduler.container +17 -0
  21. data/Plugins/Apps/ERPNext/ERPNext-Websocket.container +19 -0
  22. data/Plugins/Apps/ERPNext/ERPNext.container +18 -0
  23. data/Plugins/Apps/ERPNext/ERPNext.lmm.rb +193 -0
  24. data/Plugins/Apps/ERPNext/ERPNext.network +12 -0
  25. data/Plugins/Apps/ERPNext/sites/apps.json +10 -0
  26. data/Plugins/Apps/ERPNext/sites/apps.txt +3 -0
  27. data/Plugins/Apps/ERPNext/sites/common_site_config.json +11 -0
  28. data/Plugins/Apps/GitLab/GitLab.container +18 -0
  29. data/Plugins/Apps/GitLab/GitLab.lmm.rb +100 -0
  30. data/Plugins/Apps/LetsEncrypt/LetsEncrypt.lmm.rb +57 -0
  31. data/Plugins/Apps/LetsEncrypt/hooks/dovecot.sh +2 -0
  32. data/Plugins/Apps/LetsEncrypt/hooks/nginx.sh +2 -0
  33. data/Plugins/Apps/LetsEncrypt/hooks/postfix.sh +2 -0
  34. data/Plugins/Apps/LetsEncrypt/renew-certificates.service +7 -0
  35. data/Plugins/Apps/LetsEncrypt/renew-certificates.timer +12 -0
  36. data/Plugins/Apps/LetsEncrypt/rfc2136.ini +11 -0
  37. data/Plugins/Apps/MariaDB/MariaDB.lmm.rb +115 -0
  38. data/Plugins/Apps/Matrix/Element.container +14 -0
  39. data/Plugins/Apps/Matrix/Matrix.conf.erb +49 -5
  40. data/Plugins/Apps/Matrix/Matrix.lmm.rb +86 -1
  41. data/Plugins/Apps/Matrix/Synapse.container +17 -0
  42. data/Plugins/Apps/Matrix/config.json +50 -0
  43. data/Plugins/Apps/Matrix/homeserver.yaml +70 -0
  44. data/Plugins/Apps/Matrix/log.config +30 -0
  45. data/Plugins/Apps/Nextcloud/Nextcloud.conf.erb +48 -10
  46. data/Plugins/Apps/Nextcloud/Nextcloud.lmm.rb +83 -1
  47. data/Plugins/Apps/Nextcloud/config.php +18 -0
  48. data/Plugins/Apps/Nginx/conf.d/configlmm.conf +71 -0
  49. data/Plugins/Apps/Nginx/config-lmm/errors.conf +11 -5
  50. data/Plugins/Apps/Nginx/config-lmm/proxy.conf +5 -1
  51. data/Plugins/Apps/Nginx/main.conf.erb +31 -0
  52. data/Plugins/Apps/Nginx/nginx.conf +3 -68
  53. data/Plugins/Apps/Nginx/nginx.lmm.rb +83 -22
  54. data/Plugins/Apps/Nginx/proxy.conf.erb +13 -3
  55. data/Plugins/Apps/Odoo/Odoo.conf.erb +30 -13
  56. data/Plugins/Apps/Odoo/Odoo.container +18 -0
  57. data/Plugins/Apps/Odoo/Odoo.lmm.rb +62 -2
  58. data/Plugins/Apps/Odoo/odoo.conf +37 -0
  59. data/Plugins/Apps/OpenVidu/Ingress.container +18 -0
  60. data/Plugins/Apps/OpenVidu/OpenVidu.conf.erb +34 -0
  61. data/Plugins/Apps/OpenVidu/OpenVidu.container +16 -0
  62. data/Plugins/Apps/OpenVidu/OpenVidu.lmm.rb +90 -0
  63. data/Plugins/Apps/OpenVidu/OpenViduCall.conf.erb +35 -0
  64. data/Plugins/Apps/OpenVidu/OpenViduCall.container +15 -0
  65. data/Plugins/Apps/OpenVidu/ingress.yaml +10 -0
  66. data/Plugins/Apps/OpenVidu/livekit.yaml +13 -0
  67. data/Plugins/Apps/PHP-FPM/PHP-FPM.lmm.rb +95 -0
  68. data/Plugins/Apps/Peppermint/Peppermint.conf.erb +60 -0
  69. data/Plugins/Apps/Peppermint/Peppermint.container +15 -0
  70. data/Plugins/Apps/Peppermint/Peppermint.lmm.rb +58 -0
  71. data/Plugins/Apps/Postfix/Postfix.lmm.rb +165 -31
  72. data/Plugins/Apps/Postfix/smtpd.conf +3 -0
  73. data/Plugins/Apps/PostgreSQL/PostgreSQL.lmm.rb +242 -24
  74. data/Plugins/Apps/Roundcube/Roundcube.conf.erb +75 -0
  75. data/Plugins/Apps/Roundcube/Roundcube.lmm.rb +145 -0
  76. data/Plugins/Apps/SSH/SSH.lmm.rb +51 -0
  77. data/Plugins/Apps/Tunnel/tunnel.lmm.rb +63 -0
  78. data/Plugins/Apps/Tunnel/tunnelTCP.service +9 -0
  79. data/Plugins/Apps/Tunnel/tunnelTCP.socket +9 -0
  80. data/Plugins/Apps/Tunnel/tunnelUDP.service +9 -0
  81. data/Plugins/Apps/Tunnel/tunnelUDP.socket +9 -0
  82. data/Plugins/Apps/UVdesk/UVdesk.conf.erb +52 -0
  83. data/Plugins/Apps/UVdesk/UVdesk.lmm.rb +85 -0
  84. data/Plugins/Apps/Valkey/Valkey.lmm.rb +34 -1
  85. data/Plugins/Apps/Vaultwarden/Vaultwarden.conf.erb +35 -18
  86. data/Plugins/Apps/Vaultwarden/Vaultwarden.container +16 -0
  87. data/Plugins/Apps/Vaultwarden/Vaultwarden.lmm.rb +46 -3
  88. data/Plugins/Apps/Wiki.js/Wiki.js.conf.erb +42 -0
  89. data/Plugins/Apps/Wiki.js/Wiki.js.container +15 -0
  90. data/Plugins/Apps/Wiki.js/Wiki.js.lmm.rb +61 -0
  91. data/Plugins/Apps/gollum/gollum.conf.erb +84 -19
  92. data/Plugins/Apps/gollum/gollum.container +15 -0
  93. data/Plugins/Apps/gollum/gollum.lmm.rb +48 -11
  94. data/Plugins/OS/Linux/Debian/preseed.cfg.erb +62 -0
  95. data/Plugins/OS/Linux/Distributions.yaml +42 -0
  96. data/Plugins/OS/Linux/Flavours.yaml +11 -0
  97. data/Plugins/OS/Linux/Linux.lmm.rb +362 -41
  98. data/Plugins/OS/Linux/Packages.yaml +88 -5
  99. data/Plugins/OS/Linux/Proxmox/answer.toml.erb +30 -0
  100. data/Plugins/OS/Linux/WireGuard/WireGuard.lmm.rb +137 -0
  101. data/Plugins/OS/Linux/WireGuard/wg0.conf.erb +15 -0
  102. data/Plugins/OS/Linux/systemd/systemd.lmm.rb +28 -0
  103. data/Plugins/OS/Linux/systemd/user-0.slice +9 -0
  104. data/Plugins/OS/Linux/systemd/user@.service.d/delegate.conf +3 -0
  105. data/Plugins/Platforms/GoDaddy/GoDaddy.lmm.rb +7 -3
  106. data/Plugins/Platforms/libvirt/libvirt.lmm.rb +3 -2
  107. data/Plugins/Services/DNS/PowerDNS.lmm.rb +158 -8
  108. data/README.md +6 -0
  109. data/bootstrap.sh +92 -0
  110. data/lib/ConfigLMM/Framework/plugins/dns.rb +1 -2
  111. data/lib/ConfigLMM/Framework/plugins/linuxApp.rb +249 -45
  112. data/lib/ConfigLMM/Framework/plugins/nginxApp.rb +56 -7
  113. data/lib/ConfigLMM/Framework/plugins/plugin.rb +112 -16
  114. data/lib/ConfigLMM/cli.rb +3 -1
  115. data/lib/ConfigLMM/commands/cleanup.rb +1 -0
  116. data/lib/ConfigLMM/commands/configsCommand.rb +3 -1
  117. data/lib/ConfigLMM/io/configList.rb +3 -1
  118. data/lib/ConfigLMM/state.rb +10 -2
  119. data/lib/ConfigLMM/version.rb +1 -1
  120. metadata +82 -3
  121. data/Plugins/Apps/Nginx/main.conf +0 -30
@@ -0,0 +1,95 @@
1
+
2
+ module ConfigLMM
3
+ module LMM
4
+ class Discourse < Framework::NginxApp
5
+
6
+ USER = 'discourse'
7
+ HOME_DIR = '/var/lib/discourse'
8
+ HOST_IP = '10.0.2.2'
9
+
10
+ def actionDiscourseDeploy(id, target, activeState, context, options)
11
+ raise Framework::PluginProcessError.new('Domain field must be set!') unless target['Domain']
12
+
13
+ target['Database'] ||= {}
14
+ if target['Location'] && target['Location'] != '@me'
15
+ uri = Addressable::URI.parse(target['Location'])
16
+ raise Framework::PluginProcessError.new("#{id}: Unknown Protocol: #{uri.scheme}!") if uri.scheme != 'ssh'
17
+
18
+ self.class.sshStart(uri) do |ssh|
19
+
20
+ dbPassword = self.configurePostgreSQL(target['Database'], ssh)
21
+ distroInfo = Framework::LinuxApp.currentDistroInfo(ssh)
22
+ Framework::LinuxApp.configurePodmanServiceOverSSH(USER, HOME_DIR, 'Discourse', distroInfo, ssh)
23
+ self.class.sshExec!(ssh, "su --login #{USER} --shell /bin/sh --command 'mkdir -p ~/data ~/sidekiq'")
24
+
25
+ path = Framework::LinuxApp::SYSTEMD_CONTAINERS_PATH.gsub('~', HOME_DIR)
26
+ self.class.exec("echo 'DISCOURSE_DATABASE_HOST=#{HOST_IP}' > #{path}/Discourse.env", ssh)
27
+ self.class.exec("echo 'DISCOURSE_DATABASE_NAME=#{USER}' >> #{path}/Discourse.env", ssh)
28
+ self.class.exec(" echo 'DISCOURSE_DATABASE_USER=#{USER}' >> #{path}/Discourse.env", ssh)
29
+ self.class.exec(" echo 'DISCOURSE_DATABASE_PASSWORD=#{dbPassword}' >> #{path}/Discourse.env", ssh)
30
+ self.class.exec("echo 'DISCOURSE_HOST=#{target['Domain']}' >> #{path}/Discourse.env", ssh)
31
+
32
+ self.class.exec("echo 'DISCOURSE_REDIS_HOST=#{HOST_IP}' >> #{path}/Discourse.env", ssh)
33
+ self.class.exec(" echo 'DISCOURSE_REDIS_PASSWORD=#{ENV['REDIS_PASSWORD']}' >> #{path}/Discourse.env", ssh)
34
+
35
+ if target['SMTP']
36
+ host = target['SMTP']['Host']
37
+ host = HOST_IP if ['localhost', '127.0.0.1'].include?(host)
38
+ self.class.exec("echo 'DISCOURSE_SMTP_HOST=#{host}' >> #{path}/Discourse.env", ssh)
39
+ self.class.exec("echo 'DISCOURSE_SMTP_PORT_NUMBER=#{target['SMTP']['Port']}' >> #{path}/Discourse.env", ssh)
40
+ self.class.exec(" echo 'DISCOURSE_SMTP_USER=#{target['SMTP']['Username']}' >> #{path}/Discourse.env", ssh)
41
+ self.class.exec(" echo 'DISCOURSE_SMTP_PASSWORD=#{ENV['DISCOURSE_SMTP_PASSWORD']}' >> #{path}/Discourse.env", ssh)
42
+ auth = target['SMTP']['Auth'].to_s.downcase
43
+ auth = 'plain' if auth.empty?
44
+ self.class.exec("echo 'DISCOURSE_SMTP_AUTH=#{auth}' >> #{path}/Discourse.env", ssh)
45
+ if target['SMTP']['Port'] == 465
46
+ self.class.exec("echo 'DISCOURSE_EXTRA_CONF_CONTENT=smtp_force_tls = true' >> #{path}/Discourse.env", ssh)
47
+ end
48
+ end
49
+
50
+ self.class.exec(" echo 'DISCOURSE_PRECOMPILE_ASSETS=no' >> #{path}/Discourse.env", ssh)
51
+ self.class.exec("echo 'CHEAP_SOURCE_MAPS=1' >> #{path}/Discourse.env", ssh)
52
+ self.class.exec("echo 'JOBS=1' >> #{path}/Discourse.env", ssh)
53
+
54
+ self.class.exec("chown #{USER}:#{USER} #{path}/Discourse.env", ssh)
55
+ self.class.exec("chmod 600 #{path}/Discourse.env", ssh)
56
+
57
+ ssh.scp.upload!(__dir__ + '/Discourse.container', path)
58
+ ssh.scp.upload!(__dir__ + '/Discourse-Sidekiq.container', path)
59
+ self.class.exec("systemctl --user --machine=#{USER}@ daemon-reload", ssh)
60
+ self.class.exec("systemctl --user --machine=#{USER}@ restart Discourse", ssh)
61
+ self.class.exec("systemctl --user --machine=#{USER}@ restart Discourse-Sidekiq", ssh)
62
+
63
+ Framework::LinuxApp.ensurePackages([NGINX_PACKAGE], ssh)
64
+ Framework::LinuxApp.ensureServiceAutoStartOverSSH(NGINX_PACKAGE, ssh)
65
+ self.class.prepareNginxConfig(target, ssh)
66
+ self.writeNginxConfig(__dir__, 'Discourse', id, target, state, context, options)
67
+ self.deployNginxConfig(id, target, activeState, context, options)
68
+ Framework::LinuxApp.startServiceOverSSH(NGINX_PACKAGE, ssh)
69
+
70
+ containers = JSON.parse(self.class.exec("su --login #{USER} --shell /usr/bin/sh --command 'podman ps --format json --filter name=^Discourse$'", ssh).strip)
71
+ raise 'Failed to find container!' if containers.empty?
72
+ if !target['Plugins'].to_a.empty?
73
+ target['Plugins'].each do |plugin|
74
+ self.class.exec("su --login #{USER} --shell /usr/bin/sh --command \"podman exec --workdir /opt/bitnami/discourse #{containers.first['Id']} sh -c 'RAILS_ENV=production bundle exec rake plugin:install repo=#{plugin}'\"", ssh, true)
75
+ end
76
+ end
77
+
78
+ self.class.exec("su --login #{USER} --shell /usr/bin/sh --command \"podman exec --workdir /opt/bitnami/discourse #{containers.first['Id']} sh -c 'RAILS_ENV=production CHEAP_SOURCE_MAPS=1 JOBS=1 bundle exec rake assets:precompile'\"", ssh)
79
+ end
80
+ else
81
+ # TODO
82
+ end
83
+ end
84
+
85
+ def configurePostgreSQL(settings, ssh)
86
+ password = SecureRandom.alphanumeric(20)
87
+ PostgreSQL.createRemoteUserAndDBOverSSH(settings, USER, password, ssh)
88
+ PostgreSQL.createExtensions(settings, USER, ['hstore', 'pg_trgm'], ssh)
89
+ password
90
+ end
91
+
92
+ end
93
+ end
94
+ end
95
+
@@ -4,13 +4,184 @@ module ConfigLMM
4
4
  class Dovecot < Framework::Plugin
5
5
  PACKAGE_NAME = 'Dovecot'
6
6
  SERVICE_NAME = 'dovecot'
7
+ DOVECOT_DIR = '/etc/dovecot/'
8
+ EMAIL_HOME = '/var/lib/email'
9
+ EMAIL_USER = 'email'
7
10
 
8
11
  def actionDovecotDeploy(id, target, activeState, context, options)
9
12
  plugins[:Linux].ensurePackage(PACKAGE_NAME, target['Location'])
10
13
  plugins[:Linux].ensureServiceAutoStart(SERVICE_NAME, target['Location'])
14
+
15
+ if target['Location'] && target['Location'] != '@me'
16
+ uri = Addressable::URI.parse(target['Location'])
17
+ raise Framework::PluginProcessError.new("#{id}: Unknown Protocol: #{uri.scheme}!") if uri.scheme != 'ssh'
18
+
19
+ self.class.sshStart(uri) do |ssh|
20
+ distroInfo = Framework::LinuxApp.currentDistroInfo(ssh)
21
+ addUserCmd = "#{distroInfo['CreateServiceUser']} --home-dir '#{EMAIL_HOME}' --create-home --comment 'Dovecot EMail' #{EMAIL_USER}"
22
+ self.class.sshExec!(ssh, addUserCmd, true)
23
+ uid = self.class.sshExec!(ssh, "id -u #{EMAIL_USER}").strip
24
+
25
+ cmd = "sed -i 's|^#mail_uid =.*|mail_uid = #{uid}|' #{DOVECOT_DIR}conf.d/10-mail.conf"
26
+ self.class.sshExec!(ssh, cmd)
27
+ cmd = "sed -i 's|^#mail_gid =.*|mail_gid = #{uid}|' #{DOVECOT_DIR}conf.d/10-mail.conf"
28
+ self.class.sshExec!(ssh, cmd)
29
+ cmd = "sed -i 's|^#mail_location =.*|mail_location = maildir:~/Mail|' #{DOVECOT_DIR}conf.d/10-mail.conf"
30
+ self.class.sshExec!(ssh, cmd)
31
+
32
+ if !target['Protocols'].to_a.empty?
33
+ updateRemoteFile(ssh, DOVECOT_DIR + 'dovecot.conf', options) do |configLines|
34
+ configLines << "protocols = #{target['Protocols'].join(' ')}\n"
35
+ end
36
+ end
37
+
38
+ updateRemoteFile(ssh, DOVECOT_DIR + 'conf.d/10-mail.conf', options) do |configLines|
39
+ configLines << "mail_home = #{EMAIL_HOME}/emails/%u\n"
40
+ configLines << "first_valid_uid = #{uid}\n"
41
+ configLines << "last_valid_uid = #{uid}\n"
42
+ end
43
+
44
+ self.class.cutConfigSection(DOVECOT_DIR + 'conf.d/10-master.conf', 'service lmtp', options, ssh)
45
+ updateRemoteFile(ssh, DOVECOT_DIR + 'conf.d/10-master.conf', options) do |configLines|
46
+ configLines << "service lmtp {\n"
47
+ configLines << " unix_listener lmtp {\n"
48
+ configLines << " user = postfix\n"
49
+ configLines << " group = postfix\n"
50
+ configLines << " mode = 0600\n"
51
+ configLines << " }\n"
52
+ configLines << "}\n"
53
+ end
54
+
55
+ self.class.cutConfigSection(DOVECOT_DIR + 'conf.d/15-mailboxes.conf', 'namespace inbox', options, ssh)
56
+ updateRemoteFile(ssh, DOVECOT_DIR + 'conf.d/15-mailboxes.conf', options) do |configLines|
57
+ configLines << "namespace inbox {\n"
58
+ configLines << " mailbox Drafts {\n"
59
+ configLines << " special_use = \\Drafts\n"
60
+ configLines << " auto = subscribe\n"
61
+ configLines << " }\n"
62
+ #configLines << " mailbox Junk {\n"
63
+ #configLines << " special_use = \\Junk\n"
64
+ #configLines << " auto = subscribe\n"
65
+ #configLines << " }\n"
66
+ configLines << " mailbox Trash {\n"
67
+ configLines << " special_use = \\Trash\n"
68
+ configLines << " auto = subscribe\n"
69
+ configLines << " }\n"
70
+ configLines << " mailbox Sent {\n"
71
+ configLines << " special_use = \\Sent\n"
72
+ configLines << " auto = subscribe\n"
73
+ configLines << " }\n"
74
+ configLines << "}\n"
75
+ end
76
+
77
+ Framework::LinuxApp.firewallAddService('imaps', ssh)
78
+
79
+ cmd = "sed -i 's|^!include auth-system.conf.ext|#!include auth-system.conf.ext|' #{DOVECOT_DIR}conf.d/10-auth.conf"
80
+ self.class.sshExec!(ssh, cmd)
81
+
82
+ if target['OAuth2']
83
+ cmd = "sed -i 's|auth_mechanisms =.*|auth_mechanisms = xoauth2 oauthbearer|' #{DOVECOT_DIR}conf.d/10-auth.conf"
84
+ self.class.sshExec!(ssh, cmd)
85
+
86
+ updateRemoteFile(ssh, DOVECOT_DIR + 'conf.d/10-auth.conf', options) do |configLines|
87
+ configLines << "userdb {\n"
88
+ configLines << " driver = static\n"
89
+ configLines << " args = allow_all_users=yes\n"
90
+ configLines << "}\n"
91
+ configLines << "passdb {\n"
92
+ configLines << " driver = oauth2\n"
93
+ configLines << " mechanisms = xoauth2 oauthbearer\n"
94
+ configLines << " args = #{DOVECOT_DIR}dovecot-oauth2.conf.ext\n"
95
+ configLines << "}\n"
96
+ end
97
+
98
+ updateRemoteFile(ssh, DOVECOT_DIR + 'dovecot-oauth2.conf.ext', options) do |configLines|
99
+ # Need v2.3.16+
100
+ #configLines << "openid_configuration_url = #{target['OAuth2']['OIDC']}\n"
101
+ if target['OAuth2']['TokenInfo']
102
+ configLines << "tokeninfo_url = #{target['OAuth2']['TokenInfo']}\n"
103
+ end
104
+ if target['OAuth2']['Introspection']
105
+ configLines << "introspection_url = #{target['OAuth2']['Introspection']}\n"
106
+ end
107
+ if target['OAuth2']['ClientID']
108
+ configLines << "client_id = #{target['OAuth2']['ClientID']}\n"
109
+ end
110
+ if ENV['DOVECOT_OAUTH2_SECRET']
111
+ configLines << "client_secret = #{ENV['DOVECOT_OAUTH2_SECRET']}\n"
112
+ end
113
+ end
114
+ else
115
+ cmd = "sed -i 's|auth_mechanisms =.*|auth_mechanisms = plain|' #{DOVECOT_DIR}conf.d/10-auth.conf"
116
+ self.class.sshExec!(ssh, cmd)
117
+
118
+ updateRemoteFile(ssh, DOVECOT_DIR + 'conf.d/10-auth.conf', options) do |configLines|
119
+ configLines << "auth_username_format = %u\n"
120
+ configLines << "userdb {\n"
121
+ configLines << " driver = static\n"
122
+ configLines << " args = allow_all_users=yes\n"
123
+ configLines << "}\n"
124
+ configLines << "passdb {\n"
125
+ configLines << " driver = passwd-file\n"
126
+ configLines << " args = #{DOVECOT_DIR}passwords\n"
127
+ configLines << "}\n"
128
+ end
129
+ self.class.sshExec!(ssh, "touch #{DOVECOT_DIR}passwords")
130
+ self.class.sshExec!(ssh, "chown dovecot:dovecot #{DOVECOT_DIR}passwords")
131
+ self.class.sshExec!(ssh, "chmod 600 #{DOVECOT_DIR}passwords")
132
+ end
133
+
134
+ certDir = Framework::LinuxApp.createCertificateOverSSH(ssh)
135
+ updateRemoteFile(ssh, DOVECOT_DIR + 'conf.d/10-ssl.conf', options) do |configLines|
136
+ configLines << "ssl_cert = <#{certDir}fullchain.pem\n"
137
+ configLines << "ssl_key = <#{certDir}privkey.pem\n"
138
+ end
139
+ end
140
+ else
141
+ # TODO
142
+ end
143
+
11
144
  plugins[:Linux].startService(SERVICE_NAME, target['Location'])
145
+
146
+ activeState['Status'] = State::STATUS_DEPLOYED
12
147
  end
13
148
 
149
+ def cleanup(configs, state, context, options)
150
+ cleanupType(:Dovecot, configs, state, context, options) do |item, id, state, context, options, ssh|
151
+ Framework::LinuxApp.stopService(SERVICE_NAME, ssh, options[:dry])
152
+ Framework::LinuxApp.firewallRemoveService('imaps', ssh, options[:dry])
153
+ Framework::LinuxApp.removePackage(PACKAGE_NAME, ssh, options[:dry])
154
+
155
+ state.item(id)['Status'] = State::STATUS_DELETED unless options[:dry]
156
+
157
+ if options[:destroy]
158
+ Framework::LinuxApp.deleteUserAndGroup(EMAIL_USER, ssh, options[:dry])
159
+
160
+ state.item(id)['Status'] = State::STATUS_DESTROYED unless options[:dry]
161
+ end
162
+ end
163
+ end
164
+
165
+ def self.cutConfigSection(file, sectionStart, options, ssh)
166
+ localFile = options['output'] + '/' + SecureRandom.alphanumeric(10)
167
+ File.write(localFile, '')
168
+ self.sshExec!(ssh, "touch #{file}")
169
+ ssh.scp.download!(file, localFile)
170
+ fileData = File.read(localFile)
171
+ position = fileData.index(sectionStart)
172
+ if position
173
+ # Find the index of the closing brace of the section
174
+ # We use a regular expression to find the next non-nested closing brace
175
+ match = fileData[position..-1].match(/(?<=\{)(.*?)(^\})/m)
176
+ if match
177
+ fileData = fileData[0...position] + fileData[(position + match.end(0))..-1]
178
+ else
179
+ fileData = fileData[0...position]
180
+ end
181
+ File.write(localFile, fileData)
182
+ ssh.scp.upload!(localFile, file)
183
+ end
184
+ end
14
185
  end
15
186
 
16
187
  end
@@ -0,0 +1,19 @@
1
+
2
+ [Unit]
3
+ Description=ERPNext Frontend container
4
+ After=local-fs.target
5
+
6
+ [Container]
7
+ ContainerName=ERPNext-Frontend
8
+ Image=ConfigLM.moe/erpnext:v$VERSION
9
+ Exec=nginx-entrypoint.sh
10
+ EnvironmentFile=/var/lib/erpnext/.config/containers/systemd/ERPNext.env
11
+ PublishPort=127.0.0.1:18400:8080
12
+ Network=ERPNext
13
+ HostName=ERPNext-Frontend
14
+ UserNS=keep-id:uid=1000,gid=1000
15
+ Volume=/var/lib/erpnext/sites:/home/frappe/frappe-bench/sites
16
+ Volume=/var/lib/erpnext/logs:/home/frappe/frappe-bench/logs
17
+
18
+ [Install]
19
+ WantedBy=multi-user.target default.target
@@ -0,0 +1,17 @@
1
+
2
+ [Unit]
3
+ Description=ERPNext Queue container
4
+ After=local-fs.target
5
+
6
+ [Container]
7
+ ContainerName=ERPNext-Queue
8
+ Image=ConfigLM.moe/erpnext:v$VERSION
9
+ Exec=bench worker --queue long,default,short
10
+ EnvironmentFile=/var/lib/erpnext/.config/containers/systemd/ERPNext.env
11
+ Network=slirp4netns:allow_host_loopback=true
12
+ UserNS=keep-id:uid=1000,gid=1000
13
+ Volume=/var/lib/erpnext/sites:/home/frappe/frappe-bench/sites
14
+ Volume=/var/lib/erpnext/logs:/home/frappe/frappe-bench/logs
15
+
16
+ [Install]
17
+ WantedBy=multi-user.target default.target
@@ -0,0 +1,17 @@
1
+
2
+ [Unit]
3
+ Description=ERPNext Scheduler container
4
+ After=local-fs.target
5
+
6
+ [Container]
7
+ ContainerName=ERPNext-Scheduler
8
+ Image=ConfigLM.moe/erpnext:v$VERSION
9
+ Exec=bench schedule
10
+ EnvironmentFile=/var/lib/erpnext/.config/containers/systemd/ERPNext.env
11
+ UserNS=keep-id:uid=1000,gid=1000
12
+ Volume=/var/lib/erpnext/sites:/home/frappe/frappe-bench/sites
13
+ Volume=/var/lib/erpnext/logs:/home/frappe/frappe-bench/logs
14
+
15
+ [Install]
16
+ WantedBy=multi-user.target default.target
17
+
@@ -0,0 +1,19 @@
1
+
2
+ [Unit]
3
+ Description=ERPNext Websocket container
4
+ After=local-fs.target
5
+
6
+ [Container]
7
+ ContainerName=ERPNext-Websocket
8
+ Image=ConfigLM.moe/erpnext:v$VERSION
9
+ Exec=node /home/frappe/frappe-bench/apps/frappe/socketio.js
10
+ EnvironmentFile=/var/lib/erpnext/.config/containers/systemd/ERPNext.env
11
+ Network=ERPNext
12
+ IP=10.90.50.11
13
+ HostName=ERPNext-Websocket
14
+ UserNS=keep-id:uid=1000,gid=1000
15
+ Volume=/var/lib/erpnext/sites:/home/frappe/frappe-bench/sites
16
+ Volume=/var/lib/erpnext/logs:/home/frappe/frappe-bench/logs
17
+
18
+ [Install]
19
+ WantedBy=multi-user.target default.target
@@ -0,0 +1,18 @@
1
+
2
+ [Unit]
3
+ Description=ERPNext container
4
+ After=local-fs.target
5
+
6
+ [Container]
7
+ ContainerName=ERPNext
8
+ Image=ConfigLM.moe/erpnext:v$VERSION
9
+ EnvironmentFile=/var/lib/erpnext/.config/containers/systemd/ERPNext.env
10
+ Network=ERPNext
11
+ IP=10.90.50.10
12
+ HostName=ERPNext
13
+ UserNS=keep-id:uid=1000,gid=1000
14
+ Volume=/var/lib/erpnext/sites:/home/frappe/frappe-bench/sites
15
+ Volume=/var/lib/erpnext/logs:/home/frappe/frappe-bench/logs
16
+
17
+ [Install]
18
+ WantedBy=multi-user.target default.target
@@ -0,0 +1,193 @@
1
+
2
+ module ConfigLMM
3
+ module LMM
4
+ class ERPNext < Framework::NginxApp
5
+
6
+ USER = 'erpnext'
7
+ HOME_DIR = '/var/lib/erpnext'
8
+ VERSION = '15'
9
+ FRAPPE_REPO = 'https://github.com/frappe/frappe_docker.git'
10
+ IMAGE_ID = 'ConfigLM.moe/erpnext:v' + VERSION
11
+
12
+ def actionERPNextBuild(id, target, activeState, context, options)
13
+ buildContainer(id, target, options)
14
+ end
15
+
16
+ def buildContainer(id, target, options)
17
+ begin
18
+ Framework::LinuxApp.ensurePackage('git', '@me', 'git')
19
+ Framework::LinuxApp.ensurePackage('Podman', '@me', 'podman')
20
+ rescue RuntimeError => error
21
+ prompt.say(error, :color => :red)
22
+ end
23
+ frappe = File.expand_path(REPOS_CACHE + '/frappe_docker')
24
+ if !File.exist?(frappe)
25
+ mkdir(File.expand_path(REPOS_CACHE), false)
26
+ self.class.exec('cd #{REPOS_CACHE} && git clone --quiet #{FRAPPE_REPO}')
27
+ else
28
+ self.class.exec('cd #{REPOS_CACHE}/frappe_docker && git pull --quiet')
29
+ end
30
+ self.class.exec('cd #{REPOS_CACHE}/frappe_docker && git checkout . --quiet')
31
+
32
+ if !self.class.cmdSuccess?("podman image exists #{IMAGE_ID}")
33
+ appsJSON = Base64.urlsafe_encode64(File.read(__dir__ + '/sites/apps.json').gsub('$VERSION', VERSION))
34
+ self.class.exec("cd #{REPOS_CACHE}/frappe_docker && podman build --tag=#{IMAGE_ID} --build-arg APPS_JSON_BASE64=#{appsJSON} --build-arg FRAPPE_BRANCH=version-#{VERSION} --file images/custom/Containerfile .")
35
+ end
36
+ end
37
+
38
+ def actionERPNextDeploy(id, target, activeState, context, options)
39
+ raise Framework::PluginProcessError.new('Domain field must be set!') if (!target.key?('Proxy') || target['Proxy']) && !target['Domain']
40
+
41
+ target['Database'] ||= {}
42
+ if target['Location'] && target['Location'] != '@me'
43
+ uri = Addressable::URI.parse(target['Location'])
44
+ raise Framework::PluginProcessError.new("#{id}: Unknown Protocol: #{uri.scheme}!") if uri.scheme != 'ssh'
45
+
46
+ self.class.sshStart(uri) do |ssh|
47
+
48
+ activeState['Database'] = target['Database']
49
+ dbPassword = self.configureMariaDB(target['Database'], activeState, ssh)
50
+ distroInfo = Framework::LinuxApp.currentDistroInfo(ssh)
51
+ Framework::LinuxApp.configurePodmanServiceOverSSH(USER, HOME_DIR, 'ERPNext', distroInfo, ssh)
52
+ self.class.exec("su --login #{USER} --shell /bin/sh --command 'mkdir -p ~/sites ~/logs'", ssh)
53
+
54
+ cmd = self.class.cmdSSH(uri)
55
+ self.class.exec("podman image save ConfigLM.moe/erpnext:v#{VERSION} | #{cmd} 'cat > #{HOME_DIR}/erpnext.tar'")
56
+ self.class.exec("su --login #{USER} --shell /usr/bin/sh --command 'podman image load --input erpnext.tar'", ssh)
57
+ self.class.exec("rm -f #{HOME_DIR}/erpnext.tar", ssh)
58
+
59
+ path = Framework::LinuxApp::SYSTEMD_CONTAINERS_PATH.gsub('~', HOME_DIR)
60
+ self.class.exec(" echo 'FRAPPE_DB_PASSWORD=#{dbPassword}' > #{path}/ERPNext.env", ssh)
61
+ self.class.exec("echo 'FRAPPE_SITE_NAME_HEADER=erpnext' >> #{path}/ERPNext.env", ssh)
62
+ #self.class.exec("echo 'UPSTREAM_REAL_IP_ADDRESS=127.0.0.1' >> #{path}/ERPNext.env", ssh)
63
+ #self.class.exec("echo 'UPSTREAM_REAL_IP_RECURSIVE=on' >> #{path}/ERPNext.env", ssh)
64
+ self.class.exec("echo 'BACKEND=10.90.50.10:8000' >> #{path}/ERPNext.env", ssh)
65
+ self.class.exec("echo 'SOCKETIO=10.90.50.11:9000' >> #{path}/ERPNext.env", ssh)
66
+
67
+ self.class.exec("chown #{USER}:#{USER} #{path}/ERPNext.env", ssh)
68
+ self.class.exec("chmod 600 #{path}/ERPNext.env", ssh)
69
+
70
+ ssh.scp.upload!(__dir__ + '/sites/apps.txt', HOME_DIR + '/sites/')
71
+ ssh.scp.upload!(__dir__ + '/sites/common_site_config.json', HOME_DIR + '/sites/')
72
+
73
+ if target['Database'] && target['Database']['HostName']
74
+ self.class.exec("sed -i 's|\"10.0.2.2\"|\"#{target['Database']['HostName']}\"|' #{HOME_DIR}/sites/common_site_config.json", ssh)
75
+ end
76
+
77
+ if target['Valkey']
78
+ self.class.exec("sed -i 's|10.0.2.2:6379|#{target['Valkey']}|' #{HOME_DIR}/sites/common_site_config.json", ssh)
79
+ end
80
+
81
+ valkeyPassword = ENV[id + '-VALKEY_PASSWORD'] || ENV['VALKEY_PASSWORD']
82
+ if valkeyPassword
83
+ self.class.exec("sed -i 's|\"use_rq_auth\": false|\"use_rq_auth\": true|' #{HOME_DIR}/sites/common_site_config.json", ssh)
84
+ self.class.exec("sed -i 's|$VALKEY_PASSWORD|#{valkeyPassword}|' #{HOME_DIR}/sites/common_site_config.json", ssh)
85
+ end
86
+
87
+ self.class.exec("chown -R #{USER}:#{USER} " + HOME_DIR + '/sites', ssh)
88
+
89
+ ssh.scp.upload!(__dir__ + '/ERPNext.network', path)
90
+ ssh.scp.upload!(__dir__ + '/ERPNext.container', path)
91
+ ssh.scp.upload!(__dir__ + '/ERPNext-Queue.container', path)
92
+ ssh.scp.upload!(__dir__ + '/ERPNext-Scheduler.container', path)
93
+ ssh.scp.upload!(__dir__ + '/ERPNext-Websocket.container', path)
94
+ ssh.scp.upload!(__dir__ + '/ERPNext-Frontend.container', path)
95
+ self.class.exec("sed -i 's|$VERSION|#{VERSION}|' #{path}/ERPNext.container", ssh)
96
+ self.class.exec("sed -i 's|$VERSION|#{VERSION}|' #{path}/ERPNext-Queue.container", ssh)
97
+ self.class.exec("sed -i 's|$VERSION|#{VERSION}|' #{path}/ERPNext-Scheduler.container", ssh)
98
+ self.class.exec("sed -i 's|$VERSION|#{VERSION}|' #{path}/ERPNext-Websocket.container", ssh)
99
+ self.class.exec("sed -i 's|$VERSION|#{VERSION}|' #{path}/ERPNext-Frontend.container", ssh)
100
+
101
+ if !target.key?('Proxy') || target['Proxy']
102
+ deployNginxProxyConfig('http://127.0.0.1:18400', 'ERPNext', id, target, activeState, state, context, options, ssh)
103
+ elsif target.key?('Proxy') && target['Proxy'] == false
104
+ self.class.exec("sed -i 's|PublishPort=127.0.0.1:18400:|PublishPort=0.0.0.0:18400:|' #{path}ERPNext-Frontend.container", ssh)
105
+ Framework::LinuxApp.firewallAddPort('18400/tcp', ssh)
106
+ end
107
+
108
+ self.class.exec("systemctl --user --machine=#{USER}@ daemon-reload", ssh)
109
+ self.class.exec("systemctl --user --machine=#{USER}@ restart ERPNext-network", ssh)
110
+ self.class.exec("systemctl --user --machine=#{USER}@ restart ERPNext", ssh)
111
+
112
+ containers = JSON.parse(self.class.exec("su --login #{USER} --shell /usr/bin/sh --command 'podman ps --format json --filter name=^ERPNext$'", ssh).strip)
113
+ raise 'Failed to find container!' if containers.empty?
114
+
115
+ MariaDB.executeRemotely(target['Database'], ssh) do |sshDB|
116
+ if !MariaDB.tableExist?(USER, 'tabUser', sshDB)
117
+ adminPassword = SecureRandom.alphanumeric(20)
118
+ self.class.exec("rm -rf " + HOME_DIR + '/sites/erpnext', ssh)
119
+ #self.class.exec(" su --login #{USER} --shell /usr/bin/sh --command \"podman exec #{containers.first['Id']} sh -c 'bench new-site --no-setup-db --db-name erpnext --db-user erpnext --admin-password #{adminPassword} --install-app erpnext --set-default erpnext'\"", ssh)
120
+ dbAdminPassword = MariaDB.createAdmin(sshDB)
121
+ MariaDB.executeSQL("DROP DATABASE #{USER}", nil, sshDB)
122
+ self.class.exec(" su --login #{USER} --shell /usr/bin/sh --command \" podman exec #{containers.first['Id']} sh -c ' bench new-site --db-root-username admin --db-root-password #{dbAdminPassword} --db-name erpnext --admin-password #{adminPassword} --install-app erpnext --set-default erpnext'\"", ssh)
123
+ MariaDB.dropAdmin(sshDB)
124
+ self.class.exec("su --login #{USER} --shell /usr/bin/sh --command \"podman exec #{containers.first['Id']} sh -c 'bench --site erpnext install-app hrms'\"", ssh)
125
+ prompt.say("Administrator password: #{adminPassword}", :color => :magenta)
126
+ end
127
+ end
128
+
129
+ self.class.exec("systemctl --user --machine=#{USER}@ restart ERPNext-Queue", ssh)
130
+ self.class.exec("systemctl --user --machine=#{USER}@ restart ERPNext-Scheduler", ssh)
131
+ self.class.exec("systemctl --user --machine=#{USER}@ restart ERPNext-Websocket", ssh)
132
+ self.class.exec("systemctl --user --machine=#{USER}@ restart ERPNext-Frontend", ssh)
133
+
134
+
135
+ end
136
+ else
137
+ # TODO
138
+ end
139
+ end
140
+
141
+ def configureMariaDB(settings, activeState, ssh)
142
+ password = SecureRandom.alphanumeric(20)
143
+ MariaDB.createRemoteUserAndDB(settings, USER, password, ssh)
144
+ password
145
+ end
146
+
147
+ def cleanup(configs, state, context, options)
148
+ cleanupType(:ERPNext, configs, state, context, options) do |item, id, state, context, options, ssh|
149
+ if item['Proxy'].nil? || item['Proxy']
150
+ self.cleanupNginxConfig('ERPNext', id, state, context, options, ssh)
151
+ self.class.reload(ssh, options[:dry])
152
+ end
153
+ Framework::LinuxApp.firewallRemovePort('18400/tcp', ssh, options[:dry])
154
+
155
+ self.class.exec("systemctl --user --machine=#{USER}@ stop ERPNext", ssh, true, options[:dry])
156
+ self.class.exec("systemctl --user --machine=#{USER}@ stop ERPNext-Frontend", ssh, true, options[:dry])
157
+ self.class.exec("systemctl --user --machine=#{USER}@ stop ERPNext-Websocket", ssh, true, options[:dry])
158
+ self.class.exec("systemctl --user --machine=#{USER}@ stop ERPNext-Scheduler", ssh, true, options[:dry])
159
+ self.class.exec("systemctl --user --machine=#{USER}@ stop ERPNext-Queue", ssh, true, options[:dry])
160
+ self.class.exec("systemctl --user --machine=#{USER}@ stop ERPNext-network", ssh, true, options[:dry])
161
+
162
+ path = Framework::LinuxApp::SYSTEMD_CONTAINERS_PATH.gsub('~', HOME_DIR)
163
+ rm(path + 'ERPNext.network', options[:dry], ssh)
164
+ rm(path + 'ERPNext.container', options[:dry], ssh)
165
+ rm(path + 'ERPNext-Queue.container', options[:dry], ssh)
166
+ rm(path + 'ERPNext-Scheduler.container', options[:dry], ssh)
167
+ rm(path + 'ERPNext-Websocket.container', options[:dry], ssh)
168
+ rm(path + 'ERPNext-Frontend.container', options[:dry], ssh)
169
+
170
+ self.class.exec("podman rmi #{IMAGE_ID}", ssh, true, options[:dry])
171
+
172
+ state.item(id)['Status'] = State::STATUS_DELETED unless options[:dry]
173
+
174
+ if options[:destroy]
175
+ item['Database'] ||= {}
176
+ MariaDB.executeRemotely(item['Database'], ssh) do |sshDB|
177
+ MariaDB.executeSQL("DROP DATABASE #{USER}", nil, sshDB, true, options[:dry])
178
+ end
179
+ Framework::LinuxApp.deleteUserAndGroup(USER, ssh, options[:dry])
180
+ rm(HOME_DIR, options[:dry], ssh)
181
+ rm('/var/log/nginx/erpnext.access.log', options[:dry], ssh)
182
+ rm('/var/log/nginx/erpnext.error.log', options[:dry], ssh)
183
+
184
+ state.item(id)['Status'] = State::STATUS_DESTROYED unless options[:dry]
185
+ end
186
+ end
187
+ end
188
+
189
+ end
190
+ end
191
+ end
192
+
193
+
@@ -0,0 +1,12 @@
1
+
2
+ [Unit]
3
+ Description=ERPNext network
4
+ After=network-online.target
5
+
6
+ [Network]
7
+ NetworkName=ERPNext
8
+ Subnet=10.90.50.0/28
9
+ Gateway=10.90.50.1
10
+
11
+ [Install]
12
+ WantedBy=default.target
@@ -0,0 +1,10 @@
1
+ [
2
+ {
3
+ "url": "https://github.com/frappe/erpnext",
4
+ "branch": "version-$VERSION"
5
+ },
6
+ {
7
+ "url": "https://github.com/frappe/hrms",
8
+ "branch": "version-$VERSION"
9
+ }
10
+ ]
@@ -0,0 +1,3 @@
1
+ erpnext
2
+ frappe
3
+ hrms
@@ -0,0 +1,11 @@
1
+ {
2
+ "db_host": "10.0.2.2",
3
+ "db_port": 3306,
4
+ "redis_cache": "redis://default:$VALKEY_PASSWORD@10.0.2.2:6379",
5
+ "redis_queue": "redis://default:$VALKEY_PASSWORD@10.0.2.2:6379",
6
+ "redis_socketio": "redis://default:$VALKEY_PASSWORD@10.0.2.2:6379",
7
+ "use_rq_auth": false,
8
+ "rq_password": "$VALKEY_PASSWORD",
9
+ "rq_username": "default",
10
+ "socketio_port": 9000
11
+ }
@@ -0,0 +1,18 @@
1
+
2
+ [Unit]
3
+ Description=GitLab container
4
+ After=local-fs.target firewalld.service
5
+
6
+ [Container]
7
+ Image=docker.io/gitlab/gitlab-ce:latest
8
+ PublishPort=127.0.0.1:18100:80
9
+ PublishPort=0.0.0.0:22:22
10
+ Volume=/var/lib/gitlab/config:/etc/gitlab
11
+ Volume=/var/lib/gitlab/logs:/var/log/gitlab
12
+ Volume=/var/lib/gitlab/data:/var/opt/gitlab
13
+ Volume=/var/lib/gitlab/backups:/var/opt/gitlab/backups
14
+ ShmSize=256M
15
+ AutoUpdate=registry
16
+
17
+ [Install]
18
+ WantedBy=multi-user.target default.target