Repository: ansible-collections/community.general

Issue:
beadm module ends with " AttributeError: 'list' object has no attribute 'split' " python error
##### SUMMARY

beadm module ends with Python error.

##### ISSUE TYPE

Bug Report

##### COMPONENT NAME

lib/ansible/modules/system/beadm.py

##### ANSIBLE VERSION

```
ansible 2.9.1
config file = /home/userx/ansible/ansible.cfg
configured module search path = [u'/home/userx/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python2.7/site-packages/ansible
executable location = /usr/bin/ansible
python version = 2.7.5 (default, Jun 11 2019, 12:19:05) [GCC 4.8.5 20150623 (Red Hat 4.8.5-36)]
```

##### CONFIGURATION

```
CACHE_PLUGIN(/home/userx/ansible/ansible.cfg) = memory
DEFAULT_ACTION_PLUGIN_PATH(/home/userx/ansible/ansible.cfg) = [u'/usr/share/ansible_plugins/action_plugins']
DEFAULT_BECOME(/home/userx/ansible/ansible.cfg) = True
DEFAULT_BECOME_METHOD(/home/userx/ansible/ansible.cfg) = sudo
DEFAULT_BECOME_USER(/home/userx/ansible/ansible.cfg) = root
DEFAULT_CALLBACK_PLUGIN_PATH(/home/userx/ansible/ansible.cfg) = [u'/usr/lib/python2.7/site-packages/ansible/plugins/callback']
DEFAULT_CONNECTION_PLUGIN_PATH(/home/userx/ansible/ansible.cfg) = [u'/usr/share/ansible_plugins/connection_plugins']
DEFAULT_FILTER_PLUGIN_PATH(/home/userx/ansible/ansible.cfg) = [u'/usr/share/ansible_plugins/filter_plugins']
DEFAULT_FORKS(/home/userx/ansible/ansible.cfg) = 5
DEFAULT_GATHERING(/home/userx/ansible/ansible.cfg) = implicit
DEFAULT_HOST_LIST(/home/userx/ansible/ansible.cfg) = [u'/home/userx/ansible/hosts']
DEFAULT_LOOKUP_PLUGIN_PATH(/home/userx/ansible/ansible.cfg) = [u'/usr/share/ansible_plugins/lookup_plugins']
DEFAULT_MANAGED_STR(/home/userx/ansible/ansible.cfg) = Ansible managed: {file} modified on %Y-%m-%d %H:%M:%S by {uid} on {host}
DEFAULT_POLL_INTERVAL(/home/userx/ansible/ansible.cfg) = 15
DEFAULT_REMOTE_USER(/home/userx/ansible/ansible.cfg) = sysadm
DEFAULT_ROLES_PATH(/home/userx/ansible/ansible.cfg) = [u'/home/userx/ansible/dev_roles', u'/home/userx/ansible/roles', u'/home/userx/ansible
DEFAULT_TIMEOUT(/home/userx/ansible/ansible.cfg) = 10
DEFAULT_TRANSPORT(/home/userx/ansible/ansible.cfg) = smart
DEFAULT_VARS_PLUGIN_PATH(/home/userx/ansible/ansible.cfg) = [u'/usr/share/ansible_plugins/vars_plugins']
HOST_KEY_CHECKING(/home/userx/ansible/ansible.cfg) = False
```

##### OS / ENVIRONMENT

target OS version Solaris 11.4

##### STEPS TO REPRODUCE

The problem occurs when trying to activate a Be (boot environment) on the target machine. (This happens also whith any other actions done through the beadm module)

```
$ ansible -i hosts test_servers -m beadm -a "name=test state=activated"
```

##### EXPECTED RESULTS

The result expected is that the BE on the target server become activated. But since the module ends unsuccessfully this is not done.

##### ACTUAL RESULTS

The module fails

```
An exception occurred during task execution. To see the full traceback, use -vvv. The error was: AttributeError: 'list' object has no attribute 'spl
it'                                                                                                                                                 
[WARNING]: Platform sunos on host test_server is using the discovered Python interpreter at /usr/bin/python, but future                               
installation of another Python interpreter could change this. See                                                                                   
https://docs.ansible.com/ansible/2.9/reference_appendices/interpreter_discovery.html for more information.                                          
                                                                                                                                                    
test_server| FAILED! => {                                                                                                                            
    "ansible_facts": {                                                                                                                              
        "discovered_interpreter_python": "/usr/bin/python"                                                                                          
    },                                                                                                                                              
    "changed": false,                                                                                                                               
    "module_stderr": "Shared connection to X.X.X.X closed.\r\n",                                                                              
    "module_stdout": "Traceback (most recent call last):\r\n  File \"/export/home/sysadm/.ansible/tmp/ansible-tmp-1575455328.27-200855804216536/Ansi
ballZ_beadm.py\", line 102, in <module>\r\n    _ansiballz_main()\r\n  File \"/export/home/sysadm/.ansible/tmp/ansible-tmp-1575455328.27-200855804216
536/AnsiballZ_beadm.py\", line 94, in _ansiballz_main\r\n    invoke_module(zipped_mod, temp_path, ANSIBALLZ_PARAMS)\r\n  File \"/export/home/sysadm/
.ansible/tmp/ansible-tmp-1575455328.27-200855804216536/AnsiballZ_beadm.py\", line 40, in invoke_module\r\n    runpy.run_module(mod_name='ansible.mod
ules.system.beadm', init_globals=None, run_name='__main__', alter_sys=True)\r\n  File \"/usr/lib/python2.7/runpy.py\", line 188, in run_module\r\n  
  fname, loader, pkg_name)\r\n  File \"/usr/lib/python2.7/runpy.py\", line 82, in _run_module_code\r\n    mod_name, mod_fname, mod_loader, pkg_name)
\r\n  File \"/usr/lib/python2.7/runpy.py\", line 72, in _run_code\r\n    exec code in run_globals\r\n  File \"/tmp/ansible_beadm_payload_8VMONR/ansi
ble_beadm_payload.zip/ansible/modules/system/beadm.py\", line 425, in <module>\r\n  File \"/tmp/ansible_beadm_payload_8VMONR/ansible_beadm_payload.z
ip/ansible/modules/system/beadm.py\", line 368, in main\r\n  File \"/tmp/ansible_beadm_payload_8VMONR/ansible_beadm_payload.zip/ansible/modules/syst
em/beadm.py\", line 210, in is_activated\r\nAttributeError: 'list' object has no attribute 'split'\r\n",                                            
    "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error",                                                                                 
    "rc": 1                                                                                                                                         
}                                                                                                                                                                                                                                                                                                  
```

Copied from ansible/ansible/issues/65506.

https://github.com/ansible-collections/community.general/blob/main/plugins/modules/system/beadm.py



Discussion:
Files identified in the description:
None

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
@walterrowe: Greetings! Thanks for taking the time to open this issue. In order for the community to handle your issue effectively, we need a bit more information.

Here are the items we could not find in your description:
- issue type
- ansible version
- component name

Please set the description of this issue with this template:
https://raw.githubusercontent.com/ansible/ansible/devel/.github/ISSUE_TEMPLATE/bug_report.md

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: issue_missing_data --->
Updated format
Files identified in the description:
* [`plugins/modules/system/beadm.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/system/beadm.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @MorrisA @bcoca @d-little @fishman @flynn1973 @gforster @jasperla @jpdasma @kairoaraujo @marvin-sinister @mator @molekuul @ramooncamacho @scathatheworm @troy2914 @wtcross @xen0l
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
The initial ticket was open 04 December 2019. The module is completely broken and unusable.
All playbooks using this module are unusable. On FreeBSD too.
Could someone pay attention to this ticket and fix the module, _please_?
@Peter2121 it's a community supported module, so someone from the community has to fix it. The module seems to have no active maintainers, so whoever wants to create a fix, go for it...
> @Peter2121 it's a community supported module, so someone from the community has to fix it. The module seems to have no active maintainers, so whoever wants to create a fix, go for it...

@felixfontein the module stopped working after Ansible upgrade. I still use the old python 2.7, so it cannot be the issue. Are there any Ansible compatibility checks that could help to localize the cause of the problem? Is there any module migration guide? I am not a python developer, I just actively use some playbooks with this module to upgrade my park safely. 
I'm really disappointed with this situation as we passed many days to create our upgrade playbooks and they are not usable now because of this compatibility problem.
@Peter2121 the `beadm` module unfortunately has no tests at all. In any case, I'd look at the git history of the module before it was moved to this repository (there were no changes to the code here): https://github.com/ansible/ansible/commits/pre-ansible-base/lib/ansible/modules/system/beadm.py

Since nobody mentioned with which version of Ansible this still worked (or I overlooked it), I have no idea how far back to search. My personal guess would be to take a look at ansible/ansible#51807, this looks like the most relevant change to the module. That change first appeared in Ansible 2.9.0, so if the module worked correctly in Ansible 2.8 or earlier, but not since 2.9.x, this could be the reason.
Thanks for pointing it.
The module seems to be completely broken - _find_be_by_name returns list or re.MatchObject, but in several functions one suppose to get a string from it and split is called on the result.
I'll try to fix it.
Should I attach a patch here or open a PR?
@Peter2121 open a PR please, so we could test and review. Thanks
@Peter2121 that problem with `_find_be_by_name` seems to have been introduced in ansible/ansible#51807. I guess the line `return check` should be `return check.group(1)`.

(Also, not directly related, `check = re.match(r'.+/({0})\s+\-'.format(self.name), line)` should probably be changed to `check = re.match(r'.+/({0})\s+\-'.format(re.escape(self.name)), line)`.)
@felixfontein, thanks for your advise!

> that problem with `_find_be_by_name` seems to have been introduced in [ansible/ansible#51807](https://github.com/ansible/ansible/pull/51807). I guess the line `return check` should be `return check.group(1)`.

Yes, it seems that is was broken by this commit. I think it would be better to change the processing of results of `_find_be_by_name` - don't use split anymore. For example at line 266:
`if line.split(';')[3]:` -> `if line[3]:`
There are not so much code to change. What do you think?

> Also, not directly related, `check = re.match(r'.+/({0})\s+\-'.format(self.name), line)` should probably be changed to `check = re.match(r'.+/({0})\s+\-'.format(re.escape(self.name)), line)`.

Decoding of this regex is completely out of my competence, but I'm agree to put it to the PR too :) 
The most important thing for me here is - what type of object would be returned at line 176? It seems that to be able to use [] with the returned value, we need to use `check.group()` and not `check`.
Once again, I'm not a Python developer, so your help is really appreciated.


> @felixfontein, thanks for your advise!
> 
> > that problem with `_find_be_by_name` seems to have been introduced in [ansible/ansible#51807](https://github.com/ansible/ansible/pull/51807). I guess the line `return check` should be `return check.group(1)`.
> 
> Yes, it seems that is was broken by this commit. I think it would be better to change the processing of results of `_find_be_by_name` - don't use split anymore. For example at line 266:
> `if line.split(';')[3]:` -> `if line[3]:`
> There are not so much code to change. What do you think?

That would probably totally break the module. `line[3]` is the 3rd character of the line (counting starts with 0), which (essentially) never happens to be falsy. You don't want to work with characters of the line, but with fields.

> > Also, not directly related, `check = re.match(r'.+/({0})\s+\-'.format(self.name), line)` should probably be changed to `check = re.match(r'.+/({0})\s+\-'.format(re.escape(self.name)), line)`.
> 
> Decoding of this regex is completely out of my competence, but I'm agree to put it to the PR too :)

I only added a `re.escape()` to what is inserted into the regex. That prevents that characters in `self.name` that could be interpreted as regex special characters are correctly quoted.

> The most important thing for me here is - what type of object would be returned at line 176? It seems that to be able to use [] with the returned value, we need to use `check.group()` and not `check`.

That's a good question. It used to return the whole `line` value, and some callers still seem to assume that, but others don't. I really don't know this module enough, and I don't know BEs at all.
> That would probably totally break the module. `line[3]` is the 3rd character of the line (counting starts with 0), which (essentially) never happens to be falsy. You don't want to work with characters of the line, but with fields.

`line` is assigned to `self._find_be_by_name` result, and this function returns the result of `line.split`, so it is not a string, but an array. BTW, I fixed the module like this in my install.

I'll try to test the case where `re.match` is used before creating the PR. Normally I don't use beadm with snapshots, so I need to create a special playbook for it.

Resolving patch:
diff --git a/plugins/modules/system/beadm.py b/plugins/modules/system/beadm.py
--- a/plugins/modules/system/beadm.py
+++ b/plugins/modules/system/beadm.py
@@ -165,20 +165,29 @@ def _find_be_by_name(self, out):
         if '@' in self.name:
             for line in out.splitlines():
                 if self.is_freebsd:
-                    check = re.match(r'.+/({0})\s+\-'.format(self.name), line)
-                    if check:
+                    check = line.split()
+                    if(check == []):
+                        continue
+                    full_name = check[0].split('/')
+                    if(full_name == []):
+                        continue
+                    check[0] = full_name[len(full_name) - 1]
+                    if check[0] == self.name:
                         return check
                 else:
                     check = line.split(';')
-                    if check[1] == self.name:
+                    if check[0] == self.name:
                         return check
         else:
-            splitter = '\t' if self.is_freebsd else ';'
             for line in out.splitlines():
-                check = line.split(splitter)
-                if check[0] == self.name:
-                    return check
-
+                if self.is_freebsd:
+                    check = line.split()
+                    if check[0] == self.name:
+                        return check
+                else:
+                    check = line.split(';')
+                    if check[0] == self.name:
+                        return check
         return None
 
     def exists(self):
@@ -197,11 +206,13 @@ def is_activated(self):
 
         if rc == 0:
             line = self._find_be_by_name(out)
+            if line is None:
+                return False
             if self.is_freebsd:
-                if line is not None and 'R' in line.split('\t')[1]:
+                if 'R' in line[1]:
                     return True
             else:
-                if 'R' in line.split(';')[2]:
+                if 'R' in line[2]:
                     return True
 
         return False
@@ -250,15 +261,16 @@ def is_mounted(self):
 
         if rc == 0:
             line = self._find_be_by_name(out)
+            if line is None:
+                return False
             if self.is_freebsd:
                 # On FreeBSD, we exclude currently mounted BE on /, as it is
                 # special and can be activated even if it is mounted. That is not
                 # possible with non-root BEs.
-                if line.split('\t')[2] != '-' and \
-                        line.split('\t')[2] != '/':
+                if line[2] != '-' and line[2] != '/':
                     return True
             else:
-                if line.split(';')[3]:
+                if line[3]:
                     return True
 
         return False


Test patch: