util.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. /*
  2. Copyright 2015 The Kubernetes Authors.
  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 util
  14. import (
  15. "fmt"
  16. "io/ioutil"
  17. "os"
  18. "path/filepath"
  19. "reflect"
  20. "strings"
  21. v1 "k8s.io/api/core/v1"
  22. storage "k8s.io/api/storage/v1"
  23. "k8s.io/apimachinery/pkg/api/resource"
  24. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  25. "k8s.io/apimachinery/pkg/labels"
  26. "k8s.io/apimachinery/pkg/runtime"
  27. utypes "k8s.io/apimachinery/pkg/types"
  28. "k8s.io/apimachinery/pkg/util/sets"
  29. utilfeature "k8s.io/apiserver/pkg/util/feature"
  30. clientset "k8s.io/client-go/kubernetes"
  31. "k8s.io/klog"
  32. "k8s.io/kubernetes/pkg/api/legacyscheme"
  33. v1helper "k8s.io/kubernetes/pkg/apis/core/v1/helper"
  34. "k8s.io/kubernetes/pkg/features"
  35. "k8s.io/kubernetes/pkg/util/mount"
  36. "k8s.io/kubernetes/pkg/volume"
  37. "k8s.io/kubernetes/pkg/volume/util/types"
  38. "k8s.io/kubernetes/pkg/volume/util/volumepathhandler"
  39. utilstrings "k8s.io/utils/strings"
  40. )
  41. const (
  42. readyFileName = "ready"
  43. // ControllerManagedAttachAnnotation is the key of the annotation on Node
  44. // objects that indicates attach/detach operations for the node should be
  45. // managed by the attach/detach controller
  46. ControllerManagedAttachAnnotation string = "volumes.kubernetes.io/controller-managed-attach-detach"
  47. // KeepTerminatedPodVolumesAnnotation is the key of the annotation on Node
  48. // that decides if pod volumes are unmounted when pod is terminated
  49. KeepTerminatedPodVolumesAnnotation string = "volumes.kubernetes.io/keep-terminated-pod-volumes"
  50. // MountsInGlobalPDPath is name of the directory appended to a volume plugin
  51. // name to create the place for volume mounts in the global PD path.
  52. MountsInGlobalPDPath = "mounts"
  53. // VolumeGidAnnotationKey is the of the annotation on the PersistentVolume
  54. // object that specifies a supplemental GID.
  55. VolumeGidAnnotationKey = "pv.beta.kubernetes.io/gid"
  56. // VolumeDynamicallyCreatedByKey is the key of the annotation on PersistentVolume
  57. // object created dynamically
  58. VolumeDynamicallyCreatedByKey = "kubernetes.io/createdby"
  59. )
  60. // IsReady checks for the existence of a regular file
  61. // called 'ready' in the given directory and returns
  62. // true if that file exists.
  63. func IsReady(dir string) bool {
  64. readyFile := filepath.Join(dir, readyFileName)
  65. s, err := os.Stat(readyFile)
  66. if err != nil {
  67. return false
  68. }
  69. if !s.Mode().IsRegular() {
  70. klog.Errorf("ready-file is not a file: %s", readyFile)
  71. return false
  72. }
  73. return true
  74. }
  75. // SetReady creates a file called 'ready' in the given
  76. // directory. It logs an error if the file cannot be
  77. // created.
  78. func SetReady(dir string) {
  79. if err := os.MkdirAll(dir, 0750); err != nil && !os.IsExist(err) {
  80. klog.Errorf("Can't mkdir %s: %v", dir, err)
  81. return
  82. }
  83. readyFile := filepath.Join(dir, readyFileName)
  84. file, err := os.Create(readyFile)
  85. if err != nil {
  86. klog.Errorf("Can't touch %s: %v", readyFile, err)
  87. return
  88. }
  89. file.Close()
  90. }
  91. // GetSecretForPod locates secret by name in the pod's namespace and returns secret map
  92. func GetSecretForPod(pod *v1.Pod, secretName string, kubeClient clientset.Interface) (map[string]string, error) {
  93. secret := make(map[string]string)
  94. if kubeClient == nil {
  95. return secret, fmt.Errorf("Cannot get kube client")
  96. }
  97. secrets, err := kubeClient.CoreV1().Secrets(pod.Namespace).Get(secretName, metav1.GetOptions{})
  98. if err != nil {
  99. return secret, err
  100. }
  101. for name, data := range secrets.Data {
  102. secret[name] = string(data)
  103. }
  104. return secret, nil
  105. }
  106. // GetSecretForPV locates secret by name and namespace, verifies the secret type, and returns secret map
  107. func GetSecretForPV(secretNamespace, secretName, volumePluginName string, kubeClient clientset.Interface) (map[string]string, error) {
  108. secret := make(map[string]string)
  109. if kubeClient == nil {
  110. return secret, fmt.Errorf("Cannot get kube client")
  111. }
  112. secrets, err := kubeClient.CoreV1().Secrets(secretNamespace).Get(secretName, metav1.GetOptions{})
  113. if err != nil {
  114. return secret, err
  115. }
  116. if secrets.Type != v1.SecretType(volumePluginName) {
  117. return secret, fmt.Errorf("Cannot get secret of type %s", volumePluginName)
  118. }
  119. for name, data := range secrets.Data {
  120. secret[name] = string(data)
  121. }
  122. return secret, nil
  123. }
  124. // GetClassForVolume locates storage class by persistent volume
  125. func GetClassForVolume(kubeClient clientset.Interface, pv *v1.PersistentVolume) (*storage.StorageClass, error) {
  126. if kubeClient == nil {
  127. return nil, fmt.Errorf("Cannot get kube client")
  128. }
  129. className := v1helper.GetPersistentVolumeClass(pv)
  130. if className == "" {
  131. return nil, fmt.Errorf("Volume has no storage class")
  132. }
  133. class, err := kubeClient.StorageV1().StorageClasses().Get(className, metav1.GetOptions{})
  134. if err != nil {
  135. return nil, err
  136. }
  137. return class, nil
  138. }
  139. // CheckNodeAffinity looks at the PV node affinity, and checks if the node has the same corresponding labels
  140. // This ensures that we don't mount a volume that doesn't belong to this node
  141. func CheckNodeAffinity(pv *v1.PersistentVolume, nodeLabels map[string]string) error {
  142. return checkVolumeNodeAffinity(pv, nodeLabels)
  143. }
  144. func checkVolumeNodeAffinity(pv *v1.PersistentVolume, nodeLabels map[string]string) error {
  145. if pv.Spec.NodeAffinity == nil {
  146. return nil
  147. }
  148. if pv.Spec.NodeAffinity.Required != nil {
  149. terms := pv.Spec.NodeAffinity.Required.NodeSelectorTerms
  150. klog.V(10).Infof("Match for Required node selector terms %+v", terms)
  151. if !v1helper.MatchNodeSelectorTerms(terms, labels.Set(nodeLabels), nil) {
  152. return fmt.Errorf("No matching NodeSelectorTerms")
  153. }
  154. }
  155. return nil
  156. }
  157. // LoadPodFromFile will read, decode, and return a Pod from a file.
  158. func LoadPodFromFile(filePath string) (*v1.Pod, error) {
  159. if filePath == "" {
  160. return nil, fmt.Errorf("file path not specified")
  161. }
  162. podDef, err := ioutil.ReadFile(filePath)
  163. if err != nil {
  164. return nil, fmt.Errorf("failed to read file path %s: %+v", filePath, err)
  165. }
  166. if len(podDef) == 0 {
  167. return nil, fmt.Errorf("file was empty: %s", filePath)
  168. }
  169. pod := &v1.Pod{}
  170. codec := legacyscheme.Codecs.UniversalDecoder()
  171. if err := runtime.DecodeInto(codec, podDef, pod); err != nil {
  172. return nil, fmt.Errorf("failed decoding file: %v", err)
  173. }
  174. return pod, nil
  175. }
  176. // CalculateTimeoutForVolume calculates time for a Recycler pod to complete a
  177. // recycle operation. The calculation and return value is either the
  178. // minimumTimeout or the timeoutIncrement per Gi of storage size, whichever is
  179. // greater.
  180. func CalculateTimeoutForVolume(minimumTimeout, timeoutIncrement int, pv *v1.PersistentVolume) int64 {
  181. giQty := resource.MustParse("1Gi")
  182. pvQty := pv.Spec.Capacity[v1.ResourceStorage]
  183. giSize := giQty.Value()
  184. pvSize := pvQty.Value()
  185. timeout := (pvSize / giSize) * int64(timeoutIncrement)
  186. if timeout < int64(minimumTimeout) {
  187. return int64(minimumTimeout)
  188. }
  189. return timeout
  190. }
  191. // GenerateVolumeName returns a PV name with clusterName prefix. The function
  192. // should be used to generate a name of GCE PD or Cinder volume. It basically
  193. // adds "<clusterName>-dynamic-" before the PV name, making sure the resulting
  194. // string fits given length and cuts "dynamic" if not.
  195. func GenerateVolumeName(clusterName, pvName string, maxLength int) string {
  196. prefix := clusterName + "-dynamic"
  197. pvLen := len(pvName)
  198. // cut the "<clusterName>-dynamic" to fit full pvName into maxLength
  199. // +1 for the '-' dash
  200. if pvLen+1+len(prefix) > maxLength {
  201. prefix = prefix[:maxLength-pvLen-1]
  202. }
  203. return prefix + "-" + pvName
  204. }
  205. // GetPath checks if the path from the mounter is empty.
  206. func GetPath(mounter volume.Mounter) (string, error) {
  207. path := mounter.GetPath()
  208. if path == "" {
  209. return "", fmt.Errorf("Path is empty %s", reflect.TypeOf(mounter).String())
  210. }
  211. return path, nil
  212. }
  213. // UnmountViaEmptyDir delegates the tear down operation for secret, configmap, git_repo and downwardapi
  214. // to empty_dir
  215. func UnmountViaEmptyDir(dir string, host volume.VolumeHost, volName string, volSpec volume.Spec, podUID utypes.UID) error {
  216. klog.V(3).Infof("Tearing down volume %v for pod %v at %v", volName, podUID, dir)
  217. // Wrap EmptyDir, let it do the teardown.
  218. wrapped, err := host.NewWrapperUnmounter(volName, volSpec, podUID)
  219. if err != nil {
  220. return err
  221. }
  222. return wrapped.TearDownAt(dir)
  223. }
  224. // MountOptionFromSpec extracts and joins mount options from volume spec with supplied options
  225. func MountOptionFromSpec(spec *volume.Spec, options ...string) []string {
  226. pv := spec.PersistentVolume
  227. if pv != nil {
  228. // Use beta annotation first
  229. if mo, ok := pv.Annotations[v1.MountOptionAnnotation]; ok {
  230. moList := strings.Split(mo, ",")
  231. return JoinMountOptions(moList, options)
  232. }
  233. if len(pv.Spec.MountOptions) > 0 {
  234. return JoinMountOptions(pv.Spec.MountOptions, options)
  235. }
  236. }
  237. return options
  238. }
  239. // JoinMountOptions joins mount options eliminating duplicates
  240. func JoinMountOptions(userOptions []string, systemOptions []string) []string {
  241. allMountOptions := sets.NewString()
  242. for _, mountOption := range userOptions {
  243. if len(mountOption) > 0 {
  244. allMountOptions.Insert(mountOption)
  245. }
  246. }
  247. for _, mountOption := range systemOptions {
  248. allMountOptions.Insert(mountOption)
  249. }
  250. return allMountOptions.List()
  251. }
  252. // AccessModesContains returns whether the requested mode is contained by modes
  253. func AccessModesContains(modes []v1.PersistentVolumeAccessMode, mode v1.PersistentVolumeAccessMode) bool {
  254. for _, m := range modes {
  255. if m == mode {
  256. return true
  257. }
  258. }
  259. return false
  260. }
  261. // AccessModesContainedInAll returns whether all of the requested modes are contained by modes
  262. func AccessModesContainedInAll(indexedModes []v1.PersistentVolumeAccessMode, requestedModes []v1.PersistentVolumeAccessMode) bool {
  263. for _, mode := range requestedModes {
  264. if !AccessModesContains(indexedModes, mode) {
  265. return false
  266. }
  267. }
  268. return true
  269. }
  270. // GetWindowsPath get a windows path
  271. func GetWindowsPath(path string) string {
  272. windowsPath := strings.Replace(path, "/", "\\", -1)
  273. if strings.HasPrefix(windowsPath, "\\") {
  274. windowsPath = "c:" + windowsPath
  275. }
  276. return windowsPath
  277. }
  278. // GetUniquePodName returns a unique identifier to reference a pod by
  279. func GetUniquePodName(pod *v1.Pod) types.UniquePodName {
  280. return types.UniquePodName(pod.UID)
  281. }
  282. // GetUniqueVolumeName returns a unique name representing the volume/plugin.
  283. // Caller should ensure that volumeName is a name/ID uniquely identifying the
  284. // actual backing device, directory, path, etc. for a particular volume.
  285. // The returned name can be used to uniquely reference the volume, for example,
  286. // to prevent operations (attach/detach or mount/unmount) from being triggered
  287. // on the same volume.
  288. func GetUniqueVolumeName(pluginName, volumeName string) v1.UniqueVolumeName {
  289. return v1.UniqueVolumeName(fmt.Sprintf("%s/%s", pluginName, volumeName))
  290. }
  291. // GetUniqueVolumeNameFromSpecWithPod returns a unique volume name with pod
  292. // name included. This is useful to generate different names for different pods
  293. // on same volume.
  294. func GetUniqueVolumeNameFromSpecWithPod(
  295. podName types.UniquePodName, volumePlugin volume.VolumePlugin, volumeSpec *volume.Spec) v1.UniqueVolumeName {
  296. return v1.UniqueVolumeName(
  297. fmt.Sprintf("%s/%v-%s", volumePlugin.GetPluginName(), podName, volumeSpec.Name()))
  298. }
  299. // GetUniqueVolumeNameFromSpec uses the given VolumePlugin to generate a unique
  300. // name representing the volume defined in the specified volume spec.
  301. // This returned name can be used to uniquely reference the actual backing
  302. // device, directory, path, etc. referenced by the given volumeSpec.
  303. // If the given plugin does not support the volume spec, this returns an error.
  304. func GetUniqueVolumeNameFromSpec(
  305. volumePlugin volume.VolumePlugin,
  306. volumeSpec *volume.Spec) (v1.UniqueVolumeName, error) {
  307. if volumePlugin == nil {
  308. return "", fmt.Errorf(
  309. "volumePlugin should not be nil. volumeSpec.Name=%q",
  310. volumeSpec.Name())
  311. }
  312. volumeName, err := volumePlugin.GetVolumeName(volumeSpec)
  313. if err != nil || volumeName == "" {
  314. return "", fmt.Errorf(
  315. "failed to GetVolumeName from volumePlugin for volumeSpec %q err=%v",
  316. volumeSpec.Name(),
  317. err)
  318. }
  319. return GetUniqueVolumeName(
  320. volumePlugin.GetPluginName(),
  321. volumeName),
  322. nil
  323. }
  324. // IsPodTerminated checks if pod is terminated
  325. func IsPodTerminated(pod *v1.Pod, podStatus v1.PodStatus) bool {
  326. return podStatus.Phase == v1.PodFailed || podStatus.Phase == v1.PodSucceeded || (pod.DeletionTimestamp != nil && notRunning(podStatus.ContainerStatuses))
  327. }
  328. // notRunning returns true if every status is terminated or waiting, or the status list
  329. // is empty.
  330. func notRunning(statuses []v1.ContainerStatus) bool {
  331. for _, status := range statuses {
  332. if status.State.Terminated == nil && status.State.Waiting == nil {
  333. return false
  334. }
  335. }
  336. return true
  337. }
  338. // SplitUniqueName splits the unique name to plugin name and volume name strings. It expects the uniqueName to follow
  339. // the format plugin_name/volume_name and the plugin name must be namespaced as described by the plugin interface,
  340. // i.e. namespace/plugin containing exactly one '/'. This means the unique name will always be in the form of
  341. // plugin_namespace/plugin/volume_name, see k8s.io/kubernetes/pkg/volume/plugins.go VolumePlugin interface
  342. // description and pkg/volume/util/volumehelper/volumehelper.go GetUniqueVolumeNameFromSpec that constructs
  343. // the unique volume names.
  344. func SplitUniqueName(uniqueName v1.UniqueVolumeName) (string, string, error) {
  345. components := strings.SplitN(string(uniqueName), "/", 3)
  346. if len(components) != 3 {
  347. return "", "", fmt.Errorf("cannot split volume unique name %s to plugin/volume components", uniqueName)
  348. }
  349. pluginName := fmt.Sprintf("%s/%s", components[0], components[1])
  350. return pluginName, components[2], nil
  351. }
  352. // NewSafeFormatAndMountFromHost creates a new SafeFormatAndMount with Mounter
  353. // and Exec taken from given VolumeHost.
  354. func NewSafeFormatAndMountFromHost(pluginName string, host volume.VolumeHost) *mount.SafeFormatAndMount {
  355. mounter := host.GetMounter(pluginName)
  356. exec := host.GetExec(pluginName)
  357. return &mount.SafeFormatAndMount{Interface: mounter, Exec: exec}
  358. }
  359. // GetVolumeMode retrieves VolumeMode from pv.
  360. // If the volume doesn't have PersistentVolume, it's an inline volume,
  361. // should return volumeMode as filesystem to keep existing behavior.
  362. func GetVolumeMode(volumeSpec *volume.Spec) (v1.PersistentVolumeMode, error) {
  363. if volumeSpec == nil || volumeSpec.PersistentVolume == nil {
  364. return v1.PersistentVolumeFilesystem, nil
  365. }
  366. if volumeSpec.PersistentVolume.Spec.VolumeMode != nil {
  367. return *volumeSpec.PersistentVolume.Spec.VolumeMode, nil
  368. }
  369. return "", fmt.Errorf("cannot get volumeMode for volume: %v", volumeSpec.Name())
  370. }
  371. // GetPersistentVolumeClaimVolumeMode retrieves VolumeMode from pvc.
  372. func GetPersistentVolumeClaimVolumeMode(claim *v1.PersistentVolumeClaim) (v1.PersistentVolumeMode, error) {
  373. if claim.Spec.VolumeMode != nil {
  374. return *claim.Spec.VolumeMode, nil
  375. }
  376. return "", fmt.Errorf("cannot get volumeMode from pvc: %v", claim.Name)
  377. }
  378. // GetPersistentVolumeClaimQualifiedName returns a qualified name for pvc.
  379. func GetPersistentVolumeClaimQualifiedName(claim *v1.PersistentVolumeClaim) string {
  380. return utilstrings.JoinQualifiedName(claim.GetNamespace(), claim.GetName())
  381. }
  382. // CheckVolumeModeFilesystem checks VolumeMode.
  383. // If the mode is Filesystem, return true otherwise return false.
  384. func CheckVolumeModeFilesystem(volumeSpec *volume.Spec) (bool, error) {
  385. if utilfeature.DefaultFeatureGate.Enabled(features.BlockVolume) {
  386. volumeMode, err := GetVolumeMode(volumeSpec)
  387. if err != nil {
  388. return true, err
  389. }
  390. if volumeMode == v1.PersistentVolumeBlock {
  391. return false, nil
  392. }
  393. }
  394. return true, nil
  395. }
  396. // CheckPersistentVolumeClaimModeBlock checks VolumeMode.
  397. // If the mode is Block, return true otherwise return false.
  398. func CheckPersistentVolumeClaimModeBlock(pvc *v1.PersistentVolumeClaim) bool {
  399. return utilfeature.DefaultFeatureGate.Enabled(features.BlockVolume) && pvc.Spec.VolumeMode != nil && *pvc.Spec.VolumeMode == v1.PersistentVolumeBlock
  400. }
  401. // IsWindowsUNCPath checks if path is prefixed with \\
  402. // This can be used to skip any processing of paths
  403. // that point to SMB shares, local named pipes and local UNC path
  404. func IsWindowsUNCPath(goos, path string) bool {
  405. if goos != "windows" {
  406. return false
  407. }
  408. // Check for UNC prefix \\
  409. if strings.HasPrefix(path, `\\`) {
  410. return true
  411. }
  412. return false
  413. }
  414. // IsWindowsLocalPath checks if path is a local path
  415. // prefixed with "/" or "\" like "/foo/bar" or "\foo\bar"
  416. func IsWindowsLocalPath(goos, path string) bool {
  417. if goos != "windows" {
  418. return false
  419. }
  420. if IsWindowsUNCPath(goos, path) {
  421. return false
  422. }
  423. if strings.Contains(path, ":") {
  424. return false
  425. }
  426. if !(strings.HasPrefix(path, `/`) || strings.HasPrefix(path, `\`)) {
  427. return false
  428. }
  429. return true
  430. }
  431. // MakeAbsolutePath convert path to absolute path according to GOOS
  432. func MakeAbsolutePath(goos, path string) string {
  433. if goos != "windows" {
  434. return filepath.Clean("/" + path)
  435. }
  436. // These are all for windows
  437. // If there is a colon, give up.
  438. if strings.Contains(path, ":") {
  439. return path
  440. }
  441. // If there is a slash, but no drive, add 'c:'
  442. if strings.HasPrefix(path, "/") || strings.HasPrefix(path, "\\") {
  443. return "c:" + path
  444. }
  445. // Otherwise, add 'c:\'
  446. return "c:\\" + path
  447. }
  448. // MapBlockVolume is a utility function to provide a common way of mounting
  449. // block device path for a specified volume and pod. This function should be
  450. // called by volume plugins that implements volume.BlockVolumeMapper.Map() method.
  451. func MapBlockVolume(
  452. devicePath,
  453. globalMapPath,
  454. podVolumeMapPath,
  455. volumeMapName string,
  456. podUID utypes.UID,
  457. ) error {
  458. blkUtil := volumepathhandler.NewBlockVolumePathHandler()
  459. // map devicePath to global node path
  460. mapErr := blkUtil.MapDevice(devicePath, globalMapPath, string(podUID))
  461. if mapErr != nil {
  462. return mapErr
  463. }
  464. // map devicePath to pod volume path
  465. mapErr = blkUtil.MapDevice(devicePath, podVolumeMapPath, volumeMapName)
  466. if mapErr != nil {
  467. return mapErr
  468. }
  469. return nil
  470. }
  471. // GetPluginMountDir returns the global mount directory name appended
  472. // to the given plugin name's plugin directory
  473. func GetPluginMountDir(host volume.VolumeHost, name string) string {
  474. mntDir := filepath.Join(host.GetPluginDir(name), MountsInGlobalPDPath)
  475. return mntDir
  476. }
  477. // IsLocalEphemeralVolume determines whether the argument is a local ephemeral
  478. // volume vs. some other type
  479. func IsLocalEphemeralVolume(volume v1.Volume) bool {
  480. return volume.GitRepo != nil ||
  481. (volume.EmptyDir != nil && volume.EmptyDir.Medium != v1.StorageMediumMemory) ||
  482. volume.ConfigMap != nil || volume.DownwardAPI != nil
  483. }