cloudconfig.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package cloudinit
  15. import (
  16. "bytes"
  17. "encoding/base64"
  18. "fmt"
  19. "strings"
  20. "yunion.io/x/jsonutils"
  21. "yunion.io/x/log"
  22. "yunion.io/x/pkg/errors"
  23. "yunion.io/x/pkg/util/seclib"
  24. "yunion.io/x/pkg/utils"
  25. )
  26. /*
  27. * cloudconfig
  28. * Reference: https://cloudinit.readthedocs.io/en/latest/topics/examples.html
  29. *
  30. */
  31. type TSudoPolicy string
  32. type TSshPwauth string
  33. const (
  34. CLOUD_CONFIG_HEADER = "#cloud-config\n"
  35. CLOUD_SHELL_HEADER = "#!/usr/bin/env bash\n"
  36. CLOUD_POWER_SHELL_HEADER = "#ps1\n"
  37. USER_SUDO_NOPASSWD = TSudoPolicy("sudo_nopasswd")
  38. USER_SUDO = TSudoPolicy("sudo")
  39. USER_SUDO_DENY = TSudoPolicy("sudo_deny")
  40. USER_SUDO_NONE = TSudoPolicy("")
  41. SSH_PASSWORD_AUTH_ON = TSshPwauth("true")
  42. SSH_PASSWORD_AUTH_OFF = TSshPwauth("false")
  43. SSH_PASSWORD_AUTH_UNCHANGED = TSshPwauth("unchanged")
  44. )
  45. type SWriteFile struct {
  46. Path string
  47. Permissions string
  48. Owner string
  49. Encoding string
  50. Content string
  51. }
  52. type SUser struct {
  53. Name string
  54. PlainTextPasswd string
  55. HashedPasswd string
  56. LockPasswd bool
  57. SshAuthorizedKeys []string
  58. Sudo string
  59. }
  60. type SPhoneHome struct {
  61. Url string
  62. }
  63. type SCloudConfig struct {
  64. Users []SUser
  65. WriteFiles []SWriteFile
  66. Runcmd []string
  67. Bootcmd []string
  68. Packages []string
  69. PhoneHome *SPhoneHome
  70. DisableRoot int
  71. SshPwauth TSshPwauth
  72. }
  73. func NewWriteFile(path string, content string, perm string, owner string, isBase64 bool) SWriteFile {
  74. f := SWriteFile{}
  75. f.Path = path
  76. f.Permissions = perm
  77. f.Owner = owner
  78. if isBase64 {
  79. f.Encoding = "b64"
  80. f.Content = base64.StdEncoding.EncodeToString([]byte(content))
  81. } else {
  82. f.Content = content
  83. }
  84. return f
  85. }
  86. func setFilePermission(path, permission, owner string) []string {
  87. cmds := []string{}
  88. if len(permission) > 0 {
  89. cmds = append(cmds, fmt.Sprintf("chmod %s %s", permission, path))
  90. }
  91. if len(owner) > 0 {
  92. cmds = append(cmds, fmt.Sprintf("chown %s:%s %s", owner, owner, path))
  93. }
  94. return cmds
  95. }
  96. func mkPutFileCmd(path string, content string, permission string, owner string) []string {
  97. cmds := []string{}
  98. cmds = append(cmds, fmt.Sprintf("mkdir -p $(dirname %s)", path))
  99. cmds = append(cmds, fmt.Sprintf("cat > %s <<_END\n%s\n_END", path, content))
  100. return append(cmds, setFilePermission(path, permission, owner)...)
  101. }
  102. func mkAppendFileCmd(path string, content string, permission string, owner string) []string {
  103. cmds := []string{}
  104. cmds = append(cmds, fmt.Sprintf("mkdir -p $(dirname %s)", path))
  105. cmds = append(cmds, fmt.Sprintf("cat >> %s <<_END\n%s\n_END", path, content))
  106. return append(cmds, setFilePermission(path, permission, owner)...)
  107. }
  108. func (wf *SWriteFile) ShellScripts() []string {
  109. content := wf.Content
  110. if wf.Encoding == "b64" {
  111. _content, _ := base64.StdEncoding.DecodeString(wf.Content)
  112. content = string(_content)
  113. }
  114. return mkPutFileCmd(wf.Path, content, wf.Permissions, wf.Owner)
  115. }
  116. func NewUser(name string) SUser {
  117. u := SUser{Name: name}
  118. return u
  119. }
  120. func (u *SUser) SudoPolicy(policy TSudoPolicy) *SUser {
  121. switch policy {
  122. case USER_SUDO_NOPASSWD:
  123. u.Sudo = "ALL=(ALL) NOPASSWD:ALL"
  124. case USER_SUDO:
  125. u.Sudo = "ALL=(ALL) ALL"
  126. case USER_SUDO_DENY:
  127. u.Sudo = "False"
  128. default:
  129. u.Sudo = ""
  130. }
  131. return u
  132. }
  133. func (u *SUser) SshKey(key string) *SUser {
  134. if u.SshAuthorizedKeys == nil {
  135. u.SshAuthorizedKeys = make([]string, 0)
  136. }
  137. u.SshAuthorizedKeys = append(u.SshAuthorizedKeys, key)
  138. return u
  139. }
  140. func (u *SUser) Password(passwd string) *SUser {
  141. if len(passwd) > 0 {
  142. hash, err := seclib.GeneratePassword(passwd)
  143. if err != nil {
  144. log.Errorf("GeneratePassword error %s", err)
  145. } else {
  146. u.PlainTextPasswd = passwd
  147. u.HashedPasswd = hash
  148. }
  149. u.LockPasswd = false
  150. }
  151. return u
  152. }
  153. func (u *SUser) PowerShellScripts() []string {
  154. shells := []string{}
  155. shells = append(shells, fmt.Sprintf(`New-LocalUser -Name "%s" -Description "A New Local Account Created By PowerShell" -NoPassword`, u.Name))
  156. shells = append(shells, fmt.Sprintf(`Add-LocalGroupMember -Group "Administrators" -Member "%s"`, u.Name))
  157. if len(u.PlainTextPasswd) > 0 {
  158. shells = append(shells, fmt.Sprintf(`net user "%s" "%s"`, u.Name, u.PlainTextPasswd))
  159. }
  160. // enable需要再设置密码之后,否则会出现Enable-LocalUser : Unable to update the password. The value provided for the new password does not meet the length, complexity, or history requirements of the domain
  161. shells = append(shells, fmt.Sprintf(`Enable-LocalUser "%s"`, u.Name))
  162. return shells
  163. }
  164. func (u *SUser) ShellScripts() []string {
  165. shells := []string{}
  166. shells = append(shells, fmt.Sprintf("useradd -m %s || true", u.Name))
  167. if len(u.HashedPasswd) > 0 {
  168. shells = append(shells, fmt.Sprintf("usermod -p '%s' %s", u.HashedPasswd, u.Name))
  169. }
  170. home := "/" + u.Name
  171. if home != "/root" {
  172. home = "/home" + home
  173. }
  174. keyPath := fmt.Sprintf("%s/.ssh/authorized_keys", home)
  175. shells = append(shells, mkAppendFileCmd(keyPath, strings.Join(u.SshAuthorizedKeys, "\n"), "600", u.Name)...)
  176. shells = append(shells, fmt.Sprintf("chown -R %s:%s %s/.ssh", u.Name, u.Name, home))
  177. if !utils.IsInStringArray(u.Sudo, []string{"", "False"}) {
  178. shells = append(shells, mkPutFileCmd("/etc/sudoers.d/"+u.Name, fmt.Sprintf("%s %s", u.Name, u.Sudo), "", "")...)
  179. }
  180. return shells
  181. }
  182. func (conf *SCloudConfig) UserData() string {
  183. var buf bytes.Buffer
  184. jsonConf := jsonutils.Marshal(conf).(*jsonutils.JSONDict)
  185. if jsonConf.Contains("users") {
  186. userArray := jsonutils.NewArray(jsonutils.NewString("default"))
  187. users, _ := jsonConf.GetArray("users")
  188. if users != nil {
  189. userArray.Add(users...)
  190. jsonConf.Set("users", userArray)
  191. }
  192. }
  193. buf.WriteString(CLOUD_CONFIG_HEADER)
  194. buf.WriteString(jsonConf.YAMLString())
  195. return buf.String()
  196. }
  197. func (conf *SCloudConfig) UserDataScript() string {
  198. shells := []string{}
  199. for _, u := range conf.Users {
  200. shells = append(shells, u.ShellScripts()...)
  201. }
  202. shells = append(shells, conf.Runcmd...)
  203. if conf.DisableRoot == 0 {
  204. shells = append(shells, `sed -i "s/.*PermitRootLogin.*/PermitRootLogin yes/g" /etc/ssh/sshd_config`)
  205. shells = append(shells, `sed -i "s/.*PermitRootLogin.*/PermitRootLogin yes/g" /etc/ssh/sshd_config.d/*.conf`)
  206. }
  207. if conf.SshPwauth == SSH_PASSWORD_AUTH_ON {
  208. shells = append(shells, `sed -i 's/.*PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config`)
  209. shells = append(shells, `sed -i 's/.*PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config.d/*.conf`)
  210. }
  211. if conf.DisableRoot == 0 || conf.SshPwauth == SSH_PASSWORD_AUTH_ON {
  212. // ubuntu24.04 sshd -> ssh
  213. shells = append(shells, `systemctl restart sshd ssh`)
  214. }
  215. for _, pkg := range conf.Packages {
  216. shells = append(shells, "which yum &>/dev/null && yum install -y "+pkg)
  217. shells = append(shells, "which apt-get &>/dev/null && apt-get install -y "+pkg)
  218. }
  219. for _, wf := range conf.WriteFiles {
  220. shells = append(shells, wf.ShellScripts()...)
  221. }
  222. return CLOUD_SHELL_HEADER + strings.Join(shells, "\n")
  223. }
  224. func (conf *SCloudConfig) UserDataPowerShell() string {
  225. shells := []string{}
  226. for _, u := range conf.Users {
  227. shells = append(shells, u.PowerShellScripts()...)
  228. }
  229. shells = append(shells, conf.Runcmd...)
  230. return CLOUD_POWER_SHELL_HEADER + strings.Join(shells, "\n")
  231. }
  232. func (conf *SCloudConfig) UserDataEc2() string {
  233. shells := []string{}
  234. for _, u := range conf.Users {
  235. shells = append(shells, u.PowerShellScripts()...)
  236. }
  237. shells = append(shells, conf.Runcmd...)
  238. return "<powershell>\n" + strings.Join(shells, "\n") + "\n</powershell>"
  239. }
  240. func (conf *SCloudConfig) UserDataBase64() string {
  241. data := conf.UserData()
  242. return base64.StdEncoding.EncodeToString([]byte(data))
  243. }
  244. func (conf *SCloudConfig) UserDataScriptBase64() string {
  245. data := conf.UserDataScript()
  246. return base64.StdEncoding.EncodeToString([]byte(data))
  247. }
  248. func ParseUserDataBase64(b64data string) (*SCloudConfig, error) {
  249. data, err := base64.StdEncoding.DecodeString(b64data)
  250. if err != nil {
  251. return nil, err
  252. }
  253. return ParseUserData(string(data))
  254. }
  255. func parseShell(data string) (*SCloudConfig, error) {
  256. info := strings.Split(data, "\n")
  257. ret := &SCloudConfig{
  258. Runcmd: []string{},
  259. SshPwauth: SSH_PASSWORD_AUTH_ON,
  260. }
  261. for _, cmd := range info {
  262. if strings.HasPrefix(cmd, "#") || len(strings.Trim(cmd, "")) == 0 {
  263. continue
  264. }
  265. ret.Runcmd = append(ret.Runcmd, cmd)
  266. }
  267. return ret, nil
  268. }
  269. func ParseUserData(data string) (*SCloudConfig, error) {
  270. if !strings.HasPrefix(data, CLOUD_CONFIG_HEADER) {
  271. return parseShell(data)
  272. }
  273. jsonConf, err := jsonutils.ParseYAML(data)
  274. if err != nil {
  275. return nil, errors.Wrapf(err, "ParseYAML")
  276. }
  277. jsonDict := jsonConf.(*jsonutils.JSONDict)
  278. if jsonDict.Contains("users") {
  279. userArray := jsonutils.NewArray()
  280. users, _ := jsonConf.GetArray("users")
  281. if users != nil {
  282. for i := 0; i < len(users); i++ {
  283. if users[i].String() != `"default"` {
  284. userArray.Add(users[i])
  285. }
  286. }
  287. jsonDict.Set("users", userArray)
  288. }
  289. }
  290. config := SCloudConfig{}
  291. err = jsonDict.Unmarshal(&config)
  292. if err != nil {
  293. log.Errorf("unable to unmarchal userdata %s", err)
  294. return nil, err
  295. }
  296. return &config, nil
  297. }
  298. func (conf *SCloudConfig) MergeUser(u SUser) {
  299. for i := 0; i < len(conf.Users); i += 1 {
  300. if u.Name == conf.Users[i].Name {
  301. // replace conf user password with input
  302. if len(u.PlainTextPasswd) > 0 {
  303. conf.Users[i].PlainTextPasswd = u.PlainTextPasswd
  304. conf.Users[i].HashedPasswd = u.HashedPasswd
  305. conf.Users[i].LockPasswd = u.LockPasswd
  306. }
  307. // find user, merge keys
  308. for j := 0; j < len(u.SshAuthorizedKeys); j += 1 {
  309. if !utils.IsInStringArray(u.SshAuthorizedKeys[j], conf.Users[i].SshAuthorizedKeys) {
  310. conf.Users[i].SshAuthorizedKeys = append(conf.Users[i].SshAuthorizedKeys, u.SshAuthorizedKeys[j])
  311. }
  312. }
  313. return
  314. }
  315. }
  316. // no such user
  317. conf.Users = append(conf.Users, u)
  318. }
  319. func (conf *SCloudConfig) RemoveUser(u SUser) {
  320. for i := range conf.Users {
  321. if u.Name == conf.Users[i].Name {
  322. if len(conf.Users) == i {
  323. conf.Users = conf.Users[0:i]
  324. } else {
  325. conf.Users = append(conf.Users[0:i], conf.Users[i+1:]...)
  326. }
  327. return
  328. }
  329. }
  330. }
  331. func (conf *SCloudConfig) MergeWriteFile(f SWriteFile, replace bool) {
  332. for i := 0; i < len(conf.WriteFiles); i += 1 {
  333. if conf.WriteFiles[i].Path == f.Path {
  334. // find file
  335. if replace {
  336. conf.WriteFiles[i].Content = f.Content
  337. conf.WriteFiles[i].Encoding = f.Encoding
  338. conf.WriteFiles[i].Owner = f.Owner
  339. conf.WriteFiles[i].Permissions = f.Permissions
  340. }
  341. return
  342. }
  343. }
  344. // no such file
  345. conf.WriteFiles = append(conf.WriteFiles, f)
  346. }
  347. func (conf *SCloudConfig) MergeRuncmd(cmd string) {
  348. if !utils.IsInStringArray(cmd, conf.Runcmd) {
  349. conf.Runcmd = append(conf.Runcmd, cmd)
  350. }
  351. }
  352. func (conf *SCloudConfig) MergeBootcmd(cmd string) {
  353. if !utils.IsInStringArray(cmd, conf.Bootcmd) {
  354. conf.Bootcmd = append(conf.Bootcmd, cmd)
  355. }
  356. }
  357. func (conf *SCloudConfig) MergePackage(pkg string) {
  358. if !utils.IsInStringArray(pkg, conf.Packages) {
  359. conf.Packages = append(conf.Packages, pkg)
  360. }
  361. }
  362. func (conf *SCloudConfig) Merge(conf2 *SCloudConfig) {
  363. for _, u := range conf2.Users {
  364. conf.MergeUser(u)
  365. }
  366. for _, f := range conf2.WriteFiles {
  367. conf.MergeWriteFile(f, false)
  368. }
  369. for _, c := range conf2.Runcmd {
  370. conf.MergeRuncmd(c)
  371. }
  372. for _, c := range conf2.Bootcmd {
  373. conf.MergeBootcmd(c)
  374. }
  375. for _, p := range conf2.Packages {
  376. conf.MergePackage(p)
  377. }
  378. }