command.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. /*
  2. Copyright 2018 The Rook Authors. All rights reserved.
  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. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package client
  14. import (
  15. "fmt"
  16. "path"
  17. "strconv"
  18. "time"
  19. "github.com/pkg/errors"
  20. "github.com/rook/rook/pkg/clusterd"
  21. "github.com/rook/rook/pkg/util/exec"
  22. )
  23. // RunAllCephCommandsInToolboxPod - when running the e2e tests, all ceph commands need to be run in the toolbox.
  24. // Everywhere else, the ceph tools are assumed to be in the container where we can shell out.
  25. // This is the name of the pod.
  26. var RunAllCephCommandsInToolboxPod string
  27. const (
  28. // AdminUsername is the name of the admin user
  29. AdminUsername = "client.admin"
  30. // CephTool is the name of the CLI tool for 'ceph'
  31. CephTool = "ceph"
  32. // RBDTool is the name of the CLI tool for 'rbd'
  33. RBDTool = "rbd"
  34. // RadosTool is the name of the CLI tool for 'rados'
  35. RadosTool = "rados"
  36. // Kubectl is the name of the CLI tool for 'kubectl'
  37. Kubectl = "kubectl"
  38. // CrushTool is the name of the CLI tool for 'crushtool'
  39. CrushTool = "crushtool"
  40. // GaneshaRadosGraceTool is the name of the CLI tool for 'ganesha-rados-grace'
  41. GaneshaRadosGraceTool = "ganesha-rados-grace"
  42. // DefaultPGCount will cause Ceph to use the internal default PG count
  43. DefaultPGCount = "0"
  44. // CommandProxyInitContainerName is the name of the init container for proxying ceph command when multus is used
  45. CommandProxyInitContainerName = "cmd-proxy"
  46. // ProxyAppLabel is the label used to identify the proxy container
  47. ProxyAppLabel = "rook-ceph-mgr"
  48. )
  49. // CephConfFilePath returns the location to the cluster's config file in the operator container.
  50. func CephConfFilePath(configDir, clusterName string) string {
  51. confFile := fmt.Sprintf("%s.config", clusterName)
  52. return path.Join(configDir, clusterName, confFile)
  53. }
  54. // FinalizeCephCommandArgs builds the command line to be called
  55. func FinalizeCephCommandArgs(command string, clusterInfo *ClusterInfo, args []string, configDir string) (string, []string) {
  56. timeout := strconv.Itoa(int(exec.CephCommandsTimeout.Seconds()))
  57. cephConfPath := CephConfFilePath(configDir, clusterInfo.Namespace)
  58. // some tools not support the '--connect-timeout' option
  59. // so we only use it for the 'ceph' command
  60. switch command {
  61. case RBDTool, CrushTool, RadosTool, "radosgw-admin":
  62. // do not add timeout flag
  63. case GaneshaRadosGraceTool:
  64. // do not add timeout flag
  65. // ganesha-rados-grace uses '--cephconf' for config file path
  66. args = append(args, fmt.Sprintf("--cephconf=%s", cephConfPath))
  67. default:
  68. args = append(args, "--connect-timeout="+timeout)
  69. }
  70. // If the command should be run inside the toolbox pod, include the kubectl args to call the toolbox
  71. if RunAllCephCommandsInToolboxPod != "" {
  72. toolArgs := []string{"exec", "-i", RunAllCephCommandsInToolboxPod, "-n", clusterInfo.Namespace,
  73. "--", "timeout", timeout, command}
  74. return Kubectl, append(toolArgs, args...)
  75. }
  76. configArgs := []string{}
  77. switch command {
  78. case GaneshaRadosGraceTool:
  79. // ganesha-rados-grace does not accept any standard flags
  80. default:
  81. // Append the standard flags for config and keyring
  82. keyringFile := fmt.Sprintf("%s.keyring", clusterInfo.CephCred.Username)
  83. configArgs = []string{
  84. fmt.Sprintf("--cluster=%s", clusterInfo.Namespace),
  85. fmt.Sprintf("--conf=%s", cephConfPath),
  86. fmt.Sprintf("--name=%s", clusterInfo.CephCred.Username),
  87. fmt.Sprintf("--keyring=%s", path.Join(configDir, clusterInfo.Namespace, keyringFile)),
  88. }
  89. }
  90. return command, append(args, configArgs...)
  91. }
  92. type CephToolCommand struct {
  93. context *clusterd.Context
  94. clusterInfo *ClusterInfo
  95. tool string
  96. args []string
  97. timeout time.Duration
  98. JsonOutput bool
  99. combinedOutput bool
  100. RemoteExecution bool
  101. }
  102. func newCephToolCommand(tool string, context *clusterd.Context, clusterInfo *ClusterInfo, args []string) *CephToolCommand {
  103. return &CephToolCommand{
  104. context: context,
  105. tool: tool,
  106. clusterInfo: clusterInfo,
  107. args: args,
  108. JsonOutput: true,
  109. combinedOutput: false,
  110. }
  111. }
  112. func NewCephCommand(context *clusterd.Context, clusterInfo *ClusterInfo, args []string) *CephToolCommand {
  113. return newCephToolCommand(CephTool, context, clusterInfo, args)
  114. }
  115. func NewRBDCommand(context *clusterd.Context, clusterInfo *ClusterInfo, args []string) *CephToolCommand {
  116. cmd := newCephToolCommand(RBDTool, context, clusterInfo, args)
  117. cmd.JsonOutput = false
  118. // When Multus is enabled, the RBD tool should run inside the proxy container
  119. if clusterInfo.NetworkSpec.IsMultus() {
  120. cmd.RemoteExecution = true
  121. }
  122. return cmd
  123. }
  124. func NewRadosCommand(context *clusterd.Context, clusterInfo *ClusterInfo, args []string) *CephToolCommand {
  125. cmd := newCephToolCommand(RadosTool, context, clusterInfo, args)
  126. cmd.JsonOutput = false
  127. // When Multus is enabled, the rados tool should run inside the proxy container
  128. if clusterInfo.NetworkSpec.IsMultus() {
  129. cmd.RemoteExecution = true
  130. }
  131. return cmd
  132. }
  133. func NewGaneshaRadosGraceCommand(context *clusterd.Context, clusterInfo *ClusterInfo, args []string) *CephToolCommand {
  134. cmd := newCephToolCommand(GaneshaRadosGraceTool, context, clusterInfo, args)
  135. cmd.JsonOutput = false
  136. // When Multus is enabled, the rados tool should run inside the proxy container
  137. if clusterInfo.NetworkSpec.IsMultus() {
  138. cmd.RemoteExecution = true
  139. }
  140. return cmd
  141. }
  142. func (c *CephToolCommand) run() ([]byte, error) {
  143. // Return if the context has been canceled
  144. if c.clusterInfo.Context.Err() != nil {
  145. return nil, c.clusterInfo.Context.Err()
  146. }
  147. // Initialize the command and args
  148. command := c.tool
  149. args := c.args
  150. // If this is a remote execution, we don't want to build the full set of args. For instance all
  151. // these args are not needed since those paths don't exist inside the cmd-proxy container:
  152. // --cluster=openshift-storage
  153. // --conf=/var/lib/rook/openshift-storage/openshift-storage.config
  154. // --name=client.admin
  155. // --keyring=/var/lib/rook/openshift-storage/client.admin.keyring
  156. //
  157. // The cmd-proxy container will take care of the rest with the help of the env CEPH_ARGS
  158. if !c.RemoteExecution {
  159. command, args = FinalizeCephCommandArgs(c.tool, c.clusterInfo, c.args, c.context.ConfigDir)
  160. }
  161. if c.JsonOutput {
  162. args = append(args, "--format", "json")
  163. } else {
  164. // the `rbd` tool doesn't use special flag for plain format
  165. switch c.tool {
  166. case RBDTool, RadosTool, GaneshaRadosGraceTool:
  167. // do not add format option
  168. default:
  169. args = append(args, "--format", "plain")
  170. }
  171. }
  172. var output, stderr string
  173. var err error
  174. // NewRBDCommand does not use the --out-file option so we only check for remote execution here
  175. // Still forcing the check for the command if the behavior changes in the future
  176. if command == RBDTool || command == RadosTool || command == GaneshaRadosGraceTool {
  177. if c.RemoteExecution {
  178. output, stderr, err = c.context.RemoteExecutor.ExecCommandInContainerWithFullOutputWithTimeout(c.clusterInfo.Context, ProxyAppLabel, CommandProxyInitContainerName, c.clusterInfo.Namespace, append([]string{command}, args...)...)
  179. if err != nil {
  180. err = errors.Errorf("%s", err.Error())
  181. }
  182. if stderr != "" {
  183. err = errors.Errorf("%s", stderr)
  184. }
  185. } else if c.timeout == 0 {
  186. output, err = c.context.Executor.ExecuteCommandWithOutput(command, args...)
  187. } else {
  188. output, err = c.context.Executor.ExecuteCommandWithTimeout(c.timeout, command, args...)
  189. }
  190. } else if c.timeout == 0 {
  191. if c.combinedOutput {
  192. output, err = c.context.Executor.ExecuteCommandWithCombinedOutput(command, args...)
  193. } else {
  194. output, err = c.context.Executor.ExecuteCommandWithOutput(command, args...)
  195. }
  196. } else {
  197. output, err = c.context.Executor.ExecuteCommandWithTimeout(c.timeout, command, args...)
  198. }
  199. return []byte(output), err
  200. }
  201. func (c *CephToolCommand) Run() ([]byte, error) {
  202. c.timeout = 0
  203. return c.run()
  204. }
  205. func (c *CephToolCommand) RunWithTimeout(timeout time.Duration) ([]byte, error) {
  206. c.timeout = timeout
  207. return c.run()
  208. }
  209. // ExecuteRBDCommandWithTimeout executes the 'rbd' command with a timeout of 1
  210. // minute. This method is left as a special case in which the caller has fully
  211. // configured its arguments. It is future work to integrate this case into the
  212. // generalization.
  213. func ExecuteRBDCommandWithTimeout(context *clusterd.Context, args []string) (string, error) {
  214. output, err := context.Executor.ExecuteCommandWithTimeout(exec.CephCommandsTimeout, RBDTool, args...)
  215. return output, err
  216. }
  217. func ExecuteCephCommandWithRetry(
  218. cmd func() (string, []byte, error),
  219. retries int,
  220. waitTime time.Duration,
  221. ) ([]byte, error) {
  222. for i := 0; i < retries; i++ {
  223. action, data, err := cmd()
  224. if err != nil {
  225. logger.Infof("command failed for %s. trying again...", action)
  226. time.Sleep(waitTime)
  227. continue
  228. }
  229. if i > 0 {
  230. logger.Infof("action %s succeeded on attempt %d", action, i)
  231. }
  232. return data, nil
  233. }
  234. return nil, errors.New("max command retries exceeded")
  235. }